From cb184145e962509a296efd97f519ad895375063d Mon Sep 17 00:00:00 2001 From: maogeigei Date: Tue, 15 Sep 2026 22:11:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(cost):=20=E9=97=A8=E7=A6=81=E8=A1=A5?= =?UTF-8?q?=E4=B8=8A=20`Read`=20=E2=80=94=E2=80=94=20=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E7=AC=AC=E4=BA=8C=E5=A4=A7=E8=BE=93=E5=87=BA=E6=BA=90=EF=BC=88?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=82=B9=E5=87=BA=E3=80=8C=E9=87=8D=E7=82=B9?= =?UTF-8?q?=E6=98=AF=E6=8A=8A=E5=A4=A7=E8=BE=93=E5=87=BA=E5=8E=BB=E6=8E=89?= =?UTF-8?q?=E3=80=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户纠正(2026-09-15):① 「12 万 token 就收口」**不是他定的规则**(他选的是"设会话预算"这个方向, 12 万/80 是我自己收紧的取值,未与他确认);② 「切会话」只是**缓解**,**不是重点** —— 他早在更早一轮就点明过:「跟平台参数没关系……**为什么还要在上下文中 如何才能去掉**」。 ⇒ 重点 = **让大输出根本不进入历史**,唯一能做到的就是**入口拦截**。而原门禁**只管 Bash** —— 实测 `Read` 是**第二大输出源**且**完全没被覆盖**:某会话 44 次 / 累计 238 K 字符、单条最大 33 K 字符 (≈13 K token);另一会话 136 次 / 217 K 字符。 **改动** - `bash-output-guard.py`:新增 `READ_BIG = 400 KB` + `read_too_big()`;`tool_name == 'Read'` 时, 目标文件 > 400 KB ⇒ **deny**,文案给出两条等价做法(`Read` 带 `offset`/`limit` 分段读; 或先 `grep -n 关键词 文件 | head -20` 定位行号再读那几行) - `settings.json`:该 hook 的 matcher `"Bash"` → **`"Bash|Read"`**(⚠️ 配置是启动快照 ⇒ **需重启生效**) **实测 5/5**:Read 小文件放行 | **Read 大文件拦** | Bash `cat 大文件` 仍拦 | Bash `ls -la` 仍放行 | Write 不受影响 --- dsh-server-docs/scripts/bash-output-guard.py | 32 +++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/dsh-server-docs/scripts/bash-output-guard.py b/dsh-server-docs/scripts/bash-output-guard.py index eb2df52..68a23ba 100644 --- a/dsh-server-docs/scripts/bash-output-guard.py +++ b/dsh-server-docs/scripts/bash-output-guard.py @@ -110,6 +110,22 @@ def log(root, detail): CAT_BIG = 200 * 1024 # `cat` 目标文件 > 200 KB 才值得拦(小文件放行 ⇒ 防日常误伤) +READ_BIG = 400 * 1024 # `Read` 目标文件 > 400 KB ⇒ 至少几万 token ⇒ 拦(2026-09-15 实测补) +# ↑ 为什么补 Read:本钩子最初只管 Bash,但实测 `Read` 是**第二大输出源** —— +# 某会话 44 次 / 累计 238 K 字符、单条最大 33 K 字符(≈13 K token);另一会话 136 次 / 217 K 字符, +# 且**完全没有被任何机制覆盖**。⇒ "想把大输出从上下文里去掉",就必须覆盖所有会灌大输出的工具。 + + +def read_too_big(payload): + """`Read` 一个大文件 ⇒ True。取不到大小(文件不存在/相对路径)⇒ False(放行)。""" + ti = payload.get('tool_input') or {} + p = ti.get('file_path') or ti.get('path') or '' + if not isinstance(p, str) or not p: + return False + try: + return os.path.getsize(p) > READ_BIG + except OSError: + return False def cat_bigfile(cmd): @@ -169,7 +185,8 @@ def main(): return if (payload.get('hook_event_name') or '') != 'PreToolUse': return - if (payload.get('tool_name') or '') != 'Bash': + tool = payload.get('tool_name') or '' + if tool not in ('Bash', 'Read'): # 覆盖**所有**会灌大输出的工具(2026-09-15 由用户点出补 Read) return if os.environ.get('DSH_OUTPUT_GUARD_OFF'): return @@ -178,6 +195,19 @@ def main(): return except Exception: pass + if tool == 'Read': + if read_too_big(payload): + fp = str(((payload.get('tool_input') or {}).get('file_path')) or '') + log(str(root), 'DENY|Read 大文件|%s' % fp[:110]) + _emit({'hookSpecificOutput': { + 'hookEventName': 'PreToolUse', 'permissionDecision': 'deny', + 'permissionDecisionReason': ( + '💰 拦下:**Read 大文件**(`%s`)—— 它的**全文会写进会话历史、之后每轮全量重发**。\n' + '✅ 分段读:`Read` 加 `offset` / `limit`(例 `limit: 120`)只取你要的那段;' + '或先 `grep -n "关键词" 文件 | head -20` 定位行号,再读那几行。\n' + 'ℹ️ 确实要通读 ⇒ 用 Bash 分批 `sed -n \'1,200p\' 文件`;急停 env `DSH_OUTPUT_GUARD_OFF=1` ' + '或 `.workbuddy/bash-guard.disabled`。' % fp[:80])}}) + return cmd = ((payload.get('tool_input') or {}).get('command')) or '' if not cmd or SAFE.search(cmd): return