init the project
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
# IR 生成、验证、Diff 接口
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from server.services.ir_engine.generator import ir_generator
|
||||
from server.services.ir_engine.pipeline import ir_pipeline
|
||||
from server.services.ir_engine.validator import ir_validator
|
||||
from server.services.ir_engine.diff import ir_diff
|
||||
from server.services.prd_manager.service import prd_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
_pipeline_executor = ThreadPoolExecutor(max_workers=2)
|
||||
|
||||
|
||||
@router.get("/generate-stream")
|
||||
async def generate_ir_stream(prd_id: str = Query(...), skill_name: str = "default"):
|
||||
"""基于 PRD 生成 IR,通过 SSE 流式返回进度"""
|
||||
prd = await prd_service.get_prd(prd_id)
|
||||
if not prd:
|
||||
raise HTTPException(404, "PRD not found")
|
||||
|
||||
async def event_stream():
|
||||
# Wait for background parsing if still in progress
|
||||
if prd.get("status") == "parsing":
|
||||
yield f"data: {json.dumps({'stage': 0, 'status': 'running', 'message': '文档解析中,请稍候...', 'detail': '图片分析可能需要 30-60 秒', 'stage_total': 4})}\n\n"
|
||||
waited = 0
|
||||
while waited < 120:
|
||||
await asyncio.sleep(1)
|
||||
waited += 1
|
||||
p = await prd_service.get_prd(prd_id)
|
||||
if not p or p.get("status") in ("ready", "error"):
|
||||
break
|
||||
if waited % 5 == 0:
|
||||
yield f"data: {json.dumps({'stage': 0, 'status': 'running', 'message': f'文档解析中...({waited}s)', 'stage_total': 4})}\n\n"
|
||||
p = await prd_service.get_prd(prd_id)
|
||||
if p and p.get("status") == "error":
|
||||
yield f"data: {json.dumps({'error': p.get('error', '解析失败'), 'stage_total': 4})}\n\n"
|
||||
return
|
||||
|
||||
# Reload after parsing
|
||||
prd2 = await prd_service.get_prd(prd_id)
|
||||
parsed_doc = _load_parsed_doc(prd2 or prd)
|
||||
if not parsed_doc:
|
||||
yield f"data: {json.dumps({'error': '无法加载解析后的文档'})}\n\n"
|
||||
return
|
||||
|
||||
yield f"data: {json.dumps({'stage': 0, 'status': 'done', 'message': '文档解析完成', 'stage_total': 4})}\n\n"
|
||||
|
||||
# Run pipeline in thread → Queue → SSE
|
||||
queue: asyncio.Queue = asyncio.Queue()
|
||||
|
||||
def _run_in_thread():
|
||||
try:
|
||||
for event in ir_pipeline.run_streaming(prd_id, parsed_doc):
|
||||
queue.put_nowait(event)
|
||||
except Exception as e:
|
||||
logger.exception("Pipeline error")
|
||||
queue.put_nowait({"error": str(e), "done": True})
|
||||
finally:
|
||||
queue.put_nowait(None)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_in_executor(_pipeline_executor, _run_in_thread)
|
||||
|
||||
while True:
|
||||
event = await queue.get()
|
||||
if event is None:
|
||||
break
|
||||
yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
|
||||
yield "data: {\"done\": true}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
event_stream(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _load_parsed_doc(prd: dict) -> dict | None:
|
||||
"""Load parsed document from disk or build from PRD data."""
|
||||
import os as _os
|
||||
parsed_path = prd.get("parsed_path", "")
|
||||
if parsed_path and _os.path.isfile(parsed_path):
|
||||
try:
|
||||
with open(parsed_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sections = prd.get("sections", [])
|
||||
if not sections and prd.get("full_text"):
|
||||
sections = [{"source": "正文", "blocks": [{"type": "para", "index": 1, "text": prd["full_text"]}], "images": []}]
|
||||
|
||||
if sections or prd.get("full_text"):
|
||||
return {
|
||||
"source": "",
|
||||
"sections": sections,
|
||||
"image_sources": {},
|
||||
"image_analysis": prd.get("images", []),
|
||||
"resolved_conflicts": [],
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
@router.post("/generate")
|
||||
async def generate_ir(prd_id: str, skill_name: str = "default"):
|
||||
"""基于 PRD 和 Skill 生成 IR"""
|
||||
prd = await prd_service.get_prd(prd_id)
|
||||
if not prd:
|
||||
raise HTTPException(404, "PRD not found")
|
||||
|
||||
prd_text = prd.get("full_text", "")
|
||||
if not prd_text:
|
||||
raise HTTPException(400, "PRD has no text content")
|
||||
|
||||
result = await ir_generator.generate(prd_id, prd_text, skill_name)
|
||||
if "error" in result:
|
||||
raise HTTPException(500, result["error"])
|
||||
|
||||
return {
|
||||
"ir_id": result["ir_id"],
|
||||
"prd_id": result["prd_id"],
|
||||
"yaml_content": result["yaml_content"],
|
||||
"ir_json": result.get("ir_json", {}),
|
||||
"audit": result.get("audit", {}),
|
||||
"audit_report": result.get("audit_report", ""),
|
||||
"skill_used": result["skill_used"],
|
||||
"created_at": result["created_at"],
|
||||
"pipeline_stats": result.get("pipeline_stats", {}),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{ir_id}")
|
||||
async def get_ir(ir_id: str):
|
||||
"""获取 IR 详情"""
|
||||
ir = await ir_generator.get_ir(ir_id)
|
||||
if not ir:
|
||||
raise HTTPException(404, "IR not found")
|
||||
return ir
|
||||
|
||||
|
||||
@router.put("/{ir_id}")
|
||||
async def update_ir(ir_id: str, body: dict):
|
||||
"""更新 IR 内容(编辑后同步回后端)"""
|
||||
ir = await ir_generator.get_ir(ir_id)
|
||||
if not ir:
|
||||
raise HTTPException(404, "IR not found")
|
||||
new_content = body.get("yaml_content", "")
|
||||
if new_content:
|
||||
ir["yaml_content"] = new_content
|
||||
try:
|
||||
ir["ir_json"] = json.loads(new_content)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return {"ir_id": ir_id, "status": "updated"}
|
||||
|
||||
|
||||
@router.post("/{ir_id}/validate")
|
||||
async def validate_ir(ir_id: str):
|
||||
"""验证 IR 是否符合 Schema 和 Principles"""
|
||||
ir = await ir_generator.get_ir(ir_id)
|
||||
if not ir:
|
||||
raise HTTPException(404, "IR not found")
|
||||
|
||||
result = await ir_validator.validate(ir["yaml_content"], ir.get("skill_used", "default"))
|
||||
return {
|
||||
"ir_id": ir_id,
|
||||
"valid": result["valid"],
|
||||
"issues": result["issues"],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/diff")
|
||||
async def diff_ir(ir_id_a: str, ir_id_b: str):
|
||||
"""对比两个 IR 版本的差异"""
|
||||
result = await ir_diff.diff(ir_id_a, ir_id_b)
|
||||
if "error" in result:
|
||||
raise HTTPException(400, result["error"])
|
||||
return result
|
||||
Reference in New Issue
Block a user