/);
+
+ 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 }),
);