@wu529778790/open-im 1.8.1-beta.10 → 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.
- package/dist/config-web.js +87 -0
- package/dist/wechat/client.js +42 -4
- package/dist/wework/client.js +32 -6
- package/package.json +1 -1
package/dist/config-web.js
CHANGED
|
@@ -117,6 +117,7 @@ export function getHealthPlatformSnapshot(file, env = process.env) {
|
|
|
117
117
|
const fileQQ = file.platforms?.qq;
|
|
118
118
|
const fileWework = file.platforms?.wework;
|
|
119
119
|
const fileDingtalk = file.platforms?.dingtalk;
|
|
120
|
+
const fileWorkbuddy = file.platforms?.workbuddy;
|
|
120
121
|
const telegramBotToken = env.TELEGRAM_BOT_TOKEN ?? fileTelegram?.botToken ?? file.telegramBotToken;
|
|
121
122
|
const feishuAppId = env.FEISHU_APP_ID ?? fileFeishu?.appId ?? file.feishuAppId;
|
|
122
123
|
const feishuAppSecret = env.FEISHU_APP_SECRET ?? fileFeishu?.appSecret ?? file.feishuAppSecret;
|
|
@@ -126,6 +127,9 @@ export function getHealthPlatformSnapshot(file, env = process.env) {
|
|
|
126
127
|
const weworkSecret = env.WEWORK_SECRET ?? fileWework?.secret;
|
|
127
128
|
const dingtalkClientId = env.DINGTALK_CLIENT_ID ?? fileDingtalk?.clientId;
|
|
128
129
|
const dingtalkClientSecret = env.DINGTALK_CLIENT_SECRET ?? fileDingtalk?.clientSecret;
|
|
130
|
+
const workbuddyAccessToken = fileWorkbuddy?.accessToken;
|
|
131
|
+
const workbuddyRefreshToken = fileWorkbuddy?.refreshToken;
|
|
132
|
+
const workbuddyUserId = fileWorkbuddy?.userId;
|
|
129
133
|
return {
|
|
130
134
|
telegram: {
|
|
131
135
|
configured: !!telegramBotToken,
|
|
@@ -157,6 +161,12 @@ export function getHealthPlatformSnapshot(file, env = process.env) {
|
|
|
157
161
|
healthy: !!(dingtalkClientId && dingtalkClientSecret),
|
|
158
162
|
message: dingtalkClientId && dingtalkClientSecret ? "Client ID and Secret configured" : "Missing credentials",
|
|
159
163
|
},
|
|
164
|
+
workbuddy: {
|
|
165
|
+
configured: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId),
|
|
166
|
+
enabled: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId) && fileWorkbuddy?.enabled !== false,
|
|
167
|
+
healthy: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId),
|
|
168
|
+
message: workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId ? "OAuth credentials configured" : "Missing credentials",
|
|
169
|
+
},
|
|
160
170
|
};
|
|
161
171
|
}
|
|
162
172
|
function splitCsv(value) {
|
|
@@ -231,6 +241,15 @@ function buildInitialPayload(file) {
|
|
|
231
241
|
cardTemplateId: file.platforms?.dingtalk?.cardTemplateId ?? "",
|
|
232
242
|
allowedUserIds: (file.platforms?.dingtalk?.allowedUserIds ?? []).join(", "),
|
|
233
243
|
},
|
|
244
|
+
workbuddy: {
|
|
245
|
+
enabled: file.platforms?.workbuddy?.enabled ?? Boolean(file.platforms?.workbuddy?.accessToken && file.platforms?.workbuddy?.refreshToken && file.platforms?.workbuddy?.userId),
|
|
246
|
+
aiCommand: file.platforms?.workbuddy?.aiCommand ?? "",
|
|
247
|
+
accessToken: file.platforms?.workbuddy?.accessToken ?? "",
|
|
248
|
+
refreshToken: file.platforms?.workbuddy?.refreshToken ?? "",
|
|
249
|
+
userId: file.platforms?.workbuddy?.userId ?? "",
|
|
250
|
+
baseUrl: file.platforms?.workbuddy?.baseUrl ?? "",
|
|
251
|
+
allowedUserIds: (file.platforms?.workbuddy?.allowedUserIds ?? []).join(", "),
|
|
252
|
+
},
|
|
234
253
|
},
|
|
235
254
|
ai: {
|
|
236
255
|
aiCommand: file.aiCommand ?? "claude",
|
|
@@ -276,6 +295,12 @@ function validatePayload(payload) {
|
|
|
276
295
|
errors.push("DingTalk client ID is required.");
|
|
277
296
|
if (payload.platforms.dingtalk.enabled && !clean(payload.platforms.dingtalk.clientSecret))
|
|
278
297
|
errors.push("DingTalk client secret is required.");
|
|
298
|
+
if (payload.platforms.workbuddy.enabled && !clean(payload.platforms.workbuddy.accessToken))
|
|
299
|
+
errors.push("WorkBuddy access token is required.");
|
|
300
|
+
if (payload.platforms.workbuddy.enabled && !clean(payload.platforms.workbuddy.refreshToken))
|
|
301
|
+
errors.push("WorkBuddy refresh token is required.");
|
|
302
|
+
if (payload.platforms.workbuddy.enabled && !clean(payload.platforms.workbuddy.userId))
|
|
303
|
+
errors.push("WorkBuddy user ID is required.");
|
|
279
304
|
if (!clean(payload.ai.claudeWorkDir))
|
|
280
305
|
errors.push("Default work directory is required.");
|
|
281
306
|
if (!Number.isFinite(payload.ai.claudeTimeoutMs) || payload.ai.claudeTimeoutMs <= 0)
|
|
@@ -330,6 +355,17 @@ function validateConfigForPlatform(platform, config) {
|
|
|
330
355
|
errors.push("DingTalk client secret is required and must be a non-empty string.");
|
|
331
356
|
}
|
|
332
357
|
break;
|
|
358
|
+
case "workbuddy":
|
|
359
|
+
if (!c.accessToken || typeof c.accessToken !== "string" || !clean(c.accessToken)) {
|
|
360
|
+
errors.push("WorkBuddy access token is required and must be a non-empty string.");
|
|
361
|
+
}
|
|
362
|
+
if (!c.refreshToken || typeof c.refreshToken !== "string" || !clean(c.refreshToken)) {
|
|
363
|
+
errors.push("WorkBuddy refresh token is required and must be a non-empty string.");
|
|
364
|
+
}
|
|
365
|
+
if (!c.userId || typeof c.userId !== "string" || !clean(c.userId)) {
|
|
366
|
+
errors.push("WorkBuddy user ID is required and must be a non-empty string.");
|
|
367
|
+
}
|
|
368
|
+
break;
|
|
333
369
|
default:
|
|
334
370
|
errors.push(`Unknown platform: ${platform}`);
|
|
335
371
|
}
|
|
@@ -455,6 +491,34 @@ async function probeDingTalk(config) {
|
|
|
455
491
|
}
|
|
456
492
|
return "DingTalk credentials are valid.";
|
|
457
493
|
}
|
|
494
|
+
async function probeWorkBuddy(config) {
|
|
495
|
+
const accessToken = clean(String(config.accessToken ?? ""));
|
|
496
|
+
const refreshToken = clean(String(config.refreshToken ?? ""));
|
|
497
|
+
const userId = clean(String(config.userId ?? ""));
|
|
498
|
+
if (!accessToken || !refreshToken || !userId)
|
|
499
|
+
throw new Error("WorkBuddy access token, refresh token, and user ID are required.");
|
|
500
|
+
const baseUrl = clean(String(config.baseUrl ?? "")) || "https://copilot.tencent.com";
|
|
501
|
+
// Validate credentials by attempting to register workspace
|
|
502
|
+
const response = await fetch(`${baseUrl}/api/copilot/workspace/register`, {
|
|
503
|
+
method: "POST",
|
|
504
|
+
headers: {
|
|
505
|
+
"content-type": "application/json",
|
|
506
|
+
"authorization": `Bearer ${accessToken}`,
|
|
507
|
+
},
|
|
508
|
+
body: JSON.stringify({
|
|
509
|
+
userId,
|
|
510
|
+
hostId: "open-im-test",
|
|
511
|
+
workspaceId: "open-im-test-workspace",
|
|
512
|
+
workspaceName: "OpenIM Test Workspace",
|
|
513
|
+
}),
|
|
514
|
+
signal: AbortSignal.timeout(TEST_TIMEOUT_MS),
|
|
515
|
+
});
|
|
516
|
+
if (!response.ok) {
|
|
517
|
+
const body = await response.text();
|
|
518
|
+
throw new Error(`WorkBuddy authentication failed: ${body.slice(0, 200) || `HTTP ${response.status}`}`);
|
|
519
|
+
}
|
|
520
|
+
return "WorkBuddy credentials are valid.";
|
|
521
|
+
}
|
|
458
522
|
export async function testPlatformConfig(platform, config) {
|
|
459
523
|
const errors = validateConfigForPlatform(platform, config);
|
|
460
524
|
if (errors.length > 0) {
|
|
@@ -471,6 +535,8 @@ export async function testPlatformConfig(platform, config) {
|
|
|
471
535
|
return probeWeWork(config);
|
|
472
536
|
case "dingtalk":
|
|
473
537
|
return probeDingTalk(config);
|
|
538
|
+
case "workbuddy":
|
|
539
|
+
return probeWorkBuddy(config);
|
|
474
540
|
default:
|
|
475
541
|
throw new Error(`Unknown platform: ${platform}`);
|
|
476
542
|
}
|
|
@@ -557,6 +623,16 @@ function toFileConfig(payload, existing) {
|
|
|
557
623
|
cardTemplateId: clean(payload.platforms.dingtalk.cardTemplateId),
|
|
558
624
|
allowedUserIds: splitCsv(payload.platforms.dingtalk.allowedUserIds),
|
|
559
625
|
},
|
|
626
|
+
workbuddy: {
|
|
627
|
+
...existing.platforms?.workbuddy,
|
|
628
|
+
enabled: payload.platforms.workbuddy.enabled,
|
|
629
|
+
aiCommand: clean(payload.platforms.workbuddy.aiCommand),
|
|
630
|
+
accessToken: clean(payload.platforms.workbuddy.accessToken),
|
|
631
|
+
refreshToken: clean(payload.platforms.workbuddy.refreshToken),
|
|
632
|
+
userId: clean(payload.platforms.workbuddy.userId),
|
|
633
|
+
baseUrl: clean(payload.platforms.workbuddy.baseUrl),
|
|
634
|
+
allowedUserIds: splitCsv(payload.platforms.workbuddy.allowedUserIds),
|
|
635
|
+
},
|
|
560
636
|
},
|
|
561
637
|
};
|
|
562
638
|
}
|
|
@@ -812,6 +888,7 @@ export async function startWebConfigServer(options) {
|
|
|
812
888
|
const fileQQ = file.platforms?.qq;
|
|
813
889
|
const fileWework = file.platforms?.wework;
|
|
814
890
|
const fileDingtalk = file.platforms?.dingtalk;
|
|
891
|
+
const fileWorkbuddy = file.platforms?.workbuddy;
|
|
815
892
|
const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN ?? fileTelegram?.botToken ?? file.telegramBotToken;
|
|
816
893
|
const feishuAppId = process.env.FEISHU_APP_ID ?? fileFeishu?.appId ?? file.feishuAppId;
|
|
817
894
|
const feishuAppSecret = process.env.FEISHU_APP_SECRET ?? fileFeishu?.appSecret ?? file.feishuAppSecret;
|
|
@@ -821,6 +898,9 @@ export async function startWebConfigServer(options) {
|
|
|
821
898
|
const weworkSecret = process.env.WEWORK_SECRET ?? fileWework?.secret;
|
|
822
899
|
const dingtalkClientId = process.env.DINGTALK_CLIENT_ID ?? fileDingtalk?.clientId;
|
|
823
900
|
const dingtalkClientSecret = process.env.DINGTALK_CLIENT_SECRET ?? fileDingtalk?.clientSecret;
|
|
901
|
+
const workbuddyAccessToken = fileWorkbuddy?.accessToken;
|
|
902
|
+
const workbuddyRefreshToken = fileWorkbuddy?.refreshToken;
|
|
903
|
+
const workbuddyUserId = fileWorkbuddy?.userId;
|
|
824
904
|
const platforms = {};
|
|
825
905
|
// 检查 Telegram
|
|
826
906
|
platforms.telegram = {
|
|
@@ -857,6 +937,13 @@ export async function startWebConfigServer(options) {
|
|
|
857
937
|
healthy: !!(dingtalkClientId && dingtalkClientSecret),
|
|
858
938
|
message: (dingtalkClientId && dingtalkClientSecret) ? "Client ID and Secret configured" : "Missing credentials"
|
|
859
939
|
};
|
|
940
|
+
// 检查 WorkBuddy
|
|
941
|
+
platforms.workbuddy = {
|
|
942
|
+
configured: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId),
|
|
943
|
+
enabled: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId) && fileWorkbuddy?.enabled !== false,
|
|
944
|
+
healthy: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId),
|
|
945
|
+
message: (workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId) ? "OAuth credentials configured" : "Missing credentials"
|
|
946
|
+
};
|
|
860
947
|
json(response, 200, { platforms, serviceStatus: getServiceStatus() });
|
|
861
948
|
return;
|
|
862
949
|
}
|
package/dist/wechat/client.js
CHANGED
|
@@ -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 (
|
|
217
|
-
log.error('Max reconnect attempts reached');
|
|
246
|
+
if (reconnectTimer) {
|
|
218
247
|
return;
|
|
219
248
|
}
|
|
220
|
-
|
|
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
|
}
|
package/dist/wework/client.js
CHANGED
|
@@ -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
|
}
|