diff --git a/agent_v0/agent_v1/core/executor.py b/agent_v0/agent_v1/core/executor.py index 6fc3f8931..cb3b35978 100644 --- a/agent_v0/agent_v1/core/executor.py +++ b/agent_v0/agent_v1/core/executor.py @@ -1375,58 +1375,43 @@ Example: x_pct=0.50, y_pct=0.30""" # ========================================================================= def _type_text(self, text: str): - """Saisir du texte via copier-coller (methode principale) ou keyboard.type (fallback). + """Saisir du texte caractère par caractère (anti-détection robot). - Le copier-coller via le presse-papiers est la methode principale car - keyboard.type() de pynput envoie les scancodes QWERTY, ce qui produit - des caracteres incorrects sur les claviers AZERTY (ex: "ce" -> "ci"). - Le copier-coller est agnostique du layout clavier. + Chaque caractère est tapé individuellement avec un délai aléatoire + pour simuler une frappe humaine. Les caractères spéciaux AZERTY + (@ # € etc.) utilisent les bons VK codes via KeyCode.from_char(). + + Pas de copier-coller (détectable par les systèmes anti-robot Citrix). """ + import random + if not text: return - clipboard_ok = False - try: - import pyperclip - # Sauvegarder le contenu actuel du presse-papiers + for char in text: try: - old_clipboard = pyperclip.paste() + # Taper le caractère via from_char (respecte le layout clavier) + self.keyboard.press(KeyCode.from_char(char)) + self.keyboard.release(KeyCode.from_char(char)) except Exception: - old_clipboard = None - - pyperclip.copy(text) - # Ctrl+V pour coller - self.keyboard.press(Key.ctrl) - time.sleep(0.02) - self.keyboard.press('v') - self.keyboard.release('v') - self.keyboard.release(Key.ctrl) - time.sleep(0.1) - - # Restaurer le presse-papiers original - if old_clipboard is not None: + # Fallback : keyboard.type pour les cas spéciaux try: - pyperclip.copy(old_clipboard) - except Exception: - pass + self.keyboard.type(char) + except Exception as e: + logger.debug(f"Impossible de taper '{char}': {e}") + # Délai humain entre les frappes (40-120ms) + time.sleep(random.uniform(0.04, 0.12)) - clipboard_ok = True - logger.debug(f"Texte saisi via presse-papiers ({len(text)} chars)") - except ImportError: - logger.debug("pyperclip non disponible, fallback sur keyboard.type()") - except Exception as e: - logger.warning(f"Copier-coller echoue ({e}), fallback sur keyboard.type()") - - if not clipboard_ok: - self.keyboard.type(text) + logger.debug(f"Texte saisi char-by-char ({len(text)} chars)") def _click(self, pos, button_name): - """Deplacer la souris et cliquer. + """Deplacer la souris via courbe de Bézier puis cliquer. - Supporte les boutons : left, right, double (double-clic gauche). + Le mouvement en courbe de Bézier simule un déplacement humain + (anti-détection robot pour Citrix et systèmes surveillés). """ - self.mouse.position = pos - time.sleep(0.1) # Delai pour simuler le temps de reaction humain + self._bezier_move(pos) + time.sleep(0.05) if button_name == "double": self.mouse.click(Button.left, 2) @@ -1435,6 +1420,35 @@ Example: x_pct=0.50, y_pct=0.30""" else: self.mouse.click(Button.left) + def _bezier_move(self, target, steps=25): + """Déplacer la souris vers target via une courbe de Bézier cubique. + + Génère un mouvement naturel avec un point de contrôle aléatoire + pour éviter les lignes droites détectables par les anti-bots. + """ + import random + + start = self.mouse.position + sx, sy = start + tx, ty = target + + # Point de contrôle aléatoire (déviation latérale) + dist = ((tx - sx) ** 2 + (ty - sy) ** 2) ** 0.5 + deviation = max(20, dist * 0.2) + cx = (sx + tx) / 2 + random.uniform(-deviation, deviation) + cy = (sy + ty) / 2 + random.uniform(-deviation, deviation) + + for i in range(1, steps + 1): + t = i / steps + # Bézier quadratique : B(t) = (1-t)²·S + 2(1-t)t·C + t²·T + inv_t = 1 - t + x = inv_t * inv_t * sx + 2 * inv_t * t * cx + t * t * tx + y = inv_t * inv_t * sy + 2 * inv_t * t * cy + t * t * ty + self.mouse.position = (int(x), int(y)) + # Vitesse variable (plus lent au début et à la fin) + speed = 0.005 + 0.01 * (1 - abs(2 * t - 1)) + time.sleep(speed) + def _execute_key_combo(self, keys: list): """ Executer une combinaison de touches. diff --git a/agent_v0/server_v1/stream_processor.py b/agent_v0/server_v1/stream_processor.py index 92f9409bd..eef178f1d 100644 --- a/agent_v0/server_v1/stream_processor.py +++ b/agent_v0/server_v1/stream_processor.py @@ -1258,19 +1258,6 @@ def build_replay_from_raw_events( if session_dir_path: _attach_expected_screenshots(result, events, session_dir_path) - # ── 9. Supprimer raw_keys incomplets (chars fusionnés depuis key_combos) ── - # Quand le texte contient des caractères venant de key_combos convertis - # (ex: @ de AltGr), les raw_keys ne les couvrent pas. Forcer le copier-coller. - for action in result: - if action.get("type") == "type" and action.get("raw_keys"): - recon = _reconstruct_text_from_raw_keys(action["raw_keys"]) - if len(recon) < len(action.get("text", "")): - del action["raw_keys"] - logger.debug( - "raw_keys supprimés pour '%s' (recon=%d < text=%d)", - action["text"][:30], len(recon), len(action["text"]), - ) - # Stats visual replay visual_clicks = sum( 1 for a in result