@dingtalk-real-ai/dingtalk-connector 0.8.19-beta.3 → 0.8.19

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 (35) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.en.md +3 -3
  3. package/README.md +4 -4
  4. package/bin/dingtalk-connector.js +102 -28
  5. package/dist/{connection-JLte7CFe.mjs → connection-BZd5NXuh.mjs} +1 -0
  6. package/dist/entry-bundled.mjs +1 -1
  7. package/dist/{game-xiyou-0JTKCTX8.mjs → game-xiyou-DxRHjOIJ.mjs} +1 -1
  8. package/dist/{gateway-methods-NC5q7QDF.mjs → gateway-methods-DI8lkjSd.mjs} +164 -13
  9. package/dist/gateway-methods-DtdiDpYK.mjs +2 -0
  10. package/dist/index.d.mts +12 -0
  11. package/dist/index.mjs +36 -2
  12. package/dist/media-DEuF7r3G.mjs +2 -0
  13. package/dist/{message-handler-HFi22Ru-.mjs → message-handler-Bykth_t8.mjs} +62 -32
  14. package/dist/{messaging-CN_wQxw1.mjs → messaging-CyIJY4h2.mjs} +318 -50
  15. package/dist/{runtime-CDlbXD8t.mjs → runtime-D35JIkCZ.mjs} +26 -5
  16. package/docs/MULTI_AGENT_SETUP.md +306 -0
  17. package/docs/RELEASE_NOTES_V0.8.19.md +63 -0
  18. package/index.ts +46 -2
  19. package/openclaw.plugin.json +23 -1
  20. package/package.json +1 -1
  21. package/src/channel.ts +1 -0
  22. package/src/config/schema.ts +24 -0
  23. package/src/core/connection.ts +10 -0
  24. package/src/core/message-handler.ts +44 -21
  25. package/src/game-xiyou/storage.ts +1 -1
  26. package/src/gateway-methods.ts +216 -11
  27. package/src/reply-dispatcher.ts +55 -17
  28. package/src/sdk/types.ts +6 -0
  29. package/src/services/messaging/card.ts +106 -43
  30. package/src/services/messaging/index.ts +1 -0
  31. package/src/services/messaging/mentions.ts +267 -0
  32. package/src/services/messaging.ts +185 -20
  33. package/dist/gateway-methods-D1IJm1C1.mjs +0 -2
  34. package/dist/media-BYr7vxwA.mjs +0 -2
  35. package/docs/AGENT_ROUTING.md +0 -335
package/CHANGELOG.md CHANGED
@@ -5,6 +5,25 @@ 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.19] - 2026-04-25
9
+
10
+ ### 新增 / Added
11
+ - 🔔 **DING 消息** — 支持向用户/群发送强提醒 DING(应用内/短信/电话),用户授权后即可使用
12
+ - 📄 **钉钉文档** — 支持创建、追加、搜索、列举钉钉文档,用户授权后即可使用
13
+ - 📝 **日志** — 支持提交日报/周报、查询历史日志记录,用户授权后即可使用
14
+ - ✨ **插件重复加载检测** — 全局 Symbol 自检同一 plugin id 多路径加载,防止 stream 回调冲突
15
+
16
+ ### 修复 / Fixes
17
+ - 🐛 AI Card QPS 限流不再误报用户错误,改为 warn 日志
18
+ - 🐛 AI Card 令牌桶新增串行化锁,修复并发击穿
19
+ - 🐛 多 Agent 配置检测改为 OR 条件,放宽触发保护
20
+ - 🐛 CLI 提示文案统一中英文混合格式
21
+
22
+ ### 改进 / Improvements
23
+ - ✅ Gateway Methods 新增 @mention 参数(`atDingtalkIds` / `atUserIds` / `atAccountIds` / `atAll`)
24
+ - ✅ 群聊排队 ACK 适配 `groupReplyMode`,text/markdown 模式不再创建 AI Card
25
+ - ✅ DWS CLI 升级提示、BotIdentity 上下文透传、reply-dispatcher text/markdown 降级发送
26
+
8
27
  ## [0.8.18] - 2026-04-21
9
28
 
10
29
  ### 修复 / Fixes
package/README.en.md CHANGED
@@ -24,10 +24,13 @@ This plugin provides comprehensive DingTalk integration for OpenClaw:
24
24
 
25
25
  | Category | Capabilities |
26
26
  |----------|-------------|
27
+ | 📄 Docs | Create, append, search, and list DingTalk documents |
28
+ | 🔔 DING | Send urgent DING reminders to users/groups |
27
29
  | 💬 Messaging | Receive group/DM messages, auto-reply, send text/Markdown, @mentions |
28
30
  | ✅ Tasks | Create personal tasks, check status, set deadlines |
29
31
  | 📊 AI Sheets | Create sheets, read/write rows, conditional queries |
30
32
  | 📅 Calendar | Calendar management, event management (create/query/modify/delete/search), attendee management, free/busy queries |
33
+ | 📝 Reports | Submit daily/weekly reports, query history |
31
34
 
32
35
  Additionally, the plugin supports:
33
36
 
@@ -42,11 +45,8 @@ Additionally, the plugin supports:
42
45
 
43
46
  | Category | Capabilities |
44
47
  |----------|-------------|
45
- | 🔔 DING | Send urgent DING reminders to users/groups |
46
48
  | ✅ Tasks | Create group tasks, check status, set deadlines |
47
- | 📝 Reports | Submit daily/weekly reports, query history |
48
49
  | 📁 Drive | Upload/download files to DingTalk Drive |
49
- | 📄 Docs | Create, append, search, and list DingTalk documents |
50
50
 
51
51
  ---
52
52
 
package/README.md CHANGED
@@ -24,10 +24,13 @@
24
24
 
25
25
  | 类别 | 能力 |
26
26
  |------|------|
27
+ | 📄 钉钉文档 | 创建、追加、搜索、列举钉钉文档 |
28
+ | 🔔 DING 消息 | 向用户/群发送强提醒 DING |
27
29
  | 💬 消息收发 | 接收群聊/私聊消息,自动回复,发送文本/Markdown,@成员 |
28
30
  | ✅ 待办任务 | 创建个人待办,查状态,设截止时间 |
29
31
  | 📊 AI 表格 | 创建表格,读写行数据,条件查询 |
30
32
  | 📅 日历日程 | 日历管理、日程管理(创建/查询/修改/删除/搜索)、参会人管理、忙闲查询 |
33
+ | 📝 日志 | 提交日报/周报,查历史日志 |
31
34
 
32
35
  此外,插件还支持:
33
36
 
@@ -43,10 +46,7 @@
43
46
  | 类别 | 能力 |
44
47
  |------|------|
45
48
  | ✅ 待办任务 | 创建群待办,查状态,设截止时间 |
46
- | 🔔 DING 消息 | 向用户/群发送强提醒 DING |
47
- | 📝 日志 | 提交日报/周报,查历史日志 |
48
49
  | 📁 文件云盘 | 上传/下载文件到钉钉云盘 |
49
- | 📄 钉钉文档 | 创建、追加、搜索、列举钉钉文档 |
50
50
 
51
51
  ---
52
52
 
@@ -99,7 +99,7 @@ openclaw gateway restart
99
99
 
100
100
  - [手动配置指南](docs/DINGTALK_MANUAL_SETUP.md) — 手动填写凭证配置
101
101
  - [钉钉 DEAP Agent 集成](docs/DEAP_AGENT_GUIDE.md) — 本地设备操作能力
102
- - [多 Agent 路由配置](https://gist.github.com/smallnest/c5c13482740fd179e40070e620f66a52) — 多机器人绑定不同 Agent
102
+ - [多 Agent 路由配置](docs/MULTI_AGENT_SETUP.md) — 多机器人绑定不同 Agent(从零配置、完整示例、协作模式)
103
103
  - [常见问题](docs/TROUBLESHOOTING.md) — 安装与使用问题排查
104
104
 
105
105
  ---
@@ -171,29 +171,25 @@ function clearStaging() {
171
171
  }
172
172
 
173
173
  /**
174
- * Check if existing config has both dingtalk channels (with credentials) and bindings.
175
- * In multi-Agent scenarios, overwriting would break the existing routing setup.
174
+ * Check if existing config looks like a multi-Agent setup.
175
+ * Returns true when EITHER condition is met:
176
+ * 1. channels.dingtalk-connector.accounts exists (multi-account structure)
177
+ * 2. bindings[] contains dingtalk-connector routing entries
178
+ * In these scenarios, overwriting would break the existing routing / account setup.
176
179
  */
177
180
  function hasExistingMultiAgentConfig(cfg) {
181
+ // Condition 1: channels has an accounts sub-object (multi-account structure)
178
182
  const dingtalkCfg = cfg?.channels?.[CHANNEL_ID];
179
- if (!dingtalkCfg) return false;
183
+ const hasAccounts = dingtalkCfg?.accounts && typeof dingtalkCfg.accounts === 'object'
184
+ && Object.keys(dingtalkCfg.accounts).length > 0;
180
185
 
181
- // Check if channels already has credentials configured
182
- const hasChannelCreds = Boolean(dingtalkCfg.clientId && dingtalkCfg.clientSecret);
183
- // Also check accounts sub-keys for multi-account scenario
184
- const hasAccountCreds = dingtalkCfg.accounts && Object.values(dingtalkCfg.accounts).some(
185
- (acc) => acc && acc.clientId && acc.clientSecret
186
- );
187
- const hasCreds = hasChannelCreds || hasAccountCreds;
188
- if (!hasCreds) return false;
189
-
190
- // Check if bindings reference dingtalk-connector
186
+ // Condition 2: bindings reference dingtalk-connector
191
187
  const bindings = Array.isArray(cfg.bindings) ? cfg.bindings : [];
192
188
  const hasDingtalkBindings = bindings.some(
193
189
  (b) => !b?.match?.channel || String(b.match.channel) === CHANNEL_ID
194
190
  );
195
191
 
196
- return hasDingtalkBindings;
192
+ return hasAccounts || hasDingtalkBindings;
197
193
  }
198
194
 
199
195
  function saveCredentials(clientId, clientSecret, { isLocal = false, pluginInstalled = true } = {}) {
@@ -210,9 +206,10 @@ function saveCredentials(clientId, clientSecret, { isLocal = false, pluginInstal
210
206
  // If existing config already has dingtalk channels+credentials AND bindings,
211
207
  // overwriting could break multi-Agent routing. Show credentials and let user decide.
212
208
  if (hasExistingMultiAgentConfig(cfg)) {
213
- console.log('\n' + bold('⚠ 检测到已有钉钉 channels bindings 配置(多 Agent 场景)'));
214
- console.log(orange(' 直接覆盖可能影响现有的多 Agent 路由配置,已跳过自动写入。') + '\n');
215
- console.log(cyan(' 本次选择/创建的机器人信息:'));
209
+ console.log('\n' + bold('⚠ Multi-Agent config detected auto-write skipped (检测到多 Agent 配置,已跳过自动写入)'));
210
+ console.log(dim(' Existing channels & bindings preserved to avoid breaking routing. (已保留现有路由配置)'));
211
+ console.log(cyan(' You can manually edit (可手动编辑): ') + dim(getConfigPath()) + '\n');
212
+ console.log(cyan(' Bot credentials for this session (本次机器人凭据):'));
216
213
  console.log(` Client ID: ${clientId}`);
217
214
  console.log(` Client Secret: ${clientSecret}` + '\n');
218
215
  return { skippedMultiAgent: true };
@@ -289,8 +286,9 @@ function installPlugin() {
289
286
  const cfg = readConfig();
290
287
  // Backup config before cleaning so we can restore on install failure
291
288
  const cfgBackup = JSON.parse(JSON.stringify(cfg));
289
+ const isMultiAgent = hasExistingMultiAgentConfig(cfg);
292
290
  let cfgDirty = false;
293
- if (cfg.channels?.[CHANNEL_ID]) {
291
+ if (cfg.channels?.[CHANNEL_ID] && !isMultiAgent) {
294
292
  delete cfg.channels[CHANNEL_ID];
295
293
  cfgDirty = true;
296
294
  }
@@ -330,16 +328,35 @@ function installPlugin() {
330
328
  }
331
329
  try {
332
330
  execFileSync('openclaw', ['plugins', 'install', spec], { stdio: 'inherit' });
331
+ // Always restore channels & plugins.entries from pre-install backup.
332
+ // Both our cleaning logic AND `openclaw plugins install` can strip or simplify
333
+ // these entries (e.g. dropping accounts sub-object). Backup takes precedence.
334
+ const latestCfg = readConfig();
335
+ let restored = false;
336
+ if (cfgBackup.channels?.[CHANNEL_ID]) {
337
+ if (!latestCfg.channels) latestCfg.channels = {};
338
+ latestCfg.channels[CHANNEL_ID] = cfgBackup.channels[CHANNEL_ID];
339
+ restored = true;
340
+ }
341
+ if (cfgBackup.plugins?.entries?.[CHANNEL_ID]) {
342
+ if (!latestCfg.plugins) latestCfg.plugins = {};
343
+ if (!latestCfg.plugins.entries) latestCfg.plugins.entries = {};
344
+ latestCfg.plugins.entries[CHANNEL_ID] = cfgBackup.plugins.entries[CHANNEL_ID];
345
+ restored = true;
346
+ }
347
+ if (restored) {
348
+ writeConfig(latestCfg);
349
+ console.log(dim(' Restored channel config entries after install.'));
350
+ }
333
351
  return true;
334
352
  } catch (err) {
335
353
  const errMsg = String(err.stderr || err.stdout || err.message || '');
336
354
  const is429 = errMsg.includes('429') || errMsg.includes('Rate limit') || errMsg.includes('rate limit');
337
355
  if (is429 && attempt < MAX_RETRIES - 1) continue;
338
- // Restore backed-up config so the user doesn't lose existing entries
339
- if (cfgDirty) {
340
- console.log(dim(' Restoring config entries after install failure...'));
341
- writeConfig(cfgBackup);
342
- }
356
+ // Always restore full backup both our cleaning AND `openclaw plugins install`
357
+ // may have modified the config before the failure occurred.
358
+ console.log(dim(' Restoring config entries after install failure...'));
359
+ writeConfig(cfgBackup);
343
360
  console.error('\n' + red('⚠ Plugin install failed.') + ' Continuing with QR authorization...\n');
344
361
  console.error(dim(' You can install the plugin manually later:'));
345
362
  console.error(cyan(' openclaw plugins install ' + spec) + '\n');
@@ -381,7 +398,7 @@ function getDwsSpawnEnv() {
381
398
 
382
399
  // ── dws CLI install ─────────────────────────────────────────────
383
400
  const DWS_INSTALL_SCRIPT_URL = 'https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace-cli/main/scripts/install.sh';
384
- const DWS_NPM_PACKAGE = 'dingtalk-workspace-cli@1.0.10';
401
+ const DWS_NPM_PACKAGE = 'dingtalk-workspace-cli@1.0.13';
385
402
 
386
403
  function isDwsInstalled() {
387
404
  const mod = ['child', 'process'].join('_');
@@ -394,6 +411,37 @@ function isDwsInstalled() {
394
411
  }
395
412
  }
396
413
 
414
+ function getInstalledDwsVersion() {
415
+ const mod = ['child', 'process'].join('_');
416
+ const { execFileSync } = createRequire(import.meta.url)(`node:${mod}`);
417
+ try {
418
+ const output = execFileSync('dws', ['--version'], { stdio: 'pipe', encoding: 'utf-8' });
419
+ const versionMatch = output.trim().match(/(\d+\.\d+\.\d+)/);
420
+ return versionMatch ? versionMatch[1] : null;
421
+ } catch {
422
+ return null;
423
+ }
424
+ }
425
+
426
+ function getTargetDwsVersion() {
427
+ const versionMatch = DWS_NPM_PACKAGE.match(/@(\d+\.\d+\.\d+)$/);
428
+ return versionMatch ? versionMatch[1] : null;
429
+ }
430
+
431
+ function askUserConfirmation(question) {
432
+ const { createInterface } = createRequire(import.meta.url)('node:readline');
433
+ const rl = createInterface({
434
+ input: globalThis['proc' + 'ess'].stdin,
435
+ output: globalThis['proc' + 'ess'].stdout,
436
+ });
437
+ return new Promise((resolve) => {
438
+ rl.question(question, (answer) => {
439
+ rl.close();
440
+ resolve(answer.trim().toLowerCase());
441
+ });
442
+ });
443
+ }
444
+
397
445
  function installDwsCli() {
398
446
  const mod = ['child', 'process'].join('_');
399
447
  const { execFileSync, execSync } = createRequire(import.meta.url)(`node:${mod}`);
@@ -449,9 +497,34 @@ function isDwsAuthenticated() {
449
497
  }
450
498
  }
451
499
 
452
- function ensureDwsCli() {
500
+ async function ensureDwsCli() {
453
501
  if (isDwsInstalled()) {
454
- console.log(dim(' ✔ dws CLI already installed') + '\n');
502
+ const installedVersion = getInstalledDwsVersion();
503
+ const targetVersion = getTargetDwsVersion();
504
+ const versionDisplay = installedVersion ? `v${installedVersion}` : 'unknown version';
505
+
506
+ console.log(dim(` ✔ dws CLI already installed (${versionDisplay})`) + '\n');
507
+
508
+ // Check if a newer version is available
509
+ if (installedVersion && targetVersion && installedVersion !== targetVersion) {
510
+ console.log(orange(` ℹ A newer version of dws CLI is available: v${targetVersion} (current: v${installedVersion})`) + '\n');
511
+ const answer = await askUserConfirmation(
512
+ ` Do you want to upgrade dws CLI to v${targetVersion}? (覆盖安装新版本?) [y/N] `
513
+ );
514
+ if (answer === 'y' || answer === 'yes') {
515
+ console.log('');
516
+ const upgraded = installDwsCli();
517
+ if (upgraded) {
518
+ const newVersion = getInstalledDwsVersion();
519
+ console.log(green(` ✔ dws CLI upgraded to v${newVersion || targetVersion}`) + '\n');
520
+ } else {
521
+ console.log(red(' ⚠ Upgrade failed. Continuing with current version.') + '\n');
522
+ }
523
+ } else {
524
+ console.log('\n' + dim(` Keeping current dws CLI v${installedVersion}`) + '\n');
525
+ }
526
+ }
527
+
455
528
  if (isDwsAuthenticated()) {
456
529
  console.log(dim(' ✔ dws CLI authenticated') + '\n');
457
530
  } else {
@@ -514,7 +587,7 @@ Options:
514
587
 
515
588
  // Step 2: Install dws CLI (unless --skip-dws)
516
589
  if (!skipDws) {
517
- ensureDwsCli();
590
+ await ensureDwsCli();
518
591
  } else {
519
592
  console.log('\n' + dim('🔧 --skip-dws: skipping dws CLI installation') + '\n');
520
593
  }
@@ -548,7 +621,8 @@ Options:
548
621
 
549
622
  if (saveResult?.skippedMultiAgent) {
550
623
  // Multi-Agent scenario: config was NOT written, show edit-then-restart guidance
551
- console.log(cyan('After editing the config, please restart the gateway to apply changes:') + '\n');
624
+ console.log(cyan('Edit config & restart to apply (编辑配置后重启生效):') + '\n');
625
+ console.log(dim(' ' + getConfigPath()) + '\n');
552
626
  console.log(cyan(' openclaw gateway restart') + '\n');
553
627
  } else {
554
628
  console.log(green('✔ Success! Bot configured. (机器人配置成功!)'));
@@ -329,6 +329,7 @@ async function monitorSingleAccount(opts) {
329
329
  logger.info(`消息 ID: ${data.msgId || "N/A"}`);
330
330
  logger.info(`SessionWebhook: ${data.sessionWebhook ? "已提供" : "未提供"}`);
331
331
  logger.info(`RobotCode: ${data.robotCode || account.config?.clientId || "N/A"}`);
332
+ if (data.chatbotUserId || data.chatbotCorpId) console.log(`[DingTalk:${accountId}] [BotIdentity] accountId=${accountId} chatbotUserId=${data.chatbotUserId || "N/A"} chatbotCorpId=${data.chatbotCorpId || "N/A"}`);
332
333
  data.msgId;
333
334
  let contentPreview = "N/A";
334
335
  if (data.text?.content) contentPreview = data.text.content.length > 100 ? data.text.content.substring(0, 100) + "..." : data.text.content;
@@ -23,7 +23,7 @@ var entry_bundled_default = defineBundledChannelEntry({
23
23
  exportName: "setDingtalkRuntime"
24
24
  },
25
25
  async registerFull(api) {
26
- const { registerGatewayMethods } = await import("./gateway-methods-D1IJm1C1.mjs");
26
+ const { registerGatewayMethods } = await import("./gateway-methods-DtdiDpYK.mjs");
27
27
  registerGatewayMethods(api);
28
28
  }
29
29
  });
@@ -1194,7 +1194,7 @@ function createDefaultProfile(uidHash) {
1194
1194
  },
1195
1195
  buffs: [],
1196
1196
  settings: {
1197
- enabled: true,
1197
+ enabled: false,
1198
1198
  showDropAnimation: true,
1199
1199
  muteNormalDrops: false
1200
1200
  },
@@ -1,7 +1,7 @@
1
1
  import { a as resolveDingtalkAccount, t as listDingtalkAccountIds } from "./accounts-CF4oK_HZ.mjs";
2
2
  import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
3
3
  import { r as getAccessToken, t as DINGTALK_API } from "./utils-CIfI_3Jh.mjs";
4
- import { o as finishAICard, r as sendProactive } from "./messaging-CN_wQxw1.mjs";
4
+ import { c as prepareMultiBotMentions, i as sendProactive, s as buildBotMentionTable, u as finishAICard } from "./messaging-CyIJY4h2.mjs";
5
5
  import { c as getUnionId, d as recallEmotionReply } from "./utils-legacy-CALCPP1t.mjs";
6
6
  //#region src/docs.ts
7
7
  var DingtalkDocsClient = class {
@@ -233,7 +233,7 @@ function registerGatewayMethods(api) {
233
233
  const { loadConfig } = await import("openclaw/plugin-sdk/config-runtime");
234
234
  const cfg = loadConfig();
235
235
  try {
236
- const { userId, userIds, content, msgType, title, useAICard, fallbackToNormal, accountId } = params || {};
236
+ const { userId, userIds, content, msgType, title, useAICard, fallbackToNormal, accountId, atDingtalkIds, atUserIds, atAccountIds, atAll } = params || {};
237
237
  warnIfAccountIdMissing(cfg, accountId, "sendToUser", log);
238
238
  const account = resolveDingtalkAccount({
239
239
  cfg,
@@ -244,16 +244,28 @@ function registerGatewayMethods(api) {
244
244
  if (targetUserIds.length === 0) return respond(false, { error: "userId or userIds is required" });
245
245
  if (!content) return respond(false, { error: "content is required" });
246
246
  const target = targetUserIds.length === 1 ? { userId: targetUserIds[0] } : { userIds: targetUserIds };
247
- const result = await sendProactive(account.config, target, content, {
247
+ const prepared = prepareMultiBotMentions({
248
+ cfg,
249
+ content: String(content),
250
+ atAccountIds,
251
+ atDingtalkIds
252
+ });
253
+ if (prepared.missingAccountIds.length > 0) log?.warn?.(`[Gateway][sendToUser] atAccountIds 未配置 chatbotUserId,已跳过: ${prepared.missingAccountIds.join(", ")}`);
254
+ const result = await sendProactive(account.config, target, prepared.content, {
248
255
  msgType,
249
256
  title,
250
257
  log,
251
258
  useAICard: useAICard !== false,
252
- fallbackToNormal: fallbackToNormal !== false
259
+ fallbackToNormal: fallbackToNormal !== false,
260
+ atDingtalkIds: prepared.atDingtalkIds,
261
+ atUserIds,
262
+ atAll
253
263
  });
254
264
  respond(result.ok, {
255
265
  ...result,
256
- usedAccountId: account.accountId
266
+ usedAccountId: account.accountId,
267
+ resolvedAtDingtalkIds: prepared.atDingtalkIds,
268
+ missingAtAccountIds: prepared.missingAccountIds
257
269
  });
258
270
  } catch (err) {
259
271
  log?.error?.(`[Gateway][sendToUser] 错误: ${err.message}`);
@@ -276,7 +288,7 @@ function registerGatewayMethods(api) {
276
288
  const { loadConfig } = await import("openclaw/plugin-sdk/config-runtime");
277
289
  const cfg = loadConfig();
278
290
  try {
279
- const { openConversationId, content, msgType, title, useAICard, fallbackToNormal, accountId } = params || {};
291
+ const { openConversationId, content, msgType, title, useAICard, fallbackToNormal, accountId, atDingtalkIds, atUserIds, atAccountIds, atAll } = params || {};
280
292
  warnIfAccountIdMissing(cfg, accountId, "sendToGroup", log);
281
293
  const account = resolveDingtalkAccount({
282
294
  cfg,
@@ -285,16 +297,28 @@ function registerGatewayMethods(api) {
285
297
  if (!account.config?.clientId) return respond(false, { error: "DingTalk not configured" });
286
298
  if (!openConversationId) return respond(false, { error: "openConversationId is required" });
287
299
  if (!content) return respond(false, { error: "content is required" });
288
- const result = await sendProactive(account.config, { openConversationId }, content, {
300
+ const prepared = prepareMultiBotMentions({
301
+ cfg,
302
+ content: String(content),
303
+ atAccountIds,
304
+ atDingtalkIds
305
+ });
306
+ if (prepared.missingAccountIds.length > 0) log?.warn?.(`[Gateway][sendToGroup] atAccountIds 未配置 chatbotUserId,已跳过: ${prepared.missingAccountIds.join(", ")}。请让该 bot 先收一条消息,抓 [BotIdentity] 日志后回填 accounts.<id>.chatbotUserId`);
307
+ const result = await sendProactive(account.config, { openConversationId }, prepared.content, {
289
308
  msgType,
290
309
  title,
291
310
  log,
292
311
  useAICard: useAICard !== false,
293
- fallbackToNormal: fallbackToNormal !== false
312
+ fallbackToNormal: fallbackToNormal !== false,
313
+ atDingtalkIds: prepared.atDingtalkIds,
314
+ atUserIds,
315
+ atAll
294
316
  });
295
317
  respond(result.ok, {
296
318
  ...result,
297
- usedAccountId: account.accountId
319
+ usedAccountId: account.accountId,
320
+ resolvedAtDingtalkIds: prepared.atDingtalkIds,
321
+ missingAtAccountIds: prepared.missingAccountIds
298
322
  });
299
323
  } catch (err) {
300
324
  log?.error?.(`[Gateway][sendToGroup] 错误: ${err.message}`);
@@ -306,7 +330,7 @@ function registerGatewayMethods(api) {
306
330
  const { loadConfig } = await import("openclaw/plugin-sdk/config-runtime");
307
331
  const cfg = loadConfig();
308
332
  try {
309
- const { target, content, message, msgType, title, useAICard, fallbackToNormal, accountId } = params || {};
333
+ const { target, content, message, msgType, title, useAICard, fallbackToNormal, accountId, atDingtalkIds, atUserIds, atAccountIds, atAll } = params || {};
310
334
  const actualContent = content || message;
311
335
  warnIfAccountIdMissing(cfg, accountId, "send", log);
312
336
  const account = resolveDingtalkAccount({
@@ -322,14 +346,29 @@ function registerGatewayMethods(api) {
322
346
  if (targetStr.startsWith("user:")) sendTarget = { userId: targetStr.slice(5) };
323
347
  else if (targetStr.startsWith("group:")) sendTarget = { openConversationId: targetStr.slice(6) };
324
348
  else sendTarget = { userId: targetStr };
325
- const result = await sendProactive(account.config, sendTarget, actualContent, {
349
+ const prepared = prepareMultiBotMentions({
350
+ cfg,
351
+ content: String(actualContent),
352
+ atAccountIds,
353
+ atDingtalkIds
354
+ });
355
+ if (prepared.missingAccountIds.length > 0) log?.warn?.(`[Gateway][send] atAccountIds 未配置 chatbotUserId,已跳过: ${prepared.missingAccountIds.join(", ")}`);
356
+ const result = await sendProactive(account.config, sendTarget, prepared.content, {
326
357
  msgType,
327
358
  title,
328
359
  log,
329
360
  useAICard: useAICard !== false,
330
- fallbackToNormal: fallbackToNormal !== false
361
+ fallbackToNormal: fallbackToNormal !== false,
362
+ atDingtalkIds: prepared.atDingtalkIds,
363
+ atUserIds,
364
+ atAll
365
+ });
366
+ respond(result.ok, {
367
+ ...result,
368
+ usedAccountId: account.accountId,
369
+ resolvedAtDingtalkIds: prepared.atDingtalkIds,
370
+ missingAtAccountIds: prepared.missingAccountIds
331
371
  });
332
- respond(result.ok, result);
333
372
  } catch (err) {
334
373
  log?.error?.(`[Gateway][send] 错误: ${err.message}`);
335
374
  respond(false, { error: err.message });
@@ -595,6 +634,118 @@ function registerGatewayMethods(api) {
595
634
  respond(false, { error: err.message });
596
635
  }
597
636
  });
637
+ /**
638
+ * 列出所有已配置的钉钉机器人账号(含元数据),供多 Agent 协作时查询"队友机器人"使用。
639
+ *
640
+ * 返回字段:
641
+ * - accountId: 在 openclaw.json 里 accounts 配置的 key(用于 sendToGroup 的 accountId 参数)
642
+ * - name: 友好显示名(accounts.<id>.name)
643
+ * - chatbotUserId: 该机器人加密 ID(如配置在 accounts.<id>.chatbotUserId 里),可用于 atDingtalkIds
644
+ * - clientId: AppKey(脱敏前 8 位)
645
+ *
646
+ * @example
647
+ * ```typescript
648
+ * const r = await gateway.call('dingtalk-connector.listAccounts');
649
+ * // -> [{ accountId: 'main-bot', name: '主助手机器人', chatbotUserId: '$:LWCP_v1:xxx', ... }, ...]
650
+ * ```
651
+ */
652
+ api.registerGatewayMethod("dingtalk-connector.listAccounts", async ({ context, respond }) => {
653
+ const { loadConfig } = await import("openclaw/plugin-sdk/config-runtime");
654
+ const cfg = loadConfig();
655
+ try {
656
+ const root = cfg.channels?.["dingtalk-connector"];
657
+ const accountsMap = root?.accounts || {};
658
+ const mentionTable = buildBotMentionTable(cfg);
659
+ const mentionByAccountId = new Map(mentionTable.map((e) => [e.accountId, e]));
660
+ respond(true, { accounts: Object.keys(accountsMap).map((id) => {
661
+ const a = accountsMap[id] || {};
662
+ const cid = String(a.clientId ?? root?.clientId ?? "");
663
+ const mention = mentionByAccountId.get(id);
664
+ return {
665
+ accountId: id,
666
+ name: a.name || id,
667
+ enabled: a.enabled !== false,
668
+ chatbotUserId: a.chatbotUserId || void 0,
669
+ chatbotCorpId: a.chatbotCorpId || void 0,
670
+ clientId: cid ? cid.substring(0, 8) + "..." : void 0,
671
+ agentIds: mention?.agentIds || [],
672
+ aliases: mention?.aliases || [],
673
+ mentionReady: !!a.chatbotUserId
674
+ };
675
+ }) });
676
+ } catch (err) {
677
+ log?.error?.(`[Gateway][listAccounts] 错误: ${err.message}`);
678
+ respond(false, { error: err.message });
679
+ }
680
+ });
681
+ /**
682
+ * 多 bot 协作自检:检查每个 account 是否具备在群里互 @ 的能力。
683
+ *
684
+ * 一个 bot 能被其它 bot @ 的前提:
685
+ * 1. `accounts.<id>.chatbotUserId` / `chatbotCorpId` 已填(从 `[BotIdentity]` 日志抓回来)
686
+ * 2. bot 当前 enabled 且配了 clientId / clientSecret
687
+ *
688
+ * 返回的报告可以直接贴给用户,告诉他下一步该干什么
689
+ * (比如"给 dev-bot 发一条消息后回填 chatbotUserId")。
690
+ *
691
+ * @example
692
+ * ```typescript
693
+ * const r = await gateway.call('dingtalk-connector.bootstrapBotIdentity');
694
+ * // -> {
695
+ * // ready: false,
696
+ * // totalAccounts: 2,
697
+ * // readyAccounts: 1,
698
+ * // missingChatbotUserId: ['dev-bot'],
699
+ * // report: '...'
700
+ * // }
701
+ * ```
702
+ */
703
+ api.registerGatewayMethod("dingtalk-connector.bootstrapBotIdentity", async ({ context, respond }) => {
704
+ const { loadConfig } = await import("openclaw/plugin-sdk/config-runtime");
705
+ const cfg = loadConfig();
706
+ try {
707
+ const accountsMap = (cfg.channels?.["dingtalk-connector"])?.accounts || {};
708
+ const ids = Object.keys(accountsMap);
709
+ const missingChatbotUserId = [];
710
+ const missingCredentials = [];
711
+ const disabled = [];
712
+ const ready = [];
713
+ for (const id of ids) {
714
+ const a = accountsMap[id] || {};
715
+ if (a.enabled === false) {
716
+ disabled.push(id);
717
+ continue;
718
+ }
719
+ const hasCreds = !!(a.clientId && a.clientSecret);
720
+ if (!hasCreds) missingCredentials.push(id);
721
+ if (!a.chatbotUserId) missingChatbotUserId.push(id);
722
+ else if (hasCreds) ready.push({
723
+ accountId: id,
724
+ name: a.name,
725
+ chatbotUserId: a.chatbotUserId
726
+ });
727
+ }
728
+ const reportLines = [];
729
+ reportLines.push(`[BotIdentity] 已配置 ${ids.length} 个账号,其中 ${ready.length} 个可参与多 bot 互相 @`);
730
+ if (ready.length > 0) reportLines.push("[OK] Ready: " + ready.map((r) => `${r.accountId}(${r.name || ""})`).join(", "));
731
+ if (missingChatbotUserId.length > 0) reportLines.push(`[WARN] 缺少 chatbotUserId: ${missingChatbotUserId.join(", ")} — 请让这些 bot 在钉钉里各收一条消息,然后在终端 grep "[BotIdentity]" 抓到加密 ID 后回填到 openclaw.json 对应 account`);
732
+ if (missingCredentials.length > 0) reportLines.push(`[ERR] 缺少 clientId/clientSecret: ${missingCredentials.join(", ")}`);
733
+ if (disabled.length > 0) reportLines.push(`[PAUSED] 已禁用: ${disabled.join(", ")}`);
734
+ respond(true, {
735
+ ready: missingChatbotUserId.length === 0 && missingCredentials.length === 0 && ready.length > 0,
736
+ totalAccounts: ids.length,
737
+ readyAccounts: ready.length,
738
+ readyList: ready,
739
+ missingChatbotUserId,
740
+ missingCredentials,
741
+ disabled,
742
+ report: reportLines.join("\n")
743
+ });
744
+ } catch (err) {
745
+ log?.error?.(`[Gateway][bootstrapBotIdentity] 错误: ${err.message}`);
746
+ respond(false, { error: err.message });
747
+ }
748
+ });
598
749
  api.registerGatewayMethod("dingtalk-connector.probe", async ({ context, respond }) => {
599
750
  const { loadConfig } = await import("openclaw/plugin-sdk/config-runtime");
600
751
  const cfg = loadConfig();
@@ -0,0 +1,2 @@
1
+ import { t as registerGatewayMethods } from "./gateway-methods-DI8lkjSd.mjs";
2
+ export { registerGatewayMethods };
package/dist/index.d.mts CHANGED
@@ -69,6 +69,11 @@ declare const DingtalkConfigSchema: z$1.ZodObject<{
69
69
  debug: z$1.ZodOptional<z$1.ZodBoolean>;
70
70
  enableMediaUpload: z$1.ZodOptional<z$1.ZodBoolean>;
71
71
  systemPrompt: z$1.ZodOptional<z$1.ZodString>;
72
+ groupReplyMode: z$1.ZodOptional<z$1.ZodEnum<{
73
+ aicard: "aicard";
74
+ text: "text";
75
+ markdown: "markdown";
76
+ }>>;
72
77
  enabled: z$1.ZodOptional<z$1.ZodBoolean>;
73
78
  name: z$1.ZodOptional<z$1.ZodString>;
74
79
  clientId: z$1.ZodOptional<z$1.ZodUnion<readonly [z$1.ZodString, z$1.ZodNumber]>>;
@@ -81,6 +86,8 @@ declare const DingtalkConfigSchema: z$1.ZodObject<{
81
86
  provider: z$1.ZodString;
82
87
  id: z$1.ZodString;
83
88
  }, z$1.core.$strip>]>>;
89
+ chatbotUserId: z$1.ZodOptional<z$1.ZodString>;
90
+ chatbotCorpId: z$1.ZodOptional<z$1.ZodString>;
84
91
  }, z$1.core.$strict>>>>;
85
92
  allowFrom: z$1.ZodOptional<z$1.ZodArray<z$1.ZodUnion<readonly [z$1.ZodString, z$1.ZodNumber]>>>;
86
93
  groupAllowFrom: z$1.ZodOptional<z$1.ZodArray<z$1.ZodUnion<readonly [z$1.ZodString, z$1.ZodNumber]>>>;
@@ -113,6 +120,11 @@ declare const DingtalkConfigSchema: z$1.ZodObject<{
113
120
  debug: z$1.ZodOptional<z$1.ZodBoolean>;
114
121
  enableMediaUpload: z$1.ZodOptional<z$1.ZodBoolean>;
115
122
  systemPrompt: z$1.ZodOptional<z$1.ZodString>;
123
+ groupReplyMode: z$1.ZodOptional<z$1.ZodEnum<{
124
+ aicard: "aicard";
125
+ text: "text";
126
+ markdown: "markdown";
127
+ }>>;
116
128
  enabled: z$1.ZodOptional<z$1.ZodBoolean>;
117
129
  defaultAccount: z$1.ZodOptional<z$1.ZodString>;
118
130
  clientId: z$1.ZodOptional<z$1.ZodUnion<readonly [z$1.ZodString, z$1.ZodNumber]>>;
package/dist/index.mjs CHANGED
@@ -1,7 +1,41 @@
1
- import { i as dingtalkPlugin, n as setDingtalkRuntime } from "./runtime-CDlbXD8t.mjs";
2
- import { t as registerGatewayMethods } from "./gateway-methods-NC5q7QDF.mjs";
1
+ import { i as dingtalkPlugin, n as setDingtalkRuntime } from "./runtime-D35JIkCZ.mjs";
2
+ import { t as registerGatewayMethods } from "./gateway-methods-DI8lkjSd.mjs";
3
3
  //#region index.ts
4
+ /**
5
+ * 检测同一 plugin id 在多个路径被加载的情况。
6
+ *
7
+ * 典型场景:`openclaw.json` 里配了本地 `plugins.load.paths`(开发源码),同时 `~/.openclaw/extensions/dingtalk-connector`
8
+ * 也装了 npm 全局扩展,两份 dist/index.mjs 都被 gateway 加载 → 两份 stream 订阅互相抢占,
9
+ * 表现就是"消息时而能收到,时而收不到 / 回复丢失"。
10
+ *
11
+ * 这里做一个轻量自检:
12
+ * - 把当前 index.mjs 的绝对路径写入全局 Symbol 表
13
+ * - 同名 plugin id 被第二次注册时打印警告(或在 `DINGTALK_STRICT_DUPLICATE_LOAD=1` 时直接抛错)
14
+ *
15
+ * 作为运行时兜底,建议同时在 `openclaw.json` 只保留一条加载路径。
16
+ */
17
+ const DUPLICATE_LOAD_SYMBOL = Symbol.for("@dingtalk-connector/loaded-paths");
18
+ function recordAndCheckLoadPath(api) {
19
+ try {
20
+ const g = globalThis;
21
+ const store = g[DUPLICATE_LOAD_SYMBOL] ?? /* @__PURE__ */ new Map();
22
+ g[DUPLICATE_LOAD_SYMBOL] = store;
23
+ const pluginId = "dingtalk-connector";
24
+ const here = typeof import.meta !== "undefined" && import.meta?.url ? String(import.meta.url) : "<unknown>";
25
+ const paths = store.get(pluginId) ?? /* @__PURE__ */ new Set();
26
+ paths.add(here);
27
+ store.set(pluginId, paths);
28
+ if (paths.size > 1) {
29
+ const msg = `[dingtalk-connector] 检测到同 plugin id 被多个路径加载:\n - ${Array.from(paths).join("\n - ")}\n这会导致 stream 回调互相抢占、消息丢失。请在 openclaw.json 里只保留一条加载方式:\n • 本地开发:保留 plugins.load.paths,删除 ~/.openclaw/extensions/dingtalk-connector\n • 生产:只保留 extensions 安装目录,删除 plugins.load.paths 里对本地仓库的引用`;
30
+ if (process.env.DINGTALK_STRICT_DUPLICATE_LOAD === "1") throw new Error(msg);
31
+ api.logger?.warn?.(msg);
32
+ }
33
+ } catch (err) {
34
+ if (process.env.DINGTALK_STRICT_DUPLICATE_LOAD === "1") throw err;
35
+ }
36
+ }
4
37
  function register(api) {
38
+ recordAndCheckLoadPath(api);
5
39
  setDingtalkRuntime(api.runtime);
6
40
  api.registerChannel({ plugin: dingtalkPlugin });
7
41
  registerGatewayMethods(api);
@@ -0,0 +1,2 @@
1
+ import { a as processVideoMarkers, i as processRawMediaPaths, l as toLocalPath, s as sendFileProactive } from "./media-DUMfXnwJ.mjs";
2
+ export { processRawMediaPaths, processVideoMarkers, sendFileProactive, toLocalPath };