mirror of
https://github.com/Zippland/Snap-Solver.git
synced 2026-03-03 16:45:26 +08:00
添加API密钥管理功能,包括获取、更新和验证API密钥的接口,增强了对Mathpix和其他API的支持;优化了设置面板的用户体验,改进了API密钥的状态显示和编辑功能,确保用户能够方便地管理和验证API密钥。
This commit is contained in:
255
app.py
255
app.py
@@ -32,6 +32,9 @@ socketio = SocketIO(
|
|||||||
# 添加配置文件路径
|
# 添加配置文件路径
|
||||||
CONFIG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config')
|
CONFIG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config')
|
||||||
|
|
||||||
|
# API密钥配置文件路径
|
||||||
|
API_KEYS_FILE = os.path.join(CONFIG_DIR, 'api_keys.json')
|
||||||
|
|
||||||
# 跟踪用户生成任务的字典
|
# 跟踪用户生成任务的字典
|
||||||
generation_tasks = {}
|
generation_tasks = {}
|
||||||
|
|
||||||
@@ -76,7 +79,11 @@ def create_model_instance(model_id, settings, is_reasoning=False):
|
|||||||
|
|
||||||
# 确定需要哪个API密钥
|
# 确定需要哪个API密钥
|
||||||
api_key_id = None
|
api_key_id = None
|
||||||
if "claude" in model_id.lower() or "anthropic" in model_id.lower():
|
# 特殊情况:o3-mini使用OpenAI API密钥
|
||||||
|
if model_id.lower() == "o3-mini":
|
||||||
|
api_key_id = "OpenaiApiKey"
|
||||||
|
# 其他Anthropic/Claude模型
|
||||||
|
elif "claude" in model_id.lower() or "anthropic" in model_id.lower():
|
||||||
api_key_id = "AnthropicApiKey"
|
api_key_id = "AnthropicApiKey"
|
||||||
elif any(keyword in model_id.lower() for keyword in ["gpt", "openai"]):
|
elif any(keyword in model_id.lower() for keyword in ["gpt", "openai"]):
|
||||||
api_key_id = "OpenaiApiKey"
|
api_key_id = "OpenaiApiKey"
|
||||||
@@ -85,7 +92,13 @@ def create_model_instance(model_id, settings, is_reasoning=False):
|
|||||||
elif "qvq" in model_id.lower() or "alibaba" in model_id.lower() or "qwen" in model_id.lower():
|
elif "qvq" in model_id.lower() or "alibaba" in model_id.lower() or "qwen" in model_id.lower():
|
||||||
api_key_id = "AlibabaApiKey"
|
api_key_id = "AlibabaApiKey"
|
||||||
|
|
||||||
api_key = api_keys.get(api_key_id)
|
# 首先尝试从本地配置获取API密钥
|
||||||
|
api_key = get_api_key(api_key_id)
|
||||||
|
|
||||||
|
# 如果本地没有配置,尝试使用前端传递的密钥(向后兼容)
|
||||||
|
if not api_key:
|
||||||
|
api_key = api_keys.get(api_key_id)
|
||||||
|
|
||||||
if not api_key:
|
if not api_key:
|
||||||
raise ValueError(f"API key is required for the selected model (keyId: {api_key_id})")
|
raise ValueError(f"API key is required for the selected model (keyId: {api_key_id})")
|
||||||
|
|
||||||
@@ -266,7 +279,17 @@ def handle_text_extraction(data):
|
|||||||
if not isinstance(settings, dict):
|
if not isinstance(settings, dict):
|
||||||
raise ValueError("Invalid settings format")
|
raise ValueError("Invalid settings format")
|
||||||
|
|
||||||
mathpix_key = settings.get('mathpixApiKey')
|
# 尝试从本地配置获取Mathpix API密钥
|
||||||
|
mathpix_app_id = get_api_key('MathpixAppId')
|
||||||
|
mathpix_app_key = get_api_key('MathpixAppKey')
|
||||||
|
|
||||||
|
# 构建完整的Mathpix API密钥(格式:app_id:app_key)
|
||||||
|
mathpix_key = f"{mathpix_app_id}:{mathpix_app_key}" if mathpix_app_id and mathpix_app_key else None
|
||||||
|
|
||||||
|
# 如果本地没有配置,尝试使用前端传递的密钥(向后兼容)
|
||||||
|
if not mathpix_key:
|
||||||
|
mathpix_key = settings.get('mathpixApiKey')
|
||||||
|
|
||||||
if not mathpix_key:
|
if not mathpix_key:
|
||||||
raise ValueError("Mathpix API key is required")
|
raise ValueError("Mathpix API key is required")
|
||||||
|
|
||||||
@@ -624,6 +647,232 @@ def get_models():
|
|||||||
models = ModelFactory.get_available_models()
|
models = ModelFactory.get_available_models()
|
||||||
return jsonify(models)
|
return jsonify(models)
|
||||||
|
|
||||||
|
# 获取所有API密钥
|
||||||
|
@app.route('/api/keys', methods=['GET'])
|
||||||
|
def get_api_keys():
|
||||||
|
"""获取所有API密钥"""
|
||||||
|
api_keys = load_api_keys()
|
||||||
|
return jsonify(api_keys)
|
||||||
|
|
||||||
|
# 保存API密钥
|
||||||
|
@app.route('/api/keys', methods=['POST'])
|
||||||
|
def update_api_keys():
|
||||||
|
"""更新API密钥配置"""
|
||||||
|
try:
|
||||||
|
new_keys = request.json
|
||||||
|
if not isinstance(new_keys, dict):
|
||||||
|
return jsonify({"success": False, "message": "无效的API密钥格式"}), 400
|
||||||
|
|
||||||
|
# 加载当前密钥
|
||||||
|
current_keys = load_api_keys()
|
||||||
|
|
||||||
|
# 更新密钥
|
||||||
|
for key, value in new_keys.items():
|
||||||
|
current_keys[key] = value
|
||||||
|
|
||||||
|
# 保存回文件
|
||||||
|
if save_api_keys(current_keys):
|
||||||
|
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
|
||||||
|
|
||||||
|
# 验证API密钥
|
||||||
|
@app.route('/api/keys/validate', methods=['POST'])
|
||||||
|
def validate_api_key():
|
||||||
|
"""验证API密钥有效性"""
|
||||||
|
try:
|
||||||
|
validate_data = request.json
|
||||||
|
if not isinstance(validate_data, dict) or 'key_type' not in validate_data or 'key_value' not in validate_data:
|
||||||
|
return jsonify({"success": False, "message": "无效的请求格式"}), 400
|
||||||
|
|
||||||
|
key_type = validate_data['key_type']
|
||||||
|
key_value = validate_data['key_value']
|
||||||
|
|
||||||
|
if not key_value or key_value.strip() == "":
|
||||||
|
return jsonify({"success": False, "message": f"未提供{key_type}密钥"}), 400
|
||||||
|
|
||||||
|
# 基于密钥类型进行验证
|
||||||
|
result = {"success": False, "message": "不支持的密钥类型"}
|
||||||
|
|
||||||
|
if key_type == "AnthropicApiKey":
|
||||||
|
result = validate_anthropic_key(key_value)
|
||||||
|
elif key_type == "OpenaiApiKey":
|
||||||
|
result = validate_openai_key(key_value)
|
||||||
|
elif key_type == "DeepseekApiKey":
|
||||||
|
result = validate_deepseek_key(key_value)
|
||||||
|
elif key_type == "AlibabaApiKey":
|
||||||
|
result = validate_alibaba_key(key_value)
|
||||||
|
elif key_type == "MathpixAppId" or key_type == "MathpixAppKey":
|
||||||
|
# Mathpix需要两个密钥一起验证
|
||||||
|
mathpix_app_id = key_value if key_type == "MathpixAppId" else get_api_key("MathpixAppId")
|
||||||
|
mathpix_app_key = key_value if key_type == "MathpixAppKey" else get_api_key("MathpixAppKey")
|
||||||
|
if mathpix_app_id and mathpix_app_key:
|
||||||
|
result = validate_mathpix_key(f"{mathpix_app_id}:{mathpix_app_key}")
|
||||||
|
else:
|
||||||
|
result = {"success": False, "message": "需要同时设置Mathpix App ID和App Key"}
|
||||||
|
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"success": False, "message": f"验证API密钥时出错: {str(e)}"}), 500
|
||||||
|
|
||||||
|
def validate_anthropic_key(api_key):
|
||||||
|
"""验证Anthropic API密钥"""
|
||||||
|
try:
|
||||||
|
import anthropic
|
||||||
|
client = anthropic.Anthropic(api_key=api_key)
|
||||||
|
# 发送一个简单请求来验证密钥
|
||||||
|
response = client.messages.create(
|
||||||
|
model="claude-3-haiku-20240307",
|
||||||
|
max_tokens=10,
|
||||||
|
messages=[{"role": "user", "content": "Hello"}]
|
||||||
|
)
|
||||||
|
return {"success": True, "message": "Anthropic API密钥有效"}
|
||||||
|
except Exception as e:
|
||||||
|
error_message = str(e)
|
||||||
|
if "401" in error_message or "unauthorized" in error_message.lower():
|
||||||
|
return {"success": False, "message": "Anthropic API密钥无效或已过期"}
|
||||||
|
return {"success": False, "message": f"验证Anthropic API密钥时出错: {error_message}"}
|
||||||
|
|
||||||
|
def validate_openai_key(api_key):
|
||||||
|
"""验证OpenAI API密钥"""
|
||||||
|
try:
|
||||||
|
import openai
|
||||||
|
client = openai.OpenAI(api_key=api_key)
|
||||||
|
# 发送一个简单请求来验证密钥
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="gpt-3.5-turbo",
|
||||||
|
messages=[{"role": "user", "content": "Hello"}],
|
||||||
|
max_tokens=10
|
||||||
|
)
|
||||||
|
return {"success": True, "message": "OpenAI API密钥有效"}
|
||||||
|
except Exception as e:
|
||||||
|
error_message = str(e)
|
||||||
|
if "401" in error_message or "invalid" in error_message.lower():
|
||||||
|
return {"success": False, "message": "OpenAI API密钥无效或已过期"}
|
||||||
|
return {"success": False, "message": f"验证OpenAI API密钥时出错: {error_message}"}
|
||||||
|
|
||||||
|
def validate_deepseek_key(api_key):
|
||||||
|
"""验证DeepSeek API密钥"""
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {api_key}"
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
"messages": [{"role": "user", "content": "Hello"}],
|
||||||
|
"max_tokens": 10
|
||||||
|
}
|
||||||
|
response = requests.post(
|
||||||
|
"https://api.deepseek.com/v1/chat/completions",
|
||||||
|
headers=headers,
|
||||||
|
json=data,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return {"success": True, "message": "DeepSeek API密钥有效"}
|
||||||
|
else:
|
||||||
|
error_message = response.json().get("error", {}).get("message", "未知错误")
|
||||||
|
return {"success": False, "message": f"DeepSeek API密钥无效: {error_message}"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "message": f"验证DeepSeek API密钥时出错: {str(e)}"}
|
||||||
|
|
||||||
|
def validate_alibaba_key(api_key):
|
||||||
|
"""验证阿里巴巴DashScope API密钥"""
|
||||||
|
try:
|
||||||
|
import dashscope
|
||||||
|
dashscope.api_key = api_key
|
||||||
|
response = dashscope.Generation.call(
|
||||||
|
model='qwen-vl-plus',
|
||||||
|
messages=[{'role': 'user', 'content': 'Hello'}],
|
||||||
|
result_format='message',
|
||||||
|
max_tokens=10
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return {"success": True, "message": "阿里巴巴API密钥有效"}
|
||||||
|
else:
|
||||||
|
error_message = response.message
|
||||||
|
return {"success": False, "message": f"阿里巴巴API密钥无效: {error_message}"}
|
||||||
|
except Exception as e:
|
||||||
|
error_message = str(e)
|
||||||
|
if "unauthorized" in error_message.lower() or "invalid" in error_message.lower():
|
||||||
|
return {"success": False, "message": "阿里巴巴API密钥无效或已过期"}
|
||||||
|
return {"success": False, "message": f"验证阿里巴巴API密钥时出错: {error_message}"}
|
||||||
|
|
||||||
|
def validate_mathpix_key(api_key):
|
||||||
|
"""验证Mathpix API密钥"""
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
# 分解API密钥
|
||||||
|
app_id, app_key = api_key.split(":")
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"app_id": app_id,
|
||||||
|
"app_key": app_key,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
# 构造一个简单的请求(只检查API密钥,不实际发送图像)
|
||||||
|
response = requests.get(
|
||||||
|
"https://api.mathpix.com/v3/app-setting",
|
||||||
|
headers=headers,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return {"success": True, "message": "Mathpix API密钥有效"}
|
||||||
|
else:
|
||||||
|
error_message = response.json().get("error", "未知错误")
|
||||||
|
return {"success": False, "message": f"Mathpix API密钥无效: {error_message}"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "message": f"验证Mathpix API密钥时出错: {str(e)}"}
|
||||||
|
|
||||||
|
# 加载API密钥配置
|
||||||
|
def load_api_keys():
|
||||||
|
"""从配置文件加载API密钥"""
|
||||||
|
try:
|
||||||
|
if os.path.exists(API_KEYS_FILE):
|
||||||
|
with open(API_KEYS_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
else:
|
||||||
|
# 如果文件不存在,创建默认配置
|
||||||
|
default_keys = {
|
||||||
|
"AnthropicApiKey": "",
|
||||||
|
"OpenaiApiKey": "",
|
||||||
|
"DeepseekApiKey": "",
|
||||||
|
"AlibabaApiKey": "",
|
||||||
|
"MathpixAppId": "",
|
||||||
|
"MathpixAppKey": ""
|
||||||
|
}
|
||||||
|
save_api_keys(default_keys)
|
||||||
|
return default_keys
|
||||||
|
except Exception as e:
|
||||||
|
print(f"加载API密钥配置失败: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# 保存API密钥配置
|
||||||
|
def save_api_keys(api_keys):
|
||||||
|
"""保存API密钥到配置文件"""
|
||||||
|
try:
|
||||||
|
# 确保配置目录存在
|
||||||
|
os.makedirs(os.path.dirname(API_KEYS_FILE), exist_ok=True)
|
||||||
|
|
||||||
|
with open(API_KEYS_FILE, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(api_keys, f, ensure_ascii=False, indent=2)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"保存API密钥配置失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 获取特定API密钥
|
||||||
|
def get_api_key(key_name):
|
||||||
|
"""获取指定的API密钥"""
|
||||||
|
api_keys = load_api_keys()
|
||||||
|
return api_keys.get(key_name, "")
|
||||||
|
|
||||||
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}")
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ 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):
|
||||||
self.model_name = model_name or "QVQ-Max-2025-03-25" # 默认使用QVQ-Max模型
|
# 如果没有提供模型名称,才使用默认值
|
||||||
|
self.model_name = model_name if model_name else "QVQ-Max-2025-03-25"
|
||||||
|
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)
|
||||||
|
|
||||||
@@ -36,22 +38,35 @@ class AlibabaModel(BaseModel):
|
|||||||
"qwen-vl-max-latest": "qwen-vl-max", # 修正为正确的API标识符
|
"qwen-vl-max-latest": "qwen-vl-max", # 修正为正确的API标识符
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print(f"模型名称: {self.model_name}")
|
||||||
|
|
||||||
# 从模型映射表中获取模型标识符,如果不存在则使用默认值
|
# 从模型映射表中获取模型标识符,如果不存在则使用默认值
|
||||||
model_id = model_mapping.get(self.model_name)
|
model_id = model_mapping.get(self.model_name)
|
||||||
if model_id:
|
if model_id:
|
||||||
|
print(f"从映射表中获取到模型标识符: {model_id}")
|
||||||
return model_id
|
return model_id
|
||||||
|
|
||||||
# 如果没有精确匹配,检查是否包含特定前缀
|
# 如果没有精确匹配,检查是否包含特定前缀
|
||||||
if self.model_name and "qwen-vl" in self.model_name:
|
if self.model_name and "qwen-vl" in self.model_name.lower():
|
||||||
if "max" in self.model_name:
|
if "max" in self.model_name.lower():
|
||||||
|
print(f"识别为qwen-vl-max模型")
|
||||||
return "qwen-vl-max"
|
return "qwen-vl-max"
|
||||||
elif "plus" in self.model_name:
|
elif "plus" in self.model_name.lower():
|
||||||
|
print(f"识别为qwen-vl-plus模型")
|
||||||
return "qwen-vl-plus"
|
return "qwen-vl-plus"
|
||||||
elif "lite" in self.model_name:
|
elif "lite" in self.model_name.lower():
|
||||||
|
print(f"识别为qwen-vl-lite模型")
|
||||||
return "qwen-vl-lite"
|
return "qwen-vl-lite"
|
||||||
|
print(f"默认使用qwen-vl-max模型")
|
||||||
return "qwen-vl-max" # 默认使用最强版本
|
return "qwen-vl-max" # 默认使用最强版本
|
||||||
|
|
||||||
|
# 如果包含QVQ或alibaba关键词,默认使用qvq-max
|
||||||
|
if self.model_name and ("qvq" in self.model_name.lower() or "alibaba" in self.model_name.lower()):
|
||||||
|
print(f"识别为QVQ模型,使用qvq-max")
|
||||||
|
return "qvq-max"
|
||||||
|
|
||||||
# 最后的默认值
|
# 最后的默认值
|
||||||
|
print(f"警告:无法识别的模型名称 {self.model_name},默认使用qvq-max")
|
||||||
return "qvq-max"
|
return "qvq-max"
|
||||||
|
|
||||||
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]:
|
||||||
@@ -107,7 +122,8 @@ class AlibabaModel(BaseModel):
|
|||||||
is_answering = False
|
is_answering = False
|
||||||
|
|
||||||
# 检查是否为通义千问VL模型(不支持reasoning_content)
|
# 检查是否为通义千问VL模型(不支持reasoning_content)
|
||||||
is_qwen_vl = "qwen-vl" in self.get_model_identifier()
|
is_qwen_vl = "qwen-vl" in self.get_model_identifier().lower()
|
||||||
|
print(f"分析文本使用模型标识符: {self.get_model_identifier()}, 是否为千问VL模型: {is_qwen_vl}")
|
||||||
|
|
||||||
for chunk in response:
|
for chunk in response:
|
||||||
if not chunk.choices:
|
if not chunk.choices:
|
||||||
@@ -237,7 +253,8 @@ class AlibabaModel(BaseModel):
|
|||||||
is_answering = False
|
is_answering = False
|
||||||
|
|
||||||
# 检查是否为通义千问VL模型(不支持reasoning_content)
|
# 检查是否为通义千问VL模型(不支持reasoning_content)
|
||||||
is_qwen_vl = "qwen-vl" in self.get_model_identifier()
|
is_qwen_vl = "qwen-vl" in self.get_model_identifier().lower()
|
||||||
|
print(f"分析图像使用模型标识符: {self.get_model_identifier()}, 是否为千问VL模型: {is_qwen_vl}")
|
||||||
|
|
||||||
for chunk in response:
|
for chunk in response:
|
||||||
if not chunk.choices:
|
if not chunk.choices:
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class AnthropicModel(BaseModel):
|
|||||||
# 处理推理配置
|
# 处理推理配置
|
||||||
if hasattr(self, 'reasoning_config') and self.reasoning_config:
|
if hasattr(self, 'reasoning_config') and self.reasoning_config:
|
||||||
# 如果设置了extended reasoning
|
# 如果设置了extended reasoning
|
||||||
if self.reasoning_config.get('reasoning_depth') == 'extended':
|
if self.reasoning_config.get('reasoning_depth') == 'extended':
|
||||||
think_budget = self.reasoning_config.get('think_budget', max_tokens // 2)
|
think_budget = self.reasoning_config.get('think_budget', max_tokens // 2)
|
||||||
payload['thinking'] = {
|
payload['thinking'] = {
|
||||||
'type': 'enabled',
|
'type': 'enabled',
|
||||||
@@ -64,7 +64,6 @@ class AnthropicModel(BaseModel):
|
|||||||
}
|
}
|
||||||
# 如果设置了instant模式
|
# 如果设置了instant模式
|
||||||
elif self.reasoning_config.get('speed_mode') == 'instant':
|
elif self.reasoning_config.get('speed_mode') == 'instant':
|
||||||
payload['speed_mode'] = 'instant'
|
|
||||||
# 确保当使用speed_mode时不包含thinking参数
|
# 确保当使用speed_mode时不包含thinking参数
|
||||||
if 'thinking' in payload:
|
if 'thinking' in payload:
|
||||||
del payload['thinking']
|
del payload['thinking']
|
||||||
|
|||||||
@@ -23,8 +23,22 @@ class DeepSeekModel(BaseModel):
|
|||||||
# 通过模型名称来确定实际的API调用标识符
|
# 通过模型名称来确定实际的API调用标识符
|
||||||
if self.model_name == "deepseek-chat":
|
if self.model_name == "deepseek-chat":
|
||||||
return "deepseek-chat"
|
return "deepseek-chat"
|
||||||
# deepseek-reasoner是默认的推理模型名称
|
# 如果是deepseek-reasoner或包含reasoner的模型名称,返回推理模型标识符
|
||||||
return "deepseek-reasoner"
|
if "reasoner" in self.model_name.lower():
|
||||||
|
return "deepseek-reasoner"
|
||||||
|
# 对于deepseek-chat也返回对应的模型名称
|
||||||
|
if "chat" in self.model_name.lower() or self.model_name == "deepseek-chat":
|
||||||
|
return "deepseek-chat"
|
||||||
|
|
||||||
|
# 根据配置中的模型ID来确定实际的模型类型
|
||||||
|
if self.model_name == "deepseek-reasoner":
|
||||||
|
return "deepseek-reasoner"
|
||||||
|
elif self.model_name == "deepseek-chat":
|
||||||
|
return "deepseek-chat"
|
||||||
|
|
||||||
|
# 默认使用deepseek-chat作为API标识符
|
||||||
|
print(f"未知的DeepSeek模型名称: {self.model_name},使用deepseek-chat作为默认值")
|
||||||
|
return "deepseek-chat"
|
||||||
|
|
||||||
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]:
|
||||||
"""Stream DeepSeek's response for text analysis"""
|
"""Stream DeepSeek's response for text analysis"""
|
||||||
@@ -75,10 +89,10 @@ class DeepSeekModel(BaseModel):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 只有非推理模型才设置temperature参数
|
# 只有非推理模型才设置temperature参数
|
||||||
if not self.model_name.endswith('reasoner') and self.temperature is not None:
|
if not self.get_model_identifier().endswith('reasoner') and self.temperature is not None:
|
||||||
params["temperature"] = self.temperature
|
params["temperature"] = self.temperature
|
||||||
|
|
||||||
print(f"调用DeepSeek API: {self.get_model_identifier()}, 是否设置温度: {not self.model_name.endswith('reasoner')}")
|
print(f"调用DeepSeek API: {self.get_model_identifier()}, 是否设置温度: {not self.get_model_identifier().endswith('reasoner')}, 温度值: {self.temperature if not self.get_model_identifier().endswith('reasoner') else 'N/A'}")
|
||||||
|
|
||||||
response = client.chat.completions.create(**params)
|
response = client.chat.completions.create(**params)
|
||||||
|
|
||||||
@@ -253,7 +267,7 @@ class DeepSeekModel(BaseModel):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 只有非推理模型才设置temperature参数
|
# 只有非推理模型才设置temperature参数
|
||||||
if not self.model_name.endswith('reasoner') and self.temperature is not None:
|
if not self.get_model_identifier().endswith('reasoner') and self.temperature is not None:
|
||||||
params["temperature"] = self.temperature
|
params["temperature"] = self.temperature
|
||||||
|
|
||||||
response = client.chat.completions.create(**params)
|
response = client.chat.completions.create(**params)
|
||||||
|
|||||||
@@ -98,36 +98,26 @@ class ModelFactory:
|
|||||||
@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) -> BaseModel:
|
||||||
"""
|
"""
|
||||||
Create and return an instance of the specified model.
|
Create a model instance based on the model name.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
model_name: The identifier of the model to create
|
model_name: The identifier for the model
|
||||||
api_key: The API key for the model
|
api_key: The API key for the model service
|
||||||
temperature: Optional temperature parameter for response generation
|
temperature: The temperature to use for generation
|
||||||
system_prompt: Optional custom system prompt
|
system_prompt: The system prompt to use
|
||||||
language: Optional language preference for responses
|
language: The preferred language for responses
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
An instance of the specified model
|
A model instance
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If the model_name is not recognized
|
|
||||||
"""
|
"""
|
||||||
model_info = cls._models.get(model_name)
|
if model_name not in cls._models:
|
||||||
if not model_info:
|
|
||||||
raise ValueError(f"Unknown model: {model_name}")
|
raise ValueError(f"Unknown model: {model_name}")
|
||||||
|
|
||||||
|
model_info = cls._models[model_name]
|
||||||
model_class = model_info['class']
|
model_class = model_info['class']
|
||||||
|
|
||||||
# 对于Mathpix模型,不传递language参数
|
# 对于DeepSeek模型,需要传递正确的模型名称
|
||||||
if model_name == 'mathpix':
|
if 'deepseek' in model_name.lower():
|
||||||
return model_class(
|
|
||||||
api_key=api_key,
|
|
||||||
temperature=temperature,
|
|
||||||
system_prompt=system_prompt
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# 对于所有其他模型,传递model_name参数
|
|
||||||
return model_class(
|
return model_class(
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
@@ -135,6 +125,30 @@ class ModelFactory:
|
|||||||
language=language,
|
language=language,
|
||||||
model_name=model_name
|
model_name=model_name
|
||||||
)
|
)
|
||||||
|
# 对于阿里巴巴模型,也需要传递正确的模型名称
|
||||||
|
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
|
||||||
|
)
|
||||||
|
# 对于Mathpix模型,不传递language参数
|
||||||
|
elif model_name == 'mathpix':
|
||||||
|
return model_class(
|
||||||
|
api_key=api_key,
|
||||||
|
temperature=temperature,
|
||||||
|
system_prompt=system_prompt
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 其他模型仅传递标准参数
|
||||||
|
return model_class(
|
||||||
|
api_key=api_key,
|
||||||
|
temperature=temperature,
|
||||||
|
system_prompt=system_prompt,
|
||||||
|
language=language
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_available_models(cls) -> list[Dict[str, Any]]:
|
def get_available_models(cls) -> list[Dict[str, Any]]:
|
||||||
|
|||||||
@@ -1010,7 +1010,7 @@ class SnapSolver {
|
|||||||
|
|
||||||
if (!mathpixApiKey || mathpixApiKey === ':') {
|
if (!mathpixApiKey || mathpixApiKey === ':') {
|
||||||
window.uiManager.showToast('请在设置中输入Mathpix API凭据', 'error');
|
window.uiManager.showToast('请在设置中输入Mathpix API凭据', 'error');
|
||||||
document.getElementById('settingsPanel').classList.remove('hidden');
|
document.getElementById('settingsPanel').classList.add('active');
|
||||||
this.extractTextBtn.disabled = false;
|
this.extractTextBtn.disabled = false;
|
||||||
this.extractTextBtn.innerHTML = '<i class="fas fa-font"></i><span>提取文本</span>';
|
this.extractTextBtn.innerHTML = '<i class="fas fa-font"></i><span>提取文本</span>';
|
||||||
return;
|
return;
|
||||||
@@ -1282,14 +1282,34 @@ class SnapSolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize() {
|
async initialize() {
|
||||||
console.log('Initializing SnapSolver...');
|
console.log('Initializing SnapSolver...');
|
||||||
|
|
||||||
// 初始化managers
|
// 初始化managers
|
||||||
window.uiManager = new UIManager();
|
// 确保UIManager已经初始化,如果没有,等待它初始化
|
||||||
|
if (!window.uiManager) {
|
||||||
|
console.log('等待UI管理器初始化...');
|
||||||
|
window.uiManager = new UIManager();
|
||||||
|
// 给UIManager一些时间初始化
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
|
||||||
window.settingsManager = new SettingsManager();
|
window.settingsManager = new SettingsManager();
|
||||||
window.app = this; // 便于从其他地方访问
|
window.app = this; // 便于从其他地方访问
|
||||||
|
|
||||||
|
// 等待SettingsManager初始化完成
|
||||||
|
if (window.settingsManager) {
|
||||||
|
// 如果settingsManager还没初始化完成,等待它
|
||||||
|
if (!window.settingsManager.isInitialized) {
|
||||||
|
console.log('等待设置管理器初始化完成...');
|
||||||
|
// 最多等待5秒
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
if (window.settingsManager.isInitialized) break;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化Markdown工具
|
// 初始化Markdown工具
|
||||||
this.initializeMarkdownTools();
|
this.initializeMarkdownTools();
|
||||||
|
|
||||||
@@ -1554,9 +1574,9 @@ class SnapSolver {
|
|||||||
!e.target.closest('#settingsPanel') &&
|
!e.target.closest('#settingsPanel') &&
|
||||||
!e.target.matches('#settingsToggle') &&
|
!e.target.matches('#settingsToggle') &&
|
||||||
!e.target.closest('#settingsToggle') &&
|
!e.target.closest('#settingsToggle') &&
|
||||||
!document.getElementById('settingsPanel').classList.contains('hidden')
|
document.getElementById('settingsPanel').classList.contains('active')
|
||||||
) {
|
) {
|
||||||
document.getElementById('settingsPanel').classList.add('hidden');
|
document.getElementById('settingsPanel').classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否点击在Claude面板、分析按钮或其子元素之外
|
// 检查是否点击在Claude面板、分析按钮或其子元素之外
|
||||||
@@ -1654,10 +1674,11 @@ class SnapSolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the application when the DOM is loaded
|
// Initialize the application when the DOM is loaded
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
try {
|
try {
|
||||||
console.log('Initializing application...');
|
console.log('Initializing application...');
|
||||||
window.app = new SnapSolver();
|
window.app = new SnapSolver();
|
||||||
|
await window.app.initialize();
|
||||||
console.log('Application initialized successfully');
|
console.log('Application initialized successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize application:', error);
|
console.error('Failed to initialize application:', error);
|
||||||
|
|||||||
@@ -8,28 +8,42 @@ class SettingsManager {
|
|||||||
this.initializeElements();
|
this.initializeElements();
|
||||||
|
|
||||||
// 加载模型配置
|
// 加载模型配置
|
||||||
this.loadModelConfig()
|
this.isInitialized = false;
|
||||||
.then(() => {
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
try {
|
||||||
|
// 加载模型配置
|
||||||
|
await this.loadModelConfig();
|
||||||
|
|
||||||
// 成功加载配置后,执行后续初始化
|
// 成功加载配置后,执行后续初始化
|
||||||
this.updateModelOptions();
|
this.updateModelOptions();
|
||||||
this.loadSettings();
|
await this.loadSettings();
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
this.updateUIBasedOnModelType();
|
this.updateUIBasedOnModelType();
|
||||||
})
|
|
||||||
.catch(error => {
|
// 初始化可折叠内容逻辑
|
||||||
console.error('加载模型配置失败:', error);
|
this.initCollapsibleContent();
|
||||||
|
|
||||||
|
this.isInitialized = true;
|
||||||
|
console.log('设置管理器初始化完成');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化设置管理器失败:', error);
|
||||||
window.uiManager?.showToast('加载模型配置失败,使用默认配置', 'error');
|
window.uiManager?.showToast('加载模型配置失败,使用默认配置', 'error');
|
||||||
|
|
||||||
// 使用默认配置作为备份
|
// 使用默认配置作为备份
|
||||||
this.setupDefaultModels();
|
this.setupDefaultModels();
|
||||||
this.updateModelOptions();
|
this.updateModelOptions();
|
||||||
this.loadSettings();
|
await this.loadSettings();
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
this.updateUIBasedOnModelType();
|
this.updateUIBasedOnModelType();
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化可折叠内容逻辑
|
// 初始化可折叠内容逻辑
|
||||||
this.initCollapsibleContent();
|
this.initCollapsibleContent();
|
||||||
|
|
||||||
|
this.isInitialized = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从配置文件加载模型定义
|
// 从配置文件加载模型定义
|
||||||
@@ -186,6 +200,16 @@ class SettingsManager {
|
|||||||
'mathpixAppKey': this.mathpixAppKeyInput
|
'mathpixAppKey': this.mathpixAppKeyInput
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// API密钥状态显示相关元素
|
||||||
|
this.apiKeysList = document.getElementById('apiKeysList');
|
||||||
|
|
||||||
|
// 防止API密钥区域的点击事件冒泡
|
||||||
|
if (this.apiKeysList) {
|
||||||
|
this.apiKeysList.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Settings toggle elements
|
// Settings toggle elements
|
||||||
this.settingsToggle = document.getElementById('settingsToggle');
|
this.settingsToggle = document.getElementById('settingsToggle');
|
||||||
this.closeSettings = document.getElementById('closeSettings');
|
this.closeSettings = document.getElementById('closeSettings');
|
||||||
@@ -205,6 +229,58 @@ class SettingsManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize API key validate buttons
|
||||||
|
document.querySelectorAll('.validate-api-key').forEach(button => {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
const keyType = e.currentTarget.getAttribute('data-key-type');
|
||||||
|
const input = e.currentTarget.closest('.input-group').querySelector('input');
|
||||||
|
const keyValue = input.value;
|
||||||
|
|
||||||
|
if (keyValue.trim() === '') {
|
||||||
|
window.uiManager?.showToast('请先输入API密钥', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示验证中状态
|
||||||
|
const icon = e.currentTarget.querySelector('i');
|
||||||
|
const originalClass = icon.className;
|
||||||
|
icon.className = 'fas fa-spinner fa-spin';
|
||||||
|
e.currentTarget.disabled = true;
|
||||||
|
|
||||||
|
this.validateApiKey(keyType, keyValue)
|
||||||
|
.then(result => {
|
||||||
|
if (result.success) {
|
||||||
|
window.uiManager?.showToast(result.message, 'success');
|
||||||
|
// 更新状态显示
|
||||||
|
this.saveSettings();
|
||||||
|
} else {
|
||||||
|
window.uiManager?.showToast(result.message, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
window.uiManager?.showToast(`验证失败: ${error.message}`, 'error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
// 恢复按钮状态
|
||||||
|
icon.className = originalClass;
|
||||||
|
e.currentTarget.disabled = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 存储API密钥的对象
|
||||||
|
this.apiKeyValues = {
|
||||||
|
'AnthropicApiKey': '',
|
||||||
|
'OpenaiApiKey': '',
|
||||||
|
'DeepseekApiKey': '',
|
||||||
|
'AlibabaApiKey': '',
|
||||||
|
'MathpixAppId': '',
|
||||||
|
'MathpixAppKey': ''
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化密钥编辑功能
|
||||||
|
this.initApiKeyEditFunctions();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新模型选择下拉框
|
// 更新模型选择下拉框
|
||||||
@@ -250,27 +326,16 @@ class SettingsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSettings() {
|
async loadSettings() {
|
||||||
|
try {
|
||||||
|
// 先从localStorage加载大部分设置
|
||||||
const settings = JSON.parse(localStorage.getItem('aiSettings') || '{}');
|
const settings = JSON.parse(localStorage.getItem('aiSettings') || '{}');
|
||||||
|
|
||||||
// Load Mathpix credentials
|
// 刷新API密钥状态(自动从服务器获取最新状态)
|
||||||
if (settings.mathpixAppId) {
|
await this.refreshApiKeyStatus();
|
||||||
this.mathpixAppIdInput.value = settings.mathpixAppId;
|
console.log('已自动刷新API密钥状态');
|
||||||
}
|
|
||||||
if (settings.mathpixAppKey) {
|
// 加载其他设置
|
||||||
this.mathpixAppKeyInput.value = settings.mathpixAppKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load API keys
|
|
||||||
if (settings.apiKeys) {
|
|
||||||
Object.entries(settings.apiKeys).forEach(([keyId, value]) => {
|
|
||||||
const input = this.apiKeyInputs[keyId];
|
|
||||||
if (input) {
|
|
||||||
input.value = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load model selection
|
// Load model selection
|
||||||
if (settings.model && this.modelExists(settings.model)) {
|
if (settings.model && this.modelExists(settings.model)) {
|
||||||
this.modelSelect.value = settings.model;
|
this.modelSelect.value = settings.model;
|
||||||
@@ -329,6 +394,11 @@ class SettingsManager {
|
|||||||
|
|
||||||
// Update UI based on model type
|
// Update UI based on model type
|
||||||
this.updateUIBasedOnModelType();
|
this.updateUIBasedOnModelType();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载设置出错:', error);
|
||||||
|
window.uiManager?.showToast('加载设置出错', 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
modelExists(modelId) {
|
modelExists(modelId) {
|
||||||
@@ -356,21 +426,17 @@ class SettingsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateVisibleApiKey(selectedModel) {
|
/**
|
||||||
const modelInfo = this.modelDefinitions[selectedModel];
|
* 根据选择的模型类型更新UI显示
|
||||||
if (!modelInfo) return;
|
*/
|
||||||
|
|
||||||
// 仅更新模型版本显示
|
|
||||||
this.updateModelVersionDisplay(selectedModel);
|
|
||||||
|
|
||||||
// 不再需要高亮API密钥
|
|
||||||
// 这里我们不再进行API密钥的高亮处理
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUIBasedOnModelType() {
|
updateUIBasedOnModelType() {
|
||||||
|
// 更新UI元素显示,根据所选模型类型
|
||||||
const selectedModel = this.modelSelect.value;
|
const selectedModel = this.modelSelect.value;
|
||||||
const modelInfo = this.modelDefinitions[selectedModel];
|
const modelInfo = this.modelDefinitions[selectedModel];
|
||||||
|
|
||||||
|
// 更新当前可见的API密钥
|
||||||
|
this.updateVisibleApiKey(selectedModel);
|
||||||
|
|
||||||
if (!modelInfo) return;
|
if (!modelInfo) return;
|
||||||
|
|
||||||
// 处理温度设置显示
|
// 处理温度设置显示
|
||||||
@@ -404,11 +470,47 @@ class SettingsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveSettings() {
|
/**
|
||||||
|
* 根据选择的模型更新显示的API密钥
|
||||||
|
* @param {string} modelType 模型类型
|
||||||
|
*/
|
||||||
|
updateVisibleApiKey(modelType) {
|
||||||
|
// 获取所有API密钥状态元素
|
||||||
|
const allApiKeys = document.querySelectorAll('.api-key-status');
|
||||||
|
|
||||||
|
// 首先隐藏所有API密钥
|
||||||
|
allApiKeys.forEach(key => {
|
||||||
|
key.classList.remove('highlight');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 根据当前选择的模型类型,突出显示对应的API密钥
|
||||||
|
let apiKeyToHighlight = null;
|
||||||
|
|
||||||
|
if (modelType.startsWith('claude')) {
|
||||||
|
apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(1)'); // Anthropic
|
||||||
|
} else if (modelType.startsWith('gpt')) {
|
||||||
|
apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(2)'); // OpenAI
|
||||||
|
} else if (modelType.startsWith('deepseek')) {
|
||||||
|
apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(3)'); // DeepSeek
|
||||||
|
} else if (modelType.startsWith('qwen')) {
|
||||||
|
apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(4)'); // Alibaba
|
||||||
|
}
|
||||||
|
|
||||||
|
// 高亮显示对应的API密钥
|
||||||
|
if (apiKeyToHighlight) {
|
||||||
|
apiKeyToHighlight.classList.add('highlight');
|
||||||
|
// 延迟一秒后滚动到该元素
|
||||||
|
setTimeout(() => {
|
||||||
|
apiKeyToHighlight.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSettings() {
|
||||||
|
try {
|
||||||
|
// 保存UI设置到localStorage(不包含API密钥)
|
||||||
const settings = {
|
const settings = {
|
||||||
apiKeys: {},
|
apiKeys: this.apiKeyValues, // 保存到localStorage(向后兼容)
|
||||||
mathpixAppId: this.mathpixAppIdInput.value,
|
|
||||||
mathpixAppKey: this.mathpixAppKeyInput.value,
|
|
||||||
model: this.modelSelect.value,
|
model: this.modelSelect.value,
|
||||||
maxTokens: this.maxTokensInput.value,
|
maxTokens: this.maxTokensInput.value,
|
||||||
reasoningDepth: this.reasoningDepthSelect?.value || 'standard',
|
reasoningDepth: this.reasoningDepthSelect?.value || 'standard',
|
||||||
@@ -421,15 +523,14 @@ class SettingsManager {
|
|||||||
proxyPort: this.proxyPortInput.value
|
proxyPort: this.proxyPortInput.value
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save all API keys
|
// 保存设置到localStorage
|
||||||
Object.entries(this.apiKeyInputs).forEach(([keyId, input]) => {
|
|
||||||
if (input.value) {
|
|
||||||
settings.apiKeys[keyId] = input.value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
localStorage.setItem('aiSettings', JSON.stringify(settings));
|
localStorage.setItem('aiSettings', JSON.stringify(settings));
|
||||||
window.showToast('Settings saved successfully');
|
|
||||||
|
window.uiManager?.showToast('设置已保存', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存设置出错:', error);
|
||||||
|
window.uiManager?.showToast('保存设置出错: ' + error.message, 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getApiKey() {
|
getApiKey() {
|
||||||
@@ -482,6 +583,11 @@ class SettingsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从apiKeyValues获取Mathpix信息,而不是直接从DOM读取
|
||||||
|
const mathpixAppId = this.apiKeyValues['MathpixAppId'] || '';
|
||||||
|
const mathpixAppKey = this.apiKeyValues['MathpixAppKey'] || '';
|
||||||
|
const mathpixApiKey = mathpixAppId && mathpixAppKey ? `${mathpixAppId}:${mathpixAppKey}` : '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
model: selectedModel,
|
model: selectedModel,
|
||||||
maxTokens: maxTokens,
|
maxTokens: maxTokens,
|
||||||
@@ -491,7 +597,7 @@ class SettingsManager {
|
|||||||
proxyEnabled: this.proxyEnabledInput.checked,
|
proxyEnabled: this.proxyEnabledInput.checked,
|
||||||
proxyHost: this.proxyHostInput.value,
|
proxyHost: this.proxyHostInput.value,
|
||||||
proxyPort: this.proxyPortInput.value,
|
proxyPort: this.proxyPortInput.value,
|
||||||
mathpixApiKey: `${this.mathpixAppIdInput.value}:${this.mathpixAppKeyInput.value}`,
|
mathpixApiKey: mathpixApiKey,
|
||||||
modelInfo: {
|
modelInfo: {
|
||||||
supportsMultimodal: modelInfo.supportsMultimodal || false,
|
supportsMultimodal: modelInfo.supportsMultimodal || false,
|
||||||
isReasoning: modelInfo.isReasoning || false,
|
isReasoning: modelInfo.isReasoning || false,
|
||||||
@@ -512,15 +618,6 @@ class SettingsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// Save settings on change
|
|
||||||
Object.values(this.apiKeyInputs).forEach(input => {
|
|
||||||
input.addEventListener('change', () => this.saveSettings());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save Mathpix settings on change
|
|
||||||
this.mathpixAppIdInput.addEventListener('change', () => this.saveSettings());
|
|
||||||
this.mathpixAppKeyInput.addEventListener('change', () => this.saveSettings());
|
|
||||||
|
|
||||||
this.modelSelect.addEventListener('change', (e) => {
|
this.modelSelect.addEventListener('change', (e) => {
|
||||||
this.updateVisibleApiKey(e.target.value);
|
this.updateVisibleApiKey(e.target.value);
|
||||||
this.updateUIBasedOnModelType();
|
this.updateUIBasedOnModelType();
|
||||||
@@ -536,6 +633,9 @@ class SettingsManager {
|
|||||||
// 最大Token输入框事件处理
|
// 最大Token输入框事件处理
|
||||||
if (this.maxTokensInput) {
|
if (this.maxTokensInput) {
|
||||||
this.maxTokensInput.addEventListener('change', (e) => {
|
this.maxTokensInput.addEventListener('change', (e) => {
|
||||||
|
// 阻止事件冒泡
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
// 验证输入值在有效范围内
|
// 验证输入值在有效范围内
|
||||||
let value = parseInt(e.target.value);
|
let value = parseInt(e.target.value);
|
||||||
if (isNaN(value)) value = 8192;
|
if (isNaN(value)) value = 8192;
|
||||||
@@ -551,7 +651,10 @@ class SettingsManager {
|
|||||||
|
|
||||||
// 推理深度选择事件处理
|
// 推理深度选择事件处理
|
||||||
if (this.reasoningDepthSelect) {
|
if (this.reasoningDepthSelect) {
|
||||||
this.reasoningDepthSelect.addEventListener('change', () => {
|
this.reasoningDepthSelect.addEventListener('change', (e) => {
|
||||||
|
// 阻止事件冒泡
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
// 更新思考预算组的可见性
|
// 更新思考预算组的可见性
|
||||||
if (this.thinkBudgetGroup) {
|
if (this.thinkBudgetGroup) {
|
||||||
const showThinkBudget = this.reasoningDepthSelect.value === 'extended';
|
const showThinkBudget = this.reasoningDepthSelect.value === 'extended';
|
||||||
@@ -564,6 +667,9 @@ class SettingsManager {
|
|||||||
// 思考预算占比滑块事件处理
|
// 思考预算占比滑块事件处理
|
||||||
if (this.thinkBudgetPercentInput && this.thinkBudgetPercentValue) {
|
if (this.thinkBudgetPercentInput && this.thinkBudgetPercentValue) {
|
||||||
this.thinkBudgetPercentInput.addEventListener('input', (e) => {
|
this.thinkBudgetPercentInput.addEventListener('input', (e) => {
|
||||||
|
// 阻止事件冒泡
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
// 更新思考预算显示
|
// 更新思考预算显示
|
||||||
this.updateThinkBudgetDisplay();
|
this.updateThinkBudgetDisplay();
|
||||||
|
|
||||||
@@ -575,29 +681,78 @@ class SettingsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.temperatureInput.addEventListener('input', (e) => {
|
this.temperatureInput.addEventListener('input', (e) => {
|
||||||
|
// 阻止事件冒泡
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
this.temperatureValue.textContent = e.target.value;
|
this.temperatureValue.textContent = e.target.value;
|
||||||
this.updateRangeSliderBackground(e.target);
|
this.updateRangeSliderBackground(e.target);
|
||||||
this.saveSettings();
|
this.saveSettings();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.systemPromptInput.addEventListener('change', () => this.saveSettings());
|
this.systemPromptInput.addEventListener('change', (e) => {
|
||||||
this.languageInput.addEventListener('change', () => this.saveSettings());
|
// 阻止事件冒泡
|
||||||
|
e.stopPropagation();
|
||||||
|
this.saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.languageInput.addEventListener('change', (e) => {
|
||||||
|
// 阻止事件冒泡
|
||||||
|
e.stopPropagation();
|
||||||
|
this.saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
this.proxyEnabledInput.addEventListener('change', (e) => {
|
this.proxyEnabledInput.addEventListener('change', (e) => {
|
||||||
|
// 阻止事件冒泡
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
this.proxySettings.style.display = e.target.checked ? 'block' : 'none';
|
this.proxySettings.style.display = e.target.checked ? 'block' : 'none';
|
||||||
this.saveSettings();
|
this.saveSettings();
|
||||||
});
|
});
|
||||||
this.proxyHostInput.addEventListener('change', () => this.saveSettings());
|
|
||||||
this.proxyPortInput.addEventListener('change', () => this.saveSettings());
|
this.proxyHostInput.addEventListener('change', (e) => {
|
||||||
|
// 阻止事件冒泡
|
||||||
|
e.stopPropagation();
|
||||||
|
this.saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.proxyPortInput.addEventListener('change', (e) => {
|
||||||
|
// 阻止事件冒泡
|
||||||
|
e.stopPropagation();
|
||||||
|
this.saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
// Panel visibility
|
// Panel visibility
|
||||||
this.settingsToggle.addEventListener('click', () => {
|
this.settingsToggle.addEventListener('click', () => {
|
||||||
window.closeAllPanels();
|
this.toggleSettingsPanel();
|
||||||
this.settingsPanel.classList.toggle('hidden');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.closeSettings.addEventListener('click', () => {
|
this.closeSettings.addEventListener('click', () => {
|
||||||
this.settingsPanel.classList.add('hidden');
|
this.closeSettingsPanel();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 确保设置面板自身的点击不会干扰内部操作
|
||||||
|
if (this.settingsPanel) {
|
||||||
|
const settingsSections = this.settingsPanel.querySelectorAll('.settings-section');
|
||||||
|
settingsSections.forEach(section => {
|
||||||
|
section.addEventListener('click', (e) => {
|
||||||
|
// 只阻止直接点击设置部分的事件
|
||||||
|
if (e.target === section) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置内容区域防止冒泡
|
||||||
|
const settingsContent = this.settingsPanel.querySelector('.settings-content');
|
||||||
|
if (settingsContent) {
|
||||||
|
settingsContent.addEventListener('click', (e) => {
|
||||||
|
// 只阻止直接点击设置内容区域的事件
|
||||||
|
if (e.target === settingsContent) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助方法:更新滑块的背景颜色
|
// 辅助方法:更新滑块的背景颜色
|
||||||
@@ -628,26 +783,356 @@ class SettingsManager {
|
|||||||
* 初始化可折叠内容的交互逻辑
|
* 初始化可折叠内容的交互逻辑
|
||||||
*/
|
*/
|
||||||
initCollapsibleContent() {
|
initCollapsibleContent() {
|
||||||
// 获取API密钥折叠切换按钮和内容
|
// 在新的实现中,我们不再需要折叠API密钥区域,因为所有功能都在同一区域完成
|
||||||
const apiKeysToggle = document.getElementById('apiKeysCollapseToggle');
|
console.log('初始化API密钥编辑功能完成');
|
||||||
const apiKeysContent = document.getElementById('apiKeysContent');
|
}
|
||||||
|
|
||||||
// 添加点击事件以切换折叠状态
|
/**
|
||||||
if (apiKeysToggle && apiKeysContent) {
|
* 初始化API密钥编辑相关功能
|
||||||
apiKeysToggle.addEventListener('click', () => {
|
*/
|
||||||
// 切换折叠状态
|
initApiKeyEditFunctions() {
|
||||||
apiKeysContent.classList.toggle('collapsed');
|
// 1. 编辑按钮点击事件
|
||||||
|
document.querySelectorAll('.edit-api-key').forEach(button => {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
// 阻止事件冒泡
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
// 更新图标方向
|
const keyType = e.currentTarget.getAttribute('data-key-type');
|
||||||
const icon = apiKeysToggle.querySelector('.fa-chevron-down, .fa-chevron-up');
|
const keyStatus = e.currentTarget.closest('.key-status-wrapper');
|
||||||
if (icon) {
|
|
||||||
if (apiKeysContent.classList.contains('collapsed')) {
|
if (keyStatus) {
|
||||||
icon.classList.replace('fa-chevron-up', 'fa-chevron-down');
|
// 隐藏显示区域
|
||||||
} else {
|
const displayArea = keyStatus.querySelector('.key-display');
|
||||||
icon.classList.replace('fa-chevron-down', 'fa-chevron-up');
|
if (displayArea) displayArea.classList.add('hidden');
|
||||||
|
|
||||||
|
// 显示编辑区域
|
||||||
|
const editArea = keyStatus.querySelector('.key-edit');
|
||||||
|
if (editArea) {
|
||||||
|
editArea.classList.remove('hidden');
|
||||||
|
|
||||||
|
// 获取当前密钥值并填入输入框
|
||||||
|
const keyInput = editArea.querySelector('.key-input');
|
||||||
|
if (keyInput) {
|
||||||
|
// 从状态文本中获取当前值(如果不是"未设置")
|
||||||
|
const statusElement = keyStatus.querySelector('.key-status');
|
||||||
|
if (statusElement && statusElement.textContent !== '未设置') {
|
||||||
|
keyInput.value = this.apiKeyValues[keyType] || '';
|
||||||
|
} else {
|
||||||
|
keyInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 聚焦输入框
|
||||||
|
setTimeout(() => {
|
||||||
|
keyInput.focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 保存按钮点击事件
|
||||||
|
document.querySelectorAll('.save-api-key').forEach(button => {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
// 阻止事件冒泡
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const keyType = e.currentTarget.getAttribute('data-key-type');
|
||||||
|
const keyStatus = e.currentTarget.closest('.key-status-wrapper');
|
||||||
|
|
||||||
|
if (keyStatus) {
|
||||||
|
// 获取输入的新密钥值
|
||||||
|
const keyInput = keyStatus.querySelector('.key-input');
|
||||||
|
if (keyInput) {
|
||||||
|
const newValue = keyInput.value.trim();
|
||||||
|
|
||||||
|
// 保存到内存中
|
||||||
|
this.apiKeyValues[keyType] = newValue;
|
||||||
|
|
||||||
|
// 创建要保存的API密钥对象
|
||||||
|
const apiKeysToSave = {};
|
||||||
|
apiKeysToSave[keyType] = newValue;
|
||||||
|
|
||||||
|
// 保存到服务器
|
||||||
|
this.saveApiKey(keyType, newValue, keyStatus);
|
||||||
|
|
||||||
|
// 隐藏编辑区域
|
||||||
|
const editArea = keyStatus.querySelector('.key-edit');
|
||||||
|
if (editArea) editArea.classList.add('hidden');
|
||||||
|
|
||||||
|
// 显示状态区域
|
||||||
|
const displayArea = keyStatus.querySelector('.key-display');
|
||||||
|
if (displayArea) displayArea.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 切换密码可见性按钮
|
||||||
|
document.querySelectorAll('.toggle-visibility').forEach(button => {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
// 阻止事件冒泡
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const keyInput = e.currentTarget.closest('.key-edit').querySelector('.key-input');
|
||||||
|
if (keyInput) {
|
||||||
|
const type = keyInput.type === 'password' ? 'text' : 'password';
|
||||||
|
keyInput.type = type;
|
||||||
|
|
||||||
|
// 更新图标
|
||||||
|
const icon = e.currentTarget.querySelector('i');
|
||||||
|
if (icon) {
|
||||||
|
icon.className = `fas fa-${type === 'password' ? 'eye' : 'eye-slash'}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. 输入框按下Enter保存
|
||||||
|
document.querySelectorAll('.key-input').forEach(input => {
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
// 阻止事件冒泡
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const saveButton = e.currentTarget.closest('.key-edit').querySelector('.save-api-key');
|
||||||
|
if (saveButton) {
|
||||||
|
saveButton.click();
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
// 阻止事件冒泡
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// 取消编辑
|
||||||
|
const keyStatus = e.currentTarget.closest('.key-status-wrapper');
|
||||||
|
if (keyStatus) {
|
||||||
|
const editArea = keyStatus.querySelector('.key-edit');
|
||||||
|
if (editArea) editArea.classList.add('hidden');
|
||||||
|
|
||||||
|
const displayArea = keyStatus.querySelector('.key-display');
|
||||||
|
if (displayArea) displayArea.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新API密钥状态显示
|
||||||
|
* @param {Object} apiKeys 密钥对象
|
||||||
|
*/
|
||||||
|
updateApiKeyStatus(apiKeys) {
|
||||||
|
if (!this.apiKeysList) return;
|
||||||
|
|
||||||
|
// 保存API密钥值到内存中
|
||||||
|
for (const [key, value] of Object.entries(apiKeys)) {
|
||||||
|
this.apiKeyValues[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到所有密钥状态元素
|
||||||
|
Object.keys(apiKeys).forEach(keyId => {
|
||||||
|
const statusElement = document.getElementById(`${keyId}Status`);
|
||||||
|
if (!statusElement) return;
|
||||||
|
|
||||||
|
const value = apiKeys[keyId];
|
||||||
|
|
||||||
|
if (value && value.trim() !== '') {
|
||||||
|
// 显示密钥状态 - 已设置
|
||||||
|
statusElement.className = 'key-status set';
|
||||||
|
statusElement.innerHTML = `<i class="fas fa-check-circle"></i> 已设置`;
|
||||||
|
} else {
|
||||||
|
// 显示密钥状态 - 未设置
|
||||||
|
statusElement.className = 'key-status not-set';
|
||||||
|
statusElement.innerHTML = `<i class="fas fa-times-circle"></i> 未设置`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存单个API密钥
|
||||||
|
* @param {string} keyType 密钥类型
|
||||||
|
* @param {string} value 密钥值
|
||||||
|
* @param {HTMLElement} keyStatus 密钥状态容器
|
||||||
|
*/
|
||||||
|
async saveApiKey(keyType, value, keyStatus) {
|
||||||
|
try {
|
||||||
|
// 显示保存中状态
|
||||||
|
const saveToast = this.createToast('正在保存密钥...', 'info', true);
|
||||||
|
|
||||||
|
// 创建要保存的数据对象
|
||||||
|
const apiKeysData = {};
|
||||||
|
apiKeysData[keyType] = value;
|
||||||
|
|
||||||
|
// 发送到服务器
|
||||||
|
const response = await fetch('/api/keys', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(apiKeysData)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移除保存中提示
|
||||||
|
if (saveToast) {
|
||||||
|
saveToast.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
// 更新密钥状态显示
|
||||||
|
const statusElem = document.getElementById(`${keyType}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.createToast('密钥已保存', 'success');
|
||||||
|
} else {
|
||||||
|
this.createToast('保存密钥失败: ' + result.message, 'error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.createToast('无法连接到服务器', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存密钥出错:', error);
|
||||||
|
this.createToast('保存密钥出错: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证API密钥
|
||||||
|
* @param {string} keyType 密钥类型
|
||||||
|
* @param {string} keyValue 密钥值
|
||||||
|
* @returns {Promise<Object>} 验证结果
|
||||||
|
*/
|
||||||
|
async validateApiKey(keyType, keyValue) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/keys/validate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
key_type: keyType,
|
||||||
|
key_value: keyValue
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`服务器响应错误: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('验证API密钥时出错:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建一个Toast提示消息
|
||||||
|
* @param {string} message 提示消息内容
|
||||||
|
* @param {string} type 提示类型:'success', 'error', 'warning', 'info'
|
||||||
|
* @param {boolean} persistent 是否为持久性提示(需要手动关闭)
|
||||||
|
*/
|
||||||
|
createToast(message, type = 'success', persistent = false) {
|
||||||
|
const toastContainer = document.querySelector('.toast-container') || this.createToastContainer();
|
||||||
|
|
||||||
|
// 创建Toast元素
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `toast ${type}`;
|
||||||
|
if (persistent) {
|
||||||
|
toast.classList.add('persistent');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置消息内容
|
||||||
|
toast.textContent = message;
|
||||||
|
|
||||||
|
// 如果是持久性提示,添加关闭按钮
|
||||||
|
if (persistent) {
|
||||||
|
const closeBtn = document.createElement('button');
|
||||||
|
closeBtn.className = 'toast-close';
|
||||||
|
closeBtn.innerHTML = '×';
|
||||||
|
closeBtn.addEventListener('click', () => {
|
||||||
|
toast.remove();
|
||||||
|
});
|
||||||
|
toast.appendChild(closeBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到容器
|
||||||
|
toastContainer.appendChild(toast);
|
||||||
|
|
||||||
|
// 非持久性提示自动消失
|
||||||
|
if (!persistent) {
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toast;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建Toast容器
|
||||||
|
* @returns {HTMLElement} Toast容器元素
|
||||||
|
*/
|
||||||
|
createToastContainer() {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'toast-container';
|
||||||
|
document.body.appendChild(container);
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新API密钥状态
|
||||||
|
* 每次加载设置时自动调用,无需用户手动点击按钮
|
||||||
|
*/
|
||||||
|
async refreshApiKeyStatus() {
|
||||||
|
try {
|
||||||
|
// 先将所有状态显示为"检查中"
|
||||||
|
Object.keys(this.apiKeyValues).forEach(keyId => {
|
||||||
|
const statusElement = document.getElementById(`${keyId}Status`);
|
||||||
|
if (statusElement) {
|
||||||
|
statusElement.className = 'key-status checking';
|
||||||
|
statusElement.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 检查中...';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发送请求获取API密钥状态
|
||||||
|
const response = await fetch('/api/keys', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const apiKeys = await response.json();
|
||||||
|
this.updateApiKeyStatus(apiKeys);
|
||||||
|
console.log('API密钥状态已刷新');
|
||||||
|
} else {
|
||||||
|
console.error('刷新API密钥状态失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新API密钥状态出错:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSettingsPanel() {
|
||||||
|
if (this.settingsPanel) {
|
||||||
|
this.settingsPanel.classList.toggle('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeSettingsPanel() {
|
||||||
|
if (this.settingsPanel) {
|
||||||
|
this.settingsPanel.classList.remove('active');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
204
static/js/ui.js
204
static/js/ui.js
@@ -1,5 +1,16 @@
|
|||||||
class UIManager {
|
class UIManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
// 延迟初始化,确保DOM已加载
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => this.init());
|
||||||
|
} else {
|
||||||
|
// 如果DOM已经加载完成,则立即初始化
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
console.log('初始化UI管理器...');
|
||||||
// UI elements
|
// UI elements
|
||||||
this.settingsPanel = document.getElementById('settingsPanel');
|
this.settingsPanel = document.getElementById('settingsPanel');
|
||||||
this.settingsToggle = document.getElementById('settingsToggle');
|
this.settingsToggle = document.getElementById('settingsToggle');
|
||||||
@@ -7,11 +18,34 @@ class UIManager {
|
|||||||
this.themeToggle = document.getElementById('themeToggle');
|
this.themeToggle = document.getElementById('themeToggle');
|
||||||
this.toastContainer = document.getElementById('toastContainer');
|
this.toastContainer = document.getElementById('toastContainer');
|
||||||
|
|
||||||
|
// 验证关键元素是否存在
|
||||||
|
if (!this.themeToggle) {
|
||||||
|
console.error('主题切换按钮未找到!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.toastContainer) {
|
||||||
|
console.error('Toast容器未找到!');
|
||||||
|
// 尝试创建Toast容器
|
||||||
|
this.toastContainer = this.createToastContainer();
|
||||||
|
}
|
||||||
|
|
||||||
// Check for preferred color scheme
|
// Check for preferred color scheme
|
||||||
this.checkPreferredColorScheme();
|
this.checkPreferredColorScheme();
|
||||||
|
|
||||||
// Initialize event listeners
|
// Initialize event listeners
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
|
|
||||||
|
console.log('UI管理器初始化完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
createToastContainer() {
|
||||||
|
console.log('创建Toast容器');
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.id = 'toastContainer';
|
||||||
|
container.className = 'toast-container';
|
||||||
|
document.body.appendChild(container);
|
||||||
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkPreferredColorScheme() {
|
checkPreferredColorScheme() {
|
||||||
@@ -28,73 +62,195 @@ class UIManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTheme(isDark) {
|
setTheme(isDark) {
|
||||||
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
try {
|
||||||
this.themeToggle.innerHTML = `<i class="fas fa-${isDark ? 'sun' : 'moon'}"></i>`;
|
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
||||||
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
if (this.themeToggle) {
|
||||||
|
this.themeToggle.innerHTML = `<i class="fas fa-${isDark ? 'sun' : 'moon'}"></i>`;
|
||||||
|
}
|
||||||
|
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||||||
|
console.log(`主题已切换为: ${isDark ? '深色' : '浅色'}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('设置主题时出错:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast(message, type = 'success') {
|
/**
|
||||||
|
* 显示一个Toast消息
|
||||||
|
* @param {string} message 显示的消息内容
|
||||||
|
* @param {string} type 消息类型,可以是'success', 'error', 'info', 'warning'
|
||||||
|
* @param {number} displayTime 显示的时间(毫秒),如果为-1则持续显示直到手动关闭
|
||||||
|
* @returns {HTMLElement} 返回创建的Toast元素,可用于后续移除
|
||||||
|
*/
|
||||||
|
showToast(message, type = 'success', displayTime) {
|
||||||
|
if (!this.toastContainer) {
|
||||||
|
console.error('Toast容器不存在,无法显示消息');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否已经存在相同内容的提示
|
// 检查是否已经存在相同内容的提示
|
||||||
const existingToasts = this.toastContainer.querySelectorAll('.toast');
|
const existingToasts = this.toastContainer.querySelectorAll('.toast');
|
||||||
for (const existingToast of existingToasts) {
|
for (const existingToast of existingToasts) {
|
||||||
const existingMessage = existingToast.querySelector('span').textContent;
|
const existingMessage = existingToast.querySelector('span').textContent;
|
||||||
if (existingMessage === message) {
|
if (existingMessage === message) {
|
||||||
// 已经存在相同的提示,不再创建新的
|
// 已经存在相同的提示,不再创建新的
|
||||||
return;
|
return existingToast;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
toast.className = `toast ${type}`;
|
toast.className = `toast ${type}`;
|
||||||
|
|
||||||
|
// 根据类型设置图标
|
||||||
|
let icon = 'check-circle';
|
||||||
|
if (type === 'error') icon = 'exclamation-circle';
|
||||||
|
else if (type === 'warning') icon = 'exclamation-triangle';
|
||||||
|
else if (type === 'info') icon = 'info-circle';
|
||||||
|
|
||||||
toast.innerHTML = `
|
toast.innerHTML = `
|
||||||
<i class="fas fa-${type === 'success' ? 'check-circle' : 'exclamation-circle'}"></i>
|
<i class="fas fa-${icon}"></i>
|
||||||
<span>${message}</span>
|
<span>${message}</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// 如果是持续显示的Toast,添加关闭按钮
|
||||||
|
if (displayTime === -1) {
|
||||||
|
const closeButton = document.createElement('button');
|
||||||
|
closeButton.className = 'toast-close';
|
||||||
|
closeButton.innerHTML = '<i class="fas fa-times"></i>';
|
||||||
|
closeButton.addEventListener('click', (e) => {
|
||||||
|
this.hideToast(toast);
|
||||||
|
});
|
||||||
|
toast.appendChild(closeButton);
|
||||||
|
toast.classList.add('persistent');
|
||||||
|
}
|
||||||
|
|
||||||
this.toastContainer.appendChild(toast);
|
this.toastContainer.appendChild(toast);
|
||||||
|
|
||||||
// 为不同类型的提示设置不同的显示时间
|
// 为不同类型的提示设置不同的显示时间
|
||||||
const displayTime = message === '截图成功' ? 1500 : 3000;
|
if (displayTime !== -1) {
|
||||||
|
// 如果没有指定时间,则根据消息类型和内容长度设置默认时间
|
||||||
|
if (displayTime === undefined) {
|
||||||
|
displayTime = message === '截图成功' ? 1500 :
|
||||||
|
type === 'error' ? 5000 :
|
||||||
|
message.length > 50 ? 4000 : 3000;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.hideToast(toast);
|
||||||
|
}, displayTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toast;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏一个Toast消息
|
||||||
|
* @param {HTMLElement} toast 要隐藏的Toast元素
|
||||||
|
*/
|
||||||
|
hideToast(toast) {
|
||||||
|
if (!toast || !toast.parentNode) return;
|
||||||
|
|
||||||
|
toast.style.opacity = '0';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
toast.style.opacity = '0';
|
if (toast.parentNode) {
|
||||||
setTimeout(() => toast.remove(), 300);
|
toast.remove();
|
||||||
}, displayTime);
|
}
|
||||||
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
closeAllPanels() {
|
closeAllPanels() {
|
||||||
this.settingsPanel.classList.add('hidden');
|
if (this.settingsPanel) {
|
||||||
|
this.settingsPanel.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSettingsPanel() {
|
||||||
|
if (this.settingsPanel) {
|
||||||
|
this.settingsPanel.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSettingsPanel() {
|
||||||
|
if (this.settingsPanel) {
|
||||||
|
this.settingsPanel.classList.toggle('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeSettingsPanel() {
|
||||||
|
if (this.settingsPanel) {
|
||||||
|
this.settingsPanel.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查点击事件,如果点击了设置面板外部,则关闭设置面板
|
||||||
|
checkClickOutsideSettings(e) {
|
||||||
|
if (this.settingsPanel &&
|
||||||
|
!this.settingsPanel.contains(e.target) &&
|
||||||
|
!e.target.closest('#settingsToggle')) {
|
||||||
|
this.settingsPanel.classList.remove('active');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
|
// 确保所有元素都存在
|
||||||
|
if (!this.settingsToggle || !this.closeSettings || !this.themeToggle) {
|
||||||
|
console.error('无法设置事件监听器:一些UI元素未找到');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Settings panel
|
// Settings panel
|
||||||
this.settingsToggle.addEventListener('click', () => {
|
this.settingsToggle.addEventListener('click', () => {
|
||||||
this.closeAllPanels();
|
this.closeAllPanels();
|
||||||
this.settingsPanel.classList.toggle('hidden');
|
this.settingsPanel.classList.toggle('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
this.closeSettings.addEventListener('click', () => {
|
this.closeSettings.addEventListener('click', () => {
|
||||||
this.settingsPanel.classList.add('hidden');
|
this.settingsPanel.classList.remove('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Theme toggle
|
// Theme toggle
|
||||||
this.themeToggle.addEventListener('click', () => {
|
this.themeToggle.addEventListener('click', () => {
|
||||||
const currentTheme = document.documentElement.getAttribute('data-theme');
|
try {
|
||||||
this.setTheme(currentTheme !== 'dark');
|
const currentTheme = document.documentElement.getAttribute('data-theme');
|
||||||
|
console.log('当前主题:', currentTheme);
|
||||||
|
this.setTheme(currentTheme !== 'dark');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('切换主题时出错:', error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close panels when clicking outside
|
// Close panels when clicking outside
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
// Only close if click is outside the panel and not on the toggle button
|
this.checkClickOutsideSettings(e);
|
||||||
if (this.settingsPanel &&
|
|
||||||
!this.settingsPanel.contains(e.target) &&
|
|
||||||
!e.target.closest('#settingsToggle')) {
|
|
||||||
this.settingsPanel.classList.add('hidden');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export for use in other modules
|
// 创建全局实例
|
||||||
window.UIManager = UIManager;
|
window.UIManager = UIManager;
|
||||||
window.showToast = (message, type) => window.uiManager.showToast(message, type);
|
|
||||||
window.closeAllPanels = () => window.uiManager.closeAllPanels();
|
// 确保在DOM加载完毕后才创建UIManager实例
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
window.uiManager = new UIManager();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.uiManager = new UIManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出全局辅助函数
|
||||||
|
window.showToast = (message, type) => {
|
||||||
|
if (window.uiManager) {
|
||||||
|
return window.uiManager.showToast(message, type);
|
||||||
|
} else {
|
||||||
|
console.error('UI管理器未初始化,无法显示Toast');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.closeAllPanels = () => {
|
||||||
|
if (window.uiManager) {
|
||||||
|
window.uiManager.closeAllPanels();
|
||||||
|
} else {
|
||||||
|
console.error('UI管理器未初始化,无法关闭面板');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
1498
static/style.css
1498
static/style.css
File diff suppressed because it is too large
Load Diff
@@ -124,50 +124,54 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside id="settingsPanel" class="settings-panel hidden">
|
<aside id="settingsPanel" class="settings-panel">
|
||||||
<div class="panel-header">
|
<div class="settings-header">
|
||||||
<h2>设置</h2>
|
<h2><i class="fas fa-cog"></i> 设置</h2>
|
||||||
<button class="btn-icon" id="closeSettings">
|
<button id="closeSettings" class="btn-icon">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-content">
|
<div class="settings-content">
|
||||||
<!-- 1. 首先是最常用的AI模型选择部分 -->
|
<!-- 1. 首先是最常用的AI模型选择部分 -->
|
||||||
<div class="settings-section">
|
<div class="settings-section model-settings">
|
||||||
<h3><i class="fas fa-robot"></i> 模型设置</h3>
|
<h3><i class="fas fa-robot"></i> 模型设置</h3>
|
||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<label for="modelSelect"><i class="fas fa-microchip"></i> 选择模型</label>
|
<label for="modelSelect"><i class="fas fa-microchip"></i> AI模型</label>
|
||||||
<select id="modelSelect" class="select-styled">
|
<select id="modelSelect">
|
||||||
<!-- 选项将通过JavaScript动态加载 -->
|
<option value="gpt-4o">OpenAI - GPT-4o</option>
|
||||||
|
<option value="gpt-4-turbo">OpenAI - GPT-4 Turbo</option>
|
||||||
|
<option value="gpt-4">OpenAI - GPT-4</option>
|
||||||
|
<option value="gpt-3.5-turbo">OpenAI - GPT-3.5 Turbo</option>
|
||||||
|
<option value="claude-3-opus-20240229">Anthropic - Claude 3 Opus</option>
|
||||||
|
<option value="claude-3-sonnet-20240229">Anthropic - Claude 3 Sonnet</option>
|
||||||
|
<option value="claude-3-haiku-20240307" selected>Anthropic - Claude 3 Haiku</option>
|
||||||
|
<option value="deepseek-v2">DeepSeek - DeepSeek-V2</option>
|
||||||
|
<option value="qwen-max">Alibaba - 通义千问 Max</option>
|
||||||
</select>
|
</select>
|
||||||
<div id="modelVersionInfo" class="model-version-info">
|
<div id="modelVersionInfo" class="model-version-info">
|
||||||
<i class="fas fa-info-circle"></i> 版本: <span id="modelVersionText">-</span>
|
<i class="fas fa-info-circle"></i> <span>版本: <span id="modelVersionText">-</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<label for="maxTokens"><i class="fas fa-text-width"></i> 最大输出Token</label>
|
<label for="maxTokens"><i class="fas fa-text-width"></i> 最大输出Token</label>
|
||||||
<input type="number" id="maxTokens" min="1000" max="128000" step="1000" value="8192" class="input-styled">
|
<input type="number" id="maxTokens" min="1000" max="128000" step="1000" value="8192">
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-group reasoning-setting-group">
|
<div class="setting-group reasoning-setting-group">
|
||||||
<label for="reasoningDepth"><i class="fas fa-brain"></i> 推理深度</label>
|
<label for="reasoningDepth"><i class="fas fa-brain"></i> 推理深度</label>
|
||||||
<select id="reasoningDepth" class="select-styled">
|
<select id="reasoningDepth">
|
||||||
<option value="standard">标准模式 (快速响应)</option>
|
<option value="standard">标准模式 (快速响应)</option>
|
||||||
<option value="extended">深度思考 (更详细分析)</option>
|
<option value="extended">深度思考 (更详细分析)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-group think-budget-group">
|
<div class="setting-group think-budget-group">
|
||||||
<label for="thinkBudgetPercent"><i class="fas fa-hourglass-half"></i> 思考预算占比</label>
|
<label for="thinkBudgetPercent"><i class="fas fa-hourglass-half"></i> 思考预算占比</label>
|
||||||
<div class="range-group">
|
<input type="range" id="thinkBudgetPercent" min="10" max="80" step="5" value="50">
|
||||||
<input type="range" id="thinkBudgetPercent" min="10" max="80" step="5" value="50">
|
<span id="thinkBudgetPercentValue">50%</span>
|
||||||
<span id="thinkBudgetPercentValue">50%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<label for="temperature"><i class="fas fa-thermometer-half"></i> 温度</label>
|
<label for="temperature"><i class="fas fa-thermometer-half"></i> 温度</label>
|
||||||
<div class="range-group">
|
<input type="range" id="temperature" min="0" max="1" step="0.1" value="0.7">
|
||||||
<input type="range" id="temperature" min="0" max="1" step="0.1" value="0.7">
|
<span id="temperatureValue">0.7</span>
|
||||||
<span id="temperatureValue">0.7</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<label for="systemPrompt"><i class="fas fa-comment-alt"></i> 系统提示词</label>
|
<label for="systemPrompt"><i class="fas fa-comment-alt"></i> 系统提示词</label>
|
||||||
@@ -176,73 +180,148 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 2. 所有API密钥集中在一个区域 -->
|
<!-- 2. 所有API密钥集中在一个区域 -->
|
||||||
<div class="settings-section">
|
<div class="settings-section api-key-settings">
|
||||||
<div class="collapsible-header" id="apiKeysCollapseToggle">
|
<h3><i class="fas fa-key"></i> API密钥设置</h3>
|
||||||
<h3><i class="fas fa-key"></i> API密钥设置</h3>
|
|
||||||
<button class="toggle-btn">
|
<!-- API密钥状态显示与编辑区域 -->
|
||||||
<i class="fas fa-chevron-down"></i>
|
<div class="api-keys-list" id="apiKeysList">
|
||||||
</button>
|
<div class="api-key-status">
|
||||||
</div>
|
<span class="key-name">Anthropic API:</span>
|
||||||
<div class="collapsible-content collapsed" id="apiKeysContent">
|
<div class="key-status-wrapper">
|
||||||
<div class="setting-group api-key-group">
|
<!-- 显示状态 -->
|
||||||
<label for="AnthropicApiKey">Anthropic API Key</label>
|
<div class="key-display">
|
||||||
<div class="input-group">
|
<span id="AnthropicApiKeyStatus" class="key-status" data-key="AnthropicApiKey">未设置</span>
|
||||||
<input type="password" id="AnthropicApiKey" placeholder="输入 Anthropic API key">
|
<button class="btn-icon edit-api-key" data-key-type="AnthropicApiKey" title="编辑此密钥">
|
||||||
<button class="btn-icon toggle-api-key">
|
<i class="fas fa-edit"></i>
|
||||||
<i class="fas fa-eye"></i>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
|
<!-- 编辑状态 -->
|
||||||
|
<div class="key-edit hidden">
|
||||||
|
<input type="password" class="key-input" data-key-type="AnthropicApiKey" placeholder="输入 Anthropic 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="AnthropicApiKey" title="保存密钥">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-group api-key-group">
|
<div class="api-key-status">
|
||||||
<label for="OpenaiApiKey">OpenAI API Key</label>
|
<span class="key-name">OpenAI API:</span>
|
||||||
<div class="input-group">
|
<div class="key-status-wrapper">
|
||||||
<input type="password" id="OpenaiApiKey" placeholder="输入 OpenAI API key">
|
<!-- 显示状态 -->
|
||||||
<button class="btn-icon toggle-api-key">
|
<div class="key-display">
|
||||||
<i class="fas fa-eye"></i>
|
<span id="OpenaiApiKeyStatus" class="key-status" data-key="OpenaiApiKey">未设置</span>
|
||||||
</button>
|
<button class="btn-icon edit-api-key" data-key-type="OpenaiApiKey" title="编辑此密钥">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- 编辑状态 -->
|
||||||
|
<div class="key-edit hidden">
|
||||||
|
<input type="password" class="key-input" data-key-type="OpenaiApiKey" placeholder="输入 OpenAI 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="OpenaiApiKey" title="保存密钥">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-group api-key-group">
|
<div class="api-key-status">
|
||||||
<label for="DeepseekApiKey">DeepSeek API Key</label>
|
<span class="key-name">DeepSeek API:</span>
|
||||||
<div class="input-group">
|
<div class="key-status-wrapper">
|
||||||
<input type="password" id="DeepseekApiKey" placeholder="输入 DeepSeek API key">
|
<!-- 显示状态 -->
|
||||||
<button class="btn-icon toggle-api-key">
|
<div class="key-display">
|
||||||
<i class="fas fa-eye"></i>
|
<span id="DeepseekApiKeyStatus" class="key-status" data-key="DeepseekApiKey">未设置</span>
|
||||||
</button>
|
<button class="btn-icon edit-api-key" data-key-type="DeepseekApiKey" title="编辑此密钥">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- 编辑状态 -->
|
||||||
|
<div class="key-edit hidden">
|
||||||
|
<input type="password" class="key-input" data-key-type="DeepseekApiKey" placeholder="输入 DeepSeek 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="DeepseekApiKey" title="保存密钥">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-group api-key-group">
|
<div class="api-key-status">
|
||||||
<label for="AlibabaApiKey">Alibaba API Key</label>
|
<span class="key-name">Alibaba API:</span>
|
||||||
<div class="input-group">
|
<div class="key-status-wrapper">
|
||||||
<input type="password" id="AlibabaApiKey" placeholder="输入 Alibaba API key">
|
<!-- 显示状态 -->
|
||||||
<button class="btn-icon toggle-api-key">
|
<div class="key-display">
|
||||||
<i class="fas fa-eye"></i>
|
<span id="AlibabaApiKeyStatus" class="key-status" data-key="AlibabaApiKey">未设置</span>
|
||||||
</button>
|
<button class="btn-icon edit-api-key" data-key-type="AlibabaApiKey" title="编辑此密钥">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- 编辑状态 -->
|
||||||
|
<div class="key-edit hidden">
|
||||||
|
<input type="password" class="key-input" data-key-type="AlibabaApiKey" placeholder="输入 Alibaba 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="AlibabaApiKey" title="保存密钥">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-group api-key-group">
|
<div class="api-key-status">
|
||||||
<label for="mathpixAppId">Mathpix App ID</label>
|
<span class="key-name">Mathpix App ID:</span>
|
||||||
<div class="input-group">
|
<div class="key-status-wrapper">
|
||||||
<input type="password" id="mathpixAppId" placeholder="输入 Mathpix App ID">
|
<!-- 显示状态 -->
|
||||||
<button class="btn-icon toggle-api-key">
|
<div class="key-display">
|
||||||
<i class="fas fa-eye"></i>
|
<span id="MathpixAppIdStatus" class="key-status" data-key="MathpixAppId">未设置</span>
|
||||||
</button>
|
<button class="btn-icon edit-api-key" data-key-type="MathpixAppId" title="编辑此密钥">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- 编辑状态 -->
|
||||||
|
<div class="key-edit hidden">
|
||||||
|
<input type="password" class="key-input" data-key-type="MathpixAppId" placeholder="输入 Mathpix App ID">
|
||||||
|
<button class="btn-icon toggle-visibility">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn-icon save-api-key" data-key-type="MathpixAppId" title="保存密钥">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-group api-key-group">
|
<div class="api-key-status">
|
||||||
<label for="mathpixAppKey">Mathpix App Key</label>
|
<span class="key-name">Mathpix App Key:</span>
|
||||||
<div class="input-group">
|
<div class="key-status-wrapper">
|
||||||
<input type="password" id="mathpixAppKey" placeholder="输入 Mathpix App Key">
|
<!-- 显示状态 -->
|
||||||
<button class="btn-icon toggle-api-key">
|
<div class="key-display">
|
||||||
<i class="fas fa-eye"></i>
|
<span id="MathpixAppKeyStatus" class="key-status" data-key="MathpixAppKey">未设置</span>
|
||||||
</button>
|
<button class="btn-icon edit-api-key" data-key-type="MathpixAppKey" title="编辑此密钥">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- 编辑状态 -->
|
||||||
|
<div class="key-edit hidden">
|
||||||
|
<input type="password" class="key-input" data-key-type="MathpixAppKey" placeholder="输入 Mathpix App Key">
|
||||||
|
<button class="btn-icon toggle-visibility">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn-icon save-api-key" data-key-type="MathpixAppKey" title="保存密钥">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 3. 不常用的其他设置放在后面 -->
|
<!-- 3. 不常用的其他设置放在后面 -->
|
||||||
<div class="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>
|
||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<label for="language"><i class="fas fa-language"></i> 语言</label>
|
<label for="language"><i class="fas fa-language"></i> 语言</label>
|
||||||
@@ -265,6 +344,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 设置面板底部按钮 -->
|
||||||
|
<div class="settings-footer">
|
||||||
|
<button id="resetSettings" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-undo"></i> 重置设置
|
||||||
|
</button>
|
||||||
|
<button id="saveSettings" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> 保存设置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</main>
|
</main>
|
||||||
@@ -305,8 +394,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<!-- 确保按照正确的顺序加载脚本 -->
|
||||||
|
<!-- 先加载UI管理器,确保它能在DOM加载完成后初始化 -->
|
||||||
<script src="{{ url_for('static', filename='js/ui.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/ui.js') }}"></script>
|
||||||
|
<!-- 然后加载设置管理器,它依赖UI管理器 -->
|
||||||
<script src="{{ url_for('static', filename='js/settings.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/settings.js') }}"></script>
|
||||||
|
<!-- 最后加载主应用逻辑 -->
|
||||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
|
|
||||||
<!-- 更新检查初始化 -->
|
<!-- 更新检查初始化 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user