Files
sanguo_moziplus_v2/docs/design/mail-failure-notification.md
T
cfdaily cd1c1f76f4
Deploy / ci (push) Waiting to run
Deploy / deploy (push) Blocked by required conditions
Deploy / notify-deploy-failure (push) Blocked by required conditions
auto-sync: 2026-06-07 08:34:07
2026-06-07 08:34:07 +08:00

167 lines
6.6 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.3 | 日期:2026-06-07 | 作者:庞统 | 状态:待确认
## 1. 背景
当前 Mail 失败后没有通知发件人。F2 修复让所有 `_mark_task("failed")` 统一 @pangtong-fujunshi,但这对 Mail 不合理:
- Mail 是 A→B 点对点通信,失败应通知发件人 A,不是庞统
- **Mail 唯一特有的失败——no_reply_found(幻觉门控,Agent 没回复 request)——没有任何通知**
### 失败场景分类
| 触发点 | 场景 | 当前通知 | 应该通知谁 |
|--------|------|---------|-----------|
| `_mark_task("failed")` | spawn 级失败:crashed、auth_failed、crash_limit、timeout 等 | @pangtongF2 | 发件人(不再 @pangtong |
| `_mail_auto_complete` no_reply_found | Agent 正常退出但没回复 request 类邮件 | 无 | 发件人 |
**所有 Mail failed 都通知发件人**,不管 reason。
## 2. 设计方案
### 2.1 核心机制
Mail 最终标 failed 时,daemon 以 **系统发件人(`system`** 身份给 from_agent 发一封 inform 邮件,告知原因和处理建议。
### 2.2 系统发件人
新增 `system` 作为 Mail 的特殊发件人:
- **不暴露给外部 API**`system` 只允许 daemon 内部调用
- **实现方式**`mail_notify.py` 直接调用 `send_mail` 的内部函数(`_do_send_mail`),不走 FastAPI HTTP 路由
- `system` 不是真实 Agent,没有 main session,只用于发送通知
- 防递归:通知邮件的 must_haves 中加 `"system_notify": true`,触发前检查该标记
### 2.3 判断 _mail 项目
`_mark_task` 签名只有 `(db_path, task_id, status, detail)`,没有 project_id。从 db_path 推断:
```python
def _is_mail_project(db_path: Path) -> bool:
path_str = str(db_path)
return "/_mail/" in path_str or path_str.endswith("_mail.db")
```
覆盖两种 _mail db_path`data/_mail/blackboard.db`(新)和 `data/_mail.db`(旧)。
### 2.4 触发点
**触发点 1**`_mail_auto_complete` 中 no_reply_found 标 failed 之后(dispatcher.py
- 位置:标 failed 成功后、return 之前
- 调用:`notify_mail_failed(db_path, task_id, "no_reply_found")`
**触发点 2**`_mark_task` 中 status=="failed" 且是 _mail 项目时(spawner.py
- 位置:现有 F2 @pangtong 逻辑处
- **改动**:当 `_is_mail_project(db_path)` 时,**不 @pangtong**,改为调用 `notify_mail_failed`
- 当不是 _mail 项目时,F2 @pangtong 逻辑不变
### 2.5 实现位置
新增 `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 失败后以 system 身份给发件人发通知邮件
直接调用 _do_send_mail 内部函数,不走 HTTP。
防递归:检查原邮件 must_haves.system_notify,为 true 则跳过。
"""
```
### 2.6 通知邮件内容
| 字段 | 值 |
|------|------|
| from | `"system"` |
| to | from_agent(原邮件发件人) |
| type | `"inform"` |
| title | `"[投递失败] {原邮件title}"` |
| must_haves.system_notify | `true`(防递归标记) |
| must_haves.in_reply_to | 原邮件 task_id |
通知正文使用**统一模板**,把所有可能的失败原因和建议放在一封邮件里:
```
你的邮件投递失败了。
📧 原始邮件:「{title}」
👤 收件人:{to_agent}
❌ 失败原因:{reason}
常见失败原因及处理建议:
• no_reply_found:收件人未回复。建议重发邮件,或通过黑板任务方式联系
• auth_failed:收件人认证失败。需检查 Agent 配置,联系姜维(jiangwei-infra)排查
• crash_limit:收件人处理时多次崩溃。系统异常,建议稍后重试
• task_timeout:处理超时。建议重发或通过其他方式联系
• 其他原因:建议联系副军师(pangtong-fujunshi)排查
——系统自动通知
```
实现时 `{reason}` 替换为实际 reason 字符串,其余排查指引固定不变。
| 其他 | "邮件「{title}」投递失败(原因:{reason}" | "建议联系副军师(pangtong-fujunshi)排查" |
### 2.7 防递归
系统通知邮件本身也可能失败(spawn 崩溃等),必须防止无限递归:
```python
# 防递归:系统通知邮件失败不再发通知
meta = json.loads(original_task.must_haves) if original_task.must_haves else {}
if meta.get("system_notify"):
return # 系统邮件失败,不再递归通知
```
递归链验证:
1. 原邮件 crash → `_mark_task(failed)``_is_mail_project=true` → 不 @pangtong`notify_mail_failed`
2. 通知邮件(from=system)也 crash → `_mark_task(failed)``_is_mail_project=true``notify_mail_failed` → 检查 `system_notify=true` → 跳过 ✅
### 2.8 from_agent 获取
原 Mail 的发件人存在两个等价位置:
- `task.assigned_by`
- `must_haves` JSON 的 `"from"` 字段
优先用 `assigned_by`(无需解析 JSON),fallback 解析 must_haves。
## 3. 改动清单
### 3.1 新增文件
| 文件 | 内容 | 预估行数 |
|------|------|---------|
| `src/daemon/mail_notify.py` | `notify_mail_failed` + `_is_mail_project` | ~65 行 |
### 3.2 修改文件
| 文件 | 改动 | 预估行数 |
|------|------|---------|
| `src/daemon/dispatcher.py` | `_mail_auto_complete` 标 failed 后调 notify | ~5 行 |
| `src/daemon/spawner.py` | `_mark_task` failed 分支:_mail 项目不 @pangtong,改调 notify | ~10 行 |
| `src/api/mail_routes.py` | 拆出 `_do_send_mail` 内部函数供 daemon 调用,valid_agents 校验对内部调用跳过 from 检查 | ~15 行 |
总计约 95 行,4 个文件。
### 3.3 不改的
| 项目 | 原因 |
|------|------|
| F2 @pangtong 对 Task 的逻辑 | Task failed 仍 @pangtong,只对 Mail 不适用 |
| no_reply_found 的标 failed 逻辑 | 只在标 failed 之后加通知,不改判定逻辑 |
| inform 类型邮件的完成逻辑 | inform 直接标 done,不存在 no_reply_found 场景 |
| 外部 API 的 from 校验 | system 不走 HTTP 路由,外部无法伪造 |
## 4. 验证方式
| 场景 | 验证 | 预期 |
|------|------|------|
| request 邮件无回复 | Agent 不回复 → _mail_auto_complete 标 failed | 发件人收到 system 通知邮件 |
| spawn 崩溃导致 Mail failed | Agent spawn crash → _mark_task failed | 发件人收到 system 通知,**不** @pangtong |
| Task failed | Task spawn crash → _mark_task failed | @pangtongF2 不变) |
| 系统通知邮件本身失败 | 通知邮件也 crash | 不再递归(system_notify=true |
| inform 邮件正常完成 | Agent 收到 inform → 直接 done | 不触发通知 |
| inform 邮件 crash | inform 的 spawn 崩溃 | 发件人收到 system 通知(crash 也通知) |