feat: integrate phase3/4 settings UI and add phase4/5 regressions

This commit is contained in:
Youzini-afk
2026-04-11 16:39:33 +08:00
parent 2803066c1b
commit 3e37600399
8 changed files with 648 additions and 10 deletions

View File

@@ -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: "<think>先推断</think><action>举灯</action>艾琳说:去调查蓝钥匙。",
name: "艾琳",
speaker: "艾琳",
},
{
seq: 31,
role: "assistant",
content: "旁白补充:<status mood='tense'>雨夜</status>巷子很安静。",
name: "旁白",
speaker: "旁白",
},
{
seq: 32,
role: "user",
content: "<plan>先记路线</plan>我会继续调查蓝钥匙。",
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 mood='tense'>雨夜<\/status>巷子很安静。/,
);
assert.match(
recentContent,
/#32 \[user\|玩家\]: <plan>先记路线<\/plan>我会继续调查蓝钥匙。/,
);
assert.doesNotMatch(recentContent, /<think>|<action>/);
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");

View File

@@ -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() {

View File

@@ -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();

View File

@@ -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"),
);

View File

@@ -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",
);

View File

@@ -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 排序",
],
);

View File

@@ -1513,6 +1513,63 @@
<div class="bme-config-help">
开启后,最新 AI 楼先不自动提取,要等下一条 AI 楼出现后,才提取前一批内容。提取未处理和范围重提不受影响。
</div>
<div class="bme-config-row">
<label for="bme-setting-extract-recent-message-cap"
>最近消息上限0 = 不限)</label
>
<input
id="bme-setting-extract-recent-message-cap"
class="bme-config-input"
type="number"
min="0"
max="200"
/>
</div>
<div class="bme-config-row">
<label for="bme-setting-extract-prompt-structured-mode"
>提取结构模式</label
>
<select
id="bme-setting-extract-prompt-structured-mode"
class="bme-config-input"
>
<option value="both">混合transcript + structured</option>
<option value="transcript">仅 transcript</option>
<option value="structured">仅 structured</option>
</select>
</div>
<div class="bme-config-row">
<label for="bme-setting-extract-worldbook-mode"
>提取时世界书</label
>
<select
id="bme-setting-extract-worldbook-mode"
class="bme-config-input"
>
<option value="active">启用(解析激活条目)</option>
<option value="none">禁用</option>
</select>
</div>
<label
class="bme-inline-checkbox"
for="bme-setting-extract-include-summaries"
>
<input
id="bme-setting-extract-include-summaries"
type="checkbox"
/>
<span>提取时包含活跃总结</span>
</label>
<label
class="bme-inline-checkbox"
for="bme-setting-extract-include-story-time"
>
<input
id="bme-setting-extract-include-story-time"
type="checkbox"
/>
<span>提取时包含故事时间线</span>
</label>
</div>
</div>
</div>
@@ -1545,6 +1602,19 @@
max="9999"
/>
</div>
<label
class="bme-inline-checkbox"
for="bme-setting-recall-use-authoritative-generation-input"
>
<input
id="bme-setting-recall-use-authoritative-generation-input"
type="checkbox"
/>
<span>使用权威 generation 输入(实验性)</span>
</label>
<div class="bme-config-help">
开启后,召回查询将优先使用更接近真实发送入口的文本(如 send-intent、宿主快照、planner handoff而非回退到 chat tail 或 textarea。
</div>
</div>
</div>
</div>

View File

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