Files
ST-Bionic-Memory-Ecology/hide-engine.js
2026-04-03 13:51:46 +08:00

557 lines
16 KiB
JavaScript

// ST-BME: old-message hide engine
// Uses the host's native /hide and /unhide slash commands instead of
// mutating chat messages into is_system=true.
const hideState = {
managedChatRef: null,
managedChatKey: null,
managedSystemIndices: new Set(),
hiddenRangeEnd: -1,
lastProcessedLength: 0,
scheduledTimer: null,
operationVersion: 0,
};
const BME_HIDE_HASH_MARKER = "__st_bme_hide_managed";
function getTimerApi(runtime = {}) {
const rawSetTimeout =
typeof runtime.setTimeout === "function"
? runtime.setTimeout
: globalThis.setTimeout;
const rawClearTimeout =
typeof runtime.clearTimeout === "function"
? runtime.clearTimeout
: globalThis.clearTimeout;
return {
setTimeout(...args) {
return Reflect.apply(rawSetTimeout, globalThis, args);
},
clearTimeout(...args) {
return Reflect.apply(rawClearTimeout, globalThis, args);
},
};
}
function getCurrentContext(runtime = {}) {
try {
return typeof runtime.getContext === "function" ? runtime.getContext() : null;
} catch {
return null;
}
}
function getCurrentChatInfo(runtime = {}) {
const context = getCurrentContext(runtime);
return {
chat: Array.isArray(context?.chat) ? context.chat : null,
chatId:
context?.chatId != null && context.chatId !== ""
? String(context.chatId)
: null,
};
}
function getCurrentChatKey(runtime = {}) {
const { chat, chatId } = getCurrentChatInfo(runtime);
if (chatId) return chatId;
return Array.isArray(chat) ? chat : null;
}
function getSlashExecutor(runtime = {}) {
if (typeof runtime.executeSlashCommands === "function") {
return runtime.executeSlashCommands.bind(runtime);
}
const context = getCurrentContext(runtime);
if (typeof context?.executeSlashCommands === "function") {
return context.executeSlashCommands.bind(context);
}
if (typeof globalThis.executeSlashCommands === "function") {
return globalThis.executeSlashCommands.bind(globalThis);
}
if (typeof globalThis.executeSlashCommandsOnChatInput === "function") {
return globalThis.executeSlashCommandsOnChatInput.bind(globalThis);
}
return null;
}
async function executeSlashCommand(command, runtime = {}) {
const executor = getSlashExecutor(runtime);
if (!executor) {
throw new Error("executeSlashCommands is not available");
}
return await executor(command, true);
}
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 getJquery(runtime = {}) {
if (typeof runtime.$ === "function") return runtime.$;
if (typeof globalThis.$ === "function") return globalThis.$;
return null;
}
function syncSystemAttribute(chat, indices = [], value = "true", runtime = {}) {
if (!Array.isArray(chat) || !Array.isArray(indices) || indices.length === 0) {
return;
}
const currentChat = getCurrentChatInfo(runtime).chat;
if (currentChat !== 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 calcHideRange(chatLength, hideLastN) {
if (!Number.isFinite(chatLength) || chatLength <= 0 || hideLastN <= 0) {
return null;
}
const visibleStart =
hideLastN >= chatLength ? 0 : Math.max(0, chatLength - hideLastN);
const hideEnd = visibleStart - 1;
if (hideEnd < 0) return null;
return {
start: 0,
end: hideEnd,
};
}
function beginOperation() {
hideState.operationVersion += 1;
return hideState.operationVersion;
}
function isOperationCurrent(version) {
return version === hideState.operationVersion;
}
function clearScheduledTimer(runtime = {}) {
const timers = getTimerApi(runtime);
if (hideState.scheduledTimer) {
timers.clearTimeout(hideState.scheduledTimer);
hideState.scheduledTimer = null;
}
}
function clearManagedState() {
hideState.managedChatRef = null;
hideState.managedChatKey = null;
hideState.managedSystemIndices.clear();
hideState.hiddenRangeEnd = -1;
hideState.lastProcessedLength = 0;
}
function isManagedSystemMessage(message) {
return Boolean(
message?.is_system === true &&
message?.extra &&
typeof message.extra === "object" &&
message.extra[BME_HIDE_HASH_MARKER] === true,
);
}
function collectManagedSystemIndices(chat) {
if (!Array.isArray(chat) || chat.length === 0) return [];
const indices = [];
for (let index = 0; index < chat.length; index++) {
if (isManagedSystemMessage(chat[index])) {
indices.push(index);
}
}
return indices;
}
function hydrateManagedStateFromChat(
chat,
chatKey = getCurrentChatKey(),
{ bootstrapLength = false } = {},
) {
if (!Array.isArray(chat)) {
hideState.managedSystemIndices.clear();
hideState.hiddenRangeEnd = -1;
return { managedCount: 0, hiddenRangeEnd: -1 };
}
const managedIndices = collectManagedSystemIndices(chat);
hideState.managedSystemIndices.clear();
for (const index of managedIndices) {
hideState.managedSystemIndices.add(index);
}
hideState.managedChatRef = chat;
hideState.managedChatKey = chatKey;
hideState.hiddenRangeEnd =
managedIndices.length > 0 ? managedIndices[managedIndices.length - 1] : -1;
if (managedIndices.length > 0 && bootstrapLength) {
hideState.lastProcessedLength = chat.length;
}
return {
managedCount: managedIndices.length,
hiddenRangeEnd: hideState.hiddenRangeEnd,
};
}
function restoreManagedSystemFlags(chat, runtime = {}) {
if (!Array.isArray(chat)) {
hideState.managedSystemIndices.clear();
return 0;
}
if (hideState.managedSystemIndices.size === 0) {
hydrateManagedStateFromChat(chat, getCurrentChatKey(runtime), {
bootstrapLength: false,
});
}
if (hideState.managedSystemIndices.size === 0) {
return 0;
}
const restored = [];
for (const index of hideState.managedSystemIndices) {
const message = chat[index];
if (!message || message.is_system !== true) continue;
message.is_system = false;
if (message.extra && typeof message.extra === "object") {
delete message.extra[BME_HIDE_HASH_MARKER];
if (Object.keys(message.extra).length === 0) {
delete message.extra;
}
}
restored.push(index);
}
syncSystemAttribute(chat, restored, "false", runtime);
hideState.managedSystemIndices.clear();
return restored.length;
}
function markManagedSystemRange(chat, start, end, runtime = {}) {
if (!Array.isArray(chat) || start > end) return 0;
const marked = [];
for (let index = start; index <= end && index < chat.length; index++) {
const message = chat[index];
if (!message || message.is_system === true) continue;
message.is_system = true;
const extra =
message.extra && typeof message.extra === "object" ? message.extra : {};
extra[BME_HIDE_HASH_MARKER] = true;
message.extra = extra;
hideState.managedSystemIndices.add(index);
marked.push(index);
}
syncSystemAttribute(chat, marked, "true", runtime);
return marked.length;
}
function adoptManagedChat(chat, chatKey, runtime = {}) {
const previousChat = hideState.managedChatRef;
if (previousChat && previousChat !== chat) {
restoreManagedSystemFlags(previousChat, runtime);
hideState.hiddenRangeEnd = -1;
hideState.lastProcessedLength = 0;
}
hideState.managedChatRef = chat;
hideState.managedChatKey = chatKey;
}
function buildResult({
active = false,
hiddenCount = 0,
shownCount = 0,
chatLength = 0,
incremental = false,
stale = false,
} = {}) {
return {
active,
hiddenCount,
shownCount,
managedCount: active ? hiddenCount : 0,
chatLength,
incremental,
stale,
};
}
async function unhideCurrentRange(runtime = {}, version = null, options = {}) {
const { chat } = getCurrentChatInfo(runtime);
const chatLength = Array.isArray(chat) ? chat.length : 0;
const full = Boolean(options.full);
const previousEnd = full
? Math.max(-1, chatLength - 1)
: Math.min(hideState.hiddenRangeEnd, chatLength - 1);
if (previousEnd < 0) {
return { shownCount: 0, chatLength };
}
await executeSlashCommand(`/unhide 0-${previousEnd}`, runtime);
if (!isOperationCurrent(version ?? hideState.operationVersion)) {
return { shownCount: 0, chatLength, stale: true };
}
return { shownCount: previousEnd + 1, chatLength };
}
async function runHideApply(settings = {}, runtime = {}, options = {}) {
const normalized = normalizeHideSettings(settings);
const { incrementalPreferred = false, version = beginOperation() } = options;
const chatInfo = getCurrentChatInfo(runtime);
const chat = chatInfo.chat;
const chatLength = Array.isArray(chat) ? chat.length : 0;
if (!chat || chatLength === 0) {
clearManagedState();
return buildResult();
}
const chatKey = getCurrentChatKey(runtime);
const previousChatKey = hideState.managedChatKey;
const hadTrackedState =
hideState.managedSystemIndices.size > 0 ||
hideState.hiddenRangeEnd >= 0 ||
(Number.isFinite(hideState.lastProcessedLength) &&
hideState.lastProcessedLength > 0);
adoptManagedChat(chat, chatKey, runtime);
hydrateManagedStateFromChat(chat, chatKey, {
bootstrapLength: !hadTrackedState,
});
const sameChat =
previousChatKey !== null && chatKey !== null && previousChatKey === chatKey;
const previousHiddenEnd = hideState.hiddenRangeEnd;
const previousLength =
sameChat && Number.isFinite(hideState.lastProcessedLength)
? hideState.lastProcessedLength
: 0;
hideState.lastProcessedLength = chatLength;
if (!normalized.enabled || normalized.hideLastN <= 0) {
if (previousHiddenEnd >= 0) {
const { shownCount } = await unhideCurrentRange(runtime, version);
if (!isOperationCurrent(version)) {
return buildResult({ chatLength, shownCount, stale: true });
}
restoreManagedSystemFlags(chat, runtime);
hideState.hiddenRangeEnd = -1;
return buildResult({ chatLength, shownCount });
}
restoreManagedSystemFlags(chat, runtime);
hideState.hiddenRangeEnd = -1;
return buildResult({ chatLength });
}
const nextRange = calcHideRange(chatLength, normalized.hideLastN);
if (!nextRange) {
if (previousHiddenEnd >= 0) {
const { shownCount } = await unhideCurrentRange(runtime, version);
if (!isOperationCurrent(version)) {
return buildResult({ chatLength, shownCount, stale: true });
}
restoreManagedSystemFlags(chat, runtime);
hideState.hiddenRangeEnd = -1;
return buildResult({ chatLength, shownCount });
}
restoreManagedSystemFlags(chat, runtime);
hideState.hiddenRangeEnd = -1;
return buildResult({
active: true,
hiddenCount: 0,
chatLength,
});
}
if (
incrementalPreferred &&
sameChat &&
previousHiddenEnd >= 0 &&
chatLength > previousLength &&
previousLength > 0
) {
const previousRange = calcHideRange(previousLength, normalized.hideLastN);
const canExtendOnly =
previousRange &&
previousRange.end === previousHiddenEnd &&
nextRange.end >= previousHiddenEnd;
if (canExtendOnly && nextRange.end > previousHiddenEnd) {
const start = previousHiddenEnd + 1;
const end = nextRange.end;
await executeSlashCommand(`/hide ${start}-${end}`, runtime);
if (!isOperationCurrent(version)) {
return buildResult({ chatLength, stale: true });
}
markManagedSystemRange(chat, start, end, runtime);
hideState.hiddenRangeEnd = end;
return buildResult({
active: true,
hiddenCount: end + 1,
shownCount: 0,
chatLength,
incremental: true,
});
}
}
let shownCount = 0;
if (previousHiddenEnd >= 0) {
const unhideResult = await unhideCurrentRange(runtime, version);
if (!isOperationCurrent(version)) {
return buildResult({
chatLength,
shownCount: unhideResult.shownCount ?? 0,
stale: true,
});
}
shownCount = unhideResult.shownCount ?? 0;
}
restoreManagedSystemFlags(chat, runtime);
await executeSlashCommand(`/hide ${nextRange.start}-${nextRange.end}`, runtime);
if (!isOperationCurrent(version)) {
return buildResult({ chatLength, shownCount, stale: true });
}
markManagedSystemRange(chat, nextRange.start, nextRange.end, runtime);
hideState.hiddenRangeEnd = nextRange.end;
hideState.lastProcessedLength = chatLength;
return buildResult({
active: true,
hiddenCount: nextRange.end + 1,
shownCount,
chatLength,
incremental: false,
});
}
export async function runFullHideCheck(settings = {}, runtime = {}) {
return await runHideApply(settings, runtime, {
incrementalPreferred: false,
version: beginOperation(),
});
}
export async function runIncrementalHideCheck(settings = {}, runtime = {}) {
return await runHideApply(settings, runtime, {
incrementalPreferred: true,
version: beginOperation(),
});
}
export async function applyHideSettings(settings = {}, runtime = {}) {
return await runFullHideCheck(settings, runtime);
}
export function scheduleHideSettingsApply(
settings = {},
runtime = {},
delayMs = 120,
) {
clearScheduledTimer(runtime);
const timers = getTimerApi(runtime);
const snapshot = normalizeHideSettings(settings);
hideState.scheduledTimer = timers.setTimeout(() => {
hideState.scheduledTimer = null;
void applyHideSettings(snapshot, runtime).catch((error) => {
console.warn?.("[ST-BME] scheduled hide apply failed", error);
});
}, Math.max(0, Math.trunc(Number(delayMs) || 0)));
}
export async function unhideAll(runtime = {}) {
clearScheduledTimer(runtime);
const version = beginOperation();
const chatInfo = getCurrentChatInfo(runtime);
const chatLength = Array.isArray(chatInfo.chat) ? chatInfo.chat.length : 0;
if (chatLength === 0) {
hideState.lastProcessedLength = chatLength;
hideState.hiddenRangeEnd = -1;
hideState.managedChatKey = getCurrentChatKey(runtime);
return buildResult({ chatLength });
}
hydrateManagedStateFromChat(chatInfo.chat, getCurrentChatKey(runtime), {
bootstrapLength: false,
});
const { shownCount } = await unhideCurrentRange(runtime, version, {
full: true,
});
if (!isOperationCurrent(version)) {
return buildResult({ chatLength, shownCount, stale: true });
}
restoreManagedSystemFlags(chatInfo.chat, runtime);
hideState.hiddenRangeEnd = -1;
hideState.lastProcessedLength = chatLength;
hideState.managedChatRef = chatInfo.chat;
hideState.managedChatKey = getCurrentChatKey(runtime);
return buildResult({ chatLength, shownCount });
}
export function resetHideState(runtime = {}) {
clearScheduledTimer(runtime);
beginOperation();
const chatInfo = getCurrentChatInfo(runtime);
if (Array.isArray(chatInfo.chat)) {
hydrateManagedStateFromChat(chatInfo.chat, chatInfo.chatId || null, {
bootstrapLength: false,
});
}
restoreManagedSystemFlags(hideState.managedChatRef, runtime);
clearManagedState();
}
export function getHideStateSnapshot() {
return {
hasManagedChat: hideState.managedChatRef !== null,
managedHiddenCount: hideState.hiddenRangeEnd >= 0 ? hideState.hiddenRangeEnd + 1 : 0,
lastProcessedLength: hideState.lastProcessedLength,
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);
}