641e33b834
- 语义检索(FAISS + embedding)+ 精确查找(法规名+条号) - RAG 问答(SSE 流式,支持 thinking 折叠显示) - 法规浏览(原文阅读) - 历史记录(检索+对话持久化到 SQLite) - 设置页(系统提示词/模板/LLM 参数可配置) - 检索质量评估脚本 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
102 lines
3.0 KiB
Python
102 lines
3.0 KiB
Python
"""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)
|