From 12f2a7a6ebd5206c5e1d9c96811fa93ce1fc3984 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sun, 5 Apr 2026 18:12:09 +0800 Subject: [PATCH] Fix regex stage timing and implicit world info resolution --- panel.js | 2 +- prompt-builder.js | 12 +++++++- prompt-profiles.js | 35 ++++++++++-------------- tests/task-regex.mjs | 59 ++++++++++++++++++++++++++++++++++++++++ tests/task-worldinfo.mjs | 50 ++++++++++++++++++++++++++++++++++ 5 files changed, 136 insertions(+), 22 deletions(-) diff --git a/panel.js b/panel.js index e96d8c2..3d4b0ef 100644 --- a/panel.js +++ b/panel.js @@ -3926,7 +3926,7 @@ function _renderTaskDebugPromptCard(taskType, promptBuild) { ${_renderDebugDetails("实际投递路径", promptBuild.debug?.effectivePath || null)} ${_renderDebugDetails("渲染后的块(按配置顺序)", promptBuild.renderedBlocks)} ${_renderDebugDetails("实际执行消息序列", promptBuild.executionMessages || promptBuild.privateTaskMessages || null)} - ${_renderDebugDetails("系统提示词(兼容视图)", promptBuild.systemPrompt || "")} + ${_renderDebugDetails("系统提示词(兼容视图,不含 atDepth 消息)", promptBuild.systemPrompt || "")} ${_renderDebugDetails("世界书桶内容(诊断)", promptBuild.hostInjections)} ${_renderDebugDetails("世界书块命中计划(诊断)", promptBuild.hostInjectionPlan || null)} ${_renderDebugDetails("世界书调试", promptBuild.worldInfo?.debug || promptBuild.worldInfoResolution?.debug || null)} diff --git a/prompt-builder.js b/prompt-builder.js index c58c3d5..ed727b4 100644 --- a/prompt-builder.js +++ b/prompt-builder.js @@ -969,6 +969,13 @@ function getBlockDiagnosticInjectionPosition(block = {}) { } function profileRequiresWorldInfo(profile) { + if ( + profile?.worldInfo === false || + profile?.metadata?.disableWorldInfo === true + ) { + return false; + } + const blocks = Array.isArray(profile?.blocks) ? profile.blocks : []; for (const block of blocks) { if (!block || block.enabled === false) continue; @@ -990,7 +997,10 @@ function profileRequiresWorldInfo(profile) { return true; } } - return false; + + // atDepth world info is implicit in the final message chain, so profiles + // without explicit before/after placeholders should still resolve lore. + return blocks.some((block) => block && block.enabled !== false); } function extractWorldInfoChatMessages(context = {}) { diff --git a/prompt-profiles.js b/prompt-profiles.js index 9369151..0490533 100644 --- a/prompt-profiles.js +++ b/prompt-profiles.js @@ -596,32 +596,27 @@ function normalizeRegexStageKey(stageKey = "") { export function normalizeTaskRegexStages(stages = {}) { const source = stages && typeof stages === "object" && !Array.isArray(stages) ? stages : {}; - const normalized = { ...source }; + const normalized = {}; + + for (const [key, value] of Object.entries(source)) { + if (Object.prototype.hasOwnProperty.call(TASK_REGEX_STAGE_ALIAS_MAP, key)) { + continue; + } + normalized[key] = Boolean(value); + } for (const [legacyKey, canonicalKey] of Object.entries( TASK_REGEX_STAGE_ALIAS_MAP, )) { - if ( - !Object.prototype.hasOwnProperty.call(normalized, canonicalKey) && - Object.prototype.hasOwnProperty.call(normalized, legacyKey) - ) { - normalized[canonicalKey] = Boolean(normalized[legacyKey]); - } - delete normalized[legacyKey]; - } - - for (const [groupKey, stageKeys] of Object.entries(TASK_REGEX_STAGE_GROUPS)) { - if (normalized[groupKey] === false) { + if (Object.prototype.hasOwnProperty.call(source, legacyKey)) { + // Older exports may carry both legacy and canonical keys at the same + // time. When that happens, keep the legacy intent instead of letting a + // newer placeholder default silently flip stage timing. + normalized[canonicalKey] = Boolean(source[legacyKey]); continue; } - const allSpecificStagesFalse = - stageKeys.length > 0 && - stageKeys.every((stageKey) => normalized[stageKey] === false); - if (!allSpecificStagesFalse) { - continue; - } - for (const stageKey of stageKeys) { - delete normalized[stageKey]; + if (Object.prototype.hasOwnProperty.call(source, canonicalKey)) { + normalized[canonicalKey] = Boolean(source[canonicalKey]); } } diff --git a/tests/task-regex.mjs b/tests/task-regex.mjs index cdbc31c..eba3653 100644 --- a/tests/task-regex.mjs +++ b/tests/task-regex.mjs @@ -153,6 +153,65 @@ try { const { applyTaskRegex, inspectTaskRegexReuse } = await import( "../task-regex.js" ); + const { + createDefaultTaskProfiles, + isTaskRegexStageEnabled, + normalizeTaskRegexStages, + } = await import("../prompt-profiles.js"); + + const normalizedLegacyStages = normalizeTaskRegexStages({ + finalPrompt: true, + "input.userMessage": false, + "input.recentMessages": false, + "input.candidateText": false, + "input.finalPrompt": false, + rawResponse: false, + beforeParse: false, + "output.rawResponse": false, + "output.beforeParse": false, + }); + assert.equal(normalizedLegacyStages["input.finalPrompt"], true); + assert.equal(normalizedLegacyStages["input.userMessage"], false); + assert.equal(normalizedLegacyStages["input.recentMessages"], false); + assert.equal(normalizedLegacyStages["input.candidateText"], false); + assert.equal(normalizedLegacyStages["output.rawResponse"], false); + assert.equal(normalizedLegacyStages["output.beforeParse"], false); + assert.equal( + isTaskRegexStageEnabled(normalizedLegacyStages, "input.finalPrompt"), + true, + ); + assert.equal( + isTaskRegexStageEnabled(normalizedLegacyStages, "input.userMessage"), + false, + ); + assert.equal( + isTaskRegexStageEnabled(normalizedLegacyStages, "input.recentMessages"), + false, + ); + assert.equal( + isTaskRegexStageEnabled(normalizedLegacyStages, "input.candidateText"), + false, + ); + + const defaultProfiles = createDefaultTaskProfiles(); + const defaultExtractStages = + defaultProfiles.extract?.profiles?.[0]?.regex?.stages || {}; + assert.equal( + isTaskRegexStageEnabled(defaultExtractStages, "input.finalPrompt"), + true, + ); + assert.equal( + isTaskRegexStageEnabled(defaultExtractStages, "input.userMessage"), + false, + ); + assert.equal( + isTaskRegexStageEnabled(defaultExtractStages, "input.recentMessages"), + false, + ); + assert.equal( + isTaskRegexStageEnabled(defaultExtractStages, "input.candidateText"), + false, + ); globalThis.getTavernRegexes = () => { throw new Error("legacy global getter should not be used in regex tests"); diff --git a/tests/task-worldinfo.mjs b/tests/task-worldinfo.mjs index 922856a..a8786fe 100644 --- a/tests/task-worldinfo.mjs +++ b/tests/task-worldinfo.mjs @@ -456,6 +456,56 @@ try { assert.equal(promptBuild.additionalMessages[0].content, "这是一条 atDepth 消息。"); assert.equal(promptBuild.debug.mvu.sanitizedFieldCount >= 0, 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), + ["user", "system"], + ); + const { initializeHostAdapter } = await import("../host-adapter/index.js"); const partialBridgeCalls = []; const partialBridgeEntriesByWorldbook = {