feat(config): 新增错误日志请求体记录开关(默认关闭)

- 新增环境变量 ERROR_LOG_RECORD_REQUEST_BODY,默认 false
- Settings 增加该配置,并在各服务写入错误日志时按开关决定是否
  入库请求体,降低敏感信息泄露风险
- 配置编辑页新增对应开关,前端初始化默认值;.env.example、
  README/README_ZH 同步更新
- db: add_error_log 支持 None 请求体并更稳健解析字符串/字典
- perf(db): 将错误日志批量删除 batch_size 从 500 下调到 200,
  兼容 SQLite/MySQL 参数上限并提升稳定性
- docs: 补充 aliyun_oss 上传提供商与 OSS 配置示例
- style: 轻微代码格式化与导入顺序优化
This commit is contained in:
snaily
2025-09-18 04:21:28 +08:00
parent 7dbd3ad693
commit 708fb1604b
12 changed files with 106 additions and 34 deletions

View File

@@ -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)

View File

@@ -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` |

View File

@@ -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` |

View File

@@ -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:

View File

@@ -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 参数数量有上限(常见为 999IN 子句中过多参数会报错
# 统一使用 500兼容 SQLite/MySQL必要时可在配置中暴露该值
batch_size = 500
batch_size = 200
try:
while True:

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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;
}
// --- 结束:处理自动删除错误日志配置的默认值 ---
// --- 新增:处理自动删除请求日志配置的默认值 ---

View File

@@ -2279,6 +2279,34 @@ endblock %} {% block head_extra_styles %}
>
</div>
<!-- 错误日志记录请求体 -->
<div class="mb-6">
<div class="flex items-center justify-between">
<label
for="ERROR_LOG_RECORD_REQUEST_BODY"
class="font-semibold text-gray-700"
>错误日志记录请求体</label
>
<div
class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in"
>
<input
type="checkbox"
name="ERROR_LOG_RECORD_REQUEST_BODY"
id="ERROR_LOG_RECORD_REQUEST_BODY"
class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"
/>
<label
for="ERROR_LOG_RECORD_REQUEST_BODY"
class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"
></label>
</div>
</div>
<small class="text-gray-500 mt-1 block"
>关闭可避免敏感数据入库,默认关闭。</small
>
</div>
<!-- 自动删除错误日志 -->
<div class="mb-6">
<div class="flex items-center justify-between">