修复会话切换气泡丢失并重新打包

This commit is contained in:
shiyue
2026-09-14 22:03:23 +08:00
parent b1cd820819
commit a1e66c08f1
6 changed files with 558 additions and 26 deletions

View File

@@ -99,6 +99,8 @@
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;
const WS_CLIENT_HEARTBEAT_TIMEOUT_MS = 10_000;
@@ -296,6 +298,7 @@
let pendingNewSessionRequest = null;
let pendingSessionSwitchRequest = null;
let pendingSessionResumeRequest = null;
let pendingMissingSessionConfirmation = null;
let sessionSwitchRequestSeq = 0;
let skipDeleteConfirm = localStorage.getItem('cc-web-skip-delete-confirm') === '1';
let pendingInitialSessionLoad = false;
@@ -6510,6 +6513,71 @@
updateGenerationControls();
}
function clearPendingMissingSessionConfirmation(sessionId = null) {
const pending = pendingMissingSessionConfirmation;
if (!pending) return;
if (sessionId && pending.sessionId !== sessionId) return;
if (pending.timer) clearTimeout(pending.timer);
pendingMissingSessionConfirmation = null;
}
function scheduleMissingSessionConfirmation(sessionId) {
if (!sessionId) return;
const existing = pendingMissingSessionConfirmation;
if (existing?.sessionId === sessionId) {
existing.missingCount += 1;
return;
}
clearPendingMissingSessionConfirmation();
const pending = {
sessionId,
missingCount: 1,
timer: null,
};
const confirmMissingSession = () => {
if (pendingMissingSessionConfirmation !== pending) return;
if (currentSessionId !== sessionId) {
clearPendingMissingSessionConfirmation(sessionId);
return;
}
// 当前会话正在等待明确的加载/重连响应时,不能把等待中的视图清掉。
const hasPendingLoad = !!(
activeSessionLoad?.sessionId === sessionId
|| pendingSessionSwitchRequest?.sessionId === sessionId
|| pendingSessionResumeRequest?.sessionId === sessionId
);
if (pending.missingCount < 2 || hasPendingLoad) {
pending.timer = setTimeout(confirmMissingSession, SESSION_LIST_MISSING_CONFIRM_MS);
return;
}
pendingMissingSessionConfirmation = null;
resetChatView(currentAgent);
};
pendingMissingSessionConfirmation = pending;
pending.timer = setTimeout(confirmMissingSession, SESSION_LIST_MISSING_CONFIRM_MS);
// 主动再取一次列表,只有连续两次都缺少当前会话才允许清空视图。
send({ type: 'list_sessions' });
}
function reconcileCurrentSessionListSnapshot(nextSessions) {
const normalized = Array.isArray(nextSessions) ? nextSessions : [];
const sessionId = currentSessionId;
if (!sessionId) {
clearPendingMissingSessionConfirmation();
return normalized;
}
if (normalized.some((session) => session.id === sessionId)) {
clearPendingMissingSessionConfirmation(sessionId);
return normalized;
}
const previousCurrent = sessions.find((session) => session.id === sessionId) || null;
scheduleMissingSessionConfirmation(sessionId);
if (!previousCurrent) return normalized;
// 保留上一次确认过的当前会话元数据,避免侧栏高亮和聊天视图同时闪退。
return [previousCurrent, ...normalized.filter((session) => session.id !== sessionId)];
}
function closeAgentMenu() {
if (!chatAgentMenu) return;
chatAgentMenu.hidden = true;
@@ -6525,6 +6593,7 @@
}
function resetChatView(agent) {
clearPendingMissingSessionConfirmation();
setCurrentAgent(agent);
closeUserOutlinePanel();
closeCcwebPromptOutlinePanel();
@@ -6597,6 +6666,8 @@
resetHistoryLoadState(snapshot.sessionId);
resetOutlineHistoryState(snapshot.sessionId);
}
// session_info 是当前会话的明确存在证明,取消 session_list 缺失确认窗口。
clearPendingMissingSessionConfirmation();
currentSessionId = snapshot.sessionId;
syncHistoryLoadControl(snapshot);
syncOutlineHistoryIndex(snapshot);
@@ -6638,6 +6709,12 @@
startGenerating(snapshot.sessionId);
}
} else {
// 运行中重连/重复 load 时仍需把服务端持久化历史与现有流式 DOM 对账;
// 仅保留 streaming-msg 会导致用户气泡在第一次切换时消失。
reconcileRenderedSessionMessages(snapshot.messages || [], {
baseIndex: snapshot.historyBaseIndex || 0,
preserveScroll: true,
});
generatingSessionId = snapshot.sessionId;
}
highlightActiveSession();
@@ -7031,6 +7108,9 @@
function beginSessionSwitch(sessionId, options = {}) {
if (!sessionId) return;
if (currentSessionId && currentSessionId !== sessionId) {
clearPendingMissingSessionConfirmation(currentSessionId);
}
const blocking = options.blocking !== false;
const force = options.force === true;
if (!force && activeSessionLoad?.sessionId === sessionId && !activeSessionLoad.overlayReleased) return;
@@ -7167,7 +7247,8 @@
});
return;
}
if (!options.force && sessionId === currentSessionId && !activeSessionLoad) return;
const isPendingMissingCurrent = pendingMissingSessionConfirmation?.sessionId === sessionId;
if (!options.force && sessionId === currentSessionId && !activeSessionLoad && !isPendingMissingCurrent) return;
const disposition = getSessionCacheDisposition(sessionId);
if (disposition === 'strong') {
@@ -7185,7 +7266,7 @@
}
beginSessionSwitch(sessionId, {
blocking: options.blocking !== false,
force: options.force === true,
force: options.force === true || isPendingMissingCurrent,
label: options.label,
targetMessageIndex: options.targetMessageIndex,
});
@@ -7929,7 +8010,9 @@
break;
case 'session_list':
sessions = Array.isArray(msg.sessions) ? msg.sessions.map(normalizeSessionSnapshot) : [];
sessions = reconcileCurrentSessionListSnapshot(
Array.isArray(msg.sessions) ? msg.sessions.map(normalizeSessionSnapshot) : [],
);
reconcileSessionCacheWithSessions();
renderSessionList();
if (currentSessionId) {
@@ -7939,9 +8022,6 @@
pendingInitialSessionLoad = false;
initialSessionListHandled = true;
syncViewForAgent(currentAgent, { preserveCurrent: false, loadLast: true });
} else if (currentSessionId && !getSessionMeta(currentSessionId)) {
initialSessionListHandled = true;
resetChatView(currentAgent);
} else {
initialSessionListHandled = true;
}
@@ -8457,6 +8537,21 @@
case 'resume_session_result':
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) {
reconcileRenderedSessionMessages(msg.messages, {
preserveScroll: false,
baseIndex: Number.isFinite(Number(msg.historyBaseIndex))
? Number(msg.historyBaseIndex)
: Math.max(0, historyTotal - msg.messages.length),
});
loadedHistorySessionId = msg.sessionId;
}
}
setCurrentSessionRunningState(!!msg.isRunning);
if (!msg.isRunning && currentSessionId && msg.sessionId === currentSessionId) {
finishGenerating(msg.sessionId || currentSessionId);
@@ -11487,6 +11582,37 @@
}
}
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();
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 beforeHeight = messagesDiv.scrollHeight;
const beforeScrollTop = messagesDiv.scrollTop;
const stream = document.getElementById('streaming-msg');
let inserted = 0;
messages.forEach((message, offset) => {
const messageIndex = baseIndex + offset;
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);
inserted += 1;
});
if (inserted === 0) return;
updateUserOutlinePanel();
if (options.preserveScroll !== false) {
messagesDiv.scrollTop = beforeScrollTop + (messagesDiv.scrollHeight - beforeHeight);
}
updateScrollbar();
}
// --- Custom Scrollbar ---
const scrollbarEl = document.getElementById('custom-scrollbar');
const thumbEl = document.getElementById('custom-scrollbar-thumb');