mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-23 11:22:47 +02:00
修复多会话任务栏抖动、会话跳回与停止失效 (#264)
* fix(web): stabilize active task ordering * fix(chat): preserve navigation during conversation startup * fix(tasks): guarantee hard cancellation * fix(chat): bind navigation and hard stop targets * fix(chat): prevent replay from reclaiming navigation
This commit is contained in:
+112
-13
@@ -10,8 +10,46 @@ function syncChatConversationHash(conversationId) {
|
||||
}
|
||||
}
|
||||
window.syncChatConversationHash = syncChatConversationHash;
|
||||
|
||||
function clearChatConversationHash() {
|
||||
if (window.location.hash.split('?')[0] !== '#chat' || window.location.hash === '#chat') return;
|
||||
window.history.replaceState(null, '', '#chat');
|
||||
}
|
||||
window.clearChatConversationHash = clearChatConversationHash;
|
||||
let loadConversationRequestSeq = 0;
|
||||
let loadConversationAbortController = null;
|
||||
let loadConversationPendingId = '';
|
||||
let chatConversationNavigationSeq = 0;
|
||||
|
||||
function isChatConversationLoadPending(conversationId) {
|
||||
const id = String(conversationId || '').trim();
|
||||
return !!id && loadConversationPendingId === id;
|
||||
}
|
||||
window.isChatConversationLoadPending = isChatConversationLoadPending;
|
||||
|
||||
function markChatConversationNavigation(nextConversationId, force = false) {
|
||||
const nextId = String(nextConversationId || '').trim();
|
||||
const visibleId = String(currentConversationId || '').trim();
|
||||
if (force || nextId !== visibleId) {
|
||||
chatConversationNavigationSeq++;
|
||||
}
|
||||
return chatConversationNavigationSeq;
|
||||
}
|
||||
|
||||
/**
|
||||
* 离开聊天页时立即让尚在初始化的发送请求失去页面所有权。
|
||||
* 后端任务仍会继续执行;这里只中止浏览器前台流,避免首个 conversation
|
||||
* 事件在用户已经切到其他页面后再次抢占当前会话。
|
||||
*/
|
||||
function abandonChatConversationForPageNavigation() {
|
||||
markChatConversationNavigation('', true);
|
||||
if (typeof window.cancelScheduledChatConversationFromHash === 'function') {
|
||||
window.cancelScheduledChatConversationFromHash();
|
||||
}
|
||||
cancelPendingConversationLoad();
|
||||
detachLiveChatStreamForNavigation('', true);
|
||||
}
|
||||
window.abandonChatConversationForPageNavigation = abandonChatConversationForPageNavigation;
|
||||
|
||||
/**
|
||||
* 轻量会话 LRU 缓存。
|
||||
@@ -1768,6 +1806,18 @@ function ownsLiveChatStream(liveStream) {
|
||||
return !!liveStream && window.__csAgentLiveStream === liveStream;
|
||||
}
|
||||
|
||||
function shouldIgnoreLiveChatStreamEvent(
|
||||
liveStream,
|
||||
activeLiveStream = window.__csAgentLiveStream,
|
||||
navigationSeq = chatConversationNavigationSeq
|
||||
) {
|
||||
return !liveStream ||
|
||||
activeLiveStream !== liveStream ||
|
||||
liveStream.active !== true ||
|
||||
liveStream.detached === true ||
|
||||
liveStream.navigationSeq !== navigationSeq;
|
||||
}
|
||||
|
||||
function clearLiveChatStreamIfOwned(liveStream) {
|
||||
if (!ownsLiveChatStream(liveStream)) return false;
|
||||
liveStream.active = false;
|
||||
@@ -2208,6 +2258,8 @@ async function sendMessage() {
|
||||
const input = document.getElementById('chat-input');
|
||||
let message = input.value.trim();
|
||||
const hasAttachments = chatAttachments && chatAttachments.length > 0;
|
||||
const requestConversationId = currentConversationId;
|
||||
const requestNavigationSeq = chatConversationNavigationSeq;
|
||||
|
||||
if (!message && !hasAttachments) {
|
||||
return;
|
||||
@@ -2261,6 +2313,12 @@ async function sendMessage() {
|
||||
message = CHAT_FILE_DEFAULT_PROMPT;
|
||||
}
|
||||
|
||||
// 发送前的任务状态/附件检查可能包含异步等待。若用户已主动切换会话,
|
||||
// 保留当前页面,不再把这次尚未发出的请求写入新的可见对话。
|
||||
if (requestNavigationSeq !== chatConversationNavigationSeq) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示用户消息(含附件名,便于用户确认)
|
||||
const displayMessage = hasAttachments
|
||||
? message + '\n' + chatAttachments.map(a => '📎 ' + a.fileName).join('\n')
|
||||
@@ -2296,7 +2354,7 @@ async function sendMessage() {
|
||||
// 构建请求体(含附件)
|
||||
const body = {
|
||||
message: message,
|
||||
conversationId: currentConversationId,
|
||||
conversationId: requestConversationId,
|
||||
role: typeof getCurrentRole === 'function' ? getCurrentRole() : ''
|
||||
};
|
||||
if (window.__csNextChatFinalizationPolicy && typeof window.__csNextChatFinalizationPolicy === 'object') {
|
||||
@@ -2357,7 +2415,8 @@ async function sendMessage() {
|
||||
conversationId: streamConversationId || null,
|
||||
progressId: progressId,
|
||||
abortController: requestAbortController,
|
||||
detached: false
|
||||
detached: false,
|
||||
navigationSeq: requestNavigationSeq
|
||||
};
|
||||
window.__csAgentLiveStream = liveStreamState;
|
||||
if (streamConversationId && typeof window.notifyConversationTaskStarted === 'function') {
|
||||
@@ -2408,18 +2467,18 @@ async function sendMessage() {
|
||||
if (streamConversationId && streamConversationId !== eventConvId) {
|
||||
return;
|
||||
}
|
||||
if (!streamConversationId && eventData.type === 'conversation') {
|
||||
if (!streamConversationId) {
|
||||
streamConversationId = eventConvId;
|
||||
liveStreamState.conversationId = eventConvId;
|
||||
justBoundConversation = true;
|
||||
// 旧请求可能在用户切换对话后才收到 conversation 事件。
|
||||
// 只完成本地任务绑定,不允许它重新抢占当前对话或新的主流状态。
|
||||
if (!ownsLiveChatStream(liveStreamState) || liveStreamState.detached) {
|
||||
updateProgressConversation(progressId, eventConvId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 切换对话后仍可能收到旧响应流中已缓冲的 conversation、response_start
|
||||
// 或 response 事件。它们只能补齐后台任务归属,不能重新抢占当前对话。
|
||||
if (shouldIgnoreLiveChatStreamEvent(liveStreamState)) {
|
||||
if (eventConvId) updateProgressConversation(progressId, eventConvId);
|
||||
return;
|
||||
}
|
||||
if (!justBoundConversation && !isStreamStillVisibleForRequest()) {
|
||||
return;
|
||||
}
|
||||
@@ -5652,6 +5711,11 @@ async function startNewConversation(options = {}) {
|
||||
const requestedProjectId = hasExplicitProjectId
|
||||
? String(options.projectId || '').trim()
|
||||
: String(inheritedProjectId || '').trim();
|
||||
markChatConversationNavigation('', true);
|
||||
if (typeof window.cancelScheduledChatConversationFromHash === 'function') {
|
||||
window.cancelScheduledChatConversationFromHash();
|
||||
}
|
||||
clearChatConversationHash();
|
||||
cancelPendingConversationLoad();
|
||||
detachLiveChatStreamForNavigation('', true);
|
||||
if (typeof window.cancelRunningTaskEventStream === 'function') {
|
||||
@@ -5776,7 +5840,8 @@ function createConversationListItem(conversation) {
|
||||
item.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
loadConversation(conversation.id);
|
||||
const targetConversationId = String(item.dataset.conversationId || '').trim();
|
||||
if (targetConversationId) loadConversation(targetConversationId);
|
||||
};
|
||||
return item;
|
||||
}
|
||||
@@ -6092,16 +6157,30 @@ async function prefetchLastAssistantProcessDetails() {
|
||||
}
|
||||
|
||||
async function loadConversation(conversationId) {
|
||||
conversationId = String(conversationId || '').trim();
|
||||
if (!conversationId) return;
|
||||
// Keep the visible conversation addressable across a full page refresh.
|
||||
// Sidebar/project entries call loadConversation directly (rather than the
|
||||
// router helper), so without this synchronization #chat loses the active
|
||||
// conversation and reload falls back to the welcome screen instead of
|
||||
// reconnecting the running task event stream.
|
||||
markChatConversationNavigation(conversationId);
|
||||
if (typeof window.cancelScheduledChatConversationFromHash === 'function') {
|
||||
window.cancelScheduledChatConversationFromHash();
|
||||
}
|
||||
syncChatConversationHash(conversationId);
|
||||
const seq = ++loadConversationRequestSeq;
|
||||
const previousConversationId = currentConversationId;
|
||||
cancelPendingConversationLoad();
|
||||
detachLiveChatStreamForNavigation(conversationId);
|
||||
// 用户单击即代表新的可见会话。必须在任何网络等待之前提交该选择,
|
||||
// 否则每 2 秒的活跃任务刷新仍会把旧会话识别为可见,并排队重载旧补流,
|
||||
// 反过来取消这次切换。
|
||||
currentConversationId = conversationId;
|
||||
try {
|
||||
window.currentConversationId = conversationId;
|
||||
} catch (e) { /* ignore */ }
|
||||
loadConversationPendingId = conversationId;
|
||||
const conversationLoadController = new AbortController();
|
||||
loadConversationAbortController = conversationLoadController;
|
||||
if (typeof window.selectChatProjectConversationItem === 'function') {
|
||||
@@ -6132,6 +6211,14 @@ async function loadConversation(conversationId) {
|
||||
return;
|
||||
}
|
||||
if (response && !response.ok) {
|
||||
if (seq === loadConversationRequestSeq) {
|
||||
currentConversationId = previousConversationId;
|
||||
try {
|
||||
window.currentConversationId = previousConversationId || '';
|
||||
} catch (e) { /* ignore */ }
|
||||
if (previousConversationId) syncChatConversationHash(previousConversationId);
|
||||
else clearChatConversationHash();
|
||||
}
|
||||
showChatToast('加载对话失败: ' + (conversation.error || '未知错误'), 'error');
|
||||
return;
|
||||
}
|
||||
@@ -6418,8 +6505,16 @@ async function loadConversation(conversationId) {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error && error.name === 'AbortError') return;
|
||||
if (seq === loadConversationRequestSeq && typeof window.selectChatProjectConversationItem === 'function') {
|
||||
window.selectChatProjectConversationItem(previousConversationId);
|
||||
if (seq === loadConversationRequestSeq) {
|
||||
currentConversationId = previousConversationId;
|
||||
try {
|
||||
window.currentConversationId = previousConversationId || '';
|
||||
} catch (e) { /* ignore */ }
|
||||
if (previousConversationId) syncChatConversationHash(previousConversationId);
|
||||
else clearChatConversationHash();
|
||||
if (typeof window.selectChatProjectConversationItem === 'function') {
|
||||
window.selectChatProjectConversationItem(previousConversationId);
|
||||
}
|
||||
}
|
||||
console.error('加载对话失败:', error);
|
||||
showChatToast('加载对话失败: ' + (error && error.message ? error.message : String(error)), 'error');
|
||||
@@ -6430,6 +6525,9 @@ async function loadConversation(conversationId) {
|
||||
if (loadConversationAbortController === conversationLoadController) {
|
||||
loadConversationAbortController = null;
|
||||
}
|
||||
if (seq === loadConversationRequestSeq && loadConversationPendingId === conversationId) {
|
||||
loadConversationPendingId = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10093,7 +10191,8 @@ function createConversationListItemWithMenu(conversation, isPinned) {
|
||||
if (currentGroupId) {
|
||||
exitGroupDetail();
|
||||
}
|
||||
loadConversation(conversation.id);
|
||||
const targetConversationId = String(item.dataset.conversationId || '').trim();
|
||||
if (targetConversationId) loadConversation(targetConversationId);
|
||||
};
|
||||
|
||||
return item;
|
||||
|
||||
Reference in New Issue
Block a user