From fd5004e4a3dcb9e741d507ba978b4b040969c2b8 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Thu, 26 Mar 2026 15:43:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8DRoll=E7=B3=BB=E7=BB=9F-?= =?UTF-8?q?=E5=9B=9E=E6=BB=9Ajournal+=E9=87=8D=E6=8F=90=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.js | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ panel.html | 20 +++++++++++++++++ panel.js | 46 +++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+) diff --git a/index.js b/index.js index 0d7da09..cb6a6a4 100644 --- a/index.js +++ b/index.js @@ -3740,6 +3740,71 @@ async function onManualExtract() { } } +async function onReroll({ fromFloor } = {}) { + if (isExtracting) { + toastr.info("记忆提取正在进行中,请稍候"); + return; + } + if (!currentGraph) { + toastr.info("图谱为空,无需重 Roll"); + return; + } + + const context = getContext(); + const chat = context.chat; + if (!Array.isArray(chat) || chat.length === 0) { + toastr.info("当前聊天为空"); + return; + } + + // 确定回滚起点 + let targetFloor = Number.isFinite(fromFloor) ? fromFloor : null; + if (targetFloor === null) { + // 默认:回滚到最新 AI 楼 + targetFloor = getLastProcessedAssistantFloor(); + if (targetFloor < 0) { + toastr.info("尚未有过提取记录,无需重 Roll"); + return; + } + } + + console.log(`[ST-BME] 重 Roll 开始,目标楼层: ${targetFloor}`); + + // 1. 找到受影响的 journal 并回滚 + const recovery = findJournalRecoveryPoint(currentGraph, targetFloor); + if (recovery && recovery.affectedJournals?.length > 0) { + rollbackAffectedJournals(currentGraph, recovery.affectedJournals); + console.log( + `[ST-BME] 已回滚 ${recovery.affectedJournals.length} 个 batch`, + ); + } + + // 2. 重置提取指针 + const newFloor = targetFloor - 1; + currentGraph.historyState.lastProcessedAssistantFloor = newFloor; + currentGraph.lastProcessedSeq = newFloor; + + // 3. 清理 processedMessageHashes 中 >= targetFloor 的条目 + const hashes = currentGraph.historyState.processedMessageHashes || {}; + for (const key of Object.keys(hashes)) { + if (Number(key) >= targetFloor) { + delete hashes[key]; + } + } + + // 4. 保存回滚后的状态 + saveGraph(); + + toastr.info( + `已回滚到楼层 ${targetFloor} 之前,开始重新提取…`, + "ST-BME 重 Roll", + { timeOut: 2000 }, + ); + + // 5. 触发重新提取(复用手动提取逻辑) + await onManualExtract(); +} + async function onManualSleep() { if (!currentGraph) return; const beforeSnapshot = cloneGraphSnapshot(currentGraph); @@ -3917,6 +3982,7 @@ async function onReembedDirect() { rebuildVectorIndex: () => onRebuildVectorIndex(), rebuildVectorRange: (range) => onRebuildVectorIndex(range), reembedDirect: onReembedDirect, + reroll: onReroll, }, }); diff --git a/panel.html b/panel.html index b9dedf3..0ed7cd6 100644 --- a/panel.html +++ b/panel.html @@ -255,6 +255,26 @@ 执行遗忘 + + +
+
+ 重新提取:回滚指定楼层及之后的提取结果并重做。留空则只重做最新 AI 楼。 +
+
+ + +
diff --git a/panel.js b/panel.js index b2fe6da..318ee91 100644 --- a/panel.js +++ b/panel.js @@ -828,6 +828,52 @@ function _bindActions() { } } }); + + // 重新提取 (reroll) 绑定 + document + .getElementById("bme-act-reroll") + ?.addEventListener("click", async () => { + const btn = document.getElementById("bme-act-reroll"); + if (btn?.disabled) return; + + const floorStr = document.getElementById("bme-reroll-floor")?.value; + const fromFloor = _parseOptionalInt(floorStr); + const desc = Number.isFinite(fromFloor) + ? `从楼层 ${fromFloor} 开始回滚并重新提取` + : "回滚最新 AI 楼并重新提取"; + + if (!confirm(`确认要重新提取吗?\n\n${desc}\n\n已提取的记忆节点将被回滚。`)) { + return; + } + + if (btn) { + btn.disabled = true; + btn.style.opacity = "0.5"; + } + + try { + await _actionHandlers.reroll?.({ + fromFloor: Number.isFinite(fromFloor) ? fromFloor : undefined, + }); + _refreshDashboard(); + _refreshGraph(); + if ( + document + .getElementById("bme-pane-memory") + ?.classList.contains("active") + ) { + _refreshMemoryBrowser(); + } + } catch (error) { + console.error("[ST-BME] Action reroll failed:", error); + toastr.error(`重新提取失败: ${error?.message || error}`, "ST-BME"); + } finally { + if (btn) { + btn.disabled = false; + btn.style.opacity = ""; + } + } + }); } function _refreshConfigTab() {