diff --git a/chat-history.js b/chat-history.js index 83902bc..ad31be1 100644 --- a/chat-history.js +++ b/chat-history.js @@ -6,8 +6,24 @@ import { clampInt } from "./ui-status.js"; import { sanitizePlannerMessageText } from "./planner-tag-utils.js"; import { rollbackBatch } from "./runtime-state.js"; +export function isBmeManagedHiddenMessage(message) { + return Boolean( + message?.extra && + typeof message.extra === "object" && + message.extra.__st_bme_hide_managed === true, + ); +} + +export function isSystemMessageForExtraction(message) { + return Boolean(message?.is_system) && !isBmeManagedHiddenMessage(message); +} + export function isAssistantChatMessage(message) { - return Boolean(message) && !message.is_user && !message.is_system; + return ( + Boolean(message) && + !message.is_user && + !isSystemMessageForExtraction(message) + ); } export function getAssistantTurns(chat) { @@ -37,7 +53,7 @@ export function buildExtractionMessages(chat, startIdx, endIdx, settings) { index++ ) { const msg = chat[index]; - if (msg.is_system) continue; + if (isSystemMessageForExtraction(msg)) continue; messages.push({ seq: index, role: msg.is_user ? "user" : "assistant", @@ -54,7 +70,7 @@ export function getChatIndexForPlayableSeq(chat, playableSeq) { let currentSeq = -1; for (let index = 0; index < chat.length; index++) { const message = chat[index]; - if (message?.is_system) continue; + if (isSystemMessageForExtraction(message)) continue; currentSeq++; if (currentSeq >= playableSeq) { return index; diff --git a/index.js b/index.js index ddd313a..6ce9e59 100644 --- a/index.js +++ b/index.js @@ -33,6 +33,7 @@ import { clampRecoveryStartFloor, getAssistantTurns, isAssistantChatMessage, + isSystemMessageForExtraction, pruneProcessedMessageHashesFromFloor, resolveDirtyFloorFromMutationMeta, rollbackAffectedJournals, @@ -4359,6 +4360,22 @@ function notifyExtractionIssue(message, title = "ST-BME 提取提示") { toastr.warning(message, title, { timeOut: 4500 }); } +function settleExtractionStatusAfterHistoryRecovery( + text = "提取完成", + meta = "", + level = "success", +) { + const currentText = String(lastExtractionStatus?.text || ""); + const currentLevel = String(lastExtractionStatus?.level || ""); + if (currentText !== "AI 生成中" && currentLevel !== "running") { + return; + } + setLastExtractionStatus(text, meta, level, { + syncRuntime: true, + toastKind: "", + }); +} + async function fetchLocalWithTimeout( url, options = {}, @@ -5594,7 +5611,7 @@ const DEFAULT_TRIGGER_KEYWORDS = [ export function getSmartTriggerDecision(chat, lastProcessed, settings) { const pendingMessages = chat .slice(Math.max(0, (lastProcessed ?? -1) + 1)) - .filter((msg) => !msg.is_system) + .filter((msg) => !isSystemMessageForExtraction(msg)) .map((msg) => ({ role: msg.is_user ? "user" : "assistant", content: msg.mes || "", @@ -7695,6 +7712,11 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") { } saveGraphToChat({ reason: "history-recovery-complete" }); refreshPanelLiveState(); + settleExtractionStatusAfterHistoryRecovery( + "提取完成", + `历史恢复回放 ${replayedBatches} 批`, + "success", + ); updateStageNotice( "history", usedFullRebuild ? "历史恢复完成(全量重建)" : "历史恢复完成", @@ -7736,6 +7758,11 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") { currentGraph.vectorIndexState.replayRequiredNodeIds = []; currentGraph.vectorIndexState.dirty = false; currentGraph.vectorIndexState.dirtyReason = ""; + settleExtractionStatusAfterHistoryRecovery( + "提取已终止", + error?.message || "历史恢复已终止", + "warning", + ); updateStageNotice( "history", "历史恢复已终止", @@ -7782,6 +7809,11 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") { currentGraph.vectorIndexState.lastIntegrityIssue = null; saveGraphToChat({ reason: "history-recovery-fallback-rebuild" }); refreshPanelLiveState(); + settleExtractionStatusAfterHistoryRecovery( + "提取完成", + `历史恢复已退化为全量重建,回放 ${replayedBatches} 批`, + "warning", + ); updateStageNotice( "history", "历史恢复已退化为全量重建", @@ -7814,6 +7846,11 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") { currentGraph.vectorIndexState.lastIntegrityIssue = null; saveGraphToChat({ reason: "history-recovery-failed" }); refreshPanelLiveState(); + settleExtractionStatusAfterHistoryRecovery( + "提取失败", + fallbackError?.message || String(fallbackError), + "error", + ); updateStageNotice( "history", "历史恢复失败", diff --git a/tests/chat-history.mjs b/tests/chat-history.mjs new file mode 100644 index 0000000..7cc2389 --- /dev/null +++ b/tests/chat-history.mjs @@ -0,0 +1,68 @@ +import assert from "node:assert/strict"; +import { + buildExtractionMessages, + getAssistantTurns, + isAssistantChatMessage, + isBmeManagedHiddenMessage, + isSystemMessageForExtraction, +} from "../chat-history.js"; + +const visibleAssistant = { + is_user: false, + is_system: false, + mes: "visible assistant", +}; +assert.equal(isAssistantChatMessage(visibleAssistant), true); + +const managedHiddenAssistant = { + is_user: false, + is_system: true, + mes: "managed hidden assistant", + extra: { __st_bme_hide_managed: true }, +}; +assert.equal(isBmeManagedHiddenMessage(managedHiddenAssistant), true); +assert.equal(isSystemMessageForExtraction(managedHiddenAssistant), false); +assert.equal(isAssistantChatMessage(managedHiddenAssistant), true); + +const realSystemMessage = { + is_user: false, + is_system: true, + mes: "real system", +}; +assert.equal(isSystemMessageForExtraction(realSystemMessage), true); +assert.equal(isAssistantChatMessage(realSystemMessage), false); + +const chat = [ + { is_user: false, is_system: true, mes: "greeting/system" }, + { is_user: true, is_system: false, mes: "user-1" }, + managedHiddenAssistant, + { is_user: true, is_system: false, mes: "user-2" }, + visibleAssistant, + realSystemMessage, +]; + +assert.deepEqual( + getAssistantTurns(chat), + [2, 4], + "managed hidden assistant floors should still be extractable assistant turns", +); + +const extractionMessages = buildExtractionMessages(chat, 4, 4, { + extractContextTurns: 2, +}); +assert.deepEqual( + extractionMessages.map((message) => ({ + seq: message.seq, + role: message.role, + content: message.content, + })), + [ + { seq: 1, role: "user", content: "user-1" }, + { seq: 2, role: "assistant", content: "managed hidden assistant" }, + { seq: 3, role: "user", content: "user-2" }, + { seq: 4, role: "assistant", content: "visible assistant" }, + ], + "extraction should keep BME-managed hidden context but still skip real system messages", +); + +console.log("chat-history tests passed");