diff --git a/.env.example b/.env.example index 7a63227..1b9543a 100644 --- a/.env.example +++ b/.env.example @@ -67,6 +67,8 @@ STREAM_CHUNK_SIZE=5 ######################### 日志配置 ####################################### # 日志级别 (debug, info, warning, error, critical),默认为 info LOG_LEVEL=info +# 是否记录错误日志的请求体(可能包含敏感信息),默认 false +ERROR_LOG_RECORD_REQUEST_BODY=false # 是否开启自动删除错误日志 AUTO_DELETE_ERROR_LOGS_ENABLED=true # 自动删除多少天前的错误日志 (1, 7, 30) diff --git a/README.md b/README.md index f076250..a3d144a 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,7 @@ This endpoint is directly forwarded to official OpenAI Compatible API format end | `PROXIES` | List of proxy servers | `[]` | | **Logging & Security** | | | | `LOG_LEVEL` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR` | `INFO` | +| `ERROR_LOG_RECORD_REQUEST_BODY` | Record request body in error logs (may contain sensitive information) | `false` | | `AUTO_DELETE_ERROR_LOGS_ENABLED` | Auto-delete error logs | `true` | | `AUTO_DELETE_ERROR_LOGS_DAYS` | Error log retention period (days) | `7` | | `AUTO_DELETE_REQUEST_LOGS_ENABLED`| Auto-delete request logs | `false` | @@ -217,7 +218,13 @@ This endpoint is directly forwarded to official OpenAI Compatible API format end | **Image Generation** | | | | `PAID_KEY` | Paid API Key for advanced features | `your-paid-api-key` | | `CREATE_IMAGE_MODEL` | Image generation model | `imagen-3.0-generate-002` | -| `UPLOAD_PROVIDER` | Image upload provider: `smms`, `picgo`, `cloudflare_imgbed` | `smms` | +| `UPLOAD_PROVIDER` | Image upload provider: `smms`, `picgo`, `cloudflare_imgbed`, `aliyun_oss` | `smms` | +| `OSS_ENDPOINT` | Aliyun OSS public endpoint | `oss-cn-shanghai.aliyuncs.com` | +| `OSS_ENDPOINT_INNER` | Aliyun OSS internal endpoint (intra-VPC) | `oss-cn-shanghai-internal.aliyuncs.com` | +| `OSS_ACCESS_KEY` | Aliyun AccessKey ID | `LTAI5txxxxxxxxxxxxxxxx` | +| `OSS_ACCESS_KEY_SECRET` | Aliyun AccessKey Secret | `yXxxxxxxxxxxxxxxxxxxxxx` | +| `OSS_BUCKET_NAME` | Aliyun OSS bucket name | `your-bucket-name` | +| `OSS_REGION` | Aliyun OSS region | `cn-shanghai` | | `SMMS_SECRET_TOKEN` | SM.MS API Token | `your-smms-token` | | `PICGO_API_KEY` | PicoGo API Key | `your-picogo-apikey` | | `PICGO_API_URL` | PicoGo API Server URL | `https://www.picgo.net/api/1/upload` | diff --git a/README_ZH.md b/README_ZH.md index dfc0b34..5ca3b2c 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -205,6 +205,7 @@ app/ | `PROXIES` | 代理服务器列表 (例如 `http://user:pass@host:port`) | `[]` | | **日志与安全** | | | | `LOG_LEVEL` | 日志级别: `DEBUG`, `INFO`, `WARNING`, `ERROR` | `INFO` | +| `ERROR_LOG_RECORD_REQUEST_BODY` | 是否记录错误日志的请求体(可能包含敏感信息) | `false` | | `AUTO_DELETE_ERROR_LOGS_ENABLED` | 是否自动删除错误日志 | `true` | | `AUTO_DELETE_ERROR_LOGS_DAYS` | 错误日志保留天数 | `7` | | `AUTO_DELETE_REQUEST_LOGS_ENABLED`| 是否自动删除请求日志 | `false` | @@ -217,7 +218,13 @@ app/ | **图像生成相关** | | | | `PAID_KEY` | 付费版API Key,用于图片生成等高级功能 | `your-paid-api-key` | | `CREATE_IMAGE_MODEL` | 图片生成模型 | `imagen-3.0-generate-002` | -| `UPLOAD_PROVIDER` | 图片上传提供商: `smms`, `picgo`, `cloudflare_imgbed` | `smms` | +| `UPLOAD_PROVIDER` | 图片上传提供商: `smms`, `picgo`, `cloudflare_imgbed`, `aliyun_oss` | `smms` | +| `OSS_ENDPOINT` | 阿里云 OSS 公网 Endpoint | `oss-cn-shanghai.aliyuncs.com` | +| `OSS_ENDPOINT_INNER` | 阿里云 OSS 内网 Endpoint(同 VPC 内网访问) | `oss-cn-shanghai-internal.aliyuncs.com` | +| `OSS_ACCESS_KEY` | 阿里云 AccessKey ID | `LTAI5txxxxxxxxxxxxxxxx` | +| `OSS_ACCESS_KEY_SECRET` | 阿里云 AccessKey Secret | `yXxxxxxxxxxxxxxxxxxxxxx` | +| `OSS_BUCKET_NAME` | 阿里云 OSS Bucket 名称 | `your-bucket-name` | +| `OSS_REGION` | 阿里云 OSS 区域 Region | `cn-shanghai` | | `SMMS_SECRET_TOKEN` | SM.MS图床的API Token | `your-smms-token` | | `PICGO_API_KEY` | [PicoGo](https://www.picgo.net/)图床的API Key | `your-picogo-apikey` | | `PICGO_API_URL` | [PicoGo](https://www.picgo.net/)图床的API服务器地址 | `https://www.picgo.net/api/1/upload` | diff --git a/app/config/config.py b/app/config/config.py index 00fb7e5..df9e1a9 100644 --- a/app/config/config.py +++ b/app/config/config.py @@ -6,7 +6,7 @@ import datetime import json from typing import Any, Dict, List, Type, get_args, get_origin -from pydantic import ValidationError, ValidationInfo, field_validator, Field +from pydantic import Field, ValidationError, ValidationInfo, field_validator from pydantic_settings import BaseSettings from sqlalchemy import insert, select, update @@ -51,8 +51,8 @@ class Settings(BaseSettings): return v # API相关配置 - API_KEYS: List[str]=[] - ALLOWED_TOKENS: List[str]=[] + API_KEYS: List[str] = [] + ALLOWED_TOKENS: List[str] = [] BASE_URL: str = f"https://generativelanguage.googleapis.com/{API_VERSION}" AUTH_TOKEN: str = "" MAX_FAILURES: int = 3 @@ -62,7 +62,9 @@ class Settings(BaseSettings): PROXIES: List[str] = [] PROXIES_USE_CONSISTENCY_HASH_BY_API_KEY: bool = True # 是否使用一致性哈希来选择代理 VERTEX_API_KEYS: List[str] = [] - VERTEX_EXPRESS_BASE_URL: str = "https://aiplatform.googleapis.com/v1beta1/publishers/google" + VERTEX_EXPRESS_BASE_URL: str = ( + "https://aiplatform.googleapis.com/v1beta1/publishers/google" + ) # 智能路由配置 URL_NORMALIZATION_ENABLED: bool = False # 是否启用智能路由映射功能 @@ -77,7 +79,13 @@ class Settings(BaseSettings): TOOLS_CODE_EXECUTION_ENABLED: bool = False # 是否启用网址上下文 URL_CONTEXT_ENABLED: bool = False - URL_CONTEXT_MODELS: List[str] = ["gemini-2.5-pro","gemini-2.5-flash","gemini-2.5-flash-lite","gemini-2.0-flash","gemini-2.0-flash-live-001"] + URL_CONTEXT_MODELS: List[str] = [ + "gemini-2.5-pro", + "gemini-2.5-flash", + "gemini-2.5-flash-lite", + "gemini-2.0-flash", + "gemini-2.0-flash-live-001", + ] SHOW_SEARCH_LINK: bool = True SHOW_THINKING_PROCESS: bool = True THINKING_MODELS: List[str] = [] @@ -128,6 +136,7 @@ class Settings(BaseSettings): # 日志配置 LOG_LEVEL: str = "INFO" + ERROR_LOG_RECORD_REQUEST_BODY: bool = False AUTO_DELETE_ERROR_LOGS_ENABLED: bool = True AUTO_DELETE_ERROR_LOGS_DAYS: int = 7 AUTO_DELETE_REQUEST_LOGS_ENABLED: bool = False @@ -144,7 +153,7 @@ class Settings(BaseSettings): default=3600, ge=300, le=86400, - description="Admin session expiration time in seconds (5 minutes to 24 hours)" + description="Admin session expiration time in seconds (5 minutes to 24 hours)", ) def __init__(self, **kwargs): @@ -176,7 +185,9 @@ 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()] + 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." ) @@ -228,7 +239,9 @@ def _parse_db_value(key: str, db_value: str, target_type: Type) -> Any: f"Parsed DB value for key '{key}' is not a dictionary type. Value: {db_value}" ) except json.JSONDecodeError: - logger.error(f"Could not parse '{db_value}' as Dict[str, str] for key '{key}'. Returning empty dict.") + logger.error( + f"Could not parse '{db_value}' as Dict[str, str] for key '{key}'. Returning empty dict." + ) return parsed_dict # 处理 Dict[str, float] elif args and args == (str, float): @@ -250,7 +263,9 @@ def _parse_db_value(key: str, db_value: str, target_type: Type) -> Any: 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}" @@ -411,9 +426,7 @@ async def sync_initial_settings(): # 序列化值为字符串或 JSON 字符串 if isinstance(value, (list, dict)): - db_value = json.dumps( - value, ensure_ascii=False - ) + db_value = json.dumps(value, ensure_ascii=False) elif isinstance(value, bool): db_value = str(value).lower() elif value is None: diff --git a/app/database/services.py b/app/database/services.py index 4a54260..3de61da 100644 --- a/app/database/services.py +++ b/app/database/services.py @@ -123,16 +123,19 @@ async def add_error_log( bool: 是否添加成功 """ try: - # 如果request_msg是字典,则转换为JSON字符串 - if isinstance(request_msg, dict): - request_msg_json = request_msg - elif isinstance(request_msg, str): - try: - request_msg_json = json.loads(request_msg) - except json.JSONDecodeError: - request_msg_json = {"message": request_msg} - else: + if request_msg is None: request_msg_json = None + else: + # 如果request_msg是字典,则转换为JSON字符串 + if isinstance(request_msg, dict): + request_msg_json = request_msg + elif isinstance(request_msg, str): + try: + request_msg_json = json.loads(request_msg) + except json.JSONDecodeError: + request_msg_json = {"message": request_msg} + else: + request_msg_json = None # 插入错误日志 query = insert(ErrorLog).values( @@ -455,7 +458,7 @@ async def delete_all_error_logs() -> int: total_deleted_count = 0 # SQLite 对 SQL 参数数量有上限(常见为 999),IN 子句中过多参数会报错 # 统一使用 500,兼容 SQLite/MySQL,必要时可在配置中暴露该值 - batch_size = 500 + batch_size = 200 try: while True: diff --git a/app/service/chat/gemini_chat_service.py b/app/service/chat/gemini_chat_service.py index 45d0683..8419fb7 100644 --- a/app/service/chat/gemini_chat_service.py +++ b/app/service/chat/gemini_chat_service.py @@ -375,7 +375,7 @@ class GeminiChatService: error_type="gemini-chat-non-stream", error_log=error_log_msg, error_code=status_code, - request_msg=payload, + request_msg=payload if settings.ERROR_LOG_RECORD_REQUEST_BODY else None, request_datetime=request_datetime, ) raise e @@ -422,7 +422,7 @@ class GeminiChatService: error_type="gemini-count-tokens", error_log=error_log_msg, error_code=status_code, - request_msg=payload, + request_msg=payload if settings.ERROR_LOG_RECORD_REQUEST_BODY else None, ) raise e finally: @@ -512,7 +512,9 @@ class GeminiChatService: error_type="gemini-chat-stream", error_log=error_log_msg, error_code=status_code, - request_msg=payload, + request_msg=( + payload if settings.ERROR_LOG_RECORD_REQUEST_BODY else None + ), request_datetime=request_datetime, ) diff --git a/app/service/chat/openai_chat_service.py b/app/service/chat/openai_chat_service.py index 1ab55d0..967f02d 100644 --- a/app/service/chat/openai_chat_service.py +++ b/app/service/chat/openai_chat_service.py @@ -359,7 +359,7 @@ class OpenAIChatService: error_type="openai-chat-non-stream", error_log=error_log_msg, error_code=status_code, - request_msg=payload, + request_msg=payload if settings.ERROR_LOG_RECORD_REQUEST_BODY else None, request_datetime=request_datetime, ) raise e @@ -549,7 +549,9 @@ class OpenAIChatService: error_type="openai-chat-stream", error_log=error_log_msg, error_code=status_code, - request_msg=payload, + request_msg=( + payload if settings.ERROR_LOG_RECORD_REQUEST_BODY else None + ), request_datetime=request_datetime, ) diff --git a/app/service/chat/vertex_express_chat_service.py b/app/service/chat/vertex_express_chat_service.py index 7a59c7d..3eb1c2d 100644 --- a/app/service/chat/vertex_express_chat_service.py +++ b/app/service/chat/vertex_express_chat_service.py @@ -287,7 +287,7 @@ class GeminiChatService: error_type="gemini-chat-non-stream", error_log=error_log_msg, error_code=status_code, - request_msg=payload, + request_msg=payload if settings.ERROR_LOG_RECORD_REQUEST_BODY else None, request_datetime=request_datetime, ) raise e @@ -363,7 +363,9 @@ class GeminiChatService: error_type="gemini-chat-stream", error_log=error_log_msg, error_code=status_code, - request_msg=payload, + request_msg=( + payload if settings.ERROR_LOG_RECORD_REQUEST_BODY else None + ), request_datetime=request_datetime, ) diff --git a/app/service/embedding/gemini_embedding_service.py b/app/service/embedding/gemini_embedding_service.py index 4a0819a..21e6899 100644 --- a/app/service/embedding/gemini_embedding_service.py +++ b/app/service/embedding/gemini_embedding_service.py @@ -78,7 +78,7 @@ class GeminiEmbeddingService: error_type="gemini-embed-single", error_log=error_log_msg, error_code=status_code, - request_msg=payload, + request_msg=payload if settings.ERROR_LOG_RECORD_REQUEST_BODY else None, request_datetime=request_datetime, ) raise e @@ -124,7 +124,7 @@ class GeminiEmbeddingService: error_type="gemini-embed-batch", error_log=error_log_msg, error_code=status_code, - request_msg=payload, + request_msg=payload if settings.ERROR_LOG_RECORD_REQUEST_BODY else None, request_datetime=request_datetime, ) raise e diff --git a/app/service/openai_compatiable/openai_compatiable_service.py b/app/service/openai_compatiable/openai_compatiable_service.py index 53af167..6d960d5 100644 --- a/app/service/openai_compatiable/openai_compatiable_service.py +++ b/app/service/openai_compatiable/openai_compatiable_service.py @@ -144,7 +144,9 @@ class OpenAICompatiableService: error_type="openai-compatiable-stream", error_log=error_log_msg, error_code=status_code, - request_msg=payload, + request_msg=( + payload if settings.ERROR_LOG_RECORD_REQUEST_BODY else None + ), request_datetime=request_datetime, ) diff --git a/app/static/js/config_editor.js b/app/static/js/config_editor.js index 54e3432..93b5678 100644 --- a/app/static/js/config_editor.js +++ b/app/static/js/config_editor.js @@ -771,6 +771,10 @@ async function initConfig() { if (typeof config.AUTO_DELETE_ERROR_LOGS_DAYS === "undefined") { config.AUTO_DELETE_ERROR_LOGS_DAYS = 7; } + // 错误日志是否记录请求体(默认不记录) + if (typeof config.ERROR_LOG_RECORD_REQUEST_BODY === "undefined") { + config.ERROR_LOG_RECORD_REQUEST_BODY = false; + } // --- 结束:处理自动删除错误日志配置的默认值 --- // --- 新增:处理自动删除请求日志配置的默认值 --- diff --git a/app/templates/config_editor.html b/app/templates/config_editor.html index 1082400..c8aebaa 100644 --- a/app/templates/config_editor.html +++ b/app/templates/config_editor.html @@ -2279,6 +2279,34 @@ endblock %} {% block head_extra_styles %} > + +