Files
writing-agent/tests/preset-chat-prompts.test.ts
T
chenmoranzhi 9dc927282f 预设兼容:按真实抓包对齐装配与包装层,补上声明式工具与预设正则。
- 装配只认 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:真假上游抓包对比工具、三份预设的机制结论与实测报告
2026-10-23 00:13:08 +08:00

196 lines
6.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { importSillyTavernPreset } from "../src/preset/importer.js";
import { assemblePlayWorkerMessages } from "../src/preset/play-frame.js";
import { parseExampleDialogue } from "../src/preset/examples.js";
const rawPreset = {
new_chat_prompt: "[Start a new Chat]",
new_example_chat_prompt: "[Example Chat]",
prompts: [
{ identifier: "main", name: "main", role: "system", content: "MAIN" },
{
identifier: "dialogueExamples",
name: "examples",
role: "system",
marker: true,
content: "",
},
{
identifier: "chatHistory",
name: "history",
role: "system",
marker: true,
content: "",
},
],
prompt_order: [
{
character_id: 100001,
order: [
{ identifier: "main", enabled: true },
{ identifier: "dialogueExamples", enabled: true },
{ identifier: "chatHistory", enabled: true },
],
},
],
};
const emptyPack = {
worldBookBefore: [],
history: [],
worldBookAfter: [],
turn: "",
postTurn: [],
};
function assemble(overrides: Record<string, unknown> = {}) {
const report = importSillyTavernPreset(rawPreset, { name: "t" });
return assemblePlayWorkerMessages({
systemPrompt: "",
preset: report.preset,
pack: { ...emptyPack, ...overrides },
persona: null,
charName: "ProbeCard",
});
}
describe("酒馆包装层:new_chat_prompt 与示例对话", () => {
it("chatHistory 空时插一条 new_chat_prompt", () => {
const messages = assemble();
const history = messages.filter((m) => m.content === "[Start a new Chat]");
expect(history).toHaveLength(1);
expect(history[0]?.role).toBe("system");
});
it("有历史时 new_chat_prompt 仍排在历史最前", () => {
const messages = assemble({
history: [
{ role: "user", content: "第一句" },
{ role: "assistant", content: "第二句" },
],
});
const texts = messages.map((m) => m.content);
const head = texts.indexOf("[Start a new Chat]");
expect(head).toBeGreaterThanOrEqual(0);
expect(texts.indexOf("第一句")).toBe(head + 1);
expect(texts.indexOf("第二句")).toBe(head + 2);
});
it("示例对话按 <START> 切段,带 example_user / example_assistant", () => {
const messages = assemble({
exampleDialogue:
"<START>\n{{user}}: 今天做什么?\n{{char}}: 读书。\n更多解释",
});
expect(messages.map((m) => m.content)).toContain("[Example Chat]");
const user = messages.find((m) => m.name === "example_user");
const assistant = messages.find((m) => m.name === "example_assistant");
expect(user?.role).toBe("system");
expect(user?.content).toBe("今天做什么?");
expect(assistant?.role).toBe("system");
expect(assistant?.content).toBe("读书。\n更多解释");
});
it("没有示例对话就不发 new_example_chat_prompt", () => {
const texts = assemble().map((m) => m.content);
expect(texts).not.toContain("[Example Chat]");
});
it("人设描述为空时不塞「姓名:X」", () => {
const report = importSillyTavernPreset(
{
prompts: [
{
identifier: "personaDescription",
name: "persona",
role: "system",
marker: true,
content: "",
},
],
prompt_order: [
{ character_id: 100001, order: [{ identifier: "personaDescription", enabled: true }] },
],
},
{ name: "t2" },
);
const messages = assemblePlayWorkerMessages({
systemPrompt: "",
preset: report.preset,
pack: emptyPack,
persona: { name: "User", description: "" },
charName: "ProbeCard",
});
expect(messages.some((m) => String(m.content).startsWith("姓名:"))).toBe(false);
});
});
describe("parseExampleDialogue", () => {
it("多块示例各自成组", () => {
const blocks = parseExampleDialogue({
text: "<START>\nA: 一\nB: 二\n<START>\nA: 三\nB: 四",
userName: "A",
charName: "B",
});
expect(blocks.map((block) => block.length)).toEqual([2, 2]);
expect(blocks[1]?.map((m) => m.content)).toEqual(["三", "四"]);
});
it("续行并入上一条", () => {
const blocks = parseExampleDialogue({
text: "A: 第一行\n第二行\nB: 回复",
userName: "A",
charName: "B",
});
expect(blocks[0]?.map((m) => m.content)).toEqual(["第一行\n第二行", "回复"]);
});
});
describe("角色卡三块(槽位填)", () => {
const rawCardPreset = {
personality_format: "<character_personality>\n{{personality}}\n</character_personality>",
scenario_format: "<scenario>\n{{scenario}}\n</scenario>",
prompts: [
{ identifier: "charDescription", name: "desc", role: "system", marker: true, content: "" },
{ identifier: "charPersonality", name: "pers", role: "system", marker: true, content: "" },
{ identifier: "scenario", name: "scen", role: "system", marker: true, content: "" },
],
prompt_order: [
{
character_id: 100001,
order: [
{ identifier: "charDescription", enabled: true },
{ identifier: "charPersonality", enabled: true },
{ identifier: "scenario", enabled: true },
],
},
],
};
function build(card: unknown) {
const report = importSillyTavernPreset(rawCardPreset, { name: "card" });
return assemblePlayWorkerMessages({
systemPrompt: "",
preset: report.preset,
pack: { ...emptyPack, card },
persona: null,
charName: "ProbeCard",
});
}
it("描述原样发,性格与场景套预设模板", () => {
const messages = build({
description: "DESC",
personality: "PERS",
scenario: "SCEN",
});
const texts = messages.map((m) => m.content);
expect(texts).toContain("DESC");
expect(texts).toContain("<character_personality>\nPERS\n</character_personality>");
expect(texts).toContain("<scenario>\nSCEN\n</scenario>");
});
it("槽位为空就整条不发", () => {
expect(build(undefined)).toEqual([]);
});
});