// ST-BME: 召回输入解析与注入控制器(纯函数) import { debugLog } from "./debug-logging.js"; export function buildRecallRecentMessagesController( chat, limit, syntheticUserMessage = "", runtime, ) { if (!Array.isArray(chat) || limit <= 0) return []; const recentMessages = []; for ( let index = chat.length - 1; index >= 0 && recentMessages.length < limit; index-- ) { const message = chat[index]; if (message?.is_system) continue; recentMessages.unshift(runtime.formatRecallContextLine(message)); } const normalizedSynthetic = runtime.normalizeRecallInputText(syntheticUserMessage); if (!normalizedSynthetic) return recentMessages; const syntheticLine = `[user]: ${normalizedSynthetic}`; if (recentMessages[recentMessages.length - 1] !== syntheticLine) { recentMessages.push(syntheticLine); while (recentMessages.length > limit) { recentMessages.shift(); } } return recentMessages; } export function getRecallUserMessageSourceLabelController(source) { switch (source) { case "send-intent": return "发送意图"; case "chat-tail-user": return "当前用户楼层"; case "message-sent": return "已发送用户楼层"; case "chat-last-user": return "历史最后用户楼层"; default: return "未知"; } } export function resolveRecallInputController( chat, recentContextMessageLimit, override = null, runtime, ) { const overrideText = runtime.normalizeRecallInputText( override?.userMessage || override?.overrideUserMessage || "", ); if (overrideText) { return { userMessage: overrideText, generationType: String(override?.generationType || "normal"), targetUserMessageIndex: Number.isFinite(override?.targetUserMessageIndex) ? override.targetUserMessageIndex : null, source: String( override?.lockedSource || override?.source || override?.overrideSource || "override", ), sourceLabel: String( override?.lockedSourceLabel || override?.sourceLabel || override?.overrideSourceLabel || "发送前拦截", ), reason: String( override?.lockedReason || override?.reason || override?.overrideReason || "override-bound", ), sourceCandidates: Array.isArray(override?.sourceCandidates) ? override.sourceCandidates.map((candidate) => ({ ...candidate })) : [], recentMessages: runtime.buildRecallRecentMessages( chat, recentContextMessageLimit, override?.includeSyntheticUserMessage === false ? "" : overrideText, ), }; } const latestUserMessage = runtime.getLatestUserChatMessage(chat); const latestUserText = runtime.normalizeRecallInputText( latestUserMessage?.mes || "", ); const lastNonSystemMessage = runtime.getLastNonSystemChatMessage(chat); const tailUserText = lastNonSystemMessage?.is_user ? runtime.normalizeRecallInputText(lastNonSystemMessage?.mes || "") : ""; const pendingIntentText = runtime.isFreshRecallInputRecord( runtime.pendingRecallSendIntent, ) ? runtime.pendingRecallSendIntent.text : ""; const sentUserText = runtime.isFreshRecallInputRecord( runtime.lastRecallSentUserMessage, ) ? runtime.lastRecallSentUserMessage.text : ""; let userMessage = ""; let source = ""; let syntheticUserMessage = ""; if (pendingIntentText) { userMessage = pendingIntentText; source = "send-intent"; syntheticUserMessage = pendingIntentText; } else if (tailUserText) { userMessage = tailUserText; source = "chat-tail-user"; } else if (sentUserText) { userMessage = sentUserText; source = "message-sent"; if (!latestUserText || latestUserText !== sentUserText) { syntheticUserMessage = sentUserText; } } else if (latestUserText) { userMessage = latestUserText; source = "chat-last-user"; } return { userMessage, generationType: "normal", targetUserMessageIndex: null, source, sourceLabel: runtime.getRecallUserMessageSourceLabel(source), reason: userMessage ? `${source || "unknown"}-selected` : "no-recall-input", sourceCandidates: [], recentMessages: runtime.buildRecallRecentMessages( chat, recentContextMessageLimit, syntheticUserMessage, ), }; } export function applyRecallInjectionController( settings, recallInput, recentMessages, result, runtime, ) { const injectionText = runtime .formatInjection(result, runtime.getSchema()) .trim(); runtime.setLastInjectionContent(injectionText); const retrievalMeta = result?.meta?.retrieval || {}; const llmMeta = retrievalMeta.llm || { status: settings.recallEnableLLM ? "unknown" : "disabled", reason: settings.recallEnableLLM ? "未提供 LLM 状态" : "LLM 精排已关闭", candidatePool: 0, }; const deliveryMode = String(recallInput?.deliveryMode || "immediate").trim() || "immediate"; if (injectionText) { const tokens = runtime.estimateTokens(injectionText); debugLog( `[ST-BME] 注入 ${tokens} 估算 tokens, Core=${result.stats.coreCount}, Recall=${result.stats.recallCount}`, ); runtime.persistRecallInjectionRecord?.({ recallInput, result, injectionText, tokenEstimate: tokens, }); } let injectionTransport = { applied: false, source: "deferred", mode: "deferred", }; if (deliveryMode === "immediate") { injectionTransport = runtime.applyModuleInjectionPrompt(injectionText, settings) || injectionTransport; } runtime.recordInjectionSnapshot("recall", { taskType: "recall", source: recallInput.source, sourceLabel: recallInput.sourceLabel, reason: recallInput.reason || "", sourceCandidates: Array.isArray(recallInput.sourceCandidates) ? recallInput.sourceCandidates.map((candidate) => ({ ...candidate })) : [], hookName: recallInput.hookName, recentMessages, selectedNodeIds: result.selectedNodeIds || [], retrievalMeta, llmMeta, stats: result.stats || {}, injectionText, deliveryMode, applicationMode: deliveryMode === "immediate" ? "injection" : "pending-rewrite", rewrite: { applied: false, path: "", field: "", reason: deliveryMode === "immediate" ? "immediate-injection" : "awaiting-generation-payload-rewrite", }, transport: injectionTransport, }); runtime.setCurrentGraphLastRecallResult(result.selectedNodeIds); runtime.updateLastRecalledItems(result.selectedNodeIds || []); runtime.saveGraphToChat({ reason: "recall-result-updated" }); const llmLabel = llmMeta.status === "llm" ? "LLM 精排完成" : llmMeta.status === "fallback" ? "LLM 回退评分" : llmMeta.status === "disabled" ? "仅评分排序" : "召回完成"; const hookLabel = runtime.getRecallHookLabel(recallInput.hookName); runtime.setLastRecallStatus( llmLabel, [ hookLabel, recallInput.sourceLabel, deliveryMode === "immediate" ? "即时注入" : "等待本轮 rewrite", `ctx ${recentMessages.length}`, `vector ${retrievalMeta.vectorHits ?? 0}`, retrievalMeta.vectorMergedHits ? `merged ${retrievalMeta.vectorMergedHits}` : "", `diffusion ${retrievalMeta.diffusionHits ?? 0}`, retrievalMeta.candidatePoolAfterDpp ? `dpp ${retrievalMeta.candidatePoolAfterDpp}` : "", `llm pool ${llmMeta.candidatePool ?? 0}`, `recall ${result.stats.recallCount}`, ] .filter(Boolean) .join(" · "), llmMeta.status === "fallback" ? "warning" : "success", { syncRuntime: true, toastKind: "", }, ); if (llmMeta.status === "fallback") { const now = Date.now(); if (now - runtime.getLastRecallFallbackNoticeAt() > 15000) { runtime.setLastRecallFallbackNoticeAt(now); runtime.toastr.warning( llmMeta.reason || "LLM 精排未成功,已改用评分排序并继续注入记忆", "ST-BME 召回提示", { timeOut: 4500 }, ); } } return { injectionText, retrievalMeta, llmMeta, transport: injectionTransport, deliveryMode, }; } export async function runRecallController(runtime, options = {}) { if (runtime.getIsRecalling()) { runtime.abortRecallStageWithReason("旧召回已取消,正在启动新的召回"); const settle = await runtime.waitForActiveRecallToSettle(); if (!settle.settled && runtime.getIsRecalling()) { runtime.setLastRecallStatus( "召回忙", "上一轮召回仍在清理,请稍后重试", "warning", { syncRuntime: true, }, ); return runtime.createRecallRunResult("skipped", { reason: "上一轮召回仍在清理", }); } } const hasGraph = !!runtime.getCurrentGraph(); if (!hasGraph) { return runtime.createRecallRunResult("skipped", { reason: "当前无图谱", }); } const settings = runtime.getSettings(); if (!settings.enabled || !settings.recallEnabled) { return runtime.createRecallRunResult("skipped", { reason: "召回功能未启用", }); } const isReadableForRecall = typeof runtime.isGraphReadableForRecall === "function" ? runtime.isGraphReadableForRecall() : runtime.isGraphReadable(); if (!isReadableForRecall) { const reason = runtime.getGraphMutationBlockReason("召回"); runtime.setLastRecallStatus("等待图谱加载", reason, "warning", { syncRuntime: true, }); return runtime.createRecallRunResult("skipped", { reason, }); } if (runtime.isGraphMetadataWriteAllowed()) { if (!(await runtime.recoverHistoryIfNeeded("pre-recall"))) { return runtime.createRecallRunResult("skipped", { reason: "历史恢复未就绪", }); } } const context = runtime.getContext(); const chat = context.chat; if (!chat || chat.length === 0) { return runtime.createRecallRunResult("skipped", { reason: "当前聊天为空", }); } const runId = runtime.nextRecallRunSequence(); let recallPromise = null; recallPromise = (async () => { runtime.setIsRecalling(true); const recallController = runtime.beginStageAbortController("recall"); const recallSignal = recallController.signal; if (options.signal) { if (options.signal.aborted) { recallController.abort( options.signal.reason || runtime.createAbortError("宿主已终止生成"), ); } else { options.signal.addEventListener( "abort", () => recallController.abort( options.signal.reason || runtime.createAbortError("宿主已终止生成"), ), { once: true }, ); } } try { await runtime.ensureVectorReadyIfNeeded("pre-recall", recallSignal); const recentContextMessageLimit = runtime.clampInt( settings.recallLlmContextMessages, 4, 0, 20, ); const recallInput = runtime.resolveRecallInput( chat, recentContextMessageLimit, options, ); const userMessage = recallInput.userMessage; const recentMessages = recallInput.recentMessages; if (!userMessage) { return runtime.createRecallRunResult("skipped", { reason: "当前没有可用于召回的用户输入", }); } recallInput.hookName = options.hookName || ""; recallInput.deliveryMode = String(options.deliveryMode || "immediate").trim() || "immediate"; debugLog("[ST-BME] 开始召回", { source: recallInput.source, sourceLabel: recallInput.sourceLabel, hookName: recallInput.hookName, userMessageLength: userMessage.length, recentMessages: recentMessages.length, runId, }); runtime.setLastRecallStatus( "召回中", [ runtime.getRecallHookLabel(recallInput.hookName), `来源 ${recallInput.sourceLabel}`, `上下文 ${recentMessages.length} 条`, `当前用户消息长度 ${userMessage.length}`, ] .filter(Boolean) .join(" · "), "running", { syncRuntime: true }, ); if (recallInput.source === "send-intent") { runtime.setPendingRecallSendIntent(runtime.createRecallInputRecord()); } const cachedRecallPayload = options.cachedRecallPayload && typeof options.cachedRecallPayload === "object" ? options.cachedRecallPayload : null; if (cachedRecallPayload?.result) { // Cached planner handoff is already the authoritative source for this // generation, so any leftover send-intent snapshot must be cleared to // avoid leaking stale input into a later fallback recall path. runtime.setPendingRecallSendIntent?.(runtime.createRecallInputRecord()); const cachedResult = cachedRecallPayload.result; const recentMessages = Array.isArray(cachedRecallPayload.recentMessages) ? cachedRecallPayload.recentMessages.map((item) => String(item || "")) : recallInput.recentMessages; const applied = runtime.applyRecallInjection( settings, recallInput, recentMessages, cachedResult, ); runtime.consumePlannerRecallHandoff?.(cachedRecallPayload.chatId, { handoffId: cachedRecallPayload.handoffId, }); return runtime.createRecallRunResult("completed", { reason: cachedRecallPayload.reason || "planner-handoff-reused", selectedNodeIds: cachedResult.selectedNodeIds || [], injectionText: applied?.injectionText || "", retrievalMeta: applied?.retrievalMeta || {}, llmMeta: applied?.llmMeta || {}, transport: applied?.transport || { applied: false, source: "none", mode: "none", }, deliveryMode: applied?.deliveryMode || String(recallInput?.deliveryMode || "immediate").trim() || "immediate", source: recallInput?.source || cachedRecallPayload.source || "", sourceLabel: recallInput?.sourceLabel || cachedRecallPayload.sourceLabel || "", hookName: recallInput?.hookName || "", sourceCandidates: Array.isArray(recallInput?.sourceCandidates) ? recallInput.sourceCandidates.map((candidate) => ({ ...candidate, })) : [], stats: cachedResult?.stats || {}, }); } const result = await runtime.retrieve({ graph: runtime.getCurrentGraph(), userMessage, recentMessages, embeddingConfig: runtime.getEmbeddingConfig(), schema: runtime.getSchema(), signal: recallSignal, settings, onStreamProgress: ({ previewText, receivedChars }) => { const preview = previewText?.length > 60 ? "…" + previewText.slice(-60) : previewText || ""; runtime.setLastRecallStatus( "AI 生成中", `${preview} [${receivedChars}字]`, "running", { syncRuntime: true, noticeMarquee: true }, ); }, options: runtime.buildRecallRetrieveOptions(settings, context), }); const applied = runtime.applyRecallInjection( settings, recallInput, recentMessages, result, ); return runtime.createRecallRunResult("completed", { reason: "召回完成", selectedNodeIds: result.selectedNodeIds || [], injectionText: applied?.injectionText || "", retrievalMeta: applied?.retrievalMeta || {}, llmMeta: applied?.llmMeta || {}, transport: applied?.transport || { applied: false, source: "none", mode: "none", }, deliveryMode: applied?.deliveryMode || String(recallInput?.deliveryMode || "immediate").trim() || "immediate", source: recallInput?.source || "", sourceLabel: recallInput?.sourceLabel || "", hookName: recallInput?.hookName || "", sourceCandidates: Array.isArray(recallInput?.sourceCandidates) ? recallInput.sourceCandidates.map((candidate) => ({ ...candidate })) : [], stats: result?.stats || {}, }); } catch (e) { if (runtime.isAbortError(e)) { runtime.setLastRecallStatus( "召回已终止", e?.message || "已手动终止当前召回", "warning", { syncRuntime: true, }, ); return runtime.createRecallRunResult("aborted", { reason: e?.message || "召回已终止", }); } runtime.console.error("[ST-BME] 召回失败:", e); const message = e?.message || String(e); runtime.setLastRecallStatus("召回失败", message, "error", { syncRuntime: true, toastKind: "", }); runtime.toastr.error(`召回失败: ${message}`); return runtime.createRecallRunResult("failed", { reason: message, }); } finally { runtime.finishStageAbortController("recall", recallController); runtime.setIsRecalling(false); if (runtime.getActiveRecallPromise() === recallPromise) { runtime.setActiveRecallPromise(null); } runtime.refreshPanelLiveState(); } })(); runtime.setActiveRecallPromise(recallPromise); return await recallPromise; }