@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 +13 -0
- package/bin/dingtalk-connector.js +50 -13
- package/dist/{connection-CCvXXBXw.mjs → connection-BZd5NXuh.mjs} +6 -17
- package/dist/index.d.mts +14 -1
- package/dist/index.mjs +3 -2
- package/dist/{message-handler-DAFzVJ-a.mjs → message-handler-_vk6QsWo.mjs} +2 -2
- package/dist/{runtime-C9AAkRdJ.mjs → runtime-b4xvqwW6.mjs} +24 -9
- package/docs/RELEASE_NOTES_V0.8.20.md +49 -0
- package/index.ts +3 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/src/channel.ts +22 -7
- package/src/core/connection.ts +8 -46
- package/src/onboarding.ts +3 -7
- package/src/reply-dispatcher.ts +6 -6
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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
console.log(
|
|
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
|
|
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
|
|
517
|
-
if (
|
|
545
|
+
const downgraded = installDwsCli();
|
|
546
|
+
if (downgraded) {
|
|
518
547
|
const newVersion = getInstalledDwsVersion();
|
|
519
|
-
console.log(green(` ✔ dws CLI
|
|
548
|
+
console.log(green(` ✔ dws CLI replaced with v${newVersion || targetVersion}`) + '\n');
|
|
520
549
|
} else {
|
|
521
|
-
console.log(red(' ⚠
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 (
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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-
|
|
910
|
-
import("./connection-
|
|
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:
|
|
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
|
}
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dingtalk-real-ai/dingtalk-connector",
|
|
3
|
-
"version": "0.8.20
|
|
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.
|
|
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:
|
|
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
|
+
}
|
package/src/core/connection.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 职责:
|
|
5
5
|
* - 管理单个钉钉账号的 WebSocket 连接
|
|
6
|
-
* - 实现应用层心跳检测(10
|
|
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 =
|
|
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.
|
|
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
|
-
//
|
|
458
|
-
|
|
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 (
|
|
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
|
|
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",
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -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")
|
|
20
|
+
// ✅ 动态导入 channel-runtime 模块
|
|
21
|
+
const channelRuntimeModule = await import("openclaw/plugin-sdk/channel-runtime") as any;
|
|
22
22
|
|
|
23
23
|
const {
|
|
24
|
-
createReplyPrefixOptions
|
|
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";
|