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