chore(hooks): 输出门禁三脚本更新 + 新增技能加载闸门 skill-load-guard.py

This commit is contained in:
admin committed 2026-09-16 11:28:51 +08:00
1 parent c8b514832f
commit bda9b10161
4 files changed
+385 -10

No files matched your search

+82 -7
View File
@@ -245,14 +245,77 @@ CONTEXT = (
# ─────────────────────────────────────────────────────────────
# 会话预算(2026-09-15 用户选 A 案):到 ~15 万 token / ~120 次工具调用 ⇒ 提醒开新会话
# 会话预算(2026-09-15 用户选 A 案 · 提交 03c8363):到 **12 万 token / 80 次工具调用** ⇒ 提醒开新会话
# ⚠️ 2026-09-16 更正:本行原写「~15 万 token / ~120 次工具调用」,与代码值(`BUDGET_TOKENS=120000` /
# `BUDGET_TOOLS=80`)**不符** —— 提交 `03c8363` 的 message 里就写的是 `120000/80`,代码从未用过 15 万/120
# ⇒ 判定为注释笔误,已按代码更正(避免后人据注释误判告警点)。
# 数值本身由 AI 在实现层自定(属 §1「性能与资源调参」= 边界内);**方案(A 案四招)才是用户 09-15 拍板的**。
# 为什么加进本脚本、而不新装一个 hook:`settings.json` 的 hooks 是**应用启动时快照**(新增条目要重启),
# 而**脚本内容每次调用现读** ⇒ 改这里即刻生效。数据源 = 转录 `type=function_call` → `message.usage.input_tokens`
# (最近一条 usage 即"当前上下文体量",精确,不靠估算)。
# 依据(实测某会话):上下文 5.2 万 → 59.2 万;累计 input 1.93 亿 / output 51.9 万(**371:1**);
# 其中 33 次缓存失效,每次都把 ~50 万 token **按全价**重算 ⇒ 会话越长,单次失效越贵。
BUDGET_TOKENS = 120000
# ★ 2026-09-16 改为**分级 + 去重**(原为单一阈值 120000/80,超了之后每轮都报 ⇒ 变成"狼来了")
# 设计要点:**同一级别只报一次** ⇒ 跨级才再提醒,既不麻木也不失警。
BUDGET_TOKENS = 120000 # 一级:轻提示(保留原名,兼容既有引用)
BUDGET_STRONG = 200000 # 二级:建议收口
BUDGET_FORCE = 300000 # 三级:**强制收口**(先落盘、出接续包,再开新会话)
BUDGET_TOOLS = 80
ALERT_LEVEL_REL = os.path.join('.workbuddy', '.budget-alert-level')
# 为什么三级不是"自动开新会话"(2026-09-16 与用户讨论后定):
# ① **技术上做不到** —— hook 只有 `additionalContext`(注入)与 `permissionDecision`(拦工具)两种输出,
# 事件只有 SessionStart / PreToolUse / UserPromptSubmit / Stop,**没有"创建/切换会话"的能力**;
# AI 自身也只能在会话内行动。⇒ "自动开"这一半无法实现。
# ② **设计上不该做** —— 新会话 = 上下文清零 ⇒ **在途状态全丢**;而"该带走什么"只有 AI 判断得了
# ⇒ 顺序必须是 **先收口(落盘 + 接续包)→ 再由用户开新会话**,反了就是"突然失忆"。
# ③ 因此本级**不做"自动开"**,做**"强制收口"**:把状态固化成文档,让下一个会话能无损接上。
LV_PREFIX = {
1: '',
2: '⚠️ 上下文已过 **20 万**,本轮收口后建议开新会话。\n',
3: ('🔴 **已过 30 万 ⇒ 进入强制收口模式**(先落盘、再接续):\n'
' ① 把在途状态写进 `.workbuddy/memory/`(今日日志 + 必要的 MEMORY.md 条目);\n'
' ② 产出**接续包**:目标 / 已完成 / 在途 / 未完成 / 下一步 / 关键决定 / 回滚点;\n'
' ③ 然后明确告知用户「请开新会话,接续点在 X」,**由用户开**(钩子无法自动创建会话)。\n'
' ⛔ 不要在本会话继续开新任务 —— 每多跑一轮,成本按当前水位线性放大。\n'),
}
def _alert_level(tokens, ncalls):
"""0=未超 1=轻 2=建议收口 3=强制收口"""
t = tokens or 0
if t >= BUDGET_FORCE:
return 3
if t >= BUDGET_STRONG:
return 2
if t >= BUDGET_TOKENS:
return 1
if ncalls is not None and ncalls >= BUDGET_TOOLS:
return 1
return 0
def _last_level(root, sid):
"""读「本会话」已报到的级别。**按 sid 区分** —— 否则新会话会被上一会话的状态挡住 ⇒ **漏报**。"""
try:
with io.open(os.path.join(root, ALERT_LEVEL_REL), encoding='utf-8') as f:
s = (f.read() or '').strip()
ps = s.split('\t')
if len(ps) == 2 and ps[0] == sid:
return int(ps[1] or 0)
except Exception:
pass
return 0
def _set_level(root, sid, lv):
try:
p = os.path.join(root, ALERT_LEVEL_REL)
os.makedirs(os.path.dirname(p), exist_ok=True)
with io.open(p, 'w', encoding='utf-8', newline='\n') as f:
f.write('%s\t%d' % (sid, lv))
except Exception:
pass
def session_budget(path):
@@ -286,8 +349,15 @@ def session_budget(path):
return last_in, n_calls, prev_in
def budget_note(tokens, ncalls, prev=None):
"""超预算 ⇒ 返回注入用的提醒串;未超 ⇒ 但增量异常也提醒(<1 万 token 的增量不打扰)。"""
def budget_note(tokens, ncalls, prev=None, root='', sid=''):
"""分级提醒;**同一级别只注入一次**(跨级才再报)—— 治"超阈值后每轮都报"导致的麻木。
未超阈值时,单轮增量异常(≥4 万 token)仍提醒。"""
# ★ 分级 + 去重(2026-09-16):本级已报过 ⇒ 静默(**按会话 id 区分**,新会话重新计数)
lv = _alert_level(tokens, ncalls)
if lv and lv <= _last_level(root, sid):
return ''
if lv:
_set_level(root, sid, lv)
over_t = tokens is not None and tokens >= BUDGET_TOKENS
over_n = ncalls is not None and ncalls >= BUDGET_TOOLS
grew = (tokens is not None and prev is not None and tokens - prev >= 40000) if not (over_t or over_n) else False
@@ -296,7 +366,8 @@ def budget_note(tokens, ncalls, prev=None):
which = ('上下文 %s token' % tokens) if over_t else (
'工具调用 %s 次' % ncalls) if over_n else ('**上一轮新增 %s token**' % (tokens - prev))
return (
'💰 【会话预算告警】本会话已到 **%s**(阈值 %s token / %s 次工具调用)。'
LV_PREFIX.get(lv, '') +
'💰 【会话预算告警】本会话已到 **%s**(一级阈值 %s token / %s 次工具调用)。'
'代价机制(2026-09-15 实测):对话历史是**追加式**的 —— 一次工具调用的输出(`function_call_result`)'
'**永久留在历史里、每轮全量重发**(某会话 553 轮 × 平均 42 万 = **2.33 亿 input**,output 仅 0.35%%)。'
'**处置:本轮收口后开新会话**(新会话起点约 5 万 ⇒ 每轮降到 1/12);'
@@ -404,13 +475,17 @@ def user_prompt_mode(payload):
tail = tail_lines(text, 1) if text else ''
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)
note = budget_note(toks, ncalls, prev, root0 or '.', str(payload.get('session_id') or ''))
gh = guard_health(root0 or '.')
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 or ph):
# ⚠️ 2026-09-15 用户选 C:**取消**「会话预算」注入 —— 当时的症结是"每轮都报、太吵"。
# ★ 2026-09-16 恢复**分级注入**:`budget_note` 已改为「分级 + 同会话跨级才报一次」(见其 docstring),
# 噪音症结已消解;且用户当日提出「上下文超 30 万要不要自动收口」⇒
# **AI 必须先知道水位才谈得上收口** ⇒ 恢复注入(一级静默无关、二级建议、三级强制收口)。
if mode == 'inject' and (hit or gh or ph or note):
ctx = ''
for seg in (CONTEXT if hit else '', note, gh, ph):
if seg: