diff --git a/config/models.json b/config/models.json index 8d76a5d..5596372 100644 --- a/config/models.json +++ b/config/models.json @@ -38,15 +38,23 @@ "provider": "openai", "supportsMultimodal": false, "isReasoning": true, - "version": "不可用", + "version": "2025-01-31", "description": "OpenAI的o3-mini模型,支持图像理解和思考过程" }, - "deepseek-r1": { + "deepseek-chat": { + "name": "DeepSeek-V3", + "provider": "deepseek", + "supportsMultimodal": false, + "isReasoning": false, + "version": "2025-01", + "description": "DeepSeek最新大模型,671B MoE模型,支持60 tokens/秒的高速生成" + }, + "deepseek-reasoner": { "name": "DeepSeek-R1", "provider": "deepseek", "supportsMultimodal": false, "isReasoning": true, - "version": "不可用", + "version": "latest", "description": "DeepSeek推理模型,提供详细思考过程(仅支持文本)" } } diff --git a/models/deepseek.py b/models/deepseek.py index 046df93..bc9b7c0 100644 --- a/models/deepseek.py +++ b/models/deepseek.py @@ -1,10 +1,15 @@ import json import requests +import os from typing import Generator from openai import OpenAI from .base import BaseModel class DeepSeekModel(BaseModel): + def __init__(self, api_key: str, temperature: float = 0.7, system_prompt: str = None, language: str = None, model_name: str = "deepseek-reasoner"): + super().__init__(api_key, temperature, system_prompt, language) + self.model_name = model_name + def get_default_system_prompt(self) -> str: return """You are an expert at analyzing questions and providing detailed solutions. When presented with an image of a question: 1. First read and understand the question carefully @@ -14,6 +19,11 @@ class DeepSeekModel(BaseModel): 5. If there are multiple approaches, explain the most efficient one first""" def get_model_identifier(self) -> str: + """根据模型名称返回正确的API标识符""" + # 通过模型名称来确定实际的API调用标识符 + if self.model_name == "deepseek-chat": + return "deepseek-chat" + # deepseek-reasoner是默认的推理模型名称 return "deepseek-reasoner" def analyze_text(self, text: str, proxies: dict = None) -> Generator[dict, None, None]: @@ -22,142 +32,365 @@ class DeepSeekModel(BaseModel): # Initial status yield {"status": "started", "content": ""} - # Configure client with proxy if needed - client_args = { - "api_key": self.api_key, - "base_url": "https://api.deepseek.com" + # 保存原始环境变量 + original_env = { + 'http_proxy': os.environ.get('http_proxy'), + 'https_proxy': os.environ.get('https_proxy') } - - if proxies: - session = requests.Session() - session.proxies = proxies - client_args["http_client"] = session - client = OpenAI(**client_args) + try: + # 如果提供了代理设置,通过环境变量设置 + if proxies: + if 'http' in proxies: + os.environ['http_proxy'] = proxies['http'] + if 'https' in proxies: + os.environ['https_proxy'] = proxies['https'] - response = client.chat.completions.create( - model=self.get_model_identifier(), - messages=[{ - 'role': 'system', - 'content': self.system_prompt - }, { - 'role': 'user', - 'content': text - }], - stream=True - ) + # 初始化DeepSeek客户端,不再使用session对象 + client = OpenAI( + api_key=self.api_key, + base_url="https://api.deepseek.com" + ) - for chunk in response: - try: - if hasattr(chunk.choices[0].delta, 'reasoning_content'): - content = chunk.choices[0].delta.reasoning_content - if content: - yield { - "status": "streaming", - "content": content - } - elif hasattr(chunk.choices[0].delta, 'content'): - content = chunk.choices[0].delta.content - if content: - yield { - "status": "streaming", - "content": content - } + # 添加系统语言指令 + system_prompt = self.system_prompt + language = self.language or '中文' + if not any(phrase in system_prompt for phrase in ['Please respond in', '请用', '使用', '回答']): + system_prompt = f"{system_prompt}\n\n请务必使用{language}回答。" - except Exception as e: - print(f"Chunk processing error: {str(e)}") - continue + # 构建请求参数 + params = { + "model": self.get_model_identifier(), + "messages": [ + { + 'role': 'system', + 'content': system_prompt + }, + { + 'role': 'user', + 'content': text + } + ], + "stream": True + } + + # 只有非推理模型才设置temperature参数 + if not self.model_name.endswith('reasoner') and self.temperature is not None: + params["temperature"] = self.temperature + + print(f"调用DeepSeek API: {self.get_model_identifier()}, 是否设置温度: {not self.model_name.endswith('reasoner')}") - # Send completion status - yield { - "status": "completed", - "content": "" - } + response = client.chat.completions.create(**params) + + # 使用两个缓冲区,分别用于常规内容和思考内容 + response_buffer = "" + thinking_buffer = "" + + for chunk in response: + # 打印chunk以调试 + try: + print(f"DeepSeek API返回chunk: {chunk}") + except: + print("无法打印chunk") + + try: + # 处理推理模型的思考内容 + if hasattr(chunk.choices[0].delta, 'reasoning_content'): + content = chunk.choices[0].delta.reasoning_content + if content: + # 累积思考内容 + thinking_buffer += content + + # 只在积累一定数量的字符或遇到句子结束标记时才发送 + if len(content) >= 20 or content.endswith(('.', '!', '?', '。', '!', '?', '\n')): + yield { + "status": "thinking", + "content": thinking_buffer + } + + # 处理常规内容 + elif hasattr(chunk.choices[0].delta, 'content'): + content = chunk.choices[0].delta.content + if content: + # 累积响应内容 + response_buffer += content + print(f"累积响应内容: '{content}', 当前buffer: '{response_buffer}'") + + # 只在积累一定数量的字符或遇到句子结束标记时才发送 + if len(content) >= 10 or content.endswith(('.', '!', '?', '。', '!', '?', '\n')): + yield { + "status": "streaming", + "content": response_buffer + } + # 尝试直接从message内容获取 + elif hasattr(chunk.choices[0], 'message') and hasattr(chunk.choices[0].message, 'content'): + content = chunk.choices[0].message.content + if content: + response_buffer += content + print(f"从message获取内容: '{content}'") + yield { + "status": "streaming", + "content": response_buffer + } + # 检查是否有finish_reason,表示生成结束 + elif hasattr(chunk.choices[0], 'finish_reason') and chunk.choices[0].finish_reason: + print(f"生成结束,原因: {chunk.choices[0].finish_reason}") + + # 如果没有内容但有思考内容,把思考内容作为正文显示 + if not response_buffer and thinking_buffer: + response_buffer = thinking_buffer + yield { + "status": "streaming", + "content": response_buffer + } + except Exception as e: + print(f"解析响应chunk时出错: {str(e)}") + continue + + # 确保发送最终的缓冲内容 + if thinking_buffer: + yield { + "status": "thinking_complete", + "content": thinking_buffer + } + + # 如果推理完成后没有正文内容,则使用思考内容作为最终响应 + if not response_buffer and thinking_buffer: + response_buffer = thinking_buffer + + # 发送最终响应内容 + if response_buffer: + yield { + "status": "streaming", + "content": response_buffer + } + + # 发送完成状态 + yield { + "status": "completed", + "content": response_buffer + } + + except Exception as e: + error_msg = str(e) + print(f"DeepSeek API调用出错: {error_msg}") + + # 提供具体的错误信息 + if "invalid_api_key" in error_msg.lower(): + error_msg = "DeepSeek API密钥无效,请检查您的API密钥" + elif "rate_limit" in error_msg.lower(): + error_msg = "DeepSeek API请求频率超限,请稍后再试" + elif "quota_exceeded" in error_msg.lower(): + error_msg = "DeepSeek API配额已用完,请续费或等待下个计费周期" + + yield { + "status": "error", + "error": f"DeepSeek API错误: {error_msg}" + } + finally: + # 恢复原始环境变量 + for key, value in original_env.items(): + if value is None: + if key in os.environ: + del os.environ[key] + else: + os.environ[key] = value except Exception as e: error_msg = str(e) + print(f"调用DeepSeek模型时发生错误: {error_msg}") + if "invalid_api_key" in error_msg.lower(): - error_msg = "Invalid API key provided" + error_msg = "API密钥无效,请检查设置" elif "rate_limit" in error_msg.lower(): - error_msg = "Rate limit exceeded. Please try again later." + error_msg = "API请求频率超限,请稍后再试" yield { "status": "error", - "error": f"DeepSeek API error: {error_msg}" + "error": f"DeepSeek API错误: {error_msg}" } def analyze_image(self, image_data: str, proxies: dict = None) -> Generator[dict, None, None]: """Stream DeepSeek's response for image analysis""" try: + # 检查我们是否有支持图像的模型 + if self.model_name == "deepseek-chat" or self.model_name == "deepseek-reasoner": + yield { + "status": "error", + "error": "当前DeepSeek模型不支持图像分析,请使用Anthropic或OpenAI的多模态模型" + } + return + # Initial status yield {"status": "started", "content": ""} - # Configure client with proxy if needed - client_args = { - "api_key": self.api_key, - "base_url": "https://api.deepseek.com" + # 保存原始环境变量 + original_env = { + 'http_proxy': os.environ.get('http_proxy'), + 'https_proxy': os.environ.get('https_proxy') } - - if proxies: - session = requests.Session() - session.proxies = proxies - client_args["http_client"] = session - client = OpenAI(**client_args) + try: + # 如果提供了代理设置,通过环境变量设置 + if proxies: + if 'http' in proxies: + os.environ['http_proxy'] = proxies['http'] + if 'https' in proxies: + os.environ['https_proxy'] = proxies['https'] - # 检查系统提示词是否已包含语言设置指令 - system_prompt = self.system_prompt - language = self.language or '中文' - if not any(phrase in system_prompt for phrase in ['Please respond in', '请用', '使用', '回答']): - system_prompt = f"{system_prompt}\n\n请务必使用{language}回答,无论问题是什么语言。即使在分析图像时也请使用{language}回答。" + # 初始化DeepSeek客户端,不再使用session对象 + client = OpenAI( + api_key=self.api_key, + base_url="https://api.deepseek.com" + ) - response = client.chat.completions.create( - model=self.get_model_identifier(), - messages=[{ - 'role': 'system', - 'content': system_prompt - }, { - 'role': 'user', - 'content': f"Here's an image of a question to analyze: data:image/png;base64,{image_data}" - }], - stream=True - ) + # 检查系统提示词是否已包含语言设置指令 + system_prompt = self.system_prompt + language = self.language or '中文' + if not any(phrase in system_prompt for phrase in ['Please respond in', '请用', '使用', '回答']): + system_prompt = f"{system_prompt}\n\n请务必使用{language}回答,无论问题是什么语言。即使在分析图像时也请使用{language}回答。" - for chunk in response: - try: - if hasattr(chunk.choices[0].delta, 'reasoning_content'): - content = chunk.choices[0].delta.reasoning_content - if content: - yield { - "status": "streaming", - "content": content - } - elif hasattr(chunk.choices[0].delta, 'content'): - content = chunk.choices[0].delta.content - if content: - yield { - "status": "streaming", - "content": content - } + # 构建请求参数 + params = { + "model": self.get_model_identifier(), + "messages": [ + { + 'role': 'system', + 'content': system_prompt + }, + { + 'role': 'user', + 'content': f"Here's an image of a question to analyze: data:image/png;base64,{image_data}" + } + ], + "stream": True + } + + # 只有非推理模型才设置temperature参数 + if not self.model_name.endswith('reasoner') and self.temperature is not None: + params["temperature"] = self.temperature - except Exception as e: - print(f"Chunk processing error: {str(e)}") - continue + response = client.chat.completions.create(**params) + + # 使用两个缓冲区,分别用于常规内容和思考内容 + response_buffer = "" + thinking_buffer = "" + + for chunk in response: + # 打印chunk以调试 + try: + print(f"DeepSeek图像API返回chunk: {chunk}") + except: + print("无法打印chunk") + + try: + # 处理推理模型的思考内容 + if hasattr(chunk.choices[0].delta, 'reasoning_content'): + content = chunk.choices[0].delta.reasoning_content + if content: + # 累积思考内容 + thinking_buffer += content + + # 只在积累一定数量的字符或遇到句子结束标记时才发送 + if len(content) >= 20 or content.endswith(('.', '!', '?', '。', '!', '?', '\n')): + yield { + "status": "thinking", + "content": thinking_buffer + } + # 处理常规内容 + elif hasattr(chunk.choices[0].delta, 'content'): + content = chunk.choices[0].delta.content + if content: + # 累积响应内容 + response_buffer += content + print(f"累积图像响应内容: '{content}', 当前buffer: '{response_buffer}'") + + # 只在积累一定数量的字符或遇到句子结束标记时才发送 + if len(content) >= 10 or content.endswith(('.', '!', '?', '。', '!', '?', '\n')): + yield { + "status": "streaming", + "content": response_buffer + } + # 尝试直接从message内容获取 + elif hasattr(chunk.choices[0], 'message') and hasattr(chunk.choices[0].message, 'content'): + content = chunk.choices[0].message.content + if content: + response_buffer += content + print(f"从message获取图像内容: '{content}'") + yield { + "status": "streaming", + "content": response_buffer + } + # 检查是否有finish_reason,表示生成结束 + elif hasattr(chunk.choices[0], 'finish_reason') and chunk.choices[0].finish_reason: + print(f"图像生成结束,原因: {chunk.choices[0].finish_reason}") + + # 如果没有内容但有思考内容,把思考内容作为正文显示 + if not response_buffer and thinking_buffer: + response_buffer = thinking_buffer + yield { + "status": "streaming", + "content": response_buffer + } + except Exception as e: + print(f"解析图像响应chunk时出错: {str(e)}") + continue - # Send completion status - yield { - "status": "completed", - "content": "" - } + # 确保发送最终的缓冲内容 + if thinking_buffer: + yield { + "status": "thinking_complete", + "content": thinking_buffer + } + + # 如果推理完成后没有正文内容,则使用思考内容作为最终响应 + if not response_buffer and thinking_buffer: + response_buffer = thinking_buffer + + # 发送最终响应内容 + if response_buffer: + yield { + "status": "streaming", + "content": response_buffer + } + + # 发送完成状态 + yield { + "status": "completed", + "content": response_buffer + } + + except Exception as e: + error_msg = str(e) + print(f"DeepSeek API调用出错: {error_msg}") + + # 提供具体的错误信息 + if "invalid_api_key" in error_msg.lower(): + error_msg = "DeepSeek API密钥无效,请检查您的API密钥" + elif "rate_limit" in error_msg.lower(): + error_msg = "DeepSeek API请求频率超限,请稍后再试" + + yield { + "status": "error", + "error": f"DeepSeek API错误: {error_msg}" + } + finally: + # 恢复原始环境变量 + for key, value in original_env.items(): + if value is None: + if key in os.environ: + del os.environ[key] + else: + os.environ[key] = value except Exception as e: error_msg = str(e) if "invalid_api_key" in error_msg.lower(): - error_msg = "Invalid API key provided" + error_msg = "API密钥无效,请检查设置" elif "rate_limit" in error_msg.lower(): - error_msg = "Rate limit exceeded. Please try again later." + error_msg = "API请求频率超限,请稍后再试" yield { "status": "error", - "error": f"DeepSeek API error: {error_msg}" + "error": f"DeepSeek API错误: {error_msg}" } diff --git a/models/factory.py b/models/factory.py index 42eeb67..a459cce 100644 --- a/models/factory.py +++ b/models/factory.py @@ -126,6 +126,15 @@ class ModelFactory: temperature=temperature, system_prompt=system_prompt ) + # 对于DeepSeek模型,传递model_name参数 + elif "deepseek" in model_name.lower(): + return model_class( + api_key=api_key, + temperature=temperature, + system_prompt=system_prompt, + language=language, + model_name=model_name + ) else: # 对于其他模型,传递所有参数 return model_class( diff --git a/static/js/main.js b/static/js/main.js index cb379f7..917b240 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -698,8 +698,10 @@ class SnapSolver { this.screenshotImg.src = this.croppedImage; this.imagePreview.classList.remove('hidden'); this.cropBtn.classList.remove('hidden'); - this.sendToClaudeBtn.classList.remove('hidden'); - this.extractTextBtn.classList.remove('hidden'); + + // 根据当前选择的模型类型决定显示哪些按钮 + this.updateImageActionButtons(); + window.uiManager.showToast('Cropping successful'); } catch (error) { console.error('Cropping error details:', { @@ -725,8 +727,8 @@ class SnapSolver { this.cropper = null; } this.cropContainer.classList.add('hidden'); - this.sendToClaudeBtn.classList.add('hidden'); - this.extractTextBtn.classList.add('hidden'); + // 取消裁剪时隐藏图像预览和相关按钮 + this.imagePreview.classList.add('hidden'); document.querySelector('.crop-area').innerHTML = ''; }); } @@ -858,6 +860,17 @@ class SnapSolver { if (this.sendToClaudeBtn.disabled) return; this.sendToClaudeBtn.disabled = true; + // 获取当前模型设置 + const settings = window.settingsManager.getSettings(); + const isMultimodalModel = settings.modelInfo?.supportsMultimodal || false; + const modelName = settings.model || '未知'; + + if (!isMultimodalModel) { + window.uiManager.showToast(`当前选择的模型 ${modelName} 不支持图像分析。请先提取文本或切换到支持多模态的模型。`, 'error'); + this.sendToClaudeBtn.disabled = false; + return; + } + if (this.croppedImage) { try { // 清空之前的结果 @@ -1068,6 +1081,12 @@ class SnapSolver { // 更新图像操作按钮 this.updateImageActionButtons(); + // 确保DOM完全加载后再次更新按钮状态(以防设置未完全加载) + setTimeout(() => { + this.updateImageActionButtons(); + console.log('延时更新图像操作按钮完成'); + }, 1000); + console.log('SnapSolver initialization complete'); } @@ -1187,25 +1206,40 @@ class SnapSolver { // 新增方法:根据所选模型更新图像操作按钮 updateImageActionButtons() { - if (!window.settingsManager) return; + if (!window.settingsManager) { + console.error('Settings manager not available'); + return; + } const settings = window.settingsManager.getSettings(); const isMultimodalModel = settings.modelInfo?.supportsMultimodal || false; + const modelName = settings.model || '未知'; + + console.log(`更新图像操作按钮 - 当前模型: ${modelName}, 是否支持多模态: ${isMultimodalModel}`); // 对于截图后的操作按钮显示逻辑 if (this.sendToClaudeBtn && this.extractTextBtn) { if (!isMultimodalModel) { // 非多模态模型:只显示提取文本按钮,隐藏发送到AI按钮 + console.log('非多模态模型:隐藏"发送图片至AI"按钮'); this.sendToClaudeBtn.classList.add('hidden'); this.extractTextBtn.classList.remove('hidden'); } else { // 多模态模型:显示两个按钮 if (!this.imagePreview.classList.contains('hidden')) { // 只有在有图像时才显示按钮 + console.log('多模态模型:显示全部按钮'); this.sendToClaudeBtn.classList.remove('hidden'); this.extractTextBtn.classList.remove('hidden'); + } else { + // 无图像时隐藏所有按钮 + console.log('无图像:隐藏所有按钮'); + this.sendToClaudeBtn.classList.add('hidden'); + this.extractTextBtn.classList.add('hidden'); } } + } else { + console.warn('按钮元素不可用'); } } } diff --git a/static/js/settings.js b/static/js/settings.js index c6947a1..0ce8d1b 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -434,6 +434,11 @@ class SettingsManager { this.updateUIBasedOnModelType(); this.updateModelVersionDisplay(e.target.value); this.saveSettings(); + + // 通知应用更新图像操作按钮 + if (window.app && typeof window.app.updateImageActionButtons === 'function') { + window.app.updateImageActionButtons(); + } }); this.temperatureInput.addEventListener('input', (e) => {