feat: agent Rust Phase 1 — POC headless fonctionnel

1527 lignes Rust, compile sans warnings, testé sur Linux.
- Capture d'écran (xcap) + JPEG base64 + hash dedup
- Heartbeat toutes les 5s vers streaming server
- Poll replay + exécution actions (clic, frappe, combos)
- Serveur HTTP port 5006 (capture, health, file-action)
- Compatible avec le streaming server Python existant

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-03-18 20:22:04 +01:00
parent 792cc2aa9a
commit 757432ee19
10 changed files with 1702 additions and 0 deletions

207
agent_rust/src/main.rs Normal file
View File

@@ -0,0 +1,207 @@
//! Agent RPA Vision — Phase 1 (headless)
//!
//! 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
//!
//! Configuration via variables d'environnement ou valeurs par defaut.
//! Compatible avec le serveur streaming existant (api_stream.py, port 5005).
mod capture;
mod config;
mod executor;
mod network;
mod replay;
mod server;
use config::Config;
use reqwest::blocking::Client;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::Duration;
/// Flag global pour l'arret propre (Ctrl+C)
static RUNNING: AtomicBool = AtomicBool::new(true);
fn main() {
let config = Config::from_env();
// 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();
// Verifier que la capture d'ecran fonctionne
print!("[MAIN] Test de capture d'ecran... ");
match capture::screen_dimensions() {
Some((w, h)) => println!("OK ({}x{})", w, h),
None => {
println!("ECHEC");
eprintln!("[MAIN] ATTENTION : Capture d'ecran non disponible.");
eprintln!("[MAIN] Sur Linux sans display, les heartbeats seront desactives.");
}
}
// Thread 1 : Heartbeat loop
let hb_config = config.clone();
let heartbeat_thread = thread::Builder::new()
.name("heartbeat".to_string())
.spawn(move || {
heartbeat_loop(&hb_config);
})
.expect("Impossible de demarrer le thread heartbeat");
// Thread 2 : Replay poll loop
let rp_config = config.clone();
let _replay_thread = thread::Builder::new()
.name("replay".to_string())
.spawn(move || {
replay::replay_poll_loop(&rp_config);
})
.expect("Impossible de demarrer le thread replay");
// Thread 3 : Capture HTTP server
let srv_port = config.capture_port;
let _server_thread = thread::Builder::new()
.name("capture-server".to_string())
.spawn(move || {
server::start_capture_server(srv_port);
})
.expect("Impossible de demarrer le thread serveur");
println!("\n[MAIN] Agent operationnel. Appuyez sur Ctrl+C pour quitter.\n");
// Bloquer le thread principal en attendant Ctrl+C
while RUNNING.load(Ordering::SeqCst) {
thread::sleep(Duration::from_millis(500));
}
println!("\n[MAIN] Arret en cours...");
// Attendre le thread heartbeat (les autres sont daemon-like)
let _ = heartbeat_thread.join();
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
#[cfg(unix)]
{
// Creer un pipe pour la notification
let mut fds = [0i32; 2];
unsafe {
if libc::pipe(fds.as_mut_ptr()) != 0 {
eprintln!("[MAIN] Impossible de creer le pipe pour Ctrl+C");
return;
}
// Installer le signal handler qui ecrit dans le pipe
static mut WRITE_FD: i32 = -1;
WRITE_FD = fds[1];
extern "C" fn sigint_handler(_sig: i32) {
unsafe {
RUNNING.store(false, Ordering::SeqCst);
let buf = [1u8];
let _ = libc::write(WRITE_FD, buf.as_ptr() as *const _, 1);
}
}
libc::signal(libc::SIGINT, sigint_handler as *const () as libc::sighandler_t);
}
}
#[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)
}
}
/// 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) {
let client = Client::new();
let session_id = config.bg_session_id();
let mut last_hash: u64 = 0;
let mut consecutive_errors: u32 = 0;
println!(
"[HEARTBEAT] Boucle permanente demarree (session={}, intervalle={}s)",
session_id, config.heartbeat_interval_s
);
while RUNNING.load(Ordering::SeqCst) {
// 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;
// Encoder en JPEG
let jpeg_bytes = capture::screenshot_to_jpeg_bytes(&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);
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
);
}
}
}
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;
}
}
thread::sleep(Duration::from_secs(config.heartbeat_interval_s));
}
println!("[HEARTBEAT] 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!("------------------------------------------------------");
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!("======================================================");
}