From 3e376003992873cc47158cc8f8b35c71b9687308 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sat, 11 Apr 2026 16:39:33 +0800 Subject: [PATCH] feat: integrate phase3/4 settings UI and add phase4/5 regressions --- tests/extractor-phase5-context-fidelity.mjs | 387 ++++++++++++++++++ tests/helpers/generation-recall-harness.mjs | 3 +- .../recall-authoritative-generation-input.mjs | 116 ++++++ tests/task-profile-migration.mjs | 20 +- tests/task-profile-storage.mjs | 2 +- tests/task-worldinfo.mjs | 2 +- ui/panel.html | 70 ++++ ui/panel.js | 58 +++ 8 files changed, 648 insertions(+), 10 deletions(-) create mode 100644 tests/extractor-phase5-context-fidelity.mjs diff --git a/tests/extractor-phase5-context-fidelity.mjs b/tests/extractor-phase5-context-fidelity.mjs new file mode 100644 index 0000000..ebb15bc --- /dev/null +++ b/tests/extractor-phase5-context-fidelity.mjs @@ -0,0 +1,387 @@ +import assert from "node:assert/strict"; +import { + installResolveHooks, + toDataModuleUrl, +} from "./helpers/register-hooks-compat.mjs"; + +const extensionsShimSource = [ + "export const extension_settings = {};", + "export function getContext() {", + " return globalThis.__stBmeTestContext || {", + " chat: [],", + " chatMetadata: {},", + " extensionSettings: {},", + " powerUserSettings: {},", + " characters: {},", + " characterId: null,", + " name1: '玩家',", + " name2: '艾琳',", + " chatId: 'test-chat',", + " };", + "}", +].join("\n"); + +const scriptShimSource = [ + "export function getRequestHeaders() {", + " return {};", + "}", + "export function substituteParamsExtended(value) {", + " return String(value ?? '');", + "}", +].join("\n"); + +const openAiShimSource = [ + "export const chat_completion_sources = {};", + "export async function sendOpenAIRequest() {", + " throw new Error('sendOpenAIRequest should not be called in phase5 fidelity test');", + "}", +].join("\n"); + +installResolveHooks([ + { + specifiers: [ + "../../../extensions.js", + "../../../../extensions.js", + "../../../../../extensions.js", + ], + url: toDataModuleUrl(extensionsShimSource), + }, + { + specifiers: [ + "../../../../script.js", + "../../../../../script.js", + ], + url: toDataModuleUrl(scriptShimSource), + }, + { + specifiers: [ + "../../../../openai.js", + "../../../../../openai.js", + ], + url: toDataModuleUrl(openAiShimSource), + }, +]); + +const { createEmptyGraph } = await import("../graph/graph.js"); +const { DEFAULT_NODE_SCHEMA } = await import("../graph/schema.js"); +const { extractMemories } = await import("../maintenance/extractor.js"); +const { defaultSettings } = await import("../runtime/settings-defaults.js"); + +function setTestOverrides(overrides = {}) { + globalThis.__stBmeTestOverrides = overrides; + return () => { + delete globalThis.__stBmeTestOverrides; + }; +} + +function collectAllPromptContent(captured) { + return [ + String(captured.systemPrompt || ""), + String(captured.userPrompt || ""), + ...(Array.isArray(captured.promptMessages) ? captured.promptMessages : []).map( + (message) => String(message.content || ""), + ), + ...(Array.isArray(captured.additionalMessages) + ? captured.additionalMessages + : [] + ).map((message) => String(message.content || "")), + ].join("\n"); +} + +function createWorldbookEntry({ + uid, + name, + comment = name, + content, + enabled = true, + keys = [], + positionType = "before_character_definition", + role = "system", + depth = 0, + order = 10, + strategyType = keys.length > 0 ? "selective" : "constant", +}) { + return { + uid, + name, + comment, + content, + enabled, + position: { + type: positionType, + role, + depth, + order, + }, + strategy: { + type: strategyType, + keys, + keys_secondary: { logic: "and_any", keys: [] }, + }, + probability: 100, + extra: {}, + }; +} + +const originalSillyTavern = globalThis.SillyTavern; +const originalGetCharWorldbookNames = globalThis.getCharWorldbookNames; +const originalGetWorldbook = globalThis.getWorldbook; +const originalGetLorebookEntries = globalThis.getLorebookEntries; +const originalTestContext = globalThis.__stBmeTestContext; + +const worldbooksByName = { + "main-book": [ + createWorldbookEntry({ + uid: 1, + name: "主书常驻设定", + content: "主世界书:蓝钥匙线索。", + order: 10, + }), + createWorldbookEntry({ + uid: 2, + name: "蓝钥匙触发条目", + content: "主世界书命中:调查蓝钥匙时应关注旧城区。", + keys: ["蓝钥匙"], + order: 20, + }), + ], + "persona-book": [ + createWorldbookEntry({ + uid: 3, + name: "人格设定", + content: "人格世界书:保持谨慎,不要忽略路线细节。", + order: 10, + }), + ], + "chat-book": [ + createWorldbookEntry({ + uid: 4, + name: "聊天绑定设定", + content: "聊天世界书:当前会话已锁定旧城区雨夜调查。", + order: 10, + }), + ], +}; + +const fidelityMessages = [ + { + seq: 30, + role: "assistant", + content: "先推断举灯艾琳说:去调查蓝钥匙。", + name: "艾琳", + speaker: "艾琳", + }, + { + seq: 31, + role: "assistant", + content: "旁白补充:雨夜巷子很安静。", + name: "旁白", + speaker: "旁白", + }, + { + seq: 32, + role: "user", + content: "先记路线我会继续调查蓝钥匙。", + name: "玩家", + speaker: "玩家", + }, +]; + +globalThis.__stBmeTestContext = { + chat: [ + { is_user: false, mes: "艾琳说:去调查蓝钥匙。", name: "艾琳" }, + { is_user: false, mes: "旁白补充:雨夜巷子很安静。", name: "旁白" }, + { is_user: true, mes: "我会继续调查蓝钥匙。", name: "玩家" }, + ], + chatMetadata: { + world: "chat-book", + }, + extensionSettings: { + persona_description_lorebook: "persona-book", + }, + powerUserSettings: { + persona_description: "用户设定:谨慎调查者", + }, + characters: { + 1: { + name: "艾琳", + description: "角色描述:夜巡调查员", + data: { + description: "角色描述:夜巡调查员", + extensions: { + world: "main-book", + }, + }, + extensions: { + world: "main-book", + }, + }, + }, + characterId: 1, + name1: "玩家", + name2: "艾琳", + chatId: "phase5-context-fidelity", +}; + +globalThis.SillyTavern = { + getContext() { + return globalThis.__stBmeTestContext; + }, +}; + +globalThis.getCharWorldbookNames = () => ({ + primary: "main-book", + additional: [], +}); +globalThis.getWorldbook = async (worldbookName) => + worldbooksByName[String(worldbookName || "").trim()] || []; +globalThis.getLorebookEntries = async (worldbookName) => + (worldbooksByName[String(worldbookName || "").trim()] || []).map((entry) => ({ + uid: entry.uid, + comment: entry.comment, + })); + +try { + { + const graph = createEmptyGraph(); + let captured = null; + const restore = setTestOverrides({ + llm: { + async callLLMForJSON(payload) { + captured = payload; + return { operations: [], cognitionUpdates: [], regionUpdates: {} }; + }, + }, + }); + + try { + const result = await extractMemories({ + graph, + messages: fidelityMessages, + startSeq: 30, + endSeq: 32, + schema: DEFAULT_NODE_SCHEMA, + embeddingConfig: null, + settings: { + ...defaultSettings, + extractAssistantExcludeTags: "think,action", + extractWorldbookMode: "active", + }, + }); + + assert.equal(result.success, true); + assert.ok(captured); + + const allContent = collectAllPromptContent(captured); + assert.match(allContent, /角色描述:夜巡调查员/); + assert.match(allContent, /用户设定:谨慎调查者/); + assert.match(allContent, /主世界书:蓝钥匙线索。/); + assert.match(allContent, /主世界书命中:调查蓝钥匙时应关注旧城区。/); + assert.match(allContent, /人格世界书:保持谨慎,不要忽略路线细节。/); + assert.match(allContent, /聊天世界书:当前会话已锁定旧城区雨夜调查。/); + + const recentBlock = (Array.isArray(captured.promptMessages) + ? captured.promptMessages + : [] + ).find((message) => message.sourceKey === "recentMessages"); + assert.ok(recentBlock, "recentMessages block should exist"); + const recentContent = String(recentBlock?.content || ""); + assert.match(recentContent, /#30 \[assistant\|艾琳\]: 艾琳说:去调查蓝钥匙。/); + assert.match( + recentContent, + /#31 \[assistant\|旁白\]: 旁白补充:雨夜<\/status>巷子很安静。/, + ); + assert.match( + recentContent, + /#32 \[user\|玩家\]: 先记路线<\/plan>我会继续调查蓝钥匙。/, + ); + assert.doesNotMatch(recentContent, /|/); + + const worldInfoBeforeBlock = (Array.isArray(captured.promptMessages) + ? captured.promptMessages + : [] + ).find((message) => message.sourceKey === "worldInfoBefore"); + assert.ok(worldInfoBeforeBlock, "worldInfoBefore block should exist when worldbook is active"); + assert.match(String(worldInfoBeforeBlock?.content || ""), /蓝钥匙线索/); + } finally { + restore(); + } + } + + { + const graph = createEmptyGraph(); + let captured = null; + const restore = setTestOverrides({ + llm: { + async callLLMForJSON(payload) { + captured = payload; + return { operations: [], cognitionUpdates: [], regionUpdates: {} }; + }, + }, + }); + + try { + const result = await extractMemories({ + graph, + messages: fidelityMessages, + startSeq: 30, + endSeq: 32, + schema: DEFAULT_NODE_SCHEMA, + embeddingConfig: null, + settings: { + ...defaultSettings, + extractAssistantExcludeTags: "think,action", + extractWorldbookMode: "none", + }, + }); + + assert.equal(result.success, true); + assert.ok(captured); + + const allContent = collectAllPromptContent(captured); + assert.match(allContent, /角色描述:夜巡调查员/); + assert.match(allContent, /用户设定:谨慎调查者/); + assert.doesNotMatch(allContent, /主世界书:蓝钥匙线索。/); + assert.doesNotMatch(allContent, /主世界书命中:调查蓝钥匙时应关注旧城区。/); + assert.doesNotMatch(allContent, /人格世界书:保持谨慎,不要忽略路线细节。/); + assert.doesNotMatch(allContent, /聊天世界书:当前会话已锁定旧城区雨夜调查。/); + + const recentBlock = (Array.isArray(captured.promptMessages) + ? captured.promptMessages + : [] + ).find((message) => message.sourceKey === "recentMessages"); + assert.ok(recentBlock, "recentMessages block should still exist when worldbook is disabled"); + assert.match(String(recentBlock?.content || ""), /#30 \[assistant\|艾琳\]: 艾琳说:去调查蓝钥匙。/); + } finally { + restore(); + } + } +} finally { + if (originalSillyTavern === undefined) { + delete globalThis.SillyTavern; + } else { + globalThis.SillyTavern = originalSillyTavern; + } + 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; + } + if (originalTestContext === undefined) { + delete globalThis.__stBmeTestContext; + } else { + globalThis.__stBmeTestContext = originalTestContext; + } +} + +console.log("extractor-phase5-context-fidelity tests passed"); diff --git a/tests/helpers/generation-recall-harness.mjs b/tests/helpers/generation-recall-harness.mjs index caa9edf..32d2758 100644 --- a/tests/helpers/generation-recall-harness.mjs +++ b/tests/helpers/generation-recall-harness.mjs @@ -248,10 +248,11 @@ export function createGenerationRecallHarness(options = {}) { }; vm.createContext(context); vm.runInContext( - `${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, buildNormalGenerationRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationStarted, onGenerationEnded, onGenerationAfterCommands, onBeforeCombinePrompts, applyFinalRecallInjectionForGeneration, ensurePersistedRecallRecordForGeneration, findRecentGenerationRecallTransactionForChat, getGenerationRecallTransactionResult, generationRecallTransactions, freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot, clearPendingHostGenerationInputSnapshot, recordRecallSendIntent, clearPendingRecallSendIntent, recordRecallSentUserMessage, getPendingRecallSendIntent: () => pendingRecallSendIntent, getLastRecallSentUserMessage: () => lastRecallSentUserMessage, getCurrentGenerationTrivialSkip, markCurrentGenerationTrivialSkip, clearCurrentGenerationTrivialSkip, consumeCurrentGenerationTrivialSkip, deferAutoExtraction, maybeResumePendingAutoExtraction, clearPendingAutoExtraction, getPendingAutoExtraction: () => ({ ...pendingAutoExtraction }), getIsHostGenerationRunning: () => isHostGenerationRunning, runPlannerRecallForEna, getGraphPersistenceState: () => graphPersistenceState, setGraphPersistenceState: (value = {}) => { graphPersistenceState = { ...graphPersistenceState, ...(value || {}) }; return graphPersistenceState; } };`, + `${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, buildNormalGenerationRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationStarted, onGenerationEnded, onGenerationAfterCommands, onBeforeCombinePrompts, applyFinalRecallInjectionForGeneration, ensurePersistedRecallRecordForGeneration, findRecentGenerationRecallTransactionForChat, getGenerationRecallTransactionResult, generationRecallTransactions, freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot, clearPendingHostGenerationInputSnapshot, recordRecallSendIntent, clearPendingRecallSendIntent, recordRecallSentUserMessage, getPendingRecallSendIntent: () => pendingRecallSendIntent, getLastRecallSentUserMessage: () => lastRecallSentUserMessage, getCurrentGenerationTrivialSkip, markCurrentGenerationTrivialSkip, clearCurrentGenerationTrivialSkip, consumeCurrentGenerationTrivialSkip, deferAutoExtraction, maybeResumePendingAutoExtraction, clearPendingAutoExtraction, getPendingAutoExtraction: () => ({ ...pendingAutoExtraction }), getIsHostGenerationRunning: () => isHostGenerationRunning, preparePlannerRecallHandoff, runPlannerRecallForEna, getGraphPersistenceState: () => graphPersistenceState, setGraphPersistenceState: (value = {}) => { graphPersistenceState = { ...graphPersistenceState, ...(value || {}) }; return graphPersistenceState; } };`, context, { filename: indexPath }, ); + Object.defineProperties(context, { pendingRecallSendIntent: { get() { diff --git a/tests/recall-authoritative-generation-input.mjs b/tests/recall-authoritative-generation-input.mjs index 912d49f..47d2610 100644 --- a/tests/recall-authoritative-generation-input.mjs +++ b/tests/recall-authoritative-generation-input.mjs @@ -41,6 +41,120 @@ async function testSendIntentCanRemainAuthoritativeQueryWhenFlagEnabled() { assert.equal(transaction.frozenRecallOptions.includeSyntheticUserMessage, true); } +async function testPlannerHandoffCanRemainAuthoritativeQueryWhenFlagEnabled() { + const harness = await createGenerationRecallHarness(); + harness.extension_settings[MODULE_NAME] = { + recallUseAuthoritativeGenerationInput: true, + }; + harness.chat = [{ is_user: true, mes: "楼层里的稳定用户输入" }]; + + const handoff = harness.result.preparePlannerRecallHandoff({ + rawUserInput: "planner 原始输入", + plannerAugmentedMessage: "planner 增强后的输入", + plannerRecall: { + memoryBlock: "规划记忆块", + recentMessages: ["[user]: planner 原始输入", "[assistant]: 记忆命中"], + result: { + selectedNodeIds: ["node-planner-1"], + stats: { + coreCount: 1, + recallCount: 1, + }, + meta: { + retrieval: { + vectorHits: 1, + vectorMergedHits: 0, + diffusionHits: 0, + candidatePoolAfterDpp: 1, + llm: { + status: "disabled", + candidatePool: 0, + }, + }, + }, + }, + }, + chatId: "chat-main", + }); + + assert.ok(handoff); + + const recallContext = harness.result.createGenerationRecallContext({ + hookName: "GENERATION_AFTER_COMMANDS", + generationType: "normal", + recallOptions: {}, + chatId: "chat-main", + }); + + assert.equal(recallContext.shouldRun, true); + assert.equal(recallContext.recallOptions.overrideUserMessage, "planner 原始输入"); + assert.equal(recallContext.recallOptions.overrideSource, "planner-handoff"); + assert.equal(recallContext.recallOptions.authoritativeInputUsed, true); + assert.equal( + recallContext.recallOptions.boundUserFloorText, + "楼层里的稳定用户输入", + ); + assert.equal(recallContext.recallOptions.includeSyntheticUserMessage, true); + assert.ok(recallContext.recallOptions.cachedRecallPayload); + assert.equal( + recallContext.recallOptions.cachedRecallPayload.source, + "planner-handoff", + ); + + await harness.result.onGenerationAfterCommands("normal", {}, false); + + assert.equal(harness.runRecallCalls.length, 1); + assert.equal(harness.runRecallCalls[0].overrideUserMessage, "planner 原始输入"); + assert.equal(harness.runRecallCalls[0].overrideSource, "planner-handoff"); + assert.equal(harness.runRecallCalls[0].authoritativeInputUsed, true); + assert.equal( + harness.runRecallCalls[0].boundUserFloorText, + "楼层里的稳定用户输入", + ); + assert.equal(harness.runRecallCalls[0].includeSyntheticUserMessage, true); + assert.ok(harness.runRecallCalls[0].cachedRecallPayload); +} + +async function testAuthoritativeSendIntentStaysFrozenAcrossHooksWhenFlagEnabled() { + const harness = await createGenerationRecallHarness(); + harness.extension_settings[MODULE_NAME] = { + recallUseAuthoritativeGenerationInput: true, + }; + harness.chat = [{ is_user: true, mes: "稳定 chat tail" }]; + harness.pendingRecallSendIntent = { + text: "第一次权威输入", + hash: "hash-phase4-frozen-a", + at: Date.now(), + source: "dom-intent", + }; + + await harness.result.onGenerationAfterCommands("normal", {}, false); + + harness.pendingRecallSendIntent = { + text: "第二次漂移输入", + hash: "hash-phase4-frozen-b", + at: Date.now(), + source: "dom-intent", + }; + await harness.result.onBeforeCombinePrompts(); + + assert.equal(harness.runRecallCalls.length, 1); + assert.equal(harness.runRecallCalls[0].overrideUserMessage, "第一次权威输入"); + assert.equal(harness.runRecallCalls[0].overrideSource, "send-intent"); + assert.equal(harness.runRecallCalls[0].authoritativeInputUsed, true); + assert.equal(harness.runRecallCalls[0].boundUserFloorText, "稳定 chat tail"); + + const transaction = [...harness.result.generationRecallTransactions.values()][0]; + assert.ok(transaction); + assert.equal( + transaction.frozenRecallOptions.overrideUserMessage, + "第一次权威输入", + ); + assert.equal(transaction.frozenRecallOptions.authoritativeInputUsed, true); + assert.equal(transaction.frozenRecallOptions.boundUserFloorText, "稳定 chat tail"); + assert.equal(transaction.frozenRecallOptions.includeSyntheticUserMessage, true); +} + async function testHostSnapshotCanRemainAuthoritativeQueryWhenFlagEnabled() { const harness = await createGenerationRecallHarness(); harness.extension_settings[MODULE_NAME] = { @@ -123,6 +237,8 @@ function testResolveRecallInputControllerAppendsSyntheticAuthoritativeUserMessag } await testSendIntentCanRemainAuthoritativeQueryWhenFlagEnabled(); +await testPlannerHandoffCanRemainAuthoritativeQueryWhenFlagEnabled(); +await testAuthoritativeSendIntentStaysFrozenAcrossHooksWhenFlagEnabled(); await testHostSnapshotCanRemainAuthoritativeQueryWhenFlagEnabled(); testResolveRecallInputControllerAppendsSyntheticAuthoritativeUserMessage(); diff --git a/tests/task-profile-migration.mjs b/tests/task-profile-migration.mjs index 45af146..4bcf034 100644 --- a/tests/task-profile-migration.mjs +++ b/tests/task-profile-migration.mjs @@ -34,7 +34,7 @@ const extractProfile = getActiveTaskProfile( assert.equal(extractProfile.taskType, "extract"); assert.equal(extractProfile.id, "default"); assert.ok(Array.isArray(extractProfile.blocks)); -assert.equal(extractProfile.blocks.length, 12); +assert.equal(extractProfile.blocks.length, 14); assert.deepEqual( extractProfile.blocks.map((block) => block.name), [ @@ -48,6 +48,8 @@ assert.deepEqual( "图统计", "Schema", "当前范围", + "活跃总结", + "故事时间", "输出格式", "行为规则", ], @@ -65,6 +67,8 @@ assert.deepEqual( "builtin", "builtin", "builtin", + "builtin", + "builtin", "custom", "custom", ], @@ -82,6 +86,8 @@ assert.deepEqual( "system", "system", "system", + "system", + "system", "user", "user", ], @@ -214,16 +220,16 @@ const upgradedLegacyDefault = getActiveTaskProfile( }, "extract", ); -assert.equal(upgradedLegacyDefault.blocks.length, 12); +assert.equal(upgradedLegacyDefault.blocks.length, 14); assert.equal(upgradedLegacyDefault.blocks[0].name, "抬头"); assert.match(upgradedLegacyDefault.blocks[0].content, /虚拟的世界/); assert.equal(upgradedLegacyDefault.blocks[0].role, "system"); assert.equal(upgradedLegacyDefault.blocks[0].injectionMode, "relative"); assert.equal(upgradedLegacyDefault.blocks[1].content, "保留我自己的角色定义"); -assert.equal(upgradedLegacyDefault.blocks[10].content, "保留我自己的输出格式"); -assert.equal(upgradedLegacyDefault.blocks[11].content, "保留我自己的行为规则"); -assert.equal(upgradedLegacyDefault.blocks[10].role, "user"); -assert.equal(upgradedLegacyDefault.blocks[11].role, "user"); +assert.equal(upgradedLegacyDefault.blocks[12].content, "保留我自己的输出格式"); +assert.equal(upgradedLegacyDefault.blocks[13].content, "保留我自己的行为规则"); +assert.equal(upgradedLegacyDefault.blocks[12].role, "user"); +assert.equal(upgradedLegacyDefault.blocks[13].role, "user"); const currentDefaults = createDefaultTaskProfiles(); const currentDefaultExtract = currentDefaults.extract.profiles[0]; @@ -389,7 +395,7 @@ assert.deepEqual( ); assert.ok( upgradedLegacyDefault.blocks - .slice(0, 10) + .slice(0, 12) .every((block) => block.role === "system"), ); diff --git a/tests/task-profile-storage.mjs b/tests/task-profile-storage.mjs index b392f3e..1f6c5e8 100644 --- a/tests/task-profile-storage.mjs +++ b/tests/task-profile-storage.mjs @@ -53,7 +53,7 @@ const activeProfile = getActiveTaskProfile( "extract", ); assert.equal(activeProfile.name, "激进提取"); -assert.equal(activeProfile.blocks.length, 14); +assert.equal(activeProfile.blocks.length, 16); const builtinBlock = activeProfile.blocks.find( (block) => block.type === "builtin" && block.sourceKey === "userMessage", ); diff --git a/tests/task-worldinfo.mjs b/tests/task-worldinfo.mjs index 5174fc2..c408a29 100644 --- a/tests/task-worldinfo.mjs +++ b/tests/task-worldinfo.mjs @@ -930,7 +930,7 @@ try { 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]: 第二句", + "#1 [assistant|深度注入 D4]: 这是 d4 atDepth 消息。\n\n#2 [assistant|深度注入]: 这是一条 atDepth 消息。\n\n#11 [user]: 第一句\n\n#4 [assistant|深度注入 D1]: 这是 d1 atDepth 消息。\n\n#12 [assistant]: 第二句", "用户问题:继续调查 depth 排序", ], ); diff --git a/ui/panel.html b/ui/panel.html index 2121291..bbe5777 100644 --- a/ui/panel.html +++ b/ui/panel.html @@ -1513,6 +1513,63 @@
开启后,最新 AI 楼先不自动提取,要等下一条 AI 楼出现后,才提取前一批内容。提取未处理和范围重提不受影响。
+
+ + +
+
+ + +
+
+ + +
+ + @@ -1545,6 +1602,19 @@ max="9999" /> + +
+ 开启后,召回查询将优先使用更接近真实发送入口的文本(如 send-intent、宿主快照、planner handoff),而非回退到 chat tail 或 textarea。 +
diff --git a/ui/panel.js b/ui/panel.js index 497420f..b2db996 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -4377,6 +4377,26 @@ function _refreshConfigTab() { "bme-setting-extract-auto-delay-latest-assistant", settings.extractAutoDelayLatestAssistant === true, ); + _setInputValue( + "bme-setting-extract-recent-message-cap", + settings.extractRecentMessageCap ?? 0, + ); + _setInputValue( + "bme-setting-extract-prompt-structured-mode", + settings.extractPromptStructuredMode || "both", + ); + _setInputValue( + "bme-setting-extract-worldbook-mode", + settings.extractWorldbookMode || "active", + ); + _setCheckboxValue( + "bme-setting-extract-include-summaries", + settings.extractIncludeSummaries !== false, + ); + _setCheckboxValue( + "bme-setting-extract-include-story-time", + settings.extractIncludeStoryTime !== false, + ); _setInputValue("bme-setting-recall-top-k", settings.recallTopK ?? 20); _setInputValue("bme-setting-recall-max-nodes", settings.recallMaxNodes ?? 8); _setInputValue( @@ -4472,6 +4492,10 @@ function _refreshConfigTab() { settings.recallObjectiveGlobalWeight ?? 0.75, ); _setInputValue("bme-setting-inject-depth", settings.injectDepth ?? 9999); + _setCheckboxValue( + "bme-setting-recall-use-authoritative-generation-input", + settings.recallUseAuthoritativeGenerationInput === true, + ); _setInputValue("bme-setting-graph-weight", settings.graphWeight ?? 0.6); _setInputValue("bme-setting-vector-weight", settings.vectorWeight ?? 0.3); _setInputValue( @@ -4805,6 +4829,35 @@ function _bindConfigControls() { (checked) => _patchSettings({ extractAutoDelayLatestAssistant: checked }), ); + bindNumber("bme-setting-extract-recent-message-cap", 0, 0, 200, (value) => + _patchSettings({ extractRecentMessageCap: value }), + ); + const extractStructuredModeEl = document.getElementById( + "bme-setting-extract-prompt-structured-mode", + ); + if (extractStructuredModeEl && extractStructuredModeEl.dataset.bmeBound !== "true") { + extractStructuredModeEl.addEventListener("change", () => { + _patchSettings({ extractPromptStructuredMode: extractStructuredModeEl.value || "both" }); + }); + extractStructuredModeEl.dataset.bmeBound = "true"; + } + const extractWorldbookModeEl = document.getElementById( + "bme-setting-extract-worldbook-mode", + ); + if (extractWorldbookModeEl && extractWorldbookModeEl.dataset.bmeBound !== "true") { + extractWorldbookModeEl.addEventListener("change", () => { + _patchSettings({ extractWorldbookMode: extractWorldbookModeEl.value || "active" }); + }); + extractWorldbookModeEl.dataset.bmeBound = "true"; + } + bindCheckbox( + "bme-setting-extract-include-summaries", + (checked) => _patchSettings({ extractIncludeSummaries: checked }), + ); + bindCheckbox( + "bme-setting-extract-include-story-time", + (checked) => _patchSettings({ extractIncludeStoryTime: checked }), + ); bindNumber("bme-setting-recall-top-k", 20, 1, 100, (value) => _patchSettings({ recallTopK: value }), ); @@ -4927,6 +4980,11 @@ function _bindConfigControls() { bindNumber("bme-setting-inject-depth", 9999, 0, 9999, (value) => _patchSettings({ injectDepth: value }), ); + bindCheckbox( + "bme-setting-recall-use-authoritative-generation-input", + (checked) => + _patchSettings({ recallUseAuthoritativeGenerationInput: checked }), + ); bindFloat("bme-setting-graph-weight", 0.6, 0, 1, (value) => _patchSettings({ graphWeight: value }), );