From 5023b75f3fb021069484d2feb263c80da829a877 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sun, 29 Mar 2026 17:39:41 +0800 Subject: [PATCH] refactor: controllerize onReroll and keep reroll harness green --- extraction-controller.js | 164 +++++++++++++++++++++++++++++++++++ index.js | 180 +++++---------------------------------- tests/p0-regressions.mjs | 11 +++ 3 files changed, 196 insertions(+), 159 deletions(-) diff --git a/extraction-controller.js b/extraction-controller.js index 3ad07a1..6695850 100644 --- a/extraction-controller.js +++ b/extraction-controller.js @@ -365,3 +365,167 @@ export async function onManualExtractController(runtime) { runtime.refreshPanelLiveState(); } } + +export async function onRerollController(runtime, { fromFloor } = {}) { + if (runtime.getIsExtracting?.()) { + runtime.toastr?.info?.("记忆提取正在进行中,请稍候"); + return { + success: false, + rollbackPerformed: false, + extractionTriggered: false, + requestedFloor: null, + effectiveFromFloor: null, + recoveryPath: "busy", + affectedBatchCount: 0, + error: "记忆提取正在进行中", + }; + } + + if ( + typeof runtime.ensureGraphMutationReady === "function" && + !runtime.ensureGraphMutationReady("重新提取") + ) { + return { + success: false, + rollbackPerformed: false, + extractionTriggered: false, + requestedFloor: Number.isFinite(fromFloor) ? fromFloor : null, + effectiveFromFloor: null, + recoveryPath: runtime.getGraphPersistenceState?.()?.loadState || "graph-not-ready", + affectedBatchCount: 0, + error: + typeof runtime.getGraphMutationBlockReason === "function" + ? runtime.getGraphMutationBlockReason("重新提取") + : "重新提取已暂停:图谱尚未就绪。", + }; + } + + if (!runtime.getCurrentGraph?.()) { + runtime.toastr?.info?.("图谱为空,无需重 Roll"); + return { + success: false, + rollbackPerformed: false, + extractionTriggered: false, + requestedFloor: null, + effectiveFromFloor: null, + recoveryPath: "empty-graph", + affectedBatchCount: 0, + error: "图谱为空", + }; + } + + const context = runtime.getContext(); + const chat = context?.chat; + if (!Array.isArray(chat) || chat.length === 0) { + runtime.toastr?.info?.("当前聊天为空"); + return { + success: false, + rollbackPerformed: false, + extractionTriggered: false, + requestedFloor: null, + effectiveFromFloor: null, + recoveryPath: "empty-chat", + affectedBatchCount: 0, + error: "当前聊天为空", + }; + } + + let targetFloor = Number.isFinite(fromFloor) ? fromFloor : null; + if (targetFloor === null) { + const assistantTurns = runtime.getAssistantTurns(chat); + if (assistantTurns.length === 0) { + runtime.toastr?.info?.("聊天中没有 AI 回复"); + return { + success: false, + rollbackPerformed: false, + extractionTriggered: false, + requestedFloor: null, + effectiveFromFloor: null, + recoveryPath: "no-assistant-turn", + affectedBatchCount: 0, + error: "聊天中没有 AI 回复", + }; + } + targetFloor = assistantTurns[assistantTurns.length - 1]; + } + + runtime.setRuntimeStatus( + "重新提取中", + Number.isFinite(targetFloor) + ? `准备从楼层 ${targetFloor} 开始回滚并重新提取` + : "准备回滚最新 AI 楼并重新提取", + "running", + ); + + const lastProcessed = runtime.getLastProcessedAssistantFloor(); + const alreadyExtracted = targetFloor <= lastProcessed; + + if (!alreadyExtracted) { + runtime.toastr?.info?.("该楼层尚未提取,直接执行提取…", "ST-BME 重 Roll", { + timeOut: 2000, + }); + await runtime.onManualExtract(); + return { + success: true, + rollbackPerformed: false, + extractionTriggered: true, + requestedFloor: targetFloor, + effectiveFromFloor: lastProcessed + 1, + recoveryPath: "direct-extract", + affectedBatchCount: 0, + extractionStatus: runtime.getLastExtractionStatusLevel?.() || "idle", + error: "", + }; + } + + runtime.console.log(`[ST-BME] 重 Roll 开始,目标楼层: ${targetFloor}`); + let rollbackResult; + try { + rollbackResult = await runtime.rollbackGraphForReroll(targetFloor, context); + } catch (e) { + if (runtime.isAbortError(e)) { + runtime.setRuntimeStatus( + "重新提取已取消", + e.message || "聊天已切换", + "warning", + ); + return { + success: false, + rollbackPerformed: false, + extractionTriggered: false, + requestedFloor: targetFloor, + effectiveFromFloor: null, + recoveryPath: "aborted", + affectedBatchCount: 0, + error: e.message || "聊天已切换,重新提取已取消", + }; + } + throw e; + } + + if (!rollbackResult?.success) { + runtime.setRuntimeStatus( + "重新提取失败", + rollbackResult.error || "回滚失败", + "error", + ); + runtime.toastr?.error?.(rollbackResult.error, "ST-BME 重 Roll"); + return rollbackResult; + } + + const rerollDesc = + rollbackResult.effectiveFromFloor !== targetFloor + ? `已按批次边界回滚到楼层 ${rollbackResult.effectiveFromFloor} 开始重新提取…` + : `已回滚到楼层 ${targetFloor} 开始重新提取…`; + runtime.toastr?.info?.(rerollDesc, "ST-BME 重 Roll", { + timeOut: 2500, + }); + + await runtime.onManualExtract(); + runtime.refreshPanelLiveState(); + return { + ...rollbackResult, + extractionTriggered: true, + extractionStatus: runtime.getLastExtractionStatusLevel?.() || "idle", + }; +} diff --git a/index.js b/index.js index e24edf6..05a76e7 100644 --- a/index.js +++ b/index.js @@ -21,6 +21,7 @@ import { consolidateMemories } from "./consolidator.js"; import { executeExtractionBatchController, onManualExtractController, + onRerollController, runExtractionController, } from "./extraction-controller.js"; import { @@ -4642,166 +4643,27 @@ async function onManualExtract() { } async function onReroll({ fromFloor } = {}) { - if (isExtracting) { - toastr.info("记忆提取正在进行中,请稍候"); - return { - success: false, - rollbackPerformed: false, - extractionTriggered: false, - requestedFloor: null, - effectiveFromFloor: null, - recoveryPath: "busy", - affectedBatchCount: 0, - error: "记忆提取正在进行中", - }; - } - if ( - typeof ensureGraphMutationReady === "function" && - !ensureGraphMutationReady("重新提取") - ) { - return { - success: false, - rollbackPerformed: false, - extractionTriggered: false, - requestedFloor: Number.isFinite(fromFloor) ? fromFloor : null, - effectiveFromFloor: null, - recoveryPath: - typeof graphPersistenceState === "object" - ? graphPersistenceState.loadState - : "graph-not-ready", - affectedBatchCount: 0, - error: - typeof getGraphMutationBlockReason === "function" - ? getGraphMutationBlockReason("重新提取") - : "重新提取已暂停:图谱尚未就绪。", - }; - } - if (!currentGraph) { - toastr.info("图谱为空,无需重 Roll"); - return { - success: false, - rollbackPerformed: false, - extractionTriggered: false, - requestedFloor: null, - effectiveFromFloor: null, - recoveryPath: "empty-graph", - affectedBatchCount: 0, - error: "图谱为空", - }; - } - - const context = getContext(); - const chat = context.chat; - if (!Array.isArray(chat) || chat.length === 0) { - toastr.info("当前聊天为空"); - return { - success: false, - rollbackPerformed: false, - extractionTriggered: false, - requestedFloor: null, - effectiveFromFloor: null, - recoveryPath: "empty-chat", - affectedBatchCount: 0, - error: "当前聊天为空", - }; - } - - // 确定回滚起点 - let targetFloor = Number.isFinite(fromFloor) ? fromFloor : null; - if (targetFloor === null) { - // 默认:重做最新 AI 楼 - const assistantTurns = getAssistantTurns(chat); - if (assistantTurns.length === 0) { - toastr.info("聊天中没有 AI 回复"); - return { - success: false, - rollbackPerformed: false, - extractionTriggered: false, - requestedFloor: null, - effectiveFromFloor: null, - recoveryPath: "no-assistant-turn", - affectedBatchCount: 0, - error: "聊天中没有 AI 回复", - }; - } - targetFloor = assistantTurns[assistantTurns.length - 1]; - } - - setRuntimeStatus( - "重新提取中", - Number.isFinite(targetFloor) - ? `准备从楼层 ${targetFloor} 开始回滚并重新提取` - : "准备回滚最新 AI 楼并重新提取", - "running", + return await onRerollController( + { + console, + ensureGraphMutationReady, + getAssistantTurns, + getContext, + getCurrentGraph: () => currentGraph, + getGraphMutationBlockReason, + getGraphPersistenceState: () => graphPersistenceState, + getIsExtracting: () => isExtracting, + getLastExtractionStatusLevel: () => lastExtractionStatus?.level || "idle", + getLastProcessedAssistantFloor, + isAbortError, + onManualExtract, + refreshPanelLiveState, + rollbackGraphForReroll, + setRuntimeStatus, + toastr, + }, + { fromFloor }, ); - - const lastProcessed = getLastProcessedAssistantFloor(); - const alreadyExtracted = targetFloor <= lastProcessed; - - if (!alreadyExtracted) { - // 目标楼层未提取过 → 直接走手动提取即可,不需要回滚 - toastr.info("该楼层尚未提取,直接执行提取…", "ST-BME 重 Roll", { - timeOut: 2000, - }); - await onManualExtract(); - return { - success: true, - rollbackPerformed: false, - extractionTriggered: true, - requestedFloor: targetFloor, - effectiveFromFloor: lastProcessed + 1, - recoveryPath: "direct-extract", - affectedBatchCount: 0, - extractionStatus: lastExtractionStatus?.level || "idle", - error: "", - }; - } - - console.log(`[ST-BME] 重 Roll 开始,目标楼层: ${targetFloor}`); - let rollbackResult; - try { - rollbackResult = await rollbackGraphForReroll(targetFloor, context); - } catch (e) { - if (isAbortError(e)) { - setRuntimeStatus('重新提取已取消', e.message || '聊天已切换', 'warning'); - return { - success: false, - rollbackPerformed: false, - extractionTriggered: false, - requestedFloor: targetFloor, - effectiveFromFloor: null, - recoveryPath: 'aborted', - affectedBatchCount: 0, - error: e.message || '聊天已切换,重新提取已取消', - }; - } - throw e; - } - if (!rollbackResult?.success) { - setRuntimeStatus( - "重新提取失败", - rollbackResult.error || "回滚失败", - "error", - ); - toastr.error(rollbackResult.error, "ST-BME 重 Roll"); - return rollbackResult; - } - - const rerollDesc = - rollbackResult.effectiveFromFloor !== targetFloor - ? `已按批次边界回滚到楼层 ${rollbackResult.effectiveFromFloor} 开始重新提取…` - : `已回滚到楼层 ${targetFloor} 开始重新提取…`; - toastr.info(rerollDesc, "ST-BME 重 Roll", { - timeOut: 2500, - }); - - await onManualExtract(); - refreshPanelLiveState(); - return { - ...rollbackResult, - extractionTriggered: true, - extractionStatus: lastExtractionStatus?.level || "idle", - }; } async function onManualSleep() { diff --git a/tests/p0-regressions.mjs b/tests/p0-regressions.mjs index 174bf22..2add352 100644 --- a/tests/p0-regressions.mjs +++ b/tests/p0-regressions.mjs @@ -57,6 +57,7 @@ import { onBeforeCombinePromptsController, onGenerationAfterCommandsController, } from "../event-binding.js"; +import { onRerollController } from "../extraction-controller.js"; const extensionsShimSource = [ "export const extension_settings = globalThis.__p0ExtensionSettings || {};", @@ -461,7 +462,17 @@ function createRerollHarness() { context.onManualExtractCalls += 1; context.lastExtractionStatus = { level: context.manualExtractLevel }; }, + ensureGraphMutationReady() { + return true; + }, + getGraphMutationBlockReason() { + return "graph-not-ready"; + }, + graphPersistenceState: { + loadState: "loaded", + }, createUiStatus, + onRerollController, isAbortError: (e) => e?.name === "AbortError", assertRecoveryChatStillActive() { // no-op in test