163 lines
7.0 KiB
Python
163 lines
7.0 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""apply-reply-rules.py —— 把「回复排版与格式」核心块**配置到目标环境的规则文件**里
|
||
|
||
🔴 2026-10-06:本脚本**归位到 session-mechanism**(原先只在 agent-operating-rules 里,
|
||
而 03 号核心块却自称权威源却指着一个不在本包的注入器 ⇒ 权威声明分裂)。
|
||
权威源 = 本包 `references/03-回复排版-核心块.md`(**唯一**)。
|
||
|
||
它解决什么
|
||
──────────
|
||
用户 2026-10-02 令(逐字):
|
||
「**所有会话中**回复排版和格式要求和规则,也要**整合到会话技能中**,
|
||
使用时**配置到对应环境文件中**」
|
||
⇒ 三层分工(⛔ 别混):
|
||
· **技能**(本脚本所在包 `agent-operating-rules`)=**权威源**,跨工作区、跨机器。
|
||
· **环境文件**(`CODEBUDDY.md` / `AGENTS.md`)=该环境的**落地配置**,由本脚本生成。
|
||
· **每个会话**=由钩子 `reply-style-guard.py` **每轮**把核心块注入(优先用本环境的落地副本)。
|
||
|
||
为什么需要"注入"这一步(⛔ 不只是钩子就够)
|
||
────────────────────────────────────────────
|
||
钩子管的是"**每一轮**紧贴用户消息",而环境文件管的是"**这个环境自己的规则**":
|
||
· 换机 / 换工作区时,钩子可能还没装,但规则文件先被读到 ⇒ 规则不能只活在钩子里;
|
||
· 环境可能有**本地定稿**(如某工作区把骨架改过)⇒ 落地副本是它自己的那份,钩子优先读它。
|
||
|
||
安全设计
|
||
────────
|
||
1. **只动标记块**:只替换 `<!-- REPLY-CORE:BEGIN --> … <!-- REPLY-CORE:END -->` 之间;
|
||
块外(人工撰写的解释 / 别的规则)**一字不动**。
|
||
2. **默认不建块**:目标里没有标记块时**只报告**(rc=1),要建得显式加 `--init`
|
||
(追加到文末,不改既有内容)—— 避免与人工撰写的内容重复成两处。
|
||
3. **单向**:核心块内容由**技能**决定,⛔ 不许反过来把环境文件当源。
|
||
⇒ 本地确实要改措辞 ⇒ 改技能里那份,再对所有环境重跑本脚本。
|
||
4. **回读校验**:写完复读并断言块在、长度变化符合预期;异常 ⇒ 不写盘。
|
||
|
||
用法
|
||
────
|
||
<python> apply-reply-rules.py # 默认目标 = 当前目录的 CODEBUDDY.md(只报告)
|
||
<python> apply-reply-rules.py --ws <工作区> # 目标 = <工作区>/CODEBUDDY.md
|
||
<python> apply-reply-rules.py --target <文件> # 指定环境文件(AGENTS.md 等)
|
||
<python> apply-reply-rules.py --check # 只校验:块在不在、是否与技能一致
|
||
<python> apply-reply-rules.py --apply # 真写(需先抢锁 —— 环境文件通常属机制层)
|
||
<python> apply-reply-rules.py --apply --init # 目标没有标记块时,追加建块
|
||
退出码:0 = 一致/成功|1 = 有差异 / 需要人看|2 = 用法或环境异常
|
||
"""
|
||
import hashlib
|
||
import io
|
||
import os
|
||
import re
|
||
import shutil
|
||
import sys
|
||
import time
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
SKILL = os.path.dirname(HERE) # = <技能库>/agent-operating-rules
|
||
# 🔴 2026-10-06 本包内版本:权威源就在本包 ⇒ 默认读 `references/03-回复排版-核心块.md`
|
||
CORE = os.path.join(SKILL, 'references', '03-回复排版-核心块.md')
|
||
|
||
BEGIN = '<!-- REPLY-CORE:BEGIN'
|
||
END = '<!-- REPLY-CORE:END -->'
|
||
BANNER = ('<!-- BEGIN reply-core (generated by session-mechanism/scripts/'
|
||
'apply-reply-rules.py · 勿手改块内;要改口径改技能里那份再重跑本脚本) -->')
|
||
|
||
|
||
def rd(p, nl=''):
|
||
try:
|
||
return io.open(p, encoding='utf-8', newline=nl).read()
|
||
except OSError:
|
||
return ''
|
||
|
||
|
||
def md5s(s):
|
||
return hashlib.md5(s.encode('utf-8')).hexdigest()[:12]
|
||
|
||
|
||
def core_text():
|
||
t = rd(CORE)
|
||
if not t:
|
||
raise SystemExit('ERROR: 缺权威源 %s' % CORE)
|
||
i = t.find(BEGIN)
|
||
if i < 0:
|
||
raise SystemExit('ERROR: 权威源里没有 %s 标记' % BEGIN)
|
||
i = t.find('\n', i)
|
||
j = t.find(END, i + 1)
|
||
return t[i + 1:j].strip()
|
||
|
||
|
||
def block(body, bare=False):
|
||
if bare:
|
||
return BEGIN + ' (由 agent-operating-rules 技能生成) -->\n' + body + '\n' + END + '\n'
|
||
return (BEGIN + ' (由 agent-operating-rules 技能生成 · 与技能内那份同源) -->\n'
|
||
+ body + '\n' + END + '\n')
|
||
|
||
|
||
def cur_block(t):
|
||
i = t.find(BEGIN)
|
||
if i < 0:
|
||
return ''
|
||
i = t.find('\n', i)
|
||
j = t.find(END, i + 1)
|
||
if i < 0 or j < 0:
|
||
return ''
|
||
return t[i + 1:j].strip()
|
||
|
||
|
||
def main(argv):
|
||
tgt, apply_, init_, chk = '', '--apply' in argv, '--init' in argv, '--check' in argv
|
||
if '--target' in argv:
|
||
tgt = argv[argv.index('--target') + 1]
|
||
elif '--ws' in argv:
|
||
tgt = os.path.join(argv[argv.index('--ws') + 1], 'CODEBUDDY.md')
|
||
else:
|
||
tgt = os.path.join(os.getcwd(), 'CODEBUDDY.md')
|
||
tgt = os.path.normpath(tgt.replace('\\', '/'))
|
||
|
||
body = core_text()
|
||
t = rd(tgt)
|
||
print('权威源:%s(核心块 %d 字符 / md5=%s)' % (CORE, len(body), md5s(body)))
|
||
print('目标 :%s(%s)' % (tgt, '存在' if t else '**不存在**'))
|
||
if not t:
|
||
print('⛔ 目标文件不存在 ⇒ 停手(本脚本只在既有规则文件里注入,⛔ 不凭空造规则文件)')
|
||
return 2
|
||
|
||
have = cur_block(t)
|
||
if have:
|
||
same = have == body
|
||
print('块状态:已存在,与技能%s(正文 md5 %s vs %s)'
|
||
% ('**一致** ✅' if same else '**不一致** ⚠️', md5s(have), md5s(body)))
|
||
if chk:
|
||
return 0 if same else 1
|
||
if same and not apply_:
|
||
print('⇒ 无需改动')
|
||
return 0
|
||
else:
|
||
print('块状态:**没有标记块**')
|
||
if chk:
|
||
return 1
|
||
if not init_:
|
||
print('⇒ 默认**不建块**(避免与人工撰写内容重复成两处)。要建:加 --init(追加到文末)')
|
||
return 1
|
||
|
||
if not apply_:
|
||
print('⇒ 将写入(--apply 才真写):%s' % ('追加标记块' if not have else '替换块内内容'))
|
||
return 0
|
||
|
||
b = tgt + '.bak-replycore-' + time.strftime('%Y%m%d-%H%M%S')
|
||
shutil.copy2(tgt, b)
|
||
if have:
|
||
new = re.sub(re.escape(BEGIN) + r'.*?' + re.escape(END) + r'\n?',
|
||
block(body), t, flags=re.S)
|
||
else:
|
||
new = t.rstrip('\n') + '\n\n' + BANNER + '\n' + block(body)
|
||
io.open(tgt, 'w', encoding='utf-8', newline='').write(new)
|
||
back = rd(tgt)
|
||
assert cur_block(back) == body, '回读不符 ⇒ 已中止(备份 %s)' % b
|
||
print('✓ 已写入并回读校验通过(备份 %s)' % os.path.basename(b))
|
||
print(' ⚠️ 环境文件通常属机制层 ⇒ 本轮若没抢到锁,写入动作本身即违规;请用 --check 先看状态。')
|
||
return 0
|
||
|
||
|
||
if __name__ == '__main__':
|
||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||
raise SystemExit(main(sys.argv[1:]))
|