Files
iAs/wechat/README.md
T
2026-06-29 09:15:30 +08:00

400 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# wechat 模块使用文档
`wechat` 是微信 iLink Bot API 的 Rust 封装,提供两层能力:
- `WeChatClient`:单账号客户端,负责扫码登录、恢复登录态、注册监听器、长轮询收消息、发送文本消息。
- `WeChatMultiAccountManager`:多账号运行时管理器,按账号启动多个独立长轮询任务,并通过事件队列把消息交给业务层。
设计原则:**一个 `WeChatClient` 只代表一个微信 Bot 账号;多账号监听就是每个账号一个独立长轮询任务**。这样可以保证 `token``base_url``get_updates_buf``context_token` 不串账号。
## 模块结构
```text
wechat/
src/
lib.rs # 模块导出
client.rs # 单账号 WeChatClient
manager.rs # 多账号 WeChatMultiAccountManager
types.rs # iLink Bot API 协议类型
```
公开模块:
```rust
pub mod client;
pub mod manager;
pub mod types;
```
常用导入:
```rust
use wechat::client::WeChatClient;
use wechat::manager::{WeChatAccountConfig, WeChatEvent, WeChatMultiAccountManager};
use wechat::types::{MessageItem, WeixinMessage};
```
## Cargo 引用
workspace 内其他 crate 使用:
```toml
[dependencies]
wechat = { path = "../wechat" }
```
## 环境变量
| 变量 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `WEIXIN_BASE_URL` | 否 | `https://ilinkai.weixin.qq.com` | iLink Bot API 地址。显式传入 `base_url` 时优先使用显式值。 |
## 单账号使用
单账号生命周期:
```text
WeChatClient::new
login 或 set_auth
set_updates_buf
notify_start
receive_messages loop
send_text
notify_stop
```
### 扫码登录
```rust
use wechat::client::WeChatClient;
#[tokio::main]
async fn main() -> Result<(), String> {
let client = WeChatClient::new(None);
let login = client
.login(
|qrcode_url| println!("二维码地址: {qrcode_url}"),
120,
)
.await?;
println!("account_id={}", login.account_id);
println!("base_url={}", login.base_url);
// 业务层需要持久化:
// login.token
// login.account_id
// login.base_url
// login.user_id
Ok(())
}
```
### 恢复登录态并收消息
```rust
use std::time::Duration;
use wechat::client::WeChatClient;
use wechat::types::WeixinMessage;
#[tokio::main]
async fn main() -> Result<(), String> {
let mut client = WeChatClient::new(None);
client
.set_auth(
"持久化保存的 token",
"持久化保存的 account_id",
"持久化保存的 base_url",
)
.await;
client
.set_updates_buf("持久化保存的 get_updates_buf")
.await;
client.notify_start().await?;
loop {
let updates = client.receive_messages().await?;
let latest_buf = client.get_updates_buf().await;
// 建议每次成功 receive 后持久化 latest_buf。
persist_updates_buf(&latest_buf).await;
for msg in updates.msgs {
if msg.msg_type != Some(WeixinMessage::TYPE_USER) {
continue;
}
let Some(from_user_id) = msg.from_user_id.as_deref() else {
continue;
};
let Some(text) = msg.text_content() else {
continue;
};
client
.send_text(
from_user_id,
&format!("收到: {text}"),
msg.context_token.as_deref(),
)
.await?;
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
async fn persist_updates_buf(_buf: &str) {}
```
## 多账号监听
多账号监听使用 `WeChatMultiAccountManager`。它会为每个账号创建一个独立 `WeChatClient` 和一个独立 tokio 任务:
```text
account A -> WeChatClient A -> getupdates(token A, buf A)
account B -> WeChatClient B -> getupdates(token B, buf B)
account C -> WeChatClient C -> getupdates(token C, buf C)
```
这不是短轮询刷接口,而是长轮询。单次 `getupdates` 请求通常会被服务端挂起到有消息或超时,因此多个账号对应多个挂起请求是预期模型。
### 账号配置
```rust
use wechat::manager::WeChatAccountConfig;
let account = WeChatAccountConfig::new(
"account_id",
"token",
"base_url",
"get_updates_buf",
);
```
字段说明:
| 字段 | 说明 |
| --- | --- |
| `account_id` | 微信 iLink Bot 账号 ID,来自登录返回的 `LoginResult.account_id`。 |
| `token` | 登录返回的 `LoginResult.token`。 |
| `base_url` | 登录返回的 `LoginResult.base_url`。 |
| `updates_buf` | 上次轮询保存的 `get_updates_buf`,新账号可传空字符串。 |
### 启动多个账号
```rust
use wechat::manager::{WeChatAccountConfig, WeChatEvent, WeChatMultiAccountManager};
use wechat::types::WeixinMessage;
#[tokio::main]
async fn main() -> Result<(), String> {
let (mut manager, mut event_rx) = WeChatMultiAccountManager::new(1024);
let accounts = load_accounts().await;
for account in accounts {
manager.start_account(account).await?;
}
while let Some(event) = event_rx.recv().await {
match event {
WeChatEvent::UpdatesBuf {
account_id,
updates_buf,
} => {
persist_updates_buf(&account_id, &updates_buf).await;
}
WeChatEvent::Message {
account_id,
message,
updates_buf,
} => {
persist_updates_buf(&account_id, &updates_buf).await;
if message.msg_type != Some(WeixinMessage::TYPE_USER) {
continue;
}
let Some(from_user_id) = message.from_user_id.as_deref() else {
continue;
};
let Some(text) = message.text_content() else {
continue;
};
manager
.send_text(
&account_id,
from_user_id,
&format!("账号 {account_id} 收到: {text}"),
message.context_token.as_deref(),
)
.await?;
}
WeChatEvent::PollError { account_id, error } => {
tracing::warn!("微信账号轮询失败 account_id={account_id}: {error}");
}
}
}
Ok(())
}
async fn load_accounts() -> Vec<WeChatAccountConfig> {
vec![]
}
async fn persist_updates_buf(_account_id: &str, _buf: &str) {}
```
### 停止账号
停止单个账号:
```rust
manager.stop_account("account_id").await?;
```
停止全部账号:
```rust
let results = manager.stop_all().await;
for (account_id, result) in results {
if let Err(err) = result {
tracing::warn!("停止微信账号失败 account_id={account_id}: {err}");
}
}
```
`stop_account` 会先中止本地轮询任务,再尽力调用 `notify_stop()`。如果进程直接退出,`Drop` 会 abort 本地任务,但无法异步调用 `notify_stop()`,所以生产环境建议在收到退出信号时显式调用 `stop_all()`
## 为什么不能一个轮询监听多个账号
iLink Bot 的 `getupdates` 是按 `Authorization: Bearer <token>` 鉴权的。一个 token 只对应一个 Bot 账号,服务端不会在同一个请求里返回其他账号的消息。
必须隔离的账号状态:
| 状态 | 为什么必须隔离 |
| --- | --- |
| `token` | 决定当前请求属于哪个账号。 |
| `base_url` | 登录可能返回账号实际使用的 IDC 地址。 |
| `get_updates_buf` | 轮询游标,混用会导致消息重复、丢失或串账号。 |
| `context_token` | 回复上下文,混用会导致回复到错误会话或上下文断裂。 |
因此多账号正确模型是“多个长轮询任务”,不是“一个共享轮询”。
## 持久化建议
本 crate 不绑定数据库或文件系统,业务层应自行持久化以下数据。
账号表建议字段:
```text
account_id string primary key
token string
base_url string
user_id string optional
updates_buf string
updated_at timestamp
```
收到 `WeChatEvent::Message` 后,使用事件里的 `updates_buf` 更新对应账号的游标。
如果要支持主动发送消息,建议额外维护:
```text
account_id
from_user_id
context_token
updated_at
```
收到用户消息时按 `account_id + from_user_id` 保存 `context_token`。主动发送时优先显式传 `account_id`,再取对应用户最近的 `context_token`
## 发送消息路由
多账号场景下发送消息必须明确使用哪个账号:
```rust
manager
.send_text(
"account_id",
"to_user_id",
"回复内容",
Some("context_token"),
)
.await?;
```
不建议只按 `to_user_id` 自动推断账号。原因是同一个用户可能和多个 Bot 账号都有会话,自动推断会产生歧义。
## API 端点
当前封装使用以下 iLink Bot API
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| `GET` | `/ilink/bot/get_bot_qrcode` | 获取扫码登录二维码 |
| `GET` | `/ilink/bot/get_qrcode_status` | 查询扫码状态 |
| `POST` | `/ilink/bot/msg/notifystart` | 注册监听器 |
| `POST` | `/ilink/bot/msg/notifystop` | 注销监听器 |
| `POST` | `/ilink/bot/getupdates` | 长轮询拉取消息 |
| `POST` | `/ilink/bot/sendmessage` | 发送消息 |
登录后请求会带:
```text
Authorization: Bearer <token>
AuthorizationType: ilink_bot_token
```
## 错误处理
| 场景 | 当前行为 | 建议 |
| --- | --- | --- |
| 二维码过期 | `login()` 返回 `二维码已过期` | 重新登录。 |
| 登录超时 | `login()` 返回 `登录超时` | 增大超时或重新登录。 |
| 长轮询超时 | `receive_messages()` 返回空消息 | 正常情况,继续轮询。 |
| 游标更新 | manager 发送 `WeChatEvent::UpdatesBuf` | 立即持久化对应账号的 `updates_buf`。 |
| 轮询错误 | manager 发送 `WeChatEvent::PollError` 并继续重试 | 记录日志,必要时触发重新登录。 |
| token 失效 | API 返回错误或 HTTP 鉴权失败 | 清理该账号 token,重新扫码登录。 |
| 多账号重复启动 | `start_account()` 返回错误 | 保证同一 `account_id` 只启动一次。 |
## 日志
模块使用 `tracing`,主要 target
```text
ias::auth
```
推荐初始化:
```rust
tracing_subscriber::fmt()
.with_env_filter("info,ias::auth=debug")
.init();
```
## 当前限制
- `send_text()` 只封装文本发送。
- manager 不内置账号持久化,业务层负责数据库或文件存储。
- manager 通过 `JoinHandle::abort()` 停止长轮询任务;如需立即优雅取消 HTTP 请求,可后续在 client 层增加 cancellation 支持。
- `ILINK_APP_ID` 当前为空字符串;如果接入方要求 app id,需要在 `client.rs` 中配置或改为环境变量。
## 排障清单
1. 确认 `base_url` 可访问。
2. 确认账号已经扫码登录并保存了 token。
3. 每个账号只启动一个监听任务。
4. 每个账号独立保存 `updates_buf`
5. 登录或恢复后先调用 `notify_start()`,再开始 `getupdates`
6. 发送消息时显式指定 `account_id`,并尽量透传 `context_token`
7. 打开 `ias::auth=debug` 查看请求和轮询状态。