187 lines
7.2 KiB
Python
187 lines
7.2 KiB
Python
"""API 路由 — 项目管理"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
from fastapi import APIRouter, HTTPException
|
||
|
||
from src.blackboard.registry import ProjectRegistry
|
||
from src.utils import get_data_root
|
||
|
||
router = APIRouter(prefix="/api/projects", tags=["projects"])
|
||
|
||
|
||
def _registry() -> ProjectRegistry:
|
||
return ProjectRegistry(get_data_root())
|
||
|
||
|
||
@router.get("")
|
||
async def list_projects():
|
||
reg = _registry()
|
||
projects = reg.list_projects()
|
||
from pathlib import Path
|
||
import sqlite3
|
||
# 实时统计每个项目的任务数
|
||
for pid, info in projects.items():
|
||
if info.get("status") in ("archived", "deleted"):
|
||
continue
|
||
db_path = Path(reg.root) / pid / "blackboard.db"
|
||
if db_path.exists():
|
||
try:
|
||
conn = sqlite3.connect(str(db_path), timeout=5)
|
||
total = conn.execute("SELECT COUNT(*) FROM tasks WHERE status != 'cancelled'").fetchone()[0]
|
||
active = conn.execute("SELECT COUNT(*) FROM tasks WHERE COALESCE(archived,0)=0").fetchone()[0]
|
||
archived = total - active
|
||
conn.close()
|
||
info['task_count'] = active
|
||
info['task_count_total'] = total
|
||
info['task_count_archived'] = archived
|
||
except Exception:
|
||
pass
|
||
# 虚拟项目 _general:如果 blackboard.db 存在则插入
|
||
general_db = Path(reg.root) / "_general" / "blackboard.db"
|
||
if general_db.exists() and "_general" not in projects:
|
||
try:
|
||
conn = sqlite3.connect(str(general_db), timeout=5)
|
||
total = conn.execute("SELECT COUNT(*) FROM tasks WHERE status != 'cancelled'").fetchone()[0]
|
||
active = conn.execute("SELECT COUNT(*) FROM tasks WHERE COALESCE(archived,0)=0").fetchone()[0]
|
||
conn.close()
|
||
projects["_general"] = {
|
||
"id": "_general", "name": "一般任务", "description": "无项目归属的通用任务",
|
||
"status": "active", "source": "virtual",
|
||
"task_count": active, "task_count_total": total, "task_count_archived": total - active,
|
||
}
|
||
except Exception:
|
||
pass
|
||
elif "_general" in projects:
|
||
general_db_check = Path(reg.root) / "_general" / "blackboard.db"
|
||
if general_db_check.exists():
|
||
try:
|
||
conn = sqlite3.connect(str(general_db_check), timeout=5)
|
||
total = conn.execute("SELECT COUNT(*) FROM tasks WHERE status != 'cancelled'").fetchone()[0]
|
||
active = conn.execute("SELECT COUNT(*) FROM tasks WHERE COALESCE(archived,0)=0").fetchone()[0]
|
||
conn.close()
|
||
projects["_general"]["task_count"] = active
|
||
projects["_general"]["task_count_total"] = total
|
||
projects["_general"]["task_count_archived"] = total - active
|
||
except Exception:
|
||
pass
|
||
# 不过滤 archived 项目,前端按任务级 archived 字段自行筛选
|
||
return {"projects": projects}
|
||
|
||
|
||
@router.post("")
|
||
async def create_project(body: Dict[str, Any]):
|
||
reg = _registry()
|
||
try:
|
||
info = reg.create_project(
|
||
body["id"], body["name"],
|
||
agents=body.get("agents", []),
|
||
description=body.get("description", ""),
|
||
)
|
||
return {"ok": True, "project_id": body["id"]}
|
||
except ValueError as e:
|
||
raise HTTPException(409, str(e))
|
||
|
||
|
||
@router.get("/{project_id}")
|
||
async def get_project(project_id: str):
|
||
reg = _registry()
|
||
info = reg.get_project(project_id)
|
||
if not info:
|
||
raise HTTPException(404, f"Project not found: {project_id}")
|
||
return info
|
||
|
||
|
||
@router.post("/{project_id}/archive")
|
||
async def archive_project(project_id: str):
|
||
reg = _registry()
|
||
if not reg.archive_project(project_id):
|
||
raise HTTPException(404, f"Project not found: {project_id}")
|
||
return {"ok": True}
|
||
|
||
|
||
@router.delete("/{project_id}")
|
||
async def delete_project(project_id: str):
|
||
"""逻辑删除项目(status→deleted)"""
|
||
reg = _registry()
|
||
# 检查项目存在
|
||
info = reg.get_project(project_id)
|
||
if not info:
|
||
raise HTTPException(404, f"Project not found: {project_id}")
|
||
if not reg.delete_project(project_id):
|
||
raise HTTPException(500, "Delete failed")
|
||
return {"ok": True}
|
||
|
||
|
||
@router.patch("/{project_id}")
|
||
async def update_project(project_id: str, body: Dict[str, Any]):
|
||
"""更新项目元数据(name/description 等)"""
|
||
reg = _registry()
|
||
allowed = {"name", "description", "status"}
|
||
updates = {k: v for k, v in body.items() if k in allowed}
|
||
if not updates:
|
||
return {"ok": True}
|
||
if not reg.update_project(project_id, **updates):
|
||
raise HTTPException(404, f"Project not found: {project_id}")
|
||
return {"ok": True}
|
||
|
||
|
||
@router.post("/{project_id}/tasks/{task_id}/move")
|
||
async def move_task(project_id: str, task_id: str, body: Dict[str, Any]):
|
||
"""移动任务到另一个项目(含子任务一起移动)"""
|
||
target_project = body.get("target_project_id")
|
||
if not target_project:
|
||
raise HTTPException(422, "Missing target_project_id")
|
||
|
||
reg = _registry()
|
||
# 验证目标项目存在
|
||
target = reg.get_project(target_project)
|
||
if not target or target.get("status") not in ("active",):
|
||
raise HTTPException(404, f"Target project not found: {target_project}")
|
||
|
||
# 从源项目读任务
|
||
from src.blackboard.operations import Blackboard
|
||
from src.blackboard.models import Task as TaskModel
|
||
src_bb = Blackboard(Path(reg.root) / project_id / "blackboard.db")
|
||
task = src_bb.get_task(task_id)
|
||
if not task:
|
||
raise HTTPException(404, f"Task not found: {task_id}")
|
||
|
||
# 查找子任务
|
||
children = src_bb.list_tasks(parent_task=task_id)
|
||
|
||
tgt_bb = Blackboard(Path(reg.root) / target_project / "blackboard.db")
|
||
moved_ids = []
|
||
|
||
# 先移动子任务
|
||
for child in children:
|
||
moved_child = TaskModel(
|
||
id=child.id, title=child.title, description=child.description,
|
||
status="pending", assignee=child.assignee, assigned_by=child.assigned_by,
|
||
priority=child.priority, task_type=child.task_type,
|
||
risk_level=child.risk_level, parent_task=child.parent_task,
|
||
stage=child.stage, stages_json=child.stages_json,
|
||
depends_on=child.depends_on, must_haves=child.must_haves,
|
||
)
|
||
tgt_bb.create_task(moved_child)
|
||
src_bb.update_task_status(child.id, "cancelled", detail=f"Moved to {target_project}")
|
||
moved_ids.append(child.id)
|
||
|
||
# 移动主任务
|
||
moved_task = TaskModel(
|
||
id=task.id, title=task.title, description=task.description,
|
||
status="pending", assignee=task.assignee, assigned_by=task.assigned_by,
|
||
priority=task.priority, task_type=task.task_type,
|
||
risk_level=task.risk_level, parent_task=task.parent_task,
|
||
stage=task.stage, stages_json=task.stages_json,
|
||
depends_on=task.depends_on, must_haves=task.must_haves,
|
||
)
|
||
tgt_bb.create_task(moved_task)
|
||
src_bb.update_task_status(task_id, "cancelled", detail=f"Moved to {target_project}")
|
||
moved_ids.insert(0, task_id)
|
||
|
||
return {"ok": True, "moved_to": target_project, "moved_ids": moved_ids}
|