MessageWeave/AGENTS.md

268 lines
39 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.

# AGENTS.md — MessageWeave 开发守则
> 本文档是后续任何 AI coding agent 在本仓库工作的**操作守则与硬性约束**。
> 架构 / 模块接口 / 状态机 / 数据流 / 测试 / 实施阶段以 [docs/design.md](docs/design.md) 为准;
> 部署运维与发布(通用 HTTPS-only Docker / Secrets / Redis 状态层 / Webhook / Push / 外部 Cron 对账 / CI)以 [docs/deployment.md](docs/deployment.md) 为准。
>
> 三份文档职责分离,改动时请遵循 [§6 文档边界](#6-文档边界与引用关系),避免内容重复堆砌与跨文档冲突。
>
> **跨文档引用一律用稳定 ID**(`C-` / `NG-` / `MOD-` / `FLOW-` / `REQ-` 等),**不用章节号(§x.y)**;定义点位置与一句话说明见 [§7 跨文档引用索引](#7-跨文档引用索引表)。
>
> **版本基线 v3(2026-09-26,本轮 ④+⑤):代码 `afa6cec`,文档 `399b269`(⑤ 收口)。**
> 本轮新增 `src/debug.rs` 远程联调面与 6 个 `*DEBUG*` ID(§2 第 19 条、§5 测试清单、§7 六行),文档侧同步落位
> `docs/deployment.md` §2.1、`docs/reference.md` §3/§4、`README` 中英双版;各文档的行号锚点以该基线为准,代码变动后须同步重锚。
---
## 1. 项目目标
构建一个 Rust 写的 Telegram 机器人,作为 Stalwart JMAP 邮箱的**个人邮件助手**(详见 [design.md §1](docs/design.md#1-任务与范围)):
- 通过 Telegram 命令查询/阅读邮件、查看文件夹、发送邮件、管理关键词等。
- 利用 **JMAP Push HTTPS 回调**(唯一实时通道)+ 外部 Cron 对账(建议 5–10 分钟),新邮件到达时主动推送到 Telegram;**不使用** EventSource/SSE/长轮询(`NG-POLLING-SSE`/`NG-LONG-POLLING`,见 deployment.md `C-NO-LONG-CONN`)。
- **单账户**实现(`REQ-SINGLE-ACCOUNT`),多账户 = 多个 bot 实例;部署为**通用 HTTPS-only Docker 容器**(短请求模型,平台提供公网 HTTPS URL `C-HTTPS-URL`),详见 deployment.md。
- 可靠性目标:**通知可用性 ≥ 99.9%**,允许少量延迟(`NFR-NOTIFY-SLA`,策略见 deployment.md §6.4/§6.5)。
技术栈:`jmap-client`(rustls,**App Password + Basic** `C-AUTH-APP-BASIC`)+ Telegram Bot API(**版本不锁定**;渠道在 `src/channel.rs` 用 `reqwest` 自研实现,`teloxide` 未引入,见 `ARCH-DEPS-STAGE4`)+ tokio + **外部 Redis(用户托管 + AOF)**(`C-REDIS-MANAGED-AOF`;**服务端版本不锁定**,只要求能力集:Streams、XPING/PING、SET NX EX)。**未写代码前不得初始化 cargo 工程之外的内容**(仅文档阶段)。
---
## 2. 硬性安全边界(不可违反)
> 任何改动都不得破坏以下不变量。实现时必须有对应断言测试(见 §5 与 [design.md §9.3](docs/design.md#93-关键不变量测试))。
1. **查看原文 = JMAP 直取,绝不经 AI**:`/read` 与任何"看正文"路径调用 `JmapService::read_email`,LLM 不在查看路径上。
2. **AI 仅在用户明确要求并确认后接触正文**:用户明确发起"分析/总结/翻译"意图**并再次确认**(点 `[AI 总结]` / `/summarize`)后才把正文交给 LLM;未经确认,LLM 看不到正文。意图或目标不唯一 → 追问确认,禁止猜测执行。
3. **长邮件禁止发送完整原文**:正文 > 阈值(默认 4000 字符,见 [design.md §12.3](docs/design.md#123-正文获取策略与长邮件处理需求-1235))不发全文,只发预览 + `[AI 总结]`/`[继续查看原文(截断)]` 选项;"继续查看"也是截断版并标注"完整请电脑查看"。
4. **新邮件通知只含元数据**(`SAF-NOTIFY-META`):发件人 / 主题 / 时间(+附件数),绝不含正文;正文访问须用户显式读信/分析。
5. **附件默认不下载、不预载**:仅用户显式请求才触发 `Blob/get`;AI 永不接收附件内容。
6. **分析 / 摘要结果不持久化**:不写 Redis、不落盘、不缓存,即取即弃。
7. **AI 失败约 3 次 → 熔断 + 用户确认回退**:回退为规则模板 + 原文直取,标注"AI 不可用";恢复需用户选择。
8. **密钥不出仓库、不出日志**:`secrecy::SecretString` 包裹、`tracing` 屏蔽 `Authorization/password/token`;秘密一律运行期注入(`C-NO-SECRET-IN-IMAGE`,deployment.md)。变量命名以 `src/config.rs` 为准,模板见根目录 [`.env.example`](./.env.example)(**仅占位符,禁放真实值**)。
9. **聊天白名单(硬约束 `SAF-CHAT-ALLOWLIST`)**:`CHAT_ALLOWLIST` 必填;任何入站事件在做任何 JMAP/AI 调用或状态变更**之前**必须先校验 `chat.id ∈ CHAT_ALLOWLIST`,否则直接拒绝终止。(阶段0 已完成解析骨架;强制拒绝随阶段2 渠道接入落地。)
10. **LLM 无工具权**:AI 输出仅为文本;删除/归档/下载/发送等动作只能由用户显式指令触发,绝不由 LLM 输出驱动(邮件正文视为不可信数据,正文内指令无效)。
11. **无长连接**:运行模式仅 `webhook`/`reconcile`/`health`,禁止 SSE/EventSource/WebSocket/Telegram 长轮询(`C-NO-LONG-CONN`/`NG-POLLING-SSE`/`NG-LONG-POLLING`)。
12. **状态仅外部 Redis(用户托管 + AOF)**:会话/去重/Redis Streams/熔断计数/sinceState 一律走外部 Redis(`C-REDIS-ONLY-STATE`/`C-REDIS-MANAGED-AOF`),禁止 SQLite/本地卷作持久层(`NG-SQLITE-PERSIST`/`NG-LOCAL-VOLUME`);Redis 丢失由 JMAP 对账 `FLOW-RECONCILE` 重建 sinceState,**事实源在 JMAP**。
13. **正文仅在用户明确允许后才外发 AI**(`REQ-AI-EXTERNAL-CONSENT`):默认不外发;发送前须用户显式同意(与第 2 条叠加)。
14. **单账户**(`REQ-SINGLE-ACCOUNT`):一个实例只接一个 Stalwart 账户;多账户 = 多个 bot 实例,不做多账户单实例。
15. **三入口鉴权 fail-closed(硬约束 `SAF-AUTH-RECONCILE`/`SAF-AUTH-TG-WEBHOOK`/`SAF-AUTH-JMAP-PUSH`)**:三条写路径必须先鉴权——`/reconcile` 校验 `Authorization: Bearer RECONCILE_TOKEN`、`/webhook/tg` 校验 `X-Telegram-Bot-Api-Secret-Token == TG_WEBHOOK_SECRET`、`/push/jmap` 校验请求体 `pushSubscriptionId` + `verificationCode`——按 `pushSubscriptionId` 查 Redis 短期验证状态(值存 `session_digest` 摘要,见 `src/notify.rs` `jmap_push`),状态缺失或摘要不符即拒绝;`/reconcile`、`/webhook/tg` 的凭证比较用**常数时间**(`subtle`),失败 `401` 且**在鉴权通过前不得产生任何副作用**;三密钥必填,缺失即启动失败,**禁止任何"未配置则放行"降级**。`/healthz`(`ARCH-HEALTHZ`)与 `/ready` 为**公开探针**(`SAF-PROBE-PUBLIC`:无鉴权、仅健康状态、不含敏感信息);**`/ready` 已做端到端探测**(`ARCH-READY-BASELINE`):配置完整性 + Redis 可达性 + 出站只读探测(JMAP session `GET` / TG `getMe`,各 `PROBE_TIMEOUT`=3000ms、并行执行,最坏约 3s),任一失败返 `503`(标准错误 envelope `{"error":"service_unavailable","request_id":<id>}` + `Retry-After: 30`;Uptime Kuma 按状态码 200/503 监控,不受响应体影响);出站探测要求 egress 可达 JMAP host 与 `api.telegram.org:443`(bot token 仅用于拼 URL,不日志/不回显)——**禁止再声称 `/ready` 只做基础探测**。
16. **多实例高可用(`ARCH-LB-WORKER`)**:允许同一镜像跨多个 serverless 平台实例化、前置免费 Cloudflare Worker 做唯一入口与故障转移(`C-LB-SINGLE-REG-URL`),共享同一 Redis(`C-REDIS-ONLY-STATE`)。**信任模型为透传(`SAF-LB-PASSTHRU`)——后端鉴权不可省**(小平台无防火墙/ACL,后端入口可能被公网直连);多实例**必须共享同一组 `SAF-AUTH-*` secret**(`C-LB-SHARED-SECRETS`)。`/reconcile` **禁止多实例并发**,用 Redis 锁单实例执行(`SAF-RECONCILE-LOCK`);Streams 用**同一消费组**分摊(`MOD-STREAMS-GROUP`);Worker 可提供**聚合健康视图**(`MOD-HEALTH-AGG`)。**Redis 单点故障不在本方案范围**(`NFR-HA-MULTI-INSTANCE`,用户外部解决)。业务代码无需为 LB 改动。详见 deployment.md §10。
17. **生产红线(无数据库 / 无本地写入 / 标准输出日志,`C-NO-DB` / `C-NO-LOCAL-WRITE` / `C-LOG-STDOUT-ONLY` / `SAF-LOG-PURITY` / `C-NO-STATEFUL-RECOVERY`)**:
- **不使用任何数据库**(`C-NO-DB`):无 SQLite / Postgres / MySQL / 嵌入式数据库;Redis 是唯一生产状态存储(`C-REDIS-ONLY-STATE`)。应用**不自建、不连接第二个数据库实例**。
- **禁止本地文件/目录写入**(`C-NO-LOCAL-WRITE`):无日志文件、无数据文件、无临时缓存、不挂载本地卷(`NG-LOCAL-VOLUME`)。
- **日志只写 stdout/stderr**(`C-LOG-STDOUT-ONLY`):容器/平台负责采集落盘;禁用 `rolling-file` / `FileAppender` 等文件日志后端。
- **日志与 Redis 写入内容约束**(`SAF-LOG-PURITY`):仅限结构化事件、计数、时间戳、脱敏后的请求摘要;**禁止**写入密钥原文、JMAP 邮件正文、AI 请求/响应内容、附件内容。
- **禁止依赖进程内状态做生产恢复**(`C-NO-STATEFUL-RECOVERY`):任何"重启续跑"(去重、sinceState、Streams 断点、熔断计数、会话)必须由外部 Redis + JMAP 对账(`C-REDIS-ONLY-STATE` / `FLOW-RECONCILE`)实现;进程内缓存仅为性能优化,**丢失必须安全可重入**。
18. **无数据库/无本地写入/标准输出日志 = 发布门禁**:发布前必做 deployment.md §8.2 的 6 项红线自检(镜像无 DB 引擎、无本地可写挂载、日志仅 stdout/stderr、日志/Redis 无敏感数据、重启恢复不依赖进程内状态、Redis 由外部提供);任一失败禁止发布。
19. **远程 debug 双因子门禁(`MOD-DEBUG`/`SAF-DEBUG-GATE`/`SAF-DEBUG-AUTH`/`REQ-DEBUG-ENDPOINTS`)**:`/debug/*` 只在**两个条件同时满足**时才挂载——启动命令带 `--debug` 且 Secret 环境变量 `DEBUG_TOKEN` 非空;缺任一即完全不挂载该组路由,请求落到通用 `404`(**正常情况下绝对关闭**,禁止任何"未配置即放行"降级)。已挂载后每个 `/debug/*` 仍须 `Authorization: Bearer DEBUG_TOKEN` 并与 secret 做**常数时间**比较(`SAF-DEBUG-AUTH`),失败 `401` 且**在鉴权通过前不得产生任何副作用**。Worker 白名单(`ARCH-LB-WORKER`)不含 `/debug/*`,故公网唯一入口**永远不可达**该组路由;debug 仅供直连后端 origin 的远程联调。所有 `/debug/*` 响应体**不含任何 secret 原文**(`SAF-NO-SECRET-ECHO`),只回显"是否已设置"与探针结论;`DEBUG_TOKEN` 不回显、不入日志(`SAF-LOG-PURITY`)。
### 2.1 代码规模(软性指导,非硬限制)
- 单个 `.rs` 文件原则上不超过 **500 行**;这是**软性指导**而非硬性门禁。
- 复杂度、内聚性与可维护性优先:禁止为了凑 500 行制造奇怪的拆分(如过度抽象、跨文件跳转式"减肥")。
- 若要拆分,只按职责/内聚边界拆;超过 500 行本身不是错误,但**需要在 PR/提交说明中写明原因**(如"本文件保持命令表 + 全部子命令处理,超行因命令数量,拆开反而分散")。
### 2.2 代码注释与文档一致性(必须)
- 代码必须包含与设计文档匹配的必要注释;注释至少覆盖非显而易见的架构边界、状态流转、可靠性取舍、安全不变量和外部协议约束。
- `domain/`、`channel/`、`notify/`、Redis Streams、`sinceState`、AI 确认/回退、长邮件与附件处理等关键路径,必须能从代码注释追溯到 `design.md` 或稳定语义 ID(`C-*`、`REQ-*`、`FLOW-*`、`SAF-*`)。**注释中的稳定 ID 必须取自 §7 索引所用的前缀集**(`C-`/`NG-`/`MOD-`/`FLOW-`/`REQ-`/`SAF-`/`NFR-`/`GATE-`/`BOUND-`/`ARCH-`);**禁止自造未登记 ID**(如 `JMAP-n`、`DATA-n`、`JMAP-*`、`DATA-*`)——新增语义需先在 §7 登记再引用。
- 设计规则、安全边界或数据流变更时,必须同步更新受影响的代码注释和对应文档;禁止保留与实现不符的注释。
- 注释应解释“为什么”和约束,不要逐行复述代码;简单自解释代码不要求添加废话注释。
- Code review/测试应检查关键模块注释是否与文档一致;注释缺失或过时视为实现不完整,但不要求为每个函数添加固定格式的注释。
---
## 3. 实现顺序
按 [design.md §10](docs/design.md#10-分阶段实施计划) 推进(每阶段验收标准见对应小节):
```
阶段 0 脚手架 + HTTPS 入口骨架(已完成,待过 P0 门禁 GATE-P0)
—— 交付:cargo 工程 + 模块骨架(main/config/error/domain/channel/notify)
+ 单端口 axum 路由占位(/webhook/tg /push/jmap /reconcile /healthz /ready)
+ /healthz=liveness;/ready 阶段0 交付为占位 200,现已演进为端到端探测(ARCH-READY-BASELINE:配置完整性 + Redis 可达性 + 出站只读探测 JMAP session GET / TG getMe,各 3000ms,不就绪→503)
+ env 配置骨架 + 最小测试;实际依赖:axum 0.8 / serde / serde_json / thiserror / secrecy / subtle / url / tokio / tracing;jmap-client =0.4.2(default-features=false, features=["async","rustls"])
+ 入口鉴权已落地:/reconcile(Bearer) /webhook/tg(secret 头) /push/jmap(verificationCode),fail-closed SAF-AUTH-*
阶段 1 JMAP 只读(`jmap-client` 0.4.2 **已引入**,`ARCH-DEPS-STAGE1`;R1 入口鉴权已在阶段0 落地,不重复实现)
+ `JMAP_SESSION_URL` 归一化(`REQ-JMAP-SESSION-URL`/`SAF-JMAP-URL`):接受服务基地址或完整 /.well-known/jmap,交给 jmap-client 前归一化为 origin/base;仅 HTTPS、禁内嵌凭据
+ `read_email` 多 part 原文拼接(`REQ-JMAP-RAW-MULTIPART`)
阶段 2 渠道适配骨架(Channel/Notifier/MessageAdapter + 首个渠道 Telegram;CHAT_ALLOWLIST 强制 + throttle;/start /folders /list /read)
阶段 3 发送 + 状态(send_email draft+submission;/send FSM;/flag)
阶段 3.5 LLM 门面 + 回退(见 design.md §12.9)
阶段 4 实时推送(Push 回调 + Redis Streams worker + 外部 Cron 对账 + sinceState→Redis)
阶段 5 搜索 + 搜索片段 + 打磨
```
> **阶段门禁**:`GATE-P0`(阶段0 P0 门禁,8 项)与 `BOUND-STAGE1`(阶段1 推进边界)定义见 design.md §10.0。**未过 `GATE-P0` 不得进入阶段1**;阶段0 业务体为无副作用占位,**不应暴露公网**。**注意**:三条写路径的**入口鉴权 `R1` 已在阶段0 落地**(`SAF-AUTH-*`,fail-closed),阶段1 不再重复实现,也**禁止放宽/绕过**。
> **依赖引入节奏**:`jmap-client` **已引入**(`=0.4.2`,实际 `default-features = false, features = ["async","rustls"]`,`ARCH-DEPS-STAGE1`:版本与 features **以 `Cargo.toml` 为准**;⚠️ 其默认 features `["async","websockets","aws_lc_rs"]` **含 WebSocket 栈**,故须关闭默认 features 且不选 `websockets`,以遵守 `C-NO-LONG-CONN`);`redis`(`0.27`) / `reqwest`(`0.13`) **已加入 Cargo.toml**(阶段3.5/4 已落地,版本**以 `Cargo.toml` 为准**,`ARCH-DEPS-STAGE4`);`teloxide` **未引入**——Telegram 渠道在 `src/channel.rs` 用 reqwest 自研实现,不依赖 teloxide 类型(`src/domain.rs` 注释禁止 teloxide 类型跨越领域边界);不得在文档/注释中声称已使用未引入的依赖。配置为**环境变量手工解析**(`ARCH-CONFIG-ENV`,无 figment/TOML)。`ACCOUNT_ID` **可选**:留空 → 取 JMAP session 的**默认/主账户**;显式值经校验后使用(`REQ-SINGLE-ACCOUNT`)。
> 部署/镜像相关工作(Dockerfile、compose、健康检查、CI 发布)不是各功能阶段的目标,集中在 deployment.md;功能阶段验收通过 `cargo test`,不需要先做镜像。
> **JMAP session URL 语义**(`REQ-JMAP-SESSION-URL`/`SAF-JMAP-URL`):`JMAP_SESSION_URL` 可填**服务基地址**(`https://host[:port]`)或**完整** `…/.well-known/jmap`;代码在 `Client::connect` 前归一化为 **origin/base**(jmap-client 自行追加 `/.well-known/jmap`),**不得出现重复路径**(`…/.well-known/jmap/.well-known/jmap`)。仅 **HTTPS**;**禁止 URL 内嵌用户名/密码**(凭据只经 `JMAP_USERNAME`/`JMAP_PASSWORD`);拒绝危险 query。
---
## 4. 禁止事项
- 禁止把 AI 输出当作可执行操作(LLM 无工具权)。
- 禁止未确认就把正文 / 附件内容送入 LLM;禁止把分析结果持久化(Redis/磁盘/缓存)。
- 禁止在生产路径绕过 chat 白名单、关闭 TLS 校验、关闭熔断/回退。
- 禁止把 secrets(token / 密码 / LLM API key)写入代码、配置、日志或镜像。
- 禁止把部署/平台细节塞回 `design.md`(见 §6);反之亦然。
- 禁止引入 Alpine/musl 基础镜像或非 Docker 部署形态(deployment.md 硬约束 C-DOCKER/C-DEBIAN-SLIM)。
- 禁止引入 SSE/EventSource/WebSocket/长轮询等长连接运行模式(`C-NO-LONG-CONN`/`NG-POLLING-SSE`/`NG-LONG-POLLING`)。
- 禁止 SQLite/本地卷作持久层;状态与游标仅外部 Redis(`NG-SQLITE-PERSIST`/`NG-LOCAL-VOLUME`/`C-REDIS-ONLY-STATE`),Redis 丢失走 JMAP 对账 `FLOW-RECONCILE` 恢复。
- 禁止无授权改动 `docs/design.md`、`docs/deployment.md`、`AGENTS.md` 之外的内容;安全边界变更须同步三份文档。
- 禁止在领域层(`domain.rs` / `ai.rs` / `worker.rs`)引入具体渠道 SDK 类型;渠道 SDK 只允许出现在渠道层(当前为 `src/channel.rs`,**不引入第三方 Bot 框架**)。
- 禁止提前实现钉钉/飞书 adapter(仅保留抽象与扩展位,不写具体渠道代码、不建渠道专属配置项)。
- 禁止在文档/注释中声称已使用**尚未加入 `Cargo.toml`** 的依赖(`teloxide`/`figment` 等);`jmap-client`/`redis`/`reqwest` 已引入(版本以 `Cargo.toml` 为准,`ARCH-DEPS-STAGE4`),其中 jmap-client **禁止启用其 WebSocket feature**(`C-NO-LONG-CONN`);配置恒为环境变量手工解析(`ARCH-CONFIG-ENV`),禁止引入 figment/TOML 配置文件。
- 禁止在未通过 `GATE-P0` 时进入阶段1;禁止把阶段0 容器暴露公网(业务体仍为占位,`BOUND-STAGE1`)。入口鉴权(`SAF-AUTH-*`,R1)**已在阶段0 落地**,禁止移除、放宽或绕过(fail-closed)。
- 禁止在**透传 LB 模型**(`SAF-LB-PASSTHRU`)下削弱/关闭任何后端的 `SAF-AUTH-*` 校验(小平台无防火墙,后端可能被公网直连);禁止让多实例使用**不同**的 `SAF-AUTH-*` secret(`C-LB-SHARED-SECRETS`);禁止让 `/reconcile` 多实例并发(必须 Redis 锁,`SAF-RECONCILE-LOCK`)。
- 禁止引入**任何数据库**(`C-NO-DB`):不引入 SQLite/Postgres/MySQL/嵌入式数据库;不连接 Redis 之外的第二个状态存储。
- 禁止**本地文件/目录写入**(`C-NO-LOCAL-WRITE`):不写日志文件、不写数据文件、不写临时缓存、不挂载本地卷。
- 禁止将日志写入非 stdout/stderr 的目的地(`C-LOG-STDOUT-ONLY`):不用 `rolling-file`/`FileAppender`/自定义文件 sink;日志只能输出到容器标准输出/错误,由平台采集。
- 禁止把**密钥、邮件正文、AI 请求/响应内容、附件内容**写入日志或 Redis(`SAF-LOG-PURITY`):日志/Redis 仅允许结构化事件、计数、时间戳、脱敏摘要。
- 禁止依赖**进程内状态**做生产恢复(`C-NO-STATEFUL-RECOVERY`):任何重启续跑(去重、sinceState、Streams 断点、熔断计数、会话)必须能从 Redis + JMAP 对账重建;进程内缓存仅为性能优化,丢失必须安全可重入。
---
## 5. 测试验收(至少满足)
- `cargo fmt --check`、`cargo clippy --all-targets -- -D warnings`、`cargo test` 全绿;**并在 Debian `rust:1-slim-bookworm` 容器内执行**(`GATE-P0`/`C-DEBIAN-SLIM`;阶段0 P0 门禁要求)。
- 占位符使用**局部** `#[expect(dead_code, reason="稳定ID+阶段0占位")]`;**禁止 crate 级 `#[allow]`**;不得为过 clippy 删除安全访问器(如 `JMAP_PASSWORD` getter)或让 `Debug` 泄密。
- 不变量测试([design.md §9.3](docs/design.md#93-关键不变量测试)):
- **VIEW / 查看原文**:mock AI 端点零请求(断言 LLM client 未被调用)。
- **新邮件通知**:消息内无正文内容(断言泄漏)。
- **长邮件**:正文 > 4000 字符不发送全文,仅预览 + 选项。
- **AI 前置确认**:未确认前 LLM 零调用;确认后才发起请求。
- **结果不落盘**:分析处理后无新增磁盘/Redis 写入路径。
- **AI 3 次失败**:触发熔断 → 弹确认 → 回退带"AI 不可用"徽标;`LLM_ENABLED=false` 全走回退。
- 正文转义、sinceState 存 Redis + 模拟 Redis 删除后由对账 `FLOW-RECONCILE` 恢复、通知去重。
- **入口鉴权(`SAF-AUTH-*`)**:`/reconcile`/`/webhook/tg`/`/push/jmap` 在缺失或错误凭证下返回 `401` 且**无副作用**(无 Redis 写入、无 JMAP/AI 调用);正确凭证放行。
- **健康探针(`SAF-PROBE-PUBLIC`)**:`/healthz` 返回 `200`;`/ready` 做端到端探测(`ARCH-READY-BASELINE`:配置完整性 + Redis 可达性 + 出站只读探测 JMAP session `GET` / TG `getMe`,各 3000ms,任一失败返 `503`)——测试**可以**断言 `/ready` 在配置 / Redis / 上游探针失败时返回 `503`;两个探针(`probe_jmap_session` / `probe_telegram_get_me`)是可复用纯函数,④ 远程 debug 直接调用,**不要另写一套**;两者响应体均不含敏感信息。
- **远程 debug(`MOD-DEBUG`/`SAF-DEBUG-GATE`/`SAF-DEBUG-AUTH`/`REQ-DEBUG-ENDPOINTS`)**:缺 `--debug` 或 `DEBUG_TOKEN` 缺失/为空时,`/debug/*` **一律 `404`**(路由根本没挂载);双因子齐备时挂载,缺 `Bearer` 或 token 不符返回 `401`,正确 token 放行;`GET /debug/config` 响应体**不得出现任何 secret 原文**(`SAF-NO-SECRET-ECHO`,测试可断言只含布尔/计数/结论字段);出站只读探针直接复用 `probe_jmap_session` / `probe_telegram_get_me`(`ARCH-READY-BASELINE`),不另写实现;`POST /debug/notify` 复用真实出站路径(`TelegramChannel::send_message`),不得另建第二套 Telegram 客户端。
- **JMAP 只读 adapter(`MOD-JMAP-CLIENT`/`GATE-G1-JMAP-READONLY`)**:mock 测试覆盖 session/account 选择(`ACCOUNT_ID` 空→主账户、显式值校验)、**URL 归一化**(`JMAP_SESSION_URL` 基地址与完整 `…/.well-known/jmap` 两种输入结果一致、无重复路径、`http://` 拒绝、内嵌凭据拒绝,`REQ-JMAP-SESSION-URL`/`SAF-JMAP-URL`)、`list_folders`/`list_emails`(`limit` 边界)/`read_email`(多 part 拼接与"无可用部分"明确错误,`REQ-JMAP-RAW-MULTIPART`)、`received_at` 解析;真机测试用 `#[ignore]` 标记、经环境变量驱动(运行:`cargo test -- --ignored jmap::`;仅编译:`cargo test --no-run`),**缺环境时清晰跳过且不泄密**,CI 默认不跑真机用例。**G1/D-G1-1 代码已实现,待真实 `cargo test -- --ignored jmap::` 验证;未实际运行 `--ignored` 前不得声称"真机通过"。**
- 领域/渠道解耦测试:领域模块(`src/domain.rs` / `src/ai.rs` / `src/worker.rs`)的公开接口不出现任何渠道 SDK 类型(编译期检查);`JmapBackend` 的 `MockBackend` 可驱动全部 JMAP 领域流程(不联网)。
- 文件行数抽查:>500 行的 `.rs` 文件在 PR 说明中可见(软性,不自动 fail)。
- 代码注释一致性:抽查关键领域、渠道、通知、Redis/AI 模块,确认注释引用的稳定语义 ID、状态流转和安全边界与 `design.md`/`deployment.md` 一致。
- 涉及部署的改动需过 deployment.md 的 CI/验收清单(`C-DOCKER` 镜像扫描、非 root、health 端点等)。
- 涉及部署/日志/状态的改动需过 deployment.md §8.2 红线自检 6 项(`C-NO-DB` / `C-NO-LOCAL-WRITE` / `C-LOG-STDOUT-ONLY` / `SAF-LOG-PURITY` / `C-NO-STATEFUL-RECOVERY` / 外部 Redis);任一失败禁止发布。
---
## 6. 文档边界与引用关系
| 文件 | 内容 | 何时更新 |
|---|---|---|
| `README.md` / `README.zh-CN.md` | 面向最终用户:是什么 / 五分钟跑起来 / 配置入口 / 安全边界一句话 / 去哪读更多(英文版 / 中文版,内容须对等,顶部互链) | 用户可见行为或流程变更时 |
| `docs/reference.md` | **可核对事实的唯一权威来源**:Redis 键与 TTL、错误 envelope 与错误码、后端路由与网关白名单矩阵、环境变量三层、出站与预算常量 | 公共 API 或 Redis 键名/TTL 变更时(**必更**) |
| `docs/design.md` | 产品行为、架构、模块接口、状态机、数据流、错误处理、测试、实施阶段、渠道抽象(Channel/Notifier/MessageAdapter)、历史问题与决策归档(§11,产品/架构类,均已收口) | 行为/接口变更时 |
| `docs/deployment.md` | 通用 HTTPS-only Docker、Debian、构建/运行时、Secrets、Redis(状态唯一载体)、**可选远程联调面 `/debug/*`(§2.1,`SAF-DEBUG-GATE`)**、Webhook/Push/对账短请求路由、**多实例 LB/HA(Worker 前置,§10)**、健康检查、CI、外部调度示例、已确认决策表(`Q-DEP-A`/`Q-DEP-B` 已决策归档) | 部署/发布变更时 |
| `docs/roadmap.md` | **只放缺口、阻塞与阶段目标**(取代 `docs/todo.md`,已删除) | 缺口或阻塞项增减时 |
| `docs/retired.md` | **已废弃 / 未采用路线与虚构条目**:每条含「类型 / 原因 / 替代或现状」;记录 teloxide 未采用、FSM 未落地、幽灵文件与虚构键、已删文档。**只写结论与原因,不写当前事实** | 新增废弃项或删除文档时 |
| `AGENTS.md`(本文件) | 目标、硬性安全边界、实现顺序、禁止事项、测试验收、文档引用关系、**跨文档引用索引(§7)** | 安全边界/流程变更时 |
### 6.1 权威与冲突仲裁
- **`docs/reference.md` 是可核对事实的唯一权威来源。两份文档表述冲突时,以 `docs/reference.md` 为准。**
- 可核对事实(Redis 键名与 TTL、错误码、路由、环境变量分层、预算常量)**只存在于 `docs/reference.md`**。
`design.md` 保留"为什么",`deployment.md` 保留"怎么配";两者不得复制 reference.md 的表格或数值,只引用它。
### 6.2 同轮同步规则
- **`README.md` 与 `README.zh-CN.md` 必须同轮更新。**改了一个就必须同轮改另一个并保持内容对等;只改一份的改动不予合入。
- **改动 `src/config.rs`、`src/state.rs`、`src/worker.rs`、`src/notify.rs` 的公共 API,或改动任何 Redis 键名 / TTL,必须同轮更新 `docs/reference.md`。**评审按此拦截。
### 6.3 边界
- 改动产品规则必须同步三份文档的相关表述,保持一致、不重复堆砌。
- `design.md` 不写部署细节,`deployment.md` 不写产品行为/接口;交叉处用**稳定 ID**(§7 索引)互相引用,不用章节号。
- `README.md` 只回答"是什么 / 怎么跑 / 去哪配 / 安全边界 / 去哪读";**不写缺口、不写 roadmap、不写深水区、不写历史环境变量清单**。缺口一律进 `docs/roadmap.md`。
- **文档里出现的模块、函数、trait、Redis 键必须是 `src/` 中真实存在的。**要保留一个不存在的名字(例如未采用的框架、未落地的目标模块),必须同时写入 `docs/retired.md` 并在那里说明"从未存在 / 从未落地",不允许只出现在正文里。
---
## 7. 跨文档引用索引表
> 约定:跨文档引用一律用下表的**稳定 ID**(不用 `§x.y` 章节号)。ID 语义稳定、全仓唯一、可 grep;内容搬家时**只更新本表的"文件/锚点"列**,所有引用本身零改动。
> 新增约束/组件时,先在本表登记一行,再在定义点写 `**[ID]**` 标签。
| ID | 定义文件 | 一句话 | 类别 |
|---|---|---|---|
| `C-DOCKER` | docs/deployment.md §0 | 必须 Docker 部署 | 部署约束 |
| `C-DEBIAN-SLIM` | docs/deployment.md §0 | Debian slim,禁 Alpine | 部署约束 |
| `C-NO-SECRET-IN-IMAGE` | docs/deployment.md §0 | secrets 不进镜像 | 部署约束 |
| `C-RUSTLS` | docs/deployment.md §0 | rustls + native-roots | 部署约束 |
| `C-HTTPS-INBOUND` | docs/deployment.md §0 | HTTPS-only 入站,容器内明文 HTTP | 部署约束 |
| `C-HTTPS-URL` | docs/deployment.md §0 | 公网 HTTPS URL 由平台提供(bot 不持证书) | 部署约束 |
| `C-AUTH-APP-BASIC` | docs/deployment.md §9 / design.md §3.1 | Stalwart 认证 = App Password + Basic | 部署约束 |
| `C-NO-TCP-EXPOSE` | docs/deployment.md §0 | 单监听 `PORT`,不暴露附加 TCP 端口 | 部署约束 |
| `C-NO-LONG-CONN` | docs/deployment.md §0 | 无 SSE/WS/长轮询等长连接 | 部署约束 |
| `C-REDIS-ONLY-STATE` | docs/deployment.md §0 | 状态仅外部 Redis,不用 SQLite/本地卷 | 部署约束 |
| `C-REDIS-MANAGED-AOF` | docs/deployment.md §0 | Redis 用户托管 + 开启 AOF 持久化 | 部署约束 |
| `C-PORT` | docs/deployment.md §0 | 通用 PORT 约定 | 部署约束 |
| `NG-SERVER-MODE` | docs/deployment.md §1 | `RUN_MODE=server` 常驻,非目标 | 非目标 |
| `NG-POLLING-SSE` | docs/deployment.md §1 | EventSource/SSE 长连接,非目标 | 非目标 |
| `NG-LONG-POLLING` | docs/deployment.md §1 | Telegram 长轮询,非目标 | 非目标 |
| `NG-SQLITE-PERSIST` | docs/deployment.md §1 | SQLite 持久化,非目标 | 非目标 |
| `NG-LOCAL-VOLUME` | docs/deployment.md §1 | 本地卷持久化,非目标 | 非目标 |
| `NG-SERVERLESS-BIND` | docs/deployment.md §1 | 绑定具体 serverless 平台,非目标 | 非目标 |
| `MOD-DEDUP` | docs/deployment.md §6 | Redis `SET NX` 幂等键 | 组件 |
| `MOD-STREAMS` | docs/deployment.md §6 | Redis Streams 队列 + worker | 组件 |
| `MOD-SINCESTATE` | docs/deployment.md §6 | sinceState 游标(存 Redis) | 组件 |
| `FLOW-NEW-MAIL` | docs/design.md §5.4 / deployment.md §6 | Push 新邮件流 | 数据流 |
| `FLOW-RECONCILE` | docs/deployment.md §6 | 外部 Cron 对账补差 + Redis 丢失恢复 | 数据流 |
| `REQ-AI-CONFIRM` | docs/design.md §12 | AI 仅在明确要求+确认后接触正文 | 需求 |
| `REQ-VIEW-DIRECT` | docs/design.md §12 | 查看原文始终 JMAP 直取 | 需求 |
| `REQ-LONG-EMAIL` | docs/design.md §12 | 长邮件禁止发全文,AI 摘要 ~300 字 | 需求 |
| `REQ-ANALYSIS-EPHEMERAL` | docs/design.md §12 | 分析/摘要结果不持久化 | 需求 |
| `REQ-ATTACH-ONDEMAND` | docs/design.md §12 | 附件按需拉取 | 需求 |
| `REQ-AI-FUSE` | docs/design.md §12 | AI 失败约 3 次 → 熔断 + 用户确认回退 | 需求 |
| `REQ-LLM-OPENAI-COMPAT` | docs/design.md §12 | OpenAI-compatible 环境变量 | 需求 |
| `REQ-AI-EXTERNAL-CONSENT` | docs/design.md §12 / AGENTS.md §2 | 仅用户明确允许后才向外部 AI 发正文 | 需求 |
| `REQ-AI-CONSENT` | docs/design.md §12.3.1 | AI 授权期限与 Redis 短期 TTL(授权有效期、到期后重新询问) | 需求 |
| `REQ-SINGLE-ACCOUNT` | docs/design.md §3.1/§11.3 | 单账户;多账户 = 多 bot 实例 | 需求 |
| `REQ-RECONCILE-IDEMPOTENCY` | src/state.rs `ReconcileState` / docs/design.md §8.2 | JMAP 对账游标只有在全部分页事件成功入队(XADD)后才推进;单次对账由 Redis SET NX EX 锁 `lock:reconcile` 保证单飞(TTL 300s,owner token 续期 90s,仅持有者可续期/释放);处理端再经 `claim_dedup`(SET NX EX,86400s)保证同一流消息不重复投递 | 需求 |
| `NFR-NOTIFY-SLA` | docs/deployment.md §6.5 | 通知可用性 ≥99.9%,允许少量延迟 | 非功能 |
| `NFR-RECONCILE-INTERVAL` | docs/deployment.md §6.3 | 外部 Cron 对账间隔 5–10 分钟 | 非功能 |
| `SAF-NOTIFY-META` | AGENTS.md §2 | 新邮件通知只含元数据,正文不入通知 | 安全 |
| `SAF-CHAT-ALLOWLIST` | AGENTS.md §2 / design.md §7.3 | CHAT_ALLOWLIST 硬约束,处理前先拒绝非白名单 | 安全 |
| `SAF-AUTH-RECONCILE` | AGENTS.md §2 / design.md §7.3 | `/reconcile` 需 `Authorization: Bearer RECONCILE_TOKEN`,fail-closed | 安全 |
| `SAF-AUTH-TG-WEBHOOK` | AGENTS.md §2 / design.md §7.3 | `/webhook/tg` 需头 `X-Telegram-Bot-Api-Secret-Token == TG_WEBHOOK_SECRET` | 安全 |
| `SAF-AUTH-JMAP-PUSH` | AGENTS.md §2 / design.md §7.3 | `/push/jmap` 按 `pushSubscriptionId` 查 Redis 短期验证状态(存 `session_digest` 摘要),缺失或摘要不符拒绝;状态缺失回落 worker 重新验证(fail-closed) | 安全 |
| `SAF-ADMIN-SESSION` | src/config.rs `admin_session_digest` / §7 | 返回用于 Redis admin-session 记录的 SHA-256 摘要;bearer token 本体绝不写入 Redis | 安全 |
| `SAF-NO-SECRET-ECHO` | src/config.rs `SecretString` 字段与公共配置 API | 密钥字段不实现 `Debug` 且永不进入公共配置 API 响应;Redis 线格式类型私有,不得序列化进 HTTP 响应 | 安全 |
| `SAF-PROBE-PUBLIC` | AGENTS.md §2 / design.md §7.3 | `/healthz`、`/ready` 公开探针:无鉴权、无敏感信息 | 安全 |
| `ARCH-HEALTHZ` | docs/design.md §7.3/§10.0 | `/healthz` liveness(进程存活),语义长期稳定 | 架构 |
| `ARCH-READY-BASELINE` | docs/design.md §7.3 / src/notify.rs `ready`+`probe_jmap_session`+`probe_telegram_get_me` | `/ready` 端到端就绪:配置完整性 + Redis 可达性 + **出站只读探测**(JMAP session `GET`、Telegram `getMe`,各 `PROBE_TIMEOUT`=3000ms、并行,最坏约 3s),四者全过 `200`(就绪报告 JSON 含真实 `jmap`/`telegram` 字段),任一失败 `503`(标准错误 envelope `{"error":"service_unavailable","request_id":<id>}` + `Retry-After: 30`);探针只读、无状态写入(`refresh_business_config` 仅读 Redis),bot token 仅用于拼 URL | 架构 |
| `GATE-P0` | docs/design.md §10.0 | 阶段0 P0 门禁(fmt/clippy/test 过 Debian 容器等 8 项) | 流程 |
| `BOUND-STAGE1` | docs/design.md §10.0 | 阶段1 推进边界(过 GATE-P0 才进;R1 入口鉴权已在阶段0 落地) | 流程 |
| `ARCH-CONFIG-ENV` | docs/design.md §7.1 | 配置=环境变量手工解析,无 figment/TOML | 架构 |
| `ARCH-AXUM-08` | docs/design.md §10.0 | 单端口 axum 0.8 入口(版本以 Cargo.toml 为准) | 架构 |
| `ARCH-STAGE0` | docs/design.md §10.0 / §5.3 | 阶段0 实际交付与现状 | 架构 |
| `ARCH-DEPS-STAGE0` | docs/design.md §10.0 / AGENTS.md §3 | 阶段0 实际依赖集(axum/serde/secrecy/subtle/tokio/tracing…) | 架构 |
| `ARCH-DEPS-STAGE1` | docs/design.md §10.0-1 / AGENTS.md §3 | 阶段1 依赖现况:`jmap-client 0.4.2` 已引入,版本/features 以 Cargo.toml 为准,禁用 WebSocket feature | 架构 |
| `ARCH-DEPS-STAGE4` | docs/design.md §10.0-1 / AGENTS.md §3 | 阶段3.5/4 后依赖现况:`redis 0.27`、`reqwest 0.13` 已引入并实际使用;`teloxide` **未**引入(Telegram 由 `src/channel.rs` 用 reqwest 自研实现);版本一律以 `Cargo.toml` 为准 | 架构 |
| `MOD-JMAP-CLIENT` | docs/design.md §6-7 / AGENTS.md §3 | `domain::jmap::client` 真实只读 adapter(session/account/mailbox/email 只读) | 组件 |
| `MOD-TELEGRAM-NOTIFY` | src/worker.rs / docs/design.md §12 | 有界元数据通知 worker:消费出站队列,把脱敏通知经 Telegram 投递(不承载正文/AI 响应) | 组件 |
| `REQ-JMAP-SESSION-URL` | docs/design.md §7.1 / .env.example | `JMAP_SESSION_URL` 接受服务基地址或完整 /.well-known/jmap,归一化为 origin/base 后再交 jmap-client(无重复路径);**代码已实现(D-G1-1),待真机验证** | 需求 |
| `SAF-JMAP-URL` | docs/design.md §7.1 / AGENTS.md §3 | JMAP URL 约束:仅 HTTPS、禁止内嵌凭据、拒绝危险 query | 安全 |
| `REQ-JMAP-RAW-MULTIPART` | docs/design.md §3.2/§10.1 | `read_email` 多 part 原文:按 text_body 顺序拼接"有 part_id 且 bodyValue"的部分;无可用部分→明确错误 | 需求 |
| `GATE-G1-JMAP-READONLY` | docs/design.md §10.1 / AGENTS.md §5 | G1 门禁:只读 adapter **代码已实现**(mock + `#[ignore]` 真机测试),**待真实 `cargo test -- --ignored jmap::` 验证** | 流程 |
| `ARCH-LB-WORKER` | docs/deployment.md §10 | 多实例 LB/HA:免费 Cloudflare Worker 作唯一对外入口 + 故障转移,后端为多平台同镜像 | 架构 |
| `C-LB-SINGLE-REG-URL` | docs/deployment.md §10.1 | Telegram/Push/Cron 只登记 Worker 的稳定 URL;后端平台入口不对外登记 | 约束 |
| `C-LB-SHARED-SECRETS` | docs/deployment.md §10.3 | 多实例必须共享同一组 `SAF-AUTH-*` secret,否则随机 401 | 约束 |
| `SAF-LB-PASSTHRU` | docs/deployment.md §10.3 / AGENTS.md §2 | 信任模型=透传:Worker 不改写鉴权;后端必须继续 fail-closed 校验(后端可能被公网直连) | 安全 |
| `SAF-RECONCILE-LOCK` | docs/deployment.md §10.5 | `/reconcile` 不扇出,Redis 锁保证单实例执行,避免重复对账 | 安全 |
| `MOD-STREAMS-GROUP` | docs/deployment.md §10.5 | 多实例用同一 Streams 消费组名,Redis 自动分摊(at-least-once 不重复处理) | 组件 |
| `MOD-HEALTH-AGG` | docs/deployment.md §10.6 | Worker 聚合健康视图,报告各后端存活供外部监控 | 组件 |
| `MOD-DEBUG` | src/debug.rs / docs/deployment.md §2.1 / docs/design.md §7.6 / docs/reference.md §3 | 远程联调只读表面:`--debug` + `DEBUG_TOKEN` 双因子开启后挂载 `/debug/*`,否则不挂载 | 组件 |
| `SAF-DEBUG-GATE` | src/main.rs / src/debug.rs / docs/design.md §7.6 / docs/deployment.md §2.1 | 双因子门禁:启动带 `--debug` **且** `DEBUG_TOKEN` 非空才挂载路由;缺任一完全不挂载(请求落通用 `404`),默认绝对关闭 | 安全 |
| `SAF-DEBUG-AUTH` | src/debug.rs / docs/deployment.md §2.1 | 挂载后 `/debug/*` 须 `Authorization: Bearer DEBUG_TOKEN` 常数时间比较,失败 `401` 且无副作用 | 安全 |
| `REQ-DEBUG-ENDPOINTS` | src/debug.rs / docs/reference.md §3 / docs/deployment.md §2.1 | 端点契约:`GET /debug/ping`、`/config`、`/redis`、`/jmap`、`/telegram`、`/worker` 均只读;`POST /debug/notify` 走真实出站链路发一条测试消息;响应体不含 secret 原文(凭据字段只出 `*_configured` 布尔,非密文的身份与预算字段仍明文返回) | 需求 |
| `SAF-DEBUG-ORIGIN-ONLY` | docs/deployment.md §2.1 / docs/reference.md §4 | `/debug/*` 不在网关 14 条安全路由内,Worker 一律 `404 route not forwarded`;只能直连后端 origin,公网不可达 | 安全 |
| `SAF-DEBUG-ALLOWLIST` | src/debug.rs / docs/deployment.md §2.1 | `POST /debug/notify` 仅在 chat 白名单**非空**时校验 `chat_id`;白名单未配置(空)时不拦截,故启用本面须确认业务白名单已配置 | 安全 |
| `NFR-HA-MULTI-INSTANCE` | docs/deployment.md §10.7 / §9.1 | 多实例高可用语义;双活或主备均可;Redis 单点故障不在方案范围(用户外部解决) | 非功能 |
| `C-NO-DB` | docs/deployment.md §0 / §9.1 / AGENTS.md §2 | 生产不使用任何数据库(无 SQLite/Postgres/MySQL/嵌入式),Redis 为唯一状态存储;应用不连接第二个数据库 | 约束 |
| `C-NO-LOCAL-WRITE` | docs/deployment.md §0 / §9.1 / AGENTS.md §2 | 禁止本地文件/目录写入(日志/数据/临时缓存/本地卷) | 约束 |
| `C-LOG-STDOUT-ONLY` | docs/deployment.md §0 / §9.1 / AGENTS.md §2 | 日志只写 stdout/stderr,由平台采集;禁用文件日志后端 | 约束 |
| `SAF-LOG-PURITY` | docs/deployment.md §0 / §9.1 / AGENTS.md §2 | 日志与 Redis 写入内容仅限结构化事件/计数/时间戳/脱敏摘要;禁止密钥/邮件正文/AI 请求响应/附件内容 | 安全 |
| `C-NO-STATEFUL-RECOVERY` | docs/deployment.md §0 / §9.1 / AGENTS.md §2 | 禁止依赖进程内状态做生产恢复;恢复一律走 Redis + JMAP 对账;进程内缓存仅为性能优化,丢失须安全可重入 | 约束 |