Files
sanguo_moziplus_v2/src/api/mail_routes.py
T
2026-05-18 13:06:17 +08:00

219 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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("/agents/list")
async def list_mail_agents():
"""列出参与过 Mail 的所有 Agent(用于筛选)"""
q = _q()
conn = q._conn()
try:
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 摘要(未读数、总数)"""
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
mtype = meta.get("type", "inform")
by_type[mtype] = by_type.get(mtype, 0) + 1
return {"total": total, "unread": unread, "by_type": by_type}
@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")
bb.update_must_haves(mail_id, json.dumps(meta))
return {"ok": True}