From e1c068ed9eb385b3a0ee1b1a888daa4734b41e46 Mon Sep 17 00:00:00 2001 From: snaily Date: Thu, 8 May 2025 00:31:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=88=A0=E9=99=A4=E5=8A=9F=E8=83=BD=E5=B9=B6?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次提交主要包含以下内容: 1. **日志自动删除功能**: * 新增环境变量 (`AUTO_DELETE_ERROR_LOGS_ENABLED`, `AUTO_DELETE_ERROR_LOGS_DAYS`, `AUTO_DELETE_REQUEST_LOGS_ENABLED`, `AUTO_DELETE_REQUEST_LOGS_DAYS`) 用于控制错误日志和请求日志的自动删除策略。 * 在 `app/config/config.py` 中添加了对这些新配置项的支持和验证逻辑 (Pydantic `validator` 更新为 `field_validator`)。 * 修改了 `app/log/logger.py` 以适应新的日志配置。 * 新增 `app/scheduler/scheduled_tasks.py` 用于执行定期的日志清理任务。 * 新增 `app/service/error_log/error_log_service.py` 和 `app/service/request_log/request_log_service.py` 来处理具体的日志删除逻辑。 * 更新了 `app/router/error_log_routes.py` 和 `app/router/scheduler_routes.py` 以集成新功能。 2. **前端配置页面更新**: * 在 `app/templates/config_editor.html` 和 `app/static/js/config_editor.js` 中添加了用于配置日志自动删除选项的用户界面元素。 3. **代码和文件结构调整**: * 删除了不再使用的 `app/scheduler/key_checker.py` 文件。 * 在 `.gitignore` 文件中添加了 `default_db` 以忽略该目录。 4. **其他**: * 对 `app/core/application.py` 进行了相应调整。 该更新旨在增强应用的日志管理能力,提供更灵活的日志保留策略,并优化了配置界面的用户体验。 --- .env.example | 8 + .gitignore | 3 +- app/config/config.py | 284 +- app/core/application.py | 2 +- app/log/logger.py | 7 +- app/router/error_log_routes.py | 114 +- app/router/scheduler_routes.py | 2 +- app/scheduler/key_checker.py | 102 - app/scheduler/scheduled_tasks.py | 162 ++ app/service/error_log/error_log_service.py | 155 ++ .../request_log/request_log_service.py | 50 + app/static/js/config_editor.js | 2411 ++++++++++------- app/templates/config_editor.html | 1852 +++++++++---- 13 files changed, 3316 insertions(+), 1836 deletions(-) delete mode 100644 app/scheduler/key_checker.py create mode 100644 app/scheduler/scheduled_tasks.py create mode 100644 app/service/error_log/error_log_service.py create mode 100644 app/service/request_log/request_log_service.py diff --git a/.env.example b/.env.example index 780dd5c..c59f58d 100644 --- a/.env.example +++ b/.env.example @@ -49,6 +49,14 @@ STREAM_CHUNK_SIZE=5 ######################### 日志配置 ####################################### # 日志级别 (debug, info, warning, error, critical),默认为 info LOG_LEVEL=info +# 是否开启自动删除错误日志 +AUTO_DELETE_ERROR_LOGS_ENABLED=true +# 自动删除多少天前的错误日志 (1, 7, 30) +AUTO_DELETE_ERROR_LOGS_DAYS=7 +# 是否开启自动删除请求日志 +AUTO_DELETE_REQUEST_LOGS_ENABLED=false +# 自动删除多少天前的请求日志 (1, 7, 30) +AUTO_DELETE_REQUEST_LOGS_DAYS=30 ########################################################################## # 安全设置 (JSON 字符串格式) diff --git a/.gitignore b/.gitignore index 6b20404..7f1319e 100644 --- a/.gitignore +++ b/.gitignore @@ -257,4 +257,5 @@ $RECYCLE.BIN/ # Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) -tests/ \ No newline at end of file +tests/ +default_db \ No newline at end of file diff --git a/app/config/config.py b/app/config/config.py index 6fe40d0..a1cece3 100644 --- a/app/config/config.py +++ b/app/config/config.py @@ -1,15 +1,29 @@ """ 应用程序配置模块 """ + import datetime import json -from typing import List, Any, Dict, Type +from typing import Any, Dict, List, Type -from pydantic import ValidationError, validator +from pydantic import ValidationError, ValidationInfo, field_validator from pydantic_settings import BaseSettings -from sqlalchemy import insert, update, select +from sqlalchemy import insert, select, update -from app.core.constants import API_VERSION, DEFAULT_CREATE_IMAGE_MODEL, DEFAULT_FILTER_MODELS, DEFAULT_MODEL, DEFAULT_SAFETY_SETTINGS, DEFAULT_STREAM_CHUNK_SIZE, DEFAULT_STREAM_LONG_TEXT_THRESHOLD, DEFAULT_STREAM_MAX_DELAY, DEFAULT_STREAM_MIN_DELAY, DEFAULT_STREAM_SHORT_TEXT_THRESHOLD, DEFAULT_TIMEOUT, MAX_RETRIES +from app.core.constants import ( + API_VERSION, + DEFAULT_CREATE_IMAGE_MODEL, + DEFAULT_FILTER_MODELS, + DEFAULT_MODEL, + DEFAULT_SAFETY_SETTINGS, + DEFAULT_STREAM_CHUNK_SIZE, + DEFAULT_STREAM_LONG_TEXT_THRESHOLD, + DEFAULT_STREAM_MAX_DELAY, + DEFAULT_STREAM_MIN_DELAY, + DEFAULT_STREAM_SHORT_TEXT_THRESHOLD, + DEFAULT_TIMEOUT, + MAX_RETRIES, +) from app.log.logger import Logger @@ -23,13 +37,17 @@ class Settings(BaseSettings): MYSQL_PASSWORD: str = "" MYSQL_DATABASE: str = "" MYSQL_SOCKET: str = "" - + # 验证 MySQL 配置 - @validator('MYSQL_HOST', 'MYSQL_PORT', 'MYSQL_USER', 'MYSQL_PASSWORD', 'MYSQL_DATABASE') - def validate_mysql_config(cls, v, values): - if values.get('DATABASE_TYPE') == 'mysql': + @field_validator( + "MYSQL_HOST", "MYSQL_PORT", "MYSQL_USER", "MYSQL_PASSWORD", "MYSQL_DATABASE" + ) + def validate_mysql_config(cls, v: Any, info: ValidationInfo) -> Any: + if info.data.get("DATABASE_TYPE") == "mysql": if v is None or v == "": - raise ValueError(f"MySQL configuration is required when DATABASE_TYPE is 'mysql'") + raise ValueError( + "MySQL configuration is required when DATABASE_TYPE is 'mysql'" + ) return v # API相关配置 @@ -41,7 +59,7 @@ class Settings(BaseSettings): TEST_MODEL: str = DEFAULT_MODEL TIME_OUT: int = DEFAULT_TIMEOUT MAX_RETRIES: int = MAX_RETRIES - PROXIES: List[str] = [] # 新增:代理服务器列表 + PROXIES: List[str] = [] # 新增:代理服务器列表 # 模型相关配置 SEARCH_MODELS: List[str] = ["gemini-2.0-flash-exp"] @@ -50,9 +68,9 @@ class Settings(BaseSettings): TOOLS_CODE_EXECUTION_ENABLED: bool = False SHOW_SEARCH_LINK: bool = True SHOW_THINKING_PROCESS: bool = True - THINKING_MODELS: List[str] = [] # 新增:用于思考过程的模型列表 - THINKING_BUDGET_MAP: Dict[str, float] = {} # 新增:模型对应的预算映射 - + THINKING_MODELS: List[str] = [] # 新增:用于思考过程的模型列表 + THINKING_BUDGET_MAP: Dict[str, float] = {} # 新增:模型对应的预算映射 + # 图像生成相关配置 PAID_KEY: str = "" CREATE_IMAGE_MODEL: str = DEFAULT_CREATE_IMAGE_MODEL @@ -61,7 +79,7 @@ class Settings(BaseSettings): PICGO_API_KEY: str = "" CLOUDFLARE_IMGBED_URL: str = "" CLOUDFLARE_IMGBED_AUTH_CODE: str = "" - + # 流式输出优化器配置 STREAM_OPTIMIZER_ENABLED: bool = False STREAM_MIN_DELAY: float = DEFAULT_STREAM_MIN_DELAY @@ -71,16 +89,20 @@ class Settings(BaseSettings): STREAM_CHUNK_SIZE: int = DEFAULT_STREAM_CHUNK_SIZE # 调度器配置 - CHECK_INTERVAL_HOURS: int = 1 # 默认检查间隔为1小时 - TIMEZONE: str = "Asia/Shanghai" # 默认时区 - - # github + CHECK_INTERVAL_HOURS: int = 1 # 默认检查间隔为1小时 + TIMEZONE: str = "Asia/Shanghai" # 默认时区 + + # github GITHUB_REPO_OWNER: str = "snailyp" GITHUB_REPO_NAME: str = "gemini-balance" # 日志配置 - LOG_LEVEL: str = "INFO" # 默认日志级别 - SAFETY_SETTINGS: List[Dict[str, str]] = DEFAULT_SAFETY_SETTINGS # 新增:安全设置 + LOG_LEVEL: str = "INFO" # 默认日志级别 + AUTO_DELETE_ERROR_LOGS_ENABLED: bool = True # 是否开启自动删除错误日志 + AUTO_DELETE_ERROR_LOGS_DAYS: int = 7 # 自动删除多少天前的错误日志 (1, 7, 30) + AUTO_DELETE_REQUEST_LOGS_ENABLED: bool = False # 是否开启自动删除请求日志 + AUTO_DELETE_REQUEST_LOGS_DAYS: int = 30 # 自动删除多少天前的请求日志 (1, 7, 30) + SAFETY_SETTINGS: List[Dict[str, str]] = DEFAULT_SAFETY_SETTINGS # 新增:安全设置 def __init__(self, **kwargs): super().__init__(**kwargs) @@ -88,13 +110,16 @@ class Settings(BaseSettings): if not self.AUTH_TOKEN and self.ALLOWED_TOKENS: self.AUTH_TOKEN = self.ALLOWED_TOKENS[0] + # 创建全局配置实例 settings = Settings() + def _parse_db_value(key: str, db_value: str, target_type: Type) -> Any: """尝试将数据库字符串值解析为目标 Python 类型""" - from app.log.logger import get_config_logger # 函数内导入 - logger = get_config_logger() # 函数内初始化 + from app.log.logger import get_config_logger # 函数内导入 + + logger = get_config_logger() # 函数内初始化 try: # 处理 List[str] if target_type == List[str]: @@ -103,9 +128,11 @@ def _parse_db_value(key: str, db_value: str, target_type: Type) -> Any: if isinstance(parsed, list): return [str(item) for item in parsed] except json.JSONDecodeError: - return [item.strip() for item in db_value.split(',') if item.strip()] - logger.warning(f"Could not parse '{db_value}' as List[str] for key '{key}', falling back to comma split or empty list.") - return [item.strip() for item in db_value.split(',') if item.strip()] + return [item.strip() for item in db_value.split(",") if item.strip()] + logger.warning( + f"Could not parse '{db_value}' as List[str] for key '{key}', falling back to comma split or empty list." + ) + return [item.strip() for item in db_value.split(",") if item.strip()] # 处理 Dict[str, float] elif target_type == Dict[str, float]: parsed_dict = {} @@ -115,24 +142,34 @@ def _parse_db_value(key: str, db_value: str, target_type: Type) -> Any: if isinstance(parsed, dict): parsed_dict = {str(k): float(v) for k, v in parsed.items()} else: - logger.warning(f"Parsed DB value for key '{key}' is not a dictionary type. Value: {db_value}") + logger.warning( + f"Parsed DB value for key '{key}' is not a dictionary type. Value: {db_value}" + ) except (json.JSONDecodeError, ValueError, TypeError) as e1: # Second attempt: try replacing single quotes if JSONDecodeError occurred if isinstance(e1, json.JSONDecodeError) and "'" in db_value: - logger.warning(f"Failed initial JSON parse for key '{key}'. Attempting to replace single quotes. Error: {e1}") + logger.warning( + f"Failed initial JSON parse for key '{key}'. Attempting to replace single quotes. Error: {e1}" + ) try: corrected_db_value = db_value.replace("'", '"') parsed = json.loads(corrected_db_value) if isinstance(parsed, dict): - parsed_dict = {str(k): float(v) for k, v in parsed.items()} + parsed_dict = {str(k): float(v) for k, v in parsed.items()} else: - logger.warning(f"Parsed DB value (after quote replacement) for key '{key}' is not a dictionary type. Value: {corrected_db_value}") + logger.warning( + f"Parsed DB value (after quote replacement) for key '{key}' is not a dictionary type. Value: {corrected_db_value}" + ) except (json.JSONDecodeError, ValueError, TypeError) as e2: - logger.error(f"Could not parse '{db_value}' as Dict[str, float] for key '{key}' even after replacing quotes: {e2}. Returning empty dict.") + logger.error( + f"Could not parse '{db_value}' as Dict[str, float] for key '{key}' even after replacing quotes: {e2}. Returning empty dict." + ) else: # Log other errors (ValueError, TypeError) or JSON errors without single quotes - logger.error(f"Could not parse '{db_value}' as Dict[str, float] for key '{key}': {e1}. Returning empty dict.") - return parsed_dict # Return the parsed dict or an empty one if all attempts fail + logger.error( + f"Could not parse '{db_value}' as Dict[str, float] for key '{key}': {e1}. Returning empty dict." + ) + return parsed_dict # Return the parsed dict or an empty one if all attempts fail # 处理 List[Dict[str, str]] elif target_type == List[Dict[str, str]]: try: @@ -140,28 +177,36 @@ def _parse_db_value(key: str, db_value: str, target_type: Type) -> Any: if isinstance(parsed, list): # 验证列表中的每个元素是否为字典,并且键和值都是字符串 valid = all( - isinstance(item, dict) and - all(isinstance(k, str) for k in item.keys()) and - all(isinstance(v, str) for v in item.values()) + isinstance(item, dict) + and all(isinstance(k, str) for k in item.keys()) + and all(isinstance(v, str) for v in item.values()) for item in parsed ) if valid: return parsed else: - logger.warning(f"Invalid structure in List[Dict[str, str]] for key '{key}'. Value: {db_value}") - return [] # 或者返回默认值?这里返回空列表 + logger.warning( + f"Invalid structure in List[Dict[str, str]] for key '{key}'. Value: {db_value}" + ) + return [] # 或者返回默认值?这里返回空列表 else: - logger.warning(f"Parsed DB value for key '{key}' is not a list type. Value: {db_value}") - return [] + logger.warning( + f"Parsed DB value for key '{key}' is not a list type. Value: {db_value}" + ) + return [] except json.JSONDecodeError: - logger.error(f"Could not parse '{db_value}' as JSON for List[Dict[str, str]] for key '{key}'. Returning empty list.") + logger.error( + f"Could not parse '{db_value}' as JSON for List[Dict[str, str]] for key '{key}'. Returning empty list." + ) return [] except Exception as e: - logger.error(f"Error parsing List[Dict[str, str]] for key '{key}': {e}. Value: {db_value}. Returning empty list.") + logger.error( + f"Error parsing List[Dict[str, str]] for key '{key}': {e}. Value: {db_value}. Returning empty list." + ) return [] # 处理 bool elif target_type == bool: - return db_value.lower() in ('true', '1', 'yes', 'on') + return db_value.lower() in ("true", "1", "yes", "on") # 处理 int elif target_type == int: return int(db_value) @@ -172,8 +217,11 @@ def _parse_db_value(key: str, db_value: str, target_type: Type) -> Any: else: return db_value except (ValueError, TypeError, json.JSONDecodeError) as e: - logger.warning(f"Failed to parse db_value '{db_value}' for key '{key}' as type {target_type}: {e}. Using original string value.") - return db_value # 解析失败则返回原始字符串 + logger.warning( + f"Failed to parse db_value '{db_value}' for key '{key}' as type {target_type}: {e}. Using original string value." + ) + return db_value # 解析失败则返回原始字符串 + async def sync_initial_settings(): """ @@ -182,8 +230,9 @@ async def sync_initial_settings(): 2. 将数据库设置合并到内存 settings (数据库优先)。 3. 将最终的内存 settings 同步回数据库。 """ - from app.log.logger import get_config_logger # 函数内导入 - logger = get_config_logger() # 函数内初始化 + from app.log.logger import get_config_logger # 函数内导入 + + logger = get_config_logger() # 函数内初始化 # 延迟导入以避免循环依赖和确保数据库连接已初始化 from app.database.connection import database from app.database.models import Settings as SettingsModel @@ -196,7 +245,9 @@ async def sync_initial_settings(): await database.connect() logger.info("Database connection established for initial sync.") except Exception as e: - logger.error(f"Failed to connect to database for initial settings sync: {e}. Skipping sync.") + logger.error( + f"Failed to connect to database for initial settings sync: {e}. Skipping sync." + ) return try: @@ -205,13 +256,19 @@ async def sync_initial_settings(): try: query = select(SettingsModel.key, SettingsModel.value) results = await database.fetch_all(query) - db_settings_raw = [{"key": row["key"], "value": row["value"]} for row in results] + db_settings_raw = [ + {"key": row["key"], "value": row["value"]} for row in results + ] logger.info(f"Fetched {len(db_settings_raw)} settings from database.") except Exception as e: - logger.error(f"Failed to fetch settings from database: {e}. Proceeding with environment/dotenv settings.") + logger.error( + f"Failed to fetch settings from database: {e}. Proceeding with environment/dotenv settings." + ) # 即使数据库读取失败,也要继续执行,确保基于 env/dotenv 的配置能同步到数据库 - db_settings_map: Dict[str, str] = {s['key']: s['value'] for s in db_settings_raw} + db_settings_map: Dict[str, str] = { + s["key"]: s["value"] for s in db_settings_raw + } # 2. 将数据库设置合并到内存 settings (数据库优先) updated_in_memory = False @@ -229,35 +286,52 @@ async def sync_initial_settings(): if parsed_db_value != memory_value: # 检查类型是否匹配,以防解析函数返回了不兼容的类型 type_match = False - if target_type == List[str] and isinstance(parsed_db_value, list): + if target_type == List[str] and isinstance( + parsed_db_value, list + ): type_match = True - elif target_type == Dict[str, float] and isinstance(parsed_db_value, dict): + elif target_type == Dict[str, float] and isinstance( + parsed_db_value, dict + ): type_match = True - elif target_type not in (List[str], Dict[str, float]) and isinstance(parsed_db_value, target_type): + elif target_type not in ( + List[str], + Dict[str, float], + ) and isinstance(parsed_db_value, target_type): type_match = True if type_match: setattr(settings, key, parsed_db_value) - logger.debug(f"Updated setting '{key}' in memory from database value ({target_type}).") + logger.debug( + f"Updated setting '{key}' in memory from database value ({target_type})." + ) updated_in_memory = True else: - logger.warning(f"Parsed DB value type mismatch for key '{key}'. Expected {target_type}, got {type(parsed_db_value)}. Skipping update.") + logger.warning( + f"Parsed DB value type mismatch for key '{key}'. Expected {target_type}, got {type(parsed_db_value)}. Skipping update." + ) except Exception as e: - logger.error(f"Error processing database setting for key '{key}': {e}") + logger.error( + f"Error processing database setting for key '{key}': {e}" + ) else: - logger.warning(f"Database setting '{key}' not found in Settings model definition. Ignoring.") - + logger.warning( + f"Database setting '{key}' not found in Settings model definition. Ignoring." + ) # 如果内存中有更新,重新验证 Pydantic 模型(可选但推荐) if updated_in_memory: try: # 重新加载以确保类型转换和验证 settings = Settings(**settings.model_dump()) - logger.info("Settings object re-validated after merging database values.") + logger.info( + "Settings object re-validated after merging database values." + ) except ValidationError as e: - logger.error(f"Validation error after merging database settings: {e}. Settings might be inconsistent.") - + logger.error( + f"Validation error after merging database settings: {e}. Settings might be inconsistent." + ) # 3. 将最终的内存 settings 同步回数据库 final_memory_settings = settings.model_dump() @@ -269,20 +343,22 @@ async def sync_initial_settings(): for key, value in final_memory_settings.items(): # 序列化值为字符串或 JSON 字符串 - if isinstance(value, (list, dict)): # 处理列表和字典 - db_value = json.dumps(value, ensure_ascii=False) # 使用 ensure_ascii=False 以支持非 ASCII 字符 + if isinstance(value, (list, dict)): # 处理列表和字典 + db_value = json.dumps( + value, ensure_ascii=False + ) # 使用 ensure_ascii=False 以支持非 ASCII 字符 elif isinstance(value, bool): db_value = str(value).lower() - elif value is None: # 处理 None 值 - db_value = "" # 或者根据需要设为 NULL 或其他标记 + elif value is None: # 处理 None 值 + db_value = "" # 或者根据需要设为 NULL 或其他标记 else: db_value = str(value) data = { - 'key': key, - 'value': db_value, - 'description': f"{key} configuration setting", # 默认描述 - 'updated_at': now + "key": key, + "value": db_value, + "description": f"{key} configuration setting", # 默认描述 + "updated_at": now, } if key in existing_db_keys: @@ -291,7 +367,7 @@ async def sync_initial_settings(): settings_to_update.append(data) else: # 如果键不在数据库中,则插入 - data['created_at'] = now + data["created_at"] = now settings_to_insert.append(data) # 在事务中执行批量插入和更新 @@ -300,48 +376,78 @@ async def sync_initial_settings(): async with database.transaction(): if settings_to_insert: # 获取现有描述以避免覆盖 - query_existing = select(SettingsModel.key, SettingsModel.description).where(SettingsModel.key.in_([s['key'] for s in settings_to_insert])) - existing_desc = {row['key']: row['description'] for row in await database.fetch_all(query_existing)} + query_existing = select( + SettingsModel.key, SettingsModel.description + ).where( + SettingsModel.key.in_( + [s["key"] for s in settings_to_insert] + ) + ) + existing_desc = { + row["key"]: row["description"] + for row in await database.fetch_all(query_existing) + } for item in settings_to_insert: - item['description'] = existing_desc.get(item['key'], item['description']) + item["description"] = existing_desc.get( + item["key"], item["description"] + ) query_insert = insert(SettingsModel).values(settings_to_insert) await database.execute(query=query_insert) - logger.info(f"Synced (inserted) {len(settings_to_insert)} settings to database.") + logger.info( + f"Synced (inserted) {len(settings_to_insert)} settings to database." + ) if settings_to_update: # 获取现有描述以避免覆盖 - query_existing = select(SettingsModel.key, SettingsModel.description).where(SettingsModel.key.in_([s['key'] for s in settings_to_update])) - existing_desc = {row['key']: row['description'] for row in await database.fetch_all(query_existing)} + query_existing = select( + SettingsModel.key, SettingsModel.description + ).where( + SettingsModel.key.in_( + [s["key"] for s in settings_to_update] + ) + ) + existing_desc = { + row["key"]: row["description"] + for row in await database.fetch_all(query_existing) + } for setting_data in settings_to_update: - setting_data['description'] = existing_desc.get(setting_data['key'], setting_data['description']) + setting_data["description"] = existing_desc.get( + setting_data["key"], setting_data["description"] + ) query_update = ( update(SettingsModel) - .where(SettingsModel.key == setting_data['key']) + .where(SettingsModel.key == setting_data["key"]) .values( - value=setting_data['value'], - description=setting_data['description'], - updated_at=setting_data['updated_at'] + value=setting_data["value"], + description=setting_data["description"], + updated_at=setting_data["updated_at"], ) ) await database.execute(query=query_update) - logger.info(f"Synced (updated) {len(settings_to_update)} settings to database.") + logger.info( + f"Synced (updated) {len(settings_to_update)} settings to database." + ) except Exception as e: - logger.error(f"Failed to sync settings to database during startup: {str(e)}") + logger.error( + f"Failed to sync settings to database during startup: {str(e)}" + ) else: - logger.info("No setting changes detected between memory and database during initial sync.") + logger.info( + "No setting changes detected between memory and database during initial sync." + ) # 刷新日志等级 Logger.update_log_levels(final_memory_settings.get("LOG_LEVEL")) - + except Exception as e: logger.error(f"An unexpected error occurred during initial settings sync: {e}") finally: if database.is_connected: - try: - pass - except Exception as e: - logger.error(f"Error disconnecting database after initial sync: {e}") + try: + pass + except Exception as e: + logger.error(f"Error disconnecting database after initial sync: {e}") logger.info("Initial settings synchronization finished.") diff --git a/app/core/application.py b/app/core/application.py index 8057508..ec14eca 100644 --- a/app/core/application.py +++ b/app/core/application.py @@ -13,7 +13,7 @@ from app.service.key.key_manager import get_key_manager_instance from app.database.connection import connect_to_db, disconnect_from_db from app.utils.helpers import get_current_version # Import from helpers from app.database.initialization import initialize_database -from app.scheduler.key_checker import start_scheduler, stop_scheduler +from app.scheduler.scheduled_tasks import start_scheduler, stop_scheduler from app.service.update.update_service import check_for_updates logger = get_application_logger() diff --git a/app/log/logger.py b/app/log/logger.py index 896fe27..4ec4746 100644 --- a/app/log/logger.py +++ b/app/log/logger.py @@ -217,4 +217,9 @@ def get_api_client_logger(): def get_openai_compatible_logger(): - return Logger.setup_logger("openai_compatible") \ No newline at end of file + return Logger.setup_logger("openai_compatible") + + +def get_error_log_logger(): + return Logger.setup_logger("error_log") + diff --git a/app/router/error_log_routes.py b/app/router/error_log_routes.py index 8bed1e9..df89260 100644 --- a/app/router/error_log_routes.py +++ b/app/router/error_log_routes.py @@ -1,20 +1,25 @@ """ 日志路由模块 """ -from typing import List, Optional, Dict + from datetime import datetime +from typing import Dict, List, Optional + +from fastapi import ( + APIRouter, + Body, + HTTPException, + Path, + Query, + Request, + Response, + status, +) from pydantic import BaseModel -from fastapi import APIRouter, HTTPException, Request, Query, Path, Body, Response, status from app.core.security import verify_auth_token from app.log.logger import get_log_routes_logger -from app.database.services import ( - get_error_logs, - get_error_logs_count, - get_error_log_details, - delete_error_logs_by_ids, - delete_error_log_by_id -) +from app.service.error_log import error_log_service router = APIRouter(prefix="/api/logs", tags=["logs"]) @@ -29,22 +34,36 @@ class ErrorLogListItem(BaseModel): model_name: Optional[str] = None request_time: Optional[datetime] = None + class ErrorLogListResponse(BaseModel): logs: List[ErrorLogListItem] total: int + @router.get("/errors", response_model=ErrorLogListResponse) async def get_error_logs_api( request: Request, limit: int = Query(10, ge=1, le=1000), offset: int = Query(0, ge=0), - key_search: Optional[str] = Query(None, description="Search term for Gemini key (partial match)"), - error_search: Optional[str] = Query(None, description="Search term for error type or log message"), - error_code_search: Optional[str] = Query(None, description="Search term for error code"), - start_date: Optional[datetime] = Query(None, description="Start datetime for filtering"), - end_date: Optional[datetime] = Query(None, description="End datetime for filtering"), - sort_by: str = Query('id', description="Field to sort by (e.g., 'id', 'request_time')"), - sort_order: str = Query('desc', description="Sort order ('asc' or 'desc')") + key_search: Optional[str] = Query( + None, description="Search term for Gemini key (partial match)" + ), + error_search: Optional[str] = Query( + None, description="Search term for error type or log message" + ), + error_code_search: Optional[str] = Query( + None, description="Search term for error code" + ), + start_date: Optional[datetime] = Query( + None, description="Start datetime for filtering" + ), + end_date: Optional[datetime] = Query( + None, description="End datetime for filtering" + ), + sort_by: str = Query( + "id", description="Field to sort by (e.g., 'id', 'request_time')" + ), + sort_order: str = Query("desc", description="Sort order ('asc' or 'desc')"), ): """ 获取错误日志列表 (返回错误码),支持过滤和排序 @@ -68,9 +87,9 @@ async def get_error_logs_api( if not auth_token or not verify_auth_token(auth_token): logger.warning("Unauthorized access attempt to error logs list") raise HTTPException(status_code=401, detail="Not authenticated") - + try: - logs_data = await get_error_logs( + result = await error_log_service.process_get_error_logs( limit=limit, offset=offset, key_search=key_search, @@ -79,20 +98,18 @@ async def get_error_logs_api( start_date=start_date, end_date=end_date, sort_by=sort_by, - sort_order=sort_order - ) - total_count = await get_error_logs_count( - key_search=key_search, - error_search=error_search, - error_code_search=error_code_search, - start_date=start_date, - end_date=end_date + sort_order=sort_order, ) + logs_data = result["logs"] + total_count = result["total"] + validated_logs = [ErrorLogListItem(**log) for log in logs_data] return ErrorLogListResponse(logs=validated_logs, total=total_count) except Exception as e: logger.exception(f"Failed to get error logs list: {str(e)}") - raise HTTPException(status_code=500, detail=f"Failed to get error logs list: {str(e)}") + raise HTTPException( + status_code=500, detail=f"Failed to get error logs list: {str(e)}" + ) class ErrorLogDetailResponse(BaseModel): @@ -104,6 +121,7 @@ class ErrorLogDetailResponse(BaseModel): model_name: Optional[str] = None request_time: Optional[datetime] = None + @router.get("/errors/{log_id}/details", response_model=ErrorLogDetailResponse) async def get_error_log_detail_api(request: Request, log_id: int = Path(..., ge=1)): """ @@ -111,11 +129,15 @@ async def get_error_log_detail_api(request: Request, log_id: int = Path(..., ge= """ auth_token = request.cookies.get("auth_token") if not auth_token or not verify_auth_token(auth_token): - logger.warning(f"Unauthorized access attempt to error log details for ID: {log_id}") + logger.warning( + f"Unauthorized access attempt to error log details for ID: {log_id}" + ) raise HTTPException(status_code=401, detail="Not authenticated") try: - log_details = await get_error_log_details(log_id=log_id) + log_details = await error_log_service.process_get_error_log_details( + log_id=log_id + ) if not log_details: raise HTTPException(status_code=404, detail="Error log not found") @@ -124,13 +146,14 @@ async def get_error_log_detail_api(request: Request, log_id: int = Path(..., ge= raise http_exc except Exception as e: logger.exception(f"Failed to get error log details for ID {log_id}: {str(e)}") - raise HTTPException(status_code=500, detail=f"Failed to get error log details: {str(e)}") + raise HTTPException( + status_code=500, detail=f"Failed to get error log details: {str(e)}" + ) @router.delete("/errors", status_code=status.HTTP_204_NO_CONTENT) async def delete_error_logs_bulk_api( - request: Request, - payload: Dict[str, List[int]] = Body(...) + request: Request, payload: Dict[str, List[int]] = Body(...) ): """ 批量删除错误日志 (异步) @@ -145,20 +168,23 @@ async def delete_error_logs_bulk_api( raise HTTPException(status_code=400, detail="No log IDs provided for deletion.") try: - deleted_count = await delete_error_logs_by_ids(log_ids) + deleted_count = await error_log_service.process_delete_error_logs_by_ids( + log_ids + ) # 注意:异步函数返回的是尝试删除的数量,可能不是精确值 - logger.info(f"Attempted bulk deletion for {deleted_count} error logs with IDs: {log_ids}") + logger.info( + f"Attempted bulk deletion for {deleted_count} error logs with IDs: {log_ids}" + ) return Response(status_code=status.HTTP_204_NO_CONTENT) except Exception as e: logger.exception(f"Error bulk deleting error logs with IDs {log_ids}: {str(e)}") - raise HTTPException(status_code=500, detail="Internal server error during bulk deletion") + raise HTTPException( + status_code=500, detail="Internal server error during bulk deletion" + ) @router.delete("/errors/{log_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_error_log_api( - request: Request, - log_id: int = Path(..., ge=1) -): +async def delete_error_log_api(request: Request, log_id: int = Path(..., ge=1)): """ 删除单个错误日志 (异步) """ @@ -168,14 +194,18 @@ async def delete_error_log_api( raise HTTPException(status_code=401, detail="Not authenticated") try: - success = await delete_error_log_by_id(log_id) + success = await error_log_service.process_delete_error_log_by_id(log_id) if not success: # 服务层现在在未找到时返回 False,我们在这里转换为 404 - raise HTTPException(status_code=404, detail=f"Error log with ID {log_id} not found") + raise HTTPException( + status_code=404, detail=f"Error log with ID {log_id} not found" + ) logger.info(f"Successfully deleted error log with ID: {log_id}") return Response(status_code=status.HTTP_204_NO_CONTENT) except HTTPException as http_exc: raise http_exc except Exception as e: logger.exception(f"Error deleting error log with ID {log_id}: {str(e)}") - raise HTTPException(status_code=500, detail="Internal server error during deletion") + raise HTTPException( + status_code=500, detail="Internal server error during deletion" + ) diff --git a/app/router/scheduler_routes.py b/app/router/scheduler_routes.py index e5dc3cc..b6618f3 100644 --- a/app/router/scheduler_routes.py +++ b/app/router/scheduler_routes.py @@ -6,7 +6,7 @@ from fastapi import APIRouter, Request, HTTPException, status from fastapi.responses import JSONResponse from app.core.security import verify_auth_token -from app.scheduler.key_checker import start_scheduler, stop_scheduler +from app.scheduler.scheduled_tasks import start_scheduler, stop_scheduler from app.log.logger import get_scheduler_routes logger = get_scheduler_routes() diff --git a/app/scheduler/key_checker.py b/app/scheduler/key_checker.py deleted file mode 100644 index ed58e13..0000000 --- a/app/scheduler/key_checker.py +++ /dev/null @@ -1,102 +0,0 @@ -from apscheduler.schedulers.asyncio import AsyncIOScheduler -from app.service.key.key_manager import get_key_manager_instance -from app.service.chat.gemini_chat_service import GeminiChatService -from app.domain.gemini_models import GeminiRequest, GeminiContent -from app.config.config import settings -from app.log.logger import Logger # 导入 Logger 类 - -logger = Logger.setup_logger("scheduler") # 使用 Logger.setup_logger - -async def check_failed_keys(): - """ - 定时检查失败次数大于0的API密钥,并尝试验证它们。 - 如果验证成功,重置失败计数;如果失败,增加失败计数。 - """ - logger.info("Starting scheduled check for failed API keys...") - try: - key_manager = await get_key_manager_instance() - # 确保 KeyManager 已经初始化 - if not key_manager or not hasattr(key_manager, 'key_failure_counts'): - logger.warning("KeyManager instance not available or not initialized. Skipping check.") - return - - # 创建 GeminiChatService 实例用于验证 - # 注意:这里直接创建实例,而不是通过依赖注入,因为这是后台任务 - chat_service = GeminiChatService(settings.BASE_URL, key_manager) - - # 获取需要检查的 key 列表 (失败次数 > 0) - keys_to_check = [] - async with key_manager.failure_count_lock: # 访问共享数据需要加锁 - # 复制一份以避免在迭代时修改字典 - failure_counts_copy = key_manager.key_failure_counts.copy() - keys_to_check = [key for key, count in failure_counts_copy.items() if count > 0] # 检查所有失败次数大于0的key - - if not keys_to_check: - logger.info("No keys with failure count > 0 found. Skipping verification.") - return - - logger.info(f"Found {len(keys_to_check)} keys with failure count > 0 to verify.") - - for key in keys_to_check: - # 隐藏部分 key 用于日志记录 - log_key = f"{key[:4]}...{key[-4:]}" if len(key) > 8 else key - logger.info(f"Verifying key: {log_key}...") - try: - # 构造测试请求 - gemini_request = GeminiRequest( - contents=[ - GeminiContent( - role="user", - parts=[{"text": "hi"}] # 使用简单的文本进行验证 - ) - ] - ) - # 调用 generate_content 进行验证 - await chat_service.generate_content( - settings.TEST_MODEL, # 使用配置中定义的测试模型 - gemini_request, - key - ) - # 如果没有抛出异常,说明 key 有效 - logger.info(f"Key {log_key} verification successful. Resetting failure count.") - await key_manager.reset_key_failure_count(key) - except Exception as e: - # 验证失败,增加失败计数 - logger.warning(f"Key {log_key} verification failed: {str(e)}. Incrementing failure count.") - # 直接操作计数器,需要加锁 - async with key_manager.failure_count_lock: - # 再次检查 key 是否存在且失败次数未达上限 - if key in key_manager.key_failure_counts and key_manager.key_failure_counts[key] < key_manager.MAX_FAILURES: - key_manager.key_failure_counts[key] += 1 - logger.info(f"Failure count for key {log_key} incremented to {key_manager.key_failure_counts[key]}.") - elif key in key_manager.key_failure_counts: - logger.warning(f"Key {log_key} reached MAX_FAILURES ({key_manager.MAX_FAILURES}). Not incrementing further.") - - - except Exception as e: - logger.error(f"An error occurred during the scheduled key check: {str(e)}", exc_info=True) - -def setup_scheduler(): - """设置并启动 APScheduler""" - scheduler = AsyncIOScheduler(timezone=str(settings.TIMEZONE)) # 从配置读取时区 - # 添加定时任务,例如每小时执行一次 (可以调整) - scheduler.add_job(check_failed_keys, 'interval', hours=settings.CHECK_INTERVAL_HOURS) - scheduler.start() - logger.info(f"Scheduler started. Key check job scheduled to run every {settings.CHECK_INTERVAL_HOURS} hour(s).") - return scheduler - -# 可以在这里添加一个全局的 scheduler 实例,以便在应用关闭时优雅地停止 -scheduler_instance = None - -def start_scheduler(): - global scheduler_instance - if scheduler_instance is None or not scheduler_instance.running: - logger.info("Starting scheduler...") - scheduler_instance = setup_scheduler() - logger.info("Scheduler is already running.") - -def stop_scheduler(): - global scheduler_instance - if scheduler_instance and scheduler_instance.running: - scheduler_instance.shutdown() - logger.info("Scheduler stopped.") \ No newline at end of file diff --git a/app/scheduler/scheduled_tasks.py b/app/scheduler/scheduled_tasks.py new file mode 100644 index 0000000..b502812 --- /dev/null +++ b/app/scheduler/scheduled_tasks.py @@ -0,0 +1,162 @@ + +from apscheduler.schedulers.asyncio import AsyncIOScheduler + +from app.config.config import settings +from app.domain.gemini_models import GeminiContent, GeminiRequest +from app.log.logger import Logger +from app.service.chat.gemini_chat_service import GeminiChatService +from app.service.error_log.error_log_service import delete_old_error_logs +from app.service.key.key_manager import get_key_manager_instance +from app.service.request_log.request_log_service import delete_old_request_logs_task + +logger = Logger.setup_logger("scheduler") + + +async def check_failed_keys(): + """ + 定时检查失败次数大于0的API密钥,并尝试验证它们。 + 如果验证成功,重置失败计数;如果失败,增加失败计数。 + """ + logger.info("Starting scheduled check for failed API keys...") + try: + key_manager = await get_key_manager_instance() + # 确保 KeyManager 已经初始化 + if not key_manager or not hasattr(key_manager, "key_failure_counts"): + logger.warning( + "KeyManager instance not available or not initialized. Skipping check." + ) + return + + # 创建 GeminiChatService 实例用于验证 + # 注意:这里直接创建实例,而不是通过依赖注入,因为这是后台任务 + chat_service = GeminiChatService(settings.BASE_URL, key_manager) + + # 获取需要检查的 key 列表 (失败次数 > 0) + keys_to_check = [] + async with key_manager.failure_count_lock: # 访问共享数据需要加锁 + # 复制一份以避免在迭代时修改字典 + failure_counts_copy = key_manager.key_failure_counts.copy() + keys_to_check = [ + key for key, count in failure_counts_copy.items() if count > 0 + ] # 检查所有失败次数大于0的key + + if not keys_to_check: + logger.info("No keys with failure count > 0 found. Skipping verification.") + return + + logger.info( + f"Found {len(keys_to_check)} keys with failure count > 0 to verify." + ) + + for key in keys_to_check: + # 隐藏部分 key 用于日志记录 + log_key = f"{key[:4]}...{key[-4:]}" if len(key) > 8 else key + logger.info(f"Verifying key: {log_key}...") + try: + # 构造测试请求 + gemini_request = GeminiRequest( + contents=[ + GeminiContent( + role="user", + parts=[{"text": "hi"}], # 使用简单的文本进行验证 + ) + ] + ) + # 调用 generate_content 进行验证 + await chat_service.generate_content( + settings.TEST_MODEL, gemini_request, key # 使用配置中定义的测试模型 + ) + # 如果没有抛出异常,说明 key 有效 + logger.info( + f"Key {log_key} verification successful. Resetting failure count." + ) + await key_manager.reset_key_failure_count(key) + except Exception as e: + # 验证失败,增加失败计数 + logger.warning( + f"Key {log_key} verification failed: {str(e)}. Incrementing failure count." + ) + # 直接操作计数器,需要加锁 + async with key_manager.failure_count_lock: + # 再次检查 key 是否存在且失败次数未达上限 + if ( + key in key_manager.key_failure_counts + and key_manager.key_failure_counts[key] + < key_manager.MAX_FAILURES + ): + key_manager.key_failure_counts[key] += 1 + logger.info( + f"Failure count for key {log_key} incremented to {key_manager.key_failure_counts[key]}." + ) + elif key in key_manager.key_failure_counts: + logger.warning( + f"Key {log_key} reached MAX_FAILURES ({key_manager.MAX_FAILURES}). Not incrementing further." + ) + + except Exception as e: + logger.error( + f"An error occurred during the scheduled key check: {str(e)}", exc_info=True + ) + + +def setup_scheduler(): + """设置并启动 APScheduler""" + scheduler = AsyncIOScheduler(timezone=str(settings.TIMEZONE)) # 从配置读取时区 + # 添加检查失败密钥的定时任务 + scheduler.add_job( + check_failed_keys, + "interval", + hours=settings.CHECK_INTERVAL_HOURS, + id="check_failed_keys_job", + name="Check Failed API Keys", + ) + logger.info( + f"Key check job scheduled to run every {settings.CHECK_INTERVAL_HOURS} hour(s)." + ) + + # 新增:添加自动删除错误日志的定时任务,每天凌晨3点执行 + scheduler.add_job( + delete_old_error_logs, + "cron", + hour=3, + minute=0, + id="delete_old_error_logs_job", + name="Delete Old Error Logs", + ) + logger.info("Auto-delete error logs job scheduled to run daily at 3:00 AM.") + + # 新增:添加自动删除请求日志的定时任务,每天凌晨3点05分执行 + scheduler.add_job( + delete_old_request_logs_task, + "cron", + hour=3, + minute=5, + id="delete_old_request_logs_job", + name="Delete Old Request Logs", + ) + logger.info( + f"Auto-delete request logs job scheduled to run daily at 3:05 AM, if enabled and AUTO_DELETE_REQUEST_LOGS_DAYS is set to {settings.AUTO_DELETE_REQUEST_LOGS_DAYS} days." + ) + + scheduler.start() + logger.info("Scheduler started with all jobs.") + return scheduler + + +# 可以在这里添加一个全局的 scheduler 实例,以便在应用关闭时优雅地停止 +scheduler_instance = None + + +def start_scheduler(): + global scheduler_instance + if scheduler_instance is None or not scheduler_instance.running: + logger.info("Starting scheduler...") + scheduler_instance = setup_scheduler() + logger.info("Scheduler is already running.") + + +def stop_scheduler(): + global scheduler_instance + if scheduler_instance and scheduler_instance.running: + scheduler_instance.shutdown() + logger.info("Scheduler stopped.") diff --git a/app/service/error_log/error_log_service.py b/app/service/error_log/error_log_service.py new file mode 100644 index 0000000..2bd5ae2 --- /dev/null +++ b/app/service/error_log/error_log_service.py @@ -0,0 +1,155 @@ +from datetime import datetime, timedelta, timezone +from typing import Any, Dict, List, Optional + +from sqlalchemy import delete, func, select + +from app.config.config import settings +from app.database import services as db_services +from app.database.connection import database +from app.database.models import ErrorLog +from app.log.logger import get_error_log_logger + +logger = get_error_log_logger() + + +async def delete_old_error_logs(): + """ + Deletes error logs older than a specified number of days, + based on the AUTO_DELETE_ERROR_LOGS_ENABLED and AUTO_DELETE_ERROR_LOGS_DAYS settings. + """ + if not settings.AUTO_DELETE_ERROR_LOGS_ENABLED: + logger.info("Auto-deletion of error logs is disabled. Skipping.") + return + + days_to_keep = settings.AUTO_DELETE_ERROR_LOGS_DAYS + if not isinstance(days_to_keep, int) or days_to_keep <= 0: + logger.error( + f"Invalid AUTO_DELETE_ERROR_LOGS_DAYS value: {days_to_keep}. Must be a positive integer. Skipping deletion." + ) + return + + cutoff_date = datetime.now(timezone.utc) - timedelta(days=days_to_keep) + + logger.info( + f"Attempting to delete error logs older than {days_to_keep} days (before {cutoff_date.strftime('%Y-%m-%d %H:%M:%S %Z')})." + ) + + try: + if not database.is_connected: + await database.connect() + logger.info("Database connection established for deleting error logs.") + + # First, count how many logs will be deleted (optional, for logging) + count_query = select(func.count(ErrorLog.id)).where( + ErrorLog.request_time < cutoff_date + ) + num_logs_to_delete = await database.fetch_val(count_query) + + if num_logs_to_delete == 0: + logger.info( + "No error logs found older than the specified period. No deletion needed." + ) + return + + logger.info(f"Found {num_logs_to_delete} error logs to delete.") + + # Perform the deletion + query = delete(ErrorLog).where(ErrorLog.request_time < cutoff_date) + await database.execute(query) + logger.info( + f"Successfully deleted {num_logs_to_delete} error logs older than {days_to_keep} days." + ) + + except Exception as e: + logger.error( + f"Error during automatic deletion of error logs: {e}", exc_info=True + ) + + +async def process_get_error_logs( + limit: int, + offset: int, + key_search: Optional[str], + error_search: Optional[str], + error_code_search: Optional[str], + start_date: Optional[datetime], + end_date: Optional[datetime], + sort_by: str, + sort_order: str, +) -> Dict[str, Any]: + """ + 处理错误日志的检索,支持分页和过滤。 + """ + try: + logs_data = await db_services.get_error_logs( + limit=limit, + offset=offset, + key_search=key_search, + error_search=error_search, + error_code_search=error_code_search, + start_date=start_date, + end_date=end_date, + sort_by=sort_by, + sort_order=sort_order, + ) + total_count = await db_services.get_error_logs_count( + key_search=key_search, + error_search=error_search, + error_code_search=error_code_search, + start_date=start_date, + end_date=end_date, + ) + return {"logs": logs_data, "total": total_count} + except Exception as e: + logger.error(f"Service error in process_get_error_logs: {e}", exc_info=True) + raise + + +async def process_get_error_log_details(log_id: int) -> Optional[Dict[str, Any]]: + """ + 处理特定错误日志详细信息的检索。 + 如果未找到,则返回 None。 + """ + try: + log_details = await db_services.get_error_log_details(log_id=log_id) + return log_details + except Exception as e: + logger.error( + f"Service error in process_get_error_log_details for ID {log_id}: {e}", + exc_info=True, + ) + raise + + +async def process_delete_error_logs_by_ids(log_ids: List[int]) -> int: + """ + 按 ID 批量删除错误日志。 + 返回尝试删除的日志数量。 + """ + if not log_ids: + return 0 + try: + deleted_count = await db_services.delete_error_logs_by_ids(log_ids) + return deleted_count + except Exception as e: + logger.error( + f"Service error in process_delete_error_logs_by_ids for IDs {log_ids}: {e}", + exc_info=True, + ) + raise + + +async def process_delete_error_log_by_id(log_id: int) -> bool: + """ + 按 ID 删除单个错误日志。 + 如果删除成功(或找到日志并尝试删除),则返回 True,否则返回 False。 + """ + try: + success = await db_services.delete_error_log_by_id(log_id) + return success + except Exception as e: + logger.error( + f"Service error in process_delete_error_log_by_id for ID {log_id}: {e}", + exc_info=True, + ) + raise diff --git a/app/service/request_log/request_log_service.py b/app/service/request_log/request_log_service.py new file mode 100644 index 0000000..5681493 --- /dev/null +++ b/app/service/request_log/request_log_service.py @@ -0,0 +1,50 @@ +""" +Service for request log operations. +""" + +from datetime import datetime, timedelta + +from sqlalchemy import delete + +from app import database +from app.config.config import settings +from app.database.models import RequestLog +from app.log.logger import Logger + +logger = Logger.setup_logger("request_log_service") + + +async def delete_old_request_logs_task(): + """ + 定时删除旧的请求日志。 + """ + if not settings.AUTO_DELETE_REQUEST_LOGS_ENABLED: + logger.info( + "Auto-delete for request logs is disabled by settings. Skipping task." + ) + return + + days_to_keep = settings.AUTO_DELETE_REQUEST_LOGS_DAYS + logger.info( + f"Starting scheduled task to delete old request logs older than {days_to_keep} days." + ) + + try: + cutoff_date = datetime.now(datetime.timezone.utc) - timedelta(days=days_to_keep) + + query = delete(RequestLog).where(RequestLog.request_time < cutoff_date) + + if not database.is_connected: + logger.info("Connecting to database for request log deletion.") + await database.connect() + + result = await database.execute(query) + logger.info( + f"Request logs older than {cutoff_date} potentially deleted. Rows affected: {result.rowcount if result else 'N/A'}" + ) + + except Exception as e: + logger.error( + f"An error occurred during the scheduled request log deletion: {str(e)}", + exc_info=True, + ) diff --git a/app/static/js/config_editor.js b/app/static/js/config_editor.js index 3915d90..9d56319 100644 --- a/app/static/js/config_editor.js +++ b/app/static/js/config_editor.js @@ -1,367 +1,472 @@ // Constants -const SENSITIVE_INPUT_CLASS = 'sensitive-input'; -const ARRAY_ITEM_CLASS = 'array-item'; -const ARRAY_INPUT_CLASS = 'array-input'; -const MAP_ITEM_CLASS = 'map-item'; -const MAP_KEY_INPUT_CLASS = 'map-key-input'; -const MAP_VALUE_INPUT_CLASS = 'map-value-input'; -const SAFETY_SETTING_ITEM_CLASS = 'safety-setting-item'; -const SHOW_CLASS = 'show'; // For modals +const SENSITIVE_INPUT_CLASS = "sensitive-input"; +const ARRAY_ITEM_CLASS = "array-item"; +const ARRAY_INPUT_CLASS = "array-input"; +const MAP_ITEM_CLASS = "map-item"; +const MAP_KEY_INPUT_CLASS = "map-key-input"; +const MAP_VALUE_INPUT_CLASS = "map-value-input"; +const SAFETY_SETTING_ITEM_CLASS = "safety-setting-item"; +const SHOW_CLASS = "show"; // For modals const API_KEY_REGEX = /AIzaSy\S{33}/g; -const PROXY_REGEX = /(?:https?|socks5):\/\/(?:[^:@\/]+(?::[^@\/]+)?@)?(?:[^:\/\s]+)(?::\d+)?/g; -const MASKED_VALUE = '••••••••'; +const PROXY_REGEX = + /(?:https?|socks5):\/\/(?:[^:@\/]+(?::[^@\/]+)?@)?(?:[^:\/\s]+)(?::\d+)?/g; +const MASKED_VALUE = "••••••••"; // DOM Elements - Global Scope for frequently accessed elements -const safetySettingsContainer = document.getElementById('SAFETY_SETTINGS_container'); -const thinkingModelsContainer = document.getElementById('THINKING_MODELS_container'); -const apiKeyModal = document.getElementById('apiKeyModal'); -const apiKeyBulkInput = document.getElementById('apiKeyBulkInput'); -const apiKeySearchInput = document.getElementById('apiKeySearchInput'); -const bulkDeleteApiKeyModal = document.getElementById('bulkDeleteApiKeyModal'); -const bulkDeleteApiKeyInput = document.getElementById('bulkDeleteApiKeyInput'); -const proxyModal = document.getElementById('proxyModal'); -const proxyBulkInput = document.getElementById('proxyBulkInput'); -const bulkDeleteProxyModal = document.getElementById('bulkDeleteProxyModal'); -const bulkDeleteProxyInput = document.getElementById('bulkDeleteProxyInput'); -const resetConfirmModal = document.getElementById('resetConfirmModal'); -const configForm = document.getElementById('configForm'); // Added for frequent use +const safetySettingsContainer = document.getElementById( + "SAFETY_SETTINGS_container" +); +const thinkingModelsContainer = document.getElementById( + "THINKING_MODELS_container" +); +const apiKeyModal = document.getElementById("apiKeyModal"); +const apiKeyBulkInput = document.getElementById("apiKeyBulkInput"); +const apiKeySearchInput = document.getElementById("apiKeySearchInput"); +const bulkDeleteApiKeyModal = document.getElementById("bulkDeleteApiKeyModal"); +const bulkDeleteApiKeyInput = document.getElementById("bulkDeleteApiKeyInput"); +const proxyModal = document.getElementById("proxyModal"); +const proxyBulkInput = document.getElementById("proxyBulkInput"); +const bulkDeleteProxyModal = document.getElementById("bulkDeleteProxyModal"); +const bulkDeleteProxyInput = document.getElementById("bulkDeleteProxyInput"); +const resetConfirmModal = document.getElementById("resetConfirmModal"); +const configForm = document.getElementById("configForm"); // Added for frequent use // Modal Control Functions function openModal(modalElement) { - if (modalElement) { - modalElement.classList.add(SHOW_CLASS); - } + if (modalElement) { + modalElement.classList.add(SHOW_CLASS); + } } function closeModal(modalElement) { - if (modalElement) { - modalElement.classList.remove(SHOW_CLASS); - } + if (modalElement) { + modalElement.classList.remove(SHOW_CLASS); + } } -document.addEventListener('DOMContentLoaded', function() { - // Initialize configuration - initConfig(); +document.addEventListener("DOMContentLoaded", function () { + // Initialize configuration + initConfig(); - // Tab switching - const tabButtons = document.querySelectorAll('.tab-btn'); - tabButtons.forEach(button => { - button.addEventListener('click', function(e) { - e.stopPropagation(); - const tabId = this.getAttribute('data-tab'); - switchTab(tabId); - }); + // Tab switching + const tabButtons = document.querySelectorAll(".tab-btn"); + tabButtons.forEach((button) => { + button.addEventListener("click", function (e) { + e.stopPropagation(); + const tabId = this.getAttribute("data-tab"); + switchTab(tabId); }); + }); - // Upload provider switching - const uploadProviderSelect = document.getElementById('UPLOAD_PROVIDER'); - if (uploadProviderSelect) { - uploadProviderSelect.addEventListener('change', function() { - toggleProviderConfig(this.value); - }); - } - - // Toggle switch events - const toggleSwitches = document.querySelectorAll('.toggle-switch'); - toggleSwitches.forEach(toggleSwitch => { - toggleSwitch.addEventListener('click', function(e) { - e.stopPropagation(); - const checkbox = this.querySelector('input[type="checkbox"]'); - if (checkbox) { - checkbox.checked = !checkbox.checked; - } - }); + // Upload provider switching + const uploadProviderSelect = document.getElementById("UPLOAD_PROVIDER"); + if (uploadProviderSelect) { + uploadProviderSelect.addEventListener("change", function () { + toggleProviderConfig(this.value); }); + } - // Save button - const saveBtn = document.getElementById('saveBtn'); - if (saveBtn) { - saveBtn.addEventListener('click', saveConfig); - } - - // Reset button - const resetBtn = document.getElementById('resetBtn'); - if (resetBtn) { - resetBtn.addEventListener('click', resetConfig); // resetConfig will open the modal - } - - // Scroll buttons - window.addEventListener('scroll', toggleScrollButtons); - - // API Key Modal Elements and Events - const addApiKeyBtn = document.getElementById('addApiKeyBtn'); - const closeApiKeyModalBtn = document.getElementById('closeApiKeyModalBtn'); - const cancelAddApiKeyBtn = document.getElementById('cancelAddApiKeyBtn'); - const confirmAddApiKeyBtn = document.getElementById('confirmAddApiKeyBtn'); - - if (addApiKeyBtn) { - addApiKeyBtn.addEventListener('click', () => { - openModal(apiKeyModal); - if (apiKeyBulkInput) apiKeyBulkInput.value = ''; - }); - } - if (closeApiKeyModalBtn) closeApiKeyModalBtn.addEventListener('click', () => closeModal(apiKeyModal)); - if (cancelAddApiKeyBtn) cancelAddApiKeyBtn.addEventListener('click', () => closeModal(apiKeyModal)); - if (confirmAddApiKeyBtn) confirmAddApiKeyBtn.addEventListener('click', handleBulkAddApiKeys); - if (apiKeySearchInput) apiKeySearchInput.addEventListener('input', handleApiKeySearch); - - - // Bulk Delete API Key Modal Elements and Events - const bulkDeleteApiKeyBtn = document.getElementById('bulkDeleteApiKeyBtn'); - const closeBulkDeleteModalBtn = document.getElementById('closeBulkDeleteModalBtn'); - const cancelBulkDeleteApiKeyBtn = document.getElementById('cancelBulkDeleteApiKeyBtn'); - const confirmBulkDeleteApiKeyBtn = document.getElementById('confirmBulkDeleteApiKeyBtn'); - - if (bulkDeleteApiKeyBtn) { - bulkDeleteApiKeyBtn.addEventListener('click', () => { - openModal(bulkDeleteApiKeyModal); - if (bulkDeleteApiKeyInput) bulkDeleteApiKeyInput.value = ''; - }); - } - if (closeBulkDeleteModalBtn) closeBulkDeleteModalBtn.addEventListener('click', () => closeModal(bulkDeleteApiKeyModal)); - if (cancelBulkDeleteApiKeyBtn) cancelBulkDeleteApiKeyBtn.addEventListener('click', () => closeModal(bulkDeleteApiKeyModal)); - if (confirmBulkDeleteApiKeyBtn) confirmBulkDeleteApiKeyBtn.addEventListener('click', handleBulkDeleteApiKeys); - - - // Proxy Modal Elements and Events - const addProxyBtn = document.getElementById('addProxyBtn'); - const closeProxyModalBtn = document.getElementById('closeProxyModalBtn'); - const cancelAddProxyBtn = document.getElementById('cancelAddProxyBtn'); - const confirmAddProxyBtn = document.getElementById('confirmAddProxyBtn'); - - if (addProxyBtn) { - addProxyBtn.addEventListener('click', () => { - openModal(proxyModal); - if (proxyBulkInput) proxyBulkInput.value = ''; - }); - } - if (closeProxyModalBtn) closeProxyModalBtn.addEventListener('click', () => closeModal(proxyModal)); - if (cancelAddProxyBtn) cancelAddProxyBtn.addEventListener('click', () => closeModal(proxyModal)); - if (confirmAddProxyBtn) confirmAddProxyBtn.addEventListener('click', handleBulkAddProxies); - - - // Bulk Delete Proxy Modal Elements and Events - const bulkDeleteProxyBtn = document.getElementById('bulkDeleteProxyBtn'); - const closeBulkDeleteProxyModalBtn = document.getElementById('closeBulkDeleteProxyModalBtn'); - const cancelBulkDeleteProxyBtn = document.getElementById('cancelBulkDeleteProxyBtn'); - const confirmBulkDeleteProxyBtn = document.getElementById('confirmBulkDeleteProxyBtn'); - - if (bulkDeleteProxyBtn) { - bulkDeleteProxyBtn.addEventListener('click', () => { - openModal(bulkDeleteProxyModal); - if (bulkDeleteProxyInput) bulkDeleteProxyInput.value = ''; - }); - } - if (closeBulkDeleteProxyModalBtn) closeBulkDeleteProxyModalBtn.addEventListener('click', () => closeModal(bulkDeleteProxyModal)); - if (cancelBulkDeleteProxyBtn) cancelBulkDeleteProxyBtn.addEventListener('click', () => closeModal(bulkDeleteProxyModal)); - if (confirmBulkDeleteProxyBtn) confirmBulkDeleteProxyBtn.addEventListener('click', handleBulkDeleteProxies); - - - // Reset Confirmation Modal Elements and Events - const closeResetModalBtn = document.getElementById('closeResetModalBtn'); - const cancelResetBtn = document.getElementById('cancelResetBtn'); - const confirmResetBtn = document.getElementById('confirmResetBtn'); - - if (closeResetModalBtn) closeResetModalBtn.addEventListener('click', () => closeModal(resetConfirmModal)); - if (cancelResetBtn) cancelResetBtn.addEventListener('click', () => closeModal(resetConfirmModal)); - if (confirmResetBtn) { - confirmResetBtn.addEventListener('click', () => { - closeModal(resetConfirmModal); - executeReset(); - }); - } - - // Click outside modal to close - window.addEventListener('click', (event) => { - const modals = [apiKeyModal, resetConfirmModal, bulkDeleteApiKeyModal, proxyModal, bulkDeleteProxyModal]; - modals.forEach(modal => { - if (event.target === modal) { - closeModal(modal); - } - }); + // Toggle switch events + const toggleSwitches = document.querySelectorAll(".toggle-switch"); + toggleSwitches.forEach((toggleSwitch) => { + toggleSwitch.addEventListener("click", function (e) { + e.stopPropagation(); + const checkbox = this.querySelector('input[type="checkbox"]'); + if (checkbox) { + checkbox.checked = !checkbox.checked; + } }); + }); - // Removed static token generation button event listener, now handled dynamically if needed or by specific buttons. + // Save button + const saveBtn = document.getElementById("saveBtn"); + if (saveBtn) { + saveBtn.addEventListener("click", saveConfig); + } - // Authentication token generation button - const generateAuthTokenBtn = document.getElementById('generateAuthTokenBtn'); - const authTokenInput = document.getElementById('AUTH_TOKEN'); - if (generateAuthTokenBtn && authTokenInput) { - generateAuthTokenBtn.addEventListener('click', function() { - const newToken = generateRandomToken(); // Assuming generateRandomToken is defined elsewhere - authTokenInput.value = newToken; - if (authTokenInput.classList.contains(SENSITIVE_INPUT_CLASS)) { - const event = new Event('focusout', { bubbles: true, cancelable: true }); - authTokenInput.dispatchEvent(event); - } - showNotification('已生成新认证令牌', 'success'); + // Reset button + const resetBtn = document.getElementById("resetBtn"); + if (resetBtn) { + resetBtn.addEventListener("click", resetConfig); // resetConfig will open the modal + } + + // Scroll buttons + window.addEventListener("scroll", toggleScrollButtons); + + // API Key Modal Elements and Events + const addApiKeyBtn = document.getElementById("addApiKeyBtn"); + const closeApiKeyModalBtn = document.getElementById("closeApiKeyModalBtn"); + const cancelAddApiKeyBtn = document.getElementById("cancelAddApiKeyBtn"); + const confirmAddApiKeyBtn = document.getElementById("confirmAddApiKeyBtn"); + + if (addApiKeyBtn) { + addApiKeyBtn.addEventListener("click", () => { + openModal(apiKeyModal); + if (apiKeyBulkInput) apiKeyBulkInput.value = ""; + }); + } + if (closeApiKeyModalBtn) + closeApiKeyModalBtn.addEventListener("click", () => + closeModal(apiKeyModal) + ); + if (cancelAddApiKeyBtn) + cancelAddApiKeyBtn.addEventListener("click", () => closeModal(apiKeyModal)); + if (confirmAddApiKeyBtn) + confirmAddApiKeyBtn.addEventListener("click", handleBulkAddApiKeys); + if (apiKeySearchInput) + apiKeySearchInput.addEventListener("input", handleApiKeySearch); + + // Bulk Delete API Key Modal Elements and Events + const bulkDeleteApiKeyBtn = document.getElementById("bulkDeleteApiKeyBtn"); + const closeBulkDeleteModalBtn = document.getElementById( + "closeBulkDeleteModalBtn" + ); + const cancelBulkDeleteApiKeyBtn = document.getElementById( + "cancelBulkDeleteApiKeyBtn" + ); + const confirmBulkDeleteApiKeyBtn = document.getElementById( + "confirmBulkDeleteApiKeyBtn" + ); + + if (bulkDeleteApiKeyBtn) { + bulkDeleteApiKeyBtn.addEventListener("click", () => { + openModal(bulkDeleteApiKeyModal); + if (bulkDeleteApiKeyInput) bulkDeleteApiKeyInput.value = ""; + }); + } + if (closeBulkDeleteModalBtn) + closeBulkDeleteModalBtn.addEventListener("click", () => + closeModal(bulkDeleteApiKeyModal) + ); + if (cancelBulkDeleteApiKeyBtn) + cancelBulkDeleteApiKeyBtn.addEventListener("click", () => + closeModal(bulkDeleteApiKeyModal) + ); + if (confirmBulkDeleteApiKeyBtn) + confirmBulkDeleteApiKeyBtn.addEventListener( + "click", + handleBulkDeleteApiKeys + ); + + // Proxy Modal Elements and Events + const addProxyBtn = document.getElementById("addProxyBtn"); + const closeProxyModalBtn = document.getElementById("closeProxyModalBtn"); + const cancelAddProxyBtn = document.getElementById("cancelAddProxyBtn"); + const confirmAddProxyBtn = document.getElementById("confirmAddProxyBtn"); + + if (addProxyBtn) { + addProxyBtn.addEventListener("click", () => { + openModal(proxyModal); + if (proxyBulkInput) proxyBulkInput.value = ""; + }); + } + if (closeProxyModalBtn) + closeProxyModalBtn.addEventListener("click", () => closeModal(proxyModal)); + if (cancelAddProxyBtn) + cancelAddProxyBtn.addEventListener("click", () => closeModal(proxyModal)); + if (confirmAddProxyBtn) + confirmAddProxyBtn.addEventListener("click", handleBulkAddProxies); + + // Bulk Delete Proxy Modal Elements and Events + const bulkDeleteProxyBtn = document.getElementById("bulkDeleteProxyBtn"); + const closeBulkDeleteProxyModalBtn = document.getElementById( + "closeBulkDeleteProxyModalBtn" + ); + const cancelBulkDeleteProxyBtn = document.getElementById( + "cancelBulkDeleteProxyBtn" + ); + const confirmBulkDeleteProxyBtn = document.getElementById( + "confirmBulkDeleteProxyBtn" + ); + + if (bulkDeleteProxyBtn) { + bulkDeleteProxyBtn.addEventListener("click", () => { + openModal(bulkDeleteProxyModal); + if (bulkDeleteProxyInput) bulkDeleteProxyInput.value = ""; + }); + } + if (closeBulkDeleteProxyModalBtn) + closeBulkDeleteProxyModalBtn.addEventListener("click", () => + closeModal(bulkDeleteProxyModal) + ); + if (cancelBulkDeleteProxyBtn) + cancelBulkDeleteProxyBtn.addEventListener("click", () => + closeModal(bulkDeleteProxyModal) + ); + if (confirmBulkDeleteProxyBtn) + confirmBulkDeleteProxyBtn.addEventListener( + "click", + handleBulkDeleteProxies + ); + + // Reset Confirmation Modal Elements and Events + const closeResetModalBtn = document.getElementById("closeResetModalBtn"); + const cancelResetBtn = document.getElementById("cancelResetBtn"); + const confirmResetBtn = document.getElementById("confirmResetBtn"); + + if (closeResetModalBtn) + closeResetModalBtn.addEventListener("click", () => + closeModal(resetConfirmModal) + ); + if (cancelResetBtn) + cancelResetBtn.addEventListener("click", () => + closeModal(resetConfirmModal) + ); + if (confirmResetBtn) { + confirmResetBtn.addEventListener("click", () => { + closeModal(resetConfirmModal); + executeReset(); + }); + } + + // Click outside modal to close + window.addEventListener("click", (event) => { + const modals = [ + apiKeyModal, + resetConfirmModal, + bulkDeleteApiKeyModal, + proxyModal, + bulkDeleteProxyModal, + ]; + modals.forEach((modal) => { + if (event.target === modal) { + closeModal(modal); + } + }); + }); + + // Removed static token generation button event listener, now handled dynamically if needed or by specific buttons. + + // Authentication token generation button + const generateAuthTokenBtn = document.getElementById("generateAuthTokenBtn"); + const authTokenInput = document.getElementById("AUTH_TOKEN"); + if (generateAuthTokenBtn && authTokenInput) { + generateAuthTokenBtn.addEventListener("click", function () { + const newToken = generateRandomToken(); // Assuming generateRandomToken is defined elsewhere + authTokenInput.value = newToken; + if (authTokenInput.classList.contains(SENSITIVE_INPUT_CLASS)) { + const event = new Event("focusout", { + bubbles: true, + cancelable: true, }); - } + authTokenInput.dispatchEvent(event); + } + showNotification("已生成新认证令牌", "success"); + }); + } - // Event delegation for THINKING_MODELS input changes to update budget map keys - if (thinkingModelsContainer) { - thinkingModelsContainer.addEventListener('input', function(event) { - const target = event.target; - if (target && target.classList.contains(ARRAY_INPUT_CLASS) && target.closest(`.${ARRAY_ITEM_CLASS}[data-model-id]`)) { - const modelInput = target; - const modelItem = modelInput.closest(`.${ARRAY_ITEM_CLASS}`); - const modelId = modelItem.getAttribute('data-model-id'); - const budgetKeyInput = document.querySelector(`.${MAP_KEY_INPUT_CLASS}[data-model-id="${modelId}"]`); - if (budgetKeyInput) { - budgetKeyInput.value = modelInput.value; - } - } - }); - } - - // Event delegation for dynamically added remove buttons and generate token buttons within array items - if(configForm) { // Ensure configForm exists before adding event listener - configForm.addEventListener('click', function(event) { - const target = event.target; - const removeButton = target.closest('.remove-btn'); - const generateButton = target.closest('.generate-btn'); + // Event delegation for THINKING_MODELS input changes to update budget map keys + if (thinkingModelsContainer) { + thinkingModelsContainer.addEventListener("input", function (event) { + const target = event.target; + if ( + target && + target.classList.contains(ARRAY_INPUT_CLASS) && + target.closest(`.${ARRAY_ITEM_CLASS}[data-model-id]`) + ) { + const modelInput = target; + const modelItem = modelInput.closest(`.${ARRAY_ITEM_CLASS}`); + const modelId = modelItem.getAttribute("data-model-id"); + const budgetKeyInput = document.querySelector( + `.${MAP_KEY_INPUT_CLASS}[data-model-id="${modelId}"]` + ); + if (budgetKeyInput) { + budgetKeyInput.value = modelInput.value; + } + } + }); + } - if (removeButton && removeButton.closest(`.${ARRAY_ITEM_CLASS}`)) { - const arrayItem = removeButton.closest(`.${ARRAY_ITEM_CLASS}`); - const parentContainer = arrayItem.parentElement; - const isThinkingModelItem = arrayItem.hasAttribute('data-model-id') && parentContainer && parentContainer.id === 'THINKING_MODELS_container'; - const isSafetySettingItem = arrayItem.classList.contains(SAFETY_SETTING_ITEM_CLASS); + // Event delegation for dynamically added remove buttons and generate token buttons within array items + if (configForm) { + // Ensure configForm exists before adding event listener + configForm.addEventListener("click", function (event) { + const target = event.target; + const removeButton = target.closest(".remove-btn"); + const generateButton = target.closest(".generate-btn"); - if (isThinkingModelItem) { - const modelId = arrayItem.getAttribute('data-model-id'); - const budgetMapItem = document.querySelector(`.${MAP_ITEM_CLASS}[data-model-id="${modelId}"]`); - if (budgetMapItem) { - budgetMapItem.remove(); - } - // Check and add placeholder for budget map if empty - const budgetContainer = document.getElementById('THINKING_BUDGET_MAP_container'); - if (budgetContainer && budgetContainer.children.length === 0) { - budgetContainer.innerHTML = '
请在上方添加思考模型,预算将自动关联。
'; - } - } - arrayItem.remove(); - // Check and add placeholder for safety settings if empty - if (isSafetySettingItem && parentContainer && parentContainer.children.length === 0) { - parentContainer.innerHTML = '
定义模型的安全过滤阈值。
'; - } - } else if (generateButton && generateButton.closest(`.${ARRAY_ITEM_CLASS}`)) { - const inputField = generateButton.closest(`.${ARRAY_ITEM_CLASS}`).querySelector(`.${ARRAY_INPUT_CLASS}`); - if (inputField) { - const newToken = generateRandomToken(); - inputField.value = newToken; - if (inputField.classList.contains(SENSITIVE_INPUT_CLASS)) { - const event = new Event('focusout', { bubbles: true, cancelable: true }); - inputField.dispatchEvent(event); - } - showNotification('已生成新令牌', 'success'); - } - } - }); - } + if (removeButton && removeButton.closest(`.${ARRAY_ITEM_CLASS}`)) { + const arrayItem = removeButton.closest(`.${ARRAY_ITEM_CLASS}`); + const parentContainer = arrayItem.parentElement; + const isThinkingModelItem = + arrayItem.hasAttribute("data-model-id") && + parentContainer && + parentContainer.id === "THINKING_MODELS_container"; + const isSafetySettingItem = arrayItem.classList.contains( + SAFETY_SETTING_ITEM_CLASS + ); + if (isThinkingModelItem) { + const modelId = arrayItem.getAttribute("data-model-id"); + const budgetMapItem = document.querySelector( + `.${MAP_ITEM_CLASS}[data-model-id="${modelId}"]` + ); + if (budgetMapItem) { + budgetMapItem.remove(); + } + // Check and add placeholder for budget map if empty + const budgetContainer = document.getElementById( + "THINKING_BUDGET_MAP_container" + ); + if (budgetContainer && budgetContainer.children.length === 0) { + budgetContainer.innerHTML = + '
请在上方添加思考模型,预算将自动关联。
'; + } + } + arrayItem.remove(); + // Check and add placeholder for safety settings if empty + if ( + isSafetySettingItem && + parentContainer && + parentContainer.children.length === 0 + ) { + parentContainer.innerHTML = + '
定义模型的安全过滤阈值。
'; + } + } else if ( + generateButton && + generateButton.closest(`.${ARRAY_ITEM_CLASS}`) + ) { + const inputField = generateButton + .closest(`.${ARRAY_ITEM_CLASS}`) + .querySelector(`.${ARRAY_INPUT_CLASS}`); + if (inputField) { + const newToken = generateRandomToken(); + inputField.value = newToken; + if (inputField.classList.contains(SENSITIVE_INPUT_CLASS)) { + const event = new Event("focusout", { + bubbles: true, + cancelable: true, + }); + inputField.dispatchEvent(event); + } + showNotification("已生成新令牌", "success"); + } + } + }); + } - // Add Safety Setting button - const addSafetySettingBtn = document.getElementById('addSafetySettingBtn'); - if (addSafetySettingBtn) { - addSafetySettingBtn.addEventListener('click', () => addSafetySettingItem()); - } + // Add Safety Setting button + const addSafetySettingBtn = document.getElementById("addSafetySettingBtn"); + if (addSafetySettingBtn) { + addSafetySettingBtn.addEventListener("click", () => addSafetySettingItem()); + } - initializeSensitiveFields(); // Initialize sensitive field handling + initializeSensitiveFields(); // Initialize sensitive field handling }); // <-- DOMContentLoaded end /** * Initializes sensitive input field behavior (masking/unmasking). */ function initializeSensitiveFields() { - if (!configForm) return; + if (!configForm) return; - // Helper function: Mask field - function maskField(field) { - if (field.value && field.value !== MASKED_VALUE) { - field.setAttribute('data-real-value', field.value); - field.value = MASKED_VALUE; - } else if (!field.value) { // If field value is empty string - field.removeAttribute('data-real-value'); - // Ensure empty value doesn't show as asterisks - if (field.value === MASKED_VALUE) field.value = ''; - } + // Helper function: Mask field + function maskField(field) { + if (field.value && field.value !== MASKED_VALUE) { + field.setAttribute("data-real-value", field.value); + field.value = MASKED_VALUE; + } else if (!field.value) { + // If field value is empty string + field.removeAttribute("data-real-value"); + // Ensure empty value doesn't show as asterisks + if (field.value === MASKED_VALUE) field.value = ""; } + } - // Helper function: Unmask field - function unmaskField(field) { - if (field.hasAttribute('data-real-value')) { - field.value = field.getAttribute('data-real-value'); - } - // If no data-real-value and value is MASKED_VALUE, it might be an initial empty sensitive field, clear it - else if (field.value === MASKED_VALUE && !field.hasAttribute('data-real-value')) { - field.value = ''; - } + // Helper function: Unmask field + function unmaskField(field) { + if (field.hasAttribute("data-real-value")) { + field.value = field.getAttribute("data-real-value"); } - - // Initial masking for existing sensitive fields on page load - // This function is called after populateForm and after dynamic element additions (via event delegation) - function initialMaskAllExisting() { - const sensitiveFields = configForm.querySelectorAll(`.${SENSITIVE_INPUT_CLASS}`); - sensitiveFields.forEach(field => { - if (field.type === 'password') { - // For password fields, browser handles it. We just ensure data-original-type is set - // and if it has a value, we also store data-real-value so it can be shown when switched to text - if (field.value) { - field.setAttribute('data-real-value', field.value); - } - // No need to set to MASKED_VALUE as browser handles it. - } else if (field.type === 'text' || field.tagName.toLowerCase() === 'textarea') { - maskField(field); - } - }); + // If no data-real-value and value is MASKED_VALUE, it might be an initial empty sensitive field, clear it + else if ( + field.value === MASKED_VALUE && + !field.hasAttribute("data-real-value") + ) { + field.value = ""; } - initialMaskAllExisting(); + } - - // Event delegation for dynamic and static fields - configForm.addEventListener('focusin', function(event) { - const target = event.target; - if (target.classList.contains(SENSITIVE_INPUT_CLASS)) { - if (target.type === 'password') { - // Record original type to switch back on blur - if (!target.hasAttribute('data-original-type')) { - target.setAttribute('data-original-type', 'password'); - } - target.type = 'text'; // Switch to text type to show content - // If data-real-value exists (e.g., set during populateForm), use it - if (target.hasAttribute('data-real-value')) { - target.value = target.getAttribute('data-real-value'); - } - // Otherwise, the browser's existing password value will be shown directly - } else { // For type="text" or textarea - unmaskField(target); - } + // Initial masking for existing sensitive fields on page load + // This function is called after populateForm and after dynamic element additions (via event delegation) + function initialMaskAllExisting() { + const sensitiveFields = configForm.querySelectorAll( + `.${SENSITIVE_INPUT_CLASS}` + ); + sensitiveFields.forEach((field) => { + if (field.type === "password") { + // For password fields, browser handles it. We just ensure data-original-type is set + // and if it has a value, we also store data-real-value so it can be shown when switched to text + if (field.value) { + field.setAttribute("data-real-value", field.value); } + // No need to set to MASKED_VALUE as browser handles it. + } else if ( + field.type === "text" || + field.tagName.toLowerCase() === "textarea" + ) { + maskField(field); + } }); + } + initialMaskAllExisting(); - configForm.addEventListener('focusout', function(event) { - const target = event.target; - if (target.classList.contains(SENSITIVE_INPUT_CLASS)) { - // First, if the field is currently text and has a value, update data-real-value - if (target.type === 'text' || target.tagName.toLowerCase() === 'textarea') { - if (target.value && target.value !== MASKED_VALUE) { - target.setAttribute('data-real-value', target.value); - } else if (!target.value) { // If value is empty, remove data-real-value - target.removeAttribute('data-real-value'); - } - } - - // Then handle type switching and masking - if (target.getAttribute('data-original-type') === 'password' && target.type === 'text') { - target.type = 'password'; // Switch back to password type - // For password type, browser handles masking automatically, no need to set MASKED_VALUE manually - // data-real-value has already been updated by the logic above - } else if (target.type === 'text' || target.tagName.toLowerCase() === 'textarea') { - // For text or textarea sensitive fields, perform masking - maskField(target); - } + // Event delegation for dynamic and static fields + configForm.addEventListener("focusin", function (event) { + const target = event.target; + if (target.classList.contains(SENSITIVE_INPUT_CLASS)) { + if (target.type === "password") { + // Record original type to switch back on blur + if (!target.hasAttribute("data-original-type")) { + target.setAttribute("data-original-type", "password"); } - }); + target.type = "text"; // Switch to text type to show content + // If data-real-value exists (e.g., set during populateForm), use it + if (target.hasAttribute("data-real-value")) { + target.value = target.getAttribute("data-real-value"); + } + // Otherwise, the browser's existing password value will be shown directly + } else { + // For type="text" or textarea + unmaskField(target); + } + } + }); + + configForm.addEventListener("focusout", function (event) { + const target = event.target; + if (target.classList.contains(SENSITIVE_INPUT_CLASS)) { + // First, if the field is currently text and has a value, update data-real-value + if ( + target.type === "text" || + target.tagName.toLowerCase() === "textarea" + ) { + if (target.value && target.value !== MASKED_VALUE) { + target.setAttribute("data-real-value", target.value); + } else if (!target.value) { + // If value is empty, remove data-real-value + target.removeAttribute("data-real-value"); + } + } + + // Then handle type switching and masking + if ( + target.getAttribute("data-original-type") === "password" && + target.type === "text" + ) { + target.type = "password"; // Switch back to password type + // For password type, browser handles masking automatically, no need to set MASKED_VALUE manually + // data-real-value has already been updated by the logic above + } else if ( + target.type === "text" || + target.tagName.toLowerCase() === "textarea" + ) { + // For text or textarea sensitive fields, perform masking + maskField(target); + } + } + }); } /** @@ -369,100 +474,149 @@ function initializeSensitiveFields() { * @returns {string} A new UUID. */ function generateUUID() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + var r = (Math.random() * 16) | 0, + v = c == "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); } /** * Initializes the configuration by fetching it from the server and populating the form. */ async function initConfig() { - try { - showNotification('正在加载配置...', 'info'); - const response = await fetch('/api/config'); + try { + showNotification("正在加载配置...", "info"); + const response = await fetch("/api/config"); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const config = await response.json(); - - // 确保数组字段有默认值 - if (!config.API_KEYS || !Array.isArray(config.API_KEYS) || config.API_KEYS.length === 0) { - config.API_KEYS = ['请在此处输入 API 密钥']; - } - - if (!config.ALLOWED_TOKENS || !Array.isArray(config.ALLOWED_TOKENS) || config.ALLOWED_TOKENS.length === 0) { - config.ALLOWED_TOKENS = ['']; - } - - if (!config.IMAGE_MODELS || !Array.isArray(config.IMAGE_MODELS) || config.IMAGE_MODELS.length === 0) { - config.IMAGE_MODELS = ['gemini-1.5-pro-latest']; - } - - if (!config.SEARCH_MODELS || !Array.isArray(config.SEARCH_MODELS) || config.SEARCH_MODELS.length === 0) { - config.SEARCH_MODELS = ['gemini-1.5-flash-latest']; - } - - if (!config.FILTERED_MODELS || !Array.isArray(config.FILTERED_MODELS) || config.FILTERED_MODELS.length === 0) { - config.FILTERED_MODELS = ['gemini-1.0-pro-latest']; - } - // --- 新增:处理 PROXIES 默认值 --- - if (!config.PROXIES || !Array.isArray(config.PROXIES)) { - config.PROXIES = []; // 默认为空数组 - } - // --- 新增:处理新字段的默认值 --- - if (!config.THINKING_MODELS || !Array.isArray(config.THINKING_MODELS)) { - config.THINKING_MODELS = []; // 默认为空数组 - } - if (!config.THINKING_BUDGET_MAP || typeof config.THINKING_BUDGET_MAP !== 'object' || config.THINKING_BUDGET_MAP === null) { - config.THINKING_BUDGET_MAP = {}; // 默认为空对象 - } - // --- 新增:处理 SAFETY_SETTINGS 默认值 --- - if (!config.SAFETY_SETTINGS || !Array.isArray(config.SAFETY_SETTINGS)) { - config.SAFETY_SETTINGS = []; // 默认为空数组 - } - // --- 结束:处理 SAFETY_SETTINGS 默认值 --- - - populateForm(config); - // After populateForm, initialize masking for all populated sensitive fields - if (configForm) { // Ensure form exists - initializeSensitiveFields(); // Call initializeSensitiveFields to handle initial masking - } - - // Ensure upload provider has a default value - const uploadProvider = document.getElementById('UPLOAD_PROVIDER'); - if (uploadProvider && !uploadProvider.value) { - uploadProvider.value = 'smms'; // 设置默认值为 smms - toggleProviderConfig('smms'); - } - - showNotification('配置加载成功', 'success'); - } catch (error) { - console.error('加载配置失败:', error); - showNotification('加载配置失败: ' + error.message, 'error'); - - // 加载失败时,使用默认配置 - const defaultConfig = { - API_KEYS: [''], - ALLOWED_TOKENS: [''], - IMAGE_MODELS: ['gemini-1.5-pro-latest'], - SEARCH_MODELS: ['gemini-1.5-flash-latest'], - FILTERED_MODELS: ['gemini-1.0-pro-latest'], - UPLOAD_PROVIDER: 'smms', - PROXIES: [], // 添加默认值 - THINKING_MODELS: [], - THINKING_BUDGET_MAP: {} - }; - - populateForm(defaultConfig); - if (configForm) { // Ensure form exists - initializeSensitiveFields(); // Call initializeSensitiveFields to handle initial masking - } - toggleProviderConfig('smms'); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); } + + const config = await response.json(); + + // 确保数组字段有默认值 + if ( + !config.API_KEYS || + !Array.isArray(config.API_KEYS) || + config.API_KEYS.length === 0 + ) { + config.API_KEYS = ["请在此处输入 API 密钥"]; + } + + if ( + !config.ALLOWED_TOKENS || + !Array.isArray(config.ALLOWED_TOKENS) || + config.ALLOWED_TOKENS.length === 0 + ) { + config.ALLOWED_TOKENS = [""]; + } + + if ( + !config.IMAGE_MODELS || + !Array.isArray(config.IMAGE_MODELS) || + config.IMAGE_MODELS.length === 0 + ) { + config.IMAGE_MODELS = ["gemini-1.5-pro-latest"]; + } + + if ( + !config.SEARCH_MODELS || + !Array.isArray(config.SEARCH_MODELS) || + config.SEARCH_MODELS.length === 0 + ) { + config.SEARCH_MODELS = ["gemini-1.5-flash-latest"]; + } + + if ( + !config.FILTERED_MODELS || + !Array.isArray(config.FILTERED_MODELS) || + config.FILTERED_MODELS.length === 0 + ) { + config.FILTERED_MODELS = ["gemini-1.0-pro-latest"]; + } + // --- 新增:处理 PROXIES 默认值 --- + if (!config.PROXIES || !Array.isArray(config.PROXIES)) { + config.PROXIES = []; // 默认为空数组 + } + // --- 新增:处理新字段的默认值 --- + if (!config.THINKING_MODELS || !Array.isArray(config.THINKING_MODELS)) { + config.THINKING_MODELS = []; // 默认为空数组 + } + if ( + !config.THINKING_BUDGET_MAP || + typeof config.THINKING_BUDGET_MAP !== "object" || + config.THINKING_BUDGET_MAP === null + ) { + config.THINKING_BUDGET_MAP = {}; // 默认为空对象 + } + // --- 新增:处理 SAFETY_SETTINGS 默认值 --- + if (!config.SAFETY_SETTINGS || !Array.isArray(config.SAFETY_SETTINGS)) { + config.SAFETY_SETTINGS = []; // 默认为空数组 + } + // --- 结束:处理 SAFETY_SETTINGS 默认值 --- + + // --- 新增:处理自动删除错误日志配置的默认值 --- + if (typeof config.AUTO_DELETE_ERROR_LOGS_ENABLED === "undefined") { + config.AUTO_DELETE_ERROR_LOGS_ENABLED = false; + } + if (typeof config.AUTO_DELETE_ERROR_LOGS_DAYS === "undefined") { + config.AUTO_DELETE_ERROR_LOGS_DAYS = 7; + } + // --- 结束:处理自动删除错误日志配置的默认值 --- + + // --- 新增:处理自动删除请求日志配置的默认值 --- + if (typeof config.AUTO_DELETE_REQUEST_LOGS_ENABLED === "undefined") { + config.AUTO_DELETE_REQUEST_LOGS_ENABLED = false; + } + if (typeof config.AUTO_DELETE_REQUEST_LOGS_DAYS === "undefined") { + config.AUTO_DELETE_REQUEST_LOGS_DAYS = 30; + } + // --- 结束:处理自动删除请求日志配置的默认值 --- + + populateForm(config); + // After populateForm, initialize masking for all populated sensitive fields + if (configForm) { + // Ensure form exists + initializeSensitiveFields(); // Call initializeSensitiveFields to handle initial masking + } + + // Ensure upload provider has a default value + const uploadProvider = document.getElementById("UPLOAD_PROVIDER"); + if (uploadProvider && !uploadProvider.value) { + uploadProvider.value = "smms"; // 设置默认值为 smms + toggleProviderConfig("smms"); + } + + showNotification("配置加载成功", "success"); + } catch (error) { + console.error("加载配置失败:", error); + showNotification("加载配置失败: " + error.message, "error"); + + // 加载失败时,使用默认配置 + const defaultConfig = { + API_KEYS: [""], + ALLOWED_TOKENS: [""], + IMAGE_MODELS: ["gemini-1.5-pro-latest"], + SEARCH_MODELS: ["gemini-1.5-flash-latest"], + FILTERED_MODELS: ["gemini-1.0-pro-latest"], + UPLOAD_PROVIDER: "smms", + PROXIES: [], // 添加默认值 + THINKING_MODELS: [], + THINKING_BUDGET_MAP: {}, + AUTO_DELETE_ERROR_LOGS_ENABLED: false, // 新增默认值 + AUTO_DELETE_ERROR_LOGS_DAYS: 7, // 新增默认值 + AUTO_DELETE_REQUEST_LOGS_ENABLED: false, // 新增默认值 + AUTO_DELETE_REQUEST_LOGS_DAYS: 30, // 新增默认值 + }; + + populateForm(defaultConfig); + if (configForm) { + // Ensure form exists + initializeSensitiveFields(); // Call initializeSensitiveFields to handle initial masking + } + toggleProviderConfig("smms"); + } } /** @@ -470,284 +624,382 @@ async function initConfig() { * @param {object} config - The configuration object. */ function populateForm(config) { - const modelIdMap = {}; // modelName -> modelId + const modelIdMap = {}; // modelName -> modelId - // 1. Clear existing dynamic content first - const arrayContainers = document.querySelectorAll('.array-container'); - arrayContainers.forEach(container => { - container.innerHTML = ''; // Clear all array containers - }); - const budgetMapContainer = document.getElementById('THINKING_BUDGET_MAP_container'); - if (budgetMapContainer) { - budgetMapContainer.innerHTML = ''; // Clear budget map container - } else { - console.error("Critical: THINKING_BUDGET_MAP_container not found!"); - return; // Cannot proceed - } + // 1. Clear existing dynamic content first + const arrayContainers = document.querySelectorAll(".array-container"); + arrayContainers.forEach((container) => { + container.innerHTML = ""; // Clear all array containers + }); + const budgetMapContainer = document.getElementById( + "THINKING_BUDGET_MAP_container" + ); + if (budgetMapContainer) { + budgetMapContainer.innerHTML = ""; // Clear budget map container + } else { + console.error("Critical: THINKING_BUDGET_MAP_container not found!"); + return; // Cannot proceed + } - // 2. Populate THINKING_MODELS and build the map - if (Array.isArray(config.THINKING_MODELS)) { - const container = document.getElementById('THINKING_MODELS_container'); - if (container) { - config.THINKING_MODELS.forEach(modelName => { - if (modelName && typeof modelName === 'string' && modelName.trim()) { - const trimmedModelName = modelName.trim(); - const modelId = addArrayItemWithValue('THINKING_MODELS', trimmedModelName); - if (modelId) { - modelIdMap[trimmedModelName] = modelId; - } else { - console.warn(`Failed to get modelId for THINKING_MODEL: '${trimmedModelName}'`); - } - } else { - console.warn(`Invalid THINKING_MODEL entry found:`, modelName); - } - }); + // 2. Populate THINKING_MODELS and build the map + if (Array.isArray(config.THINKING_MODELS)) { + const container = document.getElementById("THINKING_MODELS_container"); + if (container) { + config.THINKING_MODELS.forEach((modelName) => { + if (modelName && typeof modelName === "string" && modelName.trim()) { + const trimmedModelName = modelName.trim(); + const modelId = addArrayItemWithValue( + "THINKING_MODELS", + trimmedModelName + ); + if (modelId) { + modelIdMap[trimmedModelName] = modelId; + } else { + console.warn( + `Failed to get modelId for THINKING_MODEL: '${trimmedModelName}'` + ); + } } else { - console.error("Critical: THINKING_MODELS_container not found!"); + console.warn(`Invalid THINKING_MODEL entry found:`, modelName); } + }); + } else { + console.error("Critical: THINKING_MODELS_container not found!"); } + } - // 3. Populate THINKING_BUDGET_MAP using the map - let budgetItemsAdded = false; - if (config.THINKING_BUDGET_MAP && typeof config.THINKING_BUDGET_MAP === 'object') { - for (const [modelName, budgetValue] of Object.entries(config.THINKING_BUDGET_MAP)) { - if (modelName && typeof modelName === 'string') { - const trimmedModelName = modelName.trim(); - const modelId = modelIdMap[trimmedModelName]; // Look up the ID - if (modelId) { - createAndAppendBudgetMapItem(trimmedModelName, budgetValue, modelId); - budgetItemsAdded = true; - } else { - console.warn(`Budget map: Could not find model ID for '${trimmedModelName}'. Skipping budget item.`); - } - } else { - console.warn(`Invalid key found in THINKING_BUDGET_MAP:`, modelName); - } + // 3. Populate THINKING_BUDGET_MAP using the map + let budgetItemsAdded = false; + if ( + config.THINKING_BUDGET_MAP && + typeof config.THINKING_BUDGET_MAP === "object" + ) { + for (const [modelName, budgetValue] of Object.entries( + config.THINKING_BUDGET_MAP + )) { + if (modelName && typeof modelName === "string") { + const trimmedModelName = modelName.trim(); + const modelId = modelIdMap[trimmedModelName]; // Look up the ID + if (modelId) { + createAndAppendBudgetMapItem(trimmedModelName, budgetValue, modelId); + budgetItemsAdded = true; + } else { + console.warn( + `Budget map: Could not find model ID for '${trimmedModelName}'. Skipping budget item.` + ); } + } else { + console.warn(`Invalid key found in THINKING_BUDGET_MAP:`, modelName); + } } - if (!budgetItemsAdded && budgetMapContainer) { - budgetMapContainer.innerHTML = '
请在上方添加思考模型,预算将自动关联。
'; - } + } + if (!budgetItemsAdded && budgetMapContainer) { + budgetMapContainer.innerHTML = + '
请在上方添加思考模型,预算将自动关联。
'; + } - // 4. Populate other array fields (excluding THINKING_MODELS) - for (const [key, value] of Object.entries(config)) { - if (Array.isArray(value) && key !== 'THINKING_MODELS') { - const container = document.getElementById(`${key}_container`); - if (container) { - value.forEach(itemValue => { - if (typeof itemValue === 'string') { - addArrayItemWithValue(key, itemValue); - } else { - console.warn(`Invalid item found in array '${key}':`, itemValue); - } - }); - } - } - } - - // 5. Populate non-array/non-budget fields - for (const [key, value] of Object.entries(config)) { - if (!Array.isArray(value) && !(typeof value === 'object' && value !== null && key === 'THINKING_BUDGET_MAP')) { - const element = document.getElementById(key); - if (element) { - if (element.type === 'checkbox' && typeof value === 'boolean') { - element.checked = value; - } else if (element.type !== 'checkbox') { - if (key === 'LOG_LEVEL' && typeof value === 'string') { - element.value = value.toUpperCase(); - } else { - element.value = (value !== null && value !== undefined) ? value : ''; - } - } - } - } - } - - // 6. Initialize upload provider - const uploadProvider = document.getElementById('UPLOAD_PROVIDER'); - if (uploadProvider) { - toggleProviderConfig(uploadProvider.value); - } - - // Populate SAFETY_SETTINGS - let safetyItemsAdded = false; - if (safetySettingsContainer && Array.isArray(config.SAFETY_SETTINGS)) { - config.SAFETY_SETTINGS.forEach(setting => { - if (setting && typeof setting === 'object' && setting.category && setting.threshold) { - addSafetySettingItem(setting.category, setting.threshold); - safetyItemsAdded = true; - } else { - console.warn("Invalid safety setting item found:", setting); - } + // 4. Populate other array fields (excluding THINKING_MODELS) + for (const [key, value] of Object.entries(config)) { + if (Array.isArray(value) && key !== "THINKING_MODELS") { + const container = document.getElementById(`${key}_container`); + if (container) { + value.forEach((itemValue) => { + if (typeof itemValue === "string") { + addArrayItemWithValue(key, itemValue); + } else { + console.warn(`Invalid item found in array '${key}':`, itemValue); + } }); + } } - if (safetySettingsContainer && !safetyItemsAdded) { - safetySettingsContainer.innerHTML = '
定义模型的安全过滤阈值。
'; + } + + // 5. Populate non-array/non-budget fields + for (const [key, value] of Object.entries(config)) { + if ( + !Array.isArray(value) && + !( + typeof value === "object" && + value !== null && + key === "THINKING_BUDGET_MAP" + ) + ) { + const element = document.getElementById(key); + if (element) { + if (element.type === "checkbox" && typeof value === "boolean") { + element.checked = value; + } else if (element.type !== "checkbox") { + if (key === "LOG_LEVEL" && typeof value === "string") { + element.value = value.toUpperCase(); + } else { + element.value = value !== null && value !== undefined ? value : ""; + } + } + } } + } + + // 6. Initialize upload provider + const uploadProvider = document.getElementById("UPLOAD_PROVIDER"); + if (uploadProvider) { + toggleProviderConfig(uploadProvider.value); + } + + // Populate SAFETY_SETTINGS + let safetyItemsAdded = false; + if (safetySettingsContainer && Array.isArray(config.SAFETY_SETTINGS)) { + config.SAFETY_SETTINGS.forEach((setting) => { + if ( + setting && + typeof setting === "object" && + setting.category && + setting.threshold + ) { + addSafetySettingItem(setting.category, setting.threshold); + safetyItemsAdded = true; + } else { + console.warn("Invalid safety setting item found:", setting); + } + }); + } + if (safetySettingsContainer && !safetyItemsAdded) { + safetySettingsContainer.innerHTML = + '
定义模型的安全过滤阈值。
'; + } + + // --- 新增:处理自动删除错误日志的字段 --- + const autoDeleteEnabledCheckbox = document.getElementById( + "AUTO_DELETE_ERROR_LOGS_ENABLED" + ); + const autoDeleteDaysSelect = document.getElementById( + "AUTO_DELETE_ERROR_LOGS_DAYS" + ); + + if (autoDeleteEnabledCheckbox && autoDeleteDaysSelect) { + autoDeleteEnabledCheckbox.checked = !!config.AUTO_DELETE_ERROR_LOGS_ENABLED; // 确保是布尔值 + autoDeleteDaysSelect.value = config.AUTO_DELETE_ERROR_LOGS_DAYS || 7; // 默认7天 + + // 根据复选框状态设置下拉框的禁用状态 + autoDeleteDaysSelect.disabled = !autoDeleteEnabledCheckbox.checked; + + // 添加事件监听器 + autoDeleteEnabledCheckbox.addEventListener("change", function () { + autoDeleteDaysSelect.disabled = !this.checked; + }); + } + // --- 结束:处理自动删除错误日志的字段 --- + + // --- 新增:处理自动删除请求日志的字段 --- + const autoDeleteRequestEnabledCheckbox = document.getElementById( + "AUTO_DELETE_REQUEST_LOGS_ENABLED" + ); + const autoDeleteRequestDaysSelect = document.getElementById( + "AUTO_DELETE_REQUEST_LOGS_DAYS" + ); + + if (autoDeleteRequestEnabledCheckbox && autoDeleteRequestDaysSelect) { + autoDeleteRequestEnabledCheckbox.checked = + !!config.AUTO_DELETE_REQUEST_LOGS_ENABLED; + autoDeleteRequestDaysSelect.value = + config.AUTO_DELETE_REQUEST_LOGS_DAYS || 30; + autoDeleteRequestDaysSelect.disabled = + !autoDeleteRequestEnabledCheckbox.checked; + + autoDeleteRequestEnabledCheckbox.addEventListener("change", function () { + autoDeleteRequestDaysSelect.disabled = !this.checked; + }); + } + // --- 结束:处理自动删除请求日志的字段 --- } /** * Handles the bulk addition of API keys from the modal input. */ function handleBulkAddApiKeys() { - const apiKeyContainer = document.getElementById('API_KEYS_container'); - if (!apiKeyBulkInput || !apiKeyContainer || !apiKeyModal) return; + const apiKeyContainer = document.getElementById("API_KEYS_container"); + if (!apiKeyBulkInput || !apiKeyContainer || !apiKeyModal) return; - const bulkText = apiKeyBulkInput.value; - const extractedKeys = bulkText.match(API_KEY_REGEX) || []; + const bulkText = apiKeyBulkInput.value; + const extractedKeys = bulkText.match(API_KEY_REGEX) || []; - const currentKeyInputs = apiKeyContainer.querySelectorAll(`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`); - let currentKeys = Array.from(currentKeyInputs).map(input => { - return input.hasAttribute('data-real-value') ? input.getAttribute('data-real-value') : input.value; - }).filter(key => key && key.trim() !== '' && key !== MASKED_VALUE); + const currentKeyInputs = apiKeyContainer.querySelectorAll( + `.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}` + ); + let currentKeys = Array.from(currentKeyInputs) + .map((input) => { + return input.hasAttribute("data-real-value") + ? input.getAttribute("data-real-value") + : input.value; + }) + .filter((key) => key && key.trim() !== "" && key !== MASKED_VALUE); - const combinedKeys = new Set([...currentKeys, ...extractedKeys]); - const uniqueKeys = Array.from(combinedKeys); + const combinedKeys = new Set([...currentKeys, ...extractedKeys]); + const uniqueKeys = Array.from(combinedKeys); - apiKeyContainer.innerHTML = ''; // Clear existing items more directly + apiKeyContainer.innerHTML = ""; // Clear existing items more directly - uniqueKeys.forEach(key => { - addArrayItemWithValue('API_KEYS', key); - }); - - const newKeyInputs = apiKeyContainer.querySelectorAll(`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`); - newKeyInputs.forEach(input => { - if (configForm && typeof initializeSensitiveFields === 'function') { - const focusoutEvent = new Event('focusout', { bubbles: true, cancelable: true }); - input.dispatchEvent(focusoutEvent); - } - }); + uniqueKeys.forEach((key) => { + addArrayItemWithValue("API_KEYS", key); + }); - closeModal(apiKeyModal); - showNotification(`添加/更新了 ${uniqueKeys.length} 个唯一密钥`, 'success'); + const newKeyInputs = apiKeyContainer.querySelectorAll( + `.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}` + ); + newKeyInputs.forEach((input) => { + if (configForm && typeof initializeSensitiveFields === "function") { + const focusoutEvent = new Event("focusout", { + bubbles: true, + cancelable: true, + }); + input.dispatchEvent(focusoutEvent); + } + }); + + closeModal(apiKeyModal); + showNotification(`添加/更新了 ${uniqueKeys.length} 个唯一密钥`, "success"); } /** * Handles searching/filtering of API keys in the list. */ function handleApiKeySearch() { - const apiKeyContainer = document.getElementById('API_KEYS_container'); - if (!apiKeySearchInput || !apiKeyContainer) return; + const apiKeyContainer = document.getElementById("API_KEYS_container"); + if (!apiKeySearchInput || !apiKeyContainer) return; - const searchTerm = apiKeySearchInput.value.toLowerCase(); - const keyItems = apiKeyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`); + const searchTerm = apiKeySearchInput.value.toLowerCase(); + const keyItems = apiKeyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`); - keyItems.forEach(item => { - const input = item.querySelector(`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`); - if (input) { - const realValue = input.hasAttribute('data-real-value') ? input.getAttribute('data-real-value').toLowerCase() : input.value.toLowerCase(); - item.style.display = realValue.includes(searchTerm) ? 'flex' : 'none'; - } - }); + keyItems.forEach((item) => { + const input = item.querySelector( + `.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}` + ); + if (input) { + const realValue = input.hasAttribute("data-real-value") + ? input.getAttribute("data-real-value").toLowerCase() + : input.value.toLowerCase(); + item.style.display = realValue.includes(searchTerm) ? "flex" : "none"; + } + }); } /** * Handles the bulk deletion of API keys based on input from the modal. */ function handleBulkDeleteApiKeys() { - const apiKeyContainer = document.getElementById('API_KEYS_container'); - if (!bulkDeleteApiKeyInput || !apiKeyContainer || !bulkDeleteApiKeyModal) return; + const apiKeyContainer = document.getElementById("API_KEYS_container"); + if (!bulkDeleteApiKeyInput || !apiKeyContainer || !bulkDeleteApiKeyModal) + return; - const bulkText = bulkDeleteApiKeyInput.value; - if (!bulkText.trim()) { - showNotification('请粘贴需要删除的 API 密钥', 'warning'); - return; + const bulkText = bulkDeleteApiKeyInput.value; + if (!bulkText.trim()) { + showNotification("请粘贴需要删除的 API 密钥", "warning"); + return; + } + + const keysToDelete = new Set(bulkText.match(API_KEY_REGEX) || []); + + if (keysToDelete.size === 0) { + showNotification("未在输入内容中提取到有效的 API 密钥格式", "warning"); + return; + } + + const keyItems = apiKeyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`); + let deleteCount = 0; + + keyItems.forEach((item) => { + const input = item.querySelector( + `.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}` + ); + const realValue = + input && + (input.hasAttribute("data-real-value") + ? input.getAttribute("data-real-value") + : input.value); + if (realValue && keysToDelete.has(realValue)) { + item.remove(); + deleteCount++; } + }); - const keysToDelete = new Set(bulkText.match(API_KEY_REGEX) || []); + closeModal(bulkDeleteApiKeyModal); - if (keysToDelete.size === 0) { - showNotification('未在输入内容中提取到有效的 API 密钥格式', 'warning'); - return; - } - - const keyItems = apiKeyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`); - let deleteCount = 0; - - keyItems.forEach(item => { - const input = item.querySelector(`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`); - const realValue = input && (input.hasAttribute('data-real-value') ? input.getAttribute('data-real-value') : input.value); - if (realValue && keysToDelete.has(realValue)) { - item.remove(); - deleteCount++; - } - }); - - closeModal(bulkDeleteApiKeyModal); - - if (deleteCount > 0) { - showNotification(`成功删除了 ${deleteCount} 个匹配的密钥`, 'success'); - } else { - showNotification('列表中未找到您输入的任何密钥进行删除', 'info'); - } - bulkDeleteApiKeyInput.value = ''; + if (deleteCount > 0) { + showNotification(`成功删除了 ${deleteCount} 个匹配的密钥`, "success"); + } else { + showNotification("列表中未找到您输入的任何密钥进行删除", "info"); + } + bulkDeleteApiKeyInput.value = ""; } /** * Handles the bulk addition of proxies from the modal input. */ function handleBulkAddProxies() { - const proxyContainer = document.getElementById('PROXIES_container'); - if (!proxyBulkInput || !proxyContainer || !proxyModal) return; + const proxyContainer = document.getElementById("PROXIES_container"); + if (!proxyBulkInput || !proxyContainer || !proxyModal) return; - const bulkText = proxyBulkInput.value; - const extractedProxies = bulkText.match(PROXY_REGEX) || []; + const bulkText = proxyBulkInput.value; + const extractedProxies = bulkText.match(PROXY_REGEX) || []; - const currentProxyInputs = proxyContainer.querySelectorAll(`.${ARRAY_INPUT_CLASS}`); - const currentProxies = Array.from(currentProxyInputs).map(input => input.value).filter(proxy => proxy.trim() !== ''); + const currentProxyInputs = proxyContainer.querySelectorAll( + `.${ARRAY_INPUT_CLASS}` + ); + const currentProxies = Array.from(currentProxyInputs) + .map((input) => input.value) + .filter((proxy) => proxy.trim() !== ""); - const combinedProxies = new Set([...currentProxies, ...extractedProxies]); - const uniqueProxies = Array.from(combinedProxies); + const combinedProxies = new Set([...currentProxies, ...extractedProxies]); + const uniqueProxies = Array.from(combinedProxies); - proxyContainer.innerHTML = ''; // Clear existing items + proxyContainer.innerHTML = ""; // Clear existing items - uniqueProxies.forEach(proxy => { - addArrayItemWithValue('PROXIES', proxy); - }); + uniqueProxies.forEach((proxy) => { + addArrayItemWithValue("PROXIES", proxy); + }); - closeModal(proxyModal); - showNotification(`添加/更新了 ${uniqueProxies.length} 个唯一代理`, 'success'); + closeModal(proxyModal); + showNotification(`添加/更新了 ${uniqueProxies.length} 个唯一代理`, "success"); } /** * Handles the bulk deletion of proxies based on input from the modal. */ function handleBulkDeleteProxies() { - const proxyContainer = document.getElementById('PROXIES_container'); - if (!bulkDeleteProxyInput || !proxyContainer || !bulkDeleteProxyModal) return; + const proxyContainer = document.getElementById("PROXIES_container"); + if (!bulkDeleteProxyInput || !proxyContainer || !bulkDeleteProxyModal) return; - const bulkText = bulkDeleteProxyInput.value; - if (!bulkText.trim()) { - showNotification('请粘贴需要删除的代理地址', 'warning'); - return; + const bulkText = bulkDeleteProxyInput.value; + if (!bulkText.trim()) { + showNotification("请粘贴需要删除的代理地址", "warning"); + return; + } + + const proxiesToDelete = new Set(bulkText.match(PROXY_REGEX) || []); + + if (proxiesToDelete.size === 0) { + showNotification("未在输入内容中提取到有效的代理地址格式", "warning"); + return; + } + + const proxyItems = proxyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`); + let deleteCount = 0; + + proxyItems.forEach((item) => { + const input = item.querySelector(`.${ARRAY_INPUT_CLASS}`); + if (input && proxiesToDelete.has(input.value)) { + item.remove(); + deleteCount++; } + }); - const proxiesToDelete = new Set(bulkText.match(PROXY_REGEX) || []); + closeModal(bulkDeleteProxyModal); - if (proxiesToDelete.size === 0) { - showNotification('未在输入内容中提取到有效的代理地址格式', 'warning'); - return; - } - - const proxyItems = proxyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`); - let deleteCount = 0; - - proxyItems.forEach(item => { - const input = item.querySelector(`.${ARRAY_INPUT_CLASS}`); - if (input && proxiesToDelete.has(input.value)) { - item.remove(); - deleteCount++; - } - }); - - closeModal(bulkDeleteProxyModal); - - if (deleteCount > 0) { - showNotification(`成功删除了 ${deleteCount} 个匹配的代理`, 'success'); - } else { - showNotification('列表中未找到您输入的任何代理进行删除', 'info'); - } - bulkDeleteProxyInput.value = ''; + if (deleteCount > 0) { + showNotification(`成功删除了 ${deleteCount} 个匹配的代理`, "success"); + } else { + showNotification("列表中未找到您输入的任何代理进行删除", "info"); + } + bulkDeleteProxyInput.value = ""; } /** @@ -755,29 +1007,39 @@ function handleBulkDeleteProxies() { * @param {string} tabId - The ID of the tab to switch to. */ function switchTab(tabId) { - // 更新标签按钮状态 - const tabButtons = document.querySelectorAll('.tab-btn'); - tabButtons.forEach(button => { - if (button.getAttribute('data-tab') === tabId) { - // 激活状态:主色背景,白色文字,添加阴影 - button.classList.remove('bg-white', 'bg-opacity-50', 'text-gray-700', 'hover:bg-opacity-70'); - button.classList.add('bg-primary-600', 'text-white', 'shadow-md'); - } else { - // 非激活状态:白色背景,灰色文字,无阴影 - button.classList.remove('bg-primary-600', 'text-white', 'shadow-md'); - button.classList.add('bg-white', 'bg-opacity-50', 'text-gray-700', 'hover:bg-opacity-70'); - } - }); + // 更新标签按钮状态 + const tabButtons = document.querySelectorAll(".tab-btn"); + tabButtons.forEach((button) => { + if (button.getAttribute("data-tab") === tabId) { + // 激活状态:主色背景,白色文字,添加阴影 + button.classList.remove( + "bg-white", + "bg-opacity-50", + "text-gray-700", + "hover:bg-opacity-70" + ); + button.classList.add("bg-primary-600", "text-white", "shadow-md"); + } else { + // 非激活状态:白色背景,灰色文字,无阴影 + button.classList.remove("bg-primary-600", "text-white", "shadow-md"); + button.classList.add( + "bg-white", + "bg-opacity-50", + "text-gray-700", + "hover:bg-opacity-70" + ); + } + }); - // 更新内容区域 - const sections = document.querySelectorAll('.config-section'); - sections.forEach(section => { - if (section.id === `${tabId}-section`) { - section.classList.add('active'); - } else { - section.classList.remove('active'); - } - }); + // 更新内容区域 + const sections = document.querySelectorAll(".config-section"); + sections.forEach((section) => { + if (section.id === `${tabId}-section`) { + section.classList.add("active"); + } else { + section.classList.remove("active"); + } + }); } /** @@ -785,14 +1047,14 @@ function switchTab(tabId) { * @param {string} provider - The selected upload provider. */ function toggleProviderConfig(provider) { - const providerConfigs = document.querySelectorAll('.provider-config'); - providerConfigs.forEach(config => { - if (config.getAttribute('data-provider') === provider) { - config.classList.add('active'); - } else { - config.classList.remove('active'); - } - }); + const providerConfigs = document.querySelectorAll(".provider-config"); + providerConfigs.forEach((config) => { + if (config.getAttribute("data-provider") === provider) { + config.classList.add("active"); + } else { + config.classList.remove("active"); + } + }); } /** @@ -804,20 +1066,20 @@ function toggleProviderConfig(provider) { * @returns {HTMLInputElement} The created input element. */ function createArrayInput(key, value, isSensitive, modelId = null) { - const input = document.createElement('input'); - input.type = 'text'; - input.name = `${key}[]`; // Used for form submission if not handled by JS - input.value = value; - let inputClasses = `${ARRAY_INPUT_CLASS} flex-grow px-3 py-2 border-none rounded-l-md focus:outline-none`; - if (isSensitive) { - inputClasses += ` ${SENSITIVE_INPUT_CLASS}`; - } - input.className = inputClasses; - if (modelId) { - input.setAttribute('data-model-id', modelId); - input.placeholder = '思考模型名称'; - } - return input; + const input = document.createElement("input"); + input.type = "text"; + input.name = `${key}[]`; // Used for form submission if not handled by JS + input.value = value; + let inputClasses = `${ARRAY_INPUT_CLASS} flex-grow px-3 py-2 border-none rounded-l-md focus:outline-none`; + if (isSensitive) { + inputClasses += ` ${SENSITIVE_INPUT_CLASS}`; + } + input.className = inputClasses; + if (modelId) { + input.setAttribute("data-model-id", modelId); + input.placeholder = "思考模型名称"; + } + return input; } /** @@ -825,13 +1087,14 @@ function createArrayInput(key, value, isSensitive, modelId = null) { * @returns {HTMLButtonElement} The created button element. */ function createGenerateTokenButton() { - const generateBtn = document.createElement('button'); - generateBtn.type = 'button'; - generateBtn.className = 'generate-btn px-2 py-2 text-gray-500 hover:text-primary-600 focus:outline-none rounded-r-md bg-gray-100 hover:bg-gray-200 transition-colors'; - generateBtn.innerHTML = ''; - generateBtn.title = '生成随机令牌'; - // Event listener will be added via delegation in DOMContentLoaded - return generateBtn; + const generateBtn = document.createElement("button"); + generateBtn.type = "button"; + generateBtn.className = + "generate-btn px-2 py-2 text-gray-500 hover:text-primary-600 focus:outline-none rounded-r-md bg-gray-100 hover:bg-gray-200 transition-colors"; + generateBtn.innerHTML = ''; + generateBtn.title = "生成随机令牌"; + // Event listener will be added via delegation in DOMContentLoaded + return generateBtn; } /** @@ -839,34 +1102,33 @@ function createGenerateTokenButton() { * @returns {HTMLButtonElement} The created button element. */ function createRemoveButton() { - const removeBtn = document.createElement('button'); - removeBtn.type = 'button'; - removeBtn.className = 'remove-btn text-gray-400 hover:text-red-500 focus:outline-none transition-colors duration-150'; - removeBtn.innerHTML = ''; - removeBtn.title = '删除'; - // Event listener will be added via delegation in DOMContentLoaded - return removeBtn; + const removeBtn = document.createElement("button"); + removeBtn.type = "button"; + removeBtn.className = + "remove-btn text-gray-400 hover:text-red-500 focus:outline-none transition-colors duration-150"; + removeBtn.innerHTML = ''; + removeBtn.title = "删除"; + // Event listener will be added via delegation in DOMContentLoaded + return removeBtn; } - /** * Adds a new item to an array configuration section (e.g., API_KEYS, ALLOWED_TOKENS). * This function is typically called by a "+" button. * @param {string} key - The configuration key for the array (e.g., 'API_KEYS'). */ function addArrayItem(key) { - const container = document.getElementById(`${key}_container`); - if (!container) return; + const container = document.getElementById(`${key}_container`); + if (!container) return; - const newItemValue = ''; // New items start empty - const modelId = addArrayItemWithValue(key, newItemValue); // This adds the DOM element + const newItemValue = ""; // New items start empty + const modelId = addArrayItemWithValue(key, newItemValue); // This adds the DOM element - if (key === 'THINKING_MODELS' && modelId) { - createAndAppendBudgetMapItem(newItemValue, 0, modelId); // Default budget 0 - } + if (key === "THINKING_MODELS" && modelId) { + createAndAppendBudgetMapItem(newItemValue, 0, modelId); // Default budget 0 + } } - /** * Adds an array item with a specific value to the DOM. * This is used both for initially populating the form and for adding new items. @@ -875,51 +1137,59 @@ function addArrayItem(key) { * @returns {string|null} The generated modelId if it's a thinking model, otherwise null. */ function addArrayItemWithValue(key, value) { - const container = document.getElementById(`${key}_container`); - if (!container) return null; + const container = document.getElementById(`${key}_container`); + if (!container) return null; - const isThinkingModel = key === 'THINKING_MODELS'; - const isAllowedToken = key === 'ALLOWED_TOKENS'; - const isSensitive = key === 'API_KEYS' || isAllowedToken; - const modelId = isThinkingModel ? generateUUID() : null; + const isThinkingModel = key === "THINKING_MODELS"; + const isAllowedToken = key === "ALLOWED_TOKENS"; + const isSensitive = key === "API_KEYS" || isAllowedToken; + const modelId = isThinkingModel ? generateUUID() : null; - const arrayItem = document.createElement('div'); - arrayItem.className = `${ARRAY_ITEM_CLASS} flex items-center mb-2 gap-2`; - if (isThinkingModel) { - arrayItem.setAttribute('data-model-id', modelId); + const arrayItem = document.createElement("div"); + arrayItem.className = `${ARRAY_ITEM_CLASS} flex items-center mb-2 gap-2`; + if (isThinkingModel) { + arrayItem.setAttribute("data-model-id", modelId); + } + + const inputWrapper = document.createElement("div"); + inputWrapper.className = + "flex items-center flex-grow border border-gray-300 rounded-md focus-within:border-primary-500 focus-within:ring focus-within:ring-primary-200 focus-within:ring-opacity-50"; + + const input = createArrayInput( + key, + value, + isSensitive, + isThinkingModel ? modelId : null + ); + inputWrapper.appendChild(input); + + if (isAllowedToken) { + const generateBtn = createGenerateTokenButton(); + inputWrapper.appendChild(generateBtn); + } else { + // Ensure right-side rounding if no button is present + input.classList.add("rounded-r-md"); + } + + const removeBtn = createRemoveButton(); + + arrayItem.appendChild(inputWrapper); + arrayItem.appendChild(removeBtn); + container.appendChild(arrayItem); + + // Initialize sensitive field if applicable + if (isSensitive && input.value) { + if (configForm && typeof initializeSensitiveFields === "function") { + const focusoutEvent = new Event("focusout", { + bubbles: true, + cancelable: true, + }); + input.dispatchEvent(focusoutEvent); } - - const inputWrapper = document.createElement('div'); - inputWrapper.className = 'flex items-center flex-grow border border-gray-300 rounded-md focus-within:border-primary-500 focus-within:ring focus-within:ring-primary-200 focus-within:ring-opacity-50'; - - const input = createArrayInput(key, value, isSensitive, isThinkingModel ? modelId : null); - inputWrapper.appendChild(input); - - if (isAllowedToken) { - const generateBtn = createGenerateTokenButton(); - inputWrapper.appendChild(generateBtn); - } else { - // Ensure right-side rounding if no button is present - input.classList.add('rounded-r-md'); - } - - const removeBtn = createRemoveButton(); - - arrayItem.appendChild(inputWrapper); - arrayItem.appendChild(removeBtn); - container.appendChild(arrayItem); - - // Initialize sensitive field if applicable - if (isSensitive && input.value) { - if (configForm && typeof initializeSensitiveFields === 'function') { - const focusoutEvent = new Event('focusout', { bubbles: true, cancelable: true }); - input.dispatchEvent(focusoutEvent); - } - } - return isThinkingModel ? modelId : null; + } + return isThinkingModel ? modelId : null; } - /** * Creates and appends a DOM element for a thinking model's budget mapping. * @param {string} mapKey - The model name (key for the map). @@ -927,62 +1197,68 @@ function addArrayItemWithValue(key, value) { * @param {string} modelId - The unique ID of the corresponding thinking model. */ function createAndAppendBudgetMapItem(mapKey, mapValue, modelId) { - const container = document.getElementById('THINKING_BUDGET_MAP_container'); - if (!container) { - console.error("Cannot add budget item: THINKING_BUDGET_MAP_container not found!"); - return; - } + const container = document.getElementById("THINKING_BUDGET_MAP_container"); + if (!container) { + console.error( + "Cannot add budget item: THINKING_BUDGET_MAP_container not found!" + ); + return; + } - // If container currently only has the placeholder, clear it - const placeholder = container.querySelector('.text-gray-500.italic'); - // Check if the only child is the placeholder before clearing - if (placeholder && container.children.length === 1 && container.firstChild === placeholder) { - container.innerHTML = ''; - } + // If container currently only has the placeholder, clear it + const placeholder = container.querySelector(".text-gray-500.italic"); + // Check if the only child is the placeholder before clearing + if ( + placeholder && + container.children.length === 1 && + container.firstChild === placeholder + ) { + container.innerHTML = ""; + } - const mapItem = document.createElement('div'); - mapItem.className = `${MAP_ITEM_CLASS} flex items-center mb-2 gap-2`; - mapItem.setAttribute('data-model-id', modelId); + const mapItem = document.createElement("div"); + mapItem.className = `${MAP_ITEM_CLASS} flex items-center mb-2 gap-2`; + mapItem.setAttribute("data-model-id", modelId); - const keyInput = document.createElement('input'); - keyInput.type = 'text'; - keyInput.value = mapKey; - keyInput.placeholder = '模型名称 (自动关联)'; - keyInput.readOnly = true; - keyInput.className = `${MAP_KEY_INPUT_CLASS} flex-grow px-3 py-2 border border-gray-300 rounded-md focus:outline-none bg-gray-100 text-gray-500`; - keyInput.setAttribute('data-model-id', modelId); + const keyInput = document.createElement("input"); + keyInput.type = "text"; + keyInput.value = mapKey; + keyInput.placeholder = "模型名称 (自动关联)"; + keyInput.readOnly = true; + keyInput.className = `${MAP_KEY_INPUT_CLASS} flex-grow px-3 py-2 border border-gray-300 rounded-md focus:outline-none bg-gray-100 text-gray-500`; + keyInput.setAttribute("data-model-id", modelId); - const valueInput = document.createElement('input'); - valueInput.type = 'number'; - const intValue = parseInt(mapValue, 10); - valueInput.value = isNaN(intValue) ? 0 : intValue; - valueInput.placeholder = '预算 (整数)'; - valueInput.className = `${MAP_VALUE_INPUT_CLASS} w-24 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50`; - valueInput.min = 0; - valueInput.max = 24576; - valueInput.addEventListener('input', function() { - let val = this.value.replace(/[^0-9]/g, ''); - if (val !== '') { - val = parseInt(val, 10); - if (val < 0) val = 0; - if (val > 24576) val = 24576; - } - this.value = val; // Corrected variable name - }); + const valueInput = document.createElement("input"); + valueInput.type = "number"; + const intValue = parseInt(mapValue, 10); + valueInput.value = isNaN(intValue) ? 0 : intValue; + valueInput.placeholder = "预算 (整数)"; + valueInput.className = `${MAP_VALUE_INPUT_CLASS} w-24 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50`; + valueInput.min = 0; + valueInput.max = 24576; + valueInput.addEventListener("input", function () { + let val = this.value.replace(/[^0-9]/g, ""); + if (val !== "") { + val = parseInt(val, 10); + if (val < 0) val = 0; + if (val > 24576) val = 24576; + } + this.value = val; // Corrected variable name + }); - // Remove Button - Removed for budget map items - // const removeBtn = document.createElement('button'); - // removeBtn.type = 'button'; - // removeBtn.className = 'remove-btn text-gray-300 cursor-not-allowed focus:outline-none'; // Kept original class for reference - // removeBtn.innerHTML = ''; - // removeBtn.title = '请从上方模型列表删除'; - // removeBtn.disabled = true; + // Remove Button - Removed for budget map items + // const removeBtn = document.createElement('button'); + // removeBtn.type = 'button'; + // removeBtn.className = 'remove-btn text-gray-300 cursor-not-allowed focus:outline-none'; // Kept original class for reference + // removeBtn.innerHTML = ''; + // removeBtn.title = '请从上方模型列表删除'; + // removeBtn.disabled = true; - mapItem.appendChild(keyInput); - mapItem.appendChild(valueInput); - // mapItem.appendChild(removeBtn); // Do not append the remove button + mapItem.appendChild(keyInput); + mapItem.appendChild(valueInput); + // mapItem.appendChild(removeBtn); // Do not append the remove button - container.appendChild(mapItem); + container.appendChild(mapItem); } /** @@ -990,145 +1266,220 @@ function createAndAppendBudgetMapItem(mapKey, mapValue, modelId) { * @returns {object} An object containing all configuration data. */ function collectFormData() { - const formData = {}; + const formData = {}; - // 处理普通输入和 select - const inputsAndSelects = document.querySelectorAll('input[type="text"], input[type="number"], input[type="password"], select, textarea'); - inputsAndSelects.forEach(element => { - if (element.name && !element.name.includes('[]') && !element.closest('.array-container') && !element.closest(`.${MAP_ITEM_CLASS}`) && !element.closest(`.${SAFETY_SETTING_ITEM_CLASS}`)) { - if (element.type === 'number') { - formData[element.name] = parseFloat(element.value); - } else if (element.classList.contains(SENSITIVE_INPUT_CLASS) && element.hasAttribute('data-real-value')) { - formData[element.name] = element.getAttribute('data-real-value'); - } else { - formData[element.name] = element.value; - } + // 处理普通输入和 select + const inputsAndSelects = document.querySelectorAll( + 'input[type="text"], input[type="number"], input[type="password"], select, textarea' + ); + inputsAndSelects.forEach((element) => { + if ( + element.name && + !element.name.includes("[]") && + !element.closest(".array-container") && + !element.closest(`.${MAP_ITEM_CLASS}`) && + !element.closest(`.${SAFETY_SETTING_ITEM_CLASS}`) + ) { + if (element.type === "number") { + formData[element.name] = parseFloat(element.value); + } else if ( + element.classList.contains(SENSITIVE_INPUT_CLASS) && + element.hasAttribute("data-real-value") + ) { + formData[element.name] = element.getAttribute("data-real-value"); + } else { + formData[element.name] = element.value; + } + } + }); + + const checkboxes = document.querySelectorAll('input[type="checkbox"]'); + checkboxes.forEach((checkbox) => { + formData[checkbox.name] = checkbox.checked; + }); + + const arrayContainers = document.querySelectorAll(".array-container"); + arrayContainers.forEach((container) => { + const key = container.id.replace("_container", ""); + const arrayInputs = container.querySelectorAll(`.${ARRAY_INPUT_CLASS}`); + formData[key] = Array.from(arrayInputs) + .map((input) => { + if ( + input.classList.contains(SENSITIVE_INPUT_CLASS) && + input.hasAttribute("data-real-value") + ) { + return input.getAttribute("data-real-value"); } - }); + return input.value; + }) + .filter( + (value) => value && value.trim() !== "" && value !== MASKED_VALUE + ); // Ensure MASKED_VALUE is also filtered if not handled + }); - const checkboxes = document.querySelectorAll('input[type="checkbox"]'); - checkboxes.forEach(checkbox => { - formData[checkbox.name] = checkbox.checked; + const budgetMapContainer = document.getElementById( + "THINKING_BUDGET_MAP_container" + ); + if (budgetMapContainer) { + formData["THINKING_BUDGET_MAP"] = {}; + const mapItems = budgetMapContainer.querySelectorAll(`.${MAP_ITEM_CLASS}`); + mapItems.forEach((item) => { + const keyInput = item.querySelector(`.${MAP_KEY_INPUT_CLASS}`); + const valueInput = item.querySelector(`.${MAP_VALUE_INPUT_CLASS}`); + if (keyInput && valueInput && keyInput.value.trim() !== "") { + const budgetValue = parseInt(valueInput.value, 10); + formData["THINKING_BUDGET_MAP"][keyInput.value.trim()] = isNaN( + budgetValue + ) + ? 0 + : budgetValue; + } }); + } - const arrayContainers = document.querySelectorAll('.array-container'); - arrayContainers.forEach(container => { - const key = container.id.replace('_container', ''); - const arrayInputs = container.querySelectorAll(`.${ARRAY_INPUT_CLASS}`); - formData[key] = Array.from(arrayInputs).map(input => { - if (input.classList.contains(SENSITIVE_INPUT_CLASS) && input.hasAttribute('data-real-value')) { - return input.getAttribute('data-real-value'); - } - return input.value; - }).filter(value => value && value.trim() !== '' && value !== MASKED_VALUE); // Ensure MASKED_VALUE is also filtered if not handled - }); - - const budgetMapContainer = document.getElementById('THINKING_BUDGET_MAP_container'); - if (budgetMapContainer) { - formData['THINKING_BUDGET_MAP'] = {}; - const mapItems = budgetMapContainer.querySelectorAll(`.${MAP_ITEM_CLASS}`); - mapItems.forEach(item => { - const keyInput = item.querySelector(`.${MAP_KEY_INPUT_CLASS}`); - const valueInput = item.querySelector(`.${MAP_VALUE_INPUT_CLASS}`); - if (keyInput && valueInput && keyInput.value.trim() !== '') { - const budgetValue = parseInt(valueInput.value, 10); - formData['THINKING_BUDGET_MAP'][keyInput.value.trim()] = isNaN(budgetValue) ? 0 : budgetValue; - } + if (safetySettingsContainer) { + formData["SAFETY_SETTINGS"] = []; + const settingItems = safetySettingsContainer.querySelectorAll( + `.${SAFETY_SETTING_ITEM_CLASS}` + ); + settingItems.forEach((item) => { + const categorySelect = item.querySelector(".safety-category-select"); + const thresholdSelect = item.querySelector(".safety-threshold-select"); + if ( + categorySelect && + thresholdSelect && + categorySelect.value && + thresholdSelect.value + ) { + formData["SAFETY_SETTINGS"].push({ + category: categorySelect.value, + threshold: thresholdSelect.value, }); - } + } + }); + } - if (safetySettingsContainer) { - formData['SAFETY_SETTINGS'] = []; - const settingItems = safetySettingsContainer.querySelectorAll(`.${SAFETY_SETTING_ITEM_CLASS}`); - settingItems.forEach(item => { - const categorySelect = item.querySelector('.safety-category-select'); - const thresholdSelect = item.querySelector('.safety-threshold-select'); - if (categorySelect && thresholdSelect && categorySelect.value && thresholdSelect.value) { - formData['SAFETY_SETTINGS'].push({ - category: categorySelect.value, - threshold: thresholdSelect.value - }); - } - }); - } + // --- 新增:收集自动删除错误日志的配置 --- + const autoDeleteEnabledCheckbox = document.getElementById( + "AUTO_DELETE_ERROR_LOGS_ENABLED" + ); + if (autoDeleteEnabledCheckbox) { + formData["AUTO_DELETE_ERROR_LOGS_ENABLED"] = + autoDeleteEnabledCheckbox.checked; + } - return formData; + const autoDeleteDaysSelect = document.getElementById( + "AUTO_DELETE_ERROR_LOGS_DAYS" + ); + if (autoDeleteDaysSelect) { + // 如果复选框未选中,则不应提交天数,或者可以提交一个默认/无效值, + // 但后端应该只在 ENABLED 为 true 时才关心 DAYS。 + // 这里我们总是收集它,后端逻辑会处理。 + formData["AUTO_DELETE_ERROR_LOGS_DAYS"] = parseInt( + autoDeleteDaysSelect.value, + 10 + ); + } + // --- 结束:收集自动删除错误日志的配置 --- + + // --- 新增:收集自动删除请求日志的配置 --- + const autoDeleteRequestEnabledCheckbox = document.getElementById( + "AUTO_DELETE_REQUEST_LOGS_ENABLED" + ); + if (autoDeleteRequestEnabledCheckbox) { + formData["AUTO_DELETE_REQUEST_LOGS_ENABLED"] = + autoDeleteRequestEnabledCheckbox.checked; + } + + const autoDeleteRequestDaysSelect = document.getElementById( + "AUTO_DELETE_REQUEST_LOGS_DAYS" + ); + if (autoDeleteRequestDaysSelect) { + formData["AUTO_DELETE_REQUEST_LOGS_DAYS"] = parseInt( + autoDeleteRequestDaysSelect.value, + 10 + ); + } + // --- 结束:收集自动删除请求日志的配置 --- + + return formData; } /** * Stops the scheduler task on the server. */ async function stopScheduler() { - try { - const response = await fetch('/api/scheduler/stop', { method: 'POST' }); - if (!response.ok) { - console.warn(`停止定时任务失败: ${response.status}`); - } else { - console.log('定时任务已停止'); - } - } catch (error) { - console.error('调用停止定时任务API时出错:', error); + try { + const response = await fetch("/api/scheduler/stop", { method: "POST" }); + if (!response.ok) { + console.warn(`停止定时任务失败: ${response.status}`); + } else { + console.log("定时任务已停止"); } + } catch (error) { + console.error("调用停止定时任务API时出错:", error); + } } /** * Starts the scheduler task on the server. */ async function startScheduler() { - try { - const response = await fetch('/api/scheduler/start', { method: 'POST' }); - if (!response.ok) { - console.warn(`启动定时任务失败: ${response.status}`); - } else { - console.log('定时任务已启动'); - } - } catch (error) { - console.error('调用启动定时任务API时出错:', error); + try { + const response = await fetch("/api/scheduler/start", { method: "POST" }); + if (!response.ok) { + console.warn(`启动定时任务失败: ${response.status}`); + } else { + console.log("定时任务已启动"); } + } catch (error) { + console.error("调用启动定时任务API时出错:", error); + } } /** * Saves the current configuration to the server. */ async function saveConfig() { - try { - const formData = collectFormData(); + try { + const formData = collectFormData(); - showNotification('正在保存配置...', 'info'); + showNotification("正在保存配置...", "info"); - // 1. 停止定时任务 - await stopScheduler(); + // 1. 停止定时任务 + await stopScheduler(); - const response = await fetch('/api/config', { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(formData) - }); + const response = await fetch("/api/config", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.detail || `HTTP error! status: ${response.status}`); - } - - const result = await response.json(); - - // 移除居中的 saveStatus 提示 - - showNotification('配置保存成功', 'success'); - - // 3. 启动新的定时任务 - await startScheduler(); - - } catch (error) { - console.error('保存配置失败:', error); - // 保存失败时,也尝试重启定时任务,以防万一 - await startScheduler(); - // 移除居中的 saveStatus 提示 - - showNotification('保存配置失败: ' + error.message, 'error'); + if (!response.ok) { + const errorData = await response.json(); + throw new Error( + errorData.detail || `HTTP error! status: ${response.status}` + ); } + + const result = await response.json(); + + // 移除居中的 saveStatus 提示 + + showNotification("配置保存成功", "success"); + + // 3. 启动新的定时任务 + await startScheduler(); + } catch (error) { + console.error("保存配置失败:", error); + // 保存失败时,也尝试重启定时任务,以防万一 + await startScheduler(); + // 移除居中的 saveStatus 提示 + + showNotification("保存配置失败: " + error.message, "error"); + } } /** @@ -1136,65 +1487,81 @@ async function saveConfig() { * @param {Event} [event] - The click event, if triggered by a button. */ function resetConfig(event) { - // 阻止事件冒泡和默认行为 - if (event) { - event.preventDefault(); - event.stopPropagation(); - } + // 阻止事件冒泡和默认行为 + if (event) { + event.preventDefault(); + event.stopPropagation(); + } - console.log('resetConfig called. Event target:', event ? event.target.id : 'No event'); + console.log( + "resetConfig called. Event target:", + event ? event.target.id : "No event" + ); - // Ensure modal is shown only if the event comes from the reset button - if (!event || event.target.id === 'resetBtn' || (event.currentTarget && event.currentTarget.id === 'resetBtn')) { - if (resetConfirmModal) { - openModal(resetConfirmModal); - } else { - console.error("Reset confirmation modal not found! Falling back to default confirm."); - if (confirm('确定要重置所有配置吗?这将恢复到默认值。')) { - executeReset(); - } - } + // Ensure modal is shown only if the event comes from the reset button + if ( + !event || + event.target.id === "resetBtn" || + (event.currentTarget && event.currentTarget.id === "resetBtn") + ) { + if (resetConfirmModal) { + openModal(resetConfirmModal); + } else { + console.error( + "Reset confirmation modal not found! Falling back to default confirm." + ); + if (confirm("确定要重置所有配置吗?这将恢复到默认值。")) { + executeReset(); + } } + } } /** * Executes the actual configuration reset after confirmation. */ async function executeReset() { - try { - showNotification('正在重置配置...', 'info'); + try { + showNotification("正在重置配置...", "info"); - // 1. 停止定时任务 - await stopScheduler(); - const response = await fetch('/api/config/reset', { method: 'POST' }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const config = await response.json(); - populateForm(config); - // Re-initialize masking for sensitive fields after reset - if (configForm && typeof initializeSensitiveFields === 'function') { - const sensitiveFields = configForm.querySelectorAll(`.${SENSITIVE_INPUT_CLASS}`); - sensitiveFields.forEach(field => { - if (field.type === 'password') { - if (field.value) field.setAttribute('data-real-value', field.value); - } else if (field.type === 'text' || field.tagName.toLowerCase() === 'textarea') { - const focusoutEvent = new Event('focusout', { bubbles: true, cancelable: true }); - field.dispatchEvent(focusoutEvent); - } - }); - } - showNotification('配置已重置为默认值', 'success'); - - // 3. 启动新的定时任务 - await startScheduler(); - - } catch (error) { - console.error('重置配置失败:', error); - showNotification('重置配置失败: ' + error.message, 'error'); - // 重置失败时,也尝试重启定时任务 - await startScheduler(); + // 1. 停止定时任务 + await stopScheduler(); + const response = await fetch("/api/config/reset", { method: "POST" }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); } + const config = await response.json(); + populateForm(config); + // Re-initialize masking for sensitive fields after reset + if (configForm && typeof initializeSensitiveFields === "function") { + const sensitiveFields = configForm.querySelectorAll( + `.${SENSITIVE_INPUT_CLASS}` + ); + sensitiveFields.forEach((field) => { + if (field.type === "password") { + if (field.value) field.setAttribute("data-real-value", field.value); + } else if ( + field.type === "text" || + field.tagName.toLowerCase() === "textarea" + ) { + const focusoutEvent = new Event("focusout", { + bubbles: true, + cancelable: true, + }); + field.dispatchEvent(focusoutEvent); + } + }); + } + showNotification("配置已重置为默认值", "success"); + + // 3. 启动新的定时任务 + await startScheduler(); + } catch (error) { + console.error("重置配置失败:", error); + showNotification("重置配置失败: " + error.message, "error"); + // 重置失败时,也尝试重启定时任务 + await startScheduler(); + } } /** @@ -1202,25 +1569,25 @@ async function executeReset() { * @param {string} message - The message to display. * @param {string} [type='info'] - The type of notification ('info', 'success', 'error', 'warning'). */ -function showNotification(message, type = 'info') { - const notification = document.getElementById('notification'); - notification.textContent = message; +function showNotification(message, type = "info") { + const notification = document.getElementById("notification"); + notification.textContent = message; - // 统一样式为黑色半透明,与 keys_status.js 保持一致 - notification.classList.remove('bg-danger-500'); - notification.classList.add('bg-black'); - notification.style.backgroundColor = 'rgba(0,0,0,0.8)'; - notification.style.color = '#fff'; + // 统一样式为黑色半透明,与 keys_status.js 保持一致 + notification.classList.remove("bg-danger-500"); + notification.classList.add("bg-black"); + notification.style.backgroundColor = "rgba(0,0,0,0.8)"; + notification.style.color = "#fff"; - // 应用过渡效果 - notification.style.opacity = "1"; - notification.style.transform = "translate(-50%, 0)"; + // 应用过渡效果 + notification.style.opacity = "1"; + notification.style.transform = "translate(-50%, 0)"; - // 设置自动消失 - setTimeout(() => { - notification.style.opacity = "0"; - notification.style.transform = "translate(-50%, 10px)"; - }, 3000); + // 设置自动消失 + setTimeout(() => { + notification.style.opacity = "0"; + notification.style.transform = "translate(-50%, 10px)"; + }, 3000); } /** @@ -1228,32 +1595,32 @@ function showNotification(message, type = 'info') { * @param {HTMLButtonElement} [button] - The button that triggered the refresh (to show loading state). */ function refreshPage(button) { - if (button) button.classList.add('loading'); - location.reload(); + if (button) button.classList.add("loading"); + location.reload(); } /** * Scrolls the page to the top. */ function scrollToTop() { - window.scrollTo({ top: 0, behavior: 'smooth' }); + window.scrollTo({ top: 0, behavior: "smooth" }); } /** * Scrolls the page to the bottom. */ function scrollToBottom() { - window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); + window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }); } /** * Toggles the visibility of scroll-to-top/bottom buttons based on scroll position. */ function toggleScrollButtons() { - const scrollButtons = document.querySelector('.scroll-buttons'); - if (scrollButtons) { - scrollButtons.style.display = (window.scrollY > 200) ? 'flex' : 'none'; - } + const scrollButtons = document.querySelector(".scroll-buttons"); + if (scrollButtons) { + scrollButtons.style.display = window.scrollY > 200 ? "flex" : "none"; + } } /** @@ -1261,13 +1628,14 @@ function toggleScrollButtons() { * @returns {string} A randomly generated token. */ function generateRandomToken() { - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_'; - const length = 48; - let result = 'sk-'; - for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * characters.length)); - } - return result; + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"; + const length = 48; + let result = "sk-"; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return result; } /** @@ -1275,67 +1643,76 @@ function generateRandomToken() { * @param {string} [category=''] - The initial category for the setting. * @param {string} [threshold=''] - The initial threshold for the setting. */ -function addSafetySettingItem(category = '', threshold = '') { - const container = document.getElementById('SAFETY_SETTINGS_container'); - if (!container) { - console.error("Cannot add safety setting: SAFETY_SETTINGS_container not found!"); - return; - } +function addSafetySettingItem(category = "", threshold = "") { + const container = document.getElementById("SAFETY_SETTINGS_container"); + if (!container) { + console.error( + "Cannot add safety setting: SAFETY_SETTINGS_container not found!" + ); + return; + } - // 如果容器当前只有占位符,则清除它 - const placeholder = container.querySelector('.text-gray-500.italic'); - if (placeholder && container.children.length === 1 && container.firstChild === placeholder) { - container.innerHTML = ''; - } + // 如果容器当前只有占位符,则清除它 + const placeholder = container.querySelector(".text-gray-500.italic"); + if ( + placeholder && + container.children.length === 1 && + container.firstChild === placeholder + ) { + container.innerHTML = ""; + } - const harmCategories = [ - "HARM_CATEGORY_HARASSMENT", - "HARM_CATEGORY_HATE_SPEECH", - "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "HARM_CATEGORY_DANGEROUS_CONTENT", - "HARM_CATEGORY_CIVIC_INTEGRITY" // 根据需要添加或移除 - ]; - const harmThresholds = [ - "BLOCK_NONE", - "BLOCK_LOW_AND_ABOVE", - "BLOCK_MEDIUM_AND_ABOVE", - "BLOCK_ONLY_HIGH", - "OFF" // 根据 Google API 文档添加或移除 - ]; + const harmCategories = [ + "HARM_CATEGORY_HARASSMENT", + "HARM_CATEGORY_HATE_SPEECH", + "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "HARM_CATEGORY_DANGEROUS_CONTENT", + "HARM_CATEGORY_CIVIC_INTEGRITY", // 根据需要添加或移除 + ]; + const harmThresholds = [ + "BLOCK_NONE", + "BLOCK_LOW_AND_ABOVE", + "BLOCK_MEDIUM_AND_ABOVE", + "BLOCK_ONLY_HIGH", + "OFF", // 根据 Google API 文档添加或移除 + ]; - const settingItem = document.createElement('div'); - settingItem.className = `${SAFETY_SETTING_ITEM_CLASS} flex items-center mb-2 gap-2`; + const settingItem = document.createElement("div"); + settingItem.className = `${SAFETY_SETTING_ITEM_CLASS} flex items-center mb-2 gap-2`; - const categorySelect = document.createElement('select'); - categorySelect.className = 'safety-category-select flex-grow px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 bg-white'; - harmCategories.forEach(cat => { - const option = document.createElement('option'); - option.value = cat; - option.textContent = cat.replace('HARM_CATEGORY_', ''); - if (cat === category) option.selected = true; - categorySelect.appendChild(option); - }); + const categorySelect = document.createElement("select"); + categorySelect.className = + "safety-category-select flex-grow px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 bg-white"; + harmCategories.forEach((cat) => { + const option = document.createElement("option"); + option.value = cat; + option.textContent = cat.replace("HARM_CATEGORY_", ""); + if (cat === category) option.selected = true; + categorySelect.appendChild(option); + }); - const thresholdSelect = document.createElement('select'); - thresholdSelect.className = 'safety-threshold-select w-48 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 bg-white'; - harmThresholds.forEach(thr => { - const option = document.createElement('option'); - option.value = thr; - option.textContent = thr.replace('BLOCK_', '').replace('_AND_ABOVE', '+'); - if (thr === threshold) option.selected = true; - thresholdSelect.appendChild(option); - }); + const thresholdSelect = document.createElement("select"); + thresholdSelect.className = + "safety-threshold-select w-48 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 bg-white"; + harmThresholds.forEach((thr) => { + const option = document.createElement("option"); + option.value = thr; + option.textContent = thr.replace("BLOCK_", "").replace("_AND_ABOVE", "+"); + if (thr === threshold) option.selected = true; + thresholdSelect.appendChild(option); + }); - const removeBtn = document.createElement('button'); - removeBtn.type = 'button'; - removeBtn.className = 'remove-btn text-gray-400 hover:text-red-500 focus:outline-none transition-colors duration-150'; - removeBtn.innerHTML = ''; - removeBtn.title = '删除此设置'; - // Event listener for removeBtn is now handled by event delegation in DOMContentLoaded + const removeBtn = document.createElement("button"); + removeBtn.type = "button"; + removeBtn.className = + "remove-btn text-gray-400 hover:text-red-500 focus:outline-none transition-colors duration-150"; + removeBtn.innerHTML = ''; + removeBtn.title = "删除此设置"; + // Event listener for removeBtn is now handled by event delegation in DOMContentLoaded - settingItem.appendChild(categorySelect); - settingItem.appendChild(thresholdSelect); - settingItem.appendChild(removeBtn); + settingItem.appendChild(categorySelect); + settingItem.appendChild(thresholdSelect); + settingItem.appendChild(removeBtn); - container.appendChild(settingItem); + container.appendChild(settingItem); } diff --git a/app/templates/config_editor.html b/app/templates/config_editor.html index 2d543e8..c235cb0 100644 --- a/app/templates/config_editor.html +++ b/app/templates/config_editor.html @@ -1,604 +1,1292 @@ -{% extends "base.html" %} - -{% block title %}配置编辑器 - Gemini Balance{% endblock %} - -{% block head_extra_styles %} +{% extends "base.html" %} {% block title %}配置编辑器 - Gemini Balance{% +endblock %} {% block head_extra_styles %} -{% endblock %} +{% endblock %} {% block content %} +
+
+ -{% block content %} -
-
- + + + + + +
+ + + + +
+ +
+

+ API相关配置 +

+ + +
+ +
+ +
+
+ +
+
+ - -

- Gemini Balance Logo - Gemini Balance - 配置编辑 -

- - - - - -
- - - - - - -
- - - - - - -
-

- API相关配置 -

- - -
- -
- -
-
- -
-
- - -
- Gemini API密钥列表,每行一个 -
- - -
- -
- -
-
- -
- 允许访问API的令牌列表 -
- - -
- -
-
- - -
-
- 用于API认证的令牌 -
- - -
- - - Gemini API的基础URL -
- - -
- - - API密钥失败后标记为无效的次数 -
- - -
- - - API请求的超时时间 -
- - -
- - - API请求失败后的最大重试次数 -
- -
- -
- -
-
- - -
- 代理服务器列表,支持 http 和 socks5 格式,例如: http://user:pass@host:port 或 socks5://host:port。点击按钮可批量添加或删除。 -
-
- - -
-

- 模型相关配置 -

- - -
- - - 用于测试API密钥的模型 -
- - -
- -
- -
-
- -
- 支持图像处理的模型列表 -
- - -
- -
- -
-
- -
- 支持搜索功能的模型列表 -
- - -
- -
- -
-
- -
- 需要过滤的模型列表 -
- - -
- -
- - -
-
- - -
- -
- - -
-
- - -
- -
- - -
-
+ +
+ Gemini API密钥列表,每行一个 +
- -
- -
- -
-
- -
- 用于“思考过程”的模型列表 -
+ +
+ +
+ +
+
+ +
+ 允许访问API的令牌列表 +
- -
- -
- -
请先在上方添加思考模型,然后在此处配置预算。
-
- - +
+ +
+
+ + +
+
+ 用于API认证的令牌 +
+ + +
+ + + Gemini API的基础URL +
+ + +
+ + + API密钥失败后标记为无效的次数 +
+ + +
+ + + API请求的超时时间 +
+ + +
+ + + API请求失败后的最大重试次数 +
+ +
+ +
+ +
+
+ + +
+ 代理服务器列表,支持 http 和 socks5 格式,例如: + http://user:pass@host:port 或 + socks5://host:port。点击按钮可批量添加或删除。 +
+
+ + +
+

+ 模型相关配置 +

+ + +
+ + + 用于测试API密钥的模型 +
+ + +
+ +
+ +
+
+ +
+ 支持图像处理的模型列表 +
+ + +
+ +
+ +
+
+ +
+ 支持搜索功能的模型列表 +
+ + +
+ +
+ +
+
+ +
+ 需要过滤的模型列表 +
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ +
+
+ +
+ 用于"思考过程"的模型列表 +
+ + +
+ +
+ +
+ 请先在上方添加思考模型,然后在此处配置预算。 +
+
+ + - 为每个思考模型设置预算(整数,最大值 24576),此项与上方模型列表自动关联。 -
- -
- -
- -
定义模型的安全过滤阈值。
-
-
- -
- 配置模型的安全过滤级别,例如 HARM_CATEGORY_HARASSMENT: BLOCK_NONE。 - 建议设置成OFF,其他值会影响输出速度,非必要不要随便改动。 -
-
- - -
-

- 图像生成配置 -

- - -
- - - 用于图像生成的付费API密钥 -
- - -
- - - 用于图像生成的模型 -
- - -
- - - 图片上传服务提供商 -
- - -
- - - SM.MS图床的密钥 -
- - -
- - - PicGo的API密钥 -
- - -
- - - Cloudflare图床的URL -
- - -
- - - Cloudflare图床的认证码 -
-
- - -
-

- 流式输出优化器 -

- - -
- -
- - -
-
- - -
- - - 流式输出的最小延迟时间 -
- - -
- - - 流式输出的最大延迟时间 -
- - -
- - - 短文本的字符阈值 -
- - -
- - - 长文本的字符阈值 -
- - -
- - - 流式输出的分块大小 -
-
- - -
-

- 定时任务配置 -

- - -
- - - 定时检查密钥状态的间隔时间(单位:小时) -
- - -
- - - 定时任务使用的时区,格式如 "Asia/Shanghai" 或 "UTC" -
-
- - -
-

- 日志配置 -

- - -
- - - 设置应用程序的日志记录详细程度 -
-
- - -
- - -
- + 为每个思考模型设置预算(整数,最大值 + 24576),此项与上方模型列表自动关联。
-
- - -
- - -
- - -
- - - -