| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691 |
- # from DrissionPage import ChromiumPage
- # # 假设上面的代码保存在 human_mouse.py
- # from human_mouse import HumanMouse
- # # 初始化 DrissionPage
- # page = ChromiumPage()
- # page.get('https://example.com')
- # # 初始化我们移植的鼠标控制器,开启 debug 模式可在页面上看到鼠标轨迹点
- # mouse = HumanMouse(page, debug=True)
- # # 1. 拟人化移动到一个坐标
- # mouse.move(500, 300, humanize=True)
- # # 2. 拟人化点击
- # mouse.click(600, 400, humanize=True)
- # # 3. 如果需要结合 DrissionPage 元素一起使用:
- # ele = page.ele('@href=https://www.iana.org/domains/example')
- # # 获取元素在屏幕中的中点坐标
- # x, y = ele.rect.midpoint
- # mouse.click(x, y, humanize=True)
- import logging
- import math
- import random
- import time
- from dataclasses import dataclass
- from enum import Enum
- from typing import Optional, Tuple
- from DrissionPage import ChromiumPage
- from utils.scroll import HumanScroll
- from utils.math_utils import (
- bezier_2d,
- fitts_duration,
- minimum_jerk,
- random_control_points,
- )
- logger = logging.getLogger(__name__)
- class MouseButton(Enum):
- LEFT = "left"
- RIGHT = "right"
- MIDDLE = "middle"
- class MouseEventType(Enum):
- MOUSE_MOVED = "mouseMoved"
- MOUSE_PRESSED = "mousePressed"
- MOUSE_RELEASED = "mouseReleased"
- @dataclass(frozen=True)
- class MouseTimingConfig:
- """模拟人类鼠标移动物理特性的配置"""
- fitts_a: float = 0.070
- fitts_b: float = 0.150
- frame_interval: float = 0.012
- frame_interval_variance: float = 0.004
- curvature_min: float = 0.10
- curvature_max: float = 0.28
- curvature_asymmetry: float = 0.6
- short_distance_threshold: float = 50.0
- tremor_amplitude: float = 0.85
- overshoot_probability: float = 0.22
- overshoot_distance_min: float = 0.03
- overshoot_distance_max: float = 0.10
- overshoot_speed_threshold: float = 260.0
- pre_click_pause_min: float = 0.04
- pre_click_pause_max: float = 0.16
- click_hold_min: float = 0.04
- click_hold_max: float = 0.12
- double_click_interval_min: float = 0.05
- double_click_interval_max: float = 0.09
- drag_start_pause_min: float = 0.06
- drag_start_pause_max: float = 0.16
- drag_end_pause_min: float = 0.04
- drag_end_pause_max: float = 0.12
- micro_pause_probability: float = 0.08
- micro_pause_min: float = 0.010
- micro_pause_max: float = 0.030
- min_duration: float = 0.08
- max_duration: float = 2.2
-
- # ==========================================
- # 10套拟人化鼠标行为配置预设 (User Profiles)
- # ==========================================
- MOUSE_PROFILES = {
- # 1. 【电竞玩家 / 熟练用户】
- # 特征:移动极快,轨迹趋近直线,因为速度太快偶尔会有轻微过冲,点击干脆利落。
- "gamer": MouseTimingConfig(
- fitts_a=0.040, fitts_b=0.090, # 移动时间极短
- curvature_min=0.05, curvature_max=0.15, # 轨迹很直
- tremor_amplitude=0.40, # 手极其稳
- overshoot_probability=0.25, overshoot_distance_min=0.02, overshoot_distance_max=0.06, # 容易滑过头但偏差小
- pre_click_pause_min=0.02, pre_click_pause_max=0.06, # 几乎不犹豫直接点
- click_hold_min=0.02, click_hold_max=0.06, # 点击非常轻快
- micro_pause_probability=0.02, # 中途基本不犹豫
- min_duration=0.05, max_duration=1.2
- ),
- # 2. 【老年人 / 鼠标新手】
- # 特征:移动缓慢,轨迹弧度大且手抖严重,经常在半路停顿,点击按压时间很长。
- "elderly": MouseTimingConfig(
- fitts_a=0.150, fitts_b=0.250, # 移动非常耗时
- curvature_min=0.15, curvature_max=0.40, # 巨大的弧线
- tremor_amplitude=1.80, # 手部抖动明显
- overshoot_probability=0.05, # 慢到基本不会过冲
- pre_click_pause_min=0.15, pre_click_pause_max=0.35, # 到了目标要看半天才点
- click_hold_min=0.10, click_hold_max=0.25, # 按下去很久才松开
- micro_pause_probability=0.25, micro_pause_min=0.05, micro_pause_max=0.15, # 半路经常停下来找光标
- min_duration=0.2, max_duration=3.5
- ),
- # 3. 【触控板 / 笔记本用户】
- # 特征:轨迹不对称,经常因为手指滑动到边缘而产生“微小停顿”(抬起手指重新滑动),无明显过冲。
- "trackpad": MouseTimingConfig(
- fitts_a=0.080, fitts_b=0.180,
- curvature_asymmetry=0.8, # 触控板滑动极不对称
- tremor_amplitude=0.50,
- overshoot_probability=0.05, # 触控板有天然加速度,一般不会过冲
- micro_pause_probability=0.35, micro_pause_min=0.03, micro_pause_max=0.12, # 频繁微停顿(手指滑出边缘重置)
- drag_start_pause_max=0.25, drag_end_pause_max=0.20, # 触控板拖拽很困难,停顿长
- double_click_interval_min=0.08, double_click_interval_max=0.15
- ),
- # 4. 【急躁 / 喝了咖啡的用户】
- # 特征:速度快但不精确,手抖严重,极度容易“过冲”并需要大幅度回调,点击间隔很短。
- "caffeinated": MouseTimingConfig(
- fitts_a=0.050, fitts_b=0.110,
- curvature_min=0.10, curvature_max=0.25,
- tremor_amplitude=1.50, # 兴奋状态,手抖大
- overshoot_probability=0.45, overshoot_distance_min=0.05, overshoot_distance_max=0.15, # 疯狂冲过头
- pre_click_pause_min=0.01, pre_click_pause_max=0.05,
- double_click_interval_min=0.03, double_click_interval_max=0.07, # 连击速度极快
- min_duration=0.05, max_duration=1.5
- ),
- # 5. 【心不在焉 / 看剧摸鱼的用户】
- # 特征:移动到一半可能会长时间停住(抬头看其他屏幕),点击前犹豫时间极长。
- "distracted": MouseTimingConfig(
- fitts_a=0.090, fitts_b=0.160,
- tremor_amplitude=0.90,
- overshoot_probability=0.15,
- pre_click_pause_min=0.20, pre_click_pause_max=0.80, # 到了按钮上发呆很久才点
- micro_pause_probability=0.15, micro_pause_min=0.10, micro_pause_max=0.60, # 中途长时间停顿
- click_hold_min=0.05, click_hold_max=0.15,
- max_duration=4.0
- ),
- # 6. 【疲惫 / 深夜工作的用户】
- # 特征:整体响应迟缓,弧度大(懒得走直线),拖拽和点击都显得很“沉重”。
- "tired": MouseTimingConfig(
- fitts_a=0.120, fitts_b=0.200,
- curvature_min=0.20, curvature_max=0.35, # 懒散的大弧线
- tremor_amplitude=1.00,
- overshoot_probability=0.20,
- pre_click_pause_min=0.10, pre_click_pause_max=0.25,
- click_hold_min=0.10, click_hold_max=0.22, # 手指沉重,按压长
- micro_pause_probability=0.10,
- max_duration=3.0
- ),
- # 7. 【设计师 / 精准对齐用户】
- # 特征:移动平滑优美,手极其稳(无微颤),几乎不产生过冲,会花额外时间精准停留在目标正中心。
- "designer": MouseTimingConfig(
- fitts_a=0.100, fitts_b=0.170,
- curvature_min=0.15, curvature_max=0.25, # 优美圆滑的曲线
- tremor_amplitude=0.20, # 极低的像素级抖动
- overshoot_probability=0.02, # 绝对不过冲
- pre_click_pause_min=0.08, pre_click_pause_max=0.15, # 确认对准后再点
- micro_pause_probability=0.0,
- frame_interval=0.008, frame_interval_variance=0.002 # 高刷新率的高端鼠标
- ),
- # 8. 【笨拙重手 / “重装坦克”用户】
- # 特征:速度中等但每次动作幅度都偏大,容易大幅度偏离,点击鼠标时力气很大(长hold)。
- "clumsy": MouseTimingConfig(
- fitts_a=0.080, fitts_b=0.150,
- tremor_amplitude=1.20,
- overshoot_probability=0.35, overshoot_distance_min=0.08, overshoot_distance_max=0.20, # 容易大幅度滑偏
- pre_click_pause_min=0.06, pre_click_pause_max=0.15,
- click_hold_min=0.12, click_hold_max=0.25, # 重按
- drag_start_pause_max=0.25,
- min_duration=0.1
- ),
- # 9. 【旧电脑 / 卡顿网络用户】
- # 特征:轨迹不平滑,帧率低且波动极大(模拟系统卡顿导致的鼠标瞬间瞬移和丢帧)。
- "laggy_pc": MouseTimingConfig(
- fitts_a=0.070, fitts_b=0.150,
- frame_interval=0.035, frame_interval_variance=0.025, # 极度丢帧、跳跃
- tremor_amplitude=0.70,
- overshoot_probability=0.10,
- click_hold_min=0.05, click_hold_max=0.15,
- micro_pause_probability=0.20, micro_pause_min=0.02, micro_pause_max=0.05, # 卡顿造成的强制停顿
- ),
- # 10. 【中规中矩的普通用户】
- # 特征:最标准的参数,各项指标居中。
- "average_joe": MouseTimingConfig(
- fitts_a=0.070, fitts_b=0.150,
- frame_interval=0.012, frame_interval_variance=0.004,
- curvature_min=0.10, curvature_max=0.28,
- tremor_amplitude=0.85,
- overshoot_probability=0.22, overshoot_distance_min=0.03, overshoot_distance_max=0.10,
- pre_click_pause_min=0.04, pre_click_pause_max=0.16,
- click_hold_min=0.04, click_hold_max=0.12,
- micro_pause_probability=0.08
- )
- }
- class HumanMouse:
- """
- DrissionPage 的高仿真人类鼠标控制器。
- 提供移动、点击、双击和拖拽方法,采用贝塞尔曲线、Fitts 定律、生理性微颤和过冲校正。
- """
- _DEBUG_INIT_JS = """
- if (!document.getElementById('__dp_mouse_debug')) {
- const canvas = document.createElement('canvas');
- canvas.id = '__dp_mouse_debug';
- canvas.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;'
- + 'pointer-events:none;z-index:2147483647;';
- canvas.width = window.innerWidth;
- canvas.height = window.innerHeight;
- document.body.appendChild(canvas);
- window.__dp_debug_ctx = canvas.getContext('2d');
- }
- """
- _DEBUG_DOT_JS = """
- if (window.__dp_debug_ctx) {
- const ctx = window.__dp_debug_ctx;
- ctx.beginPath();
- ctx.arc(%s, %s, %s, 0, 2 * Math.PI);
- ctx.fillStyle = '%s';
- ctx.fill();
- }
- """
- def __init__(
- self,
- page: ChromiumPage,
- timing: Optional[MouseTimingConfig] = None,
- debug: bool = False,
- ):
- """
- 初始化鼠标控制器
- :param page: DrissionPage 的 ChromiumPage 或 ChromiumTab 实例
- :param timing: 轨迹与时间配置
- :param debug: 是否在页面上绘制调试红蓝点
- """
- self._page = page
- self._timing = timing or MouseTimingConfig()
- self._position: Tuple[float, float] = self._read_initial_position()
- self._debug = debug
- self._debug_initialized = False
- self._session_profile = self._build_session_profile()
- self._scroll = HumanScroll(self._page)
- @property
- def timing(self) -> MouseTimingConfig:
- return self._timing
- @timing.setter
- def timing(self, config: MouseTimingConfig) -> None:
- self._timing = config
- @property
- def debug(self) -> bool:
- return self._debug
- @debug.setter
- def debug(self, value: bool) -> None:
- self._debug = value
- self._debug_initialized = False
- def move(self, x: float, y: float, *, humanize: bool = False) -> None:
- """移动鼠标"""
- if humanize:
- self._move_humanized(x, y)
- return
- self._dispatch_move(x, y)
-
- def human_click_ele(self, element):
- """
- 拟人化点击元素 (修复版)
- :param element: DrissionPage 的元素对象
- """
- # 1. 强制滚动,把元素尽量移到屏幕正中间,防止被顶部导航栏或底部悬浮窗遮盖
- self._scroll.scroll_to_element(element)
- time.sleep(random.uniform(0.2, 0.6)) # 等待滚动动画完成,防止移动时坐标还在变化
-
- # 2. 【核心修复】获取相对于当前屏幕视口的坐标,而不是页面绝对坐标
- # 注意:这里使用的是 viewport_midpoint
- mid_x, mid_y = element.rect.viewport_midpoint
-
- # 3. 如果元素太靠近边缘,可能会获取失败,做一个基础保护
- if mid_x is None or mid_y is None:
- logger.warning("Failed to get viewport midpoint, falling back to absolute midpoint.")
- mid_x, mid_y = element.rect.midpoint
-
- self.click(mid_x, mid_y, humanize=True)
- def click(
- self,
- x: float,
- y: float,
- *,
- button: MouseButton = MouseButton.LEFT,
- click_count: int = 1,
- humanize: bool = False,
- ) -> None:
- """点击鼠标"""
- if humanize:
- self._click_humanized(x, y, button, click_count)
- return
- self._dispatch_move(x, y)
- self._dispatch_button(MouseEventType.MOUSE_PRESSED, button, click_count)
- self._dispatch_button(MouseEventType.MOUSE_RELEASED, button, click_count)
- def double_click(
- self,
- x: float,
- y: float,
- *,
- button: MouseButton = MouseButton.LEFT,
- humanize: bool = False,
- ) -> None:
- """双击鼠标"""
- self.click(x, y, button=button, click_count=2, humanize=humanize)
- def down(self, button: MouseButton = MouseButton.LEFT) -> None:
- """按下鼠标按键"""
- self._dispatch_button(MouseEventType.MOUSE_PRESSED, button)
- def up(self, button: MouseButton = MouseButton.LEFT) -> None:
- """释放鼠标按键"""
- self._dispatch_button(MouseEventType.MOUSE_RELEASED, button)
- def drag(
- self,
- start_x: float,
- start_y: float,
- end_x: float,
- end_y: float,
- *,
- humanize: bool = False,
- ) -> None:
- """拖拽鼠标"""
- if humanize:
- self._drag_humanized(start_x, start_y, end_x, end_y)
- return
- self._dispatch_move(start_x, start_y)
- self._dispatch_button(MouseEventType.MOUSE_PRESSED, MouseButton.LEFT)
- self._dispatch_move(end_x, end_y)
- self._dispatch_button(MouseEventType.MOUSE_RELEASED, MouseButton.LEFT)
- def _move_humanized(self, target_x: float, target_y: float, custom_duration: Optional[float] = None) -> None:
- """拟人化移动核心逻辑"""
- start = self._position
- target = (target_x, target_y)
- distance = math.hypot(target_x - start[0], target_y - start[1])
- if distance < 1.0:
- self._dispatch_move(target_x, target_y)
- return
- config = self._timing
- if custom_duration is not None:
- duration = custom_duration
- else:
- duration = fitts_duration(distance, 20.0, config.fitts_a, config.fitts_b)
- duration = max(config.min_duration, min(duration, config.max_duration))
- duration *= self._session_profile["speed_scale"]
- should_overshoot = (
- distance > config.overshoot_speed_threshold
- and random.random() < config.overshoot_probability * self._session_profile["overshoot_bias"]
- )
- if should_overshoot:
- self._move_with_overshoot(start, target, duration)
- else:
- cp1, cp2 = self._get_control_points(start, target)
- self._perform_movement_loop(start, target, duration, cp1, cp2)
- self._perform_final_correction(target_x, target_y)
- def _move_with_overshoot(
- self,
- start: Tuple[float, float],
- target: Tuple[float, float],
- duration: float,
- ) -> None:
- """带过冲现象的移动"""
- config = self._timing
- overshoot_fraction = random.uniform(
- config.overshoot_distance_min, config.overshoot_distance_max
- )
- dx = target[0] - start[0]
- dy = target[1] - start[1]
-
- overshoot = (target[0] + dx * overshoot_fraction, target[1] + dy * overshoot_fraction)
- cp1, cp2 = self._get_control_points(start, overshoot)
- self._perform_movement_loop(start, overshoot, duration * 0.85, cp1, cp2)
- cp1, cp2 = self._get_control_points(overshoot, target)
- self._perform_movement_loop(overshoot, target, duration * 0.15, cp1, cp2)
- def _perform_movement_loop(
- self,
- start: Tuple[float, float],
- end: Tuple[float, float],
- duration: float,
- cp1: Tuple[float, float],
- cp2: Tuple[float, float],
- ) -> None:
- """按帧渲染并执行移动循环"""
- config = self._timing
- start_time = time.perf_counter()
- prev = (start[0], start[1], start_time)
- segment_pause_used = False
- while True:
- now = time.perf_counter()
- elapsed = now - start_time
- if elapsed >= duration:
- break
- progress = elapsed / duration
- eased = minimum_jerk(progress)
- x, y = bezier_2d(eased, start, cp1, cp2, end)
- sigma = self._compute_tremor_sigma(x, y, now, prev, config)
- x += random.gauss(0, sigma)
- y += random.gauss(0, sigma)
- self._dispatch_move(x, y)
- prev = (x, y, now)
- frame_delay = self._sample_frame_delay()
- time.sleep(max(0.001, frame_delay))
- if (not segment_pause_used) and random.random() < config.micro_pause_probability:
- pause = self._sample_micro_pause()
- time.sleep(pause)
- start_time += pause
- segment_pause_used = True
- @staticmethod
- def _compute_tremor_sigma(
- x: float,
- y: float,
- now: float,
- prev: Tuple[float, float, float],
- config: MouseTimingConfig,
- ) -> float:
- """动态计算手抖幅度"""
- dt = now - prev[2]
- if dt > 0:
- velocity = math.hypot(x - prev[0], y - prev[1]) / dt
- speed_factor = max(0.2, 1.0 - velocity / 500.0)
- else:
- speed_factor = 1.0
- return config.tremor_amplitude * speed_factor
- def _click_humanized(
- self,
- x: float,
- y: float,
- button: MouseButton,
- click_count: int,
- ) -> None:
- """拟人化点击"""
- self._move_humanized(x, y)
- self._micro_adjust_towards(x, y)
- pre_pause = self._sample_pre_click_pause()
- time.sleep(pre_pause)
- for i in range(click_count):
- current_count = i + 1
- self._dispatch_button(MouseEventType.MOUSE_PRESSED, button, current_count)
- hold = self._sample_click_hold()
- time.sleep(hold)
- self._dispatch_button(MouseEventType.MOUSE_RELEASED, button, current_count)
- if current_count < click_count:
- interval = self._sample_double_click_interval()
- time.sleep(interval)
- def _drag_humanized(
- self,
- start_x: float,
- start_y: float,
- end_x: float,
- end_y: float,
- ) -> None:
- """拟人化拖拽"""
- config = self._timing
- self._move_humanized(start_x, start_y)
- self._dispatch_button(MouseEventType.MOUSE_PRESSED, MouseButton.LEFT)
- drag_start_pause = random.uniform(config.drag_start_pause_min, config.drag_start_pause_max)
- time.sleep(drag_start_pause)
- start = self._position
- distance = math.hypot(end_x - start[0], end_y - start[1])
- duration = fitts_duration(distance, 20.0, config.fitts_a, config.fitts_b)
- duration = max(config.min_duration, min(duration, config.max_duration))
- cp1, cp2 = self._get_control_points(start, (end_x, end_y))
- self._perform_movement_loop(start, (end_x, end_y), duration, cp1, cp2)
- self._dispatch_move(end_x, end_y)
- drag_end_pause = random.uniform(config.drag_end_pause_min, config.drag_end_pause_max)
- time.sleep(drag_end_pause)
- self._dispatch_button(MouseEventType.MOUSE_RELEASED, MouseButton.LEFT)
- def _get_control_points(
- self,
- start: Tuple[float, float],
- end: Tuple[float, float],
- ) -> Tuple[Tuple[float, float], Tuple[float, float]]:
- config = self._timing
- return random_control_points(
- start,
- end,
- config.curvature_min,
- config.curvature_max,
- config.curvature_asymmetry,
- config.short_distance_threshold,
- )
- def _dispatch_move(self, x: float, y: float) -> None:
- """发送 CDP 鼠标移动指令"""
- self._page.run_cdp(
- "Input.dispatchMouseEvent",
- type=MouseEventType.MOUSE_MOVED.value,
- x=int(round(x)),
- y=int(round(y))
- )
- self._position = (x, y)
- if self._debug:
- self._debug_draw_dot(x, y, radius=2, color='rgba(0,150,255,0.6)')
- def _sample_frame_delay(self) -> float:
- config = self._timing
- base = config.frame_interval * self._session_profile["tempo_scale"]
- spread = config.frame_interval_variance * self._session_profile["tempo_jitter"]
- return random.gauss(base, max(0.001, spread))
- def _sample_pre_click_pause(self) -> float:
- config = self._timing
- return max(
- 0.0,
- random.gauss(
- (config.pre_click_pause_min + config.pre_click_pause_max) / 2.0,
- (config.pre_click_pause_max - config.pre_click_pause_min) / 6.0,
- ),
- )
- def _sample_click_hold(self) -> float:
- config = self._timing
- return max(
- 0.0,
- random.gauss(
- (config.click_hold_min + config.click_hold_max) / 2.0,
- (config.click_hold_max - config.click_hold_min) / 6.0,
- ),
- )
- def _sample_double_click_interval(self) -> float:
- config = self._timing
- return max(
- 0.0,
- random.gauss(
- (config.double_click_interval_min + config.double_click_interval_max) / 2.0,
- (config.double_click_interval_max - config.double_click_interval_min) / 6.0,
- ),
- )
- def _sample_micro_pause(self) -> float:
- config = self._timing
- return max(
- 0.0,
- random.gauss(
- (config.micro_pause_min + config.micro_pause_max) / 2.0,
- (config.micro_pause_max - config.micro_pause_min) / 6.0,
- ),
- )
- def _dispatch_button(
- self,
- event_type: MouseEventType,
- button: MouseButton,
- click_count: int = 1,
- ) -> None:
- """发送 CDP 鼠标按键指令"""
- self._page.run_cdp(
- "Input.dispatchMouseEvent",
- type=event_type.value,
- button=button.value,
- x=int(round(self._position[0])),
- y=int(round(self._position[1])),
- clickCount=click_count
- )
- if self._debug and event_type == MouseEventType.MOUSE_PRESSED:
- self._debug_draw_dot(
- self._position[0], self._position[1], radius=6, color='rgba(255,50,50,0.9)'
- )
- def _debug_draw_dot(self, x: float, y: float, radius: int, color: str) -> None:
- """绘制轨迹调试点"""
- if not self._debug_initialized:
- self._page.run_js(self._DEBUG_INIT_JS)
- self._debug_initialized = True
- script = self._DEBUG_DOT_JS % (int(round(x)), int(round(y)), radius, color)
- self._page.run_js(script)
- def _read_initial_position(self) -> Tuple[float, float]:
- try:
- data = self._page.run_js("return { x: window.screenX || 0, y: window.screenY || 0 };")
- if isinstance(data, dict):
- return (float(data.get("x", 0.0)), float(data.get("y", 0.0)))
- except Exception:
- pass
- return (0.0, 0.0)
- def _build_session_profile(self) -> dict:
- return {
- "speed_scale": random.uniform(0.90, 1.15),
- "tempo_scale": random.uniform(0.92, 1.10),
- "tempo_jitter": random.uniform(0.85, 1.25),
- "overshoot_bias": random.uniform(0.65, 1.15),
- }
- def _perform_final_correction(self, target_x: float, target_y: float) -> None:
- current_x, current_y = self._position
- distance = math.hypot(target_x - current_x, target_y - current_y)
- if distance <= 1.5:
- self._dispatch_move(target_x, target_y)
- return
- correction_end = (target_x, target_y)
- cp1, cp2 = self._get_control_points((current_x, current_y), correction_end)
- duration = min(self._timing.max_duration, max(self._timing.min_duration, 0.12 + distance / 900.0))
- self._perform_movement_loop((current_x, current_y), correction_end, duration, cp1, cp2)
- self._dispatch_move(target_x, target_y)
- def _micro_adjust_towards(self, x: float, y: float) -> None:
- current_x, current_y = self._position
- distance = math.hypot(x - current_x, y - current_y)
- if distance < 5.0:
- return
- offset_x = random.uniform(-1.0, 1.0)
- offset_y = random.uniform(-1.0, 1.0)
- self._dispatch_move(x + offset_x, y + offset_y)
- time.sleep(random.uniform(0.006, 0.018))
- self._dispatch_move(x, y)
|