Files

547 lines
18 KiB
Markdown
Raw Permalink 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.
# wecom-aibot-rust-sdk (Rust)
企业微信智能机器人 Rust SDK —— 基于 WebSocket 长连接通道,提供消息收发、流式回复、模板卡片、事件回调、文件下载解密等核心能力。
> 本项目是 Python 版本 [@wecom/aibot-python-sdk](https://pypi.org/project/wecom-aibot-python-sdk/) 的 Rust 改写版本。
## ✨ 特性
- 🔗 **WebSocket 长连接** — 基于 `wss://openws.work.weixin.qq.com` 内置默认地址,开箱即用
- 🔐 **自动认证** — 连接建立后自动发送认证帧(bot_id + secret)
- 💓 **心跳保活** — 自动维护心跳,连续未收到 ack 时自动判定连接异常
- 🔄 **断线重连** — 指数退避重连策略(1s → 2s → 4s → ... → 30s 上限),支持自定义最大重连次数
- 📨 **消息分发** — 自动解析消息类型并触发对应事件(text / image / mixed / voice / file)
- 🌊 **流式回复** — 内置流式回复方法,支持 Markdown 和图文混排
- 🃏 **模板卡片** — 支持回复模板卡片消息、流式 + 卡片组合回复、更新卡片
- 📤 **主动推送** — 支持向指定会话主动发送 Markdown 或模板卡片消息,无需依赖回调帧
- 📡 **事件回调** — 支持进入会话、模板卡片按钮点击、用户反馈等事件
- ⏩ **串行回复队列** — 同一 req_id 的回复消息串行发送,自动等待回执
- 🔑 **文件下载解密** — 内置 AES-256-CBC 文件解密,每个图片/文件消息自带独立的 aeskey
- 🪵 **可插拔日志** — 支持自定义 Logger,内置带时间戳的 DefaultLogger
- 🦀 **tokio 原生** — 基于 Rust tokio 异步运行时,支持 async/await
## 📦 安装
在 `Cargo.toml` 中添加依赖:
```toml
[dependencies]
wecom-aibot-rust-sdk = "1.0.2"
tokio = { version = "1.35", features = ["full"] }
```
**依赖:**
- Rust >= 1.70
- tokio >= 1.35
- tokio-tungstenite >= 0.21
- tokio-stream >= 0.1
- reqwest >= 0.11
- serde >= 1.0
- serde_json >= 1.0
- aes >= 0.8
- cbc >= 0.1
- base64 >= 0.21
- cpufeatures >= 0.2
- thiserror >= 1.0
- futures >= 0.3
- parking_lot >= 0.12
- rand >= 0.9
- hex >= 0.4
- chrono >= 0.4
- regex >= 1.10
- urlencoding >= 2.1
## ⚙️ 配置
```bash
# 复制示例配置文件
cp .env.example .env
# 编辑 .env 文件,填入真实配置
WECHAT_BOT_ID=your-bot-id
WECHAT_BOT_SECRET=your-bot-secret
# (可选)配置下载文件存放目录,不设置则使用系统 temp 目录
WECHAT_BOT_DOWNLOAD_DIR=/path/to/download/dir
```
## 🚀 快速开始
```rust
use dotenv::dotenv;
use std::env;
use tokio::time::{self, Duration};
use wecom_aibot_rust_sdk::{generate_req_id, WSClient, WSClientOptions};
#[tokio::main]
async fn main() {
// 加载 .env 文件
dotenv().ok();
// 1. 创建客户端实例
let client = WSClient::new(WSClientOptions::new(
env::var("WECHAT_BOT_ID").unwrap(),
env::var("WECHAT_BOT_SECRET").unwrap(),
));
// 2. 监听认证成功
client.on_authenticated(|| {
println!("🔐 认证成功");
});
// 3. 监听文本消息并进行流式回复
let client_text = client.clone();
client.on_message_text(move |frame| {
let client_text = client_text.clone();
let frame = frame.clone();
tokio::spawn(async move {
let content = frame
.body
.as_ref()
.and_then(|v| v.as_object())
.and_then(|b| b.get("text"))
.and_then(|v| v.as_object())
.and_then(|t| t.get("content"))
.and_then(|v| v.as_str())
.unwrap_or("");
println!("收到文本:{}", content);
let stream_id = generate_req_id("stream");
// 发送流式中间内容
let _ = client_text
.reply_stream(&frame, &stream_id, "正在思考中...", false, None, None)
.await;
// 发送最终结果
time::sleep(Duration::from_secs(1)).await;
let _ = client_text
.reply_stream(
&frame,
&stream_id,
&format!("你好!你说的是:\"{}\"", content),
true,
None,
None,
)
.await;
});
});
// 4. 监听进入会话事件(发送欢迎语)
let client_enter = client.clone();
client.on_event_enter_chat(move |frame| {
let client_enter = client_enter.clone();
let frame = frame.clone();
tokio::spawn(async move {
use serde_json::json;
let _ = client_enter
.reply_welcome(
&frame,
json!({
"msgtype": "text",
"text": {"content": "您好!我是智能助手,有什么可以帮您的吗?"}
}),
)
.await;
});
});
// 5. 启动客户端
println!("正在启动机器人...");
if let Err(e) = client.connect().await {
eprintln!("启动失败:{}", e);
return;
}
println!("机器人启动成功,按 Ctrl+C 退出");
// 等待退出信号
tokio::signal::ctrl_c().await.ok();
println!("\n正在停止机器人...");
client.disconnect();
}
```
## 📚 示例说明
SDK 提供了多个示例代码,位于 `examples/` 目录下:
### 1. basic.rs - 基础使用示例
演示基本的连接、消息监听和回复功能。
```bash
cargo run --example basic
```
### 2. echo_test.rs - Echo 测试机器人
收到消息后自动原样回复,适合测试基本功能。
```bash
cargo run --example echo_test
```
### 3. test_files.rs - 文件/图片下载测试
演示如何下载和解密企业微信发送的图片和文件,支持:
- 下载加密图片并保存
- 下载加密文件并保存
- 自动处理未加密的文件(如截屏图片)
```bash
cargo run --example test_files
```
### 4. sender_info.rs - 消息发送人信息示例
演示如何获取消息发送人的详细信息,包括:
- 发送人 userid
- 发送人 username(如果可用)
- 发送人 gender(如果可用)
```bash
cargo run --example sender_info
```
### 5. upload_media.rs - 上传临时素材示例
演示如何通过 WebSocket 分片上传临时素材到企业微信,支持:
- 图片(image):≤10MB, JPG/PNG 格式
- 语音(voice):≤2MB, AMR 格式
- 视频(video):≤10MB, MP4 格式
- 文件(file):≤10MB
```bash
# 上传文件
cargo run --example upload_media <文件路径>
# 示例:上传图片
cargo run --example upload_media test.jpg
```
上传成功后返回 `media_id`,可用于发送消息时引用该素材。
## 📖 API 文档
### `WSClient`
核心客户端类,提供连接管理、消息收发等功能。
#### 构造函数
```rust
let client = WSClient::new(WSClientOptions::new(bot_id, secret));
```
#### 配置选项
`WSClientOptions` 完整配置:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `bot_id` | `String` | ✅ | — | 机器人 ID(企业微信后台获取) |
| `secret` | `String` | ✅ | — | 机器人 Secret(企业微信后台获取) |
| `reconnect_interval` | `u64` | — | `1000` | 重连基础延迟(毫秒),实际延迟按指数退避递增 |
| `max_reconnect_attempts` | `i32` | — | `10` | 最大重连次数(`-1` 表示无限重连) |
| `heartbeat_interval` | `u64` | — | `30000` | 心跳间隔(毫秒) |
| `request_timeout` | `u64` | — | `10000` | HTTP 请求超时时间(毫秒) |
| `ws_url` | `Option<String>` | — | `None` | 自定义 WebSocket 连接地址 |
| `logger` | `Option<Box<dyn Logger>>` | — | `None` | 自定义日志实例 |
```rust
let options = WSClientOptions {
bot_id: "your-bot-id".to_string(),
secret: "your-bot-secret".to_string(),
reconnect_interval: 1000,
max_reconnect_attempts: 10,
heartbeat_interval: 30000,
request_timeout: 10000,
ws_url: None,
logger: None,
};
```
#### 方法
| 方法 | 说明 | 返回值 |
| --- | --- | --- |
| `async connect()` | 建立 WebSocket 连接,连接后自动认证 | `Result<(), SdkError>` |
| `disconnect()` | 主动断开连接 | `()` |
| `async reply(frame, body, cmd)` | 通过 WebSocket 通道发送回复消息(通用方法) | `Result<WsFrame, SdkError>` |
| `async reply_stream(frame, stream_id, content, finish, msg_item, feedback)` | 发送流式文本回复(便捷方法,支持 Markdown) | `Result<WsFrame, SdkError>` |
| `async reply_welcome(frame, body)` | 发送欢迎语回复(支持文本或模板卡片格式),需在收到事件 5s 内调用 | `Result<WsFrame, SdkError>` |
| `async reply_template_card(frame, template_card, feedback)` | 回复模板卡片消息 | `Result<WsFrame, SdkError>` |
| `async reply_stream_with_card(frame, stream_id, content, finish, ...)` | 发送流式消息 + 模板卡片组合回复 | `Result<WsFrame, SdkError>` |
| `async update_template_card(frame, template_card, userids)` | 更新模板卡片,需在收到事件 5s 内调用 | `Result<WsFrame, SdkError>` |
| `async send_message(chatid, body)` | 主动发送消息(支持 Markdown 或模板卡片),无需依赖回调帧 | `Result<WsFrame, SdkError>` |
| `async download_file(url, aes_key)` | 下载文件并使用 AES 密钥解密 | `Result<(Vec<u8>, Option<String>), SdkError>` |
| `async upload_media(media_type, file_data, filename)` | 上传临时素材(通过 WebSocket 分片上传) | `Result<serde_json::Value, SdkError>` |
#### 属性
| 方法 | 说明 | 返回类型 |
| --- | --- | --- |
| `is_connected()` | 当前 WebSocket 连接状态 | `bool` |
| `api()` | 内部 API 客户端实例(高级用途) | `Arc<WeComApiClient>` |
### 事件监听
所有事件均通过 `on_*` 方法监听:
```rust
// 连接事件
client.on_connected(|| { println!("WebSocket 已连接"); });
client.on_authenticated(|| { println!("认证成功"); });
client.on_disconnected(|reason| { println!("连接已断开:{}", reason); });
client.on_reconnecting(|attempt| { println!("正在进行第 {} 次重连...", attempt); });
client.on_error(|error| { eprintln!("发生错误:{}", error); });
// 消息事件
client.on_message(|frame| { println!("收到消息:{:?}", frame); });
client.on_message_text(|frame| { println!("收到文本消息"); });
client.on_message_image(|frame| { println!("收到图片消息"); });
client.on_message_mixed(|frame| { println!("收到图文混排消息"); });
client.on_message_voice(|frame| { println!("收到语音消息"); });
client.on_message_file(|frame| { println!("收到文件消息"); });
// 事件回调
client.on_event(|frame| { println!("收到事件回调"); });
client.on_event_enter_chat(|frame| { println!("用户进入会话"); });
client.on_event_template_card(|frame| { println!("模板卡片事件"); });
client.on_event_feedback(|frame| { println!("用户反馈事件"); });
```
### `reply_stream` 详细说明
```rust
await client.reply_stream(
&frame, // 收到的原始 WebSocket 帧(透传 req_id)
&stream_id, // 流式消息 ID(使用 generate_req_id("stream") 生成)
"回复内容", // 回复内容(支持 Markdown)
false, // 是否结束流式消息
None, // 图文混排项(仅 finish=true 时有效)
None, // 反馈信息(仅首次回复时设置)
);
```
### `reply_welcome` 详细说明
发送欢迎语回复,需在收到 `event.enter_chat` 事件 5 秒内调用。
```rust
// 文本欢迎语
use serde_json::json;
await client.reply_welcome(&frame, json!({
"msgtype": "text",
"text": {"content": "欢迎!"},
}));
// 模板卡片欢迎语
await client.reply_welcome(&frame, json!({
"msgtype": "template_card",
"template_card": {"card_type": "text_notice", "main_title": {"title": "欢迎"}},
}));
```
### `send_message` 详细说明
主动向指定会话推送消息,无需依赖收到的回调帧。
```rust
// 发送 Markdown 消息
await client.send_message("userid_or_chatid", json!({
"msgtype": "markdown",
"markdown": {"content": "这是一条**主动推送**的消息"},
}));
// 发送模板卡片消息
await client.send_message("userid_or_chatid", json!({
"msgtype": "template_card",
"template_card": {"card_type": "text_notice", "main_title": {"title": "通知"}},
}));
```
### `upload_media` 上传临时素材
通过 WebSocket 分片上传临时素材到企业微信,支持图片、语音、视频和文件。
```rust
use std::fs;
// 读取文件
let file_data = fs::read("test.pdf").unwrap();
// 上传文件(media_type: "image", "voice", "video", "file")
match client.upload_media("file", &file_data, "test.pdf").await {
Ok(result) => {
let media_id = result.get("media_id").and_then(|v| v.as_str()).unwrap();
println!("上传成功,media_id: {}", media_id);
}
Err(e) => eprintln!("上传失败:{}", e),
}
```
**文件大小限制:**
| 类型 | 大小限制 | 格式 |
| --- | --- | --- |
| image | ≤10MB | JPG, PNG |
| voice | ≤2MB | AMR (≤60s) |
| video | ≤10MB | MP4 |
| file | ≤10MB | - |
**注意:** 临时素材 `media_id` 有效期为 3 天。
### `download_file` 使用示例
```rust
// aes_key 取自消息体中的 image.aeskey 或 file.aeskey
client.on_message_image(|frame| {
let client = client.clone();
let frame = frame.clone();
tokio::spawn(async move {
let aes_key = frame
.body
.as_ref()
.and_then(|v| v.as_object())
.and_then(|b| b.get("image"))
.and_then(|v| v.as_object())
.and_then(|i| i.get("aeskey"))
.and_then(|v| v.as_str())
.map(String::from);
let url = frame
.body
.as_ref()
.and_then(|v| v.as_object())
.and_then(|b| b.get("image"))
.and_then(|v| v.as_object())
.and_then(|i| i.get("url"))
.and_then(|v| v.as_str())
.map(String::from);
if let (Some(url), aes_key) = (url, aes_key) {
let (buffer, filename) = client.download_file(&url, aes_key.as_deref()).await.unwrap();
println!("文件名:{:?}, 大小:{} bytes", filename, buffer.len());
}
});
});
```
## 📋 消息类型
SDK 支持以下消息类型(`MessageType` 枚举):
| 类型 | 值 | 说明 |
| --- | --- | --- |
| `MessageType::Text` | `"text"` | 文本消息 |
| `MessageType::Image` | `"image"` | 图片消息 |
| `MessageType::Mixed` | `"mixed"` | 图文混排消息 |
| `MessageType::Voice` | `"voice"` | 语音消息 |
| `MessageType::File` | `"file"` | 文件消息 |
SDK 支持以下事件类型(`EventType` 枚举):
| 类型 | 值 | 说明 |
| --- | --- | --- |
| `EventType::EnterChat` | `"enter_chat"` | 进入会话事件 |
| `EventType::TemplateCardEvent` | `"template_card_event"` | 模板卡片事件 |
| `EventType::FeedbackEvent` | `"feedback_event"` | 用户反馈事件 |
## 🪵 自定义日志
实现 `Logger` Trait 接口即可自定义日志输出:
```rust
use wecom_aibot_rust_sdk::types::Logger;
struct MyLogger;
impl Logger for MyLogger {
fn debug(&self, message: &str) {
// 静默 debug 日志
}
fn info(&self, message: &str) {
println!("[INFO] {}", message);
}
fn warn(&self, message: &str) {
println!("[WARN] {}", message);
}
fn error(&self, message: &str) {
println!("[ERROR] {}", message);
}
}
let options = WSClientOptions {
bot_id: "your-bot-id".to_string(),
secret: "your-bot-secret".to_string(),
logger: Some(Box::new(MyLogger)),
..Default::default()
};
let client = WSClient::new(options);
```
## 📂 项目结构
```
rust_wecom_bot_rust_sdk/
├── src/
│ ├── lib.rs # 库入口文件,统一导出
│ ├── client.rs # WSClient 核心客户端
│ ├── ws.rs # WebSocket 长连接管理器
│ ├── message_handler.rs # 消息解析与事件分发
│ ├── api.rs # HTTP API 客户端(文件下载)
│ ├── crypto_utils.rs # AES-256-CBC 文件解密
│ ├── logger.rs # 默认日志实现
│ ├── utils.rs # 工具方法(generate_req_id 等)
│ └── types.rs # 类型定义(枚举、结构体、常量)
├── examples/
│ ├── basic.rs # 基础使用示例
│ ├── send_test_message.rs # 发送测试消息示例
│ ├── echo_test.rs # Echo 测试机器人
│ ├── test_files.rs # 文件/图片下载测试
│ ├── sender_info.rs # 消息发送人信息示例
│ └── upload_media.rs # 上传临时素材示例
├── Cargo.toml # 项目配置
├── README.md # 本文件
└── .env.example # 环境变量示例
```
## 🏗️ 架构说明
本 SDK 采用异步事件驱动架构,主要组件:
```
┌─────────────────────────────────────────────────────┐
│ WSClient │
│ (核心客户端,提供事件注册和消息发送接口) │
└─────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ WsConnection │ │ MessageHandler │ │ WeComApiClient │
│ Manager │ │ (消息分发) │ │ (文件下载) │
│ (连接/心跳/重连) │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ WebSocket LWS │ │ Event Types │
│ (长连接) │ │ (文本/图片/事件)│
└─────────────────┘ └─────────────────┘
```
## 📄 License
MIT