This commit is contained in:
commit
5ec99ce227
668 files changed
+164448
No files matched your search
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""reply-style-guard.py —— 「回复排版与格式」的 UserPromptSubmit **全机**强制注入钩子
|
||||
|
||||
为什么需要它
|
||||
────────────
|
||||
2026-10-02 用户令(逐字):
|
||||
· 「所有会话中回复排版和格式要求和规则,也要整合到会话技能中,使用时配置到对应环境文件中」
|
||||
· 前一轮:「这个规则改为强遵循 固化在必循加入会话的地方」
|
||||
起因:同一件事 **连点两次**(「我发现你又忘记如何回复执行结果了」「我确认你是又把执行后的
|
||||
内容回复排版和格式给忘记了」)—— 根因**不是规则丢了**,是**规则只在会话开始时被读一次**:
|
||||
长会话里会被稀释,而"回复长什么样"这件事**每一轮都要判一次**。
|
||||
|
||||
机制对照(同族,已存在)
|
||||
────────────────────────
|
||||
· `stop-dialog-guard.py` —— 治「收尾时的征询句」
|
||||
· `skill-load-guard.py` —— 治「用户点名了方法却没加载技能」
|
||||
本钩子治第三条面:**「每轮回复的形态」**。三者同挂 `UserPromptSubmit`,互补。
|
||||
|
||||
🔴 三条设计判据(本需求的关键,⛔ 都别改)
|
||||
──────────────────────────────────────────
|
||||
1. **真相源在技能里**(不在本文件、也不在某个工作区):
|
||||
`<技能库>/agent-operating-rules/references/回复排版-核心块.md`
|
||||
⇒ 技能是**跨工作区**的 ⇒ 一次改动,**所有会话**都跟着变。
|
||||
⛔ **绝不在本文件里写死规则文本** —— 写死就变成第二真相源,两边必然漂。
|
||||
2. **每轮都注入,⛔ 不设冷却**:这条规矩的价值就在"紧贴用户消息、每轮重述"。
|
||||
设冷却(如 300s)= 让它按会话衰减回原样,那就白做了。
|
||||
3. **落环境文件 = 可选项不是必需**:某环境若已用 `apply-reply-rules.py` 把核心块落到自己的
|
||||
规则文件(`CODEBUDDY.md` / `AGENTS.md`)里,**优先用那份**(可能被本地化过);
|
||||
没有 ⇒ 回落到技能里那份。两级回退,⛔ 哪一级都读不到就**静默零输出**(不影响任何人)。
|
||||
|
||||
安装(`settings.json` 的 `hooks.UserPromptSubmit`,`timeout` 建议 10)
|
||||
⛔ 绝不要给本族脚本加 `-E`(会屏蔽 PYTHONUTF8 ⇒ cp936 ⇒ 含 `⛔` 的 payload 静默炸)。
|
||||
急停:env `DSH_REPLY_GUARD_OFF=1` 或 `<工作区>/.workbuddy/reply-guard.disabled`。
|
||||
退出码:始终 0;决策通过 stdout 的 JSON 表达。
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
CORE_REL = os.path.join('agent-operating-rules', 'references', '回复排版-核心块.md')
|
||||
BEGIN = '<!-- REPLY-CORE:BEGIN'
|
||||
END = '<!-- REPLY-CORE:END -->'
|
||||
MAX_CHARS = 1400
|
||||
LOG_REL = os.path.join('.workbuddy', 'reply-style-guard.log')
|
||||
DISABLE_REL = os.path.join('.workbuddy', 'reply-guard.disabled')
|
||||
|
||||
|
||||
def _read_stdin():
|
||||
try:
|
||||
raw = sys.stdin.buffer.read()
|
||||
except Exception:
|
||||
return {}
|
||||
if not raw:
|
||||
return {}
|
||||
for enc in ('utf-8', 'utf-8-sig', 'gbk'):
|
||||
try:
|
||||
return json.loads(raw.decode(enc))
|
||||
except Exception:
|
||||
continue
|
||||
return {}
|
||||
|
||||
|
||||
def _emit(obj):
|
||||
try:
|
||||
sys.stdout.buffer.write(json.dumps(obj, ensure_ascii=False).encode('utf-8'))
|
||||
sys.stdout.buffer.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _norm(p):
|
||||
"""MSYS 风格 `/e/foo` → `E:/foo`(Windows 原生 python 不认前者)。"""
|
||||
s = str(p or '')
|
||||
if len(s) > 2 and s[0] == '/' and s[2:3] == '/':
|
||||
return s[1].upper() + ':' + s[2:]
|
||||
return s
|
||||
|
||||
|
||||
def _skills_root():
|
||||
"""技能库根:env `DSH_SKILLS_ROOT` 优先;否则**沿 __file__ 上溯**找含
|
||||
`agent-operating-rules` 的那一层(⛔ 不写死盘符 / 用户名 ⇒ 换机可用)。"""
|
||||
v = os.environ.get('DSH_SKILLS_ROOT')
|
||||
if v:
|
||||
return _norm(v)
|
||||
d = os.path.dirname(os.path.abspath(__file__))
|
||||
for _ in range(6):
|
||||
d = os.path.dirname(d)
|
||||
if os.path.isdir(os.path.join(d, 'agent-operating-rules')):
|
||||
return d
|
||||
# 兜底:本机默认位置(⚠️ 只在上面全失败时用)
|
||||
return os.path.expanduser('~/.workbuddy/skills')
|
||||
|
||||
|
||||
def _extract(text):
|
||||
i = text.find(BEGIN)
|
||||
if i < 0:
|
||||
return ''
|
||||
i = text.find('\n', i)
|
||||
j = text.find(END, i + 1)
|
||||
if i < 0 or j < 0:
|
||||
return ''
|
||||
body = text[i:j].strip()
|
||||
if len(body) > MAX_CHARS:
|
||||
body = body[:MAX_CHARS].rstrip() + '\n…(超长已截断,全文见技能 `agent-operating-rules` §2)'
|
||||
return body
|
||||
|
||||
|
||||
def _read(p):
|
||||
try:
|
||||
with io.open(p, encoding='utf-8', newline='') as f:
|
||||
return f.read()
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
|
||||
def _core(workdir):
|
||||
"""两级回退:① 本环境规则文件里的落地副本 ② 技能里的权威件。"""
|
||||
for p in (os.path.join(workdir, 'CODEBUDDY.md'),
|
||||
os.path.join(workdir, '.codebuddy', 'CODEBUDDY.md'),
|
||||
os.path.join(workdir, 'AGENTS.md')):
|
||||
c = _extract(_read(p))
|
||||
if c:
|
||||
return c, p
|
||||
p = os.environ.get('DSH_REPLY_CORE') or os.path.join(_skills_root(), CORE_REL)
|
||||
return _extract(_read(p)), p
|
||||
|
||||
|
||||
def _log(workdir, line):
|
||||
try:
|
||||
with io.open(os.path.join(workdir, LOG_REL), 'a', encoding='utf-8', newline='\n') as f:
|
||||
f.write('%s %s\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), line))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
if os.environ.get('DSH_REPLY_GUARD_OFF') == '1':
|
||||
return
|
||||
obj = _read_stdin()
|
||||
if not obj:
|
||||
return
|
||||
workdir = ''
|
||||
for k in ('cwd', 'project_dir', 'workspace', 'projectDir'):
|
||||
if isinstance(obj.get(k), str) and obj[k]:
|
||||
workdir = _norm(obj[k])
|
||||
break
|
||||
if not workdir or not os.path.isdir(workdir):
|
||||
return
|
||||
if os.path.exists(os.path.join(workdir, DISABLE_REL)):
|
||||
return
|
||||
|
||||
sid = str(obj.get('session_id') or obj.get('sessionId') or 'unknown')
|
||||
core, src = _core(workdir)
|
||||
# ★ 入口即留痕:命中与否都要能回答"它到底有没有被宿主调用"
|
||||
_log(workdir, 'entry|sid=%s|core=%d 字符|源=%s' % (sid[:8], len(core), src))
|
||||
if not core:
|
||||
return
|
||||
|
||||
ctx = ('【回复排版闸门 · 强遵循】本机已把「回复形态」定为**强遵循**规则(源:%s)\n'
|
||||
% os.path.basename(src).replace('.md', '') + core +
|
||||
'\n(以上每条都是硬约束;发出前过一遍。⛔ 表格/长散文/碎标签堆叠三者一律不许出现。)')
|
||||
_emit({'hookSpecificOutput': {
|
||||
'hookEventName': 'UserPromptSubmit',
|
||||
'additionalContext': ctx,
|
||||
}})
|
||||
_log(workdir, 'HIT sid=%s|%d 字节' % (sid[:8], len(ctx.encode('utf-8'))))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except Exception:
|
||||
pass # fail-open:钩子绝不因自己出错而挡人
|
||||
sys.exit(0)
|
||||
Reference in new issue
Block a user