diff --git a/doc/TASK_DEV.md b/doc/TASK_DEV.md
index eff395c..093f0e7 100644
--- a/doc/TASK_DEV.md
+++ b/doc/TASK_DEV.md
@@ -244,18 +244,26 @@ from .generic import task # 触发 @register_task(当前唯一任务类型)
`cmp_op` 取值:`等于` / `不等于` / `包含` / `不包含`(`==`/`!=`/`contains` 这类别名也认)。
不填 = 老行为(只看元素在不在)。
+**`cmp_value` 可以填多个值**(换行或 `|` 分隔,一行一个):多个之间是 **OR**——
+"这几个号都算我的",任一命中即命中;否定式(不等于/不包含)则是"**一个都不许出现**"。
+⚠ **逗号不是分隔符**("抖音号:123,456" 这种文本本身带逗号,拆了就永远匹配不上)。
+
例子(抓元素拿到的是 `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": "登录的不是目标抖音号"}}],
+ "cmp_op": "包含",
+ "cmp_value": "35377983067\n35377983068", // 多个号,任一命中即可
+ "timeout": 3,
+ "then": [{"type": "stop_self", "params": {"reason": "登录的不是我的号"}}],
"else": []}}
```
-要判断"**不是**这个号"就用 `不等于`(`cmp_op=不等于` 时"界面上是别的号"会命中,正好用来抓
-"号被换/掉登录")。
+要判断"**不是**我的号"就用 `不等于`("界面上是别的号"会命中,正好用来抓"号被换/掉登录")。
+
+日志会写清"采集到什么、几个候选、命中的是哪个值",例如:
+`采集到 '抖音号:35377983068' 包含 2 个候选值 → 符合,命中『35377983068』`。
**两条必须知道的规则**(都为了不误判):
diff --git a/static/admin/editor.js b/static/admin/editor.js
index 4557ce3..780bdc9 100644
--- a/static/admin/editor.js
+++ b/static/admin/editor.js
@@ -431,9 +431,11 @@ var _stepEditor={
[['','不比对(只看元素在不在)'],['等于','等于'],['不等于','不等于'],['包含','包含'],['不包含','不包含']]
.map(function(o){return '';}).join('')+
''+
- '
'+
- '
';
+ '';
h+=_selRowHtml(p,
(dyn?'':''+
''+
@@ -447,6 +449,7 @@ var _stepEditor={
+'
文本比对(可选):填了就把找到的那个元素的文本和"比对的值"比,'
+'比中了才算命中。例:元素文本是 抖音号:35377983067,'
+'想判断"这台登录的是不是这个号",选包含、值填 35377983067 即可(要整段一模一样才用"等于")。'
+ +'多个值一行一个,任一命中即可——"这几个号都算我的"。'
+'
⚠ 元素没找到时一律算未命中(走"未找到时"分支)——"没读到"不会被当成"和我设的不一样"。')+'
', true);
}
// 共享:触发概率(所有步骤通用,0~100)
diff --git a/tasks/generic/task.py b/tasks/generic/task.py
index 69bbe51..016ccb4 100644
--- a/tasks/generic/task.py
+++ b/tasks/generic/task.py
@@ -145,26 +145,62 @@ _CMP_ALIASES = {
CMP_OPS = tuple(_CMP_ALIASES)
-def _text_matches(got, op, want):
- """元素文本比对:got(采集到的)对 want(用户设的)做 op 判断。
-
- op 见 `_CMP_ALIASES`(等于/不等于/包含/不包含)。认不出的 op 一律返回 False
- ——条件判断"看不懂就算不命中",比猜一个更容易排查。
- """
- got = (got or "").strip()
- want = (want or "").strip()
+def _canon_op(op):
+ """运算符规范化成中文那个(认别名);认不出返回 None。"""
key = (op or "").strip().lower()
for canon, aliases in _CMP_ALIASES.items():
if key in [a.lower() for a in aliases]:
- if canon == "等于":
- return got == want
- if canon == "不等于":
- return got != want
- if canon == "包含":
- # 想匹配空串等于"永远命中",那是误配,当不命中处理
- return bool(want) and want in got
- return bool(want) and want not in got
- return False
+ return canon
+ return None
+
+
+def split_cmp_values(raw):
+ """把"比对的值"拆成多个候选(**任一命中即可**,即 OR 语义)。
+
+ 分隔符只有两个:**换行** 和 `|`。
+ **刻意不拆英文/中文逗号**——要比对的文本本身就常常带逗号
+ (如 `抖音号:123,456`),拆了就永远匹配不上。界面上是一行一个。
+ """
+ if raw is None:
+ return []
+ s = str(raw).replace("\r\n", "\n").replace("\r", "\n").replace("|", "\n")
+ return [v.strip() for v in s.split("\n") if v.strip()]
+
+
+def _cmp_hit(got, op, want):
+ """核心比对:返回 (是否命中, 命中的那个值/惹事的值)。
+
+ 多个候选值之间是 **OR**("这个号或那个号都算我"):
+ · 等于 → 文本等于**任一**候选即命中
+ · 包含 → 文本含**任一**候选即命中
+ · 不等于 → 文本**不在**任何候选里才命中
+ · 不包含 → **一个**候选都不出现才命中
+ 认不出的运算符、或候选全为空 → 一律不命中("看不懂就算不命中",比猜一个更好排查)。
+ """
+ got = (got or "").strip()
+ wants = split_cmp_values(want)
+ canon = _canon_op(op)
+ if canon is None or not wants:
+ return False, ""
+ if canon == "等于":
+ hit = got in wants
+ return hit, (got if hit else "")
+ if canon == "包含":
+ for v in wants:
+ if v in got:
+ return True, v
+ return False, ""
+ if canon == "不等于":
+ return (got not in wants), ""
+ for v in wants: # 不包含
+ if v in got:
+ return False, v # 命中了"不该出现"的那个 -> 条件不成立
+ return True, ""
+
+
+def _text_matches(got, op, want):
+ """元素文本比对(布尔版,多处调用只看结果)。"""
+ return _cmp_hit(got, op, want)[0]
class GenericStepsWorker(BaseWorker):
@@ -619,6 +655,9 @@ class GenericStepsWorker(BaseWorker):
先按选择器**取到元素的文本**,再拿它和 cmp_value 比(等于/不等于/包含/不包含),
比对结果才是命中与否。例:`resourceId=com.ss.android.ugc.aweme:id/506` 的文本是
`抖音号:35377983067`,cmp_op=包含、cmp_value=35377983067 就是"这台上登录的是它"。
+
+ `cmp_value` **可以填多个值**(换行或 `|` 分隔,见 `split_cmp_values`),
+ 多个之间是 **OR**——"这几个号都算我的"。要跑多个号的场景就靠它。
返回 True=命中 / False=未命中 / None=缺选择器或 OCR 不可用。
"""
sel_type = params.get("selector_type", "xpath")
@@ -698,8 +737,14 @@ class GenericStepsWorker(BaseWorker):
else:
if got is None:
got = self._read_element_text(d, sel_type, sel_val)
- hit = _text_matches(got, cmp_op, cmp_val)
- detail = f"采集到 '{got}' {cmp_op} '{cmp_val}' → {'符合' if hit else '不符合'}"
+ hit, hit_val = _cmp_hit(got, cmp_op, cmp_val)
+ n_val = len(split_cmp_values(cmp_val))
+ if hit:
+ extra = f",命中『{hit_val}』" if hit_val else ""
+ else:
+ extra = f",因为出现了『{hit_val}』" if hit_val else ""
+ detail = (f"采集到 '{got}' {cmp_op} {n_val} 个候选值 → "
+ f"{'符合' if hit else '不符合'}{extra}")
found = hit
elif cmp_op and sel_type == "screen":
_log.warning(f"[{self.serial}] 屏幕状态没有文本可比,已忽略文本比对设置")