xtc-backend / app /main.py
a3216's picture
sync from GitHub 75b7c70: feat: 后端支持云端备份功能及管理面板升级
42e997e verified
Raw
History Blame Contribute Delete
6.22 kB
"""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=<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)
@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()