mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-14 02:40:45 +08:00
refactor(ui): extract message-render-limit module, migrate test off index.js slicing
This commit is contained in:
208
ui/message-render-limit.js
Normal file
208
ui/message-render-limit.js
Normal file
@@ -0,0 +1,208 @@
|
||||
// ST-BME message render-limit policy.
|
||||
//
|
||||
// Extracted from index.js so it can be unit-tested by direct import instead of
|
||||
// slicing index.js into a temp module. Pure decisions take plain arguments;
|
||||
// the one side-effecting entry (applyMessageRenderLimit) receives an explicit
|
||||
// host adapter, so this module owns no module-level mutable state and never
|
||||
// reaches for globals on its own.
|
||||
|
||||
/**
|
||||
* Normalizes render-limit settings into {enabled, render_last_n}.
|
||||
* @param {object|null} settings
|
||||
* @param {() => object} [resolveSettings] fallback settings source when none passed
|
||||
*/
|
||||
export function getMessageRenderLimitSettings(settings = null, resolveSettings = null) {
|
||||
let sourceSettings = settings;
|
||||
if (!sourceSettings || typeof sourceSettings !== "object") {
|
||||
try {
|
||||
sourceSettings =
|
||||
typeof resolveSettings === "function" ? resolveSettings() : {};
|
||||
} catch {
|
||||
sourceSettings = {};
|
||||
}
|
||||
}
|
||||
return {
|
||||
enabled:
|
||||
sourceSettings.enabled !== false &&
|
||||
Boolean(sourceSettings.hideOldMessagesRenderLimitEnabled),
|
||||
render_last_n: Math.max(
|
||||
0,
|
||||
Math.trunc(Number(sourceSettings.hideOldMessagesRenderLimit ?? 0) || 0),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the render limit to the host (power_user.chat_truncation + jQuery
|
||||
* truncation inputs), optionally reloading the chat.
|
||||
*
|
||||
* @param {object|null} settings
|
||||
* @param {object} [options] {clearWhenDisabled, reloadCurrentChat}
|
||||
* @param {object} [host] injected host adapter
|
||||
* @param {() => object|null} [host.getPowerUser]
|
||||
* @param {(selector: string) => any} [host.jq] jQuery-like selector
|
||||
* @param {() => void} [host.reloadCurrentChat]
|
||||
* @param {() => object|null} [host.resolveSettings]
|
||||
* @param {Console} [host.console]
|
||||
*/
|
||||
export function applyMessageRenderLimit(settings = null, options = {}, host = {}) {
|
||||
const logger = host.console || console;
|
||||
const normalized = getMessageRenderLimitSettings(settings, host.resolveSettings);
|
||||
const shouldClear = options.clearWhenDisabled === true;
|
||||
if (!normalized.enabled && !shouldClear) {
|
||||
return {
|
||||
active: false,
|
||||
renderLimit: 0,
|
||||
applied: false,
|
||||
skipped: true,
|
||||
};
|
||||
}
|
||||
|
||||
const renderLimit =
|
||||
normalized.enabled && normalized.render_last_n > 0
|
||||
? normalized.render_last_n
|
||||
: 0;
|
||||
let applied = false;
|
||||
const powerUserSettings =
|
||||
typeof host.getPowerUser === "function" ? host.getPowerUser() : null;
|
||||
if (powerUserSettings && typeof powerUserSettings === "object") {
|
||||
powerUserSettings.chat_truncation = renderLimit;
|
||||
applied = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const jq = typeof host.jq === "function" ? host.jq : null;
|
||||
if (jq) {
|
||||
const value = String(renderLimit);
|
||||
const truncationInput = jq("#chat_truncation");
|
||||
if (
|
||||
truncationInput &&
|
||||
Number(truncationInput.length || 0) > 0 &&
|
||||
typeof truncationInput.val === "function"
|
||||
) {
|
||||
truncationInput.val(value);
|
||||
if (typeof truncationInput.trigger === "function") {
|
||||
truncationInput.trigger("change");
|
||||
}
|
||||
applied = true;
|
||||
}
|
||||
const truncationCounter = jq("#chat_truncation_counter");
|
||||
if (
|
||||
truncationCounter &&
|
||||
Number(truncationCounter.length || 0) > 0 &&
|
||||
typeof truncationCounter.val === "function"
|
||||
) {
|
||||
truncationCounter.val(value);
|
||||
applied = true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn("[ST-BME] 同步聊天区渲染楼层限制失败:", error);
|
||||
}
|
||||
|
||||
if (options.reloadCurrentChat === true) {
|
||||
try {
|
||||
if (typeof host.reloadCurrentChat === "function") {
|
||||
host.reloadCurrentChat();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn("[ST-BME] 重新加载聊天区渲染楼层失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
active: renderLimit > 0,
|
||||
renderLimit,
|
||||
applied,
|
||||
skipped: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the effective render limit used for the history-recovery guard,
|
||||
* combining configured settings with the host power_user truncation.
|
||||
*/
|
||||
export function getActiveMessageRenderLimitForHistoryGuard(
|
||||
settings = null,
|
||||
host = {},
|
||||
) {
|
||||
const normalized = getMessageRenderLimitSettings(settings, host.resolveSettings);
|
||||
const configuredLimit =
|
||||
normalized.enabled && normalized.render_last_n > 0
|
||||
? normalized.render_last_n
|
||||
: 0;
|
||||
let hostLimit = 0;
|
||||
try {
|
||||
const powerUserSettings =
|
||||
typeof host.getPowerUser === "function" ? host.getPowerUser() : null;
|
||||
hostLimit = Math.max(
|
||||
0,
|
||||
Math.trunc(Number(powerUserSettings?.chat_truncation ?? 0) || 0),
|
||||
);
|
||||
} catch {
|
||||
hostLimit = 0;
|
||||
}
|
||||
|
||||
if (configuredLimit > 0 && hostLimit > 0) {
|
||||
return Math.min(configuredLimit, hostLimit);
|
||||
}
|
||||
return Math.max(configuredLimit, hostLimit);
|
||||
}
|
||||
|
||||
/** Highest floor index tracked in processed-history state. Pure. */
|
||||
export function getHighestTrackedProcessedHistoryFloor(historyState = {}) {
|
||||
const lastProcessed = Number.isFinite(
|
||||
Number(historyState?.lastProcessedAssistantFloor),
|
||||
)
|
||||
? Math.floor(Number(historyState.lastProcessedAssistantFloor))
|
||||
: -1;
|
||||
const hashes =
|
||||
historyState?.processedMessageHashes &&
|
||||
typeof historyState.processedMessageHashes === "object" &&
|
||||
!Array.isArray(historyState.processedMessageHashes)
|
||||
? historyState.processedMessageHashes
|
||||
: {};
|
||||
const maxHashFloor = Object.keys(hashes).reduce((maxFloor, key) => {
|
||||
const floor = Number.parseInt(key, 10);
|
||||
return Number.isFinite(floor) ? Math.max(maxFloor, floor) : maxFloor;
|
||||
}, -1);
|
||||
|
||||
return Math.max(lastProcessed, maxHashFloor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decides whether history recovery must be blocked because the chat view is
|
||||
* render-limited (a truncated view must not be mistaken for deleted history).
|
||||
*
|
||||
* @param {Array} chat
|
||||
* @param {object} [opts] {settings, historyState, host}
|
||||
*/
|
||||
export function getRenderLimitedHistoryRecoveryGuard(
|
||||
chat,
|
||||
{ settings = null, historyState = {}, host = {} } = {},
|
||||
) {
|
||||
const renderLimit = getActiveMessageRenderLimitForHistoryGuard(settings, host);
|
||||
if (!Array.isArray(chat) || renderLimit <= 0) {
|
||||
return { blocked: false };
|
||||
}
|
||||
|
||||
const chatLength = chat.length;
|
||||
const highestProcessedFloor =
|
||||
getHighestTrackedProcessedHistoryFloor(historyState);
|
||||
const renderWindowTolerance = renderLimit + 1;
|
||||
if (chatLength > renderWindowTolerance || highestProcessedFloor < chatLength) {
|
||||
return { blocked: false };
|
||||
}
|
||||
|
||||
return {
|
||||
blocked: true,
|
||||
chatLength,
|
||||
highestProcessedFloor,
|
||||
renderLimit,
|
||||
reason: "render-limited-chat-slice",
|
||||
message:
|
||||
`当前聊天区最多只渲染最近 ${renderLimit} 条消息,当前可见 ${chatLength} 条;` +
|
||||
`图谱已处理到楼层 ${highestProcessedFloor}。为避免把截断视图误判为历史删除并清空运行时图谱,已暂停历史恢复。` +
|
||||
"请临时关闭“限制聊天区渲染楼层”或调大渲染数量并刷新后再提取。",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user