新增compaction-cn插件:向compaction钩子system注入中文输出指令,压缩摘要内容简体中文、小节标题原样英文(标题译了会过不了ww校验致压缩失败)
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
# compaction-cn — 让 OC 对话压缩摘要输出简体中文
|
||||
|
||||
OpenCode 原生压缩(compaction)提示词硬编码在 `opencode.exe` 二进制里,摘要默认**纯英文**;
|
||||
后续任务里模型很容易被前面那段英文摘要带偏。本插件在压缩请求发出前往 `system` 追加
|
||||
中文输出指令,让摘要**内容用简体中文、标题原样保留英文**(标题不能译,原因见下)。
|
||||
|
||||
分支名:`compaction-cn` | 文件:`compaction-cn.ts`
|
||||
|
||||
---
|
||||
|
||||
## 一、安装
|
||||
|
||||
源码真源 / 本机生效 / WSL 生效三份同步(MD5 必须一致):
|
||||
|
||||
```
|
||||
源码真源 : D:\桌面\源码\oc_session\compaction-cn\compaction-cn.ts
|
||||
本机生效 : C:\Users\ypshidifu\.config\opencode\plugins\compaction-cn.ts
|
||||
WSL 生效 : /root/.config/opencode/plugins/compaction-cn.ts
|
||||
```
|
||||
|
||||
插件在 **session 初始化时**加载(不是进程启动时),装好后**新建一个会话**即可生效,
|
||||
已开会话不会热加载。
|
||||
|
||||
---
|
||||
|
||||
## 二、为什么只能这样干预(2026-09-25 实测定稿)
|
||||
|
||||
| 候选干预点 | 结论 |
|
||||
| --- | --- |
|
||||
| V1 hooks(`default` 对象上的字段,含官方文档的 `experimental.session.compacting`) | ❌ 2.0.15 V2 运行时**零消费者**,探针 24 次 setup 全成功、`A.*` 日志全空;且 named export 返回 `hooks` 对象会被当 cleanup 调用 → `TypeError: Ae is not a function` 整批插件崩 |
|
||||
| `opencode.config.compaction` | ❌ schema 只有 `{auto, buffer, keep.tokens}`,没有 prompt 覆盖字段 |
|
||||
| `ctx.session.hook("model.request", cb)` | ❌ payload 是 `[sessionID, agent, model, kind, baseURL, headers]`,**没有 messages**,只能识别(`kind="compaction"`)不能改写内容 |
|
||||
| `ctx.session.hook("compaction", cb)` | ✅ **唯一可行**。payload = `[sessionID, model, system, messages, options, agent, tools]`,`system` 会带进最终压缩请求 |
|
||||
|
||||
压缩请求时序(二进制逆向):
|
||||
|
||||
```
|
||||
compaction hook(prep) → [...messages, 模板 Zt.user(z$(...))] → model.request(kind="compaction")
|
||||
```
|
||||
|
||||
模板是在钩子**之后**才追加进 messages 的,所以钩子改不到模板本身,只能改 `system`。
|
||||
|
||||
### 关键约束:小节标题绝对不能翻译
|
||||
|
||||
输出校验 `ww = (e) => e.split("\n").some(a => U$.includes(a.trim()))`,
|
||||
`U$` 是模板按行过滤出的英文标题行。摘要里必须**整行命中**这些英文标题,否则**压缩直接失败**:
|
||||
|
||||
```
|
||||
## Objective / ## Requirements / ## Decisions / ## Work State /
|
||||
### Completed / ### Active / ### Blocked /
|
||||
## Next Move / ## Relevant Files / ## Important Context
|
||||
```
|
||||
|
||||
所以本插件只加规则「**内容中文、标题原样英文**」,外加路径/代码/URL/报错原文不翻译。
|
||||
|
||||
---
|
||||
|
||||
## 三、工作方式
|
||||
|
||||
`setup(ctx)` 里向 `ctx.session.hook("compaction", cb)` 注册一个回调,往 `g.system`
|
||||
数组 push 一段带 `<compaction-language>` 标记的文本。带标记是为了**幂等判重**——
|
||||
多 location、多次 reload 可能重复注册钩子,已注入过就不再加。
|
||||
|
||||
日志(不进 stdout,避免污染 TUI):
|
||||
|
||||
```
|
||||
C:\Users\ypshidifu\.config\opencode\compaction-cn.log # Windows
|
||||
/root/.config/opencode/compaction-cn.log # WSL
|
||||
```
|
||||
|
||||
正常会看到:
|
||||
|
||||
```
|
||||
compaction 钩子注册完成 registration=[...]
|
||||
已注入中文输出指令 sessionID="ses_xxx" system段=5 messages=N
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、V2 插件契约(与 ai-mcp 同款,勿破坏)
|
||||
|
||||
- `default` 导出 **必须**给 `{ id, setup }`(缺了报 `Plugin must export a default definition...`)
|
||||
- **`setup` 必须返回 void**,绝不抛异常(返回 `hooks`/truthy 对象 → `Ae is not a function` 整批崩)
|
||||
- named export 用逗号表达式安全形态:`export const X: any = async (_ctx: any) => (await ({} as any), undefined as any)`
|
||||
- 本插件所有异常都在内部吞掉并落日志,绝不影响同批其它插件
|
||||
|
||||
---
|
||||
|
||||
## 五、效果对比
|
||||
|
||||
压缩前(原生英文输出):
|
||||
|
||||
```
|
||||
## Objective
|
||||
- Respond to the user with "收到".
|
||||
```
|
||||
|
||||
装插件后(e2e 实测,`has_en_headers=True`、英文标题原样保留、正文全中文):
|
||||
|
||||
```
|
||||
## Objective
|
||||
- 按用户要求回复"收到"。
|
||||
## Requirements
|
||||
- 回复内容必须为"收到"。
|
||||
...
|
||||
```
|
||||
@@ -0,0 +1,122 @@
|
||||
// compaction-cn —— OC 对话压缩摘要中文化 server 插件
|
||||
//
|
||||
// 背景与动机:
|
||||
// OC 原生压缩(SessionCompaction)提示词硬编码在 opencode.exe 二进制内,
|
||||
// opencode.config.compaction 配置 schema 只有 {auto, buffer, keep.tokens},无 prompt 覆盖字段;
|
||||
// V1 插件 hooks(含官方文档的 experimental.session.compacting)在 OC 2.0.15 V2 运行时
|
||||
// 【没有任何消费者】(实测:default 对象上的 V1 hook 字段零调用,且 named export 返回
|
||||
// hooks 对象会被当 cleanup 调用 → TypeError: Ae is not a function 整批崩)。
|
||||
// 于是压缩摘要默认产出纯英文,后续任务容易被前面的英文摘要带偏。
|
||||
//
|
||||
// 干预点(2026-09-25 实测定稿):
|
||||
// ctx.session.hook("compaction", cb) —— 对应 SessionCompaction.prepare 阶段,
|
||||
// payload = [sessionID, model, system, messages, options, agent, tools],含 system 与 messages。
|
||||
// 时序:compaction hook(prep) → 追加压缩模板 Zt.user(z$(...)) → model.request(kind="compaction")
|
||||
// ⇒ 模板在钩子【之后】才拼进 messages,钩子改不到模板本身;
|
||||
// model.request 虽在模板之后但 payload 无 messages,只能识别不能改写。
|
||||
// ⇒ 唯一可行干预:向 g.system 追加中文输出指令(system 会带进最终压缩请求)。
|
||||
//
|
||||
// 关键约束(不能违反):
|
||||
// 输出校验 ww = (e) => e.split("\n").some(a => U$.includes(a.trim()))
|
||||
// 其中 U$ = 模板按行过滤出的英文标题(## Objective / ## Requirements / ## Decisions /
|
||||
// ## Work State / ### Completed / ### Active / ### Blocked / ## Next Move /
|
||||
// ## Relevant Files / ## Important Context)。
|
||||
// ⇒ 摘要回复必须含【整行英文标题】,翻译标题 → 校验失败 → 压缩失败。
|
||||
// ⇒ 故只能要求「内容用中文、标题原样英文」,绝不能翻译标题。
|
||||
//
|
||||
// 与 ai-mcp 同款的 V2 插件契约(勿破坏):
|
||||
// - named export 必须是逗号表达式安全形态(返回 hooks 对象会崩)
|
||||
// - default 导出 { id, setup },setup 必须返回 void、绝不抛
|
||||
//
|
||||
// 部署(三份同步 + node --check):
|
||||
// 源码真源 : D:\桌面\源码\oc_session\compaction-cn\compaction-cn.ts
|
||||
// 本机生效 : C:\Users\ypshidifu\.config\opencode\plugins\compaction-cn.ts
|
||||
// WSL 生效 : /root/.config/opencode/plugins/compaction-cn.ts
|
||||
|
||||
import { appendFileSync, existsSync, mkdirSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
|
||||
const LOG_DIR = join(homedir(), ".config", "opencode")
|
||||
const LOG_FILE = join(LOG_DIR, "compaction-cn.log")
|
||||
|
||||
// 标记串:幂等判重用(多 location / 多次 reload 可能重复注册钩子)
|
||||
const MARK = "<compaction-language>"
|
||||
|
||||
const CN_RULE = `${MARK}
|
||||
以下为本次会话压缩(compaction)摘要的输出语言要求,优先级高于模板中的其他说明:
|
||||
|
||||
1. 除第 2 条外,摘要的全部内容必须使用简体中文撰写,包括各小节的条目正文、说明与结论。
|
||||
2. 所有小节标题必须原样保留英文、不得翻译或改动大小写,包括:
|
||||
## Objective / ## Requirements / ## Decisions / ## Work State /
|
||||
### Completed / ### Active / ### Blocked /
|
||||
## Next Move / ## Relevant Files / ## Important Context
|
||||
这些标题行是压缩结果的格式校验依据,翻译或改写会导致本次压缩失败。
|
||||
3. 文件路径、代码、命令、URL、报错原文、标识符以及用户原话引用保持原样,不要翻译。
|
||||
4. 严格遵守压缩模板中的全部格式规则(单行条目、不超过 15 个相关文件、不要提及压缩过程等)。
|
||||
5. 若本次为更新模式(已有 checkpoint 要求重写),各节内容同样按上述语言要求用简体中文重写。
|
||||
${MARK}`
|
||||
|
||||
function fileLog(msg: string) {
|
||||
try {
|
||||
if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true })
|
||||
appendFileSync(LOG_FILE, `[${new Date().toISOString()}] ${msg}\n`)
|
||||
} catch {
|
||||
// 日志失败绝不影响主流程
|
||||
}
|
||||
}
|
||||
|
||||
// system 段可能是 {type:"text", text} 或纯字符串,取其文本
|
||||
function systemText(s: any): string {
|
||||
if (typeof s === "string") return s
|
||||
if (s && typeof s === "object") return typeof s.text === "string" ? s.text : ""
|
||||
return ""
|
||||
}
|
||||
|
||||
// 命名导出:ai-mcp 实验矩阵 ② 的逗号表达式安全形态(返回 hooks 对象会触发 Ae is not a function)
|
||||
export const CompactionCn: any = async (_ctx: any) => (await ({} as any), undefined as any)
|
||||
|
||||
export default {
|
||||
id: "compaction-cn",
|
||||
async setup(ctx: any): Promise<void> {
|
||||
try {
|
||||
const session = ctx && ctx.session
|
||||
if (!session || typeof session.hook !== "function") {
|
||||
fileLog("session.hook 不可用(keys=" + JSON.stringify(Object.keys(session || {})) + "),未注册")
|
||||
return
|
||||
}
|
||||
|
||||
const registration = await session.hook("compaction", (g: any) => {
|
||||
try {
|
||||
if (!g) return
|
||||
const sys = g.system
|
||||
if (!Array.isArray(sys)) {
|
||||
fileLog("compaction payload 无 system 数组(type=" + typeof sys + "),跳过")
|
||||
return
|
||||
}
|
||||
// 幂等:已注入过就不再加(防多 location / 重复注册叠加)
|
||||
if (sys.some((s: any) => systemText(s).indexOf(MARK) >= 0)) {
|
||||
return
|
||||
}
|
||||
sys.push({ type: "text", text: CN_RULE })
|
||||
fileLog(
|
||||
"已注入中文输出指令 sessionID=" +
|
||||
JSON.stringify(g.sessionID) +
|
||||
" system段=" +
|
||||
sys.length +
|
||||
" messages=" +
|
||||
(Array.isArray(g.messages) ? g.messages.length : "?"),
|
||||
)
|
||||
} catch (e: any) {
|
||||
fileLog("compaction 钩子异常(已吞掉): " + (e && e.message ? e.message : String(e)))
|
||||
}
|
||||
})
|
||||
|
||||
fileLog("compaction 钩子注册完成 registration=" + JSON.stringify(registration && typeof registration === "object" ? Object.keys(registration) : String(registration)))
|
||||
} catch (e: any) {
|
||||
fileLog("setup 异常(已吞掉,不影响其它插件): " + (e && e.message ? e.message : String(e)))
|
||||
}
|
||||
// setup 必须返回 void:返回 hooks 对象会触发 Ae is not a function 整批崩溃
|
||||
return
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user