Files
sanguo_moziplus_v2/docs/design/mail-failure-notification.md
T
cfdaily cef76a72e0
Deploy / ci (push) Waiting to run
Deploy / deploy (push) Blocked by required conditions
Deploy / notify-deploy-failure (push) Blocked by required conditions
docs: Mail 失败通知设计文档 draft
2026-06-07 00:52:27 +08:00

124 lines
5.1 KiB
Markdown
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.
# Mail 失败通知设计
> 版本:v1.0-draft | 日期:2026-06-07 | 作者:庞统 | 状态:待确认
## 1. 背景
当前 Mail 失败后没有通知发件人。F2 修复让所有 `_mark_task("failed")` 统一 @pangtong-fujunshi,但这对 Mail 不合理:
- Mail 是 A→B 点对点通信,失败应通知发件人 A
- F2 覆盖的是 spawn 级失败(crashed/auth_failed 等),属系统基础设施问题,@pangtong 合理
- **但 Mail 唯一特有的失败——no_reply_found(幻觉门控,Agent 没回复 request)——没有任何通知**
### 失败场景分类
| 触发点 | 场景 | 当前通知 | 应该通知谁 |
|--------|------|---------|-----------|
| `_mark_task("failed")` | spawn 级失败:crashed、auth_failed、crash_limit、timeout 等 | @pangtong(F2) | 两者都该通知:庞统(系统问题)+ 发件人(投递失败) |
| `_mail_auto_complete` no_reply_found | Agent 正常退出但没回复 request 类邮件 | 无 | 发件人 |
## 2. 设计方案
**核心机制**Mail 最终标 failed 时,daemon 给 from_agent 发一封系统 inform 邮件,告知原因和处理建议。
### 2.1 实现位置
新增 `src/daemon/mail_notify.py`,统一函数 `notify_mail_failed`
```python
def notify_mail_failed(db_path: Path, original_mail_id: str, reason: str, detail: dict = None):
"""Mail 失败后给发件人发系统通知邮件
直接操作 Blackboard 创建 task,不走 POST /api/mail(避免 A9 校验)。
"""
```
### 2.2 触发点
**触发点 1**`_mail_auto_complete` 中 no_reply_found 标 failed 之后(dispatcher.py
- 位置:标 failed 成功后、return 之前
- 调用:`notify_mail_failed(db_path, task_id, "no_reply_found")`
- from_agent 从 must_haves 解析
**触发点 2**`_mark_task` 中 status=="failed" 且是 _mail 项目时(spawner.py
- 位置:现有 F2 @pangtong 逻辑之后
- 条件:需要判断 task 是否属于 `_mail` 项目
- 调用:`notify_mail_failed(db_path, task_id, reason)`
- from_agent 同样从 must_haves 解析
### 2.3 通知邮件内容
通知邮件字段:
| 字段 | 值 |
|------|------|
| task_type | `"mail"` |
| assigned_by | `"daemon"` |
| assignee | from_agent(原邮件发件人) |
| status | `"pending"`(走正常投递流程,Agent 会收到) |
| title | `"[投递失败] {原邮件title}"` |
| must_haves.type | `"inform"` |
| must_haves.system_notify | `true`(防递归标记) |
| must_haves.in_reply_to | 原邮件 task_id |
通知正文按 reason 区分:
| reason | 正文模板 | 处理建议 |
|--------|---------|---------|
| `no_reply_found` | "你发送的 request 邮件「{title}」未被回复" | "建议重发邮件,或通过黑板任务方式联系" |
| `auth_failed` | "邮件「{title}」投递时认证失败" | "需检查 Agent 配置,联系姜维排查" |
| `crash_limit` | "邮件「{title}」投递时多次崩溃" | "系统异常,建议稍后重试" |
| `task_timeout` | "邮件「{title}」处理超时" | "建议重发或通过其他方式联系" |
| 其他 | "邮件「{title}」投递失败(原因:{reason}" | "建议联系庞统排查" |
### 2.4 防递归
系统通知邮件本身也可能失败(spawn 崩溃等),必须防止无限递归:
**方案**:通知邮件的 must_haves 中加 `"system_notify": true`。在触发点检查时,如果原邮件的 must_haves 含 `system_notify: true`,跳过不再发通知。
```python
# 防递归:系统通知邮件失败不再发通知
meta = json.loads(original_task.must_haves) if original_task.must_haves else {}
if meta.get("system_notify"):
return # 系统邮件失败,不再递归通知
```
### 2.5 from_agent 获取
原 Mail 的发件人存在两个等价位置:
- `task.assigned_by`
- `must_haves` JSON 的 `"from"` 字段
优先用 `assigned_by`(无需解析 JSON),fallback 解析 must_haves。
## 3. 不改的
| 项目 | 原因 |
|------|------|
| F2 @pangtong 逻辑 | spawn 级失败仍需要庞统知道,保留 |
| `POST /api/mail` | 不新增"系统发件人"角色,通知直接操作 Blackboard |
| no_reply_found 的标 failed 逻辑 | 只在标 failed 之后加通知,不改判定逻辑 |
| inform 类型邮件的完成逻辑 | inform 直接标 done,不存在 no_reply_found 场景 |
## 4. 改动范围
| 文件 | 改动 | 预估行数 |
|------|------|---------|
| `src/daemon/mail_notify.py`(新增) | `notify_mail_failed` 函数 | ~60 行 |
| `src/daemon/dispatcher.py` | `_mail_auto_complete` 标 failed 后调 notify | ~5 行 |
| `src/daemon/spawner.py` | `_mark_task` failed 分支加 _mail 判断 + 调 notify | ~10 行 |
总计约 75 行,3 个文件。
## 5. 验证方式
| 场景 | 验证 | 预期 |
|------|------|------|
| request 邮件无回复 | Agent 不回复 → _mail_auto_complete 标 failed | 发件人收到系统通知邮件 |
| spawn 崩溃导致 Mail failed | Agent spawn crash → _mark_task failed | 发件人收到通知 + 庞统收到 @mention |
| 系统通知邮件本身失败 | 通知邮件也 crash | 不再递归(system_notify=true |
| inform 邮件正常完成 | Agent 收到 inform → 直接 done | 不触发通知 |