@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
package/dist/config.js CHANGED
@@ -23,7 +23,9 @@ function resolveFilePlatformAi(file, platform) {
23
23
  ? pf?.wework?.aiCommand
24
24
  : platform === 'dingtalk'
25
25
  ? pf?.dingtalk?.aiCommand
26
- : pf?.workbuddy?.aiCommand;
26
+ : platform === 'clawbot'
27
+ ? pf?.clawbot?.aiCommand
28
+ : pf?.workbuddy?.aiCommand;
27
29
  return normalizeAiCommand(raw ?? process.env.AI_COMMAND ?? file.aiCommand, 'claude');
28
30
  }
29
31
  // Re-export file I/O and credential helpers from sub-modules
@@ -53,6 +55,7 @@ export function needsSetup() {
53
55
  const ww = file.platforms?.wework;
54
56
  const dt = file.platforms?.dingtalk;
55
57
  const wb = file.platforms?.workbuddy;
58
+ const cb = file.platforms?.clawbot;
56
59
  // Also check legacy platforms.wechat for migration path
57
60
  const legacyWc = file.platforms?.wechat;
58
61
  const hasTelegram = !!tg?.botToken;
@@ -61,8 +64,9 @@ export function needsSetup() {
61
64
  const hasWework = !!(ww?.corpId && ww?.secret);
62
65
  const hasDingtalk = !!(dt?.clientId && dt?.clientSecret);
63
66
  const hasWorkBuddy = !!(wb?.accessToken && wb?.refreshToken && wb?.userId);
67
+ const hasClawBot = !!(cb?.apiToken);
64
68
  const hasLegacyWechat = !!(legacyWc?.workbuddyAccessToken && legacyWc?.workbuddyRefreshToken);
65
- return !hasTelegram && !hasFeishu && !hasQQ && !hasWework && !hasDingtalk && !hasWorkBuddy && !hasLegacyWechat;
69
+ return !hasTelegram && !hasFeishu && !hasQQ && !hasWework && !hasDingtalk && !hasWorkBuddy && !hasClawBot && !hasLegacyWechat;
66
70
  }
67
71
  export function loadConfig() {
68
72
  const file = loadFileConfig();
@@ -73,6 +77,7 @@ export function loadConfig() {
73
77
  const fileQQ = file.platforms?.qq;
74
78
  const fileWework = file.platforms?.wework;
75
79
  const fileDingtalk = file.platforms?.dingtalk;
80
+ const fileClawbot = file.platforms?.clawbot;
76
81
  // Auto-migrate legacy platforms.wechat WorkBuddy credentials → platforms.workbuddy
77
82
  const legacyWechat = file.platforms?.wechat;
78
83
  const fileWorkBuddy = file.platforms?.workbuddy ?? (legacyWechat?.workbuddyAccessToken && legacyWechat?.workbuddyRefreshToken
@@ -122,6 +127,11 @@ export function loadConfig() {
122
127
  fileWorkBuddy?.guid;
123
128
  const workbuddyWorkspacePath = process.env.WORKBUDDY_WORKSPACE_PATH ??
124
129
  fileWorkBuddy?.workspacePath;
130
+ // ClawBot credentials
131
+ const clawbotApiUrl = process.env.CLAWBOT_API_URL ??
132
+ fileClawbot?.apiUrl;
133
+ const clawbotApiToken = process.env.CLAWBOT_API_TOKEN ??
134
+ fileClawbot?.apiToken;
125
135
  // 2. 计算启用平台
126
136
  const enabledPlatforms = [];
127
137
  const telegramEnabledFlag = fileTelegram?.enabled;
@@ -130,12 +140,14 @@ export function loadConfig() {
130
140
  const weworkEnabledFlag = fileWework?.enabled;
131
141
  const dingtalkEnabledFlag = fileDingtalk?.enabled;
132
142
  const workbuddyEnabledFlag = fileWorkBuddy?.enabled;
143
+ const clawbotEnabledFlag = fileClawbot?.enabled;
133
144
  const telegramEnabled = !!telegramBotToken && (telegramEnabledFlag !== false);
134
145
  const feishuEnabled = !!(feishuAppId && feishuAppSecret) && (feishuEnabledFlag !== false);
135
146
  const qqEnabled = !!(qqAppId && qqSecret) && (qqEnabledFlag !== false);
136
147
  const weworkEnabled = !!(weworkCorpId && weworkSecret) && (weworkEnabledFlag !== false);
137
148
  const dingtalkEnabled = !!(dingtalkClientId && dingtalkClientSecret) && (dingtalkEnabledFlag !== false);
138
149
  const workbuddyEnabled = !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId) && (workbuddyEnabledFlag !== false);
150
+ const clawbotEnabled = !!clawbotApiToken && (clawbotEnabledFlag !== false);
139
151
  if (telegramEnabled)
140
152
  enabledPlatforms.push('telegram');
141
153
  if (feishuEnabled)
@@ -148,6 +160,8 @@ export function loadConfig() {
148
160
  enabledPlatforms.push('dingtalk');
149
161
  if (workbuddyEnabled)
150
162
  enabledPlatforms.push('workbuddy');
163
+ if (clawbotEnabled)
164
+ enabledPlatforms.push('clawbot');
151
165
  if (enabledPlatforms.length === 0) {
152
166
  throw new Error('至少需要配置 Telegram、Feishu、WeChat、WeWork 或 DingTalk 其中一个平台(可以通过环境变量或 config.json)');
153
167
  }
@@ -174,6 +188,9 @@ export function loadConfig() {
174
188
  const workbuddyAllowedUserIds = process.env.WORKBUDDY_ALLOWED_USER_IDS !== undefined
175
189
  ? parseCommaSeparated(process.env.WORKBUDDY_ALLOWED_USER_IDS)
176
190
  : fileWorkBuddy?.allowedUserIds ?? allowedUserIds;
191
+ const clawbotAllowedUserIds = process.env.CLAWBOT_ALLOWED_USER_IDS !== undefined
192
+ ? parseCommaSeparated(process.env.CLAWBOT_ALLOWED_USER_IDS)
193
+ : fileClawbot?.allowedUserIds ?? allowedUserIds;
177
194
  // 5. AI / 工作目录 / 安全配置(从 tools 读取)
178
195
  const tc = file.tools?.claude ?? {};
179
196
  const tcod = file.tools?.codex ?? {};
@@ -507,6 +524,21 @@ export function loadConfig() {
507
524
  guid: workbuddyGuid,
508
525
  workspacePath: workbuddyWorkspacePath,
509
526
  },
527
+ clawbot: clawbotEnabled
528
+ ? {
529
+ enabled: true,
530
+ aiCommand: resolveFilePlatformAi(file, 'clawbot'),
531
+ allowedUserIds: clawbotAllowedUserIds,
532
+ apiUrl: clawbotApiUrl,
533
+ apiToken: clawbotApiToken,
534
+ }
535
+ : {
536
+ enabled: false,
537
+ aiCommand: resolveFilePlatformAi(file, 'clawbot'),
538
+ allowedUserIds: clawbotAllowedUserIds,
539
+ apiUrl: clawbotApiUrl,
540
+ apiToken: clawbotApiToken,
541
+ },
510
542
  };
511
543
  return {
512
544
  enabledPlatforms,
@@ -528,6 +560,7 @@ export function loadConfig() {
528
560
  weworkAllowedUserIds,
529
561
  dingtalkAllowedUserIds,
530
562
  workbuddyAllowedUserIds,
563
+ clawbotAllowedUserIds,
531
564
  codexCliPath,
532
565
  codebuddyCliPath,
533
566
  claudeProxy,
@@ -562,6 +595,9 @@ export function getPlatformsWithCredentials(config) {
562
595
  const wb = config.platforms.workbuddy;
563
596
  if (wb?.accessToken && wb?.refreshToken)
564
597
  r.push('workbuddy');
598
+ const cb = config.platforms.clawbot;
599
+ if (cb?.apiToken)
600
+ r.push('clawbot');
565
601
  return r;
566
602
  }
567
603
  export function resolvePlatformAiCommand(config, platform) {
@@ -38,3 +38,9 @@ export declare const MAX_WEWORK_MESSAGE_LENGTH = 2048;
38
38
  export declare const MAX_DINGTALK_MESSAGE_LENGTH = 2048;
39
39
  /** WeChat KF (微信客服) 单条消息最大字符数 */
40
40
  export declare const MAX_WORKBUDDY_MESSAGE_LENGTH = 2000;
41
+ /** ClawBot 流式更新节流 */
42
+ export declare const CLAWBOT_THROTTLE_MS = 1000;
43
+ /** ClawBot 单条消息最大字符数 */
44
+ export declare const MAX_CLAWBOT_MESSAGE_LENGTH = 2000;
45
+ /** ClawBot 长轮询间隔 */
46
+ export declare const CLAWBOT_POLL_INTERVAL_MS = 3000;
package/dist/constants.js CHANGED
@@ -68,3 +68,9 @@ export const MAX_WEWORK_MESSAGE_LENGTH = 2048;
68
68
  export const MAX_DINGTALK_MESSAGE_LENGTH = 2048;
69
69
  /** WeChat KF (微信客服) 单条消息最大字符数 */
70
70
  export const MAX_WORKBUDDY_MESSAGE_LENGTH = 2000;
71
+ /** ClawBot 流式更新节流 */
72
+ export const CLAWBOT_THROTTLE_MS = 1000;
73
+ /** ClawBot 单条消息最大字符数 */
74
+ export const MAX_CLAWBOT_MESSAGE_LENGTH = 2000;
75
+ /** ClawBot 长轮询间隔 */
76
+ export const CLAWBOT_POLL_INTERVAL_MS = 3000;
@@ -1,5 +1,6 @@
1
1
  import { DWClient, TOPIC_ROBOT } from 'dingtalk-stream';
2
2
  import { createLogger } from '../logger.js';
3
+ import { jitteredDelay } from '../shared/reconnect.js';
3
4
  import { registerSessionWebhook as registerWebhook, clearWebhooks, sendText as webhookSendText, sendMarkdown as webhookSendMarkdown, downloadRobotMessageFile as webhookDownloadFile, sendProactiveText as webhookSendProactive, } from './webhook.js';
4
5
  import { prepareStreamingCard as cardPrepare, updateStreamingCard as cardUpdate, finishStreamingCard as cardFinish, createAndDeliverCard as cardCreateAndDeliver, updateCardInstance as cardUpdateInstance, sendRobotInteractiveCard as cardSendInteractive, updateRobotInteractiveCard as cardUpdateInteractive, clearUnionIdCache, } from './streaming-card.js';
5
6
  const log = createLogger('DingTalk');
@@ -111,7 +112,7 @@ export async function initDingTalk(cfg, eventHandler) {
111
112
  lastErr = err;
112
113
  if (is429(err) && attempt < maxTries) {
113
114
  log.warn(`DingTalk gateway 429 (attempt ${attempt}/${maxTries}), retrying in ${retryDelayMs / 1000}s...`);
114
- await new Promise((r) => setTimeout(r, retryDelayMs));
115
+ await new Promise((r) => setTimeout(r, jitteredDelay(retryDelayMs)));
115
116
  continue;
116
117
  }
117
118
  throw new Error(formatDingTalkInitError(err));
@@ -163,7 +163,7 @@ export function setupDingTalkHandlers(config, sessionManager) {
163
163
  sessionManager,
164
164
  sender: { sendTextReply, sendDirectorySelection },
165
165
  });
166
- const { accessControl, requestQueue, runningTasks, commandHandler } = ctx;
166
+ const { accessControl, requestQueue, runningTasks } = ctx;
167
167
  // DingTalk-specific sender callbacks for the factory
168
168
  const dingtalkSender = {
169
169
  sendThinkingMessage: async (chatId, replyToMessageId, toolId) => {
package/dist/index.js CHANGED
@@ -24,14 +24,20 @@ import { setupDingTalkHandlers } from "./dingtalk/event-handler.js";
24
24
  import { initWorkBuddy, stopWorkBuddy } from "./workbuddy/client.js";
25
25
  import { setupWorkBuddyHandlers } from "./workbuddy/event-handler.js";
26
26
  import { sendTextReply as sendWorkBuddyTextReply } from "./workbuddy/message-sender.js";
27
+ import { initClawbot, stopClawbot } from "./clawbot/client.js";
28
+ import { setupClawbotHandlers } from "./clawbot/event-handler.js";
29
+ import { sendTextReply as sendClawbotTextReply } from "./clawbot/message-sender.js";
30
+ import { initClawBotSender } from "./clawbot/message-sender.js";
27
31
  import { initAdapters, cleanupAdapters } from "./adapters/registry.js";
28
32
  import { SessionManager } from "./session/session-manager.js";
29
33
  import { loadActiveChats, getActiveChatId, flushActiveChats, } from "./shared/active-chats.js";
34
+ import { destroyAllLiveChildren } from "./shared/process-kill.js";
30
35
  import { initLogger, createLogger, closeLogger, emitStructuredEvent, shutdownLoggerTelemetry, } from "./logger.js";
31
36
  import { APP_HOME, SHUTDOWN_PORT } from "./constants.js";
32
37
  import { createRequire } from "node:module";
33
38
  import { escapePathForMarkdown } from "./shared/utils.js";
34
39
  import { applyOpenImGitCoauthorToProcessEnv } from "./shared/git-coauthor.js";
40
+ import { emitInterruptedTerminals } from "./shared/task-cleanup.js";
35
41
  const require = createRequire(import.meta.url);
36
42
  const { version: APP_VERSION } = require("../package.json");
37
43
  const log = createLogger("Main");
@@ -92,6 +98,19 @@ const PLATFORM_MODULES = {
92
98
  stop: () => stopWorkBuddy(),
93
99
  sendNotification: (chatId, msg) => sendWorkBuddyTextReply(null, chatId, msg, randomUUID()),
94
100
  },
101
+ clawbot: {
102
+ init: async (config, sessionManager) => {
103
+ const pc = config.platforms.clawbot;
104
+ if (pc?.apiUrl && pc?.apiToken) {
105
+ initClawBotSender(pc.apiUrl, pc.apiToken);
106
+ }
107
+ const handle = setupClawbotHandlers(config, sessionManager);
108
+ await initClawbot(config, handle.handleEvent);
109
+ return handle;
110
+ },
111
+ stop: () => stopClawbot(),
112
+ sendNotification: (chatId, msg) => sendClawbotTextReply(chatId, msg),
113
+ },
95
114
  };
96
115
  async function sendLifecycleNotification(platform, message) {
97
116
  const mod = PLATFORM_MODULES[platform];
@@ -262,6 +281,15 @@ export async function main() {
262
281
  const uptimeSec = Math.floor((Date.now() - startedAt) / 1000);
263
282
  const m = Math.floor(uptimeSec / 60);
264
283
  const shutdownMsg = buildShutdownMessage(m);
284
+ // 在任何异步等待之前,先为仍在运行的任务补发终态遥测事件。
285
+ // 这样即使后续 await(通知发送、platform.stop)期间进程被第二次信号杀死,
286
+ // ai.task.start 也有对应的 interrupted 终态,避免遥测里的 miss。
287
+ for (const platform of successfulPlatforms) {
288
+ const handle = activeHandles.get(platform);
289
+ if (handle?.runningTasks && handle.runningTasks.size > 0) {
290
+ emitInterruptedTerminals(handle.runningTasks);
291
+ }
292
+ }
265
293
  // Send notification only to successfully initialized platforms
266
294
  for (const platform of successfulPlatforms) {
267
295
  await sendLifecycleNotification(platform, shutdownMsg).catch((err) => {
@@ -299,6 +327,16 @@ export async function main() {
299
327
  };
300
328
  process.on("SIGINT", () => shutdown().catch(() => process.exit(1)));
301
329
  process.on("SIGTERM", () => shutdown().catch(() => process.exit(1)));
330
+ // 兜底:进程退出(含异常路径,如未捕获异常 / SIGKILL)时强制清理 CLI 子进程,
331
+ // 避免僵尸 / 孤儿。正常 shutdown 已在 cleanupAdapters() 里清理过。
332
+ process.on("exit", () => {
333
+ try {
334
+ destroyAllLiveChildren();
335
+ }
336
+ catch {
337
+ /* 退出期间 best effort */
338
+ }
339
+ });
302
340
  // Global error handlers to prevent unhandled crashes
303
341
  process.on("unhandledRejection", (reason) => {
304
342
  log.error("Unhandled Promise rejection (this indicates a bug — the affected request may hang without a response):", reason);
@@ -312,6 +350,18 @@ export async function main() {
312
350
  return;
313
351
  }
314
352
  log.error("Uncaught exception (process will exit):", err);
353
+ // 进程即将因未捕获异常退出:补发在途任务的终态,避免 miss
354
+ for (const platform of successfulPlatforms) {
355
+ const handle = activeHandles.get(platform);
356
+ if (handle?.runningTasks && handle.runningTasks.size > 0) {
357
+ try {
358
+ emitInterruptedTerminals(handle.runningTasks);
359
+ }
360
+ catch {
361
+ /* best effort:不能因遥测失败再次抛出 */
362
+ }
363
+ }
364
+ }
315
365
  void shutdownLoggerTelemetry()
316
366
  .then(() => closeLogger())
317
367
  .finally(() => process.exit(1));
package/dist/qq/client.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import WebSocket from "ws";
2
2
  import { createLogger } from "../logger.js";
3
+ import { jitteredDelay, SLOW_PROBE_MS } from "../shared/reconnect.js";
3
4
  const log = createLogger("QQ");
4
5
  const TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
5
6
  const API_BASE = "https://api.sgroup.qq.com";
@@ -271,7 +272,12 @@ async function connectWebSocket(config, handler) {
271
272
  sessionId = null;
272
273
  seq = null;
273
274
  }
274
- const delay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
275
+ const isFatalClose = code === 4004 || code === 4006 || code === 4007;
276
+ const baseDelay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
277
+ const delay = isFatalClose ? jitteredDelay(SLOW_PROBE_MS) : jitteredDelay(baseDelay);
278
+ if (isFatalClose) {
279
+ log.warn(`QQ 致命关闭码 ${code}(鉴权失败),转慢探测(每 ${Math.round(SLOW_PROBE_MS / 1000)}s 一次)`);
280
+ }
275
281
  reconnectAttempt += 1;
276
282
  reconnectTimer = setTimeout(() => {
277
283
  if (currentConfig && currentHandler) {
@@ -80,17 +80,18 @@ export class RequestQueue {
80
80
  finally {
81
81
  this.runningControllers.delete(key);
82
82
  const q = this.queues.get(key);
83
- if (!q)
84
- return;
85
- const next = q.tasks.shift();
86
- if (next) {
87
- setImmediate(() => this.run(key, next.prompt, next.execute).catch((err) => {
88
- log.error(`Unhandled error in next task execution for ${key}:`, err);
89
- }));
90
- }
83
+ if (!q) { /* queue already cleared */ }
91
84
  else {
92
- q.running = false;
93
- this.queues.delete(key);
85
+ const next = q.tasks.shift();
86
+ if (next) {
87
+ setImmediate(() => this.run(key, next.prompt, next.execute).catch((err) => {
88
+ log.error(`Unhandled error in next task execution for ${key}:`, err);
89
+ }));
90
+ }
91
+ else {
92
+ q.running = false;
93
+ this.queues.delete(key);
94
+ }
94
95
  }
95
96
  }
96
97
  }
package/dist/setup.js CHANGED
@@ -31,6 +31,7 @@ function getConfiguredPlatforms(existing) {
31
31
  { k: "wework", label: "企业微信" },
32
32
  { k: "dingtalk", label: "钉钉" },
33
33
  { k: "workbuddy", label: "WorkBuddy (微信)" },
34
+ { k: "clawbot", label: "ClawBot (微信 iLink)" },
34
35
  ];
35
36
  return names
36
37
  .filter(({ k }) => {
@@ -49,6 +50,8 @@ function getConfiguredPlatforms(existing) {
49
50
  return !!(p.corpId && p.secret);
50
51
  if (k === "dingtalk")
51
52
  return !!(p.clientId && p.clientSecret);
53
+ if (k === "clawbot")
54
+ return !!p.apiToken;
52
55
  return false;
53
56
  })
54
57
  .map(({ label }) => label);
@@ -125,12 +128,19 @@ function printManualInstructions(configPath) {
125
128
  "workbuddyRefreshToken": "",
126
129
  "userId": "",
127
130
  "allowedUserIds": ["允许访问的微信用户 ID(可选)"]
131
+ },
132
+ "clawbot": {
133
+ "enabled": false,
134
+ "aiCommand": "claude",
135
+ "apiUrl": "http://127.0.0.1:26322",
136
+ "apiToken": "你的 ClawBot Bearer Token(可选)",
137
+ "allowedUserIds": ["允许访问的微信用户 ID(可选)"]
128
138
  }
129
139
  }
130
140
  }`);
131
141
  console.log("");
132
- console.log("提示:至少需要配置 Telegram、Feishu、QQ、WeChat、WeWork 或 DingTalk 其中一个平台");
133
- console.log("或设置环境变量: TELEGRAM_BOT_TOKEN=xxx、FEISHU_APP_ID=xxx、QQ_BOT_APPID=xxx、WECHAT_WORKBUDDY_ACCESS_TOKEN=xxx、WEWORK_CORP_ID=xxx 或 DINGTALK_CLIENT_ID=xxx 后再运行");
142
+ console.log("提示:至少需要配置 Telegram、Feishu、QQ、WeChat、WeWork、DingTalkClawBot 其中一个平台");
143
+ console.log("或设置环境变量: TELEGRAM_BOT_TOKEN=xxx、FEISHU_APP_ID=xxx、QQ_BOT_APPID=xxx、WECHAT_WORKBUDDY_ACCESS_TOKEN=xxx、WEWORK_CORP_ID=xxx、DINGTALK_CLIENT_ID=xxxCLAWBOT_API_TOKEN=xxx 后再运行");
134
144
  console.log("");
135
145
  }
136
146
  const CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
@@ -317,6 +327,11 @@ export async function runInteractiveSetup() {
317
327
  (hasWc ? " ✓已配置" : ""),
318
328
  value: "workbuddy",
319
329
  },
330
+ {
331
+ title: "ClawBot 微信 iLink (需要 API Token)" +
332
+ (!!existing?.platforms?.clawbot?.apiToken ? " ✓已配置" : ""),
333
+ value: "clawbot",
334
+ },
320
335
  { title: "配置多个平台", value: "multi" },
321
336
  ],
322
337
  initial: 0,
@@ -339,6 +354,7 @@ export async function runInteractiveSetup() {
339
354
  { title: "企业微信 (WeWork)" + (hasWw ? " ✓已配置" : ""), value: "wework", selected: hasWw },
340
355
  { title: "钉钉 (DingTalk)" + (hasDt ? " ✓已配置" : ""), value: "dingtalk", selected: hasDt },
341
356
  { title: "WorkBuddy 微信客服 (WeChat KF)" + (hasWc ? " ✓已配置" : ""), value: "workbuddy", selected: hasWc },
357
+ { title: "ClawBot 微信 iLink" + (!!existing?.platforms?.clawbot?.apiToken ? " ✓已配置" : ""), value: "clawbot" },
342
358
  ],
343
359
  }, { onCancel });
344
360
  if (!multiResp.platforms || multiResp.platforms.length === 0) {
@@ -563,6 +579,84 @@ export async function runInteractiveSetup() {
563
579
  return false;
564
580
  }
565
581
  }
582
+ if (selectedPlatforms.includes("clawbot")) {
583
+ const hasCbToken = !!existing?.platforms?.clawbot?.apiToken;
584
+ const cbModeResp = await prompts({
585
+ type: "select",
586
+ name: "mode",
587
+ message: "ClawBot 微信 iLink",
588
+ choices: [
589
+ { title: "扫码登录(自动获取 Token)", value: "qr" },
590
+ { title: "使用已有 Token" + (hasCbToken ? " ✓" : ""), value: "token", disabled: !hasCbToken },
591
+ ],
592
+ initial: 0,
593
+ }, { onCancel });
594
+ let cbApiToken = "";
595
+ let cbApiUrl = existing?.platforms?.clawbot?.apiUrl ?? "http://127.0.0.1:26322";
596
+ if (cbModeResp.mode === "qr") {
597
+ console.log("\n正在获取二维码...\n");
598
+ try {
599
+ const { startQRLogin, waitForQRLogin } = await import("./clawbot/qr-login.js");
600
+ const session = await startQRLogin();
601
+ console.log("请用微信扫描以下链接中的二维码:");
602
+ console.log(session.qrcodeUrl);
603
+ console.log("\n等待扫码...(最长 5 分钟)\n");
604
+ const result = await waitForQRLogin(session, (status) => {
605
+ if (status === "scaned")
606
+ process.stdout.write("已扫码,验证中...\n");
607
+ });
608
+ if (result.connected && result.botToken) {
609
+ cbApiToken = result.botToken;
610
+ if (result.baseUrl)
611
+ cbApiUrl = result.baseUrl;
612
+ console.log("\n✅ 登录成功!");
613
+ if (result.userId) {
614
+ console.log(` 用户 ID: ${result.userId}`);
615
+ console.log(" 提示:可将此 ID 添加到白名单");
616
+ }
617
+ }
618
+ else {
619
+ console.log(`\n❌ 登录失败: ${result.message}`);
620
+ if (platform === "clawbot")
621
+ return false;
622
+ }
623
+ }
624
+ catch (err) {
625
+ console.error(`\n❌ 二维码获取失败: ${err instanceof Error ? err.message : String(err)}`);
626
+ if (platform === "clawbot")
627
+ return false;
628
+ }
629
+ }
630
+ else {
631
+ const cbResp = await prompts([
632
+ {
633
+ type: "text",
634
+ name: "apiUrl",
635
+ message: "ClawBot iLink API 地址(默认 http://127.0.0.1:26322)",
636
+ initial: cbApiUrl,
637
+ },
638
+ {
639
+ type: "text",
640
+ name: "apiToken",
641
+ message: "ClawBot Bearer Token",
642
+ initial: existing?.platforms?.clawbot?.apiToken ?? "",
643
+ validate: (v) => (v.trim() ? true : "Token 不能为空"),
644
+ },
645
+ ], { onCancel });
646
+ cbApiUrl = cbResp.apiUrl?.trim() || cbApiUrl;
647
+ cbApiToken = cbResp.apiToken?.trim() || "";
648
+ }
649
+ if (cbApiToken) {
650
+ config.platforms.clawbot = {
651
+ enabled: true,
652
+ apiUrl: cbApiUrl,
653
+ apiToken: cbApiToken,
654
+ };
655
+ }
656
+ else if (platform === "clawbot") {
657
+ return false;
658
+ }
659
+ }
566
660
  if (selectedPlatforms.includes("wework")) {
567
661
  const weworkResp = await prompts([
568
662
  {
@@ -687,6 +781,15 @@ export async function runInteractiveSetup() {
687
781
  initial: dtIds,
688
782
  });
689
783
  }
784
+ if (selectedPlatforms.includes("clawbot")) {
785
+ const cbIds = existing?.platforms?.clawbot?.allowedUserIds?.join(", ") ?? "";
786
+ commonPrompts.push({
787
+ type: "text",
788
+ name: "clawbotAllowedUserIds",
789
+ message: "ClawBot 白名单用户 ID(可选,逗号分隔,留空=所有人可访问)",
790
+ initial: cbIds,
791
+ });
792
+ }
690
793
  commonPrompts.push({
691
794
  type: "text",
692
795
  name: "workDir",
@@ -796,6 +899,9 @@ export async function runInteractiveSetup() {
796
899
  const dingtalkIds = selectedPlatforms.includes("dingtalk")
797
900
  ? parseIds(commonResp.dingtalkAllowedUserIds)
798
901
  : parseIds(existing?.platforms?.dingtalk?.allowedUserIds?.join(", "));
902
+ const clawbotIds = selectedPlatforms.includes("clawbot")
903
+ ? parseIds(commonResp.clawbotAllowedUserIds)
904
+ : parseIds(existing?.platforms?.clawbot?.allowedUserIds?.join(", "));
799
905
  // 增量合并:以已有配置为底,只覆盖本次选中的平台(不写入根级旧字段 telegramBotToken 等)
800
906
  const base = existing
801
907
  ? JSON.parse(JSON.stringify(existing))
@@ -985,6 +1091,27 @@ export async function runInteractiveSetup() {
985
1091
  else {
986
1092
  outPlatforms.dingtalk = { enabled: false, aiCommand: "claude", allowedUserIds: dingtalkIds };
987
1093
  }
1094
+ if (selectedPlatforms.includes("clawbot")) {
1095
+ const cbConfig = config.platforms?.clawbot;
1096
+ const baseCb = base?.platforms?.clawbot;
1097
+ outPlatforms.clawbot = {
1098
+ enabled: true,
1099
+ aiCommand: defaultPlatformAi(baseCb?.aiCommand),
1100
+ apiUrl: String(cbConfig?.apiUrl ?? baseCb?.apiUrl ?? "http://127.0.0.1:26322"),
1101
+ apiToken: String(cbConfig?.apiToken ?? baseCb?.apiToken ?? ""),
1102
+ allowedUserIds: clawbotIds,
1103
+ };
1104
+ }
1105
+ else if (basePlatforms?.clawbot) {
1106
+ outPlatforms.clawbot = {
1107
+ ...basePlatforms.clawbot,
1108
+ aiCommand: defaultPlatformAi(basePlatforms.clawbot.aiCommand),
1109
+ allowedUserIds: clawbotIds.length > 0 ? clawbotIds : basePlatforms.clawbot.allowedUserIds ?? [],
1110
+ };
1111
+ }
1112
+ else {
1113
+ outPlatforms.clawbot = { enabled: false, aiCommand: "claude", allowedUserIds: clawbotIds };
1114
+ }
988
1115
  const dir = dirname(configPath);
989
1116
  if (!existsSync(dir)) {
990
1117
  mkdirSync(dir, { recursive: true });
@@ -1005,8 +1132,9 @@ const PLATFORM_LABELS = {
1005
1132
  wework: "企业微信",
1006
1133
  dingtalk: "钉钉",
1007
1134
  workbuddy: "WorkBuddy 微信客服",
1135
+ clawbot: "ClawBot 微信 iLink",
1008
1136
  };
1009
- const ALL_PLATFORMS = ["telegram", "feishu", "qq", "wework", "dingtalk", "workbuddy"];
1137
+ const ALL_PLATFORMS = ["telegram", "feishu", "qq", "wework", "dingtalk", "workbuddy", "clawbot"];
1010
1138
  /**
1011
1139
  * 启动时让用户选择要启用的平台(无论单通道还是多通道)
1012
1140
  * 显示全部 4 个平台,已配置的预选;若用户选择未配置的,引导运行 init
@@ -6,8 +6,8 @@ export interface DingTalkActiveTarget {
6
6
  updatedAt: number;
7
7
  }
8
8
  export declare function loadActiveChats(): void;
9
- export declare function getActiveChatId(platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework' | 'workbuddy'): string | undefined;
10
- export declare function setActiveChatId(platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework' | 'workbuddy', chatId: string): void;
9
+ export declare function getActiveChatId(platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework' | 'workbuddy' | 'clawbot'): string | undefined;
10
+ export declare function setActiveChatId(platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework' | 'workbuddy' | 'clawbot', chatId: string): void;
11
11
  export declare function getDingTalkActiveTarget(): DingTalkActiveTarget | undefined;
12
12
  export declare function setDingTalkActiveTarget(target: Omit<DingTalkActiveTarget, 'updatedAt'>): void;
13
13
  export declare function flushActiveChats(): void;
@@ -42,5 +42,19 @@ export interface TaskRunState {
42
42
  startedAt: number;
43
43
  /** AI 工具标识,用于动态显示工具名称。 */
44
44
  toolId: string;
45
+ /**
46
+ * 进程退出(shutdown / 崩溃)时,用于为仍在运行的任务补发一条终态遥测事件,
47
+ * 避免 `ai.task.start` 没有对应的 complete/error(遥测里的 `miss`)。
48
+ * 已哈希(与 ai.task.* emit 处一致),不含原始 userId/msgId。
49
+ */
50
+ taskKey: string;
51
+ platform: string;
52
+ /** 已哈希的 userId,与 ai.task.start/complete/error 中的 userKey 一致。 */
53
+ userKey: string;
45
54
  }
55
+ /**
56
+ * 将 AI/CLI 错误文本归类为一个稳定的 errorType 字符串,用于遥测聚合。
57
+ * 分类基于线上真实错误签名(见 logs/r2-events),新增分支时同步更新测试。
58
+ */
59
+ export declare function classifyErrorType(error: string): string;
46
60
  export declare function runAITask(deps: TaskDeps, ctx: TaskContext, prompt: string, toolAdapter: ToolAdapter, platformAdapter: TaskAdapter): Promise<void>;