feat(任务): 录制手势改成**纯录制回放**(真手指轨迹)+ 手机端 getevent 录制
用户反馈:录制出来的滑动又被套上滑动那套逻辑(方向/幅度/拟人重新生成),
"导致滑动还是不顺畅"。录制就该是录制——按录下的路径与时间原样重放。
- 新步骤 `gesture`「录制手势」:存完整轨迹点列 `[[x,y,t_ms],…]`,
回放时原样交给设备(`d.swipe_points(points, duration)`),不做任何加工。
与 `swipe` 是两套东西(滑动是参数化的,录制是点列)。
- core/gesture.py(新):
· 手机端录制 `PhoneRecorder`:`getevent` 读**真触屏**设备(自动挑 fts_ts 这类、
排除 uinput/vitural-sar 等合成节点),解析两套协议(BTN_TOUCH / ABS_MT_TRACKING_ID);
**合成的注入事件不会出现在真触屏节点上**(实测),所以录到的只有人手的动作;
· 点列清洗:按 ≥16ms 抽稀但**末点必留**(快划时不能把收尾丢了);
· 回放**一次调用**而不是逐点注入:实测这台设备单次触摸 RPC ≈190ms,
逐点回放 20 点要 3.8 秒——只能让设备自己插值,"时间"由点密度还原。
- 接口:POST /api/gesture/record/{start,stop}(需设备权限;重复开始返回 409)。
- 「录制手势」步骤卡片:录到就回填(手机上录 / 网页上录),显示点数/时长;
**滑动步骤上的「录制手势」按钮已移除**(按用户要求,录制不再走滑动逻辑)。
- 录制前自动唤醒设备:**息屏时 u2 抓 UI 树会从 3 秒退化到 65 秒**(实测),
截图也是黑的——这是本次排查最耗时的坑,已写进文档。
自测:假设备单测(点列/时长/抽稀/失败退回直线,10 项);
无头浏览器 + CDP 真模拟拖动 → 录到 14 点/605ms 并按实际轨迹回填;
手机录制接口起停/重复拦截;
真机回放:用录下的轨迹跑任务 → 步骤 `gesture ok`。
⚠️ 手机端"真手指轨迹"这一段需要人真的去划才能端到端验证(我没有手指)。
文档:TASK_DEV §3.2(两套东西对比 + 三个坑)、步骤表 21 种、API、README。
This commit is contained in:
@@ -215,6 +215,87 @@ def api_groups_delete(name):
|
||||
return jsonify({"ok": False, "error": "分组不存在"}), 404
|
||||
|
||||
|
||||
# ================== API:录制手势(手机端真手指轨迹) ==================
|
||||
#
|
||||
# 用 getevent 读**真触屏**设备抓手指轨迹(见 core/gesture.py)。合成的注入事件不会
|
||||
# 出现在真触屏节点上,所以录到的只有人手的动作。
|
||||
# 录制器实例存在内存里(单进程;不落库——录完的点列由前端放进步骤/动作再保存)。
|
||||
|
||||
_gesture_recorders = {} # token -> PhoneRecorder
|
||||
_gesture_lock = threading.Lock()
|
||||
_GESTURE_TTL = 300 # 超过 5 分钟自动收摊,避免忘了点"停止"
|
||||
|
||||
|
||||
def _gesture_sweep():
|
||||
"""清掉超时的录制器(停止并释放 getevent 进程)。"""
|
||||
now = time.time()
|
||||
with _gesture_lock:
|
||||
stale = [t for t, r in _gesture_recorders.items()
|
||||
if now - r._started > _GESTURE_TTL]
|
||||
for t in stale:
|
||||
_gesture_recorders.pop(t, None)
|
||||
for t in stale:
|
||||
try:
|
||||
r.stop()
|
||||
except Exception:
|
||||
pass
|
||||
_log.info("录制手势超时自动停止: %s", t)
|
||||
|
||||
|
||||
@bp.route("/api/gesture/record/start", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_gesture_record_start():
|
||||
"""开始录:{serial} → {token}。之后在手机屏幕上用手指划,停止时取回轨迹。"""
|
||||
from core import gesture
|
||||
_gesture_sweep()
|
||||
serial = ((request.json or {}).get("serial") or "").strip()
|
||||
if not serial:
|
||||
return jsonify({"ok": False, "error": "缺少 serial"}), 400
|
||||
with _gesture_lock:
|
||||
if _gesture_recorders:
|
||||
return jsonify({"ok": False, "error": "已有录制在进行中,先停止"}), 409
|
||||
try:
|
||||
w, h = 0, 0
|
||||
try:
|
||||
import uiautomator2 as u2
|
||||
from core.u2_helper import screen_size
|
||||
d = u2.connect(serial)
|
||||
d.unlock() # 息屏时 u2 抓树会退化到几十秒,先唤醒
|
||||
w, h = screen_size(d, serial)
|
||||
except Exception as e:
|
||||
_log.warning("录制前取屏幕尺寸失败(按原始坐标范围换算): %s", e)
|
||||
rec = gesture.PhoneRecorder(serial, w, h)
|
||||
rec.start()
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": f"开始录制失败: {e}"}), 502
|
||||
token = uuid.uuid4().hex[:12]
|
||||
with _gesture_lock:
|
||||
_gesture_recorders[token] = rec
|
||||
_log.info("开始录制手势: %s @ %s(%s)by %s", token, serial, rec.device,
|
||||
getattr(current_user, "username", ""))
|
||||
return jsonify({"ok": True, "token": token, "device": rec.device,
|
||||
"screen": [w, h],
|
||||
"msg": "现在请用手指在手机上划,划完点「停止」"})
|
||||
|
||||
|
||||
@bp.route("/api/gesture/record/stop", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_gesture_record_stop():
|
||||
"""停止录制:{token} → {gestures: [[[x,y,t_ms],...], ...]}。"""
|
||||
token = ((request.json or {}).get("token") or "").strip()
|
||||
with _gesture_lock:
|
||||
rec = _gesture_recorders.pop(token, None)
|
||||
if rec is None:
|
||||
return jsonify({"ok": False, "error": "录制会话不存在或已超时"}), 404
|
||||
try:
|
||||
gestures = rec.stop()
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": f"停止录制失败: {e}"}), 500
|
||||
_log.info("停止录制手势: %s → %s 条", token, len(gestures))
|
||||
return jsonify({"ok": True, "gestures": gestures, "count": len(gestures),
|
||||
"device": rec.device})
|
||||
|
||||
|
||||
# ================== API:步骤默认值(「动作配置」页) ==================
|
||||
#
|
||||
# 存 app_meta.step_defaults 一个键(不建表),校验与合并都在 core/step_defaults.py。
|
||||
|
||||
Reference in New Issue
Block a user