feat(条件判断): 比对的值支持多个(任一命中即可)
需求:`35377983067` 或 `35377983068` 都算我的号,要能一次列好几个。 - `cmp_value` 现在是**多值**:换行或 `|` 分隔,一行一个。 多个之间是 **OR**(任一命中即命中);否定式(不等于/不包含)语义是 "一个都不许出现"。**逗号刻意不当分隔符**——要比对的文本本身就常带逗号 (`抖音号:123,456`),拆了就永远匹配不上。 - 比对核心抽成 `_cmp_hit(got, op, want) -> (是否命中, 命中的那个值)`, `_text_matches` 保留布尔版(多处调用只看结果,不改签名)。 日志因此能写清人话: `采集到 '抖音号:35377983068' 包含 2 个候选值 → 符合,命中『35377983068』` 否定式不成立时则指出"因为出现了『xxx』"——排查时一眼看出是哪个值惹的。 - 行为统一(**刻意的变更**):候选值为空/全空白 = 误配 → 一律不命中。 以前"等于 + 两边都空"会返回 True(`"" == ""` 的巧合),等于选了"等于" 却没填值就永远命中、条件判断形同虚设;现在与编辑器告警、AI 草稿校验 的口径一致了("选了运算符没填值 → 永远不命中")。 - 前端:`cmp_value` 由单行输入换成**两行文本域**,标签与提示写明"可多行、 任一命中即可、逗号不是分隔符"。 - 文档:TASK_DEV.md §4.2 例子改成多号,并写明分隔符与 OR/否定语义。 自测:多值语义 15 项(换行/`|` 拆分、逗号不拆、空白清理、四种运算符的多值 OR 与否定语义、命中的是哪个值、候选全空不命中)+ 原有 38 项回归全过; `_exec_if_el` 走多值路径实测命中 then 分支、日志含候选数与命中值。
This commit is contained in:
+12
-4
@@ -244,18 +244,26 @@ from .generic import task # 触发 @register_task(当前唯一任务类型)
|
|||||||
`cmp_op` 取值:`等于` / `不等于` / `包含` / `不包含`(`==`/`!=`/`contains` 这类别名也认)。
|
`cmp_op` 取值:`等于` / `不等于` / `包含` / `不包含`(`==`/`!=`/`contains` 这类别名也认)。
|
||||||
不填 = 老行为(只看元素在不在)。
|
不填 = 老行为(只看元素在不在)。
|
||||||
|
|
||||||
|
**`cmp_value` 可以填多个值**(换行或 `|` 分隔,一行一个):多个之间是 **OR**——
|
||||||
|
"这几个号都算我的",任一命中即命中;否定式(不等于/不包含)则是"**一个都不许出现**"。
|
||||||
|
⚠ **逗号不是分隔符**("抖音号:123,456" 这种文本本身带逗号,拆了就永远匹配不上)。
|
||||||
|
|
||||||
例子(抓元素拿到的是 `android.widget.TextView text:抖音号:35377983067 id:com.ss.android.ugc.aweme:id/506`):
|
例子(抓元素拿到的是 `android.widget.TextView text:抖音号:35377983067 id:com.ss.android.ugc.aweme:id/506`):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"type": "if_el", "params": {
|
{"type": "if_el", "params": {
|
||||||
"selector_type": "resourceId", "selector_value": "com.ss.android.ugc.aweme:id/506",
|
"selector_type": "resourceId", "selector_value": "com.ss.android.ugc.aweme:id/506",
|
||||||
"cmp_op": "包含", "cmp_value": "35377983067", "timeout": 3,
|
"cmp_op": "包含",
|
||||||
"then": [{"type": "stop_self", "params": {"reason": "登录的不是目标抖音号"}}],
|
"cmp_value": "35377983067\n35377983068", // 多个号,任一命中即可
|
||||||
|
"timeout": 3,
|
||||||
|
"then": [{"type": "stop_self", "params": {"reason": "登录的不是我的号"}}],
|
||||||
"else": []}}
|
"else": []}}
|
||||||
```
|
```
|
||||||
|
|
||||||
要判断"**不是**这个号"就用 `不等于`(`cmp_op=不等于` 时"界面上是别的号"会命中,正好用来抓
|
要判断"**不是**我的号"就用 `不等于`("界面上是别的号"会命中,正好用来抓"号被换/掉登录")。
|
||||||
"号被换/掉登录")。
|
|
||||||
|
日志会写清"采集到什么、几个候选、命中的是哪个值",例如:
|
||||||
|
`采集到 '抖音号:35377983068' 包含 2 个候选值 → 符合,命中『35377983068』`。
|
||||||
|
|
||||||
**两条必须知道的规则**(都为了不误判):
|
**两条必须知道的规则**(都为了不误判):
|
||||||
|
|
||||||
|
|||||||
@@ -431,9 +431,11 @@ var _stepEditor={
|
|||||||
[['','不比对(只看元素在不在)'],['等于','等于'],['不等于','不等于'],['包含','包含'],['不包含','不包含']]
|
[['','不比对(只看元素在不在)'],['等于','等于'],['不等于','不等于'],['包含','包含'],['不包含','不包含']]
|
||||||
.map(function(o){return '<option value="'+o[0]+'"'+((p.cmp_op||'')===o[0]?' selected':'')+'>'+o[1]+'</option>';}).join('')+
|
.map(function(o){return '<option value="'+o[0]+'"'+((p.cmp_op||'')===o[0]?' selected':'')+'>'+o[1]+'</option>';}).join('')+
|
||||||
'</select></div>'+
|
'</select></div>'+
|
||||||
'<div class="form-group" style="grid-column:span 2"><label>比对的值</label>'+
|
'<div class="form-group" style="grid-column:span 2"><label>比对的值(可多行,任一命中即可)</label>'+
|
||||||
'<input type="text" class="form-control" data-key="cmp_value" value="'+esc(p.cmp_value||'')+
|
'<textarea class="form-control" data-key="cmp_value" rows="2" style="font-family:var(--mono)" '+
|
||||||
'" placeholder="如 35377983067"></div></div>';
|
'placeholder="如 35377983067 35377983068">'+esc(p.cmp_value||'')+'</textarea>'+
|
||||||
|
'<div class="help">一行一个(也认 <code>|</code> 分隔):<b>任意一个命中就算命中</b>——多个号的场景把号都列上。'+
|
||||||
|
'<b>逗号不当分隔符</b>,因为要比对的文本本身常带逗号</div></div></div>';
|
||||||
h+=_selRowHtml(p,
|
h+=_selRowHtml(p,
|
||||||
(dyn?'':'<div class="form-group"><label>查找超时(秒)</label><input type="number" class="form-control" data-key="timeout" value="'+(p.timeout||3)+'" min="0.5" max="30" step="0.5"></div>'+
|
(dyn?'':'<div class="form-group"><label>查找超时(秒)</label><input type="number" class="form-control" data-key="timeout" value="'+(p.timeout||3)+'" min="0.5" max="30" step="0.5"></div>'+
|
||||||
'<div class="form-group form-check" data-ocr-field style="display:'+(p.selector_type==='ocr'?'block':'none')+'">'+
|
'<div class="form-group form-check" data-ocr-field style="display:'+(p.selector_type==='ocr'?'block':'none')+'">'+
|
||||||
@@ -447,6 +449,7 @@ var _stepEditor={
|
|||||||
+'<br><b>文本比对</b>(可选):填了就把找到的那个元素的<b>文本</b>和"比对的值"比,'
|
+'<br><b>文本比对</b>(可选):填了就把找到的那个元素的<b>文本</b>和"比对的值"比,'
|
||||||
+'比中了才算命中。例:元素文本是 <code>抖音号:35377983067</code>,'
|
+'比中了才算命中。例:元素文本是 <code>抖音号:35377983067</code>,'
|
||||||
+'想判断"这台登录的是不是这个号",选<b>包含</b>、值填 <code>35377983067</code> 即可(要整段一模一样才用"等于")。'
|
+'想判断"这台登录的是不是这个号",选<b>包含</b>、值填 <code>35377983067</code> 即可(要整段一模一样才用"等于")。'
|
||||||
|
+'多个值一行一个,<b>任一命中即可</b>——"这几个号都算我的"。'
|
||||||
+'<br>⚠ 元素<b>没找到</b>时一律算未命中(走"未找到时"分支)——"没读到"不会被当成"和我设的不一样"。')+'</div>', true);
|
+'<br>⚠ 元素<b>没找到</b>时一律算未命中(走"未找到时"分支)——"没读到"不会被当成"和我设的不一样"。')+'</div>', true);
|
||||||
}
|
}
|
||||||
// 共享:触发概率(所有步骤通用,0~100)
|
// 共享:触发概率(所有步骤通用,0~100)
|
||||||
|
|||||||
+64
-19
@@ -145,26 +145,62 @@ _CMP_ALIASES = {
|
|||||||
CMP_OPS = tuple(_CMP_ALIASES)
|
CMP_OPS = tuple(_CMP_ALIASES)
|
||||||
|
|
||||||
|
|
||||||
def _text_matches(got, op, want):
|
def _canon_op(op):
|
||||||
"""元素文本比对:got(采集到的)对 want(用户设的)做 op 判断。
|
"""运算符规范化成中文那个(认别名);认不出返回 None。"""
|
||||||
|
|
||||||
op 见 `_CMP_ALIASES`(等于/不等于/包含/不包含)。认不出的 op 一律返回 False
|
|
||||||
——条件判断"看不懂就算不命中",比猜一个更容易排查。
|
|
||||||
"""
|
|
||||||
got = (got or "").strip()
|
|
||||||
want = (want or "").strip()
|
|
||||||
key = (op or "").strip().lower()
|
key = (op or "").strip().lower()
|
||||||
for canon, aliases in _CMP_ALIASES.items():
|
for canon, aliases in _CMP_ALIASES.items():
|
||||||
if key in [a.lower() for a in aliases]:
|
if key in [a.lower() for a in aliases]:
|
||||||
if canon == "等于":
|
return canon
|
||||||
return got == want
|
return None
|
||||||
if canon == "不等于":
|
|
||||||
return got != want
|
|
||||||
if canon == "包含":
|
def split_cmp_values(raw):
|
||||||
# 想匹配空串等于"永远命中",那是误配,当不命中处理
|
"""把"比对的值"拆成多个候选(**任一命中即可**,即 OR 语义)。
|
||||||
return bool(want) and want in got
|
|
||||||
return bool(want) and want not in got
|
分隔符只有两个:**换行** 和 `|`。
|
||||||
return False
|
**刻意不拆英文/中文逗号**——要比对的文本本身就常常带逗号
|
||||||
|
(如 `抖音号: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):
|
class GenericStepsWorker(BaseWorker):
|
||||||
@@ -619,6 +655,9 @@ class GenericStepsWorker(BaseWorker):
|
|||||||
先按选择器**取到元素的文本**,再拿它和 cmp_value 比(等于/不等于/包含/不包含),
|
先按选择器**取到元素的文本**,再拿它和 cmp_value 比(等于/不等于/包含/不包含),
|
||||||
比对结果才是命中与否。例:`resourceId=com.ss.android.ugc.aweme:id/506` 的文本是
|
比对结果才是命中与否。例:`resourceId=com.ss.android.ugc.aweme:id/506` 的文本是
|
||||||
`抖音号:35377983067`,cmp_op=包含、cmp_value=35377983067 就是"这台上登录的是它"。
|
`抖音号:35377983067`,cmp_op=包含、cmp_value=35377983067 就是"这台上登录的是它"。
|
||||||
|
|
||||||
|
`cmp_value` **可以填多个值**(换行或 `|` 分隔,见 `split_cmp_values`),
|
||||||
|
多个之间是 **OR**——"这几个号都算我的"。要跑多个号的场景就靠它。
|
||||||
返回 True=命中 / False=未命中 / None=缺选择器或 OCR 不可用。
|
返回 True=命中 / False=未命中 / None=缺选择器或 OCR 不可用。
|
||||||
"""
|
"""
|
||||||
sel_type = params.get("selector_type", "xpath")
|
sel_type = params.get("selector_type", "xpath")
|
||||||
@@ -698,8 +737,14 @@ class GenericStepsWorker(BaseWorker):
|
|||||||
else:
|
else:
|
||||||
if got is None:
|
if got is None:
|
||||||
got = self._read_element_text(d, sel_type, sel_val)
|
got = self._read_element_text(d, sel_type, sel_val)
|
||||||
hit = _text_matches(got, cmp_op, cmp_val)
|
hit, hit_val = _cmp_hit(got, cmp_op, cmp_val)
|
||||||
detail = f"采集到 '{got}' {cmp_op} '{cmp_val}' → {'符合' if hit else '不符合'}"
|
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
|
found = hit
|
||||||
elif cmp_op and sel_type == "screen":
|
elif cmp_op and sel_type == "screen":
|
||||||
_log.warning(f"[{self.serial}] 屏幕状态没有文本可比,已忽略文本比对设置")
|
_log.warning(f"[{self.serial}] 屏幕状态没有文本可比,已忽略文本比对设置")
|
||||||
|
|||||||
Reference in New Issue
Block a user