From 7b735e252ffb5bc23a2e836288a0355b8f7a9397 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Tue, 30 Dec 2025 09:16:51 +0800 Subject: [PATCH] fix: keep model activity alive and switch new chats --- static/src/app.ts | 110 +++++++++++++++++----- static/src/composables/useLegacySocket.ts | 2 + web_server.py | 23 ++++- 3 files changed, 108 insertions(+), 27 deletions(-) diff --git a/static/src/app.ts b/static/src/app.ts index f5633fb..670f31f 100644 --- a/static/src/app.ts +++ b/static/src/app.ts @@ -105,6 +105,16 @@ function debugLog(...args) { } debugLog(...args); } +// 临时排查对话切换问题的调试输出 +const TRACE_CONV = true; +const traceLog = (...args) => { + if (!TRACE_CONV) return; + try { + console.log('[conv-trace]', ...args); + } catch (e) { + // ignore + } +}; const appOptions = { data() { @@ -136,6 +146,8 @@ const appOptions = { skipConversationHistoryReload: false, _scrollListenerReady: false, historyLoading: false, + historyLoadingFor: null, + historyLoadSeq: 0, mobileViewportQuery: null, modeMenuOpen: false, conversationListRequestSeq: 0, @@ -326,19 +338,27 @@ const appOptions = { } }, - watch: { - inputMessage() { - this.autoResizeInput(); - }, - currentConversationId: { - immediate: false, - handler(newValue, oldValue) { - debugLog('currentConversationId 变化', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload }); - this.logMessageState('watch:currentConversationId', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload }); - if (!newValue || typeof newValue !== 'string' || newValue.startsWith('temp_')) { - return; - } - if (this.skipConversationHistoryReload) { + watch: { + inputMessage() { + this.autoResizeInput(); + }, + currentConversationId: { + immediate: false, + handler(newValue, oldValue) { + debugLog('currentConversationId 变化', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload }); + traceLog('watch:currentConversationId', { + oldValue, + newValue, + skipConversationHistoryReload: this.skipConversationHistoryReload, + historyLoading: this.historyLoading, + historyLoadingFor: this.historyLoadingFor, + historyLoadSeq: this.historyLoadSeq + }); + this.logMessageState('watch:currentConversationId', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload }); + if (!newValue || typeof newValue !== 'string' || newValue.startsWith('temp_')) { + return; + } + if (this.skipConversationHistoryReload) { this.skipConversationHistoryReload = false; return; } @@ -1295,12 +1315,15 @@ const appOptions = { this.loadingMoreConversations = false; }, - async loadConversation(conversationId) { + async loadConversation(conversationId, options = {}) { + const force = Boolean(options.force); debugLog('加载对话:', conversationId); - this.logMessageState('loadConversation:start', { conversationId }); + traceLog('loadConversation:start', { conversationId, currentConversationId: this.currentConversationId, force }); + this.logMessageState('loadConversation:start', { conversationId, force }); - if (conversationId === this.currentConversationId) { + if (!force && conversationId === this.currentConversationId) { debugLog('已是当前对话,跳过加载'); + traceLog('loadConversation:skip-same', { conversationId }); return; } @@ -1313,6 +1336,7 @@ const appOptions = { if (result.success) { debugLog('对话加载API成功:', result); + traceLog('loadConversation:api-success', { conversationId, title: result.title }); // 2. 更新当前对话信息 this.skipConversationHistoryReload = true; @@ -1331,6 +1355,10 @@ const appOptions = { await this.fetchAndDisplayHistory(); this.fetchConversationTokenStatistics(); this.updateCurrentContextTokens(); + traceLog('loadConversation:after-history', { + conversationId, + messagesLen: Array.isArray(this.messages) ? this.messages.length : 'n/a' + }); } else { console.error('对话加载失败:', result.message); @@ -1342,6 +1370,7 @@ const appOptions = { } } catch (error) { console.error('加载对话异常:', error); + traceLog('loadConversation:error', { conversationId, error: error?.message || String(error) }); this.uiPushToast({ title: '加载对话异常', message: error.message || String(error), @@ -1365,23 +1394,28 @@ const appOptions = { // 关键功能:获取并显示历史对话内容 // ========================================== async fetchAndDisplayHistory() { - if (this.historyLoading) { - debugLog('历史消息正在加载,跳过重复请求'); + const targetConversationId = this.currentConversationId; + if (!targetConversationId || targetConversationId.startsWith('temp_')) { + debugLog('没有当前对话ID,跳过历史加载'); return; } + + // 若同一对话正在加载,直接复用;若是切换对话则允许并发但后来的请求会赢 + if (this.historyLoading && this.historyLoadingFor === targetConversationId) { + debugLog('同一对话历史正在加载,跳过重复请求'); + return; + } + + const loadSeq = ++this.historyLoadSeq; this.historyLoading = true; + this.historyLoadingFor = targetConversationId; try { debugLog('开始获取历史对话内容...'); this.logMessageState('fetchAndDisplayHistory:start', { conversationId: this.currentConversationId }); - if (!this.currentConversationId || this.currentConversationId.startsWith('temp_')) { - debugLog('没有当前对话ID,跳过历史加载'); - return; - } - try { // 使用专门的API获取对话消息历史 - const messagesResponse = await fetch(`/api/conversations/${this.currentConversationId}/messages`); + const messagesResponse = await fetch(`/api/conversations/${targetConversationId}/messages`); if (!messagesResponse.ok) { console.warn('无法获取消息历史,尝试备用方法'); @@ -1401,6 +1435,12 @@ const appOptions = { return; } + // 如果在等待期间用户已切换到其他对话,则丢弃结果 + if (loadSeq !== this.historyLoadSeq || this.currentConversationId !== targetConversationId) { + debugLog('检测到对话已切换,丢弃过期的历史加载结果'); + return; + } + const messagesData = await messagesResponse.json(); debugLog('获取到消息数据:', messagesData); @@ -1445,7 +1485,11 @@ const appOptions = { this.logMessageState('fetchAndDisplayHistory:error-cleared'); } } finally { - this.historyLoading = false; + // 仅在本次加载仍是最新请求时清除 loading 状态 + if (loadSeq === this.historyLoadSeq) { + this.historyLoading = false; + this.historyLoadingFor = null; + } } }, @@ -1692,6 +1736,10 @@ const appOptions = { async createNewConversation() { debugLog('创建新对话...'); + traceLog('createNewConversation:start', { + currentConversationId: this.currentConversationId, + convCount: Array.isArray(this.conversations) ? this.conversations.length : 'n/a' + }); this.logMessageState('createNewConversation:start'); try { @@ -1711,6 +1759,7 @@ const appOptions = { if (result.success) { const newConversationId = result.conversation_id; debugLog('新对话创建成功:', newConversationId); + traceLog('createNewConversation:created', { newConversationId }); // 在本地列表插入占位,避免等待刷新 const placeholder = { @@ -1726,11 +1775,20 @@ const appOptions = { ]; // 直接加载新对话,确保状态一致 - await this.loadConversation(newConversationId); + // 如果 socket 事件已把 currentConversationId 设置为新ID,则强制加载一次以同步状态 + await this.loadConversation(newConversationId, { force: true }); + traceLog('createNewConversation:after-load', { + newConversationId, + currentConversationId: this.currentConversationId + }); // 刷新对话列表获取最新统计 this.conversationsOffset = 0; await this.loadConversationsList(); + traceLog('createNewConversation:after-refresh', { + newConversationId, + conversationsLen: Array.isArray(this.conversations) ? this.conversations.length : 'n/a' + }); } else { console.error('创建对话失败:', result.message); this.uiPushToast({ diff --git a/static/src/composables/useLegacySocket.ts b/static/src/composables/useLegacySocket.ts index 302c7e7..4f7f1af 100644 --- a/static/src/composables/useLegacySocket.ts +++ b/static/src/composables/useLegacySocket.ts @@ -591,6 +591,7 @@ export async function initializeLegacySocket(ctx: any) { // 监听对话变更事件 ctx.socket.on('conversation_changed', (data) => { socketLog('对话已切换:', data); + console.log('[conv-trace] socket:conversation_changed', data); ctx.currentConversationId = data.conversation_id; ctx.currentConversationTitle = data.title || ''; ctx.promoteConversationToTop(data.conversation_id); @@ -618,6 +619,7 @@ export async function initializeLegacySocket(ctx: any) { return; } const convId = data.conversation_id; + console.log('[conv-trace] socket:conversation_resolved', data); ctx.currentConversationId = convId; if (data.title) { ctx.currentConversationTitle = data.title; diff --git a/web_server.py b/web_server.py index 0d9e8c6..b0fdc16 100644 --- a/web_server.py +++ b/web_server.py @@ -2278,8 +2278,29 @@ def handle_message(data): """发送消息到客户端""" socketio.emit(event_type, data, room=client_sid) + # 模型活动事件:用于刷新“在线”心跳(回复/工具调用都算活动) + activity_events = { + 'ai_message_start', 'thinking_start', 'thinking_chunk', 'thinking_end', + 'text_start', 'text_chunk', 'text_end', + 'tool_hint', 'tool_preparing', 'tool_start', 'update_action', + 'append_payload', 'modify_payload', 'system_message', + 'task_complete' + } + last_model_activity = 0.0 + + def send_with_activity(event_type, data): + """模型产生输出或调用工具时刷新活跃时间,防止长回复被误判下线。""" + nonlocal last_model_activity + if event_type in activity_events: + now = time.time() + # 轻量节流:1 秒内多次事件只记一次 + if now - last_model_activity >= 1.0: + record_user_activity(username) + last_model_activity = now + send_to_client(event_type, data) + # 传递客户端ID - socketio.start_background_task(process_message_task, terminal, message, send_to_client, client_sid) + socketio.start_background_task(process_message_task, terminal, message, send_with_activity, client_sid) @socketio.on('client_chunk_log')