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>
This commit is contained in:
+101
@@ -0,0 +1,101 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user