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