"""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 from src.utils import get_data_root router = APIRouter(prefix="/api/projects/{project_id}", tags=["blackboard"]) def _bb(project_id: str) -> Blackboard: return Blackboard(get_data_root() / project_id / "blackboard.db") def _q(project_id: str) -> Queries: return Queries(get_data_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