feat: replay visuel VLM-first, worker séparé, package Léa, AZERTY, sécurité HTTPS

Pipeline replay visuel :
- VLM-first : l'agent appelle Ollama directement pour trouver les éléments
- Template matching en fallback (seuil strict 0.90)
- Stop immédiat si élément non trouvé (pas de clic blind)
- Replay depuis session brute (/replay-session) sans attendre le VLM
- Vérification post-action (screenshot hash avant/après)
- Gestion des popups (Enter/Escape/Tab+Enter)

Worker VLM séparé :
- run_worker.py : process distinct du serveur HTTP
- Communication par fichiers (_worker_queue.txt + _replay_active.lock)
- Le serveur HTTP ne fait plus jamais de VLM → toujours réactif
- Service systemd rpa-worker.service

Capture clavier :
- raw_keys (vk + press/release) pour replay exact indépendant du layout
- Fix AZERTY : ToUnicodeEx + AltGr detection
- Enter capturé comme \n, Tab comme \t
- Filtrage modificateurs seuls (Ctrl/Alt/Shift parasites)
- Fusion text_input consécutifs, dédup key_combo

Sécurité & Internet :
- HTTPS Let's Encrypt (lea.labs + vwb.labs.laurinebazin.design)
- Token API fixe dans .env.local
- HTTP Basic Auth sur VWB
- Security headers (HSTS, CSP, nosniff)
- CORS domaines publics, plus de wildcard

Infrastructure :
- DPI awareness (SetProcessDpiAwareness) Python + Rust
- Métadonnées système (dpi_scale, window_bounds, monitors, os_theme)
- Template matching multi-scale [0.5, 2.0]
- Résolution dynamique (plus de hardcode 1920x1080)
- VLM prefill fix (47x speedup, 3.5s au lieu de 180s)

Modules :
- core/auth/ : credential vault (Fernet AES), TOTP (RFC 6238), auth handler
- core/federation/ : LearningPack export/import anonymisé, FAISS global
- deploy/ : package Léa (config.txt, Lea.bat, install.bat, LISEZMOI.txt)

UX :
- Filtrage OS (VWB + Chat montrent que les workflows de l'OS courant)
- Bibliothèque persistante (cache local + SQLite)
- Clustering hybride (titre fenêtre + DBSCAN)
- EdgeConstraints + PostConditions peuplés
- GraphBuilder compound actions (toutes les frappes)

Agent Rust :
- Token Bearer auth (network.rs)
- sysinfo.rs (DPI, résolution, window bounds via Win32 API)
- config.txt lu automatiquement
- Support Chrome/Brave/Firefox (pas que Edge)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-03-26 10:19:18 +01:00
parent fe5e0ba83d
commit d5deac3029
162 changed files with 25669 additions and 557 deletions

View File

@@ -30,6 +30,7 @@ mod replay;
mod server;
#[allow(dead_code)]
mod state;
mod sysinfo;
mod tray;
mod visual;
@@ -40,12 +41,20 @@ use std::sync::Arc;
use std::thread;
use std::time::Duration;
/// Trouve Edge sur Windows
/// Trouve un navigateur compatible sur Windows (Edge, Chrome, Brave, Firefox)
#[cfg(target_os = "windows")]
fn find_edge() -> Option<String> {
fn find_browser() -> Option<String> {
let paths = [
// Edge
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
// Chrome
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
// Brave
r"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe",
// Firefox (supporte --kiosk mais pas --app)
r"C:\Program Files\Mozilla Firefox\firefox.exe",
];
for p in &paths {
if std::path::Path::new(p).exists() {
@@ -56,6 +65,37 @@ fn find_edge() -> Option<String> {
}
fn main() {
// --- DPI awareness (DOIT etre appele avant toute operation graphique) ---
// Rend le process DPI-aware sur Windows pour que les API (enigo, xcap,
// GetSystemMetrics, etc.) travaillent en coordonnees physiques (pixels reels)
// au lieu de coordonnees logiques (virtualisees par le DPI scaling).
// Sans cet appel, un ecran 2560x1600 a 150% DPI apparait comme 1707x1067
// pour enigo et GetSystemMetrics, ce qui cause des erreurs de positionnement
// pendant le replay.
// PROCESS_PER_MONITOR_DPI_AWARE = 2 : le niveau le plus precis.
#[cfg(target_os = "windows")]
{
// SetProcessDpiAwareness (shcore.dll) et SetProcessDPIAware (user32.dll)
// ne sont pas toujours exposes par windows-sys selon les features.
// On utilise des appels FFI raw pour eviter d'ajouter des features.
#[link(name = "shcore")]
extern "system" {
fn SetProcessDpiAwareness(value: i32) -> i32;
}
#[link(name = "user32")]
extern "system" {
fn SetProcessDPIAware() -> i32;
}
unsafe {
// Tenter SetProcessDpiAwareness(2) = PROCESS_PER_MONITOR_DPI_AWARE
let hr = SetProcessDpiAwareness(2);
if hr != 0 {
// Fallback pour Windows < 8.1 : SetProcessDPIAware()
SetProcessDPIAware();
}
}
}
// Initialiser le logging
env_logger::Builder::from_env(
env_logger::Env::default().default_filter_or("info"),
@@ -135,15 +175,41 @@ fn main() {
let chat_state = state.clone();
chat::run_chat_thread(&chat_config, chat_state);
// Synchroniser les workflows disponibles depuis le serveur
let sync_config = config.clone();
let workflows = {
let client = Client::new();
network::fetch_workflows(&client, &sync_config)
};
if workflows.is_empty() {
println!("[MAIN] Aucun workflow disponible pour cette machine.");
} else {
println!(
"[MAIN] {} workflow(s) disponible(s) :",
workflows.len()
);
for wf in &workflows {
println!(
" - {} ({} noeuds, {} transitions)",
wf.name, wf.nodes, wf.edges
);
}
}
println!("\n[MAIN] Agent operationnel — tous les threads demarres.\n");
// Ouvrir Léa (Edge mode app) automatiquement au démarrage
// Ouvrir Léa dans le navigateur disponible (mode app) au démarrage
#[cfg(target_os = "windows")]
{
let chat_url = config.chat_url();
if let Some(edge) = find_edge() {
println!("[MAIN] Ouverture de Léa dans Edge...");
let _ = std::process::Command::new(&edge)
if let Some(browser) = find_browser() {
let browser_name = if browser.contains("chrome") { "Chrome" }
else if browser.contains("edge") || browser.contains("Edge") { "Edge" }
else if browser.contains("brave") || browser.contains("Brave") { "Brave" }
else if browser.contains("firefox") || browser.contains("Firefox") { "Firefox" }
else { "navigateur" };
println!("[MAIN] Ouverture de Léa dans {}...", browser_name);
let _ = std::process::Command::new(&browser)
.args(&[
&format!("--app={}", chat_url),
"--window-size=600,800",
@@ -151,6 +217,8 @@ fn main() {
"--no-first-run",
])
.spawn();
} else {
println!("[MAIN] Aucun navigateur trouvé — ouvrez manuellement : {}", chat_url);
}
}
@@ -304,9 +372,8 @@ fn health_check_loop(config: &Config, state: &AgentState) {
while state.is_running() {
let url = format!("{}/stats", config.server_url);
let connected = client
.get(&url)
.timeout(timeout)
let request = client.get(&url).timeout(timeout);
let connected = network::with_auth(request, config)
.send()
.map(|r| r.status().is_success())
.unwrap_or(false);
@@ -327,6 +394,8 @@ fn health_check_loop(config: &Config, state: &AgentState) {
/// Affiche la banniere de demarrage.
fn print_banner(config: &Config) {
let meta = sysinfo::get_screen_metadata();
println!("======================================================");
println!(
" RPA Vision Agent v{} (Rust)",
@@ -342,6 +411,17 @@ fn print_banner(config: &Config) {
println!(" JPEG : qualite {}", config.jpeg_quality);
println!(" Floutage : {}", if config.blur_sensitive { "actif" } else { "inactif" });
println!(" Logs : retention {} jours", config.log_retention_days);
println!(" Auth : {}", if config.api_token.is_empty() { "aucune" } else { "Bearer token" });
println!(" Workflows : synchronisation au demarrage");
println!(
" Ecran : {}x{} @ {}% DPI",
meta.screen_resolution[0], meta.screen_resolution[1], meta.dpi_scale
);
println!(
" Moniteur : #{} ({})",
meta.monitor_index,
if meta.monitor_index == 0 { "principal" } else { "secondaire" }
);
println!("======================================================");
println!();
println!(" [IA] Cet agent utilise l'intelligence artificielle.");