"""FastAPI 应用入口 — 挂载静态页 + API 路由""" import time import uuid from contextlib import asynccontextmanager from fastapi import FastAPI, Request, HTTPException from fastapi.staticfiles import StaticFiles from fastapi.responses import JSONResponse, FileResponse, HTMLResponse from fastapi.middleware.cors import CORSMiddleware from pathlib import Path from app.config import APP_PORT, LAW_KB_DATA_DIR from app.routers import search, rag, law, stats, prompts, history from app.services.retriever import Retriever @asynccontextmanager async def lifespan(app: FastAPI): """应用生命周期:启动时加载索引,关闭时释放""" # 启动:加载 FAISS 索引和 SQLite retriever = Retriever.get_instance() retriever.load() yield # 关闭:无需特殊处理 app = FastAPI( title="QYLAW 法律法规知识库", version="1.0.0", lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) @app.middleware("http") async def add_trace_id(request: Request, call_next): """为每个请求添加 trace_id""" trace_id = request.headers.get("X-Trace-Id") or str(uuid.uuid4())[:8] request.state.trace_id = trace_id start = time.time() response = await call_next(request) elapsed = (time.time() - start) * 1000 response.headers["X-Trace-Id"] = trace_id response.headers["X-Response-Time-ms"] = f"{elapsed:.0f}" return response # 注册 API 路由 app.include_router(search.router, prefix="/api", tags=["检索"]) app.include_router(rag.router, prefix="/api", tags=["RAG 问答"]) app.include_router(law.router, prefix="/api", tags=["法规浏览"]) app.include_router(stats.router, prefix="/api", tags=["统计"]) app.include_router(prompts.router, prefix="/api", tags=["提示词配置"]) app.include_router(history.router, prefix="/api", tags=["历史记录"]) # 静态文件 STATIC_DIR = Path(__file__).parent / "static" app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") def _serve_index() -> HTMLResponse: """返回 index.html(带 no-cache 头,防止浏览器缓存旧版)""" html = (STATIC_DIR / "index.html").read_text(encoding="utf-8") return HTMLResponse(content=html, headers={ "Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0", }) @app.get("/") async def index(): """主页""" return _serve_index() @app.get("/health") async def health(): """健康检查""" return {"status": "ok"} # 前端路由 catch-all:必须放在所有 API 路由和 /health 之后 @app.get("/{path:path}") async def spa_fallback(path: str): """SPA 路由兜底:/search, /rag, /browse, /history, /settings 返回 index.html""" # 排除 API 和静态文件路径(已被前面的路由和 mount 处理) if path.startswith("api/") or path.startswith("static/"): raise HTTPException(status_code=404) return _serve_index() if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=APP_PORT)