refactor: delete three zero-implementor 'stage 0 placeholder' channel traits

src/channel.rs had Channel / Notifier / MessageAdapter, each marked
#[expect(dead_code, reason = "稳定ID+阶段0占位…")]. All three had no
implementor, no caller and no dyn binding: Telegram outbound actually goes
through channel::telegram::TelegramClient::send_notification -> send_text,
held directly by worker.rs's MetadataWorker and notify.rs. The traits were
never on any path.

- delete the three traits + their #[async_trait] attributes
- drop the async_trait import from channel.rs (unused afterwards); the
  dependency stays in Cargo.toml, still used by 5 other files
- drop the domain import from channel.rs (its only uses were inside the
  deleted traits; the telegram module has its own Notification import)
- delete UserCommand from src/domain.rs -- its only consumer was Channel.
  Notification stays (worker.rs:234 builds it)
- rewrite channel.rs's module doc: "only an adapter boundary in stage 0
  (no bot commands yet)" was already false -- /help, /search, /summary
  and /ai on|off are all parsed in worker.rs::parse_intent

The three dead-code attributes were the last three #[expect(dead_code)] in
src/. Two #[expect(clippy::too_many_arguments)] remain in notify.rs; those
are justified lint suppressions on router wiring, not placeholders.

External contracts unchanged: HTTP routes, Redis keys, error codes and the
error envelope are untouched.

GATE-P0: fmt / check / clippy -D warnings green, 61 passed / 0 failed / 1
ignored (unchanged).
This commit is contained in:
anonsaber 2026-09-29 02:23:38 +08:00
parent 4c09310a2f
commit b2dbe7ccbb
4 changed files with 15 additions and 43 deletions

View File

@ -213,20 +213,17 @@ Telegram 渠道用 `src/channel.rs` 的 `reqwest` 自研实现(`ARCH-DEPS-STAG
### 5.2 渠道抽象与多通道扩展策略 ### 5.2 渠道抽象与多通道扩展策略
- **现状**:首个(也是当前唯一)渠道是 Telegram。目标是保留平行扩展钉钉/飞书的能力,**但不提前实现**。 - **现状**:首个(也是当前唯一)渠道是 Telegram。目标是保留平行扩展钉钉/飞书的能力,**但不提前实现**。
- **原则**:领域层与渠道层解耦——邮件/JMAP/AI/意图状态机**不得依赖 Telegram 类型**。 - **原则**:领域层与渠道层解耦——邮件/JMAP/AI/意图状态机**不得依赖 Telegram 类型**。
- **抽象(不过度设计)**: - **抽象(不过度设计)**:不预留空 trait。`src/channel.rs` 曾有三个零实现的
`#[expect(dead_code)]` 占位 trait(`Channel` / `Notifier` / `MessageAdapter`,注释自述
| 抽象 | 职责 | 当前状态 | 「稳定ID+阶段0占位」),**已删除**(登记见 `docs/retired.md`)——实际 Telegram 出站走
|---|---|---| `channel::telegram::TelegramClient::send_text`(`reqwest` 自研),由 `worker.rs` 的
| `Channel` | 渠道生命周期:接收输入、分发命令、配置端点 | **无实现**(阶段0占位);命令解析实际在 `parse_intent`(`src/worker.rs`) | `MetadataWorker` 与 `notify.rs` 直接持有,**从未经过这三个 trait**。命令解析在
| `Notifier` | 主动推送:把领域 `Notification` 发送到用户 | **无实现**(阶段0占位);出站实际走 `TelegramClient::send`(`src/channel.rs`) | `parse_intent`(`src/worker.rs`),渲染逻辑散在 `src/worker.rs` / `src/notify.rs`。
| `MessageAdapter` | 领域数据 ↔ 渠道消息渲染(文本/按钮/转义) | **无实现**(阶段0占位);渲染逻辑散在 `src/worker.rs` / `src/notify.rs` | 领域与渠道之间传递的是领域 `Notification`(`src/domain.rs`),渠道只在边缘做适配。
- `src/channel.rs` 里三个 trait(`Channel` / `Notifier` / `MessageAdapter`)目前**没有任何实现**,都标着 `#[expect(dead_code)]`(注释:阶段0占位,供后续 adapter 使用)。实际 Telegram 出站走 `channel::telegram::TelegramClient`(`reqwest` 自研),由 `worker.rs` 的 `MetadataWorker` 和 `notify.rs` 持有,**不经过这三个 trait**。
- 领域与渠道之间用**领域 `UserCommand` / `Notification`** 数据结构传递,渠道只在边缘做适配(解析→领域命令;领域 `Notification`→渲染)。
- **约束**: - **约束**:
- `jmap` / `llm` / 意图状态机 / `notify::core` 的公开接口只接受/返回领域类型; - `jmap` / `llm` / 意图状态机 / `notify::core` 的公开接口只接受/返回领域类型;
- 领域层不得出现具体渠道 SDK 类型(渠道层当前不引入第三方 Bot 框架,见 §4); - 领域层不得出现具体渠道 SDK 类型(渠道层当前不引入第三方 Bot 框架,见 §4);
- 新增渠道 = 新 adapter + 装配,不改领域层。 - 新增渠道 = 新 adapter + 装配,不改领域层。真需要抽象时**带实现一起加**,不留空 trait。
- **不做什么**:不定义多态配置注册表、不预先抽象"渠道能力矩阵"、不建 plugins 机制;按需再演进(YAGNI)。 - **不做什么**:不定义多态配置注册表、不预先抽象"渠道能力矩阵"、不建 plugins 机制;按需再演进(YAGNI)。
### 5.3 运行模型(短请求,无长连接) ### 5.3 运行模型(短请求,无长连接)
@ -255,7 +252,7 @@ Stalwart JMAP Push → POST /push/jmap (StateChange{Email/EmailDelivery: new_sta
→ worker: XREADGROUP → 用 sinceState(存 Redis, MOD-SINCESTATE) 调 Email/changes → 取 created[] 的 id → worker: XREADGROUP → 用 sinceState(存 Redis, MOD-SINCESTATE) 调 Email/changes → 取 created[] 的 id
→ email_get([From, Subject, Preview, ReceivedAt, HasAttachment]) → email_get([From, Subject, Preview, ReceivedAt, HasAttachment])
→ notify::core 组装领域 Notification(仅元数据 + 行内按钮意图,绝不含正文) → notify::core 组装领域 Notification(仅元数据 + 行内按钮意图,绝不含正文)
→ channel::Notifier.send(chat, notification)(Telegram adapter 渲染并发送) → channel::telegram::TelegramClient::send_notification(chat, notification)(内部渲染文本后经 send_text 发出)
→ 更新 sinceState(写外部 Redis)→ XACK → 更新 sinceState(写外部 Redis)→ XACK
→ 若 Redis 丢失:由外部 Cron 对账(FLOW-RECONCILE) 重建游标并补发 → 若 Redis 丢失:由外部 Cron 对账(FLOW-RECONCILE) 重建游标并补发
``` ```
@ -317,7 +314,7 @@ message-weave/
│ ├── domain/jmap.rs # JmapBackend trait + MockBackend(契约测试不联网) │ ├── domain/jmap.rs # JmapBackend trait + MockBackend(契约测试不联网)
│ ├── domain/jmap/client.rs # 包装 jmap_client::Client(真实只读 adapter,MOD-JMAP-CLIENT) │ ├── domain/jmap/client.rs # 包装 jmap_client::Client(真实只读 adapter,MOD-JMAP-CLIENT)
│ ├── state.rs # Redis 读写封装:配置/开关/会话/TTL 键(C-REDIS-ONLY-STATE) │ ├── state.rs # Redis 读写封装:配置/开关/会话/TTL 键(C-REDIS-ONLY-STATE)
│ ├── channel.rs # 渠道层:Channel/Notifier/MessageAdapter,Telegram 用 reqwest 自研 │ ├── channel.rs # 渠道层:TelegramClient(reqwest 自研出站);领域类型在 src/domain.rs
│ ├── notify.rs # /push/jmap 校验入队 + Redis Streams worker + /reconcile 对账 │ ├── notify.rs # /push/jmap 校验入队 + Redis Streams worker + /reconcile 对账
│ ├── worker.rs # parse_intent → Intent,命令路由与 AI 授权判定 │ ├── worker.rs # parse_intent → Intent,命令路由与 AI 授权判定
│ ├── ai.rs # LlmClient / summarize:LLM 摘要调用 │ ├── ai.rs # LlmClient / summarize:LLM 摘要调用
@ -342,7 +339,7 @@ message-weave/
| `error` | — | `BotError`(§8.1,4 个变体) | 单测 | 已实现 | | `error` | — | `BotError`(§8.1,4 个变体) | 单测 | 已实现 |
| `domain` + `domain::jmap::client` | **jmap-client 0.4.2** | JMAP 只读语义;`client` = 真实只读 adapter(`MOD-JMAP-CLIENT`) | mock JMAP 响应 + `#[ignore]` 真机测试 | 代码已实现,待真实 Stalwart 端到端验证(`cargo test -- --ignored jmap::`) | | `domain` + `domain::jmap::client` | **jmap-client 0.4.2** | JMAP 只读语义;`client` = 真实只读 adapter(`MOD-JMAP-CLIENT`) | mock JMAP 响应 + `#[ignore]` 真机测试 | 代码已实现,待真实 Stalwart 端到端验证(`cargo test -- --ignored jmap::`) |
| `state` | redis 0.27 | Redis 读写:配置/开关/会话/TTL 键(`C-REDIS-ONLY-STATE`) | Redis mock | 已实现 | | `state` | redis 0.27 | Redis 读写:配置/开关/会话/TTL 键(`C-REDIS-ONLY-STATE`) | Redis mock | 已实现 |
| `channel` | reqwest 0.13;jmap-client | `Channel` / `Notifier` / `MessageAdapter`,邮件与 TG 双渠道(事件→领域 Command;领域 Notification→渲染) | mock HTTP | 已实现(`src/channel.rs`,Telegram 为 `reqwest` 自研) | | `channel` | reqwest 0.13;jmap-client | `TelegramClient`(`send_text` / `send_notification`),邮件与 TG 双渠道(事件→领域 `Notification`→渲染) | mock HTTP | 已实现(`src/channel.rs`,`reqwest` 自研,无第三方 Bot 框架) |
| `worker` | — | `parse_intent` → `Intent`(6 种,含 `Search(query)`:`/search` + `SearchSnippet/get` 高亮渲染),命令路由与 AI 授权判定 | 表驱动纯单测 | 已实现 | | `worker` | — | `parse_intent` → `Intent`(6 种,含 `Search(query)`:`/search` + `SearchSnippet/get` 高亮渲染),命令路由与 AI 授权判定 | 表驱动纯单测 | 已实现 |
| `ai` | reqwest 0.13 | `LlmClient` / `summarize` | mock OpenAI 兼容端点 | 已实现 | | `ai` | reqwest 0.13 | `LlmClient` / `summarize` | mock OpenAI 兼容端点 | 已实现 |
| `notify` | axum;redis | HTTP 鉴权、全局开关、`/push/jmap` 入队、`Email/changes` 对账和游标提交 | webhook 校验失败路径覆盖 403 | 已覆盖真实对账路径;仍需真实 Stalwart 环境做端到端验收 | | `notify` | axum;redis | HTTP 鉴权、全局开关、`/push/jmap` 入队、`Email/changes` 对账和游标提交 | webhook 校验失败路径覆盖 403 | 已覆盖真实对账路径;仍需真实 Stalwart 环境做端到端验收 |
@ -605,7 +602,7 @@ pub enum BotError {
- 验收:`cargo test`(mock)全绿;`cargo test -- --ignored jmap::`(有真机环境时)能连真实服务器列文件夹/邮件并读原文(未实际运行前不得声称已通过)。 - 验收:`cargo test`(mock)全绿;`cargo test -- --ignored jmap::`(有真机环境时)能连真实服务器列文件夹/邮件并读原文(未实际运行前不得声称已通过)。
### 10.2 阶段 2:渠道适配(已完成) ### 10.2 阶段 2:渠道适配(已完成)
- `src/channel.rs` 定义 Channel / Notifier / MessageAdapter 抽象 + 领域 Command/Notification 类型。 - `src/channel.rs` 实现 `TelegramClient`(`send_text` / `send_notification`,`reqwest` 自研);领域类型 `Notification` 在 `src/domain.rs`。
- Telegram 装配用 `reqwest` 直发 `https://api.telegram.org/bot{token}/sendMessage`(**未引入 teloxide**;评估记录见 `docs/retired.md`)。 - Telegram 装配用 `reqwest` 直发 `https://api.telegram.org/bot{token}/sendMessage`(**未引入 teloxide**;评估记录见 `docs/retired.md`)。
- 入站:`POST /webhook/tg` 校验 secret token → 按 `update_id` 去重(`dedup:tg:{update_id}`)→ 入 Redis Streams → 2xx;出站为 Telegram 唯一用到的 Bot API 端点。 - 入站:`POST /webhook/tg` 校验 secret token → 按 `update_id` 去重(`dedup:tg:{update_id}`)→ 入 Redis Streams → 2xx;出站为 Telegram 唯一用到的 Bot API 端点。
- 意图路由:`src/worker.rs` 的 `parse_intent` 解析 6 种意图 —— `Help` / `Consent { ttl, label }` / `Summary(email_id)` / `Search(query)` / `Query` / `Unknown`,全部走自然语言触发词(AI 授权词见 `docs/reference.md` 的 AI 授权态一节)。`/search` 详见 §2.3、§5.7;中文搜索词只做**前缀匹配**(`搜索/查找/检索` + `/search`),避免覆盖授权与摘要意图。 - 意图路由:`src/worker.rs` 的 `parse_intent` 解析 6 种意图 —— `Help` / `Consent { ttl, label }` / `Summary(email_id)` / `Search(query)` / `Query` / `Unknown`,全部走自然语言触发词(AI 授权词见 `docs/reference.md` 的 AI 授权态一节)。`/search` 详见 §2.3、§5.7;中文搜索词只做**前缀匹配**(`搜索/查找/检索` + `/search`),避免覆盖授权与摘要意图。

View File

@ -126,6 +126,7 @@
| `Config.port` / `Config.redis_url` 字段 | 已删除字段 | `port` 在两个构造器里硬编码 `8080`,而监听绑定直接从环境变量取值——字段值可与真实监听端口静默不一致,且全仓零读取方;`redis_url` 同样零读取 | `Config` 收窄为 6 字段:`telegram` / `jmap` / `account_id` / `llm` / `auth` / `worker_token`,与 `BusinessConfig` 同构 | | `Config.port` / `Config.redis_url` 字段 | 已删除字段 | `port` 在两个构造器里硬编码 `8080`,而监听绑定直接从环境变量取值——字段值可与真实监听端口静默不一致,且全仓零读取方;`redis_url` 同样零读取 | `Config` 收窄为 6 字段:`telegram` / `jmap` / `account_id` / `llm` / `auth` / `worker_token`,与 `BusinessConfig` 同构 |
| 两个构造器的 `redis_url` 参数 | 已删除参数 | 唯一读者(原 main.rs 第 72 行)在上一轮改造中被移除 | `redis_only()` 与 `from_business(value)`;包装器 `from_business_json` / `from_business_value` 同步去掉首参 | | 两个构造器的 `redis_url` 参数 | 已删除参数 | 唯一读者(原 main.rs 第 72 行)在上一轮改造中被移除 | `redis_only()` 与 `from_business(value)`;包装器 `from_business_json` / `from_business_value` 同步去掉首参 |
| 19 个遗留环境变量名 | 已删除读取 | 见上;这些名字在代码中已无任何读取方 | 名单见 `docs/reference.md` §5.3;语义见 `docs/design.md` §7.1 | | 19 个遗留环境变量名 | 已删除读取 | 见上;这些名字在代码中已无任何读取方 | 名单见 `docs/reference.md` §5.3;语义见 `docs/design.md` §7.1 |
| `Channel` / `Notifier` / `MessageAdapter`(`src/channel.rs`)+ `UserCommand`(`src/domain.rs`) | 已删除占位 trait 与类型 | 三者**无实现、无调用方、无 dyn 绑定**,`#[expect(dead_code)]` 属性是仅有的引用来源;实际 Telegram 出站走 `channel::telegram::TelegramClient`,由 `worker.rs` 的 `MetadataWorker` 与 `notify.rs` 直接持有,从未经过它们。`UserCommand` 的唯一使用者是被删的 `Channel` | `channel.rs` 只留 `pub mod telegram`(`TelegramClient`,`reqwest` 自研);`domain.rs` 只留领域 `Notification`(worker.rs:234 在用)。这是 src/ 里最后 3 个 `#[expect(dead_code)]` |
> 注意 `LLM_MAX_RETRIES` 与 `LLM_SUMMARY_TARGET_CHARS` 的区别:前者从来不是环境变量 > 注意 `LLM_MAX_RETRIES` 与 `LLM_SUMMARY_TARGET_CHARS` 的区别:前者从来不是环境变量
> (Redis 运行参数 `max_retries`,回落默认见 `docs/reference.md` §6.1),后者是常量 > (Redis 运行参数 `max_retries`,回落默认见 `docs/reference.md` §6.1),后者是常量

View File

@ -1,25 +1,5 @@
use async_trait::async_trait; /// Telegram adapter: messages are rendered here and sent via reqwest, with no bot
/// framework on the classpath. Command parsing lives in `worker.rs::parse_intent`.
use crate::domain::{Notification, UserCommand};
#[expect(dead_code, reason = "稳定ID+阶段0占位:渠道抽象供后续 adapter 使用")]
#[async_trait]
pub trait Channel: Send + Sync {
async fn receive(&self, command: UserCommand);
}
#[expect(dead_code, reason = "稳定ID+阶段0占位:通知抽象供后续 adapter 使用")]
#[async_trait]
pub trait Notifier: Send + Sync {
async fn notify(&self, notification: Notification);
}
#[expect(dead_code, reason = "稳定ID+阶段0占位:渠道消息适配抽象")]
pub trait MessageAdapter: Send + Sync {
fn render_notification(&self, notification: &Notification) -> String;
}
/// Telegram is intentionally only an adapter boundary in stage 0 (no bot commands yet).
pub mod telegram { pub mod telegram {
use crate::domain::Notification; use crate::domain::Notification;
use crate::state::RuntimeConfigProvider; use crate::state::RuntimeConfigProvider;

View File

@ -2,12 +2,6 @@
pub mod jmap; pub mod jmap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UserCommand {
pub text: String,
pub chat_id: i64,
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Notification { pub struct Notification {
pub sender: String, pub sender: String,