Design for long_press/double_tap atomic gestures and a centralized humanize layer (coordinate jitter, curved W3C-Actions swipe, timing jitter) gated by APEX_HUMANIZE_ENABLED. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
11 KiB
原子手势扩展 + 拟人化抖动 设计
- 日期:2026-07-15
- 状态:Draft(待用户 review)
- 作者:Jerry Yan + Claude
- 关联代码区:
runtime/tool_specs.py、runtime/executor.py、runtime/planner_prompts.py、driver/base.py、driver/wda_driver.py、driver/android_driver.py、tools/
1. 背景与动机
当前 Runtime 暴露给 AI Planner 的设备动作只有 5 个:tap / swipe / input_text / launch_app / terminate_app(见 runtime/tool_specs.py:42-145)。Driver 抽象(driver/base.py)与两个实现(WDA / Android UiAutomator2)同样没有长按、双击。
两个缺口:
- 原子手势不全:长按(long press)、双击(double tap)无法表达。AI Planner 无法完成依赖这两类手势的任务。
- 动作过于"机器化":
swipe在 driver 层走 2 点命令(WDAmobile: dragFromToForDuration、Androidmobile: dragGesture),轨迹是完美直线;tap每次落在精确坐标。这种确定性容易被目标 app 的反自动化检测识别。
本设计同时解决两者:新增两个原子手势,并引入一个集中的拟人化(humanize)层,对所有指针类动作注入坐标偏移、路径曲线、时序抖动。
2. 目标 / 非目标
目标
- 新增
long_press、double_tap两个工具,端到端贯通(driver → tool → tool_spec → executor registry → planner prompt)。 - 新增
tools/humanize.py,提供坐标抖动、滑动 waypoint 生成、时长抖动三类纯函数。 swipe在 humanize 开启时改走 W3C Actions 多点曲线;double_tap始终走 W3C Actions(两 tap + 可调间隔)。- 通过
APEX_HUMANIZE_ENABLEDenv 开关(默认开)控制;关闭时现有tap/swipe行为零回归、现有精确坐标断言零改动。
非目标
- 不做捏合、多指、3D Touch 等更复杂的手势(YAGNI,等真有需求)。
- 不重构现有
swipe关闭路径(保留mobile: dragGesture/dragFromToForDuration)。 - 不给
tap/input_text等加新的可调参数(只加抖动,不改语义)。 - 不引入时序相关的全局 sleep/节流策略。
- 不做反检测的"高级"维度(设备指纹、传感器模拟等),只做指针轨迹层面的拟人。
3. 平台命令矩阵
实现期必须按 android-driver 变更 task 1.1 的先例,对照已安装的 appium-python-client 源码/官方文档核实命令名,不得凭记忆猜测。
| 手势 | iOS (WDA / XCUITest) | Android (UiAutomator2) | 备注 |
|---|---|---|---|
long_press |
mobile: touchAndHold {x, y, duration} |
mobile: longClickGesture {x, y} |
Android 时长系统固定、不可控;duration_ms 参数在 Android 实现里被忽略并记入 Risks |
double_tap |
W3C Actions(pointerDown→up→pause→down→up) |
W3C Actions(同左) | 不用 mobile: doubleTap/doubleClickGesture,理由见 §4.3 |
swipe(humanize 开) |
W3C Actions 多点曲线 | W3C Actions 多点曲线 | 新增 Driver.swipe_path |
swipe(humanize 关) |
mobile: dragFromToForDuration(不变) |
mobile: dragGesture(不变) |
保留原 Driver.swipe |
4. 设计
4.1 新原子手势
long_press
- 参数:
x、y、duration_ms?=1200(加purpose/expected_outcome元数据,见tool_specs.py:13-26的ACTION_METADATA_PROPERTIES) - 实现:driver 新增
Driver.long_press(x, y, duration_ms)抽象方法;WDA 透传duration,Android 忽略duration_ms - 工具:
tools/long_press.py,薄封装,humanize 抖动坐标(+ WDA 时长)
double_tap
- 参数:
x、y、interval_ms?=80 - 实现:driver 新增
Driver.double_tap(x, y, interval_ms)抽象方法,两个驱动统一用 W3C Actions 实现(§4.3) - 工具:
tools/double_tap.py,薄封装,humanize 抖动坐标 + 间隔
4.2 tools/humanize.py(新)
集中式拟人模块,纯函数 + 一个 dataclass config。无 driver 依赖。
@dataclass(frozen=True)
class HumanizeConfig:
enabled: bool = True # 读 APEX_HUMANIZE_ENABLED,默认 true
tap_radius_px: float = 5.0 # APEX_HUMANIZE_TAP_RADIUS_PX
swipe_curvature: float = 0.15 # 垂直噪声幅度占路径长度的比例
swipe_waypoints: int = 8 # 中间点数
duration_spread: float = 0.15 # 时长 ±15%
def jitter_point(x, y, *, radius, rng) -> tuple[float, float]: ...
def swipe_waypoints(start, end, *, curvature, n, rng) -> list[tuple[float,float]]: ...
def jitter_duration(value_ms, *, spread, rng) -> int: ...
def jitter_interval(value_ms, *, spread, rng) -> int: ...
- 随机源:函数接受
rng: random.Random。模块级单例_rng默认random.Random();测试可注入固定种子构造的random.Random(seed)以可复现。 swipe_waypoints:在起止两点间用贝塞尔/线性插值生成n个点,每个点沿路径法线方向加高斯噪声(幅度 = 路径长度 ×curvature)。保证首末点等于(抖动后的)起止点。- config 加载沿用仓库既有
load_config模式(参考runtime/planner_config.py),env 名统一前缀APEX_HUMANIZE_*。
4.3 W3C Actions:Driver.swipe_path 与 double_tap 的共享实现
Appium Python Client 的 ActionBuilder / W3C pointer action 在 iOS 与 Android 两侧 API 一致,逐点 pointer_move 即可。为避免两个驱动各写一遍,在 driver 层新增一个共享辅助:
driver/base.py新增抽象方法:swipe_path(self, waypoints: list[tuple[float, float]], duration_ms: int) -> Nonedouble_tap(self, x: float, y: float, interval_ms: int) -> Nonelong_press(self, x: float, y: float, duration_ms: int) -> None
- 共享的 W3C Actions 构造逻辑放
driver/_w3c_actions.py(模块级函数,接收 appiumclient+ 参数),两个驱动的swipe_path/double_tap实现各自调它。这样:- driver 仍是薄适配器(只持 client、调辅助、包
DriverError) - W3C 序列化逻辑不重复
- driver 仍是薄适配器(只持 client、调辅助、包
swipe_path 序列:pointerDown(start) → move(p1) → move(p2) → … → move(end) → pointerUp,总时长按 waypoint 数均分(或按段长加权)。
double_tap 序列:pointerDown(x,y) → pointerUp → pause(interval_ms) → pointerDown(x,y) → pointerUp,单次 HTTP 往返,interval_ms 完全可控。
为什么 double_tap 不用原生 mobile: doubleTap/doubleClickGesture:它们是固定间隔、一次性机器手势,最易被反检测识别;W3C 两 tap + 高斯抖动间隔反而更像人,且和 swipe_path 共用同一套基础设施。
4.4 应用点(humanize 开/关矩阵)
| 动作 | humanize 开 | humanize 关 |
|---|---|---|
tap |
jitter_point 后调 driver.tap |
原样(零变化) |
swipe |
swipe_waypoints 后调 driver.swipe_path |
原样调 driver.swipe |
long_press |
jitter_point(+WDA jitter_duration) 后调 driver.long_press |
直接调 driver.long_press |
double_tap |
jitter_point + jitter_interval 后调 driver.double_tap |
直接调 driver.double_tap(仍走 W3C,但不抖动) |
input_text/launch_app/terminate_app |
不动 | 不动 |
关键保证:humanize 关闭时,现有 tap/swipe 走与今天完全相同的 driver 方法与命令,现有精确坐标断言零改动。long_press/double_tap 是新工具,无"原行为"可回归。
4.5 tool_specs 与 executor
runtime/tool_specs.py新增LONG_PRESS_SPEC、DOUBLE_TAP_SPEC,加入ACTION_TOOL_SPECS(ALL_TOOL_SPECS自动带上,prompt 的 tool 列表自动生效)。runtime/executor.py::default_tool_registry注册"long_press"、"double_tap"。
4.6 planner prompt
runtime/planner_prompts.py:18-22 那段工具枚举("You must then call exactly one tool: - One of tap, swipe, …")补 long_press、double_tap,并各加一句使用场景提示(长按用于长按菜单/拖拽预备;双击用于缩放/选中)。仍保持"每回合恰好一个工具"的单步约束不变(多步规划是另一条独立议题,本次不动)。
5. 配置(env)
| 变量 | 默认 | 说明 |
|---|---|---|
APEX_HUMANIZE_ENABLED |
true |
总开关;false 时所有抖动关闭,tap/swipe 走原路径 |
APEX_HUMANIZE_TAP_RADIUS_PX |
5.0 |
tap/long_press/double_tap 坐标高斯偏移半径(px) |
APEX_HUMANIZE_SWIPE_CURVATURE |
0.15 |
滑动垂直噪声占路径长度比例 |
APEX_HUMANIZE_SWIPE_WAYPOINTS |
8 |
滑动中间点数 |
APEX_HUMANIZE_DURATION_SPREAD |
0.15 |
时长/间隔 ±比例 |
未设置时全部走默认;任一缺失不影响其它。测试通过在 fixture 里设 APEX_HUMANIZE_ENABLED=false(或注入固定种子 rng)获得确定性。
6. 默认值
long_press.duration_ms = 1200double_tap.interval_ms = 80swipe.duration_ms = 500(不变)
7. 测试策略
- humanize 单测(
tests/test_humanize.py):注入random.Random(42),断言jitter_point偏移在半径内、swipe_waypoints首末点等于端点且点数正确、中间点存在垂直偏移、jitter_duration在 ±spread 内。 - driver 新方法单测(humanize 关闭):mock appium client,断言
swipe_path发出的 W3C action 序列含 N 个 pointer_move、double_tap序列含两段 pointerDown/Up + 一个 pause、long_press调对正确 mobile 命令(WDAtouchAndHold带 duration、AndroidlongClickGesture)。 - tool 集成:
tools/long_press.py/tools/double_tap.py在 fake driver 上跑通;tools/swipe.py在 humanize 开启时断言调用的是swipe_path且 waypoints > 2,关闭时调用的是swipe。 - 回归:现有
tap/swipe相关测试在 humanize 关闭下全部不变;在tests/test_executor.py补两个新动作的 dispatch。 - spec 校验:
runtime/tool_specs.py的新 spec 满足_action_parameters要求(含purpose/expected_outcome)。
8. 风险
- R1(中):W3C Actions 在某些 Appium/驱动版本上对
pause的时长支持不一致。缓解:double_tap的 interval 先用pause(duration_ms),实现期在真实 Appium 上验证;若不可靠,退化为两次mobile: tap+time.sleep(记入 Open Questions)。 - R2(低):Android
longClickGesture时长不可控,与 WDA 行为不对称。接受:参数保留,Android 忽略并文档化。 - R3(低):W3C Actions 曲线滑动比原生 2 点命令慢(多点序列化 + 执行)。接受:反检测优先于延迟,且单次滑动仍是一次 HTTP 往返。
- R4(低):humanize 默认开可能让未设 env 的既有测试出现非确定坐标。缓解:现有
tap/swipe测试若断言精确坐标,需确认它们跑在APEX_HUMANIZE_ENABLED=false下;实现期 grep 评估影响面,必要时在测试 conftest 默认关 humanize。
9. 开放问题(实现期核实)
- OQ1:
mobile: touchAndHold(WDA)与mobile: longClickGesture(Android)的精确参数名/duration 单位——对照已安装 appium-python-client 核实。 - OQ2:W3C
pauseaction 在目标 Appium 版本是否可靠传递 interval——真机验证,必要时按 R1 回退。 - OQ3:是否需要在
Driver抽象为swipe_path/double_tap/long_press提供 fake/mock 默认实现(现有driver/registry.py不注册 mock driver,保持一致——只做抽象方法 + 两个真实实现 + 单测里的 mock)。