18 KiB
18 KiB
wecom-aibot-rust-sdk (Rust)
企业微信智能机器人 Rust SDK —— 基于 WebSocket 长连接通道,提供消息收发、流式回复、模板卡片、事件回调、文件下载解密等核心能力。
本项目是 Python 版本 @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 中添加依赖:
[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
⚙️ 配置
# 复制示例配置文件
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
🚀 快速开始
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 - 基础使用示例
演示基本的连接、消息监听和回复功能。
cargo run --example basic
2. echo_test.rs - Echo 测试机器人
收到消息后自动原样回复,适合测试基本功能。
cargo run --example echo_test
3. test_files.rs - 文件/图片下载测试
演示如何下载和解密企业微信发送的图片和文件,支持:
- 下载加密图片并保存
- 下载加密文件并保存
- 自动处理未加密的文件(如截屏图片)
cargo run --example test_files
4. sender_info.rs - 消息发送人信息示例
演示如何获取消息发送人的详细信息,包括:
- 发送人 userid
- 发送人 username(如果可用)
- 发送人 gender(如果可用)
cargo run --example sender_info
5. upload_media.rs - 上传临时素材示例
演示如何通过 WebSocket 分片上传临时素材到企业微信,支持:
- 图片(image):≤10MB, JPG/PNG 格式
- 语音(voice):≤2MB, AMR 格式
- 视频(video):≤10MB, MP4 格式
- 文件(file):≤10MB
# 上传文件
cargo run --example upload_media <文件路径>
# 示例:上传图片
cargo run --example upload_media test.jpg
上传成功后返回 media_id,可用于发送消息时引用该素材。
📖 API 文档
WSClient
核心客户端类,提供连接管理、消息收发等功能。
构造函数
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 |
自定义日志实例 |
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_* 方法监听:
// 连接事件
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 详细说明
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 秒内调用。
// 文本欢迎语
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 详细说明
主动向指定会话推送消息,无需依赖收到的回调帧。
// 发送 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 分片上传临时素材到企业微信,支持图片、语音、视频和文件。
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 使用示例
// 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 接口即可自定义日志输出:
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