wechat-ilink-client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +313 -0
- package/README.zh_CN.md +313 -0
- package/dist/index.d.mts +593 -0
- package/dist/index.mjs +1065 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# wechat-ilink-client
|
|
2
|
+
|
|
3
|
+
[简体中文](./README.zh_CN.md)
|
|
4
|
+
|
|
5
|
+
Standalone TypeScript client for the WeChat iLink bot protocol, reverse-engineered from [`@tencent-weixin/openclaw-weixin`](https://www.npmjs.com/package/@tencent-weixin/openclaw-weixin).
|
|
6
|
+
|
|
7
|
+
No dependency on the OpenClaw framework. Zero runtime dependencies. A pure, stateless library you can use to build your own WeChat bots.
|
|
8
|
+
|
|
9
|
+
## Design Principles
|
|
10
|
+
|
|
11
|
+
- **Stateless** — the library does NOT read or write files. Credential storage, sync buf persistence, and QR code rendering are entirely the caller's responsibility.
|
|
12
|
+
- **Zero runtime dependencies** — only Node.js built-ins.
|
|
13
|
+
- **Minimal API surface** — a single `WeChatClient` class for most use cases, with lower-level primitives exported for advanced usage.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- QR code login (returns the URL; caller handles rendering)
|
|
18
|
+
- Long-poll message receiving (`getUpdates`) with opt-in cursor persistence
|
|
19
|
+
- Send text, image, video, and file messages
|
|
20
|
+
- CDN media upload/download with AES-128-ECB encryption
|
|
21
|
+
- Typing indicator support
|
|
22
|
+
- EventEmitter-based API
|
|
23
|
+
- Full TypeScript types for the entire protocol
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- Node.js >= 20
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm install
|
|
33
|
+
pnpm build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { WeChatClient, MessageType } from "wechat-ilink-client";
|
|
40
|
+
|
|
41
|
+
const client = new WeChatClient();
|
|
42
|
+
|
|
43
|
+
// Step 1: Login via QR code
|
|
44
|
+
const result = await client.login({
|
|
45
|
+
onQRCode(url) {
|
|
46
|
+
// You handle QR rendering — print it, show it in a GUI, etc.
|
|
47
|
+
console.log("Scan this QR code:", url);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
if (!result.connected) {
|
|
51
|
+
console.error("Login failed:", result.message);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// You handle persistence — save these yourself:
|
|
55
|
+
// result.botToken, result.accountId, result.baseUrl
|
|
56
|
+
|
|
57
|
+
// Step 2: Handle incoming messages
|
|
58
|
+
client.on("message", async (msg) => {
|
|
59
|
+
if (msg.message_type !== MessageType.USER) return;
|
|
60
|
+
|
|
61
|
+
const text = WeChatClient.extractText(msg);
|
|
62
|
+
const from = msg.from_user_id!;
|
|
63
|
+
|
|
64
|
+
await client.sendText(from, `Echo: ${text}`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Step 3: Start the long-poll loop (blocks until stop() is called)
|
|
68
|
+
await client.start();
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
On subsequent runs, construct the client directly from saved credentials:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const client = new WeChatClient({
|
|
75
|
+
accountId: savedAccountId,
|
|
76
|
+
token: savedToken,
|
|
77
|
+
baseUrl: savedBaseUrl,
|
|
78
|
+
});
|
|
79
|
+
// Ready — go straight to .on("message", ...) and .start()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Persisting the Long-Poll Cursor
|
|
83
|
+
|
|
84
|
+
To resume from where you left off across restarts, pass `loadSyncBuf` / `saveSyncBuf` callbacks to `start()`:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
await client.start({
|
|
88
|
+
loadSyncBuf: () => fs.readFileSync("sync.json", "utf-8"),
|
|
89
|
+
saveSyncBuf: (buf) => fs.writeFileSync("sync.json", buf),
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Examples
|
|
94
|
+
|
|
95
|
+
### Echo Bot
|
|
96
|
+
|
|
97
|
+
A complete working bot with file-based persistence and QR rendering.
|
|
98
|
+
|
|
99
|
+
First, install `qrcode-terminal` to render QR codes inline in your terminal:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pnpm add qrcode-terminal
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Then run:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
pnpm tsx examples/echo-bot.ts # first run — shows QR code
|
|
109
|
+
pnpm tsx examples/echo-bot.ts # subsequent — resumes session
|
|
110
|
+
pnpm tsx examples/echo-bot.ts --fresh # force re-login
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Or via the script:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
pnpm echo-bot
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
> Without `qrcode-terminal` the example still works — it prints the QR code URL instead.
|
|
120
|
+
|
|
121
|
+
The example stores credentials at `~/.wechat-echo-bot/` — this is the example's choice, not the library's.
|
|
122
|
+
|
|
123
|
+
## API Reference
|
|
124
|
+
|
|
125
|
+
### `WeChatClient`
|
|
126
|
+
|
|
127
|
+
The high-level client. Extends `EventEmitter`.
|
|
128
|
+
|
|
129
|
+
#### Constructor
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
new WeChatClient(opts?: {
|
|
133
|
+
baseUrl?: string; // default: "https://ilinkai.weixin.qq.com"
|
|
134
|
+
cdnBaseUrl?: string; // default: "https://novac2c.cdn.weixin.qq.com/c2c"
|
|
135
|
+
token?: string; // bearer token
|
|
136
|
+
accountId?: string; // account ID
|
|
137
|
+
channelVersion?: string;
|
|
138
|
+
routeTag?: string;
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### Methods
|
|
143
|
+
|
|
144
|
+
| Method | Description |
|
|
145
|
+
|--------|-------------|
|
|
146
|
+
| `login(opts?)` | Run QR code login. Sets token/accountId in memory. Does NOT persist. |
|
|
147
|
+
| `start(opts?)` | Start the long-poll monitor. Emits `"message"` events. Blocks until `stop()`. |
|
|
148
|
+
| `stop()` | Stop the long-poll loop. |
|
|
149
|
+
| `sendText(to, text, contextToken?)` | Send a text message. Context token is auto-resolved from cache. |
|
|
150
|
+
| `sendMedia(to, filePath, caption?, contextToken?)` | Upload and send a file (image/video/file routed by MIME type). |
|
|
151
|
+
| `sendUploadedImage(to, uploaded, caption?, contextToken?)` | Send a previously uploaded image. |
|
|
152
|
+
| `sendUploadedVideo(to, uploaded, caption?, contextToken?)` | Send a previously uploaded video. |
|
|
153
|
+
| `sendUploadedFile(to, fileName, uploaded, caption?, contextToken?)` | Send a previously uploaded file. |
|
|
154
|
+
| `sendTyping(userId, typingTicket, status?)` | Send/cancel typing indicator. |
|
|
155
|
+
| `getTypingTicket(userId, contextToken?)` | Fetch the typing ticket for a user. |
|
|
156
|
+
| `uploadImage(filePath, toUserId)` | Upload an image to CDN. |
|
|
157
|
+
| `uploadVideo(filePath, toUserId)` | Upload a video to CDN. |
|
|
158
|
+
| `uploadFile(filePath, toUserId)` | Upload a file to CDN. |
|
|
159
|
+
| `downloadMedia(item)` | Download and decrypt a media `MessageItem`. |
|
|
160
|
+
| `getContextToken(userId)` | Get the cached context token for a user. |
|
|
161
|
+
| `getAccountId()` | Get the current account ID. |
|
|
162
|
+
|
|
163
|
+
#### `start()` Options
|
|
164
|
+
|
|
165
|
+
| Option | Type | Description |
|
|
166
|
+
|--------|------|-------------|
|
|
167
|
+
| `longPollTimeoutMs` | `number` | Long-poll timeout in ms. Server may override. |
|
|
168
|
+
| `signal` | `AbortSignal` | For external cancellation. |
|
|
169
|
+
| `loadSyncBuf` | `() => string \| undefined \| Promise<...>` | Called once at startup to load a persisted cursor. |
|
|
170
|
+
| `saveSyncBuf` | `(buf: string) => void \| Promise<void>` | Called after each poll with the new cursor. |
|
|
171
|
+
|
|
172
|
+
#### `login()` Options
|
|
173
|
+
|
|
174
|
+
| Option | Type | Description |
|
|
175
|
+
|--------|------|-------------|
|
|
176
|
+
| `timeoutMs` | `number` | Max wait for QR scan (default: 480_000). |
|
|
177
|
+
| `botType` | `string` | bot_type parameter (default: "3"). |
|
|
178
|
+
| `maxRefreshes` | `number` | Max QR refreshes on expiry (default: 3). |
|
|
179
|
+
| `onQRCode` | `(url: string) => void` | Called with the QR code URL. **Caller renders.** |
|
|
180
|
+
| `onStatus` | `(status) => void` | Called on status changes (wait/scaned/expired/confirmed). |
|
|
181
|
+
| `signal` | `AbortSignal` | For cancellation. |
|
|
182
|
+
|
|
183
|
+
#### Events
|
|
184
|
+
|
|
185
|
+
| Event | Payload | Description |
|
|
186
|
+
|-------|---------|-------------|
|
|
187
|
+
| `message` | `WeixinMessage` | Inbound message from a user. |
|
|
188
|
+
| `error` | `Error` | Non-fatal poll/API error. |
|
|
189
|
+
| `sessionExpired` | _(none)_ | Server returned errcode -14. Bot pauses automatically. |
|
|
190
|
+
| `poll` | `GetUpdatesResp` | Raw response from each getUpdates call. |
|
|
191
|
+
|
|
192
|
+
#### Static Methods
|
|
193
|
+
|
|
194
|
+
| Method | Description |
|
|
195
|
+
|--------|-------------|
|
|
196
|
+
| `WeChatClient.extractText(msg)` | Extract text body from a `WeixinMessage`. |
|
|
197
|
+
| `WeChatClient.isMediaItem(item)` | Check if a `MessageItem` is image/voice/file/video. |
|
|
198
|
+
|
|
199
|
+
### `ApiClient`
|
|
200
|
+
|
|
201
|
+
Low-level HTTP client. Used internally by `WeChatClient`, also exported for direct use.
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
const api = new ApiClient({ baseUrl, token });
|
|
205
|
+
|
|
206
|
+
await api.getUpdates(syncBuf, timeoutMs);
|
|
207
|
+
await api.sendMessage(req);
|
|
208
|
+
await api.getUploadUrl(req);
|
|
209
|
+
await api.getConfig(userId, contextToken);
|
|
210
|
+
await api.sendTyping(req);
|
|
211
|
+
await api.getQRCode(botType);
|
|
212
|
+
await api.pollQRCodeStatus(qrcode);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### `normalizeAccountId(raw)`
|
|
216
|
+
|
|
217
|
+
Converts a raw account ID (e.g. `"hex@im.bot"`) to a safe key (`"hex-im-bot"`).
|
|
218
|
+
|
|
219
|
+
## Protocol Overview
|
|
220
|
+
|
|
221
|
+
The WeChat iLink bot backend lives at `https://ilinkai.weixin.qq.com`. All API endpoints use `POST` with JSON bodies (except QR login which uses `GET`).
|
|
222
|
+
|
|
223
|
+
### Authentication
|
|
224
|
+
|
|
225
|
+
Every request includes these headers:
|
|
226
|
+
|
|
227
|
+
| Header | Value |
|
|
228
|
+
|--------|-------|
|
|
229
|
+
| `Content-Type` | `application/json` |
|
|
230
|
+
| `AuthorizationType` | `ilink_bot_token` |
|
|
231
|
+
| `Authorization` | `Bearer <token>` |
|
|
232
|
+
| `X-WECHAT-UIN` | Base64 of a random uint32 |
|
|
233
|
+
|
|
234
|
+
The token is obtained through QR code login:
|
|
235
|
+
|
|
236
|
+
1. `GET ilink/bot/get_bot_qrcode?bot_type=3` — returns a QR code URL
|
|
237
|
+
2. `GET ilink/bot/get_qrcode_status?qrcode=...` — long-poll until `"confirmed"`
|
|
238
|
+
3. Response includes `bot_token`, `ilink_bot_id`, `baseurl`
|
|
239
|
+
|
|
240
|
+
### Endpoints
|
|
241
|
+
|
|
242
|
+
| Endpoint | Description |
|
|
243
|
+
|----------|-------------|
|
|
244
|
+
| `ilink/bot/getupdates` | Long-poll for inbound messages (cursor: `get_updates_buf`) |
|
|
245
|
+
| `ilink/bot/sendmessage` | Send a message (text/image/video/file) |
|
|
246
|
+
| `ilink/bot/getuploadurl` | Get CDN pre-signed upload parameters |
|
|
247
|
+
| `ilink/bot/getconfig` | Get account config (typing ticket) |
|
|
248
|
+
| `ilink/bot/sendtyping` | Send/cancel typing indicator |
|
|
249
|
+
|
|
250
|
+
### Message Structure
|
|
251
|
+
|
|
252
|
+
Messages use a `WeixinMessage` envelope containing an `item_list` of typed items:
|
|
253
|
+
|
|
254
|
+
| Type | Value | Item field |
|
|
255
|
+
|------|-------|------------|
|
|
256
|
+
| TEXT | 1 | `text_item.text` |
|
|
257
|
+
| IMAGE | 2 | `image_item` (CDN media ref + AES key) |
|
|
258
|
+
| VOICE | 3 | `voice_item` (CDN media ref, optional STT text) |
|
|
259
|
+
| FILE | 4 | `file_item` (CDN media ref + filename) |
|
|
260
|
+
| VIDEO | 5 | `video_item` (CDN media ref) |
|
|
261
|
+
|
|
262
|
+
The `context_token` field on inbound messages **must** be echoed back in all replies.
|
|
263
|
+
|
|
264
|
+
### CDN Media
|
|
265
|
+
|
|
266
|
+
All media is encrypted with **AES-128-ECB** (PKCS7 padding, random 16-byte key per file).
|
|
267
|
+
|
|
268
|
+
**Upload flow:**
|
|
269
|
+
1. Read file, compute MD5 and AES ciphertext size
|
|
270
|
+
2. Call `getUploadUrl` with file metadata
|
|
271
|
+
3. Encrypt with AES-128-ECB, POST to CDN URL
|
|
272
|
+
4. CDN returns `x-encrypted-param` header (the download param)
|
|
273
|
+
|
|
274
|
+
**Download flow:**
|
|
275
|
+
1. Build URL: `{cdnBaseUrl}/download?encrypted_query_param=...`
|
|
276
|
+
2. Fetch ciphertext
|
|
277
|
+
3. Decrypt with the `aes_key` from the `CDNMedia` reference
|
|
278
|
+
|
|
279
|
+
AES key encoding varies by media type:
|
|
280
|
+
- Images: `base64(raw 16 bytes)`
|
|
281
|
+
- Files/voice/video: `base64(hex string of 16 bytes)`
|
|
282
|
+
|
|
283
|
+
## Project Structure
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
src/
|
|
287
|
+
index.ts Public API exports
|
|
288
|
+
client.ts WeChatClient (high-level, stateless)
|
|
289
|
+
monitor.ts Long-poll getUpdates loop with backoff
|
|
290
|
+
api/
|
|
291
|
+
types.ts Protocol types (messages, CDN, requests/responses)
|
|
292
|
+
client.ts Low-level HTTP ApiClient
|
|
293
|
+
auth/
|
|
294
|
+
qr-login.ts QR code login flow (returns URL, no rendering)
|
|
295
|
+
cdn/
|
|
296
|
+
aes-ecb.ts AES-128-ECB encrypt/decrypt
|
|
297
|
+
cdn-url.ts CDN URL builders
|
|
298
|
+
cdn-upload.ts Encrypted upload to CDN
|
|
299
|
+
cdn-download.ts Download + decrypt from CDN
|
|
300
|
+
media/
|
|
301
|
+
upload.ts File -> CDN upload pipeline
|
|
302
|
+
download.ts Download media from inbound messages
|
|
303
|
+
send.ts Build and send text/image/video/file messages
|
|
304
|
+
util/
|
|
305
|
+
mime.ts MIME type <-> extension mapping
|
|
306
|
+
random.ts ID and filename generation
|
|
307
|
+
examples/
|
|
308
|
+
echo-bot.ts Complete echo bot (with its own persistence + QR rendering)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## License
|
|
312
|
+
|
|
313
|
+
MIT
|
package/README.zh_CN.md
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# wechat-ilink-client
|
|
2
|
+
|
|
3
|
+
[English](./README.md)
|
|
4
|
+
|
|
5
|
+
独立的 TypeScript 微信 iLink 机器人协议客户端,通过逆向 [`@tencent-weixin/openclaw-weixin`](https://www.npmjs.com/package/@tencent-weixin/openclaw-weixin) 实现。
|
|
6
|
+
|
|
7
|
+
不依赖 OpenClaw 框架。零运行时依赖。一个纯粹的、无状态的库,可直接用于构建你自己的微信机器人。
|
|
8
|
+
|
|
9
|
+
## 设计原则
|
|
10
|
+
|
|
11
|
+
- **无状态** — 库本身不读写任何文件。凭据存储、游标持久化、二维码渲染完全由调用者负责。
|
|
12
|
+
- **零运行时依赖** — 仅使用 Node.js 内置模块。
|
|
13
|
+
- **最小 API** — 一个 `WeChatClient` 类覆盖大多数场景,同时导出底层原语供高级使用。
|
|
14
|
+
|
|
15
|
+
## 功能
|
|
16
|
+
|
|
17
|
+
- 二维码扫码登录(返回 URL;调用者自行渲染)
|
|
18
|
+
- 长轮询消息接收(`getUpdates`),游标持久化可选
|
|
19
|
+
- 发送文本、图片、视频、文件消息
|
|
20
|
+
- CDN 媒体上传/下载,AES-128-ECB 加密
|
|
21
|
+
- 输入状态指示器(正在输入...)
|
|
22
|
+
- 基于 EventEmitter 的 API
|
|
23
|
+
- 完整的 TypeScript 协议类型定义
|
|
24
|
+
|
|
25
|
+
## 环境要求
|
|
26
|
+
|
|
27
|
+
- Node.js >= 20
|
|
28
|
+
|
|
29
|
+
## 安装
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm install
|
|
33
|
+
pnpm build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 快速开始
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { WeChatClient, MessageType } from "wechat-ilink-client";
|
|
40
|
+
|
|
41
|
+
const client = new WeChatClient();
|
|
42
|
+
|
|
43
|
+
// 第 1 步:二维码登录
|
|
44
|
+
const result = await client.login({
|
|
45
|
+
onQRCode(url) {
|
|
46
|
+
// 你来处理二维码渲染 — 打印、显示在 GUI 中等
|
|
47
|
+
console.log("请扫描此二维码:", url);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
if (!result.connected) {
|
|
51
|
+
console.error("登录失败:", result.message);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// 你来处理持久化 — 自行保存以下信息:
|
|
55
|
+
// result.botToken, result.accountId, result.baseUrl
|
|
56
|
+
|
|
57
|
+
// 第 2 步:处理收到的消息
|
|
58
|
+
client.on("message", async (msg) => {
|
|
59
|
+
if (msg.message_type !== MessageType.USER) return;
|
|
60
|
+
|
|
61
|
+
const text = WeChatClient.extractText(msg);
|
|
62
|
+
const from = msg.from_user_id!;
|
|
63
|
+
|
|
64
|
+
await client.sendText(from, `Echo: ${text}`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// 第 3 步:启动长轮询循环(阻塞直到调用 stop())
|
|
68
|
+
await client.start();
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
后续运行时,直接从已保存的凭据构造客户端:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const client = new WeChatClient({
|
|
75
|
+
accountId: savedAccountId,
|
|
76
|
+
token: savedToken,
|
|
77
|
+
baseUrl: savedBaseUrl,
|
|
78
|
+
});
|
|
79
|
+
// 已就绪 — 直接设置 .on("message", ...) 并调用 .start()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 持久化长轮询游标
|
|
83
|
+
|
|
84
|
+
若要在重启后从上次位置恢复,向 `start()` 传入 `loadSyncBuf` / `saveSyncBuf` 回调:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
await client.start({
|
|
88
|
+
loadSyncBuf: () => fs.readFileSync("sync.json", "utf-8"),
|
|
89
|
+
saveSyncBuf: (buf) => fs.writeFileSync("sync.json", buf),
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## 示例
|
|
94
|
+
|
|
95
|
+
### Echo Bot(回声机器人)
|
|
96
|
+
|
|
97
|
+
一个完整的示例,带有文件持久化和二维码渲染。
|
|
98
|
+
|
|
99
|
+
首先安装 `qrcode-terminal` 以在终端内渲染二维码:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pnpm add qrcode-terminal
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
然后运行:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
pnpm tsx examples/echo-bot.ts # 首次运行 — 显示二维码
|
|
109
|
+
pnpm tsx examples/echo-bot.ts # 后续运行 — 恢复会话
|
|
110
|
+
pnpm tsx examples/echo-bot.ts --fresh # 强制重新登录
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
或通过脚本:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
pnpm echo-bot
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
> 未安装 `qrcode-terminal` 时示例仍可运行 — 会直接打印二维码 URL。
|
|
120
|
+
|
|
121
|
+
示例将凭据存储在 `~/.wechat-echo-bot/` — 这是示例自己的选择,不是库的行为。
|
|
122
|
+
|
|
123
|
+
## API 参考
|
|
124
|
+
|
|
125
|
+
### `WeChatClient`
|
|
126
|
+
|
|
127
|
+
高级客户端,继承自 `EventEmitter`。
|
|
128
|
+
|
|
129
|
+
#### 构造函数
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
new WeChatClient(opts?: {
|
|
133
|
+
baseUrl?: string; // 默认: "https://ilinkai.weixin.qq.com"
|
|
134
|
+
cdnBaseUrl?: string; // 默认: "https://novac2c.cdn.weixin.qq.com/c2c"
|
|
135
|
+
token?: string; // Bearer token
|
|
136
|
+
accountId?: string; // 账户 ID
|
|
137
|
+
channelVersion?: string;
|
|
138
|
+
routeTag?: string;
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### 方法
|
|
143
|
+
|
|
144
|
+
| 方法 | 说明 |
|
|
145
|
+
|------|------|
|
|
146
|
+
| `login(opts?)` | 执行二维码登录。仅在内存中设置 token/accountId。**不持久化。** |
|
|
147
|
+
| `start(opts?)` | 启动长轮询监听。触发 `"message"` 事件。阻塞直到调用 `stop()`。 |
|
|
148
|
+
| `stop()` | 停止长轮询循环。 |
|
|
149
|
+
| `sendText(to, text, contextToken?)` | 发送文本消息。context token 自动从缓存中获取。 |
|
|
150
|
+
| `sendMedia(to, filePath, caption?, contextToken?)` | 上传并发送文件(根据 MIME 类型自动路由为图片/视频/文件)。 |
|
|
151
|
+
| `sendUploadedImage(to, uploaded, caption?, contextToken?)` | 发送已上传的图片。 |
|
|
152
|
+
| `sendUploadedVideo(to, uploaded, caption?, contextToken?)` | 发送已上传的视频。 |
|
|
153
|
+
| `sendUploadedFile(to, fileName, uploaded, caption?, contextToken?)` | 发送已上传的文件。 |
|
|
154
|
+
| `sendTyping(userId, typingTicket, status?)` | 发送/取消输入状态指示器。 |
|
|
155
|
+
| `getTypingTicket(userId, contextToken?)` | 获取用户的 typing ticket。 |
|
|
156
|
+
| `uploadImage(filePath, toUserId)` | 上传图片到 CDN。 |
|
|
157
|
+
| `uploadVideo(filePath, toUserId)` | 上传视频到 CDN。 |
|
|
158
|
+
| `uploadFile(filePath, toUserId)` | 上传文件到 CDN。 |
|
|
159
|
+
| `downloadMedia(item)` | 下载并解密 `MessageItem` 中的媒体内容。 |
|
|
160
|
+
| `getContextToken(userId)` | 获取用户的缓存 context token。 |
|
|
161
|
+
| `getAccountId()` | 获取当前账户 ID。 |
|
|
162
|
+
|
|
163
|
+
#### `start()` 选项
|
|
164
|
+
|
|
165
|
+
| 选项 | 类型 | 说明 |
|
|
166
|
+
|------|------|------|
|
|
167
|
+
| `longPollTimeoutMs` | `number` | 长轮询超时(毫秒),服务器可能覆盖此值。 |
|
|
168
|
+
| `signal` | `AbortSignal` | 用于外部取消。 |
|
|
169
|
+
| `loadSyncBuf` | `() => string \| undefined \| Promise<...>` | 启动时调用一次,加载已持久化的游标。 |
|
|
170
|
+
| `saveSyncBuf` | `(buf: string) => void \| Promise<void>` | 每次轮询后调用,传入新的游标值。 |
|
|
171
|
+
|
|
172
|
+
#### `login()` 选项
|
|
173
|
+
|
|
174
|
+
| 选项 | 类型 | 说明 |
|
|
175
|
+
|------|------|------|
|
|
176
|
+
| `timeoutMs` | `number` | 等待扫码的最大时间(默认: 480_000)。 |
|
|
177
|
+
| `botType` | `string` | bot_type 参数(默认: "3")。 |
|
|
178
|
+
| `maxRefreshes` | `number` | 二维码过期后最大刷新次数(默认: 3)。 |
|
|
179
|
+
| `onQRCode` | `(url: string) => void` | 收到二维码 URL 时调用。**调用者自行渲染。** |
|
|
180
|
+
| `onStatus` | `(status) => void` | 状态变化时调用(wait/scaned/expired/confirmed)。 |
|
|
181
|
+
| `signal` | `AbortSignal` | 用于取消。 |
|
|
182
|
+
|
|
183
|
+
#### 事件
|
|
184
|
+
|
|
185
|
+
| 事件 | 载荷 | 说明 |
|
|
186
|
+
|------|------|------|
|
|
187
|
+
| `message` | `WeixinMessage` | 收到用户消息。 |
|
|
188
|
+
| `error` | `Error` | 非致命的轮询/API 错误。 |
|
|
189
|
+
| `sessionExpired` | _(无)_ | 服务器返回 errcode -14。机器人会自动暂停。 |
|
|
190
|
+
| `poll` | `GetUpdatesResp` | 每次 getUpdates 调用的原始响应。 |
|
|
191
|
+
|
|
192
|
+
#### 静态方法
|
|
193
|
+
|
|
194
|
+
| 方法 | 说明 |
|
|
195
|
+
|------|------|
|
|
196
|
+
| `WeChatClient.extractText(msg)` | 从 `WeixinMessage` 中提取文本内容。 |
|
|
197
|
+
| `WeChatClient.isMediaItem(item)` | 判断 `MessageItem` 是否为图片/语音/文件/视频。 |
|
|
198
|
+
|
|
199
|
+
### `ApiClient`
|
|
200
|
+
|
|
201
|
+
底层 HTTP 客户端。`WeChatClient` 内部使用,也可直接使用。
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
const api = new ApiClient({ baseUrl, token });
|
|
205
|
+
|
|
206
|
+
await api.getUpdates(syncBuf, timeoutMs);
|
|
207
|
+
await api.sendMessage(req);
|
|
208
|
+
await api.getUploadUrl(req);
|
|
209
|
+
await api.getConfig(userId, contextToken);
|
|
210
|
+
await api.sendTyping(req);
|
|
211
|
+
await api.getQRCode(botType);
|
|
212
|
+
await api.pollQRCodeStatus(qrcode);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### `normalizeAccountId(raw)`
|
|
216
|
+
|
|
217
|
+
将原始账户 ID(如 `"hex@im.bot"`)转换为安全 key(`"hex-im-bot"`)。
|
|
218
|
+
|
|
219
|
+
## 协议概述
|
|
220
|
+
|
|
221
|
+
微信 iLink 机器人后端地址为 `https://ilinkai.weixin.qq.com`。所有 API 端点使用 `POST` + JSON 请求体(二维码登录使用 `GET`)。
|
|
222
|
+
|
|
223
|
+
### 认证
|
|
224
|
+
|
|
225
|
+
每个请求包含以下 HTTP 头:
|
|
226
|
+
|
|
227
|
+
| 请求头 | 值 |
|
|
228
|
+
|--------|-----|
|
|
229
|
+
| `Content-Type` | `application/json` |
|
|
230
|
+
| `AuthorizationType` | `ilink_bot_token` |
|
|
231
|
+
| `Authorization` | `Bearer <token>` |
|
|
232
|
+
| `X-WECHAT-UIN` | 随机 uint32 的 Base64 编码 |
|
|
233
|
+
|
|
234
|
+
Token 通过二维码扫码登录获取:
|
|
235
|
+
|
|
236
|
+
1. `GET ilink/bot/get_bot_qrcode?bot_type=3` — 返回二维码 URL
|
|
237
|
+
2. `GET ilink/bot/get_qrcode_status?qrcode=...` — 长轮询直到状态为 `"confirmed"`
|
|
238
|
+
3. 响应包含 `bot_token`、`ilink_bot_id`、`baseurl`
|
|
239
|
+
|
|
240
|
+
### 端点列表
|
|
241
|
+
|
|
242
|
+
| 端点 | 说明 |
|
|
243
|
+
|------|------|
|
|
244
|
+
| `ilink/bot/getupdates` | 长轮询接收消息(游标:`get_updates_buf`) |
|
|
245
|
+
| `ilink/bot/sendmessage` | 发送消息(文本/图片/视频/文件) |
|
|
246
|
+
| `ilink/bot/getuploadurl` | 获取 CDN 预签名上传参数 |
|
|
247
|
+
| `ilink/bot/getconfig` | 获取账户配置(typing ticket) |
|
|
248
|
+
| `ilink/bot/sendtyping` | 发送/取消输入状态指示器 |
|
|
249
|
+
|
|
250
|
+
### 消息结构
|
|
251
|
+
|
|
252
|
+
消息使用 `WeixinMessage` 信封,包含 `item_list` 类型化消息项:
|
|
253
|
+
|
|
254
|
+
| 类型 | 值 | 对应字段 |
|
|
255
|
+
|------|----|----------|
|
|
256
|
+
| TEXT | 1 | `text_item.text` |
|
|
257
|
+
| IMAGE | 2 | `image_item`(CDN 媒体引用 + AES 密钥) |
|
|
258
|
+
| VOICE | 3 | `voice_item`(CDN 媒体引用,可选语音转文字) |
|
|
259
|
+
| FILE | 4 | `file_item`(CDN 媒体引用 + 文件名) |
|
|
260
|
+
| VIDEO | 5 | `video_item`(CDN 媒体引用) |
|
|
261
|
+
|
|
262
|
+
收到消息中的 `context_token` 字段**必须**在所有回复中原样返回。
|
|
263
|
+
|
|
264
|
+
### CDN 媒体
|
|
265
|
+
|
|
266
|
+
所有媒体文件使用 **AES-128-ECB** 加密(PKCS7 填充,每个文件随机 16 字节密钥)。
|
|
267
|
+
|
|
268
|
+
**上传流程:**
|
|
269
|
+
1. 读取文件,计算 MD5 和 AES 密文大小
|
|
270
|
+
2. 调用 `getUploadUrl` 获取上传参数
|
|
271
|
+
3. AES-128-ECB 加密后 POST 到 CDN URL
|
|
272
|
+
4. CDN 返回 `x-encrypted-param` 响应头(即下载参数)
|
|
273
|
+
|
|
274
|
+
**下载流程:**
|
|
275
|
+
1. 构建 URL:`{cdnBaseUrl}/download?encrypted_query_param=...`
|
|
276
|
+
2. 获取密文
|
|
277
|
+
3. 使用 `CDNMedia` 引用中的 `aes_key` 解密
|
|
278
|
+
|
|
279
|
+
AES 密钥编码因媒体类型而异:
|
|
280
|
+
- 图片:`base64(原始 16 字节)`
|
|
281
|
+
- 文件/语音/视频:`base64(16 字节的十六进制字符串)`
|
|
282
|
+
|
|
283
|
+
## 项目结构
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
src/
|
|
287
|
+
index.ts 公共 API 导出
|
|
288
|
+
client.ts WeChatClient(高级,无状态)
|
|
289
|
+
monitor.ts 长轮询 getUpdates 循环(含退避策略)
|
|
290
|
+
api/
|
|
291
|
+
types.ts 协议类型(消息、CDN、请求/响应)
|
|
292
|
+
client.ts 底层 HTTP ApiClient
|
|
293
|
+
auth/
|
|
294
|
+
qr-login.ts 二维码登录流程(仅返回 URL,不渲染)
|
|
295
|
+
cdn/
|
|
296
|
+
aes-ecb.ts AES-128-ECB 加密/解密
|
|
297
|
+
cdn-url.ts CDN URL 构建器
|
|
298
|
+
cdn-upload.ts 加密上传到 CDN
|
|
299
|
+
cdn-download.ts 从 CDN 下载并解密
|
|
300
|
+
media/
|
|
301
|
+
upload.ts 文件 -> CDN 上传流水线
|
|
302
|
+
download.ts 从收到的消息中下载媒体
|
|
303
|
+
send.ts 构建并发送文本/图片/视频/文件消息
|
|
304
|
+
util/
|
|
305
|
+
mime.ts MIME 类型 <-> 扩展名映射
|
|
306
|
+
random.ts ID 和文件名生成
|
|
307
|
+
examples/
|
|
308
|
+
echo-bot.ts 完整的回声机器人(带有自己的持久化和二维码渲染)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## 许可证
|
|
312
|
+
|
|
313
|
+
MIT
|