修复会话切换气泡丢失并重新打包
This commit is contained in:
138
public/app.js
138
public/app.js
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user