Fix MVU stripping order for world info prompts

This commit is contained in:
Youzini-afk
2026-03-29 12:07:19 +08:00
parent 562c226b1d
commit 6cec03182b
4 changed files with 649 additions and 103 deletions

View File

@@ -11,11 +11,24 @@ const MVU_STATUS_CURRENT_VARIABLE_REPLACE_REGEX =
/\n?<status_current_variables?>[\s\S]*?<\/status_current_variables?>/gi;
const MVU_STATUS_CURRENT_VARIABLE_DETECT_REGEX =
/<status_current_variables?>[\s\S]*?<\/status_current_variables?>/i;
const MVU_MESSAGE_VARIABLE_MACRO_REGEX =
/\{\{\s*get_message_variable::(?:stat_data|display_data|delta_data)(?:\.[^}]+)?\s*}}/gi;
const MVU_GETVAR_REFERENCE_REGEX =
/getvar\(\s*["'](?:stat_data|display_data|delta_data)["']\s*\)/gi;
const MVU_STATEFUL_TEMPLATE_TAG_REGEX =
/<%[-=]?[\s\S]*?(?:SafeGetValue|getvar\(\s*["'](?:stat_data|display_data|delta_data)["']\s*\)|\b(?:stat_data|display_data|delta_data)\b)[\s\S]*?%>/gi;
const EJS_TEMPLATE_TAG_REGEX = /<%[-=]?[\s\S]*?%>/gi;
const MVU_VARIABLE_OUTPUT_ENTRY_REGEX = /变量输出格式:\s*[\s\S]*?<UpdateVariable>/i;
const MVU_VARIABLE_RULES_ENTRY_REGEX =
/变量更新规则:\s*[\s\S]*?(?:type:\s*|check:\s*|当前时间:|近期事务:)/i;
const MVU_FORMAT_EMPHASIS_ENTRY_REGEX =
/(?:变量输出格式强调|格式强调[:]?-?变量更新规则|格式强调[:]?-?剧情演绎|The following must be inserted to the end of (?:each )?reply,? and cannot be omitted)[\s\S]*?format:\s*\|-?/i;
const MVU_STATE_OBJECT_FIELD_REGEX =
/["']?(?:stat_data|display_data|delta_data)["']?\s*:/i;
const MVU_STATE_PATH_REFERENCE_REGEX =
/\b(?:stat_data|display_data|delta_data)(?:\.[\w$\u4e00-\u9fff\[\]"'-]+){1,}/i;
const MVU_STATE_HELPER_REFERENCE_REGEX =
/\b(?:SafeGetValue\([^)]*(?:stat_data|display_data|delta_data)[^)]*\)|message_data\[\d+\]\.data\.(?:stat_data|display_data|delta_data))\b/i;
function uniq(values = []) {
return [...new Set((Array.isArray(values) ? values : []).filter(Boolean))];
@@ -45,6 +58,14 @@ function countRegexMatches(text = "", regex) {
return count;
}
function matchesRegex(text = "", regex) {
if (!text || !(regex instanceof RegExp)) {
return false;
}
return new RegExp(regex.source, regex.flags).test(text);
}
function stripMvuPromptArtifactsDetailed(content = "") {
const input = normalizeText(content);
if (!input) {
@@ -55,15 +76,28 @@ function stripMvuPromptArtifactsDetailed(content = "") {
};
}
const statefulTemplateTagCount = countRegexMatches(
input,
MVU_STATEFUL_TEMPLATE_TAG_REGEX,
);
const artifactRemovedCount =
countRegexMatches(input, MVU_UPDATE_BLOCK_REGEX) +
countRegexMatches(input, MVU_STATUS_PLACEHOLDER_REGEX) +
countRegexMatches(input, MVU_STATUS_CURRENT_VARIABLE_REPLACE_REGEX);
countRegexMatches(input, MVU_STATUS_CURRENT_VARIABLE_REPLACE_REGEX) +
countRegexMatches(input, MVU_MESSAGE_VARIABLE_MACRO_REGEX) +
countRegexMatches(input, MVU_GETVAR_REFERENCE_REGEX) +
statefulTemplateTagCount;
const stripped = input
let stripped = input
.replace(MVU_UPDATE_BLOCK_REGEX, "")
.replace(MVU_STATUS_PLACEHOLDER_REGEX, "")
.replace(MVU_STATUS_CURRENT_VARIABLE_REPLACE_REGEX, "");
.replace(MVU_STATUS_CURRENT_VARIABLE_REPLACE_REGEX, "")
.replace(MVU_MESSAGE_VARIABLE_MACRO_REGEX, "")
.replace(MVU_GETVAR_REFERENCE_REGEX, "")
.replace(MVU_STATEFUL_TEMPLATE_TAG_REGEX, "");
if (statefulTemplateTagCount > 0) {
stripped = stripped.replace(EJS_TEMPLATE_TAG_REGEX, "");
}
const normalized = collapseWhitespace(stripped);
return {
@@ -125,12 +159,27 @@ export function isLikelyMvuWorldInfoContent(content = "") {
if (!normalized) {
return false;
}
const stateKeyMentionCount =
normalized.match(/\b(?:stat_data|display_data|delta_data)\b/gi)?.length || 0;
const stateSignals = [
MVU_MESSAGE_VARIABLE_MACRO_REGEX,
MVU_GETVAR_REFERENCE_REGEX,
MVU_STATE_OBJECT_FIELD_REGEX,
MVU_STATE_PATH_REFERENCE_REGEX,
MVU_STATE_HELPER_REFERENCE_REGEX,
].reduce(
(count, pattern) => count + (matchesRegex(normalized, pattern) ? 1 : 0),
0,
);
return (
MVU_STATUS_CURRENT_VARIABLE_DETECT_REGEX.test(normalized) ||
MVU_VARIABLE_OUTPUT_ENTRY_REGEX.test(normalized) ||
MVU_VARIABLE_RULES_ENTRY_REGEX.test(normalized) ||
MVU_FORMAT_EMPHASIS_ENTRY_REGEX.test(normalized)
matchesRegex(normalized, MVU_STATUS_CURRENT_VARIABLE_DETECT_REGEX) ||
matchesRegex(normalized, MVU_VARIABLE_OUTPUT_ENTRY_REGEX) ||
matchesRegex(normalized, MVU_VARIABLE_RULES_ENTRY_REGEX) ||
matchesRegex(normalized, MVU_FORMAT_EMPHASIS_ENTRY_REGEX) ||
stateSignals >= 2 ||
(stateSignals >= 1 && stateKeyMentionCount >= 2)
);
}

View File

@@ -19,6 +19,7 @@ const WORLD_INFO_VARIABLE_KEYS = [
const INPUT_CONTEXT_MVU_FIELDS = [
"userMessage",
"recentMessages",
"chatMessages",
"dialogueText",
"candidateText",
"candidateNodes",
@@ -34,6 +35,7 @@ const INPUT_CONTEXT_MVU_FIELDS = [
const INPUT_REGEX_STAGE_BY_FIELD = {
userMessage: "input.userMessage",
recentMessages: "input.recentMessages",
chatMessages: "input.recentMessages",
dialogueText: "input.recentMessages",
candidateText: "input.candidateText",
candidateNodes: "input.candidateText",
@@ -263,13 +265,23 @@ function sanitizeTaskPromptText(
regexStage = "",
role = "system",
regexCollector = null,
applyMvu = true,
} = {},
) {
const originalText = typeof text === "string" ? text : "";
const mvuResult = sanitizeMvuContent(originalText, {
mode,
blockedContents,
});
const mvuResult = applyMvu
? sanitizeMvuContent(originalText, {
mode,
blockedContents,
})
: {
text: originalText,
changed: false,
dropped: false,
reasons: [],
blockedHitCount: 0,
artifactRemovedCount: 0,
};
const afterMvu = String(mvuResult.text || "");
const finalText = regexStage
? applyTaskRegex(
@@ -292,58 +304,277 @@ function sanitizeTaskPromptText(
};
}
function sanitizeChatMessageList(
settings = {},
taskType,
chatMessages = [],
debugState = null,
regexCollector = null,
) {
if (!Array.isArray(chatMessages) || chatMessages.length === 0) {
return [];
function joinStructuredPath(basePath = "", segment = "") {
const normalizedSegment = String(segment || "");
if (!normalizedSegment) {
return basePath;
}
if (!basePath) {
return normalizedSegment.startsWith("[")
? normalizedSegment.slice(1, -1)
: normalizedSegment;
}
return normalizedSegment.startsWith("[")
? `${basePath}${normalizedSegment}`
: `${basePath}.${normalizedSegment}`;
}
function looksLikeMvuStateContainer(value, seen = new WeakSet()) {
if (!value || typeof value !== "object") {
return false;
}
if (seen.has(value)) {
return false;
}
seen.add(value);
if (Array.isArray(value)) {
return value.some((item) => looksLikeMvuStateContainer(item, seen));
}
return chatMessages
const keys = Object.keys(value).map((key) =>
String(key || "").trim().toLowerCase(),
);
if (
keys.some((key) =>
["stat_data", "display_data", "delta_data", "$internal"].includes(key),
)
) {
return true;
}
return Object.values(value).some((item) =>
looksLikeMvuStateContainer(item, seen),
);
}
function getMvuObjectKeyStripReason(key, value) {
const normalizedKey = String(key || "").trim().toLowerCase();
if (
["stat_data", "display_data", "delta_data", "$internal"].includes(
normalizedKey,
)
) {
return "mvu_state_key_removed";
}
if (
["variables", "message_variables", "chat_variables"].includes(normalizedKey) &&
looksLikeMvuStateContainer(value)
) {
return "mvu_variables_container_removed";
}
return "";
}
function sanitizeStructuredPromptValue(
settings = {},
taskType,
value,
{
fieldName = "",
path = fieldName,
mode = "aggressive",
blockedContents = [],
regexStage = "",
role = "system",
debugState = null,
regexCollector = null,
applyMvu = true,
stripMvuContainers = true,
seen = new WeakSet(),
} = {},
) {
if (typeof value === "string") {
const sanitized = sanitizeTaskPromptText(settings, taskType, value, {
mode,
blockedContents,
regexStage,
role,
regexCollector,
applyMvu,
});
pushMvuPromptDebugEntry(debugState, {
name: path || fieldName,
stage: regexStage,
...sanitized,
});
return {
value: sanitized.text,
changed: Boolean(sanitized.changed || sanitized.dropped),
omit:
!String(sanitized.text || "").trim() &&
String(value || "").trim().length > 0,
};
}
if (Array.isArray(value)) {
const sanitizedArray = [];
let changed = false;
for (let index = 0; index < value.length; index += 1) {
const childResult = sanitizeStructuredPromptValue(
settings,
taskType,
value[index],
{
fieldName,
path: joinStructuredPath(path, `[${index}]`),
mode,
blockedContents,
regexStage,
role,
debugState,
regexCollector,
applyMvu,
stripMvuContainers,
seen,
},
);
if (childResult.omit) {
changed = true;
continue;
}
sanitizedArray.push(childResult.value);
if (childResult.changed) {
changed = true;
}
}
return {
value: sanitizedArray,
changed: changed || sanitizedArray.length !== value.length,
omit: value.length > 0 && sanitizedArray.length === 0,
};
}
if (value && typeof value === "object") {
if (seen.has(value)) {
return {
value,
changed: false,
omit: false,
};
}
seen.add(value);
const originalLooksMvuContainer = looksLikeMvuStateContainer(value);
const sanitizedObject = {};
let changed = false;
let keptEntries = 0;
for (const [key, entryValue] of Object.entries(value)) {
const stripReason = stripMvuContainers
? getMvuObjectKeyStripReason(key, entryValue)
: "";
if (stripReason) {
changed = true;
pushMvuPromptDebugEntry(debugState, {
name: joinStructuredPath(path, key),
stage: regexStage,
changed: true,
dropped: true,
reasons: [stripReason],
blockedHitCount: 0,
});
continue;
}
const childResult = sanitizeStructuredPromptValue(
settings,
taskType,
entryValue,
{
fieldName,
path: joinStructuredPath(path, key),
mode,
blockedContents,
regexStage,
role,
debugState,
regexCollector,
applyMvu,
stripMvuContainers,
seen,
},
);
if (childResult.omit) {
changed = true;
continue;
}
sanitizedObject[key] = childResult.value;
keptEntries += 1;
if (childResult.changed) {
changed = true;
}
}
return {
value: sanitizedObject,
changed,
omit: originalLooksMvuContainer && keptEntries === 0,
};
}
return {
value,
changed: false,
omit: false,
};
}
function sanitizePromptMessages(
settings = {},
taskType,
messages = [],
{
blockedContents = [],
regexStage = "input.finalPrompt",
debugState = null,
regexCollector = null,
} = {},
) {
return (Array.isArray(messages) ? messages : [])
.map((message, index) => {
const rawContent =
typeof message === "string"
? message
: typeof message?.content === "string"
? message.content
: typeof message?.mes === "string"
? message.mes
: "";
const sanitized = sanitizeTaskPromptText(settings, taskType, rawContent, {
mode: "aggressive",
regexStage: "input.recentMessages",
role: "system",
regexCollector,
});
pushMvuPromptDebugEntry(debugState, {
name: `chatMessages[${index}]`,
stage: "input.recentMessages",
...sanitized,
});
if (!sanitized.text.trim()) {
const sanitized = sanitizeStructuredPromptValue(
settings,
taskType,
message,
{
fieldName: "message",
path: `message[${index}]`,
mode: "final-safe",
blockedContents,
regexStage,
role: message?.role || "system",
debugState,
regexCollector,
},
);
if (debugState && (sanitized.changed || sanitized.omit)) {
debugState.finalMessageStripCount += 1;
}
if (sanitized.omit) {
return null;
}
if (typeof message === "string") {
return sanitized.text;
}
if (message && typeof message === "object") {
return {
...message,
content:
typeof message.content === "string"
? sanitized.text
: message.content,
mes:
typeof message.mes === "string"
? sanitized.text
: message.mes,
};
}
return null;
const executionMessage = createExecutionMessage(
sanitized.value?.role || message?.role,
sanitized.value?.content,
{
source: String(sanitized.value?.source || message?.source || ""),
blockId: String(sanitized.value?.blockId || message?.blockId || ""),
blockName: String(
sanitized.value?.blockName || message?.blockName || "",
),
blockType: String(
sanitized.value?.blockType || message?.blockType || "",
),
sourceKey: String(
sanitized.value?.sourceKey || message?.sourceKey || "",
),
injectionMode: String(
sanitized.value?.injectionMode || message?.injectionMode || "",
),
},
);
return executionMessage;
})
.filter(Boolean);
}
@@ -354,39 +585,45 @@ function sanitizePromptContextInputs(
context = {},
debugState = null,
regexCollector = null,
options = {},
) {
const sanitizedContext = {
...context,
};
const {
applyMvu = true,
stripMvuContainers = applyMvu,
} = options || {};
for (const fieldName of INPUT_CONTEXT_MVU_FIELDS) {
const value = sanitizedContext[fieldName];
if (typeof value !== "string") {
if (!(fieldName in sanitizedContext)) {
continue;
}
const value = sanitizedContext[fieldName];
const regexStage = INPUT_REGEX_STAGE_BY_FIELD[fieldName] || "";
const sanitized = sanitizeTaskPromptText(settings, taskType, value, {
mode: "aggressive",
regexStage,
role: "system",
regexCollector,
});
sanitizedContext[fieldName] = sanitized.text;
pushMvuPromptDebugEntry(debugState, {
name: fieldName,
stage: regexStage,
...sanitized,
});
}
if (Array.isArray(sanitizedContext.chatMessages)) {
sanitizedContext.chatMessages = sanitizeChatMessageList(
const sanitized = sanitizeStructuredPromptValue(
settings,
taskType,
sanitizedContext.chatMessages,
debugState,
regexCollector,
value,
{
fieldName,
path: fieldName,
mode: "aggressive",
regexStage,
role: "system",
debugState,
regexCollector,
applyMvu,
stripMvuContainers,
},
);
sanitizedContext[fieldName] = sanitized.omit
? Array.isArray(value)
? []
: typeof value === "string"
? ""
: null
: sanitized.value;
}
return sanitizedContext;
@@ -759,7 +996,19 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
const profile = getActiveTaskProfile(settings, taskType);
const legacyPrompt = getLegacyPromptForTask(settings, taskType);
const promptRegexInput = { entries: [] };
const worldInfoRegexInput = { entries: [] };
const mvuPromptDebug = createEmptyMvuPromptDebug();
const worldInfoInputContext = sanitizePromptContextInputs(
settings,
taskType,
context,
null,
worldInfoRegexInput,
{
applyMvu: false,
stripMvuContainers: false,
},
);
const sanitizedInputContext = sanitizePromptContextInputs(
settings,
taskType,
@@ -788,9 +1037,9 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
if (worldInfoRequested) {
const worldInfo = await resolveTaskWorldInfo({
settings,
chatMessages: extractWorldInfoChatMessages(sanitizedInputContext),
userMessage: String(sanitizedInputContext.userMessage || ""),
templateContext: sanitizedInputContext,
chatMessages: extractWorldInfoChatMessages(worldInfoInputContext),
userMessage: String(worldInfoInputContext.userMessage || ""),
templateContext: worldInfoInputContext,
});
const sanitizedWorldInfo = sanitizeWorldInfoContext(
settings,
@@ -959,7 +1208,7 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
executionMessages,
privateTaskMessages,
renderedBlocks,
regexInput: promptRegexInput,
regexInput: mergeRegexCollectors(promptRegexInput, worldInfoRegexInput),
worldInfoResolution,
systemPrompt,
customMessages,
@@ -1019,6 +1268,7 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
effectivePath: {
promptAssembly: "ordered-private-messages",
hostInjectionPlan: "diagnostic-plan-only",
worldInfoInputContext: "raw-context-for-trigger-and-ejs",
ejs:
worldInfoResolution.debug?.ejsRuntimeStatus ||
"unknown",
@@ -1053,7 +1303,7 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
hostInjectionPlan,
worldInfoResolution,
mvu: result.debug.mvu,
regexInput: promptRegexInput,
regexInput: result.regexInput,
debug: result.debug,
});
@@ -1062,7 +1312,11 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
export function buildTaskLlmPayload(promptBuild = null, fallbackUserPrompt = "") {
const runtimeMvu = promptBuild?.__mvuRuntime || {};
const executionMessages = Array.isArray(promptBuild?.executionMessages)
const taskType = String(promptBuild?.debug?.taskType || "");
const blockedContents = Array.isArray(runtimeMvu?.blockedContents)
? runtimeMvu.blockedContents
: [];
const rawExecutionMessages = Array.isArray(promptBuild?.executionMessages)
? promptBuild.executionMessages
.map((message) =>
createExecutionMessage(message.role, message.content, {
@@ -1076,6 +1330,14 @@ export function buildTaskLlmPayload(promptBuild = null, fallbackUserPrompt = "")
)
.filter(Boolean)
: [];
const executionMessages = sanitizePromptMessages(
{},
taskType,
rawExecutionMessages,
{
blockedContents,
},
);
const hasUserMessage = executionMessages.some(
(message) => message.role === "user",
@@ -1086,23 +1348,29 @@ export function buildTaskLlmPayload(promptBuild = null, fallbackUserPrompt = "")
String(fallbackUserPrompt || ""),
{
mode: "final-safe",
blockedContents: Array.isArray(runtimeMvu?.blockedContents)
? runtimeMvu.blockedContents
: [],
blockedContents,
},
).text;
const additionalMessages =
executionMessages.length > 0
? []
: sanitizePromptMessages(
{},
taskType,
Array.isArray(promptBuild?.privateTaskMessages)
? promptBuild.privateTaskMessages
: [],
{
blockedContents,
},
);
return {
systemPrompt:
executionMessages.length > 0 ? "" : String(promptBuild?.systemPrompt || ""),
userPrompt: hasUserMessage ? "" : sanitizedFallbackUserPrompt,
promptMessages: executionMessages,
additionalMessages:
executionMessages.length > 0
? []
: Array.isArray(promptBuild?.privateTaskMessages)
? promptBuild.privateTaskMessages
: [],
additionalMessages,
};
}

View File

@@ -20,6 +20,12 @@ assert.equal(
),
true,
);
assert.equal(
isLikelyMvuWorldInfoContent(
'{"stat_data":{"地点":"学校"},"display_data":{"地点":"教室"}}',
),
true,
);
assert.equal(isLikelyMvuWorldInfoContent("正常世界设定"), false);
const aggressive = sanitizeMvuContent(
@@ -45,6 +51,16 @@ assert.equal(finalSafe.dropped, false);
assert.equal(finalSafe.text, "说明文字\n尾巴");
assert.deepEqual(finalSafe.reasons, ["artifact_stripped"]);
const macroSafe = sanitizeMvuContent(
"地点={{get_message_variable::stat_data.地点}}\n<%- SafeGetValue(msg_data.地点) %>",
{
mode: "final-safe",
},
);
assert.equal(macroSafe.dropped, false);
assert.equal(macroSafe.text, "地点=");
assert.deepEqual(macroSafe.reasons, ["artifact_stripped"]);
const blocked = sanitizeMvuContent("前缀\n被拦截条目\n后缀", {
mode: "final-safe",
blockedContents: ["被拦截条目"],

View File

@@ -65,6 +65,9 @@ const originalExtensionSettings = globalThis.__promptBuilderMvuExtensionSettings
const originalContext = globalThis.__promptBuilderMvuContext;
const originalSendOpenAIRequest = globalThis.__promptBuilderMvuSendOpenAIRequest;
const originalFetch = globalThis.fetch;
const originalGetWorldbook = globalThis.getWorldbook;
const originalGetLorebookEntries = globalThis.getLorebookEntries;
const originalGetCharWorldbookNames = globalThis.getCharWorldbookNames;
globalThis.require = require;
globalThis.__promptBuilderMvuExtensionSettings = {
@@ -82,6 +85,38 @@ globalThis.__promptBuilderMvuContext = {
chatId: "mvu-test-chat",
};
function createWorldbookEntry({
uid,
name,
comment = name,
content,
strategyType = "constant",
keys = [],
enabled = true,
order = 10,
}) {
return {
uid,
name,
comment,
content,
enabled,
position: {
type: "before_character_definition",
role: "system",
depth: 0,
order,
},
strategy: {
type: strategyType,
keys,
keys_secondary: { logic: "and_any", keys: [] },
},
probability: 100,
extra: {},
};
}
try {
const extensionsApi = await import("../../../../extensions.js");
const { createDefaultTaskProfiles } = await import("../prompt-profiles.js");
@@ -150,6 +185,18 @@ try {
injectionMode: "append",
order: recallProfile.blocks.length,
});
recallProfile.blocks.push({
id: "mvu-chat-custom",
name: "聊天对象检查",
type: "custom",
enabled: true,
role: "system",
sourceKey: "",
sourceField: "",
content: "聊天对象 {{chatMessages}}",
injectionMode: "append",
order: recallProfile.blocks.length,
});
return {
llmApiUrl: "https://example.com/v1",
@@ -171,20 +218,62 @@ try {
userPersona: "变量更新规则:\ntype: state\n当前时间: 12:00",
recentMessages:
"最近消息 <status_current_variable>hp=3</status_current_variable> BAD_RECENT",
chatMessages: [
{
role: "assistant",
content: "聊天内容 BAD_RECENT",
variables: {
0: {
stat_data: { hp: [3, "状态更新"] },
display_data: { hp: "2->3" },
delta_data: { hp: "2->3" },
},
},
debugStatus: "{{get_message_variable::display_data.hp}} BAD_RECENT",
},
],
userMessage:
"用户输入 <updatevariable>secret</updatevariable> BAD_USER",
candidateNodes: "候选节点 BAD_CANDIDATE",
candidateText: "候选节点 BAD_CANDIDATE",
"用户输入 <updatevariable>secret</updatevariable> {{get_message_variable::stat_data.hp}} BAD_USER",
candidateNodes: [
{
id: "node-1",
summary: "候选节点 BAD_CANDIDATE <StatusPlaceHolderImpl/>",
variables: {
0: {
stat_data: { 地点: "学校" },
display_data: { 地点: "教室" },
},
},
note: "{{get_message_variable::stat_data.地点}} BAD_CANDIDATE",
},
],
candidateText:
"候选节点 BAD_CANDIDATE {{get_message_variable::stat_data.地点}}",
graphStats: "candidate_count=1",
});
assert.match(promptBuild.systemPrompt, /GOOD_RECENT/);
assert.match(JSON.stringify(promptBuild.executionMessages), /GOOD_USER/);
assert.match(JSON.stringify(promptBuild.executionMessages), /GOOD_CANDIDATE/);
assert.match(promptBuild.systemPrompt, /FINAL_GOOD/);
assert.equal(
promptBuild.debug.mvu.sanitizedFields.some((entry) => entry.name === "userMessage"),
true,
);
assert.equal(
promptBuild.debug.mvu.sanitizedFields.some((entry) =>
String(entry.name || "").startsWith("candidateNodes[0].variables"),
),
true,
);
assert.equal(
promptBuild.debug.mvu.sanitizedFields.some((entry) =>
String(entry.name || "").startsWith("chatMessages[0].variables"),
),
true,
);
assert.doesNotMatch(
JSON.stringify(promptBuild),
/status_current_variable|updatevariable|StatusPlaceHolderImpl/i,
/status_current_variable|updatevariable|StatusPlaceHolderImpl|stat_data|display_data|delta_data|get_message_variable/i,
);
assert.equal(promptBuild.debug.mvu.sanitizedFieldCount >= 4, true);
assert.equal(promptBuild.debug.mvu.finalMessageStripCount >= 1, true);
@@ -234,6 +323,112 @@ try {
);
assert.equal(systemOnlyPayload.userPrompt, "fallback text");
const rawWorldInfoEntries = [
createWorldbookEntry({
uid: 101,
name: "raw-trigger",
comment: "原始触发命中",
content: "世界书原始触发成功。",
strategyType: "selective",
keys: ["星火密令"],
order: 10,
}),
createWorldbookEntry({
uid: 102,
name: "raw-ejs",
comment: "原始 EJS 命中",
content:
'<%= user_input.includes("星火密令") ? "EJS 看到了原始 MVU 信号。" : "EJS 丢失了原始 MVU 信号。" %>',
order: 20,
}),
];
globalThis.getCharWorldbookNames = () => ({
primary: "mvu-raw-worldbook",
additional: [],
});
globalThis.getWorldbook = async (worldbookName) =>
worldbookName === "mvu-raw-worldbook" ? rawWorldInfoEntries : [];
globalThis.getLorebookEntries = async (worldbookName) =>
(worldbookName === "mvu-raw-worldbook" ? rawWorldInfoEntries : []).map(
(entry) => ({
uid: entry.uid,
comment: entry.comment,
}),
);
globalThis.__promptBuilderMvuContext = {
...globalThis.__promptBuilderMvuContext,
chatId: "mvu-raw-trigger-chat",
chatMetadata: {},
extensionSettings: {},
powerUserSettings: {},
};
const rawWorldInfoSettings = buildSettings();
rawWorldInfoSettings.taskProfiles.recall = {
activeProfileId: "raw-worldinfo",
profiles: [
{
id: "raw-worldinfo",
name: "raw worldinfo",
taskType: "recall",
builtin: false,
blocks: [
{
id: "wi-before",
name: "世界书前块",
type: "builtin",
enabled: true,
role: "system",
sourceKey: "worldInfoBefore",
sourceField: "",
content: "",
injectionMode: "append",
order: 0,
},
{
id: "recent-messages",
name: "最近消息",
type: "builtin",
enabled: true,
role: "system",
sourceKey: "recentMessages",
sourceField: "",
content: "",
injectionMode: "append",
order: 1,
},
],
generation: createDefaultTaskProfiles().recall.profiles[0].generation,
regex: {
enabled: false,
inheritStRegex: false,
stages: {},
localRules: [],
},
},
],
};
const rawWorldInfoPromptBuild = await buildTaskPrompt(rawWorldInfoSettings, "recall", {
taskName: "recall",
recentMessages: "最近消息",
userMessage:
"继续 <status_current_variable>星火密令</status_current_variable>",
chatMessages: [],
});
assert.match(rawWorldInfoPromptBuild.systemPrompt, /世界书原始触发成功/);
assert.match(rawWorldInfoPromptBuild.systemPrompt, /EJS 看到了原始 MVU 信号/);
assert.doesNotMatch(
rawWorldInfoPromptBuild.systemPrompt,
/status_current_variable/i,
);
assert.equal(
rawWorldInfoPromptBuild.debug.effectivePath?.worldInfoInputContext,
"raw-context-for-trigger-and-ejs",
);
const capturedBodies = [];
globalThis.fetch = async (_url, options = {}) => {
capturedBodies.push(JSON.parse(String(options.body || "{}")));
@@ -273,7 +468,7 @@ try {
assert.equal(capturedBodies.length, 1);
assert.doesNotMatch(
JSON.stringify(capturedBodies[0].messages),
/status_current_variable|updatevariable|StatusPlaceHolderImpl/i,
/status_current_variable|updatevariable|StatusPlaceHolderImpl|stat_data|display_data|delta_data|get_message_variable/i,
);
const runtimePromptBuild =
@@ -285,15 +480,15 @@ try {
assert.ok(runtimeLlmRequest);
assert.doesNotMatch(
JSON.stringify(runtimePromptBuild.executionMessages),
/status_current_variable|updatevariable|StatusPlaceHolderImpl/i,
/status_current_variable|updatevariable|StatusPlaceHolderImpl|stat_data|display_data|delta_data|get_message_variable/i,
);
assert.doesNotMatch(
JSON.stringify(runtimeLlmRequest.messages),
/status_current_variable|updatevariable|StatusPlaceHolderImpl/i,
/status_current_variable|updatevariable|StatusPlaceHolderImpl|stat_data|display_data|delta_data|get_message_variable/i,
);
assert.doesNotMatch(
JSON.stringify(runtimeLlmRequest.requestBody?.messages || []),
/status_current_variable|updatevariable|StatusPlaceHolderImpl/i,
/status_current_variable|updatevariable|StatusPlaceHolderImpl|stat_data|display_data|delta_data|get_message_variable/i,
);
assert.deepEqual(
runtimeLlmRequest.messages,
@@ -331,4 +526,22 @@ try {
}
globalThis.fetch = originalFetch;
if (originalGetWorldbook === undefined) {
delete globalThis.getWorldbook;
} else {
globalThis.getWorldbook = originalGetWorldbook;
}
if (originalGetLorebookEntries === undefined) {
delete globalThis.getLorebookEntries;
} else {
globalThis.getLorebookEntries = originalGetLorebookEntries;
}
if (originalGetCharWorldbookNames === undefined) {
delete globalThis.getCharWorldbookNames;
} else {
globalThis.getCharWorldbookNames = originalGetCharWorldbookNames;
}
}