diff --git a/docs/charter.zh-CN.md b/docs/charter.zh-CN.md index d2c6394..0e064af 100644 --- a/docs/charter.zh-CN.md +++ b/docs/charter.zh-CN.md @@ -15,7 +15,7 @@ --- -## 1. Project goals +## 1. 项目目标 一个**部署在 Docker + 外部托管 Redis** 的 JMAP 邮件通知服务: @@ -27,7 +27,7 @@ **不做**:多租户、账号体系、Web UI 之外的客户端、消息持久化、邮件双向同步。 -## 2. Technology selection (locked) +## 2. 技术选型(已锁定) | 层 | 选型 | 版本 | |---|---|---| @@ -45,7 +45,7 @@ | 前端 | 原生 HTML + 单文件 JS + 单文件 CSS,**无构建工具、无框架** | — | | 网关 | Cloudflare Worker(纯 JS,零依赖) | — | -## 3. Security boundary (cannot be violated) +## 3. 安全边界(不可违反) 23 条,编号 `SAF-*` / `REQ-*` / `C-*`,全部登记在 §8 注册表。 @@ -73,7 +73,7 @@ 22. **AI 分析不持久化**:分析结果与授权状态之外的中间态不落盘。 23. **JMAP capability 动态发现**:禁止硬编码 JMAP 能力列表,必须运行时读取。 -## 4. Implementation phase (historical sequence, used to explain code structure) +## 4. 实施阶段(历史顺序,用于解释代码结构) | 阶段 | 内容 | 稳定 ID | |---|---|---| @@ -86,7 +86,7 @@ **阶段编号是历史顺序,不是当前状态声明。** 当前状态以 `reference.md` 与代码为准。 -## 5. Prohibited matters +## 5. 禁止事项 1. **不得引入未在本文件 §2 登记的依赖。** 新增依赖须先在本文件登记并说明理由。 2. **不得为通过门禁而删除测试、跳过校验、放宽检查级别、删除访问控制。** @@ -99,15 +99,15 @@ 9. **不得在 `web/`、`cloudflare-worker/` 之外引入前端资源**。 10. **不得让业务配置静默回退到环境变量。** -## 6. Test acceptance and access control +## 6. 测试验收与访问控制 -### 6.1 Change verification closed loop +### 6.1 变更验证闭环 任何涉及运行时行为或可核对事实的改动,**同一改动内**完成:实现 → 测试 → 文档。 不得只改代码不改文档,也不得只改文档不改代码。 -### 6.2 GATE-P0 (code access control) +### 6.2 GATE-P0(代码门禁) Debian 最小发行版容器内执行,容器内必须显式 `export PATH=/usr/local/cargo/bin:$PATH`: @@ -122,7 +122,7 @@ cd /home/okabe/Repo/messageweave && docker run --rm --user 1000:1000 \ **注意**:`docker run` 的 bash `-lc` 脚本必须用**单引号**包裹。用双引号会先在宿主机展开 `$PWD` / `$PATH`,容器内找不到 cargo。 -### 6.3 GATE-DOCS (Document Access Control) +### 6.3 GATE-DOCS(文档门禁) ```bash cd /home/okabe/Repo/messageweave && bash scripts/docs_check/run_all.sh @@ -143,19 +143,19 @@ cd /home/okabe/Repo/messageweave && bash scripts/docs_check/run_all.sh **`0 error` 只证明「可达」**:行号存在、引用可解析、表格列数一致、标记就位。 **它不证明语义正确。** 报告校验结果时必须说明这个区分。 -### 6.4 Front-end testing +### 6.4 前端测试 ```bash cd /home/okabe/Repo/messageweave/web && node --test *.test.mjs cd /home/okabe/Repo/messageweave/cloudflare-worker && node --test test/*.test.js ``` -### 6.5 Invariants must have assertions +### 6.5 不变量必须有断言 §3 的每条安全不变量都要有一条测试或代码级断言能证明它成立, 而不是只写在文档里。纯重构不得改变测试总数。 -### 6.6 External dependency testing +### 6.6 外部依赖测试 Stalwart 与 Telegram 的业务凭据已在真机联调通过——出站、查询与**入站回调**两个方向 均已验证(`setWebhook` 与 `PushSubscription` 的注册、回调、验证往返全部走通),托管 Redis @@ -163,7 +163,7 @@ Stalwart 与 Telegram 的业务凭据已在真机联调通过——出站、查 **环境阻塞项:无。** 最后两项也已验证:TTL 数值在真实 Upstash 实例上实测(`SET … NX EX` / `SET … EX 3600` / Lua `EXPIRE 86400` 三处原子写入点,连跑 6 轮全绿),`Email/changes` 的 `newState` 续传在真实 Stalwart 环境触发过(见 docs/design.md §10.5)。[`opengaps.md`](opengaps.zh-CN.md)「阻塞」区当前为空。 -## 7. Document boundaries +## 7. 文档边界 每个文档只回答一个问题;**不得跨文档复制表格、清单或数值。** @@ -188,7 +188,7 @@ Stalwart 与 Telegram 的业务凭据已在真机联调通过——出站、查 - **改公共 API / Redis 键与 TTL / 配置项 / 错误码 / 用户可见文案时,同轮同步全部相关文档。** - **新增文档必须加入 `scripts/docs_check/` 各校验器的文档清单**,否则它永远不会被校验。 -## 8. Stable ID registry +## 8. 稳定 ID 注册表 **唯一权威索引表。** 跨文档引用一律用稳定 ID(不用 `§x.y` 章节号)。 diff --git a/docs/deployment.zh-CN.md b/docs/deployment.zh-CN.md index b1fdd67..2ab85c7 100644 --- a/docs/deployment.zh-CN.md +++ b/docs/deployment.zh-CN.md @@ -6,9 +6,9 @@ 选择任何可以运行后端容器的平台,提供加密环境 密钥,并连接到外部管理的 Redis 服务。可选的 Cloudflare Worker gateway 在一个或多个后端前面添加一个稳定的 URL。特定于平台的设置是 -在[部署平台参考](reference.zh-CN.md#9-deployment-platform-details)中。 +在[部署平台参考](reference.zh-CN.md#9-部署平台细节)中。 -## 0. Before deployment +## 0. 部署前准备 准备启用持久性和公共 HTTPS URL 的外部管理的 Redis 服务 到达后端。平台或代理终止 TLS; MessageWeave 监听其中一个 @@ -21,9 +21,9 @@ HTTP 端口(“PORT”,默认“8080”)。该过程需要两个加密的 生产环境没有本地数据库、数据量或内部调度程序。完整的安全边界 禁止模式位于 [`charter.md`](charter.zh-CN.md) 中。 -## 1. Deploy a backend +## 1. 部署后端 -### Container platform +### 容器平台 构建根 Dockerfile 并在平台中配置所需的机密。对于本地人来说 容器冒烟,将 `.env.example` 复制到未跟踪的 `.env`,替换两个占位符, @@ -36,9 +36,9 @@ docker run --rm --env-file .env -p 8080:8080 messageweave:latest 该镜像使用 Rust 构建器和 Debian slim 运行时,以非 root 用户身份运行,并公开 仅端口“8080”。不要安装数据或日志卷。完整图像内容位于 -[平台参考](reference.zh-CN.md#91-backend-deployment-paths)。 +[平台参考](reference.zh-CN.md#91-后端部署方式)。 -## 2. First-time configuration +## 2. 首次配置 在原点打开后端的配置页面进行初始设置。使用 `CONFIG_ENCRYPTION_KEY` 建立管理会话,然后完成一次性引导 @@ -50,19 +50,19 @@ docker run --rm --env-file .env -p 8080:8080 messageweave:latest JMAP HTTPS 会话 URL 和应用程序密码、Webhook 机密和工作人员令牌。设置一个 如果使用计划的协调,则协调令牌。仅当用户 已明确同意外部处理。秘密值不会由 -配置API;完整的字段语义位于[配置参考](reference.zh-CN.md#5-environment-variables)中。 +配置API;完整的字段语义位于[配置参考](reference.zh-CN.md#5-环境变量)中。 -### 2.1 Optional remote diagnostics +### 2.1 可选的远程诊断 在生产中保留“DEBUG_ENABLED”和“DEBUG_TOKEN”未设置,除非远程诊断 刻意需要的。两者都必须配置为挂载诊断路由,并且这些路由 只能通过后端源访问。诊断表面和响应详细信息 -记录在[参考](reference.zh-CN.md#3-backend-routes)中。 +记录在[参考](reference.zh-CN.md#3-后端路由)中。 -## 3. Build and run the Docker image +## 3. 构建并运行 Docker 镜像 根 Dockerfile 是容器部署路径。它有独立的 Rust 构建器和 -Debian slim 运行时阶段;有关运行时内容,请参阅 [§8.2](#82-docker-image-details)。 +Debian slim 运行时阶段;有关运行时内容,请参阅 [§8.2](#82-docker-镜像细节)。 ## 4.1 注册 Telegram 和 Stalwart 回调 @@ -70,19 +70,19 @@ Debian slim 运行时阶段;有关运行时内容,请参阅 [§8.2](#82-dock Telegram 使用已保存的 webhook secret 和 `allowed_updates: ["message"]`。注册后,Stalwart 会自动完成验证回调和 verification-code 写回。可以安全地重复注册;更改回调 origin 会更新 Telegram webhook,并为新的 URL 创建 Stalwart subscription。 -## 5. Runtime configuration +## 5. 运行时配置 将启动时进程配置与 Redis 驻留业务配置分开。 进程启动时仅需要“REDIS_URL”和“CONFIG_ENCRYPTION_KEY”。完整的 业务字段列表在【配置参考】(reference.md#5-environment-variables)中。 -## 6. Short-request runtime and scheduling +## 6. 短请求运行模式与任务调度 该服务一次处理一个请求,并且不保留 JMAP 事件流、Telegram 长轮询,或打开后台计时器。 Telegram/JMAP 推送提供快捷路径;的 外部调度程序调用下面描述的端点。 -### 6.3 Schedule queue draining and reconciliation +### 6.3 调度队列排空与状态协调 后端没有内部调度程序。外部 cron、计划作业或平台计时器 必须调用 [`scripts/cron-drain.sh`](../scripts/cron-drain.sh);否则排队的通知 @@ -101,28 +101,28 @@ MW_APP_URL='https://' bash scripts/cron-drain.sh --once 调节节奏通常为 5-10 分钟。两个成功的端点都返回“204” 身体空虚;不要将其视为错误。脚本的“--diagnose”模式有帮助 将服务配置问题与调度问题分开。其论点和 -操作模式列在[调度程序参考](reference.zh-CN.md#93-callback-registration-and-scheduled-work)中。 +操作模式列在[调度程序参考](reference.zh-CN.md#93-回调注册与定时任务)中。 -### 6.4 Reliability behavior +### 6.4 可靠性行为 Worker 仅重试超时和后端“5xx”响应;它返回“4xx”响应 直接。 `/reconcile` 和 `/worker` 具有更长的每条路由超时,并且不会故障转移到 第二个后端,避免重复工作。后端队列处理至少一次并且依赖 关于 Redis 支持的幂等性。请参阅路线和状态参考以了解完整的行为。 -### 6.5 Delivery objective +### 6.5 投递目标 该服务允许短暂的通知延迟,并以至少 99.9% 的通知率为目标 可用性。 Redis 可用性和外部调度间隔超出范围 应用程序的控制;准确的交付保证在“NFR-NOTIFY-SLA”中进行了描述,并且 项目章程中的“NFR-RECONCILE-INTERVAL”。 -## 7. Health checks +## 7. 健康检查 使用 `/healthz` 检查进程存活(`src/notify.rs:118`),使用 `/ready` 检查依赖就绪 -(`src/notify.rs:193`)。验证步骤见[§8.1](#81-verify-the-deployment)。 +(`src/notify.rs:193`)。验证步骤见[§8.1](#81-验证部署)。 -## 8.1 Verify the deployment +## 8.1 验证部署 首先检查后端: @@ -141,7 +141,7 @@ Worker 仅重试超时和后端“5xx”响应;它返回“4xx”响应 确认外部调度程序耗尽其通知。健康的“/healthz”只能证明 进程活跃度;它并不能证明 Redis 或上游任一者可用。 -## 8.2 Docker image details +## 8.2 Docker 镜像细节 根 Dockerfile 的构建器使用 `rust:1-slim-bookworm`;运行时间是 `debian:bookworm-slim`。运行时安装 CA 证书、`tini`、`curl`、`procps`、 @@ -149,62 +149,62 @@ Worker 仅重试超时和后端“5xx”响应;它返回“4xx”响应 `PORT=8080`,并通过 `tini` 启动二进制文件。它没有“VOLUME”声明并且确实 不启动Redis。这些信息描述仓库提供的 Docker 镜像;其他平台可以使用自己的构建器和运行时镜像。 -## 9. Confirmed deployment choices +## 9. 已确认的部署选择 这些部署决定已经确定;它们不是每次安装的先决条件。 -### 9.1 Public ingress and state +### 9.1 公共入口与状态存储 后端使用外部 HTTPS 入口和外部 Redis。 Telegram、JMAP 推送和 计划的请求使用选定的公共 URL。当Worker网关启用后,注册 Telegram 和 Stalwart 的稳定 Worker URL;不要在网关处公开凭据。 本节中的稳定 ID 指向项目章程的注册表。 -## 10. Optional Cloudflare Worker gateway +## 10. 可选的 Cloudflare Worker 网关 Worker 为多个后端源提供一个公共 HTTPS 入口点。所有后端 必须共享相同的Redis和业务配置。 Worker 仅转发其固定的 路由白名单; bootstrap 和 `/debug/*` 仍然仅限原始。详细路线事实在 -[网关路由矩阵](reference.zh-CN.md#4-gateway-vs-backend-route-matrix)。 +[网关路由矩阵](reference.zh-CN.md#4-网关与后端路由对照表)。 -### 10.1 Topology +### 10.1 部署拓扑 Telegram webhooks、Stalwart 推送回调和外部调度可以使用稳定的 Worker 网址。 Worker 转发到后端 HTTPS 源;它不连接到 Redis 或 JMAP。 -### 10.2 Multi-instance prerequisites +### 10.2 多实例前提 多个后端可以共享流量,因为请求状态存储在 Redis 中,重复 交付已进行重复数据删除,协调修复了错过的推送事件。无粘性会话 或需要更改应用程序代码。 -### 10.3 Trust model +### 10.3 信任模型 Worker 传递请求标头和正文。仍需要后端身份验证 因为后端源可能是直接可达的。每个实例必须使用相同的共享 业务凭证和 Redis 状态。 -### 10.4 Routes and failover +### 10.4 路由与故障转移 HTTP 请求访问 Worker 时,会先收到永久的 `308` 重定向,前往路径和查询参数相同的 HTTPS URL,之后才进行路由校验或转发。Worker 使用“404”拒绝未注册的路由,并使用“405”拒绝错误的方法。普通 请求最多尝试“LB_MAX_ATTEMPTS”来源(默认两个),并且只有超时或“5xx” 触发故障转移。 `/reconcile` 和 `/worker` 是具有较长超时时间的单次尝试路由 -覆盖。使用[参考](reference.zh-CN.md#4-gateway-vs-backend-route-matrix)中的路由矩阵和调整值。 +覆盖。使用[参考](reference.zh-CN.md#4-网关与后端路由对照表)中的路由矩阵和调整值。 -### 10.5 Callback URLs +### 10.5 回调地址 使用网关时,将Telegram的webhook和Stalwart的push回调设置给Worker 域。外部调度程序可以对转发路径使用相同的 URL。引导程序和 诊断端点仍然需要后端源。 -### 10.6 Gateway health +### 10.6 网关健康状态 `GET /healthz-worker` 报告 Worker 版本和后端可用性。两者都使用 用于验证部署的“可用”和“版本”; `status: no-backends` 可以返回 当未配置有效源时,HTTP 200。 -### 10.7 Boundaries +### 10.7 功能边界 Worker不是Redis代理,不会使后端无状态;共享外部Redis 仍然需要。它不添加数据库、长期连接或第二层 diff --git a/docs/design.zh-CN.md b/docs/design.zh-CN.md index a2aae21..2084847 100644 --- a/docs/design.zh-CN.md +++ b/docs/design.zh-CN.md @@ -16,50 +16,50 @@ --- -## 0. Document navigation +## 0. 文档导航 -1. [任务与范围](#1-task-and-scope) -2. [jmap-client 能力分析](#2-jmap-client-capability-analysis) -3. [认证与邮箱操作适配](#3-authentication-and-email-operation-adaptation) -4. [Telegram 渠道实现选型](#4-telegram-channel-implementation-selection) -5. [整体架构与数据流](#5-overall-architecture-and-data-flow) -6. [模块划分](#6-module-division) -7. [配置与安全](#7-configuration-and-security) -8. [错误处理与可观测性](#8-error-handling-and-observability) -9. [测试策略](#9-test-strategy) -10. [分阶段实施计划](#10-phased-implementation-plan) -11. [历史问题与决策归档(产品/架构类,均已有结论)](#11-archive-of-historical-issues-and-decisions-productarchitecture-category-all-have-conclusions) -12. [AI 辅助能力:架构、确认门槛、失败回退](#12-ai-auxiliary-capabilities-architecture-confirmation-threshold-failure-fallback) +1. [任务与范围](#1-任务与范围) +2. [jmap-client 能力分析](#2-jmap-client-能力分析) +3. [认证与邮件操作适配](#3-认证与邮件操作适配) +4. [Telegram 渠道实现选型](#4-telegram-渠道实现选型) +5. [整体架构与数据流](#5-整体架构与数据流) +6. [模块划分](#6-模块划分) +7. [配置与安全](#7-配置与安全) +8. [错误处理与可观测性](#8-错误处理与可观测性) +9. [测试策略](#9-测试策略) +10. [分阶段实施计划](#10-分阶段实施计划) +11. [历史问题与决策归档(产品/架构类,均已有结论)](#11-历史问题与决策归档产品架构类均已有结论) +12. [AI 辅助能力:架构、确认门槛与失败回退](#12-ai-辅助能力架构确认门槛与失败回退) > 部署/平台类决策已确认(单账户、App Password+Basic、Redis 托管+AOF、平台 HTTPS URL、外部 Cron)并**全部收敛归档**(含 `Q-DEP-A`/`Q-DEP-B`,见 `docs/deployment.md` 的已确认决策一节);**未完成的代码缺口见 `docs/opengaps.md`**。 --- -## 1. Task and Scope +## 1. 任务与范围 -### 1.1 Goals +### 1.1 目标 构建一个 Rust 写的 Telegram 机器人,作为 Stalwart JMAP 邮箱的**个人邮件助手**: - 通过 Telegram 命令查询/阅读邮件、查看文件夹、发送邮件、管理关键词等。 - 利用 JMAP 的 **Push HTTPS 回调**(+ 外部 Cron 对账兜底)在新邮件到达时**主动推送到 Telegram**。(EventSource/SSE/长轮询为非目标,见 deployment.md `NG-POLLING-SSE`/`NG-LONG-POLLING`/`C-NO-LONG-CONN`。) - 单用户或多账户部署(默认面向单账户自托管场景)。 -### 1.2 Scope (this stage) +### 1.2 本阶段范围 - ✅ 调研 + 方案设计(本文档) - ❌ 不创建/修改项目代码、不初始化 cargo 工程 - ✅ 输出可供 lead 与用户评审的详细方案,标注关键不确定项 -### 1.3 Research basis +### 1.3 研究依据 - [`stalwartlabs/jmap-client`](https://github.com/stalwartlabs/jmap-client)(main 分支,截至本次调研) - 项目目录:`/home/okabe/Repo/messageweave/`(工具链要求见 §10.0 与 `../AGENTS.md §3.3`) --- -## 2. jmap-client capability analysis +## 2. jmap-client 能力分析 来源:直接阅读 `stalwartlabs/jmap-client` 仓库的 `stalwartlabs/jmap-client/src/lib.rs`、`stalwartlabs/jmap-client/src/client.rs`、`stalwartlabs/jmap-client/src/email/`、`stalwartlabs/jmap-client/src/email_submission/`、`stalwartlabs/jmap-client/src/event_source/`、`stalwartlabs/jmap-client/Cargo.toml`、`stalwartlabs/jmap-client/README.md`、`stalwartlabs/jmap-client/examples/`。 -### 2.1 crate overview +### 2.1 crate 概览 | 项 | 值 | |---|---| | crate 名 | `jmap-client`(crates.io) | @@ -69,7 +69,7 @@ | `forbid(unsafe_code)` | 是(lib.rs 顶部声明)✅ | | 默认 features | **0.4.2 实测 `default = ["async", "websockets", "aws_lc_rs"]`**(含 WebSocket 栈 `tokio-tungstenite`)。⚠️ `default-features = true` 会**隐式启用 WebSocket**,违反 `C-NO-LONG-CONN`。**本项目实际选用 `default-features = false, features = ["async", "rustls"]`(无 `websockets`)**,以 `Cargo.toml` 为准。 | -### 2.2 Module structure (`src/`) +### 2.2 模块结构(`src/`) ``` lib.rs 顶层:URI / Method / DataType / PushObject / Error 枚举 client.rs Client:认证、connect、session、send_request、event_source、ws @@ -88,7 +88,7 @@ event_source/ SSE 流:mod / parser / stream client_ws/ WebSocket 客户端(feature = "websockets") ``` -### 2.3 Key capabilities mapped to this Bot +### 2.3 与本 Bot 对应的关键能力 | Bot 需求 | jmap-client API | 备注 | |---|---|---| | 登录/会话 | `Client::new().credentials(...).connect(url)` | 支持 Basic 与 Bearer;connect 解析 session URL、capabilities | @@ -102,14 +102,14 @@ client_ws/ WebSocket 客户端(feature = "websockets") | 实时通知(Push + 对账兜底) | Push HTTPS 回调 → `StateChange`;外部 Cron 调用 `/reconcile` 使用 `Email/changes` 补差 | 需公网 HTTPS 入口(deployment.md `C-HTTPS-INBOUND`/`FLOW-NEW-MAIL`);Push 不是唯一可靠来源 | | SSE / WebSocket(非目标) | `event_source` / `client_ws` | 本部署**不使用**(deployment.md `NG-POLLING-SSE`/`NG-LONG-POLLING`/`C-NO-LONG-CONN`);仅列 crate 能力供调研 | -### 2.4 Authentication mechanism (`stalwartlabs/jmap-client/src/client.rs`) +### 2.4 认证机制(`stalwartlabs/jmap-client/src/client.rs`) - `Credentials::Basic { username, secret }` — 用户名/密码,Stalwart 原生支持。 - `Credentials::Bearer { token, .. }` — OAuth2 access_token;可选 `refresh_token` + `refresh_url` + `refresh_grace`,client 会在过期前自动刷新。 - `connect()` 作用:GET session URL → 解析 `accounts`/`capabilities`/`download_url`/`upload_url`/`event_source_url`,缓存 account_id。 - 支持自定义 `reqwest::Client`(`Client::new().client(reqwest_client)`):可注入代理、TLS 配置、超时、UA。 - 支持 `accept_long_responses` / `event_source(ping, ..)` 用于长连接保活。 -### 2.5 Error model (`Error` enumeration) +### 2.5 错误模型(`Error` 枚举) ``` Transport(reqwest::Error) 网络/TLS/超时 Parse(serde_json::Error) 序列化 @@ -122,7 +122,7 @@ WebSocket(...) ws 错误(feature 开启时) ``` - `MethodErrorType` 细粒度:`ServerUnavailable`/`ServerFail`/`RateLimit`/`InvalidArguments`/`Forbidden`/`StateMismatch`/`TooManyChanges`… → 可直接驱动 Bot 的重试/限流/状态重置策略。 -### 2.6 Real-time channel decision-making (determined, see deployment.md) +### 2.6 实时通信渠道的决策(已确定,参见 deployment.md) > 部署目标 = **通用 HTTPS-only Docker、无长连接**(deployment.md `C-NO-LONG-CONN`/`C-HTTPS-INBOUND`)。 > 当前通道 = JMAP Push HTTPS 回调(先由管理员通过 `POST /api/push/register` 显式注册,见 §7.3)+ 外部 Cron `/reconcile` 对账兜底。Push 订阅不会自动创建,不能把 Push 当作唯一可靠来源。 @@ -138,16 +138,16 @@ WebSocket(...) ws 错误(feature 开启时) --- -## 3. Authentication and email operation adaptation +## 3. 认证与邮件操作适配 -### 3.1 Authentication Strategy (Confirmed: App Password + Basic) +### 3.1 认证策略(已确认:应用专用密码 + Basic) - **确认采用**(`C-AUTH-APP-BASIC`):`Credentials::Basic`(Stalwart 账户邮箱 + **应用专用密码 App Password**)。App Password 可独立吊销/设到期,不用主密码。配置注入,不入代码。 - **不采用**:OAuth/OIDC `Bearer`(无 OIDC 需求时增加复杂度,不选);主密码 Basic。 - **安全**:密码仅存配置或密钥管理器;运行期用 `secrecy::SecretString` 包裹,日志永不打印明文(见 §7.2)。 - 运行期注入方式见 deployment.md(`C-NO-SECRET-IN-IMAGE`)。 - **单账户**(`REQ-SINGLE-ACCOUNT`):本实例只接一个 Stalwart 账户;多账户 = 部署多个 bot 实例。 -### 3.2 Mailbox operation adaptation layer (JMAP ↔ Bot semantics) +### 3.2 邮箱操作适配层(JMAP ↔ Bot 语义) 建议在 `domain::jmap` 模块封装一层领域语义,对上层只暴露业务动词: | Bot 动词 | 封装函数 | 内部 JMAP | @@ -163,7 +163,7 @@ WebSocket(...) ws 错误(feature 开启时) --- -## 4. Telegram channel implementation selection +## 4. Telegram 渠道实现选型 Telegram 渠道用 `src/channel.rs` 的 `reqwest` 自研实现(`ARCH-DEPS-STAGE4`),**不引入第三方 Bot 框架**。 评估过 `teloxide` 但**未采用**:本项目只用 Bot API 的少数调用,不值得为它引入 dptree 调度与自带会话中间件这两层抽象;`throttle` 与 Redis 会话能力用 `src/channel.rs` 的重试与外部 Redis TTL 各实现了一部分。 @@ -171,9 +171,9 @@ Telegram 渠道用 `src/channel.rs` 的 `reqwest` 自研实现(`ARCH-DEPS-STAG --- -## 5. Overall architecture and data flow +## 5. 整体架构与数据流 -### 5.1 High-level topology +### 5.1 总体拓扑 ``` ┌───────────────────────────────────────────────────┐ │ Telegram │ @@ -212,7 +212,7 @@ Telegram 渠道用 `src/channel.rs` 的 `reqwest` 自研实现(`ARCH-DEPS-STAG > EventSource/SSE 与长轮询均为非目标(`NG-POLLING-SSE`/`NG-LONG-POLLING`)。 > 渠道解耦(见 §5.2):领域层与渠道层以领域 Command / Notification 交互,领域层不出现任何具体渠道 SDK 类型;钉钉/飞书仅保留扩展位,不提前实现。 -### 5.2 Channel abstraction and multi-channel expansion strategy +### 5.2 渠道抽象与多渠道扩展策略 - **现状**:首个(也是当前唯一)渠道是 Telegram。目标是保留平行扩展钉钉/飞书的能力,**但不提前实现**。 - **原则**:领域层与渠道层解耦——邮件/JMAP/AI/意图状态机**不得依赖 Telegram 类型**。 - **抽象(不过度设计)**:不预留空 trait。`src/channel.rs` 曾有三个零实现的 @@ -228,7 +228,7 @@ Telegram 渠道用 `src/channel.rs` 的 `reqwest` 自研实现(`ARCH-DEPS-STAG - 新增渠道 = 新 adapter + 装配,不改领域层。真需要抽象时**带实现一起加**,不留空 trait。 - **不做什么**:不定义多态配置注册表、不预先抽象"渠道能力矩阵"、不建 plugins 机制;按需再演进(YAGNI)。 -### 5.3 Running model (short request, no long connection) +### 5.3 运行模式(短请求、无长连接) - 单一二进制 `message-weave`,`#[tokio::main]`。 - 启动时: 1. 加载配置(`config::Config`)。 @@ -247,7 +247,7 @@ Telegram 渠道用 `src/channel.rs` 的 `reqwest` 自研实现(`ARCH-DEPS-STAG - 优雅退出:信号 + `CancellationToken`,退出前 flush Redis Streams 待处理条目(**不写本地文件**,`C-NO-LOCAL-WRITE`)。 - **当前现状**:完成配置引导、HTTP 入口和写入口鉴权(`R1`/`SAF-AUTH-*`,fail-closed);JMAP session、Email/changes、PushSubscription create/update、Redis Streams worker 和 `/reconcile` 已接入。`/reconcile` 与 Push 闭环仍需在真实 Stalwart、Redis、Telegram 环境完成端到端验收,不能仅凭本地门禁宣称生产链路已验证。 -### 5.4 Data flow: new email push (critical path, FLOW-NEW-MAIL) +### 5.4 数据流:新邮件推送(关键路径,FLOW-NEW-MAIL) ``` Stalwart JMAP Push → POST /push/jmap (StateChange{Email/EmailDelivery: new_state}) → 校验 pushSubscriptionId + verificationCode → 幂等去重(MOD-DEDUP) → 入 Redis Streams(MOD-STREAMS) → 立即 2xx ACK @@ -262,7 +262,7 @@ Stalwart JMAP Push → POST /push/jmap (StateChange{Email/EmailDelivery: new_sta > 通知只含发件人/主题/时间(+附件数),正文绝不出现在通知里(见 `docs/charter.md §3` 安全边界 `SAF-NOTIFY-META`)。 > 无长连接:Push 为短请求回调,对账由**外部 HTTPS Cron**触发(`C-NO-LONG-CONN`),非容器内自持定时器。 -### 5.5 Data flow: command `/read ` (long mail branch) +### 5.5 数据流:`/read ` 命令(长邮件分支) ``` TG /read 3 → handler 取会话里的 folder+page 游标 → JmapService.list_emails(folder, page=3) @@ -275,7 +275,7 @@ TG /read 3 → handler 取会话里的 folder+page 游标 ("AI 总结"分支必须用户确认后才把正文交 LLM 生成 ~300 字摘要;见 §12.3) ``` -### 5.6 Session State Machine (Conversation FSM) +### 5.6 对话状态机(FSM) 当前**没有 FSM**。命令路由由 `src/worker.rs` 的 `parse_intent` 解析为 `Intent`;AI 授权态是 Redis 里的一个布尔加过期时间(键与 TTL 见 `docs/reference.md` 的 AI 授权态一节),不是多步确认态。 @@ -283,7 +283,7 @@ TG /read 3 → handler 取会话里的 folder+page 游标 曾设计过的 5 态 FSM(`Idle` / `AwaitClarify` / `AwaitConfirm` / `Analyzing` / `AwaitFallback`)连同状态转移表与渠道中立说明,见 `docs/retired.md`。 -### 5.7 Email search (`/search`, `bfe0fd8` implemented) +### 5.7 邮件搜索(`/search`,已在 `bfe0fd8` 实现) `/search <关键词>` 与中文前缀 `搜索`/`查找`/`检索`(**仅前缀匹配**,避免"帮我搜一下…"被劫持成搜索)触发 `Intent::Search`,经 `JmapService::search_emails(account_id, query, limit)` 走 `email_query`(`Filter::text`) 取 ID + `SearchSnippet/get` 取 `subject`/`preview` 高亮。 @@ -298,7 +298,7 @@ TG /read 3 → handler 取会话里的 folder+page 游标 --- -## 6. Module division +## 6. 模块划分 实际工程结构(本文件所有代码锚点均指此结构;历史的目标模块拆分计划见 `docs/retired.md`): ``` @@ -331,7 +331,7 @@ message-weave/ 测试:**没有集成测试目录**,也没有独立的 util 工具模块。所有 Rust 测试是各模块内的 `#[cfg(test)]` 单元测试;前端唯一测试是 `web/config.test.mjs`。 -### 6.1 Module Responsibility Matrix +### 6.1 模块职责矩阵 > **依赖现况**:`jmap-client 0.4.2`、`redis 0.27`、`reqwest 0.13` **均已引入**(`ARCH-DEPS-STAGE1` / `ARCH-DEPS-STAGE4`,版本以 `Cargo.toml` 为准)。`teloxide` 经评估**未引入**,Telegram 渠道用 `reqwest` 自研实现(§4,详见 `docs/retired.md`)。下表每一行都是**真实存在的模块**。 @@ -352,9 +352,9 @@ message-weave/ --- -## 7. Configuration and Security +## 7. 配置与安全 -### 7.1 Historical configuration reading (migrated to Redis) +### 7.1 历史配置读取(已迁移至 Redis) > **迁移目标(`C-REDIS-ONLY-STATE`)**:生产启动环境仅保留 `REDIS_URL`。空 Redis > 的首次配置必须通过 `CONFIG_ENCRYPTION_KEY` 认证的一次性 bootstrap/admin 会话完成; @@ -398,14 +398,14 @@ message-weave/ > **入口鉴权密钥(`TG_WEBHOOK_SECRET`/`RECONCILE_TOKEN`)在业务配置完成后必须有效**(fail-closed,`SAF-AUTH-*`)。JMAP Push 的验证码由 Stalwart 在订阅创建后动态生成,不属于业务配置。启动引导变量缺失时服务进入配置引导模式并保持 SPA 可访问;不会因为缺少启动变量而伪造业务成功,也不会绕过入口鉴权。 > **JMAP session URL 归一化(`REQ-JMAP-SESSION-URL`/`SAF-JMAP-URL`)**:`JMAP_SESSION_URL` 既接受**服务基地址**(`https://mail.example.com`)也接受**完整 session URL**(`https://mail.example.com/.well-known/jmap`)。代码在调用 `jmap_client::Client::connect` **之前统一归一化为 origin/base**——`jmap-client` 会自动追加 `/.well-known/jmap`,故**不会出现重复路径**(如 `…/.well-known/jmap/.well-known/jmap`)。约束:**仅 HTTPS**(http 拒绝);**禁止 URL 内嵌用户名/密码**(凭据只经 `JMAP_USERNAME`/`JMAP_PASSWORD`,`C-AUTH-APP-BASIC`);拒绝危险 query。**重定向白名单**:`jmap-client` 默认**拒绝一切重定向**,而 Stalwart 会把 `/.well-known/jmap` 307 跳到 `/jmap/session`,因此连接时把**配置里的 origin host 自身**加入信任名单,服务端改写出的其它 host 仍被拒绝(`follow_redirects([trusted_redirect_host(base)])`)。已知上游隐患:Stalwart 的 307 `Location` 会把 `user:pass` 写进 URL,且客户端把该 URL 带凭据回传——故传输错误串**不做额外日志**。(状态:D-G1-1 **代码已实现,并已用真实 Stalwart 实例通过 `cargo test -- --ignored jmap::` 验证**。) -### 7.2 Key Management +### 7.2 密钥管理 - **禁止**把 token/密码写入仓库或任何配置文件(本项目配置不进仓库,`ARCH-CONFIG-ENV`)。 - 启动期只读 2 个进程级变量(`REDIS_URL`、`CONFIG_ENCRYPTION_KEY`,由 `src/main.rs` 直接 `std::env::var`;缺任一即降级到只读 setup 模式);业务配置从 Redis 经 `config` 层反序列化(**无** `${VAR}` 插值/figment/配置文件,`ARCH-CONFIG-ENV`)。 - 运行期用 `secrecy::SecretString` 包裹,`Debug` 实现打 `***`。 - 日志过滤:`tracing` 字段层屏蔽 `Authorization`/`password`/`token`。 - 运行期 secret 注入方式(env / `*_FILE` / 编排器 secret)见 deployment.md(`C-NO-SECRET-IN-IMAGE`)。 -### 7.3 Access control +### 7.3 访问控制 - **入口鉴权(硬约束 `SAF-AUTH-RECONCILE`/`SAF-AUTH-TG-WEBHOOK`/`SAF-AUTH-JMAP-PUSH`)**:三条写路径必须先鉴权,**fail-closed**—— - `/reconcile`:请求头 `Authorization: Bearer `; - `/webhook/tg`:请求头 `X-Telegram-Bot-Api-Secret-Token == TG_WEBHOOK_SECRET`; @@ -418,7 +418,7 @@ message-weave/ - **命令最小化**:只暴露必要命令;发邮件等写操作必须二次确认(当前未实现发信,见 §10.3)。 - **速率**:出站侧未建本地令牌桶;Telegram 出站发送按 Redis 运行参数 `max_retries` 重试(默认值与硬上限见 `docs/reference.md` §6.1);当前仅对 Push 验证码写入做 Redis 限流(`ratelimit:push-verify:*`)。Telegram 服务端 30 msg/s 限制下的 429 **按 `parameters.retry_after` 秒自动退避**(`channel.rs`,`f4cae00`):`retry_after_ms` 解析后截断到 60s 预算上限,缺该字段或非数字时回退指数退避 `backoff_delay_ms`(250ms 起、封顶 4s),整体重试预算 60s。仍不做本地令牌桶限流——超出预算直接返回失败,交由上游重试。 -### 7.3.1 Configuration Management API +### 7.3.1 配置管理 API - `GET /` 提供嵌入 Rust 二进制的 SPA;`/assets/config.js` 与 `/assets/styles.css` 提供页面资源。服务不在运行时读取或写入本地文件(`C-NO-LOCAL-WRITE`)。 - `GET /api/status` 公开返回 `{ "ready": boolean, "mode": "configured" | "configuration-setup", "missing": string[], "version": string }`,只列缺少的环境变量名称。缺少 `REDIS_URL` 或 `CONFIG_ENCRYPTION_KEY` 时,SPA 只显示配置引导状态与缺失变量;服务状态确认 ready=true 后才显示管理会话授权区。`version` 是 `build.rs` 在编译期烘焙的 `BUILD_VERSION`(`+`),SPA 页脚据此确认部署是否真正落地。 - `POST /api/admin/session` 接受 `Authorization: Bearer `,返回 `{ "session": "", "expires_in": 900 }`;admin session 仅存 Redis 中的摘要并在 900 秒后过期。`POST /api/admin/session/revoke` 撤销当前 session,成功返回 `204`。 @@ -439,12 +439,12 @@ message-weave/ - `POST /api/bootstrap` 也接受**完整的** `BusinessConfigWire`(不做补丁合并),并共用上面「持久化与报错分离」的语义:校验失败是 `422`、持久化后构建失败仍是 `200` + warnings。因为不合并,它是唯一能**从零创建**业务配置的补丁路径(PUT 在已存配置缺失且补丁不全时返回 `422`,见上条)。 - `PUT /api/config` 的运行参数错误使用 `401`(未授权)、`400`(JSON 无效)、`422`(范围错误)、`503`(Redis 不可用);GET 读取失败时也以 `503` 表示 Redis 不可用。业务 PUT 使用 `401`(未授权)、`400`(JSON 无效)、`422`(补丁无法解析、已存配置不可解密、合并结果校验失败、首次保存时补丁不完整)、`409`(提交的 `revision` 已过期)、`500`(合并结果序列化失败)与 `503`(Redis 不可达、写入失败或 reload 提交失败);业务 GET 使用 `401`、`400`、`422`(已存配置不可解密)与 `503`(Redis 不可达)——注意 `Invalid` 是 422 不是 503,`Unreachable` 才是 503。**业务 PUT 有 `409`**:`revision` 是**请求体里的控制字段**(不是 header)——SPA 读配置时记下 `revision`,之后的每次增量提交都带回同一个值;已存 revision 与请求 revision 不等时返回 `409 conflict`,既不写入也不自增 revision,SPA 收到 `409` 会重新读回已存值再让操作者改,所以旧页面不会静默覆盖新的保存。请求体不带 `revision` 时保持 last-write-wins,兼容从不读 revision 的调用方。该比较是**先比较后写入**、非原子:两个请求若在同一瞬间竞争,两个都会通过、后到的覆盖先到的;没有 Lua 脚本时乐观锁的代价就是这个残留窗口,而补丁只带操作者实际改过的字段,窗口影响有限。`409 conflict` 的其余出现处是三把锁的竞争(`reconcile` 与 `register_push` / `register_worker_token` 的 `acquire_lock` 返回 false),与业务配置写入无关。依赖客户端构建失败**不**返回错误状态码,而是 `200` + warnings(见上条)。其余错误不回显敏感数据。Redis/会话初始化未完成时管理页面显示 `503` 状态;运行参数在重新读取成功前禁用保存(`C-REDIS-ONLY-STATE`)。LLM 启用时,业务配置要求提供 HTTPS Base URL、非空 API key 和模型名。 -### 7.4 TLS (code/dependent behavior) +### 7.4 TLS(代码与依赖行为) - TLS provider 由依赖 feature 决定:**jmap-client 0.4.2 默认含 `aws_lc_rs`(并引入 `rustls`)**,`default-features = true` 时并非"rustls 默认"。**本项目实际 `default-features = false, features = ["async","rustls"]`**(不启用 `aws_lc_rs`/`websockets`),最终以 `Cargo.toml` 为准。 - 证书校验**默认强制**,仅测试可关。 - 镜像内系统 CA 信任库、内部 CA 注入、入站 webhook TLS 终止等运维细节见 deployment.md。 -### 7.5 Production red line: no database / no local writing / standard output log (deployment.md §0/§8.2) +### 7.5 生产红线:无数据库、无本地写入、日志写入标准输出(deployment.md §0/§8.2) 本节汇总跨代码与部署的绝对红线,**不得**在代码中引入任何"看起来方便"的本地状态: @@ -458,15 +458,15 @@ message-weave/ **验证**:静态检查代码不出现本地路径常量 / `std::fs` 非测试调用 / 文件日志后端;运行期通过容器 `/proc/mounts` 与 `docker inspect` 确认无本地卷挂载;部署检查清单见 deployment.md §8.2。 -### 7.6 Why is the remote joint debugging interface absolutely closed by default? +### 7.6 为什么远程联调接口必须默认完全关闭? 该面存在的唯一理由是缩短生产排障路径:在无法登录容器、只能靠外部请求观察系统时,需要有人能在不发版的前提下探到 JMAP/Telegram 连通性与当前业务配置。代价是它的入口软度必然高于三条写路径——除只读探测外还保留一次**真实出站发送**,且复用业务白名单而非独立白名单。因此设计选择是「默认关闭」而不是「默认开启、靠网关挡」:进程满足「`DEBUG_ENABLED` 为真值或命令行带 `--debug`」**且**设置了非空 `DEBUG_TOKEN` 时才会挂载这组路由(`SAF-DEBUG-GATE`);缺一时路由在路由器里**根本不存在**,请求落到 axum 通用 `404`,而非「存在但 401」——后者会泄漏路由存在性。也不存在第三种状态:没有「未配置即开放」的回退,也没有任何配置项能把它设为默认开启。`DEBUG_ENABLED` 走环境变量而非启动命令,是为了让开关能在不发版的前提下从平台控制台改一次完成——启动命令保持静态。再叠一层位置约束:它不在网关的安全路由白名单内,即便后端开错,经平台入口也会被 fail-closed 拒掉,唯一可达路径是直连后端 origin。这四处——双因子挂载、404 而非 401、无默认开启回退、网关不可达——共同构成「暴露面默认为零」的架构决策。启用方式、逐端点状态码与部署确认清单见 deployment.md §2.1。 --- -## 8. Error handling and observability +## 8. 错误处理与可观测性 -### 8.1 Unified error enumeration +### 8.1 统一错误枚举 当前实现(`src/error.rs` 全文,4 个变体): @@ -488,7 +488,7 @@ pub enum BotError { 设计目标形态曾包含 `Jmap(#[from] jmap_client::Error)`、`Telegram(#[from] teloxide::errors::RequestError)`、`Storage(#[from] redis::RedisError)`、`RateLimited`、`Unauthorized(i64)`、`Llm(LlmErr)` 六个额外变体;`Telegram` 随 `teloxide` 未采用而废弃,其余至今未落地,完整清单见 `docs/retired.md`。 -### 8.2 Error → User Message Mapping +### 8.2 错误到用户消息的映射 | 底层错误 | Bot 行为 | |---|---| | `Transport` / `ServerUnavailable` / `RateLimit` | 指数退避重试(`util::retry`),超阈值给用户"暂时不可用,稍后重试" | @@ -498,7 +498,7 @@ pub enum BotError { | `StateMismatch` | 重置 sinceState → 全量补一次(防丢/防重) | | `Llm(*)` | 见 §12.7(熔断/回退/徽标) | -### 8.3 Observability +### 8.3 可观测性 - `tracing`(直接依赖)+ `tracing-subscriber`(fmt + EnvFilter)。 - `tracing`(直接依赖)+ `tracing-subscriber`(fmt + EnvFilter)。**当前只记录启动期事件**:`src/main.rs` 共 5 处 `info!`/`warn!`(启动变量缺失降级到 setup 模式、启动横幅、debug 端点开启、JMAP 服务不可用降级×2)。请求级事件(Push 回调、Reconcile 拉取、渠道推送、LLM 耗时)**尚未实现**——`src/` 里除 `main.rs` 外没有任何 tracing 调用,也没有命名 span(无 `#[instrument]` / `span!`)。 - 指标(可选 `metrics` crate):Push 回调到达数、去重命中率、Redis Streams 积压深度(pending)、DLQ 条数、对账补差条数、JMAP 请求延迟、推送失败率。**当前未接入任何指标后端**(`metrics` 不在 `Cargo.toml`)。 @@ -507,9 +507,9 @@ pub enum BotError { --- -## 9. Test strategy +## 9. 测试策略 -### 9.1 Levels +### 9.1 测试层级 | 层 | 位置 | 内容 | |---|---|---| @@ -520,13 +520,13 @@ pub enum BotError { **没有集成测试目录**;前端唯一测试是 `web/config.test.mjs`(Node 原生断言)。 -### 9.2 mock strategy +### 9.2 mock 策略 - `JmapService` 持有 trait `JmapBackend`(`list_emails` / `read_email` / `send_email` / `changes` 等),生产实现包装 `jmap_client::Client`(`src/domain/jmap/client.rs`),测试用 `MockBackend`(`src/domain/jmap.rs`)。 - Telegram 出站不引入任何框架 mock:`src/channel.rs` 起一个进程内 `TcpListener`,用 `AtomicUsize` 记录请求次数,验证 URL、状态码与超时/重试行为。 - `src/ai.rs` 通过构造 `LlmClient` 的配置断言 HTTPS 强制(`llm_requires_https`);摘要逻辑在 mock OpenAI 兼容端点上验证。 - 渠道与领域解耦靠类型约束(`domain.rs` 的公开接口不出现渠道 SDK 类型),不依赖 mock channel。 -### 9.3 Key invariant testing +### 9.3 关键不变量测试 - `sinceState` 存外部 Redis:模拟重启后从正确游标续传;模拟 Redis 清空后由对账(`reconcile`,`FLOW-RECONCILE`)从 JMAP 恢复游标。 - 通知去重:同一 `email_id` 不重复推送。 - 正文转义:含 `