"""AI Knowledge Link — FastAPI 应用工厂。 Phase 1-3: 配置校验、日志、异常处理器、健康检查、lifespan 资源管理、认证路由。 后续 Phase 逐步挂载:knowledge-bases、documents、public /k/ 路由。 """ from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.api.auth import router as auth_router from app.api.documents import router as doc_router from app.api.health import router as health_router from app.api.knowledge_bases import router as kb_router from app.public.routes import router as public_router from app.core.config import get_settings from app.core.db import dispose_engine, get_session_factory from app.core.errors import register_exception_handlers from app.core.logging import get_logger, setup_logging logger = get_logger(__name__) def _seed_free_plan() -> None: """启动时确保 free plan 存在(幂等)。""" from app.repositories.plan_repo import PlanRepository factory = get_session_factory() with factory() as session: repo = PlanRepository(session) plan = repo.get_by_code("free") if plan is None: repo.get_or_create_free() session.commit() logger.info("Seeded free plan (100MB / 20MB-per-file)") else: logger.debug("Free plan already exists (id=%s)", plan.id) @asynccontextmanager async def lifespan(app: FastAPI): settings = get_settings() setup_logging() # 确保 data 目录存在 data_dir = settings.storage_root_path data_dir.mkdir(parents=True, exist_ok=True) logger.info("Starting backend (env=%s, data=%s)", settings.environment, data_dir) # Seed:确保 free plan 存在 _seed_free_plan() yield dispose_engine() logger.info("Backend shutdown complete") def create_app() -> FastAPI: settings = get_settings() app = FastAPI( title="AI Knowledge Link", version="0.1.0", lifespan=lifespan, docs_url="/api/docs" if not settings.is_production else None, openapi_url="/api/openapi.json" if not settings.is_production else None, ) # CORS(开发期允许 Vite dev server) app.add_middleware( CORSMiddleware, allow_origins=[settings.frontend_origin], allow_credentials=True, allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], allow_headers=["Content-Type", "X-Requested-With"], ) register_exception_handlers(app) # 路由挂载 app.include_router(health_router, prefix="/api", tags=["health"]) app.include_router(auth_router, prefix="/api", tags=["auth"]) app.include_router(kb_router, prefix="/api", tags=["knowledge-bases"]) app.include_router(doc_router, prefix="/api", tags=["documents"]) app.include_router(public_router, tags=["public"]) return app app = create_app()