@dingtalk-real-ai/dingtalk-connector 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.8.2] - 2026-03-22
9
+
10
+ ### 修复 / Fixes
11
+ - 🐛 **多账号重复启动问题** - 修复 `enabled: false` 的账号仍会建立 WebSocket 连接的问题,禁用账号现在正确保持 pending 状态直到 Gateway 停止
12
+ **Multi-account duplicate startup** - Fixed accounts with `enabled: false` still establishing WebSocket connections; disabled accounts now correctly remain in a pending state until the Gateway stops
13
+
14
+ - 🐛 **相同 clientId 账号去重** - 修复多个账号配置相同 `clientId` 时建立重复连接的问题,通过静态配置分析确保同一 `clientId` 只有列表中第一个启用账号建立连接
15
+ **Duplicate clientId deduplication** - Fixed duplicate connections when multiple accounts share the same `clientId`; static config analysis now ensures only the first enabled account per `clientId` establishes a connection
16
+
17
+
18
+ ### 改进 / Improvements
19
+ - ✅ **Onboarding 配置向导优化** - 改进钉钉连接器配置引导逻辑,调整凭据输入顺序(先 Client ID 后 Client Secret),优化引导文案
20
+ **Onboarding wizard improvement** - Improved DingTalk connector onboarding flow, adjusted credential input order (Client ID first, then Client Secret), and refined guidance text
21
+
22
+ - ✅ **会话 Key 遵循 OpenClaw 规范** - 会话上下文按 OpenClaw 标准规则构建,通过 `channel`、`accountId`、`chatType`、`peerId` 唯一标识会话,支持 `sharedMemoryAcrossConversations` 跨会话记忆共享
23
+ **Session key follows OpenClaw convention** - Session context now built per OpenClaw standard rules, uniquely identified via `channel`, `accountId`, `chatType`, `peerId`; supports `sharedMemoryAcrossConversations` for cross-conversation memory sharing
24
+
25
+ - ✅ **消息处理逻辑优化** - 重构消息处理流程,提升消息响应速度和处理可靠性,确保消息按序正确处理
26
+ **Message processing logic optimization** - Refactored message processing flow to improve response speed and reliability, ensuring messages are processed correctly in order
27
+
28
+ ## [0.8.1] - 2026-03-20
29
+
30
+ ### 修复 / Fixes
31
+ - 🐛 **文件和图片下载 OSS 签名验证失败** - 修复默认 `Content-Type` 请求头导致 OSS 签名验证失败的问题,确保文件和图片能够正常下载
32
+ **File and image download OSS signature verification failure** - Fixed OSS signature verification failure caused by default `Content-Type` header, ensuring files and images download correctly
33
+
8
34
  ## [0.8.0] - 2026-03-20
9
35
 
10
36
  ### 重构 / Refactoring
package/README.en.md CHANGED
@@ -268,20 +268,26 @@ Configure multiple bots connected to different agents:
268
268
  "agents": {
269
269
  "list": [
270
270
  {
271
- "agentId": "ding-bot1",
271
+ "id": "ding-bot1",
272
+ "name": "Customer Service Bot",
272
273
  "model": "your-model-config",
273
- "persona": {
274
- "name": "Customer Service Bot",
275
- "systemPrompt": "You are a professional customer service assistant..."
274
+ "workspace": "~/.openclaw/workspace-bot1",
275
+ "identity": {
276
+ "name": "Service Assistant",
277
+ "theme": "customer service",
278
+ "emoji": "🤝"
276
279
  }
277
280
  // Other agent configurations...
278
281
  },
279
282
  {
280
- "agentId": "ding-bot2",
283
+ "id": "ding-bot2",
284
+ "name": "Technical Support Bot",
281
285
  "model": "your-model-config",
282
- "persona": {
283
- "name": "Technical Support Bot",
284
- "systemPrompt": "You are a technical support expert..."
286
+ "workspace": "~/.openclaw/workspace-bot2",
287
+ "identity": {
288
+ "name": "Tech Expert",
289
+ "theme": "technical support",
290
+ "emoji": "🔧"
285
291
  }
286
292
  // Other agent configurations...
287
293
  }
package/README.md CHANGED
@@ -279,20 +279,26 @@ openclaw logs --follow
279
279
  "agents": {
280
280
  "list": [
281
281
  {
282
- "agentId": "ding-bot1",
282
+ "id": "ding-bot1",
283
+ "name": "钉钉客服机器人",
283
284
  "model": "your-model-config",
284
- "persona": {
285
- "name": "钉钉客服机器人",
286
- "systemPrompt": "你是一个专业的客服助手..."
285
+ "workspace": "~/.openclaw/workspace-bot1",
286
+ "identity": {
287
+ "name": "客服小助手",
288
+ "theme": "专业客服",
289
+ "emoji": "🤝"
287
290
  }
288
291
  // 其他 agent 配置...
289
292
  },
290
293
  {
291
- "agentId": "ding-bot2",
294
+ "id": "ding-bot2",
295
+ "name": "钉钉技术支持机器人",
292
296
  "model": "your-model-config",
293
- "persona": {
294
- "name": "钉钉技术支持机器人",
295
- "systemPrompt": "你是一个技术支持专家..."
297
+ "workspace": "~/.openclaw/workspace-bot2",
298
+ "identity": {
299
+ "name": "技术专家",
300
+ "theme": "技术支持",
301
+ "emoji": "🔧"
296
302
  }
297
303
  // 其他 agent 配置...
298
304
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "dingtalk-connector",
3
3
  "name": "DingTalk Channel",
4
- "version": "0.8.0",
4
+ "version": "0.8.2",
5
5
  "description": "DingTalk (钉钉) messaging channel via Stream mode with AI Card streaming",
6
6
  "author": "DingTalk Real Team",
7
7
  "main": "index.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dingtalk-real-ai/dingtalk-connector",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "DingTalk (钉钉) channel connector — Stream mode with AI Card streaming",
5
5
  "main": "index.ts",
6
6
  "type": "module",
package/src/channel.ts CHANGED
@@ -495,64 +495,64 @@ export const dingtalkPlugin: ChannelPlugin<ResolvedDingtalkAccount> = {
495
495
  gateway: {
496
496
  startAccount: async (ctx) => {
497
497
  const account = resolveDingtalkAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
498
- const logger = createLogger(account.config?.debug ?? false, 'DingTalk:Gateway');
498
+
499
+ // 检查账号是否启用和配置
500
+ if (!account.enabled) {
501
+ ctx.log?.info?.(`dingtalk-connector[${ctx.accountId}] is disabled, skipping startup`);
502
+ // 返回一个永不 resolve 的 Promise,保持 pending 状态直到 abort
503
+ return new Promise<void>((resolve) => {
504
+ if (ctx.abortSignal?.aborted) {
505
+ resolve();
506
+ return;
507
+ }
508
+ ctx.abortSignal?.addEventListener('abort', () => resolve(), { once: true });
509
+ });
510
+ }
499
511
 
500
- logger.info(`startAccount 被调用:accountId=${ctx.accountId}`);
501
- try {
502
- logger.info('='.repeat(60));
503
- logger.info('开始加载 provider 模块...');
504
- const monitorModule = await import("./core/provider.ts");
505
- logger.info(`monitor module 加载完成`);
506
- logger.info(`monitor module keys: ${Object.keys(monitorModule).join(', ')}`);
507
- logger.info(`monitorModule 类型: ${typeof monitorModule}`);
508
- logger.info(`monitorModule 是否为 null: ${monitorModule === null}`);
509
- logger.info(`monitorModule 是否为 undefined: ${monitorModule === undefined}`);
510
-
511
- // 使用 Object.getOwnPropertyDescriptor 检查属性
512
- const descriptor = Object.getOwnPropertyDescriptor(monitorModule, 'monitorSingleAccount');
513
- logger.info(`monitorSingleAccount descriptor: ${JSON.stringify(descriptor)}`);
514
-
515
- // 尝试安全地访问 monitorSingleAccount
516
- let monitorSingleAccountType = 'unknown';
517
- try {
518
- monitorSingleAccountType = typeof monitorModule.monitorSingleAccount;
519
- } catch (e) {
520
- monitorSingleAccountType = `error: ${e.message}`;
521
- }
522
- logger.info(`monitorModule.monitorSingleAccount: ${monitorSingleAccountType}`);
523
-
524
- logger.info(`monitorModule.monitorDingtalkProvider: ${typeof monitorModule.monitorDingtalkProvider}`);
525
-
526
- // 使用直接属性访问而不是解构
527
- const monitorDingtalkProvider = monitorModule.monitorDingtalkProvider;
528
- logger.info(`解构 monitorDingtalkProvider 完成: ${typeof monitorDingtalkProvider}`);
529
-
530
- if (!monitorDingtalkProvider) {
531
- ctx.log?.error?.(`monitorDingtalkProvider 未找到!可用导出: ${Object.keys(monitorModule).join(', ')}`);
532
- throw new Error("monitorDingtalkProvider not found in monitor module");
512
+ if (!account.configured) {
513
+ throw new Error(`DingTalk account "${ctx.accountId}" is not properly configured`);
514
+ }
515
+
516
+ // 去重检查:如果列表中排在当前账号之前的账号已使用相同 clientId,则跳过当前账号
517
+ // 使用静态配置分析(而非运行时状态),避免并发竞态条件
518
+ // 规则:同一 clientId 只有列表中第一个启用且已配置的账号才会建立连接
519
+ if (account.clientId) {
520
+ const clientId = String(account.clientId);
521
+ const allAccountIds = listDingtalkAccountIds(ctx.cfg);
522
+ const currentIndex = allAccountIds.indexOf(ctx.accountId);
523
+ const priorAccountWithSameClientId = allAccountIds.slice(0, currentIndex).find((otherId) => {
524
+ const other = resolveDingtalkAccount({ cfg: ctx.cfg, accountId: otherId });
525
+ return other.enabled && other.configured && other.clientId && String(other.clientId) === clientId;
526
+ });
527
+ if (priorAccountWithSameClientId) {
528
+ ctx.log?.info?.(
529
+ `dingtalk-connector[${ctx.accountId}] skipped: clientId "${clientId.substring(0, 8)}..." is already used by account "${priorAccountWithSameClientId}"`
530
+ );
531
+ return new Promise<void>((resolve) => {
532
+ if (ctx.abortSignal?.aborted) {
533
+ resolve();
534
+ return;
535
+ }
536
+ ctx.abortSignal?.addEventListener('abort', () => resolve(), { once: true });
537
+ });
533
538
  }
534
- logger.info(`monitorDingtalkProvider 找到`);
535
-
536
- logger.info(`account 解析完成: ${account.accountId}, enabled=${account.enabled}, configured=${account.configured}`);
537
-
538
- ctx.setStatus({ accountId: ctx.accountId, port: null });
539
- ctx.log?.info(
540
- `starting dingtalk-connector[${ctx.accountId}] (mode: stream)`,
541
- );
542
- logger.info(`准备调用 monitorDingtalkProvider`);
543
-
544
- const result = await monitorDingtalkProvider({
539
+ }
540
+
541
+ ctx.setStatus({ accountId: ctx.accountId, port: null });
542
+ ctx.log?.info(
543
+ `starting dingtalk-connector[${ctx.accountId}] (mode: stream)`,
544
+ );
545
+ try {
546
+ return await monitorDingtalkProvider({
545
547
  config: ctx.cfg,
546
548
  runtime: ctx.runtime,
547
549
  abortSignal: ctx.abortSignal,
548
550
  accountId: ctx.accountId,
549
551
  });
550
- logger.info(`monitorDingtalkProvider 调用完成`);
551
- return result;
552
- } catch (error) {
553
- ctx.log?.error?.(`startAccount 发生错误: ${error.message}`);
554
- ctx.log?.error?.(`错误堆栈: ${error.stack}`);
555
- throw error;
552
+ } catch (err: any) {
553
+ // 打印真实错误到 stderr,绕过框架 log 系统(框架的 runtime.log 可能未初始化)
554
+ ctx.log?.error(`[dingtalk-connector][${ctx.accountId}] startAccount error:`, err?.message ?? err, err?.stack);
555
+ throw err;
556
556
  }
557
557
  },
558
558
  },
@@ -222,9 +222,21 @@ export function resolveDingtalkAccount(params: {
222
222
 
223
223
  /**
224
224
  * List all enabled and configured accounts.
225
+ * Deduplicates by clientId to avoid creating multiple connections with the same credentials.
225
226
  */
226
227
  export function listEnabledDingtalkAccounts(cfg: ClawdbotConfig): ResolvedDingtalkAccount[] {
227
- return listDingtalkAccountIds(cfg)
228
+ const accounts = listDingtalkAccountIds(cfg)
228
229
  .map((accountId) => resolveDingtalkAccount({ cfg, accountId }))
229
230
  .filter((account) => account.enabled && account.configured);
231
+
232
+ // Deduplicate by clientId to avoid multiple connections with same credentials
233
+ const seen = new Set<string>();
234
+ return accounts.filter((account) => {
235
+ if (!account.clientId) return true;
236
+ if (seen.has(account.clientId)) {
237
+ return false;
238
+ }
239
+ seen.add(account.clientId);
240
+ return true;
241
+ });
230
242
  }
@@ -13,7 +13,7 @@
13
13
  * - 连接统计和监控(每分钟输出)
14
14
  */
15
15
  import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
16
- import type { MonitorDingtalkAccountOpts, ResolvedDingtalkAccount } from "../types/index.ts";
16
+ import type { ResolvedDingtalkAccount } from "../types/index.ts";
17
17
  import {
18
18
  isMessageProcessed,
19
19
  markMessageProcessed,
@@ -30,14 +30,6 @@ export type DingtalkReactionCreatedEvent = {
30
30
  emoji: string;
31
31
  };
32
32
 
33
- export type MonitorDingtalkAccountOpts = {
34
- cfg: ClawdbotConfig;
35
- account: ResolvedDingtalkAccount;
36
- runtime?: RuntimeEnv;
37
- abortSignal?: AbortSignal;
38
- messageHandler: MessageHandler; // 直接传入消息处理器
39
- };
40
-
41
33
  // 消息处理器函数类型
42
34
  export type MessageHandler = (params: {
43
35
  accountId: string;
@@ -49,6 +41,14 @@ export type MessageHandler = (params: {
49
41
  cfg: ClawdbotConfig;
50
42
  }) => Promise<void>;
51
43
 
44
+ export type MonitorDingtalkAccountOpts = {
45
+ cfg: ClawdbotConfig;
46
+ account: ResolvedDingtalkAccount;
47
+ runtime?: RuntimeEnv;
48
+ abortSignal?: AbortSignal;
49
+ messageHandler: MessageHandler;
50
+ };
51
+
52
52
  // ============ 连接配置 ============
53
53
 
54
54
  /** 心跳间隔(毫秒) */
@@ -138,6 +138,7 @@ export async function monitorSingleAccount(
138
138
  // ============ 连接状态管理 ============
139
139
 
140
140
  let lastSocketAvailableTime = Date.now();
141
+ let connectionEstablishedTime = Date.now(); // 记录连接建立时间
141
142
  let isReconnecting = false;
142
143
  let reconnectAttempts = 0;
143
144
  let keepAliveTimer: NodeJS.Timeout | null = null;
@@ -226,21 +227,52 @@ export async function monitorSingleAccount(
226
227
  // 2. 重新建立连接
227
228
  await client.connect();
228
229
 
229
- // 3. 验证连接是否真正建立(必须检查 socket 状态)
230
- const socketState = client.socket?.readyState;
231
- if (socketState !== 1) {
232
- // socket 状态不是 OPEN (1),说明连接未真正建立
233
- throw new Error(`连接未建立,socket 状态=${socketState} (期望=1)`);
230
+ // 3. 等待连接真正建立(监听 open 事件,最多等待 10 秒)
231
+ const connectionEstablished = await new Promise<boolean>((resolve) => {
232
+ const timeout = setTimeout(() => {
233
+ resolve(false);
234
+ }, 10_000); // 10 秒超时
235
+
236
+ // 如果已经是 OPEN 状态,直接返回
237
+ if (client.socket?.readyState === 1) {
238
+ clearTimeout(timeout);
239
+ resolve(true);
240
+ return;
241
+ }
242
+
243
+ // 否则监听 open 事件
244
+ const onOpen = () => {
245
+ clearTimeout(timeout);
246
+ client.socket?.removeListener('open', onOpen);
247
+ client.socket?.removeListener('error', onError);
248
+ resolve(true);
249
+ };
250
+
251
+ const onError = (err: any) => {
252
+ clearTimeout(timeout);
253
+ client.socket?.removeListener('open', onOpen);
254
+ client.socket?.removeListener('error', onError);
255
+ logger.warn(`连接建立失败: ${err.message}`);
256
+ resolve(false);
257
+ };
258
+
259
+ client.socket?.once('open', onOpen);
260
+ client.socket?.once('error', onError);
261
+ });
262
+
263
+ if (!connectionEstablished) {
264
+ throw new Error(`连接建立超时或失败`);
234
265
  }
235
266
 
236
- // 4. 重置 socket 可用时间和重连计数
267
+ // 4. 重置 socket 可用时间、连接建立时间和重连计数
237
268
  lastSocketAvailableTime = Date.now();
269
+ connectionEstablishedTime = Date.now(); // 重置连接建立时间
238
270
  reconnectAttempts = 0; // 重连成功,重置计数
239
271
 
240
- logger.info(`✅ 重连成功 (socket 状态=${socketState})`);
272
+ logger.info(`✅ 重连成功 (socket 状态=${client.socket?.readyState})`);
241
273
  } catch (err: any) {
242
274
  reconnectAttempts++;
243
- log?.error?.(
275
+ logger.error(
244
276
  `重连失败:${err.message} (尝试 ${reconnectAttempts})`,
245
277
  );
246
278
  throw err;
@@ -266,7 +298,7 @@ export async function monitorSingleAccount(
266
298
  if (!isStopped && !isReconnecting) {
267
299
  // 立即重连,不退避
268
300
  doReconnect(true).catch((err) => {
269
- log?.error?.(`[${accountId}] 重连失败:${err.message}`);
301
+ logger.error(`[${accountId}] 重连失败:${err.message}`);
270
302
  });
271
303
  }
272
304
  }
@@ -290,7 +322,7 @@ export async function monitorSingleAccount(
290
322
  // 立即重连,不退避
291
323
  setTimeout(() => {
292
324
  doReconnect(true).catch((err) => {
293
- log?.error?.(`重连失败:${err.message}`);
325
+ logger.error(`重连失败:${err.message}`);
294
326
  });
295
327
  }, 0);
296
328
  });
@@ -329,11 +361,20 @@ export async function monitorSingleAccount(
329
361
 
330
362
  // 【心跳检测】检查 socket 状态
331
363
  const socketState = client.socket?.readyState;
364
+ const timeSinceConnection = Date.now() - connectionEstablishedTime;
332
365
  logger.debug(
333
- `🔍 心跳检测:socket 状态=${socketState}, elapsed=${Math.round(elapsed / 1000)}s`,
366
+ `🔍 心跳检测:socket 状态=${socketState}, elapsed=${Math.round(elapsed / 1000)}s, 连接已建立=${Math.round(timeSinceConnection / 1000)}s`,
334
367
  );
335
368
 
369
+ // 给新建立的连接 15 秒宽限期,避免在连接建立初期就触发重连
336
370
  if (socketState !== 1) {
371
+ if (timeSinceConnection < 15_000) {
372
+ logger.debug(
373
+ `⏳ 连接建立中(已 ${Math.round(timeSinceConnection / 1000)}s),跳过状态检查`,
374
+ );
375
+ return;
376
+ }
377
+
337
378
  logger.info(
338
379
  `⚠️ 心跳检测:socket 状态=${socketState},触发重连...`,
339
380
  );
@@ -347,11 +388,11 @@ export async function monitorSingleAccount(
347
388
  lastSocketAvailableTime = Date.now();
348
389
  logger.debug(`💓 发送 PING 心跳成功`);
349
390
  } catch (err: any) {
350
- log?.warn?.(`发送 PING 失败:${err.message}`);
391
+ logger.warn(`发送 PING 失败:${err.message}`);
351
392
  // 发送失败也计入超时
352
393
  }
353
394
  } catch (err: any) {
354
- log?.error?.(`keepAlive 检测失败:${err.message}`);
395
+ logger.error(`keepAlive 检测失败:${err.message}`);
355
396
  }
356
397
  }, HEARTBEAT_INTERVAL); // 每 10 秒检测一次
357
398
 
@@ -404,7 +445,7 @@ export async function monitorSingleAccount(
404
445
  await client.disconnect();
405
446
  }
406
447
  } catch (err: any) {
407
- log?.warn?.(`断开连接时出错:${err.message}`);
448
+ logger.warn(`断开连接时出错:${err.message}`);
408
449
  }
409
450
  resolve();
410
451
  };
@@ -445,12 +486,13 @@ export async function monitorSingleAccount(
445
486
  client.socketCallBackResponse(messageId, { success: true });
446
487
  logger.info(`✅ 已立即确认回调:messageId=${messageId}`);
447
488
  } else {
448
- log?.warn?.(`⚠️ 警告:消息没有 messageId`);
489
+ logger.warn(`⚠️ 警告:消息没有 messageId`);
449
490
  }
450
491
 
451
492
  // 消息去重
452
493
  if (messageId && isMessageProcessed(messageId)) {
453
- log?.warn?.(`⚠️ 检测到重复消息,跳过处理:messageId=${messageId}`);
494
+ processedCount++; // ✅ 修复:重复消息也要计入 processedCount
495
+ logger.warn(`⚠️ 检测到重复消息,跳过处理:messageId=${messageId} (${processedCount}/${receivedCount})`);
454
496
  logger.info(`========== 消息处理结束(重复) ==========\n`);
455
497
  return;
456
498
  }
@@ -519,8 +561,6 @@ export async function monitorSingleAccount(
519
561
 
520
562
  // ===== 第三步:开始处理消息 =====
521
563
  logger.info(`🚀 开始处理消息...`);
522
- logger.info(`AccountId: ${accountId}`);
523
- logger.info(`HasConfig: ${!!account.config}`);
524
564
 
525
565
  await messageHandler({
526
566
  accountId,
@@ -540,13 +580,9 @@ export async function monitorSingleAccount(
540
580
  const errorMsg = `❌ 处理消息异常 (${processedCount}/${receivedCount}): ${error?.message || "未知错误"}`;
541
581
  const errorStack = error?.stack || "无堆栈信息";
542
582
 
543
- // 使用 logger 确保错误信息一定会被打印
544
- logger.info(errorMsg);
545
- logger.info(`错误堆栈:\n${errorStack}`);
546
-
547
- // 同时使用 log?.error 记录(如果可用)
548
- log?.error?.(errorMsg);
549
- log?.error?.(`错误堆栈:\n${errorStack}`);
583
+ // 使用 logger 记录错误信息
584
+ logger.error(errorMsg);
585
+ logger.error(`错误堆栈:\n${errorStack}`);
550
586
 
551
587
  logger.info(`========== 消息处理结束(失败) ==========\n`);
552
588
  } finally {
@@ -633,7 +669,7 @@ export async function monitorSingleAccount(
633
669
  // client.on('close', ...) - 已移除,使用 setupCloseListener
634
670
 
635
671
  client.on("error", (err: Error) => {
636
- log?.error?.(`Connection error: ${err.message}`);
672
+ logger.error(`Connection error: ${err.message}`);
637
673
  });
638
674
 
639
675
  // 监听重连事件(仅用于日志,实际重连由自定义逻辑处理)