修复gemini接口,添加豆包接口

This commit is contained in:
skestar
2025-08-02 22:46:57 +08:00
parent 160d716fbe
commit aef6e2abef
12 changed files with 420 additions and 11 deletions

6
app.py
View File

@@ -101,6 +101,8 @@ def create_model_instance(model_id, settings, is_reasoning=False):
api_key_id = "AlibabaApiKey" api_key_id = "AlibabaApiKey"
elif "gemini" in model_id.lower() or "google" in model_id.lower(): elif "gemini" in model_id.lower() or "google" in model_id.lower():
api_key_id = "GoogleApiKey" api_key_id = "GoogleApiKey"
elif "doubao" in model_id.lower():
api_key_id = "DoubaoApiKey"
# 首先尝试从本地配置获取API密钥 # 首先尝试从本地配置获取API密钥
api_key = get_api_key(api_key_id) api_key = get_api_key(api_key_id)
@@ -156,6 +158,10 @@ def create_model_instance(model_id, settings, is_reasoning=False):
custom_base_url = api_base_urls.get('google') custom_base_url = api_base_urls.get('google')
if custom_base_url: if custom_base_url:
base_url = custom_base_url base_url = custom_base_url
elif "doubao" in model_id.lower():
custom_base_url = api_base_urls.get('doubao')
if custom_base_url:
base_url = custom_base_url
# 创建模型实例 # 创建模型实例
model_instance = ModelFactory.create_model( model_instance = ModelFactory.create_model(

View File

@@ -3,5 +3,6 @@
"OpenaiApiBaseUrl": "", "OpenaiApiBaseUrl": "",
"DeepseekApiBaseUrl": "", "DeepseekApiBaseUrl": "",
"AlibabaApiBaseUrl": "", "AlibabaApiBaseUrl": "",
"GoogleApiBaseUrl": "" "GoogleApiBaseUrl": "",
"DoubaoApiBaseUrl": ""
} }

View File

@@ -24,6 +24,11 @@
"name": "Google", "name": "Google",
"api_key_id": "GoogleApiKey", "api_key_id": "GoogleApiKey",
"class_name": "GoogleModel" "class_name": "GoogleModel"
},
"doubao": {
"name": "Doubao",
"api_key_id": "DoubaoApiKey",
"class_name": "DoubaoModel"
} }
}, },
"models": { "models": {
@@ -91,13 +96,13 @@
"version": "latest", "version": "latest",
"description": "阿里通义千问VL-MAX模型视觉理解能力最强支持图像理解和复杂任务" "description": "阿里通义千问VL-MAX模型视觉理解能力最强支持图像理解和复杂任务"
}, },
"gemini-2.5-pro-preview-03-25": { "gemini-2.5-pro": {
"name": "Gemini 2.5 Pro", "name": "Gemini 2.5 Pro",
"provider": "google", "provider": "google",
"supportsMultimodal": true, "supportsMultimodal": true,
"isReasoning": true, "isReasoning": true,
"version": "preview-03-25", "version": "latest",
"description": "Google最强大的Gemini 2.5 Pro模型支持图像理解" "description": "Google最强大的Gemini 2.5 Pro模型支持图像理解需要付费API密钥"
}, },
"gemini-2.0-flash": { "gemini-2.0-flash": {
"name": "Gemini 2.0 Flash", "name": "Gemini 2.0 Flash",
@@ -105,7 +110,15 @@
"supportsMultimodal": true, "supportsMultimodal": true,
"isReasoning": false, "isReasoning": false,
"version": "latest", "version": "latest",
"description": "Google更快速的Gemini 2.0 Flash模型支持图像理解响应更迅速" "description": "Google更快速的Gemini 2.0 Flash模型支持图像理解有免费配额"
},
"doubao-seed-1-6-250615": {
"name": "Doubao-Seed-1.6",
"provider": "doubao",
"supportsMultimodal": true,
"isReasoning": false,
"version": "latest",
"description": "支持auto/thinking/non-thinking三种思考模式、支持多模态、256K长上下文"
} }
} }
} }

View File

@@ -4,7 +4,8 @@
"anthropic": "", "anthropic": "",
"deepseek": "", "deepseek": "",
"google": "", "google": "",
"openai": "" "openai": "",
"doubao": ""
}, },
"enabled": true "enabled": true
} }

View File

@@ -4,6 +4,7 @@ from .openai import OpenAIModel
from .deepseek import DeepSeekModel from .deepseek import DeepSeekModel
from .alibaba import AlibabaModel from .alibaba import AlibabaModel
from .google import GoogleModel from .google import GoogleModel
from .doubao import DoubaoModel
from .factory import ModelFactory from .factory import ModelFactory
__all__ = [ __all__ = [
@@ -13,5 +14,6 @@ __all__ = [
'DeepSeekModel', 'DeepSeekModel',
'AlibabaModel', 'AlibabaModel',
'GoogleModel', 'GoogleModel',
'DoubaoModel',
'ModelFactory' 'ModelFactory'
] ]

View File

@@ -4,12 +4,13 @@ from openai import OpenAI
from .base import BaseModel from .base import BaseModel
class AlibabaModel(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): def __init__(self, api_key: str, temperature: float = 0.7, system_prompt: str = None, language: str = None, model_name: str = None, api_base_url: str = None):
# 如果没有提供模型名称,才使用默认值 # 如果没有提供模型名称,才使用默认值
self.model_name = model_name if model_name else "QVQ-Max-2025-03-25" self.model_name = model_name if model_name else "QVQ-Max-2025-03-25"
print(f"初始化阿里巴巴模型: {self.model_name}") print(f"初始化阿里巴巴模型: {self.model_name}")
# 在super().__init__之前设置model_name这样get_default_system_prompt能使用它 # 在super().__init__之前设置model_name这样get_default_system_prompt能使用它
super().__init__(api_key, temperature, system_prompt, language) super().__init__(api_key, temperature, system_prompt, language)
self.api_base_url = api_base_url # 存储API基础URL
def get_default_system_prompt(self) -> str: def get_default_system_prompt(self) -> str:
"""根据模型名称返回不同的默认系统提示词""" """根据模型名称返回不同的默认系统提示词"""

View File

@@ -6,9 +6,10 @@ from openai import OpenAI
from .base import BaseModel from .base import BaseModel
class DeepSeekModel(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"): def __init__(self, api_key: str, temperature: float = 0.7, system_prompt: str = None, language: str = None, model_name: str = "deepseek-reasoner", api_base_url: str = None):
super().__init__(api_key, temperature, system_prompt, language) super().__init__(api_key, temperature, system_prompt, language)
self.model_name = model_name self.model_name = model_name
self.api_base_url = api_base_url # 存储API基础URL
def get_default_system_prompt(self) -> str: 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: return """You are an expert at analyzing questions and providing detailed solutions. When presented with an image of a question:

312
models/doubao.py Normal file
View File

@@ -0,0 +1,312 @@
import json
import os
import base64
from typing import Generator, Dict, Any, Optional
import requests
from .base import BaseModel
class DoubaoModel(BaseModel):
"""
豆包API模型实现类
支持字节跳动的豆包AI模型可处理文本和图像输入
"""
def __init__(self, api_key: str, temperature: float = 0.7, system_prompt: str = None, language: str = None, model_name: str = None, api_base_url: str = None):
"""
初始化豆包模型
Args:
api_key: 豆包API密钥
temperature: 生成温度
system_prompt: 系统提示词
language: 首选语言
model_name: 指定具体模型名称,如不指定则使用默认值
api_base_url: API基础URL用于设置自定义API端点
"""
super().__init__(api_key, temperature, system_prompt, language)
self.model_name = model_name or self.get_model_identifier()
self.base_url = api_base_url or "https://ark.cn-beijing.volces.com/api/v3"
self.max_tokens = 4096 # 默认最大输出token数
def get_default_system_prompt(self) -> str:
return """你是一个专业的问题分析专家。当看到问题图片时:
1. 仔细阅读并理解问题
2. 分解问题的关键组成部分
3. 提供清晰的分步解决方案
4. 如果相关,解释涉及的概念或理论
5. 如果有多种方法,优先解释最有效的方法"""
def get_model_identifier(self) -> str:
"""返回默认的模型标识符"""
return "doubao-seed-1-6-250615" # Doubao-Seed-1.6
def get_actual_model_name(self) -> str:
"""根据配置的模型名称返回实际的API调用标识符"""
# 豆包API的实际模型名称映射
model_mapping = {
"doubao-seed-1-6-250615": "doubao-seed-1-6-250615"
}
return model_mapping.get(self.model_name, "doubao-seed-1-6-250615")
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:
# 构建请求头
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
# 构建消息 - 根据官方API文档暂时不使用系统提示词
messages = []
# 添加用户查询
user_content = text
if self.language and self.language != 'auto':
user_content = f"请使用{self.language}回答以下问题: {text}"
messages.append({
"role": "user",
"content": user_content
})
# 构建请求数据
data = {
"model": self.get_actual_model_name(),
"messages": messages,
"temperature": self.temperature,
"max_tokens": self.max_tokens,
"stream": True
}
# 发送流式请求
response = requests.post(
f"{self.base_url}/chat/completions",
headers=headers,
json=data,
stream=True,
proxies=proxies if proxies else None,
timeout=60
)
if response.status_code != 200:
error_text = response.text
raise Exception(f"HTTP {response.status_code}: {error_text}")
response.raise_for_status()
# 初始化响应缓冲区
response_buffer = ""
# 处理流式响应
for line in response.iter_lines():
if not line:
continue
line = line.decode('utf-8')
if not line.startswith('data: '):
continue
line = line[6:] # 移除 'data: ' 前缀
if line == '[DONE]':
break
try:
chunk_data = json.loads(line)
choices = chunk_data.get('choices', [])
if choices and len(choices) > 0:
delta = choices[0].get('delta', {})
content = delta.get('content', '')
if content:
response_buffer += content
# 发送响应进度
yield {
"status": "streaming",
"content": response_buffer
}
except json.JSONDecodeError:
continue
# 确保发送完整的最终内容
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"豆包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:
# 构建请求头
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
# 处理图像数据
if image_data.startswith('data:image'):
# 如果是data URI提取base64部分
image_data = image_data.split(',', 1)[1]
# 构建用户消息 - 使用豆包API官方示例格式
# 首先检查图像数据的格式,确保是有效的图像
image_format = "jpeg" # 默认使用jpeg
if image_data.startswith('/9j/'): # JPEG magic number in base64
image_format = "jpeg"
elif image_data.startswith('iVBORw0KGgo'): # PNG magic number in base64
image_format = "png"
user_content = [
{
"type": "text",
"text": f"请使用{self.language}分析这张图片并提供详细解答。" if self.language and self.language != 'auto' else "请分析这张图片并提供详细解答?"
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/{image_format};base64,{image_data}"
}
}
]
messages = [
{
"role": "user",
"content": user_content
}
]
# 构建请求数据
data = {
"model": self.get_actual_model_name(),
"messages": messages,
"temperature": self.temperature,
"max_tokens": self.max_tokens,
"stream": True
}
# 发送流式请求
response = requests.post(
f"{self.base_url}/chat/completions",
headers=headers,
json=data,
stream=True,
proxies=proxies if proxies else None,
timeout=60
)
if response.status_code != 200:
error_text = response.text
raise Exception(f"HTTP {response.status_code}: {error_text}")
response.raise_for_status()
# 初始化响应缓冲区
response_buffer = ""
# 处理流式响应
for line in response.iter_lines():
if not line:
continue
line = line.decode('utf-8')
if not line.startswith('data: '):
continue
line = line[6:] # 移除 'data: ' 前缀
if line == '[DONE]':
break
try:
chunk_data = json.loads(line)
choices = chunk_data.get('choices', [])
if choices and len(choices) > 0:
delta = choices[0].get('delta', {})
content = delta.get('content', '')
if content:
response_buffer += content
# 发送响应进度
yield {
"status": "streaming",
"content": response_buffer
}
except json.JSONDecodeError:
continue
# 确保发送完整的最终内容
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"豆包图像分析错误: {str(e)}"
}

View File

@@ -114,6 +114,25 @@ class ModelFactory:
) )
# 对于阿里巴巴模型,也需要传递正确的模型名称 # 对于阿里巴巴模型,也需要传递正确的模型名称
elif 'qwen' in model_name.lower() or 'qvq' in model_name.lower() or 'alibaba' in model_name.lower(): elif 'qwen' in model_name.lower() or 'qvq' in model_name.lower() or 'alibaba' in model_name.lower():
return model_class(
api_key=api_key,
temperature=temperature,
system_prompt=system_prompt,
language=language,
model_name=model_name
)
# 对于Google模型也需要传递正确的模型名称
elif 'gemini' in model_name.lower() or 'google' in model_name.lower():
return model_class(
api_key=api_key,
temperature=temperature,
system_prompt=system_prompt,
language=language,
model_name=model_name,
api_base_url=api_base_url
)
# 对于豆包模型,也需要传递正确的模型名称
elif 'doubao' in model_name.lower():
return model_class( return model_class(
api_key=api_key, api_key=api_key,
temperature=temperature, temperature=temperature,

View File

@@ -46,7 +46,7 @@ class GoogleModel(BaseModel):
def get_model_identifier(self) -> str: def get_model_identifier(self) -> str:
"""返回默认的模型标识符""" """返回默认的模型标识符"""
return "gemini-2.5-pro-preview-03-25" return "gemini-2.0-flash" # 使用有免费配额的模型作为默认值
def analyze_text(self, text: str, proxies: dict = None) -> Generator[dict, None, None]: def analyze_text(self, text: str, proxies: dict = None) -> Generator[dict, None, None]:
"""流式生成文本响应""" """流式生成文本响应"""

View File

@@ -381,6 +381,7 @@ class SettingsManager {
'DeepseekApiKey': '', 'DeepseekApiKey': '',
'AlibabaApiKey': '', 'AlibabaApiKey': '',
'GoogleApiKey': '', 'GoogleApiKey': '',
'DoubaoApiKey': '',
'MathpixAppId': '', 'MathpixAppId': '',
'MathpixAppKey': '' 'MathpixAppKey': ''
}; };
@@ -391,7 +392,8 @@ class SettingsManager {
'OpenaiApiBaseUrl': '', 'OpenaiApiBaseUrl': '',
'DeepseekApiBaseUrl': '', 'DeepseekApiBaseUrl': '',
'AlibabaApiBaseUrl': '', 'AlibabaApiBaseUrl': '',
'GoogleApiBaseUrl': '' 'GoogleApiBaseUrl': '',
'DoubaoApiBaseUrl': ''
}; };
// 加载模型配置 // 加载模型配置
@@ -759,6 +761,8 @@ class SettingsManager {
apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(4)'); // Alibaba apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(4)'); // Alibaba
} else if (modelType && (modelType.toLowerCase().includes('gemini') || modelType.toLowerCase().includes('google'))) { } else if (modelType && (modelType.toLowerCase().includes('gemini') || modelType.toLowerCase().includes('google'))) {
apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(5)'); // Google apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(5)'); // Google
} else if (modelType && modelType.toLowerCase().includes('doubao')) {
apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(6)'); // 豆包
} }
if (apiKeyToHighlight) { if (apiKeyToHighlight) {
@@ -869,6 +873,9 @@ class SettingsManager {
if (this.apiBaseUrlValues['GoogleApiBaseUrl']) { if (this.apiBaseUrlValues['GoogleApiBaseUrl']) {
apiBaseUrls.google = this.apiBaseUrlValues['GoogleApiBaseUrl']; apiBaseUrls.google = this.apiBaseUrlValues['GoogleApiBaseUrl'];
} }
if (this.apiBaseUrlValues['DoubaoApiBaseUrl']) {
apiBaseUrls.doubao = this.apiBaseUrlValues['DoubaoApiBaseUrl'];
}
} }
return { return {
@@ -2260,6 +2267,7 @@ class SettingsManager {
'DeepseekApiKey': '', 'DeepseekApiKey': '',
'AlibabaApiKey': '', 'AlibabaApiKey': '',
'GoogleApiKey': '', 'GoogleApiKey': '',
'DoubaoApiKey': '',
'MathpixAppId': '', 'MathpixAppId': '',
'MathpixAppKey': '' 'MathpixAppKey': ''
}; };
@@ -2359,7 +2367,8 @@ class SettingsManager {
'OpenaiApiBaseUrl': proxyApiConfig.apis?.openai || '', 'OpenaiApiBaseUrl': proxyApiConfig.apis?.openai || '',
'DeepseekApiBaseUrl': proxyApiConfig.apis?.deepseek || '', 'DeepseekApiBaseUrl': proxyApiConfig.apis?.deepseek || '',
'AlibabaApiBaseUrl': proxyApiConfig.apis?.alibaba || '', 'AlibabaApiBaseUrl': proxyApiConfig.apis?.alibaba || '',
'GoogleApiBaseUrl': proxyApiConfig.apis?.google || '' 'GoogleApiBaseUrl': proxyApiConfig.apis?.google || '',
'DoubaoApiBaseUrl': proxyApiConfig.apis?.doubao || ''
}; };
this.updateApiBaseUrlStatus(apiBaseUrls); this.updateApiBaseUrlStatus(apiBaseUrls);
console.log('API基础URL状态已刷新'); console.log('API基础URL状态已刷新');
@@ -2449,6 +2458,9 @@ class SettingsManager {
case 'GoogleApiBaseUrl': case 'GoogleApiBaseUrl':
config.apis.google = value; config.apis.google = value;
break; break;
case 'DoubaoApiBaseUrl':
config.apis.doubao = value;
break;
} }
// 确保启用中转API // 确保启用中转API

View File

@@ -425,6 +425,28 @@
</div> </div>
</div> </div>
</div> </div>
<div class="api-key-status">
<span class="key-name">Doubao API:</span>
<div class="key-status-wrapper">
<!-- 显示状态 -->
<div class="key-display">
<span id="DoubaoApiKeyStatus" class="key-status" data-key="DoubaoApiKey">未设置</span>
<button class="btn-icon edit-api-key" data-key-type="DoubaoApiKey" title="编辑此密钥">
<i class="fas fa-edit"></i>
</button>
</div>
<!-- 编辑状态 -->
<div class="key-edit hidden">
<input type="password" class="key-input" data-key-type="DoubaoApiKey" placeholder="输入Doubao API key">
<button class="btn-icon toggle-visibility">
<i class="fas fa-eye"></i>
</button>
<button class="btn-icon save-api-key" data-key-type="DoubaoApiKey" title="保存密钥">
<i class="fas fa-save"></i>
</button>
</div>
</div>
</div>
<div class="api-key-status"> <div class="api-key-status">
<span class="key-name">Mathpix App ID:</span> <span class="key-name">Mathpix App ID:</span>
<div class="key-status-wrapper"> <div class="key-status-wrapper">
@@ -577,6 +599,25 @@
</div> </div>
</div> </div>
</div> </div>
<div class="api-key-status">
<span class="key-name">Doubao API URL:</span>
<div class="key-status-wrapper">
<!-- 显示状态 -->
<div class="key-display">
<span id="DoubaoApiBaseUrlStatus" class="key-status" data-key="DoubaoApiBaseUrl">未设置</span>
<button class="btn-icon edit-api-base-url" data-key-type="DoubaoApiBaseUrl" title="编辑此URL">
<i class="fas fa-edit"></i>
</button>
</div>
<!-- 编辑状态 -->
<div class="key-edit hidden">
<input type="text" class="key-input" data-key-type="DoubaoApiBaseUrl" placeholder="https://ark.cn-beijing.volces.com/api/v3">
<button class="btn-icon save-api-base-url" data-key-type="DoubaoApiBaseUrl" title="保存URL">
<i class="fas fa-save"></i>
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>