- git mv 保留历史:ui/(Vue3+Vite)、sdk/(BFF 中间层)
- 路径语义修正:db_path/figures_cache 改相对 sdk/(BFF cwd),
历史库 sdk/backend/data 迁移至 sdk/data/(会话/图片缓存无损)
- 引用修复 ~15 处:config.py、deploy/*.sh、vitest/playwright 配置、
单测导入路径、docs、README、gitignore、.env.example
- 骨架:agent/{qa,methodology,legacy-rag,vendor}、knowledge/manifest.yaml、
tooling/{compile,kb,crypto,assemble,audit,tools,scripts} + README
- 门禁验证:sdk pytest 125 通过、ui vitest 158 通过、
生产 BFF 脚本重启四上游全绿 + 静态托管 + 会话库无损
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
225 lines
6.7 KiB
Python
225 lines
6.7 KiB
Python
"""PlantAI-rag-new mock server —— 前端开发与 E2E 场景的固定数据源。
|
||
|
||
监听 127.0.0.1:8099。实现 API.md 的全部接口,返回 fixtures/ 中的固定数据。
|
||
场景切换:查询参数 ?scenario= 或请求头 X-Mock-Scenario:
|
||
normal(默认)| empty(空结果)| slow(慢响应 5s)| error(上游故障)| degraded(QA 不可用)
|
||
SSE 流按真实时序回放,让流式 UI 在没有上游时也能开发。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import json
|
||
import time
|
||
from pathlib import Path
|
||
|
||
from fastapi import FastAPI, Header, Query, Request
|
||
from fastapi.responses import JSONResponse, StreamingResponse
|
||
|
||
FIXTURES = Path(__file__).parent / "fixtures"
|
||
app = FastAPI(title="PlantAI-rag-new mock")
|
||
|
||
|
||
def load(name: str):
|
||
return json.loads((FIXTURES / name).read_text(encoding="utf-8"))
|
||
|
||
|
||
def scenario_of(request: Request, q: str | None = None) -> str:
|
||
s = q or request.headers.get("X-Mock-Scenario") or "normal"
|
||
return s if s in {"normal", "empty", "slow", "error", "degraded"} else "normal"
|
||
|
||
|
||
def envelope(code: str, message: str, degraded: bool, status: int):
|
||
return JSONResponse(
|
||
status_code=status,
|
||
content={"ok": False, "error": {"code": code, "message": message, "degraded": degraded}},
|
||
)
|
||
|
||
|
||
# ---------- 健康与元信息 ----------
|
||
|
||
@app.get("/api/health")
|
||
async def health():
|
||
return load("health.json")
|
||
|
||
|
||
@app.get("/api/qa/info")
|
||
async def qa_info():
|
||
return load("qa_info.json")
|
||
|
||
|
||
# ---------- QA ----------
|
||
|
||
@app.post("/api/qa/chat")
|
||
async def qa_chat(request: Request):
|
||
sc = scenario_of(request)
|
||
if sc == "degraded":
|
||
return envelope("UPSTREAM_DOWN", "知识问答服务当前不可用,请稍后重试", True, 503)
|
||
if sc == "error":
|
||
return envelope("UPSTREAM_ERROR", "上游问答服务返回错误", True, 502)
|
||
if sc == "slow":
|
||
await asyncio.sleep(5)
|
||
if sc == "empty":
|
||
return load("qa_chat_empty.json")
|
||
return load("qa_chat.json")
|
||
|
||
|
||
@app.post("/api/qa/search")
|
||
async def qa_search(request: Request):
|
||
sc = scenario_of(request)
|
||
if sc in ("degraded",):
|
||
return envelope("UPSTREAM_DOWN", "知识问答服务当前不可用,请稍后重试", True, 503)
|
||
return load("qa_search.json")
|
||
|
||
|
||
@app.post("/api/qa/chat/stream")
|
||
async def qa_chat_stream(request: Request):
|
||
sc = scenario_of(request)
|
||
if sc == "degraded":
|
||
return envelope("UPSTREAM_DOWN", "知识问答服务当前不可用,请稍后重试", True, 503)
|
||
|
||
frames = load("sse_frames.json") # [{stage, phase, label, details, delay_ms}, ..., "__complete__"]
|
||
|
||
async def gen():
|
||
for f in frames:
|
||
if f == "__complete__":
|
||
yield "event: complete\n"
|
||
yield f"data: {json.dumps(load('qa_chat.json'), ensure_ascii=False)}\n\n"
|
||
break
|
||
await asyncio.sleep(f.get("delay_ms", 600) / 1000)
|
||
payload = {
|
||
"stage": f["stage"], "phase": f["phase"], "label": f["label"],
|
||
"details": f.get("details", {}), "at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||
}
|
||
yield f"event: progress\n"
|
||
yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
|
||
if int(f.get("delay_ms", 600)) > 3000: # 慢帧之间补 keepalive
|
||
yield ": ping\n\n"
|
||
|
||
return StreamingResponse(gen(), media_type="text/event-stream",
|
||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
||
|
||
|
||
# ---------- 会话与消息 ----------
|
||
|
||
_sessions = []
|
||
_messages = {}
|
||
_seq = 0
|
||
|
||
|
||
def _now():
|
||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||
|
||
|
||
@app.get("/api/sessions")
|
||
async def sessions():
|
||
return load("sessions.json")
|
||
|
||
|
||
@app.post("/api/sessions")
|
||
async def create_session(request: Request):
|
||
body = await request.json()
|
||
return {**load("session_created.json"), "title": body.get("title") or "新会话"}
|
||
|
||
|
||
@app.get("/api/sessions/{sid}")
|
||
async def session_detail(sid: str):
|
||
return load("session_detail.json")
|
||
|
||
|
||
@app.patch("/api/sessions/{sid}")
|
||
async def session_patch(sid: str, request: Request):
|
||
body = await request.json()
|
||
return {**load("session_created.json"), **body}
|
||
|
||
|
||
@app.delete("/api/sessions/{sid}")
|
||
async def session_delete(sid: str):
|
||
return JSONResponse(status_code=204, content=None)
|
||
|
||
|
||
@app.post("/api/sessions/{sid}/messages")
|
||
async def create_message(sid: str, request: Request):
|
||
body = await request.json()
|
||
return {"id": "msg-mock-001", "role": body.get("role", "user"),
|
||
"kind": body.get("kind", "qa"), "status": body.get("status", "done"),
|
||
"blocks": body.get("blocks", []), "raw": None,
|
||
"created_at": _now(), "updated_at": _now()}
|
||
|
||
|
||
@app.patch("/api/sessions/{sid}/messages/{mid}")
|
||
async def patch_message(sid: str, mid: str, request: Request):
|
||
body = await request.json()
|
||
return {"id": mid, "role": "assistant", "kind": "qa",
|
||
"status": body.get("status", "done"), "blocks": body.get("blocks", []),
|
||
"raw": body.get("raw"), "created_at": _now(), "updated_at": _now()}
|
||
|
||
|
||
@app.get("/api/sessions/{sid}/sources")
|
||
async def session_sources(sid: str):
|
||
return load("sources_aggregate.json")
|
||
|
||
|
||
# ---------- 知识图谱 ----------
|
||
|
||
@app.get("/api/kg/status")
|
||
async def kg_status():
|
||
return load("kg_status.json")
|
||
|
||
|
||
@app.get("/api/kg/overview")
|
||
async def kg_overview():
|
||
return load("kg_overview.json")
|
||
|
||
|
||
@app.get("/api/kg/entities")
|
||
async def kg_entities(q: str = "", limit: int = 20):
|
||
return load("kg_entities.json")
|
||
|
||
|
||
@app.get("/api/kg/entities/{name}")
|
||
async def kg_entity(name: str):
|
||
return load("kg_entity.json")
|
||
|
||
|
||
@app.get("/api/kg/entities/{name}/neighborhood")
|
||
async def kg_neighborhood(name: str, depth: int = 2, limit: int = 50):
|
||
return load("kg_neighborhood.json")
|
||
|
||
|
||
# ---------- 抽取任务 ----------
|
||
|
||
@app.get("/api/extraction/jobs")
|
||
async def extraction_jobs():
|
||
return load("extraction_jobs.json")
|
||
|
||
|
||
@app.get("/api/extraction/jobs/{job_id}")
|
||
async def extraction_job(job_id: str):
|
||
return load("extraction_job.json")
|
||
|
||
|
||
@app.get("/api/extraction/processing-status")
|
||
async def extraction_processing_status():
|
||
return load("extraction_processing_status.json")
|
||
|
||
|
||
@app.get("/api/extraction/run-log")
|
||
async def extraction_run_log():
|
||
return {"text": "2026-08-25 10:00:01 INFO MinerU 解析完成,11 sections\n2026-08-25 10:03:44 INFO 实体识别 712 entities\n2026-08-25 10:12:09 INFO 关系抽取 751 relations\n"}
|
||
|
||
|
||
@app.get("/api/extraction/summary")
|
||
async def extraction_summary():
|
||
return load("extraction_summary.json")
|
||
|
||
|
||
@app.post("/api/extraction/upload")
|
||
async def extraction_upload(request: Request):
|
||
return load("extraction_upload.json")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
import uvicorn
|
||
|
||
uvicorn.run(app, host="127.0.0.1", port=8099, log_level="info")
|