function getTimerApi(runtime = {}) { const rawSetTimeout = typeof runtime.setTimeout === "function" ? runtime.setTimeout : globalThis.setTimeout; const rawClearTimeout = typeof runtime.clearTimeout === "function" ? runtime.clearTimeout : globalThis.clearTimeout; return { setTimeout(...args) { return Reflect.apply(rawSetTimeout, globalThis, args); }, clearTimeout(...args) { return Reflect.apply(rawClearTimeout, globalThis, args); }, }; } function hasCompressionMutation(result = {}) { return ( Math.max(0, Number(result?.created) || 0) > 0 || Math.max(0, Number(result?.archived) || 0) > 0 ); } function hasSleepMutation(result = {}) { return Math.max(0, Number(result?.forgotten) || 0) > 0; } function hasConsolidationMutation(result = {}) { return ( Math.max(0, Number(result?.merged) || 0) > 0 || Math.max(0, Number(result?.skipped) || 0) > 0 || Math.max(0, Number(result?.evolved) || 0) > 0 || Math.max(0, Number(result?.connections) || 0) > 0 || Math.max(0, Number(result?.updates) || 0) > 0 ); } function findGraphNode(graph, nodeId) { if (!graph || !Array.isArray(graph.nodes)) return null; return graph.nodes.find((node) => node?.id === nodeId) || null; } function isManualEvolutionCandidateNode(node) { if (!node || node.archived) return false; if (Number(node.level || 0) > 0) return false; return !["synopsis", "reflection"].includes(String(node.type || "")); } function normalizeManualEvolutionCandidateIds(graph, nodeIds = []) { const unique = new Set(); for (const rawId of Array.isArray(nodeIds) ? nodeIds : []) { const nodeId = String(rawId || "").trim(); if (!nodeId || unique.has(nodeId)) continue; const node = findGraphNode(graph, nodeId); if (!isManualEvolutionCandidateNode(node)) continue; unique.add(nodeId); } return [...unique]; } function resolveManualEvolutionCandidates(runtime, graph) { const liveRecentIds = normalizeManualEvolutionCandidateIds( graph, runtime.getLastExtractedItems?.() ?.map((item) => item?.id) .filter(Boolean) || [], ); if (liveRecentIds.length > 0) { return { ids: liveRecentIds, source: "recent-extract", }; } const currentExtractionCount = Math.max( 0, Number(graph?.historyState?.extractionCount) || 0, ); const batchJournal = Array.isArray(graph?.batchJournal) ? graph.batchJournal : []; for (let index = batchJournal.length - 1; index >= 0; index -= 1) { const entry = batchJournal[index]; const beforeExtractionCount = Math.max( 0, Number(entry?.stateBefore?.extractionCount) || 0, ); if (beforeExtractionCount >= currentExtractionCount) { continue; } const fallbackIds = normalizeManualEvolutionCandidateIds( graph, entry?.createdNodeIds || [], ); if (fallbackIds.length > 0) { return { ids: fallbackIds, source: "latest-extraction-batch", }; } } return { ids: [], source: "none", }; } function describeManualEvolutionSource(source, count) { switch (String(source || "")) { case "recent-extract": return `使用最近提取的 ${count} 个节点`; case "latest-extraction-batch": return `使用最近一批提取落盘的 ${count} 个节点`; default: return `候选节点 ${count} 个`; } } function updateManualActionUiState(runtime, text, meta = "", level = "idle") { if (typeof runtime?.setRuntimeStatus === "function") { runtime.setRuntimeStatus(text, meta, level); } runtime?.refreshPanelLiveState?.(); } function rebindImportedGraphToCurrentChat(runtime, importedGraph) { if (!importedGraph || typeof importedGraph !== "object") { return { rebound: false, reason: "missing-graph", }; } const chat = runtime.getContext?.()?.chat; const assistantTurns = typeof runtime.getAssistantTurns === "function" && Array.isArray(chat) ? runtime.getAssistantTurns(chat) : []; if (typeof runtime.rebindProcessedHistoryStateToChat === "function") { return runtime.rebindProcessedHistoryStateToChat( importedGraph, chat, assistantTurns, ); } importedGraph.historyState.processedMessageHashesNeedRefresh = true; return { rebound: false, reason: "missing-history-rebind-helper", }; } export async function onViewGraphController(runtime) { const graph = runtime.getCurrentGraph(); if (!graph) { runtime.toastr.warning("当前没有加载的图谱"); return; } const stats = runtime.getGraphStats(graph); const statsText = [ `节点: ${stats.activeNodes} 活跃 / ${stats.archivedNodes} 归档`, `边: ${stats.totalEdges}`, `最后处理楼层: ${stats.lastProcessedSeq}`, `类型分布: ${ Object.entries(stats.typeCounts) .map(([k, v]) => `${k}=${v}`) .join(", ") || "(空)" }`, ].join("\n"); runtime.toastr.info(statsText, "ST-BME 图谱状态", { timeOut: 10000 }); } export async function onTestEmbeddingController(runtime) { const config = runtime.getEmbeddingConfig(); const validation = runtime.validateVectorConfig(config); if (!validation.valid) { runtime.toastr.warning(validation.error); return; } runtime.toastr.info("正在测试 Embedding API 连通性..."); const result = await runtime.testVectorConnection(config, runtime.getCurrentChatId()); if (result.success) { runtime.toastr.success(`连接成功!向量维度: ${result.dimensions}`); } else { runtime.toastr.error(`连接失败: ${result.error}`); } } export async function onTestMemoryLLMController(runtime) { runtime.toastr.info("正在测试记忆 LLM 连通性..."); const result = await runtime.testLLMConnection(); if (result.success) { runtime.toastr.success(`连接成功!模式: ${result.mode}`); } else { runtime.toastr.error(`连接失败: ${result.error}`); } } export async function onFetchMemoryLLMModelsController(runtime) { runtime.toastr.info("正在拉取记忆 LLM 模型列表..."); const result = await runtime.fetchMemoryLLMModels(); if (result.success) { runtime.toastr.success(`已拉取 ${result.models.length} 个记忆 LLM 模型`); } else { runtime.toastr.error(`拉取失败: ${result.error}`); } return result; } export async function onFetchEmbeddingModelsController(runtime, mode = null) { const config = runtime.getEmbeddingConfig(mode); const targetMode = mode || config?.mode || "direct"; const validation = runtime.validateVectorConfig(config); if (!validation.valid) { runtime.toastr.warning(validation.error); return { success: false, models: [], error: validation.error }; } runtime.toastr.info("正在拉取 Embedding 模型列表..."); const result = await runtime.fetchAvailableEmbeddingModels(config); if (result.success) { const modeLabel = targetMode === "backend" ? "后端" : "直连"; runtime.toastr.success( `已拉取 ${result.models.length} 个${modeLabel} Embedding 模型`, ); } else { runtime.toastr.error(`拉取失败: ${result.error}`); } return result; } export async function onManualCompressController(runtime) { const graph = runtime.getCurrentGraph(); if (!graph) return; if (!runtime.ensureGraphMutationReady("手动压缩")) return; updateManualActionUiState(runtime, "手动压缩中", "正在检查可压缩候选组", "running"); try { const schema = runtime.getSchema(); const inspection = runtime.inspectCompressionCandidates?.(graph, schema, true); if (inspection && !inspection.hasCandidates) { const reason = String( inspection.reason || "当前没有可压缩候选组,本次未发起 LLM 压缩", ); updateManualActionUiState(runtime, "手动压缩未执行", reason, "idle"); runtime.toastr.info(reason); return { handledToast: true, requestDispatched: false, mutated: false, reason, }; } updateManualActionUiState(runtime, "手动压缩中", "正在请求 LLM 压缩候选组", "running"); const beforeSnapshot = runtime.cloneGraphSnapshot(graph); const result = await runtime.compressAll( graph, schema, runtime.getEmbeddingConfig(), true, undefined, undefined, runtime.getSettings(), ); const mutated = hasCompressionMutation(result); if (mutated) { runtime.recordMaintenanceAction?.({ action: "compress", beforeSnapshot, mode: "manual", summary: runtime.buildMaintenanceSummary?.("compress", result, "manual"), }); await runtime.recordGraphMutation({ beforeSnapshot, artifactTags: ["compression"], }); updateManualActionUiState( runtime, "手动压缩完成", `新建 ${result.created},归档 ${result.archived}`, "success", ); runtime.toastr.success( `手动压缩完成:新建 ${result.created},归档 ${result.archived}`, ); } else { updateManualActionUiState( runtime, "手动压缩无变更", "已尝试压缩,但本轮没有产生可持久化变化", "idle", ); runtime.toastr.info("已尝试手动压缩,但本轮没有产生可持久化变化"); } return { handledToast: true, requestDispatched: true, mutated, result, }; } catch (error) { updateManualActionUiState( runtime, "手动压缩失败", error?.message || String(error), "error", ); throw error; } } export async function onExportGraphController(runtime) { const graph = runtime.getCurrentGraph(); if (!graph) return; const json = runtime.exportGraph(graph); const blob = new Blob([json], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = runtime.document.createElement("a"); a.href = url; a.download = `st-bme-graph-${Date.now()}.json`; a.click(); URL.revokeObjectURL(url); runtime.toastr.success("图谱已导出"); } export async function onViewLastInjectionController(runtime) { const content = runtime.getLastInjectionContent(); if (!content) { runtime.toastr.info("暂无注入内容"); return; } const popup = runtime.document.createElement("div"); popup.style.cssText = "position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#1a1a2e;color:#eee;padding:24px;border-radius:12px;max-width:80vw;max-height:80vh;overflow:auto;z-index:99999;white-space:pre-wrap;font-size:13px;box-shadow:0 8px 32px rgba(0,0,0,0.5);"; popup.textContent = content; const close = runtime.document.createElement("button"); close.textContent = "关闭"; close.style.cssText = "position:absolute;top:8px;right:12px;background:#e94560;color:white;border:none;padding:4px 12px;border-radius:4px;cursor:pointer;"; close.onclick = () => popup.remove(); popup.appendChild(close); runtime.document.body.appendChild(popup); } export async function onRebuildController(runtime) { if (!runtime.confirm("确定要从当前聊天重建图谱?这将清除现有图谱数据。")) { return; } if (!runtime.ensureGraphMutationReady("重建图谱")) return; const context = runtime.getContext(); const chat = context?.chat; if (!Array.isArray(chat)) { runtime.toastr.warning("当前聊天上下文不可用,无法重建"); return; } const previousGraphSnapshot = runtime.getCurrentGraph() ? runtime.cloneGraphSnapshot(runtime.getCurrentGraph()) : runtime.cloneGraphSnapshot( runtime.normalizeGraphRuntimeState( runtime.createEmptyGraph(), runtime.getCurrentChatId(), ), ); const previousUiState = runtime.snapshotRuntimeUiState(); const settings = runtime.getSettings(); runtime.setRuntimeStatus( "图谱重建中", `当前聊天 ${Array.isArray(chat) ? chat.length : 0} 条消息`, "running", ); const nextGraph = runtime.normalizeGraphRuntimeState( runtime.createEmptyGraph(), runtime.getCurrentChatId(), ); nextGraph.batchJournal = []; runtime.setCurrentGraph(nextGraph); runtime.clearInjectionState(); try { await runtime.prepareVectorStateForReplay(true); const replayedBatches = await runtime.replayExtractionFromHistory(chat, settings); runtime.clearHistoryDirty( runtime.getCurrentGraph(), runtime.buildRecoveryResult("full-rebuild", { fromFloor: 0, batches: replayedBatches, path: "full-rebuild", detectionSource: "manual-rebuild", affectedBatchCount: runtime.getCurrentGraph().batchJournal?.length || 0, replayedBatchCount: replayedBatches, reason: "用户手动触发全量重建", }), ); const recoveredLastProcessedFloor = Number.isFinite( runtime.getCurrentGraph()?.historyState?.lastProcessedAssistantFloor, ) ? runtime.getCurrentGraph().historyState.lastProcessedAssistantFloor : -1; if (recoveredLastProcessedFloor >= 0) { if (typeof runtime.updateProcessedHistorySnapshot === "function") { runtime.updateProcessedHistorySnapshot(chat, recoveredLastProcessedFloor); } else if (typeof runtime.applyProcessedHistorySnapshotToGraph === "function") { runtime.applyProcessedHistorySnapshotToGraph( runtime.getCurrentGraph(), chat, recoveredLastProcessedFloor, ); } } runtime.saveGraphToChat({ reason: "manual-rebuild-complete" }); runtime.setLastExtractionStatus( "图谱重建完成", `已回放 ${replayedBatches} 批提取`, "success", { syncRuntime: false, }, ); if (runtime.getCurrentGraph().vectorIndexState?.lastWarning) { runtime.setRuntimeStatus( "图谱重建完成", `已回放 ${replayedBatches} 批,但向量仍待修复`, "warning", ); runtime.toastr.warning( `图谱已重建,但向量索引仍待修复: ${runtime.getCurrentGraph().vectorIndexState.lastWarning}`, ); } else { runtime.setRuntimeStatus( "图谱重建完成", `已回放 ${replayedBatches} 批,图谱与向量索引已刷新`, "success", ); runtime.toastr.success("图谱与向量索引已按当前聊天全量重建"); } } catch (error) { runtime.setCurrentGraph( runtime.normalizeGraphRuntimeState( previousGraphSnapshot, runtime.getCurrentChatId(), ), ); runtime.restoreRuntimeUiState(previousUiState); runtime.saveGraphToChat({ reason: "manual-rebuild-restore-previous" }); runtime.setLastExtractionStatus("图谱重建失败", error?.message || String(error), "error", { syncRuntime: true, }); throw new Error( `图谱重建失败,已恢复到重建前状态: ${error?.message || error}`, ); } finally { runtime.refreshPanelLiveState(); } } export async function onImportGraphController(runtime) { if (!runtime.ensureGraphMutationReady("导入图谱")) { return { cancelled: true }; } const input = runtime.document.createElement("input"); input.type = "file"; input.accept = ".json"; return await new Promise((resolve, reject) => { const timers = getTimerApi(runtime); let settled = false; let focusTimer = null; const cleanup = () => { if (focusTimer) { timers.clearTimeout(focusTimer); focusTimer = null; } input.onchange = null; runtime.window.removeEventListener("focus", onWindowFocus, true); }; const finish = (value, isError = false) => { if (settled) return; settled = true; cleanup(); if (isError) { reject(value); } else { resolve(value); } }; const onWindowFocus = () => { focusTimer = timers.setTimeout(() => { if (!settled) { finish({ cancelled: true }); } }, 180); }; runtime.window.addEventListener("focus", onWindowFocus, true); input.addEventListener( "cancel", () => { finish({ cancelled: true }); }, { once: true }, ); input.onchange = async (event) => { const file = event.target.files?.[0]; if (!file) { finish({ cancelled: true }); return; } try { const text = await file.text(); const importedGraph = runtime.normalizeGraphRuntimeState( runtime.importGraph(text), runtime.getCurrentChatId(), ); const historyRebind = rebindImportedGraphToCurrentChat( runtime, importedGraph, ); runtime.setCurrentGraph(importedGraph); runtime.markVectorStateDirty("导入图谱后需要重建向量索引"); runtime.setExtractionCount( Math.max(0, Number(importedGraph?.historyState?.extractionCount) || 0), ); runtime.setLastExtractedItems([]); runtime.updateLastRecalledItems(importedGraph.lastRecallResult || []); runtime.clearInjectionState(); runtime.saveGraphToChat({ reason: "graph-import-complete" }); runtime.toastr.success( historyRebind?.rebound === true ? "图谱已导入,并已重新绑定当前聊天历史" : "图谱已导入", ); finish({ imported: true, handledToast: true }); } catch (err) { const error = err instanceof Error ? err : new Error(String(err || "导入失败")); runtime.toastr.error(`导入失败: ${error.message}`); error._stBmeToastHandled = true; finish(error, true); } }; input.click(); }); } export async function onRebuildVectorIndexController(runtime, range = null) { if (!runtime.ensureGraphMutationReady(range ? "范围重建向量" : "重建向量")) return; runtime.ensureCurrentGraphRuntimeState(); const config = runtime.getEmbeddingConfig(); const validation = runtime.validateVectorConfig(config); if (!validation.valid) { runtime.toastr.warning(validation.error); return; } const vectorController = runtime.beginStageAbortController("vector"); try { if ( !range && typeof runtime.shouldUseAuthorityJobs === "function" && runtime.shouldUseAuthorityJobs(config) && typeof runtime.submitAuthorityVectorRebuildJob === "function" ) { const jobResult = await runtime.submitAuthorityVectorRebuildJob({ config, purge: true, range, signal: vectorController.signal, }); if (jobResult?.submitted) { runtime.saveGraphToChat({ reason: "authority-vector-rebuild-job-submitted" }); runtime.toastr.info( `Authority 向量重建任务已提交:${jobResult.job?.id || "pending"}`, ); return; } if (jobResult?.error) { runtime.toastr.warning( `Authority Job 提交失败,已回退本地重建:${jobResult.error}`, ); } } const result = await runtime.syncVectorState({ force: true, purge: !range && (runtime.isBackendVectorConfig(config) || runtime.isAuthorityVectorConfig?.(config)), range, signal: vectorController.signal, }); runtime.saveGraphToChat({ reason: "vector-rebuild-complete" }); if (result?.aborted) { return; } if (result?.error) { throw new Error(result.error); } runtime.toastr.success( range ? `范围向量重建完成:indexed=${result.stats.indexed}, pending=${result.stats.pending}` : `当前聊天向量重建完成:indexed=${result.stats.indexed}, pending=${result.stats.pending}`, ); } finally { runtime.finishStageAbortController("vector", vectorController); runtime.refreshPanelLiveState(); } } export async function onReembedDirectController(runtime) { const config = runtime.getEmbeddingConfig(); if (!runtime.isDirectVectorConfig(config)) { runtime.toastr.info("当前不是直连模式,无需执行重嵌"); return; } await runtime.onRebuildVectorIndex(); } export async function onManualSleepController(runtime) { const graph = runtime.getCurrentGraph(); if (!graph) return; if (!runtime.ensureGraphMutationReady("执行遗忘")) return; updateManualActionUiState(runtime, "执行遗忘中", "正在评估可归档节点", "running"); try { const beforeSnapshot = runtime.cloneGraphSnapshot(graph); const result = runtime.sleepCycle(graph, runtime.getSettings()); const mutated = hasSleepMutation(result); if (mutated) { runtime.recordMaintenanceAction?.({ action: "sleep", beforeSnapshot, mode: "manual", summary: runtime.buildMaintenanceSummary?.("sleep", result, "manual"), }); await runtime.recordGraphMutation({ beforeSnapshot, artifactTags: ["sleep"], }); updateManualActionUiState( runtime, "执行遗忘完成", `归档 ${result.forgotten} 个节点`, "success", ); runtime.toastr.success(`执行遗忘完成:归档 ${result.forgotten} 个节点`); } else { updateManualActionUiState( runtime, "执行遗忘无变更", "当前没有符合遗忘条件的节点", "idle", ); runtime.toastr.info( "当前没有符合遗忘条件的节点。本操作只做本地图清理,不会发送 LLM 请求。", ); } return { handledToast: true, requestDispatched: false, mutated, result, }; } catch (error) { updateManualActionUiState( runtime, "执行遗忘失败", error?.message || String(error), "error", ); throw error; } } export async function onManualSynopsisController(runtime) { const graph = runtime.getCurrentGraph(); if (!graph) return; if (!runtime.ensureGraphMutationReady("生成小总结")) return; updateManualActionUiState(runtime, "生成小总结中", "正在基于原文窗口生成新的小总结", "running"); try { const chat = runtime.getContext?.()?.chat; const result = await runtime.generateSmallSummary({ graph, chat: Array.isArray(chat) ? chat : [], settings: runtime.getSettings(), currentExtractionCount: Number(graph?.historyState?.extractionCount) || 0, currentAssistantFloor: runtime.getCurrentChatSeq(), currentRange: null, currentNodeIds: [], force: true, }); if (!result?.created) { updateManualActionUiState( runtime, "小总结未生成", result?.reason || "当前没有可用于生成小总结的新范围", "idle", ); runtime.toastr.info(result?.reason || "当前没有可用于生成小总结的新范围"); return { handledToast: true, requestDispatched: false, mutated: false, reason: result?.reason || "", }; } runtime.saveGraphToChat?.({ reason: "manual-small-summary" }); runtime.refreshPanelLiveState?.(); updateManualActionUiState(runtime, "小总结生成完成", "新的小总结已加入总结前沿", "success"); runtime.toastr.success("小总结生成完成"); return { handledToast: true, requestDispatched: true, mutated: true, result, }; } catch (error) { updateManualActionUiState( runtime, "小总结生成失败", error?.message || String(error), "error", ); throw error; } } export async function onManualSummaryRollupController(runtime) { const graph = runtime.getCurrentGraph(); if (!graph) return; if (!runtime.ensureGraphMutationReady("执行总结折叠")) return; updateManualActionUiState(runtime, "总结折叠中", "正在折叠当前活跃总结前沿", "running"); try { const result = await runtime.rollupSummaryFrontier({ graph, settings: runtime.getSettings(), force: true, }); if (!Number(result?.createdCount || 0)) { updateManualActionUiState( runtime, "总结折叠未执行", result?.reason || "当前没有达到折叠门槛的活跃总结", "idle", ); runtime.toastr.info(result?.reason || "当前没有达到折叠门槛的活跃总结"); return { handledToast: true, requestDispatched: false, mutated: false, reason: result?.reason || "", }; } runtime.saveGraphToChat?.({ reason: "manual-summary-rollup" }); runtime.refreshPanelLiveState?.(); updateManualActionUiState( runtime, "总结折叠完成", `已折叠 ${result.foldedCount || 0} 条,总结产出 ${result.createdCount || 0} 条`, "success", ); runtime.toastr.success( `总结折叠完成:折叠 ${result.foldedCount || 0} 条,产出 ${result.createdCount || 0} 条`, ); return { handledToast: true, requestDispatched: true, mutated: true, result, }; } catch (error) { updateManualActionUiState( runtime, "总结折叠失败", error?.message || String(error), "error", ); throw error; } } export async function onRebuildSummaryStateController(runtime, options = {}) { const graph = runtime.getCurrentGraph(); if (!graph) return; if (!runtime.ensureGraphMutationReady("重建总结状态")) return; const hasStart = Number.isFinite(Number(options?.startFloor)); const hasEnd = Number.isFinite(Number(options?.endFloor)); const mode = hasStart || hasEnd ? "range" : "current"; updateManualActionUiState( runtime, "重建总结中", mode === "range" ? `正在按范围 ${hasStart ? Number(options.startFloor) : "?"} ~ ${hasEnd ? Number(options.endFloor) : "最新"} 重建总结链` : "正在重建当前总结相关范围", "running", ); try { const chat = runtime.getContext?.()?.chat; const result = await runtime.rebuildHierarchicalSummaryState({ graph, chat: Array.isArray(chat) ? chat : [], settings: runtime.getSettings(), mode, startFloor: hasStart ? Number(options.startFloor) : null, endFloor: hasEnd ? Number(options.endFloor) : null, }); runtime.saveGraphToChat?.({ reason: "rebuild-summary-state" }); runtime.refreshPanelLiveState?.(); if (!result?.rebuilt) { updateManualActionUiState( runtime, "重建总结未产生变化", result?.reason || "当前没有可重建的总结链", "idle", ); runtime.toastr.info(result?.reason || "当前没有可重建的总结链"); return { handledToast: true, requestDispatched: true, mutated: false, result, }; } updateManualActionUiState( runtime, "重建总结完成", `小总结 ${result.smallSummaryCount || 0} 条,折叠总结 ${result.rollupCount || 0} 条`, "success", ); runtime.toastr.success( `重建总结完成:小总结 ${result.smallSummaryCount || 0} 条,折叠总结 ${result.rollupCount || 0} 条`, ); return { handledToast: true, requestDispatched: true, mutated: true, result, }; } catch (error) { updateManualActionUiState( runtime, "重建总结失败", error?.message || String(error), "error", ); throw error; } } export async function onClearSummaryStateController(runtime) { const graph = runtime.getCurrentGraph(); if (!graph) return; if (!runtime.ensureGraphMutationReady("清空总结状态")) return; if ( typeof runtime.confirm === "function" && !runtime.confirm( "确定要清空当前聊天的总结状态?\n\n这会删除当前聊天的所有层级总结前沿与折叠历史,但不会删除图谱节点或聊天原文。", ) ) { return { cancelled: true, }; } runtime.resetHierarchicalSummaryState?.(graph); runtime.saveGraphToChat?.({ reason: "clear-summary-state" }); runtime.refreshPanelLiveState?.(); updateManualActionUiState( runtime, "总结状态已清空", "当前聊天的层级总结已重置", "success", ); runtime.toastr.success("当前聊天总结状态已清空"); return { handledToast: true, requestDispatched: false, mutated: true, }; } export async function onManualEvolveController(runtime) { const graph = runtime.getCurrentGraph(); if (!graph) return; if (!runtime.ensureGraphMutationReady("强制进化")) return; updateManualActionUiState(runtime, "强制进化中", "正在整理候选节点", "running"); try { const embeddingConfig = runtime.getEmbeddingConfig(); const vectorValidation = runtime.validateVectorConfig?.(embeddingConfig); if (vectorValidation && !vectorValidation.valid) { updateManualActionUiState( runtime, "强制进化未执行", vectorValidation.error, "warning", ); runtime.toastr.warning(vectorValidation.error); return { handledToast: true, requestDispatched: false, mutated: false, reason: vectorValidation.error, }; } const candidateResolution = resolveManualEvolutionCandidates(runtime, graph); const candidateIds = candidateResolution.ids; if (candidateIds.length === 0) { updateManualActionUiState( runtime, "强制进化未执行", "当前没有可用于进化的最近提取节点", "idle", ); runtime.toastr.info("当前没有可用于进化的最近提取节点,本次未发起整合请求"); return { handledToast: true, requestDispatched: false, mutated: false, reason: "no-candidates", }; } const beforeSnapshot = runtime.cloneGraphSnapshot(graph); const settings = runtime.getSettings(); updateManualActionUiState( runtime, "强制进化中", `正在处理 ${candidateIds.length} 个候选节点`, "running", ); const result = await runtime.consolidateMemories({ graph, newNodeIds: candidateIds, embeddingConfig, customPrompt: undefined, settings, options: { neighborCount: settings.consolidationNeighborCount, conflictThreshold: settings.consolidationThreshold, }, }); const mutated = hasConsolidationMutation(result); const sourceLabel = describeManualEvolutionSource( candidateResolution.source, candidateIds.length, ); if (mutated) { runtime.recordMaintenanceAction?.({ action: "consolidate", beforeSnapshot, mode: "manual", summary: runtime.buildMaintenanceSummary?.("consolidate", result, "manual"), }); await runtime.recordGraphMutation({ beforeSnapshot, artifactTags: ["consolidation"], }); updateManualActionUiState( runtime, "强制进化完成", `合并 ${result.merged},进化 ${result.evolved},更新 ${result.updates}`, "success", ); runtime.toastr.success( `强制进化完成:合并 ${result.merged},跳过 ${result.skipped},保留 ${result.kept},进化 ${result.evolved},新链接 ${result.connections},回溯更新 ${result.updates}。${sourceLabel}。`, ); } else { updateManualActionUiState( runtime, "强制进化无变更", `已完成整合判定,但本轮没有图谱变化。${sourceLabel}。`, "idle", ); runtime.toastr.info( `已完成整合判定,但本轮没有产生图谱变更。${sourceLabel}。`, ); } return { handledToast: true, requestDispatched: true, mutated, result, candidateSource: candidateResolution.source, }; } catch (error) { updateManualActionUiState( runtime, "强制进化失败", error?.message || String(error), "error", ); throw error; } } export async function onUndoLastMaintenanceController(runtime) { const graph = runtime.getCurrentGraph(); if (!graph) return; if (!runtime.ensureGraphMutationReady("撤销最近维护")) return; updateManualActionUiState(runtime, "撤销最近维护中", "正在恢复上一条维护变更", "running"); try { const result = runtime.undoLastMaintenance?.(); if (!result?.ok) { updateManualActionUiState( runtime, "撤销最近维护失败", result?.reason || "当前没有可撤销的维护记录", "warning", ); runtime.toastr.warning(result?.reason || "撤销最近维护失败"); return { handledToast: true }; } runtime.markVectorStateDirty?.("撤销维护后需要重建向量索引"); runtime.saveGraphToChat?.({ reason: "maintenance-undo-complete" }); updateManualActionUiState( runtime, "撤销最近维护完成", result.entry?.summary || result.entry?.action || "已恢复最近维护", "success", ); runtime.toastr.success( `已撤销最近维护:${result.entry?.summary || result.entry?.action || "未知操作"}`, ); return { handledToast: true, result, }; } catch (error) { updateManualActionUiState( runtime, "撤销最近维护失败", error?.message || String(error), "error", ); throw error; } } // ==================== 数据清理 ==================== export async function onClearGraphController(runtime) { if (!runtime.confirm("确定要清空当前图谱?\n\n所有节点和边将被删除,操作不可撤销。")) { return { cancelled: true }; } if (!runtime.ensureGraphMutationReady("清空图谱")) return; const chatId = runtime.getCurrentChatId?.(); if (chatId && typeof runtime.clearCurrentChatRecoveryAnchors === "function") { runtime.clearCurrentChatRecoveryAnchors({ chatId, reason: "manual-clear-graph", immediate: true, clearMetadataFull: true, clearCommitMarker: true, clearPendingPersist: true, }); } const nextGraph = runtime.normalizeGraphRuntimeState( runtime.createEmptyGraph(), runtime.getCurrentChatId(), ); runtime.setCurrentGraph(nextGraph); runtime.clearInjectionState(); runtime.markVectorStateDirty?.("清空图谱后需要重建向量索引"); runtime.setExtractionCount(0); runtime.setLastExtractedItems([]); runtime.saveGraphToChat({ reason: "manual-clear-graph", persistMetadata: true, captureShadow: false, }); runtime.refreshPanelLiveState(); const persistenceState = runtime.getGraphPersistenceState?.() || {}; const remoteSyncMayRestore = Number(persistenceState.lastSyncedRevision || 0) > 0 && String(runtime.getSettings?.()?.cloudStorageMode || "automatic") !== "manual"; runtime.toastr.success( remoteSyncMayRestore ? "当前图谱已清空;若刷新后旧节点重新出现,请再清空服务端同步数据" : "当前图谱已清空", ); return { handledToast: true }; } export async function onClearGraphRangeController(runtime, startSeq, endSeq) { if (!Number.isFinite(startSeq) || !Number.isFinite(endSeq) || startSeq > endSeq) { runtime.toastr.warning("请填写有效的起始和结束楼层"); return { handledToast: true }; } if ( !runtime.confirm( `确定要删除楼层 ${startSeq} ~ ${endSeq} 范围内的所有节点?\n\n操作不可撤销。`, ) ) { return { cancelled: true }; } if (!runtime.ensureGraphMutationReady("按楼层范围清理")) return; const graph = runtime.getCurrentGraph(); if (!graph) return; const nodesToRemove = graph.nodes.filter((node) => { const range = Array.isArray(node.seqRange) ? node.seqRange : [node.seq, node.seq]; const nodeStart = Number(range[0]) || 0; const nodeEnd = Number(range[1]) || 0; return nodeEnd >= startSeq && nodeStart <= endSeq; }); let removedCount = 0; for (const node of nodesToRemove) { if (runtime.removeNode(graph, node.id)) { removedCount += 1; } } if (removedCount > 0) { runtime.markVectorStateDirty?.("按楼层范围清理后需要重建向量索引"); runtime.saveGraphToChat({ reason: "manual-clear-graph-range" }); } runtime.refreshPanelLiveState(); runtime.toastr.success(`已删除楼层 ${startSeq}~${endSeq} 范围内 ${removedCount} 个节点`); return { handledToast: true }; } export async function onClearVectorCacheController(runtime) { if (!runtime.confirm("确定要清空向量缓存?\n\n清空后需要重新构建向量索引。")) { return { cancelled: true }; } const graph = runtime.getCurrentGraph(); if (!graph) { runtime.toastr.warning("当前没有加载的图谱"); return { handledToast: true }; } if (graph.vectorIndexState) { graph.vectorIndexState.hashToNodeId = {}; graph.vectorIndexState.nodeToHash = {}; graph.vectorIndexState.dirty = true; graph.vectorIndexState.dirtyReason = "manual-clear-vector-cache"; graph.vectorIndexState.lastWarning = "向量缓存已手动清空,需要重建索引"; } runtime.saveGraphToChat({ reason: "manual-clear-vector-cache" }); runtime.refreshPanelLiveState(); runtime.toastr.success("向量缓存已清空,请重建向量索引"); return { handledToast: true }; } export async function onClearBatchJournalController(runtime) { if (!runtime.confirm("确定要清空提取历史?\n\n提取批次记录和计数将被重置。")) { return { cancelled: true }; } const graph = runtime.getCurrentGraph(); if (!graph) { runtime.toastr.warning("当前没有加载的图谱"); return { handledToast: true }; } graph.batchJournal = []; if (graph.historyState) { graph.historyState.extractionCount = 0; } runtime.setExtractionCount(0); runtime.saveGraphToChat({ reason: "manual-clear-batch-journal" }); runtime.refreshPanelLiveState(); runtime.toastr.success("提取历史已清空"); return { handledToast: true }; } export async function onDeleteCurrentIdbController(runtime) { const chatId = runtime.getCurrentChatId(); if (!chatId) { runtime.toastr.warning("当前没有聊天上下文"); return { handledToast: true }; } const dbName = runtime.buildBmeDbName(chatId); const restoreSafetyDbName = runtime.buildRestoreSafetyDbName?.(chatId) || ""; const restoreSafetyChatId = typeof runtime.buildRestoreSafetyChatId === "function" ? runtime.buildRestoreSafetyChatId(chatId) : `__restore_safety__${chatId}`; const persistenceState = runtime.getGraphPersistenceState?.() || {}; const hostProfile = String(persistenceState.hostProfile || "generic-st"); const localStoreLabel = hostProfile === "luker" ? "当前聊天的本地缓存(IndexedDB / OPFS,不影响 Luker 侧车主存储)" : "当前聊天的本地图谱存储(IndexedDB / OPFS)"; if ( !runtime.confirm( `确定要删除${localStoreLabel}?\n\n将尝试清理:\n- ${dbName}\n- OPFS 当前聊天目录\n- restore safety 本地副本\n\n操作不可撤销。`, ) ) { return { cancelled: true }; } try { await runtime.closeBmeDb?.(chatId); let deletedIndexedDbCount = 0; await new Promise((resolve, reject) => { const req = indexedDB.deleteDatabase(dbName); req.onsuccess = () => { deletedIndexedDbCount += 1; resolve(); }; req.onerror = () => reject(req.error); req.onblocked = () => resolve(); }); if (restoreSafetyDbName) { await new Promise((resolve, reject) => { const req = indexedDB.deleteDatabase(restoreSafetyDbName); req.onsuccess = () => { deletedIndexedDbCount += 1; resolve(); }; req.onerror = () => reject(req.error); req.onblocked = () => resolve(); }); } const currentOpfsResult = await runtime.deleteCurrentChatOpfsStorage?.(chatId); const restoreSafetyOpfsResult = restoreSafetyChatId && restoreSafetyChatId !== chatId ? await runtime.deleteCurrentChatOpfsStorage?.(restoreSafetyChatId) : null; runtime.clearCachedIndexedDbSnapshot?.(chatId); runtime.clearCachedIndexedDbSnapshot?.(restoreSafetyChatId); if (typeof runtime.clearCurrentChatRecoveryAnchors === "function") { runtime.clearCurrentChatRecoveryAnchors({ chatId, reason: "manual-delete-current-local-storage", immediate: true, clearMetadataFull: true, clearCommitMarker: true, clearPendingPersist: true, }); if (restoreSafetyChatId && restoreSafetyChatId !== chatId) { runtime.clearCurrentChatRecoveryAnchors({ chatId: restoreSafetyChatId, reason: "manual-delete-current-local-storage:restore-safety", immediate: true, clearMetadataFull: false, clearCommitMarker: false, clearPendingPersist: false, }); } } else { runtime.clearCurrentChatCommitMarker?.({ reason: "manual-delete-current-local-storage", immediate: true, resetAcceptedRevision: true, }); } await runtime.refreshCurrentChatLocalStoreBinding?.({ chatId, forceCapabilityRefresh: true, reopenCurrentDb: true, source: "manual-delete-current-local-storage", }); runtime.syncGraphLoadFromLiveContext?.({ source: "manual-delete-current-local-storage", force: true, }); runtime.refreshPanelLiveState?.(); const deletedOpfs = currentOpfsResult?.deleted === true || restoreSafetyOpfsResult?.deleted === true; const remoteSyncMayRestore = Number(runtime.getGraphPersistenceState?.()?.lastSyncedRevision || 0) > 0 && String(runtime.getSettings?.()?.cloudStorageMode || "automatic") !== "manual"; runtime.toastr.success( `已清空当前聊天本地存储:IndexedDB ${deletedIndexedDbCount > 0 ? "已处理" : "无"},OPFS ${deletedOpfs ? "已处理" : "无"}${remoteSyncMayRestore ? ";若刷新后旧图恢复,请再删除服务端同步数据" : ""}`, ); } catch (error) { runtime.toastr.error(`删除失败: ${error?.message || error}`); } return { handledToast: true }; } export async function onDeleteAllIdbController(runtime) { const userInput = runtime.prompt( "此操作会删除所有聊天的 BME 本地图谱存储(IndexedDB / OPFS),不影响 Luker 侧车主存储。\n\n请输入 DELETE 确认:", ); if (userInput !== "DELETE") { if (userInput != null) { runtime.toastr.warning("输入不匹配,操作已取消"); } return { cancelled: true }; } try { await runtime.closeAllBmeDbs?.(); const databases = await indexedDB.databases(); const bmeDbs = databases.filter((db) => String(db.name || "").startsWith("STBME_"), ); if (bmeDbs.length === 0) { runtime.toastr.info("没有找到 BME 本地缓存数据库"); return { handledToast: true }; } let deletedCount = 0; for (const db of bmeDbs) { try { await new Promise((resolve, reject) => { const req = indexedDB.deleteDatabase(db.name); req.onsuccess = () => resolve(); req.onerror = () => reject(req.error); req.onblocked = () => resolve(); }); deletedCount += 1; } catch { // continue deleting others } } const opfsResult = await runtime.deleteAllOpfsStorage?.(); runtime.clearAllCachedIndexedDbSnapshots?.(); const activeChatId = runtime.getCurrentChatId?.(); if (activeChatId) { if (typeof runtime.clearCurrentChatRecoveryAnchors === "function") { runtime.clearCurrentChatRecoveryAnchors({ chatId: activeChatId, reason: "manual-delete-all-local-storage", immediate: true, clearMetadataFull: true, clearCommitMarker: true, clearPendingPersist: true, }); } else { runtime.clearCurrentChatCommitMarker?.({ reason: "manual-delete-all-local-storage", immediate: true, resetAcceptedRevision: true, }); } await runtime.refreshCurrentChatLocalStoreBinding?.({ chatId: activeChatId, forceCapabilityRefresh: true, reopenCurrentDb: true, source: "manual-delete-all-local-storage", }); runtime.syncGraphLoadFromLiveContext?.({ source: "manual-delete-all-local-storage", force: true, }); } runtime.refreshPanelLiveState?.(); if (bmeDbs.length === 0 && opfsResult?.deleted !== true) { runtime.toastr.info("没有找到 BME 本地图谱存储"); return { handledToast: true }; } runtime.toastr.success( `已清空 BME 本地图谱存储:IndexedDB ${deletedCount}/${bmeDbs.length},OPFS ${opfsResult?.deleted ? "已处理" : "无"}`, ); } catch (error) { runtime.toastr.error(`删除失败: ${error?.message || error}`); } return { handledToast: true }; } export async function onDeleteServerSyncFileController(runtime) { const chatId = runtime.getCurrentChatId(); if (!chatId) { runtime.toastr.warning("当前没有聊天上下文"); return { handledToast: true }; } const userInput = runtime.prompt( "此操作会删除当前聊天在服务端的同步数据。\n\n如果该聊天已经升级到远端 v2,同步 manifest 和 chunk 文件都会一起删除。\n\n请输入 DELETE 确认:", ); if (userInput !== "DELETE") { if (userInput != null) { runtime.toastr.warning("输入不匹配,操作已取消"); } return { cancelled: true }; } try { const result = await runtime.deleteRemoteSyncFile(chatId); if (result?.deleted) { runtime.toastr.success(`已删除服务端同步数据: ${result.filename}`); } else { runtime.toastr.info( result?.reason === "not-found" ? "服务端没有找到同步数据" : `删除未成功: ${result?.reason || "未知原因"}`, ); } } catch (error) { runtime.toastr.error(`删除失败: ${error?.message || error}`); } return { handledToast: true }; }