feat: anti-détection robot — Bézier mouse + frappe char-by-char
Pour les environnements Citrix avec détection de robots : - Souris : courbe de Bézier quadratique avec déviation aléatoire et vitesse variable (25 étapes, plus lent début/fin) - Texte : frappe caractère par caractère via KeyCode.from_char() avec délai aléatoire 40-120ms (pas de copier-coller) - Plus de presse-papiers (Ctrl+V détectable) Annulation du fix raw_keys→clipboard (plus nécessaire). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
for char in text:
|
||||
try:
|
||||
import pyperclip
|
||||
# Sauvegarder le contenu actuel du presse-papiers
|
||||
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
|
||||
|
||||
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()")
|
||||
self.keyboard.type(char)
|
||||
except Exception as e:
|
||||
logger.warning(f"Copier-coller echoue ({e}), fallback sur keyboard.type()")
|
||||
logger.debug(f"Impossible de taper '{char}': {e}")
|
||||
# Délai humain entre les frappes (40-120ms)
|
||||
time.sleep(random.uniform(0.04, 0.12))
|
||||
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user