From 29af3d164e25c7a19656d153a8ffd9c7211cc744 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Wed, 8 Apr 2026 14:28:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E3=80=8C=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=B8=85=E7=90=86=E3=80=8D=E9=85=8D=E7=BD=AE=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 图谱清理:清空当前图谱、按楼层范围删除节点 - 缓存清理:清空向量缓存、清空提取历史 - 存储清理:清空当前/全部 IDB、清空服务端同步文件 - 高危操作全部需要 confirm 弹窗确认 - 清空全部 IDB 和清空服务端同步文件需要输入 DELETE 确认 --- index.js | 79 +++++++++++++ style.css | 15 +++ ui/panel.html | 158 ++++++++++++++++++++++++++ ui/panel.js | 56 +++++++++ ui/ui-actions-controller.js | 218 ++++++++++++++++++++++++++++++++++++ 5 files changed, 526 insertions(+) diff --git a/index.js b/index.js index 36eb4fa..36ee194 100644 --- a/index.js +++ b/index.js @@ -27,6 +27,7 @@ import { import { autoSyncOnChatChange, autoSyncOnVisibility, + deleteRemoteSyncFile, scheduleUpload, syncNow, } from "./sync/bme-sync.js"; @@ -212,6 +213,13 @@ import { onTestMemoryLLMController, onViewGraphController, onViewLastInjectionController, + onClearGraphController, + onClearGraphRangeController, + onClearVectorCacheController, + onClearBatchJournalController, + onDeleteCurrentIdbController, + onDeleteAllIdbController, + onDeleteServerSyncFileController, } from "./ui/ui-actions-controller.js"; import { clampInt, @@ -10917,6 +10925,70 @@ async function onReembedDirect() { }); } +// ==================== 数据清理 ==================== + +const _cleanupRuntime = () => ({ + confirm: (msg) => (typeof globalThis.confirm === "function" ? globalThis.confirm(msg) : false), + prompt: (msg) => (typeof globalThis.prompt === "function" ? globalThis.prompt(msg) : null), + createEmptyGraph, + clearInjectionState, + ensureGraphMutationReady, + getCurrentChatId, + getCurrentGraph: () => currentGraph, + markVectorStateDirty: (reason) => { + if (currentGraph?.vectorIndexState) { + currentGraph.vectorIndexState.dirty = true; + currentGraph.vectorIndexState.dirtyReason = reason; + } + }, + normalizeGraphRuntimeState, + refreshPanelLiveState, + removeNode: (graph, nodeId) => removeNode(graph, nodeId), + saveGraphToChat, + setCurrentGraph: (graph) => { currentGraph = graph; }, + setExtractionCount: (count) => { + if (currentGraph?.historyState) { + currentGraph.historyState.extractionCount = count; + } + }, + setLastExtractedItems: () => { lastExtractedItems = []; }, + buildBmeDbName, + closeBmeDb: null, + deleteRemoteSyncFile: (chatId) => deleteRemoteSyncFile(chatId, { + fetch: globalThis.fetch?.bind(globalThis), + getRequestHeaders: typeof getRequestHeaders === "function" ? getRequestHeaders : undefined, + }), + toastr, +}); + +async function onClearGraph() { + return await onClearGraphController(_cleanupRuntime()); +} + +async function onClearGraphRange(startSeq, endSeq) { + return await onClearGraphRangeController(_cleanupRuntime(), startSeq, endSeq); +} + +async function onClearVectorCache() { + return await onClearVectorCacheController(_cleanupRuntime()); +} + +async function onClearBatchJournal() { + return await onClearBatchJournalController(_cleanupRuntime()); +} + +async function onDeleteCurrentIdb() { + return await onDeleteCurrentIdbController(_cleanupRuntime()); +} + +async function onDeleteAllIdb() { + return await onDeleteAllIdbController(_cleanupRuntime()); +} + +async function onDeleteServerSyncFile() { + return await onDeleteServerSyncFileController(_cleanupRuntime()); +} + // ==================== 初始化 ==================== (async function init() { @@ -10953,6 +11025,13 @@ async function onReembedDirect() { rebuildVectorRange: (range) => onRebuildVectorIndex(range), reembedDirect: onReembedDirect, reroll: onReroll, + clearGraph: onClearGraph, + clearGraphRange: (startSeq, endSeq) => onClearGraphRange(startSeq, endSeq), + clearVectorCache: onClearVectorCache, + clearBatchJournal: onClearBatchJournal, + deleteCurrentIdb: onDeleteCurrentIdb, + deleteAllIdb: onDeleteAllIdb, + deleteServerSyncFile: onDeleteServerSyncFile, }, console, document, diff --git a/style.css b/style.css index 81030a3..6e3b80c 100644 --- a/style.css +++ b/style.css @@ -1350,6 +1350,21 @@ margin-bottom: 10px; } +.bme-cleanup-warning-text { + color: #ffc54f; + border-left: 3px solid #ffc54f; + padding-left: 10px; + margin-top: 12px; + display: flex; + align-items: flex-start; + gap: 6px; +} + +.bme-cleanup-warning-text i { + flex-shrink: 0; + margin-top: 1px; +} + .bme-config-subgroup + .bme-config-subgroup { margin-top: 16px; padding-top: 16px; diff --git a/ui/panel.html b/ui/panel.html index aef9fc1..811b15f 100644 --- a/ui/panel.html +++ b/ui/panel.html @@ -144,6 +144,14 @@ 面板外观 + @@ -542,6 +550,14 @@ 面板外观 +
@@ -2327,6 +2343,148 @@
+ +
+
+
数据清理
+

图谱、缓存与存储清理

+

+ 在这里执行高危清理操作。所有操作均需二次确认,部分操作不可撤销。 +

+
+ + +
+
+
+
图谱清理
+
+ 清空整个图谱或删除指定楼层范围内的记忆节点。操作不可撤销。 +
+
+
+
+ + +
+
+
+ 按楼层范围清理:删除指定楼层范围内的所有节点和相关边。留空则不执行。 +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
缓存清理
+
+ 清空运行时向量缓存或提取历史。不影响已持久化的图谱节点。 +
+
+
+
+ + +
+
+ + +
+
+
+
数据存储清理
+
+ 删除本地 IndexedDB 缓存或服务端同步文件。 +
+
+
+
+ + + +
+
+ + 「清空全部 BME IDB」和「清空服务端同步文件」需要输入 DELETE 确认。 +
+
+
diff --git a/ui/panel.js b/ui/panel.js index 62f41f5..1e5bdfa 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -1990,6 +1990,12 @@ function _bindActions() { "bme-act-undo-maintenance": "undoMaintenance", "bme-act-vector-rebuild": "rebuildVectorIndex", "bme-act-vector-reembed": "reembedDirect", + "bme-act-clear-graph": "clearGraph", + "bme-act-clear-vector-cache": "clearVectorCache", + "bme-act-clear-batch-journal": "clearBatchJournal", + "bme-act-delete-current-idb": "deleteCurrentIdb", + "bme-act-delete-all-idb": "deleteAllIdb", + "bme-act-delete-server-sync": "deleteServerSyncFile", }; const actionLabels = { @@ -2004,6 +2010,12 @@ function _bindActions() { undoMaintenance: "撤销最近维护", rebuildVectorIndex: "重建向量", reembedDirect: "直连重嵌", + clearGraph: "清空图谱", + clearVectorCache: "清空向量缓存", + clearBatchJournal: "清空提取历史", + deleteCurrentIdb: "清空当前 IDB", + deleteAllIdb: "清空全部 IDB", + deleteServerSyncFile: "清空服务端同步文件", }; for (const [elementId, actionKey] of Object.entries(bindings)) { @@ -2148,6 +2160,50 @@ function _bindActions() { _refreshGraphAvailabilityState(); } }); + + // 按楼层范围清理 (cleanup) + document + .getElementById("bme-act-clear-graph-range") + ?.addEventListener("click", async () => { + const btn = document.getElementById("bme-act-clear-graph-range"); + if (btn?.disabled) return; + + const startStr = document.getElementById("bme-cleanup-range-start")?.value; + const endStr = document.getElementById("bme-cleanup-range-end")?.value; + const startSeq = _parseOptionalInt(startStr); + const endSeq = _parseOptionalInt(endStr); + + if (btn) { + btn.disabled = true; + btn.style.opacity = "0.5"; + } + + _showActionProgressUi("按楼层范围清理"); + try { + await _actionHandlers.clearGraphRange?.( + Number.isFinite(startSeq) ? startSeq : null, + Number.isFinite(endSeq) ? endSeq : null, + ); + _refreshDashboard(); + _refreshGraph(); + if ( + document + .getElementById("bme-pane-memory") + ?.classList.contains("active") + ) { + _refreshMemoryBrowser(); + } + } catch (error) { + console.error("[ST-BME] Action clearGraphRange failed:", error); + toastr.error(`按楼层范围清理失败: ${error?.message || error}`, "ST-BME"); + } finally { + if (btn) { + btn.style.opacity = ""; + } + _refreshRuntimeStatus(); + _refreshGraphAvailabilityState(); + } + }); } function _refreshConfigTab() { diff --git a/ui/ui-actions-controller.js b/ui/ui-actions-controller.js index a6236bc..c66f729 100644 --- a/ui/ui-actions-controller.js +++ b/ui/ui-actions-controller.js @@ -860,3 +860,221 @@ export async function onUndoLastMaintenanceController(runtime) { throw error; } } + +// ==================== 数据清理 ==================== + +export async function onClearGraphController(runtime) { + if (!runtime.confirm("确定要清空当前图谱?\n\n所有节点和边将被删除,操作不可撤销。")) { + return { cancelled: true }; + } + if (!runtime.ensureGraphMutationReady("清空图谱")) return; + + const nextGraph = runtime.normalizeGraphRuntimeState( + runtime.createEmptyGraph(), + runtime.getCurrentChatId(), + ); + runtime.setCurrentGraph(nextGraph); + runtime.clearInjectionState(); + runtime.markVectorStateDirty?.("清空图谱后需要重建向量索引"); + runtime.setExtractionCount(0); + runtime.setLastExtractedItems([]); + runtime.saveGraphToChat({ reason: "manual-clear-graph" }); + runtime.refreshPanelLiveState(); + runtime.toastr.success("当前图谱已清空"); + return { handledToast: true }; +} + +export async function onClearGraphRangeController(runtime, startSeq, endSeq) { + if (!Number.isFinite(startSeq) || !Number.isFinite(endSeq) || startSeq > endSeq) { + runtime.toastr.warning("请填写有效的起始和结束楼层"); + return { handledToast: true }; + } + if ( + !runtime.confirm( + `确定要删除楼层 ${startSeq} ~ ${endSeq} 范围内的所有节点?\n\n操作不可撤销。`, + ) + ) { + return { cancelled: true }; + } + if (!runtime.ensureGraphMutationReady("按楼层范围清理")) return; + + const graph = runtime.getCurrentGraph(); + if (!graph) return; + + const nodesToRemove = graph.nodes.filter((node) => { + const range = Array.isArray(node.seqRange) ? node.seqRange : [node.seq, node.seq]; + const nodeStart = Number(range[0]) || 0; + const nodeEnd = Number(range[1]) || 0; + return nodeEnd >= startSeq && nodeStart <= endSeq; + }); + + let removedCount = 0; + for (const node of nodesToRemove) { + if (runtime.removeNode(graph, node.id)) { + removedCount += 1; + } + } + + if (removedCount > 0) { + runtime.markVectorStateDirty?.("按楼层范围清理后需要重建向量索引"); + runtime.saveGraphToChat({ reason: "manual-clear-graph-range" }); + } + runtime.refreshPanelLiveState(); + runtime.toastr.success(`已删除楼层 ${startSeq}~${endSeq} 范围内 ${removedCount} 个节点`); + return { handledToast: true }; +} + +export async function onClearVectorCacheController(runtime) { + if (!runtime.confirm("确定要清空向量缓存?\n\n清空后需要重新构建向量索引。")) { + return { cancelled: true }; + } + + const graph = runtime.getCurrentGraph(); + if (!graph) { + runtime.toastr.warning("当前没有加载的图谱"); + return { handledToast: true }; + } + + if (graph.vectorIndexState) { + graph.vectorIndexState.hashToNodeId = {}; + graph.vectorIndexState.nodeToHash = {}; + graph.vectorIndexState.dirty = true; + graph.vectorIndexState.dirtyReason = "manual-clear-vector-cache"; + graph.vectorIndexState.lastWarning = "向量缓存已手动清空,需要重建索引"; + } + + runtime.saveGraphToChat({ reason: "manual-clear-vector-cache" }); + runtime.refreshPanelLiveState(); + runtime.toastr.success("向量缓存已清空,请重建向量索引"); + return { handledToast: true }; +} + +export async function onClearBatchJournalController(runtime) { + if (!runtime.confirm("确定要清空提取历史?\n\n提取批次记录和计数将被重置。")) { + return { cancelled: true }; + } + + const graph = runtime.getCurrentGraph(); + if (!graph) { + runtime.toastr.warning("当前没有加载的图谱"); + return { handledToast: true }; + } + + graph.batchJournal = []; + if (graph.historyState) { + graph.historyState.extractionCount = 0; + } + runtime.setExtractionCount(0); + runtime.saveGraphToChat({ reason: "manual-clear-batch-journal" }); + runtime.refreshPanelLiveState(); + runtime.toastr.success("提取历史已清空"); + return { handledToast: true }; +} + +export async function onDeleteCurrentIdbController(runtime) { + const chatId = runtime.getCurrentChatId(); + if (!chatId) { + runtime.toastr.warning("当前没有聊天上下文"); + return { handledToast: true }; + } + + const dbName = runtime.buildBmeDbName(chatId); + if ( + !runtime.confirm( + `确定要删除当前聊天的本地缓存数据库?\n\n目标: ${dbName}\n操作不可撤销。`, + ) + ) { + return { cancelled: true }; + } + + try { + await runtime.closeBmeDb?.(chatId); + await new Promise((resolve, reject) => { + const req = indexedDB.deleteDatabase(dbName); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + req.onblocked = () => resolve(); + }); + runtime.toastr.success(`已删除数据库 ${dbName}`); + } catch (error) { + runtime.toastr.error(`删除失败: ${error?.message || error}`); + } + return { handledToast: true }; +} + +export async function onDeleteAllIdbController(runtime) { + const userInput = runtime.prompt( + "此操作会删除所有聊天的 BME 本地缓存数据库,不可恢复。\n\n请输入 DELETE 确认:", + ); + if (userInput !== "DELETE") { + if (userInput != null) { + runtime.toastr.warning("输入不匹配,操作已取消"); + } + return { cancelled: true }; + } + + try { + const databases = await indexedDB.databases(); + const bmeDbs = databases.filter((db) => + String(db.name || "").startsWith("STBME_"), + ); + if (bmeDbs.length === 0) { + runtime.toastr.info("没有找到 BME 本地缓存数据库"); + return { handledToast: true }; + } + + let deletedCount = 0; + for (const db of bmeDbs) { + try { + await new Promise((resolve, reject) => { + const req = indexedDB.deleteDatabase(db.name); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + req.onblocked = () => resolve(); + }); + deletedCount += 1; + } catch { + // continue deleting others + } + } + + runtime.toastr.success(`已删除 ${deletedCount}/${bmeDbs.length} 个 BME 数据库`); + } catch (error) { + runtime.toastr.error(`删除失败: ${error?.message || error}`); + } + return { handledToast: true }; +} + +export async function onDeleteServerSyncFileController(runtime) { + const chatId = runtime.getCurrentChatId(); + if (!chatId) { + runtime.toastr.warning("当前没有聊天上下文"); + return { handledToast: true }; + } + + const userInput = runtime.prompt( + "此操作会删除当前聊天在服务端的同步文件,不可恢复。\n\n请输入 DELETE 确认:", + ); + if (userInput !== "DELETE") { + if (userInput != null) { + runtime.toastr.warning("输入不匹配,操作已取消"); + } + return { cancelled: true }; + } + + try { + const result = await runtime.deleteRemoteSyncFile(chatId); + if (result?.deleted) { + runtime.toastr.success(`已删除服务端同步文件: ${result.filename}`); + } else { + runtime.toastr.info( + result?.reason === "not-found" + ? "服务端没有找到同步文件" + : `删除未成功: ${result?.reason || "未知原因"}`, + ); + } + } catch (error) { + runtime.toastr.error(`删除失败: ${error?.message || error}`); + } + return { handledToast: true }; +}