Files
ST-Bionic-Memory-Ecology/host/event-binding.js
2026-04-11 20:47:46 +08:00

781 lines
24 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.

function getTimerApi(runtime) {
const rawSetTimeout =
typeof runtime?.setTimeout === "function"
? runtime.setTimeout
: globalThis.setTimeout;
const rawClearTimeout =
typeof runtime?.clearTimeout === "function"
? runtime.clearTimeout
: globalThis.clearTimeout;
return {
setTimeout(...args) {
return Reflect.apply(rawSetTimeout, globalThis, args);
},
clearTimeout(...args) {
return Reflect.apply(rawClearTimeout, globalThis, args);
},
};
}
function toSafeFloor(value, fallback = null) {
if (value == null || value === "") return fallback;
const numeric = Number(value);
return Number.isFinite(numeric) ? Math.floor(numeric) : fallback;
}
export function registerBeforeCombinePromptsController(runtime, listener) {
const makeFirst = runtime.getEventMakeFirst();
if (typeof makeFirst === "function") {
return makeFirst(
runtime.eventTypes.GENERATE_BEFORE_COMBINE_PROMPTS,
listener,
);
}
runtime.console.warn("[ST-BME] eventMakeFirst 不可用,回退到普通事件注册");
runtime.eventSource.on(
runtime.eventTypes.GENERATE_BEFORE_COMBINE_PROMPTS,
listener,
);
return null;
}
export function registerGenerationAfterCommandsController(runtime, listener) {
const makeFirst = runtime.getEventMakeFirst();
const eventName = runtime.eventTypes.GENERATION_AFTER_COMMANDS;
if (typeof makeFirst === "function") {
const cleanup = makeFirst(eventName, listener);
return cleanup;
}
runtime.console.warn(
"[ST-BME] eventMakeFirst 不可用GENERATION_AFTER_COMMANDS 回退到普通事件注册",
);
runtime.eventSource.on(eventName, listener);
return null;
}
export function scheduleSendIntentHookRetryController(runtime, delayMs = 400) {
const timers = getTimerApi(runtime);
timers.clearTimeout(runtime.getSendIntentHookRetryTimer());
const timer = timers.setTimeout(() => {
runtime.setSendIntentHookRetryTimer(null);
runtime.installSendIntentHooks();
}, delayMs);
runtime.setSendIntentHookRetryTimer(timer);
}
export function installSendIntentHooksController(runtime) {
for (const cleanup of runtime.consumeSendIntentHookCleanup()) {
try {
cleanup();
} catch (error) {
runtime.console.warn("[ST-BME] 清理发送意图钩子失败:", error);
}
}
const sendButton = runtime.document.getElementById("send_but");
const sendTextarea = runtime.document.getElementById("send_textarea");
if (sendButton) {
const captureSendIntent = () => {
runtime.recordRecallSendIntent(
runtime.getSendTextareaValue(),
"send-button",
);
};
sendButton.addEventListener("click", captureSendIntent, true);
sendButton.addEventListener("pointerup", captureSendIntent, true);
sendButton.addEventListener("touchend", captureSendIntent, true);
runtime.pushSendIntentHookCleanup(() => {
sendButton.removeEventListener("click", captureSendIntent, true);
sendButton.removeEventListener("pointerup", captureSendIntent, true);
sendButton.removeEventListener("touchend", captureSendIntent, true);
});
}
if (sendTextarea) {
const captureEnterIntent = (event) => {
if (
(event.key === "Enter" || event.key === "NumpadEnter") &&
!event.shiftKey
) {
runtime.recordRecallSendIntent(
runtime.getSendTextareaValue(),
"textarea-enter",
);
}
};
sendTextarea.addEventListener("keydown", captureEnterIntent, true);
runtime.pushSendIntentHookCleanup(() => {
sendTextarea.removeEventListener("keydown", captureEnterIntent, true);
});
}
if (!sendButton || !sendTextarea) {
runtime.scheduleSendIntentHookRetry();
}
}
export function registerCoreEventHooksController(runtime) {
const { eventSource, eventTypes, handlers } = runtime;
const registrationState = runtime.getCoreEventBindingState?.() || {};
if (registrationState.registered) {
runtime.console?.warn?.("[ST-BME] 核心事件已注册,跳过重复绑定");
return registrationState;
}
const cleanups = [];
const bind = (eventName, listener) => {
if (!eventName || typeof listener !== "function") return;
eventSource.on(eventName, listener);
if (typeof eventSource.off === "function") {
cleanups.push(() => eventSource.off(eventName, listener));
} else if (typeof eventSource.removeListener === "function") {
cleanups.push(() => eventSource.removeListener(eventName, listener));
}
};
bind(eventTypes.CHAT_CHANGED, handlers.onChatChanged);
if (eventTypes.CHAT_LOADED) {
bind(eventTypes.CHAT_LOADED, handlers.onChatLoaded);
}
if (eventTypes.MESSAGE_SENT) {
bind(eventTypes.MESSAGE_SENT, handlers.onMessageSent);
}
if (eventTypes.GENERATION_STARTED) {
bind(eventTypes.GENERATION_STARTED, handlers.onGenerationStarted);
}
if (eventTypes.GENERATION_ENDED) {
bind(eventTypes.GENERATION_ENDED, handlers.onGenerationEnded);
}
const beforeCombineCleanup = runtime.registerBeforeCombinePrompts(
handlers.onBeforeCombinePrompts,
);
if (typeof beforeCombineCleanup === "function") {
cleanups.push(beforeCombineCleanup);
}
const afterCommandsCleanup = runtime.registerGenerationAfterCommands(
handlers.onGenerationAfterCommands,
);
if (typeof afterCommandsCleanup === "function") {
cleanups.push(afterCommandsCleanup);
}
bind(eventTypes.MESSAGE_RECEIVED, handlers.onMessageReceived);
bind(eventTypes.MESSAGE_DELETED, handlers.onMessageDeleted);
bind(eventTypes.MESSAGE_EDITED, handlers.onMessageEdited);
bind(eventTypes.MESSAGE_SWIPED, handlers.onMessageSwiped);
if (eventTypes.MESSAGE_UPDATED) {
bind(eventTypes.MESSAGE_UPDATED, handlers.onMessageEdited);
}
if (eventTypes.USER_MESSAGE_RENDERED) {
bind(eventTypes.USER_MESSAGE_RENDERED, handlers.onUserMessageRendered);
}
if (eventTypes.CHARACTER_MESSAGE_RENDERED) {
bind(
eventTypes.CHARACTER_MESSAGE_RENDERED,
handlers.onCharacterMessageRendered,
);
}
const nextState = {
registered: true,
cleanups,
registeredAt: Date.now(),
};
runtime.setCoreEventBindingState?.(nextState);
return nextState;
}
export function onChatChangedController(runtime) {
const timers = getTimerApi(runtime);
runtime.clearPendingHistoryMutationChecks();
timers.clearTimeout(runtime.getPendingHistoryRecoveryTimer());
runtime.setPendingHistoryRecoveryTimer(null);
runtime.setPendingHistoryRecoveryTrigger("");
runtime.clearPendingAutoExtraction?.();
runtime.clearPendingGraphLoadRetry();
runtime.setSkipBeforeCombineRecallUntil(0);
runtime.setLastPreGenerationRecallKey("");
runtime.setLastPreGenerationRecallAt(0);
runtime.clearGenerationRecallTransactionsForChat("", { clearAll: true });
runtime.abortAllRunningStages();
runtime.dismissAllStageNotices();
runtime.syncGraphLoadFromLiveContext({
source: "chat-changed",
force: true,
});
runtime.clearCurrentGenerationTrivialSkip?.("chat-changed");
runtime.clearInjectionState();
runtime.clearRecallInputTracking();
runtime.installSendIntentHooks();
runtime.refreshPersistedRecallMessageUi?.();
}
export function onChatLoadedController(runtime) {
runtime.syncGraphLoadFromLiveContext({
source: "chat-loaded",
});
runtime.refreshPersistedRecallMessageUi?.();
}
export function onMessageSentController(runtime, messageId) {
const context = runtime.getContext();
const chat = context?.chat;
const normalizedMessageId =
messageId === null || messageId === undefined || messageId === ""
? null
: Number(messageId);
let resolvedMessageId = Number.isFinite(normalizedMessageId)
? normalizedMessageId
: null;
let message =
Array.isArray(chat) && Number.isFinite(resolvedMessageId)
? chat[resolvedMessageId]
: null;
if (!message?.is_user && Array.isArray(chat)) {
for (let index = chat.length - 1; index >= 0; index--) {
if (!chat[index]?.is_user) continue;
resolvedMessageId = index;
message = chat[index];
break;
}
}
if (!message?.is_user) return;
if (runtime.isTrivialUserInput?.(message.mes || "")?.trivial) {
runtime.refreshPersistedRecallMessageUi?.();
return;
}
runtime.recordRecallSentUserMessage(
resolvedMessageId,
message.mes || "",
);
// GENERATION_AFTER_COMMANDS 在 sendMessageAsUser 之前触发,此时新用户消息
// 尚未进入 chatrecall 记录会被写到上一条 user 上。这里用户消息刚入场,
// transaction 仍在桥接窗口内,立即把记录重新绑定到正确的楼层。
runtime.rebindRecallRecordToNewUserMessage?.(resolvedMessageId);
runtime.refreshPersistedRecallMessageUi?.();
}
export function onUserMessageRenderedController(runtime, messageId = null) {
// MESSAGE_SENT 早于实际 DOM 挂载;这里等宿主确认 user 楼层渲染完成后,
// 再补一次 Recall Card 刷新,避免“当前楼层没卡片,下一楼才补出来”。
runtime.refreshPersistedRecallMessageUi?.(40);
return {
messageId: Number.isFinite(Number(messageId)) ? Number(messageId) : null,
refreshed: true,
source: "user-message-rendered",
};
}
export function onCharacterMessageRenderedController(
runtime,
messageId = null,
type = "",
) {
runtime.refreshPersistedRecallMessageUi?.(80);
return {
messageId: Number.isFinite(Number(messageId)) ? Number(messageId) : null,
type: String(type || ""),
refreshed: true,
source: "character-message-rendered",
};
}
export function onGenerationStartedController(
runtime,
type,
params = {},
dryRun = false,
) {
if (dryRun) {
runtime.markDryRunPromptPreview?.();
return null;
}
runtime.clearDryRunPromptPreview?.();
if (params?.automatic_trigger || params?.quiet_prompt) return null;
const generationType = String(type || "normal").trim() || "normal";
if (generationType !== "normal") return null;
const pendingSendIntent = runtime.getPendingRecallSendIntent?.();
const pendingIntentText = runtime.isFreshRecallInputRecord?.(
pendingSendIntent,
)
? pendingSendIntent.text
: "";
const textareaText =
typeof runtime.getSendTextareaValue === "function"
? runtime.getSendTextareaValue()
: "";
const snapshotText =
runtime.normalizeRecallInputText?.(pendingIntentText || textareaText) || "";
const trivialInputResult = runtime.isTrivialUserInput?.(snapshotText);
if (trivialInputResult?.trivial) {
const context = runtime.getContext?.() || {};
runtime.markCurrentGenerationTrivialSkip?.({
reason: trivialInputResult.reason,
chatId: context?.chatId || "",
chatLength: Array.isArray(context?.chat) ? context.chat.length : 0,
});
runtime.clearPendingRecallSendIntent?.();
runtime.clearPendingHostGenerationInputSnapshot?.();
console.info?.(
`[ST-BME] trivial-input skip: reason=${trivialInputResult.reason} len=${trivialInputResult.normalizedText.length} hook=GENERATION_STARTED`,
);
return null;
}
runtime.clearCurrentGenerationTrivialSkip?.("new-non-trivial-generation");
return runtime.freezeHostGenerationInputSnapshot(
snapshotText,
pendingIntentText
? "generation-started-send-intent"
: "generation-started-textarea",
);
}
export function onMessageDeletedController(
runtime,
chatLengthOrMessageId,
meta = null,
) {
runtime.invalidateRecallAfterHistoryMutation("消息已删除");
runtime.scheduleHistoryMutationRecheck(
"message-deleted",
chatLengthOrMessageId,
meta,
);
runtime.refreshPersistedRecallMessageUi?.();
}
export function onMessageEditedController(runtime, messageId, meta = null) {
if (runtime.isMvuExtraAnalysisGuardActive?.()) {
console.debug?.("[ST-BME] skip: mvu-extra-analysis hook=MESSAGE_EDITED");
runtime.refreshPersistedRecallMessageUi?.();
return;
}
runtime.invalidateRecallAfterHistoryMutation("消息已编辑");
runtime.scheduleHistoryMutationRecheck("message-edited", messageId, meta);
runtime.refreshPersistedRecallMessageUi?.();
}
export async function onMessageSwipedController(runtime, messageId, meta = null) {
runtime.invalidateRecallAfterHistoryMutation("已切换楼层 swipe");
const parsedFloor = Number(messageId);
const fromFloor = Number.isFinite(parsedFloor) ? parsedFloor : undefined;
let result = {
success: false,
rollbackPerformed: false,
extractionTriggered: false,
requestedFloor: fromFloor ?? null,
effectiveFromFloor: null,
recoveryPath: "reroll-handler-unavailable",
affectedBatchCount: 0,
error: "swipe reroll handler unavailable",
};
if (typeof runtime.onReroll === "function") {
try {
result = await runtime.onReroll({ fromFloor, meta });
} catch (error) {
runtime.console?.error?.("[ST-BME] swipe reroll failed:", error);
result = {
success: false,
rollbackPerformed: false,
extractionTriggered: false,
requestedFloor: fromFloor ?? null,
effectiveFromFloor: null,
recoveryPath: "reroll-threw",
affectedBatchCount: 0,
error: error?.message || String(error) || "swipe reroll failed",
};
}
} else {
runtime.console?.warn?.(
"[ST-BME] MESSAGE_SWIPED missing onReroll; skip generic history recovery fallback.",
{ messageId, meta },
);
}
runtime.refreshPersistedRecallMessageUi?.();
return result;
}
export async function onGenerationAfterCommandsController(
runtime,
type,
params = {},
dryRun = false,
) {
if (dryRun) {
return;
}
if (runtime.isMvuExtraAnalysisGuardActive?.()) {
console.debug?.(
"[ST-BME] skip: mvu-extra-analysis hook=GENERATION_AFTER_COMMANDS",
);
return;
}
const generationType = String(type || "normal").trim() || "normal";
const frozenInputSnapshot =
generationType === "normal"
? runtime.consumeHostGenerationInputSnapshot?.({ preserve: true }) ||
runtime.consumeHostGenerationInputSnapshot?.()
: null;
const context = runtime.getContext();
const chat = context?.chat;
const recallOptions = runtime.buildGenerationAfterCommandsRecallInput(
type,
{
...params,
frozenInputSnapshot,
},
chat,
);
if (!recallOptions) {
return;
}
if (recallOptions?.__trivialSkip) {
return;
}
const recallContext = runtime.createGenerationRecallContext({
hookName: "GENERATION_AFTER_COMMANDS",
generationType,
recallOptions,
});
if (!recallContext.shouldRun && !recallContext.transaction) {
return;
}
const runtimeRecallOptions =
recallContext.recallOptions || recallOptions || {};
const deliveryMode =
runtime.resolveGenerationRecallDeliveryMode?.(
recallContext.hookName,
recallContext.generationType,
runtimeRecallOptions,
) || "immediate";
let recallResult = runtime.getGenerationRecallTransactionResult?.(
recallContext.transaction,
);
if (recallContext.shouldRun) {
runtime.markGenerationRecallTransactionHookState(
recallContext.transaction,
recallContext.hookName,
"running",
);
if (deliveryMode === "deferred") {
runtime.clearLiveRecallInjectionPromptForRewrite?.();
}
recallResult = await runtime.runRecall({
...runtimeRecallOptions,
deliveryMode,
recallKey: recallContext.recallKey,
hookName: recallContext.hookName,
signal: params?.signal,
});
runtime.storeGenerationRecallTransactionResult?.(
recallContext.transaction,
recallResult,
{
hookName: recallContext.hookName,
deliveryMode,
},
);
runtime.markGenerationRecallTransactionHookState(
recallContext.transaction,
recallContext.hookName,
runtime.getGenerationRecallHookStateFromResult(recallResult),
);
}
// immediate 模式下runRecall → applyRecallInjection 内部已通过
// setExtensionPrompt 完成了注入,此处直接返回召回结果。
// 后续 GENERATE_BEFORE_COMBINE_PROMPTS 阶段会通过
// applyFinalRecallInjectionForGeneration 做 deferred rewrite 兜底。
if (deliveryMode === "immediate") {
runtime.ensurePersistedRecallRecordForGeneration?.({
generationType: recallContext.generationType,
recallResult,
transaction: recallContext.transaction,
recallOptions: runtimeRecallOptions,
hookName: recallContext.hookName,
});
// immediate 路径通常会在 runRecall 内完成持久化;如果当时 user 楼层还没稳定,
// 上面的兜底补写会把 fresh recall 绑定回最终 user 楼层。
// 这里再补一次 UI 刷新,避免需要等到消息编辑/历史恢复后才看到 Recall Card。
runtime.refreshPersistedRecallMessageUi?.();
return recallResult;
}
return runtime.applyFinalRecallInjectionForGeneration({
generationType: recallContext.generationType,
freshRecallResult: recallResult,
transaction: recallContext.transaction,
hookName: recallContext.hookName,
});
}
export async function onBeforeCombinePromptsController(
runtime,
promptData = null,
) {
if (runtime.consumeDryRunPromptPreview?.()) {
return {
skipped: true,
reason: "dry-run-preview",
};
}
if (runtime.isMvuExtraAnalysisGuardActive?.()) {
console.debug?.(
"[ST-BME] skip: mvu-extra-analysis hook=GENERATE_BEFORE_COMBINE_PROMPTS",
);
return {
skipped: true,
reason: "mvu-extra-analysis",
};
}
const frozenInputSnapshot =
runtime.consumeHostGenerationInputSnapshot?.() ||
runtime.getPendingHostGenerationInputSnapshot?.() ||
runtime.createRecallInputRecord?.() ||
{};
const context = runtime.getContext();
const chat = context?.chat;
const normalInput = runtime.buildNormalGenerationRecallInput(chat, {
frozenInputSnapshot,
});
if (normalInput?.__trivialSkip) {
return {
skipped: true,
reason: `trivial:${normalInput.trivialReason || ""}`,
};
}
const recallOptions =
normalInput ||
runtime.buildHistoryGenerationRecallInput(chat) ||
{};
const recallContext = runtime.createGenerationRecallContext({
hookName: "GENERATE_BEFORE_COMBINE_PROMPTS",
generationType: "normal",
recallOptions,
});
if (!recallContext.shouldRun && !recallContext.transaction) {
return;
}
const runtimeRecallOptions =
recallContext.recallOptions || recallOptions || {};
const deliveryMode =
runtime.resolveGenerationRecallDeliveryMode?.(
recallContext.hookName,
recallContext.generationType,
runtimeRecallOptions,
) || "deferred";
let recallResult = runtime.getGenerationRecallTransactionResult?.(
recallContext.transaction,
);
if (recallContext.shouldRun) {
runtime.markGenerationRecallTransactionHookState(
recallContext.transaction,
recallContext.hookName,
"running",
);
if (deliveryMode === "deferred") {
runtime.clearLiveRecallInjectionPromptForRewrite?.();
}
recallResult = await runtime.runRecall({
...runtimeRecallOptions,
deliveryMode,
recallKey: recallContext.recallKey,
hookName: recallContext.hookName,
});
runtime.storeGenerationRecallTransactionResult?.(
recallContext.transaction,
recallResult,
{
hookName: recallContext.hookName,
deliveryMode,
},
);
runtime.markGenerationRecallTransactionHookState(
recallContext.transaction,
recallContext.hookName,
runtime.getGenerationRecallHookStateFromResult(recallResult),
);
}
return runtime.applyFinalRecallInjectionForGeneration({
generationType: recallContext.generationType,
freshRecallResult: recallResult,
transaction: recallContext.transaction,
promptData,
hookName: recallContext.hookName,
});
}
export function onMessageReceivedController(
runtime,
messageId = null,
_type = "",
) {
const enqueueMicrotask =
typeof globalThis.queueMicrotask === "function"
? globalThis.queueMicrotask.bind(globalThis)
: typeof runtime.queueMicrotask === "function"
? (task) => Reflect.apply(runtime.queueMicrotask, globalThis, [task])
: (task) => Promise.resolve().then(task);
const persistenceState = runtime.getGraphPersistenceState?.() || {};
const loadState = persistenceState.loadState || "";
const dbReady =
persistenceState.dbReady ??
(loadState === "loaded" || loadState === "empty-confirmed");
if (
!dbReady ||
loadState === "loading" ||
loadState === "shadow-restored" ||
loadState === "blocked"
) {
runtime.syncGraphLoadFromLiveContext?.({
source: "message-received-reconcile",
});
}
if (runtime.getCurrentGraph()) {
if (
runtime.getGraphPersistenceState()?.pendingPersist &&
runtime.isGraphMetadataWriteAllowed()
) {
runtime.maybeFlushQueuedGraphPersist("message-received-pending-flush");
}
}
const pendingRecallSendIntent = runtime.getPendingRecallSendIntent();
if (
pendingRecallSendIntent?.text &&
!runtime.isFreshRecallInputRecord(pendingRecallSendIntent)
) {
runtime.setPendingRecallSendIntent(runtime.createRecallInputRecord());
}
const context = runtime.getContext();
const chat = context?.chat;
const settings =
typeof runtime.getSettings === "function" ? runtime.getSettings() : {};
const lastProcessedAssistantFloor =
typeof runtime.getLastProcessedAssistantFloor === "function"
? runtime.getLastProcessedAssistantFloor()
: -1;
const receivedMessage =
Array.isArray(chat) && Number.isFinite(Number(messageId))
? chat[Number(messageId)]
: null;
const lastMessage =
Array.isArray(chat) && chat.length > 0 ? chat[chat.length - 1] : null;
const targetMessage = runtime.isAssistantChatMessage(receivedMessage)
? receivedMessage
: lastMessage;
const targetMessageIndex = runtime.isAssistantChatMessage(receivedMessage)
? Number(messageId)
: runtime.isAssistantChatMessage(lastMessage)
? chat.length - 1
: null;
if (runtime.isAssistantChatMessage(targetMessage)) {
if (runtime.consumeCurrentGenerationTrivialSkip?.(targetMessageIndex)) {
runtime.console?.info?.(
"[ST-BME] trivial-input skip: extraction bypassed",
{ messageId: targetMessageIndex },
);
runtime.refreshPersistedRecallMessageUi?.();
return;
}
const autoExtractionPlan =
typeof runtime.resolveAutoExtractionPlan === "function"
? runtime.resolveAutoExtractionPlan({
chat,
settings,
lastProcessedAssistantFloor,
})
: null;
if (!autoExtractionPlan?.canRun) {
runtime.console?.debug?.(
"[ST-BME] assistant message received, auto extraction not runnable yet",
{
messageId: Number.isFinite(Number(targetMessageIndex))
? Number(targetMessageIndex)
: null,
reason: String(autoExtractionPlan?.reason || "not-runnable"),
strategy: String(autoExtractionPlan?.strategy || "normal"),
},
);
runtime.refreshPersistedRecallMessageUi?.();
return;
}
runtime.console?.debug?.(
"[ST-BME] assistant message received, queueing auto extraction",
{
messageId: Number.isFinite(Number(targetMessageIndex))
? Number(targetMessageIndex)
: null,
chatLength: Array.isArray(chat) ? chat.length : 0,
loadState,
dbReady,
},
);
if (
runtime.getIsHostGenerationRunning?.() === true &&
typeof runtime.deferAutoExtraction === "function"
) {
runtime.console?.debug?.(
"[ST-BME] assistant message received during host generation, deferring auto extraction",
{
messageId: Number.isFinite(Number(targetMessageIndex))
? Number(targetMessageIndex)
: null,
targetEndFloor: toSafeFloor(autoExtractionPlan.plannedBatchEndFloor, null),
},
);
runtime.deferAutoExtraction("generation-running", {
messageId: targetMessageIndex,
targetEndFloor: autoExtractionPlan.plannedBatchEndFloor,
strategy: autoExtractionPlan.strategy,
});
runtime.refreshPersistedRecallMessageUi?.();
return;
}
enqueueMicrotask(() => {
void runtime
.runExtraction({
lockedEndFloor: autoExtractionPlan.plannedBatchEndFloor,
triggerSource: "message-received",
})
.catch((error) => {
runtime.console.error("[ST-BME] 异步自动提取失败:", error);
runtime.notifyExtractionIssue(
error?.message || String(error) || "自动提取失败",
);
});
});
}
runtime.refreshPersistedRecallMessageUi?.();
}