修复 Codex rollout 历史合并并重新打包
This commit is contained in:
173
public/app.js
173
public/app.js
@@ -1795,6 +1795,52 @@
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
|
||||
function stableMessageHash(value) {
|
||||
let hash = 2166136261;
|
||||
for (const char of String(value || '')) {
|
||||
hash ^= char.charCodeAt(0);
|
||||
hash = Math.imul(hash, 16777619);
|
||||
}
|
||||
return (hash >>> 0).toString(16);
|
||||
}
|
||||
|
||||
function getStableMessageKey(message) {
|
||||
if (!message || typeof message !== 'object') return '';
|
||||
const explicit = message.id
|
||||
|| message.messageId
|
||||
|| message.clientMessageId
|
||||
|| message.replyToRequestId
|
||||
|| message.crossConversation?.replyToRequestId
|
||||
|| message.codexAppTurnKey
|
||||
|| message.nativeTurnKey;
|
||||
if (explicit) return String(explicit);
|
||||
const content = typeof message.content === 'string' ? message.content : JSON.stringify(message.content || '');
|
||||
return `message:${stableMessageHash(`${message.role || ''}\u001f${message.timestamp || ''}\u001f${content}`)}`;
|
||||
}
|
||||
|
||||
function findRenderedSessionMessageByKey(key) {
|
||||
const normalizedKey = String(key || '').trim();
|
||||
if (!normalizedKey) return null;
|
||||
return Array.from(messagesDiv.querySelectorAll('[data-session-message="true"][data-message-key]'))
|
||||
.find((element) => element.dataset.messageKey === normalizedKey) || null;
|
||||
}
|
||||
|
||||
function dedupeSessionMessages(messages) {
|
||||
if (!Array.isArray(messages)) return [];
|
||||
const result = [];
|
||||
const positions = new Map();
|
||||
messages.forEach((message) => {
|
||||
const key = getStableMessageKey(message);
|
||||
if (key && positions.has(key)) {
|
||||
result[positions.get(key)] = message;
|
||||
} else {
|
||||
if (key) positions.set(key, result.length);
|
||||
result.push(message);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function rememberPendingSlashDraft(requestId, text, sessionId, agent) {
|
||||
const id = String(requestId || '').trim();
|
||||
if (!id) return;
|
||||
@@ -2442,10 +2488,16 @@
|
||||
getMessageActionRow(bubble)?.appendChild(button);
|
||||
}
|
||||
|
||||
function markSessionMessageElement(messageEl, messageIndex) {
|
||||
function markSessionMessageElement(messageEl, messageIndex, message = null) {
|
||||
if (!messageEl || !Number.isFinite(messageIndex) || messageIndex < 0) return;
|
||||
messageEl.dataset.sessionMessage = 'true';
|
||||
messageEl.dataset.messageIndex = String(messageIndex);
|
||||
if (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}`);
|
||||
}
|
||||
if (messageEl.classList.contains('assistant')) {
|
||||
syncAssistantBranchButton(messageEl, messageIndex);
|
||||
}
|
||||
@@ -7000,9 +7052,27 @@
|
||||
? 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();
|
||||
existingMessages.forEach((message, index) => indexed.set(existingBase + index, message));
|
||||
messages.forEach((message, index) => indexed.set(incomingBase + index, message));
|
||||
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;
|
||||
@@ -7029,9 +7099,27 @@
|
||||
? 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();
|
||||
existingMessages.forEach((message, index) => indexed.set(existingBase + index, message));
|
||||
messages.forEach((message, index) => indexed.set(incomingBase + index, message));
|
||||
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;
|
||||
@@ -8256,9 +8344,14 @@
|
||||
if (msg.sessionId && msg.message) {
|
||||
const isCrossConversationReply = !!msg.message.crossConversation?.replyToRequestId;
|
||||
const isProvisionalCodexAppSteer = !!msg.message.codexAppSteerStatus;
|
||||
const messageKey = getStableMessageKey(msg.message);
|
||||
updateCachedSession(msg.sessionId, (snapshot) => {
|
||||
snapshot.messages = Array.isArray(snapshot.messages) ? snapshot.messages : [];
|
||||
if (!isProvisionalCodexAppSteer) snapshot.messages.push(deepClone(msg.message));
|
||||
if (!isProvisionalCodexAppSteer) {
|
||||
const existingIndex = snapshot.messages.findIndex((item) => getStableMessageKey(item) === messageKey);
|
||||
if (existingIndex >= 0) snapshot.messages[existingIndex] = deepClone(msg.message);
|
||||
else snapshot.messages.push(deepClone(msg.message));
|
||||
}
|
||||
snapshot.updated = msg.message.timestamp || new Date().toISOString();
|
||||
if (isCrossConversationReply) {
|
||||
snapshot.readyReplyCount = Math.max(0, Number(snapshot.readyReplyCount || 0) - 1);
|
||||
@@ -8282,6 +8375,25 @@
|
||||
}
|
||||
if (msg.sessionId === currentSessionId && msg.message) {
|
||||
const isProvisionalCodexAppSteer = !!msg.message.codexAppSteerStatus;
|
||||
const existingElement = !isProvisionalCodexAppSteer
|
||||
? 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}`);
|
||||
if (existingElement.dataset.messageFingerprint !== fingerprint) {
|
||||
const messageIndex = Number(existingElement.dataset.messageIndex);
|
||||
const replacement = buildMsgElement(
|
||||
msg.message,
|
||||
Number.isFinite(messageIndex) ? messageIndex : null,
|
||||
);
|
||||
existingElement.replaceWith(replacement);
|
||||
}
|
||||
renderSessionList();
|
||||
break;
|
||||
}
|
||||
const messageIndex = isProvisionalCodexAppSteer ? null : currentSessionMessageCount;
|
||||
if (!isProvisionalCodexAppSteer) currentSessionMessageCount += 1;
|
||||
collectClosedCollabAgentIds([msg.message]).forEach((id) => closedCollabAgentIds.add(id));
|
||||
@@ -8542,7 +8654,7 @@
|
||||
const historyTotal = Number.isFinite(Number(msg.historyTotal))
|
||||
? Math.max(0, Number(msg.historyTotal))
|
||||
: msg.messages.length;
|
||||
if (renderedMessages < historyTotal) {
|
||||
if (renderedMessages < historyTotal || msg.messages.some((message) => !findRenderedSessionMessageByKey(getStableMessageKey(message)))) {
|
||||
reconcileRenderedSessionMessages(msg.messages, {
|
||||
preserveScroll: false,
|
||||
baseIndex: Number.isFinite(Number(msg.historyBaseIndex))
|
||||
@@ -9178,13 +9290,19 @@
|
||||
|| (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 || createLocalId('user');
|
||||
const resolvedMessageId = meta?.messageId || meta?.id || getStableMessageKey(meta) || 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}`;
|
||||
div.dataset.messageId = resolvedMessageId;
|
||||
if (isGoalMessage) div.dataset.goalMode = 'true';
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
|
||||
if (role === 'system') {
|
||||
const tone = String(meta.tone || 'neutral').trim() || 'neutral';
|
||||
@@ -10788,12 +10906,13 @@
|
||||
}
|
||||
}
|
||||
if (Number.isFinite(messageIndex)) {
|
||||
markSessionMessageElement(el, messageIndex);
|
||||
markSessionMessageElement(el, messageIndex, m);
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
function renderMessages(messages, options = {}) {
|
||||
messages = typeof dedupeSessionMessages === 'function' ? dedupeSessionMessages(messages) : messages;
|
||||
renderEpoch++;
|
||||
const epoch = renderEpoch;
|
||||
const baseIndex = Number.isFinite(Number(options.baseIndex)) ? Number(options.baseIndex) : 0;
|
||||
@@ -10869,11 +10988,16 @@
|
||||
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;
|
||||
if (existingIndexes.has(messageIndex)) return;
|
||||
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;
|
||||
@@ -11586,9 +11710,12 @@
|
||||
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;
|
||||
@@ -11596,6 +11723,19 @@
|
||||
let inserted = 0;
|
||||
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 replacement = buildMsgElement(message, messageIndex);
|
||||
keyed.replaceWith(replacement);
|
||||
existingByKey.set(messageKey, replacement);
|
||||
existing.set(messageIndex, replacement);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (existing.has(messageIndex)) return;
|
||||
const element = buildMsgElement(message, messageIndex);
|
||||
const next = Array.from(existing.entries())
|
||||
@@ -11603,6 +11743,7 @@
|
||||
.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;
|
||||
});
|
||||
if (inserted === 0) return;
|
||||
@@ -12461,13 +12602,21 @@
|
||||
if (currentSessionId) {
|
||||
const messageIndex = currentSessionMessageCount;
|
||||
currentSessionMessageCount += 1;
|
||||
markSessionMessageElement(element, messageIndex);
|
||||
markSessionMessageElement(element, messageIndex, { id: messageId, role: 'user', content: text, timestamp });
|
||||
}
|
||||
registerUserMessage(messageId, element, text, timestamp);
|
||||
updateUserOutlinePanel();
|
||||
scrollToBottom();
|
||||
|
||||
send({ type: 'message', text, attachments, sessionId: currentSessionId, mode: currentMode, agent: currentAgent });
|
||||
send({
|
||||
type: 'message',
|
||||
text,
|
||||
attachments,
|
||||
sessionId: currentSessionId,
|
||||
mode: currentMode,
|
||||
agent: currentAgent,
|
||||
clientMessageId: messageId,
|
||||
});
|
||||
startGenerating();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user