diff --git a/app.py b/app.py index c0d6084..1ce3097 100644 --- a/app.py +++ b/app.py @@ -98,6 +98,8 @@ def create_model_instance(model_id, settings, is_reasoning=False): api_key_id = "DeepseekApiKey" elif "qvq" in model_id.lower() or "alibaba" in model_id.lower() or "qwen" in model_id.lower(): api_key_id = "AlibabaApiKey" + elif "gemini" in model_id.lower() or "google" in model_id.lower(): + api_key_id = "GoogleApiKey" # 首先尝试从本地配置获取API密钥 api_key = get_api_key(api_key_id) @@ -758,12 +760,13 @@ def load_api_keys(): else: # 如果文件不存在,创建默认配置 default_keys = { - "AnthropicApiKey": "", - "OpenaiApiKey": "", - "DeepseekApiKey": "", - "AlibabaApiKey": "", - "MathpixAppId": "", - "MathpixAppKey": "" + "AnthropicApiKey": "", + "OpenaiApiKey": "", + "DeepseekApiKey": "", + "AlibabaApiKey": "", + "MathpixAppId": "", + "MathpixAppKey": "", + "GoogleApiKey": "" } save_api_keys(default_keys) return default_keys diff --git a/config/models.json b/config/models.json index 5c04799..2b1b7f4 100644 --- a/config/models.json +++ b/config/models.json @@ -19,6 +19,11 @@ "name": "Alibaba", "api_key_id": "AlibabaApiKey", "class_name": "AlibabaModel" + }, + "google": { + "name": "Google", + "api_key_id": "GoogleApiKey", + "class_name": "GoogleModel" } }, "models": { @@ -77,6 +82,22 @@ "isReasoning": false, "version": "latest", "description": "阿里通义千问VL-MAX模型,视觉理解能力最强,支持图像理解和复杂任务" + }, + "gemini-2.5-pro-preview-03-25": { + "name": "Gemini 2.5 Pro", + "provider": "google", + "supportsMultimodal": true, + "isReasoning": true, + "version": "preview-03-25", + "description": "Google最强大的Gemini 2.5 Pro模型,支持图像理解" + }, + "gemini-2.0-flash": { + "name": "Gemini 2.0 Flash", + "provider": "google", + "supportsMultimodal": true, + "isReasoning": false, + "version": "latest", + "description": "Google更快速的Gemini 2.0 Flash模型,支持图像理解,响应更迅速" } } } \ No newline at end of file diff --git a/config/prompts.json b/config/prompts.json index 717d393..21465ef 100644 --- a/config/prompts.json +++ b/config/prompts.json @@ -1,5 +1,5 @@ { - "default": { + "a_default": { "name": "默认提示词", "content": "您是一位专业的问题解决专家。请逐步分析问题,找出问题所在,并提供详细的解决方案。始终使用用户偏好的语言回答。", "description": "通用问题解决提示词" @@ -14,7 +14,7 @@ "content": "您是一位专业的多选题解析专家。当看到一个多选题时,请:\n1. 仔细阅读题目要求和所有选项\n2. 逐一分析每个选项的正确性\n3. 明确列出所有正确选项\n4. 详细解释每个正确选项的理由\n5. 说明错误选项的问题所在\n6. 归纳总结相关知识点", "description": "专为多选题分析设计的提示词" }, - "acm_programming": { + "programming": { "name": "ACM编程题提示词", "content": "您是一位专业的ACM编程竞赛解题专家。当看到一个编程题时,请:\n1. 分析题目要求、输入输出格式和约束条件\n2. 确定解题思路和算法策略\n3. 分析算法复杂度\n4. 提供完整、可运行的代码实现\n5. 解释代码中的关键部分\n6. 提供一些测试用例及其输出\n7. 讨论可能的优化方向", "description": "专为ACM编程竞赛题设计的提示词" diff --git a/config/version.json b/config/version.json index 3f9a942..b4eba3f 100644 --- a/config/version.json +++ b/config/version.json @@ -1,5 +1,5 @@ { - "version": "1.2.0", - "build_date": "2025-04-02", + "version": "1.3.0", + "build_date": "2025-04-11", "github_repo": "Zippland/Snap-Solver" } \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py index e3f4e65..336f66e 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -3,6 +3,7 @@ from .anthropic import AnthropicModel from .openai import OpenAIModel from .deepseek import DeepSeekModel from .alibaba import AlibabaModel +from .google import GoogleModel from .factory import ModelFactory __all__ = [ @@ -11,5 +12,6 @@ __all__ = [ 'OpenAIModel', 'DeepSeekModel', 'AlibabaModel', + 'GoogleModel', 'ModelFactory' ] diff --git a/models/factory.py b/models/factory.py index 7019265..cbf3e3d 100644 --- a/models/factory.py +++ b/models/factory.py @@ -57,35 +57,18 @@ class ModelFactory: @classmethod def _initialize_defaults(cls): """初始化默认模型(当配置加载失败时)""" - print("使用默认模型配置") - # 导入所有模型类作为备份 - from .anthropic import AnthropicModel - from .openai import OpenAIModel - from .deepseek import DeepSeekModel + print("配置加载失败,使用空模型列表") - cls._models = { - 'claude-3-7-sonnet-20250219': { - 'class': AnthropicModel, - 'is_multimodal': True, - 'is_reasoning': True, - 'display_name': 'Claude 3.7 Sonnet', - 'description': '强大的Claude 3.7 Sonnet模型,支持图像理解和思考过程' - }, - 'gpt-4o-2024-11-20': { - 'class': OpenAIModel, - 'is_multimodal': True, - 'is_reasoning': False, - 'display_name': 'GPT-4o', - 'description': 'OpenAI的GPT-4o模型,支持图像理解' - }, - 'deepseek-reasoner': { - 'class': DeepSeekModel, - 'is_multimodal': False, - 'is_reasoning': True, - 'display_name': 'DeepSeek Reasoner', - 'description': 'DeepSeek推理模型,提供详细思考过程(仅支持文本)' - }, - 'mathpix': { + # 不再硬编码模型定义,而是使用空字典 + cls._models = {} + + # 只保留Mathpix作为基础工具 + try: + # 导入MathpixModel类 + from .mathpix import MathpixModel + + # 添加Mathpix作为基础工具 + cls._models['mathpix'] = { 'class': MathpixModel, 'is_multimodal': True, 'is_reasoning': False, @@ -93,7 +76,8 @@ class ModelFactory: 'description': '文本提取工具,适用于数学公式和文本', 'is_ocr_only': True } - } + except Exception as e: + print(f"无法加载基础Mathpix工具: {str(e)}") @classmethod def create_model(cls, model_name: str, api_key: str, temperature: float = 0.7, system_prompt: str = None, language: str = None) -> BaseModel: diff --git a/models/google.py b/models/google.py new file mode 100644 index 0000000..c9c07d8 --- /dev/null +++ b/models/google.py @@ -0,0 +1,236 @@ +import json +import os +import base64 +from typing import Generator, Dict, Any, Optional, List +import google.generativeai as genai +from .base import BaseModel + +class GoogleModel(BaseModel): + """ + Google Gemini API模型实现类 + 支持Gemini 2.5 Pro等模型,可处理文本和图像输入 + """ + + def __init__(self, api_key: str, temperature: float = 0.7, system_prompt: str = None, language: str = None, model_name: str = None): + """ + 初始化Google模型 + + Args: + api_key: Google API密钥 + temperature: 生成温度 + system_prompt: 系统提示词 + language: 首选语言 + model_name: 指定具体模型名称,如不指定则使用默认值 + """ + super().__init__(api_key, temperature, system_prompt, language) + self.model_name = model_name or self.get_model_identifier() + self.max_tokens = 8192 # 默认最大输出token数 + + # 配置Google API + genai.configure(api_key=api_key) + + 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 +2. Break down the key components of the question +3. Provide a clear, step-by-step solution +4. If relevant, explain any concepts or theories involved +5. If there are multiple approaches, explain the most efficient one first""" + + def get_model_identifier(self) -> str: + """返回默认的模型标识符""" + return "gemini-2.5-pro-preview-03-25" + + def analyze_text(self, text: str, proxies: dict = None) -> Generator[dict, None, None]: + """流式生成文本响应""" + try: + yield {"status": "started"} + + # 设置环境变量代理(如果提供) + original_proxies = None + if proxies: + original_proxies = { + 'http_proxy': os.environ.get('http_proxy'), + 'https_proxy': os.environ.get('https_proxy') + } + if 'http' in proxies: + os.environ['http_proxy'] = proxies['http'] + if 'https' in proxies: + os.environ['https_proxy'] = proxies['https'] + + try: + # 初始化模型 + model = genai.GenerativeModel(self.model_name) + + # 获取最大输出Token设置 + max_tokens = self.max_tokens if hasattr(self, 'max_tokens') else 8192 + + # 创建配置参数 + generation_config = { + 'temperature': self.temperature, + 'max_output_tokens': max_tokens, + 'top_p': 0.95, + 'top_k': 64, + } + + # 构建提示 + prompt_parts = [] + + # 添加系统提示词 + if self.system_prompt: + prompt_parts.append(self.system_prompt) + + # 添加用户查询 + if self.language and self.language != 'auto': + prompt_parts.append(f"请使用{self.language}回答以下问题: {text}") + else: + prompt_parts.append(text) + + # 初始化响应缓冲区 + response_buffer = "" + + # 流式生成响应 + response = model.generate_content( + prompt_parts, + generation_config=generation_config, + stream=True + ) + + for chunk in response: + if not chunk.text: + continue + + # 累积响应文本 + response_buffer += chunk.text + + # 发送响应进度 + if len(chunk.text) >= 10 or chunk.text.endswith(('.', '!', '?', '。', '!', '?', '\n')): + yield { + "status": "streaming", + "content": response_buffer + } + + # 确保发送完整的最终内容 + yield { + "status": "completed", + "content": response_buffer + } + + finally: + # 恢复原始代理设置 + if original_proxies: + for key, value in original_proxies.items(): + if value is None: + if key in os.environ: + del os.environ[key] + else: + os.environ[key] = value + + except Exception as e: + yield { + "status": "error", + "error": f"Gemini API错误: {str(e)}" + } + + def analyze_image(self, image_data: str, proxies: dict = None) -> Generator[dict, None, None]: + """分析图像并流式生成响应""" + try: + yield {"status": "started"} + + # 设置环境变量代理(如果提供) + original_proxies = None + if proxies: + original_proxies = { + 'http_proxy': os.environ.get('http_proxy'), + 'https_proxy': os.environ.get('https_proxy') + } + if 'http' in proxies: + os.environ['http_proxy'] = proxies['http'] + if 'https' in proxies: + os.environ['https_proxy'] = proxies['https'] + + try: + # 初始化模型 + model = genai.GenerativeModel(self.model_name) + + # 获取最大输出Token设置 + max_tokens = self.max_tokens if hasattr(self, 'max_tokens') else 8192 + + # 创建配置参数 + generation_config = { + 'temperature': self.temperature, + 'max_output_tokens': max_tokens, + 'top_p': 0.95, + 'top_k': 64, + } + + # 构建提示词 + prompt_parts = [] + + # 添加系统提示词 + if self.system_prompt: + prompt_parts.append(self.system_prompt) + + # 添加默认图像分析指令 + if self.language and self.language != 'auto': + prompt_parts.append(f"请使用{self.language}分析这张图片并提供详细解答。") + else: + prompt_parts.append("请分析这张图片并提供详细解答。") + + # 处理图像数据 + if image_data.startswith('data:image'): + # 如果是data URI,提取base64部分 + image_data = image_data.split(',', 1)[1] + + # 使用genai的特定方法处理图像 + image_part = { + "mime_type": "image/jpeg", + "data": base64.b64decode(image_data) + } + prompt_parts.append(image_part) + + # 初始化响应缓冲区 + response_buffer = "" + + # 流式生成响应 + response = model.generate_content( + prompt_parts, + generation_config=generation_config, + stream=True + ) + + for chunk in response: + if not chunk.text: + continue + + # 累积响应文本 + response_buffer += chunk.text + + # 发送响应进度 + if len(chunk.text) >= 10 or chunk.text.endswith(('.', '!', '?', '。', '!', '?', '\n')): + yield { + "status": "streaming", + "content": response_buffer + } + + # 确保发送完整的最终内容 + yield { + "status": "completed", + "content": response_buffer + } + + finally: + # 恢复原始代理设置 + if original_proxies: + for key, value in original_proxies.items(): + if value is None: + if key in os.environ: + del os.environ[key] + else: + os.environ[key] = value + + except Exception as e: + yield { + "status": "error", + "error": f"Gemini图像分析错误: {str(e)}" + } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5f770b0..f10645b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ flask-socketio==5.5.1 python-engineio==4.11.2 python-socketio==5.12.1 requests==2.32.3 -openai==1.61.0 \ No newline at end of file +openai==1.61.0 +google-generativeai==0.7.0 \ No newline at end of file diff --git a/static/js/settings.js b/static/js/settings.js index 672db6f..3ef0f1c 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -697,28 +697,29 @@ class SettingsManager { * @param {string} modelType 模型类型 */ updateVisibleApiKey(modelType) { - // 获取所有API密钥状态元素 + // 高亮显示对应的API密钥 const allApiKeys = document.querySelectorAll('.api-key-status'); - // 首先隐藏所有API密钥 + // 先清除所有高亮 allApiKeys.forEach(key => { key.classList.remove('highlight'); }); - // 根据当前选择的模型类型,突出显示对应的API密钥 + // 根据模型类型高亮相应的API密钥 let apiKeyToHighlight = null; - if (modelType.startsWith('claude')) { + if (modelType && modelType.toLowerCase().includes('claude')) { apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(1)'); // Anthropic - } else if (modelType.startsWith('gpt')) { + } else if (modelType && (modelType.toLowerCase().includes('gpt') || modelType.toLowerCase().includes('openai'))) { apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(2)'); // OpenAI - } else if (modelType.startsWith('deepseek')) { + } else if (modelType && modelType.toLowerCase().includes('deepseek')) { apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(3)'); // DeepSeek - } else if (modelType.startsWith('qwen')) { + } else if (modelType && (modelType.toLowerCase().includes('qwen') || modelType.toLowerCase().includes('qvq') || modelType.toLowerCase().includes('alibaba'))) { apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(4)'); // Alibaba + } else if (modelType && (modelType.toLowerCase().includes('gemini') || modelType.toLowerCase().includes('google'))) { + apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(5)'); // Google } - // 高亮显示对应的API密钥 if (apiKeyToHighlight) { apiKeyToHighlight.classList.add('highlight'); } @@ -1541,61 +1542,43 @@ class SettingsManager { */ async loadPrompts() { try { - // 从服务器获取提示词列表 + // 从服务器获取提示词配置 const response = await fetch('/api/prompts'); if (response.ok) { - // 解析提示词列表 + // 解析提示词数据 const prompts = await response.json(); - // 保存到本地 + // 存储提示词数据(保持原始顺序) this.prompts = prompts; + // 添加提示词顺序属性,记录从服务器获取的原始顺序 + this.promptsOrder = Object.keys(prompts); // 更新提示词选择下拉框 if (this.promptSelect) { - this.updatePromptSelect(); + this.updatePromptSelect(); } - // 确定要加载的提示词ID - let promptIdToLoad = null; - - // 优先使用之前保存的currentPromptId + // 如果当前已有选中的提示词,尝试加载它 if (this.currentPromptId && this.prompts[this.currentPromptId]) { - promptIdToLoad = this.currentPromptId; - } - // 其次使用default提示词 - else if (this.prompts.default) { - promptIdToLoad = 'default'; - } - // 最后使用第一个可用的提示词 - else if (Object.keys(this.prompts).length > 0) { - promptIdToLoad = Object.keys(this.prompts)[0]; + this.loadPrompt(this.currentPromptId); } - - // 如果找到了要加载的提示词,加载它 - if (promptIdToLoad) { + // 否则尝试加载默认提示词 + else if (this.prompts.default) { + this.loadPrompt('default'); + } + // 否则加载第一个提示词 + else if (Object.keys(this.prompts).length > 0) { + const promptIdToLoad = Object.keys(this.prompts)[0]; this.loadPrompt(promptIdToLoad); - console.log('加载提示词:', promptIdToLoad); - } else { - // 如果没有提示词,显示默认描述 - if (this.promptDescriptionElement) { - this.promptDescriptionElement.innerHTML = '
暂无提示词,请点击"+"创建新提示词
'; - } } console.log('提示词加载成功:', this.prompts); } else { - console.error('加载提示词失败:', response.status, response.statusText); - window.uiManager?.showToast('加载提示词失败', 'error'); - - // 显示默认描述 - if (this.promptDescriptionElement) { - this.promptDescriptionElement.innerHTML = '加载提示词失败,请检查网络连接
'; - } + console.error('加载提示词失败:', response.statusText); } } catch (error) { console.error('加载提示词错误:', error); - window.uiManager?.showToast('加载提示词错误: ' + error.message, 'error'); // 显示错误描述 if (this.promptDescriptionElement) { @@ -1616,20 +1599,39 @@ class SettingsManager { // 清空下拉框 this.promptSelect.innerHTML = ''; - // 添加所有提示词选项 - for (const promptId in this.prompts) { - const prompt = this.prompts[promptId]; - const option = document.createElement('option'); - option.value = promptId; - option.textContent = prompt.name; - this.promptSelect.appendChild(option); + // 按prompts.json中的原始顺序添加提示词选项 + // 使用this.promptsOrder来保持原始顺序 + if (this.promptsOrder && this.promptsOrder.length > 0) { + for (const promptId of this.promptsOrder) { + if (this.prompts[promptId]) { + const prompt = this.prompts[promptId]; + const option = document.createElement('option'); + option.value = promptId; + option.textContent = prompt.name; + this.promptSelect.appendChild(option); + } + } + } else { + // 如果没有保存顺序,则使用对象键的顺序(不推荐,但作为后备方案) + for (const promptId in this.prompts) { + const prompt = this.prompts[promptId]; + const option = document.createElement('option'); + option.value = promptId; + option.textContent = prompt.name; + this.promptSelect.appendChild(option); + } } // 恢复之前选中的提示词或选择第一个提示词 if (currentPromptId && this.prompts[currentPromptId]) { this.promptSelect.value = currentPromptId; + } else if (this.promptsOrder && this.promptsOrder.length > 0) { + // 选择原始顺序的第一个提示词 + this.promptSelect.value = this.promptsOrder[0]; + // 更新当前提示词ID和描述显示 + this.loadPrompt(this.promptSelect.value); } else if (Object.keys(this.prompts).length > 0) { - // 如果之前选中的提示词不存在,选择第一个 + // 如果没有原始顺序,选择第一个提示词 this.promptSelect.value = Object.keys(this.prompts)[0]; // 更新当前提示词ID和描述显示 this.loadPrompt(this.promptSelect.value); @@ -1731,6 +1733,11 @@ class SettingsManager { description: promptDescription }; + // 如果是新增提示词,将其添加到顺序数组末尾 + if (isNew && this.promptsOrder && !this.promptsOrder.includes(promptId)) { + this.promptsOrder.push(promptId); + } + // 更新提示词选择下拉框 this.updatePromptSelect(); @@ -1779,6 +1786,11 @@ class SettingsManager { if (response.ok) { const result = await response.json(); if (result.success) { + // 从顺序数组中移除该提示词 + if (this.promptsOrder && this.promptsOrder.includes(promptId)) { + this.promptsOrder = this.promptsOrder.filter(id => id !== promptId); + } + // 删除本地提示词 delete this.prompts[promptId]; @@ -1786,9 +1798,10 @@ class SettingsManager { this.updatePromptSelect(); // 如果还有其他提示词,加载第一个 - const promptIds = Object.keys(this.prompts); - if (promptIds.length > 0) { - this.loadPrompt(promptIds[0]); + if (this.promptsOrder && this.promptsOrder.length > 0) { + this.loadPrompt(this.promptsOrder[0]); + } else if (Object.keys(this.prompts).length > 0) { + this.loadPrompt(Object.keys(this.prompts)[0]); } else { // 如果没有提示词了,清空输入框和描述显示 this.systemPromptInput.value = ''; @@ -2093,6 +2106,7 @@ class SettingsManager { 'OpenaiApiKey': document.getElementById('OpenaiApiKey'), 'DeepseekApiKey': document.getElementById('DeepseekApiKey'), 'AlibabaApiKey': document.getElementById('AlibabaApiKey'), + 'GoogleApiKey': document.getElementById('GoogleApiKey'), 'mathpixAppId': this.mathpixAppIdInput, 'mathpixAppKey': this.mathpixAppKeyInput }; @@ -2133,6 +2147,7 @@ class SettingsManager { 'OpenaiApiKey': '', 'DeepseekApiKey': '', 'AlibabaApiKey': '', + 'GoogleApiKey': '', 'MathpixAppId': '', 'MathpixAppKey': '' }; @@ -2154,6 +2169,56 @@ class SettingsManager { }); } } + + /** + * 打开提示词编辑对话框 + * @param {string|null} promptId 要编辑的提示词ID,为空则表示新建 + */ + openPromptDialog(promptId = null) { + // 判断是否为新建提示词 + const isNew = !promptId || !this.prompts[promptId]; + + if (isNew) { + // 新建提示词 - 清空输入框 + this.promptIdInput.value = ''; + this.promptNameInput.value = ''; + this.promptContentInput.value = ''; + this.promptDescriptionInput.value = ''; + + // 设置对话框标题 + if (this.promptDialogTitle) { + this.promptDialogTitle.textContent = '新建提示词'; + } + + // 启用ID输入框 + this.promptIdInput.disabled = false; + + // 设置保存按钮动作 + this.promptSaveBtn.onclick = () => this.savePrompt(true); // 明确传递isNew=true + } else { + // 编辑现有提示词 - 填充现有内容 + this.promptIdInput.value = promptId; + this.promptNameInput.value = this.prompts[promptId].name; + this.promptContentInput.value = this.prompts[promptId].content; + this.promptDescriptionInput.value = this.prompts[promptId].description || ''; + + // 设置对话框标题 + if (this.promptDialogTitle) { + this.promptDialogTitle.textContent = '编辑提示词'; + } + + // 禁用ID输入框(不允许修改ID) + this.promptIdInput.disabled = true; + + // 设置保存按钮动作 + this.promptSaveBtn.onclick = () => this.savePrompt(false); // 明确传递isNew=false + } + + // 显示对话框 + if (this.promptDialogMask) { + this.promptDialogMask.classList.remove('hidden'); + } + } } // Export for use in other modules diff --git a/static/style.css b/static/style.css index f086fc0..0084a9f 100644 --- a/static/style.css +++ b/static/style.css @@ -5284,3 +5284,42 @@ textarea, .model-dropdown-panel::-webkit-scrollbar { width: 6px; } + +/* 提示词预览样式 */ +.prompt-preview { + position: relative; + cursor: pointer; +} + +.prompt-preview-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 8px; + background-color: rgba(0, 0, 0, 0.03); + opacity: 0; + transition: opacity 0.2s ease; +} + +.prompt-preview:hover .prompt-preview-overlay { + opacity: 1; +} + +.prompt-edit-hint { + position: absolute; + right: 10px; + bottom: 10px; + /* 不显示图标,因为上方已有编辑按钮 */ +} + +[data-theme="dark"] .prompt-preview-overlay { + background-color: rgba(255, 255, 255, 0.05); +} + +@media (max-width: 768px) { + .prompt-preview-overlay { + display: none; + } +} diff --git a/templates/index.html b/templates/index.html index a14865c..eab2900 100644 --- a/templates/index.html +++ b/templates/index.html @@ -300,7 +300,7 @@ @@ -403,6 +403,28 @@ +