merge main

This commit is contained in:
2026-08-11 10:16:38 +08:00
156 changed files with 22362 additions and 1211 deletions
+109 -1
View File
@@ -3,7 +3,7 @@ import logging
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, UploadFile, File, Form, Depends, HTTPException
from fastapi import FastAPI, UploadFile, File, Form, Depends, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
@@ -518,12 +518,120 @@ def create_app() -> FastAPI:
# Routes
application.include_router(api_router, prefix="/api")
application.include_router(api_router_v2, prefix="/api/v2")
from app.api.v3 import api_router_v3
application.include_router(api_router_v3, prefix="/api/v3")
# === API v3 请求日志中间件 ===
import json as _json
import time as _time
from app.services.api_v3.logging_service import log_request, log_response, log_request_error
@application.middleware("http")
async def v3_request_logger(request: Request, call_next):
"""记录所有 /api/v3/ 请求和响应。"""
if not str(request.url.path).startswith("/api/v3"):
return await call_next(request)
start_time = _time.perf_counter()
# 提取 API Key ID
auth_header = request.headers.get("Authorization", "")
api_key_id = "unknown"
if auth_header.startswith("Bearer "):
api_key_id = auth_header[7:15] + "..."
# 读取请求体
body = None
if request.method in ("POST", "PUT", "PATCH"):
try:
body = await request.json()
except Exception:
pass
log_request(
method=request.method,
path=str(request.url.path),
api_key_id=api_key_id,
body=body,
)
try:
response = await call_next(request)
except Exception as exc:
duration_ms = int((_time.perf_counter() - start_time) * 1000)
log_request_error(
method=request.method,
path=str(request.url.path),
api_key_id=api_key_id,
error=str(exc),
)
return JSONResponse(
content={"code": 50000, "data": None, "message": f"服务器内部错误: {str(exc)[:200]}"},
status_code=200,
)
duration_ms = int((_time.perf_counter() - start_time) * 1000)
# 读取响应体
response_body = None
try:
response_body = _json.loads(response.body)
except Exception:
pass
log_response(
method=request.method,
path=str(request.url.path),
api_key_id=api_key_id,
status_code=response.status_code,
body=response_body,
duration_ms=duration_ms,
)
return response
# === API v3 统一异常处理 ===
from fastapi.exceptions import HTTPException
from fastapi.responses import JSONResponse
from app.services.api_v3.pricing_service import PricingNotConfiguredError
@application.exception_handler(HTTPException)
async def v3_http_exception_handler(request: Request, exc: HTTPException):
"""仅对 /api/v3/ 路径返回统一格式,HTTP 状态码固定 200。"""
if not str(request.url.path).startswith("/api/v3"):
# 非 v3 路径返回标准 HTTPException 响应,保持原始状态码
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
)
detail = exc.detail
message = detail.get("message", str(detail)) if isinstance(detail, dict) else str(detail)
code_map = {400: 40000, 401: 40100, 403: 40300, 404: 40400, 429: 42900, 422: 42200, 500: 50000, 504: 50400}
code = code_map.get(exc.status_code, exc.status_code * 100)
return JSONResponse(content={"code": code, "data": None, "message": message}, status_code=200)
@application.exception_handler(PricingNotConfiguredError)
async def v3_pricing_not_configured_handler(request: Request, exc: PricingNotConfiguredError):
if not str(request.url.path).startswith("/api/v3"):
raise exc
return JSONResponse(content={"code": 40001, "data": None, "message": str(exc)}, status_code=200)
@application.exception_handler(Exception)
async def v3_general_exception_handler(request: Request, exc: Exception):
if not str(request.url.path).startswith("/api/v3"):
raise exc
return JSONResponse(content={"code": 50000, "data": None, "message": f"服务器内部错误: {str(exc)[:200]}"}, status_code=200)
# Static files for uploads
upload_dir = os.path.abspath(settings.UPLOAD_LOCAL_PATH)
os.makedirs(upload_dir, exist_ok=True)
application.mount("/uploads", StaticFiles(directory=upload_dir), name="uploads")
# 挂载 API v3 生成文件静态目录
generate_dir = os.path.join(os.path.dirname(upload_dir), "generate")
os.makedirs(generate_dir, exist_ok=True)
application.mount("/generate", StaticFiles(directory=generate_dir), name="generate")
@application.get("/internal/health")
async def health():
return {"status": "ok"}