修复历史边界与流式消息去重并更新发布包
This commit is contained in:
335
public/app.js
335
public/app.js
@@ -1811,13 +1811,46 @@
|
||||
|| message.clientMessageId
|
||||
|| message.replyToRequestId
|
||||
|| message.crossConversation?.replyToRequestId
|
||||
|| message.crossConversation?.replyRequestId
|
||||
|| message.codexAppTurnKey
|
||||
|| message.nativeTurnKey;
|
||||
if (explicit) return String(explicit);
|
||||
const turnId = message.turnId || message.codexAppTurnId;
|
||||
if (turnId) return `turn:${message.role || ''}:${turnId}`;
|
||||
const content = typeof message.content === 'string' ? message.content : JSON.stringify(message.content || '');
|
||||
return `message:${stableMessageHash(`${message.role || ''}\u001f${message.timestamp || ''}\u001f${content}`)}`;
|
||||
}
|
||||
|
||||
function getMessageRenderFingerprint(message) {
|
||||
// 工具结果、附件和跨会话来源变化也必须刷新气泡,不能只比较正文。
|
||||
return stableMessageHash(JSON.stringify(message || {}));
|
||||
}
|
||||
|
||||
function mergeIndexedSessionMessages(snapshot, messages, incomingBase = 0) {
|
||||
const existingMessages = Array.isArray(snapshot.messages) ? snapshot.messages : [];
|
||||
const existingBase = Number.isFinite(Number(snapshot.historyBaseIndex))
|
||||
? Number(snapshot.historyBaseIndex)
|
||||
: Math.max(0, Number(snapshot.historyTotal || existingMessages.length) - existingMessages.length);
|
||||
const entries = new Map();
|
||||
const positions = snapshot.historyMessagePositions || {};
|
||||
const upsert = (message, index) => {
|
||||
const key = getStableMessageKey(message);
|
||||
const previous = entries.get(key);
|
||||
entries.set(key, { message, index, order: previous?.order ?? entries.size });
|
||||
};
|
||||
existingMessages.forEach((message, index) => {
|
||||
const storedIndex = positions[getStableMessageKey(message)];
|
||||
upsert(message, Number.isFinite(storedIndex) ? storedIndex : existingBase + index);
|
||||
});
|
||||
messages.forEach((message, index) => upsert(message, incomingBase + index));
|
||||
// 下标只决定位置。同下标的不同 ID 均保留,同 ID 则更新消息及位置。
|
||||
const sorted = Array.from(entries.values()).sort((a, b) => a.index - b.index || a.order - b.order);
|
||||
snapshot.messages = sorted.map((entry) => entry.message);
|
||||
snapshot.historyMessagePositions = Object.fromEntries(sorted.map((entry) => [getStableMessageKey(entry.message), entry.index]));
|
||||
snapshot.historyBaseIndex = sorted[0]?.index ?? incomingBase;
|
||||
snapshot.historyBuffered = snapshot.messages.length;
|
||||
}
|
||||
|
||||
function findRenderedSessionMessageByKey(key) {
|
||||
const normalizedKey = String(key || '').trim();
|
||||
if (!normalizedKey) return null;
|
||||
@@ -2493,10 +2526,10 @@
|
||||
messageEl.dataset.sessionMessage = 'true';
|
||||
messageEl.dataset.messageIndex = String(messageIndex);
|
||||
if (message) {
|
||||
messageEl.__sessionMessage = message;
|
||||
const key = getStableMessageKey(message);
|
||||
if (key) messageEl.dataset.messageKey = key;
|
||||
const rawContent = typeof message.content === 'string' ? message.content : JSON.stringify(message.content || '');
|
||||
messageEl.dataset.messageFingerprint = stableMessageHash(`${message.role || ''}\u001f${rawContent}`);
|
||||
messageEl.dataset.messageFingerprint = getMessageRenderFingerprint(message);
|
||||
}
|
||||
if (messageEl.classList.contains('assistant')) {
|
||||
syncAssistantBranchButton(messageEl, messageIndex);
|
||||
@@ -2673,6 +2706,7 @@
|
||||
|| payload.taskTracking?.enabled === true,
|
||||
historyTotal,
|
||||
historyBaseIndex,
|
||||
historyMessagePositions: payload.historyMessagePositions ? deepClone(payload.historyMessagePositions) : {},
|
||||
historyBuffered,
|
||||
historyCursor,
|
||||
historyTruncated,
|
||||
@@ -6755,6 +6789,7 @@
|
||||
// session_history_chunk 的 prepend 并发,造成消息视觉顺序错乱。
|
||||
immediate: !!options.immediate || !!snapshot.historyPending,
|
||||
baseIndex: snapshot.historyBaseIndex || 0,
|
||||
messagePositions: snapshot.historyMessagePositions,
|
||||
});
|
||||
scheduleAdvancedSearchJump();
|
||||
if (snapshot.isRunning && snapshot.sessionId === currentSessionId) {
|
||||
@@ -6765,6 +6800,7 @@
|
||||
// 仅保留 streaming-msg 会导致用户气泡在第一次切换时消失。
|
||||
reconcileRenderedSessionMessages(snapshot.messages || [], {
|
||||
baseIndex: snapshot.historyBaseIndex || 0,
|
||||
messagePositions: snapshot.historyMessagePositions,
|
||||
preserveScroll: true,
|
||||
});
|
||||
generatingSessionId = snapshot.sessionId;
|
||||
@@ -7047,36 +7083,8 @@
|
||||
const bufferedSnapshot = getSessionHistoryBuffer(sessionId);
|
||||
const snapshot = cachedSnapshot || bufferedSnapshot;
|
||||
if (!snapshot) return;
|
||||
const existingMessages = Array.isArray(snapshot.messages) ? snapshot.messages : [];
|
||||
const existingBase = Number.isFinite(Number(snapshot.historyBaseIndex))
|
||||
? Number(snapshot.historyBaseIndex)
|
||||
: Math.max(0, Number(snapshot.historyTotal || existingMessages.length) - existingMessages.length);
|
||||
const incomingBase = Number.isFinite(Number(options.baseIndex)) ? Number(options.baseIndex) : 0;
|
||||
const stableKey = (message) => typeof getStableMessageKey === 'function'
|
||||
? getStableMessageKey(message)
|
||||
: String(message?.id || message?.messageId || '').trim();
|
||||
const indexed = new Map();
|
||||
const keyed = new Map();
|
||||
existingMessages.forEach((message, index) => {
|
||||
const position = existingBase + index;
|
||||
indexed.set(position, message);
|
||||
const key = stableKey(message);
|
||||
if (key) keyed.set(key, position);
|
||||
});
|
||||
messages.forEach((message, index) => {
|
||||
const key = stableKey(message);
|
||||
if (key && keyed.has(key)) {
|
||||
indexed.set(keyed.get(key), message);
|
||||
return;
|
||||
}
|
||||
const position = incomingBase + index;
|
||||
indexed.set(position, message);
|
||||
if (key) keyed.set(key, position);
|
||||
});
|
||||
const indexes = Array.from(indexed.keys()).sort((a, b) => a - b);
|
||||
snapshot.messages = indexes.map((index) => indexed.get(index));
|
||||
snapshot.historyBaseIndex = indexes.length > 0 ? indexes[0] : incomingBase;
|
||||
snapshot.historyBuffered = snapshot.messages.length;
|
||||
mergeIndexedSessionMessages(snapshot, messages, incomingBase);
|
||||
if (Number.isFinite(Number(options.total))) snapshot.historyTotal = Math.max(0, Number(options.total));
|
||||
if (Number.isFinite(Number(options.cursor))) snapshot.historyCursor = Math.max(0, Number(options.cursor));
|
||||
snapshot.historyTruncated = snapshot.historyCursor > 0 || snapshot.historyBaseIndex > 0;
|
||||
@@ -7094,36 +7102,8 @@
|
||||
|
||||
function mergeHistoryChunkIntoSessionLoadSnapshot(snapshot, messages, options = {}) {
|
||||
if (!snapshot || !Array.isArray(messages) || messages.length === 0) return;
|
||||
const existingMessages = Array.isArray(snapshot.messages) ? snapshot.messages : [];
|
||||
const existingBase = Number.isFinite(Number(snapshot.historyBaseIndex))
|
||||
? Number(snapshot.historyBaseIndex)
|
||||
: Math.max(0, Number(snapshot.historyTotal || existingMessages.length) - existingMessages.length);
|
||||
const incomingBase = Number.isFinite(Number(options.baseIndex)) ? Number(options.baseIndex) : 0;
|
||||
const stableKey = (message) => typeof getStableMessageKey === 'function'
|
||||
? getStableMessageKey(message)
|
||||
: String(message?.id || message?.messageId || '').trim();
|
||||
const indexed = new Map();
|
||||
const keyed = new Map();
|
||||
existingMessages.forEach((message, index) => {
|
||||
const position = existingBase + index;
|
||||
indexed.set(position, message);
|
||||
const key = stableKey(message);
|
||||
if (key) keyed.set(key, position);
|
||||
});
|
||||
messages.forEach((message, index) => {
|
||||
const key = stableKey(message);
|
||||
if (key && keyed.has(key)) {
|
||||
indexed.set(keyed.get(key), message);
|
||||
return;
|
||||
}
|
||||
const position = incomingBase + index;
|
||||
indexed.set(position, message);
|
||||
if (key) keyed.set(key, position);
|
||||
});
|
||||
const indexes = Array.from(indexed.keys()).sort((a, b) => a - b);
|
||||
snapshot.messages = indexes.map((index) => indexed.get(index));
|
||||
snapshot.historyBaseIndex = indexes.length > 0 ? indexes[0] : incomingBase;
|
||||
snapshot.historyBuffered = snapshot.messages.length;
|
||||
mergeIndexedSessionMessages(snapshot, messages, incomingBase);
|
||||
if (Number.isFinite(Number(options.total))) {
|
||||
snapshot.historyTotal = Math.max(0, Number(options.total));
|
||||
}
|
||||
@@ -8379,10 +8359,7 @@
|
||||
? findRenderedSessionMessageByKey(getStableMessageKey(msg.message))
|
||||
: null;
|
||||
if (existingElement) {
|
||||
const rawContent = typeof msg.message.content === 'string'
|
||||
? msg.message.content
|
||||
: JSON.stringify(msg.message.content || '');
|
||||
const fingerprint = stableMessageHash(`${msg.message.role || ''}\u001f${rawContent}`);
|
||||
const fingerprint = getMessageRenderFingerprint(msg.message);
|
||||
if (existingElement.dataset.messageFingerprint !== fingerprint) {
|
||||
const messageIndex = Number(existingElement.dataset.messageIndex);
|
||||
const replacement = buildMsgElement(
|
||||
@@ -8533,7 +8510,11 @@
|
||||
}
|
||||
break;
|
||||
}
|
||||
finishGenerating(msg.sessionId, { keepRunning: msg.goalActive === true });
|
||||
finishGenerating(msg.sessionId, {
|
||||
keepRunning: msg.goalActive === true,
|
||||
assistantMessage: msg.assistantMessage,
|
||||
assistantMessageIndex: msg.assistantMessageIndex,
|
||||
});
|
||||
break;
|
||||
|
||||
case 'system_message':
|
||||
@@ -8625,6 +8606,7 @@
|
||||
const toolsDiv = document.querySelector('#streaming-msg .msg-tools');
|
||||
if (toolsDiv) toolsDiv.innerHTML = '';
|
||||
}
|
||||
bindStreamingAssistantMessage(msg.assistantMessageId, msg.turnId);
|
||||
pendingText = msg.text || '';
|
||||
flushRender();
|
||||
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
||||
@@ -8650,19 +8632,22 @@
|
||||
if (!isCurrentSessionEvent(msg)) break;
|
||||
clearPendingSessionResumeRequest(msg.sessionId || currentSessionId, msg.requestId);
|
||||
if (msg.sessionId === currentSessionId && Array.isArray(msg.messages) && msg.messages.length > 0) {
|
||||
const renderedMessages = messagesDiv.querySelectorAll('[data-session-message="true"]').length;
|
||||
const historyTotal = Number.isFinite(Number(msg.historyTotal))
|
||||
? Math.max(0, Number(msg.historyTotal))
|
||||
: msg.messages.length;
|
||||
if (renderedMessages < historyTotal || msg.messages.some((message) => !findRenderedSessionMessageByKey(getStableMessageKey(message)))) {
|
||||
reconcileRenderedSessionMessages(msg.messages, {
|
||||
preserveScroll: false,
|
||||
baseIndex: Number.isFinite(Number(msg.historyBaseIndex))
|
||||
? Number(msg.historyBaseIndex)
|
||||
: Math.max(0, historyTotal - msg.messages.length),
|
||||
});
|
||||
loadedHistorySessionId = msg.sessionId;
|
||||
}
|
||||
const historyBaseIndex = Number.isFinite(Number(msg.historyBaseIndex))
|
||||
? Number(msg.historyBaseIndex)
|
||||
: Math.max(0, historyTotal - msg.messages.length);
|
||||
mergeHistoryChunkIntoCachedSnapshot(msg.sessionId, msg.messages, {
|
||||
baseIndex: historyBaseIndex,
|
||||
total: historyTotal,
|
||||
});
|
||||
// 重连时同 ID 的正文、工具及来源仍可能更新,数量相同也需要对账。
|
||||
reconcileRenderedSessionMessages(msg.messages, {
|
||||
preserveScroll: false,
|
||||
baseIndex: historyBaseIndex,
|
||||
});
|
||||
loadedHistorySessionId = msg.sessionId;
|
||||
}
|
||||
setCurrentSessionRunningState(!!msg.isRunning);
|
||||
if (!msg.isRunning && currentSessionId && msg.sessionId === currentSessionId) {
|
||||
@@ -8896,6 +8881,44 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function bindStreamingAssistantMessage(messageId, turnId) {
|
||||
const stream = document.getElementById('streaming-msg');
|
||||
if (!stream) return;
|
||||
const key = messageId || (turnId ? getStableMessageKey({ role: 'assistant', turnId }) : '');
|
||||
if (!key) return;
|
||||
stream.dataset.messageId = String(key);
|
||||
stream.dataset.messageKey = String(key);
|
||||
if (turnId) stream.dataset.turnId = String(turnId);
|
||||
const existing = findRenderedSessionMessageByKey(key);
|
||||
if (existing && existing !== stream) {
|
||||
// 快照与运行态属于同一条助手消息,保留正在接收工具和正文的节点。
|
||||
stream.__sessionMessage = existing.__sessionMessage;
|
||||
stream.dataset.messageIndex = existing.dataset.messageIndex;
|
||||
existing.remove();
|
||||
updateUserOutlinePanel();
|
||||
}
|
||||
}
|
||||
|
||||
function commitCompletedAssistantMessage(message, requestedIndex) {
|
||||
if (!message || message.role !== 'assistant') return false;
|
||||
const stream = document.getElementById('streaming-msg');
|
||||
const existing = findRenderedSessionMessageByKey(getStableMessageKey(message));
|
||||
const knownIndex = existing?.dataset.messageIndex ?? stream?.dataset.messageIndex;
|
||||
const messageIndex = Number.isFinite(requestedIndex)
|
||||
? requestedIndex
|
||||
: knownIndex !== undefined && Number.isFinite(Number(knownIndex))
|
||||
? Number(knownIndex)
|
||||
: currentSessionMessageCount;
|
||||
stream?.remove();
|
||||
reconcileRenderedSessionMessages([message], { baseIndex: messageIndex, preserveScroll: false });
|
||||
currentSessionMessageCount = Math.max(currentSessionMessageCount, messageIndex + 1);
|
||||
mergeHistoryChunkIntoCachedSnapshot(currentSessionId, [message], {
|
||||
baseIndex: messageIndex,
|
||||
total: currentSessionMessageCount,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function finishGenerating(sessionId, options = {}) {
|
||||
if (sessionId && currentSessionId && sessionId !== currentSessionId) return;
|
||||
const keepRunning = options.keepRunning === true;
|
||||
@@ -8919,7 +8942,11 @@
|
||||
if (typing) typing.remove();
|
||||
|
||||
const streamEl = document.getElementById('streaming-msg');
|
||||
if (streamEl) {
|
||||
const committedAssistant = commitCompletedAssistantMessage(
|
||||
options.assistantMessage || streamEl?.__sessionMessage,
|
||||
options.assistantMessageIndex,
|
||||
);
|
||||
if (streamEl && !committedAssistant) {
|
||||
// 若本轮出现过父目录,把末尾散落的 .tool-call 也一并收入同一父节点
|
||||
if (hasGrouped) {
|
||||
const toolsDiv = streamEl.querySelector('.msg-tools');
|
||||
@@ -9290,7 +9317,10 @@
|
||||
|| (hasGiteaPrompt ? { source: 'gitea', label: 'Gitea', repository: '', resourceKind: 'issue', number: null, title: '' } : null);
|
||||
const isGiteaMessage = role === 'user' && !!giteaSource;
|
||||
const visibleContent = isGiteaMessage ? getGiteaVisibleMessageContent(content) : content;
|
||||
const resolvedMessageId = meta?.messageId || meta?.id || getStableMessageKey(meta) || createLocalId('message');
|
||||
const stableKey = Object.keys(meta).length > 0
|
||||
? getStableMessageKey({ ...meta, role, content, attachments })
|
||||
: '';
|
||||
const resolvedMessageId = meta?.messageId || meta?.id || stableKey || createLocalId('message');
|
||||
div.className = `msg ${role}${role === 'assistant' ? ' agent-' + currentAgent : ''}${isCrossConversation ? ' cross-conversation' : ''}${isCrossConversationReply ? ' cross-conversation-reply' : ''}${isGoalMessage ? ' goal-message' : ''}${isGiteaMessage ? ' gitea-message' : ''}`;
|
||||
if (role === 'user') {
|
||||
div.id = `hapi-message-${resolvedMessageId}`;
|
||||
@@ -9299,9 +9329,8 @@
|
||||
}
|
||||
if (role !== 'system') {
|
||||
div.dataset.messageId = resolvedMessageId;
|
||||
div.dataset.messageKey = getStableMessageKey(meta) || resolvedMessageId;
|
||||
const rawContent = typeof content === 'string' ? content : JSON.stringify(content || '');
|
||||
div.dataset.messageFingerprint = stableMessageHash(`${role}\u001f${rawContent}`);
|
||||
div.dataset.messageKey = stableKey || resolvedMessageId;
|
||||
div.dataset.messageFingerprint = getMessageRenderFingerprint({ ...meta, role, content, attachments });
|
||||
}
|
||||
|
||||
if (role === 'system') {
|
||||
@@ -10916,6 +10945,10 @@
|
||||
renderEpoch++;
|
||||
const epoch = renderEpoch;
|
||||
const baseIndex = Number.isFinite(Number(options.baseIndex)) ? Number(options.baseIndex) : 0;
|
||||
const messageIndexAt = (index) => {
|
||||
const storedIndex = options.messagePositions?.[getStableMessageKey(messages[index])];
|
||||
return Number.isFinite(storedIndex) ? storedIndex : baseIndex + index;
|
||||
};
|
||||
closedCollabAgentIds = collectClosedCollabAgentIds(messages);
|
||||
collabAgentStateCache = new Map();
|
||||
collabAgentIdsByToolUseId = new Map();
|
||||
@@ -10932,7 +10965,7 @@
|
||||
}
|
||||
if (options.immediate) {
|
||||
const frag = document.createDocumentFragment();
|
||||
messages.forEach((message, index) => frag.appendChild(buildMsgElement(message, baseIndex + index)));
|
||||
messages.forEach((message, index) => frag.appendChild(buildMsgElement(message, messageIndexAt(index))));
|
||||
messagesDiv.appendChild(frag);
|
||||
updateUserOutlinePanel();
|
||||
renderPendingNotes({ scroll: false });
|
||||
@@ -10955,7 +10988,7 @@
|
||||
|
||||
// Render first batch immediately
|
||||
const frag0 = document.createDocumentFragment();
|
||||
for (let i = batches[0][0]; i < batches[0][1]; i++) frag0.appendChild(buildMsgElement(messages[i], baseIndex + i));
|
||||
for (let i = batches[0][0]; i < batches[0][1]; i++) frag0.appendChild(buildMsgElement(messages[i], messageIndexAt(i)));
|
||||
messagesDiv.appendChild(frag0);
|
||||
updateUserOutlinePanel();
|
||||
renderPendingNotes({ scroll: false });
|
||||
@@ -10969,57 +11002,22 @@
|
||||
delay += 16;
|
||||
setTimeout(() => {
|
||||
if (renderEpoch !== epoch) return; // session switched, abort stale render
|
||||
const prevHeight = messagesDiv.scrollHeight;
|
||||
const prevScrollTop = messagesDiv.scrollTop;
|
||||
const frag = document.createDocumentFragment();
|
||||
for (let i = start; i < end; i++) frag.appendChild(buildMsgElement(messages[i], baseIndex + i));
|
||||
messagesDiv.insertBefore(frag, historyLoadMore?.nextSibling || null);
|
||||
updateUserOutlinePanel();
|
||||
// Compensate scrollTop so visible area stays unchanged
|
||||
messagesDiv.scrollTop = prevScrollTop + (messagesDiv.scrollHeight - prevHeight);
|
||||
updateScrollbar();
|
||||
const pending = [];
|
||||
const messagePositions = {};
|
||||
for (let i = start; i < end; i++) {
|
||||
const key = getStableMessageKey(messages[i]);
|
||||
// 延迟批次只补缺失气泡,不能覆盖重连期间已收到的更新。
|
||||
if (findRenderedSessionMessageByKey(key)) continue;
|
||||
pending.push(messages[i]);
|
||||
messagePositions[key] = messageIndexAt(i);
|
||||
}
|
||||
prependHistoryMessages(pending, { messagePositions, preserveScroll: true });
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
function prependHistoryMessages(messages, options = {}) {
|
||||
if (!Array.isArray(messages) || messages.length === 0) return;
|
||||
const baseIndex = Number.isFinite(Number(options.baseIndex)) ? Number(options.baseIndex) : 0;
|
||||
const existingIndexes = new Set(Array.from(
|
||||
messagesDiv.querySelectorAll('[data-session-message="true"][data-message-index]'),
|
||||
).map((element) => Number(element.dataset.messageIndex)));
|
||||
const existingKeys = new Set(Array.from(
|
||||
messagesDiv.querySelectorAll('[data-session-message="true"][data-message-key]'),
|
||||
).map((element) => String(element.dataset.messageKey || '').trim()).filter(Boolean));
|
||||
const uniqueMessages = [];
|
||||
messages.forEach((message, index) => {
|
||||
const messageIndex = baseIndex + index;
|
||||
const key = getStableMessageKey(message);
|
||||
if (existingIndexes.has(messageIndex) || (key && existingKeys.has(key))) return;
|
||||
existingIndexes.add(messageIndex);
|
||||
if (key) existingKeys.add(key);
|
||||
uniqueMessages.push({ message, index });
|
||||
});
|
||||
if (uniqueMessages.length === 0) return;
|
||||
collectClosedCollabAgentIds(uniqueMessages.map((entry) => entry.message)).forEach((id) => closedCollabAgentIds.add(id));
|
||||
const preserveScroll = options.preserveScroll !== false;
|
||||
const skipScrollbar = options.skipScrollbar === true;
|
||||
const welcome = messagesDiv.querySelector('.welcome-msg');
|
||||
if (welcome) welcome.remove();
|
||||
const frag = document.createDocumentFragment();
|
||||
uniqueMessages.forEach(({ message, index }) => frag.appendChild(buildMsgElement(message, baseIndex + index)));
|
||||
if (!preserveScroll) {
|
||||
messagesDiv.insertBefore(frag, historyLoadMore?.nextSibling || null);
|
||||
updateUserOutlinePanel();
|
||||
if (!skipScrollbar) updateScrollbar();
|
||||
return;
|
||||
}
|
||||
const prevHeight = messagesDiv.scrollHeight;
|
||||
const prevScrollTop = messagesDiv.scrollTop;
|
||||
messagesDiv.insertBefore(frag, historyLoadMore?.nextSibling || null);
|
||||
updateUserOutlinePanel();
|
||||
messagesDiv.scrollTop = prevScrollTop + (messagesDiv.scrollHeight - prevHeight);
|
||||
if (!skipScrollbar) updateScrollbar();
|
||||
reconcileRenderedSessionMessages(messages, options);
|
||||
}
|
||||
|
||||
function normalizeAskUserInput(input) {
|
||||
@@ -11709,49 +11707,72 @@
|
||||
function reconcileRenderedSessionMessages(messages, options = {}) {
|
||||
if (!Array.isArray(messages) || messages.length === 0) return;
|
||||
const baseIndex = Number.isFinite(Number(options.baseIndex)) ? Number(options.baseIndex) : 0;
|
||||
const existing = new Map();
|
||||
const existingByKey = new Map();
|
||||
messagesDiv.querySelectorAll('[data-session-message="true"][data-message-index]').forEach((element) => {
|
||||
const index = Number(element.dataset.messageIndex);
|
||||
if (Number.isFinite(index)) existing.set(index, element);
|
||||
const key = String(element.dataset.messageKey || '').trim();
|
||||
if (key) existingByKey.set(key, element);
|
||||
});
|
||||
const beforeHeight = messagesDiv.scrollHeight;
|
||||
const beforeScrollTop = messagesDiv.scrollTop;
|
||||
const stream = document.getElementById('streaming-msg');
|
||||
let inserted = 0;
|
||||
const records = [];
|
||||
const existingByKey = new Map();
|
||||
let changed = false;
|
||||
messagesDiv.querySelectorAll('[data-session-message="true"]').forEach((element) => {
|
||||
if (element === stream) return;
|
||||
const key = String(element.dataset.messageKey || element.dataset.messageId || '').trim();
|
||||
if (key && key === stream?.dataset.messageKey) {
|
||||
stream.__sessionMessage = element.__sessionMessage;
|
||||
stream.dataset.messageIndex = element.dataset.messageIndex;
|
||||
element.remove();
|
||||
changed = true;
|
||||
return;
|
||||
}
|
||||
if (key && existingByKey.has(key)) {
|
||||
element.remove();
|
||||
changed = true;
|
||||
return;
|
||||
}
|
||||
const record = { element, index: Number(element.dataset.messageIndex), order: records.length };
|
||||
records.push(record);
|
||||
if (key) existingByKey.set(key, record);
|
||||
});
|
||||
const anchor = stream || records[records.length - 1]?.element.nextSibling || null;
|
||||
collectClosedCollabAgentIds(messages).forEach((id) => closedCollabAgentIds.add(id));
|
||||
messages.forEach((message, offset) => {
|
||||
const messageIndex = baseIndex + offset;
|
||||
const messageKey = getStableMessageKey(message);
|
||||
const keyed = existingByKey.get(messageKey);
|
||||
if (keyed) {
|
||||
const rawContent = typeof message.content === 'string' ? message.content : JSON.stringify(message.content || '');
|
||||
const fingerprint = stableMessageHash(`${message.role || ''}\u001f${rawContent}`);
|
||||
if (keyed.dataset.messageFingerprint !== fingerprint) {
|
||||
const storedIndex = options.messagePositions?.[messageKey];
|
||||
const messageIndex = Number.isFinite(storedIndex) ? storedIndex : baseIndex + offset;
|
||||
if (messageKey && messageKey === stream?.dataset.messageKey) {
|
||||
stream.__sessionMessage = message;
|
||||
stream.dataset.messageIndex = String(messageIndex);
|
||||
return;
|
||||
}
|
||||
let record = existingByKey.get(messageKey);
|
||||
if (record) {
|
||||
if (record.element.dataset.messageFingerprint !== getMessageRenderFingerprint(message)) {
|
||||
const replacement = buildMsgElement(message, messageIndex);
|
||||
keyed.replaceWith(replacement);
|
||||
existingByKey.set(messageKey, replacement);
|
||||
existing.set(messageIndex, replacement);
|
||||
record.element.replaceWith(replacement);
|
||||
record.element = replacement;
|
||||
changed = true;
|
||||
}
|
||||
if (record.index !== messageIndex) {
|
||||
markSessionMessageElement(record.element, messageIndex, message);
|
||||
record.index = messageIndex;
|
||||
changed = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (existing.has(messageIndex)) return;
|
||||
const element = buildMsgElement(message, messageIndex);
|
||||
const next = Array.from(existing.entries())
|
||||
.filter(([index]) => index > messageIndex)
|
||||
.sort(([left], [right]) => left - right)[0]?.[1];
|
||||
messagesDiv.insertBefore(element, next || stream || null);
|
||||
existing.set(messageIndex, element);
|
||||
if (messageKey) existingByKey.set(messageKey, element);
|
||||
inserted += 1;
|
||||
record = { element: buildMsgElement(message, messageIndex), index: messageIndex, order: records.length };
|
||||
records.push(record);
|
||||
if (messageKey) existingByKey.set(messageKey, record);
|
||||
changed = true;
|
||||
});
|
||||
if (inserted === 0) return;
|
||||
if (!changed) return;
|
||||
messagesDiv.querySelector('.welcome-msg')?.remove();
|
||||
// 重新排序仅移动历史节点,保留正在生成的 DOM 与流式工具状态。
|
||||
records.sort((a, b) => a.index - b.index || a.order - b.order);
|
||||
records.forEach((record) => messagesDiv.insertBefore(record.element, anchor));
|
||||
updateUserOutlinePanel();
|
||||
if (options.preserveScroll !== false) {
|
||||
messagesDiv.scrollTop = beforeScrollTop + (messagesDiv.scrollHeight - beforeHeight);
|
||||
}
|
||||
updateScrollbar();
|
||||
if (!options.skipScrollbar) updateScrollbar();
|
||||
}
|
||||
|
||||
// --- Custom Scrollbar ---
|
||||
|
||||
Reference in New Issue
Block a user