diff --git a/st-native-render.js b/st-native-render.js index c960f64..3056931 100644 --- a/st-native-render.js +++ b/st-native-render.js @@ -117,7 +117,7 @@ function resolveFormatMessageVariableMacros(text, messageVars) { export async function renderTemplateWithStSupport( text, - { env = null, messageVars = null } = {}, + { env = null, messageVars = null, evaluateEjs = true } = {}, ) { const originalText = String(text ?? ""); const runtime = getTemplateRuntime(); @@ -131,7 +131,7 @@ export async function renderTemplateWithStSupport( let ejsEvaluated = false; let ejsError = null; - if (originalText.includes("<%")) { + if (evaluateEjs && originalText.includes("<%")) { try { const evalTemplate = runtime?.evalTemplate || runtime?.evaltemplate || null; diff --git a/task-worldinfo.js b/task-worldinfo.js index a1efac8..b8482fd 100644 --- a/task-worldinfo.js +++ b/task-worldinfo.js @@ -907,9 +907,6 @@ async function loadNormalizedWorldbookEntries( normalizedName, ); if (String(filterMode || "default") === "custom") { - if (!normalizedEntry.enabled) { - continue; - } if (Array.isArray(customFilterKeywords) && customFilterKeywords.length > 0) { const nameLower = normalizedEntry.name.toLowerCase(); const matchedKeyword = customFilterKeywords.find((keyword) => @@ -1631,6 +1628,7 @@ export async function resolveTaskWorldInfo({ const sourceContent = entry.cleanContent || entry.content; let renderedContent = sourceContent; let taskEjsRenderedContent = sourceContent; + let taskEjsError = null; try { taskEjsRenderedContent = await evalTaskEjsTemplate(sourceContent, renderCtx, { world_info: { @@ -1660,26 +1658,34 @@ export async function resolveTaskWorldInfo({ result.debug.ejsLastError = error instanceof Error ? error.message : String(error); } + taskEjsError = error; taskEjsRenderedContent = ""; } renderedContent = taskEjsRenderedContent; if (isCustomFilter) { - const stNativeRender = await renderTemplateWithStSupport(sourceContent, { - env: customRenderEnv, - messageVars: customRenderMessageVars, - }); + const sourceIncludesEjs = String(sourceContent || "").includes("<%"); + const shouldAttemptNativeEjsFallback = + taskEjsError?.code === "st_bme_task_ejs_runtime_unavailable" && + sourceIncludesEjs; + const stNativeRender = await renderTemplateWithStSupport( + shouldAttemptNativeEjsFallback ? sourceContent : renderedContent, + { + env: customRenderEnv, + messageVars: customRenderMessageVars, + evaluateEjs: shouldAttemptNativeEjsFallback, + }, + ); if (stNativeRender.ejsError) { result.debug.customRender.ejsErrorCount += 1; } - const sourceIncludesEjs = String(sourceContent || "").includes("<%"); const shouldUseStNativeResult = - (!sourceIncludesEjs && + (shouldAttemptNativeEjsFallback && stNativeRender.ejsEvaluated) || + (!shouldAttemptNativeEjsFallback && (stNativeRender.macroApplied || stNativeRender.messageVariableMacrosApplied || - stNativeRender.text !== sourceContent)) || - (sourceIncludesEjs && stNativeRender.ejsEvaluated); + stNativeRender.text !== renderedContent)); if (shouldUseStNativeResult) { renderedContent = stNativeRender.text; diff --git a/tests/task-worldinfo.mjs b/tests/task-worldinfo.mjs index e9f012e..ee5054d 100644 --- a/tests/task-worldinfo.mjs +++ b/tests/task-worldinfo.mjs @@ -177,6 +177,7 @@ const forcedAfterEntry = createWorldbookEntry({ name: "强制 after", comment: "强制后置", content: "这是被 EJS 强制激活的后置条目。", + enabled: false, positionType: "after_character_definition", strategyType: "selective", keys: ["永远不会命中"], @@ -242,6 +243,16 @@ const messageVarMacroEntry = createWorldbookEntry({ 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 条目", @@ -272,6 +283,7 @@ const worldbooksByName = { statDataControllerEntry, statDataTargetEntry, messageVarMacroEntry, + customContextProbeEntry, forceControlEntry, forcedAfterEntry, atDepthEntry, @@ -423,9 +435,21 @@ try { customWorldInfo.beforeText, /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/")), - false, + 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"); @@ -443,6 +467,42 @@ try { 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", @@ -668,10 +728,7 @@ try { /secret=true<\/status_current_variable>/, ); assert.match(customPromptBuild.systemPrompt, /这一条不应该进入结果/); - assert.doesNotMatch( - customPromptBuild.systemPrompt, - /控制摘要:隐藏线索:Alice 正在调查/, - ); + assert.match(customPromptBuild.systemPrompt, /控制摘要:隐藏线索:Alice 正在调查/); const customPayload = buildTaskLlmPayload(customPromptBuild, "unused fallback"); assert.equal( customPayload.promptMessages.some((message) =>