修复历史消息回看并重新打包

This commit is contained in:
shiyue
2026-09-11 09:29:53 +08:00
parent 6fff9023b2
commit b8fcc12229
18 changed files with 494 additions and 73 deletions

View File

@@ -237,6 +237,17 @@
let codexConfigCache = null;
let loadedHistorySessionId = null;
let activeSessionLoad = null;
let historyPageRequestSeq = 0;
let activeHistoryPageRequest = null;
let currentHistoryState = {
sessionId: null,
cursor: 0,
remaining: 0,
total: 0,
available: false,
loading: false,
error: '',
};
let sessionLoadOverlayTimer = null;
let sessionLoadRequestTimer = null;
let sessionResumeFallbackTimer = null;
@@ -436,6 +447,7 @@
const pendingNotesTray = $('#pending-notes-tray');
const imageUploadInput = $('#image-upload-input');
const attachBtn = $('#attach-btn');
const historyLoadMore = $('#history-load-more');
const messagesDiv = $('#messages');
const msgInput = $('#msg-input');
const inputWrapper = msgInput.closest('.input-wrapper');
@@ -2437,6 +2449,15 @@
|| payload.taskTracking?.enabled === true,
historyTotal,
historyBaseIndex,
historyBuffered: Number.isFinite(Number(payload.historyBuffered))
? Math.max(0, Number(payload.historyBuffered))
: messages.length,
historyCursor: Number.isFinite(Number(payload.historyCursor))
? Math.max(0, Number(payload.historyCursor))
: Math.max(0, historyBaseIndex),
historyTruncated: !!payload.historyTruncated,
historyAvailable: payload.historyAvailable !== false,
historySource: payload.historySource || 'snapshot',
historyPending: !!payload.historyPending,
complete: options.complete !== undefined ? !!options.complete : !payload.historyPending,
};
@@ -6277,6 +6298,7 @@
closeCcwebPromptOutlinePanel();
closeFileBrowser();
currentSessionId = null;
resetHistoryLoadState(null);
syncTaskTrackingControl(null);
loadedHistorySessionId = null;
currentSessionMessageCount = 0;
@@ -6336,7 +6358,9 @@
activeToolCalls.clear();
activeTodoCallTargets.clear();
}
if (currentSessionId !== snapshot.sessionId) resetHistoryLoadState(snapshot.sessionId);
currentSessionId = snapshot.sessionId;
syncHistoryLoadControl(snapshot);
syncTaskTrackingControl(snapshot);
loadedHistorySessionId = snapshot.sessionId;
currentOutlineTitleHistory = normalizeOutlineTitleHistory(snapshot.titleHistory);
@@ -6539,6 +6563,117 @@
setSessionLoading(null, { blocking: false });
}
function resetHistoryLoadState(sessionId = null) {
activeHistoryPageRequest = null;
currentHistoryState = {
sessionId,
cursor: 0,
remaining: 0,
total: 0,
available: false,
loading: false,
error: '',
};
renderHistoryLoadControl();
}
function renderHistoryLoadControl() {
if (!historyLoadMore) return;
const state = currentHistoryState;
historyLoadMore.innerHTML = '';
historyLoadMore.hidden = !state.sessionId || state.cursor <= 0;
if (historyLoadMore.hidden) return;
const copy = document.createElement('span');
copy.className = 'history-load-more-copy';
if (!state.available) {
copy.textContent = `还有 ${state.cursor} 条更早消息,但原始记录不可用,无法恢复。`;
historyLoadMore.classList.add('is-unavailable');
historyLoadMore.appendChild(copy);
return;
}
historyLoadMore.classList.remove('is-unavailable');
copy.textContent = state.error
? state.error
: `还有 ${state.cursor} 条更早消息`;
const button = document.createElement('button');
button.type = 'button';
button.className = 'history-load-more-button';
button.textContent = state.loading ? '正在加载…' : (state.error ? '重试' : '查看更早消息');
button.disabled = state.loading;
button.addEventListener('click', requestOlderHistory);
historyLoadMore.append(copy, button);
}
function syncHistoryLoadControl(payload = null) {
const sessionId = payload?.sessionId || currentSessionId;
if (!sessionId || sessionId !== currentSessionId) {
resetHistoryLoadState(sessionId || null);
return;
}
const nextCursor = Number.isFinite(Number(payload?.historyCursor))
? Math.max(0, Number(payload.historyCursor))
: currentHistoryState.cursor;
currentHistoryState = {
...currentHistoryState,
sessionId,
cursor: nextCursor,
remaining: nextCursor,
total: Number.isFinite(Number(payload?.historyTotal))
? Math.max(0, Number(payload.historyTotal))
: currentHistoryState.total,
available: payload?.historyAvailable !== undefined
? payload.historyAvailable !== false
: currentHistoryState.available,
error: '',
};
renderHistoryLoadControl();
}
function requestOlderHistory() {
const state = currentHistoryState;
if (!state.sessionId || state.sessionId !== currentSessionId || state.cursor <= 0 || !state.available) return;
if (activeHistoryPageRequest || !ws || ws.readyState !== 1 || !wsAuthenticated) return;
historyPageRequestSeq += 1;
const requestId = `history-page-${Date.now()}-${historyPageRequestSeq}`;
activeHistoryPageRequest = {
sessionId: state.sessionId,
requestId,
before: state.cursor,
};
currentHistoryState = { ...state, loading: true, error: '' };
renderHistoryLoadControl();
send({
type: 'load_history_page',
sessionId: state.sessionId,
before: state.cursor,
requestId,
});
}
function mergeHistoryChunkIntoCachedSnapshot(sessionId, messages, options = {}) {
if (!sessionId || !Array.isArray(messages) || messages.length === 0) return;
updateCachedSession(sessionId, (snapshot) => {
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 indexed = new Map();
existingMessages.forEach((message, index) => indexed.set(existingBase + index, message));
messages.forEach((message, index) => indexed.set(incomingBase + index, message));
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;
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.historyPending = false;
snapshot.complete = snapshot.historyCursor === 0;
});
}
function createSessionSwitchRequestId(sessionId) {
sessionSwitchRequestSeq += 1;
return `session-load-${Date.now()}-${sessionSwitchRequestSeq}-${String(sessionId || '').slice(0, 8)}`;
@@ -7255,6 +7390,15 @@
if (ws !== socket) return;
ws = null;
wsAuthenticated = false;
if (activeHistoryPageRequest) {
activeHistoryPageRequest = null;
currentHistoryState = {
...currentHistoryState,
loading: false,
error: '连接中断,连接恢复后可重试',
};
renderHistoryLoadControl();
}
if (activeSessionLoad?.sessionId && !isPageUnloading) {
pendingSessionSwitchRequest = {
sessionId: activeSessionLoad.sessionId,
@@ -7512,6 +7656,9 @@
const matchesActiveHistoryLoad = !!(activeSessionLoad
&& activeSessionLoad.sessionId === msg.sessionId
&& (!historyRequestId || activeSessionLoad.requestId === historyRequestId));
const matchesManualHistoryLoad = !!(typeof activeHistoryPageRequest !== 'undefined' && activeHistoryPageRequest
&& activeHistoryPageRequest.sessionId === msg.sessionId
&& activeHistoryPageRequest.requestId === historyRequestId);
const allowsLegacyHistory = !historyRequestId
&& msg.sessionId === currentSessionId
&& loadedHistorySessionId === msg.sessionId;
@@ -7523,7 +7670,7 @@
if (!msg.remaining) finalizeLoadedSession(msg.sessionId, historyRequestId || undefined);
break;
}
if ((matchesActiveHistoryLoad || allowsLegacyHistory)
if ((matchesActiveHistoryLoad || matchesManualHistoryLoad || allowsLegacyHistory)
&& msg.sessionId === currentSessionId
&& loadedHistorySessionId === msg.sessionId) {
const blocking = isBlockingSessionLoad(msg.sessionId);
@@ -7536,7 +7683,29 @@
baseIndex: Number.isFinite(Number(msg.historyBaseIndex)) ? Number(msg.historyBaseIndex) : 0,
});
scheduleAdvancedSearchJump();
if (!msg.remaining) {
if (matchesManualHistoryLoad) {
const nextCursor = Number.isFinite(Number(msg.historyCursor))
? Math.max(0, Number(msg.historyCursor))
: Math.max(0, activeHistoryPageRequest.before - (msg.messages || []).length);
mergeHistoryChunkIntoCachedSnapshot(msg.sessionId, msg.messages || [], {
baseIndex: Number.isFinite(Number(msg.historyBaseIndex)) ? Number(msg.historyBaseIndex) : 0,
cursor: nextCursor,
total: msg.historyTotal,
});
activeHistoryPageRequest = null;
currentHistoryState = {
...currentHistoryState,
cursor: nextCursor,
remaining: nextCursor,
total: Number.isFinite(Number(msg.historyTotal))
? Math.max(0, Number(msg.historyTotal))
: currentHistoryState.total,
available: msg.historyAvailable !== false,
loading: false,
error: '',
};
renderHistoryLoadControl();
} else if (!msg.remaining) {
finalizeLoadedSession(msg.sessionId, historyRequestId || undefined);
}
}
@@ -7836,6 +8005,19 @@
case 'error':
const errorRequestId = String(msg.requestId || '');
const matchesHistoryPageError = !!(typeof activeHistoryPageRequest !== 'undefined' && activeHistoryPageRequest
&& (!msg.sessionId || msg.sessionId === activeHistoryPageRequest.sessionId)
&& errorRequestId === activeHistoryPageRequest.requestId);
if (matchesHistoryPageError) {
activeHistoryPageRequest = null;
currentHistoryState = {
...currentHistoryState,
loading: false,
error: msg.message || '加载更早消息失败,请重试',
};
renderHistoryLoadControl();
break;
}
const matchesActiveLoadError = !!(activeSessionLoad &&
(!msg.sessionId || msg.sessionId === activeSessionLoad.sessionId) &&
(!errorRequestId || errorRequestId === activeSessionLoad.requestId));
@@ -10095,13 +10277,24 @@
function prependHistoryMessages(messages, options = {}) {
if (!Array.isArray(messages) || messages.length === 0) return;
const baseIndex = Number.isFinite(Number(options.baseIndex)) ? Number(options.baseIndex) : 0;
collectClosedCollabAgentIds(messages).forEach((id) => closedCollabAgentIds.add(id));
const existingIndexes = new Set(Array.from(
messagesDiv.querySelectorAll('[data-session-message="true"][data-message-index]'),
).map((element) => Number(element.dataset.messageIndex)));
const uniqueMessages = [];
messages.forEach((message, index) => {
const messageIndex = baseIndex + index;
if (existingIndexes.has(messageIndex)) return;
existingIndexes.add(messageIndex);
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();
messages.forEach((m, index) => frag.appendChild(buildMsgElement(m, baseIndex + index)));
uniqueMessages.forEach(({ message, index }) => frag.appendChild(buildMsgElement(message, baseIndex + index)));
if (!preserveScroll) {
messagesDiv.insertBefore(frag, messagesDiv.firstChild);
updateUserOutlinePanel();

View File

@@ -105,6 +105,7 @@
</header>
<div class="messages-wrap">
<div id="history-load-more" class="history-load-more" role="status" aria-live="polite" hidden></div>
<div id="messages" class="messages">
<div class="welcome-msg">
<section class="warframe-hero" aria-labelledby="warframe-welcome-title">

View File

@@ -2727,6 +2727,57 @@ html[data-theme='wasteland'] .advanced-search-panel {
overflow: hidden;
min-height: 0;
}
.history-load-more {
position: absolute;
z-index: 3;
top: 10px;
left: 50%;
display: inline-flex;
align-items: center;
gap: 10px;
max-width: min(92%, 560px);
padding: 7px 10px 7px 12px;
transform: translateX(-50%);
border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--border-color));
border-radius: 999px;
background: color-mix(in srgb, var(--bg-secondary) 92%, transparent);
box-shadow: 0 5px 18px rgba(35, 25, 18, 0.12);
color: var(--text-secondary);
font-size: 12px;
line-height: 1.35;
backdrop-filter: blur(12px);
}
.history-load-more-copy {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.history-load-more-button {
flex: 0 0 auto;
min-height: 26px;
padding: 3px 10px;
border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--border-color));
border-radius: 999px;
background: var(--accent-light);
color: var(--accent);
font: inherit;
font-weight: 700;
cursor: pointer;
}
.history-load-more-button:hover,
.history-load-more-button:focus-visible {
border-color: var(--accent);
outline: none;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 16%, transparent);
}
.history-load-more-button:disabled {
cursor: wait;
opacity: 0.62;
}
.history-load-more.is-unavailable {
border-color: color-mix(in srgb, var(--danger) 35%, var(--border-color));
color: var(--danger);
}
.messages {
height: 100%;
overflow-y: scroll;