改用 HTTP 加载会话历史并更新 CentOS 7 发布包

This commit is contained in:
shiyue
2026-10-15 00:36:26 +08:00
parent 2b1c91488d
commit fb2c3360c9
22 changed files with 1466 additions and 175 deletions

View File

@@ -104,7 +104,6 @@
const OLD_SESSION_COLLAPSE_MS = OLD_SESSION_COLLAPSE_DAYS * 24 * 60 * 60 * 1000;
const SESSION_LOAD_OVERLAY_TIMEOUT_MS = 12_000;
const SESSION_LOAD_REQUEST_TIMEOUT_MS = 45_000;
const SESSION_RESUME_FALLBACK_MS = 1_500;
// session_list 可能在重连/原子写入窗口中暂时缺少当前会话;不要把一次快照抖动当成删除。
const SESSION_LIST_MISSING_CONFIRM_MS = 1_500;
const WS_CLIENT_HEARTBEAT_INTERVAL_MS = 15_000;
@@ -231,7 +230,6 @@
let isGenerating = false;
let reconnectAttempts = 0;
let reconnectTimer = null;
let lastWsDisconnectNoticeAt = 0;
let isPageUnloading = false;
let pendingText = '';
let renderTimer = null;
@@ -281,7 +279,6 @@
};
let sessionLoadOverlayTimer = null;
let sessionLoadRequestTimer = null;
let sessionResumeFallbackTimer = null;
let sidebarSwipe = null;
let activeComposerToken = null;
let composerSuggestionTimer = null;
@@ -302,7 +299,11 @@
let codexAppApprovalModal = null;
let codexAppElicitationModal = null;
let pendingNewSessionRequest = null;
let pendingSessionSwitchRequest = null;
let sessionLoadAbortController = null;
let currentSessionViewRequestId = '';
let currentSessionHistoryRevision = null;
let currentSessionSubscriptionRequestId = '';
const sessionCompletionVersions = new Map();
let pendingSessionResumeRequest = null;
let pendingMissingSessionConfirmation = null;
let sessionSwitchRequestSeq = 0;
@@ -2083,6 +2084,8 @@
}
function resetOutlineHistoryState(sessionId = null) {
activeOutlineHistoryRequest?.controller?.abort();
activeOutlineTargetRequest?.controller?.abort();
activeOutlineHistoryRequest = null;
activeOutlineTargetRequest = null;
currentOutlineHistoryState = createOutlineHistoryState(sessionId);
@@ -2327,7 +2330,7 @@
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
return;
}
if (activeOutlineTargetRequest || !ws || ws.readyState !== 1 || !wsAuthenticated) return;
if (activeOutlineTargetRequest) return;
pendingAdvancedSearchJump = {
sessionId: currentSessionId,
messageIndex: Math.trunc(normalizedIndex),
@@ -2341,13 +2344,7 @@
messageIndex: Math.trunc(normalizedIndex),
};
closeUserOutlinePanel();
send({
type: 'load_history_page',
sessionId: currentSessionId,
before: Math.trunc(normalizedIndex) + 1,
requestId,
purpose: 'outline-target',
});
fetchHistoryPage(activeOutlineTargetRequest, Math.trunc(normalizedIndex) + 1);
}
function scrollToMessage(anchorId) {
@@ -2929,6 +2926,7 @@
historyTruncated,
historyAvailable: payload.historyAvailable !== false,
historySource: payload.historySource || 'snapshot',
historyRevision: payload.historyRevision ?? null,
historyPending,
complete: !historyPending
&& !historyTruncated
@@ -6953,7 +6951,6 @@
// 当前会话正在等待明确的加载/重连响应时,不能把等待中的视图清掉。
const hasPendingLoad = !!(
activeSessionLoad?.sessionId === sessionId
|| pendingSessionSwitchRequest?.sessionId === sessionId
|| pendingSessionResumeRequest?.sessionId === sessionId
);
if (pending.missingCount < 2 || hasPendingLoad) {
@@ -7004,6 +7001,10 @@
function resetChatView(agent) {
clearPendingMissingSessionConfirmation();
currentSessionViewRequestId = '';
currentSessionHistoryRevision = null;
currentSessionSubscriptionRequestId = '';
pendingSessionResumeRequest = null;
setCurrentAgent(agent);
closeUserOutlinePanel();
closeCcwebPromptOutlinePanel();
@@ -7184,25 +7185,11 @@
sessionLoadRequestTimer = null;
}
function clearSessionResumeFallbackTimer() {
if (!sessionResumeFallbackTimer) return;
clearTimeout(sessionResumeFallbackTimer);
sessionResumeFallbackTimer = null;
}
function clearPendingSessionSwitchRequest(sessionId, requestId) {
if (!pendingSessionSwitchRequest) return;
if (sessionId && pendingSessionSwitchRequest.sessionId !== sessionId) return;
if (requestId && pendingSessionSwitchRequest.requestId !== requestId) return;
pendingSessionSwitchRequest = null;
}
function clearPendingSessionResumeRequest(sessionId, requestId) {
if (!pendingSessionResumeRequest) return;
if (sessionId && pendingSessionResumeRequest.sessionId !== sessionId) return;
if (requestId && pendingSessionResumeRequest.requestId !== requestId) return;
pendingSessionResumeRequest = null;
clearSessionResumeFallbackTimer();
}
function notifySessionLoadTimeout(sessionId) {
@@ -7225,7 +7212,6 @@
activeSessionLoad.requestId !== requestId) {
return;
}
clearPendingSessionSwitchRequest(sessionId, requestId);
clearSessionLoading(sessionId);
notifySessionLoadTimeout(sessionId);
}, SESSION_LOAD_REQUEST_TIMEOUT_MS);
@@ -7246,6 +7232,8 @@
}
function setSessionLoading(sessionId, options = {}) {
sessionLoadAbortController?.abort();
sessionLoadAbortController = null;
clearSessionLoadOverlayTimer();
clearSessionLoadRequestTimer();
const loading = !!sessionId;
@@ -7258,7 +7246,7 @@
requestId,
overlayReleased: false,
recoverCurrent: options.recoverCurrent === true,
targetMessageIndex: Number.isFinite(Number(options.targetMessageIndex))
targetMessageIndex: options.targetMessageIndex != null && Number.isFinite(Number(options.targetMessageIndex))
? Math.max(0, Number(options.targetMessageIndex))
: null,
} : null;
@@ -7292,6 +7280,7 @@
}
function resetHistoryLoadState(sessionId = null) {
activeHistoryPageRequest?.controller?.abort();
activeHistoryPageRequest = null;
currentHistoryState = {
sessionId,
@@ -7368,7 +7357,7 @@
function requestOlderHistory() {
const state = currentHistoryState;
if (!state.sessionId || state.sessionId !== currentSessionId || state.cursor <= 0) return;
if (activeHistoryPageRequest || !ws || ws.readyState !== 1 || !wsAuthenticated) return;
if (activeHistoryPageRequest) return;
historyPageRequestSeq += 1;
const requestId = `history-page-${Date.now()}-${historyPageRequestSeq}`;
activeHistoryPageRequest = {
@@ -7378,34 +7367,24 @@
};
currentHistoryState = { ...state, loading: true, error: '' };
renderHistoryLoadControl();
send({
type: 'load_history_page',
sessionId: state.sessionId,
before: state.cursor,
requestId,
});
fetchHistoryPage(activeHistoryPageRequest, state.cursor);
}
function requestOutlineHistoryPage() {
const state = currentOutlineHistoryState;
if (!state.sessionId || state.sessionId !== currentSessionId || state.cursor <= 0) return;
if (activeOutlineHistoryRequest || !ws || ws.readyState !== 1 || !wsAuthenticated) return;
if (activeOutlineHistoryRequest) return;
outlineHistoryRequestSeq += 1;
const requestId = `outline-history-${Date.now()}-${outlineHistoryRequestSeq}`;
activeOutlineHistoryRequest = {
sessionId: state.sessionId,
requestId,
before: state.cursor,
purpose: 'outline',
};
currentOutlineHistoryState = { ...state, loading: true, error: '' };
updateUserOutlinePanel();
send({
type: 'load_history_page',
sessionId: state.sessionId,
before: state.cursor,
requestId,
purpose: 'outline',
});
fetchHistoryPage(activeOutlineHistoryRequest, state.cursor);
}
function mergeHistoryChunkIntoCachedSnapshot(sessionId, messages, options = {}) {
@@ -7529,21 +7508,114 @@
function requestSessionLoad(sessionId, options = {}) {
if (!sessionId) return;
pendingSessionSwitchRequest = {
sessionId,
blocking: options.blocking !== false,
label: options.label || '',
requestId: options.requestId || activeSessionLoad?.requestId || createSessionSwitchRequestId(sessionId),
recoverCurrent: options.recoverCurrent === true,
targetMessageIndex: Number.isFinite(Number(options.targetMessageIndex))
? Math.max(0, Number(options.targetMessageIndex))
: activeSessionLoad?.targetMessageIndex ?? null,
};
if (ws && ws.readyState === 1 && wsAuthenticated) {
flushPendingSessionSwitch();
return;
const requestId = options.requestId
|| (activeSessionLoad?.sessionId === sessionId ? activeSessionLoad.requestId : '')
|| createSessionSwitchRequestId(sessionId);
const targetMessageIndex = options.targetMessageIndex != null && Number.isFinite(Number(options.targetMessageIndex))
? Math.max(0, Number(options.targetMessageIndex))
: activeSessionLoad?.targetMessageIndex ?? null;
if (!activeSessionLoad || activeSessionLoad.sessionId !== sessionId || activeSessionLoad.requestId !== requestId) {
setSessionLoading(sessionId, { ...options, requestId, targetMessageIndex });
}
if (!ws || ws.readyState > 1) connect();
sessionLoadAbortController?.abort();
if (activeHistoryPageRequest) {
currentHistoryState = { ...currentHistoryState, loading: false };
renderHistoryLoadControl();
}
if (activeOutlineHistoryRequest) currentOutlineHistoryState = { ...currentOutlineHistoryState, loading: false };
activeHistoryPageRequest?.controller?.abort();
activeOutlineHistoryRequest?.controller?.abort();
activeOutlineTargetRequest?.controller?.abort();
activeHistoryPageRequest = null;
activeOutlineHistoryRequest = null;
activeOutlineTargetRequest = null;
const previousViewRequestId = currentSessionViewRequestId;
currentSessionViewRequestId = requestId;
if (options.subscribe !== false) {
currentSessionSubscriptionRequestId = '';
pendingSessionResumeRequest = null;
}
const controller = new AbortController();
sessionLoadAbortController = controller;
clearSessionLoadRequestTimer();
const completionVersion = sessionCompletionVersions.get(sessionId) || 0;
let applyingSnapshot = false;
// 历史查询不等待 WebSocket;控制器和请求代次共同隔离迟到响应。
fetchSessionQuery(sessionId, requestId, { signal: controller.signal }).then(payload => {
if (sessionLoadAbortController !== controller || currentSessionViewRequestId !== requestId) return;
// 已经收到完成事件的页面不能再被更早读出的运行快照覆盖。
if ((sessionCompletionVersions.get(sessionId) || 0) !== completionVersion) {
requestSessionLoad(sessionId, { ...options, targetMessageIndex,
requestId: createSessionSwitchRequestId(sessionId) });
return;
}
applyingSnapshot = true;
sessionLoadAbortController = null;
currentSessionHistoryRevision = payload.historyRevision ?? null;
handleServerMessage(payload);
if (currentSessionId !== sessionId || currentSessionViewRequestId !== requestId) return;
// 订阅后的历史对账保留最新实时输出,不能用较早的 HTTP 运行快照覆盖它。
if (payload.runtime && options.subscribe !== false) {
handleServerMessage({ ...payload.runtime, requestId });
if (payload.runtime.isRunning === false) finishGenerating(sessionId);
}
if (options.subscribe !== false) requestSessionResume(sessionId);
if (targetMessageIndex !== null) requestOutlineMessageTarget(targetMessageIndex);
applyingSnapshot = false;
}).catch(error => {
if ((!applyingSnapshot && sessionLoadAbortController !== controller) || currentSessionViewRequestId !== requestId) return;
currentSessionViewRequestId = previousViewRequestId;
clearSessionLoading(sessionId);
appendError(error.message || '会话历史加载失败,请重试', { transient: true, autoDismissMs: 9000 });
if (currentSessionId && options.subscribe !== false) requestSessionResume(currentSessionId);
}).finally(() => {
if (sessionLoadAbortController === controller) sessionLoadAbortController = null;
});
}
async function fetchSessionQuery(sessionId, requestId, options = {}) {
const controller = new AbortController();
const abort = () => controller.abort();
if (options.signal?.aborted) abort();
options.signal?.addEventListener('abort', abort, { once: true });
let timedOut = false;
const timer = setTimeout(() => { timedOut = true; abort(); }, SESSION_LOAD_REQUEST_TIMEOUT_MS);
try {
const parameters = new URLSearchParams({ requestId });
if (options.before !== undefined) parameters.set('before', String(options.before));
const response = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}${options.historyPage ? '/history' : ''}?${parameters}`, {
method: 'GET', headers: { Authorization: `Bearer ${authToken}` }, cache: 'no-store', signal: controller.signal,
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || '会话历史加载失败,请重试');
if (payload.sessionId !== sessionId || payload.requestId !== requestId) throw new Error('会话历史响应与请求不匹配');
if (payload.type !== (options.historyPage ? 'session_history_chunk' : 'session_info') || !Array.isArray(payload.messages)) {
throw new Error('会话历史响应格式无效');
}
return payload;
} catch (error) {
if (timedOut) throw new Error('会话历史 HTTP 请求超时,请重试');
throw error;
} finally {
clearTimeout(timer);
options.signal?.removeEventListener('abort', abort);
}
}
function fetchHistoryPage(request, before) {
request.controller = new AbortController();
const viewRequestId = currentSessionViewRequestId;
const isActive = () => currentSessionId === request.sessionId
&& currentSessionViewRequestId === viewRequestId
&& [activeHistoryPageRequest, activeOutlineHistoryRequest, activeOutlineTargetRequest].includes(request);
fetchSessionQuery(request.sessionId, request.requestId, {
historyPage: true, before, signal: request.controller.signal,
}).then(payload => {
if (isActive()) handleServerMessage(payload);
}).catch(error => {
if (isActive()) handleServerMessage({ type: 'error', sessionId: request.sessionId,
requestId: request.requestId, message: error.message || '加载更早消息失败,请重试' });
});
}
function requestSessionResume(sessionId, options = {}) {
@@ -7552,6 +7624,7 @@
sessionId,
requestId: options.requestId || createSessionSwitchRequestId(sessionId),
};
currentSessionSubscriptionRequestId = pendingSessionResumeRequest.requestId;
if (ws && ws.readyState === 1 && wsAuthenticated) {
flushPendingSessionResume();
return;
@@ -7559,56 +7632,18 @@
if (!ws || ws.readyState > 1) connect();
}
function scheduleSessionResumeFallback(sessionId, requestId) {
clearSessionResumeFallbackTimer();
if (!sessionId || !requestId) return;
sessionResumeFallbackTimer = setTimeout(() => {
sessionResumeFallbackTimer = null;
if (!pendingSessionResumeRequest ||
pendingSessionResumeRequest.sessionId !== sessionId ||
pendingSessionResumeRequest.requestId !== requestId) {
return;
}
pendingSessionResumeRequest = null;
requestSessionLoad(sessionId, {
blocking: false,
label: '正在恢复运行输出…',
requestId: createSessionSwitchRequestId(sessionId),
recoverCurrent: true,
});
}, SESSION_RESUME_FALLBACK_MS);
}
function flushPendingSessionSwitch() {
if (!pendingSessionSwitchRequest) return false;
if (!ws || ws.readyState !== 1 || !wsAuthenticated) return false;
const request = pendingSessionSwitchRequest;
pendingSessionSwitchRequest = null;
if (!activeSessionLoad) {
setSessionLoading(request.sessionId, {
blocking: request.blocking,
label: request.label || undefined,
requestId: request.requestId,
recoverCurrent: request.recoverCurrent === true,
targetMessageIndex: request.targetMessageIndex,
});
}
ws.send(JSON.stringify({
type: 'load_session',
sessionId: request.sessionId,
requestId: request.requestId,
...(Number.isFinite(request.targetMessageIndex) ? { targetMessageIndex: request.targetMessageIndex } : {}),
}));
return true;
}
function flushPendingSessionResume() {
if (!pendingSessionResumeRequest) return false;
if (!ws || ws.readyState !== 1 || !wsAuthenticated) return false;
const request = pendingSessionResumeRequest;
ws.send(JSON.stringify({ type: 'resume_session', sessionId: request.sessionId, requestId: request.requestId }));
scheduleSessionResumeFallback(request.sessionId, request.requestId);
return true;
try {
ws.send(JSON.stringify({ type: 'subscribe_session', sessionId: request.sessionId,
requestId: request.requestId, historyRevision: currentSessionHistoryRevision }));
return true;
} catch {
showToast('实时订阅暂未连接,历史仍可查看');
return false;
}
}
function showCachedSession(sessionId) {
@@ -7622,7 +7657,13 @@
closeCcwebPromptOutlinePanel();
clearSessionLoading();
touchSessionCache(sessionId);
return applySessionSnapshot(snapshot, { immediate: true, suppressUnreadToast: true });
const applied = applySessionSnapshot(snapshot, { immediate: true, suppressUnreadToast: true });
if (applied) {
currentSessionViewRequestId = createSessionSwitchRequestId(sessionId);
currentSessionHistoryRevision = snapshot.historyRevision ?? null;
requestSessionResume(sessionId);
}
return applied;
}
function openSession(sessionId, options = {}) {
@@ -8212,55 +8253,14 @@
wsAuthenticated = false;
wsSupportsClientHeartbeat = false;
clearWsHeartbeat();
if (activeOutlineHistoryRequest) {
activeOutlineHistoryRequest = null;
currentOutlineHistoryState = {
...currentOutlineHistoryState,
loading: false,
error: '连接中断,连接恢复后可重试',
};
if (userOutlinePanel && !userOutlinePanel.hidden) updateUserOutlinePanel();
}
if (activeOutlineTargetRequest) {
activeOutlineTargetRequest = null;
pendingAdvancedSearchJump = null;
if (!isPageUnloading) {
appendError('连接中断,定位消息加载失败;连接恢复后请重新点击该条目。', {
transient: true,
autoDismissMs: 7000,
});
}
}
if (activeHistoryPageRequest) {
activeHistoryPageRequest = null;
currentHistoryState = {
...currentHistoryState,
loading: false,
error: '连接中断,连接恢复后可重试',
};
renderHistoryLoadControl();
}
if (activeSessionLoad?.sessionId && !isPageUnloading) {
pendingSessionSwitchRequest = {
sessionId: activeSessionLoad.sessionId,
blocking: false,
label: sessionLoadingLabel?.textContent || '',
requestId: activeSessionLoad.requestId || createSessionSwitchRequestId(activeSessionLoad.sessionId),
recoverCurrent: activeSessionLoad.recoverCurrent === true,
targetMessageIndex: activeSessionLoad.targetMessageIndex,
};
const now = Date.now();
if (now - lastWsDisconnectNoticeAt >= 5000) {
lastWsDisconnectNoticeAt = now;
showToast('连接中断,正在重连并自动重试会话切换…');
}
} else if (currentSessionId && (isGenerating || currentSessionRunning) && !isPageUnloading) {
// 长连接中断只影响实时订阅,正在进行的 HTTP 首屏和分页继续完成。
if (currentSessionId && !isPageUnloading) {
pendingSessionResumeRequest = {
sessionId: currentSessionId,
requestId: createSessionSwitchRequestId(currentSessionId),
};
currentSessionSubscriptionRequestId = pendingSessionResumeRequest.requestId;
}
clearSessionLoading();
scheduleReconnect();
};
socket.onerror = () => {};
@@ -8359,14 +8359,11 @@
app.hidden = false;
setUsageDashboardFeatureEnabled(msg.features?.usageStatistics === true);
setTaskBoardFeatureEnabled(msg.features?.taskBoard === true);
const flushedSessionSwitch = flushPendingSessionSwitch();
const flushedSessionResume = flushedSessionSwitch ? false : flushPendingSessionResume();
if (!flushedSessionSwitch &&
const flushedSessionResume = sessionLoadAbortController ? false : flushPendingSessionResume();
if (!sessionLoadAbortController &&
!flushedSessionResume &&
!pendingSessionSwitchRequest &&
!pendingSessionResumeRequest &&
currentSessionId &&
(isGenerating || currentSessionRunning)) {
currentSessionId) {
requestSessionResume(currentSessionId, {
requestId: createSessionSwitchRequestId(currentSessionId),
});
@@ -8381,9 +8378,8 @@
} else {
clearWsHeartbeat();
wsSupportsClientHeartbeat = false;
pendingSessionSwitchRequest = null;
pendingSessionResumeRequest = null;
clearSessionResumeFallbackTimer();
currentSessionSubscriptionRequestId = '';
clearSessionLoading();
authToken = null;
wsAuthenticated = false;
@@ -8841,6 +8837,10 @@
break;
case 'done':
if (msg.sessionId) {
sessionCompletionVersions.set(msg.sessionId, (sessionCompletionVersions.get(msg.sessionId) || 0) + 1);
if (sessionCompletionVersions.size > 2048) sessionCompletionVersions.delete(sessionCompletionVersions.keys().next().value);
}
if (!isCurrentSessionEvent(msg)) {
if (msg.sessionId) {
updateCachedSession(msg.sessionId, (snapshot) => { snapshot.isRunning = msg.goalActive === true; });
@@ -8927,8 +8927,29 @@
}
break;
case 'session_subscribed':
if (msg.sessionId !== currentSessionId || String(msg.requestId || '') !== currentSessionSubscriptionRequestId) break;
clearPendingSessionResumeRequest(msg.sessionId, msg.requestId);
const subscribedMeta = getSessionMeta(msg.sessionId);
if (subscribedMeta) subscribedMeta.hasUnread = false;
updateCachedSession(msg.sessionId, cached => { cached.hasUnread = false; });
renderSessionList();
if (msg.historyChanged) {
requestSessionLoad(msg.sessionId, {
requestId: createSessionSwitchRequestId(msg.sessionId), blocking: false,
recoverCurrent: true, subscribe: false,
});
} else {
currentSessionHistoryRevision = msg.historyRevision ?? null;
updateCachedSession(msg.sessionId, cached => { cached.historyRevision = currentSessionHistoryRevision; });
setCurrentSessionRunningState(!!msg.isRunning);
if (!msg.isRunning && isGenerating) finishGenerating(msg.sessionId);
}
break;
case 'resume_generating':
if (!isCurrentSessionEvent(msg)) break;
if (msg.requestId && msg.requestId !== currentSessionViewRequestId && msg.requestId !== currentSessionSubscriptionRequestId) break;
clearPendingSessionResumeRequest(msg.sessionId || currentSessionId, msg.requestId);
// Server has an active process for this session — resume streaming
setCurrentSessionRunningState(true);
@@ -9002,6 +9023,11 @@
break;
}
const errorRequestId = String(msg.requestId || '');
if (errorRequestId && errorRequestId === currentSessionSubscriptionRequestId) {
clearPendingSessionResumeRequest(msg.sessionId, errorRequestId);
showToast(msg.message || '实时订阅失败,历史仍可查看');
break;
}
const matchesOutlineTargetError = !!(activeOutlineTargetRequest
&& (!msg.sessionId || msg.sessionId === activeOutlineTargetRequest.sessionId)
&& errorRequestId === activeOutlineTargetRequest.requestId);
@@ -9044,9 +9070,6 @@
(!msg.sessionId || msg.sessionId === activeSessionLoad.sessionId) &&
(!errorRequestId || errorRequestId === activeSessionLoad.requestId));
if (!matchesActiveLoadError && !isCurrentSessionEvent(msg)) break;
if (matchesActiveLoadError) {
clearPendingSessionSwitchRequest(activeSessionLoad.sessionId, activeSessionLoad.requestId);
}
if (msg.code === 'new_session_cwd_missing' && pendingNewSessionRequest?.cwd) {
const request = pendingNewSessionRequest;
pendingNewSessionRequest = null;