@wu529778790/open-im 1.10.9-beta.2 → 1.10.9-beta.21

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.
Files changed (56) hide show
  1. package/README.md +43 -62
  2. package/README.zh-CN.md +43 -62
  3. package/dist/adapters/claude-sdk-adapter.d.ts +13 -0
  4. package/dist/adapters/claude-sdk-adapter.js +221 -23
  5. package/dist/adapters/registry.js +3 -0
  6. package/dist/channels/capabilities.js +5 -0
  7. package/dist/clawbot/client.d.ts +14 -0
  8. package/dist/clawbot/client.js +299 -0
  9. package/dist/clawbot/event-handler.d.ts +12 -0
  10. package/dist/clawbot/event-handler.js +85 -0
  11. package/dist/clawbot/message-sender.d.ts +18 -0
  12. package/dist/clawbot/message-sender.js +109 -0
  13. package/dist/clawbot/qr-login.d.ts +33 -0
  14. package/dist/clawbot/qr-login.js +120 -0
  15. package/dist/clawbot/types.d.ts +111 -0
  16. package/dist/clawbot/types.js +7 -0
  17. package/dist/codebuddy/cli-runner.js +31 -2
  18. package/dist/codex/cli-runner.js +28 -2
  19. package/dist/config/file-io.d.ts +6 -3
  20. package/dist/config/file-io.js +12 -7
  21. package/dist/config/types.d.ts +17 -1
  22. package/dist/config-web-page-i18n.d.ts +24 -2
  23. package/dist/config-web-page-i18n.js +24 -2
  24. package/dist/config-web.js +79 -0
  25. package/dist/config.d.ts +1 -1
  26. package/dist/config.js +38 -2
  27. package/dist/constants.d.ts +6 -0
  28. package/dist/constants.js +6 -0
  29. package/dist/dingtalk/client.js +2 -1
  30. package/dist/dingtalk/event-handler.js +1 -1
  31. package/dist/index.js +50 -0
  32. package/dist/qq/client.js +7 -1
  33. package/dist/queue/request-queue.js +11 -10
  34. package/dist/setup.js +131 -3
  35. package/dist/shared/active-chats.d.ts +2 -2
  36. package/dist/shared/ai-task.d.ts +14 -0
  37. package/dist/shared/ai-task.js +57 -9
  38. package/dist/shared/process-kill.d.ts +24 -0
  39. package/dist/shared/process-kill.js +79 -0
  40. package/dist/shared/reconnect.d.ts +28 -0
  41. package/dist/shared/reconnect.js +56 -0
  42. package/dist/shared/task-cleanup.d.ts +16 -0
  43. package/dist/shared/task-cleanup.js +34 -1
  44. package/dist/telegram/client.js +7 -1
  45. package/dist/telemetry/telemetry-upload.js +1 -1
  46. package/dist/wework/client.js +17 -5
  47. package/dist/wework/event-handler.js +3 -0
  48. package/dist/workbuddy/centrifuge-client.d.ts +4 -0
  49. package/dist/workbuddy/centrifuge-client.js +76 -28
  50. package/dist/workbuddy/client.js +39 -2
  51. package/package.json +1 -1
  52. package/web/dist/assets/index-BaLTMeeF.js +57 -0
  53. package/web/dist/index.html +1 -1
  54. package/dist/config/credentials.d.ts +0 -19
  55. package/dist/config/credentials.js +0 -36
  56. package/web/dist/assets/index-B-oVSMUp.js +0 -57
@@ -0,0 +1,299 @@
1
+ /**
2
+ * ClawBot Client - WeChat iLink Bot API long-polling client
3
+ *
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
9
+ */
10
+ import { randomBytes } from 'node:crypto';
11
+ import { createLogger } from '../logger.js';
12
+ import { jitteredDelay, isFatalReconnectError, SLOW_PROBE_MS } from '../shared/reconnect.js';
13
+ import { cacheContextToken } from './message-sender.js';
14
+ import { CLAWBOT_POLL_INTERVAL_MS } from '../constants.js';
15
+ const log = createLogger('ClawBot');
16
+ const RECONNECT_DELAYS_MS = [3000, 5000, 10000, 20000, 30000];
17
+ const BASE_INFO = { channel_version: '0.1.0' };
18
+ let pollController = null;
19
+ let channelState = 'disconnected';
20
+ let messageHandler = null;
21
+ let stateChangeHandler = null;
22
+ let reconnectTimer = null;
23
+ let reconnectAttempt = 0;
24
+ let fatal = false;
25
+ let stopped = false;
26
+ let apiUrl = 'https://ilinkai.weixin.qq.com';
27
+ let apiToken = '';
28
+ /** Opaque cursor for getupdates pagination (replaces numeric offset) */
29
+ let getUpdatesBuf = '';
30
+ export function getChannelState() {
31
+ return channelState;
32
+ }
33
+ export async function initClawbot(config, eventHandler, onStateChange) {
34
+ const pc = config.platforms?.clawbot;
35
+ if (!pc?.enabled) {
36
+ throw new Error('ClawBot platform not enabled');
37
+ }
38
+ if (!pc.apiToken) {
39
+ throw new Error('ClawBot apiToken required');
40
+ }
41
+ apiUrl = pc.apiUrl ?? 'https://ilinkai.weixin.qq.com';
42
+ apiToken = pc.apiToken;
43
+ messageHandler = eventHandler;
44
+ stateChangeHandler = onStateChange ?? null;
45
+ stopped = false;
46
+ reconnectAttempt = 0;
47
+ fatal = false;
48
+ getUpdatesBuf = '';
49
+ // Verify connectivity — non-fatal, reconnect loop will retry
50
+ try {
51
+ const res = await postApi('/ilink/bot/getupdates', {
52
+ get_updates_buf: '',
53
+ base_info: BASE_INFO,
54
+ });
55
+ if (!res.ok) {
56
+ throw new Error(`API check failed: ${res.error ?? 'unknown'}`);
57
+ }
58
+ log.info(`ClawBot API reachable at ${apiUrl}`);
59
+ fatal = false;
60
+ reconnectAttempt = 0;
61
+ if (res.updatesBuf)
62
+ getUpdatesBuf = res.updatesBuf;
63
+ updateState('connected');
64
+ startPolling();
65
+ }
66
+ catch (err) {
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
+ }
74
+ updateState('connecting');
75
+ scheduleReconnect();
76
+ }
77
+ log.info('ClawBot client initialized');
78
+ }
79
+ function startPolling() {
80
+ if (stopped || pollController)
81
+ return;
82
+ pollController = new AbortController();
83
+ const signal = pollController.signal;
84
+ (async () => {
85
+ log.info('ClawBot long-polling started');
86
+ while (!stopped && !signal.aborted) {
87
+ try {
88
+ const res = await postApi('/ilink/bot/getupdates', {
89
+ get_updates_buf: getUpdatesBuf,
90
+ base_info: BASE_INFO,
91
+ }, signal);
92
+ if (signal.aborted)
93
+ break;
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
+ }
104
+ log.warn(`ClawBot getupdates error: ${res.error ?? 'unknown'}`);
105
+ await sleep(CLAWBOT_POLL_INTERVAL_MS, signal);
106
+ continue;
107
+ }
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) {
121
+ if (signal.aborted)
122
+ break;
123
+ if (msg.message_type !== 1)
124
+ continue; // skip BOT messages, only process USER
125
+ const extracted = extractTextContent(msg);
126
+ if (!extracted)
127
+ continue;
128
+ const chatId = msg.from_user_id ?? '';
129
+ const msgId = String(msg.message_id ?? msg.seq ?? '');
130
+ const content = extracted;
131
+ if (!chatId) {
132
+ log.warn('ClawBot message missing from_user_id, skipping');
133
+ continue;
134
+ }
135
+ // Cache context_token for reply capability
136
+ if (msg.context_token) {
137
+ cacheContextToken(chatId, msg.context_token);
138
+ }
139
+ log.info(`ClawBot message: chatId=${chatId}, msgId=${msgId}, content="${content.substring(0, 100)}"`);
140
+ if (messageHandler) {
141
+ try {
142
+ await messageHandler(chatId, msgId, content);
143
+ }
144
+ catch (err) {
145
+ log.error('Error in ClawBot message handler:', err);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ catch (err) {
151
+ if (signal.aborted)
152
+ break;
153
+ if (err instanceof Error && err.name === 'AbortError')
154
+ break;
155
+ log.error('ClawBot polling error:', err);
156
+ updateState('error');
157
+ scheduleReconnect();
158
+ return;
159
+ }
160
+ }
161
+ })();
162
+ }
163
+ function scheduleReconnect() {
164
+ if (stopped)
165
+ return;
166
+ if (reconnectTimer) {
167
+ clearTimeout(reconnectTimer);
168
+ reconnectTimer = null;
169
+ }
170
+ const baseDelay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
171
+ const delay = fatal ? jitteredDelay(SLOW_PROBE_MS) : jitteredDelay(baseDelay);
172
+ reconnectAttempt++;
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
+ }
179
+ reconnectTimer = setTimeout(() => {
180
+ reconnectTimer = null;
181
+ if (stopped)
182
+ return;
183
+ updateState('connected');
184
+ startPolling();
185
+ }, delay);
186
+ }
187
+ function updateState(state) {
188
+ channelState = state;
189
+ stateChangeHandler?.(state);
190
+ log.debug(`ClawBot state: ${state}`);
191
+ }
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
+ };
242
+ try {
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)}`);
258
+ }
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
+ };
266
+ }
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;
273
+ }
274
+ }
275
+ function sleep(ms, signal) {
276
+ return new Promise((resolve) => {
277
+ if (signal?.aborted) {
278
+ resolve();
279
+ return;
280
+ }
281
+ const timer = setTimeout(resolve, ms);
282
+ signal?.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true });
283
+ });
284
+ }
285
+ export function stopClawbot() {
286
+ log.info('Stopping ClawBot client...');
287
+ stopped = true;
288
+ if (pollController) {
289
+ pollController.abort();
290
+ pollController = null;
291
+ }
292
+ if (reconnectTimer) {
293
+ clearTimeout(reconnectTimer);
294
+ reconnectTimer = null;
295
+ }
296
+ messageHandler = null;
297
+ updateState('disconnected');
298
+ log.info('ClawBot client stopped');
299
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * ClawBot Event Handler - Handle messages from iLink API long-polling
3
+ */
4
+ import type { Config } from '../config.js';
5
+ import type { SessionManager } from '../session/session-manager.js';
6
+ export interface ClawBotEventHandlerHandle {
7
+ stop: () => void;
8
+ runningTasks: Map<string, import('../shared/ai-task.js').TaskRunState>;
9
+ getRunningTaskCount: () => number;
10
+ handleEvent: (chatId: string, msgId: string, content: string) => Promise<void>;
11
+ }
12
+ export declare function setupClawbotHandlers(config: Config, sessionManager: SessionManager): ClawBotEventHandlerHandle;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * ClawBot Event Handler - Handle messages from iLink API long-polling
3
+ */
4
+ import { sendTextReply, sendErrorReply } from './message-sender.js';
5
+ import { startTaskCleanup } from '../shared/task-cleanup.js';
6
+ import { CLAWBOT_THROTTLE_MS } from '../constants.js';
7
+ import { createLogger } from '../logger.js';
8
+ import { createPlatformEventContext } from '../platform/create-event-context.js';
9
+ import { createPlatformAIRequestHandler } from '../platform/handle-ai-request.js';
10
+ import { handleTextFlow } from '../platform/handle-text-flow.js';
11
+ const log = createLogger('ClawBotHandler');
12
+ export function setupClawbotHandlers(config, sessionManager) {
13
+ const ctx = createPlatformEventContext({
14
+ platform: 'clawbot',
15
+ allowedUserIds: config.clawbotAllowedUserIds,
16
+ config,
17
+ sessionManager,
18
+ sender: {
19
+ sendTextReply: async (chatId, text) => {
20
+ await sendTextReply(chatId, text);
21
+ },
22
+ },
23
+ });
24
+ const stopTaskCleanup = startTaskCleanup(ctx.runningTasks);
25
+ const platformSender = {
26
+ sendThinkingMessage: async (_chatId, _replyToMessageId, _toolId) => {
27
+ return 'clawbot_no_thinking';
28
+ },
29
+ sendTextReply: async (chatId, text) => {
30
+ await sendTextReply(chatId, text);
31
+ },
32
+ startTyping: (_chatId) => {
33
+ return () => { };
34
+ },
35
+ };
36
+ async function handleEvent(chatId, msgId, content) {
37
+ log.info(`[handleEvent] chatId=${chatId}, msgId=${msgId}, content="${content.substring(0, 100)}"`);
38
+ const userId = chatId;
39
+ const text = content.trim();
40
+ const msgIdSender = {
41
+ ...platformSender,
42
+ sendTextReply: async (c, t) => {
43
+ await sendTextReply(c, t);
44
+ },
45
+ };
46
+ const handleAIRequest = createPlatformAIRequestHandler({
47
+ platform: 'clawbot',
48
+ config,
49
+ sessionManager,
50
+ sender: msgIdSender,
51
+ throttleMs: CLAWBOT_THROTTLE_MS,
52
+ runningTasks: ctx.runningTasks,
53
+ taskKeyBuilder: (userId, _msgId) => `${userId}:${msgId}`,
54
+ taskCallbacksFactory: ({ chatId: c }) => ({
55
+ streamUpdate: async () => { },
56
+ sendComplete: async (content) => {
57
+ await sendTextReply(c, content);
58
+ },
59
+ sendError: async (error) => {
60
+ await sendErrorReply(c, error);
61
+ },
62
+ }),
63
+ });
64
+ await handleTextFlow({
65
+ platform: 'clawbot',
66
+ userId,
67
+ chatId,
68
+ text,
69
+ ctx,
70
+ handleAIRequest,
71
+ sendTextReply: (c, t) => sendTextReply(c, t),
72
+ workDir: sessionManager.getWorkDir(userId),
73
+ convId: sessionManager.getConvId(userId),
74
+ accessDeniedMessage: (uid) => `抱歉,您没有访问权限。\n您的 ID: ${uid}`,
75
+ queueFullMessage: '请求队列已满,请稍后再试。',
76
+ queuedMessage: '您的请求已排队等待。',
77
+ });
78
+ }
79
+ return {
80
+ stop: () => stopTaskCleanup(),
81
+ runningTasks: ctx.runningTasks,
82
+ getRunningTaskCount: () => ctx.runningTasks.size,
83
+ handleEvent,
84
+ };
85
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * ClawBot Message Sender - Send messages via iLink Bot API
3
+ *
4
+ * Uses POST + JSON body + Bearer token auth (iLink protocol).
5
+ */
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;
11
+ /**
12
+ * Send text reply to a ClawBot chat, splitting long messages automatically.
13
+ */
14
+ export declare function sendTextReply(chatId: string, text: string, contextToken?: string): Promise<void>;
15
+ /**
16
+ * Send error reply to a ClawBot chat.
17
+ */
18
+ export declare function sendErrorReply(chatId: string, error: string): Promise<void>;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * ClawBot Message Sender - Send messages via iLink Bot API
3
+ *
4
+ * Uses POST + JSON body + Bearer token auth (iLink protocol).
5
+ */
6
+ import { randomBytes } from 'node:crypto';
7
+ import { createLogger } from '../logger.js';
8
+ import { splitLongContent, toReplyPlainText } from '../shared/utils.js';
9
+ import { MAX_CLAWBOT_MESSAGE_LENGTH } from '../constants.js';
10
+ import { getChannelState } from './client.js';
11
+ const log = createLogger('ClawBotSender');
12
+ let apiUrl = 'https://ilinkai.weixin.qq.com';
13
+ let apiToken = '';
14
+ /** Cache of context_token per chatId, populated by incoming messages */
15
+ const contextTokenCache = new Map();
16
+ export function initClawBotSender(url, token) {
17
+ apiUrl = url;
18
+ apiToken = token;
19
+ }
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) {
42
+ if (getChannelState() !== 'connected') {
43
+ log.warn('ClawBot not connected, cannot send message');
44
+ return false;
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
+ }
51
+ try {
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,
62
+ },
63
+ base_info: { channel_version: '0.1.0' },
64
+ });
65
+ const res = await fetch(url, {
66
+ method: 'POST',
67
+ headers: buildHeaders(),
68
+ body,
69
+ });
70
+ const data = await res.json();
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}`);
74
+ return false;
75
+ }
76
+ return true;
77
+ }
78
+ catch (err) {
79
+ log.error('ClawBot sendmessage error:', err);
80
+ return false;
81
+ }
82
+ }
83
+ /**
84
+ * Send text reply to a ClawBot chat, splitting long messages automatically.
85
+ */
86
+ export async function sendTextReply(chatId, text, contextToken) {
87
+ const plainText = toReplyPlainText(text);
88
+ const parts = splitLongContent(plainText, MAX_CLAWBOT_MESSAGE_LENGTH);
89
+ if (parts.length === 1) {
90
+ log.info(`Sending ClawBot reply to chatId=${chatId}, len=${plainText.length}`);
91
+ await postMessage(chatId, plainText, contextToken);
92
+ return;
93
+ }
94
+ log.info(`Sending ClawBot reply in ${parts.length} parts to chatId=${chatId}, totalLen=${plainText.length}`);
95
+ for (let i = 0; i < parts.length; i++) {
96
+ const partText = i === 0
97
+ ? `${parts[i]}\n\n_(1/${parts.length})_`
98
+ : `_(续 ${i + 1}/${parts.length})_\n\n${parts[i]}`;
99
+ await postMessage(chatId, partText, contextToken);
100
+ log.info(`ClawBot part ${i + 1}/${parts.length} sent`);
101
+ }
102
+ }
103
+ /**
104
+ * Send error reply to a ClawBot chat.
105
+ */
106
+ export async function sendErrorReply(chatId, error) {
107
+ log.warn(`Sending ClawBot error to chatId=${chatId}`);
108
+ await postMessage(chatId, `错误: ${error}`);
109
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * ClawBot QR Code Login - WeChat iLink API
3
+ *
4
+ * Calls https://ilinkai.weixin.qq.com to get QR code and poll for login confirmation.
5
+ * Returns bot_token on success.
6
+ */
7
+ export interface QRLoginSession {
8
+ sessionKey: string;
9
+ qrcode: string;
10
+ qrcodeUrl: string;
11
+ startedAt: number;
12
+ }
13
+ export interface QRLoginResult {
14
+ connected: boolean;
15
+ botToken?: string;
16
+ accountId?: string;
17
+ baseUrl?: string;
18
+ userId?: string;
19
+ message: string;
20
+ }
21
+ export declare function fetchQRCode(): Promise<{
22
+ qrcode: string;
23
+ qrcodeUrl: string;
24
+ }>;
25
+ /**
26
+ * Start QR code login session. Returns QR code URL for display.
27
+ */
28
+ export declare function startQRLogin(): Promise<QRLoginSession>;
29
+ /**
30
+ * Wait for user to scan QR code. Polls until confirmed or timeout.
31
+ * Returns bot_token on success.
32
+ */
33
+ export declare function waitForQRLogin(session: QRLoginSession, onStatusChange?: (status: string) => void): Promise<QRLoginResult>;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * ClawBot QR Code Login - WeChat iLink API
3
+ *
4
+ * Calls https://ilinkai.weixin.qq.com to get QR code and poll for login confirmation.
5
+ * Returns bot_token on success.
6
+ */
7
+ import { randomUUID } from 'node:crypto';
8
+ import { createLogger } from '../logger.js';
9
+ const log = createLogger('ClawBotQR');
10
+ const ILINK_BASE_URL = 'https://ilinkai.weixin.qq.com';
11
+ const ILINK_APP_ID = 'bot';
12
+ const BOT_TYPE = '3';
13
+ const POLL_TIMEOUT_MS = 35_000;
14
+ const LOGIN_TIMEOUT_MS = 5 * 60_000;
15
+ function buildHeaders() {
16
+ return {
17
+ 'Content-Type': 'application/json',
18
+ 'AuthorizationType': 'ilink_bot_token',
19
+ 'X-WECHAT-UIN': randomUUID(),
20
+ 'iLink-App-Id': ILINK_APP_ID,
21
+ 'iLink-App-ClientVersion': '131588', // 2.4.4 encoded
22
+ };
23
+ }
24
+ export async function fetchQRCode() {
25
+ const url = `${ILINK_BASE_URL}/ilink/bot/get_bot_qrcode?bot_type=${BOT_TYPE}`;
26
+ const res = await fetch(url, {
27
+ method: 'POST',
28
+ headers: buildHeaders(),
29
+ body: JSON.stringify({ local_token_list: [] }),
30
+ });
31
+ const data = await res.json();
32
+ if (!data.qrcode) {
33
+ throw new Error('Failed to get QR code from iLink API');
34
+ }
35
+ return { qrcode: data.qrcode, qrcodeUrl: data.qrcode_img_content };
36
+ }
37
+ async function pollQRStatus(qrcode) {
38
+ const url = `${ILINK_BASE_URL}/ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`;
39
+ const controller = new AbortController();
40
+ const timer = setTimeout(() => controller.abort(), POLL_TIMEOUT_MS);
41
+ try {
42
+ const headers = buildHeaders();
43
+ delete headers['Content-Type'];
44
+ const res = await fetch(url, {
45
+ method: 'GET',
46
+ headers,
47
+ signal: controller.signal,
48
+ });
49
+ clearTimeout(timer);
50
+ return await res.json();
51
+ }
52
+ catch (err) {
53
+ clearTimeout(timer);
54
+ if (err instanceof Error && err.name === 'AbortError') {
55
+ return { status: 'wait' };
56
+ }
57
+ throw err;
58
+ }
59
+ }
60
+ /**
61
+ * Start QR code login session. Returns QR code URL for display.
62
+ */
63
+ export async function startQRLogin() {
64
+ log.info('Starting ClawBot QR login...');
65
+ const { qrcode, qrcodeUrl } = await fetchQRCode();
66
+ log.info(`QR code received, url=${qrcodeUrl}`);
67
+ return {
68
+ sessionKey: randomUUID(),
69
+ qrcode,
70
+ qrcodeUrl,
71
+ startedAt: Date.now(),
72
+ };
73
+ }
74
+ /**
75
+ * Wait for user to scan QR code. Polls until confirmed or timeout.
76
+ * Returns bot_token on success.
77
+ */
78
+ export async function waitForQRLogin(session, onStatusChange) {
79
+ const deadline = Date.now() + LOGIN_TIMEOUT_MS;
80
+ let currentBaseUrl = ILINK_BASE_URL;
81
+ let redirectCount = 0;
82
+ log.info('Waiting for QR code scan...');
83
+ while (Date.now() < deadline) {
84
+ const result = await pollQRStatus(session.qrcode);
85
+ onStatusChange?.(result.status);
86
+ switch (result.status) {
87
+ case 'wait':
88
+ case 'scaned':
89
+ break;
90
+ case 'scaned_but_redirect':
91
+ if (result.redirect_host) {
92
+ currentBaseUrl = `https://${result.redirect_host}`;
93
+ redirectCount++;
94
+ log.info(`IDC redirect to ${result.redirect_host} (${redirectCount})`);
95
+ }
96
+ break;
97
+ case 'confirmed':
98
+ if (!result.bot_token) {
99
+ return { connected: false, message: '登录失败:服务器未返回 bot_token' };
100
+ }
101
+ log.info(`Login confirmed! botId=${result.ilink_bot_id}, baseurl=${result.baseurl ?? '(empty)'}`);
102
+ return {
103
+ connected: true,
104
+ botToken: result.bot_token,
105
+ accountId: result.ilink_bot_id,
106
+ baseUrl: result.baseurl || ILINK_BASE_URL,
107
+ userId: result.ilink_user_id,
108
+ message: '登录成功',
109
+ };
110
+ case 'expired':
111
+ return { connected: false, message: '二维码已过期,请重新生成' };
112
+ case 'binded_redirect':
113
+ return { connected: false, message: '已连接过,无需重复连接' };
114
+ default:
115
+ log.warn(`Unknown QR status: ${result.status}`);
116
+ }
117
+ await new Promise(r => setTimeout(r, 1000));
118
+ }
119
+ return { connected: false, message: '登录超时,请重试' };
120
+ }