feat(去重): 「记为已做」留空 = 自动跟随上面「去重」检查的身份(不再要求配两遍)

用户提的(他说得对):"这个标记已做不是应该是我上面的条件判断命中哪一个就用哪一个做吗?
这怎么还要自己选择元素啊"。

原来的设计要求检查与记账**各配一次身份元素** —— 一旦一边填了、另一边忘了,两边算出的
key 不一致 → **去重静默失效**(现象就是"去重没生效、还是重复做",他刚踩过)。
而我在"有效期"上为了避免两处配置特意强制放任务级,却在"身份"上要求填两遍,自相矛盾。

改成:
- `tasks/generic/task.py`:「去重」条件解析出的身份存进 `self._last_ident`;
  `_exec_mark_done` 的身份元素**留空时复用它**(检查与记账必然是同一个字符串)。
  三种情况分清(这是关键,别退化成"用设备"误标):
    ① 自己填了身份元素 → 用自己填的(显式优先)
    ② 留空 + 上面跑过去重检查 → **复用那个身份**
    ③ 留空 + 检查**跑了但没读到身份**(页面没到)→ **不记账**,下次重跑重试
       —— 绝不能退回设备身份,那会在"什么都没做成"时把设备标成已做
    ④ 留空 + 任务里根本没有去重检查 → 退回设备身份(一号一机场景)
- `static/admin/editor.js`:`mark_done` 面板文案改为"留空 = 自动跟随上面检查的身份(推荐)";
  校验里记账侧的**空值不再参与身份比对**(留空是合法的、且会自动对齐),
  只有"填了、且和检查侧不一样"才告警,并在文案里建议留空。
- `doc/TASK_DEV.md` §4.6:身份只需在检查侧配一次(带一张四种组合的表);步骤表同步。

验证:新增 11 项(复用后账本身份 == 检查身份、换设备仍能命中=去重真生效、
显式填身份仍优先、无检查时退回设备、**读不到身份时不记账也不误标设备**);
原 dedup 套件(单元 + 真机)+ 编辑器语法回归全过;
把用户**生产任务的真实 JSON** 喂给新校验 —— **0 条警告**(旧版会报一条"身份不一致",
但现在留空=自动跟随是正确配置)。
This commit is contained in:
2026-09-24 13:39:45 +08:00
parent 4f3027b54f
commit e82fd64695
3 changed files with 57 additions and 18 deletions
+29 -2
View File
@@ -231,6 +231,10 @@ class GenericStepsWorker(BaseWorker):
self._path = []
self._step_rows = 0 # 本次运行已记录的明细条数(封顶见 _record_step)
self._step_capped = False
# 「去重」条件最近一次解析出的身份:`mark_done` 留空时复用它,
# 保证"检查"与"记账"用的是**同一个身份字符串**(否则 key 对不上、去重失效)
self._last_ident = ""
self._dedup_seen = False # 本轮是否跑过「去重」检查(区分"没配"与"配了但没读到")
# 公共巡检(任务级配置,独立于步骤画布;见 core/patrol.py):
# _wid 内部标识,用来记"下次到点 / 上次命中"两个节奏
self._watchers = []
@@ -693,11 +697,13 @@ class GenericStepsWorker(BaseWorker):
# 去重条件:**这个身份在这个任务里做过了吗**(跨设备共享一份账本,
# 见 core/dedup.py)。命中=做过 → 走 then 分支(通常放"跳过/停止/发通知")。
kind, hours = self._dedup_policy()
self._dedup_seen = True # 供「记为已做」区分"没配检查"与"配了但没读到"
ident = self._dedup_identity(d, params, "ident_type", "ident_value")
if ident is None:
found = False # 身份判定不了 → 当"没做过",照常往下做(宁可重复不误拦)
detail = "去重:身份读不到,按未做过处理"
else:
self._last_ident = ident # 供后面「记为已做」复用(见 _exec_mark_done)
found = dedup.check(self.ctx.get("job_id", ""), ident, kind, hours)
detail = f"去重:身份『{ident}』{'做过' if found else '没做过'}(有效期 {kind})"
elif sel_type == "screen":
@@ -829,10 +835,31 @@ class GenericStepsWorker(BaseWorker):
**放在动作成功之后**:动作失败就不记账,下次重跑会重试该设备——
拆成"检查在前、记账在后"两步,天然就是"成功才记账、失败不丢"。
身份读不到时**不记账**(记一个空身份会把别人挡住)。
**身份留空 = 复用前面「去重」条件刚解析出的那个身份**(推荐这样配):
检查与记账必须用**同一个身份字符串**,key 才算得到一起;让用户在两处各配一遍
元素,一旦不一致(填了一个、另一个忘了)去重就静默失效——这是最容易踩的坑,
所以默认行为改成"自动跟随上面的检查"。
"""
kind, hours = self._dedup_policy()
ident = self._dedup_identity(d, params, "selector_type", "selector_value")
if ident is None:
if (params.get("selector_value") or "").strip():
ident = self._dedup_identity(d, params, "selector_type", "selector_value")
elif self._last_ident:
ident = self._last_ident
_log.info(f"[{self.serial}] 记为已做:复用上面去重检查的身份『{ident}』")
elif self._dedup_seen:
# 上面**跑了**去重检查、但身份读不到(元素没出来/页面没到)。
# 这时**绝不能**退回设备身份记一笔:那会在"什么都没做成"的情况下
# 把设备标成已做。不记 → 下次重跑会重试。
ident = None
_log.warning(f"[{self.serial}] 记为已做:上面去重检查没读到身份(页面没到?)"
f"→ 本次不记账,下次重跑会重试")
else:
# 任务里根本没有前置去重检查、又没填身份元素 → 退回设备身份(一号一机场景)
ident = self.serial
_log.info(f"[{self.serial}] 记为已做:没填身份元素也没有前置去重检查 → "
f"按设备身份记账({ident})")
if not ident:
self.set_action("记为已做:身份读不到 → 本次不记(下次重跑会重试)")
return True
first = dedup.mark(self.ctx.get("job_id", ""), ident, kind, hours,