diff --git a/maintenance/extraction-controller.js b/maintenance/extraction-controller.js index 81f1fa0..5889afd 100644 --- a/maintenance/extraction-controller.js +++ b/maintenance/extraction-controller.js @@ -681,6 +681,29 @@ export function resolveAutoExtractionPlanController( reason: "plugin-disabled", }; } + if (resolvedSettings.extractAutoEnabled === false) { + return { + strategy, + chat: resolvedChat, + settings: resolvedSettings, + lastProcessedAssistantFloor: safeLastProcessedAssistantFloor, + lockedEndFloor: safeLockedEndFloor, + extractEvery, + pendingAssistantTurns: [], + candidateAssistantTurns: [], + eligibleAssistantTurns: [], + eligibleEndFloor: null, + waitingForNextAssistant: false, + smartTriggerDecision: { triggered: false, score: 0, reasons: [] }, + meetsExtractEvery: false, + canRun: false, + batchAssistantTurns: [], + plannedBatchEndFloor: null, + startIdx: null, + endIdx: null, + reason: "auto-extraction-disabled", + }; + } const assistantTurns = typeof runtime?.getAssistantTurns === "function" ? runtime.getAssistantTurns(resolvedChat) @@ -972,7 +995,7 @@ export async function runExtractionController(runtime, options = {}) { return; } - if (!settings.enabled) return; + if (!settings.enabled || settings.extractAutoEnabled === false) return; if (!runtime.ensureGraphMutationReady("自动提取", { notify: false })) { runtime.console?.debug?.("[ST-BME] auto extraction blocked: graph-not-ready", { loadState: runtime.getGraphPersistenceState?.()?.loadState || "", diff --git a/runtime/settings-defaults.js b/runtime/settings-defaults.js index 3255e39..e0642d4 100644 --- a/runtime/settings-defaults.js +++ b/runtime/settings-defaults.js @@ -23,6 +23,7 @@ export const defaultSettings = { hideOldMessagesRenderLimit: 10, // 提取设置 + extractAutoEnabled: true, extractEvery: 1, extractContextTurns: 2, extractAutoDelayLatestAssistant: false, diff --git a/tests/default-settings.mjs b/tests/default-settings.mjs index 43f9ec0..a7c8b8d 100644 --- a/tests/default-settings.mjs +++ b/tests/default-settings.mjs @@ -6,6 +6,7 @@ import { } from "../runtime/settings-defaults.js"; assert.equal(defaultSettings.extractContextTurns, 2); +assert.equal(defaultSettings.extractAutoEnabled, true); assert.equal(defaultSettings.extractActionMode, "pending"); assert.equal(defaultSettings.extractAutoDelayLatestAssistant, false); assert.equal(defaultSettings.hideOldMessagesRenderLimitEnabled, true); diff --git a/tests/p0-regressions.mjs b/tests/p0-regressions.mjs index fd11fa9..dc5cd65 100644 --- a/tests/p0-regressions.mjs +++ b/tests/p0-regressions.mjs @@ -4671,6 +4671,79 @@ async function testMessageReceivedQueuesExtractionWithoutRuntimeQueueMicrotask() assert.equal(refreshCalls, 1); } +async function testMessageReceivedSkipsWhenAutoExtractionDisabled() { + let runExtractionCalls = 0; + let refreshCalls = 0; + const deferredReasons = []; + const chat = [ + { is_user: true, mes: "u1" }, + { is_user: false, mes: "a1" }, + ]; + const settings = { + enabled: true, + extractAutoEnabled: false, + extractEvery: 1, + extractAutoDelayLatestAssistant: false, + enableSmartTrigger: false, + }; + + const plan = buildAutoExtractionPlan({ + chat, + settings, + lastProcessedAssistantFloor: -1, + }); + assert.equal(plan.canRun, false); + assert.equal(plan.reason, "auto-extraction-disabled"); + + onMessageReceivedController( + { + getGraphPersistenceState: () => ({ loadState: "loaded", dbReady: true }), + getCurrentGraph: () => null, + getPendingRecallSendIntent: () => ({ text: "", at: 0 }), + isFreshRecallInputRecord: () => true, + createRecallInputRecord: () => ({ text: "", at: 0 }), + setPendingRecallSendIntent() {}, + getContext: () => ({ + chat, + }), + getSettings: () => settings, + getLastProcessedAssistantFloor: () => -1, + isAssistantChatMessage(message) { + return Boolean(message) && !message.is_user && !message.is_system; + }, + resolveAutoExtractionPlan: (options = {}) => + buildAutoExtractionPlan({ + chat, + settings, + lastProcessedAssistantFloor: -1, + ...(options || {}), + }), + deferAutoExtraction(reason) { + deferredReasons.push(reason); + }, + runExtraction: async () => { + runExtractionCalls += 1; + }, + console: { + debug() {}, + error() {}, + }, + notifyExtractionIssue() {}, + refreshPersistedRecallMessageUi() { + refreshCalls += 1; + }, + }, + 1, + "assistant", + ); + + await waitForTick(); + + assert.equal(runExtractionCalls, 0); + assert.deepEqual(deferredReasons, []); + assert.equal(refreshCalls, 1); +} + async function testMessageReceivedDefersExtractionDuringHostGeneration() { let runExtractionCalls = 0; const deferred = []; @@ -7690,6 +7763,7 @@ await testMessageSentFallsBackToLatestUserWhenHostMessageIdInvalid(); await testUserMessageRenderedRefreshesRecallUiAfterRealDomRender(); await testCharacterMessageRenderedRefreshesRecallUiAfterAssistantRender(); await testMessageReceivedQueuesExtractionWithoutRuntimeQueueMicrotask(); +await testMessageReceivedSkipsWhenAutoExtractionDisabled(); await testMessageReceivedDefersExtractionDuringHostGeneration(); await testMessageReceivedLagModeWaitsSilentlyForNextAssistant(); await testMessageReceivedLagModeQueuesPreviousAssistantOnly(); diff --git a/ui/panel.html b/ui/panel.html index ba3c1f9..f482aaf 100644 --- a/ui/panel.html +++ b/ui/panel.html @@ -1154,6 +1154,14 @@