// @ts-nocheck import { debugLog } from './common'; import { useModelStore } from '../../stores/model'; export const messageMethods = { async executeSystemCommand(rawCommand, options = {}) { const command = (rawCommand || '').toString().trim(); if (!command) { return { success: false, message: '命令不能为空' }; } if (!this.isConnected) { if (options.showToast !== false) { this.uiPushToast({ title: '连接不可用', message: '当前无法执行命令,请稍后重试。', type: 'error' }); } return { success: false, message: '连接不可用' }; } try { const response = await fetch('/api/commands', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command }) }); const payload = await response.json().catch(() => ({})); const result = { command: payload.command || command.replace(/^\//, ''), success: !!payload.success, message: payload.message, data: payload.data }; this.handleSystemCommandResult(result, options); return result; } catch (error) { const message = error instanceof Error ? error.message : '命令执行失败'; const result = { command: command.replace(/^\//, ''), success: false, message }; this.handleSystemCommandResult(result, options); return result; } }, handleSystemCommandResult(data, options = {}) { const showToast = options.showToast !== false; if (data.command === 'clear' && data.success) { this.logMessageState?.('command_result-clear', { data }); this.messages = []; this.logMessageState?.('command_result-cleared', { data }); this.currentMessageIndex = -1; this.chatClearExpandedBlocks(); this.resetTokenStatistics(); if (showToast) { this.uiPushToast({ title: '已清除', message: data.message || '对话已清除', type: 'success' }); } return; } if (data.command === 'status' && data.success) { this.addSystemMessage(`系统状态:\n${JSON.stringify(data.data || {}, null, 2)}`); if (showToast) { this.uiPushToast({ title: '状态已更新', message: '已获取系统状态', type: 'success' }); } return; } if (!data.success) { this.addSystemMessage(`命令失败: ${data.message || '未知错误'}`); if (showToast) { this.uiPushToast({ title: '命令执行失败', message: data.message || '请稍后重试', type: 'error' }); } return; } if (showToast) { this.uiPushToast({ title: '命令已执行', message: data.command || '完成', type: 'success' }); } }, handleSendOrStop() { if (this.compressionInProgress) { this.uiPushToast({ title: '对话自动压缩中', message: '当前不可发送/停止,请等待压缩完成', type: 'warning' }); return; } if (this.composerBusy) { this.stopTask(); } else { this.sendMessage(); } }, async sendMessage() { console.log('[DEBUG_AWAITING] ===== sendMessage ====='); if (this.compressionInProgress) { this.uiPushToast({ title: '对话自动压缩中', message: '压缩完成后才能继续发送消息', type: 'warning' }); return; } if (this.streamingUi) { return; } if (this.mediaUploading) { this.uiPushToast({ title: '上传中', message: '请等待图片/视频上传完成后再发送', type: 'info' }); return; } const text = (this.inputMessage || '').trim(); const images = Array.isArray(this.selectedImages) ? this.selectedImages.slice(0, 9) : []; const videos = Array.isArray(this.selectedVideos) ? this.selectedVideos.slice(0, 1) : []; const hasText = text.length > 0; const hasImages = images.length > 0; const hasVideos = videos.length > 0; if (!hasText && !hasImages && !hasVideos) { return; } const quotaType = this.thinkingMode ? 'thinking' : 'fast'; if (this.isQuotaExceeded(quotaType)) { this.showQuotaToast({ type: quotaType }); return; } const modelStore = useModelStore(); const currentModel = modelStore.models.find((m) => m.key === this.currentModelKey); if (hasImages && !currentModel?.supportsImage) { this.uiPushToast({ title: '当前模型不支持图片', message: '请切换到支持图片输入的模型再发送图片', type: 'error' }); return; } if (hasVideos && !currentModel?.supportsVideo) { this.uiPushToast({ title: '当前模型不支持视频', message: '请切换到支持视频输入的模型后再发送视频', type: 'error' }); return; } if (hasVideos && hasImages) { this.uiPushToast({ title: '请勿同时发送', message: '视频与图片需分开发送,每条仅包含一种媒体', type: 'warning' }); return; } if (hasVideos) { this.uiPushToast({ title: '视频处理中', message: '读取视频需要较长时间,请耐心等待', type: 'info', duration: 5000 }); } const message = text; const wasBlank = this.isConversationBlank(); if (wasBlank) { this.blankHeroExiting = true; this.blankHeroActive = true; setTimeout(() => { this.blankHeroExiting = false; this.blankHeroActive = false; }, 320); } let targetConversationId = this.currentConversationId; if (!targetConversationId) { try { const createResp = await fetch('/api/conversations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ thinking_mode: this.thinkingMode, mode: this.runMode }) }); const createResult = await createResp.json().catch(() => ({})); if (!createResp.ok || !createResult?.success || !createResult?.conversation_id) { throw new Error(createResult?.message || createResult?.error || '创建对话失败'); } targetConversationId = createResult.conversation_id; this.skipConversationHistoryReload = true; this.currentConversationId = targetConversationId; this.currentConversationTitle = '新对话'; this.conversations = [ { id: targetConversationId, title: '新对话', updated_at: new Date().toISOString(), total_messages: 0, total_tools: 0 }, ...this.conversations.filter((conv) => conv && conv.id !== targetConversationId) ]; const pathFragment = this.stripConversationPrefix(targetConversationId); history.replaceState({ conversationId: targetConversationId }, '', `/${pathFragment}`); await this.applyPendingVersioningToConversation(targetConversationId); } catch (error) { this.uiPushToast({ title: '发送失败', message: error?.message || '创建新对话失败,请重试', type: 'error' }); return; } } // 标记任务进行中,直到任务完成或用户手动停止 this.taskInProgress = true; this.chatAddUserMessage(message, images, videos); // 使用 REST API 创建任务(轮询模式) try { const { useTaskStore } = await import('../../stores/task'); const taskStore = useTaskStore(); if (typeof this.clearProcessedEvents === 'function') { this.clearProcessedEvents(); } await taskStore.createTask(message, images, videos, targetConversationId, { model_key: this.currentModelKey, run_mode: this.runMode, thinking_mode: this.thinkingMode }); debugLog('[Message] 任务已创建,开始轮询'); } catch (error) { console.error('[Message] 创建任务失败:', error); this.uiPushToast({ title: '发送失败', message: error.message || '创建任务失败,请重试', type: 'error' }); this.taskInProgress = false; return; } if (typeof this.monitorShowPendingReply === 'function') { this.monitorShowPendingReply(); } this.inputClearMessage(); this.inputClearSelectedImages(); this.inputClearSelectedVideos(); this.inputSetImagePickerOpen(false); this.inputSetVideoPickerOpen(false); this.inputSetLineCount(1); this.inputSetMultiline(false); if (hasImages) { this.conversationHasImages = true; this.conversationHasVideos = false; } if (hasVideos) { this.conversationHasVideos = true; this.conversationHasImages = false; } if (this.autoScrollEnabled) { this.scrollToBottom(); } this.autoResizeInput(); // 发送消息后延迟更新当前上下文Token(关键修复:恢复原逻辑) setTimeout(() => { if (this.currentConversationId) { this.updateCurrentContextTokens(); } }, 1000); }, // 新增:停止任务方法 async stopTask() { console.log('[DEBUG_AWAITING] ===== stopTask ====='); if (this.compressionInProgress) { this.uiPushToast({ title: '对话自动压缩中', message: '压缩进行中,当前不可停止任务', type: 'warning' }); return; } const canStop = this.composerBusy && !this.stopRequested; if (!canStop) { return; } const shouldDropToolEvents = this.streamingUi; this.stopRequested = true; this.dropToolEvents = shouldDropToolEvents; try { const { useTaskStore } = await import('../../stores/task'); const taskStore = useTaskStore(); if (taskStore.currentTaskId) { await taskStore.cancelTask(); } // 等待后端确认 await new Promise((resolve) => setTimeout(resolve, 300)); const shouldKeepBusy = Boolean(taskStore.currentTaskId) && ['running', 'pending', 'cancel_requested'].includes(String(taskStore.taskStatus)); // 清理前端状态 this.clearPendingTools('user_stop'); this.streamingMessage = false; // 若后台已回传停止事件,不要再次把输入区锁回“停止中” this.taskInProgress = shouldKeepBusy; this.forceUnlockMonitor('user_stop'); if (typeof this.clearProcessedEvents === 'function') { this.clearProcessedEvents(); } // 清理assistant消息的等待动画状态 const lastMessage = this.messages[this.messages.length - 1]; const before = { hasLastMessage: !!lastMessage, role: lastMessage?.role, awaitingFirstContent: lastMessage?.awaitingFirstContent, generatingLabel: lastMessage?.generatingLabel }; if (lastMessage && lastMessage.role === 'assistant') { lastMessage.awaitingFirstContent = false; lastMessage.generatingLabel = ''; } console.log('[DEBUG_AWAITING] stopTask 清理完成', { before, after: { awaitingFirstContent: lastMessage?.awaitingFirstContent, generatingLabel: lastMessage?.generatingLabel } }); } catch (error) { console.error('[Message] 取消任务失败:', error); const { useTaskStore } = await import('../../stores/task'); const taskStore = useTaskStore(); const shouldKeepBusy = Boolean(taskStore.currentTaskId) && ['running', 'pending', 'cancel_requested'].includes(String(taskStore.taskStatus)); // 即使失败也清理状态 this.clearPendingTools('user_stop'); this.streamingMessage = false; // 如果任务其实已结束,允许按钮恢复发送态 this.taskInProgress = shouldKeepBusy; this.forceUnlockMonitor('user_stop'); if (typeof this.clearProcessedEvents === 'function') { this.clearProcessedEvents(); } // 清理assistant消息的等待动画状态 const lastMessage = this.messages[this.messages.length - 1]; if (lastMessage && lastMessage.role === 'assistant') { lastMessage.awaitingFirstContent = false; lastMessage.generatingLabel = ''; } this.uiPushToast({ title: '停止失败', message: '任务可能仍在运行,请刷新页面', type: 'warning' }); } finally { // 确保清除 dropToolEvents 和 stopRequested 标志 this.dropToolEvents = false; this.stopRequested = false; } }, async clearChat() { const confirmed = await this.confirmAction({ title: '清除对话', message: '确定要清除所有对话记录吗?该操作不可撤销。', confirmText: '清除', cancelText: '取消' }); if (confirmed) { await this.executeSystemCommand('/clear', { showToast: false }); } }, async compressConversation() { if (!this.currentConversationId) { this.uiPushToast({ title: '无法压缩', message: '当前没有可压缩的对话。', type: 'info' }); return; } if (this.compressing) { return; } const confirmed = await this.confirmAction({ title: '压缩对话', message: '确定要压缩当前对话记录吗?压缩后会生成新的对话副本。', confirmText: '压缩', cancelText: '取消' }); if (!confirmed) { return; } this.compressing = true; this.compressionInProgress = true; this.compressionMode = 'manual'; this.compressionStage = 'requesting'; this.compressionError = ''; if (this.compressionToastId) { this.uiDismissToast(this.compressionToastId); this.compressionToastId = null; } this.compressionToastId = this.uiPushToast({ title: '压缩中', message: '对话正在压缩,请稍候…', type: 'info', duration: null, closable: false }); try { const response = await fetch(`/api/conversations/${this.currentConversationId}/compress`, { method: 'POST' }); const result = await response.json(); if (response.ok && result.success) { this.compressionStage = 'switching'; const newId = result.compressed_conversation_id; if (newId) { await this.loadConversation(newId, { force: true }); } const guideMessage = (result.guide_message || '').trim(); const autoTaskStarted = !!result.auto_task_started; const autoTaskId = result.auto_task_id; if (autoTaskStarted && autoTaskId) { const { useTaskStore } = await import('../../stores/task'); const taskStore = useTaskStore(); if (typeof this.clearProcessedEvents === 'function') { this.clearProcessedEvents(); } taskStore.resumeTask(autoTaskId, { status: result.auto_task_status || 'running', resetOffset: true, eventHandler: (event) => this.handleTaskEvent(event) }); this.taskInProgress = true; if (typeof this.monitorShowPendingReply === 'function') { this.monitorShowPendingReply(); } } else if (guideMessage) { await this.sendAutoUserMessage(guideMessage); } this.conversationsOffset = 0; await this.loadConversationsList(); debugLog('对话压缩完成:', result); this.uiPushToast({ title: '压缩完成', message: '已生成压缩后的新对话', type: 'success', duration: 2200 }); } else { const message = result.message || result.error || '压缩失败'; this.compressionError = message; this.uiPushToast({ title: '压缩失败', message, type: 'error' }); } } catch (error) { console.error('压缩对话异常:', error); this.compressionError = error.message || '请稍后重试'; this.uiPushToast({ title: '压缩对话异常', message: error.message || '请稍后重试', type: 'error' }); } finally { if (this.compressionToastId) { this.uiDismissToast(this.compressionToastId); this.compressionToastId = null; } this.compressing = false; this.compressionInProgress = false; this.compressionMode = ''; this.compressionStage = ''; } }, async sendAutoUserMessage(text) { const message = (text || '').trim(); if (!message || !this.isConnected) { return false; } const quotaType = this.thinkingMode ? 'thinking' : 'fast'; if (this.isQuotaExceeded(quotaType)) { this.showQuotaToast({ type: quotaType }); return false; } this.taskInProgress = true; this.chatAddUserMessage(message, []); try { const { useTaskStore } = await import('../../stores/task'); const taskStore = useTaskStore(); if (typeof this.clearProcessedEvents === 'function') { this.clearProcessedEvents(); } await taskStore.createTask(message, [], [], this.currentConversationId); } catch (error) { console.error('[Message] 自动消息创建任务失败:', error); this.uiPushToast({ title: '发送失败', message: error?.message || '创建任务失败,请重试', type: 'error' }); this.taskInProgress = false; return false; } if (typeof this.monitorShowPendingReply === 'function') { this.monitorShowPendingReply(); } if (this.autoScrollEnabled) { this.scrollToBottom(); } this.autoResizeInput(); setTimeout(() => { if (this.currentConversationId) { this.updateCurrentContextTokens(); } }, 1000); return true; }, autoResizeInput() { this.$nextTick(() => { const textarea = this.getComposerElement('stadiumInput'); if (!textarea || !(textarea instanceof HTMLTextAreaElement)) { return; } const previousHeight = textarea.offsetHeight; textarea.style.height = 'auto'; const computedStyle = window.getComputedStyle(textarea); const lineHeight = parseFloat(computedStyle.lineHeight || '20') || 20; const maxHeight = lineHeight * 6; const targetHeight = Math.min(textarea.scrollHeight, maxHeight); this.inputSetLineCount(Math.max(1, Math.round(targetHeight / lineHeight))); this.inputSetMultiline(targetHeight > lineHeight * 1.4); if (Math.abs(targetHeight - previousHeight) <= 0.5) { textarea.style.height = `${targetHeight}px`; return; } textarea.style.height = `${previousHeight}px`; void textarea.offsetHeight; requestAnimationFrame(() => { textarea.style.height = `${targetHeight}px`; }); }); } };