openclaw-weixin 2.4.6 → 3.0.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.
@@ -0,0 +1,170 @@
1
+ const APPROVAL_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
2
+ const FORWARDED_EXEC_APPROVAL_HEADING = "🔒 Exec approval required";
3
+ const QUICK_REPLIES_HEADING = "Quick replies (short ID):";
4
+ const OTHER_OPTIONS_HEADER = "Other options:\n\n";
5
+ const TXT_FENCE_OPEN = "```txt\n";
6
+ const FENCE_CLOSE = "\n```";
7
+ const EXEC_APPROVAL_DECISIONS = ["allow-once", "allow-always", "deny"];
8
+ function isRecord(value) {
9
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
10
+ }
11
+ function readExecApprovalDetails(payload) {
12
+ if (!isRecord(payload.channelData)) {
13
+ return null;
14
+ }
15
+ const metadata = payload.channelData.execApproval;
16
+ if (!isRecord(metadata) || metadata.approvalKind !== "exec") {
17
+ return null;
18
+ }
19
+ const approvalId = typeof metadata.approvalId === "string" ? metadata.approvalId.trim() : "";
20
+ const approvalSlug = typeof metadata.approvalSlug === "string" ? metadata.approvalSlug.trim() : "";
21
+ if (!APPROVAL_ID_RE.test(approvalId) ||
22
+ !APPROVAL_ID_RE.test(approvalSlug) ||
23
+ !Array.isArray(metadata.allowedDecisions)) {
24
+ return null;
25
+ }
26
+ const allowed = new Set(metadata.allowedDecisions);
27
+ const allowedDecisions = EXEC_APPROVAL_DECISIONS.filter((decision) => allowed.has(decision));
28
+ return allowedDecisions.length > 0 ? { approvalId, approvalSlug, allowedDecisions } : null;
29
+ }
30
+ function formatApprovalCommandBlock(command) {
31
+ return `${TXT_FENCE_OPEN}${command}${FENCE_CLOSE}`;
32
+ }
33
+ function isExecApprovalDecision(value) {
34
+ return EXEC_APPROVAL_DECISIONS.some((decision) => decision === value);
35
+ }
36
+ function parseApprovalCommand(command) {
37
+ const match = command.match(/^\/approve ([A-Za-z0-9][A-Za-z0-9._:-]*) (allow-once|allow-always|deny)$/);
38
+ if (!match || !isExecApprovalDecision(match[2])) {
39
+ return null;
40
+ }
41
+ return {
42
+ approvalCommandId: match[1],
43
+ decision: match[2],
44
+ };
45
+ }
46
+ function readPrimaryApprovalCommand(text) {
47
+ const marker = `Approval required.\n\nRun:\n\n${TXT_FENCE_OPEN}`;
48
+ const markerStart = text.startsWith(marker) ? 0 : text.indexOf(`\n\n${marker}`);
49
+ if (markerStart < 0) {
50
+ return null;
51
+ }
52
+ const commandStart = markerStart + marker.length + (markerStart === 0 ? 0 : 2);
53
+ const commandEnd = text.indexOf(FENCE_CLOSE, commandStart);
54
+ if (commandEnd < 0) {
55
+ return null;
56
+ }
57
+ const command = parseApprovalCommand(text.slice(commandStart, commandEnd));
58
+ return command ? { command, blockEnd: commandEnd + FENCE_CLOSE.length } : null;
59
+ }
60
+ function readPendingCommandBlockEnd(text, start) {
61
+ const marker = "\n\nPending command:\n\n";
62
+ if (!text.startsWith(marker, start)) {
63
+ return null;
64
+ }
65
+ const fenceStart = start + marker.length;
66
+ const openerEnd = text.indexOf("\n", fenceStart);
67
+ if (openerEnd < 0) {
68
+ return null;
69
+ }
70
+ const opener = text.slice(fenceStart, openerEnd);
71
+ const openerMatch = opener.match(/^(`{3,})sh$/);
72
+ if (!openerMatch) {
73
+ return null;
74
+ }
75
+ const closingFence = `\n${openerMatch[1]}`;
76
+ const closingStart = text.indexOf(closingFence, openerEnd + 1);
77
+ return closingStart < 0 ? null : closingStart + closingFence.length;
78
+ }
79
+ function splitOtherOptionsText(text, approvalId, allowedDecisions) {
80
+ if (!text.endsWith(`Full id: \`${approvalId}\``)) {
81
+ return null;
82
+ }
83
+ const primary = readPrimaryApprovalCommand(text);
84
+ if (!primary) {
85
+ return null;
86
+ }
87
+ const pendingBlockEnd = readPendingCommandBlockEnd(text, primary.blockEnd);
88
+ if (pendingBlockEnd === null) {
89
+ return null;
90
+ }
91
+ const sectionMarker = `\n\n${OTHER_OPTIONS_HEADER}${TXT_FENCE_OPEN}`;
92
+ if (!text.startsWith(sectionMarker, pendingBlockEnd)) {
93
+ return null;
94
+ }
95
+ const blockStart = pendingBlockEnd + 2 + OTHER_OPTIONS_HEADER.length;
96
+ const commandsStart = blockStart + TXT_FENCE_OPEN.length;
97
+ const commandsEnd = text.indexOf(FENCE_CLOSE, commandsStart);
98
+ if (commandsEnd < 0) {
99
+ return null;
100
+ }
101
+ const blockEnd = commandsEnd + FENCE_CLOSE.length;
102
+ const suffix = text.slice(blockEnd);
103
+ if (suffix && !suffix.startsWith("\n\n")) {
104
+ return null;
105
+ }
106
+ const commands = text.slice(commandsStart, commandsEnd).split("\n");
107
+ if (commands.length < 2 || commands.some((command) => !command)) {
108
+ return null;
109
+ }
110
+ const parsedCommands = commands.map(parseApprovalCommand);
111
+ if (parsedCommands.some((command) => command === null)) {
112
+ return null;
113
+ }
114
+ const validCommands = parsedCommands.filter((command) => command !== null);
115
+ const allowed = new Set(allowedDecisions);
116
+ if (!allowed.has(primary.command.decision) ||
117
+ validCommands.some((command) => !allowed.has(command.decision)) ||
118
+ validCommands.some((command) => command.approvalCommandId !== primary.command.approvalCommandId) ||
119
+ validCommands.some((command) => command.decision === primary.command.decision) ||
120
+ new Set(validCommands.map((command) => command.decision)).size !== validCommands.length) {
121
+ return null;
122
+ }
123
+ return [text.slice(0, blockStart), commands.map(formatApprovalCommandBlock).join("\n\n"), text.slice(blockEnd)].join("");
124
+ }
125
+ /**
126
+ * Add copy-friendly commands to forwarded exec approval prompts.
127
+ *
128
+ * OpenClaw supplies the collision-aware short ID and the request-scoped decision
129
+ * set in channel metadata, so the channel never derives either value from text.
130
+ */
131
+ export function appendWeixinExecApprovalQuickReplies(params) {
132
+ if (params.hint?.kind !== "approval-pending" ||
133
+ params.hint.approvalKind !== "exec" ||
134
+ !params.payload.text?.startsWith(`${FORWARDED_EXEC_APPROVAL_HEADING}\n`)) {
135
+ return;
136
+ }
137
+ const details = readExecApprovalDetails(params.payload);
138
+ if (!details) {
139
+ return;
140
+ }
141
+ const forwardedPrefix = `${FORWARDED_EXEC_APPROVAL_HEADING}\nID: ${details.approvalId}`;
142
+ if (params.payload.text !== forwardedPrefix && !params.payload.text.startsWith(`${forwardedPrefix}\n`)) {
143
+ return;
144
+ }
145
+ const commandBlocks = details.allowedDecisions
146
+ .map((decision) => formatApprovalCommandBlock(`/approve ${details.approvalSlug} ${decision}`))
147
+ .join("\n\n");
148
+ const quickReplies = `${QUICK_REPLIES_HEADING}\n\n${commandBlocks}`;
149
+ const text = params.payload.text.trimEnd();
150
+ if (text.endsWith(quickReplies)) {
151
+ return;
152
+ }
153
+ params.payload.text = `${text}\n\n${quickReplies}`;
154
+ }
155
+ /**
156
+ * Split OpenClaw's direct exec-approval alternatives into individually copyable
157
+ * code blocks while preserving the exact command IDs rendered by OpenClaw.
158
+ */
159
+ export function splitWeixinExecApprovalOtherOptions(payload) {
160
+ if (!payload.text) {
161
+ return payload;
162
+ }
163
+ const details = readExecApprovalDetails(payload);
164
+ if (!details) {
165
+ return payload;
166
+ }
167
+ const text = splitOtherOptionsText(payload.text, details.approvalId, details.allowedDecisions);
168
+ return text ? { ...payload, text } : payload;
169
+ }
170
+ //# sourceMappingURL=approval-quick-replies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-quick-replies.js","sourceRoot":"","sources":["../../../src/messaging/approval-quick-replies.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,132 @@
1
+ # Architecture
2
+
3
+ `openclaw-weixin` adapts the Weixin HTTP/CDN protocol to the OpenClaw channel
4
+ runtime. The plugin owns login, account state, long polling, message conversion,
5
+ and outbound media transfer. OpenClaw owns routing, sessions, command
6
+ authorization, reply generation, hooks, and the unified media store.
7
+
8
+ The wire-level endpoint and message shapes are documented in the
9
+ [backend API protocol](./backend-api.md).
10
+
11
+ ## Component map
12
+
13
+ | Component | Responsibility |
14
+ | --- | --- |
15
+ | `index.ts` | Validate host compatibility and register the channel |
16
+ | `src/channel.ts` | Implement the OpenClaw channel contract and account lifecycle |
17
+ | `src/auth/` | QR login, account persistence, ID compatibility, and pairing |
18
+ | `src/api/` | Build authenticated backend requests and classify failures |
19
+ | `src/monitor/monitor.ts` | Poll updates, persist cursors, and schedule inbound work |
20
+ | `src/messaging/process-message.ts` | Authorize, route, record, and dispatch one inbound message |
21
+ | `src/messaging/send*.ts` | Convert outbound text/media to backend message items |
22
+ | `src/cdn/`, `src/media/` | Encrypt, upload, download, decrypt, and transcode media |
23
+ | `src/storage/` | Resolve state paths and persist the polling cursor |
24
+
25
+ ## Plugin and account lifecycle
26
+
27
+ ```mermaid
28
+ flowchart TD
29
+ A[index.ts register] --> B[Check OpenClaw host version]
30
+ B --> C[Register weixinPlugin]
31
+ C --> D{Operation}
32
+ D -->|login| E[Start QR session]
33
+ E --> F[Wait for confirmation]
34
+ F --> G[Persist account and pairing state]
35
+ G --> H[Trigger channel reload]
36
+ D -->|start account| I[Restore context tokens]
37
+ I --> J[Notify backend start]
38
+ J --> K[Run monitor loop]
39
+ D -->|stop or reload| L[Abort active poll]
40
+ L --> M[Notify backend stop]
41
+ ```
42
+
43
+ The plugin/channel ID and state layout are compatibility surfaces. A successful
44
+ login may replace stale account records for the same Weixin user, but it must not
45
+ silently merge unrelated accounts.
46
+
47
+ ## Inbound flow
48
+
49
+ ```mermaid
50
+ sequenceDiagram
51
+ participant Backend as Weixin backend
52
+ participant Monitor as monitorWeixinProvider
53
+ participant Processor as processOneMessage
54
+ participant Runtime as OpenClaw channel runtime
55
+
56
+ Monitor->>Backend: getUpdates(cursor, abort signal)
57
+ Backend-->>Monitor: messages + next cursor
58
+ Monitor->>Monitor: persist cursor and account-scoped context token
59
+ Monitor->>Processor: schedule message
60
+ Processor->>Processor: handle slash command or download media
61
+ Processor->>Runtime: authorize sender and resolve agent route
62
+ Processor->>Runtime: record inbound session
63
+ Processor->>Runtime: dispatch reply
64
+ Runtime-->>Processor: text, media, and item lifecycle events
65
+ ```
66
+
67
+ Ordinary messages are serialized until OpenClaw accepts the turn, after which
68
+ polling can admit the next message. Plugin approval commands use a separate lane
69
+ so an active ordinary turn cannot block approval.
70
+
71
+ ## Outbound flow
72
+
73
+ ```mermaid
74
+ flowchart LR
75
+ A[OpenClaw outbound request] --> B{Account ID supplied?}
76
+ B -->|yes| C[Resolve configured account]
77
+ B -->|no| D[Resolve by account-scoped context token]
78
+ D --> C
79
+ C --> E[Check active session]
80
+ E --> F[Run message_sending hook]
81
+ F -->|cancelled| G[Return without backend send]
82
+ F -->|continue| H{Text or media?}
83
+ H -->|text| I[Filter markdown and call sendMessage]
84
+ H -->|media| J[Download if remote]
85
+ J --> K[Encrypt and upload to CDN]
86
+ K --> L[Build media message item]
87
+ I --> M[Emit message_sent hook]
88
+ L --> M
89
+ ```
90
+
91
+ With multiple accounts, an omitted account ID is valid only when exactly one
92
+ account can be selected. Ambiguous or missing context must fail rather than risk
93
+ sending from the wrong bot.
94
+
95
+ ## Persistent state
96
+
97
+ Paths are relative to the OpenClaw state directory unless an existing framework
98
+ override applies.
99
+
100
+ | Path | Contents |
101
+ | --- | --- |
102
+ | `openclaw-weixin/accounts.json` | Registered normalized account IDs |
103
+ | `openclaw-weixin/accounts/<accountId>.json` | Token, backend URL, save time, and linked user ID |
104
+ | `openclaw-weixin/accounts/<accountId>.sync.json` | `getUpdates` cursor |
105
+ | `openclaw-weixin/accounts/<accountId>.context-tokens.json` | Recipient context tokens for that account |
106
+ | `credentials/openclaw-weixin-<accountId>-allowFrom.json` | Framework pairing allow-list |
107
+ | `openclaw.json` | Channel configuration and account overrides |
108
+
109
+ Loaders retain fallbacks for legacy raw IDs, a legacy single-account credential
110
+ file, and older sync-buffer paths. Changes to these fallbacks require migration
111
+ tests.
112
+
113
+ ## Failure and privacy boundaries
114
+
115
+ - Backend HTTP errors are surfaced with actionable context but without raw
116
+ authorization or context tokens.
117
+ - A stale token pauses all requests for that account before polling resumes.
118
+ - Long polls receive the gateway abort signal so stop/reload does not wait for a
119
+ server timeout.
120
+ - Media logs must redact URLs and encrypted query parameters.
121
+ - Tests and examples use synthetic IDs such as `account-1` and `user-1`.
122
+
123
+ ## Test seams
124
+
125
+ - API tests replace `fetch` and assert request/response boundaries.
126
+ - Monitor tests mock polling, cursor persistence, context storage, and message
127
+ processing.
128
+ - Message-processing tests use a minimal typed channel-runtime fake.
129
+ - Account and storage tests point `OPENCLAW_STATE_DIR` to isolated temporary
130
+ directories.
131
+ - Shared builders live under `test/helpers/`; test-only files must not be emitted
132
+ into `dist/`.
@@ -0,0 +1,347 @@
1
+ # Backend API Protocol
2
+
3
+ [Back to detailed guide](./guide.md) |
4
+ [简体中文](./backend-api.zh_CN.md)
5
+
6
+ This document covers every Weixin backend endpoint used by the plugin for QR
7
+ login, lifecycle notifications, messaging, and media. The two QR login requests
8
+ always use Tencent's fixed service. A backend selected by the account's
9
+ post-login `baseurl` must implement the lifecycle, messaging, and media
10
+ endpoints.
11
+
12
+ QR creation and all post-login endpoints use `POST`; QR status polling uses
13
+ `GET`. All requests include:
14
+
15
+ | Header | Description |
16
+ |--------|-------------|
17
+ | `iLink-App-Id` | Plugin application ID |
18
+ | `iLink-App-ClientVersion` | Plugin version encoded as an unsigned integer |
19
+ | `SKRouteTag` | Optional configured route tag |
20
+
21
+ `POST` requests additionally include `Content-Type: application/json`,
22
+ `AuthorizationType: ilink_bot_token`, and a random base64-encoded
23
+ `X-WECHAT-UIN`. Authenticated post-login requests also include
24
+ `Authorization: Bearer <bot-token>`; QR status `GET` requests do not include
25
+ these `POST`-specific headers.
26
+
27
+ Authenticated post-login `POST` bodies include `base_info`; QR creation does
28
+ not. The message examples below omit it for readability.
29
+
30
+ ```json
31
+ {
32
+ "base_info": {
33
+ "channel_version": "<plugin version>",
34
+ "bot_agent": "OpenClaw"
35
+ }
36
+ }
37
+ ```
38
+
39
+ `bot_agent` is for observability only. Its supported format and configuration
40
+ are documented in the [detailed guide](./guide.md#custom-botagent-optional).
41
+
42
+ ## Endpoint List
43
+
44
+ | Method | Path | Description |
45
+ |--------|------|-------------|
46
+ | `POST` | `/ilink/bot/get_bot_qrcode?bot_type=3` | Create a QR login session |
47
+ | `GET` | `/ilink/bot/get_qrcode_status?qrcode=<opaque-id>` | Poll QR login status; accepts optional `verify_code` |
48
+ | `POST` | `/ilink/bot/msg/notifystart` | Notify backend that the channel started |
49
+ | `POST` | `/ilink/bot/msg/notifystop` | Notify backend that the channel stopped |
50
+ | `POST` | `/ilink/bot/getupdates` | Long-poll for new messages |
51
+ | `POST` | `/ilink/bot/sendmessage` | Send a message (text/image/video/file) |
52
+ | `POST` | `/ilink/bot/getuploadurl` | Get CDN upload pre-signed parameters |
53
+ | `POST` | `/ilink/bot/getconfig` | Get account config (typing ticket, etc.) |
54
+ | `POST` | `/ilink/bot/sendtyping` | Send/cancel typing status |
55
+
56
+ The first two rows describe the fixed QR login service. They are not sent to the
57
+ account's post-login `baseurl`.
58
+
59
+ ## QR Login and Lifecycle
60
+
61
+ Create a QR session with:
62
+
63
+ ```http
64
+ POST /ilink/bot/get_bot_qrcode?bot_type=3
65
+ Content-Type: application/json
66
+ ```
67
+
68
+ ```json
69
+ {
70
+ "local_token_list": []
71
+ }
72
+ ```
73
+
74
+ The response contains an opaque `qrcode` identifier and
75
+ `qrcode_img_content`, the URL rendered as the QR code. Poll
76
+ `GET /ilink/bot/get_qrcode_status?qrcode=<opaque-id>` until it reaches a
77
+ terminal state. The optional `verify_code` query parameter handles verification
78
+ challenges.
79
+
80
+ | Field | Type | Description |
81
+ |-------|------|-------------|
82
+ | `status` | `string` | `wait`, `scaned`, `need_verifycode`, `verify_code_blocked`, `expired`, `scaned_but_redirect`, `binded_redirect`, or `confirmed` |
83
+ | `bot_token` | `string?` | Bot credential returned after confirmation |
84
+ | `ilink_bot_id` | `string?` | Required account ID after confirmation |
85
+ | `baseurl` | `string?` | Account API base URL |
86
+ | `ilink_user_id` | `string?` | ID of the user who scanned the QR code |
87
+ | `redirect_host` | `string?` | New polling host for `scaned_but_redirect` |
88
+
89
+ After authentication, call `/ilink/bot/msg/notifystart` when the channel starts
90
+ and `/ilink/bot/msg/notifystop` when it stops. Both receive the standard
91
+ `base_info` body and return:
92
+
93
+ ```json
94
+ {
95
+ "ret": 0,
96
+ "errmsg": ""
97
+ }
98
+ ```
99
+
100
+ QR identifiers, verification codes, bot tokens, account IDs, user IDs, and
101
+ context tokens are sensitive. Never place real values in logs or examples.
102
+
103
+ ## getUpdates
104
+
105
+ Long-polling endpoint. The server responds when new messages arrive or on
106
+ timeout.
107
+
108
+ **Request body:**
109
+
110
+ ```json
111
+ {
112
+ "get_updates_buf": ""
113
+ }
114
+ ```
115
+
116
+ | Field | Type | Description |
117
+ |-------|------|-------------|
118
+ | `get_updates_buf` | `string` | Sync cursor from the previous response; empty string for the first request |
119
+
120
+ **Response body:**
121
+
122
+ ```json
123
+ {
124
+ "ret": 0,
125
+ "msgs": [],
126
+ "get_updates_buf": "<new cursor>",
127
+ "longpolling_timeout_ms": 35000
128
+ }
129
+ ```
130
+
131
+ | Field | Type | Description |
132
+ |-------|------|-------------|
133
+ | `ret` | `number` | Return code, `0` = success |
134
+ | `errcode` | `number?` | Error code (e.g., `-14` = stale token) |
135
+ | `errmsg` | `string?` | Error description |
136
+ | `msgs` | `WeixinMessage[]` | Message list (structure below) |
137
+ | `get_updates_buf` | `string` | New sync cursor to pass in the next request |
138
+ | `longpolling_timeout_ms` | `number?` | Server-suggested long-poll timeout for the next request (ms) |
139
+
140
+ ## sendMessage
141
+
142
+ Send a message to a user.
143
+
144
+ **Request body:**
145
+
146
+ ```json
147
+ {
148
+ "msg": {
149
+ "to_user_id": "<target user ID>",
150
+ "context_token": "<conversation context token>",
151
+ "item_list": [
152
+ {
153
+ "type": 1,
154
+ "text_item": { "text": "Hello" }
155
+ }
156
+ ]
157
+ }
158
+ }
159
+ ```
160
+
161
+ **Response body:**
162
+
163
+ ```json
164
+ {
165
+ "ret": 0,
166
+ "errmsg": ""
167
+ }
168
+ ```
169
+
170
+ ## getUploadUrl
171
+
172
+ Get CDN upload pre-signed parameters. Call this endpoint before uploading a file
173
+ to obtain `upload_param` and `thumb_upload_param`.
174
+
175
+ **Request body:**
176
+
177
+ ```json
178
+ {
179
+ "filekey": "<file identifier>",
180
+ "media_type": 1,
181
+ "to_user_id": "<target user ID>",
182
+ "rawsize": 12345,
183
+ "rawfilemd5": "<plaintext MD5>",
184
+ "filesize": 12352,
185
+ "no_need_thumb": true,
186
+ "aeskey": "<32-character hex AES key>"
187
+ }
188
+ ```
189
+
190
+ | Field | Type | Description |
191
+ |-------|------|-------------|
192
+ | `filekey` | `string` | Per-upload file identifier |
193
+ | `media_type` | `number` | `1` = IMAGE, `2` = VIDEO, `3` = FILE, `4` = VOICE |
194
+ | `to_user_id` | `string` | Target user ID |
195
+ | `rawsize` | `number` | Original file plaintext size |
196
+ | `rawfilemd5` | `string` | Original file plaintext MD5 |
197
+ | `filesize` | `number` | Ciphertext size after AES-128-ECB encryption |
198
+ | `no_need_thumb` | `boolean?` | Set `true` to omit thumbnail upload parameters |
199
+ | `aeskey` | `string?` | AES-128 key as 32 hexadecimal characters |
200
+ | `thumb_rawsize` | `number?` | Thumbnail plaintext size when a thumbnail is requested |
201
+ | `thumb_rawfilemd5` | `string?` | Thumbnail plaintext MD5 when requested |
202
+ | `thumb_filesize` | `number?` | Thumbnail ciphertext size when requested |
203
+
204
+ **Response body:**
205
+
206
+ ```json
207
+ {
208
+ "upload_param": "<original image upload encrypted parameters>",
209
+ "upload_full_url": "https://cdn.example.test/upload",
210
+ "thumb_upload_param": "<optional thumbnail upload parameters>"
211
+ }
212
+ ```
213
+
214
+ | Field | Type | Description |
215
+ |-------|------|-------------|
216
+ | `upload_param` | `string?` | Parameters used to construct the CDN upload URL |
217
+ | `upload_full_url` | `string?` | Complete CDN upload URL; takes precedence over `upload_param` |
218
+ | `thumb_upload_param` | `string?` | Optional thumbnail upload parameters |
219
+
220
+ ## getConfig
221
+
222
+ Get account configuration, including the typing ticket.
223
+
224
+ **Request body:**
225
+
226
+ ```json
227
+ {
228
+ "ilink_user_id": "<user ID>",
229
+ "context_token": "<optional, conversation context token>"
230
+ }
231
+ ```
232
+
233
+ **Response body:**
234
+
235
+ ```json
236
+ {
237
+ "ret": 0,
238
+ "errmsg": "",
239
+ "typing_ticket": "<base64-encoded typing ticket>"
240
+ }
241
+ ```
242
+
243
+ ## sendTyping
244
+
245
+ Send or cancel the typing status indicator.
246
+
247
+ **Request body:**
248
+
249
+ ```json
250
+ {
251
+ "ilink_user_id": "<user ID>",
252
+ "typing_ticket": "<obtained from getConfig>",
253
+ "status": 1
254
+ }
255
+ ```
256
+
257
+ | Field | Type | Description |
258
+ |-------|------|-------------|
259
+ | `status` | `number` | `1` = typing, `2` = cancel typing |
260
+
261
+ **Response body:**
262
+
263
+ ```json
264
+ {
265
+ "ret": 0,
266
+ "errmsg": ""
267
+ }
268
+ ```
269
+
270
+ ## Message Structure
271
+
272
+ ### WeixinMessage
273
+
274
+ | Field | Type | Description |
275
+ |-------|------|-------------|
276
+ | `seq` | `number?` | Message sequence number |
277
+ | `message_id` | `number?` | Unique message ID |
278
+ | `from_user_id` | `string?` | Sender ID |
279
+ | `to_user_id` | `string?` | Receiver ID |
280
+ | `client_id` | `string?` | Client-generated message ID |
281
+ | `create_time_ms` | `number?` | Creation timestamp (ms) |
282
+ | `update_time_ms` | `number?` | Update timestamp (ms) |
283
+ | `delete_time_ms` | `number?` | Deletion timestamp (ms) |
284
+ | `session_id` | `string?` | Session ID |
285
+ | `group_id` | `string?` | Group ID |
286
+ | `message_type` | `number?` | `1` = USER, `2` = BOT |
287
+ | `message_state` | `number?` | `0` = NEW, `1` = GENERATING, `2` = FINISH |
288
+ | `item_list` | `MessageItem[]?` | Message content list |
289
+ | `context_token` | `string?` | Conversation context token, must be passed back when replying |
290
+ | `run_id` | `string?` | OpenClaw run ID for generated replies |
291
+
292
+ ### MessageItem
293
+
294
+ | Field | Type | Description |
295
+ |-------|------|-------------|
296
+ | `type` | `number` | `1` TEXT, `2` IMAGE, `3` VOICE, `4` FILE, `5` VIDEO, `11` TOOL_CALL_START, `12` TOOL_CALL_RESULT |
297
+ | `create_time_ms` | `number?` | Item creation timestamp |
298
+ | `update_time_ms` | `number?` | Item update timestamp |
299
+ | `is_completed` | `boolean?` | Whether a progress item is complete |
300
+ | `msg_id` | `string?` | Item message ID |
301
+ | `text_item` | `{ text: string }?` | Text content |
302
+ | `image_item` | `ImageItem?` | Image (with CDN reference and AES key) |
303
+ | `voice_item` | `VoiceItem?` | Voice (SILK encoded) |
304
+ | `file_item` | `FileItem?` | File attachment |
305
+ | `video_item` | `VideoItem?` | Video |
306
+ | `ref_msg` | `RefMessage?` | Referenced message |
307
+ | `tool_call_start_item` | `{ tool_name?: string; tool_call_id?: string }?` | Tool invocation metadata |
308
+ | `tool_call_result_item` | `{ tool_name?: string; tool_call_id?: string; status?: string }?` | Tool completion metadata |
309
+
310
+ ### Nested Item Structures
311
+
312
+ | Structure | Fields |
313
+ |-----------|--------|
314
+ | `RefMessage` | `message_item?: MessageItem`, `title?: string` |
315
+ | `ImageItem` | `media?: CDNMedia`, `thumb_media?: CDNMedia`, `aeskey?: string`, `url?: string`, `mid_size?: number`, `thumb_size?: number`, `thumb_height?: number`, `thumb_width?: number`, `hd_size?: number` |
316
+ | `VoiceItem` | `media?: CDNMedia`, `encode_type?: number`, `bits_per_sample?: number`, `sample_rate?: number`, `playtime?: number`, `text?: string` |
317
+ | `FileItem` | `media?: CDNMedia`, `file_name?: string`, `md5?: string`, `len?: string` |
318
+ | `VideoItem` | `media?: CDNMedia`, `video_size?: number`, `play_length?: number`, `video_md5?: string`, `thumb_media?: CDNMedia`, `thumb_size?: number`, `thumb_height?: number`, `thumb_width?: number` |
319
+ | `ToolCallStartItem` | `tool_name?: string`, `tool_call_id?: string` |
320
+ | `ToolCallResultItem` | `tool_name?: string`, `tool_call_id?: string`, `status?: string` |
321
+
322
+ ### CDN Media Reference (CDNMedia)
323
+
324
+ All media types (image/voice/file/video) are transferred via CDN using
325
+ AES-128-ECB encryption:
326
+
327
+ | Field | Type | Description |
328
+ |-------|------|-------------|
329
+ | `encrypt_query_param` | `string?` | Encrypted parameters for CDN download/upload |
330
+ | `aes_key` | `string?` | Base64-encoded AES-128 key |
331
+ | `encrypt_type` | `number?` | Encryption metadata mode |
332
+ | `full_url` | `string?` | Complete download URL returned by the backend |
333
+
334
+ ## CDN Upload Flow
335
+
336
+ 1. Calculate the file's plaintext size, MD5, and ciphertext size after
337
+ AES-128-ECB encryption
338
+ 2. If a thumbnail is needed (image/video), calculate the thumbnail's plaintext
339
+ and ciphertext parameters as well
340
+ 3. Call `getUploadUrl` to get `upload_full_url` or `upload_param` (and optional
341
+ `thumb_upload_param`)
342
+ 4. Encrypt the file content with AES-128-ECB and `POST` it to the CDN URL as
343
+ `application/octet-stream`
344
+ 5. Encrypt and upload the thumbnail in the same way when requested
345
+ 6. Read `x-encrypted-param` from the CDN response and use it as
346
+ `encrypt_query_param` in the `CDNMedia` reference
347
+ 7. Include the reference in the `MessageItem` and send