diff --git a/src/api/blackboard_routes.py b/src/api/blackboard_routes.py index 2d79141..469f14b 100644 --- a/src/api/blackboard_routes.py +++ b/src/api/blackboard_routes.py @@ -31,9 +31,10 @@ def _q(project_id: str) -> Queries: @router.get("/tasks") async def list_tasks(project_id: str, status: Optional[str] = None, - assignee: Optional[str] = None): + assignee: Optional[str] = None, + card_id: Optional[str] = None): bb = _bb(project_id) - tasks = bb.list_tasks(status=status, assignee=assignee) + tasks = bb.list_tasks(status=status, assignee=assignee, card_id=card_id) return {"tasks": [_task_to_dict(t) for t in tasks]} diff --git a/src/api/mail_routes.py b/src/api/mail_routes.py new file mode 100644 index 0000000..6933fae --- /dev/null +++ b/src/api/mail_routes.py @@ -0,0 +1,143 @@ +"""API 路由 — Mail Tab(v2.7) + +Mail 是一个特殊的 Project (_mail),每个 Mail 是一个点对点的双节点 Task。 +显示形式为独立 Tab,以列表形式展示。 +""" + +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 +from src.blackboard.db import init_db +from src.utils import get_data_root + +router = APIRouter(prefix="/api/mail", tags=["mail"]) + +MAIL_PROJECT_ID = "_mail" + + +def _db_path() -> Path: + root = get_data_root() + db = root / MAIL_PROJECT_ID / "blackboard.db" + db.parent.mkdir(parents=True, exist_ok=True) + init_db(db) + return db + + +def _bb() -> Blackboard: + return Blackboard(_db_path()) + + +@router.get("") +async def list_mail(status: Optional[str] = None, + _from: Optional[str] = None, + to: Optional[str] = None, + limit: int = Query(50, le=200)): + """Mail 列表""" + bb = _bb() + tasks = bb.list_tasks(status=status, assignee=to) + + result = [] + for t in tasks: + # 解析 Mail 元数据 + meta = json.loads(t.must_haves or "{}") if t.must_haves else {} + result.append({ + "id": t.id, + "title": t.title, + "from": meta.get("from", t.assigned_by), + "to": t.assignee, + "status": t.status, + "type": meta.get("type", "inform"), + "is_read": meta.get("is_read", False), + "created_at": t.created_at, + "description": t.description, + }) + + return {"mails": result, "total": len(result)} + + +@router.get("/{mail_id}") +async def get_mail(mail_id: str): + """Mail 详情""" + bb = _bb() + task = bb.get_task(mail_id) + if not task: + raise HTTPException(404, f"Mail not found: {mail_id}") + + meta = json.loads(task.must_haves or "{}") if task.must_haves else {} + return { + "id": task.id, + "title": task.title, + "from": meta.get("from", task.assigned_by), + "to": task.assignee, + "status": task.status, + "type": meta.get("type", "inform"), + "is_read": meta.get("is_read", False), + "created_at": task.created_at, + "description": task.description, + "outputs": [dict(o.__dict__) for o in bb.get_outputs(mail_id)], + "comments": [dict(c.__dict__) for c in bb.get_comments(mail_id)], + } + + +@router.patch("/{mail_id}") +async def update_mail(mail_id: str, body: Dict[str, Any]): + """更新 Mail(标记已读/已执行)""" + bb = _bb() + task = bb.get_task(mail_id) + if not task: + raise HTTPException(404, f"Mail not found: {mail_id}") + + # 更新 must_haves 里的 is_read + meta = json.loads(task.must_haves or "{}") if task.must_haves else {} + if "is_read" in body: + meta["is_read"] = body["is_read"] + + # 如果标记为已执行,更新状态 + if body.get("mark_executed"): + meta["is_read"] = True + if task.status not in ("done", "cancelled"): + bb.update_task_status(mail_id, "done", agent="mail-api") + + # 写回 must_haves + conn = bb._conn() + try: + conn.execute("BEGIN IMMEDIATE") + conn.execute( + "UPDATE tasks SET must_haves=? WHERE id=?", + (json.dumps(meta), mail_id), + ) + conn.commit() + finally: + conn.close() + + return {"ok": True} + + +@router.post("") +async def send_mail(body: Dict[str, Any]): + """发送 Mail(创建 Task)""" + bb = _bb() + meta = { + "from": body.get("from", "user"), + "type": body.get("type", "inform"), + "is_read": False, + } + task = Task( + id=body["id"], + title=body["title"], + description=body.get("description", ""), + assignee=body.get("to"), + assigned_by=body.get("from", "user"), + must_haves=json.dumps(meta), + card_id="default", + task_type="mail", + ) + bb.create_task(task) + return {"ok": True, "mail_id": task.id}