diff --git a/event-binding.js b/event-binding.js index 10d7251..e0f5fca 100644 --- a/event-binding.js +++ b/event-binding.js @@ -129,6 +129,9 @@ export function registerCoreEventHooksController(runtime) { if (eventTypes.GENERATION_STARTED) { bind(eventTypes.GENERATION_STARTED, handlers.onGenerationStarted); } + if (eventTypes.GENERATION_ENDED) { + bind(eventTypes.GENERATION_ENDED, handlers.onGenerationEnded); + } const beforeCombineCleanup = runtime.registerBeforeCombinePrompts( handlers.onBeforeCombinePrompts, diff --git a/hide-engine.js b/hide-engine.js new file mode 100644 index 0000000..ba5e7c5 --- /dev/null +++ b/hide-engine.js @@ -0,0 +1,303 @@ +// ST-BME: 隐藏旧楼层引擎 +// 通过临时把旧楼层标记为 is_system=true,让宿主主回复与 ST-BME 自己的聊天读取一起跳过这些楼层。 + +const hideState = { + managedChatRef: null, + hiddenIndices: new Set(), + lastProcessedLength: 0, + scheduledTimer: null, +}; + +function getTimerApi(runtime = {}) { + return { + setTimeout: + typeof runtime.setTimeout === "function" + ? runtime.setTimeout.bind(runtime) + : globalThis.setTimeout.bind(globalThis), + clearTimeout: + typeof runtime.clearTimeout === "function" + ? runtime.clearTimeout.bind(runtime) + : globalThis.clearTimeout.bind(globalThis), + }; +} + +function getJquery(runtime = {}) { + if (typeof runtime.$ === "function") return runtime.$; + if (typeof globalThis.$ === "function") return globalThis.$; + return null; +} + +function getCurrentChat(runtime = {}) { + try { + const context = + typeof runtime.getContext === "function" ? runtime.getContext() : null; + return Array.isArray(context?.chat) ? context.chat : null; + } catch { + return null; + } +} + +function normalizeHideSettings(settings = {}) { + return { + enabled: Boolean(settings.enabled), + hideLastN: Math.max( + 0, + Math.trunc( + Number( + settings.hideLastN ?? + settings.hide_last_n ?? + settings.keepLastN ?? + settings.keep_last_n ?? + 0, + ) || 0, + ), + ), + }; +} + +function syncSystemAttribute(chat, indices = [], value = "true", runtime = {}) { + if (!Array.isArray(indices) || indices.length === 0) return; + if (getCurrentChat(runtime) !== chat) return; + + const jq = getJquery(runtime); + if (!jq) return; + + const selector = indices.map((index) => `.mes[mesid="${index}"]`).join(","); + if (!selector) return; + jq(selector).attr("is_system", value); +} + +function unhideTrackedChat(chat, runtime = {}) { + if (!Array.isArray(chat) || hideState.hiddenIndices.size === 0) { + return { shownCount: 0 }; + } + + const toShow = []; + for (const index of hideState.hiddenIndices) { + const message = chat[index]; + if (!message || message.is_system !== true) continue; + message.is_system = false; + toShow.push(index); + } + + syncSystemAttribute(chat, toShow, "false", runtime); + return { shownCount: toShow.length }; +} + +function swapManagedChat(nextChat, runtime = {}) { + const previousChat = hideState.managedChatRef; + if (previousChat && previousChat !== nextChat) { + unhideTrackedChat(previousChat, runtime); + hideState.hiddenIndices.clear(); + hideState.lastProcessedLength = 0; + } + hideState.managedChatRef = nextChat; +} + +export function runFullHideCheck(settings = {}, runtime = {}) { + const normalized = normalizeHideSettings(settings); + const chat = getCurrentChat(runtime); + if (!chat || chat.length === 0) { + resetHideState(runtime); + return { + active: false, + hiddenCount: 0, + shownCount: 0, + managedCount: 0, + chatLength: 0, + }; + } + + swapManagedChat(chat, runtime); + + if (!normalized.enabled || normalized.hideLastN <= 0) { + const { shownCount } = unhideTrackedChat(chat, runtime); + hideState.hiddenIndices.clear(); + hideState.lastProcessedLength = chat.length; + return { + active: false, + hiddenCount: 0, + shownCount, + managedCount: 0, + chatLength: chat.length, + }; + } + + const visibleStart = + normalized.hideLastN >= chat.length + ? 0 + : Math.max(0, chat.length - normalized.hideLastN); + const desiredHiddenIndices = new Set(); + const managedHiddenIndices = new Set(); + const toHide = []; + const toShow = []; + + for (let index = 0; index < chat.length; index++) { + const message = chat[index]; + if (!message) continue; + + const shouldHide = index < visibleStart; + const isHidden = message.is_system === true; + const wasHiddenByBme = hideState.hiddenIndices.has(index); + + if (shouldHide) { + desiredHiddenIndices.add(index); + if (wasHiddenByBme || !isHidden) { + managedHiddenIndices.add(index); + } + if (!isHidden) { + message.is_system = true; + toHide.push(index); + } + continue; + } + + if (isHidden && wasHiddenByBme) { + message.is_system = false; + toShow.push(index); + } + } + + syncSystemAttribute(chat, [...desiredHiddenIndices], "true", runtime); + syncSystemAttribute(chat, toShow, "false", runtime); + + hideState.hiddenIndices = managedHiddenIndices; + hideState.lastProcessedLength = chat.length; + + return { + active: true, + hiddenCount: toHide.length, + shownCount: toShow.length, + managedCount: managedHiddenIndices.size, + chatLength: chat.length, + }; +} + +export function runIncrementalHideCheck(settings = {}, runtime = {}) { + const normalized = normalizeHideSettings(settings); + const chat = getCurrentChat(runtime); + if (!chat || chat.length === 0) { + resetHideState(runtime); + return { + active: false, + hiddenCount: 0, + shownCount: 0, + managedCount: 0, + chatLength: 0, + incremental: false, + }; + } + + if ( + hideState.managedChatRef !== chat || + !normalized.enabled || + normalized.hideLastN <= 0 + ) { + return { + ...runFullHideCheck(normalized, runtime), + incremental: false, + }; + } + + const chatLength = chat.length; + const previousLength = hideState.lastProcessedLength; + if (chatLength <= previousLength) { + return { + ...runFullHideCheck(normalized, runtime), + incremental: false, + }; + } + + const previousVisibleStart = + previousLength > 0 ? Math.max(0, previousLength - normalized.hideLastN) : 0; + const nextVisibleStart = Math.max(0, chatLength - normalized.hideLastN); + const toHide = []; + + if (nextVisibleStart > previousVisibleStart) { + for (let index = previousVisibleStart; index < nextVisibleStart; index++) { + const message = chat[index]; + if (!message || message.is_system === true) continue; + message.is_system = true; + hideState.hiddenIndices.add(index); + toHide.push(index); + } + } + + syncSystemAttribute(chat, toHide, "true", runtime); + hideState.lastProcessedLength = chatLength; + + return { + active: true, + hiddenCount: toHide.length, + shownCount: 0, + managedCount: hideState.hiddenIndices.size, + chatLength, + incremental: true, + }; +} + +export function applyHideSettings(settings = {}, runtime = {}) { + return runFullHideCheck(settings, runtime); +} + +export function scheduleHideSettingsApply( + settings = {}, + runtime = {}, + delayMs = 120, +) { + const timers = getTimerApi(runtime); + if (hideState.scheduledTimer) { + timers.clearTimeout(hideState.scheduledTimer); + hideState.scheduledTimer = null; + } + + const snapshot = normalizeHideSettings(settings); + hideState.scheduledTimer = timers.setTimeout(() => { + hideState.scheduledTimer = null; + applyHideSettings(snapshot, runtime); + }, Math.max(0, Math.trunc(Number(delayMs) || 0))); +} + +export function unhideAll(runtime = {}) { + const timers = getTimerApi(runtime); + if (hideState.scheduledTimer) { + timers.clearTimeout(hideState.scheduledTimer); + hideState.scheduledTimer = null; + } + + const managedChat = hideState.managedChatRef; + const { shownCount } = unhideTrackedChat(managedChat, runtime); + hideState.hiddenIndices.clear(); + hideState.lastProcessedLength = Array.isArray(managedChat) + ? managedChat.length + : 0; + + return { + active: false, + shownCount, + managedCount: 0, + }; +} + +export function resetHideState(runtime = {}) { + const timers = getTimerApi(runtime); + if (hideState.scheduledTimer) { + timers.clearTimeout(hideState.scheduledTimer); + hideState.scheduledTimer = null; + } + + const managedChat = hideState.managedChatRef; + unhideTrackedChat(managedChat, runtime); + hideState.managedChatRef = null; + hideState.hiddenIndices.clear(); + hideState.lastProcessedLength = 0; +} + +export function getHideStateSnapshot() { + return { + hasManagedChat: Boolean(hideState.managedChatRef), + managedHiddenCount: hideState.hiddenIndices.size, + lastProcessedLength: hideState.lastProcessedLength, + scheduled: Boolean(hideState.scheduledTimer), + }; +} diff --git a/index.js b/index.js index 2a83b3d..e7fa219 100644 --- a/index.js +++ b/index.js @@ -81,6 +81,14 @@ import { writeChatMetadataPatch, writeGraphShadowSnapshot, } from "./graph-persistence.js"; +import { + applyHideSettings, + getHideStateSnapshot, + resetHideState, + runIncrementalHideCheck, + scheduleHideSettingsApply, + unhideAll, +} from "./hide-engine.js"; import { createEmptyGraph, deserializeGraph, @@ -333,6 +341,8 @@ function readRuntimeDebugSnapshot() { const defaultSettings = { enabled: true, timeoutMs: 300000, + hideOldMessagesEnabled: false, + hideOldMessagesKeepLastN: 12, // 提取设置 extractEvery: 1, // 每 N 条 assistant 回复提取一次 @@ -2246,6 +2256,105 @@ function getSettings() { return mergedSettings; } +function getMessageHideSettings(settings = null) { + let sourceSettings = settings; + if (!sourceSettings || typeof sourceSettings !== "object") { + try { + sourceSettings = + typeof getSettings === "function" ? getSettings() : {}; + } catch { + sourceSettings = {}; + } + } + return { + enabled: Boolean(sourceSettings.hideOldMessagesEnabled), + hide_last_n: Math.max( + 0, + Math.trunc(Number(sourceSettings.hideOldMessagesKeepLastN ?? 0) || 0), + ), + }; +} + +function getHideRuntimeAdapters() { + return { + $, + clearTimeout, + getContext, + setTimeout, + }; +} + +function applyMessageHideNow(reason = "manual-apply") { + try { + const result = applyHideSettings( + getMessageHideSettings(), + getHideRuntimeAdapters(), + ); + console.log("[ST-BME] 已应用旧楼层隐藏:", reason, result); + return result; + } catch (error) { + console.warn("[ST-BME] 应用旧楼层隐藏失败:", reason, error); + return { + active: false, + error: error instanceof Error ? error.message : String(error || "未知错误"), + }; + } +} + +function scheduleMessageHideApply(reason = "scheduled", delayMs = 120) { + try { + scheduleHideSettingsApply( + getMessageHideSettings(), + getHideRuntimeAdapters(), + delayMs, + ); + } catch (error) { + console.warn("[ST-BME] 调度旧楼层隐藏失败:", reason, error); + } +} + +function runIncrementalMessageHide(reason = "incremental") { + try { + const result = runIncrementalHideCheck( + getMessageHideSettings(), + getHideRuntimeAdapters(), + ); + if (result?.active) { + console.log("[ST-BME] 已增量更新旧楼层隐藏:", reason, result); + } + return result; + } catch (error) { + console.warn("[ST-BME] 增量更新旧楼层隐藏失败:", reason, error); + return { + active: false, + error: error instanceof Error ? error.message : String(error || "未知错误"), + }; + } +} + +function clearMessageHideState(reason = "reset") { + try { + resetHideState(getHideRuntimeAdapters()); + console.log("[ST-BME] 已重置旧楼层隐藏状态:", reason); + } catch (error) { + console.warn("[ST-BME] 重置旧楼层隐藏状态失败:", reason, error); + } +} + +function clearAllHiddenMessages(reason = "manual-clear") { + try { + const result = unhideAll(getHideRuntimeAdapters()); + console.log("[ST-BME] 已取消全部旧楼层隐藏:", reason, result); + return result; + } catch (error) { + console.warn("[ST-BME] 取消全部旧楼层隐藏失败:", reason, error); + return { + active: false, + error: error instanceof Error ? error.message : String(error || "未知错误"), + }; + } +} + function initializeHostCapabilityBridge(options = {}) { try { initializeHostAdapter({ @@ -2324,6 +2433,7 @@ export function getPanelRuntimeDebugSnapshot(options = {}) { return { hostCapabilities, + messageHiding: getHideStateSnapshot(), runtimeDebug: readRuntimeDebugSnapshot(), }; } @@ -4732,6 +4842,10 @@ function updateModuleSettings(patch = {}) { "embeddingBackendApiUrl", "embeddingAutoSuffix", ]); + const messageHideKeys = new Set([ + "hideOldMessagesEnabled", + "hideOldMessagesKeepLastN", + ]); const settings = getSettings(); Object.assign(settings, patch); extension_settings[MODULE_NAME] = settings; @@ -4779,6 +4893,15 @@ function updateModuleSettings(patch = {}) { ); } + if (Object.keys(patch).some((key) => messageHideKeys.has(key))) { + const hideSettings = getMessageHideSettings(settings); + if (!hideSettings.enabled || hideSettings.hide_last_n <= 0) { + clearAllHiddenMessages("settings-updated-disable"); + } else { + scheduleMessageHideApply("settings-updated", 30); + } + } + scheduleServerSettingsSave(); return settings; } @@ -7638,6 +7761,9 @@ async function runRecall(options = {}) { // ==================== 事件钩子 ==================== function onChatChanged() { + if (typeof clearMessageHideState === "function") { + clearMessageHideState("chat-changed"); + } const result = onChatChangedController({ abortAllRunningStages, clearCoreEventBindingState, @@ -7682,6 +7808,10 @@ function onChatChanged() { } }); + if (typeof scheduleMessageHideApply === "function") { + scheduleMessageHideApply("chat-changed", 220); + } + return result; } @@ -7703,11 +7833,15 @@ function onChatLoaded() { } }); + if (typeof scheduleMessageHideApply === "function") { + scheduleMessageHideApply("chat-loaded", 180); + } + return result; } function onMessageSent(messageId) { - return onMessageSentController( + const result = onMessageSentController( { getContext, recordRecallSentUserMessage, @@ -7715,10 +7849,14 @@ function onMessageSent(messageId) { }, messageId, ); + if (typeof scheduleMessageHideApply === "function") { + scheduleMessageHideApply("message-sent", 40); + } + return result; } function onMessageDeleted(chatLengthOrMessageId, meta = null) { - return onMessageDeletedController( + const result = onMessageDeletedController( { invalidateRecallAfterHistoryMutation, refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh, @@ -7727,10 +7865,14 @@ function onMessageDeleted(chatLengthOrMessageId, meta = null) { chatLengthOrMessageId, meta, ); + if (typeof scheduleMessageHideApply === "function") { + scheduleMessageHideApply("message-deleted", 80); + } + return result; } function onMessageEdited(messageId, meta = null) { - return onMessageEditedController( + const result = onMessageEditedController( { invalidateRecallAfterHistoryMutation, refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh, @@ -7739,10 +7881,14 @@ function onMessageEdited(messageId, meta = null) { messageId, meta, ); + if (typeof scheduleMessageHideApply === "function") { + scheduleMessageHideApply("message-edited", 80); + } + return result; } function onMessageSwiped(messageId, meta = null) { - return onMessageSwipedController( + const result = onMessageSwipedController( { invalidateRecallAfterHistoryMutation, refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh, @@ -7751,6 +7897,10 @@ function onMessageSwiped(messageId, meta = null) { messageId, meta, ); + if (typeof scheduleMessageHideApply === "function") { + scheduleMessageHideApply("message-swiped", 80); + } + return result; } function onGenerationStarted(type, params = {}, dryRun = false) { @@ -7768,6 +7918,12 @@ function onGenerationStarted(type, params = {}, dryRun = false) { ); } +function onGenerationEnded(_chatLength = null) { + if (typeof scheduleMessageHideApply === "function") { + scheduleMessageHideApply("generation-ended", 180); + } +} + async function onGenerationAfterCommands(type, params = {}, dryRun = false) { return await onGenerationAfterCommandsController( { @@ -7812,7 +7968,7 @@ async function onBeforeCombinePrompts(promptData = null) { } function onMessageReceived(messageId = null, type = "") { - return onMessageReceivedController({ + const result = onMessageReceivedController({ console, createRecallInputRecord, getContext, @@ -7837,6 +7993,20 @@ function onMessageReceived(messageId = null, type = "") { pendingRecallSendIntent = record; }, }, messageId, type); + + const hideSettings = + typeof getMessageHideSettings === "function" + ? getMessageHideSettings() + : null; + if ( + hideSettings?.enabled && + hideSettings?.hide_last_n > 0 && + typeof runIncrementalMessageHide === "function" + ) { + runIncrementalMessageHide("message-received"); + } + + return result; } // ==================== UI 操作 ==================== @@ -8131,6 +8301,7 @@ async function onReembedDirect() { initializeHostCapabilityBridge(); installSendIntentHooks(); autoSyncOnVisibility(buildBmeSyncRuntimeOptions()); + scheduleMessageHideApply("init", 180); // 注册事件钩子 registerCoreEventHooksController({ @@ -8143,6 +8314,7 @@ async function onReembedDirect() { onChatChanged, onChatLoaded, onGenerationAfterCommands, + onGenerationEnded, onGenerationStarted, onMessageDeleted, onMessageEdited, diff --git a/package.json b/package.json index d0749e1..92d85e1 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,14 @@ "test:p0": "node tests/p0-regressions.mjs", "test:runtime-history": "node tests/runtime-history.mjs", "test:graph-persistence": "node tests/graph-persistence.mjs", + "test:hide-engine": "node tests/hide-engine.mjs", "test:indexeddb-persistence": "node tests/indexeddb-persistence.mjs", "test:indexeddb-sync": "node tests/indexeddb-sync.mjs", "test:indexeddb-migration": "node tests/indexeddb-migration.mjs", "test:indexeddb": "npm run test:indexeddb-persistence && npm run test:indexeddb-sync && npm run test:indexeddb-migration", "test:persistence-matrix": "npm run test:p0 && npm run test:runtime-history && npm run test:graph-persistence && npm run test:indexeddb", "test:all": "npm run test:persistence-matrix", - "check": "node --check index.js && node --check bme-db.js && node --check panel.js && node --check ui-status.js && node --check event-binding.js" + "check": "node --check index.js && node --check bme-db.js && node --check hide-engine.js && node --check panel.js && node --check ui-status.js && node --check event-binding.js" }, "dependencies": { "triviumdb": "^0.4.41" diff --git a/panel.html b/panel.html index b94409a..b42075f 100644 --- a/panel.html +++ b/panel.html @@ -952,6 +952,66 @@ + +
+
+
+
隐藏旧楼层
+
+ 不删除聊天内容,只是把较早楼层临时标成系统消息,让主回复和 ST-BME 自己读取聊天时一起跳过它们。 +
+
+
+ +
+ + +
+
+ 设置修改后会自动生效。`0` 表示不隐藏;“取消全部隐藏”会立即把当前聊天里由 ST-BME 隐藏的楼层恢复。 +
+
+ + +
+
diff --git a/panel.js b/panel.js index 062c1ef..bc232cb 100644 --- a/panel.js +++ b/panel.js @@ -1420,6 +1420,10 @@ function _refreshConfigTab() { const settings = _getSettings?.() || {}; _setCheckboxValue("bme-setting-enabled", settings.enabled ?? true); + _setCheckboxValue( + "bme-setting-hide-old-messages-enabled", + settings.hideOldMessagesEnabled ?? false, + ); _setCheckboxValue( "bme-setting-recall-enabled", settings.recallEnabled ?? true, @@ -1495,6 +1499,10 @@ function _refreshConfigTab() { ); _setInputValue("bme-setting-extract-every", settings.extractEvery ?? 1); + _setInputValue( + "bme-setting-hide-old-messages-keep-last-n", + settings.hideOldMessagesKeepLastN ?? 12, + ); _setInputValue( "bme-setting-extract-context-turns", settings.extractContextTurns ?? 2, @@ -1695,6 +1703,9 @@ function _bindConfigControls() { _patchSettings({ enabled: checked }); _refreshGuardedConfigStates(); }); + bindCheckbox("bme-setting-hide-old-messages-enabled", (checked) => { + _patchSettings({ hideOldMessagesEnabled: checked }); + }); bindCheckbox("bme-setting-recall-enabled", (checked) => { _patchSettings({ recallEnabled: checked }); _refreshGuardedConfigStates(); @@ -1768,6 +1779,13 @@ function _bindConfigControls() { bindNumber("bme-setting-extract-every", 1, 1, 50, (value) => _patchSettings({ extractEvery: value }), ); + bindNumber( + "bme-setting-hide-old-messages-keep-last-n", + 12, + 0, + 200, + (value) => _patchSettings({ hideOldMessagesKeepLastN: value }), + ); bindNumber("bme-setting-extract-context-turns", 2, 0, 20, (value) => _patchSettings({ extractContextTurns: value }), ); @@ -2024,6 +2042,27 @@ function _bindConfigControls() { card.dataset.bmeBound = "true"; }); + document + .getElementById("bme-apply-hide-settings") + ?.addEventListener("click", () => { + const settings = _getSettings?.() || {}; + _patchSettings({ + hideOldMessagesEnabled: settings.hideOldMessagesEnabled ?? false, + hideOldMessagesKeepLastN: settings.hideOldMessagesKeepLastN ?? 12, + }); + toastr.success("当前聊天的隐藏设置已重新应用", "ST-BME"); + }); + document + .getElementById("bme-clear-hide-settings") + ?.addEventListener("click", () => { + _patchSettings({ + hideOldMessagesEnabled: false, + hideOldMessagesKeepLastN: 0, + }); + _setCheckboxValue("bme-setting-hide-old-messages-enabled", false); + _setInputValue("bme-setting-hide-old-messages-keep-last-n", 0); + toastr.info("已取消当前聊天里由 ST-BME 应用的隐藏", "ST-BME"); + }); document .getElementById("bme-test-llm") ?.addEventListener("click", async () => { diff --git a/tests/hide-engine.mjs b/tests/hide-engine.mjs new file mode 100644 index 0000000..d87c43e --- /dev/null +++ b/tests/hide-engine.mjs @@ -0,0 +1,119 @@ +import assert from "node:assert/strict"; + +import { + applyHideSettings, + getHideStateSnapshot, + resetHideState, + runIncrementalHideCheck, + unhideAll, +} from "../hide-engine.js"; + +function createRuntime(chat) { + const domWrites = []; + return { + chat, + domWrites, + getContext() { + return { chat: this.chat }; + }, + $(selector) { + return { + attr(name, value) { + domWrites.push({ selector, name, value }); + }, + }; + }, + }; +} + +function testApplyAndUnhidePreservesOriginalSystemMessages() { + const chat = [ + { mes: "原系统", is_system: true }, + { mes: "用户1", is_user: true, is_system: false }, + { mes: "助手1", is_user: false, is_system: false }, + { mes: "用户2", is_user: true, is_system: false }, + { mes: "助手2", is_user: false, is_system: false }, + ]; + const runtime = createRuntime(chat); + + const applyResult = applyHideSettings( + { enabled: true, hide_last_n: 2 }, + runtime, + ); + assert.equal(applyResult.active, true); + assert.equal(chat[0].is_system, true); + assert.equal(chat[1].is_system, true); + assert.equal(chat[2].is_system, true); + assert.equal(chat[3].is_system, false); + assert.equal(chat[4].is_system, false); + assert.equal(applyResult.managedCount, 2); + + const unhideResult = unhideAll(runtime); + assert.equal(unhideResult.active, false); + assert.equal(chat[0].is_system, true, "原系统消息不应被恢复"); + assert.equal(chat[1].is_system, false); + assert.equal(chat[2].is_system, false); +} + +function testResetRestoresPreviousManagedChat() { + const oldChat = [ + { mes: "用户1", is_user: true, is_system: false }, + { mes: "助手1", is_user: false, is_system: false }, + { mes: "用户2", is_user: true, is_system: false }, + { mes: "助手2", is_user: false, is_system: false }, + ]; + const newChat = [ + { mes: "新用户", is_user: true, is_system: false }, + { mes: "新助手", is_user: false, is_system: false }, + ]; + const runtime = createRuntime(oldChat); + + applyHideSettings({ enabled: true, hide_last_n: 1 }, runtime); + assert.equal(oldChat[0].is_system, true); + assert.equal(oldChat[1].is_system, true); + assert.equal(oldChat[2].is_system, true); + + runtime.chat = newChat; + resetHideState(runtime); + + assert.equal(oldChat[0].is_system, false); + assert.equal(oldChat[1].is_system, false); + assert.equal(oldChat[2].is_system, false); + assert.deepEqual(getHideStateSnapshot(), { + hasManagedChat: false, + managedHiddenCount: 0, + lastProcessedLength: 0, + scheduled: false, + }); +} + +function testIncrementalHideOnlyHidesNewOverflowMessages() { + const chat = [ + { mes: "用户1", is_user: true, is_system: false }, + { mes: "助手1", is_user: false, is_system: false }, + { mes: "用户2", is_user: true, is_system: false }, + ]; + const runtime = createRuntime(chat); + + applyHideSettings({ enabled: true, hide_last_n: 2 }, runtime); + assert.equal(chat[0].is_system, true); + assert.equal(chat[1].is_system, false); + assert.equal(chat[2].is_system, false); + + chat.push({ mes: "助手2", is_user: false, is_system: false }); + const result = runIncrementalHideCheck( + { enabled: true, hide_last_n: 2 }, + runtime, + ); + assert.equal(result.incremental, true); + assert.equal(result.hiddenCount, 1); + assert.equal(chat[1].is_system, true); + assert.equal(chat[2].is_system, false); + assert.equal(chat[3].is_system, false); +} + +testApplyAndUnhidePreservesOriginalSystemMessages(); +testResetRestoresPreviousManagedChat(); +testIncrementalHideOnlyHidesNewOverflowMessages(); + +console.log("hide-engine tests passed");