Files
iAs/wechat/README.md
T
2026-06-25 17:23:11 +08:00

493 lines
13 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 crate,负责微信 Bot 的扫码登录、监听器生命周期管理、长轮询收消息和文本消息发送。
当前模块只提供库能力,不会自动启动后台任务,也不会自动持久化登录态。业务服务需要主动创建 `WeChatClient`、完成登录或恢复登录态,然后调用收发消息接口。
## 模块结构
```text
wechat/
src/
lib.rs # 模块导出
client.rs # WeChatClient,封装所有 HTTP API 调用
types.rs # iLink Bot API 请求、响应和消息结构
```
公开模块:
```rust
pub mod client;
pub mod types;
```
常用类型:
```rust
use wechat::client::WeChatClient;
use wechat::types::{GetUpdatesResp, LoginResult, MessageItem, WeixinMessage};
```
## Cargo 引用
本仓库根目录已经把 `wechat` 加入 workspace
```toml
[workspace]
members = ["service", "ai", "common", "wechat"]
```
如果其他 workspace crate 需要使用它,在对应 `Cargo.toml` 里加入:
```toml
[dependencies]
wechat = { path = "../wechat" }
```
## 环境变量
| 变量 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `WEIXIN_BASE_URL` | 否 | `https://ilinkai.weixin.qq.com` | iLink Bot API 地址。测试、代理或私有部署时可覆盖。 |
示例:
```bash
export WEIXIN_BASE_URL="https://ilinkai.weixin.qq.com"
```
## 基本生命周期
推荐调用顺序:
1. 创建客户端:`WeChatClient::new(None)`
2. 扫码登录:`login(...)`
3. 持久化登录结果:保存 `token``account_id``base_url`
4. 注册监听器:`notify_start()`
5. 循环拉取消息:`receive_messages()`
6. 处理用户消息并回复:`send_text(...)`
7. 服务退出前注销监听器:`notify_stop()`
## 快速开始
下面示例会扫码登录,注册监听器,然后持续接收用户文本消息并原样回复。
```rust
use std::time::Duration;
use wechat::client::WeChatClient;
use wechat::types::WeixinMessage;
#[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);
client.notify_start().await?;
loop {
let updates = client.receive_messages().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;
}
}
```
## 登录
完整登录接口:
```rust
pub async fn login(
&self,
on_qrcode: impl Fn(&str),
timeout_secs: u64,
) -> Result<LoginResult, String>
```
参数说明:
| 参数 | 说明 |
| --- | --- |
| `on_qrcode` | 获取二维码内容后的回调。当前实现会传入 `qrcode_img_content`,通常是可打开或可展示为二维码的内容。 |
| `timeout_secs` | 扫码确认总超时时间,单位秒。 |
返回值:
```rust
pub struct LoginResult {
pub token: String,
pub account_id: String,
pub base_url: String,
pub user_id: String,
}
```
登录内部流程:
1. `get_qrcode()` 调用 `/ilink/bot/get_bot_qrcode` 获取二维码。
2. 终端渲染二维码,同时打印二维码链接。
3. `poll_qr_status()` 轮询 `/ilink/bot/get_qrcode_status`
4. 用户扫码并确认后,把 `bot_token``ilink_bot_id` 写入客户端内存。
5. 返回 `LoginResult`,供业务层持久化。
扫码状态值:
| 状态 | 含义 | 当前处理 |
| --- | --- | --- |
| `wait` | 等待扫码 | 继续轮询 |
| `scaned` | 已扫码,等待微信确认 | 提示用户确认 |
| `scaned_but_redirect` | 需要切换 IDC | 使用 `redirect_host` 更新轮询地址 |
| `confirmed` | 登录成功 | 保存 token/account_id 并返回 |
| `expired` | 二维码过期 | 返回错误 |
## 恢复登录态
`wechat` 模块不负责把登录态写入数据库或文件。业务层应在登录成功后保存:
- `LoginResult.token`
- `LoginResult.account_id`
- `LoginResult.base_url`
- `get_updates_buf()` 返回值,建议在每次 `receive_messages()` 成功后保存
下次启动时恢复:
```rust
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?;
```
注意:`set_auth` 需要 `&mut self`,因此恢复登录态时变量要声明为 `mut`
## 注册和注销监听器
注册监听器:
```rust
client.notify_start().await?;
```
注销监听器:
```rust
client.notify_stop().await?;
```
这两个接口分别调用:
- `POST /ilink/bot/msg/notifystart`
- `POST /ilink/bot/msg/notifystop`
建议:
- 登录或恢复登录态后先调用 `notify_start()`,再开始拉消息。
- 进程收到退出信号时调用 `notify_stop()`
- 如果进程异常退出,下一次启动后仍应重新调用 `notify_start()`
## 接收消息
接口:
```rust
pub async fn receive_messages(&self) -> Result<GetUpdatesResp, String>
```
响应结构:
```rust
pub struct GetUpdatesResp {
pub ret: i32,
pub errcode: Option<i32>,
pub errmsg: Option<String>,
pub msgs: Vec<WeixinMessage>,
pub get_updates_buf: String,
pub longpolling_timeout_ms: Option<i64>,
}
```
说明:
- 该接口调用 `/ilink/bot/getupdates`
- 客户端内部维护 `updates_buf`,并在响应里的 `get_updates_buf` 非空时自动更新。
- 如果请求超时,当前实现返回一个空消息列表,不视为致命错误。
- 如果 `ret != 0``errcode != 0`,当前实现会写 warn 日志,但仍把响应返回给调用方。
文本消息提取:
```rust
for msg in updates.msgs {
if msg.msg_type == Some(WeixinMessage::TYPE_USER) {
if let Some(text) = msg.text_content() {
println!("收到文本: {text}");
}
}
}
```
消息关键字段:
| 字段 | 说明 |
| --- | --- |
| `msg_type` | `1` 表示用户消息,`2` 表示 Bot 消息。 |
| `from_user_id` | 发送者 ID。回复私聊消息时通常作为 `send_text``to`。 |
| `to_user_id` | 接收者 ID。 |
| `group_id` | 群消息 ID,群聊场景可能非空。 |
| `item_list` | 消息内容列表,可包含文本、图片、语音、文件、视频。 |
| `context_token` | 微信上下文令牌。回复时透传可保持多轮上下文。 |
## 发送文本消息
接口:
```rust
pub async fn send_text(
&self,
to: &str,
text: &str,
context_token: Option<&str>,
) -> Result<String, String>
```
参数说明:
| 参数 | 说明 |
| --- | --- |
| `to` | 接收者用户 ID。通常来自收到消息的 `from_user_id`。 |
| `text` | 要发送的文本内容。 |
| `context_token` | 可选上下文令牌,建议透传收到消息里的 `context_token`。 |
返回值是本地生成的 `client_id`,可用于日志追踪。
示例:
```rust
let client_id = client
.send_text(
from_user_id,
"你好,我是 Bot",
msg.context_token.as_deref(),
)
.await?;
println!("消息已提交: client_id={client_id}");
```
当前发送实现固定构造:
- `message_type = 2`,表示 Bot 消息
- `message_state = 2`,表示完成态
- `item_list = [MessageItem::text(text)]`
## 非文本消息
协议类型已经定义了以下消息 item:
- `TextItem`
- `ImageItem`
- `VoiceItem`
- `FileItem`
- `VideoItem`
当前便捷发送接口只实现了 `send_text()`。如果需要发送图片、文件或视频,需要新增对应的发送方法,构造 `WeixinMessage.item_list` 中的 `MessageItem`
接收侧可以通过 `MessageItem.item_type` 判断类型:
```rust
for item in msg.item_list.unwrap_or_default() {
match item.item_type {
Some(MessageItem::TYPE_TEXT) => {
let text = item.text_item.and_then(|v| v.text);
println!("文本: {:?}", text);
}
Some(MessageItem::TYPE_IMAGE) => {
println!("图片: {:?}", item.image_item);
}
Some(MessageItem::TYPE_VOICE) => {
println!("语音: {:?}", item.voice_item);
}
Some(MessageItem::TYPE_FILE) => {
println!("文件: {:?}", item.file_item);
}
Some(MessageItem::TYPE_VIDEO) => {
println!("视频: {:?}", item.video_item);
}
_ => {}
}
}
```
## 长轮询循环建议
推荐业务层把接收循环放到独立 tokio task:
```rust
let client_for_loop = client.clone();
tokio::spawn(async move {
loop {
match client_for_loop.receive_messages().await {
Ok(updates) => {
for msg in updates.msgs {
// 处理消息
}
}
Err(err) => {
tracing::warn!("接收微信消息失败: {err}");
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
}
}
}
});
```
注意:
- `WeChatClient` 实现了 `Clone`,内部 token、account_id 和 updates_buf 使用 `Arc<Mutex<_>>` 共享。
- `receive_messages()` 已经处理普通超时,业务层不需要把空消息视为错误。
- 处理消息时建议过滤 `WeixinMessage::TYPE_BOT`,避免重复处理 Bot 自己发出的消息。
- 每次成功拉取后可以调用 `get_updates_buf()` 并持久化,降低重启后重复收消息的概率。
## 与 AI 回复集成示例
示例逻辑:收到用户文本后调用业务自己的 AI 函数,再把结果发回微信。
```rust
async fn handle_wechat_message(client: &WeChatClient, msg: WeixinMessage) -> Result<(), String> {
if msg.msg_type != Some(WeixinMessage::TYPE_USER) {
return Ok(());
}
let Some(from_user_id) = msg.from_user_id.as_deref() else {
return Ok(());
};
let Some(user_text) = msg.text_content() else {
return Ok(());
};
let answer = call_ai(user_text).await?;
client
.send_text(from_user_id, &answer, msg.context_token.as_deref())
.await?;
Ok(())
}
async fn call_ai(input: &str) -> Result<String, String> {
Ok(format!("AI 回复: {input}"))
}
```
## API 端点
`WeChatClient` 当前使用以下 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` | 发送消息 |
请求头由模块内部生成:
- `Content-Type: application/json`
- `Content-Length`
- `iLink-App-ClientVersion`
- `X-WECHAT-UIN`
- 登录后请求额外带:
- `Authorization: Bearer <token>`
- `AuthorizationType: ilink_bot_token`
## 日志
模块使用 `tracing` 输出日志,主要 target 为:
```text
ias::auth
```
业务程序可初始化日志:
```rust
tracing_subscriber::fmt()
.with_env_filter("info,ias::auth=debug")
.init();
```
## 错误处理
所有公开异步方法当前返回 `Result<_, String>`
常见错误:
| 场景 | 典型错误 | 处理建议 |
| --- | --- | --- |
| 二维码过期 | `二维码已过期` | 重新调用 `login()`。 |
| 登录超时 | `登录超时` | 增大 `timeout_secs` 或重新登录。 |
| token 无效 | HTTP 401/403 或接口返回错误 | 清理持久化登录态,重新扫码登录。 |
| 长轮询超时 | 返回空 `msgs` | 正常情况,继续下一轮。 |
| 网络错误 | `请求失败` | 延迟重试,必要时检查 `WEIXIN_BASE_URL`。 |
## 当前限制
- `ILINK_APP_ID` 当前是空字符串。如接入方要求 app id,需要在 `client.rs` 中配置或改造成环境变量。
- 登录态只保存在内存中,重启恢复需要业务层调用 `set_auth()``set_updates_buf()`
- 只提供文本发送便捷方法 `send_text()`
- 群聊回复策略需要业务层结合 `group_id` 和实际 iLink 协议进一步确认。
- `send_text()` 当前不解析服务端响应体,只在 HTTP 层确认请求成功后返回本地 `client_id`
## 排障清单
1. 确认能访问 `WEIXIN_BASE_URL`
2. 确认扫码后在微信侧完成确认。
3. 登录后先调用 `notify_start()`,再调用 `receive_messages()`
4. 如果收不到消息,检查 `get_updates_buf` 是否恢复了过旧状态;必要时清空后重试。
5. 如果发送失败,检查 token 是否过期,以及 `to` 是否使用了正确的 `from_user_id`
6. 打开 `ias::auth=debug` 日志观察 API 请求和状态变化。