diff --git a/app/api/v1/admin_api/token.py b/app/api/v1/admin_api/token.py index 81b9fef..3b82277 100644 --- a/app/api/v1/admin_api/token.py +++ b/app/api/v1/admin_api/token.py @@ -15,6 +15,30 @@ from app.services.token.manager import get_token_manager router = APIRouter() +def _mask_token_for_log(token: str) -> str: + raw = str(token or "").strip() + if not raw: + return "" + if len(raw) <= 12: + return raw + return f"{raw[:6]}...{raw[-6:]}" + + +def _log_nsfw_enable_payload(api_name: str, data: dict) -> None: + single = str(data.get("token") or "").strip() + batch = data.get("tokens") + token_list = [str(t).strip() for t in batch] if isinstance(batch, list) else [] + token_list = [t for t in token_list if t] + preview = [_mask_token_for_log(t) for t in token_list[:5]] + payload = { + "token": _mask_token_for_log(single) if single else "", + "tokens_count": len(token_list), + "tokens_preview": preview, + "raw_keys": sorted(list(data.keys())) if isinstance(data, dict) else [], + } + logger.info(f"{api_name} request payload: {payload}") + + @router.get("/tokens", dependencies=[Depends(verify_app_key)]) async def get_tokens(): """获取所有 Token""" @@ -255,6 +279,7 @@ async def batch_cancel(task_id: str): async def enable_nsfw(data: dict): """批量开启 NSFW (Unhinged) 模式""" try: + _log_nsfw_enable_payload("tokens/nsfw/enable", data) mgr = await get_token_manager() tokens = [] @@ -316,6 +341,7 @@ async def enable_nsfw(data: dict): @router.post("/tokens/nsfw/enable/async", dependencies=[Depends(verify_app_key)]) async def enable_nsfw_async(data: dict): """批量开启 NSFW (Unhinged) 模式(异步批量 + SSE 进度)""" + _log_nsfw_enable_payload("tokens/nsfw/enable/async", data) mgr = await get_token_manager() tokens = [] diff --git a/app/core/response_middleware.py b/app/core/response_middleware.py index 4c0a07e..717d2e4 100644 --- a/app/core/response_middleware.py +++ b/app/core/response_middleware.py @@ -7,12 +7,99 @@ Response Middleware import time import uuid +import json +from typing import Any +from urllib.parse import parse_qs from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from app.core.logger import logger +SENSITIVE_KEYS = { + "authorization", + "api_key", + "app_key", + "public_key", + "token", + "tokens", + "password", + "cookie", +} + + +def _short_text(value: Any, max_len: int = 240) -> Any: + if not isinstance(value, str): + return value + text = value.strip() + if len(text) <= max_len: + return text + return f"{text[:max_len]}...(len={len(text)})" + + +def _sanitize_payload(value: Any, depth: int = 0) -> Any: + if depth > 4: + return "..." + if isinstance(value, dict): + result = {} + for key, item in value.items(): + k = str(key) + if k.lower() in SENSITIVE_KEYS: + result[k] = "" + else: + result[k] = _sanitize_payload(item, depth + 1) + return result + if isinstance(value, list): + if len(value) > 20: + return [_sanitize_payload(v, depth + 1) for v in value[:20]] + [f"...(len={len(value)})"] + return [_sanitize_payload(v, depth + 1) for v in value] + if isinstance(value, str): + return _short_text(value) + return value + + +async def _extract_request_payload(request: Request) -> dict[str, Any]: + query_data = {k: v if len(v) > 1 else v[0] for k, v in parse_qs(request.url.query, keep_blank_values=True).items()} + payload: dict[str, Any] = {"query": _sanitize_payload(query_data)} + + if request.method in ("GET", "HEAD", "OPTIONS"): + return payload + + content_type = (request.headers.get("content-type") or "").lower() + raw = await request.body() + if not raw: + payload["body"] = None + return payload + + if "application/json" in content_type: + try: + payload["body"] = _sanitize_payload(json.loads(raw.decode("utf-8", errors="ignore"))) + return payload + except Exception: + payload["body"] = _short_text(raw.decode("utf-8", errors="ignore")) + return payload + + if "application/x-www-form-urlencoded" in content_type: + form = parse_qs(raw.decode("utf-8", errors="ignore"), keep_blank_values=True) + payload["body"] = _sanitize_payload({k: v if len(v) > 1 else v[0] for k, v in form.items()}) + return payload + + if "multipart/form-data" in content_type: + payload["body"] = { + "_type": "multipart/form-data", + "_size": len(raw), + "_preview": _short_text(raw[:400].decode("utf-8", errors="ignore")), + } + return payload + + payload["body"] = { + "_type": content_type or "unknown", + "_size": len(raw), + "_preview": _short_text(raw[:400].decode("utf-8", errors="ignore")), + } + return payload + + class ResponseLoggerMiddleware(BaseHTTPMiddleware): """ 请求日志/响应追踪中间件 @@ -41,12 +128,14 @@ class ResponseLoggerMiddleware(BaseHTTPMiddleware): return await call_next(request) # 记录请求信息 + req_payload = await _extract_request_payload(request) logger.info( - f"Request: {request.method} {request.url.path}", + f"Request: {request.method} {request.url.path} payload={req_payload}", extra={ "traceID": trace_id, "method": request.method, "path": request.url.path, + "payload": req_payload, }, )