@dingtalk-real-ai/dingtalk-connector 0.7.8 → 0.7.9
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 +16 -0
- package/docs/RELEASE_NOTES_V0.7.9.md +65 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/plugin.ts +98 -4
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@
|
|
|
6
6
|
This document records all significant changes. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
7
7
|
and version numbers follow [Semantic Versioning](https://semver.org/).
|
|
8
8
|
|
|
9
|
+
## [0.7.9] - 2026-03-13
|
|
10
|
+
|
|
11
|
+
### 新增 / Added
|
|
12
|
+
- ✨ **应用层心跳机制** - 钉钉 Stream 客户端使用自定义心跳(WebSocket ping/pong,30 秒间隔、90 秒超时),超时后主动断开并重连,重连失败 5 秒后重试
|
|
13
|
+
**Application-layer heartbeat** - Stream client uses custom ping/pong heartbeat (30s interval, 90s timeout), reconnects on timeout with 5s retry on failure
|
|
14
|
+
- ✨ **统一停止与清理** - 停止客户端时通过 `doStop` 统一清理心跳定时器并调用 `client.disconnect()`,确保连接正确关闭
|
|
15
|
+
**Unified stop & cleanup** - `doStop` clears heartbeat timer and calls `client.disconnect()` when stopping the client
|
|
16
|
+
|
|
17
|
+
### 修复 / Fixes
|
|
18
|
+
- 🐛 **长连接静默断开** - 关闭 SDK 激进 keepAlive(8 秒超时),改用应用层心跳,减少因长时间无数据导致的静默断连且无法恢复
|
|
19
|
+
**Long-lived connection silent disconnect** - Disabled SDK aggressive keepAlive (8s timeout), use app-layer heartbeat to reduce silent disconnects when idle
|
|
20
|
+
|
|
21
|
+
### 改进 / Improvements
|
|
22
|
+
- ✅ **DWClient 配置** - 启用 `autoReconnect: true`,设置 `keepAlive: false`,由应用层心跳替代 SDK 心跳,避免与钉钉服务端策略冲突
|
|
23
|
+
**DWClient config** - `autoReconnect: true`, `keepAlive: false`; app-layer heartbeat replaces SDK keepAlive to avoid conflicts with server
|
|
24
|
+
|
|
9
25
|
## [0.7.8] - 2026-03-13
|
|
10
26
|
|
|
11
27
|
### 修复 / Fixes
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Release Notes - v0.7.9
|
|
2
|
+
|
|
3
|
+
## ✨ 功能与体验改进 / Features & Improvements
|
|
4
|
+
|
|
5
|
+
- **钉钉 Stream 客户端心跳与重连机制优化 / DingTalk Stream Client Heartbeat & Reconnect**
|
|
6
|
+
关闭 DWClient SDK 内置的激进 keepAlive(避免 8 秒超时强制断连),启用应用层自定义心跳:基于 WebSocket ping/pong,30 秒间隔、90 秒超时,超时后主动断开并重连,重连失败时 5 秒后重试,提升长连稳定性。
|
|
7
|
+
Disabled the SDK's aggressive keepAlive (which could force disconnect after 8s), and added an application-layer heartbeat: WebSocket ping/pong with 30s interval and 90s timeout; on timeout the client disconnects and reconnects, with a 5s retry on failure, improving long-lived connection stability.
|
|
8
|
+
|
|
9
|
+
- **DWClient 配置调整 / DWClient Configuration**
|
|
10
|
+
启用 `autoReconnect: true` 以在连接断开时自动重连;设置 `keepAlive: false`,由应用层心跳替代 SDK 心跳,避免与钉钉服务端策略冲突。
|
|
11
|
+
Enabled `autoReconnect: true` for automatic reconnection on disconnect; set `keepAlive: false` and rely on application-layer heartbeat to avoid conflicts with DingTalk server behavior.
|
|
12
|
+
|
|
13
|
+
- **统一停止与清理逻辑 / Unified Stop & Cleanup**
|
|
14
|
+
停止 Stream 客户端时统一通过 `doStop` 清理心跳定时器并调用 `client.disconnect()`,确保资源释放与连接正确关闭。
|
|
15
|
+
When stopping the Stream client, a unified `doStop` now clears the heartbeat timer and calls `client.disconnect()` for consistent resource cleanup and connection closure.
|
|
16
|
+
|
|
17
|
+
## 🐛 修复 / Fixes
|
|
18
|
+
|
|
19
|
+
- **长连接被服务端或中间网络提前断开 / Long-lived Connection Premature Disconnect**
|
|
20
|
+
通过应用层心跳检测连接活性,超时后主动重连,减少因长时间无数据导致的静默断连且无法恢复的问题。
|
|
21
|
+
Application-layer heartbeat detects connection liveness and triggers reconnect on timeout, reducing silent disconnects when the link is idle.
|
|
22
|
+
|
|
23
|
+
## 📋 技术细节 / Technical Details
|
|
24
|
+
|
|
25
|
+
### 应用层心跳机制 / Application-Layer Heartbeat
|
|
26
|
+
|
|
27
|
+
- **参数**:心跳间隔 30 秒(`HEARTBEAT_INTERVAL`),超时 90 秒(`HEARTBEAT_TIMEOUT`),允许约 3 次 ping 无响应后再判定超时。
|
|
28
|
+
- **流程**:定时器每 30 秒通过 `client.socket?.ping()` 发送 ping;监听 `socket.on('pong')` 更新 `lastPongTime`;若当前时间与 `lastPongTime` 差值超过 90 秒则触发重连。
|
|
29
|
+
- **重连**:先 `await client.disconnect()`,再 `await client.connect()`,成功后重置 `lastPongTime`;若重连失败则 5 秒后再次尝试 `client.connect()`。
|
|
30
|
+
- **停止**:`doStop(reason)` 中设置 `stopped = true`、清除心跳定时器、调用 `client.disconnect()`,并记录停止原因与活动。
|
|
31
|
+
|
|
32
|
+
### DWClient 配置说明 / DWClient Config
|
|
33
|
+
|
|
34
|
+
- `autoReconnect: true` — 连接断开时由 SDK 参与自动重连。
|
|
35
|
+
- `keepAlive: false` — 关闭 SDK 内置的激进心跳,避免 8 秒无活动即强制断连,由应用层 30s/90s 心跳替代。
|
|
36
|
+
|
|
37
|
+
## 📥 安装升级 / Installation & Upgrade
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# 通过 npm 安装最新版本 / Install latest version via npm
|
|
41
|
+
openclaw plugins install @dingtalk-real-ai/dingtalk-connector
|
|
42
|
+
|
|
43
|
+
# 或升级现有版本 / Or upgrade existing version
|
|
44
|
+
openclaw plugins update dingtalk-connector
|
|
45
|
+
|
|
46
|
+
# 通过 Git 安装 / Install via Git
|
|
47
|
+
openclaw plugins install https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector.git
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## ⚠️ 升级注意事项 / Upgrade Notes
|
|
51
|
+
|
|
52
|
+
- **向下兼容 / Backward Compatible**:仅调整 Stream 客户端的心跳与重连策略,对现有配置与 API 无破坏性变更。
|
|
53
|
+
- **长连场景建议**:若依赖钉钉 Stream 长连接,升级后将自动使用新的心跳与重连逻辑,无需额外配置。
|
|
54
|
+
|
|
55
|
+
## 🔗 相关链接 / Related Links
|
|
56
|
+
|
|
57
|
+
- [完整变更日志 / Full Changelog](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/blob/main/CHANGELOG.md)
|
|
58
|
+
- [使用文档 / Documentation](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/blob/main/README.md)
|
|
59
|
+
- [问题反馈 / Issue Feedback](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/issues)
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
**发布日期 / Release Date**:2026-03-13
|
|
64
|
+
**版本号 / Version**:v0.7.9
|
|
65
|
+
**兼容性 / Compatibility**:OpenClaw Gateway 0.4.0+
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dingtalk-connector",
|
|
3
3
|
"name": "DingTalk Channel",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.9",
|
|
5
5
|
"description": "DingTalk (钉钉) messaging channel via Stream mode with AI Card streaming",
|
|
6
6
|
"author": "DingTalk Real Team",
|
|
7
7
|
"channels": ["dingtalk-connector"],
|
package/package.json
CHANGED
package/plugin.ts
CHANGED
|
@@ -3551,15 +3551,15 @@ const dingtalkPlugin = {
|
|
|
3551
3551
|
|
|
3552
3552
|
ctx.log?.info(`[${account.accountId}] 启动钉钉 Stream 客户端...`);
|
|
3553
3553
|
|
|
3554
|
-
//
|
|
3554
|
+
// 配置 DWClient:关闭 SDK 内置的 keepAlive,使用应用层自定义心跳
|
|
3555
3555
|
// - autoReconnect: 连接断开时自动重连
|
|
3556
|
-
// - keepAlive:
|
|
3556
|
+
// - keepAlive: false(关闭 SDK 的激进心跳检测,避免 8 秒超时强制终止连接)
|
|
3557
3557
|
const client = new DWClient({
|
|
3558
3558
|
clientId: config.clientId,
|
|
3559
3559
|
clientSecret: config.clientSecret,
|
|
3560
3560
|
debug: config.debug || false,
|
|
3561
3561
|
autoReconnect: true,
|
|
3562
|
-
keepAlive:
|
|
3562
|
+
keepAlive: false,
|
|
3563
3563
|
} as any);
|
|
3564
3564
|
|
|
3565
3565
|
client.registerCallbackListener(TOPIC_ROBOT, async (res: any) => {
|
|
@@ -3611,16 +3611,110 @@ const dingtalkPlugin = {
|
|
|
3611
3611
|
|
|
3612
3612
|
let stopped = false;
|
|
3613
3613
|
|
|
3614
|
+
// 【应用层心跳机制】基于 WebSocket ping/pong 的主动心跳检测
|
|
3615
|
+
// - 心跳间隔:30 秒
|
|
3616
|
+
// - 超时时间:90 秒(允许 3 次 ping 无响应)
|
|
3617
|
+
// - 检测方式:主动发送 ping,等待 pong 响应
|
|
3618
|
+
let lastPongTime = Date.now();
|
|
3619
|
+
let pendingPingId: string | null = null;
|
|
3620
|
+
const HEARTBEAT_INTERVAL = 30 * 1000; // 30 秒
|
|
3621
|
+
const HEARTBEAT_TIMEOUT = 90 * 1000; // 90 秒
|
|
3622
|
+
|
|
3623
|
+
// 监听 pong 响应(SDK 的 keepAlive=false 时仍然会收到服务端的 pong)
|
|
3624
|
+
client.socket?.on('pong', () => {
|
|
3625
|
+
lastPongTime = Date.now();
|
|
3626
|
+
pendingPingId = null;
|
|
3627
|
+
ctx.log?.debug?.(`[${account.accountId}] 收到 PONG 响应`);
|
|
3628
|
+
});
|
|
3629
|
+
|
|
3630
|
+
// 启动心跳检测定时器
|
|
3631
|
+
const heartbeatTimer = setInterval(async () => {
|
|
3632
|
+
if (stopped) {
|
|
3633
|
+
clearInterval(heartbeatTimer);
|
|
3634
|
+
return;
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
const elapsed = Date.now() - lastPongTime;
|
|
3638
|
+
|
|
3639
|
+
// 如果超过 90 秒没有收到 pong,认为连接已断开
|
|
3640
|
+
if (elapsed > HEARTBEAT_TIMEOUT) {
|
|
3641
|
+
ctx.log?.warn?.(`[${account.accountId}] ⚠️ 心跳超时:已 ${Math.round(elapsed / 1000)} 秒未收到 PONG,触发重连...`);
|
|
3642
|
+
|
|
3643
|
+
// 【关键修复】主动重连:先断开再重新建立连接
|
|
3644
|
+
try {
|
|
3645
|
+
// 1. 先断开旧连接
|
|
3646
|
+
await client.disconnect();
|
|
3647
|
+
ctx.log?.info?.(`[${account.accountId}] 已断开旧连接`);
|
|
3648
|
+
|
|
3649
|
+
// 2. 重新建立连接
|
|
3650
|
+
ctx.log?.info?.(`[${account.accountId}] 正在重新建立连接...`);
|
|
3651
|
+
await client.connect();
|
|
3652
|
+
|
|
3653
|
+
// 3. 重置最后 pong 时间,避免立即再次触发重连
|
|
3654
|
+
lastPongTime = Date.now();
|
|
3655
|
+
pendingPingId = null;
|
|
3656
|
+
|
|
3657
|
+
ctx.log?.info?.(`[${account.accountId}] ✅ 重连成功`);
|
|
3658
|
+
} catch (err: any) {
|
|
3659
|
+
ctx.log?.error?.(`[${account.accountId}] ❌ 重连失败:${err.message}`);
|
|
3660
|
+
// 重连失败后,等待 5 秒后再次尝试
|
|
3661
|
+
ctx.log?.info?.(`[${account.accountId}] 5 秒后再次尝试重连...`);
|
|
3662
|
+
setTimeout(async () => {
|
|
3663
|
+
try {
|
|
3664
|
+
await client.connect();
|
|
3665
|
+
lastPongTime = Date.now();
|
|
3666
|
+
pendingPingId = null;
|
|
3667
|
+
ctx.log?.info?.(`[${account.accountId}] ✅ 重试重连成功`);
|
|
3668
|
+
} catch (retryErr: any) {
|
|
3669
|
+
ctx.log?.error?.(`[${account.accountId}] ❌ 重试重连失败:${retryErr.message}`);
|
|
3670
|
+
}
|
|
3671
|
+
}, 5000);
|
|
3672
|
+
}
|
|
3673
|
+
return;
|
|
3674
|
+
}
|
|
3675
|
+
|
|
3676
|
+
// 如果还有 ping 在等待响应,检查是否超时
|
|
3677
|
+
if (pendingPingId) {
|
|
3678
|
+
ctx.log?.debug?.(`[${account.accountId}] 心跳检测:等待 PONG 响应中...`);
|
|
3679
|
+
return;
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
// 主动发送 ping 消息
|
|
3683
|
+
try {
|
|
3684
|
+
const pingId = `ping_${Date.now()}`;
|
|
3685
|
+
pendingPingId = pingId;
|
|
3686
|
+
|
|
3687
|
+
// 通过 WebSocket 直接发送 ping(使用 SDK 的 socket)
|
|
3688
|
+
client.socket?.ping(JSON.stringify({
|
|
3689
|
+
type: 'PING',
|
|
3690
|
+
id: pingId,
|
|
3691
|
+
timestamp: Date.now()
|
|
3692
|
+
}));
|
|
3693
|
+
|
|
3694
|
+
ctx.log?.debug?.(`[${account.accountId}] 发送 PING 请求:${pingId}`);
|
|
3695
|
+
} catch (err: any) {
|
|
3696
|
+
ctx.log?.error?.(`[${account.accountId}] 发送 PING 失败:${err.message}`);
|
|
3697
|
+
// 发送失败也计入超时
|
|
3698
|
+
}
|
|
3699
|
+
}, HEARTBEAT_INTERVAL);
|
|
3700
|
+
|
|
3614
3701
|
// 统一的停止逻辑
|
|
3615
3702
|
const doStop = (reason: string) => {
|
|
3616
3703
|
if (stopped) return;
|
|
3617
3704
|
stopped = true;
|
|
3618
3705
|
ctx.log?.info(`[${account.accountId}] 停止钉钉 Stream 客户端 (${reason})...`);
|
|
3706
|
+
|
|
3707
|
+
// 清理心跳定时器
|
|
3708
|
+
if (typeof heartbeatTimer !== 'undefined') {
|
|
3709
|
+
clearInterval(heartbeatTimer);
|
|
3710
|
+
ctx.log?.debug?.(`[${account.accountId}] 心跳定时器已清理`);
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3619
3713
|
try {
|
|
3620
3714
|
// 【关键】调用 disconnect() 正确关闭 WebSocket 连接
|
|
3621
3715
|
client.disconnect();
|
|
3622
3716
|
} catch (err: any) {
|
|
3623
|
-
ctx.log?.warn?.(`[${account.accountId}]
|
|
3717
|
+
ctx.log?.warn?.(`[${account.accountId}] 断开连接时出错:${err.message}`);
|
|
3624
3718
|
}
|
|
3625
3719
|
rt.channel.activity.record('dingtalk-connector', account.accountId, 'stop');
|
|
3626
3720
|
};
|