diff --git a/app.py b/app.py
index ff00b97..ce43eee 100644
--- a/app.py
+++ b/app.py
@@ -32,6 +32,9 @@ socketio = SocketIO(
# 添加配置文件路径
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 = {}
@@ -76,7 +79,11 @@ def create_model_instance(model_id, settings, is_reasoning=False):
# 确定需要哪个API密钥
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"
elif any(keyword in model_id.lower() for keyword in ["gpt", "openai"]):
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():
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:
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):
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:
raise ValueError("Mathpix API key is required")
@@ -624,6 +647,232 @@ def get_models():
models = ModelFactory.get_available_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__':
local_ip = get_local_ip()
print(f"Local IP Address: {local_ip}")
diff --git a/models/alibaba.py b/models/alibaba.py
index d214b12..e1e2b29 100644
--- a/models/alibaba.py
+++ b/models/alibaba.py
@@ -5,7 +5,9 @@ from .base import 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):
- 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__(api_key, temperature, system_prompt, language)
@@ -36,22 +38,35 @@ class AlibabaModel(BaseModel):
"qwen-vl-max-latest": "qwen-vl-max", # 修正为正确的API标识符
}
+ print(f"模型名称: {self.model_name}")
+
# 从模型映射表中获取模型标识符,如果不存在则使用默认值
model_id = model_mapping.get(self.model_name)
if model_id:
+ print(f"从映射表中获取到模型标识符: {model_id}")
return model_id
# 如果没有精确匹配,检查是否包含特定前缀
- if self.model_name and "qwen-vl" in self.model_name:
- if "max" in self.model_name:
+ if self.model_name and "qwen-vl" in self.model_name.lower():
+ if "max" in self.model_name.lower():
+ print(f"识别为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"
- elif "lite" in self.model_name:
+ elif "lite" in self.model_name.lower():
+ print(f"识别为qwen-vl-lite模型")
return "qwen-vl-lite"
+ print(f"默认使用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"
def analyze_text(self, text: str, proxies: dict = None) -> Generator[dict, None, None]:
@@ -107,7 +122,8 @@ class AlibabaModel(BaseModel):
is_answering = False
# 检查是否为通义千问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:
if not chunk.choices:
@@ -237,7 +253,8 @@ class AlibabaModel(BaseModel):
is_answering = False
# 检查是否为通义千问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:
if not chunk.choices:
diff --git a/models/anthropic.py b/models/anthropic.py
index bec2f4a..19d98fc 100644
--- a/models/anthropic.py
+++ b/models/anthropic.py
@@ -56,7 +56,7 @@ class AnthropicModel(BaseModel):
# 处理推理配置
if hasattr(self, 'reasoning_config') and self.reasoning_config:
# 如果设置了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)
payload['thinking'] = {
'type': 'enabled',
@@ -64,7 +64,6 @@ class AnthropicModel(BaseModel):
}
# 如果设置了instant模式
elif self.reasoning_config.get('speed_mode') == 'instant':
- payload['speed_mode'] = 'instant'
# 确保当使用speed_mode时不包含thinking参数
if 'thinking' in payload:
del payload['thinking']
diff --git a/models/deepseek.py b/models/deepseek.py
index 6fc53bd..fb18522 100644
--- a/models/deepseek.py
+++ b/models/deepseek.py
@@ -23,8 +23,22 @@ class DeepSeekModel(BaseModel):
# 通过模型名称来确定实际的API调用标识符
if self.model_name == "deepseek-chat":
return "deepseek-chat"
- # deepseek-reasoner是默认的推理模型名称
- return "deepseek-reasoner"
+ # 如果是deepseek-reasoner或包含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]:
"""Stream DeepSeek's response for text analysis"""
@@ -75,10 +89,10 @@ class DeepSeekModel(BaseModel):
}
# 只有非推理模型才设置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
- 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)
@@ -253,7 +267,7 @@ class DeepSeekModel(BaseModel):
}
# 只有非推理模型才设置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
response = client.chat.completions.create(**params)
diff --git a/models/factory.py b/models/factory.py
index 38b34b8..7019265 100644
--- a/models/factory.py
+++ b/models/factory.py
@@ -98,36 +98,26 @@ class ModelFactory:
@classmethod
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:
- model_name: The identifier of the model to create
- api_key: The API key for the model
- temperature: Optional temperature parameter for response generation
- system_prompt: Optional custom system prompt
- language: Optional language preference for responses
+ model_name: The identifier for the model
+ api_key: The API key for the model service
+ temperature: The temperature to use for generation
+ system_prompt: The system prompt to use
+ language: The preferred language for responses
Returns:
- An instance of the specified model
-
- Raises:
- ValueError: If the model_name is not recognized
+ A model instance
"""
- model_info = cls._models.get(model_name)
- if not model_info:
+ if model_name not in cls._models:
raise ValueError(f"Unknown model: {model_name}")
-
+
+ model_info = cls._models[model_name]
model_class = model_info['class']
- # 对于Mathpix模型,不传递language参数
- if model_name == 'mathpix':
- return model_class(
- api_key=api_key,
- temperature=temperature,
- system_prompt=system_prompt
- )
- else:
- # 对于所有其他模型,传递model_name参数
+ # 对于DeepSeek模型,需要传递正确的模型名称
+ if 'deepseek' in model_name.lower():
return model_class(
api_key=api_key,
temperature=temperature,
@@ -135,6 +125,30 @@ class ModelFactory:
language=language,
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
def get_available_models(cls) -> list[Dict[str, Any]]:
diff --git a/static/js/main.js b/static/js/main.js
index 5061ecc..bc2c15c 100644
--- a/static/js/main.js
+++ b/static/js/main.js
@@ -1010,7 +1010,7 @@ class SnapSolver {
if (!mathpixApiKey || mathpixApiKey === ':') {
window.uiManager.showToast('请在设置中输入Mathpix API凭据', 'error');
- document.getElementById('settingsPanel').classList.remove('hidden');
+ document.getElementById('settingsPanel').classList.add('active');
this.extractTextBtn.disabled = false;
this.extractTextBtn.innerHTML = '提取文本';
return;
@@ -1282,14 +1282,34 @@ class SnapSolver {
}
}
- initialize() {
+ async initialize() {
console.log('Initializing SnapSolver...');
// 初始化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.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工具
this.initializeMarkdownTools();
@@ -1554,9 +1574,9 @@ class SnapSolver {
!e.target.closest('#settingsPanel') &&
!e.target.matches('#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面板、分析按钮或其子元素之外
@@ -1654,10 +1674,11 @@ class SnapSolver {
}
// Initialize the application when the DOM is loaded
-document.addEventListener('DOMContentLoaded', () => {
+document.addEventListener('DOMContentLoaded', async () => {
try {
console.log('Initializing application...');
window.app = new SnapSolver();
+ await window.app.initialize();
console.log('Application initialized successfully');
} catch (error) {
console.error('Failed to initialize application:', error);
diff --git a/static/js/settings.js b/static/js/settings.js
index c2f0ee0..c3e7721 100644
--- a/static/js/settings.js
+++ b/static/js/settings.js
@@ -8,28 +8,42 @@ class SettingsManager {
this.initializeElements();
// 加载模型配置
- this.loadModelConfig()
- .then(() => {
+ this.isInitialized = false;
+ this.initialize();
+ }
+
+ async initialize() {
+ try {
+ // 加载模型配置
+ await this.loadModelConfig();
+
// 成功加载配置后,执行后续初始化
this.updateModelOptions();
- this.loadSettings();
+ await this.loadSettings();
this.setupEventListeners();
this.updateUIBasedOnModelType();
- })
- .catch(error => {
- console.error('加载模型配置失败:', error);
+
+ // 初始化可折叠内容逻辑
+ this.initCollapsibleContent();
+
+ this.isInitialized = true;
+ console.log('设置管理器初始化完成');
+ } catch (error) {
+ console.error('初始化设置管理器失败:', error);
window.uiManager?.showToast('加载模型配置失败,使用默认配置', 'error');
// 使用默认配置作为备份
this.setupDefaultModels();
this.updateModelOptions();
- this.loadSettings();
+ await this.loadSettings();
this.setupEventListeners();
this.updateUIBasedOnModelType();
- });
// 初始化可折叠内容逻辑
this.initCollapsibleContent();
+
+ this.isInitialized = true;
+ }
}
// 从配置文件加载模型定义
@@ -186,6 +200,16 @@ class SettingsManager {
'mathpixAppKey': this.mathpixAppKeyInput
};
+ // API密钥状态显示相关元素
+ this.apiKeysList = document.getElementById('apiKeysList');
+
+ // 防止API密钥区域的点击事件冒泡
+ if (this.apiKeysList) {
+ this.apiKeysList.addEventListener('click', (e) => {
+ e.stopPropagation();
+ });
+ }
+
// Settings toggle elements
this.settingsToggle = document.getElementById('settingsToggle');
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') || '{}');
- // Load Mathpix credentials
- if (settings.mathpixAppId) {
- this.mathpixAppIdInput.value = settings.mathpixAppId;
- }
- 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;
- }
- });
- }
-
+ // 刷新API密钥状态(自动从服务器获取最新状态)
+ await this.refreshApiKeyStatus();
+ console.log('已自动刷新API密钥状态');
+
+ // 加载其他设置
// Load model selection
if (settings.model && this.modelExists(settings.model)) {
this.modelSelect.value = settings.model;
@@ -329,6 +394,11 @@ class SettingsManager {
// Update UI based on model type
this.updateUIBasedOnModelType();
+
+ } catch (error) {
+ console.error('加载设置出错:', error);
+ window.uiManager?.showToast('加载设置出错', 'error');
+ }
}
modelExists(modelId) {
@@ -356,21 +426,17 @@ class SettingsManager {
}
}
- updateVisibleApiKey(selectedModel) {
- const modelInfo = this.modelDefinitions[selectedModel];
- if (!modelInfo) return;
-
- // 仅更新模型版本显示
- this.updateModelVersionDisplay(selectedModel);
-
- // 不再需要高亮API密钥
- // 这里我们不再进行API密钥的高亮处理
- }
-
+ /**
+ * 根据选择的模型类型更新UI显示
+ */
updateUIBasedOnModelType() {
+ // 更新UI元素显示,根据所选模型类型
const selectedModel = this.modelSelect.value;
const modelInfo = this.modelDefinitions[selectedModel];
+ // 更新当前可见的API密钥
+ this.updateVisibleApiKey(selectedModel);
+
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 = {
- apiKeys: {},
- mathpixAppId: this.mathpixAppIdInput.value,
- mathpixAppKey: this.mathpixAppKeyInput.value,
+ apiKeys: this.apiKeyValues, // 保存到localStorage(向后兼容)
model: this.modelSelect.value,
maxTokens: this.maxTokensInput.value,
reasoningDepth: this.reasoningDepthSelect?.value || 'standard',
@@ -421,15 +523,14 @@ class SettingsManager {
proxyPort: this.proxyPortInput.value
};
- // Save all API keys
- Object.entries(this.apiKeyInputs).forEach(([keyId, input]) => {
- if (input.value) {
- settings.apiKeys[keyId] = input.value;
- }
- });
-
+ // 保存设置到localStorage
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() {
@@ -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 {
model: selectedModel,
maxTokens: maxTokens,
@@ -491,7 +597,7 @@ class SettingsManager {
proxyEnabled: this.proxyEnabledInput.checked,
proxyHost: this.proxyHostInput.value,
proxyPort: this.proxyPortInput.value,
- mathpixApiKey: `${this.mathpixAppIdInput.value}:${this.mathpixAppKeyInput.value}`,
+ mathpixApiKey: mathpixApiKey,
modelInfo: {
supportsMultimodal: modelInfo.supportsMultimodal || false,
isReasoning: modelInfo.isReasoning || false,
@@ -512,15 +618,6 @@ class SettingsManager {
}
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.updateVisibleApiKey(e.target.value);
this.updateUIBasedOnModelType();
@@ -536,6 +633,9 @@ class SettingsManager {
// 最大Token输入框事件处理
if (this.maxTokensInput) {
this.maxTokensInput.addEventListener('change', (e) => {
+ // 阻止事件冒泡
+ e.stopPropagation();
+
// 验证输入值在有效范围内
let value = parseInt(e.target.value);
if (isNaN(value)) value = 8192;
@@ -551,7 +651,10 @@ class SettingsManager {
// 推理深度选择事件处理
if (this.reasoningDepthSelect) {
- this.reasoningDepthSelect.addEventListener('change', () => {
+ this.reasoningDepthSelect.addEventListener('change', (e) => {
+ // 阻止事件冒泡
+ e.stopPropagation();
+
// 更新思考预算组的可见性
if (this.thinkBudgetGroup) {
const showThinkBudget = this.reasoningDepthSelect.value === 'extended';
@@ -564,6 +667,9 @@ class SettingsManager {
// 思考预算占比滑块事件处理
if (this.thinkBudgetPercentInput && this.thinkBudgetPercentValue) {
this.thinkBudgetPercentInput.addEventListener('input', (e) => {
+ // 阻止事件冒泡
+ e.stopPropagation();
+
// 更新思考预算显示
this.updateThinkBudgetDisplay();
@@ -575,29 +681,78 @@ class SettingsManager {
}
this.temperatureInput.addEventListener('input', (e) => {
+ // 阻止事件冒泡
+ e.stopPropagation();
+
this.temperatureValue.textContent = e.target.value;
this.updateRangeSliderBackground(e.target);
this.saveSettings();
});
- this.systemPromptInput.addEventListener('change', () => this.saveSettings());
- this.languageInput.addEventListener('change', () => this.saveSettings());
+ this.systemPromptInput.addEventListener('change', (e) => {
+ // 阻止事件冒泡
+ e.stopPropagation();
+ this.saveSettings();
+ });
+
+ this.languageInput.addEventListener('change', (e) => {
+ // 阻止事件冒泡
+ e.stopPropagation();
+ this.saveSettings();
+ });
+
this.proxyEnabledInput.addEventListener('change', (e) => {
+ // 阻止事件冒泡
+ e.stopPropagation();
+
this.proxySettings.style.display = e.target.checked ? 'block' : 'none';
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
this.settingsToggle.addEventListener('click', () => {
- window.closeAllPanels();
- this.settingsPanel.classList.toggle('hidden');
+ this.toggleSettingsPanel();
});
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() {
- // 获取API密钥折叠切换按钮和内容
- const apiKeysToggle = document.getElementById('apiKeysCollapseToggle');
- const apiKeysContent = document.getElementById('apiKeysContent');
-
- // 添加点击事件以切换折叠状态
- if (apiKeysToggle && apiKeysContent) {
- apiKeysToggle.addEventListener('click', () => {
- // 切换折叠状态
- apiKeysContent.classList.toggle('collapsed');
+ // 在新的实现中,我们不再需要折叠API密钥区域,因为所有功能都在同一区域完成
+ console.log('初始化API密钥编辑功能完成');
+ }
+
+ /**
+ * 初始化API密钥编辑相关功能
+ */
+ initApiKeyEditFunctions() {
+ // 1. 编辑按钮点击事件
+ document.querySelectorAll('.edit-api-key').forEach(button => {
+ button.addEventListener('click', (e) => {
+ // 阻止事件冒泡
+ e.stopPropagation();
- // 更新图标方向
- const icon = apiKeysToggle.querySelector('.fa-chevron-down, .fa-chevron-up');
- if (icon) {
- if (apiKeysContent.classList.contains('collapsed')) {
- icon.classList.replace('fa-chevron-up', 'fa-chevron-down');
- } else {
- icon.classList.replace('fa-chevron-down', 'fa-chevron-up');
+ const keyType = e.currentTarget.getAttribute('data-key-type');
+ const keyStatus = e.currentTarget.closest('.key-status-wrapper');
+
+ if (keyStatus) {
+ // 隐藏显示区域
+ const displayArea = keyStatus.querySelector('.key-display');
+ 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 = ` 已设置`;
+ } else {
+ // 显示密钥状态 - 未设置
+ statusElement.className = 'key-status not-set';
+ statusElement.innerHTML = ` 未设置`;
+ }
+ });
+ }
+
+ /**
+ * 保存单个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 = ` 已设置`;
+ } else {
+ statusElem.className = 'key-status not-set';
+ statusElem.innerHTML = ` 未设置`;
+ }
+ }
+
+ 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