fix: 元素选择器序号语义 + 抓取弹窗直接点击测试

- uiauto_helper: 同属性多实例的选择器由 `//*[@id="x"][k]` 改为 `(//*[@id="x"])[k]`——
  前者在 XPath 里是"父节点内排第 k",多实例时 [2..n] 全部失配(实测抖音底部 4 个同 id tab:
  仅 [1] 可用),任务里表现为"未找到元素"但界面上元素明明存在。
- tasks/generic/task.py: 新增 _norm_legacy_xpath,执行前把**历史遗留**的
  `//*[@attr=…][k]` 窄范围纠正为带括号形式(只改前缀,结构路径 …/FrameLayout[2] 的
  兄弟序号保持不动)——已存任务无需重抓即可恢复。
- editor.js: 抓取弹窗每条元素新增「▶ 点一下」(按 bounds 中心真点一次,/api/screen/tap
  snap=1 吸附)与「✓ 测选择器」(用将填入的选择器跑 /api/steps/test 验证命中),
  点击后自动刷新截图;底部加用法提示。
- doc/TASK_DEV.md:写明 xpath 序号必须整体加括号 + 旧形态自动纠正 + 优先文字/唯一 id。
This commit is contained in:
2026-09-10 15:31:33 +08:00
parent 8072f4a380
commit 3f44cd1491
4 changed files with 89 additions and 8 deletions
+25
View File
@@ -30,6 +30,7 @@ worker 按 steps 顺序执行,支持 loop 步骤循环、停止信号、进度
group - 动作组(含 children 步骤列表,按序执行一次,可折叠复用)
"""
import random
import re
import time
from tasks.base import BaseTask, register_task
@@ -39,6 +40,24 @@ from core.logger import get_logger
_log = get_logger("task.generic")
# 历史抓取器生成的"伪序号"选择器://*[@resource-id="x"][4]
# XPath 里这是"父节点内排第 4",不是"第 4 个匹配"——同属性多实例时 [2..n] 全部失配。
_LEGACY_IDX_XPATH = re.compile(r'^(//\*\[@[^\]]+\])\[(\d+)\](.*)$')
def _norm_legacy_xpath(sel_val):
"""把历史 `//*[@attr=…][k]` 纠正为 `(//*[@attr=…])[k]`(只改整体前缀,保留后续子路径)。
窄范围:只匹配"属性XPath + 数字谓词"开头的形态;结构路径里
`.../FrameLayout[2]` 的兄弟序号是有意为之,不受影响。
"""
if not isinstance(sel_val, str):
return sel_val
m = _LEGACY_IDX_XPATH.match(sel_val.strip())
if not m:
return sel_val
return f"({m.group(1)})[{m.group(2)}]{m.group(3)}"
# ================== 步骤类型定义(前端操作库 + 后端执行共用)==================
STEP_TYPES = [
@@ -165,6 +184,12 @@ class GenericStepsWorker(BaseWorker):
stype = step.get("type", "")
label = step.get("label", stype)
params = step.get("params", {})
# 兼容历史选择器://*[@attr=…][k] → (//*[@attr=…])[k](XPath 位置谓词语义)
if params.get("selector_type") == "xpath" and params.get("selector_value"):
fixed = _norm_legacy_xpath(params["selector_value"])
if fixed != params["selector_value"]:
params = dict(params, selector_value=fixed)
_log.info(f"[{self.serial}] 选择器已纠正为: {fixed}")
# 概率触发:probability=100 必执行,<100 按百分比概率决定本次是否执行
prob = float(params.get("probability", 100))
if prob < 100 and random.random() * 100 > prob: