mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-14 02:40:45 +08:00
refactor(ui): extract recall-message-ui controller, migrate p0 recall harness off slicing
This commit is contained in:
740
ui/recall-message-ui-controller.js
Normal file
740
ui/recall-message-ui-controller.js
Normal file
@@ -0,0 +1,740 @@
|
||||
export function createRecallMessageUiController(deps = {}) {
|
||||
let persistedRecallUiRefreshTimer = null;
|
||||
let persistedRecallUiRefreshObserver = null;
|
||||
let persistedRecallUiRefreshSession = 0;
|
||||
const persistedRecallUiDiagnosticTimestamps = new Map();
|
||||
const persistedRecallPersistDiagnosticTimestamps = new Map();
|
||||
|
||||
const getContextValue = () => deps.getContext?.() || null;
|
||||
const getSettingsValue = () => deps.getSettings?.() || {};
|
||||
const getCurrentGraphValue = () => deps.getCurrentGraph?.() || null;
|
||||
const getDocument = () => deps.document || globalThis.document;
|
||||
const getMutationObserver = () => deps.MutationObserver || globalThis.MutationObserver;
|
||||
const getToastr = () => deps.toastr || {};
|
||||
const getConsole = () => deps.console || console;
|
||||
const getSetTimeout = () => deps.setTimeout || setTimeout;
|
||||
const getClearTimeout = () => deps.clearTimeout || clearTimeout;
|
||||
const getRefreshRetryDelays = () =>
|
||||
Array.isArray(deps.PERSISTED_RECALL_UI_REFRESH_RETRY_DELAYS_MS)
|
||||
? deps.PERSISTED_RECALL_UI_REFRESH_RETRY_DELAYS_MS
|
||||
: [0, 80, 180, 320, 500, 850, 1300, 2000, 3000, 4200];
|
||||
const getDiagnosticThrottleMs = () =>
|
||||
Number.isFinite(Number(deps.PERSISTED_RECALL_UI_DIAGNOSTIC_THROTTLE_MS))
|
||||
? Number(deps.PERSISTED_RECALL_UI_DIAGNOSTIC_THROTTLE_MS)
|
||||
: 1500;
|
||||
|
||||
function getMessageRecallRecord(messageIndex) {
|
||||
const chat = getContextValue()?.chat;
|
||||
return deps.readPersistedRecallFromUserMessage(chat, messageIndex);
|
||||
}
|
||||
|
||||
function debugWithThrottle(cache, key, ...args) {
|
||||
if (!globalThis.__stBmeDebugLoggingEnabled) return;
|
||||
const now = Date.now();
|
||||
const lastAt = cache.get(key) || 0;
|
||||
if (now - lastAt < getDiagnosticThrottleMs()) return;
|
||||
cache.set(key, now);
|
||||
getConsole().debug(...args);
|
||||
}
|
||||
|
||||
function debugPersistedRecallUi(reason, details = null, throttleKey = reason) {
|
||||
const suffix = details ? ` ${JSON.stringify(details)}` : "";
|
||||
debugWithThrottle(
|
||||
persistedRecallUiDiagnosticTimestamps,
|
||||
`ui:${throttleKey}`,
|
||||
`[ST-BME] Recall Card UI: ${reason}${suffix}`,
|
||||
);
|
||||
}
|
||||
|
||||
function removeMessageRecallRecord(messageIndex) {
|
||||
const chat = getContextValue()?.chat;
|
||||
if (!Array.isArray(chat)) return false;
|
||||
const removed = deps.removePersistedRecallFromUserMessage(chat, messageIndex);
|
||||
if (removed) {
|
||||
deps.triggerChatMetadataSave(getContextValue(), { immediate: false });
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
function editMessageRecallRecord(messageIndex, nextInjectionText) {
|
||||
const chat = getContextValue()?.chat;
|
||||
if (!Array.isArray(chat)) return null;
|
||||
const current = deps.readPersistedRecallFromUserMessage(chat, messageIndex);
|
||||
if (!current) return null;
|
||||
|
||||
const normalizedText = deps.normalizeRecallInputText(nextInjectionText);
|
||||
if (!normalizedText) return null;
|
||||
const nowIso = new Date().toISOString();
|
||||
const nextRecord = {
|
||||
...current,
|
||||
injectionText: normalizedText,
|
||||
tokenEstimate: deps.estimateTokens(normalizedText),
|
||||
updatedAt: nowIso,
|
||||
};
|
||||
if (!deps.writePersistedRecallToUserMessage(chat, messageIndex, nextRecord)) {
|
||||
return null;
|
||||
}
|
||||
const edited = deps.markPersistedRecallManualEdit(
|
||||
chat,
|
||||
messageIndex,
|
||||
true,
|
||||
nowIso,
|
||||
);
|
||||
if (!edited) return null;
|
||||
|
||||
deps.triggerChatMetadataSave(getContextValue(), { immediate: false });
|
||||
return edited;
|
||||
}
|
||||
|
||||
function syncEditedUserMessageDom(messageIndex, nextText) {
|
||||
const chatRoot = getDocument()?.getElementById?.("chat");
|
||||
if (!chatRoot?.querySelectorAll) return false;
|
||||
|
||||
for (const messageElement of Array.from(chatRoot.querySelectorAll(".mes") || [])) {
|
||||
if (resolveMessageIndexFromElement(messageElement) !== messageIndex) continue;
|
||||
const userTextElement = messageElement.querySelector?.(".mes_text");
|
||||
if (!userTextElement) return false;
|
||||
userTextElement.textContent = String(nextText || "");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function persistEditedUserMessage(context = getContextValue()) {
|
||||
const candidates = [
|
||||
["saveChatConditional", context?.saveChatConditional],
|
||||
["saveChat", context?.saveChat],
|
||||
];
|
||||
|
||||
for (const [label, handler] of candidates) {
|
||||
if (typeof handler !== "function") continue;
|
||||
try {
|
||||
const result = handler.call(context);
|
||||
if (result && typeof result.catch === "function") {
|
||||
result.catch((error) => {
|
||||
getConsole().error(`[ST-BME] 保存用户输入编辑失败 (${label}):`, error);
|
||||
});
|
||||
}
|
||||
return label;
|
||||
} catch (error) {
|
||||
getConsole().error(`[ST-BME] 调用 ${label} 保存用户输入编辑失败:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return deps.triggerChatMetadataSave(context, { immediate: true });
|
||||
}
|
||||
|
||||
function editMessageUserInputText(messageIndex, nextUserInputText) {
|
||||
const context = getContextValue();
|
||||
const chat = context?.chat;
|
||||
if (!Array.isArray(chat)) {
|
||||
return { ok: false, error: "missing-chat" };
|
||||
}
|
||||
|
||||
const message = chat[messageIndex];
|
||||
if (!message?.is_user) {
|
||||
return { ok: false, error: "not-user-message" };
|
||||
}
|
||||
|
||||
const normalizedText = deps.normalizeRecallInputText(nextUserInputText);
|
||||
if (!normalizedText) {
|
||||
return { ok: false, error: "empty-user-input" };
|
||||
}
|
||||
|
||||
const previousText = deps.normalizeRecallInputText(message.mes || "");
|
||||
const currentRecord = deps.readPersistedRecallFromUserMessage(chat, messageIndex);
|
||||
const recallBoundText = deps.normalizeRecallInputText(
|
||||
currentRecord?.boundUserFloorText || previousText,
|
||||
);
|
||||
const recallMayBeStale = Boolean(currentRecord) && recallBoundText !== normalizedText;
|
||||
|
||||
message.mes = normalizedText;
|
||||
const swipeIndex = Number.isFinite(Number(message?.swipe_id))
|
||||
? Math.max(0, Math.floor(Number(message.swipe_id)))
|
||||
: null;
|
||||
if (
|
||||
Array.isArray(message?.swipes) &&
|
||||
swipeIndex !== null &&
|
||||
swipeIndex < message.swipes.length
|
||||
) {
|
||||
message.swipes[swipeIndex] = normalizedText;
|
||||
}
|
||||
|
||||
if (message.extra && typeof message.extra === "object") {
|
||||
if (typeof message.extra.display_text === "string") {
|
||||
message.extra.display_text = normalizedText;
|
||||
}
|
||||
if (typeof message.extra.current_display_text === "string") {
|
||||
message.extra.current_display_text = normalizedText;
|
||||
}
|
||||
}
|
||||
|
||||
const saveMode = persistEditedUserMessage(context);
|
||||
const domSynced = syncEditedUserMessageDom(messageIndex, normalizedText);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
nextText: normalizedText,
|
||||
recallMayBeStale,
|
||||
unchanged: previousText === normalizedText,
|
||||
saveMode,
|
||||
domSynced,
|
||||
};
|
||||
}
|
||||
|
||||
function clearPersistedRecallMessageUiObserver() {
|
||||
try {
|
||||
persistedRecallUiRefreshObserver?.disconnect?.();
|
||||
} catch (error) {
|
||||
getConsole().warn("[ST-BME] Recall Card UI observer disconnect 失败:", error);
|
||||
}
|
||||
persistedRecallUiRefreshObserver = null;
|
||||
}
|
||||
|
||||
function isDomNodeAttached(node) {
|
||||
if (!node) return false;
|
||||
if (node.isConnected === true) return true;
|
||||
return typeof getDocument()?.contains === "function"
|
||||
? getDocument().contains(node)
|
||||
: true;
|
||||
}
|
||||
|
||||
function cleanupRecallCardElement(cardElement) {
|
||||
if (!cardElement) return;
|
||||
const messageElement = cardElement.closest?.(".mes") || null;
|
||||
if (messageElement) {
|
||||
restoreRecallCardUserInputDisplay(messageElement);
|
||||
}
|
||||
try {
|
||||
cardElement._bmeDestroyRenderer?.();
|
||||
} catch (error) {
|
||||
getConsole().warn("[ST-BME] Recall Card renderer 清理失败:", error);
|
||||
}
|
||||
cardElement.remove?.();
|
||||
}
|
||||
|
||||
function cleanupLegacyRecallBadges(messageElement) {
|
||||
if (!messageElement?.querySelectorAll) return;
|
||||
const oldBadges = Array.from(
|
||||
messageElement.querySelectorAll(".st-bme-recall-badge") || [],
|
||||
);
|
||||
for (const oldBadge of oldBadges) oldBadge.remove();
|
||||
}
|
||||
|
||||
function cleanupRecallArtifacts(messageElement, keepMessageIndex = null) {
|
||||
if (!messageElement?.querySelectorAll) return;
|
||||
|
||||
cleanupLegacyRecallBadges(messageElement);
|
||||
restoreRecallCardUserInputDisplay(messageElement);
|
||||
|
||||
const existingCards = Array.from(
|
||||
messageElement.querySelectorAll(".bme-recall-card") || [],
|
||||
);
|
||||
for (const card of existingCards) {
|
||||
if (
|
||||
keepMessageIndex !== null &&
|
||||
card.dataset?.messageIndex === String(keepMessageIndex)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
cleanupRecallCardElement(card);
|
||||
}
|
||||
}
|
||||
|
||||
function parseStableMessageIndex(candidate) {
|
||||
const normalized = String(candidate ?? "").trim();
|
||||
if (!normalized) return null;
|
||||
if (!/^\d+$/.test(normalized)) return null;
|
||||
const parsed = Number.parseInt(normalized, 10);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function resolveMessageIndexFromElement(messageElement) {
|
||||
if (!messageElement) return null;
|
||||
|
||||
const candidates = [
|
||||
messageElement.getAttribute?.("mesid"),
|
||||
messageElement.getAttribute?.("data-mesid"),
|
||||
messageElement.getAttribute?.("data-message-id"),
|
||||
messageElement.dataset?.mesid,
|
||||
messageElement.dataset?.messageId,
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const parsed = parseStableMessageIndex(candidate);
|
||||
if (parsed !== null) return parsed;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveRecallCardAnchor(messageElement) {
|
||||
if (!messageElement || !isDomNodeAttached(messageElement)) return null;
|
||||
const mesBlock = messageElement.querySelector?.(".mes_block");
|
||||
if (isDomNodeAttached(mesBlock)) return mesBlock;
|
||||
|
||||
const mesTextParent =
|
||||
messageElement.querySelector?.(".mes_text")?.parentElement;
|
||||
if (isDomNodeAttached(mesTextParent)) return mesTextParent;
|
||||
|
||||
return isDomNodeAttached(messageElement) ? messageElement : null;
|
||||
}
|
||||
|
||||
function getRecallMessageElementPriority(messageElement) {
|
||||
if (!messageElement || !isDomNodeAttached(messageElement)) return -1;
|
||||
|
||||
let priority = 0;
|
||||
const anchor = resolveRecallCardAnchor(messageElement);
|
||||
if (anchor === messageElement) priority += 1;
|
||||
else if (anchor) priority += 3;
|
||||
|
||||
if (messageElement.querySelector?.(".mes_text")) priority += 1;
|
||||
if (messageElement.classList?.contains("last_mes")) priority += 2;
|
||||
if (
|
||||
messageElement.getAttribute?.("is_user") === "true" ||
|
||||
messageElement.dataset?.isUser === "true" ||
|
||||
messageElement.classList?.contains("user_mes")
|
||||
) {
|
||||
priority += 1;
|
||||
}
|
||||
|
||||
return priority;
|
||||
}
|
||||
|
||||
function normalizeRecallCardUserInputDisplayMode(mode) {
|
||||
const normalized = String(mode || "").trim();
|
||||
if (
|
||||
normalized === "off" ||
|
||||
normalized === "beautify_only" ||
|
||||
normalized === "mirror"
|
||||
) {
|
||||
return normalized;
|
||||
}
|
||||
return "beautify_only";
|
||||
}
|
||||
|
||||
function applyRecallCardUserInputDisplayMode(messageElement, mode) {
|
||||
if (!messageElement?.querySelector) return;
|
||||
const userTextElement = messageElement.querySelector(".mes_text");
|
||||
if (!userTextElement) return;
|
||||
userTextElement.classList.toggle(
|
||||
"bme-hide-original-user-text",
|
||||
normalizeRecallCardUserInputDisplayMode(mode) === "beautify_only",
|
||||
);
|
||||
}
|
||||
|
||||
function restoreRecallCardUserInputDisplay(messageElement) {
|
||||
if (!messageElement?.querySelector) return;
|
||||
const userTextElement = messageElement.querySelector(".mes_text");
|
||||
userTextElement?.classList?.remove("bme-hide-original-user-text");
|
||||
}
|
||||
|
||||
function buildPersistedRecallUiRetryDelays(initialDelayMs = 0) {
|
||||
const normalizedInitial = Math.max(
|
||||
0,
|
||||
Number.parseInt(initialDelayMs, 10) || 0,
|
||||
);
|
||||
if (!normalizedInitial)
|
||||
return [...getRefreshRetryDelays()];
|
||||
return [
|
||||
normalizedInitial,
|
||||
...getRefreshRetryDelays().filter(
|
||||
(delay) => delay > normalizedInitial,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function summarizePersistedRecallRefreshStatus(summary) {
|
||||
if (summary.waitingMessageIndices.length > 0) return "waiting_dom";
|
||||
if (summary.anchorFailureIndices.length > 0) return "missing_message_anchor";
|
||||
if (summary.renderedCount > 0) return "rendered";
|
||||
if (summary.skippedNonUserIndices.length > 0) return "skipped_non_user";
|
||||
if (summary.persistedRecordCount === 0) return "missing_recall_record";
|
||||
return "missing_message_anchor";
|
||||
}
|
||||
|
||||
function refreshPersistedRecallMessageUi() {
|
||||
const context = getContextValue();
|
||||
const chat = context?.chat;
|
||||
if (!Array.isArray(chat) || typeof getDocument()?.getElementById !== "function") {
|
||||
return {
|
||||
status: "missing_chat_root",
|
||||
renderedCount: 0,
|
||||
persistedRecordCount: 0,
|
||||
waitingMessageIndices: [],
|
||||
anchorFailureIndices: [],
|
||||
skippedNonUserIndices: [],
|
||||
};
|
||||
}
|
||||
|
||||
const chatRoot = getDocument().getElementById("chat");
|
||||
if (!chatRoot) {
|
||||
debugPersistedRecallUi("缺少 #chat 根节点");
|
||||
return {
|
||||
status: "missing_chat_root",
|
||||
renderedCount: 0,
|
||||
persistedRecordCount: 0,
|
||||
waitingMessageIndices: [],
|
||||
anchorFailureIndices: [],
|
||||
skippedNonUserIndices: [],
|
||||
};
|
||||
}
|
||||
|
||||
const settings = getSettingsValue();
|
||||
const themeName = settings?.panelTheme || "crimson";
|
||||
const recallCardUserInputDisplayMode =
|
||||
normalizeRecallCardUserInputDisplayMode(
|
||||
settings?.recallCardUserInputDisplayMode,
|
||||
);
|
||||
const callbacks = getRecallCardCallbacks();
|
||||
const messageElementMap = new Map();
|
||||
const messageElements = Array.from(chatRoot.querySelectorAll(".mes"));
|
||||
for (const messageElement of messageElements) {
|
||||
cleanupLegacyRecallBadges(messageElement);
|
||||
const messageIndex = resolveMessageIndexFromElement(messageElement);
|
||||
if (!Number.isFinite(messageIndex)) {
|
||||
debugPersistedRecallUi(
|
||||
"消息 DOM 缺少稳定索引属性,跳过挂载",
|
||||
{
|
||||
className: messageElement.className || "",
|
||||
},
|
||||
"missing-stable-message-index",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (messageElementMap.has(messageIndex)) {
|
||||
const previousElement = messageElementMap.get(messageIndex) || null;
|
||||
const previousPriority = getRecallMessageElementPriority(previousElement);
|
||||
const nextPriority = getRecallMessageElementPriority(messageElement);
|
||||
const shouldReplace = nextPriority >= previousPriority;
|
||||
debugPersistedRecallUi(
|
||||
"检测到重复消息 DOM 索引,已挑选更可靠的锚点",
|
||||
{
|
||||
messageIndex,
|
||||
previousPriority,
|
||||
nextPriority,
|
||||
replaced: shouldReplace,
|
||||
},
|
||||
`duplicate-message-index:${messageIndex}`,
|
||||
);
|
||||
if (shouldReplace) {
|
||||
cleanupRecallArtifacts(previousElement);
|
||||
messageElementMap.set(messageIndex, messageElement);
|
||||
} else {
|
||||
cleanupRecallArtifacts(messageElement);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
messageElementMap.set(messageIndex, messageElement);
|
||||
}
|
||||
|
||||
const summary = {
|
||||
status: "missing_recall_record",
|
||||
renderedCount: 0,
|
||||
persistedRecordCount: 0,
|
||||
waitingMessageIndices: [],
|
||||
anchorFailureIndices: [],
|
||||
skippedNonUserIndices: [],
|
||||
};
|
||||
|
||||
for (let messageIndex = 0; messageIndex < chat.length; messageIndex++) {
|
||||
const message = chat[messageIndex];
|
||||
const messageElement = messageElementMap.get(messageIndex) || null;
|
||||
const existingCard =
|
||||
messageElement?.querySelector?.(
|
||||
`.bme-recall-card[data-message-index="${messageIndex}"]`,
|
||||
) || null;
|
||||
|
||||
if (!message?.is_user) {
|
||||
if (messageElement) {
|
||||
restoreRecallCardUserInputDisplay(messageElement);
|
||||
}
|
||||
if (existingCard) cleanupRecallCardElement(existingCard);
|
||||
const unexpectedRecord = deps.readPersistedRecallFromUserMessage(
|
||||
chat,
|
||||
messageIndex,
|
||||
);
|
||||
if (unexpectedRecord) {
|
||||
summary.skippedNonUserIndices.push(messageIndex);
|
||||
debugPersistedRecallUi(
|
||||
"非 user 楼层存在持久召回记录,已跳过挂载",
|
||||
{
|
||||
messageIndex,
|
||||
},
|
||||
`skipped-non-user:${messageIndex}`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const record = deps.readPersistedRecallFromUserMessage(chat, messageIndex);
|
||||
if (!record?.injectionText) {
|
||||
if (messageElement) {
|
||||
restoreRecallCardUserInputDisplay(messageElement);
|
||||
}
|
||||
if (existingCard) cleanupRecallCardElement(existingCard);
|
||||
continue;
|
||||
}
|
||||
|
||||
summary.persistedRecordCount += 1;
|
||||
if (!messageElement) {
|
||||
summary.waitingMessageIndices.push(messageIndex);
|
||||
debugPersistedRecallUi(
|
||||
"目标 user 楼层 DOM 未就绪,等待后续刷新",
|
||||
{
|
||||
messageIndex,
|
||||
},
|
||||
`waiting-dom:${messageIndex}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const anchor = resolveRecallCardAnchor(messageElement);
|
||||
if (!anchor) {
|
||||
restoreRecallCardUserInputDisplay(messageElement);
|
||||
cleanupRecallCardElement(existingCard);
|
||||
summary.anchorFailureIndices.push(messageIndex);
|
||||
debugPersistedRecallUi(
|
||||
"目标 user 楼层锚点解析失败,跳过挂载",
|
||||
{
|
||||
messageIndex,
|
||||
},
|
||||
`missing-anchor:${messageIndex}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
cleanupRecallArtifacts(messageElement, messageIndex);
|
||||
const currentCard =
|
||||
messageElement.querySelector?.(
|
||||
`.bme-recall-card[data-message-index="${messageIndex}"]`,
|
||||
) || null;
|
||||
|
||||
if (currentCard) {
|
||||
deps.updateRecallCardData(currentCard, record, {
|
||||
userMessageText: message.mes || "",
|
||||
userInputDisplayMode: recallCardUserInputDisplayMode,
|
||||
graph: getCurrentGraphValue(),
|
||||
themeName,
|
||||
callbacks,
|
||||
});
|
||||
} else {
|
||||
const card = deps.createRecallCardElement({
|
||||
messageIndex,
|
||||
record,
|
||||
userMessageText: message.mes || "",
|
||||
userInputDisplayMode: recallCardUserInputDisplayMode,
|
||||
graph: getCurrentGraphValue(),
|
||||
themeName,
|
||||
callbacks,
|
||||
});
|
||||
anchor.appendChild(card);
|
||||
}
|
||||
applyRecallCardUserInputDisplayMode(
|
||||
messageElement,
|
||||
recallCardUserInputDisplayMode,
|
||||
);
|
||||
summary.renderedCount += 1;
|
||||
}
|
||||
|
||||
summary.status = summarizePersistedRecallRefreshStatus(summary);
|
||||
if (summary.status === "missing_recall_record") {
|
||||
debugPersistedRecallUi("当前无有效持久召回记录可渲染");
|
||||
} else if (summary.renderedCount > 0) {
|
||||
debugPersistedRecallUi(
|
||||
"Recall Card 挂载完成",
|
||||
{
|
||||
renderedCount: summary.renderedCount,
|
||||
persistedRecordCount: summary.persistedRecordCount,
|
||||
waitingDom: summary.waitingMessageIndices.length,
|
||||
},
|
||||
`rendered:${summary.renderedCount}`,
|
||||
);
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
function getRecallCardCallbacks() {
|
||||
return {
|
||||
onEdit: (messageIndex) => {
|
||||
const record = getMessageRecallRecord(messageIndex);
|
||||
if (!record) return;
|
||||
deps.openRecallSidebar({
|
||||
mode: "edit",
|
||||
messageIndex,
|
||||
record,
|
||||
node: null,
|
||||
graph: getCurrentGraphValue(),
|
||||
callbacks: {
|
||||
onSave: (idx, newText) => {
|
||||
const edited = editMessageRecallRecord(idx, newText);
|
||||
if (edited) {
|
||||
getToastr().success("已保存手动编辑");
|
||||
} else {
|
||||
getToastr().warning("编辑失败:注入文本不能为空");
|
||||
}
|
||||
schedulePersistedRecallMessageUiRefresh();
|
||||
},
|
||||
estimateTokens: deps.estimateTokens,
|
||||
},
|
||||
});
|
||||
},
|
||||
onEditUserInput: (messageIndex, nextUserInputText) => {
|
||||
const result = editMessageUserInputText(messageIndex, nextUserInputText);
|
||||
if (!result?.ok) {
|
||||
getToastr().warning("编辑失败:内容不能为空或此楼层非用户消息");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (result.unchanged) {
|
||||
getToastr().info("用户输入未变化");
|
||||
} else {
|
||||
getToastr().success("已更新本轮用户输入");
|
||||
}
|
||||
if (result.recallMayBeStale) {
|
||||
getToastr().info("输入已改,当前召回结果可能需要重新召回");
|
||||
}
|
||||
schedulePersistedRecallMessageUiRefresh();
|
||||
return result;
|
||||
},
|
||||
onDelete: (messageIndex) => {
|
||||
if (removeMessageRecallRecord(messageIndex)) {
|
||||
getToastr().success("已删除持久召回注入");
|
||||
schedulePersistedRecallMessageUiRefresh();
|
||||
}
|
||||
},
|
||||
onRerunRecall: async (messageIndex) => {
|
||||
const result = await deps.rerunRecallForMessage(messageIndex);
|
||||
if (result?.status === "completed") {
|
||||
getToastr().success("重新召回完成");
|
||||
}
|
||||
schedulePersistedRecallMessageUiRefresh();
|
||||
},
|
||||
onNodeClick: (messageIndex, node) => {
|
||||
const record = getMessageRecallRecord(messageIndex);
|
||||
if (!record) return;
|
||||
deps.openRecallSidebar({
|
||||
mode: "view",
|
||||
messageIndex,
|
||||
record,
|
||||
node,
|
||||
graph: getCurrentGraphValue(),
|
||||
callbacks: {
|
||||
onSave: (idx, newText) => {
|
||||
const edited = editMessageRecallRecord(idx, newText);
|
||||
if (edited) getToastr().success("已保存手动编辑");
|
||||
else getToastr().warning("编辑失败:注入文本不能为空");
|
||||
schedulePersistedRecallMessageUiRefresh();
|
||||
},
|
||||
estimateTokens: deps.estimateTokens,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function armPersistedRecallMessageUiObserver(sessionId, runAttempt) {
|
||||
clearPersistedRecallMessageUiObserver();
|
||||
const chatRoot = getDocument()?.getElementById?.("chat");
|
||||
const ObserverCtor = getMutationObserver();
|
||||
if (!chatRoot || typeof ObserverCtor !== "function") return false;
|
||||
|
||||
persistedRecallUiRefreshObserver = new ObserverCtor(() => {
|
||||
if (sessionId !== persistedRecallUiRefreshSession) return;
|
||||
clearPersistedRecallMessageUiObserver();
|
||||
runAttempt();
|
||||
});
|
||||
persistedRecallUiRefreshObserver.observe(chatRoot, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: [
|
||||
"mesid",
|
||||
"data-mesid",
|
||||
"data-message-id",
|
||||
"class",
|
||||
"is_user",
|
||||
],
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function schedulePersistedRecallMessageUiRefresh(delayMs = 0) {
|
||||
getClearTimeout()(persistedRecallUiRefreshTimer);
|
||||
clearPersistedRecallMessageUiObserver();
|
||||
|
||||
const retryDelays = buildPersistedRecallUiRetryDelays(delayMs);
|
||||
const sessionId = ++persistedRecallUiRefreshSession;
|
||||
let attemptIndex = 0;
|
||||
|
||||
const runAttempt = () => {
|
||||
if (sessionId !== persistedRecallUiRefreshSession) return;
|
||||
if (persistedRecallUiRefreshTimer) {
|
||||
getClearTimeout()(persistedRecallUiRefreshTimer);
|
||||
persistedRecallUiRefreshTimer = null;
|
||||
}
|
||||
|
||||
const summary = refreshPersistedRecallMessageUi();
|
||||
|
||||
const shouldRetryForPending =
|
||||
(summary.status === "missing_chat_root" ||
|
||||
summary.status === "waiting_dom" ||
|
||||
summary.status === "missing_message_anchor") &&
|
||||
attemptIndex < retryDelays.length - 1;
|
||||
|
||||
// 勿在「已成功渲染」时长期监听 MutationObserver:chat 的 class/流式更新会疯狂触发
|
||||
// runAttempt,造成满屏刷新与日志;显式事件(USER_MESSAGE_RENDERED 等)仍会 schedule 刷新。
|
||||
const shouldWatchForRepaint = false;
|
||||
|
||||
if (!shouldRetryForPending && !shouldWatchForRepaint) {
|
||||
clearPersistedRecallMessageUiObserver();
|
||||
return;
|
||||
}
|
||||
|
||||
armPersistedRecallMessageUiObserver(sessionId, runAttempt);
|
||||
if (shouldRetryForPending) {
|
||||
attemptIndex += 1;
|
||||
persistedRecallUiRefreshTimer = getSetTimeout()(
|
||||
runAttempt,
|
||||
retryDelays[attemptIndex],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const lingerMs = retryDelays[retryDelays.length - 1] || 0;
|
||||
if (lingerMs <= 0) {
|
||||
clearPersistedRecallMessageUiObserver();
|
||||
return;
|
||||
}
|
||||
persistedRecallUiRefreshTimer = getSetTimeout()(() => {
|
||||
if (sessionId !== persistedRecallUiRefreshSession) return;
|
||||
clearPersistedRecallMessageUiObserver();
|
||||
persistedRecallUiRefreshTimer = null;
|
||||
}, lingerMs);
|
||||
};
|
||||
|
||||
persistedRecallUiRefreshTimer = getSetTimeout()(
|
||||
runAttempt,
|
||||
retryDelays[attemptIndex],
|
||||
);
|
||||
}
|
||||
|
||||
function cleanupPersistedRecallMessageUi() {
|
||||
getClearTimeout()(persistedRecallUiRefreshTimer);
|
||||
persistedRecallUiRefreshTimer = null;
|
||||
clearPersistedRecallMessageUiObserver();
|
||||
const chatRoot = getDocument().getElementById("chat");
|
||||
if (!chatRoot?.querySelectorAll) return;
|
||||
for (const messageElement of Array.from(chatRoot.querySelectorAll(".mes"))) {
|
||||
cleanupRecallArtifacts(messageElement);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
refreshPersistedRecallMessageUi,
|
||||
schedulePersistedRecallMessageUiRefresh,
|
||||
cleanupPersistedRecallMessageUi,
|
||||
resolveMessageIndexFromElement,
|
||||
resolveRecallCardAnchor,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user