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.
- package/CHANGELOG.md +107 -90
- package/CHANGELOG.zh_CN.md +2 -170
- package/CHANGELOG_EN.md +199 -0
- package/LICENSE +18 -24
- package/NOTICE +11 -0
- package/README.md +56 -325
- package/README.zh_CN.md +1 -356
- package/README_EN.md +103 -0
- package/dist/src/channel.js +5 -0
- package/dist/src/channel.js.map +1 -1
- package/dist/src/messaging/approval-quick-replies.js +170 -0
- package/dist/src/messaging/approval-quick-replies.js.map +1 -0
- package/docs/architecture.md +132 -0
- package/docs/backend-api.md +347 -0
- package/docs/backend-api.zh_CN.md +338 -0
- package/docs/guide.md +109 -0
- package/docs/guide.zh_CN.md +99 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +12 -7
|
@@ -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
|