feat(任务): 滑动拟人化 + 每台设备有自己的手感(core/humanize.py)
用户反馈"滑动太像机器人""批量执行时每台设备都一样"。 core/humanize.py(新)分两个层次: - **每次不同**:起止点/幅度/时长抖动、弧线方向随机——最容易被识别的不是"慢", 是"每次都一模一样"; - **每台设备不同**:由 crc32(serial) 派生稳定的"性格"(手速 0.82~1.32、 幅度 0.86~1.16、弧度、常用横坐标 ±9% 屏宽、停顿 0.80~1.35)。同设备多次运行 风格一致,不同设备明显不同——13 台批量跑看着像 13 个人各刷各的。 用独立 Random 实例播种,不碰全局 random(多线程 worker 会打乱取值顺序)。 轨迹用二次贝塞尔走 d.swipe_points(曲线),异常时自动退回直线 d.swipe。 **点数固定 4 个**:swipe_points 在慢设备上每多一个点约多 1 秒(实测 2 点 1.4s / 6 点 6.0s / 10 点 10.6s),设备自己会插值几十步,4 点已足够弯。 顺带修一个真机上的老毛病:滑动原本每次都要读 d.info,而它在部分设备上要 **14 秒**。改用 humanize.screen_size()(走 window_size,同设备 0.6s,缓存 120s)——所以哪怕多了弧线,真机单次滑动反而从 ~15s 降到 ~5s。 - tasks/generic/task.py:swipe / swipe_until 走拟人(新增 distance_ratio、 jitter、humanize 参数,默认开);swipe_until 每轮停顿也抖动;wait 步骤新增 可选 vary_pace(默认关,按设备节奏缩放 0.8~1.35 倍); - static/admin/editor.js:滑动类步骤参数面板加"幅度/抖动/拟人轨迹"+ 说明; - 文档:TASK_DEV.md §8.4(含"点数别调大""别用 d.info"两个坑)、步骤表、 README 能力表与结构。 自测:假设备单测(曲线/抖动/设备间差异/退化路径/不越界/尺寸缓存)+ 真机联调 (4 次滑动全部 ok,标注"弧线"、坐标每次不同、对照的 humanize=false 仍是直线)。
This commit is contained in:
+34
-26
@@ -37,7 +37,7 @@ from tasks.base import BaseTask, register_task
|
||||
from core.device_worker import BaseWorker, _update_status
|
||||
from core.u2_helper import ensure_app_running, wait_for_app_home, random_sleep
|
||||
from core.logger import get_logger
|
||||
from core import notifier, step_log
|
||||
from core import notifier, step_log, humanize
|
||||
|
||||
_log = get_logger("task.generic")
|
||||
|
||||
@@ -82,10 +82,13 @@ STEP_TYPES = [
|
||||
{"type": "key_event", "label": "按键", "icon": "⌨️",
|
||||
"params": {"key": "back"}},
|
||||
{"type": "swipe", "label": "滑动", "icon": "↕",
|
||||
"params": {"direction": "up", "duration_min": 0.25, "duration_max": 0.50}},
|
||||
"params": {"direction": "up", "duration_min": 0.25, "duration_max": 0.50,
|
||||
"distance_ratio": 0.6, "jitter": 0.15, "humanize": True}},
|
||||
{"type": "swipe_until", "label": "滑动直到元素", "icon": "🔍",
|
||||
"params": {"selector_type": "xpath", "selector_value": "", "direction": "up",
|
||||
"max_swipes": 8, "click_when_found": True}},
|
||||
"max_swipes": 8, "click_when_found": True,
|
||||
"duration_min": 0.25, "duration_max": 0.50,
|
||||
"distance_ratio": 0.6, "jitter": 0.15, "humanize": True}},
|
||||
{"type": "click", "label": "点击元素", "icon": "✦",
|
||||
"params": {"selector_type": "description", "selector_value": "", "wait_timeout": 2}},
|
||||
{"type": "click_xy", "label": "点击坐标", "icon": "🎯",
|
||||
@@ -379,13 +382,14 @@ class GenericStepsWorker(BaseWorker):
|
||||
if not sel_val:
|
||||
_log.warning("swipe_until 缺少选择器")
|
||||
return None
|
||||
info = d.info
|
||||
w, h = info["displayWidth"], info["displayHeight"]
|
||||
cx = int(w * 0.5)
|
||||
if direction == "down":
|
||||
sy, ey = int(h * 0.2), int(h * 0.8)
|
||||
else:
|
||||
sy, ey = int(h * 0.8), int(h * 0.2)
|
||||
# 每次滑动都重新生成(弧线方向/幅度/时长都不一样),不用预算好的固定坐标
|
||||
swipe_kw = dict(
|
||||
serial=self.serial,
|
||||
duration_min=float(params.get("duration_min", 0.25)),
|
||||
duration_max=float(params.get("duration_max", 0.50)),
|
||||
distance_ratio=float(params.get("distance_ratio", 0.6) or 0.6),
|
||||
jitter=params.get("jitter", 0.15),
|
||||
humanize=params.get("humanize", True) is not False)
|
||||
for i in range(1, max_swipes + 1):
|
||||
if self.stopped() or self.is_time_up():
|
||||
return None
|
||||
@@ -411,8 +415,8 @@ class GenericStepsWorker(BaseWorker):
|
||||
_log.info(f"[{self.serial}] 滑动{i}次后找到: {sel_val}")
|
||||
self._track_selector_health(sel_val, True)
|
||||
return True
|
||||
d.swipe(cx, sy, cx, ey, 0.3)
|
||||
time.sleep(0.5)
|
||||
humanize.swipe(d, direction, **swipe_kw)
|
||||
time.sleep(random.uniform(0.35, 0.75)) # 每次滑动后停顿也抖动一下
|
||||
self._track_selector_health(sel_val, False)
|
||||
_log.warning(f"[{self.serial}] 滑动{max_swipes}次未找到元素: {sel_val}")
|
||||
return False
|
||||
@@ -531,21 +535,22 @@ class GenericStepsWorker(BaseWorker):
|
||||
return found
|
||||
|
||||
def _exec_swipe(self, d, params, depth=0):
|
||||
"""滑动一次(默认拟人:弧线 + 抖动 + 本设备手速,见 core/humanize.py)。
|
||||
|
||||
老行为(正中直线、每次都一样)可以用 humanize=false 还原;
|
||||
jitter=0 则保留轨迹但去掉随机抖动(对照排障用)。
|
||||
"""
|
||||
direction = params.get("direction", "up")
|
||||
dur_min = float(params.get("duration_min", 0.25))
|
||||
dur_max = float(params.get("duration_max", 0.50))
|
||||
dur = random.uniform(dur_min, dur_max)
|
||||
info = d.info
|
||||
w, h = info["displayWidth"], info["displayHeight"]
|
||||
cx = int(w * 0.5)
|
||||
if direction == "up":
|
||||
d.swipe(cx, int(h * 0.8), cx, int(h * 0.2), dur)
|
||||
elif direction == "down":
|
||||
d.swipe(cx, int(h * 0.2), cx, int(h * 0.8), dur)
|
||||
elif direction == "left":
|
||||
d.swipe(int(w * 0.8), int(h * 0.5), int(w * 0.2), int(h * 0.5), dur)
|
||||
elif direction == "right":
|
||||
d.swipe(int(w * 0.2), int(h * 0.5), int(w * 0.8), int(h * 0.5), dur)
|
||||
info = humanize.swipe(
|
||||
d, direction, serial=self.serial,
|
||||
duration_min=float(params.get("duration_min", 0.25)),
|
||||
duration_max=float(params.get("duration_max", 0.50)),
|
||||
distance_ratio=float(params.get("distance_ratio", 0.6) or 0.6),
|
||||
jitter=params.get("jitter", 0.15),
|
||||
humanize=params.get("humanize", True) is not False)
|
||||
_log.info(f"[{self.serial}] 滑动 {direction} 时长{info['duration']}s "
|
||||
f"({info['from']}→{info['to']}"
|
||||
f"{',弧线' if info.get('human') else ',直线'})")
|
||||
|
||||
def _exec_click(self, d, params, depth=0):
|
||||
sel_type = params.get("selector_type", "xpath")
|
||||
@@ -620,6 +625,9 @@ class GenericStepsWorker(BaseWorker):
|
||||
max_s = float(params.get("max", 3.0))
|
||||
# 分片 sleep:每 0.5s 检查停止/超时信号,抢占任务能及时接管(纯 sleep 无法中断)
|
||||
total = random.uniform(min_s, max_s)
|
||||
# 可选:按本设备节奏缩放(勾了"设备节奏差异"才生效,默认关——
|
||||
# 时长被放大/缩小是业务语义,不能悄悄改)
|
||||
total = humanize.pace(self.serial, total, enabled=params.get("vary_pace", False))
|
||||
end = time.time() + total
|
||||
while time.time() < end:
|
||||
if self.stopped() or self.is_time_up():
|
||||
|
||||
Reference in New Issue
Block a user