mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-13 18:31:16 +08:00
Fix manual action UI status and compression result guards
This commit is contained in:
110
compressor.js
110
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 },
|
||||
);
|
||||
|
||||
9
index.js
9
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,
|
||||
});
|
||||
|
||||
10
panel.js
10
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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user