预设兼容:按真实抓包对齐装配与包装层,补上声明式工具与预设正则。

- 装配只认 prompt_order.enabled;squash_system_messages 只合并相邻 system(新增 system 档,连接符按酒馆用单换行)
- 包装层:new_chat_prompt、示例对话(new_example_chat_prompt + <START> 切段 + name)、角色卡三块与 personality_format/scenario_format、{{user}}/{{char}} 取值、任务契约并进预设条目
- 工具协议:按 ToolBindings / 伪工具协议数据声明真 tools(presetToolPolicy=preset),工具描述用到的条目不再重复作为消息发出
- 正则:promptOnly 作用于对话历史(含 depth 过滤)、写入时脚本改写回复;{{//}} 注释按配平扫描
- tools/st-request-diff:真假上游抓包对比工具、三份预设的机制结论与实测报告
This commit is contained in:
2026-10-23 00:13:08 +08:00
parent ed09e5c831
commit 9dc927282f
94 changed files with 11558 additions and 413 deletions
+223 -2
View File
@@ -8,18 +8,25 @@ import {
assembleDirectChatMessages,
buildChatHistory,
buildDirectChatTaskMessages,
abortReroll,
beginReroll,
buildPriorForWindow,
createConversation,
createShell,
DEFAULT_DIRECT_CHAT_SYSTEM,
duplicateConversation,
EMPTY_SHELL_SLOTS,
getConversation,
mergeTurnReplies,
switchTurnVariant,
writeRerollReplies,
getShell,
listConversations,
listShells,
promoteTemporary,
resolveDirectChatPreset,
runDirectChatTurn,
type DirectChatTurn,
} from "../src/direct-chat/index.js";
function samplePreset() {
@@ -83,13 +90,13 @@ describe("direct-chat history assembly", () => {
expect(contents.some((c) => String(c).includes("契约"))).toBe(true);
});
it("uses default system prompt when slot empty", () => {
it("emits no system message when the slot is empty (酒馆没有隐式系统提示)", () => {
const task = buildDirectChatTaskMessages({
message: "hi",
slots: EMPTY_SHELL_SLOTS,
priorMessages: [],
});
expect(task[0]?.content).toBe(DEFAULT_DIRECT_CHAT_SYSTEM);
expect(task.some((m) => m.role === "system")).toBe(false);
});
});
@@ -147,6 +154,40 @@ describe("direct-chat store", () => {
expect(conversation.name).toBe("升格会话");
expect(conversation.messages).toHaveLength(2);
});
it("duplicates a conversation under the same shell", () => {
const shell = createShell({ name: "壳" });
const conv = createConversation({
shellId: shell.id,
name: "原会话 副本",
windows: [
{ id: "w1", profileId: "p1", profileName: "甲", model: "m-a" },
],
turns: [
{
id: "t1",
user: { content: "问", createdAt: "1" },
replies: [{ windowId: "w1", content: "答" }],
selectedWindowId: "w1",
createdAt: "1",
},
],
});
const copy = duplicateConversation(conv.id);
expect(copy.id).not.toBe(conv.id);
expect(copy.shellId).toBe(shell.id);
expect(copy.name).toBe("原会话 副本");
expect(copy.windows).toHaveLength(1);
expect(copy.windows[0]?.id).not.toBe("w1");
expect(copy.windows[0]?.profileId).toBe("p1");
expect(copy.turns[0]?.user.content).toBe("问");
expect(copy.turns[0]?.replies[0]?.windowId).toBe(copy.windows[0]?.id);
expect(copy.turns[0]?.selectedWindowId).toBe(copy.windows[0]?.id);
const original = getConversation(conv.id);
expect(original?.windows[0]?.id).toBe("w1");
expect(original?.turns).toHaveLength(1);
expect(listConversations(shell.id)).toHaveLength(2);
});
});
describe("direct-chat turn gating", () => {
@@ -230,3 +271,183 @@ describe("buildPriorForWindow", () => {
]);
});
});
function sampleTurn(
id: string,
content: string,
reply: string,
): {
id: string;
user: { content: string; createdAt: string };
replies: Array<{ windowId: string; content: string }>;
createdAt: string;
} {
return {
id,
user: { content, createdAt: "2026-01-01T00:00:00.000Z" },
replies: [{ windowId: "w1", content: reply }],
createdAt: "2026-01-01T00:00:00.000Z",
};
}
describe("direct-chat turn variants", () => {
const turns = () => [sampleTurn("t1", "问1", "答1"), sampleTurn("t2", "问2", "答2")];
it("edits the original line, rerolls, and keeps later turns on the previous version", () => {
const begun = beginReroll(turns(), "t1", "问1改");
expect(begun).not.toBeNull();
expect(begun!.turns.map((t) => t.id)).toEqual(["t1"]);
expect(begun!.turns[0]?.user.content).toBe("问1改");
expect(begun!.turns[0]?.replies).toEqual([]);
expect(begun!.turns[0]?.variantIndex).toBe(1);
expect(begun!.turns[0]?.variants?.[0]?.user.content).toBe("问1");
expect(begun!.turns[0]?.variants?.[0]?.replies[0]?.content).toBe("答1");
expect(begun!.turns[0]?.variants?.[0]?.after[0]?.user.content).toBe("问2");
expect(begun!.prior).toEqual([]);
const withReply = writeRerollReplies(begun!.turns, "t1", [
{ windowId: "w1", content: "新答" },
]);
const back = switchTurnVariant(withReply, "t1", "prev");
expect(back).not.toBeNull();
expect(back!.map((t) => t.user.content)).toEqual(["问1", "问2"]);
expect(back![0]?.replies[0]?.content).toBe("答1");
const again = switchTurnVariant(back!, "t1", "next");
expect(again).not.toBeNull();
expect(again!).toHaveLength(1);
expect(again![0]?.user.content).toBe("问1改");
expect(again![0]?.replies[0]?.content).toBe("新答");
});
it("direct reroll keeps the same user text and a new version", () => {
const begun = beginReroll(turns(), "t2", "问2");
expect(begun!.turns.map((t) => t.user.content)).toEqual(["问1", "问2"]);
expect(begun!.turns[1]?.variantIndex).toBe(1);
expect(begun!.turns[1]?.variants?.[0]?.replies[0]?.content).toBe("答2");
expect(begun!.prior.map((t) => t.id)).toEqual(["t1"]);
});
it("drops an empty reroll when generation produced nothing", () => {
const begun = beginReroll(turns(), "t1", "问1");
const aborted = abortReroll(begun!.turns, "t1");
expect(aborted).not.toBeNull();
expect(aborted!.map((t) => t.user.content)).toEqual(["问1", "问2"]);
expect(aborted![0]?.replies[0]?.content).toBe("答1");
expect(aborted![0]?.variants).toHaveLength(1);
});
it("rejects blank text and unknown turns", () => {
expect(beginReroll(turns(), "t1", " ")).toBeNull();
expect(beginReroll(turns(), "missing", "x")).toBeNull();
expect(switchTurnVariant(turns(), "t1", "next")).toBeNull();
});
it("retries only the failed windows and keeps the rest in place", () => {
const base: DirectChatTurn[] = [
{
id: "t1",
user: { content: "问1", createdAt: "2026-01-01T00:00:00.000Z" },
replies: [
{ windowId: "w1", content: "好的回复" },
{ windowId: "w2", error: "上游 403" },
{ windowId: "w3", error: "超时" },
],
selectedWindowId: "w1",
createdAt: "2026-01-01T00:00:00.000Z",
},
];
const merged = mergeTurnReplies(base, "t1", [
{ windowId: "w2", content: "重试后的回复" },
{ windowId: "w3", error: "还是超时" },
]);
expect(merged).toHaveLength(1);
const turn = merged[0]!;
// 只重试 w2/w3:w1 原样保留,w3 的错误被新错误覆盖
expect(turn.replies.find((r) => r.windowId === "w1")?.content).toBe(
"好的回复",
);
expect(turn.replies.find((r) => r.windowId === "w2")?.content).toBe(
"重试后的回复",
);
expect(turn.replies.find((r) => r.windowId === "w2")?.error).toBeUndefined();
expect(turn.replies.find((r) => r.windowId === "w3")?.error).toBe(
"还是超时",
);
// 不新开版本,且不动「保留此回复」
expect(turn.variants ?? []).toHaveLength(0);
expect(turn.variantIndex).toBe(0);
expect(turn.selectedWindowId).toBe("w1");
// 原数据不被就地修改
expect(base[0]!.replies.find((r) => r.windowId === "w2")?.error).toBe(
"上游 403",
);
});
it("merges a retry into the active version without dropping other versions", () => {
const begun = beginReroll(turns(), "t1", "问1改");
const withReply = writeRerollReplies(begun!.turns, "t1", [
{ windowId: "w1", content: "新答" },
]);
const merged = mergeTurnReplies(withReply, "t1", [
{ windowId: "w1", content: "重试后" },
]);
expect(merged[0]?.replies[0]?.content).toBe("重试后");
expect(merged[0]?.variants).toHaveLength(2);
expect(merged[0]?.variants?.[1]?.replies[0]?.content).toBe("重试后");
expect(merged[0]?.variants?.[0]?.replies[0]?.content).toBe("答1");
});
it("leaves turns untouched when the target turn is gone", () => {
const base = turns();
expect(mergeTurnReplies(base, "missing", [{ windowId: "w1", content: "x" }])).toBe(
base,
);
});
it("roundtrips versions and remaps window ids when copying", () => {
process.env.WRITING_AGENT_DATA_DIR = mkdtempSync(
path.join(tmpdir(), "wa-direct-chat-variants-"),
);
const shell = createShell({ name: "壳" });
const conv = createConversation({
shellId: shell.id,
windows: [{ id: "w1", profileId: "p1", profileName: "甲" }],
turns: [
{
...sampleTurn("t1", "问", "新"),
variantIndex: 1,
variants: [
{
user: { content: "问", createdAt: "2026-01-01T00:00:00.000Z" },
replies: [{ windowId: "w1", content: "旧" }],
selectedWindowId: "w1",
after: [sampleTurn("t2", "后", "续")],
},
{
user: { content: "问", createdAt: "2026-01-01T00:00:00.000Z" },
replies: [{ windowId: "w1", content: "新" }],
after: [],
},
],
},
],
});
const loaded = getConversation(conv.id);
expect(loaded?.turns[0]?.variantIndex).toBe(1);
expect(loaded?.turns[0]?.variants?.[0]?.replies[0]?.content).toBe("旧");
expect(loaded?.turns[0]?.variants?.[0]?.selectedWindowId).toBe("w1");
expect(loaded?.turns[0]?.variants?.[0]?.after[0]?.user.content).toBe("后");
const copy = duplicateConversation(conv.id);
const wid = copy.windows[0]?.id;
expect(wid).toBeTruthy();
expect(wid).not.toBe("w1");
expect(copy.turns[0]?.replies[0]?.windowId).toBe(wid);
expect(copy.turns[0]?.variants?.[0]?.replies[0]?.windowId).toBe(wid);
expect(copy.turns[0]?.variants?.[0]?.selectedWindowId).toBe(wid);
expect(copy.turns[0]?.variants?.[0]?.after[0]?.replies[0]?.windowId).toBe(wid);
});
});