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

@@ -135,27 +135,48 @@ class ContextLevel:
@dataclass
class WindowContext:
"""Contexte de fenêtre"""
"""Contexte de fenêtre avec métadonnées d'environnement graphique"""
app_name: str
window_title: str
screen_resolution: List[int]
workspace: str = "main"
monitor_index: int = 0 # Index du moniteur (0 = principal)
dpi_scale: int = 100 # Facteur DPI en % (100 = normal, 150 = haute résolution)
window_bounds: Optional[List[int]] = None # [x, y, width, height] de la fenêtre
monitors: Optional[List[Dict[str, int]]] = None # Liste des moniteurs [{width, height, x, y}]
os_theme: str = "unknown" # "light", "dark", "unknown"
os_language: str = "unknown" # Code langue (fr, en, de...)
def to_dict(self) -> Dict[str, Any]:
return {
result = {
"app_name": self.app_name,
"window_title": self.window_title,
"screen_resolution": self.screen_resolution,
"workspace": self.workspace
"workspace": self.workspace,
"monitor_index": self.monitor_index,
"dpi_scale": self.dpi_scale,
"os_theme": self.os_theme,
"os_language": self.os_language,
}
if self.window_bounds is not None:
result["window_bounds"] = self.window_bounds
if self.monitors is not None:
result["monitors"] = self.monitors
return result
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'WindowContext':
return cls(
app_name=data["app_name"],
window_title=data["window_title"],
screen_resolution=data["screen_resolution"],
workspace=data.get("workspace", "main")
workspace=data.get("workspace", "main"),
monitor_index=data.get("monitor_index", 0),
dpi_scale=data.get("dpi_scale", 100),
window_bounds=data.get("window_bounds"),
monitors=data.get("monitors"),
os_theme=data.get("os_theme", "unknown"),
os_language=data.get("os_language", "unknown"),
)