// @ts-nocheck import { debugLog, traceLog } from './common'; import { usePersonalizationStore } from '../../stores/personalization'; export const conversationMethods = { // 完整重置所有状态 resetAllStates(reason = 'unspecified', options: { preserveMonitorWindows?: boolean } = {}) { console.log('[DEBUG_AWAITING] ===== resetAllStates =====', { reason }); // 如果正在等待子智能体完成,不重置任务状态 if (this.waitingForSubAgent) { debugLog('跳过状态重置:正在等待子智能体完成', { reason }); return; } debugLog('重置所有前端状态', { reason, conversationId: this.currentConversationId }); this.logMessageState('resetAllStates:before-cleanup', { reason }); this.fileHideContextMenu(); this.monitorResetVisual({ preserveBubble: true, preservePointer: true, preserveWindows: !!options?.preserveMonitorWindows, preserveQueue: !!options?.preserveMonitorWindows }); // 重置消息和流状态 this.streamingMessage = false; this.currentMessageIndex = -1; this.stopRequested = false; this.taskInProgress = false; this.dropToolEvents = false; // 清理工具状态 this.toolResetTracking(); // 新增:将所有未完成的工具标记为已完成,并清理awaitingFirstContent状态 const assistantMsgsBefore = this.messages .filter((m) => m.role === 'assistant') .map((m) => ({ awaitingFirstContent: m.awaitingFirstContent, generatingLabel: m.generatingLabel })); this.messages.forEach((msg) => { if (msg.role === 'assistant') { // 清理等待动画状态 if (msg.awaitingFirstContent) { msg.awaitingFirstContent = false; } if (msg.generatingLabel) { msg.generatingLabel = ''; } // 清理工具状态 if (msg.actions) { msg.actions.forEach((action) => { if ( action.type === 'tool' && (action.tool.status === 'preparing' || action.tool.status === 'running') ) { action.tool.status = 'completed'; } }); } } }); const assistantMsgsAfter = this.messages .filter((m) => m.role === 'assistant') .map((m) => ({ awaitingFirstContent: m.awaitingFirstContent, generatingLabel: m.generatingLabel })); console.log('[DEBUG_AWAITING] resetAllStates 清理完成', { before: assistantMsgsBefore, after: assistantMsgsAfter }); // 清理Markdown缓存 if (this.markdownCache) { this.markdownCache.clear(); } this.chatClearThinkingLocks(); // 强制更新视图 this.$forceUpdate(); this.inputSetSettingsOpen(false); this.inputSetToolMenuOpen(false); this.inputSetQuickMenuOpen(false); this.modeMenuOpen = false; this.permissionMenuOpen = false; this.inputSetLineCount(1); this.inputSetMultiline(false); this.inputClearMessage(); this.runtimeQueuedMessages = []; this.runtimeGuidanceFallbackQueue = []; this.runtimeQueueAutoSendInProgress = false; this.runtimeQueueSyncLockKey = ''; this.runtimeQueueSyncLockUntil = 0; if (typeof this.clearRuntimeQueueSuppressionState === 'function') { this.clearRuntimeQueueSuppressionState(); } else { this.runtimeQueueSuppressedMessageIds = new Set(); this.runtimeGuidanceSuppressedTextCounts = {}; } this.composerReservedHeight = 80; this.inputClearSelectedImages(); this.inputSetImagePickerOpen(false); this.imageEntries = []; this.imageLoading = false; this.conversationHasImages = false; this.toolSetSettingsLoading(false); this.toolSetSettings([]); this.pendingToolApprovals = []; this.decidingApprovalIds = []; this.autoApprovalTitle = '自动审批记录'; // 切换对话/工作区/新建视图时,清理上一轮目标模式的本地完成提示。 // 如果目标任务仍在运行,后续 restoreTaskState 会重新恢复运行态。 this.goalModeArmed = false; this.goalRunning = false; this.goalProgress = null; this.goalDialogOpen = false; debugLog('前端状态重置完成'); this._scrollListenerReady = false; this._manualScrollSuppressUntil = 0; this._escapedByUserScroll = false; this._autoRelockCooldownUntil = 0; this.$nextTick(() => { this.ensureScrollListener(); const composerRef = typeof this.getInputComposerRef === 'function' ? this.getInputComposerRef() : null; if (composerRef && typeof composerRef.emitComposerHeight === 'function') { composerRef.emitComposerHeight(); } }); // 重置已加载对话标记,便于后续重新加载新对话历史 this.lastHistoryLoadedConversationId = null; this.logMessageState('resetAllStates:after-cleanup', { reason }); }, scheduleResetAfterTask( reason = 'unspecified', options: { preserveMonitorWindows?: boolean } = {} ) { const start = Date.now(); const maxWait = 4000; const interval = 200; const tryReset = () => { if (!this.monitorIsLocked || Date.now() - start >= maxWait) { this.resetAllStates(reason, options); return; } setTimeout(tryReset, interval); }; tryReset(); }, resetTokenStatistics() { this.resourceResetTokenStatistics(); }, // ========================================== // 对话管理核心功能 // ========================================== async loadConversationsList() { const queryOffset = this.conversationsOffset; const queryLimit = this.conversationsLimit; const refreshToken = queryOffset === 0 ? ++this.conversationListRefreshToken : this.conversationListRefreshToken; const requestSeq = ++this.conversationListRequestSeq; this.conversationsLoading = true; try { const response = await fetch(`/api/conversations?limit=${queryLimit}&offset=${queryOffset}`); const data = await response.json(); if (data.success) { if (refreshToken !== this.conversationListRefreshToken) { debugLog('忽略已过期的对话列表响应', { requestSeq, responseOffset: queryOffset }); return; } if (queryOffset === 0) { this.conversations = data.data.conversations; } else { this.conversations.push(...data.data.conversations); } if (this.currentConversationId) { this.promoteConversationToTop(this.currentConversationId); } this.hasMoreConversations = data.data.has_more; debugLog(`已加载 ${this.conversations.length} 个对话`); if ( this.conversationsOffset === 0 && !this.currentConversationId && this.conversations.length > 0 && !this.isExplicitNewConversationRoute() ) { // 只有在初始化完成后,才自动加载第一个对话 // 避免与 bootstrapRoute 冲突 if (this.initialRouteResolved) { const latestConversation = this.conversations[0]; if (latestConversation && latestConversation.id) { await this.loadConversation(latestConversation.id); } } } } else { console.error('加载对话列表失败:', data.error); } } catch (error) { console.error('加载对话列表异常:', error); } finally { if (refreshToken === this.conversationListRefreshToken) { this.conversationsLoading = false; } } }, async loadMoreConversations() { if (this.loadingMoreConversations || !this.hasMoreConversations) return; this.loadingMoreConversations = true; this.conversationsOffset += this.conversationsLimit; await this.loadConversationsList(); this.loadingMoreConversations = false; }, async loadConversation(conversationId, options = {}) { const force = Boolean(options.force); const preserveListPosition = Boolean(options.preserveListPosition); debugLog('加载对话:', conversationId); traceLog('loadConversation:start', { conversationId, currentConversationId: this.currentConversationId, force }); this.logMessageState('loadConversation:start', { conversationId, force }); this.suppressTitleTyping = true; this.titleReady = false; this.currentConversationTitle = ''; this.titleTypingText = ''; if (!force && conversationId === this.currentConversationId) { debugLog('已是当前对话,跳过加载'); traceLog('loadConversation:skip-same', { conversationId }); this.suppressTitleTyping = false; this.titleReady = true; return; } if ((this.compressionInProgress || this.compressing) && !force) { const confirmed = await this.confirmAction({ title: '压缩进行中', message: '对话正在压缩中,切换对话会导致压缩失败,确认要继续吗?', confirmText: '确认', cancelText: '取消' }); if (!confirmed) { this.suppressTitleTyping = false; this.titleReady = true; return; } try { if (this.currentConversationId) { await fetch(`/api/conversations/${this.currentConversationId}/compression_cancel`, { method: 'POST' }); } } catch (e) { console.warn('取消压缩失败:', e); } this.compressionInProgress = false; this.compressing = false; this.compressionMode = ''; this.compressionStage = ''; if (this.compressionToastId) { this.uiDismissToast(this.compressionToastId); this.compressionToastId = null; } } // 注意:加载已有对话时必须保留该对话自身的模型/模式,不能套用用户默认值。 // 多工作区并行后,切换对话只切换视图,不再取消后台任务;停止当前轮询避免事件写入新对话界面。 try { const { useTaskStore } = await import('../../stores/task'); const taskStore = useTaskStore(); if (taskStore.hasActiveTask || this.taskInProgress) { taskStore.clearTask(); if (typeof this.clearProcessedEvents === 'function') { this.clearProcessedEvents(); } } this.clearLocalTaskUiState?.(`switch-conversation:${conversationId}`); } catch (error) { console.error('[切换对话] 停止本地轮询失败:', error); } await this.persistComposerDraftNow({ reason: `switch-conversation:${conversationId}`, force: true, keepalive: true }).catch(() => {}); try { // 1. 调用加载API const response = await fetch(`/api/conversations/${conversationId}/load`, { method: 'PUT' }); const result = await response.json(); if (result.success) { debugLog('对话加载API成功:', result); traceLog('loadConversation:api-success', { conversationId, title: result.title }); if (typeof result.run_mode === 'string') { this.runMode = result.run_mode; this.thinkingMode = typeof result.thinking_mode === 'boolean' ? result.thinking_mode : result.run_mode !== 'fast'; } else if (typeof result.thinking_mode === 'boolean') { this.thinkingMode = result.thinking_mode; this.runMode = result.thinking_mode ? 'thinking' : 'fast'; } if (typeof result.model_key === 'string' && result.model_key) { this.modelSet(result.model_key); } // 2. 更新当前对话信息 this.skipConversationHistoryReload = true; this.currentConversationId = conversationId; this.refreshProjectGitSummary?.(); this.fetchTerminalCount(); this.currentConversationTitle = result.title; this.titleReady = true; this.suppressTitleTyping = false; this.startTitleTyping(this.currentConversationTitle, { animate: false }); if (!preserveListPosition) { this.promoteConversationToTop(conversationId); } history.pushState( { conversationId }, '', `/${this.stripConversationPrefix(conversationId)}` ); this.skipConversationLoadedEvent = true; // 3. 重置UI状态 this.resetAllStates(`loadConversation:${conversationId}`); this.subAgentFetch(); this.fetchTodoList(); // 4. 立即加载历史和统计,确保列表切换后界面同步更新 await this.fetchAndDisplayHistory(); await this.refreshRunningWorkspaceTasks?.(); const visibleTask = typeof this.getVisibleWorkspaceTaskForConversation === 'function' ? this.getVisibleWorkspaceTaskForConversation(conversationId) : null; const visibleTaskStatus = String(visibleTask?.status || ''); const activeStatuses = new Set(['pending', 'running', 'cancel_requested']); const terminalStatuses = new Set(['succeeded', 'failed', 'canceled']); if (visibleTask && activeStatuses.has(visibleTaskStatus)) { // 切回正在运行的对话时,必须恢复该任务的 REST 轮询和输入区忙碌态; // 否则只切换了历史视图,后续输出不会续接到当前对话,发送按钮也不会变成停止按钮。 await this.restoreTaskState?.(); } else if (visibleTask && terminalStatuses.has(visibleTaskStatus)) { // 用户已点进完成后的“待查看”对话,清除列表里的完成标记。 this.acknowledgeCompletedWorkspaceTask?.(visibleTask.task_id); } this.fetchPermissionMode(); this.fetchExecutionMode(); await this.fetchVersioningStatus(conversationId, { silent: true }); await this.maybePromptVersioningMismatch(result.versioning); this.fetchPendingToolApprovals(); this.fetchConversationTokenStatistics(); this.updateCurrentContextTokens(); traceLog('loadConversation:after-history', { conversationId, messagesLen: Array.isArray(this.messages) ? this.messages.length : 'n/a' }); } else { console.error('对话加载失败:', result.message); this.suppressTitleTyping = false; this.titleReady = true; this.uiPushToast({ title: '加载对话失败', message: result.message || '服务器未返回成功状态', type: 'error' }); } } catch (error) { console.error('加载对话异常:', error); traceLog('loadConversation:error', { conversationId, error: error?.message || String(error) }); this.suppressTitleTyping = false; this.titleReady = true; this.uiPushToast({ title: '加载对话异常', message: error.message || String(error), type: 'error' }); } }, promoteConversationToTop(conversationId) { if (!Array.isArray(this.conversations) || !conversationId) { return; } const index = this.conversations.findIndex((conv) => conv && conv.id === conversationId); if (index > 0) { const [selected] = this.conversations.splice(index, 1); this.conversations.unshift(selected); } }, async createNewConversation() { console.log('[DEBUG_AWAITING] ===== createNewConversation ====='); if (this.compressionInProgress || this.compressing) { const confirmed = await this.confirmAction({ title: '压缩进行中', message: '对话正在压缩中,切换对话会导致压缩失败,确认要继续吗?', confirmText: '确认', cancelText: '取消' }); if (!confirmed) { return; } try { if (this.currentConversationId) { await fetch(`/api/conversations/${this.currentConversationId}/compression_cancel`, { method: 'POST' }); } } catch (e) { console.warn('取消压缩失败:', e); } this.compressionInProgress = false; this.compressing = false; this.compressionMode = ''; this.compressionStage = ''; if (this.compressionToastId) { this.uiDismissToast(this.compressionToastId); this.compressionToastId = null; } } debugLog('创建新对话...'); traceLog('createNewConversation:start', { currentConversationId: this.currentConversationId, convCount: Array.isArray(this.conversations) ? this.conversations.length : 'n/a' }); this.logMessageState('createNewConversation:start'); await this.persistComposerDraftNow({ reason: 'create-new-conversation', force: true, keepalive: true }).catch(() => {}); // 应用个性化设置中的默认模型和思考模式 try { const personalizationStore = usePersonalizationStore(); if (personalizationStore.loaded) { const defaultRunMode = personalizationStore.form.default_run_mode; const defaultModel = personalizationStore.form.default_model; if (defaultRunMode) { this.runMode = defaultRunMode; debugLog('应用默认运行模式:', defaultRunMode); } if (defaultModel) { this.currentModelKey = defaultModel; debugLog('应用默认模型:', defaultModel); } // 根据默认运行模式设置思考模式 if (defaultRunMode === 'thinking') { this.thinkingMode = true; } else if (defaultRunMode === 'fast') { this.thinkingMode = false; } } } catch (error) { console.warn('应用个性化默认设置失败:', error); } // 创建新对话只切换视图,不再取消后台任务;停止当前轮询避免事件串写。 try { const { useTaskStore } = await import('../../stores/task'); const taskStore = useTaskStore(); if (taskStore.hasActiveTask || this.taskInProgress) { taskStore.clearTask(); if (typeof this.clearProcessedEvents === 'function') { this.clearProcessedEvents(); } } this.clearLocalTaskUiState?.('create-new-conversation'); } catch (error) { console.error('[创建新对话] 停止本地轮询失败:', error); } try { const response = await fetch('/api/conversations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ thinking_mode: this.thinkingMode, mode: this.runMode }) }); const result = await response.json(); if (result.success) { const newConversationId = result.conversation_id; const waitForAnimation = (durationMs) => new Promise((resolve) => { window.setTimeout(resolve, durationMs); }); debugLog('新对话创建成功:', newConversationId); traceLog('createNewConversation:created', { newConversationId }); // 在本地列表插入占位,避免等待刷新 const placeholder = { id: newConversationId, title: '新对话', updated_at: new Date().toISOString(), total_messages: 0, total_tools: 0 }; this.conversationInsertAnimations = { ...this.conversationInsertAnimations, [newConversationId]: 'create' }; this.conversationListAnimationMode = 'create'; this.conversations = [ placeholder, ...this.conversations.filter((conv) => conv && conv.id !== newConversationId) ]; window.setTimeout(() => { const rest = { ...this.conversationInsertAnimations }; delete rest[newConversationId]; this.conversationInsertAnimations = rest; this.conversationListAnimationMode = 'idle'; }, 700); await this.applyPendingVersioningToConversation(newConversationId); // 直接加载新对话,确保状态一致 // 如果 socket 事件已把 currentConversationId 设置为新ID,则强制加载一次以同步状态 await this.loadConversation(newConversationId, { force: true }); traceLog('createNewConversation:after-load', { newConversationId, currentConversationId: this.currentConversationId }); // 等待顶部插入动画完成,避免刷新列表时打断“整体下移 + 从左到右出现”。 await waitForAnimation(540); // 刷新对话列表获取最新统计 this.conversationsOffset = 0; await this.loadConversationsList(); traceLog('createNewConversation:after-refresh', { newConversationId, conversationsLen: Array.isArray(this.conversations) ? this.conversations.length : 'n/a' }); await this.refreshRunningWorkspaceTasks?.(); } else { console.error('创建对话失败:', result.message); this.uiPushToast({ title: '创建对话失败', message: result.message || '服务器未返回成功状态', type: 'error' }); } } catch (error) { console.error('创建对话异常:', error); this.uiPushToast({ title: '创建对话异常', message: error.message || String(error), type: 'error' }); } }, async deleteConversation(conversationId) { const confirmed = await this.confirmAction({ title: '删除对话', message: '确定要删除这个对话吗?删除后无法恢复。', confirmText: '删除', cancelText: '取消' }); if (!confirmed) { return; } const waitForAnimation = (durationMs) => new Promise((resolve) => { window.setTimeout(resolve, durationMs); }); // 确认弹窗自身有 250ms 退出动画;等弹窗完全消失后,再开始列表删除动画。 await waitForAnimation(270); debugLog('删除对话:', conversationId); this.logMessageState('deleteConversation:start', { conversationId }); try { const response = await fetch(`/api/conversations/${conversationId}`, { method: 'DELETE' }); const result = await response.json(); if (result.success) { debugLog('对话删除成功'); // 如果删除的是当前对话,清空界面 if (conversationId === this.currentConversationId) { this.logMessageState('deleteConversation:before-clear', { conversationId }); this.messages = []; this.logMessageState('deleteConversation:after-clear', { conversationId }); this.currentConversationId = null; this.currentConversationTitle = ''; this.resetAllStates(`deleteConversation:${conversationId}`); this.resetTokenStatistics(); history.replaceState({}, '', '/new'); } // 先给目标项加 deleting 类执行左滑动画,再从本地列表移除。 // 这比完全依赖 transition-group leave 更稳定:即使列表即将为空或分支切换,也能先播放离场动画。 this.conversationListAnimationMode = 'delete'; const deletingIds = Array.isArray(this.pendingDeletingConversationIds) ? this.pendingDeletingConversationIds : []; if (!deletingIds.includes(conversationId)) { this.pendingDeletingConversationIds = [...deletingIds, conversationId]; } await this.$nextTick(); await waitForAnimation(430); this.conversations = this.conversations.filter( (conversation) => conversation.id !== conversationId ); this.searchResults = this.searchResults.filter( (conversation) => conversation.id !== conversationId ); this.pendingDeletingConversationIds = (Array.isArray(this.pendingDeletingConversationIds) ? this.pendingDeletingConversationIds : [] ).filter((id) => id !== conversationId); // 等待“左滑离场 + 0.1s 后集体上移”动画结束,避免刷新列表打断过渡。 await waitForAnimation(180); // 刷新对话列表 this.conversationsOffset = 0; await this.loadConversationsList(); this.conversationListAnimationMode = 'idle'; } else { console.error('删除对话失败:', result.message); this.uiPushToast({ title: '删除对话失败', message: result.message || '服务器未返回成功状态', type: 'error' }); } } catch (error) { this.pendingDeletingConversationIds = (Array.isArray(this.pendingDeletingConversationIds) ? this.pendingDeletingConversationIds : [] ).filter((id) => id !== conversationId); this.conversationListAnimationMode = 'idle'; console.error('删除对话异常:', error); this.uiPushToast({ title: '删除对话异常', message: error.message || String(error), type: 'error' }); } }, async duplicateConversation(conversationId) { debugLog('复制对话:', conversationId); try { const response = await fetch(`/api/conversations/${conversationId}/duplicate`, { method: 'POST' }); const result = await response.json(); if (response.ok && result.success) { const newId = result.duplicate_conversation_id; const waitForAnimation = (durationMs) => new Promise((resolve) => { window.setTimeout(resolve, durationMs); }); if (newId) { const sourceIndex = this.conversations.findIndex( (conv) => conv && conv.id === conversationId ); const sourceConversation = sourceIndex >= 0 ? this.conversations[sourceIndex] : null; const duplicateTitle = result.load_result?.title || sourceConversation?.title || '复制的对话'; const duplicatePlaceholder = { id: newId, title: duplicateTitle, updated_at: new Date().toISOString(), total_messages: sourceConversation?.total_messages || 0, total_tools: sourceConversation?.total_tools || 0 }; this.conversationInsertAnimations = { ...this.conversationInsertAnimations, [conversationId]: 'duplicateSource', [newId]: 'duplicate' }; this.conversationListAnimationMode = 'duplicate'; const withoutDuplicate = this.conversations.filter((conv) => conv && conv.id !== newId); const insertIndex = withoutDuplicate.findIndex( (conv) => conv && conv.id === conversationId ); if (insertIndex >= 0) { this.conversations = [ ...withoutDuplicate.slice(0, insertIndex + 1), duplicatePlaceholder, ...withoutDuplicate.slice(insertIndex + 1) ]; } else { this.conversations = [duplicatePlaceholder, ...withoutDuplicate]; } window.setTimeout(() => { const rest = { ...this.conversationInsertAnimations }; delete rest[conversationId]; delete rest[newId]; this.conversationInsertAnimations = rest; this.conversationListAnimationMode = 'idle'; }, 860); // 先让“目标下方钻出 + 下方整体下移”动画播完,再加载复制出的对话。 await waitForAnimation(860); await this.loadConversation(newId, { force: true, preserveListPosition: true }); } } else { const message = result.message || result.error || '复制失败'; this.uiPushToast({ title: '复制对话失败', message, type: 'error' }); } } catch (error) { console.error('复制对话异常:', error); this.uiPushToast({ title: '复制对话异常', message: error.message || String(error), type: 'error' }); } } };