Fix recall card mounting for current user messages

This commit is contained in:
Youzini-afk
2026-04-05 17:45:53 +08:00
parent 50d820978a
commit 782d7fb3cd
2 changed files with 198 additions and 11 deletions

View File

@@ -560,7 +560,18 @@ const plannerRecallHandoffs = new Map();
let persistedRecallUiRefreshTimer = null; let persistedRecallUiRefreshTimer = null;
let persistedRecallUiRefreshObserver = null; let persistedRecallUiRefreshObserver = null;
let persistedRecallUiRefreshSession = 0; 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 PERSISTED_RECALL_UI_DIAGNOSTIC_THROTTLE_MS = 1500;
const persistedRecallUiDiagnosticTimestamps = new Map(); const persistedRecallUiDiagnosticTimestamps = new Map();
const persistedRecallPersistDiagnosticTimestamps = new Map(); const persistedRecallPersistDiagnosticTimestamps = new Map();
@@ -2281,6 +2292,27 @@ function resolveRecallCardAnchor(messageElement) {
return isDomNodeAttached(messageElement) ? messageElement : null; 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) { function normalizeRecallCardUserInputDisplayMode(mode) {
const normalized = String(mode || "").trim(); const normalized = String(mode || "").trim();
if ( if (
@@ -2383,14 +2415,26 @@ function refreshPersistedRecallMessageUi() {
continue; continue;
} }
if (messageElementMap.has(messageIndex)) { if (messageElementMap.has(messageIndex)) {
const previousElement = messageElementMap.get(messageIndex) || null;
const previousPriority = getRecallMessageElementPriority(previousElement);
const nextPriority = getRecallMessageElementPriority(messageElement);
const shouldReplace = nextPriority >= previousPriority;
debugPersistedRecallUi( debugPersistedRecallUi(
"检测到重复消息 DOM 索引,保留首个锚点", "检测到重复消息 DOM 索引,已挑选更可靠的锚点",
{ {
messageIndex, messageIndex,
previousPriority,
nextPriority,
replaced: shouldReplace,
}, },
`duplicate-message-index:${messageIndex}`, `duplicate-message-index:${messageIndex}`,
); );
cleanupRecallArtifacts(messageElement); if (shouldReplace) {
cleanupRecallArtifacts(previousElement);
messageElementMap.set(messageIndex, messageElement);
} else {
cleanupRecallArtifacts(messageElement);
}
continue; continue;
} }
messageElementMap.set(messageIndex, messageElement); messageElementMap.set(messageIndex, messageElement);
@@ -2598,7 +2642,13 @@ function armPersistedRecallMessageUiObserver(sessionId, runAttempt) {
childList: true, childList: true,
subtree: true, subtree: true,
attributes: true, attributes: true,
attributeFilter: ["mesid", "data-mesid", "data-message-id"], attributeFilter: [
"mesid",
"data-mesid",
"data-message-id",
"class",
"is_user",
],
}); });
return true; return true;
} }
@@ -2620,23 +2670,40 @@ function schedulePersistedRecallMessageUiRefresh(delayMs = 0) {
const summary = refreshPersistedRecallMessageUi(); const summary = refreshPersistedRecallMessageUi();
const shouldRetry = const shouldRetryForPending =
(summary.status === "missing_chat_root" || (summary.status === "missing_chat_root" ||
summary.status === "waiting_dom" || summary.status === "waiting_dom" ||
summary.status === "missing_message_anchor") && summary.status === "missing_message_anchor") &&
attemptIndex < retryDelays.length - 1; attemptIndex < retryDelays.length - 1;
if (!shouldRetry) { const shouldWatchForRepaint =
summary.status === "rendered" && summary.renderedCount > 0;
if (!shouldRetryForPending && !shouldWatchForRepaint) {
clearPersistedRecallMessageUiObserver(); clearPersistedRecallMessageUiObserver();
return; return;
} }
armPersistedRecallMessageUiObserver(sessionId, runAttempt); armPersistedRecallMessageUiObserver(sessionId, runAttempt);
attemptIndex += 1; if (shouldRetryForPending) {
persistedRecallUiRefreshTimer = setTimeout( attemptIndex += 1;
runAttempt, persistedRecallUiRefreshTimer = setTimeout(
retryDelays[attemptIndex], 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( persistedRecallUiRefreshTimer = setTimeout(

View File

@@ -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() { async function testRecallCardKeepsRetryingWhenOlderCardsAlreadyRendered() {
const chat = [ 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() { async function testRecallCardDoesNotMountOnNonUserFloor() {
const chat = [ const chat = [
{ {
@@ -5174,7 +5292,9 @@ await testRecallCardMountsOnStandardUserMessageDom();
await testRecallCardSkipsMountWithoutStableMessageIndex(); await testRecallCardSkipsMountWithoutStableMessageIndex();
await testRecallCardDelayedDomInsertionEventuallyRenders(); await testRecallCardDelayedDomInsertionEventuallyRenders();
await testRecallCardDelayedStableMessageIndexEventuallyRenders(); await testRecallCardDelayedStableMessageIndexEventuallyRenders();
await testRecallCardSurvivesLateMessageDomReplacement();
await testRecallCardKeepsRetryingWhenOlderCardsAlreadyRendered(); await testRecallCardKeepsRetryingWhenOlderCardsAlreadyRendered();
await testRecallCardPrefersBetterDuplicateMessageAnchor();
await testRecallCardDoesNotMountOnNonUserFloor(); await testRecallCardDoesNotMountOnNonUserFloor();
await testRecallCardRefreshCleansLegacyBadgeAndAvoidsDuplicates(); await testRecallCardRefreshCleansLegacyBadgeAndAvoidsDuplicates();
await testRecallCardExpandedContentRerendersAfterRecordUpdate(); await testRecallCardExpandedContentRerendersAfterRecordUpdate();