From aa1d194c28cf3f77af8af368220e6ac2210ae099 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Tue, 7 Apr 2026 12:32:02 +0800 Subject: [PATCH] Fix manual action UI status and compression result guards --- compressor.js | 110 +++++++++- index.js | 9 + panel.js | 10 + tests/p0-regressions.mjs | 94 ++++++++ ui-actions-controller.js | 454 +++++++++++++++++++++++++-------------- 5 files changed, 515 insertions(+), 162 deletions(-) diff --git a/compressor.js b/compressor.js index 4e8a94f..7b38afc 100644 --- a/compressor.js +++ b/compressor.js @@ -17,6 +17,7 @@ import { getScopeRegionKey, normalizeMemoryScope, } from "./memory-scope.js"; +import { ensureEventTitle, getNodeDisplayName } from "./node-labels.js"; import { buildTaskExecutionDebugContext, buildTaskLlmPayload, @@ -81,6 +82,92 @@ function resolveCompressionWindow(compression = {}, force = false) { }; } +function normalizeCompressionFieldValue(value) { + if (value == null) return ""; + if (typeof value === "string") return value.trim(); + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + if (Array.isArray(value)) { + return value + .map((item) => normalizeCompressionFieldValue(item)) + .filter(Boolean) + .join(";"); + } + if (typeof value === "object") { + try { + return JSON.stringify(value); + } catch { + return ""; + } + } + return String(value).trim(); +} + +function buildCompressionFallbackSummary(batch = []) { + return batch + .map((node) => + normalizeCompressionFieldValue( + node?.fields?.summary || + node?.fields?.title || + node?.fields?.name || + node?.fields?.insight || + getNodeDisplayName(node), + ), + ) + .filter(Boolean) + .slice(0, 6) + .join(";"); +} + +function normalizeCompressedFields(summaryResult, typeDef, batch = []) { + const rawFields = + summaryResult?.fields && + typeof summaryResult.fields === "object" && + !Array.isArray(summaryResult.fields) + ? summaryResult.fields + : summaryResult && typeof summaryResult === "object" && !Array.isArray(summaryResult) + ? summaryResult + : {}; + const columns = Array.isArray(typeDef?.columns) ? typeDef.columns : []; + const normalized = {}; + + for (const column of columns) { + const key = String(column?.name || "").trim(); + if (!key) continue; + const normalizedValue = normalizeCompressionFieldValue(rawFields[key]); + if (normalizedValue) { + normalized[key] = normalizedValue; + } + } + + const fallbackSummary = buildCompressionFallbackSummary(batch); + if (!normalized.summary && columns.some((column) => column?.name === "summary")) { + normalized.summary = fallbackSummary || "压缩批次摘要缺失"; + } + if (!normalized.insight && columns.some((column) => column?.name === "insight")) { + normalized.insight = fallbackSummary || "压缩批次洞察缺失"; + } + if (!normalized.title && columns.some((column) => column?.name === "title")) { + const titled = ensureEventTitle({ title: rawFields?.title, summary: normalized.summary }); + normalized.title = + normalizeCompressionFieldValue(titled?.title) || + normalizeCompressionFieldValue(rawFields?.name) || + normalizeCompressionFieldValue(batch[batch.length - 1]?.fields?.title) || + normalizeCompressionFieldValue(batch[batch.length - 1]?.fields?.name) || + "压缩节点"; + } + if (!normalized.name && columns.some((column) => column?.name === "name")) { + normalized.name = + normalizeCompressionFieldValue(rawFields?.title) || + normalizeCompressionFieldValue(rawFields?.name) || + normalizeCompressionFieldValue(batch[batch.length - 1]?.fields?.name) || + "压缩节点"; + } + + return normalized; +} + /** * 对指定类型执行层级压缩 * @@ -184,10 +271,20 @@ async function compressLevel({ settings, ); if (!summaryResult) continue; + const normalizedFields = normalizeCompressedFields( + summaryResult, + typeDef, + batch, + ); + if (Object.keys(normalizedFields).length === 0) { + throw new Error( + `压缩结果缺少可用 fields,无法创建 ${typeDef?.label || typeDef?.id || "压缩"} 节点`, + ); + } const compressedNode = createNode({ type: typeDef.id, - fields: summaryResult.fields, + fields: normalizedFields, seq: batch[batch.length - 1].seq, seqRange: [ batch[0].seqRange?.[0] ?? batch[0].seq, @@ -200,9 +297,16 @@ async function compressLevel({ compressedNode.level = level + 1; compressedNode.childIds = batch.map((n) => n.id); - if (isDirectVectorConfig(embeddingConfig) && summaryResult.fields.summary) { + const embeddingText = + normalizeCompressionFieldValue( + normalizedFields.summary || + normalizedFields.insight || + normalizedFields.title || + normalizedFields.name, + ) || ""; + if (isDirectVectorConfig(embeddingConfig) && embeddingText) { const vec = await embedText( - summaryResult.fields.summary, + embeddingText, embeddingConfig, { signal }, ); diff --git a/index.js b/index.js index 93dead7..fa7ead1 100644 --- a/index.js +++ b/index.js @@ -10455,8 +10455,10 @@ async function onManualCompress() { getSchema, getSettings, inspectCompressionCandidates: inspectAutoCompressionCandidates, + refreshPanelLiveState, recordMaintenanceAction, recordGraphMutation, + setRuntimeStatus, toastr, }); } @@ -10646,8 +10648,10 @@ async function onManualSleep() { ensureGraphMutationReady, getCurrentGraph: () => currentGraph, getSettings, + refreshPanelLiveState, recordMaintenanceAction, recordGraphMutation, + setRuntimeStatus, sleepCycle, toastr, }); @@ -10662,7 +10666,9 @@ async function onManualSynopsis() { getCurrentGraph: () => currentGraph, getSchema, getSettings, + refreshPanelLiveState, recordGraphMutation, + setRuntimeStatus, toastr, }); } @@ -10677,8 +10683,10 @@ async function onManualEvolve() { getEmbeddingConfig, getLastExtractedItems: () => lastExtractedItems, getSettings, + refreshPanelLiveState, recordMaintenanceAction, recordGraphMutation, + setRuntimeStatus, toastr, validateVectorConfig, }); @@ -10691,6 +10699,7 @@ async function onUndoLastMaintenance() { markVectorStateDirty, refreshPanelLiveState, saveGraphToChat, + setRuntimeStatus, toastr, undoLastMaintenance: undoLastMaintenanceAction, }); diff --git a/panel.js b/panel.js index 902ed45..e924b56 100644 --- a/panel.js +++ b/panel.js @@ -1755,6 +1755,7 @@ function _bindActions() { btn.disabled = true; btn.style.opacity = "0.5"; + _showActionProgressUi(label); toastr.info(`${label} 进行中…`, "ST-BME", { timeOut: 2000 }); try { @@ -1804,6 +1805,7 @@ function _bindActions() { btn.style.opacity = "0.5"; } + _showActionProgressUi("范围重建"); toastr.info("范围重建 进行中…", "ST-BME", { timeOut: 2000 }); try { @@ -1855,6 +1857,7 @@ function _bindActions() { btn.style.opacity = "0.5"; } + _showActionProgressUi("重新提取"); try { await _actionHandlers.reroll?.({ fromFloor: Number.isFinite(fromFloor) ? fromFloor : undefined, @@ -5810,6 +5813,13 @@ function _refreshRuntimeStatus() { _refreshGraphAvailabilityState(); } +function _showActionProgressUi(label, meta = "请稍候…") { + _setText("bme-status-text", `${label}中`); + _setText("bme-status-meta", meta); + _setText("bme-panel-status", `${label}中`); + updateFloatingBallStatus("running", `${label}中`); +} + function _patchSettings(patch = {}, options = {}) { const settings = _updateSettings?.(patch) || _getSettings?.() || {}; if (options.refreshGuards) _refreshGuardedConfigStates(settings); diff --git a/tests/p0-regressions.mjs b/tests/p0-regressions.mjs index 8028d37..04c9fa8 100644 --- a/tests/p0-regressions.mjs +++ b/tests/p0-regressions.mjs @@ -1891,6 +1891,61 @@ async function testVectorIndexKeepsDirtyOnDirectPartialEmbeddingFailure() { } } +async function testCompressTypeAcceptsTopLevelFieldsResult() { + const graph = createEmptyGraph(); + const typeDef = { + id: "event", + label: "事件", + columns: [ + { name: "title" }, + { name: "summary" }, + { name: "participants" }, + { name: "status" }, + ], + compression: { + mode: "hierarchical", + fanIn: 2, + threshold: 2, + keepRecentLeaves: 0, + }, + }; + const first = makeEvent(1, "事件甲"); + const second = makeEvent(2, "事件乙"); + addNode(graph, first); + addNode(graph, second); + + const restoreOverrides = pushTestOverrides({ + llm: { + async callLLMForJSON() { + return { + title: "压缩事件", + summary: "顶层返回的合并摘要", + participants: "Alice", + status: "done", + }; + }, + }, + }); + + try { + const result = await compressType({ + graph, + typeDef, + embeddingConfig: null, + force: true, + settings: {}, + }); + assert.equal(result.created, 1); + const compressed = graph.nodes.find( + (node) => node.level === 1 && !node.archived, + ); + assert.equal(compressed?.fields?.summary, "顶层返回的合并摘要"); + assert.equal(compressed?.fields?.title, "压缩事件"); + } finally { + restoreOverrides(); + } +} + async function testConsolidatorMergeFallbackKeepsNodeWhenTargetMissing() { const graph = createEmptyGraph(); const target = createNode({ @@ -5374,6 +5429,43 @@ async function testManualCompressUsesForcedCompressionAndPersistsRealMutation() assert.equal(result?.mutated, true); } +async function testManualCompressUpdatesRuntimeStatusForPanelUi() { + const statusUpdates = []; + const graph = { nodes: [], historyState: {} }; + + const result = await onManualCompressController({ + getCurrentGraph: () => graph, + ensureGraphMutationReady: () => true, + getSchema: () => [{ id: "event", compression: { mode: "hierarchical" } }], + inspectCompressionCandidates: () => ({ + hasCandidates: true, + reason: "", + }), + cloneGraphSnapshot: (value) => JSON.parse(JSON.stringify(value ?? null)), + compressAll: async () => ({ created: 1, archived: 2 }), + getEmbeddingConfig: () => ({}), + getSettings: () => ({}), + recordMaintenanceAction() {}, + recordGraphMutation: async () => {}, + buildMaintenanceSummary: () => "手动压缩", + setRuntimeStatus(text, meta = "", level = "idle") { + statusUpdates.push({ text, meta, level }); + }, + refreshPanelLiveState() {}, + toastr: { + info() {}, + success() {}, + }, + }); + + assert.equal(result?.handledToast, true); + assert.equal(result?.mutated, true); + assert.equal(statusUpdates[0]?.text, "手动压缩中"); + assert.equal(statusUpdates[0]?.level, "running"); + assert.equal(statusUpdates.at(-1)?.text, "手动压缩完成"); + assert.equal(statusUpdates.at(-1)?.level, "success"); +} + async function testManualEvolveFallsBackToLatestExtractionBatchAfterRefresh() { const graph = { nodes: [ @@ -5533,6 +5625,7 @@ async function testManualSleepExplainsThatItIsLocalOnlyWhenNothingChanges() { await testCompressorMigratesEdgesToCompressedNode(); await testVectorIndexKeepsDirtyOnDirectPartialEmbeddingFailure(); +await testCompressTypeAcceptsTopLevelFieldsResult(); await testExtractorFailsOnUnknownOperation(); await testExtractorNormalizesFlatCreateOperation(); await testExtractorNormalizesArrayPayloadAndPreservesScopeField(); @@ -5618,6 +5711,7 @@ await testSynopsisUsesPromptMessagesWithoutFallbackSystemPrompt(); await testReflectionUsesPromptMessagesWithoutFallbackSystemPrompt(); await testManualCompressSkipsWithoutCandidatesAndDoesNotPretendItRan(); await testManualCompressUsesForcedCompressionAndPersistsRealMutation(); +await testManualCompressUpdatesRuntimeStatusForPanelUi(); await testManualEvolveFallsBackToLatestExtractionBatchAfterRefresh(); await testManualEvolveWarnsOnInvalidVectorConfigInsteadOfPretendingComplete(); await testManualSleepExplainsThatItIsLocalOnlyWhenNothingChanges(); diff --git a/ui-actions-controller.js b/ui-actions-controller.js index 6b8d04a..c792576 100644 --- a/ui-actions-controller.js +++ b/ui-actions-controller.js @@ -119,6 +119,13 @@ function describeManualEvolutionSource(source, count) { } } +function updateManualActionUiState(runtime, text, meta = "", level = "idle") { + if (typeof runtime?.setRuntimeStatus === "function") { + runtime.setRuntimeStatus(text, meta, level); + } + runtime?.refreshPanelLiveState?.(); +} + export async function onViewGraphController(runtime) { const graph = runtime.getCurrentGraph(); if (!graph) { @@ -211,56 +218,82 @@ export async function onManualCompressController(runtime) { const graph = runtime.getCurrentGraph(); if (!graph) return; if (!runtime.ensureGraphMutationReady("手动压缩")) return; + updateManualActionUiState(runtime, "手动压缩中", "正在检查可压缩候选组", "running"); - const schema = runtime.getSchema(); - const inspection = runtime.inspectCompressionCandidates?.(graph, schema, true); - if (inspection && !inspection.hasCandidates) { - runtime.toastr.info( - String(inspection.reason || "当前没有可压缩候选组,本次未发起 LLM 压缩"), + 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: false, - mutated: false, - reason: String(inspection.reason || ""), + requestDispatched: true, + mutated, + result, }; - } - - 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"], - }); - runtime.toastr.success( - `手动压缩完成:新建 ${result.created},归档 ${result.archived}`, + } catch (error) { + updateManualActionUiState( + runtime, + "手动压缩失败", + error?.message || String(error), + "error", ); - } else { - runtime.toastr.info("已尝试手动压缩,但本轮没有产生可持久化变化"); + throw error; } - - return { - handledToast: true, - requestDispatched: true, - mutated, - result, - }; } export async function onExportGraphController(runtime) { @@ -538,150 +571,253 @@ export async function onManualSleepController(runtime) { const graph = runtime.getCurrentGraph(); if (!graph) return; if (!runtime.ensureGraphMutationReady("执行遗忘")) return; + updateManualActionUiState(runtime, "执行遗忘中", "正在评估可归档节点", "running"); - 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"], - }); - runtime.toastr.success(`执行遗忘完成:归档 ${result.forgotten} 个节点`); - } else { - runtime.toastr.info( - "当前没有符合遗忘条件的节点。本操作只做本地图清理,不会发送 LLM 请求。", + 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; } - return { - handledToast: true, - requestDispatched: false, - mutated, - result, - }; } export async function onManualSynopsisController(runtime) { const graph = runtime.getCurrentGraph(); if (!graph) return; if (!runtime.ensureGraphMutationReady("更新概要")) return; + updateManualActionUiState(runtime, "更新概要中", "正在生成新的概要节点", "running"); - const beforeSnapshot = runtime.cloneGraphSnapshot(graph); - await runtime.generateSynopsis({ - graph, - schema: runtime.getSchema(), - currentSeq: runtime.getCurrentChatSeq(), - customPrompt: undefined, - settings: runtime.getSettings(), - }); - await runtime.recordGraphMutation({ - beforeSnapshot, - artifactTags: ["synopsis"], - }); - runtime.toastr.success("概要生成完成"); + try { + const beforeSnapshot = runtime.cloneGraphSnapshot(graph); + await runtime.generateSynopsis({ + graph, + schema: runtime.getSchema(), + currentSeq: runtime.getCurrentChatSeq(), + customPrompt: undefined, + settings: runtime.getSettings(), + }); + await runtime.recordGraphMutation({ + beforeSnapshot, + artifactTags: ["synopsis"], + }); + updateManualActionUiState(runtime, "概要生成完成", "概要节点已更新", "success"); + runtime.toastr.success("概要生成完成"); + return { + handledToast: true, + requestDispatched: true, + mutated: true, + }; + } catch (error) { + updateManualActionUiState( + runtime, + "概要生成失败", + error?.message || String(error), + "error", + ); + throw error; + } } 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}。`, + ); + } - const embeddingConfig = runtime.getEmbeddingConfig(); - const vectorValidation = runtime.validateVectorConfig?.(embeddingConfig); - if (vectorValidation && !vectorValidation.valid) { - runtime.toastr.warning(vectorValidation.error); return { handledToast: true, - requestDispatched: false, - mutated: false, - reason: vectorValidation.error, + requestDispatched: true, + mutated, + result, + candidateSource: candidateResolution.source, }; - } - - const candidateResolution = resolveManualEvolutionCandidates(runtime, graph); - const candidateIds = candidateResolution.ids; - if (candidateIds.length === 0) { - runtime.toastr.info("当前没有可用于进化的最近提取节点,本次未发起整合请求"); - return { - handledToast: true, - requestDispatched: false, - mutated: false, - reason: "no-candidates", - }; - } - - const beforeSnapshot = runtime.cloneGraphSnapshot(graph); - const settings = runtime.getSettings(); - 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"], - }); - runtime.toastr.success( - `强制进化完成:合并 ${result.merged},跳过 ${result.skipped},保留 ${result.kept},进化 ${result.evolved},新链接 ${result.connections},回溯更新 ${result.updates}。${sourceLabel}。`, - ); - } else { - runtime.toastr.info( - `已完成整合判定,但本轮没有产生图谱变更。${sourceLabel}。`, + } catch (error) { + updateManualActionUiState( + runtime, + "强制进化失败", + error?.message || String(error), + "error", ); + throw error; } - - return { - handledToast: true, - requestDispatched: true, - mutated, - result, - candidateSource: candidateResolution.source, - }; } export async function onUndoLastMaintenanceController(runtime) { const graph = runtime.getCurrentGraph(); if (!graph) return; if (!runtime.ensureGraphMutationReady("撤销最近维护")) return; + updateManualActionUiState(runtime, "撤销最近维护中", "正在恢复上一条维护变更", "running"); - const result = runtime.undoLastMaintenance?.(); - if (!result?.ok) { - runtime.toastr.warning(result?.reason || "撤销最近维护失败"); - return { handledToast: true }; + 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; } - - runtime.markVectorStateDirty?.("撤销维护后需要重建向量索引"); - runtime.saveGraphToChat?.({ reason: "maintenance-undo-complete" }); - runtime.refreshPanelLiveState?.(); - runtime.toastr.success( - `已撤销最近维护:${result.entry?.summary || result.entry?.action || "未知操作"}`, - ); - return { - handledToast: true, - result, - }; }