fix(cost): 修门禁三处误伤 + 新增「自动发现并上报」

用户两问:① 机制会不会影响 AI 会话?② 后续能否自动发现并上报?——① 自查出**三处真误伤**并修掉,
② 新增两条自动上报。

**修误伤(都是实测出来的,不是推测)**
1. `ls -la` 被拦 —— 正则 `-[a-zA-Z]*[lR]` 里的 `l` 命中日常 `-la` ⇒ 改为**只拦 `-R`**(递归全树)
2. `cat 小文件` 被拦 —— 盲拦一切 `cat *.txt` ⇒ 改为**按目标文件大小**(`> cat_bigfile()`:>200 KB 才拦,
   取不到大小一律放行)
3. **"提到"也拦** —— 原按**整条命令文本**匹配 ⇒ `printf 'ls -laR /x'`、测试脚本、写文档全被拦(阻塞面过大)
   ⇒ 改为**按 shell 分段、只看每段开头**(`segments()` + `re.match`):
   `cd /tmp && ls -laR /x` 照样拦(第二段以 `ls` 开头),而"引号里提到"放行
   实测 8/8:`ls -la` 放行 · `ls -laR` 拦 · `cd x && ls -laR` 拦 · `cat` 小/大 分别放行/拦 ·
   引号内提到放行 · `grep -rn` 拦 · `grep -rn | head` 放行

**新增「自动发现并上报」**(用户第 2 问的落地)
- `stop-dialog-guard.py` → `guard_health()`:读 `bash-guard.log`,**同一规则重复命中 ≥3 次** ⇒
  经 `UserPromptSubmit` 主动报告「疑似误伤,不是你操作有问题」,并给出两条处置(换写法 / `off` + 上报用户)
- 两个钩子脚本仍保持**入口即留痕**(`entry|event=…`)⇒ "钩子有没有被调用"永远可判

⚠️ 副作用须知:门禁**已生效**(重启已加载)。默认 `soft` 只拦 6 类;急停 = env `DSH_OUTPUT_GUARD_OFF=1`
或 `<工作区>/.workbuddy/bash-guard-mode` 写 `off`;`hard` 才启用 `rg`/`git log`/`du·tree` 三条。
This commit is contained in:
admin committed 2026-09-15 21:20:02 +08:00
1 parent 3efd68517f
commit 3f141c7b4d
2 files changed
+80 -14

No files matched your search

+38 -7
View File
@@ -308,6 +308,37 @@ def budget_note(tokens, ncalls, prev=None):
)
def guard_health(root):
"""**自动发现并上报**:读 `bash-guard.log`,同一规则重复命中 ≥3 次 ⇒ 提示"可能误伤"。
为什么要有:门禁自己不会喊疼 —— 只有"写日志"没人看。让每轮都跑的脚本顺带体检,
机制问题才能在**下一次用户发言时**浮出来(而不是等人发现"AI 怎么老做不成事")。
"""
try:
p = os.path.join(root, '.workbuddy', 'bash-guard.log')
rows = [l for l in io.open(p, encoding='utf-8').read().split('\n') if 'DENY|' in l][-40:]
except OSError:
return ''
if len(rows) < 3:
return ''
cnt = {}
for l in rows:
try:
k = l.split('DENY|')[1].split('|')[0].strip()
except IndexError:
continue
cnt[k] = cnt.get(k, 0) + 1
if not cnt:
return ''
why, n = max(cnt.items(), key=lambda kv: kv[1])
if n < 3:
return ''
return ('⚠️【门禁自检】最近 %d 次 Bash 里有 **%d 次**因「%s」被拦(同一规则重复命中)⇒ 先怀疑**误伤**,'
'不是谁的操作有问题。处置:① 换等价限流写法;② 若确认误伤 ⇒ 把 `off` 写进 '
'`.workbuddy/bash-guard-mode`(或 env `DSH_OUTPUT_GUARD_OFF=1`),并**主动上报用户**'
'(规则该不该收窄是人的决定)。' % (len(rows), n, why))
def mode_of(root):
try:
m = io.open(os.path.join(root or '.', '.workbuddy', 'stop-guard-mode'), encoding='utf-8').read()
@@ -332,15 +363,15 @@ def user_prompt_mode(payload):
hit = bool(tail) and not RE_QUOTE.search(tail) and bool(RE_BAN.search(tail))
toks, ncalls, prev = session_budget(tp)
note = budget_note(toks, ncalls, prev)
log(root0 or '.', 'invoked(user-prompt)|mode=%s|上轮收尾=征询句:%s|上下文=%s tok(+%s)|工具=%s 次|预算告警=%s|%s'
% (mode, hit, toks, (toks - prev) if (toks and prev) else '-', ncalls, bool(note),
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),
(tail.replace('\n', ' ')[:60] if tail else '(取不到上一轮文本)')))
if mode == 'inject' and (hit or note):
if mode == 'inject' and (hit or note or gh):
ctx = ''
if hit:
ctx += CONTEXT
if note:
ctx += ('\n\n' + note) if ctx else note
for seg in (CONTEXT if hit else '', note, gh):
if seg:
ctx += ('\n\n' + seg) if ctx else seg
_emit({'hookSpecificOutput': {'hookEventName': 'UserPromptSubmit',
'additionalContext': ctx}})