Files
dsh_shenxian/dsh-server-docs/07-scripts/reply-style-guard.py
T
admin 5ec99ce227
build / build-and-scan (push) Canceled after 0s
chore: 文档库全量重新入库(2026-10-20)
2026-10-20 15:05:55 +08:00

178 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)