预设兼容:按真实抓包对齐装配与包装层,补上声明式工具与预设正则。
- 装配只认 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:
+223
-2
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user