Files
sanguo_moziplus_v2/src/api/project_routes.py
T
cfdaily d58e38d58f
CI / lint (pull_request) Successful in 6s
CI / test (pull_request) Successful in 9s
CI / notify-on-failure (pull_request) Successful in 0s
fix(lint): 修复 PR #14 引入的 lint 回退 (119→0)
PR #14 从旧分支复制文件导致回退了 PR #10 的 lint 修复。
修复内容:
- autoflake 移除未使用导入/变量
- autopep8 修复缩进/空格
- 手动修复 F821(pathlib→Path), F541(f-string), F841(未使用变量)
- 所有修复均通过 flake8 --max-line-length=120 --extend-ignore=E501 检查 (0 errors)
2026-06-09 23:53:29 +08:00

205 lines
7.6 KiB
Python

"""API 路由 — 项目管理"""
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict
from fastapi import APIRouter, HTTPException, Query
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:
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, physical: bool = Query(False)):
"""删除项目(默认逻辑删除,physical=true 物理删除)"""
reg = _registry()
info = reg.get_project(project_id)
if not info:
raise HTTPException(404, f"Project not found: {project_id}")
if physical:
result = reg.physical_delete_project(project_id)
if not result:
raise HTTPException(500, "Physical delete failed")
return {"ok": True, "deleted": result}
else:
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}