---
url: /docs/agentbuff-stack/task-notifications.md
---
# 任务邮件通知

I12 的第一阶段提供可选的任务完成 / 失败邮件、投递记录和有限重试。站内任务状态继续沿用原任务工作台。第二阶段站主运营频道和已核实付款通知见[独立教程](./operator-notifications)；真实外部投递与云端维护仍待验，不能将本阶段视为整个 I12 完成。

## 配置与升级

公开配置位于 `packages/core/website.ts`：

```ts
notifications: {
  taskEmail: false,
  preferenceVersion: "2026-10-11",
  retentionDays: 30,
  operator: { enabled: false, since: null },
}
```

默认关闭。启用前完整升级迁移至 `0022_task_email_language.sql`，同步构建 API、工作台与公共页，再确认已有邮件配置与定时入口。0020 新增任务邮件选择和投递表，0022 增加语言字段，没有修改已应用的旧迁移；正常预览主库仍停在 0012，两张通知表尚未应用。关闭时界面不显示卡片，偏好 / 投递列表和定时入口不读取通知表，旧库仍能运行。

使用现有 `RESEND_API_KEY`、`RESEND_EMAIL_FROM`、`APP_NAME`、`APP_ORIGIN` 与 `BETTER_AUTH_SECRET`，不需要新的供应商密钥。`preferenceVersion` 为 1—80 字符，保留期为 1—90 天。改变通知用途时升级版本，让用户重新明确选择。新站继承该配置文件，但选择和投递记录属于各自数据库；仍需检查启用状态、独立数据库、品牌、发信地址和认证密钥。

## 用户选择与权限

启用后，设置页出现任务邮件卡片。默认不勾选，当前账号必须有已验证邮箱；勾选并保存成功后才允许新通知。取消勾选也需要保存，失败会显示未确认，并提供重新读取按钮。每次修改带当前 revision，旧标签页不能覆盖较新的选择；重复保存相同有效选择不延长起点。

服务端从新鲜数据库状态检查账号、封禁和邮箱，接收人只取当前账号，客户端不能指定邮箱、所有者或外发 URL。变更要求本站 Origin，并有限频；选择与列表属于个人账号，团队切换不会把任务邮件发送给团队其他成员。这里的选择不授权营销订阅或活动统计。

选择只保存最新状态、邮件语言、版本、起点和 revision，不作为完整同意历史。接收地址以认证密钥派生的 HMAC 摘要绑定，不在通知表复制邮箱。邮箱或认证密钥变化后，需要重新保存当前邮箱的选择；旧通知不会改投新邮箱。撤回后尚未领取的旧通知会跳过，再次开启不补发先前完成的任务。已经进入发送阶段的邮件可能仍会到达，撤回无法收回已外发请求。

## 邮件语言与升级兼容

设置页提供独立的“邮件语言”选择。默认英文，只能开启本站 `i18n.locales` 已启用的语言；选择后仍需保存。页面语言、活动团队或请求头不会自动改写邮件偏好。已保存西班牙语的账号可以在英文页面查看并保留西班牙语偏好；未提交的选择在页面语言切换或刷新时不保留。

新投递将当前偏好的语言保存到自己的记录，成功 / 失败的主题、HTML / 纯文本、任务与设置链接按该语言生成。任务 ID、所有权、结果字节和文件保留期不变，西班牙语邮件使用 `/es/tasks/:id` 与 `/es/settings`。内容摘要包含新的模板版本及记录语言，重试、租约恢复和 Worker 重启仍使用同一载荷与请求键。

显式保存不同语言会增加 revision 并重新设置选择起点，尚未领取的旧邮件会跳过，不为同一终态重复建立通知。已经领取的请求可能仍按原语言发出；更改语言不能收回正在发送的邮件。站点关闭西班牙语后，其已保存偏好仍如实显示，用户可以改成英文或撤回；排队的西班牙语请求停止，不以同一幂等键改发英文。之后重新启用语言不会补发已跳过的记录。

0022 给旧偏好填入英文，新建投递会保存明确语言；旧投递的语言字段保留空值，继续使用原英文模板和 `task-mail-v2` 内容摘要，不改写已尝试的请求。新投递使用 `task-mail-v3`。两种渲染路径暂时共存，旧 HTML / 纯文本逐字节兼容测试必须保留，不能仅把空字段补成英文后用新版模板发送。此兼容规则只用于旧任务邮件；营销邮件的语言迁移仍在后续阶段。

## 已提交结果与去重

复用现有每 15 分钟定时维护入口，从已提交的任务终态派生投递记录。没有增加第二条任务队列、公开发送探针或新的通知执行引擎。任务消费者、租约恢复、死信或管理员恢复产生的真实终态都可以被补偿扫描发现；任务成功不等待邮件。

每轮最多扫描 20 个尚无记录的终态，稳定事件为任务 ID、执行租约版本与成功 / 失败结果。数据库唯一约束处理重放和并发。仅纳入当前明确选择起点之后、保留期内完成的任务；取消、中间重试和需人工核对的任务不发送失败邮件。失效接收人保存跳过记录，避免连续占据扫描预算。扫描有积压或维护延迟时，通知可能晚于 15 分钟。

手动重试会使旧未发失败通知失效；新一轮真实终态形成新事件。发送前重新检查任务结果、租约版本、选择、邮箱和配置。通知只有任务 ID、成功 / 失败说明、本站任务页及设置页链接；没有原始输入、文件名、结果附件、签名下载地址或供应商密钥。结果只在原工作台通过登录与所有权校验下载，邮件不延长文件保留期。

## 投递状态与重试

| 状态 | 含义与处理 |
| --- | --- |
| 等待发送 | 已保存通知，尚未领取 |
| 发送中 | 一次两分钟租约正在处理，其他消费者不能同时领取 |
| 等待重试 | 供应商接收未确认，15 分钟后可再次领取 |
| 供应商已接收 | 已得到有效邮件请求回执；不等于送达收件箱或用户阅读 |
| 接收未确认，停止重试 | 耗尽三次尝试或超过第一次尝试的 23 小时范围；保留排查状态，不自动换键重发 |
| 已跳过 | 选择、账号、任务或发信配置变化；不改投新地址 |

邮件复用 `sendEmail` 的 SDK 边界，同时生成 HTML 与纯文本。通知请求超时为十秒，邮件 ID 必须有效。相同投递记录使用固定供应商幂等键，保持内容与接收人一致。接收人、品牌、Origin、发信地址、供应商密钥或环境变化会停止旧请求，避免在新地址或新供应商账号重新发送。

[Resend 官方幂等说明](https://resend.com/docs/dashboard/emails/idempotency-keys)在 2026-10-11 核对：键保留 24 小时。本模块最多尝试三次，第一次尝试后仅在 23 小时内重试，包括超时租约恢复；不会跨窗口重新使用旧键。未知返回、供应商错误或回执保存失败都不能证明邮件没有发出，不承诺绝对无重复。接收成功后数据库写入还需匹配本次未过期租约，迟到响应不能覆盖另一消费者的记录。

设置页显示本人最近 20 条、保留期内的投递状态、实际记录语言、次数与下一次可重试时间，可主动刷新。失败只保存固定原因，不记录供应商原始错误、邮箱、正文或秘密。当前不提供普通用户强制重发或管理员重发命令；未确认记录需要结合供应商回执排查后再决定处理，避免随意再发。

## 保留与本地验收

旧记录按事件时间进入清理资格，每轮最多删除 100 条；积压、关闭模块或维护故障可能延迟物理删除。列表不显示超过保留期的记录，扫描也不会把清理掉的旧任务重新入队。最新选择保留至账号删除，删除账号级联删除选择与投递；删除任务也会删除其投递记录，不影响财务审计自身的保留规则。

本地 Worker 的 `LOCAL_MAIL` 捕获全部邮件，即使环境里有真实 Resend 密钥也不会外发。捕获回执只证明本地接口完成，不能证明真实送达；开发捕获文件可能包含认证邮件和合成接收地址，不应提交仓库或公开分享。

```bash
bun --cwd apps/api validate:notifications
```

该验收只建立独立 loopback 临时数据库，应用完整迁移，运行实际 Worker、Queue、R2 和定时入口，执行成功 / 失败任务、重放、捕获故障及恢复、撤回与再次开启，并销毁全部临时资源。`--languages` 额外开启英 / 西双语，核对真实西班牙语邮件、故障后的 Worker 重启与原请求恢复；`--ui` 额外构建专用工作台供桌面 / 手机验收，不改变正常预览配置或数据库。组合 `--languages --ui` 时按提示依次保存英文、保存西班牙语、撤回，再创建 `.local/notifications-ui-stop` 结束验收；只检查页面但不完成这些写入，会明确失败。

真实发信域名、供应商故障、邮箱投递与云端定时维护仍需独立验收。站主渠道及支付事件通知已有独立配置与投递边界，接收者、内容与授权规则不复用个人任务邮件选择，见[站主运营通知](./operator-notifications)。
