diff --git a/src/api/blackboard_routes.py b/src/api/blackboard_routes.py new file mode 100644 index 0000000..511e982 --- /dev/null +++ b/src/api/blackboard_routes.py @@ -0,0 +1,186 @@ +"""API 路由 — 黑板 CRUD""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Dict, List, Optional + +from fastapi import APIRouter, HTTPException, Query + +from src.blackboard.operations import Blackboard +from src.blackboard.models import Task, Review +from src.blackboard.queries import Queries +from src.blackboard.db import VALID_STATUSES, VALID_TRANSITIONS, COMMENT_TYPES, OUTPUT_TYPES + +router = APIRouter(prefix="/api/projects/{project_id}", tags=["blackboard"]) + + +def _bb(project_id: str) -> Blackboard: + import os + root = Path(os.environ.get("BLACKBOARD_ROOT", + str(Path.home() / ".sanguo_projects" / "sanguo_moziplus_v2" / "projects"))) + return Blackboard(root / project_id / "blackboard.db") + + +def _q(project_id: str) -> Queries: + import os + root = Path(os.environ.get("BLACKBOARD_ROOT", + str(Path.home() / ".sanguo_projects" / "sanguo_moziplus_v2" / "projects"))) + return Queries(root / project_id / "blackboard.db") + + +# --- Tasks --- + +@router.get("/tasks") +async def list_tasks(project_id: str, + status: Optional[str] = None, + assignee: Optional[str] = None): + bb = _bb(project_id) + tasks = bb.list_tasks(status=status, assignee=assignee) + return {"tasks": [_task_to_dict(t) for t in tasks]} + + +@router.get("/tasks/{task_id}") +async def get_task(project_id: str, task_id: str): + bb = _bb(project_id) + task = bb.get_task(task_id) + if not task: + raise HTTPException(404, f"Task not found: {task_id}") + return _task_to_dict(task) + + +@router.post("/tasks") +async def create_task(project_id: str, body: Dict[str, Any]): + bb = _bb(project_id) + task = Task( + id=body["id"], title=body["title"], + description=body.get("description"), + task_type=body.get("task_type", "coding"), + priority=body.get("priority", 5), + assignee=body.get("assignee"), + assigned_by=body.get("assigned_by", "user"), + depends_on=json.dumps(body["depends_on"]) if "depends_on" in body else None, + risk_level=body.get("risk_level", "standard"), + ) + bb.create_task(task) + return {"ok": True, "task_id": task.id} + + +@router.post("/tasks/{task_id}/claim") +async def claim_task(project_id: str, task_id: str, body: Dict[str, Any]): + bb = _bb(project_id) + if not bb.claim_task(task_id, body["agent"]): + raise HTTPException(409, "Claim failed (already claimed or wrong assignee)") + return {"ok": True} + + +@router.post("/tasks/{task_id}/status") +async def update_status(project_id: str, task_id: str, body: Dict[str, Any]): + bb = _bb(project_id) + if not bb.update_task_status(task_id, body["status"], + agent=body.get("agent")): + raise HTTPException(409, f"Invalid transition to {body['status']}") + return {"ok": True} + + +# --- Comments --- + +@router.get("/tasks/{task_id}/comments") +async def get_comments(project_id: str, task_id: str, + comment_type: Optional[str] = None): + bb = _bb(project_id) + comments = bb.get_comments(task_id, comment_type=comment_type) + return {"comments": [dict(c.__dict__) for c in comments]} + + +@router.post("/tasks/{task_id}/comments") +async def add_comment(project_id: str, task_id: str, body: Dict[str, Any]): + bb = _bb(project_id) + cid = bb.add_comment(task_id, body["author"], body["body"], + comment_type=body.get("comment_type", "general"), + mentions=body.get("mentions")) + return {"ok": True, "comment_id": cid} + + +# --- Outputs --- + +@router.get("/tasks/{task_id}/outputs") +async def get_outputs(project_id: str, task_id: str): + bb = _bb(project_id) + return {"outputs": [dict(o.__dict__) for o in bb.get_outputs(task_id)]} + + +@router.post("/tasks/{task_id}/outputs") +async def write_output(project_id: str, task_id: str, body: Dict[str, Any]): + bb = _bb(project_id) + oid = bb.write_output(task_id, body["agent"], body["type"], + body["title"], content_path=body.get("path"), + summary=body.get("summary"), + metadata=body.get("metadata")) + return {"ok": True, "output_id": oid} + + +# --- Decisions --- + +@router.post("/tasks/{task_id}/decisions") +async def add_decision(project_id: str, task_id: str, body: Dict[str, Any]): + bb = _bb(project_id) + did = bb.add_decision(task_id, body["decider"], body["decision"], + body["rationale"], + alternatives=body.get("alternatives")) + return {"ok": True, "decision_id": did} + + +# --- Observations --- + +@router.post("/tasks/{task_id}/observations") +async def add_observation(project_id: str, task_id: str, body: Dict[str, Any]): + bb = _bb(project_id) + oid = bb.add_observation(task_id, body["observer"], body["body"], + severity=body.get("severity", "info")) + return {"ok": True, "observation_id": oid} + + +# --- Reviews --- + +@router.get("/tasks/{task_id}/reviews") +async def get_reviews(project_id: str, task_id: str): + bb = _bb(project_id) + return {"reviews": [dict(r.__dict__) for r in bb.get_reviews(task_id)]} + + +@router.post("/tasks/{task_id}/reviews") +async def add_review(project_id: str, task_id: str, body: Dict[str, Any]): + bb = _bb(project_id) + review = Review( + id=body["id"], task_id=task_id, reviewer=body["reviewer"], + review_type=body["review_type"], verdict=body["verdict"], + summary=body["summary"], confidence=body.get("confidence"), + round=body.get("round", 1), max_rounds=body.get("max_rounds", 3), + ) + bb.add_review(review) + return {"ok": True, "review_id": review.id} + + +# --- Events --- + +@router.get("/events") +async def get_events(project_id: str, limit: int = Query(50, le=200)): + q = _q(project_id) + return {"events": q.recent_events(limit)} + + +# --- Summary --- + +@router.get("/summary") +async def task_summary(project_id: str): + q = _q(project_id) + return {"summary": q.task_summary()} + + +# --- Helper --- + +def _task_to_dict(t: Task) -> Dict[str, Any]: + d = {k: v for k, v in t.__dict__.items() if v is not None} + return d