mirror of
https://github.com/Zippland/Snap-Solver.git
synced 2026-01-19 09:41:15 +08:00
支持自定义api接口地址
This commit is contained in:
106
app.py
106
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')
|
VERSION_FILE = os.path.join(CONFIG_DIR, 'version.json')
|
||||||
UPDATE_INFO_FILE = os.path.join(CONFIG_DIR, 'update_info.json')
|
UPDATE_INFO_FILE = os.path.join(CONFIG_DIR, 'update_info.json')
|
||||||
PROMPT_FILE = os.path.join(CONFIG_DIR, 'prompts.json') # 新增提示词配置文件路径
|
PROMPT_FILE = os.path.join(CONFIG_DIR, 'prompts.json') # 新增提示词配置文件路径
|
||||||
|
PROXY_API_FILE = os.path.join(CONFIG_DIR, 'proxy_api.json') # 新增中转API配置文件路径
|
||||||
|
|
||||||
# 跟踪用户生成任务的字典
|
# 跟踪用户生成任务的字典
|
||||||
generation_tasks = {}
|
generation_tasks = {}
|
||||||
@@ -114,13 +115,31 @@ def create_model_instance(model_id, settings, is_reasoning=False):
|
|||||||
# 获取maxTokens参数,默认为8192
|
# 获取maxTokens参数,默认为8192
|
||||||
max_tokens = int(settings.get('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', '')
|
||||||
|
|
||||||
# 创建模型实例
|
# 创建模型实例
|
||||||
model_instance = ModelFactory.create_model(
|
model_instance = ModelFactory.create_model(
|
||||||
model_name=model_id,
|
model_name=model_id,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
temperature=None if is_reasoning else float(settings.get('temperature', 0.7)),
|
temperature=None if is_reasoning else float(settings.get('temperature', 0.7)),
|
||||||
system_prompt=settings.get('systemPrompt'),
|
system_prompt=settings.get('systemPrompt'),
|
||||||
language=settings.get('language', '中文')
|
language=settings.get('language', '中文'),
|
||||||
|
base_url=base_url # 添加中转API URL
|
||||||
)
|
)
|
||||||
|
|
||||||
# 设置最大输出Token,但不为阿里巴巴模型设置(它们有自己内部的处理逻辑)
|
# 设置最大输出Token,但不为阿里巴巴模型设置(它们有自己内部的处理逻辑)
|
||||||
@@ -159,24 +178,6 @@ def stream_model_response(response_generator, sid, model_name=None):
|
|||||||
for response in response_generator:
|
for response in response_generator:
|
||||||
# 处理Mathpix响应
|
# 处理Mathpix响应
|
||||||
if isinstance(response.get('content', ''), str) and 'mathpix' in response.get('model', ''):
|
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:
|
if current_time - last_emit_time >= 0.3:
|
||||||
socketio.emit('ai_response', {
|
socketio.emit('ai_response', {
|
||||||
'status': 'thinking',
|
'status': 'thinking',
|
||||||
@@ -774,9 +775,47 @@ def load_api_keys():
|
|||||||
print(f"加载API密钥配置失败: {e}")
|
print(f"加载API密钥配置失败: {e}")
|
||||||
return {}
|
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密钥配置
|
# 保存API密钥配置
|
||||||
def save_api_keys(api_keys):
|
def save_api_keys(api_keys):
|
||||||
"""保存API密钥到配置文件"""
|
|
||||||
try:
|
try:
|
||||||
# 确保配置目录存在
|
# 确保配置目录存在
|
||||||
os.makedirs(os.path.dirname(API_KEYS_FILE), exist_ok=True)
|
os.makedirs(os.path.dirname(API_KEYS_FILE), exist_ok=True)
|
||||||
@@ -879,6 +918,33 @@ def remove_prompt(prompt_id):
|
|||||||
print(f"删除提示词时出错: {e}")
|
print(f"删除提示词时出错: {e}")
|
||||||
return jsonify({"error": str(e)}), 500
|
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__':
|
if __name__ == '__main__':
|
||||||
local_ip = get_local_ip()
|
local_ip = get_local_ip()
|
||||||
print(f"Local IP Address: {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": ""
|
||||||
|
}
|
||||||
@@ -4,6 +4,11 @@ from typing import Generator
|
|||||||
from .base import BaseModel
|
from .base import BaseModel
|
||||||
|
|
||||||
class AnthropicModel(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:
|
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:
|
||||||
1. First read and understand the question carefully
|
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'))}")
|
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(
|
response = requests.post(
|
||||||
'https://api.anthropic.com/v1/messages',
|
api_endpoint,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json=payload,
|
json=payload,
|
||||||
stream=True,
|
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'))}")
|
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(
|
response = requests.post(
|
||||||
'https://api.anthropic.com/v1/messages',
|
api_endpoint,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json=payload,
|
json=payload,
|
||||||
stream=True,
|
stream=True,
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ class ModelFactory:
|
|||||||
print(f"无法加载基础Mathpix工具: {str(e)}")
|
print(f"无法加载基础Mathpix工具: {str(e)}")
|
||||||
|
|
||||||
@classmethod
|
@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.
|
Create a model instance based on the model name.
|
||||||
|
|
||||||
@@ -90,6 +91,7 @@ class ModelFactory:
|
|||||||
temperature: The temperature to use for generation
|
temperature: The temperature to use for generation
|
||||||
system_prompt: The system prompt to use
|
system_prompt: The system prompt to use
|
||||||
language: The preferred language for responses
|
language: The preferred language for responses
|
||||||
|
api_base_url: The base URL for API requests
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A model instance
|
A model instance
|
||||||
@@ -107,7 +109,8 @@ class ModelFactory:
|
|||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
language=language,
|
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():
|
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,
|
temperature=temperature,
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
language=language,
|
language=language,
|
||||||
model_name=model_name
|
model_name=model_name,
|
||||||
|
api_base_url=api_base_url
|
||||||
)
|
)
|
||||||
# 对于Mathpix模型,不传递language参数
|
# 对于Mathpix模型,不传递language参数
|
||||||
elif model_name == 'mathpix':
|
elif model_name == 'mathpix':
|
||||||
@@ -131,7 +135,8 @@ class ModelFactory:
|
|||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
language=language
|
language=language,
|
||||||
|
api_base_url=api_base_url
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class GoogleModel(BaseModel):
|
|||||||
支持Gemini 2.5 Pro等模型,可处理文本和图像输入
|
支持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模型
|
初始化Google模型
|
||||||
|
|
||||||
@@ -21,13 +21,20 @@ class GoogleModel(BaseModel):
|
|||||||
system_prompt: 系统提示词
|
system_prompt: 系统提示词
|
||||||
language: 首选语言
|
language: 首选语言
|
||||||
model_name: 指定具体模型名称,如不指定则使用默认值
|
model_name: 指定具体模型名称,如不指定则使用默认值
|
||||||
|
api_base_url: API基础URL,用于设置自定义API端点
|
||||||
"""
|
"""
|
||||||
super().__init__(api_key, temperature, system_prompt, language)
|
super().__init__(api_key, temperature, system_prompt, language)
|
||||||
self.model_name = model_name or self.get_model_identifier()
|
self.model_name = model_name or self.get_model_identifier()
|
||||||
self.max_tokens = 8192 # 默认最大输出token数
|
self.max_tokens = 8192 # 默认最大输出token数
|
||||||
|
self.api_base_url = api_base_url
|
||||||
|
|
||||||
# 配置Google API
|
# 配置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:
|
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:
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ from openai import OpenAI
|
|||||||
from .base import BaseModel
|
from .base import BaseModel
|
||||||
|
|
||||||
class OpenAIModel(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:
|
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:
|
||||||
1. First read and understand the question carefully
|
1. First read and understand the question carefully
|
||||||
@@ -35,8 +40,11 @@ class OpenAIModel(BaseModel):
|
|||||||
if 'https' in proxies:
|
if 'https' in proxies:
|
||||||
os.environ['https_proxy'] = proxies['https']
|
os.environ['https_proxy'] = proxies['https']
|
||||||
|
|
||||||
# Initialize OpenAI client
|
# Initialize OpenAI client with base_url if provided
|
||||||
client = OpenAI(api_key=self.api_key)
|
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
|
# Prepare messages
|
||||||
messages = [
|
messages = [
|
||||||
@@ -123,8 +131,11 @@ class OpenAIModel(BaseModel):
|
|||||||
if 'https' in proxies:
|
if 'https' in proxies:
|
||||||
os.environ['https_proxy'] = proxies['https']
|
os.environ['https_proxy'] = proxies['https']
|
||||||
|
|
||||||
# Initialize OpenAI client
|
# Initialize OpenAI client with base_url if provided
|
||||||
client = OpenAI(api_key=self.api_key)
|
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
|
system_prompt = self.system_prompt
|
||||||
|
|||||||
@@ -374,6 +374,26 @@ class SettingsManager {
|
|||||||
// 模型选择器对象
|
// 模型选择器对象
|
||||||
this.modelSelector = null;
|
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.isInitialized = false;
|
||||||
this.initialize();
|
this.initialize();
|
||||||
@@ -391,6 +411,12 @@ class SettingsManager {
|
|||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
this.updateUIBasedOnModelType();
|
this.updateUIBasedOnModelType();
|
||||||
|
|
||||||
|
// 刷新API密钥状态
|
||||||
|
await this.refreshApiKeyStatus();
|
||||||
|
|
||||||
|
// 刷新API基础URL状态
|
||||||
|
await this.refreshApiBaseUrlStatus();
|
||||||
|
|
||||||
// 初始化可折叠内容逻辑
|
// 初始化可折叠内容逻辑
|
||||||
this.initCollapsibleContent();
|
this.initCollapsibleContent();
|
||||||
|
|
||||||
@@ -824,7 +850,8 @@ class SettingsManager {
|
|||||||
isReasoning: modelInfo.isReasoning || false,
|
isReasoning: modelInfo.isReasoning || false,
|
||||||
provider: modelInfo.provider || 'unknown'
|
provider: modelInfo.provider || 'unknown'
|
||||||
},
|
},
|
||||||
reasoningConfig: reasoningConfig
|
reasoningConfig: reasoningConfig,
|
||||||
|
apiBaseUrls: this.apiBaseUrlValues // 添加API基础URL值
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1125,6 +1152,9 @@ class SettingsManager {
|
|||||||
if (this.modelSelectorDisplay && this.modelDropdown) {
|
if (this.modelSelectorDisplay && this.modelDropdown) {
|
||||||
this.initCustomSelectorEvents();
|
this.initCustomSelectorEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化API基础URL编辑功能
|
||||||
|
this.initApiBaseUrlEditFunctions();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新思考预算显示
|
// 更新思考预算显示
|
||||||
@@ -1196,8 +1226,38 @@ class SettingsManager {
|
|||||||
* 初始化可折叠内容的交互逻辑
|
* 初始化可折叠内容的交互逻辑
|
||||||
*/
|
*/
|
||||||
initCollapsibleContent() {
|
initCollapsibleContent() {
|
||||||
// 在新的实现中,我们不再需要折叠API密钥区域,因为所有功能都在同一区域完成
|
const collapsibleHeaders = document.querySelectorAll('.collapsible-header');
|
||||||
console.log('初始化API密钥编辑功能完成');
|
|
||||||
|
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 +2128,18 @@ class SettingsManager {
|
|||||||
this.proxyPortInput = document.getElementById('proxyPort');
|
this.proxyPortInput = document.getElementById('proxyPort');
|
||||||
this.proxySettings = document.getElementById('proxySettings');
|
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.promptSelect = document.getElementById('promptSelect');
|
||||||
this.savePromptBtn = document.getElementById('savePromptBtn');
|
this.savePromptBtn = document.getElementById('savePromptBtn');
|
||||||
@@ -2219,6 +2291,235 @@ class SettingsManager {
|
|||||||
this.promptDialogMask.classList.remove('hidden');
|
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/base_urls', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const apiBaseUrls = await response.json();
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 创建要保存的数据对象
|
||||||
|
const apiBaseUrlsData = {};
|
||||||
|
apiBaseUrlsData[urlType] = value;
|
||||||
|
|
||||||
|
// 发送到服务器
|
||||||
|
const response = await fetch('/api/base_urls', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(apiBaseUrlsData)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移除保存中提示
|
||||||
|
if (saveToast) {
|
||||||
|
saveToast.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.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
|
// Export for use in other modules
|
||||||
|
|||||||
@@ -5323,3 +5323,41 @@ textarea,
|
|||||||
display: none;
|
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>
|
||||||
</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. 不常用的其他设置放在后面 -->
|
<!-- 3. 不常用的其他设置放在后面 -->
|
||||||
<div class="settings-section proxy-settings-section">
|
<div class="settings-section proxy-settings-section">
|
||||||
<h3><i class="fas fa-cog"></i> 其他设置</h3>
|
<h3><i class="fas fa-cog"></i> 其他设置</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user