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

@@ -14,7 +14,10 @@ import uuid
import time
import logging
import threading
from .config import SESSIONS_ROOT, AGENT_VERSION, SERVER_URL, MACHINE_ID, LOG_RETENTION_DAYS
from .config import (
SESSIONS_ROOT, AGENT_VERSION, SERVER_URL, MACHINE_ID, LOG_RETENTION_DAYS,
SCREEN_RESOLUTION, DPI_SCALE, OS_THEME,
)
from .core.captor import EventCaptorV1
from .core.executor import ActionExecutorV1
from .network.streamer import TraceStreamer
@@ -103,6 +106,14 @@ class AgentV1:
self._capture_server = CaptureServer()
self._capture_server.start()
# Bannière de démarrage avec métadonnées système
logger.info(
f"Agent V1 v{AGENT_VERSION} | Machine={self.machine_id} | "
f"Ecran={SCREEN_RESOLUTION[0]}x{SCREEN_RESOLUTION[1]} | "
f"DPI={DPI_SCALE}% | Theme={OS_THEME} | "
f"Serveur={SERVER_URL}"
)
# UI Tray intelligent (remplace TrayAppV1, plus de PyQt5)
self.ui = SmartTrayV1(
self.start_session,
@@ -142,8 +153,9 @@ class AgentV1:
# Watchdog de Commandes (GHOST Replay — legacy fichier)
threading.Thread(target=self._command_watchdog_loop, daemon=True).start()
# Boucle de polling replay (P0-5 — pull depuis le serveur)
threading.Thread(target=self._replay_poll_loop, daemon=True).start()
# Note: la boucle de polling replay est déjà lancée dans __init__ (ligne 102)
# Ne PAS en relancer une ici — deux threads poll simultanés causent
# une race condition où les actions sont consommées mais pas exécutées.
logger.info(f"Session {self.session_id} ({workflow_name}) sur machine {self.machine_id} en cours...")
@@ -159,7 +171,7 @@ class AgentV1:
else:
cmd_path = str(BASE_DIR / "command.json")
while self.running:
while self.running and self.session_id:
# Ne pas traiter les commandes fichier pendant un replay serveur
if self._replay_active:
time.sleep(1)
@@ -197,8 +209,11 @@ class AgentV1:
time.sleep(REPLAY_POLL_INTERVAL)
continue
# Utiliser la session active ou un ID par défaut pour le replay
poll_session = self.session_id or f"agent_{self.user_id}"
# TOUJOURS utiliser un session_id stable pour le replay.
# L'enregistrement et le replay sont indépendants : le serveur
# envoie les actions sur agent_{user_id}, pas sur la session
# d'enregistrement (sess_xxx).
poll_session = f"agent_{self.user_id}"
# Log periodique pour confirmer que la boucle tourne (toutes les 60s)
poll_count += 1
@@ -290,18 +305,40 @@ class AgentV1:
time.sleep(5)
def stop_session(self):
self.running = False
# Arrêter la capture et le streaming de la session d'enregistrement
if self.captor: self.captor.stop()
if self.streamer: self.streamer.stop()
logger.info(f"Session {self.session_id} terminée.")
# Reset le session_id pour que le poll replay utilise l'ID stable
self.session_id = None
# Reset le backoff de l'executor pour reprendre le polling immédiatement
if self._executor:
self._executor._poll_backoff = self._executor._poll_backoff_min
self._executor._server_available = True
if hasattr(self._executor, '_last_conn_error_logged'):
self._executor._last_conn_error_logged = False
# NE PAS mettre self.running = False ici !
# self.running contrôle la boucle _replay_poll_loop (permanente).
# Seule la sortie du programme doit le mettre à False.
# Les boucles _heartbeat_loop et _command_watchdog_loop vérifieront
# self.session_id pour savoir si elles doivent fonctionner.
logger.info(
f"Session arrêtée — replay poll actif avec session="
f"agent_{self.user_id}"
)
_last_heartbeat_hash: str = ""
def _heartbeat_loop(self):
"""Capture périodique pour donner du contexte au stagiaire.
Déduplication : n'envoie que si l'écran a changé.
Tourne tant que session_id est défini (= enregistrement actif).
"""
while self.running:
while self.running and self.session_id:
try:
full_path = self.vision.capture_full_context("heartbeat")
if full_path: