diff --git a/chat-history.js b/chat-history.js index ad31be1..332f490 100644 --- a/chat-history.js +++ b/chat-history.js @@ -5,8 +5,20 @@ import { clampInt } from "./ui-status.js"; import { sanitizePlannerMessageText } from "./planner-tag-utils.js"; import { rollbackBatch } from "./runtime-state.js"; +import { isInManagedHideRange } from "./hide-engine.js"; + +export function isBmeManagedHiddenMessage( + message, + { index = null, chat = null } = {}, +) { + if ( + Number.isFinite(index) && + index > 0 && + isInManagedHideRange(index, chat) + ) { + return true; + } -export function isBmeManagedHiddenMessage(message) { return Boolean( message?.extra && typeof message.extra === "object" && @@ -14,15 +26,24 @@ export function isBmeManagedHiddenMessage(message) { ); } -export function isSystemMessageForExtraction(message) { - return Boolean(message?.is_system) && !isBmeManagedHiddenMessage(message); +export function isSystemMessageForExtraction( + message, + { index = null, chat = null } = {}, +) { + if (!message?.is_system) return false; + if (Number.isFinite(index) && index === 0) return true; + + return !isBmeManagedHiddenMessage(message, { index, chat }); } -export function isAssistantChatMessage(message) { +export function isAssistantChatMessage( + message, + { index = null, chat = null } = {}, +) { return ( Boolean(message) && !message.is_user && - !isSystemMessageForExtraction(message) + !isSystemMessageForExtraction(message, { index, chat }) ); } @@ -30,7 +51,7 @@ export function getAssistantTurns(chat) { const assistantTurns = []; // 从 index 1 开始:index 0 是角色卡首条消息(greeting),不参与提取 for (let index = 1; index < chat.length; index++) { - if (isAssistantChatMessage(chat[index])) { + if (isAssistantChatMessage(chat[index], { index, chat })) { assistantTurns.push(index); } } @@ -53,7 +74,7 @@ export function buildExtractionMessages(chat, startIdx, endIdx, settings) { index++ ) { const msg = chat[index]; - if (isSystemMessageForExtraction(msg)) continue; + if (isSystemMessageForExtraction(msg, { index, chat })) continue; messages.push({ seq: index, role: msg.is_user ? "user" : "assistant", @@ -70,7 +91,7 @@ export function getChatIndexForPlayableSeq(chat, playableSeq) { let currentSeq = -1; for (let index = 0; index < chat.length; index++) { const message = chat[index]; - if (isSystemMessageForExtraction(message)) continue; + if (isSystemMessageForExtraction(message, { index, chat })) continue; currentSeq++; if (currentSeq >= playableSeq) { return index; @@ -85,7 +106,7 @@ export function getChatIndexForAssistantSeq(chat, assistantSeq) { let currentSeq = -1; for (let index = 0; index < chat.length; index++) { - if (!isAssistantChatMessage(chat[index])) continue; + if (!isAssistantChatMessage(chat[index], { index, chat })) continue; currentSeq++; if (currentSeq >= assistantSeq) { return index; diff --git a/hide-engine.js b/hide-engine.js index 7eef184..97292f4 100644 --- a/hide-engine.js +++ b/hide-engine.js @@ -546,3 +546,11 @@ export function getHideStateSnapshot() { scheduled: Boolean(hideState.scheduledTimer), }; } + +export function isInManagedHideRange(index, chat = null) { + if (!Number.isFinite(index) || index < 0) return false; + if (!hideState.managedChatRef) return false; + if (Array.isArray(chat) && chat !== hideState.managedChatRef) return false; + + return hideState.managedSystemIndices.has(index); +} diff --git a/index.js b/index.js index 6ce9e59..f68d578 100644 --- a/index.js +++ b/index.js @@ -4360,22 +4360,6 @@ 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 = {}, @@ -5611,8 +5595,12 @@ const DEFAULT_TRIGGER_KEYWORDS = [ export function getSmartTriggerDecision(chat, lastProcessed, settings) { const pendingMessages = chat .slice(Math.max(0, (lastProcessed ?? -1) + 1)) - .filter((msg) => !isSystemMessageForExtraction(msg)) - .map((msg) => ({ + .map((msg, offset) => ({ + msg, + index: Math.max(0, (lastProcessed ?? -1) + 1) + offset, + })) + .filter(({ msg, index }) => !isSystemMessageForExtraction(msg, { index, chat })) + .map(({ msg }) => ({ role: msg.is_user ? "user" : "assistant", content: msg.mes || "", })) @@ -7879,6 +7867,30 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") { } } +function settleExtractionStatusAfterHistoryRecovery( + text = "提取完成", + meta = "", + level = "success", +) { + const statusSnapshot = + typeof lastExtractionStatus === "object" && lastExtractionStatus + ? lastExtractionStatus + : null; + if (!statusSnapshot || typeof setLastExtractionStatus !== "function") { + return; + } + + const currentText = String(statusSnapshot.text || ""); + const currentLevel = String(statusSnapshot.level || ""); + if (currentText !== "AI 生成中" && currentLevel !== "running") { + return; + } + setLastExtractionStatus(text, meta, level, { + syncRuntime: true, + toastKind: "", + }); +} + /** * 提取管线:处理未提取的对话楼层 */ diff --git a/tests/chat-history.mjs b/tests/chat-history.mjs index 7cc2389..0b1b6a3 100644 --- a/tests/chat-history.mjs +++ b/tests/chat-history.mjs @@ -1,4 +1,9 @@ import assert from "node:assert/strict"; +import { + applyHideSettings, + isInManagedHideRange, + resetHideState, +} from "../hide-engine.js"; import { buildExtractionMessages, getAssistantTurns, @@ -32,6 +37,23 @@ const realSystemMessage = { assert.equal(isSystemMessageForExtraction(realSystemMessage), true); assert.equal(isAssistantChatMessage(realSystemMessage), false); +function createRuntime(chat, chatId = "chat-a") { + return { + chat, + chatId, + async executeSlashCommands() { + return ""; + }, + getContext() { + return { + chat: this.chat, + chatId: this.chatId, + executeSlashCommands: this.executeSlashCommands.bind(this), + }; + }, + }; +} + const chat = [ { is_user: false, is_system: true, mes: "greeting/system" }, { is_user: true, is_system: false, mes: "user-1" }, @@ -65,4 +87,41 @@ assert.deepEqual( "extraction should keep BME-managed hidden context but still skip real system messages", ); +resetHideState(); +const autoHiddenChat = [ + { is_user: false, is_system: true, mes: "greeting/system" }, + { is_user: true, is_system: false, mes: "user-1" }, + { is_user: false, is_system: false, mes: "assistant-1" }, + { is_user: true, is_system: false, mes: "user-2" }, + { is_user: false, is_system: false, mes: "assistant-2" }, + { is_user: true, is_system: false, mes: "user-3" }, + { is_user: false, is_system: false, mes: "assistant-3" }, +]; +await applyHideSettings( + { enabled: true, hide_last_n: 2 }, + createRuntime(autoHiddenChat), +); + +assert.equal( + isInManagedHideRange(2, autoHiddenChat), + true, + "auto-hidden ordinary floors should be queryable from hide-engine managed range", +); +assert.equal( + isSystemMessageForExtraction(autoHiddenChat[2], { + index: 2, + chat: autoHiddenChat, + }), + false, + "auto-hidden ordinary floors inside managed range should remain extractable", +); +assert.equal( + isSystemMessageForExtraction(autoHiddenChat[0], { + index: 0, + chat: autoHiddenChat, + }), + true, + "greeting/system floor should still be treated as system even if hide range starts at 0", +); + console.log("chat-history tests passed");