diff --git a/src/daemon/mail_notify.py b/src/daemon/mail_notify.py new file mode 100644 index 0000000..9e7d632 --- /dev/null +++ b/src/daemon/mail_notify.py @@ -0,0 +1,104 @@ +"""Mail 失败通知 — 以 system 身份通知发件人""" + +from __future__ import annotations + +import json +import logging +from datetime import datetime +from pathlib import Path +from typing import Optional + +from src.blackboard.models import Task +from src.blackboard.operations import Blackboard + +logger = logging.getLogger(__name__) + +# 邮件通知正文模板(统一模板,包含所有可能的失败原因和建议) +_NOTIFY_TEMPLATE = """你的邮件投递失败了。 + +📧 原始邮件:「{title}」 +👤 收件人:{to_agent} +❌ 失败原因:{reason} + +常见失败原因及处理建议: +• no_reply_found:收件人未回复。建议重发邮件,或通过黑板任务方式联系 +• auth_failed:收件人认证失败。需检查 Agent 配置,联系姜维(jiangwei-infra)排查 +• crash_limit:收件人处理时多次崩溃。系统异常,建议稍后重试 +• task_timeout:处理超时。建议重发或通过其他方式联系 +• 其他原因:建议联系副军师(pangtong-fujunshi)排查 + +——系统自动通知""" + + +def _is_mail_project(db_path: Path) -> bool: + """从 db_path 推断是否为 _mail 项目""" + path_str = str(db_path) + return "/_mail/" in path_str or path_str.endswith("_mail.db") + + +def notify_mail_failed(db_path: Path, original_mail_id: str, + reason: str, detail: Optional[dict] = None) -> None: + """Mail 失败后以 system 身份给发件人发通知邮件 + + 直接通过 Blackboard 创建 Task,不走 HTTP API。 + 防递归:检查原邮件 must_hives.system_notify,为 true 则跳过。 + """ + try: + bb = Blackboard(db_path) + original = bb.get_task(original_mail_id) + if not original: + logger.warning("notify_mail_failed: original mail %s not found", original_mail_id) + return + + # 解析原邮件元数据 + meta = json.loads(original.must_haves) if original.must_haves else {} + + # 防递归:系统通知邮件失败不再发通知 + if meta.get("system_notify"): + logger.info("Mail %s: system notify mail failed, skipping recursive notification", original_mail_id) + return + + # 获取发件人(优先 assigned_by,fallback must_haves.from) + from_agent = original.assigned_by or meta.get("from", "") + to_agent = original.assignee or "" + title = original.title or "" + + if not from_agent: + logger.warning("notify_mail_failed: cannot determine sender for mail %s", original_mail_id) + return + + # 构造通知正文 + text = _NOTIFY_TEMPLATE.format( + title=title, + to_agent=to_agent, + reason=reason, + ) + + # 创建通知邮件 Task + notify_id = f"mail-{int(datetime.now().timestamp() * 1000)}" + notify_meta = { + "type": "inform", + "performative": "inform", + "is_read": False, + "conversation_id": meta.get("conversation_id", ""), + "in_reply_to": original_mail_id, + "from": "system", + "system_notify": True, + } + + notify_task = Task( + id=notify_id, + title=f"[投递失败] {title}", + description=text, + assignee=from_agent, + assigned_by="system", + must_haves=json.dumps(notify_meta, ensure_ascii=False), + task_type="mail", + status="pending", + ) + bb.create_task(notify_task) + logger.info("Mail %s: sent failure notification to %s (reason=%s, notify_id=%s)", + original_mail_id, from_agent, reason, notify_id) + + except Exception as e: + logger.warning("notify_mail_failed: failed to send notification for mail %s: %s", original_mail_id, e)