diff --git a/graph/summary-state.js b/graph/summary-state.js index 598dc21..522ab09 100644 --- a/graph/summary-state.js +++ b/graph/summary-state.js @@ -73,6 +73,7 @@ export function normalizeSummaryEntry(entry = {}, options = {}) { sourceTask: String(source.sourceTask || "synopsis").trim() || "synopsis", extractionRange: normalizeNumberRange(source.extractionRange), messageRange: normalizeNumberRange(source.messageRange), + dialogueRange: normalizeNumberRange(source.dialogueRange), sourceBatchIds: normalizeStringArray(source.sourceBatchIds), sourceSummaryIds: normalizeStringArray(source.sourceSummaryIds), sourceNodeIds: normalizeStringArray(source.sourceNodeIds), diff --git a/index.js b/index.js index 898ec05..c4a0e21 100644 --- a/index.js +++ b/index.js @@ -78,6 +78,7 @@ import { } from "./host/event-binding.js"; import { executeExtractionBatchController, + onExtractionTaskController, onManualExtractController, onRerollController, resolveAutoExtractionPlanController, @@ -12762,6 +12763,47 @@ async function onManualExtract(options = {}) { ); } +async function onExtractionTask(options = {}) { + return await onExtractionTaskController( + { + beginStageAbortController, + clampInt, + console, + createEmptyGraph, + ensureGraphMutationReady, + executeExtractionBatch, + finishStageAbortController, + getAssistantTurns, + getContext, + getCurrentChatId, + getCurrentGraph: () => currentGraph, + getGraphMutationBlockReason, + getGraphPersistenceState: () => graphPersistenceState, + getIsExtracting: () => isExtracting, + getLastExtractionStatusLevel: () => lastExtractionStatus?.level || "idle", + getLastProcessedAssistantFloor, + getSettings, + isAbortError, + normalizeGraphRuntimeState, + onManualExtract, + recoverHistoryIfNeeded, + refreshPanelLiveState, + retryPendingGraphPersist, + rollbackGraphForReroll, + setCurrentGraph: (graph) => { + currentGraph = graph; + }, + setIsExtracting: (value) => { + isExtracting = value; + }, + setLastExtractionStatus, + setRuntimeStatus, + toastr, + }, + options, + ); +} + async function onReroll({ fromFloor } = {}) { return await onRerollController( { @@ -12830,7 +12872,7 @@ async function onManualSummaryRollup() { }); } -async function onRebuildSummaryState() { +async function onRebuildSummaryState(options = {}) { return await onRebuildSummaryStateController({ ensureGraphMutationReady, getContext, @@ -12841,7 +12883,7 @@ async function onRebuildSummaryState() { saveGraphToChat, setRuntimeStatus, toastr, - }); + }, options); } async function onClearSummaryState() { @@ -13181,6 +13223,7 @@ async function onRollbackLastRestore() { syncGraphLoadFromLiveContext({ source: "panel-open-sync", }), + extractTask: onExtractionTask, extract: onManualExtract, compress: onManualCompress, sleep: onManualSleep, diff --git a/maintenance/chat-history.js b/maintenance/chat-history.js index d52eec2..22a96b4 100644 --- a/maintenance/chat-history.js +++ b/maintenance/chat-history.js @@ -26,6 +26,157 @@ export function isBmeManagedHiddenMessage( ); } +export function isDialogueGreetingMessage( + message, + { index = null } = {}, +) { + if (!Number.isFinite(index) || index !== 0) return false; + if (!message || typeof message !== "object") return false; + return String(message?.mes ?? "").trim().length > 0; +} + +export function isTrueSystemMessage( + message, + { index = null, chat = null } = {}, +) { + if (!message?.is_system) return false; + if (isDialogueGreetingMessage(message, { index, chat })) return false; + return !isBmeManagedHiddenMessage(message, { index, chat }); +} + +export function isDialogueCountedMessage( + message, + { index = null, chat = null } = {}, +) { + if (!message || typeof message !== "object") return false; + if (!String(message?.mes ?? "").trim()) return false; + return !isTrueSystemMessage(message, { index, chat }); +} + +export function isDialogueAssistantMessage( + message, + { index = null, chat = null } = {}, +) { + if (!isDialogueCountedMessage(message, { index, chat })) return false; + if (isDialogueGreetingMessage(message, { index, chat })) return false; + return Boolean(message) && !message.is_user; +} + +export function buildDialogueFloorMap(chat = []) { + const floorToChatIndex = []; + const chatIndexToFloor = {}; + const floorToRole = {}; + const assistantDialogueFloors = []; + const assistantChatIndices = []; + + if (!Array.isArray(chat)) { + return { + latestDialogueFloor: -1, + floorToChatIndex, + chatIndexToFloor, + floorToRole, + assistantDialogueFloors, + assistantChatIndices, + }; + } + + let currentFloor = -1; + for (let index = 0; index < chat.length; index += 1) { + const message = chat[index]; + if (!isDialogueCountedMessage(message, { index, chat })) continue; + currentFloor += 1; + floorToChatIndex[currentFloor] = index; + chatIndexToFloor[index] = currentFloor; + + if (isDialogueGreetingMessage(message, { index, chat })) { + floorToRole[currentFloor] = "greeting"; + continue; + } + + const role = message?.is_user ? "user" : "assistant"; + floorToRole[currentFloor] = role; + if (role === "assistant") { + assistantDialogueFloors.push(currentFloor); + assistantChatIndices.push(index); + } + } + + return { + latestDialogueFloor: currentFloor, + floorToChatIndex, + chatIndexToFloor, + floorToRole, + assistantDialogueFloors, + assistantChatIndices, + }; +} + +export function normalizeDialogueFloorRange( + chat = [], + startFloor = null, + endFloor = null, +) { + const map = buildDialogueFloorMap(chat); + const latestDialogueFloor = Number(map.latestDialogueFloor); + const hasStart = + startFloor !== null && + startFloor !== undefined && + startFloor !== "" && + Number.isFinite(Number(startFloor)); + const hasEnd = + endFloor !== null && + endFloor !== undefined && + endFloor !== "" && + Number.isFinite(Number(endFloor)); + if (latestDialogueFloor < 0) { + return { + map, + latestDialogueFloor, + valid: false, + reason: "empty-dialogue", + startFloor: null, + endFloor: null, + }; + } + if (!hasStart && hasEnd) { + return { + map, + latestDialogueFloor, + valid: false, + reason: "end-without-start", + startFloor: null, + endFloor: null, + }; + } + const normalizedStart = hasStart + ? Math.max(0, Math.min(latestDialogueFloor, Math.floor(Number(startFloor)))) + : null; + const normalizedEnd = hasEnd + ? Math.max( + normalizedStart ?? 0, + Math.min(latestDialogueFloor, Math.floor(Number(endFloor))), + ) + : hasStart + ? latestDialogueFloor + : null; + + return { + map, + latestDialogueFloor, + valid: true, + reason: "", + startFloor: normalizedStart, + endFloor: normalizedEnd, + }; +} + +export function getDialogueFloorForChatIndex(chat = [], chatIndex = null) { + if (!Number.isFinite(Number(chatIndex))) return null; + const map = buildDialogueFloorMap(chat); + const floor = map.chatIndexToFloor[Math.floor(Number(chatIndex))]; + return Number.isFinite(Number(floor)) ? Number(floor) : null; +} + function cloneChatMessageForPluginView(message) { if (!message || typeof message !== "object") { return message; diff --git a/maintenance/extraction-controller.js b/maintenance/extraction-controller.js index 7cb4ee0..5e5b497 100644 --- a/maintenance/extraction-controller.js +++ b/maintenance/extraction-controller.js @@ -2,6 +2,10 @@ // 通过 runtime 依赖注入,避免直接访问 index.js 模块级状态。 import { debugLog } from "../runtime/debug-logging.js"; +import { + buildDialogueFloorMap, + normalizeDialogueFloorRange, +} from "./chat-history.js"; function toSafeFloor(value, fallback = null) { if (value == null || value === "") return fallback; @@ -92,6 +96,122 @@ function cloneSerializable(value, fallback = null) { } } +function resolveLatestAssistantDialogueFloor(chat = []) { + const map = buildDialogueFloorMap(chat); + const assistantDialogueFloors = Array.isArray(map.assistantDialogueFloors) + ? map.assistantDialogueFloors + : []; + return assistantDialogueFloors.length > 0 + ? assistantDialogueFloors[assistantDialogueFloors.length - 1] + : null; +} + +function resolveRerunDialogueTask(chat = [], options = {}) { + const hasStart = Number.isFinite(Number(options?.startFloor)); + const hasEnd = Number.isFinite(Number(options?.endFloor)); + if (!hasStart && !hasEnd) { + const latestAssistantDialogueFloor = resolveLatestAssistantDialogueFloor(chat); + if (!Number.isFinite(Number(latestAssistantDialogueFloor))) { + return { + valid: false, + reason: "当前没有可重提的 AI 回复", + }; + } + const normalizedRange = normalizeDialogueFloorRange( + chat, + latestAssistantDialogueFloor, + latestAssistantDialogueFloor, + ); + return { + ...normalizedRange, + mode: "current", + requestedStartFloor: null, + requestedEndFloor: null, + }; + } + + const normalizedRange = normalizeDialogueFloorRange( + chat, + options?.startFloor, + options?.endFloor, + ); + return { + ...normalizedRange, + mode: "range", + requestedStartFloor: hasStart ? Number(options.startFloor) : null, + requestedEndFloor: hasEnd ? Number(options.endFloor) : null, + }; +} + +function resolveAssistantTargetRange(chat = [], dialogueRange = [-1, -1]) { + const map = buildDialogueFloorMap(chat); + const assistantDialogueFloors = Array.isArray(map.assistantDialogueFloors) + ? map.assistantDialogueFloors + : []; + const assistantChatIndices = Array.isArray(map.assistantChatIndices) + ? map.assistantChatIndices + : []; + const [startFloor, endFloor] = Array.isArray(dialogueRange) + ? dialogueRange + : [-1, -1]; + const targeted = []; + + for (let index = 0; index < assistantDialogueFloors.length; index += 1) { + const floor = Number(assistantDialogueFloors[index]); + const chatIndex = Number(assistantChatIndices[index]); + if (!Number.isFinite(floor) || !Number.isFinite(chatIndex)) continue; + if (floor < startFloor || floor > endFloor) continue; + targeted.push({ + dialogueFloor: floor, + chatIndex, + }); + } + + return { + map, + targeted, + startAssistantChatIndex: targeted.length > 0 ? targeted[0].chatIndex : null, + endAssistantChatIndex: + targeted.length > 0 ? targeted[targeted.length - 1].chatIndex : null, + latestAssistantDialogueFloor: + assistantDialogueFloors.length > 0 + ? assistantDialogueFloors[assistantDialogueFloors.length - 1] + : null, + }; +} + +function buildRerunFallbackInfo(chat = [], targetDialogueRange = [-1, -1]) { + const assistantRange = resolveAssistantTargetRange(chat, targetDialogueRange); + if (!assistantRange.targeted.length) { + return { + valid: false, + reason: "目标范围内没有可重提的 AI 回复", + fallbackToLatest: false, + ...assistantRange, + }; + } + + const latestTargetedDialogueFloor = Number( + assistantRange.targeted[assistantRange.targeted.length - 1]?.dialogueFloor, + ); + const latestAssistantDialogueFloor = Number( + assistantRange.latestAssistantDialogueFloor, + ); + const fallbackToLatest = + Number.isFinite(latestTargetedDialogueFloor) && + Number.isFinite(latestAssistantDialogueFloor) && + latestTargetedDialogueFloor < latestAssistantDialogueFloor; + + return { + valid: true, + reason: fallbackToLatest + ? "当前图谱对中段范围重提的后缀保留证据不足,已退化为从起始楼层到最新重提" + : "", + fallbackToLatest, + ...assistantRange, + }; +} + function buildCommittedBatchPersistSnapshot( runtime, { @@ -119,6 +239,29 @@ function buildCommittedBatchPersistSnapshot( const range = Array.isArray(processedRange) ? processedRange : [null, null]; const rangeStart = Number.isFinite(Number(range[0])) ? Number(range[0]) : null; const rangeEnd = Number.isFinite(Number(range[1])) ? Number(range[1]) : null; + const dialogueMap = buildDialogueFloorMap(chat); + const processedDialogueRange = [ + Number.isFinite(Number(rangeStart)) + ? dialogueMap.chatIndexToFloor[rangeStart] + : null, + Number.isFinite(Number(rangeEnd)) + ? dialogueMap.chatIndexToFloor[rangeEnd] + : null, + ]; + const sourceChatIndexRange = [ + Number.isFinite(Number(rangeStart)) + ? Math.max( + 0, + Number(rangeStart) - + Math.max( + 0, + Number(runtime?.getSettings?.()?.extractContextTurns) || 0, + ) * + 2, + ) + : null, + rangeEnd, + ]; const afterSnapshot = runtime.cloneGraphSnapshot(graph); const effectiveArtifacts = Array.isArray(postProcessArtifacts) ? [...postProcessArtifacts] @@ -149,6 +292,8 @@ function buildCommittedBatchPersistSnapshot( typeof runtime.createBatchJournalEntry === "function" ? runtime.createBatchJournalEntry(beforeSnapshot, afterSnapshot, { processedRange: [rangeStart, rangeEnd], + processedDialogueRange, + sourceChatIndexRange, postProcessArtifacts: effectiveArtifacts, vectorHashesInserted: Array.isArray(vectorHashesInserted) ? vectorHashesInserted @@ -686,13 +831,17 @@ export async function onManualExtractController(runtime, options = {}) { runtime.toastr.info("记忆提取正在进行中,请稍候"); return; } - if (!runtime.ensureGraphMutationReady("手动提取")) return; + const taskLabel = String(options?.taskLabel || "手动提取").trim() || "手动提取"; + const toastTitle = String(options?.toastTitle || `ST-BME ${taskLabel}`).trim() || + `ST-BME ${taskLabel}`; + const lockedEndFloor = toSafeFloor(options?.lockedEndFloor, null); + if (!runtime.ensureGraphMutationReady(taskLabel)) return; const pendingPersistGate = await maybeRetryPendingPersistence( runtime, "manual-extraction-persist-retry", ); const pendingPersistMessage = pendingPersistGate - ? formatPendingPersistenceGateMessage(runtime, "手动提取") + ? formatPendingPersistenceGateMessage(runtime, taskLabel) : ""; if (pendingPersistMessage) { runtime.setLastExtractionStatus( @@ -745,17 +894,22 @@ export async function onManualExtractController(runtime, options = {}) { const extractionController = runtime.beginStageAbortController("extraction"); const extractionSignal = extractionController.signal; runtime.setLastExtractionStatus( - "手动提取中", - `待处理 assistant 楼层 ${pendingAssistantTurns.length} 条`, + `${taskLabel}中`, + lockedEndFloor != null + ? `待处理 assistant 楼层 ${pendingAssistantTurns.length} 条 · 截止 chatIndex ${lockedEndFloor}` + : `待处理 assistant 楼层 ${pendingAssistantTurns.length} 条`, "running", - - { syncRuntime: true, toastKind: "info", toastTitle: "ST-BME 手动提取" }, + { syncRuntime: true, toastKind: "info", toastTitle }, ); try { while (true) { const pendingTurns = runtime .getAssistantTurns(chat) - .filter((i) => i > runtime.getLastProcessedAssistantFloor()); + .filter((i) => { + if (i <= runtime.getLastProcessedAssistantFloor()) return false; + if (lockedEndFloor != null && i > lockedEndFloor) return false; + return true; + }); if (pendingTurns.length === 0) break; const batchAssistantTurns = pendingTurns.slice(0, extractEvery); @@ -802,7 +956,9 @@ export async function onManualExtractController(runtime, options = {}) { if (totals.batches === 0) { runtime.setLastExtractionStatus( "无待提取内容", - "没有新的 assistant 回复需要处理", + lockedEndFloor != null + ? "指定范围内没有新的 assistant 回复需要处理" + : "没有新的 assistant 回复需要处理", "info", { syncRuntime: true, @@ -818,13 +974,13 @@ export async function onManualExtractController(runtime, options = {}) { `提取完成但持久化待确认:${pendingAfterRun.reason || pendingAfterRun.outcome || "unknown"}`, ); runtime.setLastExtractionStatus( - "手动提取完成,持久化待确认", + `${taskLabel}完成,持久化待确认`, `${totals.batches} 批 · 新建 ${totals.newNodes} · 更新 ${totals.updatedNodes} · 新边 ${totals.newEdges}${pendingAfterRun.reason ? ` · ${pendingAfterRun.reason}` : ""}`, "warning", { syncRuntime: true, toastKind: "", - toastTitle: "ST-BME 手动提取", + toastTitle, }, ); } else { @@ -832,13 +988,13 @@ export async function onManualExtractController(runtime, options = {}) { `提取完成:${totals.batches} 批,新建 ${totals.newNodes},更新 ${totals.updatedNodes},新边 ${totals.newEdges}`, ); runtime.setLastExtractionStatus( - "手动提取完成", + `${taskLabel}完成`, `${totals.batches} 批 · 新建 ${totals.newNodes} · 更新 ${totals.updatedNodes} · 新边 ${totals.newEdges}`, "success", { syncRuntime: true, toastKind: "success", - toastTitle: "ST-BME 手动提取", + toastTitle, }, ); } @@ -850,7 +1006,7 @@ export async function onManualExtractController(runtime, options = {}) { } catch (e) { if (runtime.isAbortError(e)) { runtime.setLastExtractionStatus( - "手动提取已终止", + `${taskLabel}已终止`, e?.message || "已手动终止当前提取", "warning", { @@ -860,12 +1016,12 @@ export async function onManualExtractController(runtime, options = {}) { return; } runtime.console.error("[ST-BME] 手动提取失败:", e); - runtime.setLastExtractionStatus("手动提取失败", e?.message || String(e), "error", { + runtime.setLastExtractionStatus(`${taskLabel}失败`, e?.message || String(e), "error", { syncRuntime: true, toastKind: "", - toastTitle: "ST-BME 手动提取", + toastTitle, }); - runtime.toastr.error(`手动提取失败: ${e.message || e}`); + runtime.toastr.error(`${taskLabel}失败: ${e.message || e}`); } finally { runtime.finishStageAbortController("extraction", extractionController); runtime.setIsExtracting(false); @@ -873,6 +1029,116 @@ export async function onManualExtractController(runtime, options = {}) { } } +export async function onExtractionTaskController(runtime, options = {}) { + const requestedMode = String(options?.mode || "pending").trim().toLowerCase(); + const context = runtime.getContext?.() || {}; + const chat = Array.isArray(context?.chat) ? context.chat : []; + const runManualExtract = async (manualOptions = {}) => { + if (typeof runtime?.onManualExtract === "function") { + return await runtime.onManualExtract(manualOptions); + } + return await onManualExtractController(runtime, manualOptions); + }; + + if (requestedMode === "pending") { + return await runManualExtract({ + ...options, + taskLabel: "提取未处理", + toastTitle: "ST-BME 重新提取", + }); + } + + const rerunTask = resolveRerunDialogueTask(chat, options); + if (!rerunTask.valid) { + runtime.toastr?.info?.(rerunTask.reason || "当前没有可重提的范围"); + return { + success: false, + rerunPerformed: false, + fallbackToLatest: false, + requestedRange: [null, null], + effectiveDialogueRange: [null, null], + reason: rerunTask.reason || "invalid-rerun-range", + }; + } + + const fallbackInfo = buildRerunFallbackInfo(chat, [ + rerunTask.startFloor, + rerunTask.endFloor, + ]); + if (!fallbackInfo.valid) { + runtime.toastr?.info?.(fallbackInfo.reason || "目标范围内没有可重提的 AI 回复"); + return { + success: false, + rerunPerformed: false, + fallbackToLatest: false, + requestedRange: [rerunTask.requestedStartFloor, rerunTask.requestedEndFloor], + effectiveDialogueRange: [rerunTask.startFloor, rerunTask.endFloor], + reason: fallbackInfo.reason || "no-assistant-in-range", + }; + } + + const effectiveLockedEndFloor = fallbackInfo.fallbackToLatest + ? null + : fallbackInfo.endAssistantChatIndex; + const effectiveDialogueRange = [ + rerunTask.startFloor, + fallbackInfo.fallbackToLatest + ? Number.isFinite(Number(fallbackInfo.latestAssistantDialogueFloor)) + ? Number(fallbackInfo.latestAssistantDialogueFloor) + : rerunTask.endFloor + : rerunTask.endFloor, + ]; + + runtime.setRuntimeStatus( + "重新提取中", + fallbackInfo.fallbackToLatest + ? `范围 ${rerunTask.startFloor} ~ ${rerunTask.endFloor} 命中旧批次,但当前将退化为从 ${effectiveDialogueRange[0]} 到最新重提` + : `准备重提范围 ${rerunTask.startFloor} ~ ${rerunTask.endFloor}`, + fallbackInfo.fallbackToLatest ? "warning" : "running", + ); + + const rollbackResult = await runtime.rollbackGraphForReroll( + fallbackInfo.startAssistantChatIndex, + context, + ); + if (!rollbackResult?.success) { + return { + ...rollbackResult, + rerunPerformed: false, + fallbackToLatest: fallbackInfo.fallbackToLatest, + requestedRange: [rerunTask.requestedStartFloor, rerunTask.requestedEndFloor], + effectiveDialogueRange, + }; + } + + if (fallbackInfo.reason) { + runtime.toastr?.warning?.(fallbackInfo.reason, "ST-BME 重新提取", { + timeOut: 3500, + }); + } + + await runManualExtract({ + drainAll: true, + lockedEndFloor: effectiveLockedEndFloor, + taskLabel: "重新提取", + toastTitle: "ST-BME 重新提取", + }); + + return { + success: true, + rerunPerformed: true, + fallbackToLatest: fallbackInfo.fallbackToLatest, + requestedRange: [rerunTask.requestedStartFloor, rerunTask.requestedEndFloor], + effectiveDialogueRange, + effectiveAssistantChatRange: [ + fallbackInfo.startAssistantChatIndex, + effectiveLockedEndFloor, + ], + rollbackResult, + reason: fallbackInfo.reason || "", + }; +} + export async function onRerollController(runtime, { fromFloor } = {}) { if (runtime.getIsExtracting?.()) { runtime.toastr?.info?.("记忆提取正在进行中,请稍候"); diff --git a/maintenance/hierarchical-summary.js b/maintenance/hierarchical-summary.js index 23c7235..1a07689 100644 --- a/maintenance/hierarchical-summary.js +++ b/maintenance/hierarchical-summary.js @@ -9,13 +9,16 @@ import { applyTaskRegex } from "../prompting/task-regex.js"; import { getActiveTaskProfile } from "../prompting/prompt-profiles.js"; import { appendSummaryEntry, - createSummaryEntry, createDefaultSummaryState, getActiveSummaryEntries, markSummaryEntriesFolded, normalizeGraphSummaryState, } from "../graph/summary-state.js"; -import { buildSummarySourceMessages } from "./chat-history.js"; +import { + buildDialogueFloorMap, + buildSummarySourceMessages, + getDialogueFloorForChatIndex, +} from "./chat-history.js"; import { getSTContextForPrompt } from "../host/st-context.js"; import { deriveStoryTimeSpanFromNodes, @@ -128,6 +131,63 @@ function collectJournalTouchedNodeIds(journal = {}) { ]); } +function intersectsRange(leftRange, rightRange) { + const [leftStart, leftEnd] = normalizeRange(leftRange); + const [rightStart, rightEnd] = normalizeRange(rightRange); + if (leftStart < 0 || leftEnd < 0 || rightStart < 0 || rightEnd < 0) { + return false; + } + return leftStart <= rightEnd && rightStart <= leftEnd; +} + +function buildDialogueRangeFromMessageRange(chat = [], messageRange = [-1, -1]) { + const [messageStart, messageEnd] = normalizeRange(messageRange); + if (messageStart < 0 || messageEnd < 0) { + return [-1, -1]; + } + const startFloor = getDialogueFloorForChatIndex(chat, messageStart); + const endFloor = getDialogueFloorForChatIndex(chat, messageEnd); + return [ + Number.isFinite(Number(startFloor)) ? Number(startFloor) : -1, + Number.isFinite(Number(endFloor)) ? Number(endFloor) : -1, + ]; +} + +function getSummaryEntryDialogueRange(chat = [], entry = {}) { + const directRange = normalizeRange(entry?.dialogueRange); + if (directRange[0] >= 0 && directRange[1] >= 0) { + return directRange; + } + return buildDialogueRangeFromMessageRange(chat, entry?.messageRange); +} + +function removeSummaryEntriesByIds(graph, entryIds = []) { + normalizeGraphSummaryState(graph); + const targetIds = new Set(uniqueIds(entryIds)); + if (targetIds.size === 0) return 0; + const queue = [...targetIds]; + while (queue.length > 0) { + const currentId = queue.shift(); + for (const entry of graph.summaryState.entries || []) { + if (targetIds.has(entry.id)) continue; + const sourceSummaryIds = Array.isArray(entry?.sourceSummaryIds) + ? entry.sourceSummaryIds + : []; + if (!sourceSummaryIds.includes(currentId)) continue; + targetIds.add(entry.id); + queue.push(entry.id); + } + } + + graph.summaryState.entries = (graph.summaryState.entries || []).filter( + (entry) => !targetIds.has(entry.id), + ); + graph.summaryState.activeEntryIds = (graph.summaryState.activeEntryIds || []).filter( + (entryId) => !targetIds.has(entryId), + ); + return targetIds.size; +} + function findJournalForExtractionCount(graph, extractionCountBefore) { const target = Number(extractionCountBefore); const journals = Array.isArray(graph?.batchJournal) ? graph.batchJournal : []; @@ -143,12 +203,18 @@ function findJournalForExtractionCount(graph, extractionCountBefore) { return null; } -function buildPseudoCurrentSlice(currentExtractionCount, currentRange, currentNodeIds = []) { +function buildPseudoCurrentSlice( + currentExtractionCount, + currentRange, + currentNodeIds = [], + currentDialogueRange = null, +) { return { id: `summary-pending-${currentExtractionCount}`, extractionCountBefore: Math.max(0, currentExtractionCount - 1), extractionCountAfter: currentExtractionCount, processedRange: normalizeRange(currentRange), + processedDialogueRange: normalizeRange(currentDialogueRange), touchedNodeIds: uniqueIds(currentNodeIds), }; } @@ -160,7 +226,11 @@ function buildSliceFromJournal(journal = {}) { extractionCountAfter: clampInt(journal?.stateBefore?.extractionCount, 0, 0, 999999) + 1, processedRange: normalizeRange(journal?.processedRange), - touchedNodeIds: collectJournalTouchedNodeIds(journal), + processedDialogueRange: normalizeRange(journal?.processedDialogueRange), + touchedNodeIds: uniqueIds([ + ...(Array.isArray(journal?.touchedNodeIds) ? journal.touchedNodeIds : []), + ...collectJournalTouchedNodeIds(journal), + ]), }; } @@ -170,6 +240,7 @@ function collectSlicesForSummaryWindow( lastSummarizedExtractionCount = 0, currentExtractionCount = 0, currentRange = null, + currentDialogueRange = null, currentNodeIds = [], includeCurrentPending = false, } = {}, @@ -194,7 +265,12 @@ function collectSlicesForSummaryWindow( } if (hasCurrentPendingRange && safeCurrentCount > safeLastCount) { slices.push( - buildPseudoCurrentSlice(safeCurrentCount, currentRange, currentNodeIds), + buildPseudoCurrentSlice( + safeCurrentCount, + currentRange, + currentNodeIds, + currentDialogueRange, + ), ); } return slices.sort( @@ -348,6 +424,7 @@ export async function generateSmallSummary({ lastSummarizedExtractionCount: summaryState.lastSummarizedExtractionCount, currentExtractionCount, currentRange, + currentDialogueRange: buildDialogueRangeFromMessageRange(chat, currentRange), currentNodeIds, includeCurrentPending: true, }); @@ -384,6 +461,7 @@ export async function generateSmallSummary({ ? Number(sourceMessages[sourceMessages.length - 1].seq) : messageEnd, ]; + const dialogueRange = buildDialogueRangeFromMessageRange(chat, messageRange); const sourceNodeIds = uniqueIds( slices.flatMap((slice) => Array.isArray(slice.touchedNodeIds) ? slice.touchedNodeIds : []), ); @@ -446,6 +524,7 @@ export async function generateSmallSummary({ sourceTask: "synopsis", extractionRange: [firstSlice.extractionCountAfter, lastSlice.extractionCountAfter], messageRange, + dialogueRange, sourceBatchIds: uniqueIds(slices.map((slice) => slice.id)), sourceSummaryIds: [], sourceNodeIds, @@ -466,6 +545,7 @@ export async function generateSmallSummary({ sourceMessages, sourceNodeIds, messageRange, + dialogueRange, }; } @@ -574,6 +654,20 @@ export async function rollupSummaryFrontier({ Math.min(...candidates.map((entry) => normalizeRange(entry.messageRange)[0])), Math.max(...candidates.map((entry) => normalizeRange(entry.messageRange)[1])), ]; + const dialogueRange = [ + Math.min( + ...candidates.map((entry) => { + const range = normalizeRange(entry?.dialogueRange, normalizeRange(entry?.messageRange)); + return range[0]; + }), + ), + Math.max( + ...candidates.map((entry) => { + const range = normalizeRange(entry?.dialogueRange, normalizeRange(entry?.messageRange)); + return range[1]; + }), + ), + ]; const storyTimeSpan = deriveStoryTimeSpanFromNodes( graph, nodeHints.nodes, @@ -592,6 +686,7 @@ export async function rollupSummaryFrontier({ sourceTask: "summary_rollup", extractionRange, messageRange, + dialogueRange, sourceBatchIds: uniqueIds( candidates.flatMap((entry) => Array.isArray(entry.sourceBatchIds) ? entry.sourceBatchIds : [], @@ -684,14 +779,121 @@ function clearSummaryState(graph) { graph.summaryState = createDefaultSummaryState(); } +function getSliceDialogueRange(chat = [], slice = {}) { + const directRange = normalizeRange(slice?.processedDialogueRange); + if (directRange[0] >= 0 && directRange[1] >= 0) { + return directRange; + } + return buildDialogueRangeFromMessageRange(chat, slice?.processedRange); +} + +function getSuffixRebuildStartFromDialogueRange( + graph, + chat = [], + targetDialogueRange = [-1, -1], +) { + const slices = collectSlicesForSummaryWindow(graph, { + lastSummarizedExtractionCount: 0, + currentExtractionCount: clampInt( + graph?.historyState?.extractionCount, + 0, + 0, + 999999, + ), + currentRange: null, + currentNodeIds: [], + includeCurrentPending: false, + }); + const affectedSlices = slices.filter((slice) => + intersectsRange(getSliceDialogueRange(chat, slice), targetDialogueRange), + ); + if (affectedSlices.length === 0) { + return null; + } + return { + rebuildFromExtractionCount: Math.min( + ...affectedSlices.map((slice) => + clampInt(slice.extractionCountBefore, 0, 0, 999999), + ), + ), + affectedSlices, + }; +} + +function resolveCurrentSummaryDialogueRange(graph, chat = []) { + const activeEntries = getActiveSummaryEntries(graph); + if (activeEntries.length > 0) { + return getSummaryEntryDialogueRange( + chat, + activeEntries[activeEntries.length - 1], + ); + } + const slices = collectSlicesForSummaryWindow(graph, { + lastSummarizedExtractionCount: 0, + currentExtractionCount: clampInt( + graph?.historyState?.extractionCount, + 0, + 0, + 999999, + ), + currentRange: null, + currentNodeIds: [], + includeCurrentPending: false, + }); + if (slices.length === 0) { + return [-1, -1]; + } + return getSliceDialogueRange(chat, slices[slices.length - 1]); +} + +function trimSummaryStateForSuffixRebuild(graph, rebuildFromExtractionCount = 0) { + normalizeGraphSummaryState(graph); + const entries = Array.isArray(graph.summaryState?.entries) + ? graph.summaryState.entries + : []; + const removeIds = entries + .filter((entry) => { + const extractionRange = normalizeRange(entry?.extractionRange); + return extractionRange[1] >= rebuildFromExtractionCount; + }) + .map((entry) => entry.id); + const removedCount = removeSummaryEntriesByIds(graph, removeIds); + const remainingEntries = Array.isArray(graph.summaryState?.entries) + ? graph.summaryState.entries + : []; + graph.summaryState.lastSummarizedExtractionCount = + remainingEntries.length > 0 + ? Math.max( + 0, + ...remainingEntries.map((entry) => + normalizeRange(entry?.extractionRange)[1], + ), + ) + : Math.max(0, rebuildFromExtractionCount); + graph.summaryState.lastSummarizedAssistantFloor = + remainingEntries.length > 0 + ? Math.max( + -1, + ...remainingEntries.map((entry) => + normalizeRange(entry?.messageRange)[1], + ), + ) + : -1; + return { + removedCount, + }; +} + export async function rebuildHierarchicalSummaryState({ graph, chat = [], settings = {}, signal, + mode = "current", + startFloor = null, + endFloor = null, } = {}) { normalizeGraphSummaryState(graph); - clearSummaryState(graph); const currentExtractionCount = clampInt( graph?.historyState?.extractionCount, 0, @@ -707,9 +909,63 @@ export async function rebuildHierarchicalSummaryState({ }; } + let targetDialogueRange = [-1, -1]; + if (String(mode || "current") === "range") { + if (!Number.isFinite(Number(startFloor))) { + return { + rebuilt: false, + smallSummaryCount: 0, + rollupCount: 0, + reason: "范围重建必须填写起始楼层", + }; + } + const latestDialogueFloor = buildDialogueFloorMap(chat).latestDialogueFloor; + targetDialogueRange = [ + clampInt(startFloor, 0, 0, Math.max(0, latestDialogueFloor)), + Number.isFinite(Number(endFloor)) + ? clampInt(endFloor, 0, 0, Math.max(0, latestDialogueFloor)) + : Math.max(0, latestDialogueFloor), + ]; + targetDialogueRange[1] = Math.max( + targetDialogueRange[0], + targetDialogueRange[1], + ); + } else { + targetDialogueRange = resolveCurrentSummaryDialogueRange(graph, chat); + } + + if (targetDialogueRange[0] < 0 || targetDialogueRange[1] < 0) { + return { + rebuilt: false, + smallSummaryCount: 0, + rollupCount: 0, + reason: "当前没有可重建的总结范围", + }; + } + + const rebuildWindow = getSuffixRebuildStartFromDialogueRange( + graph, + chat, + targetDialogueRange, + ); + if (!rebuildWindow) { + return { + rebuilt: false, + smallSummaryCount: 0, + rollupCount: 0, + reason: "目标范围内没有命中的总结切片", + targetDialogueRange, + }; + } + + const trimmed = trimSummaryStateForSuffixRebuild( + graph, + rebuildWindow.rebuildFromExtractionCount, + ); + const threshold = clampInt(settings.smallSummaryEveryNExtractions, 3, 1, 100); const slices = collectSlicesForSummaryWindow(graph, { - lastSummarizedExtractionCount: 0, + lastSummarizedExtractionCount: rebuildWindow.rebuildFromExtractionCount, currentExtractionCount, currentRange: null, currentNodeIds: [], @@ -770,6 +1026,10 @@ export async function rebuildHierarchicalSummaryState({ }); const summaryText = String(result?.summary || "").trim(); if (summaryText) { + const entryMessageRange = [ + Number(sourceMessages[0]?.seq ?? -1), + Number(sourceMessages[sourceMessages.length - 1]?.seq ?? -1), + ]; const entry = appendSummaryEntry(graph, { level: 0, kind: "small", @@ -777,10 +1037,8 @@ export async function rebuildHierarchicalSummaryState({ text: summaryText, sourceTask: "synopsis", extractionRange: [firstSlice.extractionCountAfter, lastSlice.extractionCountAfter], - messageRange: [ - Number(sourceMessages[0]?.seq ?? -1), - Number(sourceMessages[sourceMessages.length - 1]?.seq ?? -1), - ], + messageRange: entryMessageRange, + dialogueRange: buildDialogueRangeFromMessageRange(chat, entryMessageRange), sourceBatchIds: pendingSlices.map((item) => item.id), sourceSummaryIds: [], sourceNodeIds, @@ -813,6 +1071,9 @@ export async function rebuildHierarchicalSummaryState({ rebuilt: smallSummaryCount > 0 || rollupCount > 0, smallSummaryCount, rollupCount, + targetDialogueRange, + rebuildFromExtractionCount: rebuildWindow.rebuildFromExtractionCount, + removedEntryCount: trimmed.removedCount, reason: smallSummaryCount > 0 || rollupCount > 0 ? "" diff --git a/manifest.json b/manifest.json index fd263ff..4b38381 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Youzini", - "version": "4.3.7", + "version": "4.3.6", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" } diff --git a/runtime/runtime-state.js b/runtime/runtime-state.js index 1c82739..dfb5465 100644 --- a/runtime/runtime-state.js +++ b/runtime/runtime-state.js @@ -811,10 +811,22 @@ export function createBatchJournalEntry( journalVersion: BATCH_JOURNAL_VERSION, createdAt: Date.now(), processedRange: meta.processedRange || [-1, -1], + processedDialogueRange: Array.isArray(meta.processedDialogueRange) + ? meta.processedDialogueRange + : [-1, -1], + sourceChatIndexRange: Array.isArray(meta.sourceChatIndexRange) + ? meta.sourceChatIndexRange + : [-1, -1], createdNodeIds, createdEdgeIds, previousNodeSnapshots, previousEdgeSnapshots, + touchedNodeIds: normalizeStringArray( + meta.touchedNodeIds || [ + ...createdNodeIds, + ...previousNodeSnapshots.map((node) => node?.id), + ], + ), stateBefore: buildJournalStateBefore(snapshotBefore, meta), vectorDelta: buildVectorDelta(snapshotBefore, snapshotAfter, meta), postProcessArtifacts: Array.isArray(meta.postProcessArtifacts) diff --git a/tests/dialogue-floor-range-tasks.mjs b/tests/dialogue-floor-range-tasks.mjs new file mode 100644 index 0000000..05f5f0e --- /dev/null +++ b/tests/dialogue-floor-range-tasks.mjs @@ -0,0 +1,174 @@ +import assert from "node:assert/strict"; + +import { + buildDialogueFloorMap, + normalizeDialogueFloorRange, +} from "../maintenance/chat-history.js"; +import { onExtractionTaskController } from "../maintenance/extraction-controller.js"; +import { onRebuildSummaryStateController } from "../ui/ui-actions-controller.js"; + +const chat = [ + { is_system: true, is_user: false, mes: "greeting" }, + { is_user: true, mes: "user-1" }, + { is_user: false, mes: "assistant-1" }, + { is_system: true, is_user: false, mes: "real-system" }, + { + is_system: true, + is_user: false, + mes: "managed-hidden-assistant", + extra: { __st_bme_hide_managed: true }, + }, + { is_user: true, mes: "user-2" }, + { is_user: false, mes: "assistant-2" }, +]; + +{ + const mapping = buildDialogueFloorMap(chat); + assert.equal(mapping.latestDialogueFloor, 5); + assert.deepEqual(Array.from(mapping.floorToChatIndex), [0, 1, 2, 4, 5, 6]); + assert.equal(mapping.floorToRole[0], "greeting"); + assert.deepEqual(Array.from(mapping.assistantDialogueFloors), [2, 3, 5]); + assert.deepEqual(Array.from(mapping.assistantChatIndices), [2, 4, 6]); +} + +{ + const normalized = normalizeDialogueFloorRange(chat, 2, null); + assert.equal(normalized.valid, true); + assert.equal(normalized.startFloor, 2); + assert.equal(normalized.endFloor, 5); +} + +{ + const normalized = normalizeDialogueFloorRange(chat, null, 4); + assert.equal(normalized.valid, false); + assert.equal(normalized.reason, "end-without-start"); +} + +{ + const calls = { + rollback: [], + manual: [], + warning: [], + info: [], + }; + const runtime = { + getContext() { + return { chat }; + }, + getIsExtracting() { + return false; + }, + ensureGraphMutationReady() { + return true; + }, + setRuntimeStatus() {}, + rollbackGraphForReroll: async (fromFloor) => { + calls.rollback.push(fromFloor); + return { success: true, effectiveFromFloor: fromFloor }; + }, + onManualExtract: async (options = {}) => { + calls.manual.push({ ...options }); + }, + toastr: { + warning(message) { + calls.warning.push(String(message || "")); + }, + info(message) { + calls.info.push(String(message || "")); + }, + }, + }; + + const result = await onExtractionTaskController(runtime, { + mode: "rerun", + startFloor: 2, + endFloor: 2, + }); + + assert.equal(result.success, true); + assert.equal(result.fallbackToLatest, true); + assert.deepEqual(calls.rollback, [2]); + assert.equal(calls.manual.length, 1); + assert.equal(calls.manual[0].lockedEndFloor, null); + assert.equal(calls.manual[0].taskLabel, "重新提取"); + assert.match(result.reason, /退化为从起始楼层到最新重提/); +} + +{ + const calls = { + rollback: [], + manual: [], + }; + const runtime = { + getContext() { + return { chat }; + }, + getIsExtracting() { + return false; + }, + ensureGraphMutationReady() { + return true; + }, + setRuntimeStatus() {}, + rollbackGraphForReroll: async (fromFloor) => { + calls.rollback.push(fromFloor); + return { success: true, effectiveFromFloor: fromFloor }; + }, + onManualExtract: async (options = {}) => { + calls.manual.push({ ...options }); + }, + toastr: { + warning() {}, + info() {}, + }, + }; + + const result = await onExtractionTaskController(runtime, { + mode: "rerun", + }); + + assert.equal(result.success, true); + assert.equal(result.fallbackToLatest, false); + assert.deepEqual(calls.rollback, [6]); + assert.equal(calls.manual[0].lockedEndFloor, 6); +} + +{ + const captured = []; + const runtime = { + getCurrentGraph() { + return {}; + }, + ensureGraphMutationReady() { + return true; + }, + getContext() { + return { chat }; + }, + getSettings() { + return {}; + }, + rebuildHierarchicalSummaryState: async (payload) => { + captured.push(payload); + return { rebuilt: false, reason: "noop" }; + }, + saveGraphToChat() {}, + refreshPanelLiveState() {}, + setRuntimeStatus() {}, + toastr: { + info() {}, + success() {}, + }, + }; + + await onRebuildSummaryStateController(runtime, {}); + await onRebuildSummaryStateController(runtime, { startFloor: 1, endFloor: 3 }); + + assert.equal(captured[0].mode, "current"); + assert.equal(captured[0].startFloor, null); + assert.equal(captured[1].mode, "range"); + assert.equal(captured[1].startFloor, 1); + assert.equal(captured[1].endFloor, 3); +} + +console.log("dialogue-floor-range-tasks tests passed"); diff --git a/ui/panel.html b/ui/panel.html index 5c764d7..deeb6e0 100644 --- a/ui/panel.html +++ b/ui/panel.html @@ -324,8 +324,8 @@
-
- 重新提取:回滚指定楼层及之后的提取结果并重做。留空则只重做最新 AI 楼。 + 重新提取:按总楼层计数(用户+AI,首条 greeting 为 0)。真正 system 不计入,且不受隐藏助手影响。
-
- - +
+
+ + +
+
+ + +
+
+ + +
+
+
+ 重新提取范围:起始/终止都留空 = 当前重提;只填起始 = 从起始到最新。 +
+
+ 重建总结状态:默认按当前总结相关范围重建;填写楼层后按范围重建。 +
+
+
+ + +
+
+ + +
@@ -1397,7 +1443,7 @@ 自动提取晚一楼
- 开启后,最新 AI 楼先不自动提取,要等下一条 AI 楼出现后,才提取前一批内容。手动提取和重 Roll 不受影响。 + 开启后,最新 AI 楼先不自动提取,要等下一条 AI 楼出现后,才提取前一批内容。提取未处理和范围重提不受影响。
diff --git a/ui/panel.js b/ui/panel.js index 3ea88fd..3ea8a48 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -112,7 +112,6 @@ const GRAPH_WRITE_ACTION_IDS = [ "bme-act-vector-rebuild", "bme-act-vector-range", "bme-act-vector-reembed", - "bme-act-reroll", "bme-detail-delete", "bme-detail-save", "bme-cog-region-apply", @@ -857,11 +856,11 @@ function _onFabSingleClick() { } async function _onFabDoubleClick() { - if (!_actionHandlers.reroll) return; + if (!_actionHandlers.extractTask) return; try { _fabEl?.setAttribute("data-status", "running"); - await _actionHandlers.reroll({}); + await _actionHandlers.extractTask({ mode: "rerun" }); _fabEl?.setAttribute("data-status", "success"); _refreshDashboard(); _refreshGraph(); @@ -870,7 +869,7 @@ async function _onFabDoubleClick() { _fabEl?.setAttribute("data-status", status.status || "idle"); }, 3000); } catch (err) { - console.error("[ST-BME] FAB reroll failed:", err); + console.error("[ST-BME] FAB extract task failed:", err); _fabEl?.setAttribute("data-status", "error"); } } @@ -1725,7 +1724,11 @@ function _refreshMobileCognition() { } function _formatSummaryEntryCard(entry = {}) { - const messageRange = Array.isArray(entry?.messageRange) ? entry.messageRange : ["?", "?"]; + const messageRange = Array.isArray(entry?.dialogueRange) + ? entry.dialogueRange + : Array.isArray(entry?.messageRange) + ? entry.messageRange + : ["?", "?"]; const extractionRange = Array.isArray(entry?.extractionRange) ? entry.extractionRange : ["?", "?"]; @@ -3736,12 +3739,10 @@ function _bindDashboardControls() { function _bindActions() { const bindings = { - "bme-act-extract": "extract", "bme-act-compress": "compress", "bme-act-sleep": "sleep", "bme-act-synopsis": "synopsis", "bme-act-summary-rollup": "summaryRollup", - "bme-act-summary-rebuild": "rebuildSummaryState", "bme-act-summary-clear": "clearSummaryState", "bme-act-export": "export", "bme-act-import": "import", @@ -3763,7 +3764,6 @@ function _bindActions() { }; const actionLabels = { - extract: "手动提取", compress: "手动压缩", sleep: "执行遗忘", synopsis: "生成小总结", @@ -3852,6 +3852,67 @@ function _bindActions() { }); } + document + .getElementById("bme-act-extract") + ?.addEventListener("click", async () => { + const btn = document.getElementById("bme-act-extract"); + if (btn?.disabled) return; + const mode = + String(document.getElementById("bme-extract-mode")?.value || "pending") + .trim() + .toLowerCase() === "rerun" + ? "rerun" + : "pending"; + const startFloor = _parseOptionalInt( + document.getElementById("bme-extract-start-floor")?.value, + ); + const endFloor = _parseOptionalInt( + document.getElementById("bme-extract-end-floor")?.value, + ); + const desc = + mode === "pending" + ? "提取当前尚未处理的内容" + : Number.isFinite(startFloor) || Number.isFinite(endFloor) + ? `重提范围 ${Number.isFinite(startFloor) ? startFloor : "当前"} ~ ${Number.isFinite(endFloor) ? endFloor : "最新"}` + : "当前重提"; + + if (!confirm(`确认要执行吗?\n\n${desc}`)) { + return; + } + + if (btn) { + btn.disabled = true; + btn.style.opacity = "0.5"; + } + + _showActionProgressUi("重新提取"); + try { + await _actionHandlers.extractTask?.({ + mode, + startFloor: Number.isFinite(startFloor) ? startFloor : undefined, + endFloor: Number.isFinite(endFloor) ? endFloor : undefined, + }); + _refreshDashboard(); + _refreshGraph(); + if ( + document + .getElementById("bme-pane-memory") + ?.classList.contains("active") + ) { + _refreshMemoryBrowser(); + } + } catch (error) { + console.error("[ST-BME] Action extractTask failed:", error); + toastr.error(`重新提取失败: ${error?.message || error}`, "ST-BME"); + } finally { + if (btn) { + btn.style.opacity = ""; + } + _refreshRuntimeStatus(); + _refreshGraphAvailabilityState(); + } + }); + document .getElementById("bme-act-vector-range") ?.addEventListener("click", async () => { @@ -3892,32 +3953,31 @@ function _bindActions() { } }); - // 重新提取 (reroll) 绑定 document - .getElementById("bme-act-reroll") + .getElementById("bme-act-summary-rebuild") ?.addEventListener("click", async () => { - const btn = document.getElementById("bme-act-reroll"); + const btn = document.getElementById("bme-act-summary-rebuild"); 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; - } + const startFloor = _parseOptionalInt( + document.getElementById("bme-summary-rebuild-start-floor")?.value, + ); + const endFloor = _parseOptionalInt( + document.getElementById("bme-summary-rebuild-end-floor")?.value, + ); + const desc = Number.isFinite(startFloor) || Number.isFinite(endFloor) + ? `按范围 ${Number.isFinite(startFloor) ? startFloor : "当前"} ~ ${Number.isFinite(endFloor) ? endFloor : "最新"} 重建总结状态` + : "按当前总结相关范围重建总结状态"; if (btn) { btn.disabled = true; btn.style.opacity = "0.5"; } - _showActionProgressUi("重新提取"); + _showActionProgressUi("重建总结状态"); try { - await _actionHandlers.reroll?.({ - fromFloor: Number.isFinite(fromFloor) ? fromFloor : undefined, + await _actionHandlers.rebuildSummaryState?.({ + startFloor: Number.isFinite(startFloor) ? startFloor : undefined, + endFloor: Number.isFinite(endFloor) ? endFloor : undefined, }); _refreshDashboard(); _refreshGraph(); @@ -3929,8 +3989,8 @@ function _bindActions() { _refreshMemoryBrowser(); } } catch (error) { - console.error("[ST-BME] Action reroll failed:", error); - toastr.error(`重新提取失败: ${error?.message || error}`, "ST-BME"); + console.error("[ST-BME] Action rebuildSummaryState failed:", error); + toastr.error(`重建总结状态失败: ${error?.message || error}`, "ST-BME"); } finally { if (btn) { btn.style.opacity = ""; diff --git a/ui/ui-actions-controller.js b/ui/ui-actions-controller.js index 9a6af69..0418eca 100644 --- a/ui/ui-actions-controller.js +++ b/ui/ui-actions-controller.js @@ -771,11 +771,21 @@ export async function onManualSummaryRollupController(runtime) { } } -export async function onRebuildSummaryStateController(runtime) { +export async function onRebuildSummaryStateController(runtime, options = {}) { const graph = runtime.getCurrentGraph(); if (!graph) return; if (!runtime.ensureGraphMutationReady("重建总结状态")) return; - updateManualActionUiState(runtime, "重建总结中", "正在按现有提取批次重建总结链", "running"); + const hasStart = Number.isFinite(Number(options?.startFloor)); + const hasEnd = Number.isFinite(Number(options?.endFloor)); + const mode = hasStart || hasEnd ? "range" : "current"; + updateManualActionUiState( + runtime, + "重建总结中", + mode === "range" + ? `正在按范围 ${hasStart ? Number(options.startFloor) : "?"} ~ ${hasEnd ? Number(options.endFloor) : "最新"} 重建总结链` + : "正在重建当前总结相关范围", + "running", + ); try { const chat = runtime.getContext?.()?.chat; @@ -783,6 +793,9 @@ export async function onRebuildSummaryStateController(runtime) { graph, chat: Array.isArray(chat) ? chat : [], settings: runtime.getSettings(), + mode, + startFloor: hasStart ? Number(options.startFloor) : null, + endFloor: hasEnd ? Number(options.endFloor) : null, }); runtime.saveGraphToChat?.({ reason: "rebuild-summary-state" }); runtime.refreshPanelLiveState?.();