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:
@@ -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": "",
|
||||
|
||||
@@ -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'):
|
||||
|
||||
Reference in New Issue
Block a user