auto-sync: 2026-05-18 12:53:50

This commit is contained in:
cfdaily
2026-05-18 12:53:50 +08:00
parent 3fa790a892
commit afe9f18291
+230
View File
@@ -0,0 +1,230 @@
"""API 路由 — Mail Tabv2.7
Mail 是一个特殊的 Project (_mail),每封 Mail 是一个两点 Taskfrom → to)。
显示为独立 Tab,list 形式展示:时间 | From | To | Title | 状态(已读/已执行)。
"""
from __future__ import annotations
import json
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, HTTPException, Query
from src.blackboard.db import init_db
from src.blackboard.models import Task
from src.blackboard.operations import Blackboard
from src.blackboard.queries import Queries
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())
def _q() -> Queries:
return Queries(_db_path())
def _mail_meta(task: Task) -> Dict[str, Any]:
"""从 Task 的 must_haves 字段解析 Mail 元数据"""
if task.must_haves:
try:
return json.loads(task.must_haves)
except (json.JSONDecodeError, TypeError):
pass
return {}
def _task_to_mail(task: Task) -> Dict[str, Any]:
"""Task → Mail JSON"""
meta = _mail_meta(task)
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"),
"performative": meta.get("performative", "inform"),
"is_read": meta.get("is_read", False),
"created_at": task.created_at,
"updated_at": task.updated_at,
"description": task.description,
"conversation_id": meta.get("conversation_id"),
"in_reply_to": meta.get("in_reply_to"),
}
@router.get("")
async def list_mail(
status: Optional[str] = None,
from_agent: Optional[str] = None,
to_agent: Optional[str] = None,
unread: Optional[bool] = None,
limit: int = Query(50, le=200),
):
"""Mail 列表(按时间倒序)"""
bb = _bb()
tasks = bb.list_tasks(status=status, assignee=to_agent)
mails = []
for t in tasks:
meta = _mail_meta(t)
# from 过滤
if from_agent and meta.get("from", t.assigned_by) != from_agent:
continue
# unread 过滤
if unread is True and meta.get("is_read", False):
continue
mails.append(_task_to_mail(t))
# 按时间倒序
mails.sort(key=lambda m: m.get("created_at", ""), reverse=True)
mails = mails[:limit]
return {"mails": mails, "total": len(mails)}
@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}")
result = _task_to_mail(task)
# 附加 comments(用于对话线程)
comments = bb.get_comments(mail_id)
result["comments"] = [
{
"id": c.id,
"author": c.author,
"type": c.comment_type,
"body": c.body,
"created_at": c.created_at,
}
for c in comments
]
return result
@router.post("")
async def send_mail(body: Dict[str, Any]):
"""发送 Mail(创建 Task"""
bb = _bb()
mail_id = body.get("id", f"mail-{int(datetime.now().timestamp() * 1000)}")
meta = {
"from": body.get("from", "user"),
"type": body.get("type", "text"),
"performative": body.get("performative", "inform"),
"is_read": False,
"conversation_id": body.get("conversation_id"),
"in_reply_to": body.get("in_reply_to"),
}
task = Task(
id=mail_id,
title=body.get("title", ""),
description=body.get("text", body.get("description", "")),
assignee=body.get("to"),
assigned_by=body.get("from", "user"),
must_haves=json.dumps(meta),
task_type="mail",
status="done" if body.get("type") == "inform" else "pending",
)
bb.create_task(task)
return {"ok": True, "mail_id": task.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}")
meta = _mail_meta(task)
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=?, updated_at=datetime('now') WHERE id=?",
(json.dumps(meta), mail_id),
)
conn.commit()
finally:
conn.close()
return {"ok": True}
@router.get("/agents/list")
async def list_mail_agents():
"""列出参与过 Mail 的所有 Agent(用于筛选)"""
q = _q()
conn = q._conn()
try:
# 从 assigned_by 和 assignee 去重
senders = conn.execute(
"SELECT DISTINCT assigned_by FROM tasks WHERE assigned_by IS NOT NULL"
).fetchall()
receivers = conn.execute(
"SELECT DISTINCT assignee FROM tasks WHERE assignee IS NOT NULL"
).fetchall()
agents = list(set(
[r["assigned_by"] for r in senders] +
[r["assignee"] for r in receivers]
))
agents.sort()
return {"agents": agents}
finally:
conn.close()
@router.get("/summary")
async def mail_summary():
"""Mail 摘要(未读数、总数)"""
q = _q()
bb = _bb()
all_tasks = bb.list_tasks()
total = len(all_tasks)
unread = 0
by_type: Dict[str, int] = {}
for t in all_tasks:
meta = _mail_meta(t)
if not meta.get("is_read", False):
unread += 1
t = meta.get("type", "inform")
by_type[t] = by_type.get(t, 0) + 1
return {"total": total, "unread": unread, "by_type": by_type}