新增: df-im-gateway 网关骨架
This commit is contained in:
@@ -0,0 +1,546 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user