mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
1064 lines
31 KiB
JavaScript
1064 lines
31 KiB
JavaScript
import assert from "node:assert/strict";
|
||
import {
|
||
installResolveHooks,
|
||
} from "./helpers/register-hooks-compat.mjs";
|
||
|
||
const extensionsShimSource = [
|
||
"export const extension_settings = {};",
|
||
"export function getContext(...args) {",
|
||
" return globalThis.SillyTavern?.getContext?.(...args) || null;",
|
||
"}",
|
||
].join("\n");
|
||
const scriptShimSource = [
|
||
"export function substituteParamsExtended(text) {",
|
||
" return String(text ?? '');",
|
||
"}",
|
||
].join("\n");
|
||
const extensionsShimUrl = `data:text/javascript,${encodeURIComponent(
|
||
extensionsShimSource,
|
||
)}`;
|
||
const scriptShimUrl = `data:text/javascript,${encodeURIComponent(
|
||
scriptShimSource,
|
||
)}`;
|
||
|
||
installResolveHooks([
|
||
{
|
||
specifiers: [
|
||
"../../../extensions.js",
|
||
"../../../../extensions.js",
|
||
"../../../../../extensions.js",
|
||
],
|
||
url: extensionsShimUrl,
|
||
},
|
||
{
|
||
specifiers: [
|
||
"../../../../script.js",
|
||
"../../../../../script.js",
|
||
],
|
||
url: scriptShimUrl,
|
||
},
|
||
]);
|
||
|
||
const originalSillyTavern = globalThis.SillyTavern;
|
||
const originalEjsTemplate = globalThis.EjsTemplate;
|
||
const originalMvu = globalThis.Mvu;
|
||
const originalGetCharWorldbookNames = globalThis.getCharWorldbookNames;
|
||
const originalGetWorldbook = globalThis.getWorldbook;
|
||
const originalGetLorebookEntries = globalThis.getLorebookEntries;
|
||
|
||
function createWorldbookEntry({
|
||
uid,
|
||
name,
|
||
comment = name,
|
||
content,
|
||
enabled = true,
|
||
positionType = "before_character_definition",
|
||
role = "system",
|
||
depth = 0,
|
||
order = 10,
|
||
strategyType = "constant",
|
||
keys = [],
|
||
keysSecondary = [],
|
||
}) {
|
||
return {
|
||
uid,
|
||
name,
|
||
comment,
|
||
content,
|
||
enabled,
|
||
position: {
|
||
type: positionType,
|
||
role,
|
||
depth,
|
||
order,
|
||
},
|
||
strategy: {
|
||
type: strategyType,
|
||
keys,
|
||
keys_secondary: { logic: "and_any", keys: keysSecondary },
|
||
},
|
||
probability: 100,
|
||
extra: {},
|
||
};
|
||
}
|
||
|
||
function createConstantWorldbookEntry(uid, name, content, comment = name) {
|
||
return createWorldbookEntry({
|
||
uid,
|
||
name,
|
||
comment,
|
||
content,
|
||
});
|
||
}
|
||
|
||
const constantEntry = createWorldbookEntry({
|
||
uid: 1,
|
||
name: "常驻设定",
|
||
comment: "常驻设定",
|
||
content: "这里是常驻世界设定。",
|
||
order: 10,
|
||
});
|
||
|
||
const dynEntry = createWorldbookEntry({
|
||
uid: 2,
|
||
name: "EW/Dyn/线索",
|
||
comment: "线索条目",
|
||
content: "隐藏线索:<%= charName %> 正在调查。",
|
||
enabled: false,
|
||
strategyType: "selective",
|
||
keys: ["调查"],
|
||
order: 15,
|
||
});
|
||
|
||
const inlineSummaryEntry = createWorldbookEntry({
|
||
uid: 3,
|
||
name: "普通 EJS 汇总",
|
||
comment: "EJS 汇总",
|
||
content: '控制摘要:<%= await getwi("EW/Dyn/线索") %>',
|
||
order: 20,
|
||
});
|
||
|
||
const inlineDataSummaryEntry = createWorldbookEntry({
|
||
uid: 12,
|
||
name: "数据 EJS 汇总",
|
||
comment: "数据 EJS 汇总",
|
||
content:
|
||
'数据摘要:<%= await getwi("数据模板", { clue: "蓝钥匙", mood: "紧张" }) %>',
|
||
order: 21,
|
||
});
|
||
|
||
const inlineDataTemplateEntry = createWorldbookEntry({
|
||
uid: 13,
|
||
name: "数据模板",
|
||
comment: "数据模板",
|
||
content:
|
||
"线索=<%= clue %>;情绪=<%= mood %>;角色=<%= char %>;用户=<%= user %>;上下文=<%= recentMessages %>",
|
||
enabled: false,
|
||
order: 22,
|
||
});
|
||
|
||
const commentKeywordProbeEntry = createWorldbookEntry({
|
||
uid: 14,
|
||
name: "备注命中测试",
|
||
comment: "常驻备注",
|
||
content: "这条只用于验证 comment 不参与自定义过滤。",
|
||
strategyType: "selective",
|
||
keys: ["绝不会匹配到这里"],
|
||
order: 23,
|
||
});
|
||
|
||
const extensionLiteralEntry = createWorldbookEntry({
|
||
uid: 4,
|
||
name: "扩展语义正文",
|
||
comment: "扩展语义正文",
|
||
content: "@@generate\n[GENERATE:Test]\n扩展语义只是普通文本。",
|
||
order: 25,
|
||
});
|
||
|
||
const externalInlineEntry = createWorldbookEntry({
|
||
uid: 5,
|
||
name: "外部书汇总",
|
||
comment: "外部书汇总",
|
||
content: '外部补充:<%= await getwi("bonus-book", "Bonus 条目") %>',
|
||
order: 26,
|
||
});
|
||
|
||
const forceControlEntry = createWorldbookEntry({
|
||
uid: 6,
|
||
name: "普通 EJS 控制",
|
||
comment: "EJS 控制",
|
||
content: '<% await activewi("强制 after") %>',
|
||
order: 30,
|
||
});
|
||
|
||
const forcedAfterEntry = createWorldbookEntry({
|
||
uid: 7,
|
||
name: "强制 after",
|
||
comment: "强制后置",
|
||
content: "这是被 EJS 强制激活的后置条目。",
|
||
enabled: false,
|
||
positionType: "after_character_definition",
|
||
strategyType: "selective",
|
||
keys: ["永远不会命中"],
|
||
order: 40,
|
||
});
|
||
|
||
const atDepthEntry = createWorldbookEntry({
|
||
uid: 8,
|
||
name: "深度注入",
|
||
comment: "深度注入",
|
||
content: "这是一条 atDepth 消息。",
|
||
positionType: "at_depth_as_system",
|
||
depth: 2,
|
||
order: 5,
|
||
});
|
||
|
||
const mvuTaggedEntry = createWorldbookEntry({
|
||
uid: 9,
|
||
name: "[mvu_update] 状态同步",
|
||
comment: "MVU tagged",
|
||
content: "这一条不应该进入结果。",
|
||
order: 28,
|
||
});
|
||
|
||
const mvuHeuristicEntry = createWorldbookEntry({
|
||
uid: 10,
|
||
name: "MVU 启发式条目",
|
||
comment: "MVU heuristic",
|
||
content: "<status_current_variable>secret=true</status_current_variable>",
|
||
order: 29,
|
||
});
|
||
|
||
const mvuLazyProbeEntry = createWorldbookEntry({
|
||
uid: 11,
|
||
name: "MVU 懒加载探测",
|
||
comment: "MVU 懒加载探测",
|
||
content: 'MVU lazy: <%= await getwi("bonus-book", "Bonus MVU") %>',
|
||
order: 27,
|
||
});
|
||
|
||
const statDataControllerEntry = createWorldbookEntry({
|
||
uid: 15,
|
||
name: "StatData Controller",
|
||
comment: "StatData Controller",
|
||
content:
|
||
'<% if (typeof stat_data !== "undefined" && stat_data?.user?.["\u610f\u8bc6\u72b6\u6001"] === "\u6c89\u7720") { %>stat_data controller payload<% } %>',
|
||
order: 24,
|
||
});
|
||
|
||
const statDataTargetEntry = createWorldbookEntry({
|
||
uid: 16,
|
||
name: "StatData Target",
|
||
comment: "StatData Target",
|
||
content: "stat_data controller payload",
|
||
enabled: false,
|
||
order: 24.1,
|
||
});
|
||
|
||
const messageVarMacroEntry = createWorldbookEntry({
|
||
uid: 17,
|
||
name: "MessageVar Macro",
|
||
comment: "MessageVar Macro",
|
||
content: "latest state={{get_message_variable::stat_data.user.\u610f\u8bc6\u72b6\u6001}}",
|
||
order: 24.2,
|
||
});
|
||
|
||
const customContextProbeEntry = createWorldbookEntry({
|
||
uid: 18,
|
||
name: "Custom Context Probe",
|
||
comment: "Custom Context Probe",
|
||
content: "上下文探针:user=<%= user_input %>;char=<%= charName %>",
|
||
strategyType: "selective",
|
||
keys: ["probe custom mode"],
|
||
order: 24.3,
|
||
});
|
||
const bonusEntry = createWorldbookEntry({
|
||
uid: 101,
|
||
name: "Bonus 条目",
|
||
comment: "Bonus 条目",
|
||
content: "来自 bonus-book 的补充内容。",
|
||
order: 10,
|
||
});
|
||
|
||
const bonusMvuEntry = createWorldbookEntry({
|
||
uid: 102,
|
||
name: "Bonus MVU",
|
||
comment: "Bonus MVU",
|
||
content: "变量更新规则:\ntype: sync\n当前时间: 12:00",
|
||
order: 20,
|
||
});
|
||
|
||
const worldbooksByName = {
|
||
"main-book": [
|
||
constantEntry,
|
||
dynEntry,
|
||
inlineSummaryEntry,
|
||
inlineDataSummaryEntry,
|
||
inlineDataTemplateEntry,
|
||
commentKeywordProbeEntry,
|
||
extensionLiteralEntry,
|
||
externalInlineEntry,
|
||
mvuLazyProbeEntry,
|
||
statDataControllerEntry,
|
||
statDataTargetEntry,
|
||
messageVarMacroEntry,
|
||
customContextProbeEntry,
|
||
forceControlEntry,
|
||
forcedAfterEntry,
|
||
atDepthEntry,
|
||
mvuTaggedEntry,
|
||
mvuHeuristicEntry,
|
||
],
|
||
"bonus-book": [bonusEntry, bonusMvuEntry],
|
||
};
|
||
|
||
try {
|
||
globalThis.SillyTavern = {
|
||
getContext() {
|
||
return {
|
||
name1: "User",
|
||
name2: "Alice",
|
||
chat: [{ is_user: true, mes: "我们继续调查那条线索" }],
|
||
chatMetadata: {},
|
||
extensionSettings: {},
|
||
};
|
||
},
|
||
};
|
||
globalThis.getCharWorldbookNames = () => ({
|
||
primary: "main-book",
|
||
additional: [],
|
||
});
|
||
globalThis.getWorldbook = async (worldbookName) =>
|
||
worldbooksByName[worldbookName] || [];
|
||
globalThis.getLorebookEntries = async (worldbookName) =>
|
||
(worldbooksByName[worldbookName] || []).map((entry) => ({
|
||
uid: entry.uid,
|
||
comment: entry.comment,
|
||
}));
|
||
|
||
const { resolveTaskWorldInfo } = await import("../prompting/task-worldinfo.js");
|
||
const { buildTaskPrompt, buildTaskLlmPayload } = await import(
|
||
"../prompting/prompt-builder.js"
|
||
);
|
||
|
||
const emptyTriggerWorldInfo = await resolveTaskWorldInfo({
|
||
chatMessages: [],
|
||
userMessage: "",
|
||
templateContext: {},
|
||
});
|
||
assert.equal(
|
||
emptyTriggerWorldInfo.beforeEntries.some((entry) => entry.name === "常驻设定"),
|
||
true,
|
||
"constant world info should still resolve without trigger text",
|
||
);
|
||
assert.equal(
|
||
emptyTriggerWorldInfo.beforeEntries.some((entry) => entry.name === "数据 EJS 汇总"),
|
||
true,
|
||
"constant EJS entry should still render with empty template context defaults",
|
||
);
|
||
assert.match(emptyTriggerWorldInfo.beforeText, /数据摘要:线索=蓝钥匙;情绪=紧张;角色=Alice;用户=User;上下文=/);
|
||
assert.equal(
|
||
emptyTriggerWorldInfo.debug.warnings.some((warning) => warning.includes("渲染失败")),
|
||
false,
|
||
);
|
||
|
||
const worldInfo = await resolveTaskWorldInfo({
|
||
templateContext: {
|
||
recentMessages: "我们继续调查那条线索",
|
||
charName: "Alice",
|
||
},
|
||
userMessage: "继续调查",
|
||
});
|
||
assert.equal(worldInfo.beforeEntries.length, 6);
|
||
assert.equal(worldInfo.afterEntries.length, 1);
|
||
assert.equal(worldInfo.additionalMessages.length, 1);
|
||
assert.match(worldInfo.additionalMessages[0].content, /atDepth/);
|
||
assert.match(worldInfo.beforeText, /Alice/);
|
||
assert.match(worldInfo.beforeText, /bonus-book/);
|
||
assert.match(worldInfo.beforeText, /MVU lazy:/);
|
||
assert.match(worldInfo.beforeText, /@@generate/);
|
||
assert.match(worldInfo.beforeText, /\[GENERATE:Test\]/);
|
||
assert.doesNotMatch(worldInfo.beforeText, /getwi|<%=?/);
|
||
assert.doesNotMatch(worldInfo.beforeText, /status_current_variable|updatevariable/i);
|
||
assert.equal(worldInfo.debug.ejsInlinePullCount, 3);
|
||
assert.equal(worldInfo.debug.ejsForcedActivationCount, 1);
|
||
assert.equal(worldInfo.debug.resolvePassCount >= 2, true);
|
||
assert.equal(worldInfo.debug.forcedActivatedEntries.length, 1);
|
||
assert.equal(worldInfo.debug.inlinePulledEntries.length, 3);
|
||
assert.deepEqual(worldInfo.debug.lazyLoadedWorldbooks, ["bonus-book"]);
|
||
assert.equal(worldInfo.debug.mvu.filteredEntryCount, 3);
|
||
assert.equal(worldInfo.debug.mvu.lazyFilteredEntryCount, 1);
|
||
assert.equal(worldInfo.debug.mvu.blockedContentsCount, 4);
|
||
const defaultFilteredSourceNames = worldInfo.debug.mvu.filteredEntries
|
||
.map((entry) => entry.sourceName)
|
||
.sort();
|
||
assert.equal(defaultFilteredSourceNames.includes("Bonus MVU"), true);
|
||
assert.equal(defaultFilteredSourceNames.some((name) => String(name || "").includes("MVU")), true);
|
||
assert.equal(defaultFilteredSourceNames.some((name) => String(name || "").startsWith("[mvu_update]")), true);
|
||
assert.equal(
|
||
worldInfo.debug.warnings.some((warning) => warning.includes("EW/")),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
worldInfo.debug.recursionWarnings.some((warning) =>
|
||
warning.includes("mvu filtered world info blocked"),
|
||
),
|
||
true,
|
||
);
|
||
assert.equal(worldInfo.debug.customFilter.mode, "default");
|
||
assert.equal(worldInfo.debug.customFilter.filteredEntryCount, 0);
|
||
|
||
globalThis.Mvu = {
|
||
getMvuData({ type, message_id: messageId } = {}) {
|
||
if (type === "message" && messageId === "latest") {
|
||
return {
|
||
stat_data: {
|
||
user: {
|
||
"意识状态": "沉眠",
|
||
},
|
||
"恼恼": {
|
||
"发情值": 71,
|
||
},
|
||
},
|
||
};
|
||
}
|
||
return {};
|
||
},
|
||
};
|
||
|
||
const customWorldInfo = await resolveTaskWorldInfo({
|
||
settings: {
|
||
worldInfoFilterMode: "custom",
|
||
worldInfoFilterCustomKeywords: "",
|
||
},
|
||
templateContext: {
|
||
recentMessages: "custom-mode regression probe",
|
||
charName: "Alice",
|
||
},
|
||
userMessage: "probe custom mode",
|
||
});
|
||
|
||
assert.equal(
|
||
customWorldInfo.beforeEntries.some((entry) =>
|
||
String(entry.sourceName || "").startsWith("[mvu_update]"),
|
||
),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
customWorldInfo.beforeEntries.some((entry) =>
|
||
String(entry.sourceName || "").includes("MVU"),
|
||
),
|
||
true,
|
||
);
|
||
assert.match(
|
||
customWorldInfo.beforeText,
|
||
/<status_current_variable>secret=true<\/status_current_variable>/,
|
||
);
|
||
assert.match(
|
||
customWorldInfo.beforeText,
|
||
/控制摘要:隐藏线索:Alice 正在调查。/,
|
||
);
|
||
assert.match(
|
||
customWorldInfo.beforeText,
|
||
/上下文探针:user=probe custom mode;char=Alice/,
|
||
);
|
||
assert.equal(
|
||
customWorldInfo.allEntries.some((entry) => String(entry.name || "").startsWith("EW/Dyn/")),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
customWorldInfo.afterEntries.some((entry) => entry.sourceName === "强制 after"),
|
||
true,
|
||
);
|
||
assert.equal(customWorldInfo.debug.mvu.filteredEntryCount, 0);
|
||
assert.equal(customWorldInfo.debug.customFilter.mode, "custom");
|
||
assert.equal(customWorldInfo.debug.customFilter.filteredEntryCount, 0);
|
||
assert.equal(
|
||
customWorldInfo.debug.customRender.bridgedStatDataFromLatestMessage,
|
||
true,
|
||
);
|
||
assert.equal(customWorldInfo.debug.customRender.taskEjsStatDataRoots.cache, true);
|
||
assert.equal(
|
||
customWorldInfo.debug.customRender.taskEjsStatDataRoots.message,
|
||
true,
|
||
);
|
||
assert.equal(customWorldInfo.debug.customRender.fallbackEntryCount > 0, true);
|
||
assert.match(customWorldInfo.beforeText, /stat_data controller payload/);
|
||
assert.match(customWorldInfo.beforeText, /latest state=.+/);
|
||
|
||
globalThis.EjsTemplate = {
|
||
async prepareContext() {
|
||
return {
|
||
user_input: "OLD_FROM_NATIVE",
|
||
charName: "OLD_CHAR",
|
||
};
|
||
},
|
||
async evalTemplate(text, env) {
|
||
return String(text)
|
||
.replace(/<%=\s*user_input\s*%>/g, String(env.user_input ?? ""))
|
||
.replace(/<%=\s*charName\s*%>/g, String(env.charName ?? ""));
|
||
},
|
||
};
|
||
|
||
const customWorldInfoWithNativeRuntime = await resolveTaskWorldInfo({
|
||
settings: {
|
||
worldInfoFilterMode: "custom",
|
||
worldInfoFilterCustomKeywords: "",
|
||
},
|
||
templateContext: {
|
||
recentMessages: "custom-mode regression probe",
|
||
charName: "Alice",
|
||
},
|
||
userMessage: "probe custom mode",
|
||
});
|
||
|
||
assert.match(
|
||
customWorldInfoWithNativeRuntime.beforeText,
|
||
/上下文探针:user=probe custom mode;char=Alice/,
|
||
);
|
||
assert.doesNotMatch(
|
||
customWorldInfoWithNativeRuntime.beforeText,
|
||
/OLD_FROM_NATIVE|OLD_CHAR/,
|
||
);
|
||
delete globalThis.EjsTemplate;
|
||
|
||
const keywordWorldInfo = await resolveTaskWorldInfo({
|
||
settings: {
|
||
worldInfoFilterMode: "custom",
|
||
worldInfoFilterCustomKeywords: "常驻",
|
||
},
|
||
templateContext: {
|
||
recentMessages: "我们继续调查那条线索",
|
||
charName: "Alice",
|
||
},
|
||
userMessage: "继续调查",
|
||
});
|
||
|
||
assert.equal(
|
||
keywordWorldInfo.beforeEntries.some(
|
||
(entry) => entry.sourceName === "常驻设定",
|
||
),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
keywordWorldInfo.allEntries.some((entry) => entry.name === "备注命中测试"),
|
||
true,
|
||
);
|
||
assert.equal(keywordWorldInfo.debug.customFilter.filteredEntryCount, 1);
|
||
assert.equal(
|
||
keywordWorldInfo.debug.customFilter.filteredEntries[0].name,
|
||
"常驻设定",
|
||
);
|
||
assert.equal(
|
||
keywordWorldInfo.debug.customFilter.filteredEntries[0].matchedKeyword,
|
||
"常驻",
|
||
);
|
||
|
||
const keywordCachePrime = await resolveTaskWorldInfo({
|
||
settings: {
|
||
worldInfoFilterMode: "custom",
|
||
worldInfoFilterCustomKeywords: "常驻,缓存探针",
|
||
},
|
||
templateContext: {
|
||
recentMessages: "我们继续调查那条线索",
|
||
charName: "Alice",
|
||
},
|
||
userMessage: "继续调查",
|
||
});
|
||
assert.equal(keywordCachePrime.debug.cache.hit, false);
|
||
assert.equal(keywordCachePrime.debug.customFilter.filteredEntryCount, 1);
|
||
|
||
const keywordCacheHit = await resolveTaskWorldInfo({
|
||
settings: {
|
||
worldInfoFilterMode: "custom",
|
||
worldInfoFilterCustomKeywords: "常驻,缓存探针",
|
||
},
|
||
templateContext: {
|
||
recentMessages: "我们继续调查那条线索",
|
||
charName: "Alice",
|
||
},
|
||
userMessage: "继续调查",
|
||
});
|
||
assert.equal(keywordCacheHit.debug.cache.hit, true);
|
||
assert.equal(keywordCacheHit.debug.customFilter.filteredEntryCount, 1);
|
||
assert.equal(
|
||
keywordCacheHit.debug.customFilter.filteredEntries[0].name,
|
||
"常驻设定",
|
||
);
|
||
|
||
delete globalThis.Mvu;
|
||
|
||
const defaultModeWithKeywords = await resolveTaskWorldInfo({
|
||
settings: {
|
||
worldInfoFilterMode: "default",
|
||
worldInfoFilterCustomKeywords: "常驻",
|
||
},
|
||
templateContext: {
|
||
recentMessages: "我们继续调查那条线索",
|
||
charName: "Alice",
|
||
},
|
||
userMessage: "继续调查",
|
||
});
|
||
assert.equal(
|
||
defaultModeWithKeywords.beforeEntries.some(
|
||
(entry) => entry.sourceName === "常驻设定",
|
||
),
|
||
true,
|
||
);
|
||
assert.equal(defaultModeWithKeywords.debug.mvu.filteredEntryCount > 0, true);
|
||
assert.equal(defaultModeWithKeywords.debug.customFilter.filteredEntryCount, 0);
|
||
|
||
const settings = {
|
||
taskProfiles: {
|
||
recall: {
|
||
activeProfileId: "custom",
|
||
profiles: [
|
||
{
|
||
id: "custom",
|
||
name: "测试预设",
|
||
taskType: "recall",
|
||
builtin: false,
|
||
blocks: [
|
||
{
|
||
id: "b1",
|
||
type: "builtin",
|
||
sourceKey: "worldInfoBefore",
|
||
role: "system",
|
||
enabled: true,
|
||
order: 0,
|
||
injectionMode: "append",
|
||
},
|
||
{
|
||
id: "b2",
|
||
type: "builtin",
|
||
sourceKey: "worldInfoAfter",
|
||
role: "system",
|
||
enabled: true,
|
||
order: 1,
|
||
injectionMode: "append",
|
||
},
|
||
{
|
||
id: "b3",
|
||
type: "custom",
|
||
content: "角色: {{charName}}",
|
||
role: "user",
|
||
enabled: true,
|
||
order: 2,
|
||
injectionMode: "append",
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
},
|
||
};
|
||
|
||
const promptBuild = await buildTaskPrompt(settings, "recall", {
|
||
taskName: "recall",
|
||
userMessage: "继续调查",
|
||
recentMessages: "我们继续调查那条线索",
|
||
charName: "Alice",
|
||
});
|
||
|
||
assert.match(promptBuild.systemPrompt, /这里是常驻世界设定/);
|
||
assert.match(promptBuild.systemPrompt, /控制摘要:隐藏线索:Alice 正在调查/);
|
||
assert.match(
|
||
promptBuild.systemPrompt,
|
||
/数据摘要:线索=蓝钥匙;情绪=紧张;角色=Alice;用户=User;上下文=我们继续调查那条线索/,
|
||
);
|
||
assert.match(promptBuild.systemPrompt, /扩展语义只是普通文本/);
|
||
assert.match(promptBuild.systemPrompt, /来自 bonus-book 的补充内容/);
|
||
assert.match(promptBuild.systemPrompt, /MVU lazy:/);
|
||
assert.doesNotMatch(promptBuild.systemPrompt, /getwi|<%=?/);
|
||
assert.doesNotMatch(promptBuild.systemPrompt, /status_current_variable|变量更新规则|updatevariable/i);
|
||
assert.equal(
|
||
promptBuild.privateTaskMessages.length,
|
||
2,
|
||
"custom user block + atDepth world info should both enter private task messages",
|
||
);
|
||
assert.deepEqual(
|
||
promptBuild.privateTaskMessages.map((message) => message.role),
|
||
["system", "user"],
|
||
);
|
||
assert.equal(
|
||
promptBuild.privateTaskMessages[0].content,
|
||
"这是一条 atDepth 消息。",
|
||
);
|
||
assert.deepEqual(
|
||
promptBuild.hostInjections.before.map((entry) => entry.name),
|
||
[
|
||
"常驻设定",
|
||
"EJS 汇总",
|
||
"数据 EJS 汇总",
|
||
"扩展语义正文",
|
||
"外部书汇总",
|
||
"MVU 懒加载探测",
|
||
],
|
||
);
|
||
assert.deepEqual(
|
||
promptBuild.hostInjections.after.map((entry) => entry.name),
|
||
["强制后置"],
|
||
);
|
||
assert.equal(promptBuild.hostInjections.atDepth.length, 1);
|
||
assert.equal(promptBuild.hostInjections.atDepth[0].depth, 2);
|
||
assert.equal(promptBuild.hostInjectionPlan.before.length, 1);
|
||
assert.equal(promptBuild.hostInjectionPlan.before[0].blockId, "b1");
|
||
assert.equal(promptBuild.hostInjectionPlan.before[0].sourceKey, "worldInfoBefore");
|
||
assert.deepEqual(promptBuild.hostInjectionPlan.before[0].entryNames, [
|
||
"常驻设定",
|
||
"EJS 汇总",
|
||
"数据 EJS 汇总",
|
||
"扩展语义正文",
|
||
"外部书汇总",
|
||
"MVU 懒加载探测",
|
||
]);
|
||
assert.equal(promptBuild.hostInjectionPlan.after.length, 1);
|
||
assert.equal(promptBuild.hostInjectionPlan.after[0].blockId, "b2");
|
||
assert.equal(promptBuild.hostInjectionPlan.after[0].sourceKey, "worldInfoAfter");
|
||
assert.deepEqual(promptBuild.hostInjectionPlan.after[0].entryNames, ["强制后置"]);
|
||
assert.equal(promptBuild.hostInjectionPlan.atDepth.length, 1);
|
||
assert.equal(promptBuild.hostInjectionPlan.atDepth[0].entryName, "深度注入");
|
||
assert.equal(typeof promptBuild.debug.worldInfoCacheHit, "boolean");
|
||
assert.equal(promptBuild.executionMessages.length, 4);
|
||
assert.deepEqual(
|
||
promptBuild.executionMessages.map((message) => message.role),
|
||
["system", "system", "system", "user"],
|
||
);
|
||
assert.equal(
|
||
promptBuild.executionMessages[0].content,
|
||
"这是一条 atDepth 消息。",
|
||
);
|
||
assert.deepEqual(
|
||
promptBuild.renderedBlocks.map((block) => block.delivery),
|
||
["private.system", "private.system", "private.message"],
|
||
);
|
||
assert.equal(promptBuild.additionalMessages.length, 1);
|
||
assert.equal(promptBuild.additionalMessages[0].content, "这是一条 atDepth 消息。");
|
||
assert.equal(promptBuild.debug.mvu.sanitizedFieldCount >= 0, true);
|
||
|
||
const customPromptBuild = await buildTaskPrompt(
|
||
{
|
||
...settings,
|
||
worldInfoFilterMode: "custom",
|
||
worldInfoFilterCustomKeywords: "",
|
||
},
|
||
"recall",
|
||
{
|
||
taskName: "recall",
|
||
userMessage: "继续调查",
|
||
recentMessages: "我们继续调查那条线索",
|
||
charName: "Alice",
|
||
},
|
||
);
|
||
assert.match(
|
||
customPromptBuild.systemPrompt,
|
||
/<status_current_variable>secret=true<\/status_current_variable>/,
|
||
);
|
||
assert.match(customPromptBuild.systemPrompt, /这一条不应该进入结果/);
|
||
assert.match(customPromptBuild.systemPrompt, /控制摘要:隐藏线索:Alice 正在调查/);
|
||
const customPayload = buildTaskLlmPayload(customPromptBuild, "unused fallback");
|
||
assert.equal(
|
||
customPayload.promptMessages.some((message) =>
|
||
/<status_current_variable>secret=true<\/status_current_variable>/.test(
|
||
message.content,
|
||
),
|
||
),
|
||
true,
|
||
);
|
||
|
||
const interpolatedSettings = {
|
||
taskProfiles: {
|
||
recall: {
|
||
activeProfileId: "interpolated",
|
||
profiles: [
|
||
{
|
||
id: "interpolated",
|
||
name: "插值预设",
|
||
taskType: "recall",
|
||
builtin: false,
|
||
blocks: [
|
||
{
|
||
id: "interp-system",
|
||
type: "custom",
|
||
content: "世界书插值:\\n{{worldInfoBefore}}",
|
||
role: "system",
|
||
enabled: true,
|
||
order: 0,
|
||
injectionMode: "append",
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
},
|
||
worldInfoFilterMode: "custom",
|
||
worldInfoFilterCustomKeywords: "",
|
||
};
|
||
const customInterpolatedPromptBuild = await buildTaskPrompt(
|
||
interpolatedSettings,
|
||
"recall",
|
||
{
|
||
taskName: "recall",
|
||
userMessage: "继续调查",
|
||
recentMessages: "我们继续调查那条线索",
|
||
charName: "Alice",
|
||
},
|
||
);
|
||
assert.match(
|
||
customInterpolatedPromptBuild.systemPrompt,
|
||
/<status_current_variable>secret=true<\/status_current_variable>/,
|
||
);
|
||
const customInterpolatedPayload = buildTaskLlmPayload(
|
||
customInterpolatedPromptBuild,
|
||
"unused fallback",
|
||
);
|
||
assert.equal(
|
||
customInterpolatedPayload.promptMessages.some((message) =>
|
||
/<status_current_variable>secret=true<\/status_current_variable>/.test(
|
||
message.content,
|
||
),
|
||
),
|
||
true,
|
||
);
|
||
|
||
const noWorldInfoBlockSettings = {
|
||
taskProfiles: {
|
||
recall: {
|
||
activeProfileId: "custom",
|
||
profiles: [
|
||
{
|
||
id: "custom",
|
||
name: "无世界书显式块",
|
||
taskType: "recall",
|
||
builtin: false,
|
||
blocks: [
|
||
{
|
||
id: "u1",
|
||
type: "custom",
|
||
content: "角色: {{charName}}",
|
||
role: "user",
|
||
enabled: true,
|
||
order: 0,
|
||
injectionMode: "append",
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
},
|
||
};
|
||
|
||
const atDepthOnlyPromptBuild = await buildTaskPrompt(
|
||
noWorldInfoBlockSettings,
|
||
"recall",
|
||
{
|
||
taskName: "recall",
|
||
userMessage: "继续调查",
|
||
recentMessages: "我们继续调查那条线索",
|
||
charName: "Alice",
|
||
},
|
||
);
|
||
|
||
assert.equal(atDepthOnlyPromptBuild.debug.worldInfoRequested, true);
|
||
assert.equal(atDepthOnlyPromptBuild.debug.worldInfoAtDepthCount, 1);
|
||
assert.equal(atDepthOnlyPromptBuild.additionalMessages.length, 1);
|
||
assert.equal(
|
||
atDepthOnlyPromptBuild.additionalMessages[0].content,
|
||
"这是一条 atDepth 消息。",
|
||
);
|
||
assert.deepEqual(
|
||
atDepthOnlyPromptBuild.executionMessages.map((message) => message.role),
|
||
["system", "user"],
|
||
);
|
||
assert.equal(
|
||
atDepthOnlyPromptBuild.executionMessages[0].content,
|
||
"这是一条 atDepth 消息。",
|
||
);
|
||
|
||
const depthD4Entry = createWorldbookEntry({
|
||
uid: 201,
|
||
name: "深度注入 D4",
|
||
comment: "深度注入 D4",
|
||
content: "这是 d4 atDepth 消息。",
|
||
positionType: "at_depth_as_system",
|
||
depth: 4,
|
||
order: 8,
|
||
});
|
||
const depthD1Entry = createWorldbookEntry({
|
||
uid: 202,
|
||
name: "深度注入 D1",
|
||
comment: "深度注入 D1",
|
||
content: "这是 d1 atDepth 消息。",
|
||
positionType: "at_depth_as_system",
|
||
depth: 1,
|
||
order: 3,
|
||
});
|
||
worldbooksByName["main-book"].push(depthD4Entry, depthD1Entry);
|
||
const previousGetContext = globalThis.SillyTavern.getContext;
|
||
globalThis.SillyTavern.getContext = () => ({
|
||
...previousGetContext(),
|
||
chatId: "depth-aware-chat",
|
||
});
|
||
|
||
const depthAwareSettings = {
|
||
taskProfiles: {
|
||
recall: {
|
||
activeProfileId: "depth-aware",
|
||
profiles: [
|
||
{
|
||
id: "depth-aware",
|
||
name: "深度顺序预设",
|
||
taskType: "recall",
|
||
builtin: false,
|
||
blocks: [
|
||
{
|
||
id: "depth-recent",
|
||
type: "builtin",
|
||
sourceKey: "recentMessages",
|
||
role: "system",
|
||
enabled: true,
|
||
order: 0,
|
||
injectionMode: "append",
|
||
},
|
||
{
|
||
id: "depth-user",
|
||
type: "custom",
|
||
content: "用户问题:{{userMessage}}",
|
||
role: "user",
|
||
enabled: true,
|
||
order: 1,
|
||
injectionMode: "append",
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
},
|
||
};
|
||
|
||
const depthAwarePromptBuild = await buildTaskPrompt(depthAwareSettings, "recall", {
|
||
taskName: "recall",
|
||
userMessage: "继续调查 depth 排序",
|
||
recentMessages: "这里会被 chatMessages 替换",
|
||
chatMessages: [
|
||
{ seq: 11, role: "user", content: "第一句" },
|
||
{ seq: 12, role: "assistant", content: "第二句" },
|
||
],
|
||
charName: "Alice",
|
||
});
|
||
|
||
assert.deepEqual(
|
||
depthAwarePromptBuild.executionMessages.map((message) => message.content),
|
||
[
|
||
"#1 [assistant]: 这是 d4 atDepth 消息。\n\n#2 [assistant]: 这是一条 atDepth 消息。\n\n#11 [user]: 第一句\n\n#4 [assistant]: 这是 d1 atDepth 消息。\n\n#12 [assistant]: 第二句",
|
||
"用户问题:继续调查 depth 排序",
|
||
],
|
||
);
|
||
assert.deepEqual(
|
||
depthAwarePromptBuild.hostInjections.atDepth.map((entry) => entry.name),
|
||
["深度注入 D4", "深度注入", "深度注入 D1"],
|
||
);
|
||
assert.deepEqual(
|
||
depthAwarePromptBuild.hostInjectionPlan.atDepth.map((entry) => entry.entryName),
|
||
["深度注入 D4", "深度注入", "深度注入 D1"],
|
||
);
|
||
assert.equal(
|
||
depthAwarePromptBuild.executionMessages.at(-1)?.content.includes("atDepth"),
|
||
false,
|
||
);
|
||
worldbooksByName["main-book"].splice(-2, 2);
|
||
globalThis.SillyTavern.getContext = previousGetContext;
|
||
|
||
const { initializeHostAdapter } = await import("../host/adapter/index.js");
|
||
const partialBridgeCalls = [];
|
||
const partialBridgeEntriesByWorldbook = {
|
||
"main-book": [createConstantWorldbookEntry(11, "主书原名", "主书内容。", "主书注释")],
|
||
"side-book": [createConstantWorldbookEntry(12, "支线原名", "支线内容。", "支线注释")],
|
||
"persona-book": [createConstantWorldbookEntry(13, "人格原名", "人格内容。", "人格注释")],
|
||
"chat-book": [createConstantWorldbookEntry(14, "聊天原名", "聊天内容。", "聊天注释")],
|
||
};
|
||
|
||
globalThis.SillyTavern = {
|
||
getContext() {
|
||
return {
|
||
name1: "User",
|
||
name2: "Alice",
|
||
chat: [{ is_user: true, mes: "我们继续调查那条线索" }],
|
||
chatMetadata: {
|
||
world: "chat-book",
|
||
},
|
||
extensionSettings: {
|
||
persona_description_lorebook: "persona-book",
|
||
},
|
||
};
|
||
},
|
||
};
|
||
globalThis.getCharWorldbookNames = () => ({
|
||
primary: "main-book",
|
||
additional: ["side-book"],
|
||
});
|
||
globalThis.getWorldbook = async () => {
|
||
throw new Error(
|
||
"legacy getWorldbook should not be used when bridge getWorldbook is available",
|
||
);
|
||
};
|
||
globalThis.getLorebookEntries = async (worldbookName) =>
|
||
(partialBridgeEntriesByWorldbook[worldbookName] || []).map((entry) => ({
|
||
uid: entry.uid,
|
||
comment: entry.comment,
|
||
}));
|
||
|
||
initializeHostAdapter({
|
||
worldbookProvider: {
|
||
async getWorldbook(worldbookName) {
|
||
partialBridgeCalls.push(worldbookName);
|
||
return partialBridgeEntriesByWorldbook[worldbookName] || [];
|
||
},
|
||
},
|
||
});
|
||
|
||
const partialBridgeWorldInfo = await resolveTaskWorldInfo({
|
||
templateContext: {
|
||
recentMessages: "我们继续调查那条线索",
|
||
charName: "Alice",
|
||
},
|
||
userMessage: "继续调查",
|
||
});
|
||
|
||
assert.deepEqual(partialBridgeCalls, [
|
||
"main-book",
|
||
"side-book",
|
||
"persona-book",
|
||
"chat-book",
|
||
]);
|
||
assert.deepEqual(
|
||
partialBridgeWorldInfo.beforeEntries.map((entry) => entry.name).sort(),
|
||
["主书注释", "支线注释", "人格注释", "聊天注释"].sort(),
|
||
);
|
||
|
||
console.log("task-worldinfo tests passed");
|
||
} finally {
|
||
if (originalSillyTavern === undefined) {
|
||
delete globalThis.SillyTavern;
|
||
} else {
|
||
globalThis.SillyTavern = originalSillyTavern;
|
||
}
|
||
|
||
if (originalEjsTemplate === undefined) {
|
||
delete globalThis.EjsTemplate;
|
||
} else {
|
||
globalThis.EjsTemplate = originalEjsTemplate;
|
||
}
|
||
|
||
if (originalMvu === undefined) {
|
||
delete globalThis.Mvu;
|
||
} else {
|
||
globalThis.Mvu = originalMvu;
|
||
}
|
||
|
||
if (originalGetCharWorldbookNames === undefined) {
|
||
delete globalThis.getCharWorldbookNames;
|
||
} else {
|
||
globalThis.getCharWorldbookNames = originalGetCharWorldbookNames;
|
||
}
|
||
|
||
if (originalGetWorldbook === undefined) {
|
||
delete globalThis.getWorldbook;
|
||
} else {
|
||
globalThis.getWorldbook = originalGetWorldbook;
|
||
}
|
||
|
||
if (originalGetLorebookEntries === undefined) {
|
||
delete globalThis.getLorebookEntries;
|
||
} else {
|
||
globalThis.getLorebookEntries = originalGetLorebookEntries;
|
||
}
|
||
|
||
try {
|
||
const { initializeHostAdapter } = await import("../host/adapter/index.js");
|
||
initializeHostAdapter({});
|
||
} catch {
|
||
// ignore reset failures in test cleanup
|
||
}
|
||
}
|