"""FastAPI 应用装配。""" from __future__ import annotations import asyncio import logging from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from .config import get_settings from .database import close_db, init_db from .db_writer import start as start_db_writer, stop as stop_db_writer from .errors import HttpError, http_error_handler, unhandled_exception_handler from .hf_storage import is_hub_enabled from .http_client import close_http_client from .services import config_store, pseudo_store, user_files_sync async def validation_error_handler(request: Request, exc: RequestValidationError) -> JSONResponse: """把 FastAPI 422 校验错误转为统一格式,避免前端无法解析。""" import json details = [] for err in getattr(exc, "errors", lambda: [])(): details.append(err) msg = "request validation failed" if details: try: msg = json.dumps(details, ensure_ascii=False, default=str) except Exception: msg = str(details) return JSONResponse( status_code=422, content={ "ok": False, "error": { "code": "unprocessable_entity", "message": msg, "status": 422, "retryable": False, "hint": "请求参数格式有误,请检查字段", }, }, headers={ "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "*", "Access-Control-Allow-Headers": "*", }, ) @asynccontextmanager async def lifespan(app: FastAPI): # 启动 logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") init_db() # 启动后台 DB 写线程(usage_log / request_log 等分析类写入移出事件循环) start_db_writer() # 预热配置 try: cfg = await config_store.load_config() logging.getLogger(__name__).info( "config loaded: %d providers, default=%s", len(cfg.providers), cfg.default_provider_id, ) except Exception as e: logging.getLogger(__name__).warning("config preload failed: %s", e) # 启动伪流式清理任务 cleanup_task = asyncio.create_task(pseudo_store.cleanup_loop()) # 启动用户文件 Hub 定时同步任务 s = get_settings() if is_hub_enabled(): logging.getLogger(__name__).info( "[startup] Hub sync enabled: repo=%s token=***%s", s.hf_config_repo, s.hf_token[-4:] if s.hf_token else "(empty)", ) sync_task = user_files_sync.start_sync_task() else: logging.getLogger(__name__).warning( "[startup] Hub sync DISABLED: HF_TOKEN/HF_CONFIG_REPO not set, " "user files will only persist locally (lost on Space rebuild). " "Set HF_CONFIG_REPO= and HF_TOKEN= to enable." ) sync_task = None yield # 关闭 cleanup_task.cancel() try: await cleanup_task except asyncio.CancelledError: pass if sync_task is not None: await user_files_sync.stop_sync_task() await close_http_client() stop_db_writer() close_db() def create_app() -> FastAPI: settings = get_settings() app = FastAPI( title="XTC Backend (Hugging Face)", description="OpenAI/Gemini 兼容网关,迁移自 Netlify 版", version="1.0.0", lifespan=lifespan, docs_url="/docs", redoc_url=None, ) # CORS app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=False, allow_methods=["*"], allow_headers=["*"], expose_headers=["x-xtc-provider", "x-xtc-model", "x-xtc-image-fix-mode", "Retry-After", "X-RateLimit-Scope"], ) # 速率限制中间件(必须在内层,让 CORS 先处理) from .middleware import RateLimitMiddleware app.add_middleware(RateLimitMiddleware) # 请求日志中间件(最内层,确保能捕获下游所有响应) from .request_log_middleware import RequestLogMiddleware app.add_middleware(RequestLogMiddleware) # 异常处理 app.add_exception_handler(HttpError, http_error_handler) app.add_exception_handler(RequestValidationError, validation_error_handler) app.add_exception_handler(Exception, unhandled_exception_handler) # 路由注册 from .api import ( admin, admin_data, health, image_fix, logs, openai_compat, pseudo_stream, request_logs, sessions, tts, usage_audit, user_files, user_backups, webhooks, xtc, ) from .admin_html import router as admin_html_router from .user_html import router as user_html_router app.include_router(health.router) app.include_router(openai_compat.router) app.include_router(xtc.router) app.include_router(pseudo_stream.router) app.include_router(image_fix.router) app.include_router(tts.router) app.include_router(sessions.router) app.include_router(admin.router) app.include_router(admin_data.router) app.include_router(usage_audit.router) app.include_router(webhooks.router) app.include_router(request_logs.router) app.include_router(admin_html_router) app.include_router(user_html_router) app.include_router(user_files.router) app.include_router(user_backups.router) app.include_router(logs.router) @app.options("/{path:path}") async def cors_preflight(path: str, request: Request): return JSONResponse( status_code=204, content=None, headers={ "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "*", "Access-Control-Allow-Headers": "*", "Access-Control-Max-Age": "86400", }, ) return app app = create_app()