init the project

This commit is contained in:
evyzacq
2026-05-25 15:09:42 +08:00
commit 7fc0e7852e
122 changed files with 14557 additions and 0 deletions
+191
View File
@@ -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