@jeik/dingtalk-connector 0.8.27 → 0.8.28
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 +49 -0
- package/README.en.md +28 -35
- package/README.md +28 -43
- package/dist/background-work-Cxb4b-91.mjs +42 -0
- package/dist/card-content-cache-c4Ls96eJ.mjs +2 -0
- package/dist/chunk-upload-PwhogwfE.mjs +2 -0
- package/dist/{common-DE0gd0Jg.mjs → common-BZiVgd3A.mjs} +2 -2
- package/dist/{common-B_A78n8u.mjs → common-DgL28gCZ.mjs} +1 -1
- package/dist/{connection-fc6B4z4G.mjs → connection-CZ7ZoN5_.mjs} +313 -86
- package/dist/entry-bundled.mjs +1 -1
- package/dist/gateway-methods-BkI1gKaC.mjs +2 -0
- package/dist/{gateway-methods--aKsyMs-.mjs → gateway-methods-kNX3qvqw.mjs} +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{message-handler-DQZiKx3i.mjs → message-handler-B1usz1wb.mjs} +238 -122
- package/dist/{messaging-WleKf54h.mjs → messaging-DVS3UKOp.mjs} +6 -2
- package/dist/{runtime-DzZo6Gvk.mjs → runtime-A6G7WAWn.mjs} +3 -3
- package/docs/RELEASE_NOTES_V0.8.28.md +56 -0
- package/package.json +2 -2
- package/src/core/connection.ts +465 -158
- package/src/core/message-handler.ts +173 -86
- package/src/reply-dispatcher.ts +234 -77
- package/src/services/messaging/card.ts +2 -0
- package/src/utils/background-work.ts +63 -0
- package/dist/card-content-cache-Dwd8HICP.mjs +0 -2
- package/dist/chunk-upload-DaLXXZH3.mjs +0 -2
- package/dist/gateway-methods-BQd4E0Sk.mjs +0 -2
- /package/dist/{card-content-cache-CYUzCaQJ.mjs → card-content-cache-bDQFz2u8.mjs} +0 -0
- /package/dist/{chunk-upload-BBQgGtcZ.mjs → chunk-upload-BE71drPg.mjs} +0 -0
- /package/dist/{game-xiyou-CAT0ilSj.mjs → game-xiyou-uwsv23AC.mjs} +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { i as checkAndMarkDingtalkMessage } from "./utils-legacy-DQ3HeGHh.mjs";
|
|
2
|
+
import { n as onBackgroundWorkCountChange, t as getActiveBackgroundWorkCount } from "./background-work-Cxb4b-91.mjs";
|
|
2
3
|
import * as fs from "fs";
|
|
3
4
|
//#region src/core/connection.ts
|
|
4
5
|
/**
|
|
@@ -6,26 +7,69 @@ import * as fs from "fs";
|
|
|
6
7
|
*
|
|
7
8
|
* 职责:
|
|
8
9
|
* - 管理单个钉钉账号的 WebSocket 连接
|
|
9
|
-
* - 实现应用层心跳检测(10 秒间隔,
|
|
10
|
+
* - 实现应用层心跳检测(10 秒间隔,20 秒无 pong 超时)
|
|
10
11
|
* - 处理连接重连逻辑,带指数退避
|
|
11
12
|
* - 消息去重(内置 Map,5 分钟 TTL)
|
|
12
13
|
*
|
|
13
14
|
* 核心特性:
|
|
14
15
|
* - 关闭 SDK 内置 keepAlive,使用自定义心跳
|
|
16
|
+
* - connect 后等 OPEN + SYSTEM/REGISTERED 再报 ready(防首条消息丢失)
|
|
17
|
+
* - 重连后先挂 pong/message/close listener 再等就绪(防幽灵重连)
|
|
15
18
|
* - 详细的消息接收日志(三阶段:接收、解析、处理)
|
|
16
19
|
* - 连接统计和监控(每分钟输出)
|
|
17
20
|
*/
|
|
18
21
|
/** 心跳间隔(毫秒) */
|
|
19
22
|
const HEARTBEAT_INTERVAL = 10 * 1e3;
|
|
20
|
-
/**
|
|
21
|
-
|
|
23
|
+
/**
|
|
24
|
+
* 无 pong 软超时:仅计「连续未确认」次数,不立刻 disconnect。
|
|
25
|
+
* 过去这里直接 doReconnect(),会在 socket 仍 OPEN 时拆掉活连接,
|
|
26
|
+
* 造成数秒空窗 → 用户连发 3~7 条消息钉钉推不到本进程(网关 UI 完全收不到)。
|
|
27
|
+
*/
|
|
28
|
+
const SOFT_STALE_MS = 20 * 1e3;
|
|
29
|
+
/** 连续软超时次数达到该值,且 socket 非 OPEN 或 ping 失败,才硬重连 */
|
|
30
|
+
const HARD_RECONNECT_AFTER_MISSES = 3;
|
|
31
|
+
/** 两次硬重连之间的最小间隔(除非 socket 已死) */
|
|
32
|
+
const MIN_RECONNECT_GAP_MS = 30 * 1e3;
|
|
33
|
+
/**
|
|
34
|
+
* 消息处理期间刷新 lastSocketAvailableTime 的间隔。
|
|
35
|
+
* 必须明显小于 SOFT_STALE_MS。
|
|
36
|
+
*/
|
|
37
|
+
const MESSAGE_PROCESSING_KEEPALIVE_MS = 15 * 1e3;
|
|
22
38
|
/** 基础退避时间(毫秒) */
|
|
23
39
|
const BASE_BACKOFF_DELAY = 1e3;
|
|
24
40
|
/** 最大退避时间(毫秒) */
|
|
25
41
|
const MAX_BACKOFF_DELAY = 30 * 1e3;
|
|
42
|
+
/** 单次等待 OPEN + REGISTERED 的超时 */
|
|
43
|
+
const STREAM_READY_TIMEOUT_MS = 15e3;
|
|
44
|
+
/**
|
|
45
|
+
* REGISTERED 未就绪时最多完整重连次数(含首次)。
|
|
46
|
+
* 超时禁止 OPEN-only 假 ready,避免「显示已连接但收不到 CALLBACK」。
|
|
47
|
+
*/
|
|
48
|
+
const REGISTERED_CONNECT_MAX_ATTEMPTS = 5;
|
|
49
|
+
let _streamNoiseSilenced = false;
|
|
50
|
+
function silenceDingtalkStreamConsoleNoise() {
|
|
51
|
+
if (_streamNoiseSilenced) return;
|
|
52
|
+
_streamNoiseSilenced = true;
|
|
53
|
+
const origConsoleInfo = console.info.bind(console);
|
|
54
|
+
console.info = (...args) => {
|
|
55
|
+
const first = args[0];
|
|
56
|
+
if (typeof first === "string") {
|
|
57
|
+
if (first === "Disconnecting.") return;
|
|
58
|
+
if (/^\[[^\]]+\] connect success$/.test(first)) return;
|
|
59
|
+
}
|
|
60
|
+
return origConsoleInfo(...args);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
let _connectionNoticePrinted = false;
|
|
64
|
+
function printConnectionNoticeOnce() {
|
|
65
|
+
if (_connectionNoticePrinted) return;
|
|
66
|
+
_connectionNoticePrinted = true;
|
|
67
|
+
console.log("[dingtalk-connector] ℹ️ 上游 dingtalk-stream 噪音已过滤;须 OPEN+REGISTERED 才报 ready,REGISTERED 超时会强制重连(禁止假 online)。正常运行不应出现 ≤30s 周期性硬重连。");
|
|
68
|
+
}
|
|
26
69
|
async function monitorSingleAccount(opts) {
|
|
27
70
|
const { cfg, account, runtime, abortSignal, messageHandler, onStatusChange } = opts;
|
|
28
71
|
const { accountId } = account;
|
|
72
|
+
silenceDingtalkStreamConsoleNoise();
|
|
29
73
|
const clawdbotConfig = cfg;
|
|
30
74
|
const log = runtime?.log;
|
|
31
75
|
const { createLoggerFromConfig } = await import("./logger-BmJkQkm1.mjs");
|
|
@@ -71,43 +115,192 @@ async function monitorSingleAccount(opts) {
|
|
|
71
115
|
let reconnectAttempts = 0;
|
|
72
116
|
let keepAliveTimer = null;
|
|
73
117
|
let isStopped = false;
|
|
118
|
+
/** 连续 keepAlive 周期内未收到 pong / 未刷新的次数 */
|
|
119
|
+
let consecutiveStaleMisses = 0;
|
|
120
|
+
let lastHardReconnectAt = 0;
|
|
121
|
+
let wsCallbackActive = false;
|
|
74
122
|
let activeMessageProcessing = false;
|
|
75
123
|
let messageProcessingKeepAliveTimer = null;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
* 防止长时间处理(如复杂的 AI 任务)触发心跳超时
|
|
80
|
-
*/
|
|
81
|
-
function markMessageProcessingStart() {
|
|
82
|
-
activeMessageProcessing = true;
|
|
83
|
-
lastSocketAvailableTime = Date.now();
|
|
84
|
-
if (messageProcessingKeepAliveTimer) clearInterval(messageProcessingKeepAliveTimer);
|
|
124
|
+
let unsubscribeBackgroundWork = null;
|
|
125
|
+
function ensureProcessingKeepAliveTimer() {
|
|
126
|
+
if (messageProcessingKeepAliveTimer) return;
|
|
85
127
|
messageProcessingKeepAliveTimer = setInterval(() => {
|
|
86
128
|
if (activeMessageProcessing) {
|
|
87
129
|
lastSocketAvailableTime = Date.now();
|
|
88
|
-
logger.debug(`📝 消息处理中,更新 socket
|
|
130
|
+
logger.debug(`📝 消息处理中,更新 socket 可用时间 (ws=${wsCallbackActive} bg=${getActiveBackgroundWorkCount()})`);
|
|
89
131
|
}
|
|
90
|
-
},
|
|
91
|
-
logger.debug(`📝 消息处理开始,启动活跃标记定时器`);
|
|
132
|
+
}, MESSAGE_PROCESSING_KEEPALIVE_MS);
|
|
92
133
|
}
|
|
93
|
-
|
|
94
|
-
* 标记消息处理结束,停止定期更新机制
|
|
95
|
-
*/
|
|
96
|
-
function markMessageProcessingEnd() {
|
|
97
|
-
activeMessageProcessing = false;
|
|
134
|
+
function clearProcessingKeepAliveTimer() {
|
|
98
135
|
if (messageProcessingKeepAliveTimer) {
|
|
99
136
|
clearInterval(messageProcessingKeepAliveTimer);
|
|
100
137
|
messageProcessingKeepAliveTimer = null;
|
|
101
138
|
}
|
|
139
|
+
}
|
|
140
|
+
/** 根据 WS 入队中 / 后台任务数 合成 activeMessageProcessing */
|
|
141
|
+
function refreshProcessingActive(reason) {
|
|
142
|
+
const bg = getActiveBackgroundWorkCount();
|
|
143
|
+
const next = wsCallbackActive || bg > 0;
|
|
144
|
+
activeMessageProcessing = next;
|
|
102
145
|
lastSocketAvailableTime = Date.now();
|
|
103
|
-
|
|
146
|
+
if (next) {
|
|
147
|
+
ensureProcessingKeepAliveTimer();
|
|
148
|
+
logger.debug(`📝 处理活跃 reason=${reason} ws=${wsCallbackActive} bg=${bg}`);
|
|
149
|
+
} else {
|
|
150
|
+
clearProcessingKeepAliveTimer();
|
|
151
|
+
logger.debug(`✅ 处理空闲 reason=${reason}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function markMessageProcessingStart() {
|
|
155
|
+
wsCallbackActive = true;
|
|
156
|
+
refreshProcessingActive("ws-start");
|
|
104
157
|
}
|
|
158
|
+
function markMessageProcessingEnd() {
|
|
159
|
+
wsCallbackActive = false;
|
|
160
|
+
refreshProcessingActive("ws-end");
|
|
161
|
+
}
|
|
162
|
+
unsubscribeBackgroundWork = onBackgroundWorkCountChange(() => {
|
|
163
|
+
refreshProcessingActive("bg-count");
|
|
164
|
+
});
|
|
105
165
|
/** 计算指数退避延迟(带抖动) */
|
|
106
166
|
function calculateBackoffDelay(attempt) {
|
|
107
167
|
const exponentialDelay = BASE_BACKOFF_DELAY * Math.pow(2, attempt);
|
|
108
168
|
const jitter = Math.random() * 1e3;
|
|
109
169
|
return Math.min(exponentialDelay + jitter, MAX_BACKOFF_DELAY);
|
|
110
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* 等待钉钉 Stream 真正可收消息。
|
|
173
|
+
*
|
|
174
|
+
* dingtalk-stream 的 client.connect() 在创建 WebSocket 后立刻 resolve,
|
|
175
|
+
* **不等** socket open,更不等服务端 SYSTEM/REGISTERED。
|
|
176
|
+
*
|
|
177
|
+
* 顺序:socket OPEN → SYSTEM topic=REGISTERED(client.registered=true)
|
|
178
|
+
* 任一步失败均 throw,禁止 OPEN-only 假 ready。
|
|
179
|
+
*/
|
|
180
|
+
async function waitForStreamReady(timeoutMs = STREAM_READY_TIMEOUT_MS) {
|
|
181
|
+
const started = Date.now();
|
|
182
|
+
if (client.socket?.readyState !== 1) {
|
|
183
|
+
if (!await new Promise((resolve) => {
|
|
184
|
+
const socket = client.socket;
|
|
185
|
+
if (!socket) {
|
|
186
|
+
resolve(false);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (socket.readyState === 1) {
|
|
190
|
+
resolve(true);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const remain = Math.max(1e3, timeoutMs - (Date.now() - started));
|
|
194
|
+
let settled = false;
|
|
195
|
+
const finish = (ok) => {
|
|
196
|
+
if (settled) return;
|
|
197
|
+
settled = true;
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
socket.removeListener?.("open", onOpen);
|
|
200
|
+
socket.removeListener?.("error", onError);
|
|
201
|
+
resolve(ok);
|
|
202
|
+
};
|
|
203
|
+
const timer = setTimeout(() => finish(false), remain);
|
|
204
|
+
const onOpen = () => finish(true);
|
|
205
|
+
const onError = () => finish(false);
|
|
206
|
+
socket.once("open", onOpen);
|
|
207
|
+
socket.once("error", onError);
|
|
208
|
+
})) throw new Error(`WebSocket OPEN 超时(${timeoutMs}ms)`);
|
|
209
|
+
}
|
|
210
|
+
if (client.registered === true) {
|
|
211
|
+
logger.info(`✅ Stream 已就绪(OPEN + REGISTERED),耗时 ${Date.now() - started}ms`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (await new Promise((resolve) => {
|
|
215
|
+
const socket = client.socket;
|
|
216
|
+
if (!socket) {
|
|
217
|
+
resolve(false);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const remain = Math.max(1e3, timeoutMs - (Date.now() - started));
|
|
221
|
+
let settled = false;
|
|
222
|
+
const finish = (ok) => {
|
|
223
|
+
if (settled) return;
|
|
224
|
+
settled = true;
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
clearInterval(poll);
|
|
227
|
+
socket.removeListener?.("message", onMessage);
|
|
228
|
+
resolve(ok);
|
|
229
|
+
};
|
|
230
|
+
const timer = setTimeout(() => finish(false), remain);
|
|
231
|
+
const onMessage = (data) => {
|
|
232
|
+
try {
|
|
233
|
+
const raw = typeof data === "string" ? data : data?.toString?.() ?? String(data);
|
|
234
|
+
const msg = JSON.parse(raw);
|
|
235
|
+
if (msg?.type === "SYSTEM" && msg?.headers?.topic === "REGISTERED") {
|
|
236
|
+
finish(true);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
} catch {}
|
|
240
|
+
if (client.registered === true) finish(true);
|
|
241
|
+
};
|
|
242
|
+
const poll = setInterval(() => {
|
|
243
|
+
if (client.registered === true) finish(true);
|
|
244
|
+
}, 100);
|
|
245
|
+
socket.on("message", onMessage);
|
|
246
|
+
}) || client.registered === true) {
|
|
247
|
+
logger.info(`✅ Stream 已就绪(OPEN + REGISTERED),耗时 ${Date.now() - started}ms`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
throw new Error(`SYSTEM/REGISTERED 超时(${timeoutMs}ms):socket 已 OPEN 但订阅未生效,禁止报 connected`);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* connect 之后、wait ready 之前挂上 pong/message/close。
|
|
254
|
+
* 官方 #566:若在 connect 前 setup,socket 为 undefined,listener 静默 no-op →
|
|
255
|
+
* pong 无人接 → lastSocketAvailableTime 不刷新 → TIMEOUT 幽灵重连。
|
|
256
|
+
* 必须在 wait OPEN 之前挂好,否则等待窗内的 pong 也会丢。
|
|
257
|
+
*/
|
|
258
|
+
function attachSocketLifecycleListeners() {
|
|
259
|
+
setupPongListener();
|
|
260
|
+
setupMessageListener();
|
|
261
|
+
setupCloseListener();
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* 单次:connect + 挂 listener + 等 OPEN/REGISTERED。
|
|
265
|
+
* 失败 throw,不报 connected。
|
|
266
|
+
*/
|
|
267
|
+
async function connectAndWaitRegistered() {
|
|
268
|
+
await client.connect();
|
|
269
|
+
attachSocketLifecycleListeners();
|
|
270
|
+
await waitForStreamReady(STREAM_READY_TIMEOUT_MS);
|
|
271
|
+
if (client.registered !== true) throw new Error("connect 后 client.registered 仍为 false");
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* 直到 REGISTERED 成功或次数用尽。
|
|
275
|
+
* 用于初次启动与硬重连:绝不在未订阅时对外 connected=true。
|
|
276
|
+
*/
|
|
277
|
+
async function ensureRegisteredConnection(maxAttempts = REGISTERED_CONNECT_MAX_ATTEMPTS) {
|
|
278
|
+
let lastError;
|
|
279
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
280
|
+
if (isStopped) throw new Error("连接已停止,中止 REGISTERED 等待");
|
|
281
|
+
try {
|
|
282
|
+
if (attempt > 1) {
|
|
283
|
+
logger.warn(`🔄 REGISTERED 未就绪,强制重连 ${attempt}/${maxAttempts}…`);
|
|
284
|
+
try {
|
|
285
|
+
if (client.socket) await client.disconnect();
|
|
286
|
+
} catch (discErr) {
|
|
287
|
+
logger.debug(`断开旧连接: ${discErr?.message || discErr}`);
|
|
288
|
+
}
|
|
289
|
+
const delay = Math.min(1e3 * attempt, 5e3);
|
|
290
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
291
|
+
} else logger.info(`⏳ 建立 Stream 并等待 REGISTERED(单次超时 ${STREAM_READY_TIMEOUT_MS}ms,最多 ${maxAttempts} 次)…`);
|
|
292
|
+
await connectAndWaitRegistered();
|
|
293
|
+
noteSocketAlive("registered-ok");
|
|
294
|
+
connectionEstablishedTime = Date.now();
|
|
295
|
+
logger.info(`✅ 订阅已生效 registered=true(attempt ${attempt}/${maxAttempts}, socket=${client.socket?.readyState})`);
|
|
296
|
+
return;
|
|
297
|
+
} catch (err) {
|
|
298
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
299
|
+
logger.warn(`⚠️ Stream 就绪失败 attempt=${attempt}/${maxAttempts}: ${lastError.message}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
throw new Error(`钉钉 Stream 在 ${maxAttempts} 次尝试后仍未 REGISTERED,拒绝假 connected。 最后错误: ${lastError?.message || "unknown"}`);
|
|
303
|
+
}
|
|
111
304
|
/** 统一重连函数,带指数退避(无限重连) */
|
|
112
305
|
async function doReconnect(immediate = false) {
|
|
113
306
|
if (isReconnecting || isStopped) {
|
|
@@ -125,45 +318,19 @@ async function monitorSingleAccount(opts) {
|
|
|
125
318
|
await client.disconnect();
|
|
126
319
|
logger.info(`已断开旧连接`);
|
|
127
320
|
}
|
|
128
|
-
await
|
|
129
|
-
|
|
130
|
-
const timeout = setTimeout(() => {
|
|
131
|
-
resolve(false);
|
|
132
|
-
}, 1e4);
|
|
133
|
-
if (client.socket?.readyState === 1) {
|
|
134
|
-
clearTimeout(timeout);
|
|
135
|
-
resolve(true);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
const onOpen = () => {
|
|
139
|
-
clearTimeout(timeout);
|
|
140
|
-
client.socket?.removeListener("open", onOpen);
|
|
141
|
-
client.socket?.removeListener("error", onError);
|
|
142
|
-
resolve(true);
|
|
143
|
-
};
|
|
144
|
-
const onError = (err) => {
|
|
145
|
-
clearTimeout(timeout);
|
|
146
|
-
client.socket?.removeListener("open", onOpen);
|
|
147
|
-
client.socket?.removeListener("error", onError);
|
|
148
|
-
logger.warn(`连接建立失败: ${err.message}`);
|
|
149
|
-
resolve(false);
|
|
150
|
-
};
|
|
151
|
-
client.socket?.once("open", onOpen);
|
|
152
|
-
client.socket?.once("error", onError);
|
|
153
|
-
})) throw new Error(`连接建立超时或失败`);
|
|
154
|
-
lastSocketAvailableTime = Date.now();
|
|
321
|
+
await ensureRegisteredConnection(REGISTERED_CONNECT_MAX_ATTEMPTS);
|
|
322
|
+
noteSocketAlive("reconnect-ok");
|
|
155
323
|
connectionEstablishedTime = Date.now();
|
|
156
324
|
reconnectAttempts = 0;
|
|
325
|
+
lastHardReconnectAt = Date.now();
|
|
157
326
|
onStatusChange?.({
|
|
158
327
|
connected: true,
|
|
159
328
|
lastConnectedAt: Date.now()
|
|
160
329
|
});
|
|
161
|
-
|
|
162
|
-
setupMessageListener();
|
|
163
|
-
setupCloseListener();
|
|
164
|
-
logger.info(`✅ 重连成功 (socket 状态=${client.socket?.readyState})`);
|
|
330
|
+
logger.info(`✅ 重连成功 (socket=${client.socket?.readyState}, registered=${Boolean(client.registered)})`);
|
|
165
331
|
} catch (err) {
|
|
166
332
|
reconnectAttempts++;
|
|
333
|
+
onStatusChange?.({ connected: false });
|
|
167
334
|
logger.error(`重连失败:${err.message} (尝试 ${reconnectAttempts})`);
|
|
168
335
|
throw err;
|
|
169
336
|
} finally {
|
|
@@ -174,9 +341,15 @@ async function monitorSingleAccount(opts) {
|
|
|
174
341
|
function setupPongListener() {
|
|
175
342
|
client.socket?.on("pong", () => {
|
|
176
343
|
lastSocketAvailableTime = Date.now();
|
|
344
|
+
consecutiveStaleMisses = 0;
|
|
177
345
|
logger.debug(`收到 PONG 响应`);
|
|
178
346
|
});
|
|
179
347
|
}
|
|
348
|
+
function noteSocketAlive(reason) {
|
|
349
|
+
lastSocketAvailableTime = Date.now();
|
|
350
|
+
consecutiveStaleMisses = 0;
|
|
351
|
+
logger.debug(`socket alive reason=${reason}`);
|
|
352
|
+
}
|
|
180
353
|
/** 监听 WebSocket message 事件,收到 disconnect 消息时立即触发重连 */
|
|
181
354
|
function setupMessageListener() {
|
|
182
355
|
client.socket?.on("message", (data) => {
|
|
@@ -204,12 +377,41 @@ async function monitorSingleAccount(opts) {
|
|
|
204
377
|
});
|
|
205
378
|
}
|
|
206
379
|
/**
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
* -
|
|
211
|
-
|
|
212
|
-
|
|
380
|
+
* 请求硬重连(拆 socket)。
|
|
381
|
+
* - socket 仍 OPEN:先补 PING,连续 misses 未达阈值则绝不拆连接(防空窗吞消息)
|
|
382
|
+
* - socket 已死:立即重连
|
|
383
|
+
* - 冷却:非死连接时距上次硬重连 < MIN_RECONNECT_GAP_MS 则跳过
|
|
384
|
+
*/
|
|
385
|
+
async function requestHardReconnect(reason, socketDead = false) {
|
|
386
|
+
if (isReconnecting || isStopped) return;
|
|
387
|
+
const socketState = client.socket?.readyState;
|
|
388
|
+
if (!socketDead && socketState === 1) {
|
|
389
|
+
try {
|
|
390
|
+
client.socket?.ping();
|
|
391
|
+
logger.warn(`⚠️ 软超时但 socket 仍 OPEN (reason=${reason}, misses=${consecutiveStaleMisses}/${HARD_RECONNECT_AFTER_MISSES}),仅补 PING`);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
logger.warn(`补 PING 失败: ${err.message}`);
|
|
394
|
+
consecutiveStaleMisses += 1;
|
|
395
|
+
}
|
|
396
|
+
if (consecutiveStaleMisses < HARD_RECONNECT_AFTER_MISSES) return;
|
|
397
|
+
}
|
|
398
|
+
if (!socketDead) {
|
|
399
|
+
const gap = Date.now() - lastHardReconnectAt;
|
|
400
|
+
if (lastHardReconnectAt > 0 && gap < MIN_RECONNECT_GAP_MS) {
|
|
401
|
+
logger.warn(`⚠️ 跳过硬重连:距上次仅 ${Math.round(gap / 1e3)}s < ${MIN_RECONNECT_GAP_MS / 1e3}s 冷却 (reason=${reason})`);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
lastHardReconnectAt = Date.now();
|
|
406
|
+
consecutiveStaleMisses = 0;
|
|
407
|
+
logger.info(`🔄 硬重连 reason=${reason} socket=${socketState}`);
|
|
408
|
+
await doReconnect(socketDead);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* 启动 keepAlive:
|
|
412
|
+
* - 10s 发 ping;pong 刷新 lastSocketAvailableTime
|
|
413
|
+
* - 软超时只计数 + 补 ping,达到 HARD_RECONNECT_AFTER_MISSES 且 socket 异常才拆连接
|
|
414
|
+
* - 处理中任务期间绝不硬重连
|
|
213
415
|
*/
|
|
214
416
|
function startKeepAlive() {
|
|
215
417
|
logger.debug(`🚀 启动 keepAlive 定时器,间隔=${HEARTBEAT_INTERVAL / 1e3}秒`);
|
|
@@ -220,28 +422,40 @@ async function monitorSingleAccount(opts) {
|
|
|
220
422
|
}
|
|
221
423
|
try {
|
|
222
424
|
const elapsed = Date.now() - lastSocketAvailableTime;
|
|
223
|
-
if (elapsed > TIMEOUT_THRESHOLD) {
|
|
224
|
-
logger.info(`⚠️ 超时检测:已 ${Math.round(elapsed / 1e3)} 秒未确认 socket 可用,触发重连...`);
|
|
225
|
-
await doReconnect();
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
425
|
const socketState = client.socket?.readyState;
|
|
229
426
|
const timeSinceConnection = Date.now() - connectionEstablishedTime;
|
|
230
|
-
logger.debug(`心跳检测:socket
|
|
427
|
+
logger.debug(`心跳检测:socket=${socketState}, elapsed=${Math.round(elapsed / 1e3)}s, misses=${consecutiveStaleMisses}, processing=${activeMessageProcessing}, connectedFor=${Math.round(timeSinceConnection / 1e3)}s`);
|
|
231
428
|
if (socketState !== 1) {
|
|
232
429
|
if (timeSinceConnection < 15e3) {
|
|
233
430
|
logger.debug(`⏳ 连接建立中(已 ${Math.round(timeSinceConnection / 1e3)}s),跳过状态检查`);
|
|
234
431
|
return;
|
|
235
432
|
}
|
|
236
|
-
|
|
237
|
-
|
|
433
|
+
if (activeMessageProcessing) {
|
|
434
|
+
logger.warn(`⚠️ socket 非 OPEN 但消息处理中,暂缓硬重连 state=${socketState}`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
await requestHardReconnect(`socket-dead state=${socketState}`, true);
|
|
238
438
|
return;
|
|
239
439
|
}
|
|
440
|
+
if (elapsed > SOFT_STALE_MS) {
|
|
441
|
+
consecutiveStaleMisses += 1;
|
|
442
|
+
if (activeMessageProcessing) {
|
|
443
|
+
noteSocketAlive("processing-hold");
|
|
444
|
+
logger.warn(`⚠️ 软超时但处理中,刷新活跃时间不重连 (elapsed=${Math.round(elapsed / 1e3)}s)`);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
logger.warn(`⚠️ 软超时 misses=${consecutiveStaleMisses}/${HARD_RECONNECT_AFTER_MISSES} elapsed=${Math.round(elapsed / 1e3)}s socket=OPEN`);
|
|
448
|
+
await requestHardReconnect("stale-pong", false);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
consecutiveStaleMisses = 0;
|
|
240
452
|
try {
|
|
241
453
|
client.socket?.ping();
|
|
242
454
|
logger.debug(`💓 发送 PING 心跳成功`);
|
|
243
455
|
} catch (err) {
|
|
244
456
|
logger.warn(`发送 PING 失败:${err.message}`);
|
|
457
|
+
consecutiveStaleMisses += 1;
|
|
458
|
+
if (consecutiveStaleMisses >= HARD_RECONNECT_AFTER_MISSES) await requestHardReconnect("ping-failed", true);
|
|
245
459
|
}
|
|
246
460
|
} catch (err) {
|
|
247
461
|
logger.error(`keepAlive 检测失败:${err.message}`);
|
|
@@ -259,10 +473,11 @@ async function monitorSingleAccount(opts) {
|
|
|
259
473
|
isStopped = true;
|
|
260
474
|
if (keepAliveTimer) clearInterval(keepAliveTimer);
|
|
261
475
|
keepAliveTimer = null;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
476
|
+
clearProcessingKeepAliveTimer();
|
|
477
|
+
wsCallbackActive = false;
|
|
478
|
+
activeMessageProcessing = false;
|
|
479
|
+
unsubscribeBackgroundWork?.();
|
|
480
|
+
unsubscribeBackgroundWork = null;
|
|
266
481
|
if (client.socket) client.socket.removeAllListeners();
|
|
267
482
|
logger.debug(`Connection 已停止`);
|
|
268
483
|
}
|
|
@@ -290,6 +505,7 @@ async function monitorSingleAccount(opts) {
|
|
|
290
505
|
client.registerCallbackListener(TOPIC_ROBOT, async (res) => {
|
|
291
506
|
receivedCount++;
|
|
292
507
|
lastMessageTime = Date.now();
|
|
508
|
+
noteSocketAlive("inbound-callback");
|
|
293
509
|
onStatusChange?.({ lastInboundAt: Date.now() });
|
|
294
510
|
const messageId = res.headers?.messageId;
|
|
295
511
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -298,16 +514,17 @@ async function monitorSingleAccount(opts) {
|
|
|
298
514
|
logger.info(`MessageId: ${messageId || "N/A"}`);
|
|
299
515
|
logger.info(`Headers: ${JSON.stringify(res.headers || {})}`);
|
|
300
516
|
logger.info(`Data 长度:${res.data?.length || 0} 字符`);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
517
|
+
let acked = false;
|
|
518
|
+
const ackMessage = (why) => {
|
|
519
|
+
if (acked || !messageId) return;
|
|
520
|
+
try {
|
|
521
|
+
client.socketCallBackResponse(messageId, { success: true });
|
|
522
|
+
acked = true;
|
|
523
|
+
logger.info(`✅ 已确认回调 messageId=${messageId} (${why})`);
|
|
524
|
+
} catch (ackErr) {
|
|
525
|
+
logger.warn(`确认回调失败: ${ackErr?.message || ackErr}`);
|
|
526
|
+
}
|
|
527
|
+
};
|
|
311
528
|
markMessageProcessingStart();
|
|
312
529
|
try {
|
|
313
530
|
let data;
|
|
@@ -330,7 +547,14 @@ async function monitorSingleAccount(opts) {
|
|
|
330
547
|
logger.info(`SessionWebhook: ${data.sessionWebhook ? "已提供" : "未提供"}`);
|
|
331
548
|
logger.info(`RobotCode: ${data.robotCode || account.config?.clientId || "N/A"}`);
|
|
332
549
|
if (data.chatbotUserId || data.chatbotCorpId) console.log(`[DingTalk:${accountId}] [BotIdentity] accountId=${accountId} chatbotUserId=${data.chatbotUserId || "N/A"} chatbotCorpId=${data.chatbotCorpId || "N/A"}`);
|
|
333
|
-
data.msgId;
|
|
550
|
+
const businessMsgId = data.msgId;
|
|
551
|
+
if (checkAndMarkDingtalkMessage(accountId, messageId, businessMsgId)) {
|
|
552
|
+
processedCount++;
|
|
553
|
+
ackMessage("duplicate");
|
|
554
|
+
logger.warn(`⚠️ 检测到重复消息,跳过:protocol=${messageId || "-"} business=${businessMsgId || "-"} (${processedCount}/${receivedCount})`);
|
|
555
|
+
logger.info(`========== 消息处理结束(重复) ==========\n`);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
334
558
|
let contentPreview = "N/A";
|
|
335
559
|
if (data.text?.content) contentPreview = data.text.content.length > 100 ? data.text.content.substring(0, 100) + "..." : data.text.content;
|
|
336
560
|
else if (data.content) contentPreview = JSON.stringify(data.content).substring(0, 100) + "...";
|
|
@@ -347,6 +571,8 @@ async function monitorSingleAccount(opts) {
|
|
|
347
571
|
log,
|
|
348
572
|
cfg: clawdbotConfig
|
|
349
573
|
});
|
|
574
|
+
ackMessage("enqueued");
|
|
575
|
+
noteSocketAlive("after-enqueue");
|
|
350
576
|
processedCount++;
|
|
351
577
|
logger.info(`✅ 消息处理完成 (${processedCount}/${receivedCount})`);
|
|
352
578
|
logger.info(`========== 消息处理结束(成功) ==========\n`);
|
|
@@ -356,6 +582,7 @@ async function monitorSingleAccount(opts) {
|
|
|
356
582
|
const errorStack = error?.stack || "无堆栈信息";
|
|
357
583
|
logger.error(errorMsg);
|
|
358
584
|
logger.error(`错误堆栈:\n${errorStack}`);
|
|
585
|
+
if (!acked) logger.warn(`⚠️ 未 ACK messageId=${messageId || "N/A"},等待钉钉重投(避免网关永久丢消息)`);
|
|
359
586
|
logger.info(`========== 消息处理结束(失败) ==========\n`);
|
|
360
587
|
} finally {
|
|
361
588
|
markMessageProcessingEnd();
|
|
@@ -366,13 +593,13 @@ async function monitorSingleAccount(opts) {
|
|
|
366
593
|
stop();
|
|
367
594
|
};
|
|
368
595
|
try {
|
|
369
|
-
await
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
setupCloseListener();
|
|
596
|
+
await ensureRegisteredConnection(REGISTERED_CONNECT_MAX_ATTEMPTS);
|
|
597
|
+
noteSocketAlive("initial-ready");
|
|
598
|
+
connectionEstablishedTime = Date.now();
|
|
373
599
|
logger.info(`Connected to DingTalk Stream successfully`);
|
|
374
600
|
logger.info(`PID: ${process.pid}`);
|
|
375
|
-
logger.info(`✅
|
|
601
|
+
logger.info(`✅ keepAlive: 心跳 ${HEARTBEAT_INTERVAL / 1e3}s / 软超时 ${SOFT_STALE_MS / 1e3}s×${HARD_RECONNECT_AFTER_MISSES} / 处理中刷新 ${MESSAGE_PROCESSING_KEEPALIVE_MS / 1e3}s / 重连冷却 ${MIN_RECONNECT_GAP_MS / 1e3}s, registered=${Boolean(client.registered)}`);
|
|
602
|
+
printConnectionNoticeOnce();
|
|
376
603
|
onStatusChange?.({
|
|
377
604
|
connected: true,
|
|
378
605
|
lastConnectedAt: Date.now()
|
package/dist/entry-bundled.mjs
CHANGED
|
@@ -23,7 +23,7 @@ var entry_bundled_default = defineBundledChannelEntry({
|
|
|
23
23
|
exportName: "setDingtalkRuntime"
|
|
24
24
|
},
|
|
25
25
|
async registerFull(api) {
|
|
26
|
-
const { registerGatewayMethods } = await import("./gateway-methods-
|
|
26
|
+
const { registerGatewayMethods } = await import("./gateway-methods-BkI1gKaC.mjs");
|
|
27
27
|
registerGatewayMethods(api);
|
|
28
28
|
}
|
|
29
29
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as resolveDingtalkAccount, t as listDingtalkAccountIds } from "./accounts-BAzdqkAV.mjs";
|
|
2
2
|
import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
|
|
3
3
|
import { r as getAccessToken, t as DINGTALK_API } from "./utils-CIfI_3Jh.mjs";
|
|
4
|
-
import { c as prepareMultiBotMentions, d as finishAICard, i as sendProactive, s as buildBotMentionTable } from "./messaging-
|
|
4
|
+
import { c as prepareMultiBotMentions, d as finishAICard, i as sendProactive, s as buildBotMentionTable } from "./messaging-DVS3UKOp.mjs";
|
|
5
5
|
import { d as getUnionId, h as recallEmotionReply } from "./utils-legacy-DQ3HeGHh.mjs";
|
|
6
6
|
//#region src/docs.ts
|
|
7
7
|
var DingtalkDocsClient = class {
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as initDingtalkPluginConfigSchema, i as dingtalkPlugin, n as setDingtalkRuntime } from "./runtime-
|
|
2
|
-
import { t as registerGatewayMethods } from "./gateway-methods
|
|
1
|
+
import { a as initDingtalkPluginConfigSchema, i as dingtalkPlugin, n as setDingtalkRuntime } from "./runtime-A6G7WAWn.mjs";
|
|
2
|
+
import { t as registerGatewayMethods } from "./gateway-methods-kNX3qvqw.mjs";
|
|
3
3
|
//#region index.ts
|
|
4
4
|
/**
|
|
5
5
|
* 检测同一 plugin id 在多个路径被加载的情况。
|