fix: xdotool prioritaire sur clipboard (VM/Citrix), cosmétique sidebar
Some checks failed
security-audit / Bandit (scan statique) (push) Successful in 12s
security-audit / pip-audit (CVE dépendances) (push) Successful in 11s
security-audit / Scan secrets (grep) (push) Successful in 8s
tests / Lint (ruff + black) (push) Successful in 14s
tests / Tests unitaires (sans GPU) (push) Failing after 13s
tests / Tests sécurité (critique) (push) Has been skipped

safe_type_text() : xdotool type en priorité au lieu du presse-papier.
Le clipboard xclip ne traverse pas les VM (QEMU) ni Citrix/RDP.
xdotool envoie des frappes X11 réelles que les VM capturent.
Délai 20ms entre caractères pour fiabilité.

Cosmétique : couleur texte forcée sur les items workflow du sidebar
(color: var(--text-primary)) — était blanc sur blanc.

Logs diagnostic ajoutés dans execute_workflow_thread et execute_action.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-18 23:11:10 +02:00
parent 2431a6c9e9
commit 3d243d731d
2 changed files with 40 additions and 28 deletions

View File

@@ -24,23 +24,32 @@ logger = logging.getLogger(__name__)
def safe_type_text(text):
"""Saisie de texte compatible avec tous les claviers (AZERTY/QWERTY).
pyautogui.write() envoie des codes de touches QWERTY qui produisent
des caractères erronés sur AZERTY (* → µ, ( → 5, ) → °, etc.).
"""Saisie de texte compatible VM/Citrix et claviers AZERTY/QWERTY.
Priorité :
1. Presse-papier (xclip) + Ctrl+V → instantané, fiable pour tout texte
2. xdotool type → respecte le layout clavier
3. pyautogui.write() → dernier recours
1. xdotool type → frappes clavier réelles, traverse QEMU/Citrix/RDP
2. Presse-papier (xclip) + Ctrl+V → fallback local uniquement
3. pyautogui.write() → dernier recours
"""
import shutil
import pyautogui
# Méthode 1 : Presse-papier (le plus fiable, gère UTF-8/accents/CJK)
# xclip reste en vie comme daemon X11 pour servir le clipboard.
# On ne doit PAS attendre sa terminaison — juste envoyer les données
# et coller immédiatement.
# Méthode 1 : xdotool type (frappes clavier réelles)
# Envoie des événements X11 que QEMU/Citrix/RDP capturent et transmettent.
# Le presse-papier ne traverse PAS les VM — xdotool si.
if shutil.which('xdotool'):
try:
subprocess.run(
['xdotool', 'type', '--delay', '20', '--clearmodifiers', '--', text],
timeout=max(30, len(text) * 0.05),
check=True
)
print(f" ✅ Saisie via xdotool type ({len(text)} car.)")
return
except Exception as e:
print(f" ⚠️ xdotool type échoué: {e}")
# Méthode 2 : Presse-papier (fonctionne seulement en local, pas en VM)
xclip = shutil.which('xclip')
if xclip:
try:
@@ -52,7 +61,6 @@ def safe_type_text(text):
)
p.stdin.write(text.encode('utf-8'))
p.stdin.close()
# Ne PAS attendre xclip — il reste vivant comme owner du clipboard
time.sleep(0.2)
pyautogui.hotkey('ctrl', 'v')
time.sleep(0.3)
@@ -61,19 +69,6 @@ def safe_type_text(text):
except Exception as e:
print(f" ⚠️ xclip échoué: {e}")
# Méthode 2 : xdotool type (respecte le layout clavier actif)
if shutil.which('xdotool'):
try:
subprocess.run(
['xdotool', 'type', '--delay', '0', '--clearmodifiers', '--', text],
timeout=max(30, len(text) * 0.05),
check=True
)
print(f" ✅ Saisie via xdotool type ({len(text)} car.)")
return
except Exception as e:
print(f" ⚠️ xdotool type échoué: {e}")
# Méthode 3 : pyautogui (dernier recours — problèmes AZERTY possibles)
print(" ⚠️ Saisie via pyautogui.write() (AZERTY non garanti)")
pyautogui.write(text)
@@ -87,7 +82,8 @@ def minimize_active_window():
# Minimiser la fenêtre active
subprocess.run(['xdotool', 'getactivewindow', 'windowminimize'],
capture_output=True, timeout=2)
print("📦 [Execute] Fenêtre du navigateur minimisée")
time.sleep(1.5)
print("📦 [Execute] Fenêtre du navigateur minimisée (+ 1.5s d'attente)")
return True
except FileNotFoundError:
print("⚠️ [Execute] xdotool non installé - impossible de minimiser")
@@ -183,7 +179,11 @@ def execute_workflow_thread(execution_id: str, workflow_id: str, app):
db.session.add(step_result)
db.session.commit()
print(f"📋 [Execute] Étape {index + 1}/{len(steps)}: {step.action_type} (id={step.id})")
print(f"\n{'='*60}")
print(f"📋 [Execute] Étape {index + 1}/{len(steps)}: {step.action_type}")
print(f" step_id={step.id}, label={step.label}")
print(f" anchor_id={step.anchor_id or 'aucune'}")
print(f" mode={_execution_state.get('execution_mode')}")
try:
# === VALIDATION CONTRAT STRICT ===
@@ -780,13 +780,19 @@ def execute_action(action_type: str, params: dict) -> dict:
execution_mode = _execution_state.get('execution_mode', 'basic')
print(f" [dispatch] action={action_type} mode={execution_mode}")
print(f" [dispatch] params_keys={list(params.keys())}")
try:
if action_type in ['click_anchor', 'click', 'double_click_anchor', 'right_click_anchor']:
# Récupérer les coordonnées depuis l'ancre
anchor = params.get('visual_anchor', {})
bbox = anchor.get('bounding_box', {})
screenshot_base64 = anchor.get('screenshot')
print(f" [click] bbox={bbox}")
print(f" [click] has_screenshot={bool(screenshot_base64)} ({len(screenshot_base64) if screenshot_base64 else 0} chars)")
print(f" [click] screen_size={pyautogui.size()}")
if not bbox:
return {'success': False, 'error': 'Pas de bounding_box dans visual_anchor'}