mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
885 lines
28 KiB
JavaScript
885 lines
28 KiB
JavaScript
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;
|
||
}
|
||
|
||
function isTavernHelperPromptViewerSyntheticGeneration(runtime) {
|
||
if (!runtime.isTavernHelperPromptViewerRefreshActive?.()) {
|
||
return false;
|
||
}
|
||
|
||
const pendingSendIntent = runtime.getPendingRecallSendIntent?.();
|
||
return !runtime.isFreshRecallInputRecord?.(pendingSendIntent);
|
||
}
|
||
|
||
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;
|
||
const trivialInputResult = runtime.isTrivialUserInput?.(message.mes || "") || {
|
||
trivial: false,
|
||
reason: "",
|
||
normalizedText: "",
|
||
};
|
||
const tokenEstimate =
|
||
typeof runtime.estimateTokens === "function"
|
||
? Number(runtime.estimateTokens(message.mes || ""))
|
||
: Number.NaN;
|
||
const isZeroTokenInput =
|
||
Number.isFinite(tokenEstimate) && tokenEstimate <= 0;
|
||
|
||
if (trivialInputResult.trivial || isZeroTokenInput) {
|
||
runtime.markCurrentGenerationTrivialSkip?.({
|
||
reason: trivialInputResult.trivial
|
||
? trivialInputResult.reason
|
||
: "zero-token",
|
||
chatId: context?.chatId || "",
|
||
chatLength: Array.isArray(chat) ? chat.length : 0,
|
||
});
|
||
runtime.clearPendingRecallSendIntent?.();
|
||
runtime.clearPendingHostGenerationInputSnapshot?.();
|
||
console.info?.(
|
||
`[ST-BME] trivial-input skip: reason=${
|
||
trivialInputResult.trivial ? trivialInputResult.reason : "zero-token"
|
||
} len=${String(trivialInputResult.normalizedText || message.mes || "").length} hook=MESSAGE_SENT`,
|
||
);
|
||
runtime.refreshPersistedRecallMessageUi?.();
|
||
return;
|
||
}
|
||
runtime.recordRecallSentUserMessage(
|
||
resolvedMessageId,
|
||
message.mes || "",
|
||
);
|
||
// GENERATION_AFTER_COMMANDS 在 sendMessageAsUser 之前触发,此时新用户消息
|
||
// 尚未进入 chat,recall 记录会被写到上一条 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;
|
||
|
||
if (isTavernHelperPromptViewerSyntheticGeneration(runtime)) {
|
||
const context = runtime.getContext?.() || {};
|
||
runtime.markCurrentGenerationTrivialSkip?.({
|
||
reason: "tavern-helper-prompt-viewer",
|
||
chatId: context?.chatId || "",
|
||
chatLength: Array.isArray(context?.chat) ? context.chat.length : 0,
|
||
});
|
||
runtime.clearPendingRecallSendIntent?.();
|
||
runtime.clearPendingHostGenerationInputSnapshot?.();
|
||
console.debug?.(
|
||
"[ST-BME] skip: tavern-helper-prompt-viewer hook=GENERATION_STARTED",
|
||
);
|
||
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;
|
||
}
|
||
|
||
const generationType = String(type || "normal").trim() || "normal";
|
||
|
||
if (runtime.isMvuExtraAnalysisGuardActive?.()) {
|
||
console.debug?.(
|
||
"[ST-BME] skip: mvu-extra-analysis hook=GENERATION_AFTER_COMMANDS",
|
||
);
|
||
return;
|
||
}
|
||
|
||
if (
|
||
generationType === "normal" &&
|
||
isTavernHelperPromptViewerSyntheticGeneration(runtime)
|
||
) {
|
||
const context = runtime.getContext?.() || {};
|
||
runtime.markCurrentGenerationTrivialSkip?.({
|
||
reason: "tavern-helper-prompt-viewer",
|
||
chatId: runtime.getCurrentChatId?.() || context?.chatId || "",
|
||
chatLength: Array.isArray(context?.chat) ? context.chat.length : 0,
|
||
});
|
||
runtime.clearPendingRecallSendIntent?.();
|
||
runtime.clearPendingHostGenerationInputSnapshot?.();
|
||
console.debug?.(
|
||
"[ST-BME] skip: tavern-helper-prompt-viewer hook=GENERATION_AFTER_COMMANDS",
|
||
);
|
||
return;
|
||
}
|
||
|
||
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 || {};
|
||
if (
|
||
params &&
|
||
typeof params === "object" &&
|
||
runtimeRecallOptions?.authoritativeInputUsed === true
|
||
) {
|
||
const authoritativePrompt = String(
|
||
runtimeRecallOptions?.overrideUserMessage ||
|
||
runtimeRecallOptions?.userMessage ||
|
||
"",
|
||
).trim();
|
||
if (authoritativePrompt) {
|
||
params.prompt = authoritativePrompt;
|
||
if (Object.prototype.hasOwnProperty.call(params, "user_input")) {
|
||
params.user_input = authoritativePrompt;
|
||
}
|
||
}
|
||
}
|
||
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",
|
||
};
|
||
}
|
||
|
||
if (isTavernHelperPromptViewerSyntheticGeneration(runtime)) {
|
||
const context = runtime.getContext?.() || {};
|
||
runtime.markCurrentGenerationTrivialSkip?.({
|
||
reason: "tavern-helper-prompt-viewer",
|
||
chatId: runtime.getCurrentChatId?.() || context?.chatId || "",
|
||
chatLength: Array.isArray(context?.chat) ? context.chat.length : 0,
|
||
});
|
||
runtime.clearPendingRecallSendIntent?.();
|
||
runtime.clearPendingHostGenerationInputSnapshot?.();
|
||
console.debug?.(
|
||
"[ST-BME] skip: tavern-helper-prompt-viewer hook=GENERATE_BEFORE_COMBINE_PROMPTS",
|
||
);
|
||
return {
|
||
skipped: true,
|
||
reason: "tavern-helper-prompt-viewer",
|
||
};
|
||
}
|
||
|
||
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?.();
|
||
}
|