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 };
+}