Reorganize modules into layered directories

This commit is contained in:
Youzini-afk
2026-04-08 01:17:47 +08:00
parent 59942541ea
commit feec17f3e3
90 changed files with 284 additions and 219 deletions

862
ui/ui-actions-controller.js Normal file
View File

@@ -0,0 +1,862 @@
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: "用户手动触发全量重建",
}),
);
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 {
const result = await runtime.syncVectorState({
force: true,
purge: runtime.isBackendVectorConfig(config) && !range,
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 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}`,
);
}
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;
}
}