Files
ST-Bionic-Memory-Ecology/maintenance/extraction-controller.js

1627 lines
51 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ST-BME: 提取编排控制器(纯函数)
// 通过 runtime 依赖注入,避免直接访问 index.js 模块级状态。
import { debugLog } from "../runtime/debug-logging.js";
import {
buildDialogueFloorMap,
normalizeDialogueFloorRange,
} from "./chat-history.js";
let nativePersistDeltaInstallPromise = null;
function toSafeFloor(value, fallback = null) {
if (value == null || value === "") return fallback;
const numeric = Number(value);
return Number.isFinite(numeric) ? Math.floor(numeric) : fallback;
}
function clampIntValue(value, fallback = 0, min = 0, max = 9999) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return fallback;
return Math.min(max, Math.max(min, Math.trunc(numeric)));
}
function isAssistantFloor(runtime, chat, index) {
if (!Array.isArray(chat)) return false;
const message = chat[index];
if (!message) return false;
if (typeof runtime?.isAssistantChatMessage === "function") {
return Boolean(
runtime.isAssistantChatMessage(message, {
index,
chat,
}),
);
}
return Boolean(message) && !message.is_user && !message.is_system;
}
function getAssistantTurnsFallback(runtime, chat = []) {
if (!Array.isArray(chat)) return [];
const assistantTurns = [];
for (let index = 0; index < chat.length; index++) {
if (!isAssistantFloor(runtime, chat, index)) continue;
if (!String(chat[index]?.mes ?? "").trim()) continue;
assistantTurns.push(index);
}
return assistantTurns;
}
function normalizeSmartTriggerDecision(decision = null) {
if (!decision || typeof decision !== "object") {
return { triggered: false, score: 0, reasons: [] };
}
return {
triggered: decision.triggered === true,
score: Number.isFinite(Number(decision.score)) ? Number(decision.score) : 0,
reasons: Array.isArray(decision.reasons)
? decision.reasons.map((item) => String(item || "")).filter(Boolean)
: [],
};
}
function normalizePersistenceStateRecord(persistResult = null) {
const accepted = persistResult?.accepted === true;
const queued = persistResult?.queued === true;
const blocked = persistResult?.blocked === true;
let outcome = "failed";
if (accepted && String(persistResult?.storageTier || "") === "indexeddb") {
outcome = "saved";
} else if (accepted) {
outcome = "fallback";
} else if (queued) {
outcome = "queued";
} else if (blocked) {
outcome = "blocked";
}
return {
outcome,
accepted,
storageTier: String(persistResult?.storageTier || "none"),
reason: String(persistResult?.reason || ""),
revision: Number.isFinite(Number(persistResult?.revision))
? Number(persistResult.revision)
: 0,
saveMode: String(persistResult?.saveMode || ""),
recoverable: persistResult?.recoverable === true,
saved: persistResult?.saved === true,
queued,
blocked,
attempted: true,
};
}
function hasMeaningfulPersistenceRecord(persistence = null) {
if (!persistence || typeof persistence !== "object") return false;
if (persistence.attempted === true) return true;
const revision = Number(persistence?.revision || 0);
if (Number.isFinite(revision) && revision > 0) return true;
if (String(persistence?.storageTier || "").trim() && persistence.storageTier !== "none") {
return true;
}
if (String(persistence?.saveMode || "").trim()) return true;
if (String(persistence?.reason || "").trim()) return true;
return (
persistence.saved === true ||
persistence.queued === true ||
persistence.blocked === true
);
}
function cloneSerializable(value, fallback = null) {
try {
return JSON.parse(JSON.stringify(value));
} catch {
return fallback;
}
}
function readNow() {
if (typeof performance === "object" && typeof performance.now === "function") {
return performance.now();
}
return Date.now();
}
async function ensureNativePersistDeltaHookInstalled() {
if (typeof globalThis.__stBmeNativeBuildPersistDelta === "function") {
return {
loaded: true,
source: "global-hook",
};
}
if (!nativePersistDeltaInstallPromise) {
nativePersistDeltaInstallPromise = import("../vendor/wasm/stbme_core.js")
.then((module) => module?.installNativePersistDeltaHook?.())
.catch((error) => {
nativePersistDeltaInstallPromise = null;
throw error;
});
}
return await nativePersistDeltaInstallPromise;
}
function setExtractionProgressStatus(
runtime,
text,
meta = "",
level = "info",
options = {},
) {
if (typeof runtime?.setLastExtractionStatus === "function") {
runtime.setLastExtractionStatus(text, meta, level, options);
return;
}
if (options?.syncRuntime !== false && typeof runtime?.setRuntimeStatus === "function") {
runtime.setRuntimeStatus(text, meta, level);
}
}
function resolveLatestAssistantDialogueFloor(chat = []) {
const map = buildDialogueFloorMap(chat);
const assistantDialogueFloors = Array.isArray(map.assistantDialogueFloors)
? map.assistantDialogueFloors
: [];
return assistantDialogueFloors.length > 0
? assistantDialogueFloors[assistantDialogueFloors.length - 1]
: null;
}
function resolveRerunDialogueTask(chat = [], options = {}) {
const hasStart = Number.isFinite(Number(options?.startFloor));
const hasEnd = Number.isFinite(Number(options?.endFloor));
if (!hasStart && !hasEnd) {
const latestAssistantDialogueFloor = resolveLatestAssistantDialogueFloor(chat);
if (!Number.isFinite(Number(latestAssistantDialogueFloor))) {
return {
valid: false,
reason: "当前没有可重提的 AI 回复",
};
}
const normalizedRange = normalizeDialogueFloorRange(
chat,
latestAssistantDialogueFloor,
latestAssistantDialogueFloor,
);
return {
...normalizedRange,
mode: "current",
requestedStartFloor: null,
requestedEndFloor: null,
};
}
const normalizedRange = normalizeDialogueFloorRange(
chat,
options?.startFloor,
options?.endFloor,
);
return {
...normalizedRange,
mode: "range",
requestedStartFloor: hasStart ? Number(options.startFloor) : null,
requestedEndFloor: hasEnd ? Number(options.endFloor) : null,
};
}
function resolveAssistantTargetRange(chat = [], dialogueRange = [-1, -1]) {
const map = buildDialogueFloorMap(chat);
const assistantDialogueFloors = Array.isArray(map.assistantDialogueFloors)
? map.assistantDialogueFloors
: [];
const assistantChatIndices = Array.isArray(map.assistantChatIndices)
? map.assistantChatIndices
: [];
const [startFloor, endFloor] = Array.isArray(dialogueRange)
? dialogueRange
: [-1, -1];
const targeted = [];
for (let index = 0; index < assistantDialogueFloors.length; index += 1) {
const floor = Number(assistantDialogueFloors[index]);
const chatIndex = Number(assistantChatIndices[index]);
if (!Number.isFinite(floor) || !Number.isFinite(chatIndex)) continue;
if (floor < startFloor || floor > endFloor) continue;
targeted.push({
dialogueFloor: floor,
chatIndex,
});
}
return {
map,
targeted,
startAssistantChatIndex: targeted.length > 0 ? targeted[0].chatIndex : null,
endAssistantChatIndex:
targeted.length > 0 ? targeted[targeted.length - 1].chatIndex : null,
latestAssistantDialogueFloor:
assistantDialogueFloors.length > 0
? assistantDialogueFloors[assistantDialogueFloors.length - 1]
: null,
};
}
function buildRerunFallbackInfo(chat = [], targetDialogueRange = [-1, -1]) {
const assistantRange = resolveAssistantTargetRange(chat, targetDialogueRange);
if (!assistantRange.targeted.length) {
return {
valid: false,
reason: "目标范围内没有可重提的 AI 回复",
fallbackToLatest: false,
...assistantRange,
};
}
const latestTargetedDialogueFloor = Number(
assistantRange.targeted[assistantRange.targeted.length - 1]?.dialogueFloor,
);
const latestAssistantDialogueFloor = Number(
assistantRange.latestAssistantDialogueFloor,
);
const fallbackToLatest =
Number.isFinite(latestTargetedDialogueFloor) &&
Number.isFinite(latestAssistantDialogueFloor) &&
latestTargetedDialogueFloor < latestAssistantDialogueFloor;
return {
valid: true,
reason: fallbackToLatest
? "当前图谱对中段范围重提的后缀保留证据不足,已退化为从起始楼层到最新重提"
: "",
fallbackToLatest,
...assistantRange,
};
}
async function buildCommittedBatchPersistSnapshot(
runtime,
{
graph = null,
chat = [],
settings = null,
beforeSnapshot = null,
processedRange = [null, null],
postProcessArtifacts = [],
vectorHashesInserted = [],
extractionCountBefore = 0,
} = {},
) {
if (!graph || typeof runtime?.cloneGraphSnapshot !== "function") {
return {
persistGraphSnapshot: null,
committedBatchJournalEntry: null,
afterSnapshot: null,
committedAfterSnapshot: null,
postProcessArtifacts: Array.isArray(postProcessArtifacts)
? [...postProcessArtifacts]
: [],
};
}
const range = Array.isArray(processedRange) ? processedRange : [null, null];
const rangeStart = Number.isFinite(Number(range[0])) ? Number(range[0]) : null;
const rangeEnd = Number.isFinite(Number(range[1])) ? Number(range[1]) : null;
const runtimeSettings =
settings && typeof settings === "object" && !Array.isArray(settings)
? settings
: runtime?.getSettings?.() || {};
const dialogueMap = buildDialogueFloorMap(chat);
const processedDialogueRange = [
Number.isFinite(Number(rangeStart))
? dialogueMap.chatIndexToFloor[rangeStart]
: null,
Number.isFinite(Number(rangeEnd))
? dialogueMap.chatIndexToFloor[rangeEnd]
: null,
];
const sourceChatIndexRange = [
Number.isFinite(Number(rangeStart))
? Math.max(
0,
Number(rangeStart) -
Math.max(
0,
Number(runtimeSettings?.extractContextTurns) || 0,
) *
2,
)
: null,
rangeEnd,
];
const afterSnapshot = graph;
const effectiveArtifacts = Array.isArray(postProcessArtifacts)
? [...postProcessArtifacts]
: [];
const committedGraphSnapshot = runtime.cloneGraphSnapshot(graph);
if (typeof runtime.applyProcessedHistorySnapshotToGraph === "function") {
runtime.applyProcessedHistorySnapshotToGraph(
committedGraphSnapshot,
chat,
rangeEnd,
);
} else {
if (
!committedGraphSnapshot.historyState ||
typeof committedGraphSnapshot.historyState !== "object" ||
Array.isArray(committedGraphSnapshot.historyState)
) {
committedGraphSnapshot.historyState = {};
}
committedGraphSnapshot.historyState.lastProcessedAssistantFloor =
Number.isFinite(rangeEnd) ? Math.floor(rangeEnd) : -1;
committedGraphSnapshot.lastProcessedSeq =
Number.isFinite(rangeEnd) ? Math.floor(rangeEnd) : -1;
}
const committedBatchJournalEntry =
typeof runtime.createBatchJournalEntry === "function"
? runtime.createBatchJournalEntry(beforeSnapshot, afterSnapshot, {
processedRange: [rangeStart, rangeEnd],
processedDialogueRange,
sourceChatIndexRange,
postProcessArtifacts: effectiveArtifacts,
vectorHashesInserted: Array.isArray(vectorHashesInserted)
? vectorHashesInserted
: [],
extractionCountBefore,
})
: null;
if (
committedBatchJournalEntry &&
typeof runtime.appendBatchJournal === "function"
) {
runtime.appendBatchJournal(
committedGraphSnapshot,
cloneSerializable(committedBatchJournalEntry, committedBatchJournalEntry),
);
}
let persistDelta = null;
let persistSnapshot = null;
const shouldUseNativePersistDelta =
runtimeSettings?.persistUseNativeDelta === true &&
runtimeSettings?.graphNativeForceDisable !== true;
const nativeFailOpen = runtimeSettings?.nativeEngineFailOpen !== false;
if (typeof runtime.buildSnapshotFromGraph === "function") {
persistSnapshot = runtime.buildSnapshotFromGraph(committedGraphSnapshot, {
chatId:
committedGraphSnapshot?.historyState?.chatId ||
beforeSnapshot?.meta?.chatId ||
"",
revision: Number(beforeSnapshot?.meta?.revision || 0) + 1,
baseSnapshot: beforeSnapshot || undefined,
lastModified: Date.now(),
});
}
if (typeof runtime.buildPersistDelta === "function") {
if (shouldUseNativePersistDelta) {
const preloadStartedAt = readNow();
try {
await ensureNativePersistDeltaHookInstalled();
} catch (error) {
if (!nativeFailOpen) {
throw error;
}
runtime?.console?.warn?.(
"[ST-BME] extraction native persist delta preload failed, fallback to JS delta:",
{
error: error?.message || String(error),
preloadMs: readNow() - preloadStartedAt,
},
);
}
}
persistDelta = runtime.buildPersistDelta(
beforeSnapshot,
persistSnapshot || committedGraphSnapshot,
{
useNativeDelta: shouldUseNativePersistDelta,
nativeFailOpen,
persistNativeDeltaThresholdRecords:
runtimeSettings?.persistNativeDeltaThresholdRecords,
persistNativeDeltaThresholdStructuralDelta:
runtimeSettings?.persistNativeDeltaThresholdStructuralDelta,
persistNativeDeltaThresholdSerializedChars:
runtimeSettings?.persistNativeDeltaThresholdSerializedChars,
persistNativeDeltaBridgeMode: runtimeSettings?.persistNativeDeltaBridgeMode,
},
);
}
return {
persistDelta,
persistSnapshot,
persistGraphSnapshot: committedGraphSnapshot,
committedBatchJournalEntry,
afterSnapshot,
committedAfterSnapshot: committedGraphSnapshot,
postProcessArtifacts: effectiveArtifacts,
};
}
function isPersistenceRevisionAccepted(runtime, persistence = null) {
const persistenceRevision = Number(persistence?.revision || 0);
if (!Number.isFinite(persistenceRevision) || persistenceRevision <= 0) {
return false;
}
const lastAcceptedRevision = Number(
runtime?.getGraphPersistenceState?.()?.lastAcceptedRevision || 0,
);
return Number.isFinite(lastAcceptedRevision) && lastAcceptedRevision >= persistenceRevision;
}
function hasRecoverablePendingPersistence(runtime) {
const persistenceState = runtime?.getGraphPersistenceState?.() || {};
if (persistenceState.pendingPersist !== true) {
return false;
}
const recoverableTier = String(
persistenceState.lastRecoverableStorageTier || "none",
).trim();
if (recoverableTier === "metadata-full") {
return true;
}
if (recoverableTier !== "shadow") {
return false;
}
const queuedRevision = Number(persistenceState.queuedPersistRevision || 0);
const shadowRevision = Number(persistenceState.shadowSnapshotRevision || 0);
if (!Number.isFinite(queuedRevision) || queuedRevision <= 0) {
return true;
}
return Number.isFinite(shadowRevision) && shadowRevision >= queuedRevision;
}
function getPendingPersistenceGateInfo(runtime) {
const graph = runtime?.getCurrentGraph?.();
const batchStatus = graph?.historyState?.lastBatchStatus || null;
const persistence = batchStatus?.persistence || null;
const pendingPersist = runtime?.getGraphPersistenceState?.()?.pendingPersist === true;
const accepted = isPersistenceRevisionAccepted(runtime, persistence);
const attempted = hasMeaningfulPersistenceRecord(persistence);
if (!pendingPersist && (!attempted || accepted)) {
return null;
}
return {
pendingPersist,
accepted,
attempted,
outcome: String(persistence?.outcome || ""),
reason: String(persistence?.reason || ""),
revision: Number.isFinite(Number(persistence?.revision))
? Number(persistence.revision)
: 0,
};
}
async function maybeRetryPendingPersistence(runtime, reason = "pending-persist-retry") {
const gate = getPendingPersistenceGateInfo(runtime);
if (!gate) {
return gate;
}
if (typeof runtime?.retryPendingGraphPersist !== "function") {
return hasRecoverablePendingPersistence(runtime) ? null : gate;
}
try {
const retryResult = await runtime.retryPendingGraphPersist({ reason });
if (retryResult?.accepted === true) {
return null;
}
} catch (error) {
runtime?.console?.warn?.("[ST-BME] pending persistence retry failed", error);
}
const nextGate = getPendingPersistenceGateInfo(runtime);
if (nextGate && hasRecoverablePendingPersistence(runtime)) {
return null;
}
return nextGate;
}
function formatPendingPersistenceGateMessage(runtime, operationLabel = "当前提取") {
const gate = getPendingPersistenceGateInfo(runtime);
if (!gate) return "";
const reason = gate.reason ? ` · ${gate.reason}` : "";
const revision =
Number.isFinite(Number(gate.revision)) && Number(gate.revision) > 0
? ` · rev ${Number(gate.revision)}`
: "";
return `${operationLabel}已暂停:上一批持久化尚未确认,请先使用“重试持久化”或“重新探测图谱”${revision}${reason}`;
}
export function resolveAutoExtractionPlanController(
runtime,
{
chat = null,
settings = null,
lastProcessedAssistantFloor = null,
lockedEndFloor = null,
} = {},
) {
const resolvedChat = Array.isArray(chat)
? chat
: runtime?.getContext?.()?.chat || [];
const resolvedSettings =
settings && typeof settings === "object"
? settings
: runtime?.getSettings?.() || {};
const safeLastProcessedAssistantFloor = toSafeFloor(
lastProcessedAssistantFloor,
toSafeFloor(runtime?.getLastProcessedAssistantFloor?.(), -1),
);
const safeLockedEndFloor = toSafeFloor(lockedEndFloor, null);
const strategy =
resolvedSettings.extractAutoDelayLatestAssistant === true
? "lag-one-assistant"
: "normal";
const extractEvery = clampIntValue(
resolvedSettings.extractEvery,
1,
1,
50,
);
const assistantTurns =
typeof runtime?.getAssistantTurns === "function"
? runtime.getAssistantTurns(resolvedChat)
: getAssistantTurnsFallback(runtime, resolvedChat);
const pendingAssistantTurns = assistantTurns.filter(
(floor) => floor > safeLastProcessedAssistantFloor,
);
const candidateAssistantTurns =
safeLockedEndFloor == null
? pendingAssistantTurns
: pendingAssistantTurns.filter((floor) => floor <= safeLockedEndFloor);
let eligibleAssistantTurns = candidateAssistantTurns;
let waitingForNextAssistant = false;
if (safeLockedEndFloor == null && strategy === "lag-one-assistant") {
if (candidateAssistantTurns.length <= 1) {
eligibleAssistantTurns = [];
waitingForNextAssistant = candidateAssistantTurns.length === 1;
} else {
eligibleAssistantTurns = candidateAssistantTurns.slice(0, -1);
}
}
const eligibleEndFloor =
eligibleAssistantTurns.length > 0
? eligibleAssistantTurns[eligibleAssistantTurns.length - 1]
: null;
const smartTriggerDecision =
resolvedSettings.enableSmartTrigger && eligibleEndFloor != null
? normalizeSmartTriggerDecision(
runtime?.getSmartTriggerDecision?.(
resolvedChat,
safeLastProcessedAssistantFloor,
resolvedSettings,
eligibleEndFloor,
),
)
: { triggered: false, score: 0, reasons: [] };
const meetsExtractEvery = eligibleAssistantTurns.length >= extractEvery;
const canRun =
eligibleAssistantTurns.length > 0 &&
(meetsExtractEvery || smartTriggerDecision.triggered);
const batchAssistantTurns = canRun
? smartTriggerDecision.triggered
? eligibleAssistantTurns
: eligibleAssistantTurns.slice(0, extractEvery)
: [];
const plannedBatchEndFloor =
batchAssistantTurns.length > 0
? batchAssistantTurns[batchAssistantTurns.length - 1]
: null;
let reason = "";
if (pendingAssistantTurns.length === 0) {
reason = "no-unprocessed-assistant-turns";
} else if (candidateAssistantTurns.length === 0) {
reason =
safeLockedEndFloor == null
? "no-candidate-assistant-turns"
: "locked-target-missing";
} else if (waitingForNextAssistant) {
reason = "waiting-next-assistant";
} else if (!canRun && !smartTriggerDecision.triggered) {
reason = "below-extract-every";
}
return {
strategy,
chat: resolvedChat,
settings: resolvedSettings,
lastProcessedAssistantFloor: safeLastProcessedAssistantFloor,
lockedEndFloor: safeLockedEndFloor,
extractEvery,
pendingAssistantTurns,
candidateAssistantTurns,
eligibleAssistantTurns,
eligibleEndFloor,
waitingForNextAssistant,
smartTriggerDecision,
meetsExtractEvery,
canRun,
batchAssistantTurns,
plannedBatchEndFloor,
startIdx: batchAssistantTurns[0] ?? null,
endIdx: plannedBatchEndFloor,
reason,
};
}
export async function executeExtractionBatchController(
runtime,
{
chat,
startIdx,
endIdx,
settings,
smartTriggerDecision = null,
signal = undefined,
} = {},
) {
runtime.ensureCurrentGraphRuntimeState();
runtime.throwIfAborted(signal, "提取已终止");
const currentGraph = runtime.getCurrentGraph();
const lastProcessed = runtime.getLastProcessedAssistantFloor();
const extractionCountBefore = runtime.getExtractionCount();
const beforeSnapshot = runtime.cloneGraphSnapshot(currentGraph);
const messages = runtime.buildExtractionMessages(chat, startIdx, endIdx, settings);
const batchStatus = runtime.createBatchStatusSkeleton({
processedRange: [startIdx, endIdx],
extractionCountBefore,
});
debugLog(
`[ST-BME] 开始提取: 楼层 ${startIdx}-${endIdx}` +
(smartTriggerDecision?.triggered
? ` [智能触发 score=${smartTriggerDecision.score}; ${smartTriggerDecision.reasons.join(" / ")}]`
: ""),
);
const result = await runtime.extractMemories({
graph: currentGraph,
messages,
startSeq: startIdx,
endSeq: endIdx,
lastProcessedSeq: lastProcessed,
schema: runtime.getSchema(),
embeddingConfig: runtime.getEmbeddingConfig(),
extractPrompt: undefined,
settings,
signal,
onStreamProgress: ({ previewText, receivedChars }) => {
const preview =
previewText?.length > 60 ? "…" + previewText.slice(-60) : previewText || "";
runtime.setLastExtractionStatus(
"AI 生成中",
`${preview} [${receivedChars}字]`,
"running",
{ noticeMarquee: true },
);
},
});
if (!result.success) {
runtime.setBatchStageOutcome(
batchStatus,
"core",
"failed",
result?.error || "提取阶段未返回有效操作",
);
runtime.setBatchStageOutcome(
batchStatus,
"finalize",
"failed",
"提取阶段失败,未进入持久化",
);
batchStatus.persistence = null;
batchStatus.historyAdvanceAllowed = false;
batchStatus.historyAdvanced = false;
runtime.finalizeBatchStatus(batchStatus, runtime.getExtractionCount());
runtime.getCurrentGraph().historyState.lastBatchStatus = batchStatus;
return {
success: false,
result,
effects: null,
batchStatus,
error: result?.error || "提取阶段未返回有效操作",
};
}
runtime.setBatchStageOutcome(batchStatus, "core", "success");
const effects = await runtime.handleExtractionSuccess(
result,
endIdx,
settings,
signal,
batchStatus,
);
const batchStatusRef = effects?.batchStatus || batchStatus;
const committedPersistState = await buildCommittedBatchPersistSnapshot(runtime, {
graph: runtime.getCurrentGraph(),
chat,
settings,
beforeSnapshot,
processedRange: [startIdx, endIdx],
postProcessArtifacts: runtime.computePostProcessArtifacts(
beforeSnapshot,
runtime.getCurrentGraph(),
effects?.postProcessArtifacts || [],
),
vectorHashesInserted: effects?.vectorHashesInserted || [],
extractionCountBefore,
});
const persistResult = await runtime.persistExtractionBatchResult({
reason: "extraction-batch-complete",
lastProcessedAssistantFloor: endIdx,
graphSnapshot: committedPersistState.persistGraphSnapshot,
persistSnapshot: committedPersistState.persistSnapshot,
persistDelta: committedPersistState.persistDelta,
});
const persistence = normalizePersistenceStateRecord(persistResult);
batchStatusRef.persistence = persistence;
batchStatusRef.historyAdvanceAllowed = persistence.accepted === true;
const finalizedBatchStatus = runtime.finalizeBatchStatus(
batchStatusRef,
runtime.getExtractionCount(),
);
runtime.getCurrentGraph().historyState.lastBatchStatus = {
...finalizedBatchStatus,
persistence,
historyAdvanceAllowed: persistence.accepted === true,
historyAdvanced: runtime.shouldAdvanceProcessedHistory({
...finalizedBatchStatus,
historyAdvanceAllowed: persistence.accepted === true,
}),
};
if (runtime.getCurrentGraph().historyState.lastBatchStatus.historyAdvanced) {
runtime.updateProcessedHistorySnapshot(chat, endIdx);
if (committedPersistState.committedBatchJournalEntry) {
runtime.appendBatchJournal(
runtime.getCurrentGraph(),
cloneSerializable(
committedPersistState.committedBatchJournalEntry,
committedPersistState.committedBatchJournalEntry,
),
);
}
} else if (!persistence.accepted) {
// 即使持久化未被接受,仍在内存中推进 lastProcessedAssistantFloor
// 防止同一会话内对已经抽取过的楼层重复提取。
// 此时不追加 batchJournal保持回滚完整性
// 如果用户重载floor 和图谱都会回退到最后持久化状态,保持一致。
runtime.updateProcessedHistorySnapshot(chat, endIdx);
runtime.setLastExtractionStatus(
"提取待恢复",
`楼层 ${startIdx}-${endIdx} 已抽取,但持久化状态为 ${persistence.outcome || "failed"}${persistence.reason ? ` · ${persistence.reason}` : ""}`,
"warning",
{ syncRuntime: true },
);
runtime.console?.warn?.("[ST-BME] extraction persist not accepted", {
chatId: runtime.getGraphPersistenceState?.()?.chatId || "",
persistence,
processedRange: [startIdx, endIdx],
});
}
return {
success: finalizedBatchStatus.completed,
result,
effects: {
...(effects || {}),
persistResult,
},
batchStatus: finalizedBatchStatus,
persistResult,
historyAdvanceAllowed: persistence.accepted === true,
error: finalizedBatchStatus.completed
? ""
: effects?.vectorError ||
finalizedBatchStatus.errors?.[0] ||
"批次未完成 finalize 闭环",
};
}
export async function runExtractionController(runtime, options = {}) {
const lockedEndFloor = toSafeFloor(options?.lockedEndFloor, null);
const triggerSource = String(options?.triggerSource || "auto").trim() || "auto";
const settings = runtime.getSettings?.() || {};
const context = runtime.getContext?.() || {};
const chat = Array.isArray(context?.chat) ? context.chat : [];
const plan = resolveAutoExtractionPlanController(runtime, {
chat,
settings,
lockedEndFloor,
});
const deferredTargetEndFloor =
plan.plannedBatchEndFloor ?? lockedEndFloor;
if (runtime.getIsExtracting()) {
runtime.console?.debug?.("[ST-BME] auto extraction deferred: extraction already in progress");
runtime.deferAutoExtraction?.("extracting", {
targetEndFloor: deferredTargetEndFloor,
strategy: plan.strategy,
});
return;
}
if (!settings.enabled) return;
if (!runtime.ensureGraphMutationReady("自动提取", { notify: false })) {
runtime.console?.debug?.("[ST-BME] auto extraction blocked: graph-not-ready", {
loadState: runtime.getGraphPersistenceState?.()?.loadState || "",
});
runtime.deferAutoExtraction?.("graph-not-ready", {
targetEndFloor: deferredTargetEndFloor,
strategy: plan.strategy,
});
runtime.setLastExtractionStatus(
"等待图谱加载",
runtime.getGraphMutationBlockReason("自动提取"),
"warning",
{ syncRuntime: true },
);
return;
}
const pendingPersistGate = await maybeRetryPendingPersistence(
runtime,
"auto-extraction-persist-retry",
);
const pendingPersistMessage = pendingPersistGate
? formatPendingPersistenceGateMessage(runtime, "自动提取")
: "";
if (pendingPersistMessage) {
runtime.console?.debug?.("[ST-BME] auto extraction paused: pending persistence", {
persistence: runtime.getCurrentGraph?.()?.historyState?.lastBatchStatus?.persistence || null,
});
runtime.deferAutoExtraction?.("pending-persist", {
targetEndFloor: deferredTargetEndFloor,
strategy: plan.strategy,
});
runtime.setLastExtractionStatus(
"等待持久化确认",
pendingPersistMessage,
"warning",
{ syncRuntime: true },
);
return;
}
if (!runtime.getCurrentGraph()) {
runtime.ensureCurrentGraphRuntimeState?.();
}
if (!(await runtime.recoverHistoryIfNeeded("auto-extract"))) {
runtime.console?.debug?.("[ST-BME] auto extraction paused during history recovery", {
recovering: runtime.getIsRecoveringHistory?.() === true,
});
if (runtime.getIsRecoveringHistory?.()) {
runtime.deferAutoExtraction?.("history-recovering", {
targetEndFloor: deferredTargetEndFloor,
strategy: plan.strategy,
});
}
return;
}
if (!chat || chat.length === 0) return;
if (!plan.canRun || plan.startIdx == null || plan.endIdx == null) {
return;
}
const startIdx = plan.startIdx;
const endIdx = plan.endIdx;
const smartTriggerDecision = plan.smartTriggerDecision;
runtime.setIsExtracting(true);
const extractionController = runtime.beginStageAbortController("extraction");
const extractionSignal = extractionController.signal;
runtime.setLastExtractionStatus(
"提取中",
`楼层 ${startIdx}-${endIdx}${smartTriggerDecision.triggered ? " · 智能触发" : ""}${triggerSource !== "auto" ? ` · ${triggerSource}` : ""}`,
"running",
{ syncRuntime: true },
);
try {
const batchResult = await runtime.executeExtractionBatch({
chat,
startIdx,
endIdx,
settings,
smartTriggerDecision,
signal: extractionSignal,
});
if (!batchResult.success) {
const message =
batchResult.error ||
batchResult?.result?.error ||
"提取批次未返回有效结果";
runtime.console.warn("[ST-BME] 提取批次未返回有效结果:", message);
runtime.notifyExtractionIssue(message);
return;
}
const persistence = batchResult.batchStatus?.persistence || null;
if (batchResult.historyAdvanceAllowed === false) {
runtime.setLastExtractionStatus(
"提取完成,持久化待确认",
`楼层 ${startIdx}-${endIdx} · 新建 ${batchResult.result?.newNodes || 0} · 更新 ${batchResult.result?.updatedNodes || 0} · 新边 ${batchResult.result?.newEdges || 0}${persistence?.reason ? ` · ${persistence.reason}` : ""}`,
"warning",
{ syncRuntime: true },
);
} else {
runtime.setLastExtractionStatus(
"提取完成",
`楼层 ${startIdx}-${endIdx} · 新建 ${batchResult.result?.newNodes || 0} · 更新 ${batchResult.result?.updatedNodes || 0} · 新边 ${batchResult.result?.newEdges || 0}`,
"success",
{ syncRuntime: true },
);
}
} catch (e) {
if (runtime.isAbortError(e)) {
runtime.setLastExtractionStatus(
"提取已终止",
e?.message || "已手动终止当前提取",
"warning",
{
syncRuntime: true,
},
);
return;
}
runtime.console.error("[ST-BME] 提取失败:", e);
runtime.notifyExtractionIssue(e?.message || String(e) || "自动提取失败");
} finally {
runtime.finishStageAbortController("extraction", extractionController);
runtime.setIsExtracting(false);
}
}
export async function onManualExtractController(runtime, options = {}) {
if (runtime.getIsExtracting()) {
runtime.toastr.info("记忆提取正在进行中,请稍候");
return;
}
const taskLabel = String(options?.taskLabel || "手动提取").trim() || "手动提取";
const toastTitle = String(options?.toastTitle || `ST-BME ${taskLabel}`).trim() ||
`ST-BME ${taskLabel}`;
const showStartToast = options?.showStartToast !== false;
const lockedEndFloor = toSafeFloor(options?.lockedEndFloor, null);
if (!runtime.ensureGraphMutationReady(taskLabel)) return;
const pendingPersistGate = await maybeRetryPendingPersistence(
runtime,
"manual-extraction-persist-retry",
);
const pendingPersistMessage = pendingPersistGate
? formatPendingPersistenceGateMessage(runtime, taskLabel)
: "";
if (pendingPersistMessage) {
runtime.setLastExtractionStatus(
"等待持久化确认",
pendingPersistMessage,
"warning",
{
syncRuntime: true,
},
);
runtime.toastr.warning("上一批持久化尚未确认,请先点“重试持久化”或“重新探测图谱”");
return;
}
if (!(await runtime.recoverHistoryIfNeeded("manual-extract"))) return;
if (!runtime.getCurrentGraph()) {
runtime.setCurrentGraph(
runtime.normalizeGraphRuntimeState(
runtime.createEmptyGraph(),
runtime.getCurrentChatId(),
),
);
}
const context = runtime.getContext();
const chat = context.chat;
if (!Array.isArray(chat) || chat.length === 0) {
runtime.toastr.info("当前聊天为空,暂无可提取内容");
return;
}
const assistantTurns = runtime.getAssistantTurns(chat);
const lastProcessed = runtime.getLastProcessedAssistantFloor();
const pendingAssistantTurns = assistantTurns.filter((i) => i > lastProcessed);
const targetAssistantTurns = pendingAssistantTurns.filter((i) => {
if (lockedEndFloor != null && i > lockedEndFloor) return false;
return true;
});
if (pendingAssistantTurns.length === 0) {
runtime.toastr.info("没有待提取的新回复");
return;
}
const settings = runtime.getSettings();
const extractEvery = runtime.clampInt(settings.extractEvery, 1, 1, 50);
const totals = {
newNodes: 0,
updatedNodes: 0,
newEdges: 0,
batches: 0,
};
let processedAssistantTurns = 0;
const warnings = [];
runtime.setIsExtracting(true);
const extractionController = runtime.beginStageAbortController("extraction");
const extractionSignal = extractionController.signal;
setExtractionProgressStatus(
runtime,
`${taskLabel}`,
lockedEndFloor != null
? `待处理 AI 回复 ${targetAssistantTurns.length} 条 · 截止 chatIndex ${lockedEndFloor}`
: `待处理 AI 回复 ${targetAssistantTurns.length}`,
"running",
{
syncRuntime: true,
toastKind: showStartToast ? "info" : "",
toastTitle,
},
);
try {
while (true) {
const pendingTurns = runtime
.getAssistantTurns(chat)
.filter((i) => {
if (i <= runtime.getLastProcessedAssistantFloor()) return false;
if (lockedEndFloor != null && i > lockedEndFloor) return false;
return true;
});
if (pendingTurns.length === 0) break;
const batchAssistantTurns = pendingTurns.slice(0, extractEvery);
const startIdx = batchAssistantTurns[0];
const endIdx = batchAssistantTurns[batchAssistantTurns.length - 1];
const batchResult = await runtime.executeExtractionBatch({
chat,
startIdx,
endIdx,
settings,
signal: extractionSignal,
});
if (!batchResult.success) {
throw new Error(
batchResult.error ||
batchResult?.result?.error ||
"手动提取未返回有效结果",
);
}
totals.newNodes += batchResult.result.newNodes || 0;
totals.updatedNodes += batchResult.result.updatedNodes || 0;
totals.newEdges += batchResult.result.newEdges || 0;
totals.batches++;
processedAssistantTurns += batchAssistantTurns.length;
if (Array.isArray(batchResult.effects?.warnings)) {
warnings.push(...batchResult.effects.warnings);
}
const totalTurnsForDisplay = Math.max(
processedAssistantTurns,
targetAssistantTurns.length,
);
setExtractionProgressStatus(
runtime,
`${taskLabel}`,
totalTurnsForDisplay > 0
? `已处理 ${processedAssistantTurns}/${totalTurnsForDisplay} 条 AI 回复 · 当前楼层 ${startIdx}-${endIdx} · 累计 ${totals.batches}`
: `当前楼层 ${startIdx}-${endIdx} · 累计 ${totals.batches}`,
"running",
{
syncRuntime: true,
toastKind: "",
toastTitle,
},
);
if (batchResult.historyAdvanceAllowed === false) {
warnings.push(
batchResult.batchStatus?.persistence?.reason ||
"当前批次持久化尚未确认",
);
break;
}
if (options?.drainAll === false) {
break;
}
}
if (totals.batches === 0) {
setExtractionProgressStatus(
runtime,
"无待提取内容",
lockedEndFloor != null
? "指定范围内没有新的 assistant 回复需要处理"
: "没有新的 assistant 回复需要处理",
"info",
{
syncRuntime: true,
},
);
runtime.toastr.info("没有待提取的新回复");
return;
}
const pendingAfterRun = getPendingPersistenceGateInfo(runtime);
if (pendingAfterRun) {
runtime.toastr.warning(
`提取完成但持久化待确认:${pendingAfterRun.reason || pendingAfterRun.outcome || "unknown"}`,
);
runtime.setLastExtractionStatus(
`${taskLabel}完成,持久化待确认`,
`${totals.batches} 批 · 新建 ${totals.newNodes} · 更新 ${totals.updatedNodes} · 新边 ${totals.newEdges}${pendingAfterRun.reason ? ` · ${pendingAfterRun.reason}` : ""}`,
"warning",
{
syncRuntime: true,
toastKind: "",
toastTitle,
},
);
} else {
runtime.toastr.success(
`提取完成:${totals.batches} 批,新建 ${totals.newNodes},更新 ${totals.updatedNodes},新边 ${totals.newEdges}`,
);
runtime.setLastExtractionStatus(
`${taskLabel}完成`,
`${totals.batches} 批 · 新建 ${totals.newNodes} · 更新 ${totals.updatedNodes} · 新边 ${totals.newEdges}`,
"success",
{
syncRuntime: true,
toastKind: "success",
toastTitle,
},
);
}
if (warnings.length > 0) {
runtime.toastr.warning(warnings.slice(0, 2).join(""), "ST-BME 提取警告", {
timeOut: 5000,
});
}
} catch (e) {
if (runtime.isAbortError(e)) {
runtime.setLastExtractionStatus(
`${taskLabel}已终止`,
e?.message || "已手动终止当前提取",
"warning",
{
syncRuntime: true,
},
);
return;
}
runtime.console.error("[ST-BME] 手动提取失败:", e);
runtime.setLastExtractionStatus(`${taskLabel}失败`, e?.message || String(e), "error", {
syncRuntime: true,
toastKind: "",
toastTitle,
});
runtime.toastr.error(`${taskLabel}失败: ${e.message || e}`);
} finally {
runtime.finishStageAbortController("extraction", extractionController);
runtime.setIsExtracting(false);
runtime.refreshPanelLiveState();
}
}
export async function onExtractionTaskController(runtime, options = {}) {
const requestedMode = String(options?.mode || "pending").trim().toLowerCase();
const context = runtime.getContext?.() || {};
const chat = Array.isArray(context?.chat) ? context.chat : [];
const runManualExtract = async (manualOptions = {}) => {
if (typeof runtime?.onManualExtract === "function") {
return await runtime.onManualExtract(manualOptions);
}
return await onManualExtractController(runtime, manualOptions);
};
if (requestedMode === "pending") {
return await runManualExtract({
...options,
taskLabel: "提取未处理",
toastTitle: "ST-BME 重新提取",
});
}
const rerunTask = resolveRerunDialogueTask(chat, options);
if (!rerunTask.valid) {
runtime.toastr?.info?.(rerunTask.reason || "当前没有可重提的范围");
return {
success: false,
rerunPerformed: false,
fallbackToLatest: false,
requestedRange: [null, null],
effectiveDialogueRange: [null, null],
reason: rerunTask.reason || "invalid-rerun-range",
};
}
const fallbackInfo = buildRerunFallbackInfo(chat, [
rerunTask.startFloor,
rerunTask.endFloor,
]);
if (!fallbackInfo.valid) {
runtime.toastr?.info?.(fallbackInfo.reason || "目标范围内没有可重提的 AI 回复");
return {
success: false,
rerunPerformed: false,
fallbackToLatest: false,
requestedRange: [rerunTask.requestedStartFloor, rerunTask.requestedEndFloor],
effectiveDialogueRange: [rerunTask.startFloor, rerunTask.endFloor],
reason: fallbackInfo.reason || "no-assistant-in-range",
};
}
const effectiveLockedEndFloor = fallbackInfo.fallbackToLatest
? null
: fallbackInfo.endAssistantChatIndex;
const effectiveDialogueRange = [
rerunTask.startFloor,
fallbackInfo.fallbackToLatest
? Number.isFinite(Number(fallbackInfo.latestAssistantDialogueFloor))
? Number(fallbackInfo.latestAssistantDialogueFloor)
: rerunTask.endFloor
: rerunTask.endFloor,
];
setExtractionProgressStatus(
runtime,
"重新提取准备中",
fallbackInfo.fallbackToLatest
? `范围 ${rerunTask.startFloor} ~ ${rerunTask.endFloor} 命中旧批次,但当前将退化为从 ${effectiveDialogueRange[0]} 到最新重提`
: `准备重提范围 ${rerunTask.startFloor} ~ ${rerunTask.endFloor}`,
fallbackInfo.fallbackToLatest ? "warning" : "running",
{
syncRuntime: true,
toastKind: "info",
toastTitle: "ST-BME 重新提取",
},
);
let rollbackResult = await runtime.rollbackGraphForReroll(
fallbackInfo.startAssistantChatIndex,
context,
);
// 回滚点不可用时,自动尝试历史恢复后降级为 pending 模式
if (
!rollbackResult?.success &&
rollbackResult?.resultCode === "reroll.rollback.unavailable" &&
typeof runtime.recoverHistoryIfNeeded === "function"
) {
setExtractionProgressStatus(
runtime,
"重新提取准备中",
"未找到回滚点,正在自动执行历史恢复后重新提取",
"running",
{
syncRuntime: true,
toastKind: "info",
toastTitle: "ST-BME 重新提取",
},
);
const recovered = await runtime.recoverHistoryIfNeeded(
"rerun-rollback-unavailable",
);
if (recovered) {
// 历史恢复成功,降级为 pending 模式继续提取
setExtractionProgressStatus(
runtime,
"重新提取中",
"历史恢复完成,正在提取未处理内容",
"running",
{
syncRuntime: true,
toastKind: "",
toastTitle: "ST-BME 重新提取",
},
);
await runManualExtract({
drainAll: true,
taskLabel: "重新提取(恢复后)",
toastTitle: "ST-BME 重新提取",
showStartToast: false,
});
return {
success: true,
rerunPerformed: true,
recoveryFallback: true,
fallbackToLatest: true,
requestedRange: [
rerunTask.requestedStartFloor,
rerunTask.requestedEndFloor,
],
effectiveDialogueRange,
reason: "rollback-unavailable-recovered-pending",
};
}
}
if (!rollbackResult?.success) {
const rollbackError = String(
rollbackResult?.error ||
rollbackResult?.reason ||
rollbackResult?.recoveryPath ||
"回滚失败",
).trim() || "回滚失败";
setExtractionProgressStatus(
runtime,
"重新提取失败",
rollbackError,
"warning",
{
syncRuntime: true,
toastKind: "",
toastTitle: "ST-BME 重新提取",
},
);
runtime.toastr?.warning?.(
`重新提取未开始:${rollbackError}`,
"ST-BME 重新提取",
{
timeOut: 4500,
},
);
return {
...rollbackResult,
rerunPerformed: false,
fallbackToLatest: fallbackInfo.fallbackToLatest,
requestedRange: [rerunTask.requestedStartFloor, rerunTask.requestedEndFloor],
effectiveDialogueRange,
};
}
if (fallbackInfo.reason) {
runtime.toastr?.warning?.(fallbackInfo.reason, "ST-BME 重新提取", {
timeOut: 3500,
});
}
const rollbackDesc =
rollbackResult.effectiveFromFloor !== fallbackInfo.startAssistantChatIndex
? `已按批次边界回滚到楼层 ${rollbackResult.effectiveFromFloor},正在开始重新提取`
: `已回滚到楼层 ${fallbackInfo.startAssistantChatIndex},正在开始重新提取`;
setExtractionProgressStatus(
runtime,
"重新提取中",
rollbackDesc,
"running",
{
syncRuntime: true,
toastKind: "",
toastTitle: "ST-BME 重新提取",
},
);
await runManualExtract({
drainAll: true,
lockedEndFloor: effectiveLockedEndFloor,
taskLabel: "重新提取",
toastTitle: "ST-BME 重新提取",
showStartToast: false,
});
return {
success: true,
rerunPerformed: true,
fallbackToLatest: fallbackInfo.fallbackToLatest,
requestedRange: [rerunTask.requestedStartFloor, rerunTask.requestedEndFloor],
effectiveDialogueRange,
effectiveAssistantChatRange: [
fallbackInfo.startAssistantChatIndex,
effectiveLockedEndFloor,
],
rollbackResult,
reason: fallbackInfo.reason || "",
};
}
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];
}
setExtractionProgressStatus(
runtime,
"重新提取准备中",
Number.isFinite(targetFloor)
? `准备从楼层 ${targetFloor} 开始回滚并重新提取`
: "准备回滚最新 AI 楼并重新提取",
"running",
{
syncRuntime: true,
toastKind: "info",
toastTitle: "ST-BME 重 Roll",
},
);
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: "",
};
}
debugLog(`[ST-BME] 重 Roll 开始,目标楼层: ${targetFloor}`);
let rollbackResult;
try {
rollbackResult = await runtime.rollbackGraphForReroll(targetFloor, context);
} catch (e) {
if (runtime.isAbortError(e)) {
setExtractionProgressStatus(
runtime,
"重新提取已取消",
e.message || "聊天已切换",
"warning",
{
syncRuntime: true,
},
);
return {
success: false,
rollbackPerformed: false,
extractionTriggered: false,
requestedFloor: targetFloor,
effectiveFromFloor: null,
recoveryPath: "aborted",
affectedBatchCount: 0,
error: e.message || "聊天已切换,重新提取已取消",
};
}
throw e;
}
if (!rollbackResult?.success) {
setExtractionProgressStatus(
runtime,
"重新提取失败",
rollbackResult.error || "回滚失败",
"error",
{
syncRuntime: true,
},
);
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,
});
setExtractionProgressStatus(
runtime,
"重新提取中",
rerollDesc,
"running",
{
syncRuntime: true,
toastKind: "",
toastTitle: "ST-BME 重 Roll",
},
);
await runtime.onManualExtract({ drainAll: false, showStartToast: false });
runtime.refreshPanelLiveState();
return {
...rollbackResult,
extractionTriggered: true,
extractionStatus: runtime.getLastExtractionStatusLevel?.() || "idle",
};
}