feat(cost): 路径自检 —— hooks 指向失效 / 配置被清空时自动上报(用户选 A)
**来历(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 被清空**报** ✓
This commit is contained in:
1 parent
fff2519ad3
commit
c25609c4c1
1 file changed
+47
-4
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user