feat(发布计划): 新增「校验页面」步骤(expect_el)—— 每一步先确认上一步真的完成了

用户要求:"进行每一步的时候先校验上一步有没有完成,比如相册界面肯定有特征,如果没有到相册界面
就出现执行,你看看怎么处理" → 方案 A:新增只做校验、不带副作用的步骤。

· tasks/generic/task.py:新步骤 expect_el
  - 参数:特征元素选择器 / timeout(10) / what("在等什么") / on_missing
  - on_missing:skip_rest(默认,**跳过本轮剩余步骤** —— 这一条不发、换下一个号) / notify / stop
  - 为此加了 worker 级的 `_skip_rest`:_exec_steps 每步前检查,foreach_account 每轮复位
· 骨架:关键点击前各插一个 —— 相册页(//*[@text='所有照片']) → 素材已选中(desc 选择/预览)
  → 剪辑页(desc 文字/话题) → 发布页(含'添加标题'/'添加作品描述') → 发布按钮
· 通知:新增 task.page.mismatch(页面不对,本轮剩余步骤已跳过)
· 昵称选择器改**两版通吃**(实测 A06=40.7.0):xpath 联合
  `//*[@resource-id="…:id/tkv"] | //*[@resource-id="…:id/tk+"]`
  —— 40.7 是 tkv、40.6 是 tk+,联合在 40.7 上实测能读到名字;.env(dev)已改,切号入口两版同为 desc=切换账号
· 前端:编辑器面板 + 参数表单 + 发布页就地编辑字段表都认 expect_el
· 文档:TASK_DEV §3(27→28 种)+ §4.7("每步先校验上一步")、NOTIFY、research §七·补(40.7.0 对照表
  + 联合 xpath + 各页特征 + "别按坐标点页签"的坑)、.env.example
· 测试:新增 scripts/tests/test_expect_el.py(特征在/不在、三种 on_missing、选择器为空),全套 7 个用例通过
This commit is contained in:
2026-10-12 11:49:48 +08:00
parent 50255af899
commit 10d2eb8334
12 changed files with 328 additions and 14 deletions
+82
View File
@@ -130,6 +130,9 @@ STEP_TYPES = [
"then": [], "else": []}},
{"type": "mark_done", "label": "记为已做", "icon": "✅",
"params": {"selector_type": "text", "selector_value": ""}},
{"type": "expect_el", "label": "校验页面", "icon": "🔎",
"params": {"selector_type": "xpath", "selector_value": "", "timeout": 10,
"on_missing": "skip_rest", "what": ""}},
{"type": "read_account_name", "label": "读取本机账号名字", "icon": "🏷",
"params": {"selector_type": "xpath", "selector_value": "",
"timeout": 8, "on_missing": "fail"}},
@@ -268,6 +271,8 @@ class GenericStepsWorker(BaseWorker):
# 保证"检查"与"记账"用的是**同一个身份字符串**(否则 key 对不上、去重失效)
self._last_ident = ""
self._last_release = None # 「推送发布视频」推的那条计划,供「标记发布结果」用
# 「校验页面」(expect_el) 判"这页不对"时置位:跳过本轮剩余步骤(见 _exec_steps)
self._skip_rest = False
# 运行期变量(一台设备一个 worker 实例,天然隔离,不同任务不会串):
# account_name 「读取本机账号名字」刚读到的名字
# name_selector 读名字用的选择器 (sel_type, sel_val) —— 推送前核对要用同一个控件
@@ -334,6 +339,12 @@ class GenericStepsWorker(BaseWorker):
for i, step in enumerate(steps):
if self.stopped() or self.is_time_up():
return
# 「校验页面」判"这一页不对"时置位:**本轮剩下的步骤不再执行**
# (由 foreach_account 在下一轮开头复位 —— 效果就是"这一条不发,换下一个号")
if self._skip_rest:
_log.warning(f"[{self.serial}] 本页校验没过 → 跳过本轮剩余 {len(steps) - i} 步"
f"(不再盲点后面的按钮)")
return
self._maybe_patrol(d) # 穿插:每步之前看一眼有没有巡检到点了
# 压入本步在兄弟里的序号(1 起):容器步骤执行 children 时会在其后
# 继续追加,得到 "2.1" 这样的嵌套路径
@@ -979,6 +990,76 @@ class GenericStepsWorker(BaseWorker):
f"({plan.get('release_date')} #{plan.get('seq')})")
return cands, note
def _exec_expect_el(self, d, params, depth=0):
"""校验页面:**只做校验、不带副作用** —— 确认"上一步真的完成了"(该出现的特征元素在不在)。
参数:`selector_type`/`selector_value`(**这一页的特征元素**,例如相册页的「所有照片」、
剪辑页的「文字/话题」、发布页的标题框)、`timeout`(10,最多等它出现多久)、
`what`(可选,人话说明"在等什么",写进日志/通知)、`on_missing`:
| 值 | 等不到时 |
|---|---|
| `skip_rest`(默认,推荐) | **本轮剩下的步骤不再执行**(不盲点后面的按钮)—— 配合「循环本机待发的号」= 这一条不发、换下一个号 |
| `notify` | 发一条通知,然后**继续**后面的步骤(你自己判断要不要接着点) |
| `stop` | 停本设备 + 通知(出了真问题、要人来看时用) |
为什么要有它:抖音发布流程是一串"点 A → 点 B → 点 C"。中间任何一步没生效,
后面就是**盲点**(点到别的控件上),最后"任务跑完了、什么都没发出去"还不报错。
在每个关键点击**前面**放一个"应当在某页"的校验,页面不对就停下来留痕/换号。
返回 True=看到了;False=没看到(已按 `on_missing` 处置)。
"""
sel_type = (params.get("selector_type") or "xpath").strip()
sel_val = (params.get("selector_value") or "").strip()
what = (params.get("what") or "").strip() or sel_val[:40]
timeout = float(params.get("timeout") or 10)
on_missing = (params.get("on_missing") or "skip_rest").strip().lower()
if not sel_val:
_log.error(f"[{self.serial}] 校验页面:选择器是空的(应填『这一页的特征元素』)")
self.set_action("校验页面:没填选择器")
return None
found = False
try:
if sel_type == "xpath":
found = bool(d.xpath(sel_val).wait(timeout=timeout))
else:
found = bool(d(**{sel_type: sel_val}).exists(timeout=timeout))
except Exception as e:
_log.warning(f"[{self.serial}] 校验页面异常(按没看到处理): {e}")
if found:
_log.info(f"[{self.serial}] 校验页面 ✅ {what}")
self.set_action(f"页面对:{what}")
return True
_log.error(f"[{self.serial}] 校验页面 ❌ 没等到「{what}」—— {self._screen_digest(d)}")
self.set_action(f"页面不对:没等到「{what}」")
if on_missing == "stop":
try:
notifier.notify("task.page.mismatch", serial=self.serial,
device_name=(self.ctx or {}).get("device_name", ""),
model=(self.ctx or {}).get("model", ""),
want=what, msg="页面不对,本设备已停止(请看设备屏幕)")
except Exception:
pass
self.stop()
elif on_missing == "notify":
try:
notifier.notify("task.page.mismatch", serial=self.serial,
device_name=(self.ctx or {}).get("device_name", ""),
model=(self.ctx or {}).get("model", ""),
want=what, msg="页面不对,已跳过校验继续执行(有盲点风险)")
except Exception:
pass
else: # skip_rest(默认)
self._skip_rest = True
try:
notifier.notify("task.page.mismatch", serial=self.serial,
device_name=(self.ctx or {}).get("device_name", ""),
model=(self.ctx or {}).get("model", ""),
want=what, msg="页面不对,本轮剩余步骤已跳过(这一条没发)")
except Exception:
pass
return False
def _exec_read_account_name(self, d, params, depth=0):
"""读取本机当前登录的账号名字(抖音「我」页面上的昵称控件)。
@@ -1770,6 +1851,7 @@ class GenericStepsWorker(BaseWorker):
target = names[0]
tried.append(target) # 试过就不回头(防死循环)
self._vars["target_name"] = target
self._skip_rest = False # 新一轮:复位"跳过剩余步骤"(上一轮某步判过页面不对)
sent += 1
self.set_action(f"第 {sent} 个号:{target}(本机还有 {len(names)} 个待发名字)")
_log.info(f"[{self.serial}] 循环本机待发的号:轮到『{target}』"