fix: keep model activity alive and switch new chats

This commit is contained in:
JOJO 2025-12-30 09:16:51 +08:00
parent 427a1f7ea8
commit 7b735e252f
3 changed files with 108 additions and 27 deletions

View File

@ -105,6 +105,16 @@ function debugLog(...args) {
} }
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 = { const appOptions = {
data() { data() {
@ -136,6 +146,8 @@ const appOptions = {
skipConversationHistoryReload: false, skipConversationHistoryReload: false,
_scrollListenerReady: false, _scrollListenerReady: false,
historyLoading: false, historyLoading: false,
historyLoadingFor: null,
historyLoadSeq: 0,
mobileViewportQuery: null, mobileViewportQuery: null,
modeMenuOpen: false, modeMenuOpen: false,
conversationListRequestSeq: 0, conversationListRequestSeq: 0,
@ -334,6 +346,14 @@ const appOptions = {
immediate: false, immediate: false,
handler(newValue, oldValue) { handler(newValue, oldValue) {
debugLog('currentConversationId 变化', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload }); 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 }); this.logMessageState('watch:currentConversationId', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload });
if (!newValue || typeof newValue !== 'string' || newValue.startsWith('temp_')) { if (!newValue || typeof newValue !== 'string' || newValue.startsWith('temp_')) {
return; return;
@ -1295,12 +1315,15 @@ const appOptions = {
this.loadingMoreConversations = false; this.loadingMoreConversations = false;
}, },
async loadConversation(conversationId) { async loadConversation(conversationId, options = {}) {
const force = Boolean(options.force);
debugLog('加载对话:', conversationId); 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('已是当前对话,跳过加载'); debugLog('已是当前对话,跳过加载');
traceLog('loadConversation:skip-same', { conversationId });
return; return;
} }
@ -1313,6 +1336,7 @@ const appOptions = {
if (result.success) { if (result.success) {
debugLog('对话加载API成功:', result); debugLog('对话加载API成功:', result);
traceLog('loadConversation:api-success', { conversationId, title: result.title });
// 2. 更新当前对话信息 // 2. 更新当前对话信息
this.skipConversationHistoryReload = true; this.skipConversationHistoryReload = true;
@ -1331,6 +1355,10 @@ const appOptions = {
await this.fetchAndDisplayHistory(); await this.fetchAndDisplayHistory();
this.fetchConversationTokenStatistics(); this.fetchConversationTokenStatistics();
this.updateCurrentContextTokens(); this.updateCurrentContextTokens();
traceLog('loadConversation:after-history', {
conversationId,
messagesLen: Array.isArray(this.messages) ? this.messages.length : 'n/a'
});
} else { } else {
console.error('对话加载失败:', result.message); console.error('对话加载失败:', result.message);
@ -1342,6 +1370,7 @@ const appOptions = {
} }
} catch (error) { } catch (error) {
console.error('加载对话异常:', error); console.error('加载对话异常:', error);
traceLog('loadConversation:error', { conversationId, error: error?.message || String(error) });
this.uiPushToast({ this.uiPushToast({
title: '加载对话异常', title: '加载对话异常',
message: error.message || String(error), message: error.message || String(error),
@ -1365,23 +1394,28 @@ const appOptions = {
// 关键功能:获取并显示历史对话内容 // 关键功能:获取并显示历史对话内容
// ========================================== // ==========================================
async fetchAndDisplayHistory() { async fetchAndDisplayHistory() {
if (this.historyLoading) { const targetConversationId = this.currentConversationId;
debugLog('历史消息正在加载,跳过重复请求'); if (!targetConversationId || targetConversationId.startsWith('temp_')) {
return;
}
this.historyLoading = true;
try {
debugLog('开始获取历史对话内容...');
this.logMessageState('fetchAndDisplayHistory:start', { conversationId: this.currentConversationId });
if (!this.currentConversationId || this.currentConversationId.startsWith('temp_')) {
debugLog('没有当前对话ID跳过历史加载'); debugLog('没有当前对话ID跳过历史加载');
return; 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 });
try { try {
// 使用专门的API获取对话消息历史 // 使用专门的API获取对话消息历史
const messagesResponse = await fetch(`/api/conversations/${this.currentConversationId}/messages`); const messagesResponse = await fetch(`/api/conversations/${targetConversationId}/messages`);
if (!messagesResponse.ok) { if (!messagesResponse.ok) {
console.warn('无法获取消息历史,尝试备用方法'); console.warn('无法获取消息历史,尝试备用方法');
@ -1401,6 +1435,12 @@ const appOptions = {
return; return;
} }
// 如果在等待期间用户已切换到其他对话,则丢弃结果
if (loadSeq !== this.historyLoadSeq || this.currentConversationId !== targetConversationId) {
debugLog('检测到对话已切换,丢弃过期的历史加载结果');
return;
}
const messagesData = await messagesResponse.json(); const messagesData = await messagesResponse.json();
debugLog('获取到消息数据:', messagesData); debugLog('获取到消息数据:', messagesData);
@ -1445,7 +1485,11 @@ const appOptions = {
this.logMessageState('fetchAndDisplayHistory:error-cleared'); this.logMessageState('fetchAndDisplayHistory:error-cleared');
} }
} finally { } finally {
// 仅在本次加载仍是最新请求时清除 loading 状态
if (loadSeq === this.historyLoadSeq) {
this.historyLoading = false; this.historyLoading = false;
this.historyLoadingFor = null;
}
} }
}, },
@ -1692,6 +1736,10 @@ const appOptions = {
async createNewConversation() { async createNewConversation() {
debugLog('创建新对话...'); debugLog('创建新对话...');
traceLog('createNewConversation:start', {
currentConversationId: this.currentConversationId,
convCount: Array.isArray(this.conversations) ? this.conversations.length : 'n/a'
});
this.logMessageState('createNewConversation:start'); this.logMessageState('createNewConversation:start');
try { try {
@ -1711,6 +1759,7 @@ const appOptions = {
if (result.success) { if (result.success) {
const newConversationId = result.conversation_id; const newConversationId = result.conversation_id;
debugLog('新对话创建成功:', newConversationId); debugLog('新对话创建成功:', newConversationId);
traceLog('createNewConversation:created', { newConversationId });
// 在本地列表插入占位,避免等待刷新 // 在本地列表插入占位,避免等待刷新
const placeholder = { 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; this.conversationsOffset = 0;
await this.loadConversationsList(); await this.loadConversationsList();
traceLog('createNewConversation:after-refresh', {
newConversationId,
conversationsLen: Array.isArray(this.conversations) ? this.conversations.length : 'n/a'
});
} else { } else {
console.error('创建对话失败:', result.message); console.error('创建对话失败:', result.message);
this.uiPushToast({ this.uiPushToast({

View File

@ -591,6 +591,7 @@ export async function initializeLegacySocket(ctx: any) {
// 监听对话变更事件 // 监听对话变更事件
ctx.socket.on('conversation_changed', (data) => { ctx.socket.on('conversation_changed', (data) => {
socketLog('对话已切换:', data); socketLog('对话已切换:', data);
console.log('[conv-trace] socket:conversation_changed', data);
ctx.currentConversationId = data.conversation_id; ctx.currentConversationId = data.conversation_id;
ctx.currentConversationTitle = data.title || ''; ctx.currentConversationTitle = data.title || '';
ctx.promoteConversationToTop(data.conversation_id); ctx.promoteConversationToTop(data.conversation_id);
@ -618,6 +619,7 @@ export async function initializeLegacySocket(ctx: any) {
return; return;
} }
const convId = data.conversation_id; const convId = data.conversation_id;
console.log('[conv-trace] socket:conversation_resolved', data);
ctx.currentConversationId = convId; ctx.currentConversationId = convId;
if (data.title) { if (data.title) {
ctx.currentConversationTitle = data.title; ctx.currentConversationTitle = data.title;

View File

@ -2278,8 +2278,29 @@ def handle_message(data):
"""发送消息到客户端""" """发送消息到客户端"""
socketio.emit(event_type, data, room=client_sid) 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 # 传递客户端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') @socketio.on('client_chunk_log')