@wu529778790/open-im 1.8.1-beta.2 → 1.8.1-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 (55) hide show
  1. package/dist/access/access-control.js +1 -1
  2. package/dist/adapters/claude-sdk-adapter.js +94 -36
  3. package/dist/channels/capabilities.js +5 -0
  4. package/dist/cli.js +5 -2
  5. package/dist/commands/handler.d.ts +1 -2
  6. package/dist/commands/handler.js +6 -18
  7. package/dist/config-web-page-i18n.d.ts +12 -0
  8. package/dist/config-web-page-i18n.js +12 -0
  9. package/dist/config-web-page-script.js +1 -0
  10. package/dist/config-web-page-template.js +48 -1
  11. package/dist/config-web.js +110 -7
  12. package/dist/config.d.ts +25 -1
  13. package/dist/config.js +46 -0
  14. package/dist/constants.d.ts +2 -0
  15. package/dist/constants.js +2 -0
  16. package/dist/dingtalk/client.js +11 -3
  17. package/dist/dingtalk/event-handler.js +18 -3
  18. package/dist/dingtalk/message-sender.js +13 -0
  19. package/dist/feishu/event-handler.js +144 -10
  20. package/dist/index.js +26 -2
  21. package/dist/manager-control.js +7 -0
  22. package/dist/qq/client.js +111 -88
  23. package/dist/qq/event-handler.js +16 -2
  24. package/dist/qq/message-sender.js +11 -0
  25. package/dist/service-control.js +4 -0
  26. package/dist/session/session-manager.js +11 -1
  27. package/dist/setup.js +2 -1
  28. package/dist/shared/active-chats.d.ts +2 -2
  29. package/dist/shared/ai-task.js +13 -1
  30. package/dist/shared/chat-user-map.js +11 -0
  31. package/dist/shared/media-storage.js +27 -0
  32. package/dist/telegram/client.js +25 -3
  33. package/dist/telegram/event-handler.js +44 -8
  34. package/dist/telegram/message-sender.js +13 -0
  35. package/dist/wechat/auth/qclaw-api.js +1 -1
  36. package/dist/wechat/client.js +81 -4
  37. package/dist/wechat/event-handler.js +10 -3
  38. package/dist/wework/client.js +36 -14
  39. package/dist/wework/event-handler.js +39 -4
  40. package/dist/wework/message-sender.js +53 -21
  41. package/dist/workbuddy/centrifuge-client.d.ts +74 -0
  42. package/dist/workbuddy/centrifuge-client.js +272 -0
  43. package/dist/workbuddy/client.d.ts +27 -0
  44. package/dist/workbuddy/client.js +162 -0
  45. package/dist/workbuddy/event-handler.d.ts +11 -0
  46. package/dist/workbuddy/event-handler.js +118 -0
  47. package/dist/workbuddy/index.d.ts +8 -0
  48. package/dist/workbuddy/index.js +8 -0
  49. package/dist/workbuddy/message-sender.d.ts +16 -0
  50. package/dist/workbuddy/message-sender.js +51 -0
  51. package/dist/workbuddy/oauth.d.ts +114 -0
  52. package/dist/workbuddy/oauth.js +310 -0
  53. package/dist/workbuddy/types.d.ts +86 -0
  54. package/dist/workbuddy/types.js +4 -0
  55. package/package.json +4 -2
@@ -117,6 +117,7 @@ export function getHealthPlatformSnapshot(file, env = process.env) {
117
117
  const fileQQ = file.platforms?.qq;
118
118
  const fileWework = file.platforms?.wework;
119
119
  const fileDingtalk = file.platforms?.dingtalk;
120
+ const fileWorkbuddy = file.platforms?.workbuddy;
120
121
  const telegramBotToken = env.TELEGRAM_BOT_TOKEN ?? fileTelegram?.botToken ?? file.telegramBotToken;
121
122
  const feishuAppId = env.FEISHU_APP_ID ?? fileFeishu?.appId ?? file.feishuAppId;
122
123
  const feishuAppSecret = env.FEISHU_APP_SECRET ?? fileFeishu?.appSecret ?? file.feishuAppSecret;
@@ -126,6 +127,9 @@ export function getHealthPlatformSnapshot(file, env = process.env) {
126
127
  const weworkSecret = env.WEWORK_SECRET ?? fileWework?.secret;
127
128
  const dingtalkClientId = env.DINGTALK_CLIENT_ID ?? fileDingtalk?.clientId;
128
129
  const dingtalkClientSecret = env.DINGTALK_CLIENT_SECRET ?? fileDingtalk?.clientSecret;
130
+ const workbuddyAccessToken = fileWorkbuddy?.accessToken;
131
+ const workbuddyRefreshToken = fileWorkbuddy?.refreshToken;
132
+ const workbuddyUserId = fileWorkbuddy?.userId;
129
133
  return {
130
134
  telegram: {
131
135
  configured: !!telegramBotToken,
@@ -157,6 +161,12 @@ export function getHealthPlatformSnapshot(file, env = process.env) {
157
161
  healthy: !!(dingtalkClientId && dingtalkClientSecret),
158
162
  message: dingtalkClientId && dingtalkClientSecret ? "Client ID and Secret configured" : "Missing credentials",
159
163
  },
164
+ workbuddy: {
165
+ configured: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId),
166
+ enabled: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId) && fileWorkbuddy?.enabled !== false,
167
+ healthy: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId),
168
+ message: workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId ? "OAuth credentials configured" : "Missing credentials",
169
+ },
160
170
  };
161
171
  }
162
172
  function splitCsv(value) {
@@ -170,10 +180,20 @@ function clean(value) {
170
180
  const trimmed = value.trim();
171
181
  return trimmed ? trimmed : undefined;
172
182
  }
183
+ const MAX_REQUEST_BODY_BYTES = 1 * 1024 * 1024; // 1 MB
173
184
  function readJson(request) {
174
185
  return new Promise((resolve, reject) => {
175
186
  const chunks = [];
176
- request.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
187
+ let totalBytes = 0;
188
+ request.on("data", (chunk) => {
189
+ totalBytes += chunk.length;
190
+ if (totalBytes > MAX_REQUEST_BODY_BYTES) {
191
+ reject(new Error("Request body too large (max 1 MB)"));
192
+ request.destroy();
193
+ return;
194
+ }
195
+ chunks.push(Buffer.from(chunk));
196
+ });
177
197
  request.on("end", () => {
178
198
  try {
179
199
  const raw = Buffer.concat(chunks).toString("utf-8");
@@ -190,6 +210,11 @@ function json(response, statusCode, body) {
190
210
  response.writeHead(statusCode, { "content-type": "application/json; charset=utf-8" });
191
211
  response.end(JSON.stringify(body));
192
212
  }
213
+ function maskSecret(value) {
214
+ if (!value || value.length <= 4)
215
+ return value ? "****" : "";
216
+ return value.slice(0, 2) + "****" + value.slice(-2);
217
+ }
193
218
  function buildInitialPayload(file) {
194
219
  // Load Claude settings from ~/.claude/settings.json
195
220
  const claudeEnv = loadClaudeSettingsEnv();
@@ -198,7 +223,7 @@ function buildInitialPayload(file) {
198
223
  telegram: {
199
224
  enabled: file.platforms?.telegram?.enabled ?? Boolean(file.platforms?.telegram?.botToken),
200
225
  aiCommand: file.platforms?.telegram?.aiCommand ?? "",
201
- botToken: file.platforms?.telegram?.botToken ?? "",
226
+ botToken: maskSecret(file.platforms?.telegram?.botToken),
202
227
  proxy: file.platforms?.telegram?.proxy ?? "",
203
228
  allowedUserIds: (file.platforms?.telegram?.allowedUserIds ?? []).join(", "),
204
229
  },
@@ -206,31 +231,40 @@ function buildInitialPayload(file) {
206
231
  enabled: file.platforms?.feishu?.enabled ?? Boolean(file.platforms?.feishu?.appId && file.platforms?.feishu?.appSecret),
207
232
  aiCommand: file.platforms?.feishu?.aiCommand ?? "",
208
233
  appId: file.platforms?.feishu?.appId ?? "",
209
- appSecret: file.platforms?.feishu?.appSecret ?? "",
234
+ appSecret: maskSecret(file.platforms?.feishu?.appSecret),
210
235
  allowedUserIds: (file.platforms?.feishu?.allowedUserIds ?? []).join(", "),
211
236
  },
212
237
  qq: {
213
238
  enabled: file.platforms?.qq?.enabled ?? Boolean(file.platforms?.qq?.appId && file.platforms?.qq?.secret),
214
239
  aiCommand: file.platforms?.qq?.aiCommand ?? "",
215
240
  appId: file.platforms?.qq?.appId ?? "",
216
- secret: file.platforms?.qq?.secret ?? "",
241
+ secret: maskSecret(file.platforms?.qq?.secret),
217
242
  allowedUserIds: (file.platforms?.qq?.allowedUserIds ?? []).join(", "),
218
243
  },
219
244
  wework: {
220
245
  enabled: file.platforms?.wework?.enabled ?? Boolean(file.platforms?.wework?.corpId && file.platforms?.wework?.secret),
221
246
  aiCommand: file.platforms?.wework?.aiCommand ?? "",
222
247
  corpId: file.platforms?.wework?.corpId ?? "",
223
- secret: file.platforms?.wework?.secret ?? "",
248
+ secret: maskSecret(file.platforms?.wework?.secret),
224
249
  allowedUserIds: (file.platforms?.wework?.allowedUserIds ?? []).join(", "),
225
250
  },
226
251
  dingtalk: {
227
252
  enabled: file.platforms?.dingtalk?.enabled ?? Boolean(file.platforms?.dingtalk?.clientId && file.platforms?.dingtalk?.clientSecret),
228
253
  aiCommand: file.platforms?.dingtalk?.aiCommand ?? "",
229
254
  clientId: file.platforms?.dingtalk?.clientId ?? "",
230
- clientSecret: file.platforms?.dingtalk?.clientSecret ?? "",
255
+ clientSecret: maskSecret(file.platforms?.dingtalk?.clientSecret),
231
256
  cardTemplateId: file.platforms?.dingtalk?.cardTemplateId ?? "",
232
257
  allowedUserIds: (file.platforms?.dingtalk?.allowedUserIds ?? []).join(", "),
233
258
  },
259
+ workbuddy: {
260
+ enabled: file.platforms?.workbuddy?.enabled ?? Boolean(file.platforms?.workbuddy?.accessToken && file.platforms?.workbuddy?.refreshToken && file.platforms?.workbuddy?.userId),
261
+ aiCommand: file.platforms?.workbuddy?.aiCommand ?? "",
262
+ accessToken: maskSecret(file.platforms?.workbuddy?.accessToken),
263
+ refreshToken: maskSecret(file.platforms?.workbuddy?.refreshToken),
264
+ userId: file.platforms?.workbuddy?.userId ?? "",
265
+ baseUrl: file.platforms?.workbuddy?.baseUrl ?? "",
266
+ allowedUserIds: (file.platforms?.workbuddy?.allowedUserIds ?? []).join(", "),
267
+ },
234
268
  },
235
269
  ai: {
236
270
  aiCommand: file.aiCommand ?? "claude",
@@ -239,7 +273,7 @@ function buildInitialPayload(file) {
239
273
  claudeConfigPath: process.platform === 'win32'
240
274
  ? getClaudeConfigHome() + "\\.claude\\settings.json"
241
275
  : getClaudeConfigHome() + "/.claude/settings.json",
242
- claudeAuthToken: claudeEnv.ANTHROPIC_AUTH_TOKEN ?? "",
276
+ claudeAuthToken: maskSecret(claudeEnv.ANTHROPIC_AUTH_TOKEN),
243
277
  claudeBaseUrl: claudeEnv.ANTHROPIC_BASE_URL ?? "",
244
278
  claudeModel: claudeEnv.ANTHROPIC_MODEL ?? "",
245
279
  claudeProxy: file.tools?.claude?.proxy ?? "",
@@ -276,6 +310,12 @@ function validatePayload(payload) {
276
310
  errors.push("DingTalk client ID is required.");
277
311
  if (payload.platforms.dingtalk.enabled && !clean(payload.platforms.dingtalk.clientSecret))
278
312
  errors.push("DingTalk client secret is required.");
313
+ if (payload.platforms.workbuddy.enabled && !clean(payload.platforms.workbuddy.accessToken))
314
+ errors.push("WorkBuddy access token is required.");
315
+ if (payload.platforms.workbuddy.enabled && !clean(payload.platforms.workbuddy.refreshToken))
316
+ errors.push("WorkBuddy refresh token is required.");
317
+ if (payload.platforms.workbuddy.enabled && !clean(payload.platforms.workbuddy.userId))
318
+ errors.push("WorkBuddy user ID is required.");
279
319
  if (!clean(payload.ai.claudeWorkDir))
280
320
  errors.push("Default work directory is required.");
281
321
  if (!Number.isFinite(payload.ai.claudeTimeoutMs) || payload.ai.claudeTimeoutMs <= 0)
@@ -330,6 +370,17 @@ function validateConfigForPlatform(platform, config) {
330
370
  errors.push("DingTalk client secret is required and must be a non-empty string.");
331
371
  }
332
372
  break;
373
+ case "workbuddy":
374
+ if (!c.accessToken || typeof c.accessToken !== "string" || !clean(c.accessToken)) {
375
+ errors.push("WorkBuddy access token is required and must be a non-empty string.");
376
+ }
377
+ if (!c.refreshToken || typeof c.refreshToken !== "string" || !clean(c.refreshToken)) {
378
+ errors.push("WorkBuddy refresh token is required and must be a non-empty string.");
379
+ }
380
+ if (!c.userId || typeof c.userId !== "string" || !clean(c.userId)) {
381
+ errors.push("WorkBuddy user ID is required and must be a non-empty string.");
382
+ }
383
+ break;
333
384
  default:
334
385
  errors.push(`Unknown platform: ${platform}`);
335
386
  }
@@ -359,6 +410,7 @@ function createProbeConfig(values) {
359
410
  wechatAllowedUserIds: [],
360
411
  weworkAllowedUserIds: [],
361
412
  dingtalkAllowedUserIds: [],
413
+ workbuddyAllowedUserIds: [],
362
414
  aiCommand: "claude",
363
415
  codexCliPath: "codex",
364
416
  claudeWorkDir: process.cwd(),
@@ -454,6 +506,34 @@ async function probeDingTalk(config) {
454
506
  }
455
507
  return "DingTalk credentials are valid.";
456
508
  }
509
+ async function probeWorkBuddy(config) {
510
+ const accessToken = clean(String(config.accessToken ?? ""));
511
+ const refreshToken = clean(String(config.refreshToken ?? ""));
512
+ const userId = clean(String(config.userId ?? ""));
513
+ if (!accessToken || !refreshToken || !userId)
514
+ throw new Error("WorkBuddy access token, refresh token, and user ID are required.");
515
+ const baseUrl = clean(String(config.baseUrl ?? "")) || "https://copilot.tencent.com";
516
+ // Validate credentials by attempting to register workspace
517
+ const response = await fetch(`${baseUrl}/api/copilot/workspace/register`, {
518
+ method: "POST",
519
+ headers: {
520
+ "content-type": "application/json",
521
+ "authorization": `Bearer ${accessToken}`,
522
+ },
523
+ body: JSON.stringify({
524
+ userId,
525
+ hostId: "open-im-test",
526
+ workspaceId: "open-im-test-workspace",
527
+ workspaceName: "OpenIM Test Workspace",
528
+ }),
529
+ signal: AbortSignal.timeout(TEST_TIMEOUT_MS),
530
+ });
531
+ if (!response.ok) {
532
+ const body = await response.text();
533
+ throw new Error(`WorkBuddy authentication failed: ${body.slice(0, 200) || `HTTP ${response.status}`}`);
534
+ }
535
+ return "WorkBuddy credentials are valid.";
536
+ }
457
537
  export async function testPlatformConfig(platform, config) {
458
538
  const errors = validateConfigForPlatform(platform, config);
459
539
  if (errors.length > 0) {
@@ -470,6 +550,8 @@ export async function testPlatformConfig(platform, config) {
470
550
  return probeWeWork(config);
471
551
  case "dingtalk":
472
552
  return probeDingTalk(config);
553
+ case "workbuddy":
554
+ return probeWorkBuddy(config);
473
555
  default:
474
556
  throw new Error(`Unknown platform: ${platform}`);
475
557
  }
@@ -556,6 +638,16 @@ function toFileConfig(payload, existing) {
556
638
  cardTemplateId: clean(payload.platforms.dingtalk.cardTemplateId),
557
639
  allowedUserIds: splitCsv(payload.platforms.dingtalk.allowedUserIds),
558
640
  },
641
+ workbuddy: {
642
+ ...existing.platforms?.workbuddy,
643
+ enabled: payload.platforms.workbuddy.enabled,
644
+ aiCommand: clean(payload.platforms.workbuddy.aiCommand),
645
+ accessToken: clean(payload.platforms.workbuddy.accessToken),
646
+ refreshToken: clean(payload.platforms.workbuddy.refreshToken),
647
+ userId: clean(payload.platforms.workbuddy.userId),
648
+ baseUrl: clean(payload.platforms.workbuddy.baseUrl),
649
+ allowedUserIds: splitCsv(payload.platforms.workbuddy.allowedUserIds),
650
+ },
559
651
  },
560
652
  };
561
653
  }
@@ -811,6 +903,7 @@ export async function startWebConfigServer(options) {
811
903
  const fileQQ = file.platforms?.qq;
812
904
  const fileWework = file.platforms?.wework;
813
905
  const fileDingtalk = file.platforms?.dingtalk;
906
+ const fileWorkbuddy = file.platforms?.workbuddy;
814
907
  const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN ?? fileTelegram?.botToken ?? file.telegramBotToken;
815
908
  const feishuAppId = process.env.FEISHU_APP_ID ?? fileFeishu?.appId ?? file.feishuAppId;
816
909
  const feishuAppSecret = process.env.FEISHU_APP_SECRET ?? fileFeishu?.appSecret ?? file.feishuAppSecret;
@@ -820,6 +913,9 @@ export async function startWebConfigServer(options) {
820
913
  const weworkSecret = process.env.WEWORK_SECRET ?? fileWework?.secret;
821
914
  const dingtalkClientId = process.env.DINGTALK_CLIENT_ID ?? fileDingtalk?.clientId;
822
915
  const dingtalkClientSecret = process.env.DINGTALK_CLIENT_SECRET ?? fileDingtalk?.clientSecret;
916
+ const workbuddyAccessToken = fileWorkbuddy?.accessToken;
917
+ const workbuddyRefreshToken = fileWorkbuddy?.refreshToken;
918
+ const workbuddyUserId = fileWorkbuddy?.userId;
823
919
  const platforms = {};
824
920
  // 检查 Telegram
825
921
  platforms.telegram = {
@@ -856,6 +952,13 @@ export async function startWebConfigServer(options) {
856
952
  healthy: !!(dingtalkClientId && dingtalkClientSecret),
857
953
  message: (dingtalkClientId && dingtalkClientSecret) ? "Client ID and Secret configured" : "Missing credentials"
858
954
  };
955
+ // 检查 WorkBuddy
956
+ platforms.workbuddy = {
957
+ configured: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId),
958
+ enabled: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId) && fileWorkbuddy?.enabled !== false,
959
+ healthy: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId),
960
+ message: (workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId) ? "OAuth credentials configured" : "Missing credentials"
961
+ };
859
962
  json(response, 200, { platforms, serviceStatus: getServiceStatus() });
860
963
  return;
861
964
  }
package/dist/config.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type LogLevel } from './logger.js';
2
- export type Platform = 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework';
2
+ export type Platform = 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework' | 'workbuddy';
3
3
  export type AiCommand = 'claude' | 'codex' | 'codebuddy';
4
4
  export interface Config {
5
5
  enabledPlatforms: Platform[];
@@ -29,6 +29,7 @@ export interface Config {
29
29
  wechatAllowedUserIds: string[];
30
30
  weworkAllowedUserIds: string[];
31
31
  dingtalkAllowedUserIds: string[];
32
+ workbuddyAllowedUserIds: string[];
32
33
  aiCommand: AiCommand;
33
34
  codexCliPath: string;
34
35
  codebuddyCliPath: string;
@@ -82,6 +83,17 @@ export interface Config {
82
83
  allowedUserIds: string[];
83
84
  cardTemplateId?: string;
84
85
  };
86
+ workbuddy?: {
87
+ enabled: boolean;
88
+ aiCommand?: AiCommand;
89
+ allowedUserIds: string[];
90
+ accessToken?: string;
91
+ refreshToken?: string;
92
+ userId?: string;
93
+ baseUrl?: string;
94
+ guid?: string;
95
+ workspacePath?: string;
96
+ };
85
97
  };
86
98
  }
87
99
  export interface FilePlatformTelegram {
@@ -134,6 +146,17 @@ export interface FilePlatformDingtalk {
134
146
  allowedUserIds?: string[];
135
147
  cardTemplateId?: string;
136
148
  }
149
+ interface FilePlatformWorkBuddy {
150
+ enabled?: boolean;
151
+ aiCommand?: AiCommand;
152
+ allowedUserIds?: string[];
153
+ accessToken?: string;
154
+ refreshToken?: string;
155
+ userId?: string;
156
+ baseUrl?: string;
157
+ guid?: string;
158
+ workspacePath?: string;
159
+ }
137
160
  export interface FileToolClaude {
138
161
  cliPath?: string;
139
162
  workDir?: string;
@@ -167,6 +190,7 @@ export interface FileConfig {
167
190
  wechat?: FilePlatformWechat;
168
191
  wework?: FilePlatformWework;
169
192
  dingtalk?: FilePlatformDingtalk;
193
+ workbuddy?: FilePlatformWorkBuddy;
170
194
  };
171
195
  env?: Record<string, string>;
172
196
  aiCommand?: string;
package/dist/config.js CHANGED
@@ -233,6 +233,7 @@ export function loadConfig() {
233
233
  const fileWechat = file.platforms?.wechat;
234
234
  const fileWework = file.platforms?.wework;
235
235
  const fileDingtalk = file.platforms?.dingtalk;
236
+ const fileWorkBuddy = file.platforms?.workbuddy;
236
237
  // 1. 加载各平台凭证(env 优先,其次新结构,最后旧字段)
237
238
  const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN ??
238
239
  fileTelegram?.botToken ??
@@ -276,6 +277,19 @@ export function loadConfig() {
276
277
  fileDingtalk?.clientSecret;
277
278
  const dingtalkCardTemplateId = process.env.DINGTALK_CARD_TEMPLATE_ID ??
278
279
  fileDingtalk?.cardTemplateId;
280
+ // WorkBuddy credentials
281
+ const workbuddyAccessToken = process.env.WORKBUDDY_ACCESS_TOKEN ??
282
+ fileWorkBuddy?.accessToken;
283
+ const workbuddyRefreshToken = process.env.WORKBUDDY_REFRESH_TOKEN ??
284
+ fileWorkBuddy?.refreshToken;
285
+ const workbuddyUserId = process.env.WORKBUDDY_USER_ID ??
286
+ fileWorkBuddy?.userId;
287
+ const workbuddyBaseUrl = process.env.WORKBUDDY_BASE_URL ??
288
+ fileWorkBuddy?.baseUrl;
289
+ const workbuddyGuid = process.env.WORKBUDDY_GUID ??
290
+ fileWorkBuddy?.guid;
291
+ const workbuddyWorkspacePath = process.env.WORKBUDDY_WORKSPACE_PATH ??
292
+ fileWorkBuddy?.workspacePath;
279
293
  // 2. 计算启用平台
280
294
  const enabledPlatforms = [];
281
295
  const telegramEnabledFlag = fileTelegram?.enabled;
@@ -284,6 +298,7 @@ export function loadConfig() {
284
298
  const wechatEnabledFlag = fileWechat?.enabled;
285
299
  const weworkEnabledFlag = fileWework?.enabled;
286
300
  const dingtalkEnabledFlag = fileDingtalk?.enabled;
301
+ const workbuddyEnabledFlag = fileWorkBuddy?.enabled;
287
302
  const telegramEnabled = !!telegramBotToken && (telegramEnabledFlag !== false);
288
303
  const feishuEnabled = !!(feishuAppId && feishuAppSecret) && (feishuEnabledFlag !== false);
289
304
  const qqEnabled = !!(qqAppId && qqSecret) && (qqEnabledFlag !== false);
@@ -294,6 +309,8 @@ export function loadConfig() {
294
309
  // 企业微信只需要 corpId (botId) 和 secret
295
310
  const weworkEnabled = !!(weworkCorpId && weworkSecret) && (weworkEnabledFlag !== false);
296
311
  const dingtalkEnabled = !!(dingtalkClientId && dingtalkClientSecret) && (dingtalkEnabledFlag !== false);
312
+ // WorkBuddy 需要 OAuth 凭证
313
+ const workbuddyEnabled = !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId) && (workbuddyEnabledFlag !== false);
297
314
  if (telegramEnabled)
298
315
  enabledPlatforms.push('telegram');
299
316
  if (feishuEnabled)
@@ -306,6 +323,8 @@ export function loadConfig() {
306
323
  enabledPlatforms.push('wework');
307
324
  if (dingtalkEnabled)
308
325
  enabledPlatforms.push('dingtalk');
326
+ if (workbuddyEnabled)
327
+ enabledPlatforms.push('workbuddy');
309
328
  if (enabledPlatforms.length === 0) {
310
329
  throw new Error('至少需要配置 Telegram、Feishu、WeChat、WeWork 或 DingTalk 其中一个平台(可以通过环境变量或 config.json)');
311
330
  }
@@ -332,6 +351,9 @@ export function loadConfig() {
332
351
  const dingtalkAllowedUserIds = process.env.DINGTALK_ALLOWED_USER_IDS !== undefined
333
352
  ? parseCommaSeparated(process.env.DINGTALK_ALLOWED_USER_IDS)
334
353
  : fileDingtalk?.allowedUserIds ?? allowedUserIds;
354
+ const workbuddyAllowedUserIds = process.env.WORKBUDDY_ALLOWED_USER_IDS !== undefined
355
+ ? parseCommaSeparated(process.env.WORKBUDDY_ALLOWED_USER_IDS)
356
+ : fileWorkBuddy?.allowedUserIds ?? allowedUserIds;
335
357
  // 5. AI / 工作目录 / 安全配置(从 tools 读取)
336
358
  const aiCommand = normalizeAiCommand(process.env.AI_COMMAND ?? file.aiCommand, 'claude');
337
359
  const tc = file.tools?.claude ?? {};
@@ -579,6 +601,29 @@ export function loadConfig() {
579
601
  allowedUserIds: dingtalkAllowedUserIds,
580
602
  cardTemplateId: dingtalkCardTemplateId,
581
603
  },
604
+ workbuddy: workbuddyEnabled
605
+ ? {
606
+ enabled: true,
607
+ aiCommand: normalizeAiCommand(file.platforms?.workbuddy?.aiCommand, aiCommand),
608
+ allowedUserIds: workbuddyAllowedUserIds,
609
+ accessToken: workbuddyAccessToken,
610
+ refreshToken: workbuddyRefreshToken,
611
+ userId: workbuddyUserId,
612
+ baseUrl: workbuddyBaseUrl,
613
+ guid: workbuddyGuid,
614
+ workspacePath: workbuddyWorkspacePath,
615
+ }
616
+ : {
617
+ enabled: false,
618
+ aiCommand: normalizeAiCommand(file.platforms?.workbuddy?.aiCommand, aiCommand),
619
+ allowedUserIds: workbuddyAllowedUserIds,
620
+ accessToken: workbuddyAccessToken,
621
+ refreshToken: workbuddyRefreshToken,
622
+ userId: workbuddyUserId,
623
+ baseUrl: workbuddyBaseUrl,
624
+ guid: workbuddyGuid,
625
+ workspacePath: workbuddyWorkspacePath,
626
+ },
582
627
  };
583
628
  return {
584
629
  enabledPlatforms,
@@ -608,6 +653,7 @@ export function loadConfig() {
608
653
  wechatAllowedUserIds,
609
654
  weworkAllowedUserIds,
610
655
  dingtalkAllowedUserIds,
656
+ workbuddyAllowedUserIds,
611
657
  aiCommand,
612
658
  codexCliPath,
613
659
  codebuddyCliPath,
@@ -11,6 +11,8 @@ export declare const CARDKIT_THROTTLE_MS = 80;
11
11
  export declare const TELEGRAM_THROTTLE_MS = 200;
12
12
  /** WeChat 流式更新节流:1000ms(AGP 协议建议值) */
13
13
  export declare const WECHAT_THROTTLE_MS = 1000;
14
+ /** WorkBuddy 流式更新节流:1000ms(Centrifuge 协议建议值) */
15
+ export declare const WORKBUDDY_THROTTLE_MS = 1000;
14
16
  export declare const WEWORK_THROTTLE_MS = 500;
15
17
  export declare const MAX_TELEGRAM_MESSAGE_LENGTH = 4000;
16
18
  export declare const MAX_FEISHU_MESSAGE_LENGTH = 4000;
package/dist/constants.js CHANGED
@@ -32,6 +32,8 @@ export const CARDKIT_THROTTLE_MS = 80;
32
32
  export const TELEGRAM_THROTTLE_MS = 200;
33
33
  /** WeChat 流式更新节流:1000ms(AGP 协议建议值) */
34
34
  export const WECHAT_THROTTLE_MS = 1000;
35
+ /** WorkBuddy 流式更新节流:1000ms(Centrifuge 协议建议值) */
36
+ export const WORKBUDDY_THROTTLE_MS = 1000;
35
37
  export const WEWORK_THROTTLE_MS = 500;
36
38
  export const MAX_TELEGRAM_MESSAGE_LENGTH = 4000;
37
39
  export const MAX_FEISHU_MESSAGE_LENGTH = 4000;
@@ -7,7 +7,9 @@ const TEXT_MSG_KEY = 'sampleText';
7
7
  const DINGTALK_STREAM_HOST = 'wss-open-connection.dingtalk.com';
8
8
  let client = null;
9
9
  let messageHandler = null;
10
+ // sessionWebhook 有过期时间(约 2 小时),需要记录时间戳
10
11
  const sessionWebhookByChat = new Map();
12
+ const WEBHOOK_TTL_MS = 90 * 60 * 1000; // 90 分钟后视为过期
11
13
  const unionIdByUserId = new Map();
12
14
  let dingtalkWarnFilterInstalled = false;
13
15
  export function shouldSuppressDingTalkSocketWarn(args) {
@@ -44,13 +46,19 @@ function getClient() {
44
46
  export function registerSessionWebhook(chatId, sessionWebhook) {
45
47
  if (!chatId || !sessionWebhook)
46
48
  return;
47
- sessionWebhookByChat.set(chatId, sessionWebhook);
49
+ sessionWebhookByChat.set(chatId, { webhook: sessionWebhook, registeredAt: Date.now() });
48
50
  }
49
51
  async function sendByWebhook(chatId, body) {
50
- const sessionWebhook = sessionWebhookByChat.get(chatId);
51
- if (!sessionWebhook) {
52
+ const entry = sessionWebhookByChat.get(chatId);
53
+ if (!entry) {
52
54
  throw new Error(`DingTalk sessionWebhook unavailable for chat ${chatId}`);
53
55
  }
56
+ // 检查 webhook 是否过期
57
+ if (Date.now() - entry.registeredAt > WEBHOOK_TTL_MS) {
58
+ sessionWebhookByChat.delete(chatId);
59
+ throw new Error(`DingTalk sessionWebhook expired for chat ${chatId}`);
60
+ }
61
+ const sessionWebhook = entry.webhook;
54
62
  const accessToken = await getClient().getAccessToken();
55
63
  const res = await fetch(sessionWebhook, {
56
64
  method: 'POST',
@@ -99,7 +99,8 @@ async function buildMediaPrompt(message, kind, robotCodeFallback) {
99
99
  fallbackExtension: kind === 'image' ? 'jpg' : 'bin',
100
100
  });
101
101
  }
102
- catch {
102
+ catch (err) {
103
+ log.warn('Failed to download DingTalk media from URL:', err);
103
104
  localPath = undefined;
104
105
  }
105
106
  }
@@ -118,7 +119,8 @@ async function buildMediaPrompt(message, kind, robotCodeFallback) {
118
119
  localPath = await saveBufferMedia(downloaded.buffer, extension, basenameHint);
119
120
  }
120
121
  }
121
- catch {
122
+ catch (err) {
123
+ log.warn('Failed to download DingTalk media via robotCode:', err);
122
124
  localPath = undefined;
123
125
  }
124
126
  }
@@ -185,7 +187,20 @@ export function setupDingTalkHandlers(config, sessionManager) {
185
187
  : undefined;
186
188
  log.info(`[AI_REQUEST] Running ${aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
187
189
  const toolId = aiCommand;
188
- const msgId = await sendThinkingMessage(chatId, replyToMessageId, toolId, dingtalkTarget);
190
+ let msgId;
191
+ try {
192
+ msgId = await sendThinkingMessage(chatId, replyToMessageId, toolId, dingtalkTarget);
193
+ }
194
+ catch (err) {
195
+ log.error('Failed to send thinking message:', err);
196
+ try {
197
+ await sendTextReply(chatId, '启动 AI 处理失败,请重试。');
198
+ }
199
+ catch (err) {
200
+ log.warn('Failed to send startup error reply:', err);
201
+ }
202
+ return;
203
+ }
189
204
  const stopTyping = startTypingLoop(chatId);
190
205
  const taskKey = `${userId}:${msgId}`;
191
206
  await runAITask({ config, sessionManager }, { userId, chatId, workDir, sessionId, convId, platform: 'dingtalk', taskKey }, prompt, toolAdapter, {
@@ -24,6 +24,19 @@ const FLOW_STATUS = {
24
24
  };
25
25
  let senderSettings = {};
26
26
  const streamStates = new Map();
27
+ // Periodic cleanup of orphaned stream states (max 30 minutes)
28
+ const STREAM_MAX_AGE_MS = 30 * 60 * 1000;
29
+ setInterval(() => {
30
+ const now = Date.now();
31
+ for (const [id, state] of streamStates) {
32
+ // streamStates in DingTalk don't have createdAt, clean up by size
33
+ if (streamStates.size > 50) {
34
+ streamStates.delete(id);
35
+ log.info(`Cleaned up old DingTalk stream state: ${id}`);
36
+ break; // Clean one at a time to avoid blocking
37
+ }
38
+ }
39
+ }, STREAM_MAX_AGE_MS);
27
40
  function generateMessageId() {
28
41
  return `${Date.now()}-${randomBytes(6).toString('hex')}`;
29
42
  }