From 0c4f586ae579139ac187753b7e70cd893a5b0944 Mon Sep 17 00:00:00 2001 From: butubb <1422726308@qq.com> Date: Sat, 8 Aug 2026 21:35:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=80=89=E6=8B=A9=E5=99=A8=E5=81=A5?= =?UTF-8?q?=E5=BA=B7=E6=A3=80=E6=B5=8B=20-=20=E8=BF=9E=E7=BB=AD=E6=9C=AA?= =?UTF-8?q?=E5=91=BD=E4=B8=AD=E8=87=AA=E5=8A=A8=E4=B8=8A=E6=8A=A5=EF=BC=8C?= =?UTF-8?q?=E6=9A=B4=E9=9C=B2"=E9=9D=99=E9=BB=98=E7=A9=BA=E8=BD=AC"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 通用任务 click 步骤连续 10 次找不到元素时判定选择器可能失效(App 改版), 记录 last_warning 并日志 ERROR,监控页设备行显示 ⚠ 警告。 解决"选择器失效但任务仍显示成功"的静默空转问题。 --- core/task_manager.py | 1 + static/admin/monitor.js | 4 +++- tasks/generic/task.py | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/core/task_manager.py b/core/task_manager.py index 76cc08e..b79a5b4 100644 --- a/core/task_manager.py +++ b/core/task_manager.py @@ -744,6 +744,7 @@ class TaskManager: "progress": w.get("progress", {}), "current_action": w.get("current_action", ""), "last_error": w.get("last_error", ""), + "last_warning": w.get("last_warning", ""), "running_job": r.get("job_id", ""), "task_job": w.get("task_job", ""), "attempt": r.get("attempt", 0), diff --git a/static/admin/monitor.js b/static/admin/monitor.js index 699a6c9..ca483c1 100644 --- a/static/admin/monitor.js +++ b/static/admin/monitor.js @@ -454,6 +454,8 @@ function renderDeviceRow(dev){ const retry=dev.attempt>0?' '+dev.attempt+'次':''; // 预计结束时间(设置了运行时长上限时显示) const endInfo=(dev.worker_status==='running'&&dev.end_time)?'
预计结束 '+new Date(dev.end_time*1000).toLocaleTimeString()+'
':''; + // 选择器健康警告(某步骤连续未找到元素,可能 App 改版选择器失效) + const warnInfo=dev.last_warning?'
⚠ '+esc(dev.last_warning)+'
':''; const stopBtn=(dev.worker_status==='running'||dev.worker_status==='connecting')?'':''; // 截图按钮(任何状态都可看,离线时会报错) const shotBtn=''; @@ -464,7 +466,7 @@ function renderDeviceRow(dev){ const checked=_selectedSerials.has(dev.serial)?'checked':''; const rowCls=_selectedSerials.has(dev.serial)?'row-checked':''; const cb=''; - return ''+cb+''+devLabel+''+esc(dev.model)+''+stf+offline+''+wb+''+fg+''+prog+''+tname+retry+endInfo+''+esc(dev.current_action||'-')+''+esc(dev.last_error||'-')+''+btns+''; + return ''+cb+''+devLabel+''+esc(dev.model)+''+stf+offline+''+wb+''+fg+''+prog+''+tname+retry+endInfo+''+esc(dev.current_action||'-')+warnInfo+''+esc(dev.last_error||'-')+''+btns+''; } async function loadJobsForMonitor(){ diff --git a/tasks/generic/task.py b/tasks/generic/task.py index ff6cebe..468af01 100644 --- a/tasks/generic/task.py +++ b/tasks/generic/task.py @@ -83,6 +83,25 @@ class GenericStepsWorker(BaseWorker): self.max_duration = int(p.get("max_duration", 0)) self.steps = p.get("steps", []) self._action_counts = {} # 步骤执行计数 {step_label: count} + self._miss_counts = {} # 选择器健康:selector -> 连续未命中次数 + + # 某 click 选择器连续未找到元素的次数达到该值,判定可能失效(App 改版) + _MAX_CONSECUTIVE_MISS = 10 + + def _track_selector_health(self, sel_val, found): + """记录某选择器的连续未命中次数,达到阈值上报 last_warning 并重置计数。 + + 用于发现"选择器失效但任务仍显示成功"的静默空转问题(如 App 改版后元素找不到了)。 + """ + if found: + self._miss_counts.pop(sel_val, None) + return + n = self._miss_counts.get(sel_val, 0) + 1 + self._miss_counts[sel_val] = n + if n >= self._MAX_CONSECUTIVE_MISS: + _log.error(f"[{self.serial}] 选择器连续 {n} 次未找到元素,可能已失效(App 改版?):{sel_val}") + _update_status(self.serial, last_warning=f"选择器连续{n}次未命中") + self._miss_counts.pop(sel_val, None) # 已上报,重置避免刷屏 def run_task(self, d): """按 steps 顺序执行。loop 步骤递归执行其 children。""" @@ -179,6 +198,7 @@ class GenericStepsWorker(BaseWorker): # 兼容旧配置:selector_type 不是 xpath 但值是 XPath(以 // 开头) if sel_type != "xpath" and sel_val.strip().startswith("//"): sel_type = "xpath" + found = False try: if sel_type == "xpath": # u2 的 d.xpath() 返回 XPathSelector,exists 是属性需用 wait() @@ -186,6 +206,7 @@ class GenericStepsWorker(BaseWorker): if el.wait(timeout=timeout): el.click() _log.info(f"[{self.serial}] click 命中: xpath={sel_val}") + found = True else: _log.warning(f"[{self.serial}] click 未找到元素: xpath={sel_val} (等待{timeout}s)") else: @@ -194,8 +215,10 @@ class GenericStepsWorker(BaseWorker): if el.exists(timeout=timeout): el.click() _log.info(f"[{self.serial}] click 命中: {sel_type}={sel_val}") + found = True else: _log.warning(f"[{self.serial}] click 未找到元素: {sel_type}={sel_val} (等待{timeout}s)") + self._track_selector_health(sel_val, found) except Exception as e: _log.warning(f"[{self.serial}] click 异常: {e}")