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

- 装配只认 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
+185
View File
@@ -0,0 +1,185 @@
import { describe, expect, it } from "vitest";
import { expandAtDirectives, expandPresetDirectives } from "../src/directives/expand.js";
import { collectMacroNames, createMacroVars, expandMacros } from "../src/directives/macros.js";
import { assemblePresetMessages } from "../src/preset/assembler.js";
import type { PresetPackage } from "../src/types/preset.js";
describe("expandMacros", () => {
it("strips {{// }} comments, including multi-line ones", () => {
const r = expandMacros("前{{// \n声明:禁止外传\n\n}}后");
expect(r.text).toBe("前后");
expect(r.hits.map((h) => h.kind)).toEqual(["comment"]);
});
it("does not expand @ tokens inside a comment", () => {
const r = expandAtDirectives("{{// 不要动 @玩家 }}", {
persona: { name: "阿泽" },
});
expect(r.text).toBe("");
expect(r.hits.map((h) => h.kind)).toEqual(["comment"]);
});
it("{{trim}} expands to empty and asks caller to trim", () => {
const r = expandMacros(" {{// 备注 }}{{trim}}\n正文 ");
expect(r.text).toBe(" \n正文 ");
expect(r.trim).toBe(true);
expect(expandAtDirectives(" {{trim}}\n正文 ").text).toBe("正文");
});
it("{{user}} and {{char}}", () => {
const r = expandMacros("{{user}} 与 {{char}}", {
user: "林晚",
char: "长夜行",
});
expect(r.text).toBe("林晚 与 长夜行");
});
it("{{char}} falls back to 作品", () => {
expect(expandMacros("{{char}}").text).toBe("作品");
});
it("{{persona}} expands to the persona description", () => {
expect(expandMacros("{{persona}}", { persona: "二十岁,缄默" }).text).toBe(
"二十岁,缄默",
);
// 没设人设 → 空串(与酒馆一致)
expect(expandMacros("[{{persona}}]").text).toBe("[]");
});
it("treats {{persona}} as a known macro in the import report", () => {
expect(collectMacroNames("{{persona}} {{charPrompt}}")).toContain(
"{{persona}}",
);
});
it("setvar then getvar in the same scope", () => {
const vars = createMacroVars();
expect(expandMacros("{{setvar::a::你好}}", { vars }).text).toBe("");
expect(expandMacros("{{getvar::a}}!", { vars }).text).toBe("你好!");
});
it("getvar on unset variable is empty", () => {
expect(expandMacros("[{{getvar::nope}}]").text).toBe("[]");
});
it("keeps whitespace inside setvar values", () => {
const vars = createMacroVars();
expandMacros("{{setvar::set1:: }}", { vars });
expect(vars.get("set1")).toBe(" ");
});
it("addvar appends", () => {
const vars = createMacroVars();
expandMacros("{{setvar::k::a}}{{addvar::k::b}}", { vars });
expect(vars.get("k")).toBe("ab");
});
it("{{roll N}} yields an integer in range", () => {
for (let i = 0; i < 20; i++) {
const value = Number(expandMacros("{{roll 60}}").text);
expect(Number.isInteger(value)).toBe(true);
expect(value).toBeGreaterThanOrEqual(1);
expect(value).toBeLessThanOrEqual(60);
}
});
it("{{roll:1d6}} and {{roll::d9}} roll dice", () => {
for (let i = 0; i < 20; i++) {
const six = Number(expandMacros("{{roll:1d6}}").text);
expect(six).toBeGreaterThanOrEqual(1);
expect(six).toBeLessThanOrEqual(6);
const nine = Number(expandMacros("{{roll::d9}}").text);
expect(nine).toBeGreaterThanOrEqual(1);
expect(nine).toBeLessThanOrEqual(9);
}
});
it("does not roll when allowDice is false", () => {
expect(expandMacros("{{roll 60}}", { allowDice: false }).text).toBe(
"{{roll 60}}",
);
});
it("leaves unknown macros untouched", () => {
const r = expandMacros("{{turnPlus1}} {{MEMORY_TABLE}} {{ }}");
expect(r.text).toBe("{{turnPlus1}} {{MEMORY_TABLE}} {{ }}");
expect(r.hits).toEqual([]);
});
it("collectMacroNames reports unknown macros by raw text", () => {
const names = collectMacroNames(
"{{// c}}{{user}}{{setvar::a::b}}{{turnPlus1}}{{roll 9}}",
);
expect(names).toContain("{{comment}}");
expect(names).toContain("{{user}}");
expect(names).toContain("{{setvar}}");
expect(names).toContain("{{roll}}");
expect(names.some((n) => n.includes("turnPlus1"))).toBe(true);
});
});
function presetWith(prompts: Array<{ id: string; content: string }>): PresetPackage {
return {
id: "p",
name: "p",
source: "sillytavern",
prompts: prompts.map((p) => ({
id: p.id,
name: p.id,
enabled: true,
role: "system" as const,
content: p.content,
marker: false,
sourceIdentifier: p.id,
})),
promptOrder: prompts.map((p, i) => ({
promptId: p.id,
enabled: true,
orderIndex: i,
})),
generation: {},
unsupported: [],
importedAt: "2026-01-01T00:00:00.000Z",
};
}
describe("preset assembly macro scope", () => {
it("setvar in an earlier entry is visible to getvar in a later entry", () => {
const preset = presetWith([
{ id: "a", content: "{{setvar::set1:: }}{{// 预设说明 }}" },
{ id: "b", content: "规则{{getvar::set1}}结束" },
]);
const vars = createMacroVars();
const messages = assemblePresetMessages(
preset,
() => null,
(content) => expandPresetDirectives(content, { vars, user: "阿泽" }).text,
);
expect(messages.map((m) => m.content)).toEqual(["规则 结束"]);
});
it("expands {{char}} from assembly context", () => {
const preset = presetWith([{ id: "a", content: "<{{char}}>正文</{{char}}>" }]);
const messages = assemblePresetMessages(
preset,
() => null,
(content) => expandPresetDirectives(content, { charName: "长夜行" }).text,
);
expect(messages[0]?.content).toBe("<长夜行>正文</长夜行>");
});
});
describe("{{// 注释 }} 配平", () => {
it("注释里嵌套宏不会漏出尾巴", () => {
const text = expandMacros("{{// 注释:把 {{user}} 换成别的\n}}|用户:{{user}}|", {
user: "User",
}).text;
expect(text).toBe("|用户:User|");
});
it("普通注释照旧吃掉", () => {
expect(expandMacros("A{{// 说明 }}B", {}).text).toBe("AB");
});
});