Spaces:
Running
Running
| """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": "*", | |
| }, | |
| ) | |
| 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=<your-dataset-repo> and HF_TOKEN=<your-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) | |
| 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() | |