// ST-BME: 主入口 // 事件钩子、设置管理、流程调度 import { eventSource, event_types, extension_prompt_roles, extension_prompt_types, getRequestHeaders, saveMetadata, saveSettingsDebounced, } from "../../../../script.js"; import { extension_settings, getContext, saveMetadataDebounced, } from "../../../extensions.js"; import { BmeChatManager } from "./bme-chat-manager.js"; import { buildGraphFromSnapshot, buildSnapshotFromGraph, ensureDexieLoaded, } from "./bme-db.js"; import { autoSyncOnChatChange, autoSyncOnVisibility, scheduleUpload, syncNow, } from "./bme-sync.js"; import { compressAll, sleepCycle } from "./compressor.js"; import { consolidateMemories } from "./consolidator.js"; import { executeExtractionBatchController, onManualExtractController, onRerollController, runExtractionController, } from "./extraction-controller.js"; import { extractMemories, generateReflection, generateSynopsis, } from "./extractor.js"; import { applyRecallInjectionController, buildRecallRecentMessagesController, getRecallUserMessageSourceLabelController, runRecallController, resolveRecallInputController, } from "./recall-controller.js"; import { createEmptyGraph, deserializeGraph, exportGraph, getGraphStats, getNode, importGraph, serializeGraph, } from "./graph.js"; import { HOST_ADAPTER_STATE_SEMANTICS, getHostAdapter, getHostCapabilitySnapshot, initializeHostAdapter, readHostCapability, refreshHostCapabilitySnapshot, } from "./host-adapter/index.js"; import { estimateTokens, formatInjection } from "./injector.js"; import { fetchMemoryLLMModels, testLLMConnection } from "./llm.js"; import { getNodeDisplayName } from "./node-labels.js"; import { showManagedBmeNotice } from "./notice.js"; import { installSendIntentHooksController, onChatChangedController, onChatLoadedController, onBeforeCombinePromptsController, onGenerationAfterCommandsController, onMessageDeletedController, onMessageEditedController, onMessageReceivedController, onMessageSentController, onMessageSwipedController, registerBeforeCombinePromptsController, registerCoreEventHooksController, registerGenerationAfterCommandsController, scheduleSendIntentHookRetryController, } from "./event-binding.js"; import { createDefaultTaskProfiles, migrateLegacyTaskProfiles, } from "./prompt-profiles.js"; import { onFetchEmbeddingModelsController, onFetchMemoryLLMModelsController, onExportGraphController, onImportGraphController, onManualCompressController, onManualEvolveController, onManualSleepController, onManualSynopsisController, onRebuildVectorIndexController, onRebuildController, onTestEmbeddingController, onTestMemoryLLMController, onReembedDirectController, onViewLastInjectionController, onViewGraphController, } from "./ui-actions-controller.js"; import { createNoticePanelActionController, initializePanelBridgeController, refreshPanelLiveStateController, } from "./panel-bridge.js"; import { resolveConfiguredTimeoutMs } from "./request-timeout.js"; import { retrieve } from "./retriever.js"; import { appendBatchJournal, buildRecoveryResult, buildReverseJournalRecoveryPlan, clearHistoryDirty, cloneGraphSnapshot, createBatchJournalEntry, detectHistoryMutation, findJournalRecoveryPoint, markHistoryDirty, normalizeGraphRuntimeState, rollbackBatch, snapshotProcessedMessageHashes, } from "./runtime-state.js"; import { DEFAULT_NODE_SCHEMA, validateSchema } from "./schema.js"; import { deleteBackendVectorHashesForRecovery, fetchAvailableEmbeddingModels, getVectorConfigFromSettings, getVectorIndexStats, isBackendVectorConfig, isDirectVectorConfig, syncGraphVectorIndex, testVectorConnection, validateVectorConfig, } from "./vector-index.js"; import { BATCH_STAGE_ORDER, BATCH_STAGE_SEVERITY, clampFloat, clampInt, createBatchStageStatus, createBatchStatusSkeleton, createGraphPersistenceState, createRecallInputRecord, createRecallRunResult, createUiStatus, finalizeBatchStatus, formatRecallContextLine, getGenerationRecallHookStateFromResult, getRecallHookLabel, getStageNoticeDuration, getStageNoticeTitle, hashRecallInput, isFreshRecallInputRecord, isTerminalGenerationRecallHookState, normalizeRecallInputText, normalizeStageNoticeLevel, pushBatchStageArtifact, setBatchStageOutcome, shouldRunRecallForTransaction, } from "./ui-status.js"; import { cloneGraphForPersistence, cloneRuntimeDebugValue, getGraphPersistenceMeta, getGraphPersistedRevision, getGraphShadowSnapshotStorageKey, GRAPH_LOAD_PENDING_CHAT_ID, GRAPH_LOAD_STATES, GRAPH_METADATA_KEY, GRAPH_PERSISTENCE_META_KEY, GRAPH_PERSISTENCE_SESSION_ID, GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX, GRAPH_STARTUP_RECONCILE_DELAYS_MS, MODULE_NAME, readGraphShadowSnapshot, removeGraphShadowSnapshot, shouldPreferShadowSnapshotOverOfficial, stampGraphPersistenceMeta, writeChatMetadataPatch, writeGraphShadowSnapshot, } from "./graph-persistence.js"; import { buildExtractionMessages, clampRecoveryStartFloor, getAssistantTurns, getChatIndexForAssistantSeq, getChatIndexForPlayableSeq, getMinExtractableAssistantFloor, isAssistantChatMessage, pruneProcessedMessageHashesFromFloor, resolveDirtyFloorFromMutationMeta, rollbackAffectedJournals, } from "./chat-history.js"; import { buildPersistedRecallRecord, bumpPersistedRecallGenerationCount, markPersistedRecallManualEdit, readPersistedRecallFromUserMessage, removePersistedRecallFromUserMessage, resolveFinalRecallInjectionSource, resolveGenerationTargetUserMessageIndex, writePersistedRecallToUserMessage, } from "./recall-persistence.js"; import { createRecallCardElement, openRecallSidebar, updateRecallCardData, } from "./recall-message-ui.js"; // 操控面板模块(动态加载,防止加载失败崩溃整个扩展) let _panelModule = null; let _themesModule = null; const SERVER_SETTINGS_FILENAME = "st-bme-settings.json"; const SERVER_SETTINGS_URL = `/user/files/${SERVER_SETTINGS_FILENAME}`; function getChatMetadataIntegrity(context = getContext()) { return normalizeChatIdCandidate(context?.chatMetadata?.integrity); } function triggerChatMetadataSave( context = getContext(), { immediate = false } = {}, ) { if (immediate) { const immediateSave = typeof context?.saveMetadata === "function" ? context.saveMetadata : saveMetadata; if (typeof immediateSave === "function") { try { const result = immediateSave.call(context); if (result && typeof result.catch === "function") { result.catch((error) => { console.error("[ST-BME] 立即保存聊天元数据失败:", error); }); } return "immediate"; } catch (error) { console.error("[ST-BME] 触发立即保存聊天元数据失败:", error); } } } if (typeof context?.saveMetadataDebounced === "function") { context.saveMetadataDebounced(); return "debounced"; } saveMetadataDebounced(); return "debounced"; } function getRuntimeDebugState() { const stateKey = "__stBmeRuntimeDebugState"; if (!globalThis[stateKey] || typeof globalThis[stateKey] !== "object") { globalThis[stateKey] = { hostCapabilities: null, taskPromptBuilds: {}, taskLlmRequests: {}, injections: {}, graphPersistence: null, updatedAt: "", }; } return globalThis[stateKey]; } function touchRuntimeDebugState() { const state = getRuntimeDebugState(); state.updatedAt = new Date().toISOString(); return state; } function recordHostCapabilitySnapshot(snapshot = null) { const state = touchRuntimeDebugState(); state.hostCapabilities = cloneRuntimeDebugValue(snapshot, null); } function recordInjectionSnapshot(kind, snapshot = {}) { const normalizedKind = String(kind || "").trim() || "default"; const state = touchRuntimeDebugState(); state.injections[normalizedKind] = { updatedAt: new Date().toISOString(), ...cloneRuntimeDebugValue(snapshot, {}), }; } function recordGraphPersistenceSnapshot(snapshot = null) { const state = touchRuntimeDebugState(); state.graphPersistence = cloneRuntimeDebugValue(snapshot, null); } function readRuntimeDebugSnapshot() { const state = getRuntimeDebugState(); return cloneRuntimeDebugValue( { hostCapabilities: state.hostCapabilities, taskPromptBuilds: state.taskPromptBuilds, taskLlmRequests: state.taskLlmRequests, injections: state.injections, graphPersistence: state.graphPersistence, updatedAt: state.updatedAt, }, { hostCapabilities: null, taskPromptBuilds: {}, taskLlmRequests: {}, injections: {}, graphPersistence: null, updatedAt: "", }, ); } // ==================== 默认设置 ==================== const defaultSettings = { enabled: true, timeoutMs: 300000, // 提取设置 extractEvery: 1, // 每 N 条 assistant 回复提取一次 extractContextTurns: 2, // 提取时包含的上下文楼层数 // 召回设置 recallEnabled: true, recallTopK: 20, // 向量预筛 Top-K recallMaxNodes: 8, // LLM 召回最大节点数 recallEnableLLM: true, // 是否启用 LLM 精确召回 recallEnableVectorPrefilter: true, // 是否启用向量预筛 recallEnableGraphDiffusion: true, // 是否启用图扩散 recallDiffusionTopK: 100, // 图扩散阶段保留的候选上限 recallLlmCandidatePool: 30, // 传给 LLM 精排的候选池大小 recallLlmContextMessages: 4, // 传给 LLM 精排的最近非系统消息数 recallEnableMultiIntent: true, recallMultiIntentMaxSegments: 4, recallTeleportAlpha: 0.15, recallEnableTemporalLinks: true, recallTemporalLinkStrength: 0.2, recallEnableDiversitySampling: true, recallDppCandidateMultiplier: 3, recallDppQualityWeight: 1.0, recallEnableCooccurrenceBoost: false, recallCooccurrenceScale: 0.1, recallCooccurrenceMaxNeighbors: 10, recallEnableResidualRecall: false, recallResidualBasisMaxNodes: 24, recallNmfTopics: 15, recallNmfNoveltyThreshold: 0.4, recallResidualThreshold: 0.3, recallResidualTopK: 5, // 注入设置 injectPosition: "atDepth", // 注入位置 injectDepth: 9999, // IN_CHAT@Depth 注入深度,数值越大越靠前 injectRole: 0, // 0=system, 1=user, 2=assistant // 混合评分权重 graphWeight: 0.6, vectorWeight: 0.3, importanceWeight: 0.1, // 记忆 LLM(留空时复用当前酒馆模型) llmApiUrl: "", llmApiKey: "", llmModel: "", // Embedding API 配置 embeddingApiUrl: "", embeddingApiKey: "", embeddingModel: "text-embedding-3-small", embeddingTransportMode: "direct", embeddingBackendSource: "openai", embeddingBackendModel: "text-embedding-3-small", embeddingBackendApiUrl: "", embeddingAutoSuffix: true, // Schema nodeTypeSchema: null, // null 表示使用默认 // 自定义提示词 extractPrompt: "", recallPrompt: "", consolidationPrompt: "", compressPrompt: "", synopsisPrompt: "", reflectionPrompt: "", taskProfilesVersion: 3, taskProfiles: createDefaultTaskProfiles(), // ====== v2 增强设置 ====== // ③ 记忆整合(合并精确对照 + 记忆进化) enableConsolidation: true, // 启用记忆整合 consolidationNeighborCount: 5, // 近邻搜索数量 consolidationThreshold: 0.85, // 冲突判定相似度阈值 // ⑨ 全局故事概要 enableSynopsis: true, // 启用全局概要 synopsisEveryN: 5, // 每 N 次提取后更新概要 // ⑥ 认知边界过滤(P1) enableVisibility: true, // 启用认知边界 // ⑦ 双记忆交叉检索(P1) enableCrossRecall: true, // 启用交叉检索 // ① 惊奇度分割(P2) enableSmartTrigger: false, // 启用惊奇度分割 triggerPatterns: "", // 自定义触发正则 smartTriggerThreshold: 2, // 轻量触发阈值 // ⑤ 主动遗忘(P2) enableSleepCycle: false, // 启用主动遗忘 forgetThreshold: 0.5, // 保留价值阈值 sleepEveryN: 10, // 每 N 次提取后执行 // ⑧ 概率触发回忆(P2) enableProbRecall: false, // 启用概率触发 probRecallChance: 0.15, // 触发概率 // ⑩ 反思条目(P2) enableReflection: true, // 启用反思 reflectEveryN: 10, // 每 N 次提取后反思 // UI 面板 panelTheme: "crimson", // 面板主题 crimson|cyan|amber|violet }; // ==================== 状态 ==================== let currentGraph = null; let isExtracting = false; let isRecalling = false; let activeRecallPromise = null; let recallRunSequence = 0; let lastInjectionContent = ""; let lastExtractedItems = []; // 最近提取的节点(面板展示用) let lastRecalledItems = []; // 最近召回的节点(面板展示用) let extractionCount = 0; // v2: 提取次数计数器(定期触发概要/遗忘/反思) let serverSettingsSaveTimer = null; let isRecoveringHistory = false; let lastHistoryWarningAt = 0; let lastRecallFallbackNoticeAt = 0; let lastExtractionWarningAt = 0; const LOCAL_VECTOR_TIMEOUT_MS = 300000; const STATUS_TOAST_THROTTLE_MS = 1500; const RECALL_INPUT_RECORD_TTL_MS = 60000; const HISTORY_RECOVERY_SETTLE_MS = 80; const HISTORY_MUTATION_RETRY_DELAYS_MS = [80, 220, 500, 900]; const GRAPH_LOAD_RETRY_DELAYS_MS = [120, 450, 1200, 2500]; let runtimeStatus = createUiStatus("待命", "准备就绪", "idle"); let lastExtractionStatus = createUiStatus("待命", "尚未执行提取", "idle"); let lastVectorStatus = createUiStatus("待命", "尚未执行向量任务", "idle"); let lastRecallStatus = createUiStatus("待命", "尚未执行召回", "idle"); let graphPersistenceState = createGraphPersistenceState(); const lastStatusToastAt = {}; let pendingRecallSendIntent = createRecallInputRecord(); let lastRecallSentUserMessage = createRecallInputRecord(); let sendIntentHookCleanup = []; let sendIntentHookRetryTimer = null; let pendingHistoryRecoveryTimer = null; let pendingHistoryRecoveryTrigger = ""; let pendingHistoryMutationCheckTimers = []; let pendingGraphLoadRetryTimer = null; let pendingGraphLoadRetryChatId = ""; let skipBeforeCombineRecallUntil = 0; let lastPreGenerationRecallKey = ""; let lastPreGenerationRecallAt = 0; const generationRecallTransactions = new Map(); let persistedRecallUiRefreshTimer = null; let persistedRecallUiRefreshObserver = null; let persistedRecallUiRefreshSession = 0; const PERSISTED_RECALL_UI_REFRESH_RETRY_DELAYS_MS = [0, 80, 180, 320, 500]; const PERSISTED_RECALL_UI_DIAGNOSTIC_THROTTLE_MS = 1500; const persistedRecallUiDiagnosticTimestamps = new Map(); const persistedRecallPersistDiagnosticTimestamps = new Map(); const GENERATION_RECALL_TRANSACTION_TTL_MS = 15000; const stageNoticeHandles = { extraction: null, vector: null, recall: null, history: null, }; const stageAbortControllers = { extraction: null, vector: null, recall: null, history: null, }; let bmeChatManager = null; let bmeChatManagerUnavailableWarned = false; const bmeIndexedDbSnapshotCacheByChatId = new Map(); const bmeIndexedDbLoadInFlightByChatId = new Map(); const bmeIndexedDbWriteInFlightByChatId = new Map(); const bmeIndexedDbLegacyMigrationInFlightByChatId = new Map(); const bmeIndexedDbLatestQueuedRevisionByChatId = new Map(); const BME_INDEXEDDB_FALLBACK_LOAD_STATE_SET = new Set([GRAPH_LOAD_STATES.LOADING, GRAPH_LOAD_STATES.BLOCKED, GRAPH_LOAD_STATES.NO_CHAT, GRAPH_LOAD_STATES.SHADOW_RESTORED]); function isGraphLoadStateDbReady(loadState = graphPersistenceState.loadState) { return ( loadState === GRAPH_LOAD_STATES.LOADED || loadState === GRAPH_LOAD_STATES.EMPTY_CONFIRMED ); } function normalizeGraphSyncState(value = "idle") { const normalized = String(value || "idle").trim().toLowerCase(); if (["idle", "syncing", "warning", "error"].includes(normalized)) return normalized; return "idle"; } function getGraphPersistenceLiveState() { const snapshot = { loadState: graphPersistenceState.loadState, chatId: graphPersistenceState.chatId, reason: graphPersistenceState.reason, attemptIndex: graphPersistenceState.attemptIndex, graphRevision: graphPersistenceState.revision, lastPersistedRevision: graphPersistenceState.lastPersistedRevision, queuedPersistRevision: graphPersistenceState.queuedPersistRevision, queuedPersistChatId: graphPersistenceState.queuedPersistChatId, shadowSnapshotUsed: graphPersistenceState.shadowSnapshotUsed, shadowSnapshotRevision: graphPersistenceState.shadowSnapshotRevision, shadowSnapshotUpdatedAt: graphPersistenceState.shadowSnapshotUpdatedAt, shadowSnapshotReason: graphPersistenceState.shadowSnapshotReason, lastPersistReason: graphPersistenceState.lastPersistReason, lastPersistMode: graphPersistenceState.lastPersistMode, metadataIntegrity: graphPersistenceState.metadataIntegrity, writesBlocked: graphPersistenceState.writesBlocked, pendingPersist: graphPersistenceState.pendingPersist, queuedPersistMode: graphPersistenceState.queuedPersistMode, queuedPersistRotateIntegrity: graphPersistenceState.queuedPersistRotateIntegrity, queuedPersistReason: graphPersistenceState.queuedPersistReason, canWriteToMetadata: isGraphMetadataWriteAllowed( graphPersistenceState.loadState, ), updatedAt: graphPersistenceState.updatedAt, storagePrimary: graphPersistenceState.storagePrimary || "indexeddb", storageMode: graphPersistenceState.storageMode || "indexeddb", dbReady: graphPersistenceState.dbReady ?? isGraphLoadStateDbReady(graphPersistenceState.loadState), indexedDbRevision: graphPersistenceState.indexedDbRevision || 0, indexedDbLastError: graphPersistenceState.indexedDbLastError || "", syncState: normalizeGraphSyncState(graphPersistenceState.syncState), lastSyncUploadedAt: Number(graphPersistenceState.lastSyncUploadedAt) || 0, lastSyncDownloadedAt: Number(graphPersistenceState.lastSyncDownloadedAt) || 0, lastSyncedRevision: Number(graphPersistenceState.lastSyncedRevision) || 0, lastSyncError: String(graphPersistenceState.lastSyncError || ""), dualWriteLastResult: cloneRuntimeDebugValue(graphPersistenceState.dualWriteLastResult, null), }; return cloneRuntimeDebugValue(snapshot, snapshot); } function syncGraphPersistenceDebugState() { recordGraphPersistenceSnapshot(getGraphPersistenceLiveState()); } function updateGraphPersistenceState(patch = {}) { graphPersistenceState = { ...graphPersistenceState, ...(patch || {}), updatedAt: new Date().toISOString(), }; syncGraphPersistenceDebugState(); return graphPersistenceState; } function bumpGraphRevision(reason = "graph-mutation") { const nextRevision = Math.max( graphPersistenceState.revision || 0, graphPersistenceState.lastPersistedRevision || 0, graphPersistenceState.queuedPersistRevision || 0, ) + 1; updateGraphPersistenceState({ revision: nextRevision, lastPersistReason: String( reason || graphPersistenceState.lastPersistReason || "", ), }); return nextRevision; } function isGraphMetadataWriteAllowed( loadState = graphPersistenceState.loadState, ) { return ( loadState === GRAPH_LOAD_STATES.LOADED || loadState === GRAPH_LOAD_STATES.EMPTY_CONFIRMED ); } function isGraphReadable(loadState = graphPersistenceState.loadState) { return ( loadState === GRAPH_LOAD_STATES.LOADED || loadState === GRAPH_LOAD_STATES.EMPTY_CONFIRMED || loadState === GRAPH_LOAD_STATES.SHADOW_RESTORED || (loadState === GRAPH_LOAD_STATES.BLOCKED && graphPersistenceState.shadowSnapshotUsed) ); } function createGraphLoadUiStatus() { const state = graphPersistenceState.loadState; const chatId = graphPersistenceState.chatId || getCurrentChatId(); switch (state) { case GRAPH_LOAD_STATES.NO_CHAT: return createUiStatus("待命", "当前尚未进入聊天", "idle"); case GRAPH_LOAD_STATES.LOADING: return createUiStatus( "图谱加载中", chatId ? `正在读取聊天 ${chatId} 的 IndexedDB 图谱` : "正在等待聊天上下文准备完成", "running", ); case GRAPH_LOAD_STATES.SHADOW_RESTORED: return createUiStatus( "图谱临时恢复", "已从本次会话临时恢复,正在等待正式聊天元数据", "warning", ); case GRAPH_LOAD_STATES.EMPTY_CONFIRMED: return createUiStatus( "图谱待命", chatId ? "当前聊天还没有图谱" : "当前尚未进入聊天", "idle", ); case GRAPH_LOAD_STATES.BLOCKED: return createUiStatus( "图谱加载受阻", "当前图谱尚未完成 IndexedDB 初始化", "warning", ); case GRAPH_LOAD_STATES.LOADED: default: return createUiStatus("待命", "已加载聊天图谱,等待下一次任务", "idle"); } } function getPanelRuntimeStatus() { const graphStatus = createGraphLoadUiStatus(); if ( !graphPersistenceState.dbReady || graphPersistenceState.loadState === GRAPH_LOAD_STATES.LOADING || graphPersistenceState.loadState === GRAPH_LOAD_STATES.SHADOW_RESTORED || graphPersistenceState.loadState === GRAPH_LOAD_STATES.BLOCKED || graphPersistenceState.loadState === GRAPH_LOAD_STATES.NO_CHAT ) { return graphStatus; } return runtimeStatus; } function getGraphMutationBlockReason(operationLabel = "当前操作") { const loadState = graphPersistenceState.loadState; if (!getCurrentChatId()) { return `${operationLabel}已暂停:当前尚未进入聊天。`; } if (graphPersistenceState.dbReady || isGraphLoadStateDbReady(loadState)) { return `${operationLabel}暂不可用。`; } switch (graphPersistenceState.loadState) { case GRAPH_LOAD_STATES.LOADING: return `${operationLabel}已暂停:正在加载 IndexedDB 图谱。`; case GRAPH_LOAD_STATES.SHADOW_RESTORED: return `${operationLabel}已暂停:当前图谱仍处于旧恢复状态,请等待 IndexedDB 初始化完成。`; case GRAPH_LOAD_STATES.BLOCKED: return `${operationLabel}已暂停:IndexedDB 初始化受阻,请稍后重试。`; case GRAPH_LOAD_STATES.NO_CHAT: return `${operationLabel}已暂停:当前尚未进入聊天。`; default: return `${operationLabel}已暂停:图谱尚未完成初始化。`; } } function ensureGraphMutationReady( operationLabel = "当前操作", { notify = true } = {}, ) { if (graphPersistenceState.dbReady || isGraphLoadStateDbReady()) return true; if (notify) { toastr.info(getGraphMutationBlockReason(operationLabel), "ST-BME"); } return false; } function applyGraphLoadState( loadState, { chatId = getCurrentChatId(), reason = "", attemptIndex = 0, shadowSnapshotUsed = false, shadowSnapshotRevision = 0, shadowSnapshotUpdatedAt = "", shadowSnapshotReason = "", revision = graphPersistenceState.revision, lastPersistedRevision = graphPersistenceState.lastPersistedRevision, queuedPersistRevision = graphPersistenceState.queuedPersistRevision, pendingPersist = graphPersistenceState.pendingPersist, dbReady = isGraphLoadStateDbReady(loadState), writesBlocked = !isGraphMetadataWriteAllowed(loadState), } = {}, ) { updateGraphPersistenceState({ loadState, chatId: String(chatId || ""), reason: String(reason || ""), attemptIndex, revision, lastPersistedRevision, queuedPersistRevision, shadowSnapshotUsed, shadowSnapshotRevision, shadowSnapshotUpdatedAt, shadowSnapshotReason, pendingPersist, writesBlocked, dbReady, storageMode: "indexeddb", }); } function createAbortError(message = "操作已终止") { const error = new Error(message); error.name = "AbortError"; return error; } function isAbortError(error) { return error?.name === "AbortError"; } function throwIfAborted(signal, message = "操作已终止") { if (signal?.aborted) { throw signal.reason instanceof Error ? signal.reason : createAbortError(message); } } function assertRecoveryChatStillActive(expectedChatId, label = '') { if (!expectedChatId) return; const currentId = getCurrentChatId(); if (currentId && currentId !== expectedChatId) { throw createAbortError( `历史恢复已终止:聊天已从 ${expectedChatId} 切换到 ${currentId}${label ? ` (${label})` : ''}` ); } } function getStageAbortLabel(stage) { switch (stage) { case "extraction": return "提取"; case "vector": return "向量"; case "recall": return "召回"; case "history": return "历史恢复"; default: return "当前流程"; } } function beginStageAbortController(stage) { const controller = new AbortController(); stageAbortControllers[stage] = controller; syncStageNoticeAbortAction(stage); return controller; } function finishStageAbortController(stage, controller = null) { if (!controller || stageAbortControllers[stage] === controller) { stageAbortControllers[stage] = null; } } function findAbortableStageForNotice(stage) { const preferred = [stage]; if (stage === "vector") { preferred.push("history", "extraction", "recall"); } for (const candidate of preferred) { const controller = stageAbortControllers[candidate]; if (controller && !controller.signal.aborted) { return candidate; } } return null; } function abortStage(stage) { const controller = stageAbortControllers[stage]; if (!controller || controller.signal.aborted) return false; controller.abort(createAbortError(`${getStageAbortLabel(stage)}已终止`)); return true; } function abortRecallStageWithReason(reason = "召回已终止") { const controller = stageAbortControllers.recall; if (!controller || controller.signal.aborted) return false; controller.abort(createAbortError(reason)); return true; } async function waitForActiveRecallToSettle(timeoutMs = 1800) { const pending = activeRecallPromise; if (!pending) { return { settled: !isRecalling, timedOut: false, }; } let settled = false; await Promise.race([ Promise.resolve(pending) .catch(() => {}) .then(() => { settled = true; }), new Promise((resolve) => setTimeout(resolve, timeoutMs)), ]); return { settled: settled || !isRecalling, timedOut: !settled && isRecalling, }; } function buildAbortStageAction(stage) { const abortStageName = findAbortableStageForNotice(stage); if (!abortStageName) return undefined; return { label: `终止${getStageAbortLabel(abortStageName)}`, kind: "danger", onClick: () => { abortStage(abortStageName); }, }; } function createNoticePanelAction() { return createNoticePanelActionController({ getPanelModule: () => _panelModule, }); } function dismissStageNotice(stage) { stageNoticeHandles[stage]?.dismiss?.(); stageNoticeHandles[stage] = null; } function dismissAllStageNotices() { for (const stage of Object.keys(stageNoticeHandles)) { dismissStageNotice(stage); } } function abortAllRunningStages() { for (const stage of Object.keys(stageAbortControllers)) { abortStage(stage); } } function getStageUiStatus(stage) { switch (stage) { case "extraction": return lastExtractionStatus; case "vector": return lastVectorStatus; case "recall": return lastRecallStatus; default: return null; } } function syncStageNoticeAbortAction(stage) { const status = getStageUiStatus(stage); if (!status || !stageNoticeHandles[stage]) return; updateStageNotice(stage, status.text, status.meta, status.level, { title: getStageNoticeTitle(stage), }); } function updateStageNotice( stage, text, meta = "", level = "info", options = {}, ) { const noticeLevel = normalizeStageNoticeLevel(level); const busy = options.busy ?? level === "running"; const persist = options.persist ?? busy; const title = options.title || getStageNoticeTitle(stage); const message = [text, meta].filter(Boolean).join("\n"); const input = { title, message, level: noticeLevel, busy, persist, marquee: options.noticeMarquee ?? false, duration_ms: options.duration_ms ?? getStageNoticeDuration(noticeLevel), action: options.action === undefined ? busy ? buildAbortStageAction(stage) : noticeLevel === "warning" || noticeLevel === "error" ? createNoticePanelAction() : undefined : options.action, }; const currentHandle = stageNoticeHandles[stage]; if (!currentHandle || currentHandle.isClosed?.()) { stageNoticeHandles[stage] = showManagedBmeNotice(input); return; } currentHandle.update(input); } function toPanelNodeItem(node, meta = "") { return { id: node.id, type: node.type, name: getNodeDisplayName(node), meta, }; } function updateLastExtractedItems(nodeIds = []) { if (!currentGraph || !Array.isArray(nodeIds)) { lastExtractedItems = []; return; } lastExtractedItems = nodeIds .map((id) => getNode(currentGraph, id)) .filter(Boolean) .slice(-5) .reverse() .map((node) => toPanelNodeItem( node, `seq ${node.seqRange?.[1] ?? node.seq ?? 0} · ${new Date( node.createdTime || Date.now(), ).toLocaleTimeString()}`, ), ); } function updateLastRecalledItems(nodeIds = []) { if (!currentGraph || !Array.isArray(nodeIds)) { lastRecalledItems = []; return; } lastRecalledItems = nodeIds .map((id) => getNode(currentGraph, id)) .filter(Boolean) .slice(0, 8) .map((node) => toPanelNodeItem( node, `imp ${node.importance ?? 5} · seq ${node.seqRange?.[1] ?? node.seq ?? 0}`, ), ); } function clearRecallInputTracking() { pendingRecallSendIntent = createRecallInputRecord(); lastRecallSentUserMessage = createRecallInputRecord(); } function recordRecallSendIntent(text, source = "dom-intent") { const normalized = normalizeRecallInputText(text); if (!normalized) return; pendingRecallSendIntent = createRecallInputRecord({ text: normalized, hash: hashRecallInput(normalized), source, at: Date.now(), }); } function recordRecallSentUserMessage(messageId, text, source = "message-sent") { const normalized = normalizeRecallInputText(text); if (!normalized) return; const hash = hashRecallInput(normalized); lastRecallSentUserMessage = createRecallInputRecord({ text: normalized, hash, messageId: Number.isFinite(messageId) ? messageId : null, source, at: Date.now(), }); if (pendingRecallSendIntent.hash && pendingRecallSendIntent.hash === hash) { pendingRecallSendIntent = createRecallInputRecord(); } } function getMessageRecallRecord(messageIndex) { const chat = getContext()?.chat; return readPersistedRecallFromUserMessage(chat, messageIndex); } function debugWithThrottle(cache, key, ...args) { const now = Date.now(); const lastAt = cache.get(key) || 0; if (now - lastAt < PERSISTED_RECALL_UI_DIAGNOSTIC_THROTTLE_MS) return; cache.set(key, now); console.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 debugPersistedRecallPersistence(reason, details = null, throttleKey = reason) { const suffix = details ? ` ${JSON.stringify(details)}` : ""; debugWithThrottle( persistedRecallPersistDiagnosticTimestamps, `persist:${throttleKey}`, `[ST-BME] Recall Card persist: ${reason}${suffix}`, ); } function persistRecallInjectionRecord({ recallInput = {}, result = {}, injectionText = "", tokenEstimate = 0, } = {}) { const chat = getContext()?.chat; if (!Array.isArray(chat)) return null; const generationType = String(recallInput?.generationType || "normal").trim() || "normal"; let resolvedTargetIndex = Number.isFinite(recallInput?.targetUserMessageIndex) ? recallInput.targetUserMessageIndex : resolveGenerationTargetUserMessageIndex(chat, { generationType }); if ( !Number.isFinite(resolvedTargetIndex) && Number.isFinite(lastRecallSentUserMessage?.messageId) && chat[lastRecallSentUserMessage.messageId]?.is_user ) { resolvedTargetIndex = lastRecallSentUserMessage.messageId; } if (!Number.isFinite(resolvedTargetIndex)) { debugPersistedRecallPersistence("目标 user 楼层解析失败", { generationType, explicitTargetUserMessageIndex: recallInput?.targetUserMessageIndex, lastSentUserMessageId: lastRecallSentUserMessage?.messageId, }); return null; } if (!chat[resolvedTargetIndex]?.is_user) { debugPersistedRecallPersistence("目标楼层不是 user 消息,跳过持久化", { targetUserMessageIndex: resolvedTargetIndex, messageKeys: Object.keys(chat[resolvedTargetIndex] || {}), }); return null; } const record = buildPersistedRecallRecord( { injectionText, selectedNodeIds: result?.selectedNodeIds || [], recallInput: String(recallInput?.userMessage || ""), recallSource: String(recallInput?.source || ""), hookName: String(recallInput?.hookName || ""), tokenEstimate, manuallyEdited: false, }, readPersistedRecallFromUserMessage(chat, resolvedTargetIndex), ); if (!String(record?.injectionText || "").trim()) { debugPersistedRecallPersistence("无有效 injectionText,跳过持久化", { targetUserMessageIndex: resolvedTargetIndex, selectedNodeCount: Array.isArray(result?.selectedNodeIds) ? result.selectedNodeIds.length : 0, }); return null; } if (!writePersistedRecallToUserMessage(chat, resolvedTargetIndex, record)) { debugPersistedRecallPersistence("写入 user 楼层失败", { targetUserMessageIndex: resolvedTargetIndex, }); return null; } triggerChatMetadataSave(getContext(), { immediate: false }); debugPersistedRecallPersistence("召回记录已写入 user 楼层", { targetUserMessageIndex: resolvedTargetIndex, injectionTextLength: String(record?.injectionText || "").length, selectedNodeCount: Array.isArray(record?.selectedNodeIds) ? record.selectedNodeIds.length : 0, }, `persist-success:${resolvedTargetIndex}`); return { index: resolvedTargetIndex, record, }; } function removeMessageRecallRecord(messageIndex) { const chat = getContext()?.chat; if (!Array.isArray(chat)) return false; const removed = removePersistedRecallFromUserMessage(chat, messageIndex); if (removed) { triggerChatMetadataSave(getContext(), { immediate: false }); } return removed; } function editMessageRecallRecord(messageIndex, nextInjectionText) { const chat = getContext()?.chat; if (!Array.isArray(chat)) return null; const current = readPersistedRecallFromUserMessage(chat, messageIndex); if (!current) return null; const normalizedText = normalizeRecallInputText(nextInjectionText); if (!normalizedText) return null; const nowIso = new Date().toISOString(); const nextRecord = { ...current, injectionText: normalizedText, tokenEstimate: estimateTokens(normalizedText), updatedAt: nowIso, }; if (!writePersistedRecallToUserMessage(chat, messageIndex, nextRecord)) { return null; } const edited = markPersistedRecallManualEdit(chat, messageIndex, true, nowIso); if (!edited) return null; triggerChatMetadataSave(getContext(), { immediate: false }); return edited; } function applyFinalRecallInjectionForGeneration({ generationType = "normal", freshRecallResult = null, } = {}) { const chat = getContext()?.chat; if (!Array.isArray(chat)) { applyModuleInjectionPrompt("", getSettings()); return { source: "none", targetUserMessageIndex: null, usedText: "" }; } let targetUserMessageIndex = resolveGenerationTargetUserMessageIndex(chat, { generationType, }); if ( !Number.isFinite(targetUserMessageIndex) && Number.isFinite(lastRecallSentUserMessage?.messageId) && chat[lastRecallSentUserMessage.messageId]?.is_user ) { targetUserMessageIndex = lastRecallSentUserMessage.messageId; } const persistedRecord = Number.isFinite(targetUserMessageIndex) ? readPersistedRecallFromUserMessage(chat, targetUserMessageIndex) : null; const resolved = resolveFinalRecallInjectionSource({ freshRecallResult, persistedRecord, }); if (resolved.source === "persisted") { applyModuleInjectionPrompt(resolved.injectionText || "", getSettings()); } else if (resolved.source === "none") { applyModuleInjectionPrompt("", getSettings()); } if (resolved.source === "persisted" && Number.isFinite(targetUserMessageIndex)) { bumpPersistedRecallGenerationCount(chat, targetUserMessageIndex); triggerChatMetadataSave(getContext(), { immediate: false }); } if (resolved.source === "fresh") { runtimeStatus = createUiStatus( "召回已注入", "本轮已使用最新召回结果", "success", ); } else if (resolved.source === "persisted") { lastInjectionContent = resolved.injectionText || ""; runtimeStatus = createUiStatus("召回回退", "已使用消息楼层持久化注入", "info"); } else { lastInjectionContent = ""; runtimeStatus = createUiStatus("待命", "当前无有效注入内容", "idle"); } refreshPanelLiveState(); schedulePersistedRecallMessageUiRefresh(); return { source: resolved.source, isFallback: resolved.source === "persisted", targetUserMessageIndex, usedText: resolved.injectionText || "", }; } function clearPersistedRecallMessageUiObserver() { try { persistedRecallUiRefreshObserver?.disconnect?.(); } catch (error) { console.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 document?.contains === "function" ? document.contains(node) : true; } function cleanupRecallCardElement(cardElement) { if (!cardElement) return; try { cardElement._bmeDestroyRenderer?.(); } catch (error) { console.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); 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 buildPersistedRecallUiRetryDelays(initialDelayMs = 0) { const normalizedInitial = Math.max(0, Number.parseInt(initialDelayMs, 10) || 0); if (!normalizedInitial) return [...PERSISTED_RECALL_UI_REFRESH_RETRY_DELAYS_MS]; return [ normalizedInitial, ...PERSISTED_RECALL_UI_REFRESH_RETRY_DELAYS_MS.filter((delay) => delay > normalizedInitial), ]; } function summarizePersistedRecallRefreshStatus(summary) { if (summary.renderedCount > 0) return "rendered"; if (summary.waitingMessageIndices.length > 0) return "waiting_dom"; if (summary.anchorFailureIndices.length > 0) return "missing_message_anchor"; 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 = getContext(); const chat = context?.chat; if (!Array.isArray(chat) || typeof document?.getElementById !== "function") { return { status: "missing_chat_root", renderedCount: 0, persistedRecordCount: 0, waitingMessageIndices: [], anchorFailureIndices: [], skippedNonUserIndices: [], }; } const chatRoot = document.getElementById("chat"); if (!chatRoot) { debugPersistedRecallUi("缺少 #chat 根节点"); return { status: "missing_chat_root", renderedCount: 0, persistedRecordCount: 0, waitingMessageIndices: [], anchorFailureIndices: [], skippedNonUserIndices: [], }; } const themeName = getSettings()?.panelTheme || "crimson"; 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)) { debugPersistedRecallUi("检测到重复消息 DOM 索引,保留首个锚点", { messageIndex, }, `duplicate-message-index:${messageIndex}`); 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 (existingCard) cleanupRecallCardElement(existingCard); const unexpectedRecord = readPersistedRecallFromUserMessage(chat, messageIndex); if (unexpectedRecord) { summary.skippedNonUserIndices.push(messageIndex); debugPersistedRecallUi("非 user 楼层存在持久召回记录,已跳过挂载", { messageIndex, }, `skipped-non-user:${messageIndex}`); } continue; } const record = readPersistedRecallFromUserMessage(chat, messageIndex); if (!record?.injectionText) { 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) { 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) { updateRecallCardData(currentCard, record); } else { const card = createRecallCardElement({ messageIndex, record, userMessageText: message.mes || "", graph: currentGraph, themeName, callbacks, }); anchor.appendChild(card); } 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; openRecallSidebar({ mode: "edit", messageIndex, record, node: null, graph: currentGraph, callbacks: { onSave: (idx, newText) => { const edited = editMessageRecallRecord(idx, newText); if (edited) { toastr.success("已保存手动编辑"); } else { toastr.warning("编辑失败:注入文本不能为空"); } schedulePersistedRecallMessageUiRefresh(); }, estimateTokens, }, }); }, onDelete: (messageIndex) => { if (removeMessageRecallRecord(messageIndex)) { toastr.success("已删除持久召回注入"); schedulePersistedRecallMessageUiRefresh(); } }, onRerunRecall: async (messageIndex) => { const result = await rerunRecallForMessage(messageIndex); if (result?.status === "completed") { toastr.success("重新召回完成"); } schedulePersistedRecallMessageUiRefresh(); }, onNodeClick: (messageIndex, node) => { const record = getMessageRecallRecord(messageIndex); if (!record) return; openRecallSidebar({ mode: "view", messageIndex, record, node, graph: currentGraph, callbacks: { onSave: (idx, newText) => { const edited = editMessageRecallRecord(idx, newText); if (edited) toastr.success("已保存手动编辑"); else toastr.warning("编辑失败:注入文本不能为空"); schedulePersistedRecallMessageUiRefresh(); }, estimateTokens, }, }); }, }; } function armPersistedRecallMessageUiObserver(sessionId, runAttempt) { clearPersistedRecallMessageUiObserver(); const chatRoot = document?.getElementById?.("chat"); const ObserverCtor = globalThis.MutationObserver; if (!chatRoot || typeof ObserverCtor !== "function") return false; persistedRecallUiRefreshObserver = new ObserverCtor(() => { if (sessionId !== persistedRecallUiRefreshSession) return; clearPersistedRecallMessageUiObserver(); runAttempt(); }); persistedRecallUiRefreshObserver.observe(chatRoot, { childList: true, subtree: true }); return true; } function schedulePersistedRecallMessageUiRefresh(delayMs = 0) { clearTimeout(persistedRecallUiRefreshTimer); clearPersistedRecallMessageUiObserver(); const retryDelays = buildPersistedRecallUiRetryDelays(delayMs); const sessionId = ++persistedRecallUiRefreshSession; let attemptIndex = 0; const runAttempt = () => { if (sessionId !== persistedRecallUiRefreshSession) return; if (persistedRecallUiRefreshTimer) { clearTimeout(persistedRecallUiRefreshTimer); persistedRecallUiRefreshTimer = null; } const summary = refreshPersistedRecallMessageUi(); const shouldRetry = (summary.status === "missing_chat_root" || summary.status === "waiting_dom" || summary.status === "missing_message_anchor") && attemptIndex < retryDelays.length - 1; if (!shouldRetry) { clearPersistedRecallMessageUiObserver(); return; } armPersistedRecallMessageUiObserver(sessionId, runAttempt); attemptIndex += 1; persistedRecallUiRefreshTimer = setTimeout(runAttempt, retryDelays[attemptIndex]); }; persistedRecallUiRefreshTimer = setTimeout(runAttempt, retryDelays[attemptIndex]); } function cleanupPersistedRecallMessageUi() { clearTimeout(persistedRecallUiRefreshTimer); persistedRecallUiRefreshTimer = null; clearPersistedRecallMessageUiObserver(); const chatRoot = document.getElementById("chat"); if (!chatRoot?.querySelectorAll) return; for (const messageElement of Array.from(chatRoot.querySelectorAll(".mes"))) { cleanupRecallArtifacts(messageElement); } } async function rerunRecallForMessage(messageIndex) { const chat = getContext()?.chat; const message = Array.isArray(chat) ? chat[messageIndex] : null; cleanupPersistedRecallMessageUi(); if (!message?.is_user) { toastr.info("仅用户消息支持重新召回"); return null; } const userMessage = normalizeRecallInputText(message.mes || ""); if (!userMessage) { toastr.info("该楼层内容为空,无法重新召回"); return null; } const result = await runRecall({ overrideUserMessage: userMessage, overrideSource: "message-floor-rerecall", overrideSourceLabel: `用户楼层 ${messageIndex}`, generationType: "history", targetUserMessageIndex: messageIndex, includeSyntheticUserMessage: false, hookName: "MESSAGE_RECALL_BADGE_RERUN", }); applyFinalRecallInjectionForGeneration({ generationType: "history", freshRecallResult: result, }); return result; } function getSendTextareaValue() { return String(document.getElementById("send_textarea")?.value ?? ""); } function scheduleSendIntentHookRetry(delayMs = 400) { return scheduleSendIntentHookRetryController( { clearTimeout, getSendIntentHookRetryTimer: () => sendIntentHookRetryTimer, installSendIntentHooks, setSendIntentHookRetryTimer: (timer) => { sendIntentHookRetryTimer = timer; }, setTimeout, }, delayMs, ); } function registerBeforeCombinePrompts(listener) { return registerBeforeCombinePromptsController( { console, eventSource, eventTypes: event_types, getEventMakeFirst: () => globalThis.eventMakeFirst, }, listener, ); } function registerGenerationAfterCommands(listener) { return registerGenerationAfterCommandsController( { console, eventSource, eventTypes: event_types, getEventMakeFirst: () => globalThis.eventMakeFirst, }, listener, ); } function installSendIntentHooks() { return installSendIntentHooksController({ console, consumeSendIntentHookCleanup: () => sendIntentHookCleanup.splice(0, sendIntentHookCleanup.length), document, getSendTextareaValue, pushSendIntentHookCleanup: (cleanup) => { sendIntentHookCleanup.push(cleanup); }, recordRecallSendIntent, scheduleSendIntentHookRetry, }); } // ==================== 设置管理 ==================== function getSettings() { const mergedSettings = { ...defaultSettings, ...(extension_settings[MODULE_NAME] || {}), }; const migrated = migrateLegacyTaskProfiles(mergedSettings); mergedSettings.taskProfilesVersion = migrated.taskProfilesVersion; mergedSettings.taskProfiles = migrated.taskProfiles; extension_settings[MODULE_NAME] = mergedSettings; return mergedSettings; } function initializeHostCapabilityBridge(options = {}) { try { initializeHostAdapter({ getContext, ...options, }); } catch (error) { console.warn("[ST-BME] 宿主桥接初始化失败:", error); } return getHostCapabilityStatus(); } function buildHostCapabilityErrorStatus(error) { const snapshot = { available: false, mode: "error", fallbackReason: error instanceof Error ? error.message : String(error || "未知错误"), versionHints: { stateSemantics: HOST_ADAPTER_STATE_SEMANTICS, refreshMode: "manual-rebuild", }, stateSemantics: HOST_ADAPTER_STATE_SEMANTICS, refreshMode: "manual-rebuild", snapshotRevision: -1, snapshotCreatedAt: "", }; recordHostCapabilitySnapshot(snapshot); return snapshot; } export function getHostCapabilityStatus(options = {}) { const normalizedOptions = options && typeof options === "object" ? { ...options } : {}; const shouldRefresh = normalizedOptions.refresh === true; delete normalizedOptions.refresh; try { const snapshot = shouldRefresh ? refreshHostCapabilitySnapshot(normalizedOptions) : getHostCapabilitySnapshot(); recordHostCapabilitySnapshot(snapshot); return snapshot; } catch (error) { console.warn("[ST-BME] 读取宿主桥接状态失败:", error); return buildHostCapabilityErrorStatus(error); } } export function refreshHostCapabilityStatus(options = {}) { return getHostCapabilityStatus({ ...options, refresh: true, }); } export function getHostCapability(name, options = {}) { const normalizedName = String(name || "").trim(); if (!normalizedName) return null; try { return readHostCapability(normalizedName, options) || null; } catch (error) { console.warn("[ST-BME] 读取宿主桥接能力失败:", error); return getHostCapabilityStatus(options)?.[normalizedName] || null; } } export function getPanelRuntimeDebugSnapshot(options = {}) { const shouldRefreshHost = options?.refreshHost === true; const hostCapabilities = shouldRefreshHost ? refreshHostCapabilityStatus() : getHostCapabilityStatus(); return { hostCapabilities, runtimeDebug: readRuntimeDebugSnapshot(), }; } function getSchema() { const settings = getSettings(); const schema = settings.nodeTypeSchema || DEFAULT_NODE_SCHEMA; const validation = validateSchema(schema); if (!validation.valid) { console.warn("[ST-BME] Schema 非法,回退到默认 Schema:", validation.errors); return DEFAULT_NODE_SCHEMA; } return schema; } function getConfiguredTimeoutMs(settings = getSettings()) { return typeof resolveConfiguredTimeoutMs === "function" ? resolveConfiguredTimeoutMs(settings, LOCAL_VECTOR_TIMEOUT_MS) : (() => { const timeoutMs = Number(settings?.timeoutMs); return Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : LOCAL_VECTOR_TIMEOUT_MS; })(); } function getEmbeddingConfig(mode = null) { const settings = getSettings(); return getVectorConfigFromSettings( mode ? { ...settings, embeddingTransportMode: mode } : settings, ); } function normalizeChatIdCandidate(value = "") { return String(value ?? "").trim(); } function readGlobalCurrentChatId() { try { return normalizeChatIdCandidate( globalThis.SillyTavern?.getCurrentChatId?.() || globalThis.getCurrentChatId?.() || "", ); } catch { return ""; } } function hasLikelySelectedChatContext(context = getContext()) { if (!context || typeof context !== "object") { return false; } const hasMeaningfulChatMetadata = context.chatMetadata && typeof context.chatMetadata === "object" && !Array.isArray(context.chatMetadata) && Object.keys(context.chatMetadata).length > 0; const hasChatMessages = Array.isArray(context.chat) && context.chat.length > 0; const hasCharacterId = context.characterId !== undefined && context.characterId !== null && String(context.characterId).trim() !== ""; const hasGroupId = context.groupId !== undefined && context.groupId !== null && String(context.groupId).trim() !== ""; return ( hasMeaningfulChatMetadata || hasChatMessages || hasCharacterId || hasGroupId ); } function hasHostMetadataReadySignal(metadata = {}) { if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) { return false; } if (normalizeChatIdCandidate(metadata.integrity)) { return true; } const chatIdentityCandidates = [ metadata.chat_id, metadata.chatId, metadata.session_id, metadata.sessionId, ]; if ( chatIdentityCandidates.some((candidate) => Boolean(normalizeChatIdCandidate(candidate)), ) ) { return true; } return false; } function isHostChatMetadataReady(context = getContext()) { if ( !context?.chatMetadata || typeof context.chatMetadata !== "object" || Array.isArray(context.chatMetadata) ) { return false; } const metadata = context.chatMetadata; // 仅接受宿主“强信号”,避免把中间态/占位 metadata 误判为 ready。 if (hasHostMetadataReadySignal(metadata)) return true; return false; } function resolveCurrentChatIdentity(context = getContext()) { const candidates = [ context?.chatId, context?.getCurrentChatId?.(), readGlobalCurrentChatId(), context?.chatMetadata?.chat_id, context?.chatMetadata?.chatId, context?.chatMetadata?.session_id, context?.chatMetadata?.sessionId, ]; const chatId = candidates .map((candidate) => normalizeChatIdCandidate(candidate)) .find(Boolean) || ""; return { chatId, hasLikelySelectedChat: hasLikelySelectedChatContext(context), }; } function getCurrentChatId(context = getContext()) { return resolveCurrentChatIdentity(context).chatId; } function buildBmeSyncRuntimeOptions(extra = {}) { return { getDb: async (chatId) => { const manager = ensureBmeChatManager(); if (!manager) { throw new Error("BmeChatManager 不可用"); } return await manager.getCurrentDb(chatId); }, getCurrentChatId: () => getCurrentChatId(), getRequestHeaders, ...extra, }; } async function syncIndexedDbMetaToPersistenceState( chatId, { syncState = "idle", lastSyncError = "" } = {}, ) { const normalizedChatId = normalizeChatIdCandidate(chatId); if (!normalizedChatId) return null; try { const manager = ensureBmeChatManager(); if (!manager) return null; const db = await manager.getCurrentDb(normalizedChatId); const [revision, lastSyncUploadedAt, lastSyncDownloadedAt, lastSyncedRevision] = await Promise.all([ db.getRevision(), db.getMeta("lastSyncUploadedAt", 0), db.getMeta("lastSyncDownloadedAt", 0), db.getMeta("lastSyncedRevision", 0), ]); const patch = { storagePrimary: "indexeddb", storageMode: "indexeddb", indexedDbRevision: normalizeIndexedDbRevision(revision), syncState: normalizeGraphSyncState(syncState), lastSyncUploadedAt: Number(lastSyncUploadedAt) || 0, lastSyncDownloadedAt: Number(lastSyncDownloadedAt) || 0, lastSyncedRevision: Number(lastSyncedRevision) || 0, lastSyncError: String(lastSyncError || ""), }; updateGraphPersistenceState(patch); return patch; } catch (error) { console.warn("[ST-BME] 读取 IndexedDB 同步元数据失败:", error); updateGraphPersistenceState({ syncState: "error", lastSyncError: error?.message || String(error), }); return null; } } async function runBmeAutoSyncForChat(source = "unknown", chatId = "") { const normalizedChatId = String(chatId || "").trim(); if (!normalizedChatId) return { synced: false, reason: "missing-chat-id" }; updateGraphPersistenceState({ syncState: "syncing", lastSyncError: "", }); try { const syncResult = await autoSyncOnChatChange( normalizedChatId, buildBmeSyncRuntimeOptions({ trigger: source, reason: String(source || "chat-change"), }), ); await syncIndexedDbMetaToPersistenceState(normalizedChatId, { syncState: syncResult?.synced ? "idle" : "warning", lastSyncError: syncResult?.error || "", }); return syncResult; } catch (error) { await syncIndexedDbMetaToPersistenceState(normalizedChatId, { syncState: "error", lastSyncError: error?.message || String(error), }); throw error; } } function ensureBmeChatManager() { if (typeof BmeChatManager !== "function") { if (!bmeChatManagerUnavailableWarned) { console.warn("[ST-BME] BmeChatManager 不可用,IndexedDB 能力暂时停用"); bmeChatManagerUnavailableWarned = true; } return null; } if (!bmeChatManager) { bmeChatManager = new BmeChatManager(); } return bmeChatManager; } function scheduleBmeIndexedDbTask(task) { const scheduler = typeof globalThis.queueMicrotask === "function" ? globalThis.queueMicrotask.bind(globalThis) : (callback) => setTimeout(callback, 0); scheduler(() => { Promise.resolve() .then(task) .catch((error) => { console.warn("[ST-BME] IndexedDB 后台任务失败:", error); }); }); } async function syncBmeChatManagerWithCurrentChat( source = "unknown", context = getContext(), ) { const manager = ensureBmeChatManager(); if (!manager) { return { chatId: "", opened: false, skipped: true, reason: "manager-unavailable", }; } const chatId = getCurrentChatId(context); if (!chatId) { await manager.closeCurrent(); console.debug("[ST-BME] IndexedDB 会话已关闭(无活动聊天)", { source, }); return { chatId: "", opened: false, skipped: false, }; } const db = await manager.switchChat(chatId); console.debug("[ST-BME] IndexedDB 会话已同步", { source, chatId, }); return { chatId, opened: Boolean(db), skipped: false, }; } function scheduleBmeIndexedDbWarmup(source = "init") { scheduleBmeIndexedDbTask(async () => { await ensureDexieLoaded(); await syncBmeChatManagerWithCurrentChat(source); }); } function normalizeIndexedDbRevision(value, fallbackValue = 0) { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed < 0) { return Math.max(0, Number(fallbackValue) || 0); } return Math.floor(parsed); } function isIndexedDbSnapshotMeaningful(snapshot = null) { if (!snapshot || typeof snapshot !== "object") return false; if (Array.isArray(snapshot.nodes) && snapshot.nodes.length > 0) return true; if (Array.isArray(snapshot.edges) && snapshot.edges.length > 0) return true; if (Array.isArray(snapshot.tombstones) && snapshot.tombstones.length > 0) return true; const state = snapshot.state || {}; if (Number.isFinite(Number(state.lastProcessedFloor)) && Number(state.lastProcessedFloor) >= 0) { return true; } if (Number.isFinite(Number(state.extractionCount)) && Number(state.extractionCount) > 0) { return true; } const runtimeHistoryState = snapshot.meta?.runtimeHistoryState; if ( runtimeHistoryState && typeof runtimeHistoryState === "object" && !Array.isArray(runtimeHistoryState) ) { if ( Number.isFinite(Number(runtimeHistoryState.lastProcessedAssistantFloor)) && Number(runtimeHistoryState.lastProcessedAssistantFloor) >= 0 ) { return true; } if ( runtimeHistoryState.processedMessageHashes && typeof runtimeHistoryState.processedMessageHashes === "object" && !Array.isArray(runtimeHistoryState.processedMessageHashes) && Object.keys(runtimeHistoryState.processedMessageHashes).length > 0 ) { return true; } } return false; } function cacheIndexedDbSnapshot(chatId, snapshot = null) { const normalizedChatId = normalizeChatIdCandidate(chatId); if (!normalizedChatId || !snapshot || typeof snapshot !== "object") return; bmeIndexedDbSnapshotCacheByChatId.set(normalizedChatId, { chatId: normalizedChatId, revision: normalizeIndexedDbRevision(snapshot?.meta?.revision), snapshot, updatedAt: Date.now(), }); } function readCachedIndexedDbSnapshot(chatId) { const normalizedChatId = normalizeChatIdCandidate(chatId); if (!normalizedChatId) return null; const cacheEntry = bmeIndexedDbSnapshotCacheByChatId.get(normalizedChatId); if (!cacheEntry?.snapshot) return null; return cacheEntry.snapshot; } function readLegacyGraphFromChatMetadata(chatId, context = getContext()) { const normalizedChatId = normalizeChatIdCandidate(chatId); if (!normalizedChatId) return null; const legacyGraph = context?.chatMetadata?.[GRAPH_METADATA_KEY]; if (!legacyGraph) return null; try { const hydratedLegacyGraph = typeof legacyGraph === "string" ? deserializeGraph(legacyGraph) : legacyGraph; return cloneGraphForPersistence( normalizeGraphRuntimeState(hydratedLegacyGraph, normalizedChatId), normalizedChatId, ); } catch (error) { console.warn("[ST-BME] 读取 legacy chat_metadata 图谱失败:", error); return null; } } async function maybeMigrateLegacyGraphToIndexedDb( chatId, context = getContext(), { source = "unknown", db = null } = {}, ) { const normalizedChatId = normalizeChatIdCandidate(chatId); if (!normalizedChatId) { return { migrated: false, reason: "migration-missing-chat-id", chatId: "", }; } const inFlightMigration = bmeIndexedDbLegacyMigrationInFlightByChatId.get( normalizedChatId, ); if (inFlightMigration) { return await inFlightMigration; } const migrationTask = (async () => { try { const manager = ensureBmeChatManager(); if (!manager) { return { migrated: false, reason: "migration-manager-unavailable", chatId: normalizedChatId, }; } const targetDb = db || (await manager.getCurrentDb(normalizedChatId)); if (!targetDb) { return { migrated: false, reason: "migration-db-unavailable", chatId: normalizedChatId, }; } const contextChatId = resolveCurrentChatIdentity(context).chatId; if (contextChatId && contextChatId !== normalizedChatId) { return { migrated: false, reason: "migration-context-chat-mismatch", chatId: normalizedChatId, contextChatId, }; } const migrationCompletedAt = Number( await targetDb.getMeta("migrationCompletedAt", 0), ); if (Number.isFinite(migrationCompletedAt) && migrationCompletedAt > 0) { return { migrated: false, reason: "migration-already-completed", chatId: normalizedChatId, migrationCompletedAt, }; } const legacyGraph = readLegacyGraphFromChatMetadata(normalizedChatId, context); if (!legacyGraph) { return { migrated: false, reason: "migration-legacy-graph-missing", chatId: normalizedChatId, }; } const emptyStatus = await targetDb.isEmpty(); if (!emptyStatus?.empty) { return { migrated: false, reason: "migration-indexeddb-not-empty", chatId: normalizedChatId, emptyStatus, }; } const legacyRevision = Math.max( normalizeIndexedDbRevision(getGraphPersistedRevision(legacyGraph), 0), 1, ); const migrationResult = await targetDb.importLegacyGraph(legacyGraph, { source: "chat_metadata", revision: legacyRevision, }); if (!migrationResult?.migrated) { return { migrated: false, reason: migrationResult?.reason || "migration-skipped", chatId: normalizedChatId, migrationResult, }; } const postMigrationSnapshot = await targetDb.exportSnapshot(); cacheIndexedDbSnapshot(normalizedChatId, postMigrationSnapshot); console.debug("[ST-BME] legacy chat_metadata 图谱迁移完成", { source, chatId: normalizedChatId, revision: postMigrationSnapshot?.meta?.revision || migrationResult?.revision || 0, imported: migrationResult.imported, }); let syncResult = { synced: false, reason: "post-migration-sync-skipped", chatId: normalizedChatId, }; try { syncResult = await syncNow( normalizedChatId, buildBmeSyncRuntimeOptions({ reason: "post-migration", trigger: `${String(source || "migration")}:post-migration`, }), ); } catch (syncError) { console.warn("[ST-BME] legacy 迁移后立即同步失败:", syncError); syncResult = { synced: false, reason: "post-migration-sync-failed", chatId: normalizedChatId, error: syncError?.message || String(syncError), }; } return { migrated: true, reason: "migration-completed", chatId: normalizedChatId, migrationResult, snapshot: postMigrationSnapshot, syncResult, }; } catch (error) { console.warn("[ST-BME] legacy chat_metadata 迁移失败:", error); return { migrated: false, reason: "migration-failed", chatId: normalizedChatId, error: error?.message || String(error), }; } })().finally(() => { if ( bmeIndexedDbLegacyMigrationInFlightByChatId.get(normalizedChatId) === migrationTask ) { bmeIndexedDbLegacyMigrationInFlightByChatId.delete(normalizedChatId); } }); bmeIndexedDbLegacyMigrationInFlightByChatId.set(normalizedChatId, migrationTask); return await migrationTask; } function applyIndexedDbEmptyToRuntime( chatId, { source = "indexeddb-empty", attemptIndex = 0 } = {}, ) { const normalizedChatId = normalizeChatIdCandidate(chatId); if (!normalizedChatId) { return { success: false, loaded: false, reason: "indexeddb-missing-chat-id", chatId: "", attemptIndex, }; } currentGraph = normalizeGraphRuntimeState(createEmptyGraph(), normalizedChatId); extractionCount = 0; lastExtractedItems = []; lastRecalledItems = []; lastInjectionContent = ""; runtimeStatus = createUiStatus("待命", "当前聊天还没有图谱", "idle"); lastExtractionStatus = createUiStatus("待命", "当前聊天尚未执行提取", "idle"); lastVectorStatus = createUiStatus("待命", "当前聊天尚未执行向量任务", "idle"); lastRecallStatus = createUiStatus("待命", "当前聊天尚未建立记忆图谱", "idle"); applyGraphLoadState(GRAPH_LOAD_STATES.EMPTY_CONFIRMED, { chatId: normalizedChatId, reason: `indexeddb-empty:${String(source || "indexeddb-empty")}`, attemptIndex, revision: 0, lastPersistedRevision: 0, queuedPersistRevision: 0, queuedPersistChatId: "", pendingPersist: false, shadowSnapshotUsed: false, shadowSnapshotRevision: 0, shadowSnapshotUpdatedAt: "", shadowSnapshotReason: "", dbReady: true, writesBlocked: false, }); updateGraphPersistenceState({ storagePrimary: "indexeddb", storageMode: "indexeddb", dbReady: true, indexedDbRevision: 0, indexedDbLastError: "", dualWriteLastResult: { action: "load", source: String(source || "indexeddb-empty"), success: true, empty: true, at: Date.now(), }, }); refreshPanelLiveState(); return { success: true, loaded: false, emptyConfirmed: true, loadState: GRAPH_LOAD_STATES.EMPTY_CONFIRMED, reason: `indexeddb-empty:${String(source || "indexeddb-empty")}`, chatId: normalizedChatId, attemptIndex, }; } function applyIndexedDbSnapshotToRuntime( chatId, snapshot, { source = "indexeddb", attemptIndex = 0 } = {}, ) { const normalizedChatId = normalizeChatIdCandidate(chatId); if (!normalizedChatId || !isIndexedDbSnapshotMeaningful(snapshot)) { return { success: false, loaded: false, reason: "indexeddb-empty", chatId: normalizedChatId, attemptIndex, }; } const revision = Math.max(1, normalizeIndexedDbRevision(snapshot?.meta?.revision)); const graphFromSnapshot = buildGraphFromSnapshot(snapshot, { chatId: normalizedChatId, }); currentGraph = cloneGraphForPersistence( normalizeGraphRuntimeState(graphFromSnapshot, normalizedChatId), normalizedChatId, ); extractionCount = Number.isFinite(currentGraph?.historyState?.extractionCount) ? currentGraph.historyState.extractionCount : 0; lastExtractedItems = []; updateLastRecalledItems(currentGraph.lastRecallResult || []); lastInjectionContent = ""; runtimeStatus = createUiStatus( "待命", "已从 IndexedDB 加载聊天图谱", "idle", ); lastExtractionStatus = createUiStatus( "待命", "已从 IndexedDB 加载聊天图谱,等待下一次提取", "idle", ); lastVectorStatus = createUiStatus( "待命", currentGraph.vectorIndexState?.lastWarning || "已从 IndexedDB 加载聊天图谱,等待下一次向量任务", "idle", ); lastRecallStatus = createUiStatus( "待命", "已从 IndexedDB 加载聊天图谱,等待下一次召回", "idle", ); applyGraphLoadState(GRAPH_LOAD_STATES.LOADED, { chatId: normalizedChatId, reason: `indexeddb:${source}`, attemptIndex, revision, lastPersistedRevision: Math.max( graphPersistenceState.lastPersistedRevision || 0, revision, ), queuedPersistRevision: 0, pendingPersist: false, shadowSnapshotUsed: false, shadowSnapshotRevision: 0, shadowSnapshotUpdatedAt: "", shadowSnapshotReason: "", writesBlocked: false, }); updateGraphPersistenceState({ storagePrimary: "indexeddb", storageMode: "indexeddb", dbReady: true, indexedDbRevision: revision, metadataIntegrity: getChatMetadataIntegrity(getContext()) || graphPersistenceState.metadataIntegrity, indexedDbLastError: "", lastSyncError: "", dualWriteLastResult: { action: "load", source: String(source || "indexeddb"), revision, at: Date.now(), }, }); removeGraphShadowSnapshot(normalizedChatId); refreshPanelLiveState(); console.debug("[ST-BME] 已从 IndexedDB 加载图谱", { chatId: normalizedChatId, source, revision, ...getGraphStats(currentGraph), }); return { success: true, loaded: true, loadState: GRAPH_LOAD_STATES.LOADED, reason: `indexeddb:${source}`, chatId: normalizedChatId, attemptIndex, shadowSnapshotUsed: false, revision, }; } async function loadGraphFromIndexedDb( chatId, { source = "indexeddb-probe", attemptIndex = 0, allowOverride = false, applyEmptyState = false, } = {}, ) { const normalizedChatId = normalizeChatIdCandidate(chatId); if (!normalizedChatId) { return { success: false, loaded: false, reason: "indexeddb-missing-chat-id", chatId: "", attemptIndex, }; } try { const manager = ensureBmeChatManager(); if (!manager) { return { success: false, loaded: false, reason: "indexeddb-manager-unavailable", chatId: normalizedChatId, attemptIndex, }; } const db = await manager.getCurrentDb(normalizedChatId); const migrationResult = await maybeMigrateLegacyGraphToIndexedDb( normalizedChatId, getContext(), { source, db, }, ); if (migrationResult?.migrated) { const migratedRevision = normalizeIndexedDbRevision( migrationResult?.snapshot?.meta?.revision || migrationResult?.migrationResult?.revision, ); updateGraphPersistenceState({ storagePrimary: "indexeddb", storageMode: "indexeddb", indexedDbRevision: migratedRevision, indexedDbLastError: "", lastSyncError: "", dualWriteLastResult: { action: "migration", source: "chat_metadata", success: true, chatId: normalizedChatId, revision: migratedRevision, reason: migrationResult?.reason || "migration-completed", at: Date.now(), syncResult: cloneRuntimeDebugValue(migrationResult?.syncResult, null), }, }); } else if (migrationResult?.reason === "migration-failed") { updateGraphPersistenceState({ indexedDbLastError: String(migrationResult?.error || "migration-failed"), dualWriteLastResult: { action: "migration", source: "chat_metadata", success: false, error: String(migrationResult?.error || "migration-failed"), at: Date.now(), }, }); } const snapshot = migrationResult?.snapshot || (await db.exportSnapshot()); cacheIndexedDbSnapshot(normalizedChatId, snapshot); if (!isIndexedDbSnapshotMeaningful(snapshot)) { if ( applyEmptyState && getCurrentChatId() === normalizedChatId ) { return applyIndexedDbEmptyToRuntime(normalizedChatId, { source, attemptIndex, }); } return { success: false, loaded: false, reason: "indexeddb-empty", chatId: normalizedChatId, attemptIndex, }; } const snapshotRevision = normalizeIndexedDbRevision(snapshot?.meta?.revision); const shouldAllowOverride = allowOverride || BME_INDEXEDDB_FALLBACK_LOAD_STATE_SET.has(graphPersistenceState.loadState) || graphPersistenceState.storagePrimary === "indexeddb" || snapshotRevision >= normalizeIndexedDbRevision(graphPersistenceState.revision); if (!shouldAllowOverride) { return { success: false, loaded: false, reason: "indexeddb-stale", chatId: normalizedChatId, attemptIndex, revision: snapshotRevision, }; } if (getCurrentChatId() !== normalizedChatId) { return { success: false, loaded: false, reason: "indexeddb-chat-switched", chatId: normalizedChatId, attemptIndex, revision: snapshotRevision, }; } return applyIndexedDbSnapshotToRuntime(normalizedChatId, snapshot, { source, attemptIndex, }); } catch (error) { console.warn("[ST-BME] IndexedDB 读取失败,回退 metadata:", error); updateGraphPersistenceState({ indexedDbLastError: error?.message || String(error), dualWriteLastResult: { action: "load", source: String(source || "indexeddb"), success: false, error: error?.message || String(error), at: Date.now(), }, }); return { success: false, loaded: false, reason: "indexeddb-read-failed", chatId: normalizedChatId, attemptIndex, error, }; } } function scheduleIndexedDbGraphProbe(chatId, options = {}) { const normalizedChatId = normalizeChatIdCandidate(chatId); if (!normalizedChatId || bmeIndexedDbLoadInFlightByChatId.has(normalizedChatId)) { return; } scheduleBmeIndexedDbTask(() => { const loadPromise = loadGraphFromIndexedDb(normalizedChatId, options) .catch((error) => { console.warn("[ST-BME] IndexedDB 后台加载失败:", error); }) .finally(() => { if (bmeIndexedDbLoadInFlightByChatId.get(normalizedChatId) === loadPromise) { bmeIndexedDbLoadInFlightByChatId.delete(normalizedChatId); } }); bmeIndexedDbLoadInFlightByChatId.set(normalizedChatId, loadPromise); return loadPromise; }); } function resolveInjectionPromptType(settings = {}) { const normalized = String(settings?.injectPosition || "atDepth") .trim() .toLowerCase(); switch (normalized) { case "none": return extension_prompt_types.NONE; case "beforeprompt": case "before_prompt": case "before-prompt": return extension_prompt_types.BEFORE_PROMPT; case "inprompt": case "in_prompt": case "in-prompt": return extension_prompt_types.IN_PROMPT; case "atdepth": case "at_depth": case "inchat": case "in_chat": case "chat": default: return extension_prompt_types.IN_CHAT; } } function resolveInjectionPromptRole(settings = {}) { switch (Number(settings?.injectRole)) { case 1: return extension_prompt_roles.USER; case 2: return extension_prompt_roles.ASSISTANT; default: return extension_prompt_roles.SYSTEM; } } function applyModuleInjectionPrompt(content = "", settings = getSettings()) { const position = resolveInjectionPromptType(settings); const depth = position === extension_prompt_types.IN_CHAT ? clampInt(settings?.injectDepth, 9999, 0, 9999) : 0; const role = resolveInjectionPromptRole(settings); const adapter = getHostAdapter?.(); const injectionHost = adapter?.injection; if ( typeof injectionHost?.setExtensionPrompt === "function" && injectionHost.setExtensionPrompt( MODULE_NAME, content, position, depth, false, role, ) ) { return { applied: true, source: "host-adapter", mode: injectionHost.readInjectionSupport?.()?.mode || "", position, depth, role, }; } const context = getContext(); if (typeof context?.setExtensionPrompt === "function") { context.setExtensionPrompt( MODULE_NAME, content, position, depth, false, role, ); return { applied: true, source: "context", mode: "legacy-context-setter", position, depth, role, }; } return { applied: false, source: "unavailable", mode: "unavailable", position, depth, role, }; } function ensureCurrentGraphRuntimeState() { if (!currentGraph) { currentGraph = createEmptyGraph(); } currentGraph = normalizeGraphRuntimeState(currentGraph, getCurrentChatId()); return currentGraph; } function clearPendingGraphLoadRetry({ resetChatId = true } = {}) { if (pendingGraphLoadRetryTimer) { clearTimeout(pendingGraphLoadRetryTimer); pendingGraphLoadRetryTimer = null; } if (resetChatId) { pendingGraphLoadRetryChatId = ""; } } function isGraphLoadRetryPending(chatId = getCurrentChatId()) { const normalizedChatId = String(chatId || ""); return ( Boolean(normalizedChatId) && pendingGraphLoadRetryChatId === normalizedChatId ); } function isGraphEffectivelyEmpty(graph) { if (!graph || typeof graph !== "object") { return true; } const stats = getGraphStats(graph); if ((stats.totalNodes || 0) > 0 || (stats.totalEdges || 0) > 0) { return false; } if (Number.isFinite(stats.lastProcessedSeq) && stats.lastProcessedSeq >= 0) { return false; } if (Array.isArray(graph.batchJournal) && graph.batchJournal.length > 0) { return false; } if ( graph.lastRecallResult && (!Array.isArray(graph.lastRecallResult) || graph.lastRecallResult.length > 0) ) { return false; } if ( Object.keys(graph?.historyState?.processedMessageHashes || {}).length > 0 ) { return false; } if (Object.keys(graph?.vectorIndexState?.hashToNodeId || {}).length > 0) { return false; } return true; } function buildGraphPersistResult({ saved = false, queued = false, blocked = false, reason = "", loadState = graphPersistenceState.loadState, revision = graphPersistenceState.revision, saveMode = graphPersistenceState.lastPersistMode, } = {}) { return { saved, queued, blocked, reason: String(reason || ""), loadState, revision: Number.isFinite(revision) ? revision : 0, saveMode: String(saveMode || ""), }; } function maybeCaptureGraphShadowSnapshot(reason = "runtime-shadow") { const chatId = graphPersistenceState.chatId || getCurrentChatId(); if (!chatId || !currentGraph) return false; const hasMeaningfulGraphData = !isGraphEffectivelyEmpty(currentGraph) || graphPersistenceState.shadowSnapshotUsed || graphPersistenceState.lastPersistedRevision > 0; if (!hasMeaningfulGraphData) return false; return writeGraphShadowSnapshot(chatId, currentGraph, { revision: graphPersistenceState.revision, reason, }); } function persistGraphToChatMetadata( context = getContext(), { reason = "graph-persist", revision = graphPersistenceState.revision, immediate = false, } = {}, ) { if (!context || !currentGraph) { return buildGraphPersistResult({ saved: false, blocked: true, reason: "missing-context-or-graph", revision, }); } const chatId = getCurrentChatId(context); if (!chatId) { return buildGraphPersistResult({ saved: false, blocked: true, reason: "missing-chat-id", revision, }); } const nextIntegrity = getChatMetadataIntegrity(context); const persistedGraph = cloneGraphForPersistence(currentGraph, chatId); stampGraphPersistenceMeta(persistedGraph, { revision, reason, chatId, integrity: nextIntegrity, }); stampGraphPersistenceMeta(currentGraph, { revision, reason, chatId, integrity: nextIntegrity, }); writeChatMetadataPatch(context, { [GRAPH_METADATA_KEY]: persistedGraph, }); const saveMode = triggerChatMetadataSave(context, { immediate }); applyGraphLoadState(graphPersistenceState.loadState, { chatId, reason: graphPersistenceState.reason, attemptIndex: graphPersistenceState.attemptIndex, shadowSnapshotUsed: false, shadowSnapshotRevision: 0, shadowSnapshotUpdatedAt: "", shadowSnapshotReason: "", revision, lastPersistedRevision: revision, queuedPersistRevision: 0, queuedPersistChatId: "", pendingPersist: false, writesBlocked: false, }); removeGraphShadowSnapshot(chatId); updateGraphPersistenceState({ lastPersistReason: String(reason || ""), lastPersistMode: saveMode, metadataIntegrity: String(nextIntegrity || ""), storagePrimary: "metadata", storageMode: "metadata", indexedDbLastError: "", queuedPersistChatId: "", queuedPersistMode: "", queuedPersistRotateIntegrity: false, queuedPersistReason: "", }); return buildGraphPersistResult({ saved: true, reason, loadState: graphPersistenceState.loadState, revision, saveMode, }); } function queueGraphPersist( reason = "graph-persist-blocked", revision = graphPersistenceState.revision, { immediate = true } = {}, ) { const queuedChatId = graphPersistenceState.chatId || getCurrentChatId(); maybeCaptureGraphShadowSnapshot(reason); updateGraphPersistenceState({ queuedPersistRevision: Math.max( graphPersistenceState.queuedPersistRevision || 0, revision || 0, ), queuedPersistChatId: String(queuedChatId || ""), queuedPersistMode: immediate ? "immediate" : "debounced", queuedPersistRotateIntegrity: false, queuedPersistReason: String(reason || ""), pendingPersist: true, writesBlocked: true, lastPersistReason: String(reason || ""), }); return buildGraphPersistResult({ queued: true, blocked: true, reason, loadState: graphPersistenceState.loadState, revision, saveMode: immediate ? "immediate" : "debounced", }); } function maybeFlushQueuedGraphPersist(reason = "queued-graph-persist") { if (!currentGraph || !isGraphMetadataWriteAllowed()) { return buildGraphPersistResult({ queued: graphPersistenceState.pendingPersist, blocked: !isGraphMetadataWriteAllowed(), reason: isGraphMetadataWriteAllowed() ? "missing-current-graph" : "write-protected", }); } if ( !graphPersistenceState.pendingPersist && graphPersistenceState.queuedPersistRevision <= graphPersistenceState.lastPersistedRevision ) { return buildGraphPersistResult({ saved: false, reason: "no-queued-persist", }); } const activeChatId = getCurrentChatId(); const queuedChatId = String(graphPersistenceState.queuedPersistChatId || ""); if (queuedChatId && activeChatId && queuedChatId !== activeChatId) { return buildGraphPersistResult({ saved: false, queued: graphPersistenceState.pendingPersist, blocked: true, reason: "queued-chat-mismatch", revision: graphPersistenceState.queuedPersistRevision, saveMode: graphPersistenceState.queuedPersistMode, }); } const targetRevision = Math.max( graphPersistenceState.revision || 0, graphPersistenceState.queuedPersistRevision || 0, ); if (targetRevision > (graphPersistenceState.revision || 0)) { updateGraphPersistenceState({ revision: targetRevision, }); } return persistGraphToChatMetadata(getContext(), { reason, revision: targetRevision, immediate: graphPersistenceState.queuedPersistMode !== "debounced", }); } function scheduleGraphLoadRetry( chatId, reason = "metadata-pending", attemptIndex = 0, { allowPendingChat = false, expectedChatId = "" } = {}, ) { const normalizedChatId = String(chatId || ""); const normalizedExpectedChatId = String( expectedChatId || normalizedChatId || "", ); const delayMs = GRAPH_LOAD_RETRY_DELAYS_MS[attemptIndex]; if ((!normalizedChatId && !allowPendingChat) || !Number.isFinite(delayMs)) { clearPendingGraphLoadRetry(); return false; } clearPendingGraphLoadRetry({ resetChatId: false }); pendingGraphLoadRetryChatId = normalizedChatId || (allowPendingChat ? GRAPH_LOAD_PENDING_CHAT_ID : ""); console.debug( `[ST-BME] 图谱元数据尚未就绪,${delayMs}ms 后重试加载(chat=${normalizedChatId || "pending"},attempt=${attemptIndex + 1},reason=${reason})`, ); pendingGraphLoadRetryTimer = setTimeout(() => { pendingGraphLoadRetryTimer = null; const currentChatId = getCurrentChatId(); if ( normalizedExpectedChatId && currentChatId && currentChatId !== normalizedExpectedChatId ) { clearPendingGraphLoadRetry(); return; } if ( !allowPendingChat && normalizedChatId && currentChatId !== normalizedChatId ) { clearPendingGraphLoadRetry(); return; } loadGraphFromChat({ attemptIndex: attemptIndex + 1, expectedChatId: normalizedExpectedChatId, source: `retry:${reason}`, }); }, delayMs); return true; } function shouldSyncGraphLoadFromLiveContext( context = getContext(), { force = false } = {}, ) { if (force) return true; const chatIdentity = resolveCurrentChatIdentity(context); const liveChatId = chatIdentity.chatId; const stateChatId = normalizeChatIdCandidate(graphPersistenceState.chatId); if (liveChatId !== stateChatId) return true; if (!liveChatId && graphPersistenceState.loadState !== GRAPH_LOAD_STATES.NO_CHAT) { return true; } if (liveChatId && !graphPersistenceState.dbReady) return true; return false; } function syncGraphLoadFromLiveContext(options = {}) { const { source = "live-context-sync", force = false } = options; const context = getContext(); if (!shouldSyncGraphLoadFromLiveContext(context, { force })) { return { synced: false, reason: "no-sync-needed", loadState: graphPersistenceState.loadState, chatId: graphPersistenceState.chatId, }; } const chatId = resolveCurrentChatIdentity(context).chatId; if (!chatId) { const result = loadGraphFromChat({ source, attemptIndex: 0, }); return { synced: true, ...result, }; } const cachedSnapshot = readCachedIndexedDbSnapshot(chatId); if (isIndexedDbSnapshotMeaningful(cachedSnapshot)) { const result = applyIndexedDbSnapshotToRuntime(chatId, cachedSnapshot, { source: `${source}:indexeddb-cache`, attemptIndex: 0, }); return { synced: true, ...result, }; } applyGraphLoadState(GRAPH_LOAD_STATES.LOADING, { chatId, reason: `indexeddb-sync:${String(source || "live-context-sync")}`, attemptIndex: 0, dbReady: false, writesBlocked: true, }); updateGraphPersistenceState({ storagePrimary: "indexeddb", storageMode: "indexeddb", dbReady: false, indexedDbLastError: "", }); scheduleIndexedDbGraphProbe(chatId, { source: `${source}:indexeddb-probe`, allowOverride: true, applyEmptyState: true, }); refreshPanelLiveState(); return { synced: true, success: false, loaded: false, loadState: GRAPH_LOAD_STATES.LOADING, reason: "indexeddb-loading", chatId, attemptIndex: 0, }; } function scheduleStartupGraphReconciliation() { for (const delayMs of GRAPH_STARTUP_RECONCILE_DELAYS_MS) { setTimeout(() => { syncGraphLoadFromLiveContext({ source: `startup-reconcile:${delayMs}`, }); }, delayMs); } } function clearInjectionState(options = {}) { const { preserveRecallStatus = false, preserveRuntimeStatus = preserveRecallStatus, } = options; lastInjectionContent = ""; lastRecalledItems = []; if (!preserveRecallStatus) { lastRecallStatus = createUiStatus("待命", "当前无有效注入内容", "idle"); } if (!preserveRuntimeStatus) { runtimeStatus = createUiStatus("待命", "当前无有效注入内容", "idle"); } recordInjectionSnapshot("recall", { injectionText: "", selectedNodeIds: [], retrievalMeta: {}, llmMeta: {}, transport: { applied: false, source: "cleared", mode: "cleared", }, }); if (!isRecalling && !preserveRecallStatus) { dismissStageNotice("recall"); } try { applyModuleInjectionPrompt("", getSettings()); } catch (error) { console.warn("[ST-BME] 清理旧注入失败:", error); } refreshPanelLiveState(); } function refreshPanelLiveState() { refreshPanelLiveStateController({ getPanelModule: () => _panelModule, }); } function notifyStatusToast(key, kind, message, title = "ST-BME") { const now = Date.now(); if (now - (lastStatusToastAt[key] || 0) < STATUS_TOAST_THROTTLE_MS) return; lastStatusToastAt[key] = now; const method = typeof toastr?.[kind] === "function" ? kind : "info"; toastr[method](message, title, { timeOut: 2200 }); } function setRuntimeStatus(text, meta, level = "info") { runtimeStatus = createUiStatus(text, meta, level); refreshPanelLiveState(); // 同步悬浮球状态 const fabStatus = level === "info" ? "idle" : level; _panelModule?.updateFloatingBallStatus?.(fabStatus, text || "BME 记忆图谱"); } function setLastExtractionStatus( text, meta, level = "info", { syncRuntime = true, toastKind = "", toastTitle = "ST-BME 提取", noticeMarquee = false, } = {}, ) { lastExtractionStatus = createUiStatus(text, meta, level); if (syncRuntime) { setRuntimeStatus(text, meta, level); } else { refreshPanelLiveState(); } updateStageNotice("extraction", text, meta, level, { title: toastTitle, noticeMarquee, }); if (toastKind) { notifyStatusToast( `extract:${toastKind}`, toastKind, meta || text, toastTitle, ); } } function setLastVectorStatus( text, meta, level = "info", { syncRuntime = false, toastKind = "", toastTitle = "ST-BME 向量" } = {}, ) { lastVectorStatus = createUiStatus(text, meta, level); if (syncRuntime) { setRuntimeStatus(text, meta, level); } else { refreshPanelLiveState(); } updateStageNotice("vector", text, meta, level, { title: toastTitle, }); if (toastKind) { notifyStatusToast( `vector:${toastKind}`, toastKind, meta || text, toastTitle, ); } } function setLastRecallStatus( text, meta, level = "info", { syncRuntime = true, toastKind = "", toastTitle = "ST-BME 召回", noticeMarquee = false, } = {}, ) { lastRecallStatus = createUiStatus(text, meta, level); if (syncRuntime) { setRuntimeStatus(text, meta, level); } else { refreshPanelLiveState(); } updateStageNotice("recall", text, meta, level, { title: toastTitle, noticeMarquee, }); if (toastKind) { notifyStatusToast( `recall:${toastKind}`, toastKind, meta || text, toastTitle, ); } } function notifyExtractionIssue(message, title = "ST-BME 提取提示") { setLastExtractionStatus("提取失败", message, "warning", { syncRuntime: true, }); const now = Date.now(); if (now - lastExtractionWarningAt < 5000) return; lastExtractionWarningAt = now; toastr.warning(message, title, { timeOut: 4500 }); } async function fetchLocalWithTimeout( url, options = {}, timeoutMs = getConfiguredTimeoutMs(), ) { const controller = new AbortController(); const timeout = setTimeout( () => controller.abort( new DOMException( `本地请求超时 (${Math.round(timeoutMs / 1000)}s)`, "AbortError", ), ), timeoutMs, ); let signal = controller.signal; if (options.signal) { if ( typeof AbortSignal !== "undefined" && typeof AbortSignal.any === "function" ) { signal = AbortSignal.any([options.signal, controller.signal]); } else { signal = controller.signal; options.signal.addEventListener( "abort", () => controller.abort(options.signal.reason), { once: true, }, ); } } try { return await fetch(url, { ...options, signal, }); } finally { clearTimeout(timeout); } } function snapshotRuntimeUiState() { return { extractionCount, lastInjectionContent, lastExtractedItems: Array.isArray(lastExtractedItems) ? lastExtractedItems.map((item) => ({ ...item })) : [], lastRecalledItems: Array.isArray(lastRecalledItems) ? lastRecalledItems.map((item) => ({ ...item })) : [], runtimeStatus: { ...(runtimeStatus || {}) }, lastExtractionStatus: { ...(lastExtractionStatus || {}) }, lastVectorStatus: { ...(lastVectorStatus || {}) }, lastRecallStatus: { ...(lastRecallStatus || {}) }, graphPersistenceState: getGraphPersistenceLiveState(), }; } function restoreRuntimeUiState(snapshot = {}) { extractionCount = Number.isFinite(snapshot.extractionCount) ? snapshot.extractionCount : 0; lastInjectionContent = String(snapshot.lastInjectionContent || ""); lastExtractedItems = Array.isArray(snapshot.lastExtractedItems) ? snapshot.lastExtractedItems.map((item) => ({ ...item })) : []; lastRecalledItems = Array.isArray(snapshot.lastRecalledItems) ? snapshot.lastRecalledItems.map((item) => ({ ...item })) : []; runtimeStatus = { ...createUiStatus("待命", "准备就绪", "idle"), ...(snapshot.runtimeStatus || {}), }; lastExtractionStatus = { ...createUiStatus("待命", "尚未执行提取", "idle"), ...(snapshot.lastExtractionStatus || {}), }; lastVectorStatus = { ...createUiStatus("待命", "尚未执行向量任务", "idle"), ...(snapshot.lastVectorStatus || {}), }; lastRecallStatus = { ...createUiStatus("待命", "尚未执行召回", "idle"), ...(snapshot.lastRecallStatus || {}), }; if (snapshot.graphPersistenceState) { updateGraphPersistenceState(snapshot.graphPersistenceState); } refreshPanelLiveState(); } function getLastProcessedAssistantFloor() { const historyFloor = Number(currentGraph?.historyState?.lastProcessedAssistantFloor); if (Number.isFinite(historyFloor)) { return historyFloor; } const legacySeq = Number(currentGraph?.lastProcessedSeq); if (Number.isFinite(legacySeq)) return legacySeq; return -1; } async function recordGraphMutation({ beforeSnapshot, processedRange = null, artifactTags = [], syncRange = null, signal = undefined, extractionCountBefore = extractionCount, } = {}) { ensureCurrentGraphRuntimeState(); const vectorSync = await syncVectorState({ force: true, purge: isBackendVectorConfig(getEmbeddingConfig()) && !syncRange, range: syncRange, signal, }); const afterSnapshot = cloneGraphSnapshot(currentGraph); const effectiveRange = Array.isArray(processedRange) ? processedRange : [getLastProcessedAssistantFloor(), getLastProcessedAssistantFloor()]; appendBatchJournal( currentGraph, createBatchJournalEntry(beforeSnapshot, afterSnapshot, { processedRange: effectiveRange, postProcessArtifacts: computePostProcessArtifacts( beforeSnapshot, afterSnapshot, artifactTags, ), vectorHashesInserted: vectorSync?.insertedHashes || [], extractionCountBefore, }), ); saveGraphToChat({ reason: "record-graph-mutation" }); return vectorSync; } function markVectorStateDirty(reason = "向量状态已标记为待重建") { if (!currentGraph) return; ensureCurrentGraphRuntimeState(); currentGraph.vectorIndexState.dirty = true; currentGraph.vectorIndexState.lastWarning = reason; } function updateProcessedHistorySnapshot(chat, lastProcessedAssistantFloor) { ensureCurrentGraphRuntimeState(); currentGraph.historyState.lastProcessedAssistantFloor = lastProcessedAssistantFloor; currentGraph.historyState.processedMessageHashes = snapshotProcessedMessageHashes(chat, lastProcessedAssistantFloor); currentGraph.lastProcessedSeq = lastProcessedAssistantFloor; } function shouldAdvanceProcessedHistory(batchStatus) { if (!batchStatus || typeof batchStatus !== "object") return false; return ( batchStatus.completed === true && batchStatus.outcome === "success" && batchStatus.consistency === "strong" ); } function computePostProcessArtifacts( beforeSnapshot, afterSnapshot, extraTags = [], ) { const beforeNodeIds = new Set( (beforeSnapshot?.nodes || []).map((node) => node.id), ); const afterNodes = afterSnapshot?.nodes || []; const tags = new Set(extraTags.filter(Boolean)); for (const node of afterNodes) { if (!beforeNodeIds.has(node.id)) { if (node.type === "synopsis") tags.add("synopsis"); if (node.type === "reflection") tags.add("reflection"); if (node.level > 0) tags.add("compression"); } } const beforeNodes = new Map( (beforeSnapshot?.nodes || []).map((node) => [node.id, node]), ); for (const node of afterNodes) { const beforeNode = beforeNodes.get(node.id); if (!beforeNode) continue; if (!beforeNode.archived && node.archived) { tags.add(node.level > 0 ? "compression-archive" : "sleep/archive"); } } return [...tags]; } async function syncVectorState({ force = false, purge = false, range = null, signal = undefined, } = {}) { ensureCurrentGraphRuntimeState(); const scopeLabel = range && Number.isFinite(range.start) && Number.isFinite(range.end) ? `范围 ${Math.min(range.start, range.end)}-${Math.max(range.start, range.end)}` : "当前聊天"; setLastVectorStatus( "向量处理中", `${scopeLabel} · ${force ? "强制同步" : "增量同步"}`, "running", { syncRuntime: true }, ); const config = getEmbeddingConfig(); const validation = validateVectorConfig(config); if (!validation.valid) { currentGraph.vectorIndexState.lastWarning = validation.error; currentGraph.vectorIndexState.dirty = true; setLastVectorStatus("向量不可用", validation.error, "warning", { syncRuntime: true, }); return { insertedHashes: [], stats: getVectorIndexStats(currentGraph), error: validation.error, }; } try { const result = await syncGraphVectorIndex(currentGraph, config, { chatId: getCurrentChatId(), force, purge, range, signal, }); setLastVectorStatus( "向量完成", `${scopeLabel} · indexed ${result.stats?.indexed ?? 0} · pending ${result.stats?.pending ?? 0}`, "success", { syncRuntime: true }, ); return result; } catch (error) { if (isAbortError(error)) { setLastVectorStatus("向量已终止", scopeLabel, "warning", { syncRuntime: true, }); return { insertedHashes: [], stats: getVectorIndexStats(currentGraph), error: error?.message || "向量任务已终止", aborted: true, }; } const message = error?.message || String(error) || "向量同步失败"; markVectorStateDirty(message); console.error("[ST-BME] 向量同步失败:", error); setLastVectorStatus("向量失败", message, "error", { syncRuntime: true, toastKind: "error", }); return { insertedHashes: [], stats: getVectorIndexStats(currentGraph), error: message, }; } } async function ensureVectorReadyIfNeeded( reason = "vector-ready-check", signal = undefined, ) { if (!currentGraph) return; ensureCurrentGraphRuntimeState(); if (!isGraphMetadataWriteAllowed()) return; if (!currentGraph.vectorIndexState?.dirty) return; const config = getEmbeddingConfig(); const validation = validateVectorConfig(config); if (!validation.valid) return; const result = await syncVectorState({ force: true, purge: isBackendVectorConfig(config), signal, }); if (result?.error) { currentGraph.vectorIndexState.lastWarning = result.error; saveGraphToChat({ reason: "vector-auto-repair-failed" }); console.warn("[ST-BME] 向量状态自动修复失败:", reason, result.error); return result; } currentGraph.vectorIndexState.lastWarning = ""; saveGraphToChat({ reason: "vector-auto-repair-succeeded" }); console.log("[ST-BME] 向量状态已自动修复:", reason, result.stats); return result; } async function resetVectorStateForConfigChange(reason = "向量配置已变更") { if (!currentGraph) return; ensureCurrentGraphRuntimeState(); markVectorStateDirty(reason); currentGraph.vectorIndexState.hashToNodeId = {}; currentGraph.vectorIndexState.nodeToHash = {}; currentGraph.vectorIndexState.lastStats = { total: 0, indexed: 0, stale: 0, pending: 0, }; saveGraphToChat({ reason: "vector-config-reset" }); } function getPersistedSettingsSnapshot(settings = getSettings()) { const persisted = {}; for (const key of Object.keys(defaultSettings)) { persisted[key] = settings[key]; } return persisted; } function mergePersistedSettings(loaded = {}) { const merged = { ...defaultSettings }; for (const key of Object.keys(defaultSettings)) { if (Object.prototype.hasOwnProperty.call(loaded, key)) { merged[key] = loaded[key]; } } return merged; } function encodeBase64Utf8(text) { const bytes = new TextEncoder().encode(String(text ?? "")); const chunkSize = 0x8000; let binary = ""; for (let i = 0; i < bytes.length; i += chunkSize) { binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize)); } return btoa(binary); } async function loadServerSettings() { try { const response = await fetch(`${SERVER_SETTINGS_URL}?t=${Date.now()}`, { cache: "no-store", }); if (response.status === 404) { return; } if (!response.ok) { throw new Error(response.statusText || `HTTP ${response.status}`); } const loaded = await response.json(); if (loaded && typeof loaded === "object" && !Array.isArray(loaded)) { extension_settings[MODULE_NAME] = mergePersistedSettings(loaded); saveSettingsDebounced(); } } catch (error) { console.warn("[ST-BME] 读取服务端设置失败,回退到本地运行时设置:", error); } } async function saveServerSettings(settings = getSettings()) { const payload = JSON.stringify( getPersistedSettingsSnapshot(settings), null, 2, ); const response = await fetch("/api/files/upload", { method: "POST", headers: getRequestHeaders(), body: JSON.stringify({ name: SERVER_SETTINGS_FILENAME, data: encodeBase64Utf8(payload), }), }); if (!response.ok) { const message = await response.text().catch(() => response.statusText); throw new Error(message || `HTTP ${response.status}`); } } function scheduleServerSettingsSave() { clearTimeout(serverSettingsSaveTimer); serverSettingsSaveTimer = setTimeout(async () => { try { await saveServerSettings(); } catch (error) { console.error("[ST-BME] 保存服务端设置失败:", error); } }, 300); } function updateModuleSettings(patch = {}) { const vectorConfigKeys = new Set([ "embeddingApiUrl", "embeddingApiKey", "embeddingModel", "embeddingTransportMode", "embeddingBackendSource", "embeddingBackendModel", "embeddingBackendApiUrl", "embeddingAutoSuffix", ]); const settings = getSettings(); Object.assign(settings, patch); extension_settings[MODULE_NAME] = settings; saveSettingsDebounced(); if ( Object.prototype.hasOwnProperty.call(patch, "enabled") && patch.enabled === false ) { abortAllRunningStages(); dismissAllStageNotices(); try { applyModuleInjectionPrompt("", settings); lastInjectionContent = ""; lastRecalledItems = []; runtimeStatus = createUiStatus( "已停用", "插件已关闭,注入内容已清空", "idle", ); lastExtractionStatus = createUiStatus( "已停用", "插件已关闭,自动提取已停止", "idle", ); lastVectorStatus = createUiStatus( "已停用", "插件已关闭,向量任务已停止", "idle", ); lastRecallStatus = createUiStatus( "已停用", "插件已关闭,注入内容已清空", "idle", ); refreshPanelLiveState(); } catch (error) { console.warn("[ST-BME] 关闭插件时清理注入失败:", error); } } if (Object.keys(patch).some((key) => vectorConfigKeys.has(key))) { void resetVectorStateForConfigChange( "Embedding 配置已变更,向量索引待重建", ); } scheduleServerSettingsSave(); return settings; } // ==================== 图状态持久化 ==================== function loadGraphFromChat(options = {}) { const { attemptIndex = 0, expectedChatId = "", source = "direct-load", allowMetadataFallback = true, } = options; const context = getContext(); const chatIdentity = resolveCurrentChatIdentity(context); const chatId = chatIdentity.chatId; const normalizedExpectedChatId = String(expectedChatId || ""); if (attemptIndex === 0) { clearPendingGraphLoadRetry(); } if ( normalizedExpectedChatId && chatId && normalizedExpectedChatId !== chatId ) { clearPendingGraphLoadRetry(); return { success: false, loaded: false, loadState: graphPersistenceState.loadState, reason: "expected-chat-mismatch", chatId, attemptIndex, }; } if (!chatId) { if (chatIdentity.hasLikelySelectedChat) { currentGraph = normalizeGraphRuntimeState(createEmptyGraph(), ""); extractionCount = 0; lastExtractedItems = []; lastRecalledItems = []; lastInjectionContent = ""; runtimeStatus = createUiStatus( "图谱加载中", "正在等待当前聊天会话 ID 就绪", "running", ); lastExtractionStatus = createUiStatus( "待命", "正在等待当前聊天会话 ID 就绪", "idle", ); lastVectorStatus = createUiStatus( "待命", "正在等待当前聊天会话 ID 就绪", "idle", ); lastRecallStatus = createUiStatus( "待命", "正在等待当前聊天会话 ID 就绪", "idle", ); applyGraphLoadState(GRAPH_LOAD_STATES.LOADING, { chatId: "", reason: "chat-id-missing", attemptIndex, revision: 0, lastPersistedRevision: 0, queuedPersistRevision: 0, queuedPersistChatId: "", pendingPersist: false, shadowSnapshotUsed: false, shadowSnapshotRevision: 0, shadowSnapshotUpdatedAt: "", shadowSnapshotReason: "", dbReady: false, writesBlocked: true, }); refreshPanelLiveState(); return { success: false, loaded: false, loadState: GRAPH_LOAD_STATES.LOADING, reason: "chat-id-missing", chatId: "", attemptIndex, }; } clearPendingGraphLoadRetry(); currentGraph = normalizeGraphRuntimeState(createEmptyGraph(), ""); extractionCount = 0; lastExtractedItems = []; lastRecalledItems = []; lastInjectionContent = ""; runtimeStatus = createUiStatus("待命", "当前尚未进入聊天", "idle"); lastExtractionStatus = createUiStatus("待命", "当前尚未进入聊天", "idle"); lastVectorStatus = createUiStatus("待命", "当前尚未进入聊天", "idle"); lastRecallStatus = createUiStatus("待命", "当前尚未进入聊天", "idle"); applyGraphLoadState(GRAPH_LOAD_STATES.NO_CHAT, { chatId: "", reason: "no-chat", attemptIndex, revision: 0, lastPersistedRevision: 0, queuedPersistRevision: 0, queuedPersistChatId: "", pendingPersist: false, shadowSnapshotUsed: false, shadowSnapshotRevision: 0, shadowSnapshotUpdatedAt: "", shadowSnapshotReason: "", writesBlocked: true, }); refreshPanelLiveState(); return { success: false, loaded: false, loadState: GRAPH_LOAD_STATES.NO_CHAT, reason: "no-chat", chatId: "", attemptIndex, }; } const cachedSnapshot = readCachedIndexedDbSnapshot(chatId); if (isIndexedDbSnapshotMeaningful(cachedSnapshot)) { const cachedResult = applyIndexedDbSnapshotToRuntime(chatId, cachedSnapshot, { source: `${source}:indexeddb-cache`, attemptIndex, }); if (cachedResult?.loaded) { clearPendingGraphLoadRetry(); return cachedResult; } } const savedData = allowMetadataFallback ? context?.chatMetadata?.[GRAPH_METADATA_KEY] : undefined; if (savedData != null && savedData !== "") { try { const officialGraph = cloneGraphForPersistence( normalizeGraphRuntimeState(deserializeGraph(savedData), chatId), chatId, ); const officialRevision = Math.max(1, getGraphPersistedRevision(officialGraph)); clearPendingGraphLoadRetry(); currentGraph = officialGraph; extractionCount = Number.isFinite(currentGraph?.historyState?.extractionCount) ? currentGraph.historyState.extractionCount : 0; lastExtractedItems = []; updateLastRecalledItems(currentGraph.lastRecallResult || []); lastInjectionContent = ""; runtimeStatus = createUiStatus("待命", "已从兼容 metadata 加载图谱", "idle"); lastExtractionStatus = createUiStatus("待命", "已加载聊天图谱,等待下一次提取", "idle"); lastVectorStatus = createUiStatus( "待命", currentGraph.vectorIndexState?.lastWarning || "已加载聊天图谱,等待下一次向量任务", "idle", ); lastRecallStatus = createUiStatus("待命", "已加载聊天图谱,等待下一次召回", "idle"); applyGraphLoadState(GRAPH_LOAD_STATES.LOADED, { chatId, reason: `${source}:metadata-compat`, attemptIndex, revision: officialRevision, lastPersistedRevision: officialRevision, queuedPersistRevision: 0, queuedPersistChatId: "", pendingPersist: false, shadowSnapshotUsed: false, shadowSnapshotRevision: 0, shadowSnapshotUpdatedAt: "", shadowSnapshotReason: "", dbReady: true, writesBlocked: false, }); updateGraphPersistenceState({ metadataIntegrity: getChatMetadataIntegrity(context), storagePrimary: "metadata", storageMode: "metadata", dbReady: true, indexedDbLastError: "", }); scheduleIndexedDbGraphProbe(chatId, { source: `${source}:indexeddb-probe`, attemptIndex, allowOverride: true, applyEmptyState: true, }); refreshPanelLiveState(); return { success: true, loaded: true, loadState: GRAPH_LOAD_STATES.LOADED, reason: `${source}:metadata-compat`, chatId, attemptIndex, }; } catch (error) { console.warn("[ST-BME] 兼容 metadata 图谱读取失败,将回退 IndexedDB:", error); } } applyGraphLoadState(GRAPH_LOAD_STATES.LOADING, { chatId, reason: `indexeddb-probe-pending:${String(source || "direct-load")}`, attemptIndex, dbReady: false, writesBlocked: true, }); updateGraphPersistenceState({ storagePrimary: "indexeddb", storageMode: "indexeddb", dbReady: false, indexedDbLastError: "", }); scheduleIndexedDbGraphProbe(chatId, { source: `${source}:indexeddb-probe`, attemptIndex, allowOverride: true, applyEmptyState: true, }); refreshPanelLiveState(); return { success: false, loaded: false, loadState: GRAPH_LOAD_STATES.LOADING, reason: "indexeddb-probe-pending", chatId, attemptIndex, }; } async function saveGraphToIndexedDb( chatId, graph, { revision = 0, reason = "graph-save" } = {}, ) { const normalizedChatId = normalizeChatIdCandidate(chatId); if (!normalizedChatId || !graph) { return { saved: false, chatId: normalizedChatId, reason: "indexeddb-missing-chat-or-graph", revision: normalizeIndexedDbRevision(revision), }; } try { const manager = ensureBmeChatManager(); if (!manager) { return { saved: false, chatId: normalizedChatId, reason: "indexeddb-manager-unavailable", revision: normalizeIndexedDbRevision(revision), }; } const db = await manager.getCurrentDb(normalizedChatId); const baseSnapshot = readCachedIndexedDbSnapshot(normalizedChatId) || (await db.exportSnapshot()); const snapshot = buildSnapshotFromGraph(graph, { chatId: normalizedChatId, revision, baseSnapshot, lastModified: Date.now(), meta: { storagePrimary: "indexeddb", lastMutationReason: String(reason || "graph-save"), }, }); const importResult = await db.importSnapshot(snapshot, { mode: "replace", preserveRevision: true, revision, markSyncDirty: true, }); await db.markSyncDirty(reason); snapshot.meta.revision = normalizeIndexedDbRevision(importResult?.revision, revision); cacheIndexedDbSnapshot(normalizedChatId, snapshot); scheduleUpload(normalizedChatId, buildBmeSyncRuntimeOptions({ trigger: `graph-mutation:${String(reason || "graph-save")}`, })); updateGraphPersistenceState({ storagePrimary: "indexeddb", storageMode: "indexeddb", dbReady: true, indexedDbRevision: snapshot.meta.revision, metadataIntegrity: getChatMetadataIntegrity(getContext()) || graphPersistenceState.metadataIntegrity, indexedDbLastError: "", lastSyncError: "", dualWriteLastResult: { action: "save", target: "indexeddb", success: true, chatId: normalizedChatId, revision: snapshot.meta.revision, reason: String(reason || "graph-save"), at: Date.now(), }, }); return { saved: true, chatId: normalizedChatId, revision: snapshot.meta.revision, reason: String(reason || "graph-save"), }; } catch (error) { console.warn("[ST-BME] IndexedDB 写入失败,保留 metadata 兜底:", error); updateGraphPersistenceState({ indexedDbLastError: error?.message || String(error), dualWriteLastResult: { action: "save", target: "indexeddb", success: false, chatId: normalizedChatId, revision: normalizeIndexedDbRevision(revision), reason: String(reason || "graph-save"), error: error?.message || String(error), at: Date.now(), }, }); return { saved: false, chatId: normalizedChatId, revision: normalizeIndexedDbRevision(revision), reason: "indexeddb-write-failed", error, }; } } function queueGraphPersistToIndexedDb( chatId, graph, { revision = 0, reason = "graph-save" } = {}, ) { const normalizedChatId = normalizeChatIdCandidate(chatId); if (!normalizedChatId || !graph) return; const graphSnapshot = cloneGraphForPersistence(graph, normalizedChatId); const normalizedRevision = normalizeIndexedDbRevision(revision); const latestQueuedRevision = normalizeIndexedDbRevision( bmeIndexedDbLatestQueuedRevisionByChatId.get(normalizedChatId), ); bmeIndexedDbLatestQueuedRevisionByChatId.set( normalizedChatId, Math.max(latestQueuedRevision, normalizedRevision), ); const previousWritePromise = bmeIndexedDbWriteInFlightByChatId.get(normalizedChatId) || Promise.resolve(); const nextWritePromise = previousWritePromise .catch(() => null) .then(async () => { const currentLatestRevision = normalizeIndexedDbRevision( bmeIndexedDbLatestQueuedRevisionByChatId.get(normalizedChatId), ); if (normalizedRevision > 0 && normalizedRevision < currentLatestRevision) { return { saved: false, skipped: true, reason: "indexeddb-write-superseded", revision: normalizedRevision, }; } return await saveGraphToIndexedDb(normalizedChatId, graphSnapshot, { revision: normalizedRevision, reason, }); }) .finally(() => { if (bmeIndexedDbWriteInFlightByChatId.get(normalizedChatId) === nextWritePromise) { bmeIndexedDbWriteInFlightByChatId.delete(normalizedChatId); } }); bmeIndexedDbWriteInFlightByChatId.set(normalizedChatId, nextWritePromise); } function saveGraphToChat(options = {}) { const context = getContext(); if (!context || !currentGraph) { return buildGraphPersistResult({ saved: false, blocked: true, reason: "missing-context-or-graph", }); } const chatId = getCurrentChatId(context); const { reason = "graph-save", markMutation = true, persistMetadata = false, captureShadow = Boolean(persistMetadata), immediate = markMutation, } = options; ensureCurrentGraphRuntimeState(); currentGraph.historyState.extractionCount = extractionCount; if (!chatId) { return buildGraphPersistResult({ saved: false, blocked: true, reason: "missing-chat-id", }); } const revision = markMutation ? bumpGraphRevision(reason) : graphPersistenceState.revision || 0; if (captureShadow) { maybeCaptureGraphShadowSnapshot(reason); } const shouldQueueIndexedDbPersist = markMutation || !isGraphEffectivelyEmpty(currentGraph); if (shouldQueueIndexedDbPersist) { queueGraphPersistToIndexedDb(chatId, currentGraph, { revision, reason, }); } const metadataFallbackEnabled = Boolean(persistMetadata) || !ensureBmeChatManager(); if (!markMutation) { const hasMeaningfulGraphData = !isGraphEffectivelyEmpty(currentGraph); if ( !hasMeaningfulGraphData || graphPersistenceState.loadState === GRAPH_LOAD_STATES.EMPTY_CONFIRMED ) { return buildGraphPersistResult({ saved: false, blocked: false, reason: hasMeaningfulGraphData ? "passive-empty-confirmed-skipped" : "passive-empty-graph-skipped", revision, }); } } if (!metadataFallbackEnabled) { const saveMode = shouldQueueIndexedDbPersist ? "indexeddb-queued" : "indexeddb-skip"; updateGraphPersistenceState({ storagePrimary: "indexeddb", storageMode: "indexeddb", dbReady: graphPersistenceState.dbReady ?? isGraphLoadStateDbReady(graphPersistenceState.loadState), lastPersistReason: String(reason || "graph-save"), lastPersistMode: saveMode, pendingPersist: false, queuedPersistChatId: "", queuedPersistMode: "", queuedPersistReason: "", queuedPersistRotateIntegrity: false, dualWriteLastResult: { action: "save", target: "indexeddb", queued: Boolean(shouldQueueIndexedDbPersist), success: true, chatId, revision: normalizeIndexedDbRevision(revision), reason: String(reason || "graph-save"), at: Date.now(), }, }); return buildGraphPersistResult({ saved: Boolean(shouldQueueIndexedDbPersist), queued: false, blocked: false, reason: shouldQueueIndexedDbPersist ? "indexeddb-queued" : "indexeddb-empty-skip", revision, saveMode, }); } if (!isGraphMetadataWriteAllowed()) { console.warn( `[ST-BME] 图谱写回已被安全保护拦截(chat=${chatId},state=${graphPersistenceState.loadState},reason=${reason})`, ); return queueGraphPersist(reason, revision, { immediate }); } const metadataPersistResult = persistGraphToChatMetadata(context, { reason, revision, immediate, }); updateGraphPersistenceState({ storagePrimary: "metadata", storageMode: "metadata", dualWriteLastResult: { action: "save", target: "metadata", success: Boolean(metadataPersistResult?.saved), queued: Boolean(metadataPersistResult?.queued), blocked: Boolean(metadataPersistResult?.blocked), chatId, revision: normalizeIndexedDbRevision(revision), reason: String(reason || "graph-save"), at: Date.now(), }, }); return metadataPersistResult; } function handleGraphShadowSnapshotPageHide() { maybeCaptureGraphShadowSnapshot("pagehide"); } function handleGraphShadowSnapshotVisibilityChange() { if (document.visibilityState === "hidden") { maybeCaptureGraphShadowSnapshot("visibility-hidden"); } } // ==================== 核心流程 ==================== const DEFAULT_TRIGGER_KEYWORDS = [ "突然", "没想到", "原来", "其实", "发现", "背叛", "死亡", "复活", "恢复记忆", "失忆", "告白", "暴露", "秘密", "计划", "规则", "契约", "位置", "地点", "离开", "来到", ]; export function getSmartTriggerDecision(chat, lastProcessed, settings) { const pendingMessages = chat .slice(Math.max(0, (lastProcessed ?? -1) + 1)) .filter((msg) => !msg.is_system) .map((msg) => ({ role: msg.is_user ? "user" : "assistant", content: msg.mes || "", })) .filter((msg) => msg.content.trim().length > 0); if (pendingMessages.length === 0) { return { triggered: false, score: 0, reasons: [] }; } const reasons = []; let score = 0; const combinedText = pendingMessages.map((m) => m.content).join("\n"); const keywordHits = DEFAULT_TRIGGER_KEYWORDS.filter((keyword) => combinedText.includes(keyword), ); if (keywordHits.length > 0) { score += Math.min(2, keywordHits.length); reasons.push(`关键词: ${keywordHits.slice(0, 3).join(", ")}`); } const customPatterns = String(settings.triggerPatterns || "") .split(/\r?\n|,/) .map((s) => s.trim()) .filter(Boolean); for (const pattern of customPatterns) { try { const regex = new RegExp(pattern, "i"); if (regex.test(combinedText)) { score += 2; reasons.push(`自定义触发: ${pattern}`); break; } } catch { // 忽略无效正则,避免影响主流程 } } const roleSwitchCount = pendingMessages.reduce((count, message, index) => { if (index === 0) return count; return count + (message.role !== pendingMessages[index - 1].role ? 1 : 0); }, 0); if (roleSwitchCount >= 2) { score += 1; reasons.push("多轮往返互动"); } const punctuationHits = (combinedText.match(/[!?!?]/g) || []).length; if (punctuationHits >= 2) { score += 1; reasons.push("情绪/冲突波动"); } const entityLikeHits = combinedText.match( /[A-Z][a-z]{2,}|[\u4e00-\u9fff]{2,6}(先生|小姐|王国|城|镇|村|学院|组织|公司|小队|军团)/g, ) || []; if (entityLikeHits.length > 0) { score += 1; reasons.push("疑似新实体/新地点"); } const threshold = Math.max(1, settings.smartTriggerThreshold || 2); return { triggered: score >= threshold, score, reasons, }; } function getLatestUserChatMessage(chat) { if (!Array.isArray(chat)) return null; for (let index = chat.length - 1; index >= 0; index--) { const message = chat[index]; if (message?.is_system) continue; if (message?.is_user) return message; } return null; } function getLastNonSystemChatMessage(chat) { if (!Array.isArray(chat)) return null; for (let index = chat.length - 1; index >= 0; index--) { const message = chat[index]; if (!message?.is_system) return message; } return null; } function buildRecallRecentMessages(chat, limit, syntheticUserMessage = "") { return buildRecallRecentMessagesController(chat, limit, syntheticUserMessage, { formatRecallContextLine, normalizeRecallInputText, }); } function getRecallUserMessageSourceLabel(source) { return getRecallUserMessageSourceLabelController(source); } function resolveRecallInput(chat, recentContextMessageLimit, override = null) { return resolveRecallInputController(chat, recentContextMessageLimit, override, { buildRecallRecentMessages, getLastNonSystemChatMessage, getLatestUserChatMessage, getRecallUserMessageSourceLabel, isFreshRecallInputRecord, lastRecallSentUserMessage, normalizeRecallInputText, pendingRecallSendIntent, }); } function buildGenerationAfterCommandsRecallInput(type, params = {}, chat) { if (params?.automatic_trigger || params?.quiet_prompt) { return null; } const generationType = String(type || "").trim() || "normal"; if (!["normal", "continue", "regenerate", "swipe"].includes(generationType)) { return null; } const targetUserMessageIndex = resolveGenerationTargetUserMessageIndex(chat, { generationType, }); if (!Number.isFinite(targetUserMessageIndex)) { return { generationType, targetUserMessageIndex: null, }; } if (generationType !== "normal") { const historyInput = buildHistoryGenerationRecallInput(chat); if (!historyInput) { return { generationType, targetUserMessageIndex, }; } return { ...historyInput, generationType, targetUserMessageIndex, }; } return buildNormalGenerationRecallInput(chat); } function buildNormalGenerationRecallInput(chat) { const lastNonSystemMessage = getLastNonSystemChatMessage(chat); const tailUserText = lastNonSystemMessage?.is_user ? normalizeRecallInputText(lastNonSystemMessage?.mes || "") : ""; const targetUserMessageIndex = resolveGenerationTargetUserMessageIndex(chat, { generationType: "normal" }); const textareaText = normalizeRecallInputText( pendingRecallSendIntent.text || getSendTextareaValue(), ); const userMessage = tailUserText || textareaText; if (!userMessage) return null; return { overrideUserMessage: userMessage, generationType: "normal", targetUserMessageIndex, overrideSource: tailUserText ? "chat-tail-user" : "send-intent", overrideSourceLabel: tailUserText ? "当前用户楼层" : "发送意图", includeSyntheticUserMessage: !tailUserText, }; } function buildHistoryGenerationRecallInput(chat) { const latestUserText = normalizeRecallInputText( getLatestUserChatMessage(chat)?.mes || lastRecallSentUserMessage.text, ); if (!latestUserText) return null; const targetUserMessageIndex = resolveGenerationTargetUserMessageIndex(chat, { generationType: "history", }); return { overrideUserMessage: latestUserText, generationType: "history", targetUserMessageIndex, overrideSource: Number.isFinite(targetUserMessageIndex) ? "chat-last-user" : "chat-last-user-missing", overrideSourceLabel: Number.isFinite(targetUserMessageIndex) ? "历史最后用户楼层" : "历史用户楼层缺失", includeSyntheticUserMessage: false, }; } function buildPreGenerationRecallKey(type, options = {}) { const targetUserMessageIndex = Number.isFinite(options.targetUserMessageIndex) ? options.targetUserMessageIndex : "none"; const seedText = options.overrideUserMessage || options.userMessage || `@target:${targetUserMessageIndex}`; return [ getCurrentChatId(), String(type || "normal").trim() || "normal", hashRecallInput(seedText), ].join(":"); } function cleanupGenerationRecallTransactions(now = Date.now()) { for (const [ transactionId, transaction, ] of generationRecallTransactions.entries()) { if ( !transaction || now - (transaction.updatedAt || 0) > GENERATION_RECALL_TRANSACTION_TTL_MS ) { generationRecallTransactions.delete(transactionId); } } } function buildGenerationRecallTransactionId(chatId, generationType, recallKey) { return [ String(chatId || ""), String(generationType || "normal").trim() || "normal", String(recallKey || ""), ].join(":"); } function beginGenerationRecallTransaction({ chatId, generationType = "normal", recallKey = "", } = {}) { const normalizedChatId = String(chatId || ""); const normalizedGenerationType = String(generationType || "normal").trim() || "normal"; const normalizedRecallKey = String(recallKey || ""); if (!normalizedChatId || !normalizedRecallKey) return null; cleanupGenerationRecallTransactions(); const transactionId = buildGenerationRecallTransactionId( normalizedChatId, normalizedGenerationType, normalizedRecallKey, ); const now = Date.now(); const transaction = generationRecallTransactions.get(transactionId) || { id: transactionId, chatId: normalizedChatId, generationType: normalizedGenerationType, recallKey: normalizedRecallKey, hookStates: {}, createdAt: now, }; transaction.updatedAt = now; generationRecallTransactions.set(transactionId, transaction); return transaction; } function markGenerationRecallTransactionHookState( transaction, hookName, state = "completed", ) { if (!transaction?.id || !hookName) return transaction; transaction.hookStates ||= {}; transaction.hookStates[hookName] = state; transaction.updatedAt = Date.now(); generationRecallTransactions.set(transaction.id, transaction); return transaction; } function clearGenerationRecallTransactionsForChat( chatId = getCurrentChatId(), { clearAll = false } = {}, ) { let removed = 0; const normalizedChatId = String(chatId || ""); if (clearAll || !normalizedChatId) { removed = generationRecallTransactions.size; generationRecallTransactions.clear(); return removed; } for (const [ transactionId, transaction, ] of generationRecallTransactions.entries()) { if (String(transaction?.chatId || "") !== normalizedChatId) continue; generationRecallTransactions.delete(transactionId); removed += 1; } return removed; } function invalidateRecallAfterHistoryMutation(reason = "聊天记录已变更") { const hadActiveRecall = Boolean( isRecalling || (stageAbortControllers.recall && !stageAbortControllers.recall.signal?.aborted), ); if (hadActiveRecall) { abortRecallStageWithReason(`${reason},当前召回已取消`); } clearGenerationRecallTransactionsForChat(); clearRecallInputTracking(); clearInjectionState({ preserveRecallStatus: hadActiveRecall, preserveRuntimeStatus: hadActiveRecall, }); if (hadActiveRecall) { setLastRecallStatus( "召回已取消", `${reason},等待新的召回请求`, "warning", { syncRuntime: true, }, ); } return hadActiveRecall; } function createGenerationRecallContext({ hookName, generationType = "normal", recallOptions = {}, chatId = getCurrentChatId(), } = {}) { const recallKey = recallOptions.recallKey || buildPreGenerationRecallKey(generationType, recallOptions); const transaction = beginGenerationRecallTransaction({ chatId, generationType, recallKey, }); return { hookName, generationType, recallKey, transaction, shouldRun: shouldRunRecallForTransaction(transaction, hookName), }; } function getCurrentChatSeq(context = getContext()) { const chat = context?.chat; if (Array.isArray(chat) && chat.length > 0) { return chat.length - 1; } return currentGraph?.lastProcessedSeq ?? 0; } async function handleExtractionSuccess( result, endIdx, settings, signal = undefined, status = createBatchStatusSkeleton({ processedRange: [endIdx, endIdx], extractionCountBefore: extractionCount, }), ) { const postProcessArtifacts = []; throwIfAborted(signal, "提取已终止"); extractionCount++; ensureCurrentGraphRuntimeState(); currentGraph.historyState.extractionCount = extractionCount; updateLastExtractedItems(result.newNodeIds || []); setBatchStageOutcome(status, "core", "success"); if (settings.enableConsolidation && result.newNodeIds?.length > 0) { try { await consolidateMemories({ graph: currentGraph, newNodeIds: result.newNodeIds, embeddingConfig: getEmbeddingConfig(), options: { neighborCount: settings.consolidationNeighborCount, conflictThreshold: settings.consolidationThreshold, }, settings, signal, }); postProcessArtifacts.push("consolidation"); pushBatchStageArtifact(status, "structural", "consolidation"); } catch (e) { if (isAbortError(e)) throw e; const message = e?.message || String(e) || "记忆整合阶段失败"; setBatchStageOutcome( status, "structural", "partial", `记忆整合失败: ${message}`, ); console.error("[ST-BME] 记忆整合失败:", e); } } if ( settings.enableSynopsis && extractionCount % settings.synopsisEveryN === 0 ) { try { await generateSynopsis({ graph: currentGraph, schema: getSchema(), currentSeq: endIdx, settings, signal, }); postProcessArtifacts.push("synopsis"); pushBatchStageArtifact(status, "semantic", "synopsis"); } catch (e) { if (isAbortError(e)) throw e; const message = e?.message || String(e) || "概要生成阶段失败"; setBatchStageOutcome( status, "semantic", "failed", `概要生成失败: ${message}`, ); console.error("[ST-BME] 概要生成失败:", e); } } if ( settings.enableReflection && extractionCount % settings.reflectEveryN === 0 ) { try { await generateReflection({ graph: currentGraph, currentSeq: endIdx, settings, signal, }); postProcessArtifacts.push("reflection"); pushBatchStageArtifact(status, "semantic", "reflection"); } catch (e) { if (isAbortError(e)) throw e; const message = e?.message || String(e) || "反思生成阶段失败"; setBatchStageOutcome( status, "semantic", "failed", `反思生成失败: ${message}`, ); console.error("[ST-BME] 反思生成失败:", e); } } if ( settings.enableSleepCycle && extractionCount % settings.sleepEveryN === 0 ) { try { sleepCycle(currentGraph, settings); postProcessArtifacts.push("sleep"); pushBatchStageArtifact(status, "semantic", "sleep"); } catch (e) { const message = e?.message || String(e) || "主动遗忘阶段失败"; setBatchStageOutcome( status, "semantic", "failed", `主动遗忘失败: ${message}`, ); console.error("[ST-BME] 主动遗忘失败:", e); } } try { throwIfAborted(signal, "提取已终止"); const compressionResult = await compressAll( currentGraph, getSchema(), getEmbeddingConfig(), false, undefined, signal, settings, ); if (compressionResult.created > 0 || compressionResult.archived > 0) { postProcessArtifacts.push("compression"); pushBatchStageArtifact(status, "structural", "compression"); } } catch (error) { if (isAbortError(error)) throw error; const message = error?.message || String(error) || "压缩阶段失败"; setBatchStageOutcome( status, "structural", "partial", `压缩阶段失败: ${message}`, ); console.error("[ST-BME] 记忆压缩失败:", error); } let vectorSync = null; try { vectorSync = await syncVectorState({ signal }); } catch (error) { if (isAbortError(error)) throw error; const message = error?.message || String(error) || "向量同步阶段失败"; setBatchStageOutcome( status, "finalize", "failed", `向量同步失败: ${message}`, ); return { postProcessArtifacts, vectorHashesInserted: [], vectorStats: getVectorIndexStats(currentGraph), vectorError: message, warnings: status.warnings, batchStatus: finalizeBatchStatus(status, extractionCount), }; } if (vectorSync?.aborted) { throw createAbortError(vectorSync.error || "提取已终止"); } if (vectorSync?.error) { setBatchStageOutcome( status, "finalize", "failed", `向量同步失败: ${vectorSync.error}`, ); } else { setBatchStageOutcome(status, "finalize", "success"); } return { postProcessArtifacts, vectorHashesInserted: vectorSync?.insertedHashes || [], vectorStats: vectorSync?.stats || getVectorIndexStats(currentGraph), vectorError: vectorSync?.error || "", warnings: status.warnings, batchStatus: finalizeBatchStatus(status, extractionCount), }; } function notifyHistoryDirty(dirtyFrom, reason) { updateStageNotice( "history", "检测到楼层历史变化", `将从楼层 ${dirtyFrom} 之后自动恢复${reason ? `\n${reason}` : ""}`, "warning", { persist: true, busy: true, }, ); const now = Date.now(); if (now - lastHistoryWarningAt < 3000) return; lastHistoryWarningAt = now; toastr.warning( `检测到楼层历史变化,将从楼层 ${dirtyFrom} 之后自动恢复图谱`, reason || "ST-BME 历史回退保护", ); } function clearPendingHistoryMutationChecks() { for (const timer of pendingHistoryMutationCheckTimers) { clearTimeout(timer); } pendingHistoryMutationCheckTimers = []; } function scheduleImmediateHistoryRecovery( trigger = "history-change", delayMs = HISTORY_RECOVERY_SETTLE_MS, ) { if (!getSettings().enabled) return; const scheduledChatId = getCurrentChatId(); pendingHistoryRecoveryTrigger = trigger; clearTimeout(pendingHistoryRecoveryTimer); pendingHistoryRecoveryTimer = setTimeout(() => { pendingHistoryRecoveryTimer = null; const effectiveTrigger = pendingHistoryRecoveryTrigger || trigger; pendingHistoryRecoveryTrigger = ""; if (!getSettings().enabled) return; if (getCurrentChatId() !== scheduledChatId) return; void recoverHistoryIfNeeded(`event:${effectiveTrigger}`) .then(() => { refreshPanelLiveState(); }) .catch((error) => { console.error("[ST-BME] 事件触发的历史恢复失败:", error); updateStageNotice( "history", "历史恢复失败", error?.message || String(error), "error", { busy: false, persist: false, }, ); toastr.error(`历史恢复失败: ${error?.message || error}`); }); }, delayMs); } function scheduleHistoryMutationRecheck( trigger = "history-change", primaryArg = null, meta = null, ) { if (!getSettings().enabled) return; const scheduledChatId = getCurrentChatId(); clearPendingHistoryMutationChecks(); clearTimeout(pendingHistoryRecoveryTimer); pendingHistoryRecoveryTimer = null; pendingHistoryRecoveryTrigger = ""; updateStageNotice( "history", "检测到楼层变动", "正在等待宿主楼层状态稳定后重新核对图谱", "warning", { persist: true, busy: true, }, ); for (const delayMs of HISTORY_MUTATION_RETRY_DELAYS_MS) { const timer = setTimeout(() => { pendingHistoryMutationCheckTimers = pendingHistoryMutationCheckTimers.filter( (candidate) => candidate !== timer, ); if (!getSettings().enabled) return; if (getCurrentChatId() !== scheduledChatId) return; const detection = inspectHistoryMutation( `settled:${trigger}`, primaryArg, meta, ); if ( detection.dirty || Number.isFinite(currentGraph?.historyState?.historyDirtyFrom) ) { clearPendingHistoryMutationChecks(); scheduleImmediateHistoryRecovery(trigger, 0); } else if (pendingHistoryMutationCheckTimers.length === 0) { dismissStageNotice("history"); refreshPanelLiveState(); } }, delayMs); pendingHistoryMutationCheckTimers.push(timer); } } function inspectHistoryMutation( trigger = "history-change", primaryArg = null, meta = null, ) { if (!currentGraph) return { dirty: false, earliestAffectedFloor: null, reason: "" }; ensureCurrentGraphRuntimeState(); const context = getContext(); const chat = context?.chat; const metaDetection = resolveDirtyFloorFromMutationMeta( trigger, primaryArg, meta, chat, ); const metaReason = String(trigger || "").includes("message-deleted") ? `${trigger} 元数据检测到删除边界变动` : `${trigger} 元数据检测到楼层变动`; if ( metaDetection && Number.isFinite(metaDetection.floor) && metaDetection.floor <= getLastProcessedAssistantFloor() ) { clearInjectionState(); markHistoryDirty( currentGraph, metaDetection.floor, metaReason, metaDetection.source, ); saveGraphToChat({ reason: "history-dirty-meta-detection" }); notifyHistoryDirty(metaDetection.floor, metaReason); return { dirty: true, earliestAffectedFloor: metaDetection.floor, reason: metaReason, source: metaDetection.source, }; } const detection = detectHistoryMutation(chat, currentGraph.historyState); if (detection.dirty) { clearInjectionState(); markHistoryDirty( currentGraph, detection.earliestAffectedFloor, detection.reason || trigger, "hash-recheck", ); saveGraphToChat({ reason: "history-dirty-hash-recheck" }); notifyHistoryDirty(detection.earliestAffectedFloor, detection.reason); return { ...detection, source: "hash-recheck", }; } if (trigger === "message-edited" || trigger === "message-swiped") { clearInjectionState(); } return detection; } async function purgeCurrentVectorCollection(signal = undefined) { if (!currentGraph?.vectorIndexState?.collectionId) return; const response = await fetchLocalWithTimeout("/api/vector/purge", { method: "POST", headers: getRequestHeaders(), signal, body: JSON.stringify({ collectionId: currentGraph.vectorIndexState.collectionId, }), }); if (!response.ok) { const message = await response.text().catch(() => response.statusText); throw new Error(message || `HTTP ${response.status}`); } } async function prepareVectorStateForReplay( fullReset = false, signal = undefined, { skipBackendPurge = false } = {}, ) { ensureCurrentGraphRuntimeState(); const config = getEmbeddingConfig(); if (isBackendVectorConfig(config)) { if (!skipBackendPurge) { try { await purgeCurrentVectorCollection(signal); } catch (error) { if (isAbortError(error)) { throw error; } console.warn("[ST-BME] 清理后端向量索引失败,继续本地恢复:", error); } currentGraph.vectorIndexState.hashToNodeId = {}; currentGraph.vectorIndexState.nodeToHash = {}; } currentGraph.vectorIndexState.dirty = true; if (!currentGraph.vectorIndexState.dirtyReason) { currentGraph.vectorIndexState.dirtyReason = skipBackendPurge ? "history-recovery-replay" : "history-recovery-reset"; } if (fullReset) { currentGraph.vectorIndexState.replayRequiredNodeIds = []; currentGraph.vectorIndexState.pendingRepairFromFloor = 0; } currentGraph.vectorIndexState.lastWarning = skipBackendPurge ? "历史恢复后需要修复受影响后缀的后端向量索引" : "历史恢复后需要重建后端向量索引"; return; } if (fullReset) { currentGraph.vectorIndexState.hashToNodeId = {}; currentGraph.vectorIndexState.nodeToHash = {}; currentGraph.vectorIndexState.replayRequiredNodeIds = []; currentGraph.vectorIndexState.dirty = true; currentGraph.vectorIndexState.dirtyReason = "history-recovery-reset"; currentGraph.vectorIndexState.pendingRepairFromFloor = 0; currentGraph.vectorIndexState.lastWarning = "历史恢复后需要重嵌当前聊天向量"; } } async function executeExtractionBatch({ chat, startIdx, endIdx, settings, smartTriggerDecision = null, signal = undefined, } = {}) { return await executeExtractionBatchController( { appendBatchJournal, buildExtractionMessages, cloneGraphSnapshot, computePostProcessArtifacts, console, createBatchJournalEntry, createBatchStatusSkeleton, ensureCurrentGraphRuntimeState, extractMemories, finalizeBatchStatus, getCurrentGraph: () => currentGraph, getEmbeddingConfig, getExtractionCount: () => extractionCount, getLastProcessedAssistantFloor, getSchema, handleExtractionSuccess, saveGraphToChat, setBatchStageOutcome, setLastExtractionStatus, shouldAdvanceProcessedHistory, throwIfAborted, updateProcessedHistorySnapshot, }, { chat, startIdx, endIdx, settings, smartTriggerDecision, signal }, ); } async function replayExtractionFromHistory(chat, settings, signal = undefined, expectedChatId = undefined) { let replayedBatches = 0; while (true) { throwIfAborted(signal, "历史恢复已终止"); assertRecoveryChatStillActive(expectedChatId, 'replay-loop'); const pendingAssistantTurns = getAssistantTurns(chat).filter( (index) => index > getLastProcessedAssistantFloor(), ); if (pendingAssistantTurns.length === 0) break; const extractEvery = clampInt(settings.extractEvery, 1, 1, 50); const batchAssistantTurns = pendingAssistantTurns.slice(0, extractEvery); const startIdx = batchAssistantTurns[0]; const endIdx = batchAssistantTurns[batchAssistantTurns.length - 1]; const batchResult = await executeExtractionBatch({ chat, startIdx, endIdx, settings, signal, }); if (!batchResult.success) { throw new Error( batchResult.error || batchResult?.result?.error || "历史恢复回放过程中出现提取失败", ); } replayedBatches++; } return replayedBatches; } function applyRecoveryPlanToVectorState( recoveryPlan, dirtyFallbackFloor = null, ) { ensureCurrentGraphRuntimeState(); const vectorState = currentGraph.vectorIndexState; const replayRequiredNodeIds = new Set( Array.isArray(vectorState.replayRequiredNodeIds) ? vectorState.replayRequiredNodeIds.filter(Boolean) : [], ); for (const nodeId of recoveryPlan?.replayRequiredNodeIds || []) { if (nodeId) replayRequiredNodeIds.add(nodeId); } vectorState.replayRequiredNodeIds = [...replayRequiredNodeIds]; vectorState.dirty = true; vectorState.dirtyReason = recoveryPlan?.dirtyReason || vectorState.dirtyReason || "history-recovery-replay"; const fallbackFloor = Number.isFinite(dirtyFallbackFloor) ? dirtyFallbackFloor : currentGraph.historyState?.historyDirtyFrom; vectorState.pendingRepairFromFloor = Number.isFinite( recoveryPlan?.pendingRepairFromFloor, ) ? recoveryPlan.pendingRepairFromFloor : Number.isFinite(fallbackFloor) ? fallbackFloor : null; vectorState.lastWarning = recoveryPlan?.legacyGapFallback ? "历史恢复检测到 legacy-gap,向量索引需按受影响后缀修复" : "历史恢复后需要修复受影响后缀的向量索引"; } async function rollbackGraphForReroll(targetFloor, context = getContext()) { ensureCurrentGraphRuntimeState(); const chatId = getCurrentChatId(context); const recoveryPoint = findJournalRecoveryPoint(currentGraph, targetFloor); if (!recoveryPoint) { return { success: false, rollbackPerformed: false, extractionTriggered: false, requestedFloor: targetFloor, effectiveFromFloor: null, recoveryPath: "unavailable", affectedBatchCount: 0, error: "未找到可用的回滚点,无法安全重新提取。请先执行一次历史恢复或重新提取更早的批次。", }; } clearInjectionState(); lastExtractedItems = []; const config = getEmbeddingConfig(); const recoveryPath = recoveryPoint.path || "unknown"; const affectedBatchCount = recoveryPoint.affectedBatchCount || 0; if (recoveryPath === "reverse-journal") { const recoveryPlan = buildReverseJournalRecoveryPlan( recoveryPoint.affectedJournals, targetFloor, ); rollbackAffectedJournals(currentGraph, recoveryPoint.affectedJournals); currentGraph = normalizeGraphRuntimeState(currentGraph, chatId); extractionCount = currentGraph.historyState.extractionCount || 0; applyRecoveryPlanToVectorState(recoveryPlan, targetFloor); if ( isBackendVectorConfig(config) && recoveryPlan.backendDeleteHashes.length > 0 ) { assertRecoveryChatStillActive(chatId, 'reroll-pre-vector'); await deleteBackendVectorHashesForRecovery( currentGraph.vectorIndexState.collectionId, config, recoveryPlan.backendDeleteHashes, ); } assertRecoveryChatStillActive(chatId, 'reroll-pre-prepare'); await prepareVectorStateForReplay(false, undefined, { skipBackendPurge: isBackendVectorConfig(config), }); } else if (recoveryPath === "legacy-snapshot") { currentGraph = normalizeGraphRuntimeState( recoveryPoint.snapshotBefore, chatId, ); extractionCount = currentGraph.historyState.extractionCount || 0; await prepareVectorStateForReplay(false); } else { return { success: false, rollbackPerformed: false, extractionTriggered: false, requestedFloor: targetFloor, effectiveFromFloor: null, recoveryPath, affectedBatchCount, error: `不支持的回滚路径: ${recoveryPath}`, }; } const effectiveFromFloor = Number.isFinite( currentGraph.historyState?.lastProcessedAssistantFloor, ) ? currentGraph.historyState.lastProcessedAssistantFloor + 1 : 0; pruneProcessedMessageHashesFromFloor(currentGraph, effectiveFromFloor); currentGraph.lastProcessedSeq = currentGraph.historyState?.lastProcessedAssistantFloor ?? -1; clearHistoryDirty( currentGraph, buildRecoveryResult("reroll-rollback", { fromFloor: targetFloor, effectiveFromFloor, path: recoveryPath, affectedBatchCount, detectionSource: "manual-reroll", reason: "manual-reroll", }), ); saveGraphToChat({ reason: "reroll-rollback-complete" }); refreshPanelLiveState(); return { success: true, rollbackPerformed: true, extractionTriggered: false, requestedFloor: targetFloor, effectiveFromFloor, recoveryPath, affectedBatchCount, error: "", }; } async function recoverHistoryIfNeeded(trigger = "history-recovery") { if (!currentGraph || isRecoveringHistory) { return !isRecoveringHistory; } ensureCurrentGraphRuntimeState(); const context = getContext(); const chat = context?.chat; if (!Array.isArray(chat)) return true; const detection = inspectHistoryMutation(trigger); const dirtyFrom = currentGraph?.historyState?.historyDirtyFrom; if (!detection.dirty && !Number.isFinite(dirtyFrom)) { return true; } isRecoveringHistory = true; clearInjectionState(); const chatId = getCurrentChatId(context); const settings = getSettings(); const initialDirtyFromRaw = Number.isFinite(dirtyFrom) ? dirtyFrom : detection.earliestAffectedFloor; const initialDirtyFrom = clampRecoveryStartFloor(chat, initialDirtyFromRaw); let replayedBatches = 0; let usedFullRebuild = false; let recoveryPath = "full-rebuild"; let affectedBatchCount = 0; const historyController = beginStageAbortController("history"); const historySignal = historyController.signal; updateStageNotice( "history", "历史恢复中", Number.isFinite(initialDirtyFrom) ? `受影响起点楼层 ${initialDirtyFrom} · 正在回滚并重放` : "正在回滚并重放受影响后缀", "running", { persist: true, busy: true, }, ); try { throwIfAborted(historySignal, "历史恢复已终止"); const recoveryPoint = findJournalRecoveryPoint( currentGraph, initialDirtyFrom, ); if (recoveryPoint?.path === "reverse-journal") { recoveryPath = "reverse-journal"; affectedBatchCount = recoveryPoint.affectedBatchCount || 0; const config = getEmbeddingConfig(); const recoveryPlan = buildReverseJournalRecoveryPlan( recoveryPoint.affectedJournals, initialDirtyFrom, ); rollbackAffectedJournals(currentGraph, recoveryPoint.affectedJournals); currentGraph = normalizeGraphRuntimeState(currentGraph, chatId); extractionCount = currentGraph.historyState.extractionCount || 0; applyRecoveryPlanToVectorState(recoveryPlan, initialDirtyFrom); if ( isBackendVectorConfig(config) && recoveryPlan.backendDeleteHashes.length > 0 ) { assertRecoveryChatStillActive(chatId, 'pre-backend-delete'); await deleteBackendVectorHashesForRecovery( currentGraph.vectorIndexState.collectionId, config, recoveryPlan.backendDeleteHashes, historySignal, ); } await prepareVectorStateForReplay(false, historySignal, { skipBackendPurge: isBackendVectorConfig(config), }); } else if (recoveryPoint?.path === "legacy-snapshot") { recoveryPath = "legacy-snapshot"; affectedBatchCount = recoveryPoint.affectedBatchCount || 0; currentGraph = normalizeGraphRuntimeState( recoveryPoint.snapshotBefore, chatId, ); extractionCount = currentGraph.historyState.extractionCount || 0; await prepareVectorStateForReplay(false, historySignal); } else { recoveryPath = "full-rebuild"; currentGraph = normalizeGraphRuntimeState(createEmptyGraph(), chatId); usedFullRebuild = true; extractionCount = 0; await prepareVectorStateForReplay(true, historySignal); } assertRecoveryChatStillActive(chatId, 'pre-replay'); replayedBatches = await replayExtractionFromHistory( chat, settings, historySignal, chatId, ); clearHistoryDirty( currentGraph, buildRecoveryResult(usedFullRebuild ? "full-rebuild" : "replayed", { fromFloor: initialDirtyFrom, batches: replayedBatches, path: recoveryPath, detectionSource: detection.source || currentGraph?.historyState?.lastMutationSource || "hash-recheck", affectedBatchCount, replayedBatchCount: replayedBatches, reason: detection.reason || currentGraph?.historyState?.lastMutationReason || trigger, }), ); saveGraphToChat({ reason: "history-recovery-complete" }); refreshPanelLiveState(); updateStageNotice( "history", usedFullRebuild ? "历史恢复完成(全量重建)" : "历史恢复完成", `path ${recoveryPath} · 起点楼层 ${initialDirtyFrom} · 受影响 ${affectedBatchCount} 批 · 回放 ${replayedBatches} 批`, usedFullRebuild ? "warning" : "success", { busy: false, persist: false, }, ); toastr.success( usedFullRebuild ? "历史变化已触发全量重建" : "历史变化已完成受影响后缀恢复", ); return true; } catch (error) { if (isAbortError(error)) { updateStageNotice( "history", "历史恢复已终止", error?.message || "已手动终止当前恢复流程", "warning", { busy: false, persist: false, }, ); saveGraphToChat({ reason: "history-recovery-aborted" }); return false; } console.error("[ST-BME] 历史恢复失败,尝试全量重建:", error); try { currentGraph = normalizeGraphRuntimeState(createEmptyGraph(), chatId); extractionCount = 0; await prepareVectorStateForReplay(true, historySignal); assertRecoveryChatStillActive(chatId, 'pre-fallback-replay'); replayedBatches = await replayExtractionFromHistory( chat, settings, historySignal, chatId, ); clearHistoryDirty( currentGraph, buildRecoveryResult("full-rebuild", { fromFloor: 0, batches: replayedBatches, path: "full-rebuild", detectionSource: detection.source || currentGraph?.historyState?.lastMutationSource || "hash-recheck", affectedBatchCount, replayedBatchCount: replayedBatches, reason: `恢复失败后兜底全量重建: ${error?.message || error}`, }), ); saveGraphToChat({ reason: "history-recovery-fallback-rebuild" }); refreshPanelLiveState(); updateStageNotice( "history", "历史恢复已退化为全量重建", `path full-rebuild · 起点楼层 ${initialDirtyFrom} · 回放 ${replayedBatches} 批`, "warning", { busy: false, persist: false, }, ); toastr.warning("历史恢复已退化为全量重建"); return true; } catch (fallbackError) { currentGraph.historyState.lastRecoveryResult = buildRecoveryResult( "failed", { fromFloor: initialDirtyFrom, path: recoveryPath, detectionSource: detection.source || currentGraph?.historyState?.lastMutationSource || "hash-recheck", affectedBatchCount, replayedBatchCount: replayedBatches, reason: String(fallbackError), }, ); saveGraphToChat({ reason: "history-recovery-failed" }); refreshPanelLiveState(); updateStageNotice( "history", "历史恢复失败", fallbackError?.message || String(fallbackError), "error", { busy: false, persist: false, }, ); toastr.error(`历史恢复失败: ${fallbackError?.message || fallbackError}`); return false; } } finally { finishStageAbortController("history", historyController); isRecoveringHistory = false; } } /** * 提取管线:处理未提取的对话楼层 */ async function runExtraction() { return await runExtractionController({ beginStageAbortController, clampInt, console, ensureGraphMutationReady, executeExtractionBatch, finishStageAbortController, getAssistantTurns, getContext, getCurrentGraph: () => currentGraph, getGraphMutationBlockReason, getIsExtracting: () => isExtracting, getLastProcessedAssistantFloor, getSettings, getSmartTriggerDecision, isAbortError, notifyExtractionIssue, recoverHistoryIfNeeded, setIsExtracting: (value) => { isExtracting = value; }, setLastExtractionStatus, }); } function applyRecallInjection(settings, recallInput, recentMessages, result) { return applyRecallInjectionController( settings, recallInput, recentMessages, result, { persistRecallInjectionRecord, applyModuleInjectionPrompt, console, estimateTokens, formatInjection, getLastRecallFallbackNoticeAt: () => lastRecallFallbackNoticeAt, getRecallHookLabel, getSchema, recordInjectionSnapshot, saveGraphToChat, setCurrentGraphLastRecallResult: (selectedNodeIds) => { currentGraph.lastRecallResult = selectedNodeIds; }, setLastInjectionContent: (value) => { lastInjectionContent = value; }, setLastRecallFallbackNoticeAt: (value) => { lastRecallFallbackNoticeAt = value; }, setLastRecallStatus, toastr, updateLastRecalledItems, }, ); } function buildRecallRetrieveOptions(settings, context) { return { topK: settings.recallTopK, maxRecallNodes: settings.recallMaxNodes, enableLLMRecall: settings.recallEnableLLM, enableVectorPrefilter: settings.recallEnableVectorPrefilter, enableGraphDiffusion: settings.recallEnableGraphDiffusion, diffusionTopK: settings.recallDiffusionTopK, llmCandidatePool: settings.recallLlmCandidatePool, recallPrompt: undefined, weights: { graphWeight: settings.graphWeight, vectorWeight: settings.vectorWeight, importanceWeight: settings.importanceWeight, }, // v2 options enableVisibility: settings.enableVisibility ?? false, visibilityFilter: context.name2 || null, enableCrossRecall: settings.enableCrossRecall ?? false, enableProbRecall: settings.enableProbRecall ?? false, probRecallChance: settings.probRecallChance ?? 0.15, enableMultiIntent: settings.recallEnableMultiIntent ?? true, multiIntentMaxSegments: settings.recallMultiIntentMaxSegments ?? 4, teleportAlpha: settings.recallTeleportAlpha ?? 0.15, enableTemporalLinks: settings.recallEnableTemporalLinks ?? true, temporalLinkStrength: settings.recallTemporalLinkStrength ?? 0.2, enableDiversitySampling: settings.recallEnableDiversitySampling ?? true, dppCandidateMultiplier: settings.recallDppCandidateMultiplier ?? 3, dppQualityWeight: settings.recallDppQualityWeight ?? 1.0, enableCooccurrenceBoost: settings.recallEnableCooccurrenceBoost ?? false, cooccurrenceScale: settings.recallCooccurrenceScale ?? 0.1, cooccurrenceMaxNeighbors: settings.recallCooccurrenceMaxNeighbors ?? 10, enableResidualRecall: settings.recallEnableResidualRecall ?? false, residualBasisMaxNodes: settings.recallResidualBasisMaxNodes ?? 24, residualNmfTopics: settings.recallNmfTopics ?? 15, residualNmfNoveltyThreshold: settings.recallNmfNoveltyThreshold ?? 0.4, residualThreshold: settings.recallResidualThreshold ?? 0.3, residualTopK: settings.recallResidualTopK ?? 5, }; } /** * 召回管线:检索并注入记忆 */ async function runRecall(options = {}) { return await runRecallController( { abortRecallStageWithReason, applyRecallInjection, beginStageAbortController, buildRecallRetrieveOptions, clampInt, console, createAbortError, createRecallInputRecord, createRecallRunResult, ensureVectorReadyIfNeeded, finishStageAbortController, getActiveRecallPromise: () => activeRecallPromise, getContext, getCurrentGraph: () => currentGraph, getEmbeddingConfig, getGraphMutationBlockReason, getIsRecalling: () => isRecalling, getRecallHookLabel, getSchema, getSettings, isAbortError, isGraphMetadataWriteAllowed, isGraphReadable, nextRecallRunSequence: () => ++recallRunSequence, recoverHistoryIfNeeded, refreshPanelLiveState, resolveRecallInput, retrieve, setActiveRecallPromise: (value) => { activeRecallPromise = value; }, setIsRecalling: (value) => { isRecalling = value; }, setLastRecallStatus, setPendingRecallSendIntent: (value) => { pendingRecallSendIntent = value; }, toastr, waitForActiveRecallToSettle, }, options, ); } // ==================== 事件钩子 ==================== function onChatChanged() { const result = onChatChangedController({ abortAllRunningStages, clearGenerationRecallTransactionsForChat, clearInjectionState, clearPendingGraphLoadRetry, clearPendingHistoryMutationChecks, clearRecallInputTracking, clearTimeout, dismissAllStageNotices, getPendingHistoryRecoveryTimer: () => pendingHistoryRecoveryTimer, installSendIntentHooks, refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh, setLastPreGenerationRecallAt: (value) => { lastPreGenerationRecallAt = value; }, setLastPreGenerationRecallKey: (value) => { lastPreGenerationRecallKey = value; }, setPendingHistoryRecoveryTimer: (value) => { pendingHistoryRecoveryTimer = value; }, setPendingHistoryRecoveryTrigger: (value) => { pendingHistoryRecoveryTrigger = value; }, setSkipBeforeCombineRecallUntil: (value) => { skipBeforeCombineRecallUntil = value; }, syncGraphLoadFromLiveContext, }); scheduleBmeIndexedDbTask(async () => { const syncResult = await syncBmeChatManagerWithCurrentChat("chat-changed"); if (syncResult?.chatId) { await runBmeAutoSyncForChat("chat-changed", syncResult.chatId); await loadGraphFromIndexedDb(syncResult.chatId, { source: "chat-changed", allowOverride: true, applyEmptyState: true, }); } }); return result; } function onChatLoaded() { const result = onChatLoadedController({ refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh, syncGraphLoadFromLiveContext, }); scheduleBmeIndexedDbTask(async () => { const syncResult = await syncBmeChatManagerWithCurrentChat("chat-loaded"); if (syncResult?.chatId) { await runBmeAutoSyncForChat("chat-loaded", syncResult.chatId); await loadGraphFromIndexedDb(syncResult.chatId, { source: "chat-loaded", allowOverride: true, applyEmptyState: true, }); } }); return result; } function onMessageSent(messageId) { return onMessageSentController( { getContext, recordRecallSentUserMessage, refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh, }, messageId, ); } function onMessageDeleted(chatLengthOrMessageId, meta = null) { return onMessageDeletedController( { invalidateRecallAfterHistoryMutation, refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh, scheduleHistoryMutationRecheck, }, chatLengthOrMessageId, meta, ); } function onMessageEdited(messageId, meta = null) { return onMessageEditedController( { invalidateRecallAfterHistoryMutation, refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh, scheduleHistoryMutationRecheck, }, messageId, meta, ); } function onMessageSwiped(messageId, meta = null) { return onMessageSwipedController( { invalidateRecallAfterHistoryMutation, refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh, scheduleHistoryMutationRecheck, }, messageId, meta, ); } async function onGenerationAfterCommands(type, params = {}, dryRun = false) { return await onGenerationAfterCommandsController( { applyFinalRecallInjectionForGeneration, buildGenerationAfterCommandsRecallInput, createGenerationRecallContext, getContext, getGenerationRecallHookStateFromResult, markGenerationRecallTransactionHookState, runRecall, }, type, params, dryRun, ); } async function onBeforeCombinePrompts() { return await onBeforeCombinePromptsController({ applyFinalRecallInjectionForGeneration, buildHistoryGenerationRecallInput, buildNormalGenerationRecallInput, createGenerationRecallContext, getContext, getGenerationRecallHookStateFromResult, markGenerationRecallTransactionHookState, runRecall, }); } function onMessageReceived() { return onMessageReceivedController({ console, createRecallInputRecord, getContext, getCurrentGraph: () => currentGraph, getGraphPersistenceState: () => graphPersistenceState, getPendingRecallSendIntent: () => pendingRecallSendIntent, isAssistantChatMessage, isFreshRecallInputRecord, isGraphMetadataWriteAllowed, syncGraphLoadFromLiveContext, maybeCaptureGraphShadowSnapshot, maybeFlushQueuedGraphPersist, notifyExtractionIssue, queueMicrotask, runExtraction, refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh, setPendingRecallSendIntent: (record) => { pendingRecallSendIntent = record; }, }); } // ==================== UI 操作 ==================== async function onViewGraph() { return await onViewGraphController({ getCurrentGraph: () => currentGraph, getGraphStats, toastr, }); } async function onRebuild() { return await onRebuildController({ buildRecoveryResult, clearHistoryDirty, clearInjectionState, cloneGraphSnapshot, confirm: (message) => { if (typeof globalThis.confirm === "function") { return globalThis.confirm(message); } return false; }, createEmptyGraph, ensureGraphMutationReady, getContext, getCurrentChatId, getCurrentGraph: () => currentGraph, getSettings, normalizeGraphRuntimeState, prepareVectorStateForReplay, refreshPanelLiveState, replayExtractionFromHistory, restoreRuntimeUiState, saveGraphToChat, setCurrentGraph: (graph) => { currentGraph = graph; }, setLastExtractionStatus, setRuntimeStatus, snapshotRuntimeUiState, toastr, }); } async function onManualCompress() { return await onManualCompressController({ cloneGraphSnapshot, compressAll, ensureGraphMutationReady, getCurrentGraph: () => currentGraph, getEmbeddingConfig, getSchema, getSettings, recordGraphMutation, toastr, }); } async function onExportGraph() { return await onExportGraphController({ document, exportGraph, getCurrentGraph: () => currentGraph, toastr, }); } async function onImportGraph() { return await onImportGraphController({ clearInjectionState, clearTimeout, document, ensureGraphMutationReady, getCurrentChatId, importGraph, markVectorStateDirty, normalizeGraphRuntimeState, saveGraphToChat, setCurrentGraph: (graph) => { currentGraph = graph; }, setExtractionCount: (value) => { extractionCount = value; }, setLastExtractedItems: (items) => { lastExtractedItems = items; }, toastr, updateLastRecalledItems, window, }); } async function onViewLastInjection() { return await onViewLastInjectionController({ document, getLastInjectionContent: () => lastInjectionContent, toastr, }); } async function onTestEmbedding() { return await onTestEmbeddingController({ getCurrentChatId, getEmbeddingConfig, testVectorConnection, toastr, validateVectorConfig, }); } async function onTestMemoryLLM() { return await onTestMemoryLLMController({ testLLMConnection, toastr, }); } async function onFetchMemoryLLMModels() { return await onFetchMemoryLLMModelsController({ fetchMemoryLLMModels, toastr, }); } async function onFetchEmbeddingModels(mode = null) { return await onFetchEmbeddingModelsController( { fetchAvailableEmbeddingModels, getEmbeddingConfig, toastr, validateVectorConfig, }, mode, ); } async function onManualExtract(options = {}) { return await onManualExtractController({ beginStageAbortController, clampInt, console, createEmptyGraph, ensureGraphMutationReady, executeExtractionBatch, finishStageAbortController, getAssistantTurns, getContext, getCurrentChatId, getCurrentGraph: () => currentGraph, getIsExtracting: () => isExtracting, getLastProcessedAssistantFloor, getSettings, isAbortError, normalizeGraphRuntimeState, recoverHistoryIfNeeded, refreshPanelLiveState, setCurrentGraph: (graph) => { currentGraph = graph; }, setIsExtracting: (value) => { isExtracting = value; }, setLastExtractionStatus, toastr, }, options); } async function onReroll({ fromFloor } = {}) { return await onRerollController( { console, ensureGraphMutationReady, getAssistantTurns, getContext, getCurrentGraph: () => currentGraph, getGraphMutationBlockReason, getGraphPersistenceState: () => graphPersistenceState, getIsExtracting: () => isExtracting, getLastExtractionStatusLevel: () => lastExtractionStatus?.level || "idle", getLastProcessedAssistantFloor, isAbortError, onManualExtract, refreshPanelLiveState, rollbackGraphForReroll, setRuntimeStatus, toastr, }, { fromFloor }, ); } async function onManualSleep() { return await onManualSleepController({ cloneGraphSnapshot, ensureGraphMutationReady, getCurrentGraph: () => currentGraph, getSettings, recordGraphMutation, sleepCycle, toastr, }); } async function onManualSynopsis() { return await onManualSynopsisController({ cloneGraphSnapshot, ensureGraphMutationReady, generateSynopsis, getCurrentChatSeq, getCurrentGraph: () => currentGraph, getSchema, getSettings, recordGraphMutation, toastr, }); } async function onManualEvolve() { return await onManualEvolveController({ cloneGraphSnapshot, consolidateMemories, ensureGraphMutationReady, getCurrentGraph: () => currentGraph, getEmbeddingConfig, getLastExtractedItems: () => lastExtractedItems, getSettings, recordGraphMutation, toastr, }); } async function onRebuildVectorIndex(range = null) { return await onRebuildVectorIndexController( { beginStageAbortController, ensureCurrentGraphRuntimeState, ensureGraphMutationReady, finishStageAbortController, getEmbeddingConfig, isBackendVectorConfig, refreshPanelLiveState, saveGraphToChat, syncVectorState, toastr, validateVectorConfig, }, range, ); } async function onReembedDirect() { return await onReembedDirectController({ getEmbeddingConfig, isDirectVectorConfig, onRebuildVectorIndex: async () => await onRebuildVectorIndex(), toastr, }); } // ==================== 初始化 ==================== (async function init() { await loadServerSettings(); syncGraphPersistenceDebugState(); ensureBmeChatManager(); scheduleBmeIndexedDbWarmup("init"); initializeHostCapabilityBridge(); installSendIntentHooks(); autoSyncOnVisibility(buildBmeSyncRuntimeOptions()); // 注册事件钩子 registerCoreEventHooksController({ eventSource, eventTypes: event_types, handlers: { onBeforeCombinePrompts, onChatChanged, onChatLoaded, onGenerationAfterCommands, onMessageDeleted, onMessageEdited, onMessageReceived, onMessageSent, onMessageSwiped, }, registerBeforeCombinePrompts, registerGenerationAfterCommands, }); // 加载当前聊天的图谱 scheduleBmeIndexedDbTask(async () => { const syncResult = await syncBmeChatManagerWithCurrentChat("initial-load"); if (!syncResult?.chatId) { syncGraphLoadFromLiveContext({ source: "initial-load:no-chat", force: true }); return; } await runBmeAutoSyncForChat("initial-load", syncResult.chatId); await loadGraphFromIndexedDb(syncResult.chatId, { source: "initial-load", allowOverride: true, applyEmptyState: true, }); }); // ==================== 操控面板初始化 ==================== await initializePanelBridgeController({ $, actions: { syncGraphLoad: () => syncGraphLoadFromLiveContext({ source: "panel-open-sync", }), extract: onManualExtract, compress: onManualCompress, sleep: onManualSleep, synopsis: onManualSynopsis, export: onExportGraph, import: onImportGraph, rebuild: onRebuild, evolve: onManualEvolve, testEmbedding: onTestEmbedding, testMemoryLLM: onTestMemoryLLM, fetchMemoryLLMModels: onFetchMemoryLLMModels, fetchEmbeddingModels: onFetchEmbeddingModels, rebuildVectorIndex: () => onRebuildVectorIndex(), rebuildVectorRange: (range) => onRebuildVectorIndex(range), reembedDirect: onReembedDirect, reroll: onReroll, }, console, document, getGraph: () => currentGraph, getGraphPersistenceState: () => getGraphPersistenceLiveState(), getLastBatchStatus: () => currentGraph?.historyState?.lastBatchStatus || null, getLastExtract: () => lastExtractedItems, getLastExtractionStatus: () => lastExtractionStatus, getLastInjection: () => lastInjectionContent, getLastRecall: () => lastRecalledItems, getLastRecallStatus: () => lastRecallStatus, getLastVectorStatus: () => lastVectorStatus, getPanelModule: () => _panelModule, getRuntimeDebugSnapshot: (options = {}) => getPanelRuntimeDebugSnapshot(options), getRuntimeStatus: () => getPanelRuntimeStatus(), getSettings, getThemesModule: () => _themesModule, importPanelModule: async () => await import("./panel.js"), importThemesModule: async () => await import("./themes.js"), setPanelModule: (module) => { _panelModule = module; }, setThemesModule: (module) => { _themesModule = module; }, updateSettings: updateModuleSettings, }); schedulePersistedRecallMessageUiRefresh(120); console.log("[ST-BME] 初始化完成"); })();