export function createRecallMessageUiController(deps = {}) { let persistedRecallUiRefreshTimer = null; let persistedRecallUiRefreshObserver = null; let persistedRecallUiRefreshSession = 0; const persistedRecallUiDiagnosticTimestamps = new Map(); const persistedRecallPersistDiagnosticTimestamps = new Map(); const getContextValue = () => deps.getContext?.() || null; const getSettingsValue = () => deps.getSettings?.() || {}; const getCurrentGraphValue = () => deps.getCurrentGraph?.() || null; const getDocument = () => deps.document || globalThis.document; const getMutationObserver = () => deps.MutationObserver || globalThis.MutationObserver; const getToastr = () => deps.toastr || {}; const getConsole = () => deps.console || console; const getSetTimeout = () => deps.setTimeout || setTimeout; const getClearTimeout = () => deps.clearTimeout || clearTimeout; const getRefreshRetryDelays = () => Array.isArray(deps.PERSISTED_RECALL_UI_REFRESH_RETRY_DELAYS_MS) ? deps.PERSISTED_RECALL_UI_REFRESH_RETRY_DELAYS_MS : [0, 80, 180, 320, 500, 850, 1300, 2000, 3000, 4200]; const getDiagnosticThrottleMs = () => Number.isFinite(Number(deps.PERSISTED_RECALL_UI_DIAGNOSTIC_THROTTLE_MS)) ? Number(deps.PERSISTED_RECALL_UI_DIAGNOSTIC_THROTTLE_MS) : 1500; function getMessageRecallRecord(messageIndex) { const chat = getContextValue()?.chat; return deps.readPersistedRecallFromUserMessage(chat, messageIndex); } function debugWithThrottle(cache, key, ...args) { if (!globalThis.__stBmeDebugLoggingEnabled) return; const now = Date.now(); const lastAt = cache.get(key) || 0; if (now - lastAt < getDiagnosticThrottleMs()) return; cache.set(key, now); getConsole().debug(...args); } function debugPersistedRecallUi(reason, details = null, throttleKey = reason) { const suffix = details ? ` ${JSON.stringify(details)}` : ""; debugWithThrottle( persistedRecallUiDiagnosticTimestamps, `ui:${throttleKey}`, `[ST-BME] Recall Card UI: ${reason}${suffix}`, ); } function removeMessageRecallRecord(messageIndex) { const chat = getContextValue()?.chat; if (!Array.isArray(chat)) return false; const removed = deps.removePersistedRecallFromUserMessage(chat, messageIndex); if (removed) { deps.triggerChatMetadataSave(getContextValue(), { immediate: false }); } return removed; } function editMessageRecallRecord(messageIndex, nextInjectionText) { const chat = getContextValue()?.chat; if (!Array.isArray(chat)) return null; const current = deps.readPersistedRecallFromUserMessage(chat, messageIndex); if (!current) return null; const normalizedText = deps.normalizeRecallInputText(nextInjectionText); if (!normalizedText) return null; const nowIso = new Date().toISOString(); const nextRecord = { ...current, injectionText: normalizedText, tokenEstimate: deps.estimateTokens(normalizedText), updatedAt: nowIso, }; if (!deps.writePersistedRecallToUserMessage(chat, messageIndex, nextRecord)) { return null; } const edited = deps.markPersistedRecallManualEdit( chat, messageIndex, true, nowIso, ); if (!edited) return null; deps.triggerChatMetadataSave(getContextValue(), { immediate: false }); return edited; } function syncEditedUserMessageDom(messageIndex, nextText) { const chatRoot = getDocument()?.getElementById?.("chat"); if (!chatRoot?.querySelectorAll) return false; for (const messageElement of Array.from(chatRoot.querySelectorAll(".mes") || [])) { if (resolveMessageIndexFromElement(messageElement) !== messageIndex) continue; const userTextElement = messageElement.querySelector?.(".mes_text"); if (!userTextElement) return false; userTextElement.textContent = String(nextText || ""); return true; } return false; } function persistEditedUserMessage(context = getContextValue()) { const candidates = [ ["saveChatConditional", context?.saveChatConditional], ["saveChat", context?.saveChat], ]; for (const [label, handler] of candidates) { if (typeof handler !== "function") continue; try { const result = handler.call(context); if (result && typeof result.catch === "function") { result.catch((error) => { getConsole().error(`[ST-BME] 保存用户输入编辑失败 (${label}):`, error); }); } return label; } catch (error) { getConsole().error(`[ST-BME] 调用 ${label} 保存用户输入编辑失败:`, error); } } return deps.triggerChatMetadataSave(context, { immediate: true }); } function editMessageUserInputText(messageIndex, nextUserInputText) { const context = getContextValue(); const chat = context?.chat; if (!Array.isArray(chat)) { return { ok: false, error: "missing-chat" }; } const message = chat[messageIndex]; if (!message?.is_user) { return { ok: false, error: "not-user-message" }; } const normalizedText = deps.normalizeRecallInputText(nextUserInputText); if (!normalizedText) { return { ok: false, error: "empty-user-input" }; } const previousText = deps.normalizeRecallInputText(message.mes || ""); const currentRecord = deps.readPersistedRecallFromUserMessage(chat, messageIndex); const recallBoundText = deps.normalizeRecallInputText( currentRecord?.boundUserFloorText || previousText, ); const recallMayBeStale = Boolean(currentRecord) && recallBoundText !== normalizedText; message.mes = normalizedText; const swipeIndex = Number.isFinite(Number(message?.swipe_id)) ? Math.max(0, Math.floor(Number(message.swipe_id))) : null; if ( Array.isArray(message?.swipes) && swipeIndex !== null && swipeIndex < message.swipes.length ) { message.swipes[swipeIndex] = normalizedText; } if (message.extra && typeof message.extra === "object") { if (typeof message.extra.display_text === "string") { message.extra.display_text = normalizedText; } if (typeof message.extra.current_display_text === "string") { message.extra.current_display_text = normalizedText; } } const saveMode = persistEditedUserMessage(context); const domSynced = syncEditedUserMessageDom(messageIndex, normalizedText); return { ok: true, nextText: normalizedText, recallMayBeStale, unchanged: previousText === normalizedText, saveMode, domSynced, }; } function clearPersistedRecallMessageUiObserver() { try { persistedRecallUiRefreshObserver?.disconnect?.(); } catch (error) { getConsole().warn("[ST-BME] Recall Card UI observer disconnect 失败:", error); } persistedRecallUiRefreshObserver = null; } function isDomNodeAttached(node) { if (!node) return false; if (node.isConnected === true) return true; return typeof getDocument()?.contains === "function" ? getDocument().contains(node) : true; } function cleanupRecallCardElement(cardElement) { if (!cardElement) return; const messageElement = cardElement.closest?.(".mes") || null; if (messageElement) { restoreRecallCardUserInputDisplay(messageElement); } try { cardElement._bmeDestroyRenderer?.(); } catch (error) { getConsole().warn("[ST-BME] Recall Card renderer 清理失败:", error); } cardElement.remove?.(); } function cleanupLegacyRecallBadges(messageElement) { if (!messageElement?.querySelectorAll) return; const oldBadges = Array.from( messageElement.querySelectorAll(".st-bme-recall-badge") || [], ); for (const oldBadge of oldBadges) oldBadge.remove(); } function cleanupRecallArtifacts(messageElement, keepMessageIndex = null) { if (!messageElement?.querySelectorAll) return; cleanupLegacyRecallBadges(messageElement); restoreRecallCardUserInputDisplay(messageElement); const existingCards = Array.from( messageElement.querySelectorAll(".bme-recall-card") || [], ); for (const card of existingCards) { if ( keepMessageIndex !== null && card.dataset?.messageIndex === String(keepMessageIndex) ) { continue; } cleanupRecallCardElement(card); } } function parseStableMessageIndex(candidate) { const normalized = String(candidate ?? "").trim(); if (!normalized) return null; if (!/^\d+$/.test(normalized)) return null; const parsed = Number.parseInt(normalized, 10); return Number.isFinite(parsed) ? parsed : null; } function resolveMessageIndexFromElement(messageElement) { if (!messageElement) return null; const candidates = [ messageElement.getAttribute?.("mesid"), messageElement.getAttribute?.("data-mesid"), messageElement.getAttribute?.("data-message-id"), messageElement.dataset?.mesid, messageElement.dataset?.messageId, ]; for (const candidate of candidates) { const parsed = parseStableMessageIndex(candidate); if (parsed !== null) return parsed; } return null; } function resolveRecallCardAnchor(messageElement) { if (!messageElement || !isDomNodeAttached(messageElement)) return null; const mesBlock = messageElement.querySelector?.(".mes_block"); if (isDomNodeAttached(mesBlock)) return mesBlock; const mesTextParent = messageElement.querySelector?.(".mes_text")?.parentElement; if (isDomNodeAttached(mesTextParent)) return mesTextParent; return isDomNodeAttached(messageElement) ? messageElement : null; } function getRecallMessageElementPriority(messageElement) { if (!messageElement || !isDomNodeAttached(messageElement)) return -1; let priority = 0; const anchor = resolveRecallCardAnchor(messageElement); if (anchor === messageElement) priority += 1; else if (anchor) priority += 3; if (messageElement.querySelector?.(".mes_text")) priority += 1; if (messageElement.classList?.contains("last_mes")) priority += 2; if ( messageElement.getAttribute?.("is_user") === "true" || messageElement.dataset?.isUser === "true" || messageElement.classList?.contains("user_mes") ) { priority += 1; } return priority; } function normalizeRecallCardUserInputDisplayMode(mode) { const normalized = String(mode || "").trim(); if ( normalized === "off" || normalized === "beautify_only" || normalized === "mirror" ) { return normalized; } return "beautify_only"; } function applyRecallCardUserInputDisplayMode(messageElement, mode) { if (!messageElement?.querySelector) return; const userTextElement = messageElement.querySelector(".mes_text"); if (!userTextElement) return; userTextElement.classList.toggle( "bme-hide-original-user-text", normalizeRecallCardUserInputDisplayMode(mode) === "beautify_only", ); } function restoreRecallCardUserInputDisplay(messageElement) { if (!messageElement?.querySelector) return; const userTextElement = messageElement.querySelector(".mes_text"); userTextElement?.classList?.remove("bme-hide-original-user-text"); } function buildPersistedRecallUiRetryDelays(initialDelayMs = 0) { const normalizedInitial = Math.max( 0, Number.parseInt(initialDelayMs, 10) || 0, ); if (!normalizedInitial) return [...getRefreshRetryDelays()]; return [ normalizedInitial, ...getRefreshRetryDelays().filter( (delay) => delay > normalizedInitial, ), ]; } function summarizePersistedRecallRefreshStatus(summary) { if (summary.waitingMessageIndices.length > 0) return "waiting_dom"; if (summary.anchorFailureIndices.length > 0) return "missing_message_anchor"; if (summary.renderedCount > 0) return "rendered"; if (summary.skippedNonUserIndices.length > 0) return "skipped_non_user"; if (summary.persistedRecordCount === 0) return "missing_recall_record"; return "missing_message_anchor"; } function refreshPersistedRecallMessageUi() { const context = getContextValue(); const chat = context?.chat; if (!Array.isArray(chat) || typeof getDocument()?.getElementById !== "function") { return { status: "missing_chat_root", renderedCount: 0, persistedRecordCount: 0, waitingMessageIndices: [], anchorFailureIndices: [], skippedNonUserIndices: [], }; } const chatRoot = getDocument().getElementById("chat"); if (!chatRoot) { debugPersistedRecallUi("缺少 #chat 根节点"); return { status: "missing_chat_root", renderedCount: 0, persistedRecordCount: 0, waitingMessageIndices: [], anchorFailureIndices: [], skippedNonUserIndices: [], }; } const settings = getSettingsValue(); const themeName = settings?.panelTheme || "crimson"; const recallCardUserInputDisplayMode = normalizeRecallCardUserInputDisplayMode( settings?.recallCardUserInputDisplayMode, ); const callbacks = getRecallCardCallbacks(); const messageElementMap = new Map(); const messageElements = Array.from(chatRoot.querySelectorAll(".mes")); for (const messageElement of messageElements) { cleanupLegacyRecallBadges(messageElement); const messageIndex = resolveMessageIndexFromElement(messageElement); if (!Number.isFinite(messageIndex)) { debugPersistedRecallUi( "消息 DOM 缺少稳定索引属性,跳过挂载", { className: messageElement.className || "", }, "missing-stable-message-index", ); continue; } if (messageElementMap.has(messageIndex)) { const previousElement = messageElementMap.get(messageIndex) || null; const previousPriority = getRecallMessageElementPriority(previousElement); const nextPriority = getRecallMessageElementPriority(messageElement); const shouldReplace = nextPriority >= previousPriority; debugPersistedRecallUi( "检测到重复消息 DOM 索引,已挑选更可靠的锚点", { messageIndex, previousPriority, nextPriority, replaced: shouldReplace, }, `duplicate-message-index:${messageIndex}`, ); if (shouldReplace) { cleanupRecallArtifacts(previousElement); messageElementMap.set(messageIndex, messageElement); } else { cleanupRecallArtifacts(messageElement); } continue; } messageElementMap.set(messageIndex, messageElement); } const summary = { status: "missing_recall_record", renderedCount: 0, persistedRecordCount: 0, waitingMessageIndices: [], anchorFailureIndices: [], skippedNonUserIndices: [], }; for (let messageIndex = 0; messageIndex < chat.length; messageIndex++) { const message = chat[messageIndex]; const messageElement = messageElementMap.get(messageIndex) || null; const existingCard = messageElement?.querySelector?.( `.bme-recall-card[data-message-index="${messageIndex}"]`, ) || null; if (!message?.is_user) { if (messageElement) { restoreRecallCardUserInputDisplay(messageElement); } if (existingCard) cleanupRecallCardElement(existingCard); const unexpectedRecord = deps.readPersistedRecallFromUserMessage( chat, messageIndex, ); if (unexpectedRecord) { summary.skippedNonUserIndices.push(messageIndex); debugPersistedRecallUi( "非 user 楼层存在持久召回记录,已跳过挂载", { messageIndex, }, `skipped-non-user:${messageIndex}`, ); } continue; } const record = deps.readPersistedRecallFromUserMessage(chat, messageIndex); if (!record?.injectionText) { if (messageElement) { restoreRecallCardUserInputDisplay(messageElement); } if (existingCard) cleanupRecallCardElement(existingCard); continue; } summary.persistedRecordCount += 1; if (!messageElement) { summary.waitingMessageIndices.push(messageIndex); debugPersistedRecallUi( "目标 user 楼层 DOM 未就绪,等待后续刷新", { messageIndex, }, `waiting-dom:${messageIndex}`, ); continue; } const anchor = resolveRecallCardAnchor(messageElement); if (!anchor) { restoreRecallCardUserInputDisplay(messageElement); cleanupRecallCardElement(existingCard); summary.anchorFailureIndices.push(messageIndex); debugPersistedRecallUi( "目标 user 楼层锚点解析失败,跳过挂载", { messageIndex, }, `missing-anchor:${messageIndex}`, ); continue; } cleanupRecallArtifacts(messageElement, messageIndex); const currentCard = messageElement.querySelector?.( `.bme-recall-card[data-message-index="${messageIndex}"]`, ) || null; if (currentCard) { deps.updateRecallCardData(currentCard, record, { userMessageText: message.mes || "", userInputDisplayMode: recallCardUserInputDisplayMode, graph: getCurrentGraphValue(), themeName, callbacks, }); } else { const card = deps.createRecallCardElement({ messageIndex, record, userMessageText: message.mes || "", userInputDisplayMode: recallCardUserInputDisplayMode, graph: getCurrentGraphValue(), themeName, callbacks, }); anchor.appendChild(card); } applyRecallCardUserInputDisplayMode( messageElement, recallCardUserInputDisplayMode, ); summary.renderedCount += 1; } summary.status = summarizePersistedRecallRefreshStatus(summary); if (summary.status === "missing_recall_record") { debugPersistedRecallUi("当前无有效持久召回记录可渲染"); } else if (summary.renderedCount > 0) { debugPersistedRecallUi( "Recall Card 挂载完成", { renderedCount: summary.renderedCount, persistedRecordCount: summary.persistedRecordCount, waitingDom: summary.waitingMessageIndices.length, }, `rendered:${summary.renderedCount}`, ); } return summary; } function getRecallCardCallbacks() { return { onEdit: (messageIndex) => { const record = getMessageRecallRecord(messageIndex); if (!record) return; deps.openRecallSidebar({ mode: "edit", messageIndex, record, node: null, graph: getCurrentGraphValue(), callbacks: { onSave: (idx, newText) => { const edited = editMessageRecallRecord(idx, newText); if (edited) { getToastr().success("已保存手动编辑"); } else { getToastr().warning("编辑失败:注入文本不能为空"); } schedulePersistedRecallMessageUiRefresh(); }, estimateTokens: deps.estimateTokens, }, }); }, onEditUserInput: (messageIndex, nextUserInputText) => { const result = editMessageUserInputText(messageIndex, nextUserInputText); if (!result?.ok) { getToastr().warning("编辑失败:内容不能为空或此楼层非用户消息"); return result; } if (result.unchanged) { getToastr().info("用户输入未变化"); } else { getToastr().success("已更新本轮用户输入"); } if (result.recallMayBeStale) { getToastr().info("输入已改,当前召回结果可能需要重新召回"); } schedulePersistedRecallMessageUiRefresh(); return result; }, onDelete: (messageIndex) => { if (removeMessageRecallRecord(messageIndex)) { getToastr().success("已删除持久召回注入"); schedulePersistedRecallMessageUiRefresh(); } }, onRerunRecall: async (messageIndex) => { const result = await deps.rerunRecallForMessage(messageIndex); if (result?.status === "completed") { getToastr().success("重新召回完成"); } schedulePersistedRecallMessageUiRefresh(); }, onNodeClick: (messageIndex, node) => { const record = getMessageRecallRecord(messageIndex); if (!record) return; deps.openRecallSidebar({ mode: "view", messageIndex, record, node, graph: getCurrentGraphValue(), callbacks: { onSave: (idx, newText) => { const edited = editMessageRecallRecord(idx, newText); if (edited) getToastr().success("已保存手动编辑"); else getToastr().warning("编辑失败:注入文本不能为空"); schedulePersistedRecallMessageUiRefresh(); }, estimateTokens: deps.estimateTokens, }, }); }, }; } function armPersistedRecallMessageUiObserver(sessionId, runAttempt) { clearPersistedRecallMessageUiObserver(); const chatRoot = getDocument()?.getElementById?.("chat"); const ObserverCtor = getMutationObserver(); if (!chatRoot || typeof ObserverCtor !== "function") return false; persistedRecallUiRefreshObserver = new ObserverCtor(() => { if (sessionId !== persistedRecallUiRefreshSession) return; clearPersistedRecallMessageUiObserver(); runAttempt(); }); persistedRecallUiRefreshObserver.observe(chatRoot, { childList: true, subtree: true, attributes: true, attributeFilter: [ "mesid", "data-mesid", "data-message-id", "class", "is_user", ], }); return true; } function schedulePersistedRecallMessageUiRefresh(delayMs = 0) { getClearTimeout()(persistedRecallUiRefreshTimer); clearPersistedRecallMessageUiObserver(); const retryDelays = buildPersistedRecallUiRetryDelays(delayMs); const sessionId = ++persistedRecallUiRefreshSession; let attemptIndex = 0; const runAttempt = () => { if (sessionId !== persistedRecallUiRefreshSession) return; if (persistedRecallUiRefreshTimer) { getClearTimeout()(persistedRecallUiRefreshTimer); persistedRecallUiRefreshTimer = null; } const summary = refreshPersistedRecallMessageUi(); const shouldRetryForPending = (summary.status === "missing_chat_root" || summary.status === "waiting_dom" || summary.status === "missing_message_anchor") && attemptIndex < retryDelays.length - 1; // 勿在「已成功渲染」时长期监听 MutationObserver:chat 的 class/流式更新会疯狂触发 // runAttempt,造成满屏刷新与日志;显式事件(USER_MESSAGE_RENDERED 等)仍会 schedule 刷新。 const shouldWatchForRepaint = false; if (!shouldRetryForPending && !shouldWatchForRepaint) { clearPersistedRecallMessageUiObserver(); return; } armPersistedRecallMessageUiObserver(sessionId, runAttempt); if (shouldRetryForPending) { attemptIndex += 1; persistedRecallUiRefreshTimer = getSetTimeout()( runAttempt, retryDelays[attemptIndex], ); return; } const lingerMs = retryDelays[retryDelays.length - 1] || 0; if (lingerMs <= 0) { clearPersistedRecallMessageUiObserver(); return; } persistedRecallUiRefreshTimer = getSetTimeout()(() => { if (sessionId !== persistedRecallUiRefreshSession) return; clearPersistedRecallMessageUiObserver(); persistedRecallUiRefreshTimer = null; }, lingerMs); }; persistedRecallUiRefreshTimer = getSetTimeout()( runAttempt, retryDelays[attemptIndex], ); } function cleanupPersistedRecallMessageUi() { getClearTimeout()(persistedRecallUiRefreshTimer); persistedRecallUiRefreshTimer = null; clearPersistedRecallMessageUiObserver(); const chatRoot = getDocument().getElementById("chat"); if (!chatRoot?.querySelectorAll) return; for (const messageElement of Array.from(chatRoot.querySelectorAll(".mes"))) { cleanupRecallArtifacts(messageElement); } } return { refreshPersistedRecallMessageUi, schedulePersistedRecallMessageUiRefresh, cleanupPersistedRecallMessageUi, resolveMessageIndexFromElement, resolveRecallCardAnchor, }; }