"""main.py —— BFF 应用入口(ARCHITECTURE.md §2/§3)。 - 挂全部 5 个 router(qa/kg/extraction 为 A2/A3 实现,import 路径不变)。 - CORS:允许 http://localhost:5180 与 http://127.0.0.1:5180(Vite dev)。 - lifespan:init_db(幂等迁移);关闭回收 httpx 客户端与 Neo4j 连接。 - 全局 RequestValidationError → VALIDATION 信封(API.md §1,2026-08-25 PM 集成)。 - 生产形态(M6):frontend/dist 存在时静态托管(ARCHITECTURE.md §3)。 """ from __future__ import annotations import logging from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from .config import get_settings from .db import close_db, init_db from .routers import extraction, health, kg, qa, sessions from .upstream import close_client logging.basicConfig(level=logging.INFO) _ALLOWED_ORIGINS = [ "http://localhost:5180", "http://127.0.0.1:5180", ] def create_app() -> FastAPI: settings = get_settings() app = FastAPI(title=settings.app_name, version=settings.version) app.add_middleware( CORSMiddleware, allow_origins=_ALLOWED_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(health.router) app.include_router(sessions.router) app.include_router(qa.router) app.include_router(kg.router) app.include_router(extraction.router) @app.exception_handler(RequestValidationError) async def _validation_handler(_: Request, exc: RequestValidationError) -> JSONResponse: # API.md §1 已裁决:参数校验错误收敛为统一 VALIDATION 信封(422) return JSONResponse( status_code=422, content={ "ok": False, "error": {"code": "VALIDATION", "message": "请求参数不合法", "degraded": False}, }, ) @asynccontextmanager async def _lifespan(_app: FastAPI): await init_db() try: yield finally: await close_client() try: from .services.neo4j import close_neo4j # A3 提供;缺失则跳过 await close_neo4j() except ImportError: pass await close_db() app.router.lifespan_context = _lifespan # 生产形态:静态托管 frontend/dist(存在时挂载;本地开发期可无)。 # 2026-08-25 PM 修复:dist 路径必须基于项目根解析(BFF 工作目录是 backend/, # 相对路径会解析成 backend/frontend/dist);sync.sh 排除 dist,需单独同步。 project_root = Path(__file__).resolve().parent.parent.parent dist = project_root / settings.frontend_dist_dir if not Path(settings.frontend_dist_dir).is_absolute() else Path(settings.frontend_dist_dir) if dist.is_dir(): app.mount("/", StaticFiles(directory=str(dist), html=True), name="frontend") return app app = create_app()