@dingtalk-real-ai/dingtalk-connector 0.8.20-beta.7 → 0.8.20

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,19 @@ 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.20] - 2026-04-28
9
+
10
+ ### 修复 / Fixes
11
+ - 🐛 **OpenClaw 插件加载兼容性 (Issue #527)** — `configSchema` 改为延迟初始化,通过 `createRequire` 解析 `openclaw/plugin-sdk/core`,修复插件安装到 `~/.openclaw/extensions/` 时 ESM 裸说明符解析失败导致的 "Cannot find package 'openclaw'" 崩溃
12
+ **OpenClaw plugin load compatibility (Issue #527)** — `configSchema` deferred to lazy init via `createRequire`, fixing "Cannot find package 'openclaw'" crash when plugin is installed to `~/.openclaw/extensions/`
13
+
14
+ - 🐛 **Onboarding 动态导入** — `promptSingleChannelSecretInput` 从静态 import 改为动态 `import()`,避免在 ESM 加载阶段触发同样的裸说明符解析错误
15
+ **Onboarding dynamic import** — `promptSingleChannelSecretInput` switched from static to dynamic `import()` to avoid bare specifier resolution error during ESM loading
16
+
17
+ ### 改进 / Improvements
18
+ - ✅ **DWS CLI 版本管理重构** — `ensureDwsCli()` 新增 `compareVersions()` 语义版本比较,支持四种场景:目标版本更高时自动升级、本地版本更高时询问是否覆盖、版本一致时跳过、全新安装时显示已安装版本号
19
+ **DWS CLI version management refactor** — `ensureDwsCli()` now uses `compareVersions()` for semver comparison with four scenarios: auto-upgrade when target is newer, prompt before downgrade, skip when equal, show version on fresh install
20
+
8
21
  ## [0.8.19] - 2026-04-25
9
22
 
10
23
  ### 新增 / Added
@@ -428,6 +428,22 @@ function getTargetDwsVersion() {
428
428
  return versionMatch ? versionMatch[1] : null;
429
429
  }
430
430
 
431
+ /**
432
+ * Compare two semver version strings.
433
+ * Returns: positive if a > b, negative if a < b, 0 if equal.
434
+ */
435
+ function compareVersions(versionA, versionB) {
436
+ const partsA = versionA.split('.').map(Number);
437
+ const partsB = versionB.split('.').map(Number);
438
+ const maxLength = Math.max(partsA.length, partsB.length);
439
+ for (let i = 0; i < maxLength; i++) {
440
+ const partA = partsA[i] || 0;
441
+ const partB = partsB[i] || 0;
442
+ if (partA !== partB) return partA - partB;
443
+ }
444
+ return 0;
445
+ }
446
+
431
447
  function askUserConfirmation(question) {
432
448
  const { createInterface } = createRequire(import.meta.url)('node:readline');
433
449
  const rl = createInterface({
@@ -498,33 +514,50 @@ function isDwsAuthenticated() {
498
514
  }
499
515
 
500
516
  async function ensureDwsCli() {
517
+ const targetVersion = getTargetDwsVersion();
518
+
501
519
  if (isDwsInstalled()) {
502
520
  const installedVersion = getInstalledDwsVersion();
503
- const targetVersion = getTargetDwsVersion();
504
521
  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');
522
+ const comparison = (installedVersion && targetVersion) ? compareVersions(targetVersion, installedVersion) : 0;
523
+
524
+ if (comparison > 0) {
525
+ // Scenario 1: target > local upgrade directly
526
+ console.log(dim(` ℹ dws CLI detected (${versionDisplay}), upgrading to v${targetVersion}...`) + '\n');
527
+ console.log(dim(` v${installedVersion} v${targetVersion}`) + '\n');
528
+ const upgraded = installDwsCli();
529
+ if (upgraded) {
530
+ const newVersion = getInstalledDwsVersion();
531
+ console.log(green(` ✔ dws CLI upgraded to v${newVersion || targetVersion}`) + '\n');
532
+ } else {
533
+ console.log(red(' ⚠ Upgrade failed. Continuing with current version.') + '\n');
534
+ }
535
+ } else if (comparison < 0) {
536
+ // Scenario 2: target < local → ask user before downgrading
537
+ console.log(dim(` ℹ dws CLI detected (${versionDisplay})`) + '\n');
538
+ console.log(orange(` ⚠ Your local dws CLI (v${installedVersion}) is newer than the bundled version (v${targetVersion}).`) + '\n');
539
+ console.log(dim(` Overwriting would downgrade: v${installedVersion} → v${targetVersion}`) + '\n');
511
540
  const answer = await askUserConfirmation(
512
- ` Do you want to upgrade dws CLI to v${targetVersion}? (覆盖安装新版本?) [y/N] `
541
+ ` Do you want to overwrite with v${targetVersion}? (是否覆盖为旧版本?) [y/N] `
513
542
  );
514
543
  if (answer === 'y' || answer === 'yes') {
515
544
  console.log('');
516
- const upgraded = installDwsCli();
517
- if (upgraded) {
545
+ const downgraded = installDwsCli();
546
+ if (downgraded) {
518
547
  const newVersion = getInstalledDwsVersion();
519
- console.log(green(` ✔ dws CLI upgraded to v${newVersion || targetVersion}`) + '\n');
548
+ console.log(green(` ✔ dws CLI replaced with v${newVersion || targetVersion}`) + '\n');
520
549
  } else {
521
- console.log(red(' ⚠ Upgrade failed. Continuing with current version.') + '\n');
550
+ console.log(red(' ⚠ Overwrite failed. Continuing with current version.') + '\n');
522
551
  }
523
552
  } else {
524
553
  console.log('\n' + dim(` Keeping current dws CLI v${installedVersion}`) + '\n');
525
554
  }
555
+ } else {
556
+ // Scenario 3: versions are equal → skip
557
+ console.log(dim(` ✔ dws CLI already installed (${versionDisplay}), version is up to date`) + '\n');
526
558
  }
527
559
 
560
+ // Check authentication status regardless of version scenario
528
561
  if (isDwsAuthenticated()) {
529
562
  console.log(dim(' ✔ dws CLI authenticated') + '\n');
530
563
  } else {
@@ -534,6 +567,7 @@ async function ensureDwsCli() {
534
567
  return;
535
568
  }
536
569
 
570
+ // Scenario 4: dws not installed → install and show version
537
571
  const installed = installDwsCli();
538
572
  if (!installed) {
539
573
  console.log(red(' ⚠ Could not install dws CLI automatically.') + '\n');
@@ -544,7 +578,10 @@ async function ensureDwsCli() {
544
578
  return;
545
579
  }
546
580
 
547
- console.log(dim(' ℹ dws CLI installed. Authorization will be triggered when Agent uses dws features.') + '\n');
581
+ const freshVersion = getInstalledDwsVersion();
582
+ const freshDisplay = freshVersion ? `v${freshVersion}` : (targetVersion ? `v${targetVersion}` : '');
583
+ console.log(green(` ✔ dws CLI installed (${freshDisplay})`) + '\n');
584
+ console.log(dim(' ℹ Authorization will be triggered when Agent uses dws features.') + '\n');
548
585
  console.log(dim(' You can also authorize manually anytime: ') + cyan('dws auth login') + '\n');
549
586
  }
550
587
 
@@ -6,7 +6,7 @@ import * as fs from "fs";
6
6
  *
7
7
  * 职责:
8
8
  * - 管理单个钉钉账号的 WebSocket 连接
9
- * - 实现应用层心跳检测(10 秒间隔 ping,90 秒无 pong 响应则重连)
9
+ * - 实现应用层心跳检测(10 秒间隔,90 秒超时)
10
10
  * - 处理连接重连逻辑,带指数退避
11
11
  * - 消息去重(内置 Map,5 分钟 TTL)
12
12
  *
@@ -18,7 +18,7 @@ import * as fs from "fs";
18
18
  /** 心跳间隔(毫秒) */
19
19
  const HEARTBEAT_INTERVAL = 10 * 1e3;
20
20
  /** 超时阈值(毫秒) */
21
- const TIMEOUT_THRESHOLD = 90 * 1e3;
21
+ const TIMEOUT_THRESHOLD = 20 * 1e3;
22
22
  /** 基础退避时间(毫秒) */
23
23
  const BASE_BACKOFF_DELAY = 1e3;
24
24
  /** 最大退避时间(毫秒) */
@@ -151,9 +151,6 @@ async function monitorSingleAccount(opts) {
151
151
  client.socket?.once("open", onOpen);
152
152
  client.socket?.once("error", onError);
153
153
  })) throw new Error(`连接建立超时或失败`);
154
- setupPongListener();
155
- setupMessageListener();
156
- setupCloseListener();
157
154
  lastSocketAvailableTime = Date.now();
158
155
  connectionEstablishedTime = Date.now();
159
156
  reconnectAttempts = 0;
@@ -266,6 +263,9 @@ async function monitorSingleAccount(opts) {
266
263
  if (client.socket) client.socket.removeAllListeners();
267
264
  logger.debug(`Connection 已停止`);
268
265
  }
266
+ setupPongListener();
267
+ setupMessageListener();
268
+ setupCloseListener();
269
269
  return new Promise(async (resolve, reject) => {
270
270
  if (abortSignal) {
271
271
  const onAbort = async () => {
@@ -369,10 +369,7 @@ async function monitorSingleAccount(opts) {
369
369
  await client.connect();
370
370
  logger.info(`Connected to DingTalk Stream successfully`);
371
371
  logger.info(`PID: ${process.pid}`);
372
- logger.info(`✅ 自定义 keepAlive: true (${HEARTBEAT_INTERVAL / 1e3}秒心跳,${TIMEOUT_THRESHOLD / 1e3}秒超时), 指数退避重连`);
373
- setupPongListener();
374
- setupMessageListener();
375
- setupCloseListener();
372
+ logger.info(`✅ 自定义 keepAlive: true (10 秒心跳,90 秒超时), 指数退避重连`);
376
373
  onStatusChange?.({
377
374
  connected: true,
378
375
  lastConnectedAt: Date.now()
@@ -402,14 +399,6 @@ async function monitorSingleAccount(opts) {
402
399
  reject(/* @__PURE__ */ new Error(`[DingTalk][${accountId}] Authentication failed (401 Unauthorized):\n - Your clientId or clientSecret is invalid, expired, or revoked\n - clientId: ${clientIdStr.substring(0, 8)}...\n - Please verify your credentials at DingTalk Developer Console\n - Error details: ${error.message}`));
403
400
  return;
404
401
  }
405
- if (error.response?.status === 403 || error.message?.includes("status code 403") || error.message?.includes("403")) {
406
- reject(/* @__PURE__ */ new Error(`[DingTalk][${accountId}] Forbidden (403):\n - 可能原因 / Possible causes:\n 1. 机器人未开启 Stream 模式(需在钉钉开放平台 → 消息接收模式中设置)\n 2. 机器人处于"开发中"状态,尚未发布上线\n 3. API 调用频率超限(QPS Limit)\n - Error details: ${error.message}\n - Response data: ${JSON.stringify(error.response?.data || {})}`));
407
- return;
408
- }
409
- if (error.message?.includes("protocol") || error.message?.includes("mismatch") || error.message?.includes("upgrade")) {
410
- reject(/* @__PURE__ */ new Error(`[DingTalk][${accountId}] Failed to connect to DingTalk Stream: ${error.message}\n - 可能原因 / Possible causes:\n 1. dingtalk-stream SDK 版本过旧,请升级插件: npx openclaw@latest add @dingtalk-real-ai/dingtalk-connector\n 2. 网络代理或防火墙拦截了 WebSocket 连接\n 3. 钉钉服务端协议已更新,当前版本不兼容\n - 建议 / Suggestions:\n 1. 升级到最新版本的 dingtalk-connector 插件\n 2. 检查网络环境是否允许 WebSocket 连接\n 3. 如仍有问题,请提 Issue 并附上完整日志`));
411
- return;
412
- }
413
402
  reject(/* @__PURE__ */ new Error(`[DingTalk][${accountId}] Failed to connect to DingTalk Stream: ${error.message}`));
414
403
  return;
415
404
  }
package/dist/index.d.mts CHANGED
@@ -156,6 +156,19 @@ type ResolvedDingtalkAccount = {
156
156
  //#endregion
157
157
  //#region src/channel.d.ts
158
158
  declare const dingtalkPlugin: ChannelPlugin<ResolvedDingtalkAccount>;
159
+ /**
160
+ * Synchronously initializes `dingtalkPlugin.configSchema` using `createRequire`.
161
+ *
162
+ * Static `import ... from "openclaw/plugin-sdk/core"` causes
163
+ * "Cannot find package 'openclaw'" when the plugin is installed to
164
+ * `~/.openclaw/extensions/` (Issue #527) because the ESM loader resolves
165
+ * bare specifiers at parse time before the gateway's jiti alias map is active.
166
+ *
167
+ * By deferring the resolve to `register()` time and using `createRequire`
168
+ * (which searches the gateway's own `node_modules`), we avoid the crash
169
+ * while keeping the call synchronous as required by the plugin API.
170
+ */
171
+ declare function initDingtalkPluginConfigSchema(): void;
159
172
  //#endregion
160
173
  //#region src/runtime.d.ts
161
174
  declare const setDingtalkRuntime: (next: PluginRuntime) => void, getDingtalkRuntime: () => PluginRuntime;
@@ -169,4 +182,4 @@ declare function registerGatewayMethods(api: OpenClawPluginApi): void;
169
182
  //#region index.d.ts
170
183
  declare function register(api: OpenClawPluginApi): void;
171
184
  //#endregion
172
- export { register as default, dingtalkPlugin, registerGatewayMethods, setDingtalkRuntime };
185
+ export { register as default, dingtalkPlugin, initDingtalkPluginConfigSchema, registerGatewayMethods, setDingtalkRuntime };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as dingtalkPlugin, n as setDingtalkRuntime } from "./runtime-C9AAkRdJ.mjs";
1
+ import { a as initDingtalkPluginConfigSchema, i as dingtalkPlugin, n as setDingtalkRuntime } from "./runtime-b4xvqwW6.mjs";
2
2
  import { t as registerGatewayMethods } from "./gateway-methods-DI8lkjSd.mjs";
3
3
  //#region index.ts
4
4
  /**
@@ -37,8 +37,9 @@ function recordAndCheckLoadPath(api) {
37
37
  function register(api) {
38
38
  recordAndCheckLoadPath(api);
39
39
  setDingtalkRuntime(api.runtime);
40
+ initDingtalkPluginConfigSchema();
40
41
  api.registerChannel({ plugin: dingtalkPlugin });
41
42
  registerGatewayMethods(api);
42
43
  }
43
44
  //#endregion
44
- export { register as default, dingtalkPlugin, registerGatewayMethods, setDingtalkRuntime };
45
+ export { register as default, dingtalkPlugin, initDingtalkPluginConfigSchema, registerGatewayMethods, setDingtalkRuntime };
@@ -1,6 +1,6 @@
1
1
  import { u as uploadMediaToDingTalk } from "./media-DUMfXnwJ.mjs";
2
2
  import { a as resolveDingtalkAccount } from "./accounts-CF4oK_HZ.mjs";
3
- import { r as CHANNEL_ID, t as getDingtalkRuntime } from "./runtime-C9AAkRdJ.mjs";
3
+ import { r as CHANNEL_ID, t as getDingtalkRuntime } from "./runtime-b4xvqwW6.mjs";
4
4
  import { n as createLoggerFromConfig } from "./logger-BDWwViGT.mjs";
5
5
  import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
6
6
  import { i as getOapiAccessToken } from "./utils-CIfI_3Jh.mjs";
@@ -204,7 +204,7 @@ async function uploadAndReplaceFileMarkers(content, sessionWebhook, config, oapi
204
204
  }
205
205
  //#endregion
206
206
  //#region src/reply-dispatcher.ts
207
- const { createReplyPrefixOptions = () => void 0, createTypingCallbacks = () => ({}), logTypingFailure = () => {} } = await import("openclaw/plugin-sdk/channel-runtime").catch(() => null) ?? {};
207
+ const { createReplyPrefixOptions, createTypingCallbacks, logTypingFailure } = await import("openclaw/plugin-sdk/channel-runtime");
208
208
  function createDingtalkReplyDispatcher(params) {
209
209
  const core = getDingtalkRuntime();
210
210
  const { cfg, agentId, conversationId, senderId, isDirect, accountId, sessionWebhook, asyncMode = false, preCreatedCard } = params;
@@ -4,6 +4,7 @@ import { t as createLogger } from "./logger-BDWwViGT.mjs";
4
4
  import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
5
5
  import "./utils-CIfI_3Jh.mjs";
6
6
  import { n as sendMediaToDingTalk, o as sendTextToDingTalk } from "./messaging-CyIJY4h2.mjs";
7
+ import { createRequire } from "node:module";
7
8
  import { z, z as z$1 } from "zod";
8
9
  //#region src/secret-input.ts
9
10
  function buildSecretInputSchema() {
@@ -481,9 +482,6 @@ async function renderQrCodeText(content) {
481
482
  }
482
483
  //#endregion
483
484
  //#region src/onboarding.ts
484
- const { promptSingleChannelSecretInput } = await import("openclaw/plugin-sdk/setup").catch(() => ({ promptSingleChannelSecretInput: async () => {
485
- throw new Error("openclaw SDK not available — cannot prompt for secret input. Please upgrade OpenClaw.");
486
- } }));
487
485
  const _env$1 = globalThis["process"].env;
488
486
  const channel = "dingtalk-connector";
489
487
  const DINGTALK_MANUAL_SETUP_DOC = "docs/DINGTALK_MANUAL_SETUP.md";
@@ -750,7 +748,8 @@ const dingtalkOnboardingAdapter = {
750
748
  prompter,
751
749
  initialValue: normalizeString(dingtalkCfg?.clientId) ?? normalizeString(_env$1.DINGTALK_CLIENT_ID)
752
750
  });
753
- const clientSecretResult = await promptSingleChannelSecretInput({
751
+ const { promptSingleChannelSecretInput: promptSecret } = await import("openclaw/plugin-sdk/setup");
752
+ const clientSecretResult = await promptSecret({
754
753
  cfg: next,
755
754
  prompter,
756
755
  providerHint: "dingtalk",
@@ -906,8 +905,8 @@ async function monitorDingtalkProvider(opts = {}) {
906
905
  const log = createLogger(cfg.channels?.["dingtalk-connector"]?.debug ?? false);
907
906
  const [accountsModule, monitorAccountModule, monitorSingleModule] = await Promise.all([
908
907
  import("./accounts-BSIiLyZa.mjs"),
909
- import("./message-handler-DAFzVJ-a.mjs"),
910
- import("./connection-CCvXXBXw.mjs")
908
+ import("./message-handler-_vk6QsWo.mjs"),
909
+ import("./connection-BZd5NXuh.mjs")
911
910
  ]);
912
911
  const { resolveDingtalkAccount, listEnabledDingtalkAccounts } = accountsModule;
913
912
  const { handleDingTalkMessage } = monitorAccountModule;
@@ -949,7 +948,6 @@ async function monitorDingtalkProvider(opts = {}) {
949
948
  }
950
949
  //#endregion
951
950
  //#region src/channel.ts
952
- const { buildChannelConfigSchema } = await import("openclaw/plugin-sdk/core").catch(() => ({ buildChannelConfigSchema: (schema) => ({ schema }) }));
953
951
  /** Channel identifier used across the plugin. Single source of truth. */
954
952
  const CHANNEL_ID = "dingtalk-connector";
955
953
  /**
@@ -1000,7 +998,7 @@ const dingtalkPlugin = {
1000
998
  groups: { resolveToolPolicy: resolveDingtalkGroupToolPolicy },
1001
999
  mentions: { stripPatterns: () => ["@[^\\s]+"] },
1002
1000
  reload: { configPrefixes: [`channels.${CHANNEL_ID}`] },
1003
- configSchema: buildChannelConfigSchema(DingtalkConfigBaseSchema),
1001
+ configSchema: void 0,
1004
1002
  config: {
1005
1003
  listAccountIds: (cfg) => listDingtalkAccountIds(cfg),
1006
1004
  resolveAccount: (cfg, accountId) => resolveDingtalkAccount({
@@ -1346,6 +1344,23 @@ const dingtalkPlugin = {
1346
1344
  }
1347
1345
  } }
1348
1346
  };
1347
+ /**
1348
+ * Synchronously initializes `dingtalkPlugin.configSchema` using `createRequire`.
1349
+ *
1350
+ * Static `import ... from "openclaw/plugin-sdk/core"` causes
1351
+ * "Cannot find package 'openclaw'" when the plugin is installed to
1352
+ * `~/.openclaw/extensions/` (Issue #527) because the ESM loader resolves
1353
+ * bare specifiers at parse time before the gateway's jiti alias map is active.
1354
+ *
1355
+ * By deferring the resolve to `register()` time and using `createRequire`
1356
+ * (which searches the gateway's own `node_modules`), we avoid the crash
1357
+ * while keeping the call synchronous as required by the plugin API.
1358
+ */
1359
+ function initDingtalkPluginConfigSchema() {
1360
+ if (dingtalkPlugin.configSchema != null) return;
1361
+ const { buildChannelConfigSchema } = createRequire(import.meta.url)("openclaw/plugin-sdk/core");
1362
+ dingtalkPlugin.configSchema = buildChannelConfigSchema(DingtalkConfigBaseSchema);
1363
+ }
1349
1364
  //#endregion
1350
1365
  //#region src/runtime.ts
1351
1366
  /**
@@ -1372,4 +1387,4 @@ function createRuntimeStore(errorMessage) {
1372
1387
  }
1373
1388
  const { setRuntime: setDingtalkRuntime, getRuntime: getDingtalkRuntime } = createRuntimeStore("DingTalk runtime not initialized");
1374
1389
  //#endregion
1375
- export { dingtalkPlugin as i, setDingtalkRuntime as n, CHANNEL_ID as r, getDingtalkRuntime as t };
1390
+ export { initDingtalkPluginConfigSchema as a, dingtalkPlugin as i, setDingtalkRuntime as n, CHANNEL_ID as r, getDingtalkRuntime as t };
@@ -0,0 +1,49 @@
1
+ # Release Notes - v0.8.20
2
+
3
+ ## 🎉 新版本亮点 / Highlights
4
+
5
+ 本次版本聚焦 **OpenClaw 兼容性修复** 和 **DWS CLI 版本管理优化**。修复了插件安装到 `~/.openclaw/extensions/` 时因 ESM 裸说明符解析导致的加载崩溃(Issue #527),并重构了 DWS CLI 的版本升级/降级策略,提升安装体验。
6
+
7
+ This release focuses on **OpenClaw compatibility fixes** and **DWS CLI version management improvements**. Fixes a plugin load crash caused by ESM bare specifier resolution when installed to `~/.openclaw/extensions/` (Issue #527), and refactors DWS CLI upgrade/downgrade logic for a smoother install experience.
8
+
9
+ ## 🐛 修复 / Fixes
10
+
11
+ - **OpenClaw 插件加载兼容性 (Issue #527) / Plugin load compatibility**
12
+ `configSchema` 改为延迟初始化,通过 `createRequire` 解析 `openclaw/plugin-sdk/core`,修复插件安装到 `~/.openclaw/extensions/` 时 ESM 裸说明符解析失败导致的 "Cannot find package 'openclaw'" 崩溃。
13
+ `configSchema` deferred to lazy init via `createRequire`, fixing "Cannot find package 'openclaw'" crash when plugin is installed to `~/.openclaw/extensions/`.
14
+
15
+ - **Onboarding 动态导入 / Dynamic import for onboarding**
16
+ `promptSingleChannelSecretInput` 从静态 import 改为动态 `import()`,避免在 ESM 加载阶段触发同样的裸说明符解析错误。
17
+ `promptSingleChannelSecretInput` switched from static to dynamic `import()` to avoid bare specifier resolution error during ESM loading.
18
+
19
+ ## ✅ 改进 / Improvements
20
+
21
+ - **DWS CLI 版本管理重构 / Version management refactor**
22
+ `ensureDwsCli()` 新增 `compareVersions()` 语义版本比较,支持四种场景:
23
+ - 目标版本更高 → 自动升级 / Auto-upgrade when target is newer
24
+ - 本地版本更高 → 询问是否覆盖 / Prompt before downgrade
25
+ - 版本一致 → 跳过 / Skip when equal
26
+ - 全新安装 → 显示已安装版本号 / Show version on fresh install
27
+
28
+ ## 📥 安装升级 / Installation & Upgrade
29
+
30
+ ```bash
31
+ npx openclaw@latest add @dingtalk-real-ai/dingtalk-connector
32
+ ```
33
+
34
+ 或指定版本:
35
+ ```bash
36
+ npx openclaw@latest add @dingtalk-real-ai/dingtalk-connector@0.8.20
37
+ ```
38
+
39
+ ## 🔗 相关链接 / Related Links
40
+
41
+ - [完整变更日志](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/blob/main/CHANGELOG.md)
42
+ - [使用文档](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/blob/main/README.md)
43
+ - [故障排查](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/blob/main/docs/TROUBLESHOOTING.md)
44
+
45
+ ---
46
+
47
+ **发布日期 / Release Date**:2026-04-28
48
+ **版本号 / Version**:v0.8.20
49
+ **兼容性 / Compatibility**:OpenClaw Gateway 2026.4.9+
package/index.ts CHANGED
@@ -15,11 +15,11 @@
15
15
  */
16
16
 
17
17
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
18
- import { dingtalkPlugin } from "./src/channel.ts";
18
+ import { dingtalkPlugin, initDingtalkPluginConfigSchema } from "./src/channel.ts";
19
19
  import { setDingtalkRuntime } from "./src/runtime.ts";
20
20
  import { registerGatewayMethods } from "./src/gateway-methods.ts";
21
21
 
22
- export { dingtalkPlugin } from "./src/channel.ts";
22
+ export { dingtalkPlugin, initDingtalkPluginConfigSchema } from "./src/channel.ts";
23
23
  export { setDingtalkRuntime } from "./src/runtime.ts";
24
24
  export { registerGatewayMethods } from "./src/gateway-methods.ts";
25
25
 
@@ -71,6 +71,7 @@ function recordAndCheckLoadPath(api: OpenClawPluginApi): void {
71
71
  export default function register(api: OpenClawPluginApi) {
72
72
  recordAndCheckLoadPath(api);
73
73
  setDingtalkRuntime(api.runtime);
74
+ initDingtalkPluginConfigSchema();
74
75
  api.registerChannel({ plugin: dingtalkPlugin });
75
76
  registerGatewayMethods(api);
76
77
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "dingtalk-connector",
3
3
  "name": "DingTalk Channel",
4
- "version": "0.8.20-beta.7",
4
+ "version": "0.8.20",
5
5
  "description": "Official OpenClaw DingTalk channel plugin | 钉钉官方 OpenClaw 插件",
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.20-beta.7",
3
+ "version": "0.8.20",
4
4
  "description": "Official OpenClaw DingTalk channel plugin | 钉钉官方 OpenClaw 插件",
5
5
  "type": "module",
6
6
  "exports": {
@@ -90,7 +90,7 @@
90
90
  },
91
91
  "dependencies": {
92
92
  "axios": "1.14.0",
93
- "dingtalk-stream": "2.1.5",
93
+ "dingtalk-stream": "2.1.4",
94
94
  "form-data": "4.0.0",
95
95
  "qrcode-terminal": "0.12.0",
96
96
  "zod": "4.3.6"
package/src/channel.ts CHANGED
@@ -1,12 +1,8 @@
1
+ import { createRequire as nodeCreateRequire } from "node:module";
1
2
  import type {
2
3
  ChannelPlugin,
3
4
  ClawdbotConfig,
4
5
  } from "openclaw/plugin-sdk";
5
- // 动态导入 buildChannelConfigSchema,避免 openclaw 包不可达时插件加载崩溃
6
- const { buildChannelConfigSchema } = await import("openclaw/plugin-sdk/core").catch(() => ({
7
- // 降级:直接返回 Zod schema 包装,Gateway 仍可运行(仅丢失 Web UI JSON Schema 展示)
8
- buildChannelConfigSchema: (schema: any) => ({ schema }),
9
- }));
10
6
  import {
11
7
  createDefaultChannelRuntimeState,
12
8
  DEFAULT_ACCOUNT_ID,
@@ -124,7 +120,7 @@ export const dingtalkPlugin: ChannelPlugin<ResolvedDingtalkAccount> = {
124
120
  stripPatterns: () => ['@[^\\s]+'], // Strip @mentions
125
121
  },
126
122
  reload: { configPrefixes: [`channels.${CHANNEL_ID}`] },
127
- configSchema: buildChannelConfigSchema(DingtalkConfigBaseSchema),
123
+ configSchema: undefined as any, // Initialized lazily by initDingtalkPluginConfigSchema()
128
124
  config: {
129
125
  listAccountIds: (cfg) => listDingtalkAccountIds(cfg),
130
126
  resolveAccount: (cfg, accountId) => resolveDingtalkAccount({ cfg, accountId }),
@@ -535,4 +531,23 @@ export const dingtalkPlugin: ChannelPlugin<ResolvedDingtalkAccount> = {
535
531
  }
536
532
  },
537
533
  },
538
- };
534
+ };
535
+
536
+ /**
537
+ * Synchronously initializes `dingtalkPlugin.configSchema` using `createRequire`.
538
+ *
539
+ * Static `import ... from "openclaw/plugin-sdk/core"` causes
540
+ * "Cannot find package 'openclaw'" when the plugin is installed to
541
+ * `~/.openclaw/extensions/` (Issue #527) because the ESM loader resolves
542
+ * bare specifiers at parse time before the gateway's jiti alias map is active.
543
+ *
544
+ * By deferring the resolve to `register()` time and using `createRequire`
545
+ * (which searches the gateway's own `node_modules`), we avoid the crash
546
+ * while keeping the call synchronous as required by the plugin API.
547
+ */
548
+ export function initDingtalkPluginConfigSchema(): void {
549
+ if (dingtalkPlugin.configSchema != null) return;
550
+ const require_ = nodeCreateRequire(import.meta.url);
551
+ const { buildChannelConfigSchema } = require_("openclaw/plugin-sdk/core");
552
+ (dingtalkPlugin as any).configSchema = buildChannelConfigSchema(DingtalkConfigBaseSchema);
553
+ }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 职责:
5
5
  * - 管理单个钉钉账号的 WebSocket 连接
6
- * - 实现应用层心跳检测(10 秒间隔 ping,90 秒无 pong 响应则重连)
6
+ * - 实现应用层心跳检测(10 秒间隔,90 秒超时)
7
7
  * - 处理连接重连逻辑,带指数退避
8
8
  * - 消息去重(内置 Map,5 分钟 TTL)
9
9
  *
@@ -58,7 +58,7 @@ export type MonitorDingtalkAccountOpts = {
58
58
  /** 心跳间隔(毫秒) */
59
59
  const HEARTBEAT_INTERVAL = 10 * 1000; // 10 秒
60
60
  /** 超时阈值(毫秒) */
61
- const TIMEOUT_THRESHOLD = 90 * 1000; // 90 秒(约 9 次心跳未响应)
61
+ const TIMEOUT_THRESHOLD = 20 * 1000; // 20 秒(2 次心跳未响应)
62
62
  /** 基础退避时间(毫秒) */
63
63
  const BASE_BACKOFF_DELAY = 1000; // 1 秒
64
64
  /** 最大退避时间(毫秒) */
@@ -279,12 +279,7 @@ export async function monitorSingleAccount(
279
279
  throw new Error(`连接建立超时或失败`);
280
280
  }
281
281
 
282
- // 4. 重新注册 socket 事件监听器(新连接产生新 socket,旧监听器已失效)
283
- setupPongListener();
284
- setupMessageListener();
285
- setupCloseListener();
286
-
287
- // 5. 重置 socket 可用时间、连接建立时间和重连计数
282
+ // 4. 重置 socket 可用时间、连接建立时间和重连计数
288
283
  lastSocketAvailableTime = Date.now();
289
284
  connectionEstablishedTime = Date.now(); // 重置连接建立时间
290
285
  reconnectAttempts = 0; // 重连成功,重置计数
@@ -454,8 +449,10 @@ export async function monitorSingleAccount(
454
449
  logger.debug(`Connection 已停止`);
455
450
  }
456
451
 
457
- // 注意:socket 事件监听器在 client.connect() 成功后注册(见下方),
458
- // 因为 connect 之前 client.socket 为 null,可选链 ?.on() 不会生效。
452
+ // 初始化:设置所有事件监听器
453
+ setupPongListener();
454
+ setupMessageListener();
455
+ setupCloseListener();
459
456
 
460
457
  return new Promise<void>(async (resolve, reject) => {
461
458
  // Handle abort signal
@@ -644,14 +641,9 @@ export async function monitorSingleAccount(
644
641
  logger.info(`Connected to DingTalk Stream successfully`);
645
642
  logger.info(`PID: ${process.pid}`);
646
643
  logger.info(
647
- `✅ 自定义 keepAlive: true (${HEARTBEAT_INTERVAL / 1000}秒心跳,${TIMEOUT_THRESHOLD / 1000}秒超时), 指数退避重连`,
644
+ `✅ 自定义 keepAlive: true (10 秒心跳,90 秒超时), 指数退避重连`,
648
645
  );
649
646
 
650
- // 连接成功后注册 socket 事件监听器(此时 client.socket 已可用)
651
- setupPongListener();
652
- setupMessageListener();
653
- setupCloseListener();
654
-
655
647
  // 初次连接成功,向框架报告 connected: true
656
648
  onStatusChange?.({ connected: true, lastConnectedAt: Date.now() });
657
649
 
@@ -710,36 +702,6 @@ export async function monitorSingleAccount(
710
702
  return;
711
703
  }
712
704
 
713
- // 处理 403 错误(权限/限流)
714
- if (error.response?.status === 403 || error.message?.includes("status code 403") || error.message?.includes("403")) {
715
- reject(new Error(
716
- `[DingTalk][${accountId}] Forbidden (403):\n` +
717
- ` - 可能原因 / Possible causes:\n` +
718
- ` 1. 机器人未开启 Stream 模式(需在钉钉开放平台 → 消息接收模式中设置)\n` +
719
- ` 2. 机器人处于"开发中"状态,尚未发布上线\n` +
720
- ` 3. API 调用频率超限(QPS Limit)\n` +
721
- ` - Error details: ${error.message}\n` +
722
- ` - Response data: ${JSON.stringify(error.response?.data || {})}`,
723
- ));
724
- return;
725
- }
726
-
727
- // 处理协议不匹配(通常是 dingtalk-stream SDK 版本过旧)
728
- if (error.message?.includes('protocol') || error.message?.includes('mismatch') || error.message?.includes('upgrade')) {
729
- reject(new Error(
730
- `[DingTalk][${accountId}] Failed to connect to DingTalk Stream: ${error.message}\n` +
731
- ` - 可能原因 / Possible causes:\n` +
732
- ` 1. dingtalk-stream SDK 版本过旧,请升级插件: npx openclaw@latest add @dingtalk-real-ai/dingtalk-connector\n` +
733
- ` 2. 网络代理或防火墙拦截了 WebSocket 连接\n` +
734
- ` 3. 钉钉服务端协议已更新,当前版本不兼容\n` +
735
- ` - 建议 / Suggestions:\n` +
736
- ` 1. 升级到最新版本的 dingtalk-connector 插件\n` +
737
- ` 2. 检查网络环境是否允许 WebSocket 连接\n` +
738
- ` 3. 如仍有问题,请提 Issue 并附上完整日志`,
739
- ));
740
- return;
741
- }
742
-
743
705
  // 处理其他连接错误
744
706
  reject(new Error(
745
707
  `[DingTalk][${accountId}] Failed to connect to DingTalk Stream: ${error.message}`,
package/src/onboarding.ts CHANGED
@@ -7,6 +7,7 @@ import type {
7
7
  ChannelSetupWizardAdapter,
8
8
  ChannelSetupDmPolicy,
9
9
  DmPolicy,
10
+ // promptSingleChannelSecretInput is dynamically imported at call sites (Issue #527)
10
11
  } from "openclaw/plugin-sdk/setup";
11
12
  import {
12
13
  addWildcardAllowFrom,
@@ -14,12 +15,6 @@ import {
14
15
  formatDocsLink,
15
16
  hasConfiguredSecretInput,
16
17
  } from "./sdk/helpers.ts";
17
- // 动态导入,避免 openclaw 包不可达时插件加载崩溃
18
- const { promptSingleChannelSecretInput } = await import("openclaw/plugin-sdk/setup").catch(() => ({
19
- promptSingleChannelSecretInput: async () => {
20
- throw new Error("openclaw SDK not available — cannot prompt for secret input. Please upgrade OpenClaw.");
21
- },
22
- })) as { promptSingleChannelSecretInput: typeof import("openclaw/plugin-sdk/setup")["promptSingleChannelSecretInput"] };
23
18
  import { resolveDingtalkAccount, resolveDingtalkCredentials } from "./config/accounts.ts";
24
19
  import { probeDingtalk } from "./probe.ts";
25
20
  import type { DingtalkConfig } from "./types/index.ts";
@@ -453,7 +448,8 @@ export const dingtalkOnboardingAdapter: ChannelSetupWizardAdapter = {
453
448
  normalizeString(dingtalkCfg?.clientId) ?? normalizeString(_env.DINGTALK_CLIENT_ID),
454
449
  });
455
450
 
456
- const clientSecretResult = await promptSingleChannelSecretInput({
451
+ const { promptSingleChannelSecretInput: promptSecret } = await import("openclaw/plugin-sdk/setup");
452
+ const clientSecretResult = await promptSecret({
457
453
  cfg: next,
458
454
  prompter,
459
455
  providerHint: "dingtalk",
@@ -17,14 +17,14 @@ interface ReplyPayload {
17
17
  [key: string]: any;
18
18
  }
19
19
 
20
- // ✅ 动态导入 channel-runtime 模块,带降级处理
21
- const channelRuntimeModule = await import("openclaw/plugin-sdk/channel-runtime").catch(() => null) as any;
20
+ // ✅ 动态导入 channel-runtime 模块
21
+ const channelRuntimeModule = await import("openclaw/plugin-sdk/channel-runtime") as any;
22
22
 
23
23
  const {
24
- createReplyPrefixOptions = () => undefined,
25
- createTypingCallbacks = () => ({}),
26
- logTypingFailure = () => {},
27
- } = channelRuntimeModule ?? {};
24
+ createReplyPrefixOptions,
25
+ createTypingCallbacks,
26
+ logTypingFailure,
27
+ } = channelRuntimeModule;
28
28
 
29
29
  import { createLoggerFromConfig } from "./utils/logger.ts";
30
30
  import { CHANNEL_ID } from "./channel.ts";