From 782d7fb3cdaffc10665fbc038b4661c8538a7916 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sun, 5 Apr 2026 17:45:53 +0800 Subject: [PATCH] Fix recall card mounting for current user messages --- index.js | 89 +++++++++++++++++++++++++---- tests/p0-regressions.mjs | 120 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+), 11 deletions(-) diff --git a/index.js b/index.js index 9c01258..713c701 100644 --- a/index.js +++ b/index.js @@ -560,7 +560,18 @@ const plannerRecallHandoffs = new Map(); let persistedRecallUiRefreshTimer = null; let persistedRecallUiRefreshObserver = null; let persistedRecallUiRefreshSession = 0; -const PERSISTED_RECALL_UI_REFRESH_RETRY_DELAYS_MS = [0, 80, 180, 320, 500]; +const PERSISTED_RECALL_UI_REFRESH_RETRY_DELAYS_MS = [ + 0, + 80, + 180, + 320, + 500, + 850, + 1300, + 2000, + 3000, + 4200, +]; const PERSISTED_RECALL_UI_DIAGNOSTIC_THROTTLE_MS = 1500; const persistedRecallUiDiagnosticTimestamps = new Map(); const persistedRecallPersistDiagnosticTimestamps = new Map(); @@ -2281,6 +2292,27 @@ function resolveRecallCardAnchor(messageElement) { 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 ( @@ -2383,14 +2415,26 @@ function refreshPersistedRecallMessageUi() { 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 索引,保留首个锚点", + "检测到重复消息 DOM 索引,已挑选更可靠的锚点", { messageIndex, + previousPriority, + nextPriority, + replaced: shouldReplace, }, `duplicate-message-index:${messageIndex}`, ); - cleanupRecallArtifacts(messageElement); + if (shouldReplace) { + cleanupRecallArtifacts(previousElement); + messageElementMap.set(messageIndex, messageElement); + } else { + cleanupRecallArtifacts(messageElement); + } continue; } messageElementMap.set(messageIndex, messageElement); @@ -2598,7 +2642,13 @@ function armPersistedRecallMessageUiObserver(sessionId, runAttempt) { childList: true, subtree: true, attributes: true, - attributeFilter: ["mesid", "data-mesid", "data-message-id"], + attributeFilter: [ + "mesid", + "data-mesid", + "data-message-id", + "class", + "is_user", + ], }); return true; } @@ -2620,23 +2670,40 @@ function schedulePersistedRecallMessageUiRefresh(delayMs = 0) { const summary = refreshPersistedRecallMessageUi(); - const shouldRetry = + const shouldRetryForPending = (summary.status === "missing_chat_root" || summary.status === "waiting_dom" || summary.status === "missing_message_anchor") && attemptIndex < retryDelays.length - 1; - if (!shouldRetry) { + const shouldWatchForRepaint = + summary.status === "rendered" && summary.renderedCount > 0; + + if (!shouldRetryForPending && !shouldWatchForRepaint) { clearPersistedRecallMessageUiObserver(); return; } armPersistedRecallMessageUiObserver(sessionId, runAttempt); - attemptIndex += 1; - persistedRecallUiRefreshTimer = setTimeout( - runAttempt, - retryDelays[attemptIndex], - ); + if (shouldRetryForPending) { + attemptIndex += 1; + persistedRecallUiRefreshTimer = setTimeout( + runAttempt, + retryDelays[attemptIndex], + ); + return; + } + + const lingerMs = retryDelays[retryDelays.length - 1] || 0; + if (lingerMs <= 0) { + clearPersistedRecallMessageUiObserver(); + return; + } + persistedRecallUiRefreshTimer = setTimeout(() => { + if (sessionId !== persistedRecallUiRefreshSession) return; + clearPersistedRecallMessageUiObserver(); + persistedRecallUiRefreshTimer = null; + }, lingerMs); }; persistedRecallUiRefreshTimer = setTimeout( diff --git a/tests/p0-regressions.mjs b/tests/p0-regressions.mjs index d279469..9108dc2 100644 --- a/tests/p0-regressions.mjs +++ b/tests/p0-regressions.mjs @@ -1551,6 +1551,73 @@ async function testRecallCardDelayedStableMessageIndexEventuallyRenders() { } } +async function testRecallCardSurvivesLateMessageDomReplacement() { + const chat = [ + { + is_user: true, + mes: "user-0", + extra: { + bme_recall: buildPersistedRecallRecord({ + injectionText: "recall-0", + selectedNodeIds: ["n1"], + nowIso: "2026-01-01T00:00:00.000Z", + }), + }, + }, + ]; + const harness = await createRecallUiHarness({ chat }); + harness.context.PERSISTED_RECALL_UI_REFRESH_RETRY_DELAYS_MS = [ + 0, + 20, + 40, + 120, + 260, + ]; + const originalElement = createMessageElement(harness.document, 0, { + stableId: true, + withMesBlock: true, + isUser: true, + }); + harness.chatRoot.appendChild(originalElement); + + try { + harness.api.schedulePersistedRecallMessageUiRefresh(); + await waitForTick(); + await waitForTick(); + assert.equal( + harness.chatRoot.querySelectorAll(".bme-recall-card").length, + 1, + ); + + originalElement.remove(); + await new Promise((resolve) => setTimeout(resolve, 180)); + + const replacementElement = createMessageElement(harness.document, 0, { + stableId: true, + withMesBlock: true, + isUser: true, + }); + harness.chatRoot.appendChild(replacementElement); + + await waitForTick(); + await new Promise((resolve) => setTimeout(resolve, 120)); + await waitForTick(); + + assert.equal( + harness.chatRoot.querySelectorAll(".bme-recall-card").length, + 1, + "延迟重渲染后的当前 user 楼层应自动补挂 Recall Card", + ); + assert.equal( + replacementElement.querySelectorAll(".bme-recall-card").length, + 1, + "卡片应重新挂到替换后的消息 DOM 上", + ); + } finally { + harness.restoreGlobals(); + } +} + async function testRecallCardKeepsRetryingWhenOlderCardsAlreadyRendered() { const chat = [ { @@ -1613,6 +1680,57 @@ async function testRecallCardKeepsRetryingWhenOlderCardsAlreadyRendered() { } } +async function testRecallCardPrefersBetterDuplicateMessageAnchor() { + const chat = [ + { + is_user: true, + mes: "user-0", + extra: { + bme_recall: buildPersistedRecallRecord({ + injectionText: "recall-0", + selectedNodeIds: ["n1"], + nowIso: "2026-01-01T00:00:00.000Z", + }), + }, + }, + ]; + const harness = await createRecallUiHarness({ chat }); + const staleElement = createMessageElement(harness.document, 0, { + stableId: true, + withMesBlock: false, + isUser: true, + }); + const liveElement = createMessageElement(harness.document, 0, { + stableId: true, + withMesBlock: true, + isUser: true, + }); + liveElement.classList.add("last_mes"); + harness.chatRoot.appendChild(staleElement); + harness.chatRoot.appendChild(liveElement); + + try { + const summary = harness.api.refreshPersistedRecallMessageUi(); + assert.equal(summary.status, "rendered"); + assert.equal( + staleElement.querySelectorAll(".bme-recall-card").length, + 0, + "低质量的重复 DOM 不应抢走当前楼层卡片", + ); + assert.equal( + liveElement.querySelectorAll(".bme-recall-card").length, + 1, + "应优先挂到结构更完整的那条消息 DOM 上", + ); + assert.equal( + harness.chatRoot.querySelectorAll(".mes_block .bme-recall-card").length, + 1, + ); + } finally { + harness.restoreGlobals(); + } +} + async function testRecallCardDoesNotMountOnNonUserFloor() { const chat = [ { @@ -5174,7 +5292,9 @@ await testRecallCardMountsOnStandardUserMessageDom(); await testRecallCardSkipsMountWithoutStableMessageIndex(); await testRecallCardDelayedDomInsertionEventuallyRenders(); await testRecallCardDelayedStableMessageIndexEventuallyRenders(); +await testRecallCardSurvivesLateMessageDomReplacement(); await testRecallCardKeepsRetryingWhenOlderCardsAlreadyRendered(); +await testRecallCardPrefersBetterDuplicateMessageAnchor(); await testRecallCardDoesNotMountOnNonUserFloor(); await testRecallCardRefreshCleansLegacyBadgeAndAvoidsDuplicates(); await testRecallCardExpandedContentRerendersAfterRecordUpdate();