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 @@
+