"""FastAPI 应用入口。""" import logging from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from slowapi.middleware import SlowAPIMiddleware from app.api import ( ai, auth, images, jobs, notebook, questions, settings as settings_api, tags, ) from app.config import settings from app.db import Base, engine from app.ratelimit import limiter from app.services.startup import fail_stale_jobs # 确保所有模型被导入注册到 metadata import app.models # noqa: F401 logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): # 公网暴露,配置有问题就别启动了,免得带着弱密钥上线 problems = settings.check_production_secrets() if problems: for p in problems: logger.error("配置错误:%s", p) raise RuntimeError("生产配置校验失败:" + ";".join(problems)) # 仅开发环境自动建表,方便快速迭代;生产用 Alembic 迁移(见 deploy/) if settings.debug: async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) # BackgroundTasks 不跨重启,把残留的进行中任务标记为失败, # 否则前端会对着永不完成的任务无限轮询 await fail_stale_jobs() yield app = FastAPI(title=settings.app_name, lifespan=lifespan) # 限流:超限返回 429 app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origin_list, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(auth.router) app.include_router(questions.router) app.include_router(tags.router) app.include_router(images.router) app.include_router(jobs.router) app.include_router(settings_api.router) app.include_router(ai.router) app.include_router(notebook.router) @app.get("/api/health") async def health(): return {"status": "ok"}