diff --git a/core/main_terminal.py b/core/main_terminal.py index 9455c49..7ca1990 100644 --- a/core/main_terminal.py +++ b/core/main_terminal.py @@ -2668,6 +2668,9 @@ class MainTerminal: # fast-only 模型强制快速模式 if profile.get("fast_only") and self.run_mode != "fast": self.set_run_mode("fast") + # Qwen-VL 不支持深度思考,自动回落到思考模式 + if model_key == "qwen3-vl-plus" and self.run_mode == "deep": + self.set_run_mode("thinking") # 如果模型支持思考,但当前 run_mode 为 thinking/deep,则保持;否则无需调整 self.api_client.start_new_task(force_deep=self.deep_thinking_mode) return self.model_key diff --git a/static/src/App.vue b/static/src/App.vue index acc2528..cfbca65 100644 --- a/static/src/App.vue +++ b/static/src/App.vue @@ -209,6 +209,7 @@ @file-selected="handleFileSelected" @pick-images="openImagePicker" @remove-image="handleRemoveImage" + @open-review="openReviewDialog" /> @@ -226,15 +227,41 @@ - + + + + + +
0); + const hasImages = Array.isArray(this.selectedImages) && this.selectedImages.length > 0; + return this.quickMenuOpen || hasText || hasImages; } }, @@ -2722,6 +2739,23 @@ const appOptions = { if (data.run_mode) { this.runMode = data.run_mode; this.thinkingMode = data.thinking_mode ?? (data.run_mode !== 'fast'); + } else { + // 前端兼容策略:根据模型特性自动调整运行模式 + if (key === 'qwen3-vl-plus') { + // Qwen-VL 不支持深度思考,若当前为 deep 则回落到思考模式 + if (this.runMode === 'deep') { + this.runMode = 'thinking'; + this.thinkingMode = true; + } else { + this.thinkingMode = this.runMode !== 'fast'; + } + } else if (key === 'qwen3-max') { + // Qwen-Max 仅快速模式 + this.runMode = 'fast'; + this.thinkingMode = false; + } else { + this.thinkingMode = this.runMode !== 'fast'; + } } this.uiPushToast({ title: '模型已切换', @@ -2859,6 +2893,183 @@ const appOptions = { this.compressConversation(); }, + openReviewDialog() { + if (!this.isConnected) { + this.uiPushToast({ + title: '无法使用', + message: '当前未连接,无法生成回顾文件', + type: 'warning' + }); + return; + } + if (!this.conversations.length && !this.conversationsLoading) { + this.loadConversationsList(); + } + const fallback = this.conversations.find((c) => c.id !== this.currentConversationId); + if (!fallback) { + this.uiPushToast({ + title: '暂无可用对话', + message: '没有可供回顾的其他对话记录', + type: 'info' + }); + return; + } + this.reviewSelectedConversationId = fallback.id; + this.reviewDialogOpen = true; + this.reviewPreviewLines = []; + this.reviewPreviewError = null; + this.reviewGeneratedPath = null; + this.loadReviewPreview(fallback.id); + this.closeQuickMenu(); + }, + + handleReviewSelect(id) { + if (id === this.currentConversationId) { + this.uiPushToast({ + title: '无法引用当前对话', + message: '请选择其他对话生成回顾', + type: 'warning' + }); + return; + } + this.reviewSelectedConversationId = id; + this.loadReviewPreview(id); + }, + + async handleConfirmReview() { + if (this.reviewSubmitting) return; + if (!this.reviewSelectedConversationId) { + this.uiPushToast({ + title: '请选择对话', + message: '请选择要生成回顾的对话记录', + type: 'info' + }); + return; + } + if (this.reviewSelectedConversationId === this.currentConversationId) { + this.uiPushToast({ + title: '无法引用当前对话', + message: '请选择其他对话生成回顾', + type: 'warning' + }); + return; + } + if (!this.currentConversationId) { + this.uiPushToast({ + title: '无法发送', + message: '当前没有活跃对话,无法自动发送提示消息', + type: 'warning' + }); + return; + } + + this.reviewSubmitting = true; + try { + const { path, char_count } = await this.generateConversationReview(this.reviewSelectedConversationId); + if (!path) { + throw new Error('未获取到生成的文件路径'); + } + const count = typeof char_count === 'number' ? char_count : 0; + this.reviewGeneratedPath = path; + const suggestion = + count && count <= 10000 + ? '建议直接完整阅读。' + : '建议使用 read 工具进行搜索或分段阅读。'; + if (this.reviewSendToModel) { + const message = `帮我继续这个任务,对话文件在 ${path},文件长 ${count || '未知'} 字符,${suggestion} 请阅读文件了解后,不要直接继续工作,而是向我汇报你的理解,然后等我做出指示。`; + const sent = this.sendAutoUserMessage(message); + if (sent) { + this.reviewDialogOpen = false; + } + } else { + this.uiPushToast({ + title: '回顾文件已生成', + message: path, + type: 'success' + }); + } + } catch (error) { + const msg = error instanceof Error ? error.message : String(error || '生成失败'); + this.uiPushToast({ + title: '生成回顾失败', + message: msg, + type: 'error' + }); + } finally { + this.reviewSubmitting = false; + } + }, + + async loadReviewPreview(conversationId) { + this.reviewPreviewLoading = true; + this.reviewPreviewError = null; + this.reviewPreviewLines = []; + try { + const resp = await fetch(`/api/conversations/${conversationId}/review_preview?limit=${this.reviewPreviewLimit}`); + const payload = await resp.json().catch(() => ({})); + if (!resp.ok || !payload?.success) { + const msg = payload?.message || payload?.error || '获取预览失败'; + throw new Error(msg); + } + this.reviewPreviewLines = payload?.data?.preview || []; + } catch (error) { + const msg = error instanceof Error ? error.message : String(error || '获取预览失败'); + this.reviewPreviewError = msg; + } finally { + this.reviewPreviewLoading = false; + } + }, + + async generateConversationReview(conversationId) { + const response = await fetch(`/api/conversations/${conversationId}/review`, { + method: 'POST' + }); + const payload = await response.json().catch(() => ({})); + if (!response.ok || !payload?.success) { + const msg = payload?.message || payload?.error || '生成失败'; + throw new Error(msg); + } + const data = payload.data || payload; + return { + path: data.path || data.file_path || data.relative_path, + char_count: data.char_count ?? data.length ?? data.size ?? 0 + }; + }, + + 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, []); + if (this.socket) { + this.socket.emit('send_message', { + message, + images: [], + conversation_id: this.currentConversationId + }); + } + 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'); @@ -3231,7 +3442,8 @@ const appOptions = { QuickMenu, InputComposer, AppShell, - ImagePicker + ImagePicker, + ConversationReviewDialog }; export default appOptions; diff --git a/static/src/components/input/InputComposer.vue b/static/src/components/input/InputComposer.vue index de68985..a2c8ffc 100644 --- a/static/src/components/input/InputComposer.vue +++ b/static/src/components/input/InputComposer.vue @@ -87,6 +87,7 @@ @toggle-focus-panel="$emit('toggle-focus-panel')" @toggle-token-panel="$emit('toggle-token-panel')" @compress-conversation="$emit('compress-conversation')" + @open-review="$emit('open-review')" />
@@ -121,7 +122,8 @@ const emit = defineEmits([ 'toggle-token-panel', 'compress-conversation', 'file-selected', - 'remove-image' + 'remove-image', + 'open-review' ]); const props = defineProps<{ diff --git a/static/src/components/input/QuickMenu.vue b/static/src/components/input/QuickMenu.vue index 2608895..979f97d 100644 --- a/static/src/components/input/QuickMenu.vue +++ b/static/src/components/input/QuickMenu.vue @@ -4,6 +4,14 @@ +