@wu529778790/open-im 1.8.1-beta.11 → 1.8.1-beta.12

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.
@@ -8,6 +8,7 @@ import { createLogger } from '../logger.js';
8
8
  const log = createLogger('WeChat');
9
9
  const TOKEN_FILE = 'wechat-token.json';
10
10
  const DEFAULT_WECHAT_WS_URL = 'wss://openclau-wechat.henryxiaoyang.workers.dev';
11
+ const PONG_TIMEOUT_FACTOR = 3; // 3倍心跳间隔无响应则判定连接死亡
11
12
  // Global state
12
13
  let ws = null;
13
14
  let channelState = 'disconnected';
@@ -16,6 +17,8 @@ let heartbeatTimer = null;
16
17
  let reconnectAttempts = 0;
17
18
  let currentToken = null;
18
19
  let tokenStoragePath = null;
20
+ let lastServerResponseTime = 0; // 上次收到服务端消息的时间
21
+ let wsConfigRef = null; // 保存配置供心跳重连使用
19
22
  // Event handlers
20
23
  let messageHandler = null;
21
24
  let stateChangeHandler = null;
@@ -92,6 +95,7 @@ export async function initWeChat(config, eventHandler, onStateChange) {
92
95
  * Connect to AGP WebSocket server
93
96
  */
94
97
  async function connectWebSocket(config) {
98
+ wsConfigRef = config;
95
99
  if (channelState === 'connecting') {
96
100
  log.warn('WebSocket connection already in progress');
97
101
  return;
@@ -108,6 +112,7 @@ async function connectWebSocket(config) {
108
112
  resolve();
109
113
  });
110
114
  ws.on('message', async (data) => {
115
+ lastServerResponseTime = Date.now();
111
116
  try {
112
117
  const envelope = JSON.parse(data.toString());
113
118
  log.debug('Received AGP message:', envelope.method);
@@ -190,11 +195,35 @@ function updateState(state) {
190
195
  }
191
196
  /**
192
197
  * Start heartbeat to keep connection alive
198
+ * 同时检测服务端是否响应,超时无响应则主动断开触发重连
193
199
  */
194
200
  function startHeartbeat(interval) {
195
201
  stopHeartbeat();
202
+ lastServerResponseTime = Date.now();
196
203
  heartbeatTimer = setInterval(() => {
197
204
  if (channelState === 'connected') {
205
+ // 检测连接是否已死:长时间未收到任何服务端响应
206
+ const elapsed = Date.now() - lastServerResponseTime;
207
+ const pongTimeout = interval * PONG_TIMEOUT_FACTOR;
208
+ if (lastServerResponseTime > 0 && elapsed > pongTimeout) {
209
+ log.warn(`No server response for ${Math.round(elapsed / 1000)}s, forcing reconnect`);
210
+ stopHeartbeat();
211
+ if (ws) {
212
+ try {
213
+ ws.removeAllListeners();
214
+ ws.close();
215
+ }
216
+ catch {
217
+ /* ignore */
218
+ }
219
+ ws = null;
220
+ }
221
+ updateState('disconnected');
222
+ if (wsConfigRef) {
223
+ scheduleReconnect(wsConfigRef);
224
+ }
225
+ return;
226
+ }
198
227
  sendAGPMessage('ping', { timestamp: Date.now() });
199
228
  }
200
229
  }, interval);
@@ -210,17 +239,26 @@ function stopHeartbeat() {
210
239
  }
211
240
  /**
212
241
  * Schedule reconnection attempt
242
+ * 超过 maxAttempts 后自动重置计数器继续重试,避免永久断连
213
243
  */
214
244
  function scheduleReconnect(config) {
215
245
  const maxAttempts = config.maxReconnectAttempts ?? 10;
216
- if (reconnectAttempts >= maxAttempts) {
217
- log.error('Max reconnect attempts reached');
246
+ if (reconnectTimer) {
218
247
  return;
219
248
  }
220
- const interval = config.reconnectInterval ?? 5000;
249
+ // 超过最大重试次数后重置计数器,降低频率继续重试
250
+ if (reconnectAttempts >= maxAttempts) {
251
+ log.warn(`Max reconnect attempts (${maxAttempts}) reached, resetting counter and retrying at lower frequency`);
252
+ reconnectAttempts = 0;
253
+ }
254
+ const baseInterval = config.reconnectInterval ?? 5000;
255
+ // 超过一半次数后逐渐增加间隔,最大 60 秒
256
+ const backoff = Math.min(baseInterval * Math.pow(1.5, Math.floor(reconnectAttempts / 3)), 60000);
257
+ const interval = Math.round(backoff);
221
258
  reconnectTimer = setTimeout(async () => {
259
+ reconnectTimer = null;
222
260
  reconnectAttempts++;
223
- log.info(`Reconnecting... Attempt ${reconnectAttempts}/${maxAttempts}`);
261
+ log.info(`Reconnecting... Attempt ${reconnectAttempts}/${maxAttempts} (interval: ${interval}ms)`);
224
262
  try {
225
263
  await connectWebSocket(config);
226
264
  }
@@ -13,6 +13,7 @@ import { createLogger } from '../logger.js';
13
13
  const log = createLogger('WeWork');
14
14
  const DEFAULT_WS_URL = 'wss://openws.work.weixin.qq.com';
15
15
  const HEARTBEAT_INTERVAL = 30000; // 30秒
16
+ const PONG_TIMEOUT = HEARTBEAT_INTERVAL * 3; // 90秒无任何服务端响应则判定连接死亡
16
17
  const MAX_RECONNECT_ATTEMPTS = 100;
17
18
  // Global state
18
19
  let ws = null;
@@ -22,6 +23,7 @@ let heartbeatTimer = null;
22
23
  let reconnectAttempts = 0;
23
24
  let shouldReconnect = false;
24
25
  let isStopping = false;
26
+ let lastServerResponseTime = 0; // 上次收到服务端消息的时间
25
27
  // Event handlers
26
28
  let messageHandler = null;
27
29
  let stateChangeHandler = null;
@@ -173,6 +175,7 @@ async function connectWebSocket() {
173
175
  }
174
176
  });
175
177
  ws.on('message', async (data) => {
178
+ lastServerResponseTime = Date.now();
176
179
  try {
177
180
  const message = JSON.parse(data.toString());
178
181
  await handleMessage(message);
@@ -326,11 +329,30 @@ function updateState(state) {
326
329
  }
327
330
  /**
328
331
  * Start heartbeat to keep connection alive
332
+ * 同时检测服务端是否响应,超时无响应则主动断开触发重连
329
333
  */
330
334
  function startHeartbeat() {
331
335
  stopHeartbeat();
336
+ lastServerResponseTime = Date.now();
332
337
  heartbeatTimer = setInterval(() => {
333
338
  if (connectionState === 'connected' && ws) {
339
+ // 检测连接是否已死:长时间未收到任何服务端响应
340
+ const elapsed = Date.now() - lastServerResponseTime;
341
+ if (lastServerResponseTime > 0 && elapsed > PONG_TIMEOUT) {
342
+ log.warn(`No server response for ${Math.round(elapsed / 1000)}s, forcing reconnect`);
343
+ stopHeartbeat();
344
+ try {
345
+ ws.removeAllListeners();
346
+ ws.close();
347
+ }
348
+ catch {
349
+ /* ignore */
350
+ }
351
+ ws = null;
352
+ updateState('disconnected');
353
+ scheduleReconnect();
354
+ return;
355
+ }
334
356
  const pingMessage = {
335
357
  cmd: "ping" /* WeWorkCommand.PING */,
336
358
  headers: {
@@ -359,23 +381,27 @@ function stopHeartbeat() {
359
381
  }
360
382
  /**
361
383
  * Schedule reconnection attempt
384
+ * 超过 MAX_RECONNECT_ATTEMPTS 后自动重置计数器继续重试,避免永久断连
362
385
  */
363
386
  function scheduleReconnect() {
364
387
  if (isStopping || !shouldReconnect) {
365
388
  return;
366
389
  }
367
- if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
368
- log.error('Max reconnect attempts reached');
369
- return;
370
- }
371
- const interval = 5000; // 5秒后重连
372
390
  if (reconnectTimer) {
373
391
  return;
374
392
  }
393
+ // 超过最大重试次数后重置计数器,降低频率继续重试
394
+ if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
395
+ log.warn(`Max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached, resetting counter and retrying at lower frequency`);
396
+ reconnectAttempts = 0;
397
+ }
398
+ // 逐步增加间隔,5s → 7.5s → 11s → ... 最大 60s
399
+ const backoff = Math.min(5000 * Math.pow(1.5, Math.floor(reconnectAttempts / 5)), 60000);
400
+ const interval = Math.round(backoff);
375
401
  reconnectTimer = setTimeout(async () => {
376
402
  reconnectTimer = null;
377
403
  reconnectAttempts++;
378
- log.info(`Reconnecting... Attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}`);
404
+ log.info(`Reconnecting... Attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS} (interval: ${interval}ms)`);
379
405
  try {
380
406
  await connectWebSocket();
381
407
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.8.1-beta.11",
3
+ "version": "1.8.1-beta.12",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",