@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,111 @@
1
+ /**
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
6
+ */
7
+ /** Connection state */
8
+ export type ClawBotState = 'disconnected' | 'connecting' | 'connected' | 'error';
9
+ /** ClawBot configuration */
10
+ export interface ClawBotConfig {
11
+ /** iLink API base URL (default: https://ilinkai.weixin.qq.com) */
12
+ apiUrl: string;
13
+ /** Bearer token for authentication */
14
+ apiToken: string;
15
+ }
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;
54
+ };
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
+ }
96
+ /** getupdates response */
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;
105
+ }
106
+ /** sendmessage response */
107
+ export interface SendMessageResponse {
108
+ ret?: number;
109
+ errcode?: number;
110
+ errmsg?: string;
111
+ }
@@ -0,0 +1,7 @@
1
+ /**
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
6
+ */
7
+ export {};
@@ -3,6 +3,7 @@ import { accessSync, constants } from 'node:fs';
3
3
  import { isAbsolute, join } from 'node:path';
4
4
  import { createLogger } from '../logger.js';
5
5
  import { processEnvForNonClaudeCliChild } from '../config/file-io.js';
6
+ import { killProcessTree, trackChild } from '../shared/process-kill.js';
6
7
  const log = createLogger('CodeBuddyCli');
7
8
  export function buildCodeBuddyArgs(prompt, sessionId, options) {
8
9
  const args = ['--print', '--output-format', 'stream-json'];
@@ -200,10 +201,14 @@ export function runCodeBuddy(cliPath, prompt, sessionId, workDir, callbacks, opt
200
201
  log.info(`Spawning CodeBuddy CLI: path=${normalizedCliPath}, cwd=${workDir}, session=${sessionId ?? 'new'}, args=${args.join(' ')}`);
201
202
  const child = spawn(spawnCmd, spawnArgs, {
202
203
  cwd: workDir,
204
+ // Unix 上放入独立进程组,使 abort/关停能用 process.kill(-pid) 杀掉整棵子树(含 MCP/shell 孙进程)
205
+ detached: process.platform !== 'win32',
203
206
  stdio: ['ignore', 'pipe', 'pipe'],
204
207
  env,
205
208
  windowsHide: process.platform === 'win32',
206
209
  });
210
+ trackChild(child);
211
+ let cliTimeoutHandle = null;
207
212
  let accumulated = '';
208
213
  let accumulatedThinking = '';
209
214
  let completed = false;
@@ -311,6 +316,10 @@ export function runCodeBuddy(cliPath, prompt, sessionId, workDir, callbacks, opt
311
316
  }
312
317
  });
313
318
  child.on('close', (code) => {
319
+ if (cliTimeoutHandle) {
320
+ clearTimeout(cliTimeoutHandle);
321
+ cliTimeoutHandle = null;
322
+ }
314
323
  if (completed)
315
324
  return;
316
325
  if (stdoutState.buffer.trim()) {
@@ -343,16 +352,36 @@ export function runCodeBuddy(cliPath, prompt, sessionId, workDir, callbacks, opt
343
352
  });
344
353
  });
345
354
  child.on('error', (err) => {
355
+ if (cliTimeoutHandle) {
356
+ clearTimeout(cliTimeoutHandle);
357
+ cliTimeoutHandle = null;
358
+ }
346
359
  if (completed)
347
360
  return;
348
361
  completed = true;
349
362
  callbacks.onError(`Failed to start CodeBuddy CLI: ${err.message}`);
350
363
  });
364
+ // 墙钟超时:防止 CLI 挂死永久占用用户队列槽
365
+ const cliTimeoutMs = Number(process.env.OPEN_IM_CLI_TIMEOUT_MS) || 30 * 60 * 1000;
366
+ cliTimeoutHandle = setTimeout(() => {
367
+ if (completed)
368
+ return;
369
+ log.warn(`CodeBuddy CLI 超过 ${cliTimeoutMs}ms,强制终止 (pid=${child.pid})`);
370
+ completed = true;
371
+ killProcessTree(child);
372
+ callbacks.onError(`CodeBuddy CLI 运行超时(${Math.round(cliTimeoutMs / 1000)}s),已终止。请重试。`);
373
+ }, cliTimeoutMs);
374
+ cliTimeoutHandle.unref();
351
375
  return {
352
376
  abort: () => {
377
+ if (completed)
378
+ return;
353
379
  completed = true;
354
- if (!child.killed)
355
- child.kill('SIGTERM');
380
+ if (cliTimeoutHandle) {
381
+ clearTimeout(cliTimeoutHandle);
382
+ cliTimeoutHandle = null;
383
+ }
384
+ killProcessTree(child);
356
385
  },
357
386
  };
358
387
  }
@@ -7,6 +7,7 @@ import { dirname, join } from 'node:path';
7
7
  import { createInterface } from 'node:readline';
8
8
  import { createLogger } from '../logger.js';
9
9
  import { processEnvForNonClaudeCliChild } from '../config/file-io.js';
10
+ import { killProcessTree, trackChild } from '../shared/process-kill.js';
10
11
  const log = createLogger('CodexCli');
11
12
  const windowsCodexLaunchCache = new Map();
12
13
  const SUPPORTED_IMAGE_EXTENSIONS = new Set([
@@ -201,10 +202,13 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
201
202
  : args;
202
203
  const child = spawn(spawnCmd, spawnArgs, {
203
204
  cwd: workDir,
205
+ // Unix 上放入独立进程组,使 abort/关停能用 process.kill(-pid) 杀掉整棵子树(含 MCP/shell 孙进程)
206
+ detached: process.platform !== 'win32',
204
207
  stdio: ['pipe', 'pipe', 'pipe'],
205
208
  env,
206
209
  windowsHide: process.platform === 'win32',
207
210
  });
211
+ trackChild(child);
208
212
  child.stdin?.write(prompt);
209
213
  child.stdin?.end();
210
214
  let accumulated = '';
@@ -327,7 +331,12 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
327
331
  let exitCode = null;
328
332
  let rlClosed = false;
329
333
  let childClosed = false;
334
+ let cliTimeoutHandle = null;
330
335
  const finalize = () => {
336
+ if (cliTimeoutHandle) {
337
+ clearTimeout(cliTimeoutHandle);
338
+ cliTimeoutHandle = null;
339
+ }
331
340
  if (!rlClosed || !childClosed)
332
341
  return;
333
342
  if (completed)
@@ -387,12 +396,29 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
387
396
  childClosed = true;
388
397
  finalize();
389
398
  });
399
+ // 墙钟超时:防止 CLI 挂死(网络卡住、工具死循环等)永久占用用户队列槽
400
+ const cliTimeoutMs = Number(process.env.OPEN_IM_CLI_TIMEOUT_MS) || 30 * 60 * 1000;
401
+ cliTimeoutHandle = setTimeout(() => {
402
+ if (completed)
403
+ return;
404
+ log.warn(`Codex CLI 超过 ${cliTimeoutMs}ms,强制终止 (pid=${child.pid})`);
405
+ completed = true;
406
+ rl.close();
407
+ killProcessTree(child);
408
+ callbacks.onError(`Codex CLI 运行超时(${Math.round(cliTimeoutMs / 1000)}s),已终止。请重试。`);
409
+ }, cliTimeoutMs);
410
+ cliTimeoutHandle.unref();
390
411
  return {
391
412
  abort: () => {
413
+ if (completed)
414
+ return;
392
415
  completed = true;
416
+ if (cliTimeoutHandle) {
417
+ clearTimeout(cliTimeoutHandle);
418
+ cliTimeoutHandle = null;
419
+ }
393
420
  rl.close();
394
- if (!child.killed)
395
- child.kill('SIGTERM');
421
+ killProcessTree(child);
396
422
  },
397
423
  };
398
424
  }
@@ -19,8 +19,11 @@ export declare function getClaudeSdkRuntimeIssue(): string | null;
19
19
  export declare function parseCommaSeparated(value: string): string[];
20
20
  /**
21
21
  * 将最新的 Claude 认证环境变量按优先级合并到 process.env。
22
- * 优先级:shell 环境变量 > ~/.open-im/config.json tools.claude.env >
23
- * 本机 Claude 配置(~/.claude/settings.json,与 Claude Code 共用)。
24
- * 多数用户只维护本机 settings;每次创建 Claude SDK 会话前调用,本机文件变更后下次会话即生效。
22
+ * 优先级:shell 环境变量 > 本机 Claude 配置(~/.claude/settings.json,与 Claude Code 共用)>
23
+ * ~/.open-im/config.json tools.claude.env。
24
+ *
25
+ * 设计意图:用户只需维护 ~/.claude/settings.json(与 Claude Code CLI 共用),
26
+ * open-im 自动跟随本地 Claude 配置,无需单独配置。config.json 的 tools.claude.env
27
+ * 仅作为兜底,供没有本地 Claude 安装的场景使用。
25
28
  */
26
29
  export declare function refreshClaudeEnvToProcess(): void;
@@ -350,9 +350,12 @@ export function parseCommaSeparated(value) {
350
350
  }
351
351
  /**
352
352
  * 将最新的 Claude 认证环境变量按优先级合并到 process.env。
353
- * 优先级:shell 环境变量 > ~/.open-im/config.json tools.claude.env >
354
- * 本机 Claude 配置(~/.claude/settings.json,与 Claude Code 共用)。
355
- * 多数用户只维护本机 settings;每次创建 Claude SDK 会话前调用,本机文件变更后下次会话即生效。
353
+ * 优先级:shell 环境变量 > 本机 Claude 配置(~/.claude/settings.json,与 Claude Code 共用)>
354
+ * ~/.open-im/config.json tools.claude.env。
355
+ *
356
+ * 设计意图:用户只需维护 ~/.claude/settings.json(与 Claude Code CLI 共用),
357
+ * open-im 自动跟随本地 Claude 配置,无需单独配置。config.json 的 tools.claude.env
358
+ * 仅作为兜底,供没有本地 Claude 安装的场景使用。
356
359
  */
357
360
  export function refreshClaudeEnvToProcess() {
358
361
  const file = loadFileConfig();
@@ -363,14 +366,16 @@ export function refreshClaudeEnvToProcess() {
363
366
  process.env[key] = originalShellEnv[key];
364
367
  continue;
365
368
  }
366
- if (key in claudeToolEnv) {
367
- process.env[key] = claudeToolEnv[key];
368
- continue;
369
- }
369
+ // 优先读取 ~/.claude/settings.json(与 Claude Code CLI 共用同一配置)
370
370
  if (key in claudeSettingsEnv) {
371
371
  process.env[key] = claudeSettingsEnv[key];
372
372
  continue;
373
373
  }
374
+ // 兜底:config.json tools.claude.env(仅在没有本地 Claude 安装时需要)
375
+ if (key in claudeToolEnv) {
376
+ process.env[key] = claudeToolEnv[key];
377
+ continue;
378
+ }
374
379
  delete process.env[key];
375
380
  }
376
381
  }
@@ -1,5 +1,5 @@
1
1
  import type { LogLevel } from '../logger.js';
2
- export type Platform = 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wework' | 'workbuddy';
2
+ export type Platform = 'clawbot' | 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wework' | 'workbuddy';
3
3
  export type AiCommand = 'claude' | 'codex' | 'codebuddy';
4
4
  export interface Config {
5
5
  enabledPlatforms: Platform[];
@@ -21,6 +21,7 @@ export interface Config {
21
21
  weworkAllowedUserIds: string[];
22
22
  dingtalkAllowedUserIds: string[];
23
23
  workbuddyAllowedUserIds: string[];
24
+ clawbotAllowedUserIds: string[];
24
25
  codexCliPath: string;
25
26
  codebuddyCliPath: string;
26
27
  /** Claude 访问 API 的代理(如 http://127.0.0.1:7890) */
@@ -87,6 +88,13 @@ export interface Config {
87
88
  guid?: string;
88
89
  workspacePath?: string;
89
90
  };
91
+ clawbot?: {
92
+ enabled: boolean;
93
+ aiCommand?: AiCommand;
94
+ allowedUserIds: string[];
95
+ apiUrl?: string;
96
+ apiToken?: string;
97
+ };
90
98
  };
91
99
  }
92
100
  export interface FilePlatformTelegram {
@@ -147,6 +155,13 @@ export interface FilePlatformWorkBuddy {
147
155
  guid?: string;
148
156
  workspacePath?: string;
149
157
  }
158
+ export interface FilePlatformClawbot {
159
+ enabled?: boolean;
160
+ aiCommand?: AiCommand;
161
+ allowedUserIds?: string[];
162
+ apiUrl?: string;
163
+ apiToken?: string;
164
+ }
150
165
  export interface FileToolClaude {
151
166
  cliPath?: string;
152
167
  workDir?: string;
@@ -180,6 +195,7 @@ export interface FileConfig {
180
195
  wework?: FilePlatformWework;
181
196
  dingtalk?: FilePlatformDingtalk;
182
197
  workbuddy?: FilePlatformWorkBuddy;
198
+ clawbot?: FilePlatformClawbot;
183
199
  };
184
200
  /** @deprecated 仅旧配置兼容;运行时以各 platforms.*.aiCommand 为准 */
185
201
  aiCommand?: string;
@@ -4,7 +4,7 @@ export declare const PAGE_TEXTS: {
4
4
  readonly heroBadge: "open-im local control";
5
5
  readonly heroTitle: "Local bridge control.";
6
6
  readonly heroBody: "";
7
- readonly heroBodyFull: "One local bridge for Telegram, Feishu, QQ, WeWork, DingTalk, and WorkBuddy.";
7
+ readonly heroBodyFull: "One local bridge for Telegram, Feishu, QQ, WeWork, DingTalk, WorkBuddy, and ClawBot.";
8
8
  readonly heroKicker: "Local AI bridge";
9
9
  readonly langButton: "中文";
10
10
  readonly darkModeToggle: "Toggle dark mode";
@@ -56,6 +56,16 @@ export declare const PAGE_TEXTS: {
56
56
  readonly weworkSummary: "Corp ID and secret for enterprise delivery.";
57
57
  readonly dingtalkSummary: "Client credentials plus optional card template.";
58
58
  readonly workbuddySummary: "CodeBuddy OAuth for WeChat customer service.";
59
+ readonly clawbotSummary: "WeChat iLink API via ClawBot.";
60
+ readonly clawbotApiUrl: "API URL";
61
+ readonly clawbotApiToken: "API Token (Bearer)";
62
+ readonly clawbotHelp: "Click \"Scan QR Login\" to authenticate via WeChat, or paste token manually.";
63
+ readonly qrLoginBtn: "Scan QR Login";
64
+ readonly qrLoginScanning: "Waiting for QR code scan...";
65
+ readonly qrLoginSuccess: "Login successful! Credentials saved.";
66
+ readonly qrLoginFailed: "Login failed";
67
+ readonly qrLoginExpired: "QR code expired, please try again";
68
+ readonly qrScanHint: "Use WeChat to scan the QR code";
59
69
  readonly platformCredentialsTitle: "Credentials";
60
70
  readonly platformAccessTitle: "Routing and access";
61
71
  readonly platformTestNote: "Checks required credentials against the platform.";
@@ -152,13 +162,14 @@ export declare const PAGE_TEXTS: {
152
162
  readonly tipWeworkCorp: "WeCom admin → app → view Corp ID (or smart-bot Bot ID) and Secret.";
153
163
  readonly tipDingtalkClient: "<a href=\"https://open-dev.dingtalk.com\" target=\"_blank\" rel=\"noopener\">DingTalk Open Platform</a> → internal app → App Key & App Secret.";
154
164
  readonly tipWorkbuddyToken: "Use terminal <code>open-im init</code> for WorkBuddy OAuth, or paste tokens from CodeBuddy login.";
165
+ readonly tipClawbotApiToken: "Run <code>open-im init</code> and select ClawBot to scan QR code, or paste token from ClawBot iLink login.";
155
166
  };
156
167
  readonly zh: {
157
168
  readonly pageTitle: "open-im 本地控制台";
158
169
  readonly heroBadge: "open-im";
159
170
  readonly heroTitle: "本地桥接控制台";
160
171
  readonly heroBody: "";
161
- readonly heroBodyFull: "一个本地桥接入口,统一连接 Telegram、Feishu、QQ、WeWork、DingTalk 和 WorkBuddy。";
172
+ readonly heroBodyFull: "一个本地桥接入口,统一连接 Telegram、Feishu、QQ、WeWork、DingTalk、WorkBuddyClawBot。";
162
173
  readonly heroKicker: "本地 AI 桥接";
163
174
  readonly langButton: "EN";
164
175
  readonly darkModeToggle: "切换暗黑模式";
@@ -210,6 +221,16 @@ export declare const PAGE_TEXTS: {
210
221
  readonly weworkSummary: "企业微信 Corp ID 与 Secret。";
211
222
  readonly dingtalkSummary: "钉钉 Client 凭证,可选配置卡片模板 ID。";
212
223
  readonly workbuddySummary: "CodeBuddy OAuth 连接微信客服。";
224
+ readonly clawbotSummary: "通过 ClawBot 连接微信 iLink API。";
225
+ readonly clawbotApiUrl: "API 地址";
226
+ readonly clawbotApiToken: "API Token (Bearer)";
227
+ readonly clawbotHelp: "点击“扫码登录”通过微信认证,或手动粘贴 Token。";
228
+ readonly qrLoginBtn: "扫码登录";
229
+ readonly qrLoginScanning: "等待扫码...";
230
+ readonly qrLoginSuccess: "登录成功!凭证已保存。";
231
+ readonly qrLoginFailed: "登录失败";
232
+ readonly qrLoginExpired: "二维码已过期,请重试";
233
+ readonly qrScanHint: "请使用微信扫描二维码";
213
234
  readonly platformCredentialsTitle: "凭证";
214
235
  readonly platformAccessTitle: "路由与访问";
215
236
  readonly platformTestNote: "只会检查该平台的必填凭证是否可用。";
@@ -303,5 +324,6 @@ export declare const PAGE_TEXTS: {
303
324
  readonly tipWeworkCorp: "企业微信管理后台 → 应用 → 查省 Corp ID / 智能机器人 Bot ID 与 Secret。";
304
325
  readonly tipDingtalkClient: "<a href=\"https://open-dev.dingtalk.com\" target=\"_blank\" rel=\"noopener\">钉钉开放平台</a> → 企业内部应用 → AppKey / AppSecret。";
305
326
  readonly tipWorkbuddyToken: "建议在终端运行 <code>open-im init</code> 完成 WorkBuddy 授权;或粘贴 CodeBuddy 登录后的 Token。";
327
+ readonly tipClawbotApiToken: "在终端运行 <code>open-im init</code> 选择 ClawBot 扫码登录,或从 ClawBot iLink 登录后复制 Token。";
306
328
  };
307
329
  };
@@ -4,7 +4,7 @@ export const PAGE_TEXTS = {
4
4
  heroBadge: "open-im local control",
5
5
  heroTitle: "Local bridge control.",
6
6
  heroBody: "",
7
- heroBodyFull: "One local bridge for Telegram, Feishu, QQ, WeWork, DingTalk, and WorkBuddy.",
7
+ heroBodyFull: "One local bridge for Telegram, Feishu, QQ, WeWork, DingTalk, WorkBuddy, and ClawBot.",
8
8
  heroKicker: "Local AI bridge",
9
9
  langButton: "\u4e2d\u6587",
10
10
  darkModeToggle: "Toggle dark mode",
@@ -56,6 +56,16 @@ export const PAGE_TEXTS = {
56
56
  weworkSummary: "Corp ID and secret for enterprise delivery.",
57
57
  dingtalkSummary: "Client credentials plus optional card template.",
58
58
  workbuddySummary: "CodeBuddy OAuth for WeChat customer service.",
59
+ clawbotSummary: "WeChat iLink API via ClawBot.",
60
+ clawbotApiUrl: "API URL",
61
+ clawbotApiToken: "API Token (Bearer)",
62
+ clawbotHelp: 'Click "Scan QR Login" to authenticate via WeChat, or paste token manually.',
63
+ qrLoginBtn: "Scan QR Login",
64
+ qrLoginScanning: "Waiting for QR code scan...",
65
+ qrLoginSuccess: "Login successful! Credentials saved.",
66
+ qrLoginFailed: "Login failed",
67
+ qrLoginExpired: "QR code expired, please try again",
68
+ qrScanHint: "Use WeChat to scan the QR code",
59
69
  platformCredentialsTitle: "Credentials",
60
70
  platformAccessTitle: "Routing and access",
61
71
  platformTestNote: "Checks required credentials against the platform.",
@@ -152,13 +162,14 @@ export const PAGE_TEXTS = {
152
162
  tipWeworkCorp: "WeCom admin → app → view Corp ID (or smart-bot Bot ID) and Secret.",
153
163
  tipDingtalkClient: '<a href="https://open-dev.dingtalk.com" target="_blank" rel="noopener">DingTalk Open Platform</a> → internal app → App Key & App Secret.',
154
164
  tipWorkbuddyToken: 'Use terminal <code>open-im init</code> for WorkBuddy OAuth, or paste tokens from CodeBuddy login.',
165
+ tipClawbotApiToken: 'Run <code>open-im init</code> and select ClawBot to scan QR code, or paste token from ClawBot iLink login.',
155
166
  },
156
167
  zh: {
157
168
  pageTitle: "open-im \u672c\u5730\u63a7\u5236\u53f0",
158
169
  heroBadge: "open-im",
159
170
  heroTitle: "\u672c\u5730\u6865\u63a5\u63a7\u5236\u53f0",
160
171
  heroBody: "",
161
- heroBodyFull: "\u4e00\u4e2a\u672c\u5730\u6865\u63a5\u5165\u53e3\uff0c\u7edf\u4e00\u8fde\u63a5 Telegram\u3001Feishu\u3001QQ\u3001WeWork\u3001DingTalk \u548c WorkBuddy\u3002",
172
+ heroBodyFull: "\u4e00\u4e2a\u672c\u5730\u6865\u63a5\u5165\u53e3\uff0c\u7edf\u4e00\u8fde\u63a5 Telegram\u3001Feishu\u3001QQ\u3001WeWork\u3001DingTalk\u3001WorkBuddy \u548c ClawBot\u3002",
162
173
  heroKicker: "\u672c\u5730 AI \u6865\u63a5",
163
174
  langButton: "EN",
164
175
  darkModeToggle: "\u5207\u6362\u6697\u9ed1\u6a21\u5f0f",
@@ -210,6 +221,16 @@ export const PAGE_TEXTS = {
210
221
  weworkSummary: "\u4f01\u4e1a\u5fae\u4fe1 Corp ID \u4e0e Secret\u3002",
211
222
  dingtalkSummary: "\u9489\u9489 Client \u51ed\u8bc1\uff0c\u53ef\u9009\u914d\u7f6e\u5361\u7247\u6a21\u677f ID\u3002",
212
223
  workbuddySummary: "CodeBuddy OAuth \u8fde\u63a5\u5fae\u4fe1\u5ba2\u670d\u3002",
224
+ clawbotSummary: "\u901a\u8fc7 ClawBot \u8fde\u63a5\u5fae\u4fe1 iLink API\u3002",
225
+ clawbotApiUrl: "API \u5730\u5740",
226
+ clawbotApiToken: "API Token (Bearer)",
227
+ clawbotHelp: '\u70b9\u51fb\u201c\u626b\u7801\u767b\u5f55\u201d\u901a\u8fc7\u5fae\u4fe1\u8ba4\u8bc1\uff0c\u6216\u624b\u52a8\u7c98\u8d34 Token\u3002',
228
+ qrLoginBtn: "\u626b\u7801\u767b\u5f55",
229
+ qrLoginScanning: "\u7b49\u5f85\u626b\u7801...",
230
+ qrLoginSuccess: "\u767b\u5f55\u6210\u529f\uff01\u51ed\u8bc1\u5df2\u4fdd\u5b58\u3002",
231
+ qrLoginFailed: "\u767b\u5f55\u5931\u8d25",
232
+ qrLoginExpired: "\u4e8c\u7ef4\u7801\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u8bd5",
233
+ qrScanHint: "\u8bf7\u4f7f\u7528\u5fae\u4fe1\u626b\u63cf\u4e8c\u7ef4\u7801",
213
234
  platformCredentialsTitle: "\u51ed\u8bc1",
214
235
  platformAccessTitle: "\u8def\u7531\u4e0e\u8bbf\u95ee",
215
236
  platformTestNote: "\u53ea\u4f1a\u68c0\u67e5\u8be5\u5e73\u53f0\u7684\u5fc5\u586b\u51ed\u8bc1\u662f\u5426\u53ef\u7528\u3002",
@@ -303,5 +324,6 @@ export const PAGE_TEXTS = {
303
324
  tipWeworkCorp: "\u4f01\u4e1a\u5fae\u4fe1\u7ba1\u7406\u540e\u53f0 \u2192 \u5e94\u7528 \u2192 \u67e5\u7701 Corp ID / \u667a\u80fd\u673a\u5668\u4eba Bot ID \u4e0e Secret\u3002",
304
325
  tipDingtalkClient: '<a href="https://open-dev.dingtalk.com" target="_blank" rel="noopener">\u9489\u9489\u5f00\u653e\u5e73\u53f0</a> \u2192 \u4f01\u4e1a\u5185\u90e8\u5e94\u7528 \u2192 AppKey / AppSecret\u3002',
305
326
  tipWorkbuddyToken: "\u5efa\u8bae\u5728\u7ec8\u7aef\u8fd0\u884c <code>open-im init</code> \u5b8c\u6210 WorkBuddy \u6388\u6743\uff1b\u6216\u7c98\u8d34 CodeBuddy \u767b\u5f55\u540e\u7684 Token\u3002",
327
+ tipClawbotApiToken: '\u5728\u7ec8\u7aef\u8fd0\u884c <code>open-im init</code> \u9009\u62e9 ClawBot \u626b\u7801\u767b\u5f55\uff0c\u6216\u4ece ClawBot iLink \u767b\u5f55\u540e\u590d\u5236 Token\u3002',
306
328
  }
307
329
  };
@@ -163,6 +163,7 @@ export function getHealthPlatformSnapshot(file, env = process.env) {
163
163
  const fileWework = file.platforms?.wework;
164
164
  const fileDingtalk = file.platforms?.dingtalk;
165
165
  const fileWorkbuddy = file.platforms?.workbuddy;
166
+ const fileClawbot = file.platforms?.clawbot;
166
167
  const telegramBotToken = env.TELEGRAM_BOT_TOKEN ?? fileTelegram?.botToken ?? file.telegramBotToken;
167
168
  const feishuAppId = env.FEISHU_APP_ID ?? fileFeishu?.appId ?? file.feishuAppId;
168
169
  const feishuAppSecret = env.FEISHU_APP_SECRET ?? fileFeishu?.appSecret ?? file.feishuAppSecret;
@@ -212,6 +213,12 @@ export function getHealthPlatformSnapshot(file, env = process.env) {
212
213
  healthy: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId),
213
214
  message: workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId ? "OAuth credentials configured" : "Missing credentials",
214
215
  },
216
+ clawbot: {
217
+ configured: !!fileClawbot?.apiToken,
218
+ enabled: !!fileClawbot?.apiToken && fileClawbot?.enabled !== false,
219
+ healthy: !!fileClawbot?.apiToken,
220
+ message: fileClawbot?.apiToken ? "API Token configured" : "Missing API Token",
221
+ },
215
222
  };
216
223
  }
217
224
  function splitCsv(value) {
@@ -325,6 +332,13 @@ function buildInitialPayload(file) {
325
332
  baseUrl: file.platforms?.workbuddy?.baseUrl ?? "",
326
333
  allowedUserIds: (file.platforms?.workbuddy?.allowedUserIds ?? []).join(", "),
327
334
  },
335
+ clawbot: {
336
+ enabled: file.platforms?.clawbot?.enabled ?? Boolean(file.platforms?.clawbot?.apiToken),
337
+ aiCommand: normalizeAiCommand(file.platforms?.clawbot?.aiCommand, "claude"),
338
+ apiUrl: file.platforms?.clawbot?.apiUrl ?? "http://127.0.0.1:26322",
339
+ apiToken: maskSecret(file.platforms?.clawbot?.apiToken),
340
+ allowedUserIds: (file.platforms?.clawbot?.allowedUserIds ?? []).join(", "),
341
+ },
328
342
  },
329
343
  ai: {
330
344
  claudeWorkDir: file.tools?.claude?.workDir ?? process.cwd(),
@@ -388,6 +402,8 @@ function validatePayload(payload) {
388
402
  errors.push("WorkBuddy refresh token is required.");
389
403
  if (payload.platforms.workbuddy.enabled && !clean(payload.platforms.workbuddy.userId))
390
404
  errors.push("WorkBuddy user ID is required.");
405
+ if (payload.platforms.clawbot.enabled && !clean(payload.platforms.clawbot.apiToken))
406
+ errors.push("ClawBot API token is required.");
391
407
  if (!clean(payload.ai.claudeWorkDir))
392
408
  errors.push("Default work directory is required.");
393
409
  return errors;
@@ -447,6 +463,11 @@ function validateConfigForPlatform(platform, config) {
447
463
  errors.push("WorkBuddy user ID is required and must be a non-empty string.");
448
464
  }
449
465
  break;
466
+ case "clawbot":
467
+ if (!c.apiToken || typeof c.apiToken !== "string" || !clean(c.apiToken)) {
468
+ errors.push("ClawBot API token is required and must be a non-empty string.");
469
+ }
470
+ break;
450
471
  default:
451
472
  errors.push(`Unknown platform: ${platform}`);
452
473
  }
@@ -476,6 +497,7 @@ function createProbeConfig(values) {
476
497
  weworkAllowedUserIds: [],
477
498
  dingtalkAllowedUserIds: [],
478
499
  workbuddyAllowedUserIds: [],
500
+ clawbotAllowedUserIds: [],
479
501
  codexCliPath: "codex",
480
502
  claudeWorkDir: process.cwd(),
481
503
  claudeSessionIdleTtlMinutes: 30,
@@ -598,6 +620,29 @@ async function probeWorkBuddy(config) {
598
620
  }
599
621
  return "WorkBuddy credentials are valid.";
600
622
  }
623
+ async function probeClawBot(config) {
624
+ const apiUrl = clean(String(config.apiUrl ?? "http://127.0.0.1:26322"));
625
+ const apiToken = clean(String(config.apiToken ?? ""));
626
+ if (!apiToken)
627
+ throw new Error("ClawBot API token is required.");
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
+ },
637
+ signal: AbortSignal.timeout(TEST_TIMEOUT_MS),
638
+ });
639
+ const body = await readJsonResponse(response);
640
+ const ok = body.ok === true || body.ret === 0 || body.ret === '0';
641
+ if (!response.ok || !ok) {
642
+ throw new Error(String(body.error ?? body.description ?? `HTTP ${response.status}`));
643
+ }
644
+ return "ClawBot API reachable.";
645
+ }
601
646
  export async function testPlatformConfig(platform, config) {
602
647
  const errors = validateConfigForPlatform(platform, config);
603
648
  if (errors.length > 0) {
@@ -616,6 +661,8 @@ export async function testPlatformConfig(platform, config) {
616
661
  return probeDingTalk(config);
617
662
  case "workbuddy":
618
663
  return probeWorkBuddy(config);
664
+ case "clawbot":
665
+ return probeClawBot(config);
619
666
  default:
620
667
  throw new Error(`Unknown platform: ${platform}`);
621
668
  }
@@ -711,6 +758,14 @@ function toFileConfig(payload, existing) {
711
758
  baseUrl: clean(payload.platforms.workbuddy.baseUrl),
712
759
  allowedUserIds: splitCsv(payload.platforms.workbuddy.allowedUserIds),
713
760
  },
761
+ clawbot: {
762
+ ...existing.platforms?.clawbot,
763
+ enabled: payload.platforms.clawbot.enabled,
764
+ aiCommand: persistedPlatformAi(payload.platforms.clawbot.aiCommand),
765
+ apiUrl: clean(payload.platforms.clawbot.apiUrl) ?? "http://127.0.0.1:26322",
766
+ apiToken: resolveSecret(payload.platforms.clawbot.apiToken, existing.platforms?.clawbot?.apiToken),
767
+ allowedUserIds: splitCsv(payload.platforms.clawbot.allowedUserIds),
768
+ },
714
769
  },
715
770
  };
716
771
  }
@@ -1071,6 +1126,30 @@ export async function startWebConfigServer(options) {
1071
1126
  }
1072
1127
  return;
1073
1128
  }
1129
+ if (request.method === "POST" && requestUrl.pathname === "/api/clawbot/qr-login/start") {
1130
+ try {
1131
+ const { startQRLogin } = await import("./clawbot/qr-login.js");
1132
+ const session = await startQRLogin();
1133
+ json(response, 200, { success: true, qrcodeUrl: session.qrcodeUrl, sessionKey: session.sessionKey }, request);
1134
+ }
1135
+ catch (error) {
1136
+ json(response, 500, { success: false, error: toErrorMessage(error) }, request);
1137
+ }
1138
+ return;
1139
+ }
1140
+ if (request.method === "POST" && requestUrl.pathname === "/api/clawbot/qr-login/wait") {
1141
+ try {
1142
+ const body = await readJson(request);
1143
+ const { waitForQRLogin } = await import("./clawbot/qr-login.js");
1144
+ const session = { sessionKey: body.sessionKey, qrcode: body.qrcode, qrcodeUrl: body.qrcodeUrl, startedAt: Date.now() };
1145
+ const result = await waitForQRLogin(session);
1146
+ json(response, 200, { success: result.connected, botToken: result.botToken, baseUrl: result.baseUrl, userId: result.userId, message: result.message }, request);
1147
+ }
1148
+ catch (error) {
1149
+ json(response, 500, { success: false, error: toErrorMessage(error) }, request);
1150
+ }
1151
+ return;
1152
+ }
1074
1153
  if (request.method === "GET" && tryServeDashboardStatic(requestUrl, request, response, mergeCors)) {
1075
1154
  return;
1076
1155
  }
package/dist/config.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { Platform, AiCommand, Config, FilePlatformTelegram, FilePlatformFeishu, FilePlatformQQ, FilePlatformWechat, FilePlatformWework, FilePlatformDingtalk, FilePlatformWorkBuddy, FileToolClaude, FileToolCodex, FileToolCodeBuddy, FileConfig, } from './config/types.js';
1
+ export type { Platform, AiCommand, Config, FilePlatformTelegram, FilePlatformFeishu, FilePlatformQQ, FilePlatformWechat, FilePlatformWework, FilePlatformDingtalk, FilePlatformWorkBuddy, FilePlatformClawbot, FileToolClaude, FileToolCodex, FileToolCodeBuddy, FileConfig, } from './config/types.js';
2
2
  import type { Platform, AiCommand, Config } from './config/types.js';
3
3
  export { CONFIG_PATH, loadFileConfig, saveFileConfig, getClaudeConfigHome, getClaudeSdkRuntimeIssue, hasCodeBuddyAuthIndicators, loadClaudeSettingsEnv, saveClaudeSettingsEnv, normalizeAiCommand, hasCodexAuth, parseCommaSeparated, CLAUDE_AUTH_ENV_KEYS, refreshClaudeEnvToProcess, processEnvForNonClaudeCliChild, CODEX_AUTH_PATHS, } from './config/file-io.js';
4
4
  /** 检测是否需要交互式配置(无 token 且无环境变量) */