26 KiB
MessageWeave — 项目章程
本文件是项目专属规范:项目目标、技术选型、安全不变量、实现阶段、禁止事项、 测试验收、文档边界与稳定 ID 注册表。
通用、语言无关的代码编写与环境构建规范见
../AGENTS.md; 可核对事实(路由 / Redis 键与 TTL / 配置项 / 错误码 / 出站常量)的唯一权威来源见reference.md;缺口与阻塞见opengaps.md。三者冲突时:可核对事实以
reference.md为准,项目约束以本文件为准,质量规则以../AGENTS.md为准。
代码基线
fc686ab(src/ 行号锚点)|文档以当前main为准,锚点须随代码基线复核。
1. Project goals
一个部署在 Docker + 外部托管 Redis 的 JMAP 邮件通知服务:
- 通过 JMAP 只读拉取邮件变更,判断是否需要通知。
- 可选的 LLM 分析(AI 开关 + 用户级外部授权),分析结果不存储、不进入消息。
- 通过 Telegram 向白名单 chat 推送仅含元数据的通知。
- 用户通过 Telegram 命令(
/summary、/search)与/help交互。 - 通过浏览器 SPA 配置业务参数;通过受保护接口开关业务处理。
不做:多租户、账号体系、Web UI 之外的客户端、消息持久化、邮件双向同步。
2. Technology selection (locked)
| 层 | 选型 | 版本 |
|---|---|---|
| 语言 | Rust | edition 2021 |
| Web 框架 | axum | 0.8 |
| 异步 | tokio | 1 |
| 状态存储 | Redis | redis 0.27 |
| JMAP 客户端 | jmap-client | 0.4.2(default-features = false + features ["async","rustls"]) |
| Telegram | 自建轻量客户端(src/channel.rs),不引入 bot 框架 |
— |
| HTTPS 客户端 | reqwest | 0.13 |
| 密钥类型 | secrecy | 0.10 |
| 加密 | ring | 0.17 |
| 日志 | tracing + tracing-subscriber | 0.1 / 0.3 |
| 调度 | 外部 cron → POST /reconcile |
— |
| 前端 | 原生 HTML + 单文件 JS + 单文件 CSS,无构建工具、无框架 | — |
| 网关 | Cloudflare Worker(纯 JS,零依赖) | — |
3. Security boundary (cannot be violated)
23 条,编号 SAF-* / REQ-* / C-*,全部登记在 §8 注册表。
- JMAP 只读:不得使用 Send 类接口,禁止修改邮箱任何内容。
- 不出站发信:
src/channel.rs是唯一 Telegram 出站出口;领域层禁止直接发信。 - 禁止长连接:不得引入 WebSocket / SSE / 长轮询;所有通信为短请求-响应。
- 无状态恢复:不得用本地文件或内存状态做恢复;崩溃后仅依赖 Redis 中的可重放记录。
- 无数据库:不得引入关系型或文档型数据库。
- 无本地写:除 stdout 日志外,进程不写任何文件。
- 日志只走 stdout:不写日志文件、不接外部日志 agent。
- 状态只在 Redis:一切可变状态只存在于外部托管 Redis。
- 密钥只来自环境变量:进程配置不从 Redis 读取。
- 密钥不回显:任何日志、错误响应、Debug 输出都不得包含密钥本体。
- 密钥常量时间比较:管理凭据与 admin session 摘要必须常数时间比对。
- 通知只含元数据:Telegram 通知只含发件人 / 主题 / 时间(+ 附件数),正文绝不进入通知。
- AI 结果不进入消息:LLM 输出只用于决策,不落 Redis、不进入 Telegram 消息。
- AI 需用户级外部授权:只有显式授权过且未过期的 chat 才触发 LLM;授权是外部行为,进程不代授权。
- 业务白名单:出站 Telegram chat 必须命中
CHAT_ALLOWLIST。 - 诊断面默认关闭:
/debug/*需要DEBUG_ENABLED(或--debug)+DEBUG_TOKEN双因子同时满足。 - 诊断面不进网关:
/debug/*不在 WorkerSAFE_ROUTES,只能直连后端 origin。 - 网关只透传、不决策:Worker 不解析请求体、不校验业务逻辑、不下发
Retry-After。 - AI 结果不重试:LLM 分析失败即视为「不通知」,不重试、不降级为无 AI 通知。
- 无状态进程:两个容器实例并发安全;无本地写。
- 无长轮询调度:调度由外部 cron 触发
POST /reconcile。 - AI 分析不持久化:分析结果与授权状态之外的中间态不落盘。
- JMAP capability 动态发现:禁止硬编码 JMAP 能力列表,必须运行时读取。
4. Implementation phase (historical sequence, used to explain code structure)
| 阶段 | 内容 | 稳定 ID |
|---|---|---|
| 0 | 配置 / 状态存储接口抽象,HTTP 骨架 | ARCH-DEPS-STAGE0 |
| 1 | JMAP 只读 adapter | ARCH-DEPS-STAGE1, MOD-JMAP-CLIENT |
| 2 | Telegram 渠道自建客户端(不引入 bot 框架) | ARCH-DEPS-STAGE4, MOD-TELEGRAM-NOTIFY |
| 3 | AI 可选接入 | REQ-AI-EXTERNAL-CONSENT, REQ-AI-FUSE |
| 4 | Redis 落地 | ARCH-DEPS-STAGE4 |
| 5 | 生产化(debug 面、网关、对账) | MOD-DEBUG, SAF-DEBUG-GATE |
阶段编号是历史顺序,不是当前状态声明。 当前状态以 reference.md 与代码为准。
5. Prohibited matters
- 不得引入未在本文件 §2 登记的依赖。 新增依赖须先在本文件登记并说明理由。
- 不得为通过门禁而删除测试、跳过校验、放宽检查级别、删除访问控制。
- 不得在文档或注释中引用不存在的依赖、函数、类型、Redis 键或行号。
- 不得在代码中写「计划实现 X」类注释;占位实现必须显式标注为占位。
- 不得在 SPA / 网关引入构建工具或框架;
web/保持单文件 JS + 单文件 CSS。 - 不得让 SPA 持久化任何凭据(无
localStorage/sessionStorage/ Cookie 赋值)。 - 不得提交未验证的代码;门禁结果必须来自本轮实际执行。
- 不得声称已通过真实环境验证,除非本轮真的连过。
- 不得在
web/、cloudflare-worker/之外引入前端资源。 - 不得让业务配置静默回退到环境变量。
6. Test acceptance and access control
6.1 Change verification closed loop
任何涉及运行时行为或可核对事实的改动,同一改动内完成:实现 → 测试 → 文档。
不得只改代码不改文档,也不得只改文档不改代码。
6.2 GATE-P0 (code access control)
Debian 最小发行版容器内执行,容器内必须显式 export PATH=/usr/local/cargo/bin:$PATH:
cd /home/okabe/Repo/messageweave && docker run --rm --user 1000:1000 \
-e HOME=/tmp -e RUSTUP_HOME=/app/.gate-cache/rustup -e CARGO_HOME=/app/.gate-cache \
-v "$PWD":/app -w /app rust:1-slim-bookworm \
bash -lc 'export PATH=/usr/local/cargo/bin:$PATH; cargo fmt --all -- --check && cargo check --locked && cargo clippy --locked --all-targets -- -D warnings && cargo test --locked 2>&1 | tail -12'
当前基线:93 passed / 0 failed / 4 ignored(新增 4 个游标解析与 walk 分页单测后复测;ignored:real_server_tests::session_list_and_read_smoke 与 debug::tests::debug_config_reports_timezone_of_business_configured_app 需外部真实 JMAP 服务器凭据,real_redis_ttl_tests::ttl_claim_dedup_sets_the_exactly_requested_expiry 与 real_redis_ttl_tests::ttl_consent_and_retry_landing_on_real_redis 需 REDIS_TEST_URL)。
注意:docker run 的 bash -lc 脚本必须用单引号包裹。用双引号会先在宿主机展开 $PWD / $PATH,容器内找不到 cargo。
6.3 GATE-DOCS (Document Access Control)
cd /home/okabe/Repo/messageweave && bash scripts/docs_check/run_all.sh
6 个校验器全过、exit 0 才算通过。只读,从不写文件。
新增 check_file_size:src/、web/、cloudflare-worker/src/ 下超过 500 行的源文件必须带
SPLIT-EVAL: 标记(AGENTS.md §2.1)。它证明标记存在,不证明写下的拆分理由站得住脚。
新增 audit_paths:docs/*.md 与 AGENTS.md 里反引号内的路径型 token 必须真实存在。
它存在的原因是:文档曾出现从未创建的路径,而虚构路径读起来和真实路径完全一样,读者无法分辨
「我们决定不做」和「这写错了」。docs/retired.md 同样在检查范围内——废弃登记里的路径也必须真实,
未落地的形态只以文字描述。豁免只有三类,都在脚本里可见:指向外部仓库的所有者前缀
(stalwartlabs/...)、git 历史可证已删除的文件、以及一份注明理由的知名文件名白名单。
它不检查反引号外的散文,裸文件名也只按同名校验解析。
0 error 只证明「可达」:行号存在、引用可解析、表格列数一致、标记就位。
它不证明语义正确。 报告校验结果时必须说明这个区分。
6.4 Front-end testing
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
§3 的每条安全不变量都要有一条测试或代码级断言能证明它成立, 而不是只写在文档里。纯重构不得改变测试总数。
6.6 External dependency testing
Stalwart 与 Telegram 的业务凭据已在真机联调通过——出站、查询与入站回调两个方向
均已验证(setWebhook 与 PushSubscription 的注册、回调、验证往返全部走通),托管 Redis
已连接。真实环境集成测试必须用 #[ignore] 标注,缺凭据时静默跳过(不泄露凭据、不判失败)。
环境阻塞项:无。 最后两项也已验证:TTL 数值在真实 Upstash 实例上实测(SET … NX EX / SET … EX 3600 / Lua EXPIRE 86400 三处原子写入点,连跑 6 轮全绿),Email/changes 的 newState 续传在真实 Stalwart 环境触发过(见 docs/design.md §10.5)。opengaps.md「阻塞」区当前为空。
7. Document boundaries
每个文档只回答一个问题;不得跨文档复制表格、清单或数值。
| 文档 | 回答什么 | 权威范围 |
|---|---|---|
../AGENTS.md |
通用、语言无关的代码编写与环境构建规范 | 质量规则 |
README.md / README.zh-CN.md |
面向使用者:是什么、怎么跑、怎么配置 | 用户可见事实(中英必须信息对等) |
docs/design.md |
为什么这样设计:数据流、模块边界、状态机、错误处理 | 架构意图 |
docs/deployment.md |
怎么部署:Dockerfile、secrets、网关、多实例、cron | 部署与运维 |
docs/reference.md |
可核对事实的唯一权威来源:路由、Redis 键与 TTL、配置项、错误码、出站常量 | 可核对事实 |
docs/opengaps.md |
缺口、阻塞、下一阶段目标 | 未决项 |
docs/retired.md |
已废弃或已改名方案的记录与替代指向 | 历史决策 |
docs/charter.md |
本文件:项目约束、技术选型、实现阶段、安全不变量、稳定 ID 注册表 | 项目约束 |
web/(无文档,4 个文件) |
管理 SPA 源:静态配置页与前端逻辑,由 web/config.test.mjs 覆盖 |
前端行为(权威事实记在 docs/design.md 与 docs/reference.md) |
cloudflare-worker/README.md |
网关自身的配置与语义 | 网关 |
跨文档引用规则:
- 跨文档禁止用
§x.y章节号(章节号随编辑漂移);用稳定 ID。 - 同文档内允许
§x.y。 - 新增稳定 ID 必须先登记到 §8 注册表;未登记 ID 不得出现在注释、文档或标识符中。
- 改公共 API / Redis 键与 TTL / 配置项 / 错误码 / 用户可见文案时,同轮同步全部相关文档。
- 新增文档必须加入
scripts/docs_check/各校验器的文档清单,否则它永远不会被校验。
8. Stable ID registry
唯一权威索引表。 跨文档引用一律用稳定 ID(不用 §x.y 章节号)。
- ID 语义稳定、全仓唯一、可 grep;本表登记每个 ID 的定义文件与一句话说明。
- 新增约束 / 组件时,先在本表登记一行,再在定义点写
**ID**标签。 - 新增 ID 必须先登记,再出现在注释、文档或标识符中;校验器会拒绝未登记 ID。
- 内容搬家时只更新本表「定义文件」列,所有引用本身零改动。
共 85 个 ID,按前缀分组:C-*(部署约束)· NG-*(非目标)· MOD-*(模块)·
FLOW-*(数据流)· REQ-*(需求)· SAF-*(安全)· NFR-*(非功能)· GATE-*(门禁)·
BOUND-*(边界)· ARCH-*(架构)。
| 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 / docs/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 常驻,非目标(该变量已随 Config::from_env() 删除,代码中已无此标识符) |
非目标 |
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 / docs/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 / §3 | 仅用户明确允许后才向外部 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-PUSH-TYPES |
src/domain/jmap/client.rs:110 注释 | PushSubscription/set create 在 jmap-client 0.4.2 中没有 types 参数;订阅 id 对外暴露前须经 push_subscription_update_types 收窄为 Email + EmailDelivery |
需求 |
REQ-RECONCILE-IDEMPOTENCY |
src/state.rs claim_dedup + get_reconcile_state / docs/design.md §8.2 |
JMAP 对账游标只有在全部分页事件成功入队(XADD)后才推进;单次对账由 Redis SET NX EX 锁 lock:reconcile 保证单飞(TTL 300s,owner token 续期 90s,仅持有者可续期/释放);处理端再经 claim_dedup(SET NX EX,86400s)保证同一流消息不重复投递 |
需求 |
REQ-TIMEZONE-DISPLAY |
src/config.rs SUPPORTED_TIMEZONES / docs/reference.md §5.4 |
通知的收件时间按业务配置 timezone(IANA,默认 Asia/Shanghai)渲染为 %Y-%m-%d %H:%M;仅接受 16 个无夏令时区域,未匹配返回 422,不做时区库推断 |
需求 |
NFR-NOTIFY-SLA |
docs/deployment.md §6.5 | 通知可用性 ≥99.9%,允许少量延迟 | 非功能 |
NFR-RECONCILE-INTERVAL |
docs/deployment.md §6.3 | 外部 Cron 对账间隔 5–10 分钟 | 非功能 |
SAF-NOTIFY-META |
§3 | 新邮件通知只含元数据,正文不入通知 | 安全 |
SAF-CHAT-ALLOWLIST |
§3 / docs/design.md §7.3 | CHAT_ALLOWLIST 硬约束,处理前先拒绝非白名单 | 安全 |
SAF-AUTH-RECONCILE |
§3 / docs/design.md §7.3 | /reconcile 需 Authorization: Bearer RECONCILE_TOKEN,fail-closed |
安全 |
SAF-AUTH-TG-WEBHOOK |
§3 / docs/design.md §7.3 | /webhook/tg 需头 X-Telegram-Bot-Api-Secret-Token == TG_WEBHOOK_SECRET |
安全 |
SAF-AUTH-JMAP-PUSH |
§3 / docs/design.md §7.3 | /push/jmap 按 pushSubscriptionId 查 Redis 短期验证状态(存 session_digest 摘要),缺失或摘要不符拒绝;状态缺失回落 worker 重新验证(fail-closed) |
安全 |
SAF-ADMIN-SESSION |
src/config.rs session_digest / §3 |
返回用于 Redis admin-session 记录的 SHA-256 摘要;bearer token 本体绝不写入 Redis | 安全 |
SAF-NO-SECRET-ECHO |
src/config.rs SecretString 字段与公共配置 API |
密钥字段不实现 Debug 且永不进入公共配置 API 响应;Redis 线格式类型私有,不得序列化进 HTTP 响应 |
安全 |
SAF-PROBE-PUBLIC |
§3 / docs/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 / §4 | 阶段0 实际依赖集(axum/serde/secrecy/subtle/tokio/tracing…) | 架构 |
ARCH-DEPS-STAGE1 |
docs/design.md §10.0-1 / §4 | 阶段1 依赖现况:jmap-client 0.4.2 已引入,版本/features 以 Cargo.toml 为准,禁用 WebSocket feature |
架构 |
ARCH-DEPS-STAGE4 |
docs/design.md §10.0-1 / §4 | 阶段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 / §4 | 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),已在真实账号真机验证(2026-09-28:fetchChanges 持久化 baseline: 游标、Email/query 搜索回帖,均经用户确认) |
需求 |
SAF-JMAP-URL |
docs/design.md §7.1 / §4 | 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 / §6 | 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 / §3 | 信任模型=透传: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_ENABLED(或 --debug)+ DEBUG_TOKEN 双因子开启后挂载 /debug/*,否则不挂载 |
组件 |
SAF-DEBUG-GATE |
src/main.rs / src/debug.rs / docs/design.md §7.6 / docs/deployment.md §2.1 | 双因子门禁:「DEBUG_ENABLED 为真值或命令行带 --debug」且 DEBUG_TOKEN 非空才挂载路由;缺任一完全不挂载(请求落通用 404),默认绝对关闭。开启信号走 env 而非 argv,使启动命令保持静态、开关可在平台控制台单点切换 |
安全 |
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/* 不在网关 18 条安全路由内,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 / §3 | 生产不使用任何数据库(无 SQLite/Postgres/MySQL/嵌入式),Redis 为唯一状态存储;应用不连接第二个数据库 | 约束 |
C-NO-LOCAL-WRITE |
docs/deployment.md §0 / §9.1 / §3 | 禁止本地文件/目录写入(日志/数据/临时缓存/本地卷) | 约束 |
C-LOG-STDOUT-ONLY |
docs/deployment.md §0 / §9.1 / §3 | 日志只写 stdout/stderr,由平台采集;禁用文件日志后端 | 约束 |
SAF-LOG-PURITY |
docs/deployment.md §0 / §9.1 / §3 | 日志与 Redis 写入内容仅限结构化事件/计数/时间戳/脱敏摘要;禁止密钥/邮件正文/AI 请求响应/附件内容 | 安全 |
SAF-ENABLE-FLAG |
src/notify.rs put_enabled / src/state.rs config:enabled |
全局开关是 Redis 单键 config:enabled;未写入即视为关闭,business_enabled 出错也按关闭处理(fail-closed);写入须 admin-session Bearer |
安全 |
C-NO-STATEFUL-RECOVERY |
docs/deployment.md §0 / §9.1 / §3 | 禁止依赖进程内状态做生产恢复;恢复一律走 Redis + JMAP 对账;进程内缓存仅为性能优化,丢失须安全可重入 | 约束 |
ARCH-STATE-REDIS |
docs/design.md §10.0 / Redis 为唯一状态来源 | 状态层统一走 Redis(Streams/SET NX/锁/摘要),进程不持有可恢复状态 | 架构 |
C-REDIS-EXTERNAL |
docs/deployment.md §8.1 | Redis 由外部已认证实例提供,不与本服务同容器 | 部署约束 |
GATE-UPTIME-KUMA |
docs/deployment.md §10.1 | /healthz 稳定语义可直接接 Uptime Kuma 等外部探针 |
门禁 |
GATE-DOCS |
scripts/docs_check/run_all.sh / docs/deployment.md | 文档门禁:校验器全部 0 error、exit 0 才算通过 | 门禁 |