From 8c40d7da6ae7dd33b19f0e44c2fd3f6d73d3ae1e Mon Sep 17 00:00:00 2001 From: Nyaser <61904116+ProjectNyaser@users.noreply.github.com> Date: Sun, 4 Oct 2026 19:14:14 +0800 Subject: [PATCH] =?UTF-8?q?fix(provider):=20=E5=9B=9E=E6=94=BE=E7=AD=BE?= =?UTF-8?q?=E5=90=8D=E6=8C=89=E5=9D=97=E6=95=B0=E5=AF=B9=E9=BD=90=EF=BC=8C?= =?UTF-8?q?=E9=95=BF=E5=BA=A6=E4=B8=8D=E7=AC=A6=E6=95=B4=E4=BD=93=E4=B8=A2?= =?UTF-8?q?=E5=BC=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit assistantToHanaContent 此前按下标取 replay.blocks 里的签名,不做长度校验。 落盘消息一旦出现 content 与 replay.blocks 不等(例如宿主中断提交路径以 interruptedBlocks() 裁掉 tool-call 却保留完整 replayState),签名会挂到别的块上。 现按宿主 assembler 同一口径:块数一致才采用签名,否则整体丢弃。 新增 provider-messages 三例覆盖长度不等与错位防护。 --- packages/provider/lib/messages.ts | 8 ++- tests/cordis/provider-messages.test.mjs | 73 +++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/packages/provider/lib/messages.ts b/packages/provider/lib/messages.ts index f97a102c..3885aab3 100644 --- a/packages/provider/lib/messages.ts +++ b/packages/provider/lib/messages.ts @@ -69,7 +69,13 @@ export function assistantToHanaContent(message) { message.source.replayState.kind === "hana" ? message.source.replayState : null; - const metaBlocks = replay && Array.isArray(replay.blocks) ? replay.blocks : null; + // 块数一致才采用签名:replay.blocks 与 content 按下标对齐,长度不等时按下标取值会把 + // 签名挂到别的块上。宿主 assembler 对自身剪枝已保证等长、否则整体丢弃 replayState + // (dsh-llm assembler.assembled()),但落盘消息仍可能不等(例如中断提交以 + // interruptedBlocks() 裁掉 tool-call 却保留完整 replayState),故此处按同一口径整体 + // 丢弃。只对齐长度,不逐块校验类型,与改动前一致。 + const metaBlocks = + replay && Array.isArray(replay.blocks) && replay.blocks.length === blocks.length ? replay.blocks : null; const out: any[] = []; for (let i = 0; i < blocks.length; i += 1) { const b = blocks[i]; diff --git a/tests/cordis/provider-messages.test.mjs b/tests/cordis/provider-messages.test.mjs index d0372882..f73645de 100644 --- a/tests/cordis/provider-messages.test.mjs +++ b/tests/cordis/provider-messages.test.mjs @@ -107,6 +107,79 @@ test("图片块无解析字节 → UNSUPPORTED_CONTENT", () => { assert.throws(() => toHanaMessages({ messages: msgs, images: null }), (e) => e.code === "UNSUPPORTED_CONTENT"); }); +test("replay.blocks 与 content 长度不等 → 整体丢弃签名,其余行为不变", () => { + // 宿主 assembler 在剪枝/压缩后会连带剪掉 replay 条目或 content 块;一旦两者长度 + // 不等,按下标取值就会把签名挂到错误的块上,这里按同样口径整体丢弃。 + const msgs = [ + { + role: "assistant", + content: [ + { type: "text", text: "保留" }, + { type: "reasoning", text: "思考" }, + { type: "tool-call", id: "call-1", name: "read", arguments: "{}" }, + ], + // 只有 2 个 meta,却有 3 个 content 块(content 多出块) + source: { + kind: "model", + provider: "deepseek", + model: "m", + replayState: { kind: "hana", version: 1, response: {}, blocks: [{ textSignature: "s0" }, { signature: "s1" }] }, + }, + }, + ]; + const { messages } = toHanaMessages({ messages: msgs, images: null }); + const content = messages[0].content; + // 内容与顺序不变 + assert.deepEqual(content.map((c) => c.type), ["text", "reasoning", "toolCall"]); + assert.equal(content[0].text, "保留"); + assert.equal(content[1].reasoning, "思考"); + assert.deepEqual(content[2].arguments, {}); + // 没有任何签名挂上去(尤其不能错位) + assert.equal(content[0].textSignature, undefined); + assert.equal(content[1].signature, undefined); + assert.equal(content[2].thoughtSignature, undefined); +}); + +test("replay.blocks 比 content 短:meta 不被挂到别的块上(错位防护)", () => { + // 压缩剪掉了 content 里的尾块、replay 里也少了一条;剩下的那条 meta 属于谁已不可知。 + // 按下标取值会把它挂到 index 0 的 tool-call 上,这里必须整体丢弃。 + const msgs = [ + { + role: "assistant", + content: [ + { type: "tool-call", id: "c1", name: "read", arguments: "{}" }, + { type: "text", text: "尾块" }, + ], + source: { + kind: "model", + provider: "p", + model: "m", + replayState: { kind: "hana", version: 1, response: {}, blocks: [{ thoughtSignature: "sig-of-removed-block" }] }, + }, + }, + ]; + const { messages } = toHanaMessages({ messages: msgs, images: null }); + const content = messages[0].content; + assert.equal(content[0].type, "toolCall"); + assert.equal(content[0].thoughtSignature, undefined, "长度不等时不得采用任何签名"); + assert.equal(content[1].textSignature, undefined); +}); + +test("replay 信封非 hana / blocks 非数组 → 不采用签名(原有行为)", () => { + const base = { role: "assistant", content: [{ type: "text", text: "t" }] }; + for (const replayState of [ + { kind: "pi-ai", blocks: [{ textSignature: "x" }] }, + { kind: "hana", blocks: "not-an-array" }, + { kind: "hana" }, + ]) { + const { messages } = toHanaMessages({ + messages: [{ ...base, source: { kind: "model", provider: "p", model: "m", replayState } }], + images: null, + }); + assert.equal(messages[0].content[0].textSignature, undefined, JSON.stringify(replayState)); + } +}); + test("助手 arguments 非法 JSON 回落 {};collectToolNames/isToolResultMessage", () => { const msgs = [ { role: "assistant", content: [{ type: "tool-call", id: "c2", name: "x", arguments: "{bad" }], source: { kind: "model", provider: "p", model: "m" } },