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

- 装配只认 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
+295
View File
@@ -0,0 +1,295 @@
import { describe, expect, it } from "vitest";
import {
buildPresetDiagnostics,
diagnosticsWarnings,
} from "../src/preset/diagnostics.js";
import { importSillyTavernPreset } from "../src/preset/importer.js";
function entry(
id: string,
content: string,
extra: Partial<Record<string, unknown>> = {},
) {
return {
identifier: id,
name: id,
role: "system",
content,
enabled: true,
...extra,
};
}
function rawPreset(prompts: unknown[], extras: Record<string, unknown> = {}) {
return {
prompts,
prompt_order: [
{
character_id: 100001,
order: (prompts as Array<{ identifier: string }>).map((p) => ({
identifier: p.identifier,
enabled: true,
})),
},
],
...extras,
};
}
describe("导入诊断:跨条目标签", () => {
it("detects tags opened in one entry and closed in another", () => {
const report = importSillyTavernPreset(
rawPreset([
entry("a", "<system>开局"),
entry("b", "中间"),
entry("c", "</system>收尾"),
]),
{ name: "t" },
);
const cross = report.diagnostics.tags.crossEntry;
expect(cross).toEqual([{ tag: "system", openIn: "a", closeIn: "c" }]);
expect(
report.warnings.some((w) => w.includes("跨条目标签配对")),
).toBe(true);
});
it("supports CJK tag names", () => {
const report = importSillyTavernPreset(
rawPreset([entry("a", "以下是<对话历史>"), entry("b", "</对话历史>结束")]),
{ name: "t" },
);
expect(report.diagnostics.tags.crossEntry).toEqual([
{ tag: "对话历史", openIn: "a", closeIn: "b" },
]);
});
it("reports tags never closed", () => {
const report = importSillyTavernPreset(
rawPreset([entry("a", "<game>正文")]),
{ name: "t" },
);
expect(report.diagnostics.tags.unclosed).toEqual([
{ tag: "game", openIn: "a" },
]);
});
it("ignores self-closing tags", () => {
const report = importSillyTavernPreset(
rawPreset([entry("a", "换行<br/>结束")]),
{ name: "t" },
);
expect(report.diagnostics.tags.unclosed).toEqual([]);
expect(report.diagnostics.tags.orphanClose).toEqual([]);
});
});
describe("导入诊断:伪工具协议", () => {
it("finds placeholder-style protocols", () => {
const report = importSillyTavernPreset(
rawPreset([
entry(
"sys",
"[Interface]\nactive_interface = %TOOL_NAME%\noutput_channel = %TOOL_NAME%",
),
entry("tool", '[contract]\nstream_target = "display_stream"'),
]),
{ name: "t" },
);
const proto = report.diagnostics.toolProtocol;
expect(proto.entries).toEqual(["sys", "tool"]);
expect(proto.placeholders).toContain("%TOOL_NAME%");
expect(report.warnings.some((w) => w.includes("伪工具协议"))).toBe(true);
});
it("reads SPreset.ToolBindings form into tool name and params", () => {
const report = importSillyTavernPreset(
rawPreset([entry("bound", "所有输出必须调用 game_content 工具")], {
extensions: {
SPreset: {
ToolBindings: {
bound: {
enabled: true,
form: {
name: "game_content",
description: "全部输出在此工具内",
parameters: [{ name: "content", type: "string", required: true }],
},
},
},
},
},
}),
{ name: "t" },
);
expect(report.diagnostics.toolProtocol.bindings).toEqual([
{
entry: "bound",
toolName: "game_content",
params: ["content"],
description: "全部输出在此工具内",
},
]);
});
});
describe("导入诊断:宏与随机宏", () => {
it("separates known from unknown macros", () => {
const report = importSillyTavernPreset(
rawPreset([
entry("a", "{{user}} {{setvar::k::v}}{{getvar::k}}{{turnPlus1}}"),
]),
{ name: "t" },
);
const macros = report.diagnostics.macros;
expect(macros.find((m) => m.name === "{{user}}")?.known).toBe(true);
expect(macros.find((m) => m.name === "{{turnPlus1}}")?.known).toBe(false);
expect(
report.warnings.some((w) => w.includes("未识别宏")),
).toBe(true);
});
it("marks random macros before chatHistory as cache-breaking", () => {
const report = importSillyTavernPreset(
rawPreset([
entry("noise", "{{roll 999}}"),
entry("chatHistory", "", { marker: true }),
]),
{ name: "t" },
);
const noise = report.diagnostics.randomMacros.find(
(r) => r.entry === "noise",
);
expect(noise?.afterHistory).toBe(false);
expect(
report.warnings.some((w) => w.includes("提示缓存每轮失效")),
).toBe(true);
});
it("marks random macros after chatHistory as harmless", () => {
const report = importSillyTavernPreset(
rawPreset([
entry("chatHistory", "", { marker: true }),
entry("noise", "{{roll 999}}"),
]),
{ name: "t" },
);
const noise = report.diagnostics.randomMacros.find(
(r) => r.entry === "noise",
);
expect(noise?.afterHistory).toBe(true);
expect(
report.warnings.some((w) => w.includes("提示缓存每轮失效")),
).toBe(false);
});
});
describe("导入诊断:squash_system_messages 映射", () => {
it.each([
[true, "system"],
[false, "none"],
["semi", "semi"],
["STRICT", "strict"],
[2, "semi"],
[undefined, undefined],
] as const)("%s → %s", (input, expected) => {
const report = importSillyTavernPreset(
rawPreset([entry("a", "x")], { squash_system_messages: input }),
{ name: "t" },
);
expect(report.preset.messagePolicy?.squash).toBe(expected);
});
});
describe("导入诊断:静默失效告警", () => {
it("reports injection_position=1 entries as depth insertions", () => {
const report = importSillyTavernPreset(
rawPreset([
entry("main", "主线"),
entry("deepjail", "深度注入的破限", { injection_position: 1 }),
]),
{ name: "t" },
);
expect(report.diagnostics.silentGaps.deepInjection).toEqual(["deepjail"]);
expect(
report.warnings.some((w) => w.includes("injection_depth")),
).toBe(true);
});
it("ignores depth-injection entries that prompt_order leaves off", () => {
const report = importSillyTavernPreset(
rawPreset(
[entry("main", "主线"), entry("off", "关掉的", { injection_position: 1 })],
{
prompt_order: [
{
character_id: 100001,
order: [
{ identifier: "main", enabled: true },
{ identifier: "off", enabled: false },
],
},
],
},
),
{ name: "t" },
);
expect(report.diagnostics.silentGaps.deepInjection).toEqual([]);
});
it("flags script capabilities that have no native replacement", () => {
const report = importSillyTavernPreset(
rawPreset([entry("main", "主线")], {
extensions: {
tavern_helper: {
scripts: [
{
type: "script",
name: "防截断",
content:
"const r = await fetch(url); const reader = res.body.getReader(); // text/event-stream",
},
],
},
},
}),
{ name: "t" },
);
const script = report.diagnostics.scripts[0];
expect(script?.features).toContain("改写请求 / 拦截 fetch");
expect(script?.features).toContain("流式解包 / 防截断");
expect(report.diagnostics.silentGaps.unportedScriptFeatures[0]?.script).toBe(
"防截断",
);
expect(
report.warnings.some((w) => w.includes("本版本没有替代")),
).toBe(true);
});
it("mentions display-layer regexes leaking their tags", () => {
const report = importSillyTavernPreset(
rawPreset([entry("main", "主线")], {
regex_scripts: [
{ id: "r1", scriptName: "隐藏选项", markdownOnly: true, placement: [2] },
],
}),
{ name: "t" },
);
expect(
report.warnings.some((w) => w.includes("显示层规则")),
).toBe(true);
});
});
describe("diagnosticsWarnings", () => {
it("keeps quiet for a clean preset", () => {
const report = importSillyTavernPreset(
rawPreset([
entry("main", "主线"),
entry("chatHistory", "", { marker: true }),
entry("jailbreak", "收尾"),
]),
{ name: "t" },
);
expect(diagnosticsWarnings(report.diagnostics)).toEqual([]);
});
});