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:
@@ -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!();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user