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:
@@ -219,6 +219,21 @@ def validate_steps(steps, depth=1, path="steps", errors=None, warnings=None,
|
||||
if isinstance(else_, list) and else_:
|
||||
validate_steps(else_, depth + 1, f"{here}.params.else",
|
||||
errors, warnings, types, counter)
|
||||
# 文本比对(可选):运算符必须认得,且配了运算符就得有值——
|
||||
# 执行器对认不出的运算符是"一律不命中",写错了会静默走 else,
|
||||
# 所以在这里拦住,别让 AI 草稿悄悄变成"永远不命中"
|
||||
cmp_op = (params.get("cmp_op") or "").strip()
|
||||
if cmp_op:
|
||||
from tasks.generic.task import CMP_OPS
|
||||
if cmp_op not in CMP_OPS:
|
||||
errors.append(f"{here}.params.cmp_op: 不支持的比对运算符 {cmp_op!r}"
|
||||
f"(可用:{'、'.join(CMP_OPS)})")
|
||||
elif not str(params.get("cmp_value") or "").strip():
|
||||
errors.append(f"{here}.params.cmp_value: 选了文本比对"
|
||||
f"({cmp_op})就必须填比对的值,否则永远不命中")
|
||||
elif (params.get("selector_type") or "xpath") == "screen":
|
||||
warnings.append(f"{here}: 屏幕状态没有文本可比,"
|
||||
"cmp_op/cmp_value 会被忽略")
|
||||
|
||||
if stype == "swipe":
|
||||
_check_direction(params, here, errors)
|
||||
|
||||
+39
-2
@@ -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 |
|
||||
| 测试 | 「测试此步骤」真机试执行 |
|
||||
|
||||
+24
-3
@@ -19,7 +19,7 @@ var STEP_LIB=[
|
||||
{type:'wait_el',label:'等待元素',icon:'⏳',cat:'flow',params:{selector_type:'xpath',selector_value:'',timeout:10}},
|
||||
{type:'loop',label:'循环块',icon:'↻',cat:'flow',params:{loop_mode:'rounds',max_iterations:10,loop_duration:600,children:[]}},
|
||||
{type:'group',label:'动作组',icon:'📦',cat:'flow',params:{children:[]}},
|
||||
{type:'if_el',label:'条件判断',icon:'❓',cat:'flow',params:{selector_type:'xpath',selector_value:'',timeout:3,ocr_click:false,then:[],else:[]}},
|
||||
{type:'if_el',label:'条件判断',icon:'❓',cat:'flow',params:{selector_type:'xpath',selector_value:'',timeout:3,ocr_click:false,cmp_op:'',cmp_value:'',then:[],else:[]}},
|
||||
{type:'notify',label:'发通知',icon:'🔔',cat:'flow',params:{title:'',message:'',level:'info'}},
|
||||
{type:'stop_self',label:'停止本设备',icon:'⛔',cat:'flow',params:{reason:''}},
|
||||
{type:'gesture',label:'录制手势',icon:'⏺',cat:'interact',params:{points:[],speed:1.0}}
|
||||
@@ -425,15 +425,29 @@ var _stepEditor={
|
||||
}else if(step.type==='if_el'){
|
||||
// 屏幕状态 / 前台App 是"直接读设备当前状态",没有等待超时、也没有选择器可抓
|
||||
var dyn=(p.selector_type==='screen'||p.selector_type==='foreground');
|
||||
// 文本比对:屏幕状态没有文本可比(亮/熄),只有元素/OCR/前台 App 才给这块
|
||||
var cmpRow=dyn?'':'<div class="form-row"><div class="form-group"><label>文本比对</label>'+
|
||||
'<select class="form-control" data-key="cmp_op">'+
|
||||
[['','不比对(只看元素在不在)'],['等于','等于'],['不等于','不等于'],['包含','包含'],['不包含','不包含']]
|
||||
.map(function(o){return '<option value="'+o[0]+'"'+((p.cmp_op||'')===o[0]?' selected':'')+'>'+o[1]+'</option>';}).join('')+
|
||||
'</select></div>'+
|
||||
'<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||'')+
|
||||
'" placeholder="如 35377983067"></div></div>';
|
||||
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>'+
|
||||
'<div class="form-group form-check" data-ocr-field style="display:'+(p.selector_type==='ocr'?'block':'none')+'">'+
|
||||
'<input type="checkbox" data-key="ocr_click" '+(p.ocr_click?'checked':'')+'><label>OCR 命中后自动点击该文字</label></div>')+
|
||||
'<input type="checkbox" data-key="ocr_click" '+(p.ocr_click?'checked':'')+'><label>OCR 命中后自动点击该文字</label></div>'+
|
||||
cmpRow)+
|
||||
'<div class="help">'+(dyn
|
||||
? '命中 → 执行下方"✅ 找到时"分支;未命中 → 执行"❌ 未找到时"分支。<b>屏幕状态</b>读系统 mWakefulness(亮/熄),'
|
||||
+'<b>前台App是</b>判断当前前台是不是某个包名——都只看当前状态,不等待。'
|
||||
: '超时内找到元素 → 执行下方"✅ 找到时"分支;未找到 → 执行"❌ 未找到时"分支。两分支可放任意步骤,也可再嵌套条件判断/循环;'
|
||||
+'<b>OCR识别</b>可匹配图片/画布里的文字(截屏识别,跨平台)')+'</div>', true);
|
||||
+'<b>OCR识别</b>可匹配图片/画布里的文字(截屏识别,跨平台)'
|
||||
+'<br><b>文本比对</b>(可选):填了就把找到的那个元素的<b>文本</b>和"比对的值"比,'
|
||||
+'比中了才算命中。例:元素文本是 <code>抖音号:35377983067</code>,'
|
||||
+'想判断"这台登录的是不是这个号",选<b>包含</b>、值填 <code>35377983067</code> 即可(要整段一模一样才用"等于")。'
|
||||
+'<br>⚠ 元素<b>没找到</b>时一律算未命中(走"未找到时"分支)——"没读到"不会被当成"和我设的不一样"。')+'</div>', true);
|
||||
}
|
||||
// 共享:触发概率(所有步骤通用,0~100)
|
||||
h+='<div class="form-row" style="margin-top:8px"><div class="form-group"><label>触发概率(%)</label><input type="number" class="form-control" data-key="probability" value="'+(p.probability!=null?p.probability:100)+'" min="0" max="100"><div class="help">0~100,<100 表示按概率执行(如 30=隔几次才触发一次,适合"偶尔点赞")</div></div></div>';
|
||||
@@ -844,6 +858,13 @@ var _stepEditor={
|
||||
if(!s.params.selector_value){
|
||||
warnings.push(name+' "'+label+'": 条件判断的选择器为空,会直接走"未找到"分支。');
|
||||
}
|
||||
if(s.params.cmp_op&&!String(s.params.cmp_value||'').trim()){
|
||||
warnings.push(name+' "'+label+'": 选了"文本比对('+s.params.cmp_op+')"但没填比对的值,'
|
||||
+'执行时永远不命中(相当于每次都走"未找到时"分支)。');
|
||||
}
|
||||
if(s.params.cmp_op&&s.params.selector_type==='screen'){
|
||||
warnings.push(name+' "'+label+'": 屏幕状态没有文本可比,文本比对的设置会被忽略。');
|
||||
}
|
||||
}
|
||||
_childArrs(s).forEach(function(a){
|
||||
if(a.length)check(a,name+' 子步骤');
|
||||
|
||||
+103
-3
@@ -113,7 +113,7 @@ STEP_TYPES = [
|
||||
"params": {"children": []}},
|
||||
{"type": "if_el", "label": "条件判断", "icon": "❓",
|
||||
"params": {"selector_type": "xpath", "selector_value": "", "timeout": 3,
|
||||
"then": [], "else": []}},
|
||||
"cmp_op": "", "cmp_value": "", "then": [], "else": []}},
|
||||
{"type": "notify", "label": "发通知", "icon": "🔔",
|
||||
"params": {"title": "", "message": "", "level": "info"}},
|
||||
{"type": "stop_self", "label": "停止本设备", "icon": "⛔",
|
||||
@@ -129,6 +129,43 @@ DEFAULT_PARAMS = {
|
||||
# 缺 steps 的任务执行时会明确报错(见 run_task),不会静默空跑。
|
||||
}
|
||||
|
||||
# 「条件判断」文本比对时读元素文本的超时(秒):只读一次属性,正常几十毫秒
|
||||
_TEXT_READ_TIMEOUT = 2
|
||||
|
||||
# 「条件判断」的文本比对运算符:中文为主(界面上就是这个),英文/符号也认,
|
||||
# 方便 AI 建任务或手写 JSON 时不用猜用哪个词。
|
||||
_CMP_ALIASES = {
|
||||
"等于": ("等于", "==", "=", "eq", "equals", "equal"),
|
||||
"不等于": ("不等于", "!=", "<>", "ne", "not_equal", "notequal"),
|
||||
"包含": ("包含", "含", "contains", "in", "has"),
|
||||
"不包含": ("不包含", "不含", "not_contains", "notcontains", "excludes"),
|
||||
}
|
||||
# 界面上给的运算符(顺序即下拉框顺序);上面那张表另收了英文/符号别名。
|
||||
# AI 草稿校验(core/task_draft.py)用它拦"运算符拼错 -> 永远不命中"。
|
||||
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()
|
||||
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
|
||||
|
||||
|
||||
class GenericStepsWorker(BaseWorker):
|
||||
"""通用步骤 worker:按 params.steps 顺序执行步骤链。
|
||||
@@ -576,15 +613,24 @@ class GenericStepsWorker(BaseWorker):
|
||||
selector_type=ocr 时用屏幕 OCR 匹配文字(图片/画布/WebView 里的文字也能找到),
|
||||
命中且 ocr_click=True 时自动点击该文字中心。
|
||||
分支里可放任意步骤(含循环/嵌套条件判断),实现"有弹窗就关掉、没弹窗就继续"这类逻辑。
|
||||
|
||||
**文本比对(cmp_op / cmp_value,可选)**:光"元素在不在"不够用——常见需求是
|
||||
"这台机器上登录的抖音号是不是我配的那个"。填了 cmp_op 就变成:
|
||||
先按选择器**取到元素的文本**,再拿它和 cmp_value 比(等于/不等于/包含/不包含),
|
||||
比对结果才是命中与否。例:`resourceId=com.ss.android.ugc.aweme:id/506` 的文本是
|
||||
`抖音号:35377983067`,cmp_op=包含、cmp_value=35377983067 就是"这台上登录的是它"。
|
||||
返回 True=命中 / False=未命中 / None=缺选择器或 OCR 不可用。
|
||||
"""
|
||||
sel_type = params.get("selector_type", "xpath")
|
||||
sel_val = params.get("selector_value", "")
|
||||
timeout = float(params.get("timeout", 3))
|
||||
cmp_op = str(params.get("cmp_op") or "").strip()
|
||||
cmp_val = str(params.get("cmp_value") or "")
|
||||
if not sel_val:
|
||||
_log.warning(f"[{self.serial}] if_el 缺少选择器,跳过")
|
||||
return None
|
||||
found = False
|
||||
got = None # 比对用的"采集到的值":元素文本 / OCR 命中文本 / 前台包名
|
||||
# 非元素类条件(屏幕状态/前台 App):不参与"选择器健康"统计——它们不是选择器,
|
||||
# 连续未命中不该被记成"选择器失效"
|
||||
dynamic = sel_type in ("screen", "foreground")
|
||||
@@ -599,6 +645,7 @@ class GenericStepsWorker(BaseWorker):
|
||||
f"(判断{'熄屏' if want_off else '亮屏'})")
|
||||
elif sel_type == "foreground":
|
||||
cur = current_package(d)
|
||||
got = cur or ""
|
||||
found = cur == str(sel_val).strip()
|
||||
detail = f"前台='{cur or '未知'}'(期望 {sel_val})"
|
||||
elif sel_type == "ocr":
|
||||
@@ -614,6 +661,7 @@ class GenericStepsWorker(BaseWorker):
|
||||
_log.warning(f"[{self.serial}] OCR 截图失败")
|
||||
else:
|
||||
found, center, matched = find_on_screen(img, sel_val)
|
||||
got = matched or "" if found else ""
|
||||
if found:
|
||||
if params.get("ocr_click") and center:
|
||||
d.click(*center)
|
||||
@@ -634,16 +682,68 @@ class GenericStepsWorker(BaseWorker):
|
||||
except Exception as e:
|
||||
_log.warning(f"[{self.serial}] 条件判断异常: {e}")
|
||||
found = False
|
||||
|
||||
# 元素"在不在"的原始结果:选择器健康统计要用它、**不能**用比对结果——
|
||||
# "找到了但文本不对"是条件按预期走了 else 分支,不是选择器失效。
|
||||
existed = found
|
||||
|
||||
# ---- 文本比对(可选)----
|
||||
if cmp_op and sel_type != "screen":
|
||||
if not found:
|
||||
# 元素都没找到就没什么可比的:算未命中(走 else 分支)。
|
||||
# 这条规则很重要——"没读到"永远不能被当成"和我设的不一样",
|
||||
# 否则界面还没加载出来时会误判成"账号被别人换了"。
|
||||
nofind = f"未找到目标,无法比对({cmp_op} {cmp_val})"
|
||||
detail = f"{detail};{nofind}" if detail else nofind
|
||||
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 '不符合'}"
|
||||
found = hit
|
||||
elif cmp_op and sel_type == "screen":
|
||||
_log.warning(f"[{self.serial}] 屏幕状态没有文本可比,已忽略文本比对设置")
|
||||
|
||||
if dynamic:
|
||||
_log.info(f"[{self.serial}] 条件判断 {detail} → {'命中' if found else '未命中'}")
|
||||
else:
|
||||
self._track_selector_health(sel_val, found)
|
||||
self._track_selector_health(sel_val, existed)
|
||||
branch = params.get("then" if found else "else") or []
|
||||
self.set_action(f"条件{'命中' if found else '未命中'} → 执行{'找到' if found else '未找到'}分支({len(branch)}步)")
|
||||
if cmp_op:
|
||||
cmp_txt = "比对命中" if found else "比对未命中"
|
||||
else:
|
||||
cmp_txt = "命中" if found else "未命中"
|
||||
self.set_action(f"条件{cmp_txt} → 执行{'then' if found else 'else'}分支({len(branch)}步)")
|
||||
_log.info(f"[{self.serial}] 条件判断 {'命中' if found else '未命中'}: {detail or sel_val},执行{'then' if found else 'else'}分支 {len(branch)} 步")
|
||||
self._exec_steps(d, branch, depth + 1)
|
||||
return found
|
||||
|
||||
def _read_element_text(self, d, sel_type, sel_val):
|
||||
"""取选中元素的文本(文本比对用)。读不到返回 ""。
|
||||
|
||||
只读操作,不改任何状态;失败静默返回空串,由调用方按"空文本"去比对
|
||||
(日志里会打出实际读到的值,排查时能看出是空还是不对)。
|
||||
|
||||
注意 u2 两种元素的 exists 语义不同,别"统一"掉:
|
||||
· `d.xpath(...)` → XPathSelector.exists 是**纯 bool 属性**
|
||||
· `d(text=...)` → UiObject.exists 是**可调用的 Exists 对象**
|
||||
(`bool()` 即时判断、`exists(timeout=t)` 带等待)——两种写法都对
|
||||
"""
|
||||
try:
|
||||
if sel_type == "xpath":
|
||||
el = d.xpath(sel_val)
|
||||
if not el.exists:
|
||||
return ""
|
||||
return (el.get_text() or "").strip()
|
||||
el = d(**{sel_type: sel_val})
|
||||
if not el.exists:
|
||||
return ""
|
||||
# 限时读:元素在"检查"与"读"之间消失时,别挂满默认等待(默认可能是 10s)
|
||||
return (el.get_text(timeout=_TEXT_READ_TIMEOUT) or "").strip()
|
||||
except Exception as e:
|
||||
_log.warning(f"[{self.serial}] 读取元素文本失败(按空文本比对): {e}")
|
||||
return ""
|
||||
|
||||
def _exec_swipe(self, d, params, depth=0):
|
||||
"""滑动一次(默认拟人:弧线 + 抖动 + 本设备手速,见 core/humanize.py)。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user