aibot 1.0.2

WeChat iLink Bot API — full async Rust implementation
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
//! # 协议类型定义
//!
//! 所有类型严格对应 iLink Bot HTTP/JSON 协议的请求/响应结构。
//!
//! 协议端点: `https://ilinkai.weixin.qq.com`
//!
//! ## 分类
//!
//! - **Auth** —— 扫码登录相关的请求/响应
//! - **Messaging (Inbound)** —— 收消息相关(`getupdates`)
//! - **Messaging (Outbound)** —— 发消息相关(`sendmessage`)
//! - **Media** —— 媒体上传/下载相关
//! - **Config** —— 配置与状态相关
//! - **Common** —— 公共基类型

// serde 由 xbw 依赖传递引入(extern prelude 可见),不在此 crate 直接声明
use serde::{Deserialize, Serialize};

// ============================================================================
// Auth — 扫码登录
// ============================================================================

/// `GET /ilink/bot/get_bot_qrcode?bot_type=3` 的响应。
///
/// 包含二维码的字符串标识和 Base64 编码的 PNG 图片。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QrCodeResponse {
    /// 二维码唯一标识(用于轮询扫码状态)。
    pub qrcode: String,
    /// Base64 编码的二维码 PNG 图片内容。
    #[serde(rename = "qrcode_img_content")]
    pub qrcode_img_content: String,
    /// 返回码(0 表示成功)。
    #[serde(default)]
    pub ret: i64,
}

/// `GET /ilink/bot/get_qrcode_status?qrcode=xxx` 的响应。
///
/// 轮询扫码状态,当 `status == "confirmed"` 时 `bot_token` 和 `baseurl` 会被填充。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QrCodeStatusResponse {
    /// 扫码状态,取值:
    /// - `"waiting"`   — 等待扫码
    /// - `"scanned"`   — 已扫码(未确认)
    /// - `"confirmed"` — 已确认(登录成功)
    /// - `"expired"`   — 二维码已过期
    pub status: String,
    /// 认证后的 Bot Token(仅 `status == "confirmed"` 时有值)。
    #[serde(default)]
    pub bot_token: String,
    /// 服务端返回的 base URL(仅 `status == "confirmed"` 时可能有值)。
    #[serde(default)]
    pub baseurl: String,
    /// 返回码。
    #[serde(default)]
    pub ret: i64,
}

// ============================================================================
// Messaging — Inbound(收消息 / getupdates)
// ============================================================================

/// `POST /ilink/bot/getupdates` 的请求体。
#[derive(Debug, Clone, Serialize)]
pub struct GetUpdatesRequest {
    /// 游标(首次传空字符串,后续用上次返回的 `get_updates_buf`)。
    #[serde(rename = "get_updates_buf")]
    pub get_updates_buf: String,
    /// 公共基础信息。
    #[serde(rename = "base_info")]
    pub base_info: BaseInfo,
}

/// `POST /ilink/bot/getupdates` 的响应。
///
/// 服务端会维持长连接等待消息到达(最长约 35 秒)。
#[derive(Debug, Clone, Deserialize)]
pub struct GetUpdatesResponse {
    /// 返回码。
    #[serde(default)]
    pub ret: i64,
    /// 本次拉取到的消息列表(可能为空)。
    #[serde(default)]
    pub msgs: Vec<WeChatMessage>,
    /// 下一次请求用的游标。
    #[serde(rename = "get_updates_buf", default)]
    pub get_updates_buf: String,
    /// 服务端长轮询超时时间(毫秒)。
    #[serde(rename = "longpolling_timeout_ms", default)]
    pub longpolling_timeout_ms: i64,
}

/// 单条入站消息。
#[derive(Debug, Clone, Deserialize)]
pub struct WeChatMessage {
    /// 消息发送方(微信用户)。
    #[serde(rename = "from_user_id", default)]
    pub from_user_id: String,
    /// 消息接收方(Bot 自身)。
    #[serde(rename = "to_user_id", default)]
    pub to_user_id: String,
    /// 上下文令牌(回复消息时需传回)。
    #[serde(rename = "context_token", default)]
    pub context_token: String,
    /// 消息内容数据。
    #[serde(default)]
    pub data: MessageData,
    /// 消息 ID。
    #[serde(rename = "msg_id", default)]
    pub msg_id: String,
}

/// 消息内容容器,包含零条或多条内容项。
#[derive(Debug, Clone, Default, Deserialize)]
pub struct MessageData {
    /// 内容项列表(一条消息可以包含多条内容,例如图文混排)。
    #[serde(rename = "item_list", default)]
    pub item_list: Vec<ContentItem>,
}

/// 入站消息的内容项。
///
/// 通过 `type` 字段做标签反序列化:
/// - `"1"` — 文本
/// - `"2"` — 图片
/// - `"3"` — 文件
/// - `"4"` — 语音
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type")]
pub enum ContentItem {
    /// 文本消息。
    #[serde(rename = "1")]
    Text {
        /// 文本内容。
        #[serde(rename = "text_item")]
        text_item: TextItem,
    },
    /// 图片消息(CDN 链接 + AES 加密)。
    #[serde(rename = "2")]
    Image {
        /// 图片信息(含 CDN 地址、AES 密钥等)。
        #[serde(rename = "image_item")]
        image_item: ImageItem,
    },
    /// 文件消息(CDN 链接 + AES 加密)。
    #[serde(rename = "3")]
    File {
        /// 文件信息。
        #[serde(rename = "file_item")]
        file_item: FileItem,
    },
    /// 语音消息(CDN 链接 + AES 加密,含 ASR 转写)。
    #[serde(rename = "4")]
    Voice {
        /// 语音信息。
        #[serde(rename = "voice_item")]
        voice_item: VoiceItem,
    },
}

// ── 入站消息的子类型 ──────────────────────────

/// 文本内容。
#[derive(Debug, Clone, Deserialize)]
pub struct TextItem {
    /// 文本正文。
    pub text: String,
}

/// 图片内容(CDN 存储,AES-128-ECB 加密)。
#[derive(Debug, Clone, Deserialize)]
pub struct ImageItem {
    /// 图片 CDN 下载地址。
    #[serde(rename = "cdn_image_url", default)]
    pub cdn_image_url: String,
    /// AES 密钥(多种编码格式,见 `crypto::parse_aes_key`)。
    #[serde(rename = "aes_key", default)]
    pub aes_key: String,
    /// 图片 MD5。
    #[serde(default)]
    pub md5: String,
    /// 图片文件大小(字节)。
    #[serde(rename = "file_size", default)]
    pub file_size: i64,
}

/// 文件内容(CDN 存储,AES-128-ECB 加密)。
#[derive(Debug, Clone, Deserialize)]
pub struct FileItem {
    /// 文件 CDN 下载地址。
    #[serde(rename = "cdn_file_url", default)]
    pub cdn_file_url: String,
    /// 文件名。
    #[serde(rename = "file_name", default)]
    pub file_name: String,
    /// AES 密钥。
    #[serde(rename = "aes_key", default)]
    pub aes_key: String,
    /// 加密查询参数(下载时需拼接在 CDN URL 后)。
    #[serde(rename = "encrypt_query_param", default)]
    pub encrypt_query_param: String,
    /// 文件 MD5。
    #[serde(default)]
    pub md5: String,
    /// 文件大小(字节)。
    #[serde(rename = "file_size", default)]
    pub file_size: i64,
}

/// 语音内容(CDN 存储,AES-128-ECB 加密,含自动语音识别结果)。
#[derive(Debug, Clone, Deserialize)]
pub struct VoiceItem {
    /// 语音 CDN 下载地址。
    #[serde(rename = "cdn_voice_url", default)]
    pub cdn_voice_url: String,
    /// AES 密钥。
    #[serde(rename = "aes_key", default)]
    pub aes_key: String,
    /// ASR 语音转文字结果。
    #[serde(rename = "asr_text", default)]
    pub asr_text: String,
    /// 语音时长(秒)。
    #[serde(rename = "voice_sec", default)]
    pub voice_sec: i64,
}

// ============================================================================
// Messaging — Outbound(发消息 / sendmessage)
// ============================================================================

/// `POST /ilink/bot/sendmessage` 的请求体。
#[derive(Debug, Clone, Serialize)]
pub struct SendMessageRequest {
    /// 待发送的消息本体。
    pub msg: OutboundMessage,
    /// 公共基础信息。
    #[serde(rename = "base_info")]
    pub base_info: BaseInfo,
}

/// 出站消息容器。
#[derive(Debug, Clone, Serialize)]
pub struct OutboundMessage {
    /// 发送方(Bot 自身,可为空)。
    #[serde(rename = "from_user_id")]
    pub from_user_id: String,
    /// 接收方(微信用户 ID)。
    #[serde(rename = "to_user_id")]
    pub to_user_id: String,
    /// 客户端消息 ID(用于去重,建议 UUID)。
    #[serde(rename = "client_id")]
    pub client_id: String,
    /// 消息类型(2 = 文本, 3 = 图片, 4 = 文件, 5 = 语音等)。
    #[serde(rename = "message_type")]
    pub message_type: i64,
    /// 消息状态(一般设为 2)。
    #[serde(rename = "message_state")]
    pub message_state: i64,
    /// 上下文令牌(回复消息时从入站消息获取)。
    #[serde(rename = "context_token")]
    pub context_token: String,
    /// 内容项列表。
    #[serde(rename = "item_list")]
    pub item_list: Vec<OutboundContentItem>,
}

/// 出站消息的内容项。
///
/// 通过 `type` 字段做标签:
/// - `"1"` — 文本
/// - `"2"` — 图片(需先上传媒体)
/// - `"3"` — 文件(需先上传媒体)
/// - `"4"` — 语音(需先上传媒体)
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type")]
pub enum OutboundContentItem {
    /// 出站文本内容。
    #[serde(rename = "1")]
    Text {
        #[serde(rename = "text_item")]
        text_item: OutboundTextItem,
    },
    /// 出站图片(需先调用 `upload_media` 获取媒体参数)。
    #[serde(rename = "2")]
    Image {
        #[serde(rename = "image_item")]
        image_item: OutboundMediaItem,
    },
    /// 出站文件(需先调用 `upload_media` 获取媒体参数)。
    #[serde(rename = "3")]
    File {
        #[serde(rename = "file_item")]
        file_item: OutboundMediaItem,
    },
    /// 出站语音(需先调用 `upload_media` 获取媒体参数)。
    #[serde(rename = "4")]
    Voice {
        #[serde(rename = "voice_item")]
        voice_item: OutboundMediaItem,
    },
}

/// 出站文本内容。
#[derive(Debug, Clone, Serialize)]
pub struct OutboundTextItem {
    /// 文本正文。
    pub text: String,
}

/// 出站媒体内容(图片/文件/语音),由 `upload_media` 准备。
#[derive(Debug, Clone, Serialize)]
pub struct OutboundMediaItem {
    /// 加密后的文件 MD5。
    #[serde(rename = "md5")]
    pub md5: String,
    /// AES 密钥(Base64 编码,由 `upload_media` 生成)。
    #[serde(rename = "aeskey")]
    pub aeskey: String,
    /// 加密后的文件大小(字节)。
    #[serde(rename = "file_size")]
    pub file_size: i64,
    /// CDN 上传参数(由 `upload_media` 返回)。
    #[serde(rename = "encrypt_query_param")]
    pub encrypt_query_param: String,
}

/// 上传媒体后的结果参数(供构建 `OutboundMediaItem` 使用)。
#[derive(Debug, Clone)]
pub struct UploadedMedia {
    /// CDN 上传参数。
    pub encrypt_query_param: String,
    /// AES 密钥(Base64 编码)。
    pub aes_key_b64: String,
    /// 加密后的文件大小。
    pub filesize: i64,
    /// 加密后的文件 MD5。
    pub md5: String,
}

// ============================================================================
// Media — 媒体上传/下载
// ============================================================================

/// `POST /ilink/bot/getuploadurl` 的请求体。
#[derive(Debug, Clone, Serialize)]
pub struct GetUploadUrlRequest {
    /// 文件标识(16 位随机十六进制字符串)。
    pub filekey: String,
    /// 媒体类型(3=图片, 4=文件, 5=语音)。
    #[serde(rename = "media_type")]
    pub media_type: i64,
    /// 接收方用户 ID。
    #[serde(rename = "to_user_id")]
    pub to_user_id: String,
    /// 原始文件大小(字节)。
    pub rawsize: i64,
    /// 原始文件 MD5。
    pub rawfilemd5: String,
    /// 加密后的文件大小。
    pub filesize: i64,
    /// AES 密钥(十六进制字符串)。
    pub aeskey: String,
    /// 是否需要缩略图(图片类型通常为 false)。
    #[serde(rename = "no_need_thumb")]
    pub no_need_thumb: bool,
    /// 公共基础信息。
    #[serde(rename = "base_info")]
    pub base_info: BaseInfo,
}

/// `POST /ilink/bot/getuploadurl` 的响应。
#[derive(Debug, Clone, Deserialize)]
pub struct GetUploadUrlResponse {
    /// 文件上传完整 URL。
    #[serde(rename = "upload_full_url", default)]
    pub upload_full_url: String,
    /// 上传参数(后续发送消息和下载时需要)。
    #[serde(rename = "upload_param", default)]
    pub upload_param: String,
    /// 返回码。
    #[serde(default)]
    pub ret: i64,
    /// 错误信息。
    #[serde(default)]
    pub err_msg: String,
}

// ============================================================================
// Typing — 正在输入状态
// ============================================================================

/// `POST /ilink/bot/sendtyping` 的请求体。
#[derive(Debug, Clone, Serialize)]
pub struct SendTypingRequest {
    /// 目标用户 ID。
    #[serde(rename = "ilink_user_id")]
    pub ilink_user_id: String,
    /// 打字状态票据(通过 `get_config` 获取)。
    #[serde(rename = "typing_ticket")]
    pub typing_ticket: String,
    /// 状态值(1 = 正在输入, 0 = 停止输入)。
    pub status: i64,
    /// 公共基础信息。
    #[serde(rename = "base_info")]
    pub base_info: BaseInfo,
}

// ============================================================================
// Config — 配置
// ============================================================================

/// `POST /ilink/bot/ilink_bot_getconfig` 的响应。
#[derive(Debug, Clone, Deserialize)]
pub struct GetConfigResponse {
    /// 返回码。
    #[serde(default)]
    pub ret: i64,
    /// 打字状态票据(用于 `send_typing`)。
    #[serde(rename = "typing_ticket", default)]
    pub typing_ticket: String,
    /// 其他配置项(按需解析)。
    #[serde(flatten)]
    pub extra: std::collections::HashMap<String, xbw::all::JsonValue>,
}

// ============================================================================
// Common — 公共基类型
// ============================================================================

/// API 请求中的公共基础信息。
///
/// 大多数 iLink 请求体都需要携带此字段。
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BaseInfo {
    /// 应用 ID(目前可留空)。
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub appid: String,
    /// 辅助信息(扩展字段,目前可留空)。
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub sub_info: String,
}