@dingtalk-real-ai/dingtalk-connector 0.7.8 → 0.7.10
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 +55 -6
- package/README.md +1 -1
- package/docs/RELEASE_NOTES_V0.7.10.md +40 -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 +133 -47
- package/src/socket-manager.ts +312 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,60 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
版本号遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
and
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.7.10] - 2026-03-16
|
|
9
|
+
|
|
10
|
+
### 新增 / Added
|
|
11
|
+
- ✨ **WebSocket 心跳重连机制优化** - 关闭 DWClient 的 `autoConnect`,采用应用层自动重连机制(修复 DWClient 重连 bug);添加指数退避重连策略,避免雪崩效应;使用 WebSocket 原生 Ping 进行心跳检测
|
|
12
|
+
**WebSocket heartbeat & reconnect optimization** - Disabled DWClient's `autoConnect`, implemented app-layer auto-reconnect (fixing DWClient bug); added exponential backoff to avoid avalanche; using WebSocket native Ping for heartbeat
|
|
13
|
+
- ✨ **socket-manager 模块** - 新增模块统一管理 WebSocket 连接生命周期,包括心跳检测、自动重连、指数退避、事件监听等
|
|
14
|
+
**socket-manager module** - New module for unified WebSocket connection lifecycle management, including heartbeat, auto-reconnect, exponential backoff, event listening
|
|
15
|
+
- ✨ **debug 参数** - 添加 `debug` 配置项控制详细日志输出,便于问题排查
|
|
16
|
+
**debug parameter** - Added `debug` config to control detailed log output for easier troubleshooting
|
|
17
|
+
- ✨ **WebSocket 无限重连机制** - 移除最大重连次数限制,实现无限重连,确保长连接服务的高可用性
|
|
18
|
+
**WebSocket infinite reconnection** - Removed maximum reconnection attempt limit, implemented infinite reconnection to ensure high availability for long-lived connection services
|
|
19
|
+
|
|
20
|
+
### 修复 / Fixes
|
|
21
|
+
- 🐛 **修复 DWClient 重连 bug** - DWClient 内置重连机制存在缺陷,通过应用层重连机制替代,确保连接稳定可靠
|
|
22
|
+
**Fixed DWClient reconnect bug** - DWClient's built-in reconnect has defects; replaced with app-layer reconnect for stable connection
|
|
23
|
+
- 🐛 **长连接静默断开** - 通过应用层心跳检测连接活性,超时后主动重连,减少因长时间无数据导致的静默断连且无法恢复
|
|
24
|
+
**Long-lived connection silent disconnect** - App-layer heartbeat detects liveness and triggers reconnect on timeout, reducing silent disconnects when idle
|
|
25
|
+
### 改进 / Improvements
|
|
26
|
+
- ✅ **DWClient 配置** - 设置 `autoReconnect: false`、`keepAlive: false`,由应用层接管重连和心跳,避免与钉钉服务端策略冲突
|
|
27
|
+
**DWClient config** - Set `autoReconnect: false`, `keepAlive: false`; app-layer takes over reconnect and heartbeat to avoid server conflicts
|
|
28
|
+
- ✅ **指数退避策略** - 公式 `baseBackoffDelay * Math.pow(2, attempt) + jitter(0-1s)`,最大退避 30 秒,避免雪崩效应
|
|
29
|
+
**Exponential backoff strategy** - Formula `baseBackoffDelay * Math.pow(2, attempt) + jitter(0-1s)`, max 30s backoff to avoid avalanche effect
|
|
30
|
+
- ✅ **统一事件监听** - `pong`、`message`、`close`、`open` 四个事件统一管理和清理,提升代码可维护性
|
|
31
|
+
**Unified event listening** - `pong`, `message`, `close`, `open` events managed and cleaned up uniformly, improving maintainability
|
|
32
|
+
- ✅ **配置简化** - 从 `SocketManagerConfig` 中移除 `maxReconnectAttempts` 配置项,简化配置复杂度
|
|
33
|
+
**Configuration simplification** - Removed `maxReconnectAttempts` from `SocketManagerConfig`, simplifying configuration
|
|
34
|
+
- ✅ **日志输出优化** - 更新重连日志格式,移除最大次数显示(从 "尝试 X/5" 改为 "尝试 X")
|
|
35
|
+
**Log output optimization** - Updated reconnection log format, removed maximum attempt display (from "attempt X/5" to "attempt X")
|
|
36
|
+
|
|
37
|
+
### 技术细节 / Technical Details
|
|
38
|
+
- **退避策略**:指数退避 + 随机抖动,公式 `baseBackoffDelay * Math.pow(2, attempt) + jitter(0-1s)`
|
|
39
|
+
- **最大退避**:30 秒(由 `maxBackoffDelay` 限制)
|
|
40
|
+
- **重置条件**:重连成功后 `reconnectAttempts` 归零
|
|
41
|
+
- **立即重连**:心跳检测失败、WebSocket close、disconnect 消息触发时立即重连,不退避
|
|
42
|
+
|
|
43
|
+
## [0.7.9] - 2026-03-13
|
|
44
|
+
|
|
45
|
+
### 新增 / Added
|
|
46
|
+
- ✨ **应用层心跳机制** - 钉钉 Stream 客户端使用自定义心跳(WebSocket ping/pong,30 秒间隔、90 秒超时),超时后主动断开并重连,重连失败 5 秒后重试
|
|
47
|
+
**Application-layer heartbeat** - Stream client uses custom ping/pong heartbeat (30s interval, 90s timeout), reconnects on timeout with 5s retry on failure
|
|
48
|
+
- ✨ **统一停止与清理** - 停止客户端时通过 `doStop` 统一清理心跳定时器并调用 `client.disconnect()`,确保连接正确关闭
|
|
49
|
+
**Unified stop & cleanup** - `doStop` clears heartbeat timer and calls `client.disconnect()` when stopping the client
|
|
50
|
+
|
|
51
|
+
### 修复 / Fixes
|
|
52
|
+
- 🐛 **长连接静默断开** - 关闭 SDK 激进 keepAlive(8 秒超时),改用应用层心跳,减少因长时间无数据导致的静默断连且无法恢复
|
|
53
|
+
**Long-lived connection silent disconnect** - Disabled SDK aggressive keepAlive (8s timeout), use app-layer heartbeat to reduce silent disconnects when idle
|
|
54
|
+
|
|
55
|
+
### 改进 / Improvements
|
|
56
|
+
- ✅ **DWClient 配置** - 启用 `autoReconnect: true`,设置 `keepAlive: false`,由应用层心跳替代 SDK 心跳,避免与钉钉服务端策略冲突
|
|
57
|
+
**DWClient config** - `autoReconnect: true`, `keepAlive: false`; app-layer heartbeat replaces SDK keepAlive to avoid conflicts with server
|
|
8
58
|
|
|
9
59
|
## [0.7.8] - 2026-03-13
|
|
10
60
|
|
|
@@ -227,5 +277,4 @@ and version numbers follow [Semantic Versioning](https://semver.org/).
|
|
|
227
277
|
- 新增"多 Agent 配置"章节,提供详细的配置示例和说明
|
|
228
278
|
Added "Multi-Agent Configuration" section with detailed configuration examples and instructions
|
|
229
279
|
- 补充常见问题解答
|
|
230
|
-
Added FAQ section
|
|
231
|
-
|
|
280
|
+
Added FAQ section
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
以下提供两种方案连接到 [OpenClaw](https://openclaw.ai) Gateway,分别是钉钉机器人和钉钉 DEAP Agent。
|
|
5
5
|
|
|
6
|
-
> 📝 **版本信息**:当前版本 v0.7.
|
|
6
|
+
> 📝 **版本信息**:当前版本 v0.7.10 | [查看变更日志](CHANGELOG.md) | [发布说明](docs/RELEASE_NOTES_V0.7.10.md) | [发布指南](RELEASE.md)
|
|
7
7
|
|
|
8
8
|
## 快速导航
|
|
9
9
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Release Notes - v0.7.10
|
|
2
|
+
|
|
3
|
+
## ✨ 功能与体验改进 / Features & Improvements
|
|
4
|
+
|
|
5
|
+
- **WebSocket 心跳重连机制优化 / WebSocket Reconnection**
|
|
6
|
+
WebSocket 会持续尝试重连,保留指数退避策略(1s → 2s → 4s → 8s → 16s → 30s),避免雪崩效应。重连成功后重置计数器,下次失败从 1 秒开始退避。
|
|
7
|
+
Removed maximum reconnection attempt limit, implemented infinite reconnection mechanism. WebSocket will continuously attempt to reconnect without giving up. Exponential backoff strategy retained (1s → 2s → 4s → 8s → 16s → 30s) to avoid avalanche effect. Counter resets on successful reconnection, starting from 1 second on next failure.
|
|
8
|
+
|
|
9
|
+
- **配置简化 / Configuration Simplification**
|
|
10
|
+
从 `SocketManagerConfig` 中移除 `maxReconnectAttempts` 配置项,简化配置复杂度。
|
|
11
|
+
Removed `maxReconnectAttempts` configuration from `SocketManagerConfig`, simplifying configuration complexity.
|
|
12
|
+
|
|
13
|
+
- **日志输出优化 / Log Output Optimization**
|
|
14
|
+
更新重连日志格式,移除最大次数显示(从 "尝试 X/5" 改为 "尝试 X"),更清晰地展示重连进度。
|
|
15
|
+
Updated reconnection log format, removed maximum attempt display (from "attempt X/5" to "attempt X"), providing clearer reconnection progress.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## 📥 安装升级 / Installation & Upgrade
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# 通过 npm 安装最新版本 / Install latest version via npm
|
|
22
|
+
openclaw plugins install @dingtalk-real-ai/dingtalk-connector
|
|
23
|
+
|
|
24
|
+
# 或升级现有版本 / Or upgrade existing version
|
|
25
|
+
openclaw plugins update dingtalk-connector
|
|
26
|
+
|
|
27
|
+
# 通过 Git 安装 / Install via Git
|
|
28
|
+
openclaw plugins install https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector.git
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 🔗 相关链接 / Related Links
|
|
32
|
+
|
|
33
|
+
- [完整变更日志 / Full Changelog](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/blob/main/CHANGELOG.md)
|
|
34
|
+
- [使用文档 / Documentation](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/blob/main/README.md)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
**发布日期 / Release Date**:2026-03-16
|
|
39
|
+
**版本号 / Version**:v0.7.10
|
|
40
|
+
**兼容性 / Compatibility**:OpenClaw Gateway 0.4.0+
|
|
@@ -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.10",
|
|
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
|
@@ -11,6 +11,7 @@ import * as fs from 'fs';
|
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import * as os from 'os';
|
|
13
13
|
import type { ClawdbotPluginApi, PluginRuntime, ClawdbotConfig } from 'clawdbot/plugin-sdk';
|
|
14
|
+
import { createSocketManager, addToPendingAckQueue, removeFromPendingAckQueue, clearPendingAckQueue } from './src/socket-manager';
|
|
14
15
|
|
|
15
16
|
// ============ 常量 ============
|
|
16
17
|
|
|
@@ -1263,6 +1264,7 @@ async function finishAICard(
|
|
|
1263
1264
|
order: ['msgContent'], // 只声明实际使用的字段,避免部分客户端显示空占位
|
|
1264
1265
|
}),
|
|
1265
1266
|
},
|
|
1267
|
+
cardUpdateOptions:{updateCardDataByKey:true}
|
|
1266
1268
|
},
|
|
1267
1269
|
};
|
|
1268
1270
|
|
|
@@ -1469,47 +1471,64 @@ async function* streamFromGateway(options: GatewayOptions, accountId: string): A
|
|
|
1469
1471
|
|
|
1470
1472
|
log?.info?.(`[DingTalk][Gateway] POST ${gatewayUrl}, session=${sessionKey}, accountId=${accountId}, agentId=${agentId}, peerKind=${peerKind}, messages=${messages.length}`);
|
|
1471
1473
|
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
}
|
|
1481
|
-
});
|
|
1474
|
+
// 【TLS 模式修复】保存原始 TLS 设置,用于 finally 块中恢复
|
|
1475
|
+
const originalRejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
|
1476
|
+
|
|
1477
|
+
try {
|
|
1478
|
+
// TLS 模式:如果是 HTTPS URL,临时禁用证书验证(用于自签名证书场景)
|
|
1479
|
+
if (gatewayUrl.startsWith('https://')) {
|
|
1480
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
1481
|
+
log?.debug?.(`[DingTalk][Gateway] TLS 模式:已临时禁用证书验证`);
|
|
1482
|
+
}
|
|
1482
1483
|
|
|
1483
|
-
|
|
1484
|
+
const response = await fetch(gatewayUrl, {
|
|
1485
|
+
method: 'POST',
|
|
1486
|
+
headers,
|
|
1487
|
+
body: JSON.stringify({
|
|
1488
|
+
model: 'main',
|
|
1489
|
+
messages,
|
|
1490
|
+
stream: true,
|
|
1491
|
+
user: sessionKey, // 用于 session 持久化
|
|
1492
|
+
}),
|
|
1493
|
+
});
|
|
1484
1494
|
|
|
1485
|
-
|
|
1486
|
-
const errText = response.body ? await response.text() : '(no body)';
|
|
1487
|
-
log?.error?.(`[DingTalk][Gateway] 错误响应: ${errText}`);
|
|
1488
|
-
throw new Error(`Gateway error: ${response.status} - ${errText}`);
|
|
1489
|
-
}
|
|
1495
|
+
log?.info?.(`[DingTalk][Gateway] 响应 status=${response.status}, ok=${response.ok}, hasBody=${!!response.body}`);
|
|
1490
1496
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1497
|
+
if (!response.ok || !response.body) {
|
|
1498
|
+
const errText = response.body ? await response.text() : '(no body)';
|
|
1499
|
+
log?.error?.(`[DingTalk][Gateway] 错误响应:${errText}`);
|
|
1500
|
+
throw new Error(`Gateway error: ${response.status} - ${errText}`);
|
|
1501
|
+
}
|
|
1494
1502
|
|
|
1495
|
-
|
|
1496
|
-
const
|
|
1497
|
-
|
|
1503
|
+
const reader = response.body.getReader();
|
|
1504
|
+
const decoder = new TextDecoder();
|
|
1505
|
+
let buffer = '';
|
|
1498
1506
|
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1507
|
+
while (true) {
|
|
1508
|
+
const { done, value } = await reader.read();
|
|
1509
|
+
if (done) break;
|
|
1502
1510
|
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
if (data === '[DONE]') return;
|
|
1511
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1512
|
+
const lines = buffer.split('\n');
|
|
1513
|
+
buffer = lines.pop() || '';
|
|
1507
1514
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
const
|
|
1511
|
-
if (
|
|
1512
|
-
|
|
1515
|
+
for (const line of lines) {
|
|
1516
|
+
if (!line.startsWith('data: ')) continue;
|
|
1517
|
+
const data = line.slice(6).trim();
|
|
1518
|
+
if (data === '[DONE]') return;
|
|
1519
|
+
|
|
1520
|
+
try {
|
|
1521
|
+
const chunk = JSON.parse(data);
|
|
1522
|
+
const content = chunk.choices?.[0]?.delta?.content;
|
|
1523
|
+
if (content) yield content;
|
|
1524
|
+
} catch {}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
} finally {
|
|
1528
|
+
// 【TLS 模式修复】恢复原始 TLS 证书验证设置,避免影响其他 HTTPS 请求
|
|
1529
|
+
if (gatewayUrl.startsWith('https://')) {
|
|
1530
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = originalRejectUnauthorized;
|
|
1531
|
+
log?.debug?.(`[DingTalk][Gateway] TLS 模式:已恢复证书验证设置`);
|
|
1513
1532
|
}
|
|
1514
1533
|
}
|
|
1515
1534
|
}
|
|
@@ -1864,7 +1883,11 @@ async function createAICardForTarget(
|
|
|
1864
1883
|
const createBody = {
|
|
1865
1884
|
cardTemplateId: AI_CARD_TEMPLATE_ID,
|
|
1866
1885
|
outTrackId: cardInstanceId,
|
|
1867
|
-
cardData: {
|
|
1886
|
+
cardData: {
|
|
1887
|
+
cardParamMap: {
|
|
1888
|
+
config: JSON.stringify({ autoLayout: true }), // 启用宽屏模式
|
|
1889
|
+
},
|
|
1890
|
+
},
|
|
1868
1891
|
callbackType: 'STREAM',
|
|
1869
1892
|
imGroupOpenSpaceModel: { supportForward: true },
|
|
1870
1893
|
imRobotOpenSpaceModel: { supportForward: true },
|
|
@@ -2950,7 +2973,7 @@ async function handleDingTalkMessage(params: {
|
|
|
2950
2973
|
const finalContent = accumulated.trim();
|
|
2951
2974
|
if (finalContent.length === 0) {
|
|
2952
2975
|
log?.info?.(`[DingTalk][AICard] 内容为空(纯媒体消息),使用默认提示`);
|
|
2953
|
-
await finishAICard(card, '
|
|
2976
|
+
await finishAICard(card, '当前没有可展示的回复内容', log);
|
|
2954
2977
|
} else {
|
|
2955
2978
|
await finishAICard(card, finalContent, log);
|
|
2956
2979
|
}
|
|
@@ -3550,27 +3573,49 @@ const dingtalkPlugin = {
|
|
|
3550
3573
|
}
|
|
3551
3574
|
|
|
3552
3575
|
ctx.log?.info(`[${account.accountId}] 启动钉钉 Stream 客户端...`);
|
|
3576
|
+
ctx.log?.info(`[${account.accountId}] 配置信息:clientId=${config.clientId}, endpoint=${config.endpoint || '默认'}`);
|
|
3553
3577
|
|
|
3554
|
-
//
|
|
3555
|
-
// - autoReconnect:
|
|
3556
|
-
// - keepAlive:
|
|
3578
|
+
// 配置 DWClient:关闭 SDK 内置的 keepAlive 和 autoReconnect,使用应用层自定义心跳和重连
|
|
3579
|
+
// - autoReconnect: false(关闭 SDK 的自动重连,避免与应用层重连冲突)
|
|
3580
|
+
// - keepAlive: false(关闭 SDK 的激进心跳检测,避免 8 秒超时强制终止连接)
|
|
3581
|
+
// - endpoint: 可选,自定义钉钉 API 网关地址,默认使用 SDK 内置的 https://api.dingtalk.com/v1.0/gateway/connections/open
|
|
3557
3582
|
const client = new DWClient({
|
|
3558
3583
|
clientId: config.clientId,
|
|
3559
3584
|
clientSecret: config.clientSecret,
|
|
3560
3585
|
debug: config.debug || false,
|
|
3561
|
-
autoReconnect:
|
|
3562
|
-
keepAlive:
|
|
3586
|
+
autoReconnect: false, // ← 关闭 SDK 的自动重连,使用应用层重连
|
|
3587
|
+
keepAlive: false,
|
|
3588
|
+
// ← 可选:自定义 endpoint,如使用内网代理或测试环境。如果不配置或配置错误,使用 SDK 默认值
|
|
3589
|
+
...(config.endpoint ? { endpoint: config.endpoint } : {}),
|
|
3563
3590
|
} as any);
|
|
3564
3591
|
|
|
3592
|
+
ctx.log?.info(`[${account.accountId}] DWClient 初始化完成,endpoint=${client.getConfig()?.endpoint || '默认'}`);
|
|
3593
|
+
|
|
3565
3594
|
client.registerCallbackListener(TOPIC_ROBOT, async (res: any) => {
|
|
3566
3595
|
const messageId = res.headers?.messageId;
|
|
3567
3596
|
ctx.log?.info?.(`[DingTalk] 收到 Stream 回调, messageId=${messageId}, headers=${JSON.stringify(res.headers)}`);
|
|
3568
3597
|
|
|
3569
|
-
//
|
|
3570
|
-
// 钉钉 Stream 模式要求及时响应,否则约60秒后会重发消息
|
|
3598
|
+
// 【关键修复】检查 WebSocket 状态后再确认回调,避免在 CONNECTING 状态下发送失败
|
|
3571
3599
|
if (messageId) {
|
|
3572
|
-
client.
|
|
3573
|
-
|
|
3600
|
+
if (client.socket?.readyState === 1) { // 1 = OPEN
|
|
3601
|
+
client.socketCallBackResponse(messageId, { success: true });
|
|
3602
|
+
ctx.log?.info?.(`[DingTalk] 已立即确认回调:messageId=${messageId}`);
|
|
3603
|
+
} else {
|
|
3604
|
+
ctx.log?.warn?.(`[DingTalk] WebSocket 未就绪 (readyState=${client.socket?.readyState}),延迟确认回调:messageId=${messageId}`);
|
|
3605
|
+
// 【关键修复】将消息 ID 加入待确认队列,等待 WebSocket 打开后批量确认
|
|
3606
|
+
pendingAckQueue.add(messageId);
|
|
3607
|
+
// 等待 WebSocket 打开后再确认(兼容旧逻辑,但主要依赖 open 事件)
|
|
3608
|
+
setTimeout(() => {
|
|
3609
|
+
if (client.socket?.readyState === 1) {
|
|
3610
|
+
client.socketCallBackResponse(messageId, { success: true });
|
|
3611
|
+
pendingAckQueue.delete(messageId); // 确认成功后从队列移除
|
|
3612
|
+
ctx.log?.info?.(`[DingTalk] 延迟确认回调成功:messageId=${messageId}`);
|
|
3613
|
+
} else {
|
|
3614
|
+
ctx.log?.warn?.(`[DingTalk] 延迟确认回调失败:WebSocket 仍未就绪,messageId=${messageId},将在 open 事件中批量确认`);
|
|
3615
|
+
// 不再从队列移除,等待 open 事件处理
|
|
3616
|
+
}
|
|
3617
|
+
}, 500); // 【关键修复】增加延迟时间到 500ms,确保 WebSocket 有足够时间打开
|
|
3618
|
+
}
|
|
3574
3619
|
}
|
|
3575
3620
|
|
|
3576
3621
|
// 【消息去重】检查是否已处理过该消息
|
|
@@ -3604,6 +3649,19 @@ const dingtalkPlugin = {
|
|
|
3604
3649
|
});
|
|
3605
3650
|
|
|
3606
3651
|
await client.connect();
|
|
3652
|
+
|
|
3653
|
+
// 【关键修复】等待 WebSocket 完全打开后再继续
|
|
3654
|
+
// 避免 Gateway 重启后 WebSocket 还在 CONNECTING 状态就开始处理消息
|
|
3655
|
+
if (client.socket) {
|
|
3656
|
+
if (client.socket.readyState !== 1) { // 1 = OPEN
|
|
3657
|
+
ctx.log?.info?.(`[${account.accountId}] 等待 WebSocket 打开...`);
|
|
3658
|
+
await new Promise((resolve) => {
|
|
3659
|
+
client.socket!.once('open', resolve);
|
|
3660
|
+
setTimeout(resolve, 5000); // 最多等 5 秒
|
|
3661
|
+
});
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3607
3665
|
ctx.log?.info(`[${account.accountId}] 钉钉 Stream 客户端已连接`);
|
|
3608
3666
|
|
|
3609
3667
|
const rt = getRuntime();
|
|
@@ -3611,16 +3669,45 @@ const dingtalkPlugin = {
|
|
|
3611
3669
|
|
|
3612
3670
|
let stopped = false;
|
|
3613
3671
|
|
|
3672
|
+
// 【关键修复】待确认消息队列:重连期间暂存需要确认的消息 ID
|
|
3673
|
+
const pendingAckQueue = new Set<string>();
|
|
3674
|
+
|
|
3675
|
+
// 【使用 SocketManager 统一管理 WebSocket 连接、心跳、重连】
|
|
3676
|
+
const debugMode = config.debug || false;
|
|
3677
|
+
const socketManager = createSocketManager(client, {
|
|
3678
|
+
accountId: account.accountId,
|
|
3679
|
+
log: ctx.log,
|
|
3680
|
+
stopped: () => stopped,
|
|
3681
|
+
onReconnect: () => {
|
|
3682
|
+
// 重连成功后的回调(如果需要)
|
|
3683
|
+
},
|
|
3684
|
+
pendingAckQueue,
|
|
3685
|
+
client,
|
|
3686
|
+
debug: debugMode,
|
|
3687
|
+
});
|
|
3688
|
+
|
|
3689
|
+
// 启动 keepAlive 机制
|
|
3690
|
+
const stopKeepAlive = socketManager.startKeepAlive();
|
|
3691
|
+
|
|
3614
3692
|
// 统一的停止逻辑
|
|
3615
3693
|
const doStop = (reason: string) => {
|
|
3616
3694
|
if (stopped) return;
|
|
3617
3695
|
stopped = true;
|
|
3618
3696
|
ctx.log?.info(`[${account.accountId}] 停止钉钉 Stream 客户端 (${reason})...`);
|
|
3697
|
+
|
|
3698
|
+
// 清理 keepAlive 定时器
|
|
3699
|
+
if (typeof stopKeepAlive === 'function') {
|
|
3700
|
+
stopKeepAlive();
|
|
3701
|
+
}
|
|
3702
|
+
|
|
3703
|
+
// 清理 SocketManager
|
|
3704
|
+
socketManager.stop();
|
|
3705
|
+
|
|
3619
3706
|
try {
|
|
3620
3707
|
// 【关键】调用 disconnect() 正确关闭 WebSocket 连接
|
|
3621
3708
|
client.disconnect();
|
|
3622
3709
|
} catch (err: any) {
|
|
3623
|
-
ctx.log?.warn?.(`[${account.accountId}]
|
|
3710
|
+
ctx.log?.warn?.(`[${account.accountId}] 断开连接时出错:${err.message}`);
|
|
3624
3711
|
}
|
|
3625
3712
|
rt.channel.activity.record('dingtalk-connector', account.accountId, 'stop');
|
|
3626
3713
|
};
|
|
@@ -3958,7 +4045,6 @@ const plugin = {
|
|
|
3958
4045
|
respond(true, { docs });
|
|
3959
4046
|
});
|
|
3960
4047
|
|
|
3961
|
-
api.logger?.info('[DingTalk] 插件已注册(支持主动发送 AI Card 消息、文档读写)');
|
|
3962
4048
|
},
|
|
3963
4049
|
};
|
|
3964
4050
|
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DingTalk WebSocket Socket 管理器
|
|
3
|
+
*
|
|
4
|
+
* 负责管理 DingTalk Stream 客户端的 WebSocket 连接、心跳检测、自动重连等生命周期
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { DWClient } from 'dingtalk-stream';
|
|
8
|
+
|
|
9
|
+
export interface SocketManagerOptions {
|
|
10
|
+
accountId: string;
|
|
11
|
+
log?: any;
|
|
12
|
+
stopped: () => boolean;
|
|
13
|
+
onReconnect?: () => void;
|
|
14
|
+
pendingAckQueue?: Set<string>; // 待确认消息队列
|
|
15
|
+
client?: any; // DWClient 实例,用于批量确认消息
|
|
16
|
+
debug?: boolean; // 是否开启 debug 日志
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SocketManagerConfig {
|
|
20
|
+
/** 心跳间隔(毫秒) */
|
|
21
|
+
heartbeatInterval: number;
|
|
22
|
+
/** 超时阈值(毫秒) */
|
|
23
|
+
timeoutThreshold: number;
|
|
24
|
+
/** 基础退避时间(毫秒) */
|
|
25
|
+
baseBackoffDelay: number;
|
|
26
|
+
/** 最大退避时间(毫秒) */
|
|
27
|
+
maxBackoffDelay: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SocketManager {
|
|
31
|
+
/** 启动 keepAlive 机制,返回清理函数 */
|
|
32
|
+
startKeepAlive: () => () => void;
|
|
33
|
+
/** 停止并清理所有资源 */
|
|
34
|
+
stop: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 创建 Socket 管理器
|
|
39
|
+
*
|
|
40
|
+
* @param client - DingTalk Stream 客户端实例
|
|
41
|
+
* @param options - 配置选项
|
|
42
|
+
* @returns SocketManager 实例
|
|
43
|
+
*/
|
|
44
|
+
export function createSocketManager(
|
|
45
|
+
client: DWClient,
|
|
46
|
+
options: SocketManagerOptions
|
|
47
|
+
): SocketManager {
|
|
48
|
+
const { accountId, log, stopped, onReconnect, pendingAckQueue: externalPendingAckQueue, client: externalClient, debug = false } = options;
|
|
49
|
+
|
|
50
|
+
// 使用传入的 pendingAckQueue 或创建新的
|
|
51
|
+
const pendingAckQueue = externalPendingAckQueue || new Set<string>();
|
|
52
|
+
const targetClient = externalClient || client;
|
|
53
|
+
|
|
54
|
+
// 【业界最佳实践配置】
|
|
55
|
+
const config: SocketManagerConfig = {
|
|
56
|
+
heartbeatInterval: 10 * 1000, // 10 秒心跳间隔
|
|
57
|
+
timeoutThreshold: 90 * 1000, // 90 秒超时阈值
|
|
58
|
+
baseBackoffDelay: 1000, // 基础退避 1 秒
|
|
59
|
+
maxBackoffDelay: 30 * 1000, // 最大退避 30 秒
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// 日志辅助函数
|
|
63
|
+
const debugLog = (...args: any[]) => {
|
|
64
|
+
if (debug && log) {
|
|
65
|
+
log?.info?.(...args);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// 状态管理
|
|
70
|
+
let lastSocketAvailableTime = Date.now();
|
|
71
|
+
let isReconnecting = false;
|
|
72
|
+
let reconnectAttempts = 0;
|
|
73
|
+
|
|
74
|
+
// 定时器引用
|
|
75
|
+
let keepAliveTimer: NodeJS.Timeout | null = null;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 计算指数退避延迟(带抖动)
|
|
79
|
+
*/
|
|
80
|
+
function calculateBackoffDelay(attempt: number): number {
|
|
81
|
+
const exponentialDelay = config.baseBackoffDelay * Math.pow(2, attempt);
|
|
82
|
+
const jitter = Math.random() * 1000; // 0-1 秒随机抖动
|
|
83
|
+
return Math.min(exponentialDelay + jitter, config.maxBackoffDelay);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 统一重连函数,带指数退避(无限重连)
|
|
88
|
+
*/
|
|
89
|
+
async function doReconnect(immediate = false) {
|
|
90
|
+
if (isReconnecting) {
|
|
91
|
+
log?.debug?.(`[${accountId}] 正在重连中,跳过`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
isReconnecting = true;
|
|
96
|
+
|
|
97
|
+
// 应用指数退避(非立即重连时)
|
|
98
|
+
if (!immediate && reconnectAttempts > 0) {
|
|
99
|
+
const delay = calculateBackoffDelay(reconnectAttempts);
|
|
100
|
+
log?.info?.(`[${accountId}] ⏳ 等待 ${Math.round(delay / 1000)} 秒后重连 (尝试 ${reconnectAttempts + 1})`);
|
|
101
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// 1. 先断开旧连接(检查 WebSocket 状态)
|
|
106
|
+
if (client.socket?.readyState === 1 || client.socket?.readyState === 3) {
|
|
107
|
+
await client.disconnect();
|
|
108
|
+
log?.info?.(`[${accountId}] 已断开旧连接`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 2. 重新建立连接
|
|
112
|
+
await client.connect();
|
|
113
|
+
|
|
114
|
+
// 3. 重置 socket 可用时间和重连计数
|
|
115
|
+
lastSocketAvailableTime = Date.now();
|
|
116
|
+
reconnectAttempts = 0; // 重连成功,重置计数
|
|
117
|
+
|
|
118
|
+
log?.info?.(`[${accountId}] ✅ 重连成功`);
|
|
119
|
+
|
|
120
|
+
// 调用外部回调
|
|
121
|
+
if (onReconnect) {
|
|
122
|
+
onReconnect();
|
|
123
|
+
}
|
|
124
|
+
} catch (err: any) {
|
|
125
|
+
reconnectAttempts++;
|
|
126
|
+
log?.error?.(`[${accountId}] 重连失败:${err.message} (尝试 ${reconnectAttempts})`);
|
|
127
|
+
throw err;
|
|
128
|
+
} finally {
|
|
129
|
+
isReconnecting = false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 监听 pong 响应(更新 socket 可用时间)
|
|
135
|
+
*/
|
|
136
|
+
function setupPongListener() {
|
|
137
|
+
client.socket?.on('pong', () => {
|
|
138
|
+
lastSocketAvailableTime = Date.now();
|
|
139
|
+
log?.debug?.(`[${accountId}] 收到 PONG 响应`);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 监听 WebSocket message 事件,收到 disconnect 消息时立即触发重连
|
|
145
|
+
*/
|
|
146
|
+
function setupMessageListener() {
|
|
147
|
+
client.socket?.on('message', (data: any) => {
|
|
148
|
+
try {
|
|
149
|
+
const msg = JSON.parse(data);
|
|
150
|
+
if (msg.type === 'SYSTEM' && msg.headers?.topic === 'disconnect') {
|
|
151
|
+
if (!stopped() && !isReconnecting) {
|
|
152
|
+
// 立即重连,不退避
|
|
153
|
+
doReconnect(true).catch(err => {
|
|
154
|
+
log?.error?.(`[${accountId}] 重连失败:${err.message}`);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch (e) {
|
|
159
|
+
// 忽略解析错误
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 监听 WebSocket close 事件,服务端主动断开时立即触发重连
|
|
166
|
+
*/
|
|
167
|
+
function setupCloseListener() {
|
|
168
|
+
client.socket?.on('close', (code, reason) => {
|
|
169
|
+
log?.info?.(`[${accountId}] WebSocket close: code=${code}, reason=${reason || '未知'}, stopped=${stopped()}`);
|
|
170
|
+
|
|
171
|
+
if (stopped()) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 立即重连,不退避
|
|
176
|
+
setTimeout(() => {
|
|
177
|
+
doReconnect(true).catch(err => {
|
|
178
|
+
log?.error?.(`[${accountId}] 重连失败:${err.message}`);
|
|
179
|
+
});
|
|
180
|
+
}, 0);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 监听 WebSocket open 事件,批量确认重连期间积压的消息
|
|
186
|
+
*/
|
|
187
|
+
function setupOpenListener() {
|
|
188
|
+
client.socket?.on('open', () => {
|
|
189
|
+
if (pendingAckQueue.size > 0) {
|
|
190
|
+
log?.info?.(`[${accountId}] WebSocket 已打开,批量确认 ${pendingAckQueue.size} 条积压消息`);
|
|
191
|
+
for (const msgId of pendingAckQueue) {
|
|
192
|
+
try {
|
|
193
|
+
targetClient.socketCallBackResponse(msgId, { success: true });
|
|
194
|
+
log?.info?.(`[DingTalk] 批量确认成功:messageId=${msgId}`);
|
|
195
|
+
} catch (err: any) {
|
|
196
|
+
log?.error?.(`[DingTalk] 批量确认失败:messageId=${msgId}, error=${err.message}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
pendingAckQueue.clear();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 启动 keepAlive 机制(单定时器 + 指数退避)
|
|
206
|
+
*
|
|
207
|
+
* 业界最佳实践:
|
|
208
|
+
* - 单定时器:每 10 秒检查一次,同时完成心跳和超时检测
|
|
209
|
+
* - 使用 WebSocket 原生 Ping
|
|
210
|
+
* - 指数退避重连:避免雪崩效应
|
|
211
|
+
*/
|
|
212
|
+
function startKeepAlive(): () => void {
|
|
213
|
+
debugLog(`[${accountId}] 🚀 启动 keepAlive 定时器,间隔=${config.heartbeatInterval / 1000}秒`);
|
|
214
|
+
|
|
215
|
+
keepAliveTimer = setInterval(async () => {
|
|
216
|
+
if (stopped()) {
|
|
217
|
+
if (keepAliveTimer) clearInterval(keepAliveTimer);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const elapsed = Date.now() - lastSocketAvailableTime;
|
|
223
|
+
|
|
224
|
+
// 【超时检测】超过 90 秒未确认 socket 可用,触发重连
|
|
225
|
+
if (elapsed > config.timeoutThreshold) {
|
|
226
|
+
log?.info?.(`[${accountId}] ⚠️ 超时检测:已 ${Math.round(elapsed / 1000)} 秒未确认 socket 可用,触发重连...`);
|
|
227
|
+
await doReconnect();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 【心跳检测】检查 socket 状态
|
|
232
|
+
const socketState = client.socket?.readyState;
|
|
233
|
+
debugLog(`[${accountId}] 🔍 心跳检测:socket 状态=${socketState}, elapsed=${Math.round(elapsed / 1000)}s`);
|
|
234
|
+
|
|
235
|
+
if (socketState !== 1) {
|
|
236
|
+
log?.info?.(`[${accountId}] ⚠️ 心跳检测:socket 状态=${socketState},触发重连...`);
|
|
237
|
+
await doReconnect(true); // 立即重连,不退避
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 【发送原生 Ping】更新可用时间
|
|
242
|
+
try {
|
|
243
|
+
client.socket?.ping();
|
|
244
|
+
lastSocketAvailableTime = Date.now();
|
|
245
|
+
debugLog(`[${accountId}] 💓 发送 PING 心跳成功`);
|
|
246
|
+
} catch (err: any) {
|
|
247
|
+
log?.warn?.(`[${accountId}] 发送 PING 失败:${err.message}`);
|
|
248
|
+
// 发送失败也计入超时
|
|
249
|
+
}
|
|
250
|
+
} catch (err: any) {
|
|
251
|
+
log?.error?.(`[${accountId}] keepAlive 检测失败:${err.message}`);
|
|
252
|
+
}
|
|
253
|
+
}, config.heartbeatInterval); // 每 10 秒检测一次
|
|
254
|
+
|
|
255
|
+
debugLog(`[${accountId}] ✅ keepAlive 定时器已启动`);
|
|
256
|
+
|
|
257
|
+
// 返回清理函数
|
|
258
|
+
return () => {
|
|
259
|
+
if (keepAliveTimer) clearInterval(keepAliveTimer);
|
|
260
|
+
keepAliveTimer = null;
|
|
261
|
+
debugLog(`[${accountId}] keepAlive 定时器已清理`);
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 停止并清理所有资源
|
|
267
|
+
*/
|
|
268
|
+
function stop() {
|
|
269
|
+
// 清理定时器
|
|
270
|
+
if (keepAliveTimer) clearInterval(keepAliveTimer);
|
|
271
|
+
keepAliveTimer = null;
|
|
272
|
+
|
|
273
|
+
// 清理事件监听器(WebSocket 会自动清理)
|
|
274
|
+
if (client.socket) {
|
|
275
|
+
client.socket.removeAllListeners();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
log?.debug?.(`[${accountId}] SocketManager 已停止`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 初始化:设置所有事件监听器
|
|
282
|
+
setupPongListener();
|
|
283
|
+
setupMessageListener();
|
|
284
|
+
setupCloseListener();
|
|
285
|
+
setupOpenListener();
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
startKeepAlive,
|
|
289
|
+
stop,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 添加消息到待确认队列
|
|
295
|
+
*/
|
|
296
|
+
export function addToPendingAckQueue(queue: Set<string>, messageId: string) {
|
|
297
|
+
queue.add(messageId);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 从待确认队列移除消息
|
|
302
|
+
*/
|
|
303
|
+
export function removeFromPendingAckQueue(queue: Set<string>, messageId: string) {
|
|
304
|
+
queue.delete(messageId);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 清空待确认队列
|
|
309
|
+
*/
|
|
310
|
+
export function clearPendingAckQueue(queue: Set<string>) {
|
|
311
|
+
queue.clear();
|
|
312
|
+
}
|