mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-13 18:31:16 +08:00
refactor(runtime): extract auto-extraction defer factory (Phase 4e)
This commit is contained in:
346
runtime/auto-extraction-defer.js
Normal file
346
runtime/auto-extraction-defer.js
Normal file
@@ -0,0 +1,346 @@
|
||||
export function createAutoExtractionDefer(deps = {}) {
|
||||
let pendingAutoExtractionTimer = null;
|
||||
let pendingAutoExtraction = {
|
||||
chatId: "",
|
||||
messageId: null,
|
||||
reason: "",
|
||||
requestedAt: 0,
|
||||
attempts: 0,
|
||||
targetEndFloor: null,
|
||||
strategy: "normal",
|
||||
};
|
||||
|
||||
const normalizeChatIdCandidate = (value = "") =>
|
||||
deps.normalizeChatIdCandidate?.(value) ?? String(value ?? "").trim();
|
||||
const getCurrentChatId = (...args) => deps.getCurrentChatId?.(...args);
|
||||
const getContext = (...args) => deps.getContext?.(...args);
|
||||
const getSettings = (...args) => deps.getSettings?.(...args);
|
||||
const clearTimeoutImpl = deps.clearTimeout || globalThis.clearTimeout;
|
||||
const setTimeoutImpl = deps.setTimeout || globalThis.setTimeout;
|
||||
const consoleImpl = deps.console || console;
|
||||
const deferRetryDelays = Array.isArray(deps.AUTO_EXTRACTION_DEFER_RETRY_DELAYS_MS)
|
||||
? deps.AUTO_EXTRACTION_DEFER_RETRY_DELAYS_MS
|
||||
: [120, 320, 800, 1600, 2800];
|
||||
const hostSettleMs = Number.isFinite(Number(deps.AUTO_EXTRACTION_HOST_SETTLE_MS))
|
||||
? Number(deps.AUTO_EXTRACTION_HOST_SETTLE_MS)
|
||||
: 120;
|
||||
|
||||
function getPendingAutoExtraction() {
|
||||
return { ...pendingAutoExtraction };
|
||||
}
|
||||
|
||||
function clearPendingAutoExtraction({ resetState = true } = {}) {
|
||||
if (pendingAutoExtractionTimer) {
|
||||
clearTimeoutImpl(pendingAutoExtractionTimer);
|
||||
pendingAutoExtractionTimer = null;
|
||||
}
|
||||
|
||||
if (resetState) {
|
||||
pendingAutoExtraction = {
|
||||
chatId: "",
|
||||
messageId: null,
|
||||
reason: "",
|
||||
requestedAt: 0,
|
||||
attempts: 0,
|
||||
targetEndFloor: null,
|
||||
strategy: "normal",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function deferAutoExtraction(
|
||||
reason = "auto-extraction-deferred",
|
||||
{
|
||||
chatId = getCurrentChatId(),
|
||||
messageId = null,
|
||||
delayMs = null,
|
||||
targetEndFloor = null,
|
||||
strategy = "",
|
||||
} = {},
|
||||
) {
|
||||
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
||||
if (!normalizedChatId) {
|
||||
clearPendingAutoExtraction();
|
||||
return {
|
||||
scheduled: false,
|
||||
reason: "missing-chat-id",
|
||||
chatId: "",
|
||||
};
|
||||
}
|
||||
|
||||
const sameChat = normalizedChatId === pendingAutoExtraction.chatId;
|
||||
const previousAttempts = sameChat
|
||||
? Math.max(0, Math.floor(Number(pendingAutoExtraction.attempts) || 0))
|
||||
: 0;
|
||||
const nextAttempts = previousAttempts + 1;
|
||||
const resolvedDelayMs =
|
||||
delayMs !== null &&
|
||||
delayMs !== undefined &&
|
||||
Number.isFinite(Number(delayMs))
|
||||
? Math.max(0, Math.floor(Number(delayMs)))
|
||||
: deferRetryDelays[
|
||||
Math.min(
|
||||
previousAttempts,
|
||||
deferRetryDelays.length - 1,
|
||||
)
|
||||
];
|
||||
|
||||
pendingAutoExtraction = {
|
||||
chatId: normalizedChatId,
|
||||
messageId: Number.isFinite(Number(messageId))
|
||||
? Math.floor(Number(messageId))
|
||||
: sameChat
|
||||
? pendingAutoExtraction.messageId
|
||||
: null,
|
||||
reason: String(reason || "auto-extraction-deferred"),
|
||||
requestedAt:
|
||||
sameChat && pendingAutoExtraction.requestedAt > 0
|
||||
? pendingAutoExtraction.requestedAt
|
||||
: Date.now(),
|
||||
attempts: nextAttempts,
|
||||
targetEndFloor: Number.isFinite(Number(targetEndFloor))
|
||||
? sameChat &&
|
||||
Number.isFinite(Number(pendingAutoExtraction.targetEndFloor))
|
||||
? Math.max(
|
||||
Math.floor(Number(targetEndFloor)),
|
||||
Math.floor(Number(pendingAutoExtraction.targetEndFloor)),
|
||||
)
|
||||
: Math.floor(Number(targetEndFloor))
|
||||
: sameChat
|
||||
? pendingAutoExtraction.targetEndFloor
|
||||
: null,
|
||||
strategy: String(strategy || "")
|
||||
? String(strategy || "")
|
||||
: sameChat
|
||||
? String(pendingAutoExtraction.strategy || "normal")
|
||||
: "normal",
|
||||
};
|
||||
|
||||
if (pendingAutoExtractionTimer) {
|
||||
clearTimeoutImpl(pendingAutoExtractionTimer);
|
||||
}
|
||||
|
||||
pendingAutoExtractionTimer = setTimeoutImpl(() => {
|
||||
pendingAutoExtractionTimer = null;
|
||||
void maybeResumePendingAutoExtraction(
|
||||
`retry:${pendingAutoExtraction.reason || "auto-extraction-deferred"}`,
|
||||
);
|
||||
}, resolvedDelayMs);
|
||||
consoleImpl.debug?.("[ST-BME] auto extraction deferred", {
|
||||
reason: pendingAutoExtraction.reason,
|
||||
chatId: normalizedChatId,
|
||||
messageId: pendingAutoExtraction.messageId,
|
||||
targetEndFloor: pendingAutoExtraction.targetEndFloor,
|
||||
strategy: pendingAutoExtraction.strategy,
|
||||
attempts: nextAttempts,
|
||||
delayMs: resolvedDelayMs,
|
||||
});
|
||||
|
||||
return {
|
||||
scheduled: true,
|
||||
chatId: normalizedChatId,
|
||||
messageId: pendingAutoExtraction.messageId,
|
||||
reason: pendingAutoExtraction.reason,
|
||||
targetEndFloor: pendingAutoExtraction.targetEndFloor,
|
||||
strategy: pendingAutoExtraction.strategy,
|
||||
attempts: nextAttempts,
|
||||
delayMs: resolvedDelayMs,
|
||||
};
|
||||
}
|
||||
|
||||
function maybeResumePendingAutoExtraction(source = "auto-extraction-resume") {
|
||||
const pendingChatId = normalizeChatIdCandidate(pendingAutoExtraction.chatId);
|
||||
if (!pendingChatId) {
|
||||
return {
|
||||
resumed: false,
|
||||
reason: "no-pending-auto-extraction",
|
||||
};
|
||||
}
|
||||
|
||||
if (deps.isRestoreLockActive()) {
|
||||
return {
|
||||
resumed: false,
|
||||
reason: "restore-lock-active",
|
||||
restoreLock: deps.cloneRuntimeDebugValue(
|
||||
deps.normalizeRestoreLockState(deps.getGraphPersistenceState?.()?.restoreLock),
|
||||
null,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const currentChatId = normalizeChatIdCandidate(getCurrentChatId());
|
||||
if (!currentChatId || currentChatId !== pendingChatId) {
|
||||
clearPendingAutoExtraction();
|
||||
return {
|
||||
resumed: false,
|
||||
reason: "chat-switched",
|
||||
chatId: pendingChatId,
|
||||
currentChatId,
|
||||
};
|
||||
}
|
||||
|
||||
if (deps.getIsExtracting?.()) {
|
||||
return deferAutoExtraction("extracting", {
|
||||
chatId: pendingChatId,
|
||||
messageId: pendingAutoExtraction.messageId,
|
||||
targetEndFloor: pendingAutoExtraction.targetEndFloor,
|
||||
strategy: pendingAutoExtraction.strategy,
|
||||
});
|
||||
}
|
||||
|
||||
if (deps.getIsHostGenerationRunning?.()) {
|
||||
return deferAutoExtraction("generation-running", {
|
||||
chatId: pendingChatId,
|
||||
messageId: pendingAutoExtraction.messageId,
|
||||
targetEndFloor: pendingAutoExtraction.targetEndFloor,
|
||||
strategy: pendingAutoExtraction.strategy,
|
||||
});
|
||||
}
|
||||
|
||||
const lastHostGenerationEndedAt = Number(deps.getLastHostGenerationEndedAt?.() || 0);
|
||||
const hostGenerationSettleRemainingMs =
|
||||
lastHostGenerationEndedAt > 0
|
||||
? hostSettleMs -
|
||||
(Date.now() - lastHostGenerationEndedAt)
|
||||
: 0;
|
||||
if (hostGenerationSettleRemainingMs > 0) {
|
||||
return deferAutoExtraction("generation-settling", {
|
||||
chatId: pendingChatId,
|
||||
messageId: pendingAutoExtraction.messageId,
|
||||
delayMs: hostGenerationSettleRemainingMs,
|
||||
targetEndFloor: pendingAutoExtraction.targetEndFloor,
|
||||
strategy: pendingAutoExtraction.strategy,
|
||||
});
|
||||
}
|
||||
|
||||
if (deps.getIsRecoveringHistory?.()) {
|
||||
return deferAutoExtraction("history-recovering", {
|
||||
chatId: pendingChatId,
|
||||
messageId: pendingAutoExtraction.messageId,
|
||||
targetEndFloor: pendingAutoExtraction.targetEndFloor,
|
||||
strategy: pendingAutoExtraction.strategy,
|
||||
});
|
||||
}
|
||||
|
||||
if (!deps.ensureGraphMutationReady("自动提取", { notify: false })) {
|
||||
consoleImpl.debug?.(
|
||||
"[ST-BME] pending auto extraction resume blocked: graph-not-ready",
|
||||
{
|
||||
source,
|
||||
chatId: pendingChatId,
|
||||
attempts: pendingAutoExtraction.attempts || 0,
|
||||
loadState: deps.getGraphPersistenceState?.()?.loadState || "",
|
||||
},
|
||||
);
|
||||
return deferAutoExtraction("graph-not-ready", {
|
||||
chatId: pendingChatId,
|
||||
messageId: pendingAutoExtraction.messageId,
|
||||
targetEndFloor: pendingAutoExtraction.targetEndFloor,
|
||||
strategy: pendingAutoExtraction.strategy,
|
||||
});
|
||||
}
|
||||
|
||||
const resumeContext = getContext();
|
||||
const resumeChat = resumeContext?.chat;
|
||||
const settings = getSettings();
|
||||
let lockedEndFloor = Number.isFinite(Number(pendingAutoExtraction.targetEndFloor))
|
||||
? Math.floor(Number(pendingAutoExtraction.targetEndFloor))
|
||||
: null;
|
||||
if (
|
||||
Array.isArray(resumeChat) &&
|
||||
Number.isFinite(Number(pendingAutoExtraction.messageId))
|
||||
) {
|
||||
const pendingMessageIndex = Math.floor(
|
||||
Number(pendingAutoExtraction.messageId),
|
||||
);
|
||||
const pendingMessage = resumeChat[pendingMessageIndex];
|
||||
if (
|
||||
deps.isAssistantChatMessage(pendingMessage, {
|
||||
index: pendingMessageIndex,
|
||||
chat: resumeChat,
|
||||
}) &&
|
||||
!String(pendingMessage?.mes ?? "").trim()
|
||||
) {
|
||||
return deferAutoExtraction("assistant-message-empty", {
|
||||
chatId: pendingChatId,
|
||||
messageId: pendingMessageIndex,
|
||||
delayMs: hostSettleMs,
|
||||
targetEndFloor: pendingAutoExtraction.targetEndFloor,
|
||||
strategy: pendingAutoExtraction.strategy,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(resumeChat) && resumeChat.length > 0 && lockedEndFloor != null) {
|
||||
const lockedPlan = deps.resolveAutoExtractionPlan({
|
||||
chat: resumeChat,
|
||||
settings,
|
||||
lockedEndFloor,
|
||||
});
|
||||
if (
|
||||
!lockedPlan.canRun &&
|
||||
lockedPlan.candidateAssistantTurns.length === 0
|
||||
) {
|
||||
const fallbackPlan = deps.resolveAutoExtractionPlan({
|
||||
chat: resumeChat,
|
||||
settings,
|
||||
});
|
||||
lockedEndFloor = fallbackPlan.canRun
|
||||
? fallbackPlan.plannedBatchEndFloor
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
const pendingRequest = { ...pendingAutoExtraction };
|
||||
clearPendingAutoExtraction();
|
||||
if (lockedEndFloor == null) {
|
||||
const currentPlan = deps.resolveAutoExtractionPlan({
|
||||
chat: resumeChat,
|
||||
settings,
|
||||
});
|
||||
if (!currentPlan.canRun) {
|
||||
return {
|
||||
resumed: false,
|
||||
reason: "no-runnable-auto-extraction",
|
||||
source,
|
||||
...pendingRequest,
|
||||
};
|
||||
}
|
||||
lockedEndFloor = currentPlan.plannedBatchEndFloor;
|
||||
}
|
||||
consoleImpl.debug?.("[ST-BME] resuming pending auto extraction", {
|
||||
source,
|
||||
chatId: pendingRequest.chatId,
|
||||
messageId: pendingRequest.messageId,
|
||||
targetEndFloor: lockedEndFloor,
|
||||
attempts: pendingRequest.attempts || 0,
|
||||
});
|
||||
const enqueueMicrotask =
|
||||
typeof globalThis.queueMicrotask === "function"
|
||||
? globalThis.queueMicrotask.bind(globalThis)
|
||||
: (task) => Promise.resolve().then(task);
|
||||
enqueueMicrotask(() => {
|
||||
void deps.runExtraction({
|
||||
lockedEndFloor,
|
||||
triggerSource: source,
|
||||
}).catch((error) => {
|
||||
consoleImpl.error("[ST-BME] 延迟自动提取失败:", error);
|
||||
deps.notifyExtractionIssue(error?.message || String(error) || "自动提取失败");
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
resumed: true,
|
||||
source,
|
||||
lockedEndFloor,
|
||||
...pendingRequest,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
clearPendingAutoExtraction,
|
||||
deferAutoExtraction,
|
||||
maybeResumePendingAutoExtraction,
|
||||
getPendingAutoExtraction,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user