feat: agent Rust complet — systray, chat, enregistrement, floutage (2.4 MB)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-03-18 23:18:09 +01:00
parent ad7ff3bce4
commit 90ee91caf9
11 changed files with 2329 additions and 191 deletions

View File

@@ -1,41 +1,64 @@
//! Agent RPA Vision — Phase 1 (headless)
//! Agent RPA Vision — Phases 1-5 (parite complete)
//!
//! Point d'entree principal. Demarre 3 threads :
//! 1. Heartbeat loop : capture + envoi toutes les 5s (avec dedup par hash)
//! 2. Replay poll loop : poll toutes les 1s, execute les actions
//! 3. Capture HTTP server : port 5006 pour les captures a la demande
//! Point d'entree principal. Architecture multi-threads :
//!
//! - Thread principal : boucle d'evenements systray (Windows) ou attente console (Linux)
//! - Thread heartbeat : capture + envoi toutes les 5s (avec dedup par hash)
//! - Thread replay : poll toutes les 1s, execute les actions
//! - Thread serveur : HTTP port 5006 pour les captures a la demande
//! - Thread recorder : capture evenements souris/clavier (quand enregistrement actif)
//! - Thread chat : fenetre WebView2 (Windows, a la demande)
//! - Thread health : verification connexion serveur (toutes les 30s)
//!
//! Le thread principal gere le systray sur Windows via winit.
//! Sur Linux, le thread principal attend Ctrl+C (mode console).
//!
//! Configuration via variables d'environnement ou valeurs par defaut.
//! Compatible avec le serveur streaming existant (api_stream.py, port 5005).
#[allow(dead_code)]
mod blur;
mod capture;
mod chat;
mod config;
mod executor;
mod network;
#[allow(dead_code)]
mod notifications;
mod recorder;
mod replay;
mod server;
#[allow(dead_code)]
mod state;
mod tray;
mod visual;
use config::Config;
use reqwest::blocking::Client;
use std::sync::atomic::{AtomicBool, Ordering};
use state::AgentState;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
/// Flag global pour l'arret propre (Ctrl+C)
static RUNNING: AtomicBool = AtomicBool::new(true);
fn main() {
// Initialiser le logging
env_logger::Builder::from_env(
env_logger::Env::default().default_filter_or("info"),
)
.format_timestamp_secs()
.init();
let config = Config::from_env();
let config = Arc::new(config);
// Etat partage thread-safe
let state = AgentState::new();
// Banniere de demarrage
print_banner(&config);
// Handler Ctrl+C pour arret propre
// On utilise le flag global RUNNING (AtomicBool) — le handler SIGINT
// est installe via un thread qui bloque sur un pipe/signal.
// Approche simple : polling du flag depuis tous les threads.
install_ctrlc_handler();
install_ctrlc_handler(state.clone());
// Verifier que la capture d'ecran fonctionne
print!("[MAIN] Test de capture d'ecran... ");
@@ -50,19 +73,21 @@ fn main() {
// Thread 1 : Heartbeat loop
let hb_config = config.clone();
let heartbeat_thread = thread::Builder::new()
let hb_state = state.clone();
let _heartbeat_thread = thread::Builder::new()
.name("heartbeat".to_string())
.spawn(move || {
heartbeat_loop(&hb_config);
heartbeat_loop(&hb_config, &hb_state);
})
.expect("Impossible de demarrer le thread heartbeat");
// Thread 2 : Replay poll loop
let rp_config = config.clone();
let rp_state = state.clone();
let _replay_thread = thread::Builder::new()
.name("replay".to_string())
.spawn(move || {
replay::replay_poll_loop(&rp_config);
replay::replay_poll_loop(&rp_config, &rp_state);
})
.expect("Impossible de demarrer le thread replay");
@@ -75,31 +100,46 @@ fn main() {
})
.expect("Impossible de demarrer le thread serveur");
println!("\n[MAIN] Agent operationnel. Appuyez sur Ctrl+C pour quitter.\n");
// Thread 4 : Health check (verification connexion serveur)
let hc_config = config.clone();
let hc_state = state.clone();
let _health_thread = thread::Builder::new()
.name("health-check".to_string())
.spawn(move || {
health_check_loop(&hc_config, &hc_state);
})
.expect("Impossible de demarrer le thread health check");
// Bloquer le thread principal en attendant Ctrl+C
while RUNNING.load(Ordering::SeqCst) {
thread::sleep(Duration::from_millis(500));
}
// Thread 5 : Recorder (capture evenements — inactif jusqu'a enregistrement)
let rec_config = config.clone();
let rec_state = state.clone();
let _recorder_rx = recorder::start_recorder(rec_config, rec_state);
// Thread 6 : Chat window (WebView2, a la demande)
let chat_config = config.clone();
let chat_state = state.clone();
chat::start_chat_thread(chat_config, chat_state);
println!("\n[MAIN] Agent operationnel — tous les threads demarres.\n");
// Thread principal : boucle systray (Windows) ou attente console (Linux)
// Le systray bloque le thread principal (necessaire pour la message pump Windows)
tray::run_tray_loop(config.clone(), state.clone());
// Si on arrive ici, l'agent doit s'arreter
println!("\n[MAIN] Arret en cours...");
state.request_shutdown();
// Attendre le thread heartbeat (les autres sont daemon-like)
let _ = heartbeat_thread.join();
// Laisser le temps aux threads de se terminer
thread::sleep(Duration::from_millis(500));
println!("[MAIN] Agent arrete.");
}
/// Installe un handler Ctrl+C qui met RUNNING a false.
///
/// Sur Unix : intercepte SIGINT via un pipe auto-referent.
/// Sur Windows : sera ameliore en Phase 2 avec le crate windows.
fn install_ctrlc_handler() {
// Approche portable : un thread qui attend sur stdin/signal
// En pratique, on utilise un pipe trick simple
/// Installe un handler Ctrl+C qui met l'etat a "arret demande".
fn install_ctrlc_handler(state: Arc<AgentState>) {
#[cfg(unix)]
{
// Creer un pipe pour la notification
let mut fds = [0i32; 2];
unsafe {
if libc::pipe(fds.as_mut_ptr()) != 0 {
@@ -107,13 +147,21 @@ fn install_ctrlc_handler() {
return;
}
// Installer le signal handler qui ecrit dans le pipe
static mut WRITE_FD: i32 = -1;
WRITE_FD = fds[1];
// Sauvegarder un pointeur vers l'etat dans une static
// pour pouvoir y acceder depuis le handler
static mut STATE_PTR: *const AgentState = std::ptr::null();
STATE_PTR = Arc::as_ptr(&state);
extern "C" fn sigint_handler(_sig: i32) {
unsafe {
RUNNING.store(false, Ordering::SeqCst);
if !STATE_PTR.is_null() {
(*STATE_PTR)
.running
.store(false, std::sync::atomic::Ordering::SeqCst);
}
let buf = [1u8];
let _ = libc::write(WRITE_FD, buf.as_ptr() as *const _, 1);
}
@@ -125,15 +173,16 @@ fn install_ctrlc_handler() {
#[cfg(not(unix))]
{
// Sur Windows, on utilise un thread simple qui verifie periodiquement
// Le vrai handler sera SetConsoleCtrlHandler en Phase 2
// Pour l'instant, Ctrl+C termine le process directement (comportement par defaut)
// Sur Windows, le systray gere l'arret via le menu "Quitter"
// Le handler console est un bonus pour le mode headless
let _ = state;
}
}
/// Boucle de heartbeat : capture un screenshot toutes les N secondes
/// et l'envoie au serveur si l'ecran a change.
fn heartbeat_loop(config: &Config) {
/// Applique le floutage des zones sensibles si active dans la config.
fn heartbeat_loop(config: &Config, state: &AgentState) {
let client = Client::new();
let session_id = config.bg_session_id();
let mut last_hash: u64 = 0;
@@ -144,34 +193,50 @@ fn heartbeat_loop(config: &Config) {
session_id, config.heartbeat_interval_s
);
while RUNNING.load(Ordering::SeqCst) {
while state.is_running() {
// Verifier l'arret d'urgence
if state
.emergency_stop
.load(std::sync::atomic::Ordering::SeqCst)
{
thread::sleep(Duration::from_secs(1));
continue;
}
// Capturer l'ecran
match capture::capture_screenshot() {
Some(img) => {
// Deduplication par hash perceptuel
let current_hash = capture::image_hash(&img);
if current_hash == last_hash {
// Ecran identique, on skip l'envoi
thread::sleep(Duration::from_secs(config.heartbeat_interval_s));
continue;
}
last_hash = current_hash;
// Appliquer le floutage des zones sensibles si active
let final_img = if config.blur_sensitive {
blur::blur_sensitive_fields(&img)
} else {
img
};
// Encoder en JPEG
let jpeg_bytes = capture::screenshot_to_jpeg_bytes(&img, config.jpeg_quality);
let jpeg_bytes =
capture::screenshot_to_jpeg_bytes(&final_img, config.jpeg_quality);
if jpeg_bytes.is_empty() {
thread::sleep(Duration::from_secs(config.heartbeat_interval_s));
continue;
}
// Envoyer au serveur
let success = network::send_heartbeat(&client, config, &jpeg_bytes, &session_id);
let success =
network::send_heartbeat(&client, config, &jpeg_bytes, &session_id);
if success {
consecutive_errors = 0;
} else {
consecutive_errors += 1;
if consecutive_errors == 1 || consecutive_errors % 12 == 0 {
// Log seulement la premiere erreur et toutes les minutes
eprintln!(
"[HEARTBEAT] {} erreur(s) consecutives",
consecutive_errors
@@ -180,8 +245,6 @@ fn heartbeat_loop(config: &Config) {
}
}
None => {
// Pas de capture possible (pas de display, etc.)
// On attend plus longtemps pour ne pas spammer les logs
thread::sleep(Duration::from_secs(config.heartbeat_interval_s * 2));
continue;
}
@@ -193,16 +256,58 @@ fn heartbeat_loop(config: &Config) {
println!("[HEARTBEAT] Boucle arretee.");
}
/// Boucle de health check : verifie la connexion au serveur toutes les 30s.
/// Met a jour l'etat de connexion dans AgentState.
fn health_check_loop(config: &Config, state: &AgentState) {
let client = Client::new();
let check_interval = Duration::from_secs(30);
let timeout = Duration::from_secs(5);
println!("[HEALTH] Boucle health check demarree (intervalle=30s)");
while state.is_running() {
let url = format!("{}/stats", config.server_url);
let connected = client
.get(&url)
.timeout(timeout)
.send()
.map(|r| r.status().is_success())
.unwrap_or(false);
let was_connected = state.connected.load(std::sync::atomic::Ordering::SeqCst);
state.set_connected(connected);
// Notifier si le statut a change
if connected != was_connected {
notifications::connection_changed(connected);
}
thread::sleep(check_interval);
}
println!("[HEALTH] Boucle arretee.");
}
/// Affiche la banniere de demarrage.
fn print_banner(config: &Config) {
println!("======================================================");
println!(" RPA Vision Agent v{} (Rust)", config::AGENT_VERSION);
println!(" Phase 1 -- Headless");
println!(
" RPA Vision Agent v{} (Rust)",
config::AGENT_VERSION
);
println!(" Phases 1-5 — Parite complete");
println!("------------------------------------------------------");
println!(" Machine : {}", config.machine_id);
println!(" Serveur : {}", config.server_url);
println!(" Capture : port {}", config.capture_port);
println!(" Heartbeat: toutes les {}s", config.heartbeat_interval_s);
println!(" JPEG : qualite {}", config.jpeg_quality);
println!(" Machine : {}", config.machine_id);
println!(" Serveur : {}", config.server_url);
println!(" Capture : port {}", config.capture_port);
println!(" Chat : port {}", config.chat_port);
println!(" Heartbeat : toutes les {}s", config.heartbeat_interval_s);
println!(" JPEG : qualite {}", config.jpeg_quality);
println!(" Floutage : {}", if config.blur_sensitive { "actif" } else { "inactif" });
println!(" Logs : retention {} jours", config.log_retention_days);
println!("======================================================");
println!();
println!(" [IA] Cet agent utilise l'intelligence artificielle.");
println!(" Article 50 du Reglement europeen sur l'IA.");
println!();
}