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

@@ -80,7 +80,13 @@ app = Flask(__name__)
import secrets as _secrets
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', _secrets.token_hex(32))
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50 MB max upload (sécurité HIGH)
socketio = SocketIO(app, cors_allowed_origins="*")
_ALLOWED_ORIGINS = [
"http://localhost:3002",
"http://localhost:5002",
"https://vwb.labs.laurinebazin.design",
"https://lea.labs.laurinebazin.design",
]
socketio = SocketIO(app, cors_allowed_origins=_ALLOWED_ORIGINS)
# ============================================================
@@ -92,6 +98,7 @@ def set_security_headers(response):
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
return response
@@ -116,6 +123,16 @@ STREAMING_SERVER_URL = os.environ.get(
"RPA_STREAMING_URL", "http://localhost:5005"
)
# Token API pour le streaming server
_STREAMING_API_TOKEN = os.environ.get("RPA_API_TOKEN", "")
def _streaming_headers() -> dict:
"""Headers d'authentification pour les appels au streaming server."""
headers = {"Content-Type": "application/json"}
if _STREAMING_API_TOKEN:
headers["Authorization"] = f"Bearer {_STREAMING_API_TOKEN}"
return headers
execution_status = {
"running": False,
"workflow": None,
@@ -135,6 +152,7 @@ def _fetch_connected_machines() -> List[Dict[str, Any]]:
try:
resp = http_requests.get(
f"{STREAMING_SERVER_URL}/api/v1/traces/stream/machines",
headers=_streaming_headers(),
timeout=3,
)
if resp.ok:
@@ -384,7 +402,7 @@ def api_status():
@app.route('/api/workflows')
def api_workflows():
"""Liste unifiée des workflows (appris + VWB).
"""Liste unifiée des workflows (appris + VWB), filtrée par OS.
Sources fusionnées :
1. Workflows appris (SemanticMatcher — data/training/workflows/)
@@ -392,10 +410,20 @@ def api_workflows():
Dédupliqués par nom : si un workflow appris a été importé dans le VWB,
seule la version VWB est retournée (c'est la version validée/corrigée).
Query params:
os: Filtrer par OS — 'windows' ou 'linux' (optionnel).
Par défaut, détecte l'OS du serveur Léa (= la machine du docteur).
"""
if not matcher:
return jsonify({"workflows": [], "directories": []})
# Détecter l'OS : paramètre explicite ou auto-détection depuis la plateforme
os_filter = request.args.get('os')
if not os_filter:
import platform
os_filter = 'windows' if platform.system().lower() == 'windows' else 'linux'
seen_ids = set()
workflows = []
@@ -433,6 +461,21 @@ def api_workflows():
workflows.append(vwb_wf)
seen_ids.add(vwb_id)
# Filtrer par OS : ne montrer que les workflows compatibles avec la machine du docteur
# Le machine_id ou source_dir contient le nom OS (ex: DESKTOP-58D5CAC_windows, dom-X870_linux)
if os_filter:
os_lower = os_filter.lower()
filtered_workflows = []
for wf in workflows:
mid = (wf.get("machine_id") or "").lower()
src = (wf.get("source") or "").lower()
# Un workflow VWB (sans machine_id) passe toujours le filtre
if wf.get("origin") == "vwb" and not mid:
filtered_workflows.append(wf)
elif os_lower in mid or os_lower in src:
filtered_workflows.append(wf)
workflows = filtered_workflows
# Récupérer la liste des machines connectées depuis le streaming server
machines = _fetch_connected_machines()
@@ -1128,6 +1171,7 @@ def _execute_gesture(gesture):
try:
resp = http_requests.post(
f"{STREAMING_SERVER_URL}/api/v1/traces/stream/replay/raw",
headers=_streaming_headers(),
json={
"actions": [action],
"session_id": "",
@@ -1654,6 +1698,7 @@ def _try_streaming_server_replay(
resp = http_requests.post(
f"{STREAMING_SERVER_URL}/api/v1/traces/stream/replay",
headers=_streaming_headers(),
json=payload,
timeout=15,
)
@@ -1696,6 +1741,7 @@ def _poll_replay_progress(replay_id: str, workflow_name: str, total_actions: int
try:
resp = http_requests.get(
f"{STREAMING_SERVER_URL}/api/v1/traces/stream/replay/{replay_id}",
headers=_streaming_headers(),
timeout=3,
)
if not resp.ok:
@@ -1968,6 +2014,7 @@ def execute_workflow_copilot(match, params: Dict[str, Any]):
try:
resp = http_requests.post(
f"{STREAMING_SERVER_URL}/api/v1/traces/stream/replay/single",
headers=_streaming_headers(),
json={
"action": action,
"session_id": "",

View File

@@ -197,7 +197,8 @@ NOT_FOUND"""
prompt=prompt,
image=screenshot,
temperature=0.1,
max_tokens=100
max_tokens=100,
assistant_prefill="COORDINATES:",
)
if result.get('success'):