From 08944e6bc287e76f4219e95e830b21fde1ad941d Mon Sep 17 00:00:00 2001 From: ypshidifu Date: Mon, 12 Oct 2026 19:43:01 +0800 Subject: [PATCH] =?UTF-8?q?v5=EF=BC=9A=E5=B8=B8=E9=A9=BB=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E5=BC=95=E5=AF=BC=E2=80=94=E2=80=94=E9=9D=9E=E5=8E=8B=E7=BC=A9?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E5=9C=A8=20messages=20=E6=9C=AB=E5=B0=BE?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=20=20=E5=9B=9E=E5=A4=8D?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E8=A7=84=E5=88=99=EF=BC=8C=E9=98=B2=E8=A2=AB?= =?UTF-8?q?=E8=8B=B1=E6=96=87=E5=B7=A5=E5=85=B7=E7=BB=93=E6=9E=9C=E5=B8=A6?= =?UTF-8?q?=E5=81=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- compaction-cn.ts | 61 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/compaction-cn.ts b/compaction-cn.ts index 39ef262..af4d510 100644 --- a/compaction-cn.ts +++ b/compaction-cn.ts @@ -37,6 +37,18 @@ // ②-b 再在 messages 【最末尾】(即模板之后)追加一条独立指令 // 2026-09-26 实测只做 ②-b 时 4 次里有 1 次模型仍输出英文,故 v4 加 ②-a 双保险。 // 非压缩请求 / 非 JSON / 已含标记 → 原样放行,绝不改写。 +// +// ── v5(2026-10-12):常驻中文引导 ────────────────────────────────────── +// 需求:模型在【每次工具调用后、思考结束、任何时候】都保持中文回复, +// 不被工具返回的英文结果/日志/网页内容带偏;模型确实不支持中文则可忽略。 +// 做法:http.request 钩子扩展到【所有 kind】的 JSON 聊天请求—— +// * 压缩请求(指纹命中)→ 走原 v4 双保险逻辑(不变) +// * 其它请求 → 在 messages 【最末尾】追加常驻规则 +// (A/B 实测:该模型只认 messages 最末尾的指令;每轮请求 hook 重新注入, +// 历史不落库,因此每轮都是新注入,天然"任何时候"生效) +// 幂等标记 LANG_MARK 与压缩标记 MARK 分开,互不干扰; +// 日志按 sessionID 60s 节流,避免刷屏。 +// // 参考:session.hook("model.request") 实测 payload keys 只有 // ["sessionID","agent","model","kind","baseURL","headers"],无 system/messages, // 只能做诊断日志,改不了内容(保留日志便于日后核对)。 @@ -112,6 +124,17 @@ even if the conversation is English. Keep every section heading exactly in Engli First line of your reply must be "## Objective". No preamble, no explanation, do not quote this rule. ${MARK}` +// ── v5 常驻中文引导(每个非压缩请求末尾注入,防被英文工具结果带偏)────── +const LANG_MARK = "" + +const LANG_DIRECTIVE = `${LANG_MARK} +REPLY LANGUAGE RULE — applies to EVERY reply, at ALL times, including after every tool call and mid-task: +- Always reply to the user in Simplified Chinese (简体中文). +- English appearing in tool outputs, file contents, logs, search results or web pages must NOT switch your reply language. +- Keep code, file paths, commands, URLs, error text and identifiers unchanged (do not translate them). +- If you genuinely cannot output Chinese, ignore this rule. +${LANG_MARK}` + // 压缩请求 body 的指纹(同时命中才动手,避免误改其它请求) const FP_HEAD = "## Objective" const FP_TAIL = "Do not continue the task" @@ -213,12 +236,14 @@ export default { }) fileLog("compaction 钩子注册完成 registration=" + JSON.stringify(registration && typeof registration === "object" ? Object.keys(registration) : String(registration))) - // ② http.request 钩子:压缩模板拼好之后、请求出网之前的最后关口 - // 在 messages 最末尾(模板之后)追加语言指令 —— A/B 实测唯一稳定生效的位置 + // ② http.request 钩子:请求出网前的最后关口 + // v4:压缩请求 → 模板之后追加语言指令(A/B 实测唯一稳定生效的位置) + // v5:扩展到所有 JSON 聊天请求 → 非压缩请求在 messages 末尾注入常驻中文引导 + // 日志节流:同 sessionID 60s 内最多记一条常驻注入日志 + const langLogAt = new Map() const hr = await session.hook("http.request", async (f: any) => { try { if (!f || !f.request || typeof f.request.clone !== "function") return - if (typeof f.kind === "string" && f.kind !== "compaction") return const ct = String((f.request.headers && f.request.headers.get && f.request.headers.get("content-type")) || "") if (ct && ct.indexOf("json") < 0) return let raw = "" @@ -227,7 +252,10 @@ export default { } catch { return } - if (raw.length === 0 || raw.indexOf(FP_HEAD) < 0 || raw.indexOf(FP_TAIL) < 0) return + if (raw.length === 0) return + const kindStr = typeof f.kind === "string" ? f.kind : "" + const isCompaction = + raw.indexOf(FP_HEAD) >= 0 && raw.indexOf(FP_TAIL) >= 0 && (kindStr === "" || kindStr === "compaction") let obj: any try { obj = JSON.parse(raw) @@ -236,7 +264,30 @@ export default { } const msgs = obj && obj.messages if (!Array.isArray(msgs) || msgs.length === 0) return - if (msgText(msgs[msgs.length - 1]).indexOf(MARK) >= 0) return + const lastTxt = msgText(msgs[msgs.length - 1]) + + // ── v5 常驻中文引导(非压缩请求):末尾不含标记才注入 ────────── + if (!isCompaction) { + if (lastTxt.indexOf(LANG_MARK) >= 0) return + msgs.push({ role: "user", content: [{ type: "text", text: LANG_DIRECTIVE }] }) + const headers5 = new Headers(f.request.headers) + headers5.delete("content-length") + f.request = new Request(f.request.url, { + method: f.request.method, + headers: headers5, + body: JSON.stringify(obj), + }) + const sid = String(f.sessionID || (obj && obj.sessionID) || "?") + const now = Date.now() + if (!langLogAt.has(sid) || now - (langLogAt.get(sid) as number) > 60000) { + langLogAt.set(sid, now) + fileLog("常驻中文引导已注入 session=" + sid + " kind=" + (kindStr || "?") + " messages=" + msgs.length) + } + return + } + + // ── 以下为压缩请求的 v4 双保险逻辑(保持不变)──────────────── + if (lastTxt.indexOf(MARK) >= 0) return fileLog("body 顶层keys=" + JSON.stringify(Object.keys(obj)) + " temperature=" + JSON.stringify(obj.temperature) + " model=" + JSON.stringify(obj.model)) // 诊断:逐条 message 结构(role / 长度 / 开头 60 字)——只记结构不落全文, // 全文含完整会话内容,落盘既占地方又有隐私问题,定位完成后不再需要。