@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.
Files changed (29) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.en.md +28 -35
  3. package/README.md +28 -43
  4. package/dist/background-work-Cxb4b-91.mjs +42 -0
  5. package/dist/card-content-cache-c4Ls96eJ.mjs +2 -0
  6. package/dist/chunk-upload-PwhogwfE.mjs +2 -0
  7. package/dist/{common-DE0gd0Jg.mjs → common-BZiVgd3A.mjs} +2 -2
  8. package/dist/{common-B_A78n8u.mjs → common-DgL28gCZ.mjs} +1 -1
  9. package/dist/{connection-fc6B4z4G.mjs → connection-CZ7ZoN5_.mjs} +313 -86
  10. package/dist/entry-bundled.mjs +1 -1
  11. package/dist/gateway-methods-BkI1gKaC.mjs +2 -0
  12. package/dist/{gateway-methods--aKsyMs-.mjs → gateway-methods-kNX3qvqw.mjs} +1 -1
  13. package/dist/index.mjs +2 -2
  14. package/dist/{message-handler-DQZiKx3i.mjs → message-handler-B1usz1wb.mjs} +238 -122
  15. package/dist/{messaging-WleKf54h.mjs → messaging-DVS3UKOp.mjs} +6 -2
  16. package/dist/{runtime-DzZo6Gvk.mjs → runtime-A6G7WAWn.mjs} +3 -3
  17. package/docs/RELEASE_NOTES_V0.8.28.md +56 -0
  18. package/package.json +2 -2
  19. package/src/core/connection.ts +465 -158
  20. package/src/core/message-handler.ts +173 -86
  21. package/src/reply-dispatcher.ts +234 -77
  22. package/src/services/messaging/card.ts +2 -0
  23. package/src/utils/background-work.ts +63 -0
  24. package/dist/card-content-cache-Dwd8HICP.mjs +0 -2
  25. package/dist/chunk-upload-DaLXXZH3.mjs +0 -2
  26. package/dist/gateway-methods-BQd4E0Sk.mjs +0 -2
  27. /package/dist/{card-content-cache-CYUzCaQJ.mjs → card-content-cache-bDQFz2u8.mjs} +0 -0
  28. /package/dist/{chunk-upload-BBQgGtcZ.mjs → chunk-upload-BE71drPg.mjs} +0 -0
  29. /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 秒间隔,90 秒超时)
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
- const TIMEOUT_THRESHOLD = 20 * 1e3;
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
- * 在消息处理期间,每 30 秒更新一次 lastSocketAvailableTime
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
- }, 30 * 1e3);
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
- logger.debug(`✅ 消息处理结束,清理活跃标记定时器`);
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 client.connect();
129
- if (!await new Promise((resolve) => {
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
- setupPongListener();
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
- * 启动 keepAlive 机制(单定时器 + 指数退避)
208
- *
209
- * 业界最佳实践:
210
- * - 单定时器:每 10 秒检查一次,同时完成心跳和超时检测
211
- * - 使用 WebSocket 原生 Ping
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 状态=${socketState}, elapsed=${Math.round(elapsed / 1e3)}s, 连接已建立=${Math.round(timeSinceConnection / 1e3)}s`);
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
- logger.info(`⚠️ 心跳检测:socket 状态=${socketState},触发重连...`);
237
- await doReconnect(true);
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
- if (messageProcessingKeepAliveTimer) {
263
- clearInterval(messageProcessingKeepAliveTimer);
264
- messageProcessingKeepAliveTimer = null;
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
- if (messageId) {
302
- client.socketCallBackResponse(messageId, { success: true });
303
- logger.info(`✅ 已立即确认回调:messageId=${messageId}`);
304
- } else logger.warn(`⚠️ 警告:消息没有 messageId`);
305
- if (messageId && checkAndMarkDingtalkMessage(accountId, messageId, void 0)) {
306
- processedCount++;
307
- logger.warn(`⚠️ 检测到重复消息(协议层),跳过处理:messageId=${messageId} (${processedCount}/${receivedCount})`);
308
- logger.info(`========== 消息处理结束(重复) ==========\n`);
309
- return;
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 client.connect();
370
- setupPongListener();
371
- setupMessageListener();
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(`✅ 自定义 keepAlive: true (10 秒心跳,90 秒超时), 指数退避重连`);
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()
@@ -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-BQd4E0Sk.mjs");
26
+ const { registerGatewayMethods } = await import("./gateway-methods-BkI1gKaC.mjs");
27
27
  registerGatewayMethods(api);
28
28
  }
29
29
  });
@@ -0,0 +1,2 @@
1
+ import { t as registerGatewayMethods } from "./gateway-methods-kNX3qvqw.mjs";
2
+ export { registerGatewayMethods };
@@ -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-WleKf54h.mjs";
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-DzZo6Gvk.mjs";
2
- import { t as registerGatewayMethods } from "./gateway-methods--aKsyMs-.mjs";
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 在多个路径被加载的情况。