Fix manual action UI status and compression result guards

This commit is contained in:
Youzini-afk
2026-04-07 12:32:02 +08:00
parent c32ce01e4c
commit aa1d194c28
5 changed files with 515 additions and 162 deletions

View File

@@ -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 },
);

View File

@@ -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,
});

View File

@@ -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);

View File

@@ -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();

View File

@@ -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,
};
}