@wu529778790/open-im 1.10.9-beta.17 → 1.10.9-beta.19
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/dist/clawbot/client.d.ts +6 -3
- package/dist/clawbot/client.js +155 -41
- package/dist/clawbot/message-sender.d.ts +8 -2
- package/dist/clawbot/message-sender.js +56 -16
- package/dist/clawbot/types.d.ts +95 -27
- package/dist/clawbot/types.js +4 -1
- package/dist/config-web.js +11 -3
- package/package.json +1 -1
package/dist/clawbot/client.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ClawBot Client - WeChat iLink API long-polling client
|
|
2
|
+
* ClawBot Client - WeChat iLink Bot API long-polling client
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Uses the official iLink protocol: POST + JSON body + Bearer token auth.
|
|
5
|
+
* Receives messages via long-polling ilink/bot/getupdates and dispatches
|
|
6
|
+
* them to the event handler.
|
|
7
|
+
*
|
|
8
|
+
* Reference: @tencent-weixin/openclaw-weixin, cc-wechat, claude-code-wechat-channel
|
|
6
9
|
*/
|
|
7
10
|
import type { Config } from '../config.js';
|
|
8
11
|
import type { ClawBotState } from './types.js';
|
package/dist/clawbot/client.js
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ClawBot Client - WeChat iLink API long-polling client
|
|
2
|
+
* ClawBot Client - WeChat iLink Bot API long-polling client
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Uses the official iLink protocol: POST + JSON body + Bearer token auth.
|
|
5
|
+
* Receives messages via long-polling ilink/bot/getupdates and dispatches
|
|
6
|
+
* them to the event handler.
|
|
7
|
+
*
|
|
8
|
+
* Reference: @tencent-weixin/openclaw-weixin, cc-wechat, claude-code-wechat-channel
|
|
6
9
|
*/
|
|
10
|
+
import { randomBytes } from 'node:crypto';
|
|
7
11
|
import { createLogger } from '../logger.js';
|
|
8
|
-
import { jitteredDelay } from '../shared/reconnect.js';
|
|
12
|
+
import { jitteredDelay, isFatalReconnectError, SLOW_PROBE_MS } from '../shared/reconnect.js';
|
|
13
|
+
import { cacheContextToken } from './message-sender.js';
|
|
9
14
|
import { CLAWBOT_POLL_INTERVAL_MS } from '../constants.js';
|
|
10
15
|
const log = createLogger('ClawBot');
|
|
11
16
|
const RECONNECT_DELAYS_MS = [3000, 5000, 10000, 20000, 30000];
|
|
17
|
+
const BASE_INFO = { channel_version: '0.1.0' };
|
|
12
18
|
let pollController = null;
|
|
13
19
|
let channelState = 'disconnected';
|
|
14
20
|
let messageHandler = null;
|
|
15
21
|
let stateChangeHandler = null;
|
|
16
22
|
let reconnectTimer = null;
|
|
17
23
|
let reconnectAttempt = 0;
|
|
24
|
+
let fatal = false;
|
|
18
25
|
let stopped = false;
|
|
19
|
-
let apiUrl = '
|
|
26
|
+
let apiUrl = 'https://ilinkai.weixin.qq.com';
|
|
20
27
|
let apiToken = '';
|
|
21
|
-
|
|
28
|
+
/** Opaque cursor for getupdates pagination (replaces numeric offset) */
|
|
29
|
+
let getUpdatesBuf = '';
|
|
22
30
|
export function getChannelState() {
|
|
23
31
|
return channelState;
|
|
24
32
|
}
|
|
@@ -30,25 +38,39 @@ export async function initClawbot(config, eventHandler, onStateChange) {
|
|
|
30
38
|
if (!pc.apiToken) {
|
|
31
39
|
throw new Error('ClawBot apiToken required');
|
|
32
40
|
}
|
|
33
|
-
apiUrl = pc.apiUrl ?? '
|
|
41
|
+
apiUrl = pc.apiUrl ?? 'https://ilinkai.weixin.qq.com';
|
|
34
42
|
apiToken = pc.apiToken;
|
|
35
43
|
messageHandler = eventHandler;
|
|
36
44
|
stateChangeHandler = onStateChange ?? null;
|
|
37
45
|
stopped = false;
|
|
38
46
|
reconnectAttempt = 0;
|
|
39
|
-
|
|
47
|
+
fatal = false;
|
|
48
|
+
getUpdatesBuf = '';
|
|
40
49
|
// Verify connectivity — non-fatal, reconnect loop will retry
|
|
41
50
|
try {
|
|
42
|
-
const res = await
|
|
51
|
+
const res = await postApi('/ilink/bot/getupdates', {
|
|
52
|
+
get_updates_buf: '',
|
|
53
|
+
base_info: BASE_INFO,
|
|
54
|
+
});
|
|
43
55
|
if (!res.ok) {
|
|
44
56
|
throw new Error(`API check failed: ${res.error ?? 'unknown'}`);
|
|
45
57
|
}
|
|
46
58
|
log.info(`ClawBot API reachable at ${apiUrl}`);
|
|
59
|
+
fatal = false;
|
|
60
|
+
reconnectAttempt = 0;
|
|
61
|
+
if (res.updatesBuf)
|
|
62
|
+
getUpdatesBuf = res.updatesBuf;
|
|
47
63
|
updateState('connected');
|
|
48
64
|
startPolling();
|
|
49
65
|
}
|
|
50
66
|
catch (err) {
|
|
51
|
-
|
|
67
|
+
if (isFatalReconnectError(err)) {
|
|
68
|
+
fatal = true;
|
|
69
|
+
log.warn('ClawBot API auth/session error, will slow-probe:', err);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
log.warn('ClawBot API not reachable, will retry:', err);
|
|
73
|
+
}
|
|
52
74
|
updateState('connecting');
|
|
53
75
|
scheduleReconnect();
|
|
54
76
|
}
|
|
@@ -63,30 +85,57 @@ function startPolling() {
|
|
|
63
85
|
log.info('ClawBot long-polling started');
|
|
64
86
|
while (!stopped && !signal.aborted) {
|
|
65
87
|
try {
|
|
66
|
-
const
|
|
67
|
-
|
|
88
|
+
const res = await postApi('/ilink/bot/getupdates', {
|
|
89
|
+
get_updates_buf: getUpdatesBuf,
|
|
90
|
+
base_info: BASE_INFO,
|
|
91
|
+
}, signal);
|
|
68
92
|
if (signal.aborted)
|
|
69
93
|
break;
|
|
70
94
|
if (!res.ok) {
|
|
95
|
+
// Detect fatal errors (e.g. errcode -14 "session timeout") — retrying won't help
|
|
96
|
+
if (res.errcode === -14 || isFatalReconnectError(res.error)) {
|
|
97
|
+
log.warn(`ClawBot fatal error (errcode=${res.errcode}), entering slow-probe mode`);
|
|
98
|
+
fatal = true;
|
|
99
|
+
getUpdatesBuf = ''; // session expired, cursor is stale
|
|
100
|
+
updateState('error');
|
|
101
|
+
scheduleReconnect();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
71
104
|
log.warn(`ClawBot getupdates error: ${res.error ?? 'unknown'}`);
|
|
72
105
|
await sleep(CLAWBOT_POLL_INTERVAL_MS, signal);
|
|
73
106
|
continue;
|
|
74
107
|
}
|
|
75
|
-
|
|
76
|
-
|
|
108
|
+
// Successful response — clear fatal mode and reset backoff
|
|
109
|
+
if (fatal || reconnectAttempt > 0) {
|
|
110
|
+
log.info('ClawBot connection recovered');
|
|
111
|
+
fatal = false;
|
|
112
|
+
reconnectAttempt = 0;
|
|
113
|
+
}
|
|
114
|
+
// Update cursor for next poll
|
|
115
|
+
if (res.updatesBuf) {
|
|
116
|
+
getUpdatesBuf = res.updatesBuf;
|
|
117
|
+
}
|
|
118
|
+
// Process messages
|
|
119
|
+
const messages = res.messages ?? [];
|
|
120
|
+
for (const msg of messages) {
|
|
77
121
|
if (signal.aborted)
|
|
78
122
|
break;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
123
|
+
if (msg.message_type !== 1)
|
|
124
|
+
continue; // skip BOT messages, only process USER
|
|
125
|
+
const extracted = extractTextContent(msg);
|
|
126
|
+
if (!extracted)
|
|
82
127
|
continue;
|
|
83
|
-
const chatId = msg.
|
|
84
|
-
const msgId = String(msg.message_id ??
|
|
85
|
-
const content =
|
|
128
|
+
const chatId = msg.from_user_id ?? '';
|
|
129
|
+
const msgId = String(msg.message_id ?? msg.seq ?? '');
|
|
130
|
+
const content = extracted;
|
|
86
131
|
if (!chatId) {
|
|
87
|
-
log.warn('ClawBot message missing
|
|
132
|
+
log.warn('ClawBot message missing from_user_id, skipping');
|
|
88
133
|
continue;
|
|
89
134
|
}
|
|
135
|
+
// Cache context_token for reply capability
|
|
136
|
+
if (msg.context_token) {
|
|
137
|
+
cacheContextToken(chatId, msg.context_token);
|
|
138
|
+
}
|
|
90
139
|
log.info(`ClawBot message: chatId=${chatId}, msgId=${msgId}, content="${content.substring(0, 100)}"`);
|
|
91
140
|
if (messageHandler) {
|
|
92
141
|
try {
|
|
@@ -119,9 +168,14 @@ function scheduleReconnect() {
|
|
|
119
168
|
reconnectTimer = null;
|
|
120
169
|
}
|
|
121
170
|
const baseDelay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
122
|
-
const delay = jitteredDelay(baseDelay);
|
|
171
|
+
const delay = fatal ? jitteredDelay(SLOW_PROBE_MS) : jitteredDelay(baseDelay);
|
|
123
172
|
reconnectAttempt++;
|
|
124
|
-
|
|
173
|
+
if (fatal) {
|
|
174
|
+
log.warn(`ClawBot fatal error, slow-probe in ${Math.round(delay / 1000)}s (attempt ${reconnectAttempt})...`);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
log.info(`ClawBot reconnecting in ${delay}ms (attempt ${reconnectAttempt})...`);
|
|
178
|
+
}
|
|
125
179
|
reconnectTimer = setTimeout(() => {
|
|
126
180
|
reconnectTimer = null;
|
|
127
181
|
if (stopped)
|
|
@@ -135,27 +189,87 @@ function updateState(state) {
|
|
|
135
189
|
stateChangeHandler?.(state);
|
|
136
190
|
log.debug(`ClawBot state: ${state}`);
|
|
137
191
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Extract text content from an iLink message's item_list.
|
|
194
|
+
* Returns the first text item found, or a placeholder for media types.
|
|
195
|
+
*/
|
|
196
|
+
function extractTextContent(msg) {
|
|
197
|
+
if (!msg.item_list?.length)
|
|
198
|
+
return null;
|
|
199
|
+
for (const item of msg.item_list) {
|
|
200
|
+
switch (item.type) {
|
|
201
|
+
case 1 /* MessageItemType.TEXT */: {
|
|
202
|
+
if (!item.text_item?.text)
|
|
203
|
+
continue;
|
|
204
|
+
let text = item.text_item.text;
|
|
205
|
+
if (item.ref_msg?.title) {
|
|
206
|
+
text = `[引用: ${item.ref_msg.title}]\n${text}`;
|
|
207
|
+
}
|
|
208
|
+
return text;
|
|
209
|
+
}
|
|
210
|
+
case 3 /* MessageItemType.VOICE */: {
|
|
211
|
+
const transcript = item.voice_item?.text;
|
|
212
|
+
if (transcript)
|
|
213
|
+
return `[语音转文字] ${transcript}`;
|
|
214
|
+
return '[语音消息(无文字转录)]';
|
|
215
|
+
}
|
|
216
|
+
case 2 /* MessageItemType.IMAGE */:
|
|
217
|
+
return '[图片]';
|
|
218
|
+
case 4 /* MessageItemType.FILE */: {
|
|
219
|
+
const name = item.file_item?.file_name ? ` "${item.file_item.file_name}"` : '';
|
|
220
|
+
return `[文件${name}]`;
|
|
221
|
+
}
|
|
222
|
+
case 5 /* MessageItemType.VIDEO */:
|
|
223
|
+
return '[视频]';
|
|
224
|
+
default:
|
|
225
|
+
return `[未知消息类型 ${item.type}]`;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* POST to iLink API with JSON body and Bearer token auth.
|
|
232
|
+
*/
|
|
233
|
+
async function postApi(endpoint, body, signal) {
|
|
234
|
+
const url = `${apiUrl}${endpoint}`;
|
|
235
|
+
const bodyStr = JSON.stringify(body);
|
|
236
|
+
const headers = {
|
|
237
|
+
'Content-Type': 'application/json',
|
|
238
|
+
'AuthorizationType': 'ilink_bot_token',
|
|
239
|
+
'X-WECHAT-UIN': randomBytes(4).readUInt32BE(0).toString(10),
|
|
240
|
+
'Authorization': `Bearer ${apiToken}`,
|
|
241
|
+
};
|
|
149
242
|
try {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
243
|
+
const res = await fetch(url, {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
headers,
|
|
246
|
+
body: bodyStr,
|
|
247
|
+
signal,
|
|
248
|
+
});
|
|
249
|
+
const text = await res.text();
|
|
250
|
+
const raw = JSON.parse(text);
|
|
251
|
+
// iLink API: { ret: 0 } for success, { errcode: -14 } for session timeout, etc.
|
|
252
|
+
const ret = typeof raw.ret === 'number' ? raw.ret : undefined;
|
|
253
|
+
const errcode = typeof raw.errcode === 'number' ? raw.errcode : undefined;
|
|
254
|
+
const ok = ret === 0 || ret === undefined;
|
|
255
|
+
const error = ok ? undefined : String(raw.errmsg ?? raw.msg ?? `ret=${ret}`);
|
|
256
|
+
if (!ok) {
|
|
257
|
+
log.warn(`ClawBot API ${endpoint} response: ${text.substring(0, 500)}`);
|
|
153
258
|
}
|
|
154
|
-
return
|
|
259
|
+
return {
|
|
260
|
+
ok,
|
|
261
|
+
error,
|
|
262
|
+
errcode,
|
|
263
|
+
updatesBuf: typeof raw.get_updates_buf === 'string' ? raw.get_updates_buf : undefined,
|
|
264
|
+
messages: Array.isArray(raw.msgs) ? raw.msgs : undefined,
|
|
265
|
+
};
|
|
155
266
|
}
|
|
156
|
-
catch {
|
|
157
|
-
|
|
158
|
-
|
|
267
|
+
catch (err) {
|
|
268
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
269
|
+
throw err;
|
|
270
|
+
}
|
|
271
|
+
log.warn(`ClawBot API ${endpoint} error:`, err);
|
|
272
|
+
throw err;
|
|
159
273
|
}
|
|
160
274
|
}
|
|
161
275
|
function sleep(ms, signal) {
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ClawBot Message Sender - Send messages via iLink API
|
|
2
|
+
* ClawBot Message Sender - Send messages via iLink Bot API
|
|
3
|
+
*
|
|
4
|
+
* Uses POST + JSON body + Bearer token auth (iLink protocol).
|
|
3
5
|
*/
|
|
4
6
|
export declare function initClawBotSender(url: string, token: string): void;
|
|
7
|
+
/** Cache a context_token for a chatId (called when receiving messages) */
|
|
8
|
+
export declare function cacheContextToken(chatId: string, token: string): void;
|
|
9
|
+
/** Get cached context_token for a chatId */
|
|
10
|
+
export declare function getCachedContextToken(chatId: string): string | undefined;
|
|
5
11
|
/**
|
|
6
12
|
* Send text reply to a ClawBot chat, splitting long messages automatically.
|
|
7
13
|
*/
|
|
8
|
-
export declare function sendTextReply(chatId: string, text: string): Promise<void>;
|
|
14
|
+
export declare function sendTextReply(chatId: string, text: string, contextToken?: string): Promise<void>;
|
|
9
15
|
/**
|
|
10
16
|
* Send error reply to a ClawBot chat.
|
|
11
17
|
*/
|
|
@@ -1,36 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ClawBot Message Sender - Send messages via iLink API
|
|
2
|
+
* ClawBot Message Sender - Send messages via iLink Bot API
|
|
3
|
+
*
|
|
4
|
+
* Uses POST + JSON body + Bearer token auth (iLink protocol).
|
|
3
5
|
*/
|
|
6
|
+
import { randomBytes } from 'node:crypto';
|
|
4
7
|
import { createLogger } from '../logger.js';
|
|
5
8
|
import { splitLongContent, toReplyPlainText } from '../shared/utils.js';
|
|
6
9
|
import { MAX_CLAWBOT_MESSAGE_LENGTH } from '../constants.js';
|
|
7
10
|
import { getChannelState } from './client.js';
|
|
8
11
|
const log = createLogger('ClawBotSender');
|
|
9
|
-
let apiUrl = '
|
|
12
|
+
let apiUrl = 'https://ilinkai.weixin.qq.com';
|
|
10
13
|
let apiToken = '';
|
|
14
|
+
/** Cache of context_token per chatId, populated by incoming messages */
|
|
15
|
+
const contextTokenCache = new Map();
|
|
11
16
|
export function initClawBotSender(url, token) {
|
|
12
17
|
apiUrl = url;
|
|
13
18
|
apiToken = token;
|
|
14
19
|
}
|
|
15
|
-
|
|
20
|
+
/** Cache a context_token for a chatId (called when receiving messages) */
|
|
21
|
+
export function cacheContextToken(chatId, token) {
|
|
22
|
+
contextTokenCache.set(chatId, token);
|
|
23
|
+
}
|
|
24
|
+
/** Get cached context_token for a chatId */
|
|
25
|
+
export function getCachedContextToken(chatId) {
|
|
26
|
+
return contextTokenCache.get(chatId);
|
|
27
|
+
}
|
|
28
|
+
/** Build iLink API request headers */
|
|
29
|
+
function buildHeaders() {
|
|
30
|
+
return {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
'AuthorizationType': 'ilink_bot_token',
|
|
33
|
+
'X-WECHAT-UIN': randomBytes(4).readUInt32BE(0).toString(10),
|
|
34
|
+
'Authorization': `Bearer ${apiToken}`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Generate a unique client_id for outbound messages */
|
|
38
|
+
function generateClientId() {
|
|
39
|
+
return `open-im:${Date.now()}-${randomBytes(4).toString('hex')}`;
|
|
40
|
+
}
|
|
41
|
+
async function postMessage(chatId, text, contextToken) {
|
|
16
42
|
if (getChannelState() !== 'connected') {
|
|
17
43
|
log.warn('ClawBot not connected, cannot send message');
|
|
18
44
|
return false;
|
|
19
45
|
}
|
|
46
|
+
const token = contextToken ?? getCachedContextToken(chatId);
|
|
47
|
+
if (!token) {
|
|
48
|
+
log.warn(`ClawBot no context_token for chatId=${chatId}, cannot send reply`);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
20
51
|
try {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
52
|
+
const url = `${apiUrl}/ilink/bot/sendmessage`;
|
|
53
|
+
const body = JSON.stringify({
|
|
54
|
+
msg: {
|
|
55
|
+
from_user_id: '',
|
|
56
|
+
to_user_id: chatId,
|
|
57
|
+
client_id: generateClientId(),
|
|
58
|
+
message_type: 2, // BOT
|
|
59
|
+
message_state: 2, // FINISH
|
|
60
|
+
item_list: [{ type: 1, text_item: { text } }],
|
|
61
|
+
context_token: token,
|
|
28
62
|
},
|
|
29
|
-
|
|
63
|
+
base_info: { channel_version: '0.1.0' },
|
|
64
|
+
});
|
|
65
|
+
const res = await fetch(url, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: buildHeaders(),
|
|
68
|
+
body,
|
|
30
69
|
});
|
|
31
70
|
const data = await res.json();
|
|
32
|
-
|
|
33
|
-
|
|
71
|
+
const ok = data.ret === 0 || data.ret === undefined;
|
|
72
|
+
if (!ok) {
|
|
73
|
+
log.error(`ClawBot sendmessage failed: ret=${data.ret} errcode=${data.errcode} errmsg=${data.errmsg}`);
|
|
34
74
|
return false;
|
|
35
75
|
}
|
|
36
76
|
return true;
|
|
@@ -43,12 +83,12 @@ async function postMessage(chatId, text) {
|
|
|
43
83
|
/**
|
|
44
84
|
* Send text reply to a ClawBot chat, splitting long messages automatically.
|
|
45
85
|
*/
|
|
46
|
-
export async function sendTextReply(chatId, text) {
|
|
86
|
+
export async function sendTextReply(chatId, text, contextToken) {
|
|
47
87
|
const plainText = toReplyPlainText(text);
|
|
48
88
|
const parts = splitLongContent(plainText, MAX_CLAWBOT_MESSAGE_LENGTH);
|
|
49
89
|
if (parts.length === 1) {
|
|
50
90
|
log.info(`Sending ClawBot reply to chatId=${chatId}, len=${plainText.length}`);
|
|
51
|
-
await postMessage(chatId, plainText);
|
|
91
|
+
await postMessage(chatId, plainText, contextToken);
|
|
52
92
|
return;
|
|
53
93
|
}
|
|
54
94
|
log.info(`Sending ClawBot reply in ${parts.length} parts to chatId=${chatId}, totalLen=${plainText.length}`);
|
|
@@ -56,7 +96,7 @@ export async function sendTextReply(chatId, text) {
|
|
|
56
96
|
const partText = i === 0
|
|
57
97
|
? `${parts[i]}\n\n_(1/${parts.length})_`
|
|
58
98
|
: `_(续 ${i + 1}/${parts.length})_\n\n${parts[i]}`;
|
|
59
|
-
await postMessage(chatId, partText);
|
|
99
|
+
await postMessage(chatId, partText, contextToken);
|
|
60
100
|
log.info(`ClawBot part ${i + 1}/${parts.length} sent`);
|
|
61
101
|
}
|
|
62
102
|
}
|
package/dist/clawbot/types.d.ts
CHANGED
|
@@ -1,43 +1,111 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ClawBot Types - WeChat iLink API
|
|
2
|
+
* ClawBot Types - WeChat iLink Bot API
|
|
3
|
+
*
|
|
4
|
+
* Matches the official iLink API protocol (POST + JSON body + Bearer token).
|
|
5
|
+
* Reference: @tencent-weixin/openclaw-weixin, cc-wechat, claude-code-wechat-channel
|
|
3
6
|
*/
|
|
4
7
|
/** Connection state */
|
|
5
8
|
export type ClawBotState = 'disconnected' | 'connecting' | 'connected' | 'error';
|
|
6
9
|
/** ClawBot configuration */
|
|
7
10
|
export interface ClawBotConfig {
|
|
8
|
-
/** iLink API base URL (default:
|
|
11
|
+
/** iLink API base URL (default: https://ilinkai.weixin.qq.com) */
|
|
9
12
|
apiUrl: string;
|
|
10
13
|
/** Bearer token for authentication */
|
|
11
14
|
apiToken: string;
|
|
12
15
|
}
|
|
13
|
-
/**
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
/** iLink message content item types */
|
|
17
|
+
export declare const enum MessageItemType {
|
|
18
|
+
NONE = 0,
|
|
19
|
+
TEXT = 1,
|
|
20
|
+
IMAGE = 2,
|
|
21
|
+
VOICE = 3,
|
|
22
|
+
FILE = 4,
|
|
23
|
+
VIDEO = 5
|
|
24
|
+
}
|
|
25
|
+
/** Text content item */
|
|
26
|
+
export interface TextItem {
|
|
27
|
+
text?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Image content item */
|
|
30
|
+
export interface ImageItem {
|
|
31
|
+
media?: {
|
|
32
|
+
aes_key?: string;
|
|
33
|
+
cdn_url?: string;
|
|
34
|
+
};
|
|
35
|
+
width?: number;
|
|
36
|
+
height?: number;
|
|
37
|
+
}
|
|
38
|
+
/** Voice content item */
|
|
39
|
+
export interface VoiceItem {
|
|
40
|
+
text?: string;
|
|
41
|
+
media?: {
|
|
42
|
+
aes_key?: string;
|
|
43
|
+
cdn_url?: string;
|
|
44
|
+
};
|
|
45
|
+
playtime?: number;
|
|
46
|
+
}
|
|
47
|
+
/** File content item */
|
|
48
|
+
export interface FileItem {
|
|
49
|
+
file_name?: string;
|
|
50
|
+
file_size?: number;
|
|
51
|
+
media?: {
|
|
52
|
+
aes_key?: string;
|
|
53
|
+
cdn_url?: string;
|
|
28
54
|
};
|
|
29
55
|
}
|
|
56
|
+
/** Video content item */
|
|
57
|
+
export interface VideoItem {
|
|
58
|
+
media?: {
|
|
59
|
+
aes_key?: string;
|
|
60
|
+
cdn_url?: string;
|
|
61
|
+
};
|
|
62
|
+
duration_ms?: number;
|
|
63
|
+
}
|
|
64
|
+
/** A single content item in a message */
|
|
65
|
+
export interface MessageItem {
|
|
66
|
+
type?: number;
|
|
67
|
+
text_item?: TextItem;
|
|
68
|
+
image_item?: ImageItem;
|
|
69
|
+
voice_item?: VoiceItem;
|
|
70
|
+
file_item?: FileItem;
|
|
71
|
+
video_item?: VideoItem;
|
|
72
|
+
ref_msg?: {
|
|
73
|
+
title?: string;
|
|
74
|
+
message_item?: MessageItem;
|
|
75
|
+
};
|
|
76
|
+
msg_id?: string;
|
|
77
|
+
}
|
|
78
|
+
/** iLink message from getupdates */
|
|
79
|
+
export interface ILinkMessage {
|
|
80
|
+
seq?: number;
|
|
81
|
+
message_id?: number;
|
|
82
|
+
from_user_id?: string;
|
|
83
|
+
to_user_id?: string;
|
|
84
|
+
client_id?: string;
|
|
85
|
+
create_time_ms?: number;
|
|
86
|
+
session_id?: string;
|
|
87
|
+
/** 1=USER (inbound), 2=BOT (outbound) */
|
|
88
|
+
message_type?: number;
|
|
89
|
+
/** 0=NEW, 1=GENERATING, 2=FINISH */
|
|
90
|
+
message_state?: number;
|
|
91
|
+
item_list?: MessageItem[];
|
|
92
|
+
/** Token required for sending replies to this conversation */
|
|
93
|
+
context_token?: string;
|
|
94
|
+
group_id?: string;
|
|
95
|
+
}
|
|
30
96
|
/** getupdates response */
|
|
31
|
-
export interface
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
97
|
+
export interface GetUpdatesResponse {
|
|
98
|
+
ret?: number;
|
|
99
|
+
errcode?: number;
|
|
100
|
+
errmsg?: string;
|
|
101
|
+
msgs?: ILinkMessage[];
|
|
102
|
+
/** Opaque cursor for next poll — pass back as-is */
|
|
103
|
+
get_updates_buf?: string;
|
|
104
|
+
longpolling_timeout_ms?: number;
|
|
35
105
|
}
|
|
36
106
|
/** sendmessage response */
|
|
37
|
-
export interface
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
};
|
|
42
|
-
error?: string;
|
|
107
|
+
export interface SendMessageResponse {
|
|
108
|
+
ret?: number;
|
|
109
|
+
errcode?: number;
|
|
110
|
+
errmsg?: string;
|
|
43
111
|
}
|
package/dist/clawbot/types.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ClawBot Types - WeChat iLink API
|
|
2
|
+
* ClawBot Types - WeChat iLink Bot API
|
|
3
|
+
*
|
|
4
|
+
* Matches the official iLink API protocol (POST + JSON body + Bearer token).
|
|
5
|
+
* Reference: @tencent-weixin/openclaw-weixin, cc-wechat, claude-code-wechat-channel
|
|
3
6
|
*/
|
|
4
7
|
export {};
|
package/dist/config-web.js
CHANGED
|
@@ -625,12 +625,20 @@ async function probeClawBot(config) {
|
|
|
625
625
|
const apiToken = clean(String(config.apiToken ?? ""));
|
|
626
626
|
if (!apiToken)
|
|
627
627
|
throw new Error("ClawBot API token is required.");
|
|
628
|
-
const
|
|
629
|
-
|
|
628
|
+
const { randomUUID } = await import('node:crypto');
|
|
629
|
+
const response = await fetch(`${apiUrl}/ilink/bot/getupdates?timeout=1&bot_token=${encodeURIComponent(apiToken)}`, {
|
|
630
|
+
headers: {
|
|
631
|
+
'Content-Type': 'application/json',
|
|
632
|
+
'AuthorizationType': 'ilink_bot_token',
|
|
633
|
+
'iLink-App-Id': 'bot',
|
|
634
|
+
'iLink-App-ClientVersion': '131588',
|
|
635
|
+
'X-WECHAT-UIN': randomUUID(),
|
|
636
|
+
},
|
|
630
637
|
signal: AbortSignal.timeout(TEST_TIMEOUT_MS),
|
|
631
638
|
});
|
|
632
639
|
const body = await readJsonResponse(response);
|
|
633
|
-
|
|
640
|
+
const ok = body.ok === true || body.ret === 0 || body.ret === '0';
|
|
641
|
+
if (!response.ok || !ok) {
|
|
634
642
|
throw new Error(String(body.error ?? body.description ?? `HTTP ${response.status}`));
|
|
635
643
|
}
|
|
636
644
|
return "ClawBot API reachable.";
|