Files
freedak 641e33b834 feat: QYLAW 法律法规知识库
- 语义检索(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>
2026-08-07 14:55:25 +08:00

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)