mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-07-08 08:11:36 +08:00
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:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
// --- 结束:处理自动删除错误日志配置的默认值 ---
|
||||
|
||||
// --- 新增:处理自动删除请求日志配置的默认值 ---
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user