|
|
@@ -64,35 +64,35 @@ class MouseTimingConfig:
|
|
|
frame_interval_variance: float = 0.004
|
|
|
|
|
|
curvature_min: float = 0.10
|
|
|
- curvature_max: float = 0.30
|
|
|
+ curvature_max: float = 0.28
|
|
|
curvature_asymmetry: float = 0.6
|
|
|
|
|
|
short_distance_threshold: float = 50.0
|
|
|
|
|
|
- tremor_amplitude: float = 1.0
|
|
|
+ tremor_amplitude: float = 0.85
|
|
|
|
|
|
- overshoot_probability: float = 0.70
|
|
|
+ overshoot_probability: float = 0.22
|
|
|
overshoot_distance_min: float = 0.03
|
|
|
- overshoot_distance_max: float = 0.12
|
|
|
- overshoot_speed_threshold: float = 200.0
|
|
|
+ overshoot_distance_max: float = 0.10
|
|
|
+ overshoot_speed_threshold: float = 260.0
|
|
|
|
|
|
- pre_click_pause_min: float = 0.05
|
|
|
- pre_click_pause_max: float = 0.20
|
|
|
- click_hold_min: float = 0.05
|
|
|
- click_hold_max: float = 0.15
|
|
|
+ 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.10
|
|
|
- drag_start_pause_min: float = 0.08
|
|
|
- drag_start_pause_max: float = 0.20
|
|
|
- drag_end_pause_min: float = 0.05
|
|
|
- drag_end_pause_max: float = 0.15
|
|
|
+ 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.03
|
|
|
- micro_pause_min: float = 0.015
|
|
|
- micro_pause_max: float = 0.04
|
|
|
+ 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.5
|
|
|
+ max_duration: float = 2.2
|
|
|
|
|
|
|
|
|
class HumanMouse:
|
|
|
@@ -138,9 +138,10 @@ class HumanMouse:
|
|
|
"""
|
|
|
self._page = page
|
|
|
self._timing = timing or MouseTimingConfig()
|
|
|
- self._position: Tuple[float, float] = (0.0, 0.0)
|
|
|
+ self._position: Tuple[float, float] = self._read_initial_position()
|
|
|
self._debug = debug
|
|
|
self._debug_initialized = False
|
|
|
+ self._session_profile = self._build_session_profile()
|
|
|
|
|
|
@property
|
|
|
def timing(self) -> MouseTimingConfig:
|
|
|
@@ -254,16 +255,17 @@ class HumanMouse:
|
|
|
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
|
|
|
+ and random.random() < config.overshoot_probability * self._session_profile["overshoot_bias"]
|
|
|
)
|
|
|
|
|
|
if should_overshoot:
|
|
|
@@ -272,7 +274,7 @@ class HumanMouse:
|
|
|
cp1, cp2 = self._get_control_points(start, target)
|
|
|
self._perform_movement_loop(start, target, duration, cp1, cp2)
|
|
|
|
|
|
- self._dispatch_move(target_x, target_y)
|
|
|
+ self._perform_final_correction(target_x, target_y)
|
|
|
|
|
|
def _move_with_overshoot(
|
|
|
self,
|
|
|
@@ -308,6 +310,7 @@ class HumanMouse:
|
|
|
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()
|
|
|
@@ -316,8 +319,9 @@ class HumanMouse:
|
|
|
if elapsed >= duration:
|
|
|
break
|
|
|
|
|
|
- t = minimum_jerk(elapsed / duration)
|
|
|
- x, y = bezier_2d(t, start, cp1, cp2, end)
|
|
|
+ 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)
|
|
|
@@ -326,15 +330,14 @@ class HumanMouse:
|
|
|
self._dispatch_move(x, y)
|
|
|
prev = (x, y, now)
|
|
|
|
|
|
- frame_delay = config.frame_interval + random.uniform(
|
|
|
- -config.frame_interval_variance, config.frame_interval_variance
|
|
|
- )
|
|
|
+ frame_delay = self._sample_frame_delay()
|
|
|
time.sleep(max(0.001, frame_delay))
|
|
|
|
|
|
- if random.random() < config.micro_pause_probability:
|
|
|
- pause = random.uniform(config.micro_pause_min, config.micro_pause_max)
|
|
|
+ if (not segment_pause_used) and random.random() < config.micro_pause_probability:
|
|
|
+ pause = self._sample_micro_pause()
|
|
|
time.sleep(pause)
|
|
|
- start_time += pause # 补偿停顿时间
|
|
|
+ start_time += pause
|
|
|
+ segment_pause_used = True
|
|
|
|
|
|
@staticmethod
|
|
|
def _compute_tremor_sigma(
|
|
|
@@ -361,27 +364,23 @@ class HumanMouse:
|
|
|
click_count: int,
|
|
|
) -> None:
|
|
|
"""拟人化点击"""
|
|
|
- config = self._timing
|
|
|
-
|
|
|
self._move_humanized(x, y)
|
|
|
+ self._micro_adjust_towards(x, y)
|
|
|
|
|
|
- pre_pause = random.uniform(config.pre_click_pause_min, config.pre_click_pause_max)
|
|
|
+ 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 = random.uniform(config.click_hold_min, config.click_hold_max)
|
|
|
+ hold = self._sample_click_hold()
|
|
|
time.sleep(hold)
|
|
|
|
|
|
self._dispatch_button(MouseEventType.MOUSE_RELEASED, button, current_count)
|
|
|
|
|
|
if current_count < click_count:
|
|
|
- interval = random.uniform(
|
|
|
- config.double_click_interval_min,
|
|
|
- config.double_click_interval_max,
|
|
|
- )
|
|
|
+ interval = self._sample_double_click_interval()
|
|
|
time.sleep(interval)
|
|
|
|
|
|
def _drag_humanized(
|
|
|
@@ -442,6 +441,52 @@ class HumanMouse:
|
|
|
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,
|
|
|
@@ -470,4 +515,45 @@ class HumanMouse:
|
|
|
self._debug_initialized = True
|
|
|
|
|
|
script = self._DEBUG_DOT_JS % (int(round(x)), int(round(y)), radius, color)
|
|
|
- self._page.run_js(script)
|
|
|
+ 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)
|