# wechat-ilink
[](https://crates.io/crates/wechat-ilink)
[](https://docs.rs/wechat-ilink)
[](LICENSE)
[](https://www.rust-lang.org/)
非官方 WeChat iLink 协议客户端:stream-first API · QR 登录 · 事件驱动轮询 · 自动从入站消息观测并刷新 context · 类型化速率限制(ret=-2)与自动退避 · context TTL 过期交互事件 · 显式 context 发送文本/媒体/typing · CDN 上传/下载。全无状态 —— 所有 credentials / context / cursor 由应用管理。
English: [README.en.md](README.en.md)
## 状态
`wechat-ilink` 仍是实验性 `0.x` crate。
- 非官方客户端,WeChat iLink 协议可能随时变化。
- API 还会根据真实使用继续收敛。
- SDK 不持久化 credentials、用户 `context_token`,也不持久化业务 cursor。
- 凭证、用户 context、cursor、提醒策略、重试策略都由调用方负责。
## 安装
```toml
[dependencies]
wechat-ilink = "0.4"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs", "time", "sync"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
```
Rust 模块名:
```rust
use wechat_ilink::WechatIlinkClient;
```
## Quick start
```rust,no_run
use std::sync::Arc;
use wechat_ilink::{LoginQrEvent, WechatEvent, WechatIlinkClient};
#[tokio::main]
async fn main() -> wechat_ilink::Result<()> {
let client = Arc::new(
WechatIlinkClient::builder()
.bot_agent("MyBot/0.1")
.ilink_app_id("bot")
// 默认启用。关闭后,发送文本不会做 WeChat Markdown 兼容过滤。
.markdown_filter(true)
// ret=-2 默认等待 90s,最多重试 5 次;接近 5 分钟 6 条时发事件。
.rate_limit_retry_after(std::time::Duration::from_secs(90))
.rate_limit_max_retries(5)
.rate_limit_interaction_threshold(6)
// context 默认 24h TTL,到期前 30 分钟发 UserInteractionRequested 事件。
.context_ttl(std::time::Duration::from_secs(24 * 60 * 60))
.context_expiry_remind_before(std::time::Duration::from_secs(30 * 60))
.build(),
);
// 凭证由应用层加载;SDK 不读写 credential 文件。
if let Some(credentials) = my_load_credentials().await {
client.set_credentials(credentials).await;
} else {
let mut login = client.login_qr();
while let Some(event) = login.next().await {
match event? {
LoginQrEvent::QrCode { content } => eprintln!("scan QR: {content}"),
LoginQrEvent::StatusChanged { status } => eprintln!("login status: {status}"),
LoginQrEvent::NeedVerifyCode { responder, .. } => {
// 应用可以读取验证码后 responder.send(code)。
let _ = responder.cancel();
}
LoginQrEvent::Confirmed { credentials } => {
my_save_credentials(&credentials).await;
break;
}
}
}
}
let cursor = my_load_cursor().await;
let mut events = client.events_from_cursor(cursor);
while let Some(event) = events.next().await {
match event? {
WechatEvent::ContextObserved(context) => {
// 自动从入站消息观测/刷新 context;保存后用于主动 send_*_with_context。
let _ = context;
}
WechatEvent::CursorAdvanced { account_key, cursor } => {
// 保存 cursor,下次启动传给 events_from_cursor。
let _ = (account_key, cursor);
}
WechatEvent::Message(message) => {
println!("{}: {}", message.user_id, message.text);
}
WechatEvent::AuthSessionExpired { account_key } => {
eprintln!("auth expired: {account_key}");
break;
}
WechatEvent::UserInteractionRequested { account_key, user_id, reason } => {
// SDK 只通知;应用决定是否提醒用户在微信里发一条消息来刷新窗口。
eprintln!("user interaction suggested: {account_key} {user_id:?} {reason:?}");
}
}
}
Ok(())
}
async fn my_load_credentials() -> Option<wechat_ilink::Credentials> { None }
async fn my_save_credentials(_: &wechat_ilink::Credentials) {}
async fn my_load_cursor() -> Option<String> { None }
```
完整的外部存储与保活提醒示例见 [`examples/external_store_keepalive.rs`](examples/external_store_keepalive.rs)。
多登录账号示例见 [`examples/multi_account_context_store.rs`](examples/multi_account_context_store.rs)。
## 这个 crate 做什么
`wechat-ilink` 是底层异步协议客户端,负责:
- 以 stream 形式暴露 QR 登录状态、轮询事件和错误;
- WeChat iLink 事件驱动轮询;
- wire message 到 `IncomingMessage` 的解析;
- 自动从入站消息观测并刷新 `WechatContext`;
- 显式 context 发送文本、媒体、typing;
- 入站媒体下载、CDN 上传辅助;
- 协议错误映射。
它不是 bot framework,不负责你的应用状态。
## 核心模型
WeChat 入站消息可能带 `context_token`。SDK 会把它暴露为:
```rust
IncomingMessage.context: Option<WechatContext>
```
并通过事件发出:
```rust
WechatEvent::ContextObserved(WechatContext)
```
调用方应该自己保存:
- 每个用户/账号的 `WechatContext`,用于之后主动发送;
- `WechatEvent::CursorAdvanced` 中的 cursor,用于重启后继续轮询;
- `WechatEvent::UserInteractionRequested` 用于通知应用“需要用户在微信里发条消息”:包括 context 接近过期、每账号 5 分钟内主动发送达到 6 条;用户在微信端发来消息会重置这些窗口;
- 自己的提醒记录,例如每几个小时提醒用户发测试消息刷新 context。
## 发送消息
回复入站消息:
```rust,no_run
# async fn example(bot: &wechat_ilink::WechatIlinkClient, msg: &wechat_ilink::IncomingMessage) -> wechat_ilink::Result<()> {
bot.reply(msg, "收到").await?;
# Ok(())
# }
```
主动发送必须提供调用方保存的 `WechatContext`:
```rust,no_run
# async fn example(bot: &wechat_ilink::WechatIlinkClient, context: &wechat_ilink::WechatContext) -> wechat_ilink::Result<()> {
bot.send_text_with_context(context, "你好").await?;
bot.send_typing_with_context(context).await?;
# Ok(())
# }
```
媒体发送同样需要显式 context:
```rust,no_run
# async fn example(bot: &wechat_ilink::WechatIlinkClient, context: &wechat_ilink::WechatContext) -> wechat_ilink::Result<()> {
bot.send_media_with_context(
context,
wechat_ilink::SendContent::File {
data: b"hello".to_vec(),
file_name: "hello.txt".into(),
},
)
.await?;
# Ok(())
# }
```
## 事件
推荐通过 `events_from_cursor` 集成:
- `ContextObserved(WechatContext)`:从入站消息观察到新的 context token。
- `Message(IncomingMessage)`:解析后的入站消息。
- `CursorAdvanced { account_key, cursor }`:轮询 cursor 前进,调用方应保存。
- `AuthSessionExpired { account_key }`:登录态过期,需要重新登录。
- `UserInteractionRequested { account_key, user_id, reason }`:SDK 建议应用请求用户在微信里发条消息。`reason` 可能是 context 接近过期,或该账号主动发送在窗口内达到阈值(默认 5 分钟内 6 条)。SDK 只通知,调用方决定是否提示用户、发什么、走哪个通道。
同一个 update batch 中,SDK 会先发 `ContextObserved` / `Message`,再发 `CursorAdvanced`。因此调用方可以先保存消息派生状态,再保存 cursor。用户在微信终端发来消息时,SDK 会重置该账号的主动发送计数和 rate-limit 退避。
## 外部存储 context 与保活提醒示例
完整示例不放在 README 里:
- [`examples/external_store_keepalive.rs`](examples/external_store_keepalive.rs):外部存储 + 每 4 小时保活提醒。
- [`examples/multi_account_context_store.rs`](examples/multi_account_context_store.rs):多个登录账号,每个账号独立 credentials/cursor/context store。
这个示例演示:
1. 在 SDK 外部用 JSON 文件保存 `WechatContext`;
2. 在 SDK 外部保存 polling cursor;
3. 每 4 小时主动提醒用户回复“测试”;
4. 用户回复后通过 `ContextObserved` 刷新 context token,并重置 SDK 的主动发送窗口。
生产环境建议把示例里的 JSON 文件替换成 SQLite、Postgres、Redis 或你自己的存储。
## 本 crate 不负责什么
`wechat-ilink` 有意不提供:
- 用户数据库;
- 收件人管理;
- credentials 持久化;
- `context_token` 持久化;
- cursor 持久化;
- 保活提醒策略;
- 大量业务消息的持久队列或合并策略;
- 应用层重试/抑制策略;
- bot 工作流或会话编排。
这些属于应用层。
## 安全与隐私
- 不要记录 `context_token` 到日志。
- 不要把凭证文件提交到仓库。
- `WechatContext` 的 `Debug` 会隐藏 token,但序列化会包含原始 token。
- 如果把 context 存到数据库,请按凭证级别保护。
## 错误处理
常见错误:
- `WechatIlinkError::NoContext(user_id)`:消息没有 context,无法用 `reply` 回复。
- `WechatIlinkError::Api { .. }`:iLink API 返回错误。
- `WechatIlinkError::Auth(..)`:未登录或鉴权失败。
- `WechatIlinkError::Transport(..)`:网络或 HTTP 客户端错误。
## 版本策略
在 `0.x` 阶段,minor 版本可能包含 API 变化。生产部署建议锁定精确版本。
## 许可证
MIT