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 %}
-
-
-