mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
Fix recall card mounting for current user messages
This commit is contained in:
89
index.js
89
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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user