# 2026-05-29: 将 broadcast 任务关闭机制替换为 JoinHandle::abort

## 概述

本次更改将项目内所有使用 `tokio::sync::broadcast` 发送关闭信号来控制异步任务终止的模式，全面替换为存储 `tokio::task::JoinHandle` 并在需要时调用 `abort()` 的方案。

这一改动消除了对 broadcast channel 的依赖，简化了服务生命周期管理，并统一了所有 `CommunicationService` 实现的任务关闭方式。

## 核心变更

### 1. `CommunicationService` trait 方法签名调整

**文件**: `src/communication/utils.rs`

- `stop(&self)` → `stop(&mut self)`
- `start(&self)` → `start(&mut self)`
- `restart(&self)` → `restart(&mut self)`
- `Box<dyn CommunicationService>` 的委托实现同步更新
- `Client` 的 `start_service(&self)` → `start_service(&mut self)`
- `Client` 的 `stop_service(&self)` → `stop_service(&mut self)`
- `Client` 的 `restart_service(&self)` → `restart_service(&mut self)`

将 `&self` 改为 `&mut self` 后，服务可以直接修改自身的 `task_handle` 字段，无需引入 `Mutex` 等同步原语。

### 2. `HttpService` — 移除 broadcast

**文件**: `src/communication/http.rs`

- 移除字段 `close_signal_sender: broadcast::Sender<()>`
- 新增字段 `task_handle: Option<JoinHandle<()>>`
- 移除 `tokio::sync::broadcast` 依赖
- `api_processor` 内部不再监听 `close_signal.recv()`，改为纯 `loop { api_receiver.recv_async().await }`
- `start(&mut self)`：spawn 任务并将 handle 存入 `task_handle`
- `stop(&mut self)`：`self.task_handle.take().map(|h| h.abort())`
- 手动实现 `Clone`，跳过不可 Clone 的 `JoinHandle`

### 3. `SseService` — 移除 broadcast

**文件**: `src/communication/sse.rs`

- 与 `HttpService` 采用完全一致的替换模式
- `eventsource_listener` 移除 `close_signal` 和 `select!`，改为直接 `loop { es.next().await }`

### 4. `HttpPostService` — 移除 broadcast

**文件**: `src/communication/http_post.rs`

- 移除 `close_signal_sender` 字段和 broadcast 依赖
- 使用 `task_handle` 管理 serve task
- `start` 中不再使用 `with_graceful_shutdown`，改为直接 `axum::serve(listener, router).await`
- `stop` 中调用 `abort()`

### 5. `WsService` — 移除 broadcast

**文件**: `src/communication/ws.rs`

- 移除 `close_signal_sender` 字段和 broadcast 依赖
- `handle_connection` 移除 `close_signal: &mut broadcast::Receiver<()>` 参数
- 移除 `select!` 中的 `close_signal.recv()` 分支，`handle_connection` 直接 `transfer.await`
- `spawn_processor` 中的重连循环不再需要监听退出信号（abort 会终止整个 task）
- 移除不再使用的 `Either`、`select`、`pin` 导入

### 6. `WsReverseService` — 移除 broadcast 与 connection_close_signal

**文件**: `src/communication/ws_reverse.rs`

**服务级变更**:
- 移除 `close_signal_sender` 字段和 broadcast 依赖
- 新增 `serve_handle: Option<JoinHandle<()>>` 和 `app_state: Option<Arc<AppState>>`

**连接级重构**:
- `send_processor` 和 `read_processor` 移除所有 signal 参数，改为纯异步循环
- 移除 `connection_close_signal` broadcast channel
- `handler` 中不再分别 spawn send/read task 后用 `try_join!` 等待
- 改为在 `handler` 内使用 `tokio::select!` 同时 await send 和 read：
  - 当 read 检测到 `Message::Close(_)` 时返回，`select!` 自动取消 send 分支
  - 当 send 的 `api_receiver` 断开时返回，`select!` 自动取消 read 分支
- `AppState` 新增 `connection_handle: Arc<Mutex<Option<JoinHandle<anyhow::Result<()>>>>>` 用于 `stop` 时 abort 当前活跃连接

**生命周期**:
- `start`：创建 `AppState`（含 `connection_handle`）→ 绑定 listener → spawn serve future → 存储 handle
- `stop`：从 `app_state.connection_handle` 取出并 abort 当前连接 → abort `serve_handle`

### 7. `SplitCombiner` — 移除 broadcast

**文件**: `src/communication/combiner.rs`

- 移除 `close_signal_sender` 字段和 broadcast 依赖
- 新增 `task_handle: Option<JoinHandle<()>>`
- `processor` 函数移除 `close_signal` 参数和 `select!` 分支
- `start` 中 spawn processor 并存入 handle
- `stop` 中 abort handle

### 8. `BothEventCombiner` — 同步 trait 签名

**文件**: `src/communication/combiner.rs`

- `stop(&self)` → `stop(&mut self)`
- `start(&self)` → `start(&mut self)`
- `restart(&self)` → `restart(&mut self)`

## 技术细节

### 为什么使用 `abort` 而非 broadcast signal？

1. **简化代码**：每个服务不再需要创建 broadcast channel、在 task 中 `select!` 监听 signal、处理 signal 接收错误
2. **统一模式**：所有服务的关闭逻辑一致为 "存 handle → abort"，降低心智负担
3. **避免泄漏**：broadcast 模式下如果 task 在 `select!` 的某个分支阻塞，signal 发送后可能无法及时响应；`abort` 会立即取消整个 future

### JoinHandle 类型处理

由于 `tokio::spawn` 的返回类型取决于被 spawn future 的输出类型，而各服务的 processor 返回类型不同（`anyhow::Result<()>`、`std::io::Result<()>` 等），统一将 `task_handle` 声明为 `Option<JoinHandle<()>>`，spawn 时在外层包一层 `async move { processor().await.ok(); }` 忽略结果。

### Clone 实现调整

`JoinHandle` 不实现 `Clone`，因此 `HttpService`、`SseService`、`WsService` 的自动 `#[derive(Clone)]` 被移除，改为手动实现 `Clone`。Clone 出的实例的 `task_handle` 为 `None`，`is_running` 共享同一个 `Arc<AtomicBool>`（与原有行为一致）。

## 受影响文件

- `src/communication/utils.rs`
- `src/communication/http.rs`
- `src/communication/sse.rs`
- `src/communication/http_post.rs`
- `src/communication/ws.rs`
- `src/communication/ws_reverse.rs`
- `src/communication/combiner.rs`

## 未受影响文件

- `src/communication/decorator.rs`：该文件使用 `tokio::sync::broadcast` 进行事件广播（`EventBroadcastDecorator`），并非 task 关闭机制，因此保持不变。

## 破坏性变更

- `Client::start_service` 现为 `&mut self` 方法
- `Client::stop_service` 现为 `&mut self` 方法
- `Client::restart_service` 现为 `&mut self` 方法
- 所有 `CommunicationService` 实现的方法签名同步变更

调用方需要将持有 `Client` 的引用从 `&Client` 改为 `&mut Client`（或使用 `Arc<Mutex<Client>>` 等可变访问方式）。

---

# 2026-05-29: 更新 AGENTS.md 并新增变更日志规则

## 概述

对 `AGENTS.md` 进行全面更新，以准确反映项目当前状态。同时新增变更日志记录规则，确保所有项目修改都能被追踪。

## 核心变更

### 1. 更新 AGENTS.md 项目概述

**文件**: `AGENTS.md`

- 补充说明主 crate `onebot-api` (version 1.2.3) 与独立的 proc-macro crate `onebot-api-macros` 的关系
- 新增 `examples/communication/` 和 `changelogs/` 文件夹说明
- 保留原有的构建指令、格式化规则、架构说明、发布流程和关键约定

### 2. 新增变更日志规则

**文件**: `AGENTS.md`

- 在文档末尾新增 `Changelog rule` 章节
- 规定：**无论对项目做出何种修改（无论修改的是 `src/` 还是其他任意文件夹），都必须在 `changelogs/` 文件夹下记录变更**
- 明确记录流程：查找当前日期 markdown 文件 → 存在则追加，不存在则创建
- 要求每条记录包含：修改内容、受影响文件、修改原因或影响

## 受影响文件

- `AGENTS.md`
- `changelogs/2026-05-29.md`

## 未受影响文件

无
