diff --git a/mvu-compat.js b/mvu-compat.js index 742edff..f070376 100644 --- a/mvu-compat.js +++ b/mvu-compat.js @@ -11,11 +11,24 @@ const MVU_STATUS_CURRENT_VARIABLE_REPLACE_REGEX = /\n?[\s\S]*?<\/status_current_variables?>/gi; const MVU_STATUS_CURRENT_VARIABLE_DETECT_REGEX = /[\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]*?/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) ); } diff --git a/prompt-builder.js b/prompt-builder.js index c691854..a8ee7ce 100644 --- a/prompt-builder.js +++ b/prompt-builder.js @@ -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, }; } diff --git a/tests/mvu-compat.mjs b/tests/mvu-compat.mjs index 53d5c24..68819d6 100644 --- a/tests/mvu-compat.mjs +++ b/tests/mvu-compat.mjs @@ -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: ["被拦截条目"], diff --git a/tests/prompt-builder-mvu.mjs b/tests/prompt-builder-mvu.mjs index 51367d7..5e5eb40 100644 --- a/tests/prompt-builder-mvu.mjs +++ b/tests/prompt-builder-mvu.mjs @@ -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: "最近消息 hp=3 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: - "用户输入 secret BAD_USER", - candidateNodes: "候选节点 BAD_CANDIDATE", - candidateText: "候选节点 BAD_CANDIDATE", + "用户输入 secret {{get_message_variable::stat_data.hp}} BAD_USER", + candidateNodes: [ + { + id: "node-1", + summary: "候选节点 BAD_CANDIDATE ", + 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: + "继续 星火密令", + 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; + } }