From c25609c4c1b3755cdd297d3349d7ceb203b34e10 Mon Sep 17 00:00:00 2001 From: maogeigei Date: Tue, 15 Sep 2026 22:06:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(cost):=20=E8=B7=AF=E5=BE=84=E8=87=AA?= =?UTF-8?q?=E6=A3=80=20=E2=80=94=E2=80=94=20hooks=20=E6=8C=87=E5=90=91?= =?UTF-8?q?=E5=A4=B1=E6=95=88=20/=20=E9=85=8D=E7=BD=AE=E8=A2=AB=E6=B8=85?= =?UTF-8?q?=E7=A9=BA=E6=97=B6=E8=87=AA=E5=8A=A8=E4=B8=8A=E6=8A=A5=EF=BC=88?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E9=80=89=20A=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **来历(2026-09-15 实测事故)**:文档库被整目录搬到 `_中间产物_待清理/` ⇒ 宿主 hooks 仍指向旧位置 ⇒ **锁闸门静默失效**;当时唯一线索是"`lock-hook.log` 今天 0 条 PreToolUse",而**没人会主动去数**。 `stop-dialog-guard.py` 新增 `path_health()`(每轮随 `UserPromptSubmit` 跑): 1. **从 `settings.json` 现读** hooks 的 command ⇒ 抽出其中的 `.py` 逐个 `os.path.exists` (配置里是权威指向 ⇒ 能发现"指向了不存在的文件",正是今晚那种形态) 2. **hooks 里没有任何 command 条目** ⇒ 报"可能被清空 / 被整段覆盖"(用户记忆里的真实风险) 命中即经 `UserPromptSubmit` 注入「🚨【路径自检】…请立刻上报用户」。 **fail-open**:读不到配置就直接跳过,绝不因体检本身误报。 ⚠️ 设计取舍(实测发现):**没有**去做"文档库是否在预期位置"的检查 —— 新位置在**另一个盘** (`D:/github/dsh_shenxian`),硬编码候选路径会**误报**;而检查 ① 已能覆盖同一类事故。 实测三种情形:真实环境**不报** ✓ | 指向不存在脚本**报** ✓ | hooks 被清空**报** ✓ --- dsh-server-docs/scripts/stop-dialog-guard.py | 51 ++++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/dsh-server-docs/scripts/stop-dialog-guard.py b/dsh-server-docs/scripts/stop-dialog-guard.py index 987a1fa..9cb5a75 100644 --- a/dsh-server-docs/scripts/stop-dialog-guard.py +++ b/dsh-server-docs/scripts/stop-dialog-guard.py @@ -339,6 +339,48 @@ def guard_health(root): '(规则该不该收窄是人的决定)。' % (len(rows), n, why)) +def path_health(root): + """**自动发现并上报**:hooks 指向的脚本、文档库位置,是否还在。 + + 来历(2026-09-15 实测事故):文档库被整目录搬到 `_中间产物_待清理/` ⇒ 宿主 hooks 仍指向旧位置 ⇒ + **锁闸门静默失效**;当时唯一线索是"`lock-hook.log` 今天 0 条 PreToolUse",而**没人会主动去数**。 + ⇒ 让每轮都跑的脚本顺带体检,机制失效能在**下一次用户发言时**自己浮出来。 + + 判据(任一命中即报):① **从 `settings.json` 现读** hooks 的 command,抽出其中的 `.py` 逐个 `os.path.exists` + (配置里是权威指向 ⇒ 能发现"指向了不存在的文件");② 文档库在两处候选位置**都不存在**。 + 取不到配置 ⇒ 跳过该项(**fail-open**,绝不因体检本身误报)。 + """ + bad = [] + cfg = os.path.join(os.environ.get('WORKBUDDY_CONFIG_DIR') + or os.path.join(os.path.expanduser('~'), '.workbuddy'), 'settings.json') + seen = 0 + try: + d = json.load(io.open(cfg, encoding='utf-8')) + for ev, arr in (d.get('hooks') or {}).items(): + for blk in (arr or []): + for h in ((blk or {}).get('hooks') or []): + c = h.get('command') or '' + if c: + seen += 1 + for m in re.finditer(r'([A-Za-z]:[\\/][^"\']*?\.py)', c): + p = m.group(1) + if not os.path.exists(p): + bad.append('hooks「%s」指向的 `%s` 不存在' % (ev, os.path.basename(p))) + except Exception: + return '' # 读不到配置 ⇒ fail-open,绝不因体检本身误报 + if seen == 0: + bad.append('hooks 配置里**没有任何 command 条目** ⇒ 钩子可能被清空 / 被整段覆盖') + if not bad: + return '' + uniq = [] + for x in bad: + if x not in uniq: + uniq.append(x) + return ('🚨【路径自检】%s ⇒ **机制可能已静默失效**。处置:**立刻上报用户**,并核对 `settings.json` 的 ' + 'hooks 路径与文档库当前位置(2026-09-15 同类事故:文档库被整目录搬走,锁闸门失效一整天无人察觉)。' + % ';'.join(uniq[:3])) + + def mode_of(root): try: m = io.open(os.path.join(root or '.', '.workbuddy', 'stop-guard-mode'), encoding='utf-8').read() @@ -364,12 +406,13 @@ def user_prompt_mode(payload): toks, ncalls, prev = session_budget(tp) note = budget_note(toks, ncalls, prev) gh = guard_health(root0 or '.') - log(root0 or '.', 'invoked(user-prompt)|mode=%s|上轮收尾=征询句:%s|上下文=%s tok(+%s)|工具=%s 次|预算告警=%s|门禁自检=%s|%s' - % (mode, hit, toks, (toks - prev) if (toks and prev) else '-', ncalls, bool(note), bool(gh), + ph = path_health(root0 or '.') + log(root0 or '.', 'invoked(user-prompt)|mode=%s|上轮收尾=征询句:%s|上下文=%s tok(+%s)|工具=%s 次|预算告警=%s|门禁自检=%s|路径自检=%s|%s' + % (mode, hit, toks, (toks - prev) if (toks and prev) else '-', ncalls, bool(note), bool(gh), bool(ph), (tail.replace('\n', ' ')[:60] if tail else '(取不到上一轮文本)'))) - if mode == 'inject' and (hit or note or gh): + if mode == 'inject' and (hit or note or gh or ph): ctx = '' - for seg in (CONTEXT if hit else '', note, gh): + for seg in (CONTEXT if hit else '', note, gh, ph): if seg: ctx += ('\n\n' + seg) if ctx else seg _emit({'hookSpecificOutput': {'hookEventName': 'UserPromptSubmit',