mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-14 02:40:45 +08:00
refactor: controllerize onReroll and keep reroll harness green
This commit is contained in:
@@ -365,3 +365,167 @@ export async function onManualExtractController(runtime) {
|
|||||||
runtime.refreshPanelLiveState();
|
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
180
index.js
@@ -21,6 +21,7 @@ import { consolidateMemories } from "./consolidator.js";
|
|||||||
import {
|
import {
|
||||||
executeExtractionBatchController,
|
executeExtractionBatchController,
|
||||||
onManualExtractController,
|
onManualExtractController,
|
||||||
|
onRerollController,
|
||||||
runExtractionController,
|
runExtractionController,
|
||||||
} from "./extraction-controller.js";
|
} from "./extraction-controller.js";
|
||||||
import {
|
import {
|
||||||
@@ -4642,166 +4643,27 @@ async function onManualExtract() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onReroll({ fromFloor } = {}) {
|
async function onReroll({ fromFloor } = {}) {
|
||||||
if (isExtracting) {
|
return await onRerollController(
|
||||||
toastr.info("记忆提取正在进行中,请稍候");
|
{
|
||||||
return {
|
console,
|
||||||
success: false,
|
ensureGraphMutationReady,
|
||||||
rollbackPerformed: false,
|
getAssistantTurns,
|
||||||
extractionTriggered: false,
|
getContext,
|
||||||
requestedFloor: null,
|
getCurrentGraph: () => currentGraph,
|
||||||
effectiveFromFloor: null,
|
getGraphMutationBlockReason,
|
||||||
recoveryPath: "busy",
|
getGraphPersistenceState: () => graphPersistenceState,
|
||||||
affectedBatchCount: 0,
|
getIsExtracting: () => isExtracting,
|
||||||
error: "记忆提取正在进行中",
|
getLastExtractionStatusLevel: () => lastExtractionStatus?.level || "idle",
|
||||||
};
|
getLastProcessedAssistantFloor,
|
||||||
}
|
isAbortError,
|
||||||
if (
|
onManualExtract,
|
||||||
typeof ensureGraphMutationReady === "function" &&
|
refreshPanelLiveState,
|
||||||
!ensureGraphMutationReady("重新提取")
|
rollbackGraphForReroll,
|
||||||
) {
|
setRuntimeStatus,
|
||||||
return {
|
toastr,
|
||||||
success: false,
|
},
|
||||||
rollbackPerformed: false,
|
{ fromFloor },
|
||||||
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",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
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() {
|
async function onManualSleep() {
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import {
|
|||||||
onBeforeCombinePromptsController,
|
onBeforeCombinePromptsController,
|
||||||
onGenerationAfterCommandsController,
|
onGenerationAfterCommandsController,
|
||||||
} from "../event-binding.js";
|
} from "../event-binding.js";
|
||||||
|
import { onRerollController } from "../extraction-controller.js";
|
||||||
|
|
||||||
const extensionsShimSource = [
|
const extensionsShimSource = [
|
||||||
"export const extension_settings = globalThis.__p0ExtensionSettings || {};",
|
"export const extension_settings = globalThis.__p0ExtensionSettings || {};",
|
||||||
@@ -461,7 +462,17 @@ function createRerollHarness() {
|
|||||||
context.onManualExtractCalls += 1;
|
context.onManualExtractCalls += 1;
|
||||||
context.lastExtractionStatus = { level: context.manualExtractLevel };
|
context.lastExtractionStatus = { level: context.manualExtractLevel };
|
||||||
},
|
},
|
||||||
|
ensureGraphMutationReady() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
getGraphMutationBlockReason() {
|
||||||
|
return "graph-not-ready";
|
||||||
|
},
|
||||||
|
graphPersistenceState: {
|
||||||
|
loadState: "loaded",
|
||||||
|
},
|
||||||
createUiStatus,
|
createUiStatus,
|
||||||
|
onRerollController,
|
||||||
isAbortError: (e) => e?.name === "AbortError",
|
isAbortError: (e) => e?.name === "AbortError",
|
||||||
assertRecoveryChatStillActive() {
|
assertRecoveryChatStillActive() {
|
||||||
// no-op in test
|
// no-op in test
|
||||||
|
|||||||
Reference in New Issue
Block a user