refactor: controllerize onReroll and keep reroll harness green

This commit is contained in:
Youzini-afk
2026-03-29 17:39:41 +08:00
parent b7c986d9ea
commit 5023b75f3f
3 changed files with 196 additions and 159 deletions

View File

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

180
index.js
View File

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

View File

@@ -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