diff --git a/app.py b/app.py index f809956..5118bac 100644 --- a/app.py +++ b/app.py @@ -65,34 +65,21 @@ def handle_connect(): def handle_disconnect(): print('Client disconnected') -def create_model_instance(model_id, api_keys, settings): - """创建模型实例并配置参数""" +def create_model_instance(model_id, settings, is_reasoning=False): + """创建模型实例""" + # 提取API密钥 + api_keys = settings.get('apiKeys', {}) - # 获取模型信息 - model_info = settings.get('modelInfo', {}) - is_reasoning = model_info.get('isReasoning', False) - provider = model_info.get('provider', '').lower() - - # 确定API密钥ID + # 确定需要哪个API密钥 api_key_id = None - if provider == 'anthropic': + if "claude" in model_id.lower() or "anthropic" in model_id.lower(): api_key_id = "AnthropicApiKey" - elif provider == 'openai': + elif any(keyword in model_id.lower() for keyword in ["gpt", "openai"]): api_key_id = "OpenaiApiKey" - elif provider == 'deepseek': + elif "deepseek" in model_id.lower(): api_key_id = "DeepseekApiKey" - elif provider == 'alibaba': + elif "qvq" in model_id.lower() or "alibaba" in model_id.lower() or "qwen" in model_id.lower(): api_key_id = "AlibabaApiKey" - else: - # 根据模型名称 - if "claude" in model_id.lower(): - api_key_id = "AnthropicApiKey" - elif any(keyword in model_id.lower() for keyword in ["gpt", "openai"]): - api_key_id = "OpenaiApiKey" - elif "deepseek" in model_id.lower(): - api_key_id = "DeepseekApiKey" - elif "qvq" in model_id.lower() or "alibaba" in model_id.lower(): - api_key_id = "AlibabaApiKey" api_key = api_keys.get(api_key_id) if not api_key: @@ -110,8 +97,10 @@ def create_model_instance(model_id, api_keys, settings): language=settings.get('language', '中文') ) - # 设置最大输出Token - model_instance.max_tokens = max_tokens + # 设置最大输出Token,但不为阿里巴巴模型设置(它们有自己内部的处理逻辑) + is_alibaba_model = "qvq" in model_id.lower() or "alibaba" in model_id.lower() or "qwen" in model_id.lower() + if not is_alibaba_model: + model_instance.max_tokens = max_tokens return model_instance @@ -338,13 +327,16 @@ def handle_analyze_text(data): # 获取模型和API密钥 model_id = settings.get('model', 'claude-3-7-sonnet-20250219') - api_keys = settings.get('apiKeys', {}) if not text: socketio.emit('error', {'message': '文本内容不能为空'}) return - model_instance = create_model_instance(model_id, api_keys, settings) + # 获取模型信息,判断是否为推理模型 + model_info = settings.get('modelInfo', {}) + is_reasoning = model_info.get('isReasoning', False) + + model_instance = create_model_instance(model_id, settings, is_reasoning) # 将推理配置传递给模型 if reasoning_config: @@ -383,13 +375,16 @@ def handle_analyze_image(data): # 获取模型和API密钥 model_id = settings.get('model', 'claude-3-7-sonnet-20250219') - api_keys = settings.get('apiKeys', {}) if not image_data: socketio.emit('error', {'message': '图像数据不能为空'}) return - model_instance = create_model_instance(model_id, api_keys, settings) + # 获取模型信息,判断是否为推理模型 + model_info = settings.get('modelInfo', {}) + is_reasoning = model_info.get('isReasoning', False) + + model_instance = create_model_instance(model_id, settings, is_reasoning) # 将推理配置传递给模型 if reasoning_config: diff --git a/config/models.json b/config/models.json index 117ac05..5c04799 100644 --- a/config/models.json +++ b/config/models.json @@ -43,7 +43,7 @@ "provider": "openai", "supportsMultimodal": false, "isReasoning": true, - "version": "2025-01-31", + "version": "latest", "description": "OpenAI的o3-mini模型,支持图像理解和思考过程" }, "deepseek-chat": { @@ -51,7 +51,7 @@ "provider": "deepseek", "supportsMultimodal": false, "isReasoning": false, - "version": "2025-01", + "version": "latest", "description": "DeepSeek最新大模型,671B MoE模型,支持60 tokens/秒的高速生成" }, "deepseek-reasoner": { @@ -69,6 +69,14 @@ "isReasoning": true, "version": "2025-03-25", "description": "阿里巴巴通义千问-QVQ-Max版本,支持图像理解和思考过程" + }, + "qwen-vl-max-latest": { + "name": "Qwen-VL-MAX", + "provider": "alibaba", + "supportsMultimodal": true, + "isReasoning": false, + "version": "latest", + "description": "阿里通义千问VL-MAX模型,视觉理解能力最强,支持图像理解和复杂任务" } } } \ No newline at end of file diff --git a/models/alibaba.py b/models/alibaba.py index 0e27d7d..d214b12 100644 --- a/models/alibaba.py +++ b/models/alibaba.py @@ -4,15 +4,54 @@ from openai import OpenAI from .base import BaseModel class AlibabaModel(BaseModel): + def __init__(self, api_key: str, temperature: float = 0.7, system_prompt: str = None, language: str = None, model_name: str = None): + self.model_name = model_name or "QVQ-Max-2025-03-25" # 默认使用QVQ-Max模型 + # 在super().__init__之前设置model_name,这样get_default_system_prompt能使用它 + super().__init__(api_key, temperature, system_prompt, language) + def get_default_system_prompt(self) -> str: - return """你是一位专业的问题分析与解答助手。当看到一个问题图片时,请: -1. 仔细阅读并理解问题 -2. 分析问题的关键组成部分 -3. 提供清晰的、逐步的解决方案 -4. 如果相关,解释涉及的概念或理论 -5. 如果有多种解决方法,先解释最高效的方法""" + """根据模型名称返回不同的默认系统提示词""" + # 检查是否是通义千问VL模型 + if self.model_name and "qwen-vl" in self.model_name: + return """你是通义千问VL视觉语言助手,擅长图像理解、文字识别、内容分析和创作。请根据用户提供的图像: + 1. 仔细阅读并理解问题 + 2. 分析问题的关键组成部分 + 3. 提供清晰的、逐步的解决方案 + 4. 如果相关,解释涉及的概念或理论 + 5. 如果有多种解决方法,先解释最高效的方法""" + else: + # QVQ模型使用原先的提示词 + return """你是一位专业的问题分析与解答助手。当看到一个问题图片时,请: + 1. 仔细阅读并理解问题 + 2. 分析问题的关键组成部分 + 3. 提供清晰的、逐步的解决方案 + 4. 如果相关,解释涉及的概念或理论 + 5. 如果有多种解决方法,先解释最高效的方法""" def get_model_identifier(self) -> str: + """根据模型名称返回对应的模型标识符""" + # 直接映射模型ID到DashScope API使用的标识符 + model_mapping = { + "QVQ-Max-2025-03-25": "qvq-max", + "qwen-vl-max-latest": "qwen-vl-max", # 修正为正确的API标识符 + } + + # 从模型映射表中获取模型标识符,如果不存在则使用默认值 + model_id = model_mapping.get(self.model_name) + if model_id: + return model_id + + # 如果没有精确匹配,检查是否包含特定前缀 + if self.model_name and "qwen-vl" in self.model_name: + if "max" in self.model_name: + return "qwen-vl-max" + elif "plus" in self.model_name: + return "qwen-vl-plus" + elif "lite" in self.model_name: + return "qwen-vl-lite" + return "qwen-vl-max" # 默认使用最强版本 + + # 最后的默认值 return "qvq-max" def analyze_text(self, text: str, proxies: dict = None) -> Generator[dict, None, None]: @@ -59,7 +98,7 @@ class AlibabaModel(BaseModel): messages=messages, temperature=self.temperature, stream=True, - max_tokens=self.max_tokens if hasattr(self, 'max_tokens') and self.max_tokens else 4000 + max_tokens=self._get_max_tokens() ) # 记录思考过程和回答 @@ -67,14 +106,17 @@ class AlibabaModel(BaseModel): answer_content = "" is_answering = False + # 检查是否为通义千问VL模型(不支持reasoning_content) + is_qwen_vl = "qwen-vl" in self.get_model_identifier() + for chunk in response: if not chunk.choices: continue delta = chunk.choices[0].delta - # 处理思考过程 - if hasattr(delta, 'reasoning_content') and delta.reasoning_content is not None: + # 处理思考过程(仅适用于QVQ模型) + if not is_qwen_vl and hasattr(delta, 'reasoning_content') and delta.reasoning_content is not None: reasoning_content += delta.reasoning_content # 思考过程作为一个独立的内容发送 yield { @@ -84,7 +126,7 @@ class AlibabaModel(BaseModel): } elif delta.content != "": # 判断是否开始回答(从思考过程切换到回答) - if not is_answering: + if not is_answering and not is_qwen_vl: is_answering = True # 发送完整的思考过程 if reasoning_content: @@ -126,7 +168,7 @@ class AlibabaModel(BaseModel): } def analyze_image(self, image_data: str, proxies: dict = None) -> Generator[dict, None, None]: - """Stream QVQ-Max's response for image analysis""" + """Stream model's response for image analysis""" try: # Initial status yield {"status": "started", "content": ""} @@ -186,7 +228,7 @@ class AlibabaModel(BaseModel): messages=messages, temperature=self.temperature, stream=True, - max_tokens=self.max_tokens if hasattr(self, 'max_tokens') and self.max_tokens else 4000 + max_tokens=self._get_max_tokens() ) # 记录思考过程和回答 @@ -194,14 +236,17 @@ class AlibabaModel(BaseModel): answer_content = "" is_answering = False + # 检查是否为通义千问VL模型(不支持reasoning_content) + is_qwen_vl = "qwen-vl" in self.get_model_identifier() + for chunk in response: if not chunk.choices: continue delta = chunk.choices[0].delta - # 处理思考过程 - if hasattr(delta, 'reasoning_content') and delta.reasoning_content is not None: + # 处理思考过程(仅适用于QVQ模型) + if not is_qwen_vl and hasattr(delta, 'reasoning_content') and delta.reasoning_content is not None: reasoning_content += delta.reasoning_content # 思考过程作为一个独立的内容发送 yield { @@ -211,7 +256,7 @@ class AlibabaModel(BaseModel): } elif delta.content != "": # 判断是否开始回答(从思考过程切换到回答) - if not is_answering: + if not is_answering and not is_qwen_vl: is_answering = True # 发送完整的思考过程 if reasoning_content: @@ -250,4 +295,12 @@ class AlibabaModel(BaseModel): yield { "status": "error", "error": str(e) - } \ No newline at end of file + } + + def _get_max_tokens(self) -> int: + """根据模型类型返回合适的max_tokens值""" + # 检查是否为通义千问VL模型 + if "qwen-vl" in self.get_model_identifier(): + return 2000 # 通义千问VL模型最大支持2048,留一些余量 + # QVQ模型或其他模型 + return self.max_tokens if hasattr(self, 'max_tokens') and self.max_tokens else 4000 \ No newline at end of file diff --git a/models/factory.py b/models/factory.py index a459cce..38b34b8 100644 --- a/models/factory.py +++ b/models/factory.py @@ -126,8 +126,8 @@ class ModelFactory: temperature=temperature, system_prompt=system_prompt ) - # 对于DeepSeek模型,传递model_name参数 - elif "deepseek" in model_name.lower(): + else: + # 对于所有其他模型,传递model_name参数 return model_class( api_key=api_key, temperature=temperature, @@ -135,14 +135,6 @@ class ModelFactory: language=language, model_name=model_name ) - else: - # 对于其他模型,传递所有参数 - return model_class( - api_key=api_key, - temperature=temperature, - system_prompt=system_prompt, - language=language - ) @classmethod def get_available_models(cls) -> list[Dict[str, Any]]: diff --git a/static/js/main.js b/static/js/main.js index 440bdef..91ddd38 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -4,7 +4,12 @@ class SnapSolver { // 初始化属性 this.socket = null; + this.socketId = null; + this.isProcessing = false; this.cropper = null; + this.autoScrollInterval = null; + this.capturedImage = null; // 存储截图的base64数据 + this.userThinkingExpanded = false; // 用户思考过程展开状态偏好 this.originalImage = null; this.croppedImage = null; this.extractedContent = ''; @@ -384,14 +389,21 @@ class SnapSolver { // 添加打字动画效果 this.thinkingContent.classList.add('thinking-typing'); - // 默认为折叠状态 - this.thinkingContent.classList.add('collapsed'); + // 根据用户偏好设置展开/折叠状态 this.thinkingContent.classList.remove('expanded'); + this.thinkingContent.classList.remove('collapsed'); + + if (this.userThinkingExpanded) { + this.thinkingContent.classList.add('expanded'); + } else { + this.thinkingContent.classList.add('collapsed'); + } // 更新切换按钮图标 const toggleIcon = document.querySelector('#thinkingToggle .toggle-btn i'); if (toggleIcon) { - toggleIcon.className = 'fas fa-chevron-down'; + toggleIcon.className = this.userThinkingExpanded ? + 'fas fa-chevron-up' : 'fas fa-chevron-down'; } } break; @@ -411,14 +423,21 @@ class SnapSolver { // 添加打字动画效果 this.thinkingContent.classList.add('thinking-typing'); - // 默认为折叠状态 - this.thinkingContent.classList.add('collapsed'); + // 根据用户偏好设置展开/折叠状态 this.thinkingContent.classList.remove('expanded'); + this.thinkingContent.classList.remove('collapsed'); + + if (this.userThinkingExpanded) { + this.thinkingContent.classList.add('expanded'); + } else { + this.thinkingContent.classList.add('collapsed'); + } // 更新切换按钮图标 const toggleIcon = document.querySelector('#thinkingToggle .toggle-btn i'); if (toggleIcon) { - toggleIcon.className = 'fas fa-chevron-down'; + toggleIcon.className = this.userThinkingExpanded ? + 'fas fa-chevron-up' : 'fas fa-chevron-down'; } } break; @@ -438,10 +457,21 @@ class SnapSolver { // 移除打字动画 this.thinkingContent.classList.remove('thinking-typing'); + // 根据用户偏好设置展开/折叠状态 + this.thinkingContent.classList.remove('expanded'); + this.thinkingContent.classList.remove('collapsed'); + + if (this.userThinkingExpanded) { + this.thinkingContent.classList.add('expanded'); + } else { + this.thinkingContent.classList.add('collapsed'); + } + // 确保图标正确显示 const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i'); if (toggleIcon) { - toggleIcon.className = 'fas fa-chevron-down'; + toggleIcon.className = this.userThinkingExpanded ? + 'fas fa-chevron-up' : 'fas fa-chevron-down'; } } break; @@ -460,6 +490,23 @@ class SnapSolver { // 移除打字动画 this.thinkingContent.classList.remove('thinking-typing'); + + // 根据用户偏好设置展开/折叠状态 + this.thinkingContent.classList.remove('expanded'); + this.thinkingContent.classList.remove('collapsed'); + + if (this.userThinkingExpanded) { + this.thinkingContent.classList.add('expanded'); + } else { + this.thinkingContent.classList.add('collapsed'); + } + + // 确保图标正确显示 + const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i'); + if (toggleIcon) { + toggleIcon.className = this.userThinkingExpanded ? + 'fas fa-chevron-up' : 'fas fa-chevron-down'; + } } break; @@ -502,11 +549,21 @@ class SnapSolver { if (hasThinkingContent) { // 有思考内容,显示思考组件 this.thinkingSection.classList.remove('hidden'); + + // 根据用户偏好设置展开/折叠状态 this.thinkingContent.classList.remove('expanded'); - this.thinkingContent.classList.add('collapsed'); + this.thinkingContent.classList.remove('collapsed'); + + if (this.userThinkingExpanded) { + this.thinkingContent.classList.add('expanded'); + } else { + this.thinkingContent.classList.add('collapsed'); + } + const toggleBtn = document.querySelector('#thinkingToggle .toggle-btn i'); if (toggleBtn) { - toggleBtn.className = 'fas fa-chevron-down'; + toggleBtn.className = this.userThinkingExpanded ? + 'fas fa-chevron-up' : 'fas fa-chevron-down'; } // 简化提示信息 @@ -580,16 +637,20 @@ class SnapSolver { // 使用setElementContent方法处理Markdown this.setElementContent(this.thinkingContent, data.thinking); - // 记住用户的展开/折叠状态 - const wasExpanded = this.thinkingContent.classList.contains('expanded'); + // 根据用户偏好设置展开/折叠状态 + this.thinkingContent.classList.remove('expanded'); + this.thinkingContent.classList.remove('collapsed'); - // 如果之前没有设置状态,默认为折叠 - if (!wasExpanded && !this.thinkingContent.classList.contains('collapsed')) { + if (this.userThinkingExpanded) { + this.thinkingContent.classList.add('expanded'); + } else { this.thinkingContent.classList.add('collapsed'); - const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i'); - if (toggleIcon) { - toggleIcon.className = 'fas fa-chevron-down'; - } + } + + const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i'); + if (toggleIcon) { + toggleIcon.className = this.userThinkingExpanded ? + 'fas fa-chevron-up' : 'fas fa-chevron-down'; } }); @@ -604,10 +665,21 @@ class SnapSolver { // 使用setElementContent方法处理Markdown this.setElementContent(this.thinkingContent, data.thinking); + // 根据用户偏好设置展开/折叠状态 + this.thinkingContent.classList.remove('expanded'); + this.thinkingContent.classList.remove('collapsed'); + + if (this.userThinkingExpanded) { + this.thinkingContent.classList.add('expanded'); + } else { + this.thinkingContent.classList.add('collapsed'); + } + // 确保图标正确显示 const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i'); if (toggleIcon) { - toggleIcon.className = 'fas fa-chevron-down'; + toggleIcon.className = this.userThinkingExpanded ? + 'fas fa-chevron-up' : 'fas fa-chevron-down'; } }); @@ -633,12 +705,20 @@ class SnapSolver { // 使用setElementContent方法处理Markdown this.setElementContent(this.thinkingContent, data.thinking); - // 确保初始状态为折叠 + // 根据用户偏好设置展开/折叠状态 this.thinkingContent.classList.remove('expanded'); - this.thinkingContent.classList.add('collapsed'); + this.thinkingContent.classList.remove('collapsed'); + + if (this.userThinkingExpanded) { + this.thinkingContent.classList.add('expanded'); + } else { + this.thinkingContent.classList.add('collapsed'); + } + const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i'); if (toggleIcon) { - toggleIcon.className = 'fas fa-chevron-down'; + toggleIcon.className = this.userThinkingExpanded ? + 'fas fa-chevron-up' : 'fas fa-chevron-down'; } // 弹出提示 @@ -1079,6 +1159,8 @@ class SnapSolver { if (toggleIcon) { toggleIcon.className = 'fas fa-chevron-down'; } + // 更新用户偏好 + this.userThinkingExpanded = false; } else { console.log('展开思考内容'); // 添加展开状态 @@ -1086,6 +1168,8 @@ class SnapSolver { if (toggleIcon) { toggleIcon.className = 'fas fa-chevron-up'; } + // 更新用户偏好 + this.userThinkingExpanded = true; // 当展开思考内容时,确保代码高亮生效 if (window.hljs) { diff --git a/static/js/settings.js b/static/js/settings.js index b1e5fce..c2f0ee0 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -393,6 +393,15 @@ class SettingsManager { this.reasoningDepthSelect.value === 'extended'; this.thinkBudgetGroup.style.display = showThinkBudget ? 'block' : 'none'; } + + // 控制最大Token设置的显示 + // 阿里巴巴模型不支持自定义Token设置 + const maxTokensGroup = this.maxTokensInput ? this.maxTokensInput.closest('.setting-group') : null; + if (maxTokensGroup) { + // 如果是阿里巴巴模型,隐藏Token设置 + const isAlibabaModel = modelInfo.provider === 'alibaba'; + maxTokensGroup.style.display = isAlibabaModel ? 'none' : 'block'; + } } saveSettings() {