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):
|
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
|
Chaque caractère est tapé individuellement avec un délai aléatoire
|
||||||
keyboard.type() de pynput envoie les scancodes QWERTY, ce qui produit
|
pour simuler une frappe humaine. Les caractères spéciaux AZERTY
|
||||||
des caracteres incorrects sur les claviers AZERTY (ex: "ce" -> "ci").
|
(@ # € etc.) utilisent les bons VK codes via KeyCode.from_char().
|
||||||
Le copier-coller est agnostique du layout clavier.
|
|
||||||
|
Pas de copier-coller (détectable par les systèmes anti-robot Citrix).
|
||||||
"""
|
"""
|
||||||
|
import random
|
||||||
|
|
||||||
if not text:
|
if not text:
|
||||||
return
|
return
|
||||||
|
|
||||||
clipboard_ok = False
|
for char in text:
|
||||||
try:
|
|
||||||
import pyperclip
|
|
||||||
# Sauvegarder le contenu actuel du presse-papiers
|
|
||||||
try:
|
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:
|
except Exception:
|
||||||
old_clipboard = None
|
# Fallback : keyboard.type pour les cas spéciaux
|
||||||
|
|
||||||
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:
|
|
||||||
try:
|
try:
|
||||||
pyperclip.copy(old_clipboard)
|
self.keyboard.type(char)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
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 char-by-char ({len(text)} chars)")
|
||||||
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)
|
|
||||||
|
|
||||||
def _click(self, pos, button_name):
|
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
|
self._bezier_move(pos)
|
||||||
time.sleep(0.1) # Delai pour simuler le temps de reaction humain
|
time.sleep(0.05)
|
||||||
|
|
||||||
if button_name == "double":
|
if button_name == "double":
|
||||||
self.mouse.click(Button.left, 2)
|
self.mouse.click(Button.left, 2)
|
||||||
@@ -1435,6 +1420,35 @@ Example: x_pct=0.50, y_pct=0.30"""
|
|||||||
else:
|
else:
|
||||||
self.mouse.click(Button.left)
|
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):
|
def _execute_key_combo(self, keys: list):
|
||||||
"""
|
"""
|
||||||
Executer une combinaison de touches.
|
Executer une combinaison de touches.
|
||||||
|
|||||||
@@ -1258,19 +1258,6 @@ def build_replay_from_raw_events(
|
|||||||
if session_dir_path:
|
if session_dir_path:
|
||||||
_attach_expected_screenshots(result, events, 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
|
# Stats visual replay
|
||||||
visual_clicks = sum(
|
visual_clicks = sum(
|
||||||
1 for a in result
|
1 for a in result
|
||||||
|
|||||||
Reference in New Issue
Block a user