diff --git a/.env.example b/.env.example index 5d9b749..c40fbac 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,14 @@ # MessageWeave production bootstrap (C-REDIS-ONLY-STATE) # -# The process requires only this environment variable. Redis ACL credentials are -# both the application connection credential and the one-shot bootstrap trust root. -# Never commit or log a real URL/password. +# The process requires only these two variables. CONFIG_ENCRYPTION_KEY is the SPA +# admin credential and the one-shot bootstrap trust root; the Redis ACL password +# in REDIS_URL only gates the database connection (src/main.rs:88-91). +# Never commit or log a real URL/password/key. REDIS_URL=redis://:REPLACE_WITH_REDIS_ACL_PASSWORD@redis.example.com:6379/0 # Required 32-byte random hex key (64 hex characters). Never store in Redis or logs. CONFIG_ENCRYPTION_KEY=REPLACE_WITH_64_HEX_CHAR_RANDOM_KEY -# PORT/RUN_MODE are optional platform overrides (defaults: 8080/webhook). Business +# PORT is an optional platform override (default: 8080). RUN_MODE is accepted but +# currently has no effect — both modes share one router (default: webhook). Business # secrets (Telegram/JMAP/LLM/auth) must be supplied once via HTTPS /api/bootstrap; # they are then persisted in Redis and are never echoed by the API. diff --git a/AGENTS.md b/AGENTS.md index e1fbedc..875a577 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -145,7 +145,7 @@ - 正文转义、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 客户端。 + - **远程 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` 复用真实出站路径(`TelegramClient::send_text`,`src/channel.rs:134`),不得另建第二套 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)。 diff --git a/README.md b/README.md index 23fa053..285fff6 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,9 @@ Three layers. They are not interchangeable. | `PORT` | no | `8080` | | `RUN_MODE` | no | `webhook` | -`RUN_MODE` accepts `webhook` or `reconcile`. `.env.example` ships only the two required +`RUN_MODE` accepts `webhook` or `reconcile`, but **both currently produce identical +behavior** (the two modes share one router), so it is a forward placeholder: keep the +default `webhook`. `.env.example` ships only the two required variables; the rest are documented defaults. **Business configuration — Redis, written by the browser.** diff --git a/docs/deployment.md b/docs/deployment.md index 6a85797..2116a26 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -59,8 +59,8 @@ **双因子启用条件(`SAF-DEBUG-GATE`,两者必须同时成立)** -1. 进程命令行必须带 `--debug`(读取于 `src/main.rs:99`)。 -2. `DEBUG_TOKEN` 环境变量必须存在且非空(读取于 `src/main.rs:100-103`)。 +1. 进程命令行必须带 `--debug`(读取于 `src/main.rs:96`)。 +2. `DEBUG_TOKEN` 环境变量必须存在且非空(读取于 `src/main.rs:97-100`)。 缺一即不挂载:`debug_router()` 本身会构造出全部 7 条路由(`src/debug.rs:72-80`),但主入口**只在双因子成立时才合并它**(`src/notify.rs:1320-1321`)。所以未开启时 `/debug/*` 路由根本不存在,请求走 axum 兜底返回普通 `404 not found`——**不是** 401,也不会泄露「此路径存在」。开启成功时打一条 WARN 日志标记该面已打开(`src/main.rs:107-109`,`SAF-LOG-PURITY`:只记开启状态,从不记录 token 值)。 @@ -148,7 +148,7 @@ HTTP 端口并以 `/healthz` 做 interval 30 秒、timeout 5 秒的健康检查 | `CHAT_ALLOWLIST` | ✅ | 聊天白名单(**硬约束 `SAF-CHAT-ALLOWLIST`**):逗号分隔整数 chat id;处理任何事件前先校验,非白名单直接拒绝 | | `REDIS_URL` | ✅ | **外部 Redis(用户托管 + AOF)**(`C-REDIS-ONLY-STATE`/`C-REDIS-MANAGED-AOF`):session / dedup / Streams / fuse / sinceState 全部在此;支持 `redis://` 与带默认 ACL 用户的 `rediss://default:@host:6379/0`,Redis 进程不在此 compose 内 | | `PORT` | 默认 8080 | 单监听端口(`C-NO-TCP-EXPOSE`) | -| `RUN_MODE` | 默认 `webhook` | `webhook` / `reconcile` 二选一(`NG-SERVER-MODE` 已删除) | +| `RUN_MODE` | 默认 `webhook` | `webhook` / `reconcile`(`NG-SERVER-MODE` 已删除);**两种取值当前行为完全相同**(共享同一套路由表),仅作前向占位,保持默认即可 | | `RECONCILE_TOKEN` | ✅ | `/reconcile` 的 `Authorization: Bearer ` 承载令牌(`SAF-AUTH-RECONCILE`)。因 `/reconcile` 路由**始终挂载**,此变量为**必填**(`SecretString`) | | `WORKER_TOKEN` | ✅ | `/worker` 的有界处理令牌;管理 API 兼容接受该 Bearer 值,SPA 使用短期 Redis admin session | | `TG_WEBHOOK_SECRET` | ✅ | `/webhook/tg` 校验请求头 `X-Telegram-Bot-Api-Secret-Token`(`SAF-AUTH-TG-WEBHOOK`)。须与 Telegram `setWebhook` 的 `secret_token` **完全一致**(`SecretString`) | diff --git a/docs/design.md b/docs/design.md index 4e6ad47..2562eeb 100644 --- a/docs/design.md +++ b/docs/design.md @@ -347,7 +347,7 @@ message-weave/ | `notify` | axum;redis | HTTP 鉴权、全局开关、`/push/jmap` 入队、`Email/changes` 对账和游标提交 | webhook 校验失败路径覆盖 403 | 已覆盖真实对账路径;仍需真实 Stalwart 环境做端到端验收 | | `web` | axum | `/config` 静态页 + `include_str!` 嵌入 + CSP | 前端由 `web/config.test.mjs` 覆盖 | 已实现 | -> 注:「现状」列是**模块级**口径(模块已落地),不代表行为完备。行为级缺口不在本表内:见 `docs/roadmap.md`「代码缺口」(2 条:多实例重复投递残余窗口、`SAF-DEBUG-ALLOWLIST`)。`worker` 模块的 `/search` 路径已随 `bfe0fd8` 落地。 +> 注:「现状」列是**模块级**口径(模块已落地),不代表行为完备。行为级缺口不在本表内:`docs/roadmap.md`「代码缺口」中 `/search`、Telegram 429 退避、多实例重复投递窗口均已实现,仅剩 1 条按产品决策保留不改(`SAF-DEBUG-ALLOWLIST`)。`worker` 模块的 `/search` 路径已随 `bfe0fd8` 落地。 --- @@ -371,7 +371,7 @@ message-weave/ | 变量 | 必填 | 默认 | 说明 | |---|---|---|---| | `PORT` | 否 | `8080` | 单监听端口(`C-NO-TCP-EXPOSE`) | -| `RUN_MODE` | 否 | `webhook` | `webhook` / `reconcile`(`NG-SERVER-MODE` 已删除) | +| `RUN_MODE` | 否 | `webhook` | `webhook` / `reconcile`(`NG-SERVER-MODE` 已删除);**两种取值当前不改变任何运行时行为**(两模式共享同一套路由表),仅作前向占位 | | `BOT_TOKEN` | 是 | — | Telegram Bot Token(`SecretString`) | | `TG_WEBHOOK_SECRET` | 是 | — | `/webhook/tg` 鉴权:请求头 `X-Telegram-Bot-Api-Secret-Token`(`SAF-AUTH-TG-WEBHOOK`;`SecretString`) | | `CHAT_ALLOWLIST` | 是 | — | 逗号分隔整数 chat id;**白名单硬约束**(`SAF-CHAT-ALLOWLIST`) | @@ -491,7 +491,7 @@ pub enum BotError { ### 8.3 可观测性 - `tracing`(直接依赖)+ `tracing-subscriber`(fmt + EnvFilter)。 -- `tracing`(直接依赖)+ `tracing-subscriber`(fmt + EnvFilter)。**当前只记录启动期事件**:`src/main.rs` 共 6 处 `info!`/`warn!`(RUN_MODE、加密密钥缺失、Redis URL 缺失、JMAP 连接不可用降级、启动横幅等)。请求级事件(Push 回调、Reconcile 拉取、渠道推送、LLM 耗时)**尚未实现**——`src/` 里除 `main.rs` 外没有任何 tracing 调用,也没有命名 span(无 `#[instrument]` / `span!`)。 +- `tracing`(直接依赖)+ `tracing-subscriber`(fmt + EnvFilter)。**当前只记录启动期事件**:`src/main.rs` 共 7 处 `info!`/`warn!`(`RUN_MODE` 取值横幅×2、启动变量缺失降级到 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`)。 - 优雅退出:**当前未实现**——`src/` 里没有信号处理(无 `tokio::signal` / `ctrl_c`),也没有 Streams pending 条目的 flush 逻辑;容器停机即终止进程。Redis 侧 `C-NO-STATEFUL-RECOVERY` 保证重启后从 Redis 重建,不依赖进程内状态。 - **可靠性目标与策略**(Streams ACK/retry、幂等去重、Push 重试、对账恢复、指标/告警、**≥99.9% 通知可用性及边界**)见 deployment.md §6.4/§6.5(`NFR-NOTIFY-SLA`)。 @@ -551,13 +551,13 @@ pub enum BotError { - axum 采用 **0.8**(`ARCH-AXUM-08`);如后续审核决定调整版本,以 Cargo.toml 为准并同步本节。 - 当前不实现 SSE/WebSocket/长轮询/SQLite/本地卷(`C-NO-LONG-CONN`/`NG-SQLITE-PERSIST`/`NG-LOCAL-VOLUME`);入口鉴权(`R1`/`SAF-AUTH-*`)作为 fail-closed 硬门禁落地,鉴权之后的 Push、Streams worker 和 `/reconcile` 业务路径已实现。 -**阶段0 P0 门禁(`GATE-P0`)——已通过(冻结基线 45 passed / 0 failed / 1 ignored,见 `docs/roadmap.md` 头部)。以下为当时的判据,保留作历史记录:** +**阶段0 P0 门禁(`GATE-P0`)——已通过。首次冻结时为 45 passed / 0 failed / 1 ignored,此后随 ④–⑥ 轮实现持续增长,当前基线以 `docs/roadmap.md` 头部为准。以下为阶段0 当时的判据,保留作历史记录:** 1. `cargo fmt --check` 通过(无格式差异)。 2. `cargo clippy --all-targets -- -D warnings` 通过(零告警;禁 crate 级 `allow`)。 3. `cargo test` 通过(含路由/配置最小测试)。 4. 上述三条**在 Debian `rust:1-slim-bookworm` 容器内**执行通过(`C-DEBIAN-SLIM`)。 5. 配置读取为 env-only(无 figment/TOML);`SecretString` 包裹秘密且 `Debug` 不泄密;`JMAP_PASSWORD` 安全访问器保留。 -6. `RUN_MODE` 被实际消费(当前为 `webhook` 入口的单一路由表,无副作用;`reconcile` 作为独立 HTTP 端点 `POST /reconcile` 提供,不通过 `RUN_MODE` 分派,见 `src/main.rs::validate_env_or_exit`)。 +6. `RUN_MODE` **不**参与路由分派:`webhook` 与 `reconcile` 共享同一套路由表,`reconcile` 作为独立 HTTP 端点 `POST /reconcile` 提供。`RUN_MODE` 仅在启动时读取并校验取值(`src/main.rs:75`,只接受 `webhook`/`reconcile`),**当前不影响任何运行时行为**,留待后续阶段在此挂载差异化副作用(见 §12.2 的运行模式说明)。 7. `.gitignore` 存在(排除 `target/` 等;**不**擅自初始化 git)。 8. 所有新增代码有引用稳定 ID 的必要注释;单 `.rs` ≤ 500 行。 @@ -689,7 +689,7 @@ Push 事件经 Streams 消费并投递到 Telegram,其关键路径交付语义 - **多发件身份**(Q12):`Identity` 未使用,单账户(`REQ-SINGLE-ACCOUNT`)。 - **unsafe**(Q13):`src/` 中没有 `unsafe`,但也没有加 `#![forbid(unsafe_code)]`。 - **监控**(Q15):实现为 `/healthz` + `/ready` 两个 HTTP 探针,不引入 Prometheus/Exporter;运行平台配置见 deployment.md。 -- **LLM 提供方 / 网出许可**(Q25、Q26、Q30):`LLM_BASE_URL` 由部署方指定(只校验 https),`LLM_ALLOW_NET` 默认 `true`;`LLM_ENABLED=false` 时直接走 `LlmClient::noop()`,不探测(见 §12.2)。 +- **LLM 提供方 / 网出许可**(Q25、Q26、Q30):`LLM_BASE_URL` 由部署方指定(只校验 https);`LLM_ENABLED` 与 `LLM_ALLOW_NET` **默认均为 `false`**,二者须同时为真才构造客户端,否则 `llm` 字段为 `None`(`Option>`,仓库内不存在 `noop()` 实现),不探测(见 §12.2)。 - **熔断冷却 / 阈值**(Q27):不适用——熔断本身未实现(见 §12.4)。 部署/平台类决策已无未决项:`Q-DEP-A`(平台 URL / 域名与证书由谁配置,由部署环境在发布时确定)与 `Q-DEP-B`(外部 Cron 用哪个调度器,不限定实现)均已决策,归档到 `docs/deployment.md` 的已确认决策一节,不在本文重复。 @@ -714,20 +714,20 @@ LLM 相关代码只有**一个文件**: ``` src/ai.rs # LlmClient(唯一实现):summarize() 打 OpenAI 兼容 /chat/completions ``` -配置装载在 `src/config.rs` 的 `LlmConfig`,运行时参数在 `src/state.rs` 的 `RuntimeConfig`。 +配置装载在 `src/config.rs` 的 `LlmConfig`,运行时参数在 `src/state.rs` 的 `OutboundConfig`(经 `RuntimeConfigProvider` 下发)。 **不存在** `src/domain/llm/` 目录,也不存在其中的 `mod.rs` / `client.rs` / `config.rs` / `policy.rs` / `fallback.rs` / `audit.rs`——熔断、规则回退、审计 span 全部未实现(见 §12.4–12.7)。 调用方只有 `worker.rs` 的 `MetadataWorker`(已授权摘要)和 `notify.rs`(新邮件通知)。 ### 12.2 环境变量(当前实现) | 变量 | 类型 | 默认 | 用途 | |---|---|---|---| -| `LLM_ENABLED` | bool | `true` | 总开关 | -| `LLM_ALLOW_NET` | bool | `true` | 运行时出站许可;`LLM_ENABLED && LLM_ALLOW_NET` 同时为真才构造 `LlmClient`,否则用 `LlmClient::noop()`(`src/main.rs:128`) | -| `LLM_API_KEY` | string | 必填 | Bearer token;缺失即视为未启用 | -| `LLM_BASE_URL` | URL | 必填 | 必须 `https`,否则 `AiError::InvalidEndpoint` | -| `LLM_MODEL` | string | 必填 | 透传给 `/chat/completions` 的 `model` | +| `LLM_ENABLED` | bool | `false` | 总开关;默认关闭,未启用时不校验 `LLM_API_KEY`/`LLM_BASE_URL`/`LLM_MODEL` | +| `LLM_ALLOW_NET` | bool | `false` | 运行时出站许可;`LLM_ENABLED && LLM_ALLOW_NET` 同时为真才构造 `LlmClient`,否则 `llm` 字段为 `None`(`src/main.rs:125`) | +| `LLM_API_KEY` | string | 可省略 | Bearer token;仅 `LLM_ENABLED=true` 时必填 | +| `LLM_BASE_URL` | URL | 可省略 | 仅启用时必填,且必须 `https`,否则 `AiError::InvalidEndpoint` | +| `LLM_MODEL` | string | 可省略 | 仅启用时必填;透传给 `/chat/completions` 的 `model` | | `LLM_SUMMARY_TARGET_CHARS` | int | `1024` | `LlmClient::max_chars`,对返回摘要文本做字符截断(**代码侧确有截断**) | -| `max_retries`(运行参数) | int | `3` | 由 `RuntimeConfig` 下发,硬上限 5;LLM 与 Telegram 出站共用 | +| `max_retries`(运行参数) | int | `3` | 由 `OutboundConfig` 下发(`RuntimeConfigProvider`),硬上限 5;LLM 与 Telegram 出站共用 | | `llm_timeout_ms`(运行参数) | int | `30000` | 硬下限 100ms | **不存在** `LLM_TEMPERATURE` / `LLM_MAX_TOKENS` / `LLM_TIMEOUT_SECS` / `LLM_MAX_RETRIES`:请求体只有 `model` / `messages` / `max_tokens`(= `max_chars * 2`),没有 temperature,也没有独立于 `llm_timeout_ms` 的超时开关。API key 只从配置读,不入源码、不打日志(`SAF-LOG-PURITY`),运行期 secret 注入方式见 deployment.md(`C-NO-SECRET-IN-IMAGE`)。 diff --git a/docs/reference.md b/docs/reference.md index f443f40..45af7a2 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -211,7 +211,7 @@ All registered in `router_with_worker_state_runtime_bootstrap_config` (notify.rs | GET | `/`, `/assets/config.js`, `/assets/styles.css` | `index` / `web_config` | The remote-debug surface (`src/debug.rs:72-80`) is merged **only when the process is started -with `--debug` and `DEBUG_TOKEN` is non-empty** (`SAF-DEBUG-GATE`, `src/main.rs:99-103`, +with `--debug` and `DEBUG_TOKEN` is non-empty** (`SAF-DEBUG-GATE`, `src/main.rs:96-100`, `src/notify.rs:1320-1321`); otherwise none of these routes exist and requests fall through to axum's generic `404`, not a 401. All seven require `Authorization: Bearer `, checked by `debug_authorized` (`src/debug.rs:45`), which delegates to the production @@ -315,7 +315,9 @@ Three distinct layers. They are not interchangeable. `RUN_MODE` accepts only `webhook` or `reconcile`; anything else is rejected at boot (main.rs:78-81). Both modes currently route to the same no-op router; the distinction is -load-bearing for future splits, not for behaviour today. +load-bearing for future splits, not for behaviour today. Note the value is only read on +the legacy env path (`config.rs:338`), so with the current two-variable production config +it always resolves to the hardcoded default — setting it to `reconcile` has no effect. **Missing a required variable does not crash the process.** It logs a warning and serves `router_configuration_setup`, which builds a router over `MemoryState` with empty tokens and @@ -341,7 +343,7 @@ The single prefixed name is `TELEGRAM_CHAT_ID`. Anything else documented as `TELEGRAM_BOT_TOKEN` or similar is a documentation error, not a supported variable. **Trust root.** The SPA admin credential is `CONFIG_ENCRYPTION_KEY` itself: read at startup -(main.rs:49) and held as `admin_token` (main.rs:95), it is checked in constant time by +(main.rs:49) and held as `admin_token` (main.rs:92), it is checked in constant time by `worker_authorized` (notify.rs:445; compare at notify.rs:1135) at the top of both `POST /api/bootstrap` (notify.rs:604) and `POST /api/admin/session` (notify.rs:774). It is only compared against the request bearer — never echoed, logged, or stored. The session diff --git a/docs/retired.md b/docs/retired.md index 09e5b62..c447a5a 100644 --- a/docs/retired.md +++ b/docs/retired.md @@ -82,7 +82,7 @@ --- | `src/domain/llm/` 整个模块族 | 虚构模块树 | `domain/` 下只有 `jmap.rs` 与 `jmap/client.rs`;LLM 只有 `src/ai.rs` 一个文件(`LlmClient`) | 无 | -| `ai::config` / `ai::fallback` / `ai::policy` / `ai::audit` | 虚构模块 | LLM 配置在 `config.rs::LlmConfig`,运行时参数在 `state.rs::RuntimeConfig`,无 policy/audit 概念 | 无 | +| `ai::config` / `ai::fallback` / `ai::policy` / `ai::audit` | 虚构模块 | LLM 配置在 `config.rs::LlmConfig`,运行时参数在 `state.rs::OutboundConfig`(经 `RuntimeConfigProvider` 下发),无 policy/audit 概念 | 无 | | `channel/mod.rs` | 虚构文件 | 渠道层就是 `src/channel.rs`,不是 `channel/` 目录 | 无 | | `notify::push_handler` / `notify::worker` / `notify::reconcile` 作为模块路径 | 虚构模块路径 | 这些是 `src/notify.rs` 内的自由函数(`jmap_push` / `worker` / `reconcile`),不是模块路径 | 无 | | `mod_dedup` / `mod_streams` / `mod_sincestate` | 虚构模块名 | `state.rs` / `notify.rs` 都是平铺文件,无子模块;去重与 Streams 逻辑以自由函数存在 | 无 | @@ -90,14 +90,14 @@ | `CancellationToken` | 虚构类型 | 未使用;无优雅关闭、无信号处理(`src/` 零命中 `tokio::signal` / `ctrl_c`) | 无 | | `Preview` 类型 / 4000 字符长邮件保护 / `[继续查看原文]` 按钮 / `/llm-fallback` 按钮 | 虚构类型与 UI | 全部未实现;授权后把全文交给 LLM,失败即回退前 300 字符,无任何截断标注或按钮 | 无 | | `LlmErr`(5 变体) | 虚构枚举 | 真实是 `AiError`,仅 3 个变体(`InvalidEndpoint` / `Request` / `Response`) | 无 | -| `LLM_TEMPERATURE` / `LLM_MAX_TOKENS` / `LLM_TIMEOUT_SECS` / `LLM_MAX_RETRIES` 环境变量 | 虚构环境变量 | `src/` 零命中;真实只有 `LLM_API_KEY` / `LLM_BASE_URL` / `LLM_MODEL` / `LLM_SUMMARY_TARGET_CHARS` 四个环境变量,加 `llm_timeout_ms` / `max_retries` 两个运行时参数 | 见 `docs/reference.md` AI 授权态一节 | -| `llm.call` tracing span | 虚构观测点 | `src/` 中除 `main.rs`(6 个事件)外**零 tracing 事件、零 span**;LLM 调用无任何日志 | 无 | +| `LLM_TEMPERATURE` / `LLM_MAX_TOKENS` / `LLM_TIMEOUT_SECS` / `LLM_MAX_RETRIES` 环境变量 | 虚构环境变量 | `src/` 零命中;真实有 6 个:`LLM_ENABLED` / `LLM_ALLOW_NET` / `LLM_API_KEY` / `LLM_BASE_URL` / `LLM_MODEL` / `LLM_SUMMARY_TARGET_CHARS`,加 `llm_timeout_ms` / `max_retries` 两个运行时参数 | 见 `docs/reference.md` AI 授权态一节 | +| `llm.call` tracing span | 虚构观测点 | `src/` 中除 `main.rs`(7 个事件)外**零 tracing 事件、零 span**;LLM 调用无任何日志 | 无 | | 熔断器 / 半开态 / 熔断后 60s 冷却 / 规则兜底(Redis 共享计数) | 未实施设计 | 只有超时 + 重试;LLM 失败静默降级为前 300 字回退,无用户侧提示、无状态记录 | 无 | | 定时摘要 / 每日邮件摘要推送 | 未实施 | 未实现 | 无 | | 附件下载(`send_document` / `Blob/get` / 下载按钮) | 未实施 | JMAP 侧只读;邮件附件仅以 `has_attachment: bool` 形式出现 | 无 | -| `/flag` / `/unseen` / 发信命令 | 未实施 | 当前只识别 5 个意图(帮助 / 同意 / 摘要 / 普通消息 / 未识别) | 无 | +| `/flag` / `/unseen` / 发信命令 | 未实施 | 当前识别 6 个意图(帮助 / 同意 / 摘要 / 搜索 / 普通消息 / 未识别);其中 `/search` 已实现(`worker.rs:517`) | 无 | | `Identity` 概念 | 未实施 | 账号识别只依赖 `ACCOUNT_ID`,无身份层抽象 | 无 | -| `run_mode.rs`(作为单独文件) | 虚构文件 | RUN_MODE 校验内联在 `main.rs::validate_env_or_exit` | 无 | +| `run_mode.rs`(作为单独文件) | 虚构文件 | RUN_MODE 由 `config.rs:338` 读取、在 `src/main.rs:75` 校验取值;不存在 `validate_env_or_exit` 这类函数 | 无 | | docker-compose `message-weave health --addr` 示例 | 虚构命令 | 应用无 CLI 子命令;健康检查端点是 `GET /healthz` 与 `GET /ready` | 见 `docs/deployment.md` 的 Health-check 表 | | 早期设计辩论问题(消息格式 / 长邮件阈值 / 附件策略 / Identity / 监控 / LLM 供应商 / 熔断等 17 条) | 已由代码回答 | 均已被实现的代码给出答案,不再属于待确认项 | 见 `docs/design.md` 的「已由代码回答的早期问题」一节 | diff --git a/src/config.rs b/src/config.rs index 7ec14fb..7c356c6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -32,6 +32,12 @@ pub fn encryption_key_from_env() -> Result<[u8; 32], BotError> { /// Runtime-only settings. SecretString prevents accidental formatting/logging of credentials. /// C-AUTH-APP-BASIC and REQ-SINGLE-ACCOUNT are intentionally represented explicitly. pub struct Config { + // The listener binds from main.rs before Config exists, so the field is parsed + // here only for validation parity; main.rs reads PORT directly. + #[expect( + dead_code, + reason = "parsed in from_env() for validation parity; listener binds from main.rs" + )] pub port: u16, pub run_mode: String, pub telegram: TelegramConfig, diff --git a/src/main.rs b/src/main.rs index beede81..7e3a188 100644 --- a/src/main.rs +++ b/src/main.rs @@ -81,10 +81,7 @@ async fn main() -> Result<(), error::BotError> { ))) } } - tracing::info!( - port = bootstrap_config.port, - "starting webhook HTTP entrypoint" - ); + tracing::info!(port, "starting webhook HTTP entrypoint"); let redis = RedisState::connect_with_encryption(redis_url.expose_secret(), Some(encryption_key)) .await?;