mirror of
https://github.com/Zippland/Snap-Solver.git
synced 2026-01-19 01:21:13 +08:00
Merge pull request #7 from Cb1ock/feature/custom-api-endpoints
Feature/custom api endpoints
This commit is contained in:
131
app.py
131
app.py
@@ -41,6 +41,7 @@ API_KEYS_FILE = os.path.join(CONFIG_DIR, 'api_keys.json')
|
||||
VERSION_FILE = os.path.join(CONFIG_DIR, 'version.json')
|
||||
UPDATE_INFO_FILE = os.path.join(CONFIG_DIR, 'update_info.json')
|
||||
PROMPT_FILE = os.path.join(CONFIG_DIR, 'prompts.json') # 新增提示词配置文件路径
|
||||
PROXY_API_FILE = os.path.join(CONFIG_DIR, 'proxy_api.json') # 新增中转API配置文件路径
|
||||
|
||||
# 跟踪用户生成任务的字典
|
||||
generation_tasks = {}
|
||||
@@ -114,13 +115,56 @@ def create_model_instance(model_id, settings, is_reasoning=False):
|
||||
# 获取maxTokens参数,默认为8192
|
||||
max_tokens = int(settings.get('maxTokens', 8192))
|
||||
|
||||
# 检查是否启用中转API
|
||||
proxy_api_config = load_proxy_api()
|
||||
base_url = None
|
||||
|
||||
if proxy_api_config.get('enabled', False):
|
||||
# 根据模型类型选择对应的中转API
|
||||
if "claude" in model_id.lower() or "anthropic" in model_id.lower():
|
||||
base_url = proxy_api_config.get('apis', {}).get('anthropic', '')
|
||||
elif any(keyword in model_id.lower() for keyword in ["gpt", "openai"]):
|
||||
base_url = proxy_api_config.get('apis', {}).get('openai', '')
|
||||
elif "deepseek" in model_id.lower():
|
||||
base_url = proxy_api_config.get('apis', {}).get('deepseek', '')
|
||||
elif "qvq" in model_id.lower() or "alibaba" in model_id.lower() or "qwen" in model_id.lower():
|
||||
base_url = proxy_api_config.get('apis', {}).get('alibaba', '')
|
||||
elif "gemini" in model_id.lower() or "google" in model_id.lower():
|
||||
base_url = proxy_api_config.get('apis', {}).get('google', '')
|
||||
|
||||
# 从前端设置获取自定义API基础URL (apiBaseUrls)
|
||||
api_base_urls = settings.get('apiBaseUrls', {})
|
||||
if api_base_urls:
|
||||
# 根据模型类型选择对应的自定义API基础URL
|
||||
if "claude" in model_id.lower() or "anthropic" in model_id.lower():
|
||||
custom_base_url = api_base_urls.get('anthropic')
|
||||
if custom_base_url:
|
||||
base_url = custom_base_url
|
||||
elif any(keyword in model_id.lower() for keyword in ["gpt", "openai"]):
|
||||
custom_base_url = api_base_urls.get('openai')
|
||||
if custom_base_url:
|
||||
base_url = custom_base_url
|
||||
elif "deepseek" in model_id.lower():
|
||||
custom_base_url = api_base_urls.get('deepseek')
|
||||
if custom_base_url:
|
||||
base_url = custom_base_url
|
||||
elif "qvq" in model_id.lower() or "alibaba" in model_id.lower() or "qwen" in model_id.lower():
|
||||
custom_base_url = api_base_urls.get('alibaba')
|
||||
if custom_base_url:
|
||||
base_url = custom_base_url
|
||||
elif "gemini" in model_id.lower() or "google" in model_id.lower():
|
||||
custom_base_url = api_base_urls.get('google')
|
||||
if custom_base_url:
|
||||
base_url = custom_base_url
|
||||
|
||||
# 创建模型实例
|
||||
model_instance = ModelFactory.create_model(
|
||||
model_name=model_id,
|
||||
api_key=api_key,
|
||||
temperature=None if is_reasoning else float(settings.get('temperature', 0.7)),
|
||||
system_prompt=settings.get('systemPrompt'),
|
||||
language=settings.get('language', '中文')
|
||||
language=settings.get('language', '中文'),
|
||||
api_base_url=base_url # 现在BaseModel支持api_base_url参数
|
||||
)
|
||||
|
||||
# 设置最大输出Token,但不为阿里巴巴模型设置(它们有自己内部的处理逻辑)
|
||||
@@ -159,24 +203,6 @@ def stream_model_response(response_generator, sid, model_name=None):
|
||||
for response in response_generator:
|
||||
# 处理Mathpix响应
|
||||
if isinstance(response.get('content', ''), str) and 'mathpix' in response.get('model', ''):
|
||||
socketio.emit('text_extracted', {
|
||||
'content': response['content']
|
||||
}, room=sid)
|
||||
continue
|
||||
|
||||
# 获取状态和内容
|
||||
status = response.get('status', '')
|
||||
content = response.get('content', '')
|
||||
|
||||
# 根据不同的状态进行处理
|
||||
if status == 'thinking':
|
||||
# 仅对推理模型处理思考过程
|
||||
if is_reasoning:
|
||||
# 直接使用模型提供的完整思考内容
|
||||
thinking_buffer = content
|
||||
|
||||
# 控制发送频率,至少间隔0.3秒
|
||||
current_time = time.time()
|
||||
if current_time - last_emit_time >= 0.3:
|
||||
socketio.emit('ai_response', {
|
||||
'status': 'thinking',
|
||||
@@ -774,9 +800,47 @@ def load_api_keys():
|
||||
print(f"加载API密钥配置失败: {e}")
|
||||
return {}
|
||||
|
||||
# 加载中转API配置
|
||||
def load_proxy_api():
|
||||
"""从配置文件加载中转API配置"""
|
||||
try:
|
||||
if os.path.exists(PROXY_API_FILE):
|
||||
with open(PROXY_API_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
else:
|
||||
# 如果文件不存在,创建默认配置
|
||||
default_proxy_apis = {
|
||||
"enabled": False,
|
||||
"apis": {
|
||||
"anthropic": "",
|
||||
"openai": "",
|
||||
"deepseek": "",
|
||||
"alibaba": "",
|
||||
"google": ""
|
||||
}
|
||||
}
|
||||
save_proxy_api(default_proxy_apis)
|
||||
return default_proxy_apis
|
||||
except Exception as e:
|
||||
print(f"加载中转API配置失败: {e}")
|
||||
return {"enabled": False, "apis": {}}
|
||||
|
||||
# 保存中转API配置
|
||||
def save_proxy_api(proxy_api_config):
|
||||
"""保存中转API配置到文件"""
|
||||
try:
|
||||
# 确保配置目录存在
|
||||
os.makedirs(os.path.dirname(PROXY_API_FILE), exist_ok=True)
|
||||
|
||||
with open(PROXY_API_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(proxy_api_config, f, ensure_ascii=False, indent=2)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"保存中转API配置失败: {e}")
|
||||
return False
|
||||
|
||||
# 保存API密钥配置
|
||||
def save_api_keys(api_keys):
|
||||
"""保存API密钥到配置文件"""
|
||||
try:
|
||||
# 确保配置目录存在
|
||||
os.makedirs(os.path.dirname(API_KEYS_FILE), exist_ok=True)
|
||||
@@ -879,6 +943,33 @@ def remove_prompt(prompt_id):
|
||||
print(f"删除提示词时出错: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/proxy-api', methods=['GET'])
|
||||
def get_proxy_api():
|
||||
"""API端点:获取中转API配置"""
|
||||
try:
|
||||
proxy_api_config = load_proxy_api()
|
||||
return jsonify(proxy_api_config)
|
||||
except Exception as e:
|
||||
print(f"获取中转API配置时出错: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/proxy-api', methods=['POST'])
|
||||
def update_proxy_api():
|
||||
"""API端点:更新中转API配置"""
|
||||
try:
|
||||
new_config = request.json
|
||||
if not isinstance(new_config, dict):
|
||||
return jsonify({"success": False, "message": "无效的中转API配置格式"}), 400
|
||||
|
||||
# 保存回文件
|
||||
if save_proxy_api(new_config):
|
||||
return jsonify({"success": True, "message": "中转API配置已保存"})
|
||||
else:
|
||||
return jsonify({"success": False, "message": "保存中转API配置失败"}), 500
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "message": f"更新中转API配置错误: {str(e)}"}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
local_ip = get_local_ip()
|
||||
print(f"Local IP Address: {local_ip}")
|
||||
|
||||
7
config/api_base_urls.json
Normal file
7
config/api_base_urls.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"AnthropicApiBaseUrl": "",
|
||||
"OpenaiApiBaseUrl": "",
|
||||
"DeepseekApiBaseUrl": "",
|
||||
"AlibabaApiBaseUrl": "",
|
||||
"GoogleApiBaseUrl": ""
|
||||
}
|
||||
10
config/proxy_api.json
Normal file
10
config/proxy_api.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"apis": {
|
||||
"alibaba": "",
|
||||
"anthropic": "",
|
||||
"deepseek": "",
|
||||
"google": "",
|
||||
"openai": ""
|
||||
},
|
||||
"enabled": true
|
||||
}
|
||||
@@ -4,6 +4,11 @@ from typing import Generator
|
||||
from .base import BaseModel
|
||||
|
||||
class AnthropicModel(BaseModel):
|
||||
def __init__(self, api_key, temperature=0.7, system_prompt=None, language=None, api_base_url=None):
|
||||
super().__init__(api_key, temperature, system_prompt, language)
|
||||
# 设置API基础URL,默认为Anthropic官方API
|
||||
self.api_base_url = api_base_url or "https://api.anthropic.com/v1"
|
||||
|
||||
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
|
||||
@@ -82,8 +87,11 @@ class AnthropicModel(BaseModel):
|
||||
|
||||
print(f"Debug - 推理配置: max_tokens={max_tokens}, thinking={payload.get('thinking', payload.get('speed_mode', 'default'))}")
|
||||
|
||||
# 使用配置的API基础URL
|
||||
api_endpoint = f"{self.api_base_url}/messages"
|
||||
|
||||
response = requests.post(
|
||||
'https://api.anthropic.com/v1/messages',
|
||||
api_endpoint,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
stream=True,
|
||||
@@ -257,8 +265,11 @@ class AnthropicModel(BaseModel):
|
||||
|
||||
print(f"Debug - 图像分析推理配置: max_tokens={max_tokens}, thinking={payload.get('thinking', payload.get('speed_mode', 'default'))}")
|
||||
|
||||
# 使用配置的API基础URL
|
||||
api_endpoint = f"{self.api_base_url}/messages"
|
||||
|
||||
response = requests.post(
|
||||
'https://api.anthropic.com/v1/messages',
|
||||
api_endpoint,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
stream=True,
|
||||
|
||||
@@ -2,11 +2,12 @@ from abc import ABC, abstractmethod
|
||||
from typing import Generator, Any
|
||||
|
||||
class BaseModel(ABC):
|
||||
def __init__(self, api_key: str, temperature: float = 0.7, system_prompt: str = None, language: str = None):
|
||||
def __init__(self, api_key: str, temperature: float = 0.7, system_prompt: str = None, language: str = None, api_base_url: str = None):
|
||||
self.api_key = api_key
|
||||
self.temperature = temperature
|
||||
self.language = language
|
||||
self.system_prompt = system_prompt or self.get_default_system_prompt()
|
||||
self.api_base_url = api_base_url
|
||||
|
||||
@abstractmethod
|
||||
def analyze_image(self, image_data: str, proxies: dict = None) -> Generator[dict, None, None]:
|
||||
|
||||
@@ -80,7 +80,8 @@ class ModelFactory:
|
||||
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:
|
||||
def create_model(cls, model_name: str, api_key: str, temperature: float = 0.7,
|
||||
system_prompt: str = None, language: str = None, api_base_url: str = None) -> BaseModel:
|
||||
"""
|
||||
Create a model instance based on the model name.
|
||||
|
||||
@@ -90,6 +91,7 @@ class ModelFactory:
|
||||
temperature: The temperature to use for generation
|
||||
system_prompt: The system prompt to use
|
||||
language: The preferred language for responses
|
||||
api_base_url: The base URL for API requests
|
||||
|
||||
Returns:
|
||||
A model instance
|
||||
@@ -107,7 +109,8 @@ class ModelFactory:
|
||||
temperature=temperature,
|
||||
system_prompt=system_prompt,
|
||||
language=language,
|
||||
model_name=model_name
|
||||
model_name=model_name,
|
||||
api_base_url=api_base_url
|
||||
)
|
||||
# 对于阿里巴巴模型,也需要传递正确的模型名称
|
||||
elif 'qwen' in model_name.lower() or 'qvq' in model_name.lower() or 'alibaba' in model_name.lower():
|
||||
@@ -116,7 +119,8 @@ class ModelFactory:
|
||||
temperature=temperature,
|
||||
system_prompt=system_prompt,
|
||||
language=language,
|
||||
model_name=model_name
|
||||
model_name=model_name,
|
||||
api_base_url=api_base_url
|
||||
)
|
||||
# 对于Mathpix模型,不传递language参数
|
||||
elif model_name == 'mathpix':
|
||||
@@ -131,7 +135,8 @@ class ModelFactory:
|
||||
api_key=api_key,
|
||||
temperature=temperature,
|
||||
system_prompt=system_prompt,
|
||||
language=language
|
||||
language=language,
|
||||
api_base_url=api_base_url
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -11,7 +11,7 @@ class GoogleModel(BaseModel):
|
||||
支持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):
|
||||
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):
|
||||
"""
|
||||
初始化Google模型
|
||||
|
||||
@@ -21,13 +21,20 @@ class GoogleModel(BaseModel):
|
||||
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.max_tokens = 8192 # 默认最大输出token数
|
||||
self.api_base_url = api_base_url
|
||||
|
||||
# 配置Google API
|
||||
genai.configure(api_key=api_key)
|
||||
if api_base_url:
|
||||
# 如果提供了自定义API基础URL,设置genai的api_url
|
||||
genai.configure(api_key=api_key, transport="rest", client_options={"api_endpoint": api_base_url})
|
||||
else:
|
||||
# 使用默认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:
|
||||
|
||||
@@ -4,6 +4,11 @@ from openai import OpenAI
|
||||
from .base import BaseModel
|
||||
|
||||
class OpenAIModel(BaseModel):
|
||||
def __init__(self, api_key, temperature=0.7, system_prompt=None, language=None, api_base_url=None):
|
||||
super().__init__(api_key, temperature, system_prompt, language)
|
||||
# 设置API基础URL,默认为OpenAI官方API
|
||||
self.api_base_url = api_base_url
|
||||
|
||||
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
|
||||
@@ -35,8 +40,11 @@ class OpenAIModel(BaseModel):
|
||||
if 'https' in proxies:
|
||||
os.environ['https_proxy'] = proxies['https']
|
||||
|
||||
# Initialize OpenAI client
|
||||
client = OpenAI(api_key=self.api_key)
|
||||
# Initialize OpenAI client with base_url if provided
|
||||
if self.api_base_url:
|
||||
client = OpenAI(api_key=self.api_key, base_url=self.api_base_url)
|
||||
else:
|
||||
client = OpenAI(api_key=self.api_key)
|
||||
|
||||
# Prepare messages
|
||||
messages = [
|
||||
@@ -123,8 +131,11 @@ class OpenAIModel(BaseModel):
|
||||
if 'https' in proxies:
|
||||
os.environ['https_proxy'] = proxies['https']
|
||||
|
||||
# Initialize OpenAI client
|
||||
client = OpenAI(api_key=self.api_key)
|
||||
# Initialize OpenAI client with base_url if provided
|
||||
if self.api_base_url:
|
||||
client = OpenAI(api_key=self.api_key, base_url=self.api_base_url)
|
||||
else:
|
||||
client = OpenAI(api_key=self.api_key)
|
||||
|
||||
# 使用系统提供的系统提示词,不再自动添加语言指令
|
||||
system_prompt = self.system_prompt
|
||||
|
||||
@@ -374,6 +374,26 @@ class SettingsManager {
|
||||
// 模型选择器对象
|
||||
this.modelSelector = null;
|
||||
|
||||
// 存储API密钥的对象
|
||||
this.apiKeyValues = {
|
||||
'AnthropicApiKey': '',
|
||||
'OpenaiApiKey': '',
|
||||
'DeepseekApiKey': '',
|
||||
'AlibabaApiKey': '',
|
||||
'GoogleApiKey': '',
|
||||
'MathpixAppId': '',
|
||||
'MathpixAppKey': ''
|
||||
};
|
||||
|
||||
// 存储API基础URL的对象
|
||||
this.apiBaseUrlValues = {
|
||||
'AnthropicApiBaseUrl': '',
|
||||
'OpenaiApiBaseUrl': '',
|
||||
'DeepseekApiBaseUrl': '',
|
||||
'AlibabaApiBaseUrl': '',
|
||||
'GoogleApiBaseUrl': ''
|
||||
};
|
||||
|
||||
// 加载模型配置
|
||||
this.isInitialized = false;
|
||||
this.initialize();
|
||||
@@ -391,6 +411,12 @@ class SettingsManager {
|
||||
this.setupEventListeners();
|
||||
this.updateUIBasedOnModelType();
|
||||
|
||||
// 刷新API密钥状态
|
||||
await this.refreshApiKeyStatus();
|
||||
|
||||
// 刷新API基础URL状态
|
||||
await this.refreshApiBaseUrlStatus();
|
||||
|
||||
// 初始化可折叠内容逻辑
|
||||
this.initCollapsibleContent();
|
||||
|
||||
@@ -428,8 +454,16 @@ class SettingsManager {
|
||||
this.setupEventListeners();
|
||||
this.updateUIBasedOnModelType();
|
||||
|
||||
// 初始化可折叠内容逻辑
|
||||
this.initCollapsibleContent();
|
||||
// 刷新API密钥状态(即使在出错情况下也尝试)
|
||||
try {
|
||||
await this.refreshApiKeyStatus();
|
||||
await this.refreshApiBaseUrlStatus();
|
||||
} catch (e) {
|
||||
console.error('刷新API状态失败:', e);
|
||||
}
|
||||
|
||||
// 初始化可折叠内容逻辑
|
||||
this.initCollapsibleContent();
|
||||
|
||||
// 初始化模型选择器
|
||||
this.initModelSelector();
|
||||
@@ -513,6 +547,13 @@ class SettingsManager {
|
||||
await this.refreshApiKeyStatus();
|
||||
console.log('已自动刷新API密钥状态');
|
||||
|
||||
// 加载 API 基础 URL 设置
|
||||
if (settings.apiBaseUrlValues) {
|
||||
this.apiBaseUrlValues = settings.apiBaseUrlValues;
|
||||
await this.refreshApiBaseUrlStatus();
|
||||
console.log('已加载 API 基础 URL 设置');
|
||||
}
|
||||
|
||||
// 加载其他设置
|
||||
// Load model selection
|
||||
if (settings.model && this.modelExists(settings.model)) {
|
||||
@@ -730,6 +771,7 @@ class SettingsManager {
|
||||
// 保存UI设置到localStorage(不包含API密钥)
|
||||
const settings = {
|
||||
apiKeys: this.apiKeyValues, // 保存到localStorage(向后兼容)
|
||||
apiBaseUrlValues: this.apiBaseUrlValues, // 添加API基础URL保存到localStorage
|
||||
model: this.modelSelect.value,
|
||||
maxTokens: this.maxTokens.value,
|
||||
reasoningDepth: this.reasoningDepthSelect?.value || 'standard',
|
||||
@@ -809,6 +851,26 @@ class SettingsManager {
|
||||
const mathpixAppKey = this.apiKeyValues['MathpixAppKey'] || '';
|
||||
const mathpixApiKey = mathpixAppId && mathpixAppKey ? `${mathpixAppId}:${mathpixAppKey}` : '';
|
||||
|
||||
// 从apiBaseUrlValues映射到服务器API所需格式
|
||||
const apiBaseUrls = {};
|
||||
if (this.apiBaseUrlValues) {
|
||||
if (this.apiBaseUrlValues['AnthropicApiBaseUrl']) {
|
||||
apiBaseUrls.anthropic = this.apiBaseUrlValues['AnthropicApiBaseUrl'];
|
||||
}
|
||||
if (this.apiBaseUrlValues['OpenaiApiBaseUrl']) {
|
||||
apiBaseUrls.openai = this.apiBaseUrlValues['OpenaiApiBaseUrl'];
|
||||
}
|
||||
if (this.apiBaseUrlValues['DeepseekApiBaseUrl']) {
|
||||
apiBaseUrls.deepseek = this.apiBaseUrlValues['DeepseekApiBaseUrl'];
|
||||
}
|
||||
if (this.apiBaseUrlValues['AlibabaApiBaseUrl']) {
|
||||
apiBaseUrls.alibaba = this.apiBaseUrlValues['AlibabaApiBaseUrl'];
|
||||
}
|
||||
if (this.apiBaseUrlValues['GoogleApiBaseUrl']) {
|
||||
apiBaseUrls.google = this.apiBaseUrlValues['GoogleApiBaseUrl'];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
model: selectedModel,
|
||||
maxTokens: maxTokens,
|
||||
@@ -824,7 +886,9 @@ class SettingsManager {
|
||||
isReasoning: modelInfo.isReasoning || false,
|
||||
provider: modelInfo.provider || 'unknown'
|
||||
},
|
||||
reasoningConfig: reasoningConfig
|
||||
reasoningConfig: reasoningConfig,
|
||||
apiBaseUrls: apiBaseUrls,
|
||||
apiKeys: this.apiKeyValues // 确保传递API密钥
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1125,6 +1189,12 @@ class SettingsManager {
|
||||
if (this.modelSelectorDisplay && this.modelDropdown) {
|
||||
this.initCustomSelectorEvents();
|
||||
}
|
||||
|
||||
// 初始化API基础URL编辑功能
|
||||
this.initApiBaseUrlEditFunctions();
|
||||
|
||||
// 初始化API密钥编辑功能
|
||||
this.initApiKeyEditFunctions();
|
||||
}
|
||||
|
||||
// 更新思考预算显示
|
||||
@@ -1196,8 +1266,38 @@ class SettingsManager {
|
||||
* 初始化可折叠内容的交互逻辑
|
||||
*/
|
||||
initCollapsibleContent() {
|
||||
// 在新的实现中,我们不再需要折叠API密钥区域,因为所有功能都在同一区域完成
|
||||
console.log('初始化API密钥编辑功能完成');
|
||||
const collapsibleHeaders = document.querySelectorAll('.collapsible-header');
|
||||
|
||||
collapsibleHeaders.forEach(header => {
|
||||
header.addEventListener('click', () => {
|
||||
const content = header.nextElementSibling;
|
||||
if (content && content.classList.contains('collapsible-content')) {
|
||||
// 切换展开/折叠状态
|
||||
content.classList.toggle('expanded');
|
||||
|
||||
// 切换箭头方向
|
||||
const arrow = header.querySelector('i.fa-chevron-down, i.fa-chevron-up');
|
||||
if (arrow) {
|
||||
arrow.classList.toggle('fa-chevron-down');
|
||||
arrow.classList.toggle('fa-chevron-up');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 默认展开API基础URL设置区域
|
||||
const apiBaseUrlHeader = document.querySelector('.api-url-settings .collapsible-header');
|
||||
if (apiBaseUrlHeader) {
|
||||
const content = apiBaseUrlHeader.nextElementSibling;
|
||||
if (content) {
|
||||
content.classList.add('expanded');
|
||||
const arrow = apiBaseUrlHeader.querySelector('i.fa-chevron-down');
|
||||
if (arrow) {
|
||||
arrow.classList.remove('fa-chevron-down');
|
||||
arrow.classList.add('fa-chevron-up');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2068,6 +2168,18 @@ class SettingsManager {
|
||||
this.proxyPortInput = document.getElementById('proxyPort');
|
||||
this.proxySettings = document.getElementById('proxySettings');
|
||||
|
||||
// API基础URL相关元素
|
||||
this.apiBaseUrlsList = document.getElementById('apiBaseUrlsList');
|
||||
|
||||
// 获取所有API基础URL状态元素
|
||||
this.apiBaseUrlStatusElements = {
|
||||
'AnthropicApiBaseUrl': document.getElementById('AnthropicApiBaseUrlStatus'),
|
||||
'OpenaiApiBaseUrl': document.getElementById('OpenaiApiBaseUrlStatus'),
|
||||
'DeepseekApiBaseUrl': document.getElementById('DeepseekApiBaseUrlStatus'),
|
||||
'AlibabaApiBaseUrl': document.getElementById('AlibabaApiBaseUrlStatus'),
|
||||
'GoogleApiBaseUrl': document.getElementById('GoogleApiBaseUrlStatus')
|
||||
};
|
||||
|
||||
// 提示词管理相关元素
|
||||
this.promptSelect = document.getElementById('promptSelect');
|
||||
this.savePromptBtn = document.getElementById('savePromptBtn');
|
||||
@@ -2151,9 +2263,6 @@ class SettingsManager {
|
||||
'MathpixAppId': '',
|
||||
'MathpixAppKey': ''
|
||||
};
|
||||
|
||||
// 初始化密钥编辑功能
|
||||
this.initApiKeyEditFunctions();
|
||||
|
||||
this.reasoningOptions = document.querySelectorAll('.reasoning-option');
|
||||
this.thinkPresets = document.querySelectorAll('.think-preset');
|
||||
@@ -2219,6 +2328,282 @@ class SettingsManager {
|
||||
this.promptDialogMask.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新API基础URL状态
|
||||
*/
|
||||
async refreshApiBaseUrlStatus() {
|
||||
try {
|
||||
// 先将所有状态显示为"检查中"
|
||||
Object.keys(this.apiBaseUrlValues).forEach(urlId => {
|
||||
const statusElement = document.getElementById(`${urlId}Status`);
|
||||
if (statusElement) {
|
||||
statusElement.className = 'key-status checking';
|
||||
statusElement.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 检查中...';
|
||||
}
|
||||
});
|
||||
|
||||
// 发送请求获取API基础URL
|
||||
const response = await fetch('/api/proxy-api', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const proxyApiConfig = await response.json();
|
||||
// 提取APIs对象并更新状态
|
||||
const apiBaseUrls = {
|
||||
'AnthropicApiBaseUrl': proxyApiConfig.apis?.anthropic || '',
|
||||
'OpenaiApiBaseUrl': proxyApiConfig.apis?.openai || '',
|
||||
'DeepseekApiBaseUrl': proxyApiConfig.apis?.deepseek || '',
|
||||
'AlibabaApiBaseUrl': proxyApiConfig.apis?.alibaba || '',
|
||||
'GoogleApiBaseUrl': proxyApiConfig.apis?.google || ''
|
||||
};
|
||||
this.updateApiBaseUrlStatus(apiBaseUrls);
|
||||
console.log('API基础URL状态已刷新');
|
||||
} else {
|
||||
console.error('刷新API基础URL状态失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('刷新API基础URL状态出错:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新API基础URL状态显示
|
||||
* @param {Object} apiBaseUrls 基础URL对象
|
||||
*/
|
||||
updateApiBaseUrlStatus(apiBaseUrls) {
|
||||
if (!this.apiBaseUrlsList) return;
|
||||
|
||||
// 保存API基础URL值到内存中
|
||||
for (const [key, value] of Object.entries(apiBaseUrls)) {
|
||||
this.apiBaseUrlValues[key] = value;
|
||||
}
|
||||
|
||||
// 找到所有基础URL状态元素
|
||||
Object.keys(apiBaseUrls).forEach(urlId => {
|
||||
const statusElement = document.getElementById(`${urlId}Status`);
|
||||
if (!statusElement) return;
|
||||
|
||||
const value = apiBaseUrls[urlId];
|
||||
|
||||
if (value && value.trim() !== '') {
|
||||
// 显示基础URL状态 - 已设置
|
||||
statusElement.className = 'key-status set';
|
||||
statusElement.innerHTML = `<i class="fas fa-check-circle"></i> 已设置`;
|
||||
} else {
|
||||
// 显示基础URL状态 - 未设置
|
||||
statusElement.className = 'key-status not-set';
|
||||
statusElement.innerHTML = `<i class="fas fa-times-circle"></i> 未设置`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存单个API基础URL
|
||||
* @param {string} urlType URL类型
|
||||
* @param {string} value URL值
|
||||
* @param {HTMLElement} urlStatus URL状态容器
|
||||
*/
|
||||
async saveApiBaseUrl(urlType, value, urlStatus) {
|
||||
try {
|
||||
// 显示保存中状态
|
||||
const saveToast = this.createToast('正在保存API基础URL...', 'info', true);
|
||||
|
||||
// 获取当前中转API配置
|
||||
const response = await fetch('/api/proxy-api', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('获取现有配置失败');
|
||||
}
|
||||
|
||||
const config = await response.json();
|
||||
|
||||
// 确保apis对象存在
|
||||
if (!config.apis) {
|
||||
config.apis = {};
|
||||
}
|
||||
|
||||
// 根据URL类型更新对应的API URL
|
||||
switch(urlType) {
|
||||
case 'AnthropicApiBaseUrl':
|
||||
config.apis.anthropic = value;
|
||||
break;
|
||||
case 'OpenaiApiBaseUrl':
|
||||
config.apis.openai = value;
|
||||
break;
|
||||
case 'DeepseekApiBaseUrl':
|
||||
config.apis.deepseek = value;
|
||||
break;
|
||||
case 'AlibabaApiBaseUrl':
|
||||
config.apis.alibaba = value;
|
||||
break;
|
||||
case 'GoogleApiBaseUrl':
|
||||
config.apis.google = value;
|
||||
break;
|
||||
}
|
||||
|
||||
// 确保启用中转API
|
||||
if (value && value.trim() !== '') {
|
||||
config.enabled = true;
|
||||
}
|
||||
|
||||
// 保存到服务器
|
||||
const saveResponse = await fetch('/api/proxy-api', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
// 移除保存中提示
|
||||
if (saveToast) {
|
||||
saveToast.remove();
|
||||
}
|
||||
|
||||
if (saveResponse.ok) {
|
||||
const result = await saveResponse.json();
|
||||
if (result.success) {
|
||||
// 更新基础URL状态显示
|
||||
const statusElem = document.getElementById(`${urlType}Status`);
|
||||
if (statusElem) {
|
||||
if (value && value.trim() !== '') {
|
||||
statusElem.className = 'key-status set';
|
||||
statusElem.innerHTML = `<i class="fas fa-check-circle"></i> 已设置`;
|
||||
} else {
|
||||
statusElem.className = 'key-status not-set';
|
||||
statusElem.innerHTML = `<i class="fas fa-times-circle"></i> 未设置`;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存到内存
|
||||
this.apiBaseUrlValues[urlType] = value;
|
||||
|
||||
// 显示成功提示
|
||||
this.createToast('API基础URL已保存', 'success');
|
||||
} else {
|
||||
this.createToast(`保存失败: ${result.message || '未知错误'}`, 'error');
|
||||
}
|
||||
} else {
|
||||
this.createToast('保存API基础URL失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存API基础URL错误:', error);
|
||||
this.createToast(`保存失败: ${error.message || '未知错误'}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化API基础URL编辑相关功能
|
||||
*/
|
||||
initApiBaseUrlEditFunctions() {
|
||||
// 1. 编辑按钮点击事件
|
||||
document.querySelectorAll('.edit-api-base-url').forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
// 阻止事件冒泡
|
||||
e.stopPropagation();
|
||||
|
||||
const urlType = e.currentTarget.getAttribute('data-key-type');
|
||||
const urlStatus = e.currentTarget.closest('.key-status-wrapper');
|
||||
|
||||
if (urlStatus) {
|
||||
// 隐藏显示区域
|
||||
const displayArea = urlStatus.querySelector('.key-display');
|
||||
if (displayArea) displayArea.classList.add('hidden');
|
||||
|
||||
// 显示编辑区域
|
||||
const editArea = urlStatus.querySelector('.key-edit');
|
||||
if (editArea) {
|
||||
editArea.classList.remove('hidden');
|
||||
|
||||
// 获取当前URL值并填入输入框
|
||||
const urlInput = editArea.querySelector('.key-input');
|
||||
if (urlInput) {
|
||||
// 从状态文本中获取当前值(如果不是"未设置")
|
||||
const statusElement = urlStatus.querySelector('.key-status');
|
||||
if (statusElement && statusElement.textContent !== '未设置') {
|
||||
urlInput.value = this.apiBaseUrlValues[urlType] || '';
|
||||
} else {
|
||||
urlInput.value = '';
|
||||
}
|
||||
|
||||
// 聚焦输入框
|
||||
setTimeout(() => {
|
||||
urlInput.focus();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 2. 保存按钮点击事件
|
||||
document.querySelectorAll('.save-api-base-url').forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
// 阻止事件冒泡
|
||||
e.stopPropagation();
|
||||
|
||||
const urlType = e.currentTarget.getAttribute('data-key-type');
|
||||
const urlStatus = e.currentTarget.closest('.key-status-wrapper');
|
||||
|
||||
if (urlStatus) {
|
||||
// 获取输入的新URL值
|
||||
const urlInput = urlStatus.querySelector('.key-input');
|
||||
if (urlInput) {
|
||||
const newValue = urlInput.value.trim();
|
||||
|
||||
// 保存到服务器
|
||||
this.saveApiBaseUrl(urlType, newValue, urlStatus);
|
||||
|
||||
// 隐藏编辑区域
|
||||
const editArea = urlStatus.querySelector('.key-edit');
|
||||
if (editArea) editArea.classList.add('hidden');
|
||||
|
||||
// 显示状态区域
|
||||
const displayArea = urlStatus.querySelector('.key-display');
|
||||
if (displayArea) displayArea.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 3. 输入框按下Enter保存
|
||||
document.querySelectorAll('#apiBaseUrlsList .key-input').forEach(input => {
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
// 阻止事件冒泡
|
||||
e.stopPropagation();
|
||||
|
||||
const saveButton = e.currentTarget.closest('.key-edit').querySelector('.save-api-base-url');
|
||||
if (saveButton) {
|
||||
saveButton.click();
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
// 阻止事件冒泡
|
||||
e.stopPropagation();
|
||||
|
||||
// 取消编辑
|
||||
const urlStatus = e.currentTarget.closest('.key-status-wrapper');
|
||||
if (urlStatus) {
|
||||
const editArea = urlStatus.querySelector('.key-edit');
|
||||
if (editArea) editArea.classList.add('hidden');
|
||||
|
||||
const displayArea = urlStatus.querySelector('.key-display');
|
||||
if (displayArea) displayArea.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other modules
|
||||
|
||||
@@ -5323,3 +5323,41 @@ textarea,
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* API基础URL设置区域 */
|
||||
.api-url-settings {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.api-url-settings .collapsible-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.api-url-settings .collapsible-header:hover {
|
||||
background-color: var(--bg-hover);
|
||||
}
|
||||
|
||||
.api-url-settings .collapsible-content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
.api-url-settings .collapsible-content.expanded {
|
||||
max-height: 1000px;
|
||||
}
|
||||
|
||||
.api-url-settings small {
|
||||
color: var(--text-muted);
|
||||
font-weight: normal;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
@@ -472,6 +472,116 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加API基础URL设置区域 -->
|
||||
<div class="settings-section api-url-settings">
|
||||
<h3><i class="fas fa-link"></i> API基础URL设置</h3>
|
||||
<div class="setting-group">
|
||||
<div class="collapsible-header">
|
||||
<span><i class="fas fa-info-circle"></i> 中转API配置 <small>(可选)</small></span>
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<div class="collapsible-content">
|
||||
<div class="api-keys-list" id="apiBaseUrlsList">
|
||||
<div class="api-key-status">
|
||||
<span class="key-name">Anthropic API URL:</span>
|
||||
<div class="key-status-wrapper">
|
||||
<!-- 显示状态 -->
|
||||
<div class="key-display">
|
||||
<span id="AnthropicApiBaseUrlStatus" class="key-status" data-key="AnthropicApiBaseUrl">未设置</span>
|
||||
<button class="btn-icon edit-api-base-url" data-key-type="AnthropicApiBaseUrl" title="编辑此URL">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 编辑状态 -->
|
||||
<div class="key-edit hidden">
|
||||
<input type="text" class="key-input" data-key-type="AnthropicApiBaseUrl" placeholder="https://api.anthropic.com/v1">
|
||||
<button class="btn-icon save-api-base-url" data-key-type="AnthropicApiBaseUrl" title="保存URL">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="api-key-status">
|
||||
<span class="key-name">OpenAI API URL:</span>
|
||||
<div class="key-status-wrapper">
|
||||
<!-- 显示状态 -->
|
||||
<div class="key-display">
|
||||
<span id="OpenaiApiBaseUrlStatus" class="key-status" data-key="OpenaiApiBaseUrl">未设置</span>
|
||||
<button class="btn-icon edit-api-base-url" data-key-type="OpenaiApiBaseUrl" title="编辑此URL">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 编辑状态 -->
|
||||
<div class="key-edit hidden">
|
||||
<input type="text" class="key-input" data-key-type="OpenaiApiBaseUrl" placeholder="https://api.openai.com/v1">
|
||||
<button class="btn-icon save-api-base-url" data-key-type="OpenaiApiBaseUrl" title="保存URL">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="api-key-status">
|
||||
<span class="key-name">DeepSeek API URL:</span>
|
||||
<div class="key-status-wrapper">
|
||||
<!-- 显示状态 -->
|
||||
<div class="key-display">
|
||||
<span id="DeepseekApiBaseUrlStatus" class="key-status" data-key="DeepseekApiBaseUrl">未设置</span>
|
||||
<button class="btn-icon edit-api-base-url" data-key-type="DeepseekApiBaseUrl" title="编辑此URL">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 编辑状态 -->
|
||||
<div class="key-edit hidden">
|
||||
<input type="text" class="key-input" data-key-type="DeepseekApiBaseUrl" placeholder="https://api.deepseek.com/v1">
|
||||
<button class="btn-icon save-api-base-url" data-key-type="DeepseekApiBaseUrl" title="保存URL">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="api-key-status">
|
||||
<span class="key-name">Alibaba API URL:</span>
|
||||
<div class="key-status-wrapper">
|
||||
<!-- 显示状态 -->
|
||||
<div class="key-display">
|
||||
<span id="AlibabaApiBaseUrlStatus" class="key-status" data-key="AlibabaApiBaseUrl">未设置</span>
|
||||
<button class="btn-icon edit-api-base-url" data-key-type="AlibabaApiBaseUrl" title="编辑此URL">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 编辑状态 -->
|
||||
<div class="key-edit hidden">
|
||||
<input type="text" class="key-input" data-key-type="AlibabaApiBaseUrl" placeholder="https://dashscope.aliyuncs.com/api/v1">
|
||||
<button class="btn-icon save-api-base-url" data-key-type="AlibabaApiBaseUrl" title="保存URL">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="api-key-status">
|
||||
<span class="key-name">Google API URL:</span>
|
||||
<div class="key-status-wrapper">
|
||||
<!-- 显示状态 -->
|
||||
<div class="key-display">
|
||||
<span id="GoogleApiBaseUrlStatus" class="key-status" data-key="GoogleApiBaseUrl">未设置</span>
|
||||
<button class="btn-icon edit-api-base-url" data-key-type="GoogleApiBaseUrl" title="编辑此URL">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 编辑状态 -->
|
||||
<div class="key-edit hidden">
|
||||
<input type="text" class="key-input" data-key-type="GoogleApiBaseUrl" placeholder="https://generativelanguage.googleapis.com/v1beta">
|
||||
<button class="btn-icon save-api-base-url" data-key-type="GoogleApiBaseUrl" title="保存URL">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 不常用的其他设置放在后面 -->
|
||||
<div class="settings-section proxy-settings-section">
|
||||
<h3><i class="fas fa-cog"></i> 其他设置</h3>
|
||||
|
||||
Reference in New Issue
Block a user