feat(条件判断): 支持「选中元素 → 拿它的文本和设定的值比」(等于/不等于/包含/不包含)

需求原话:条件判断需要能选择元素,比如我设置了一个抖音号,让它采集这个元素的
文本和我设的值做条件判断。

原来的条件判断只会判「元素在不在」(找到走 then、没找到走 else),没法判"内容对不对"。
现在加两个可选参数:

- `cmp_op`:等于 / 不等于 / 包含 / 不包含(`==`/`!=`/`contains` 这类别名也认);
  留空 = 老行为,**完全向后兼容**(现有任务不用改)。
- `cmp_value`:要比对的值。

填了就变成:先按选择器取到元素文本 → 和 cmp_value 比 → 比对结果才是命中与否。
用户那个例子就是:元素 `com.ss.android.ugc.aweme:id/506` 的文本是
`抖音号:35377983067`,选「包含」、值填 `35377983067` 即可;
要抓"登录的不是这个号"就用「不等于」。

- 读取只走 `get_text()`(只读,2 秒超时),不点击、不改状态。
- **两条防误判规则**(都写进文档了):
  · 元素**没找到**时一律算未命中(走 else)——"没读到"绝不能被当成"和我设的不一样",
    否则界面还没加载出来就会误判成"账号被换了";
  · 元素在但文本为空时按空文本参与比对(用于抓"栏位是空的"),日志会打出实际读到的值。
- 比对**不污染「选择器健康」统计**:统计用的仍是"元素在不在"——"找到了但值不对"
  是条件按预期走了 else,不是选择器失效,不该攒出「连续未命中」告警。
- 支持比对的类型:所有元素类选择器 + `foreground`(比前台包名)+ `ocr`(比 OCR 命中的那段字)。
  `screen`(亮/熄)没有文本可比,设了会忽略并告警。
- 前端:条件判断面板多一块「文本比对」(运算符下拉 + 比对的值),
  只在元素/OCR/前台App 条件下出现;保存前校验"选了运算符却没填值"和"screen 上设了比对"。
- AI 建任务草稿校验(`core/task_draft.py`)同步拦"运算符拼错""有运算符没值"——
  执行器对认不出的运算符是"一律不命中",不拦就会变成悄悄走 else。

自测:比对函数 16 项(含别名、空值、None、拼错运算符)、`_exec_if_el` 全分支 20 项
(含"没找到+不等于"这个关键边界、健康统计用存在性、screen 忽略比对、u2 抛异常不炸)、
**真机验证**(cs1 上真读一个 TextView 的文本再比对:包含/等于/不等于 全对,
元素不存在时不误判)、草稿校验 5 项、GET 路由冒烟 55 个 0 个 500。

(测试里踩到一个坑记一笔:u2 的 `XPathSelector.exists` 是纯 bool 属性,而
`UiObject.exists` 是"可调用的 Exists 对象"(`bool()` 即时判断、`exists(timeout=)` 带等待),
两套语义不能想当然统一——单测的假设备一开始没模拟对,是测试写错了不是代码写错了。)
This commit is contained in:
2026-09-24 09:22:32 +08:00
parent bf432b4f56
commit 34a03db1b9
4 changed files with 181 additions and 8 deletions
+39 -2
View File
@@ -131,7 +131,7 @@ from .generic import task # 触发 @register_task(当前唯一任务类型)
| 15 | `wait` | 等待 | — | `min`(1.0)、`max`(3.0)、`vary_pace`(False) | — | 随机时长;**分片 sleep**(每 ≤0.5s 检查停止/超时),可被抢占打断;勾了 `vary_pace` 再按**本设备节奏**缩放 0.8~1.35 倍(批量跑时设备之间会逐渐错开) |
| 16 | `loop` | 循环块 | — | `loop_mode`("rounds")、`max_iterations`(10)、`loop_duration`(600) | `children` | 见 §4.1 |
| 17 | `group` | 动作组 | — | — | `children` | 子步骤**按序执行一次**(不循环);自定义动作拖入画布就是展开成 group |
| 18 | `if_el` | 条件判断 | `selector_type`、`selector_value`、`timeout`(3) | `ocr_click`(False) | `then` / `else` | 见 §4.2;条件类型除元素/OCR 外还支持 **屏幕状态**、**前台App** |
| 18 | `if_el` | 条件判断 | `selector_type`、`selector_value`、`timeout`(3) | `ocr_click`(False)、`cmp_op`("")、`cmp_value`("") | `then` / `else` | 见 §4.2;条件类型除元素/OCR 外还支持 **屏幕状态**、**前台App**;填 `cmp_op` 则改成**比元素的文本**(等于/不等于/包含/不包含) |
| 19 | `notify` | 发通知 | — | `title`("")、`message`("")、`level`("info") | — | 推一条**自定义**通知(事件 `task.notify.custom`):标题正文自己写,支持 `{device} {serial} {job} {time} {app} {screen}`;谁收到取决于 webhook 的事件订阅。两者都空则跳过 |
| 20 | `stop_self` | 停止本设备 | — | `reason`("") | — | 只停**本设备**的任务(其它设备照跑):置 worker 停止位,后续步骤不再执行,任务记成**被停止而不是失败**(不触发重试) |
| 21 | `gesture` | 录制手势 | `points`(录出来的) | `speed`(1.0) | — | **纯录制回放**:把录下的轨迹点列 `[[x,y,t_ms],…]` 按原路径与时长交给设备插值,不做弧线/抖动/手速加工。与「滑动」是两套东西,见 §3.2 |
@@ -236,6 +236,43 @@ from .generic import task # 触发 @register_task(当前唯一任务类型)
- OCR 不可用(未装 `rapidocr_onnxruntime`)时该步返回 `None`(不判定)
- 命中长文本时点击位置按关键词在文本中的比例估算(避免点到整块中心偏离)
#### 文本比对(`cmp_op` / `cmp_value`,可选)——"这台登录的是不是那个号"
光判断"元素在不在"不够用。填了 `cmp_op` 就变成:**先按选择器取到元素的文本,再拿它和
`cmp_value` 比,比对结果才是命中与否**。
`cmp_op` 取值:`等于` / `不等于` / `包含` / `不包含`(`==`/`!=`/`contains` 这类别名也认)。
不填 = 老行为(只看元素在不在)。
例子(抓元素拿到的是 `android.widget.TextView text:抖音号:35377983067 id:com.ss.android.ugc.aweme:id/506`):
```json
{"type": "if_el", "params": {
"selector_type": "resourceId", "selector_value": "com.ss.android.ugc.aweme:id/506",
"cmp_op": "包含", "cmp_value": "35377983067", "timeout": 3,
"then": [{"type": "stop_self", "params": {"reason": "登录的不是目标抖音号"}}],
"else": []}}
```
要判断"**不是**这个号"就用 `不等于`(`cmp_op=不等于` 时"界面上是别的号"会命中,正好用来抓
"号被换/掉登录")。
**两条必须知道的规则**(都为了不误判):
| 情况 | 结果 | 为什么 |
|---|---|---|
| 元素**没找到** | 一律**未命中**(走 `else`),日志写"未找到目标,无法比对" | "没读到"绝不能被当成"和我设的不一样"——否则界面还没加载出来就误判成"账号被换了" |
| 元素在、但文本为空 | 按**空文本**参与比对(空 `不等于` 任何非空值 → 命中) | 用于抓"栏位是空的"(如未登录)。日志会打出实际读到的值,好排查 |
其它细节:
- 支持比对的类型:所有**元素类**选择器(xpath / text / resourceId / description / className…)、
**`foreground`**(比当前前台包名)、**`ocr`**(比 OCR 命中的那段文字,如 `抖音号:35377983067`)。
**`screen`(亮/熄)没有文本可比**,设了比对会被忽略并告警。
- 比对**不影响「选择器健康」统计**:统计用的仍是"元素在不在"——"找到了但值不对"是条件按预期
走了 `else`,不是选择器失效,不该攒出「连续未命中」告警。
- 读元素文本是**只读**操作(`get_text`,2 秒超时),不会点击或改动任何状态。
### 4.3 公共参数
| 参数 | 说明 |
@@ -412,7 +449,7 @@ from .generic import task # 触发 @register_task(当前唯一任务类型)
|------|------|
| 操作库拖拽 | 左侧分类库(interact / screen / flow),拖到画布;支持跨层级嵌套(循环套循环)、拖拽排序 |
| 参数表单 | 按步骤类型渲染对应字段(选择器行、时长、概率、循环模式等) |
| 条件分支 | `if_el` 的 then/else 两个分支区;OCR 模式下多出"命中后点击"复选框 |
| 条件分支 | `if_el` 的 then/else 两个分支区;OCR 模式下多出"命中后点击"复选框;元素/OCR/前台App 条件还多一块**文本比对**(运算符下拉 + 比对的值,见 §4.2) |
| 多选与打包 | ☑ 多选 → 打包成自定义动作 |
| 元素抓取 | 「抓取元素」打开抓取弹窗(选设备 → 截图 + 元素树 → 点选回填),见 §5.5 |
| 测试 | 「测试此步骤」真机试执行 |