@wu529778790/open-im 1.11.8-beta.2 → 1.11.8-beta.21
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/adapters/codebuddy-adapter.js +3 -17
- package/dist/adapters/codex-adapter.js +3 -16
- package/dist/adapters/opencode-adapter.d.ts +0 -5
- package/dist/adapters/opencode-adapter.js +10 -25
- package/dist/adapters/registry.js +10 -1
- package/dist/clawbot/client.js +72 -78
- package/dist/clawbot/qr-login.d.ts +1 -0
- package/dist/clawbot/qr-login.js +7 -0
- package/dist/codebuddy/cli-runner.d.ts +2 -22
- package/dist/codebuddy/cli-runner.js +9 -27
- package/dist/codex/cli-runner.d.ts +2 -22
- package/dist/codex/cli-runner.js +24 -75
- package/dist/commands/handler.d.ts +1 -0
- package/dist/commands/handler.js +34 -0
- package/dist/config/types.d.ts +23 -0
- package/dist/config-web-auth.d.ts +13 -0
- package/dist/config-web-auth.js +114 -0
- package/dist/config-web-browser.d.ts +4 -0
- package/dist/config-web-browser.js +48 -0
- package/dist/config-web-cors.d.ts +5 -0
- package/dist/config-web-cors.js +48 -0
- package/dist/config-web-health.d.ts +7 -0
- package/dist/config-web-health.js +65 -0
- package/dist/config-web-http.d.ts +3 -0
- package/dist/config-web-http.js +31 -0
- package/dist/config-web-page-i18n.d.ts +36 -2
- package/dist/config-web-page-i18n.js +36 -2
- package/dist/config-web-payload.d.ts +84 -0
- package/dist/config-web-payload.js +260 -0
- package/dist/config-web-probes.d.ts +2 -0
- package/dist/config-web-probes.js +271 -0
- package/dist/config-web.d.ts +3 -11
- package/dist/config-web.js +45 -815
- package/dist/config.js +22 -1
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +11 -0
- package/dist/opencode/cli-runner.d.ts +2 -22
- package/dist/opencode/cli-runner.js +47 -68
- package/dist/opencode/sdk-manager.d.ts +11 -0
- package/dist/opencode/sdk-manager.js +46 -0
- package/dist/opencode/sdk-runner.d.ts +7 -0
- package/dist/opencode/sdk-runner.js +225 -0
- package/dist/platform/handle-ai-request.js +18 -1
- package/dist/qq/client.js +45 -53
- package/dist/shared/ai-task.d.ts +15 -0
- package/dist/shared/ai-task.js +174 -2
- package/dist/shared/cli-runner-base.d.ts +39 -0
- package/dist/shared/cli-runner-base.js +108 -0
- package/dist/shared/connection-manager.d.ts +111 -0
- package/dist/shared/connection-manager.js +157 -0
- package/dist/shared/session-invalid-detector.d.ts +5 -0
- package/dist/shared/session-invalid-detector.js +20 -0
- package/dist/wework/client.js +79 -127
- package/dist/workbuddy/client.js +40 -74
- package/package.json +4 -1
- package/web/dist/assets/index-6e51Wtaa.css +1 -0
- package/web/dist/assets/index-Da9qKWA2.js +57 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-XKVTb-0p.css +0 -1
- package/web/dist/assets/index-yDVGC_84.js +0 -57
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared connection management infrastructure for self-managing platform clients.
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates three cross-cutting concerns duplicated across wework, clawbot,
|
|
5
|
+
* qq, and workbuddy clients:
|
|
6
|
+
*
|
|
7
|
+
* - **ReconnectManager**: jittered backoff scheduling with fatal→slow-probe mode
|
|
8
|
+
* - **HeartbeatMonitor**: interval-based heartbeat / watchdog timer
|
|
9
|
+
* - **StateManager**: state variable with change-notification callback
|
|
10
|
+
*/
|
|
11
|
+
import { createLogger } from '../logger.js';
|
|
12
|
+
import { jitteredDelay, SLOW_PROBE_MS } from './reconnect.js';
|
|
13
|
+
/**
|
|
14
|
+
* Manages reconnection scheduling with jittered backoff and fatal→slow-probe.
|
|
15
|
+
*
|
|
16
|
+
* Replaces the duplicated `scheduleReconnect()` pattern across 4 client files:
|
|
17
|
+
* timer cleanup, attempt counting, backoff computation, jitter, logging.
|
|
18
|
+
*/
|
|
19
|
+
export class ReconnectManager {
|
|
20
|
+
timer = null;
|
|
21
|
+
attempt = 0;
|
|
22
|
+
_fatal = false;
|
|
23
|
+
_stopped = false;
|
|
24
|
+
log;
|
|
25
|
+
cfg;
|
|
26
|
+
constructor(cfg) {
|
|
27
|
+
this.cfg = cfg;
|
|
28
|
+
this.log = createLogger(`${cfg.name}/Reconnect`);
|
|
29
|
+
}
|
|
30
|
+
get attemptCount() { return this.attempt; }
|
|
31
|
+
get fatal() { return this._fatal; }
|
|
32
|
+
get stopped() { return this._stopped; }
|
|
33
|
+
/** Mark a fatal (auth) error — next scheduleReconnect uses slow-probe delay. */
|
|
34
|
+
setFatal(value) { this._fatal = value; }
|
|
35
|
+
/** Reset attempt counter and fatal flag after a successful connection. */
|
|
36
|
+
reset() {
|
|
37
|
+
this.attempt = 0;
|
|
38
|
+
this._fatal = false;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Schedule a reconnect attempt. Clears any pending timer, computes the
|
|
42
|
+
* backoff delay (with jitter), and invokes `onReconnect` after the delay.
|
|
43
|
+
*/
|
|
44
|
+
schedule() {
|
|
45
|
+
this.clearTimer();
|
|
46
|
+
if (this._stopped)
|
|
47
|
+
return;
|
|
48
|
+
const max = this.cfg.maxAttempts;
|
|
49
|
+
if (max && max > 0 && this.attempt >= max) {
|
|
50
|
+
this.log.warn(`Max reconnect attempts (${max}) reached, resetting counter`);
|
|
51
|
+
this.attempt = 0;
|
|
52
|
+
}
|
|
53
|
+
const baseDelay = this.computeBaseDelay();
|
|
54
|
+
const delay = this._fatal ? jitteredDelay(SLOW_PROBE_MS) : jitteredDelay(baseDelay);
|
|
55
|
+
this.attempt++;
|
|
56
|
+
const fatalTag = this._fatal ? ' [slow-probe]' : '';
|
|
57
|
+
this.log.info(`Reconnecting in ${delay}ms (attempt ${this.attempt})${fatalTag}...`);
|
|
58
|
+
this.timer = setTimeout(async () => {
|
|
59
|
+
this.timer = null;
|
|
60
|
+
try {
|
|
61
|
+
await this.cfg.onReconnect();
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
this.log.warn(`Reconnect callback failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
65
|
+
}
|
|
66
|
+
}, delay);
|
|
67
|
+
}
|
|
68
|
+
/** Cancel any pending reconnect timer. */
|
|
69
|
+
stop() {
|
|
70
|
+
this._stopped = true;
|
|
71
|
+
this.clearTimer();
|
|
72
|
+
}
|
|
73
|
+
/** Allow scheduling again (undo `stop`). */
|
|
74
|
+
resume() {
|
|
75
|
+
this._stopped = false;
|
|
76
|
+
}
|
|
77
|
+
clearTimer() {
|
|
78
|
+
if (this.timer) {
|
|
79
|
+
clearTimeout(this.timer);
|
|
80
|
+
this.timer = null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
computeBaseDelay() {
|
|
84
|
+
const { backoff } = this.cfg;
|
|
85
|
+
if (backoff.mode === 'exponential') {
|
|
86
|
+
const cap = backoff.cap ?? 5;
|
|
87
|
+
return Math.min(backoff.baseMs * Math.pow(1.5, Math.floor(this.attempt / cap)), backoff.maxMs);
|
|
88
|
+
}
|
|
89
|
+
const { delays } = backoff;
|
|
90
|
+
return delays[Math.min(this.attempt, delays.length - 1)];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ── HeartbeatMonitor ─────────────────────────────────────────────
|
|
94
|
+
/**
|
|
95
|
+
* Manages a setInterval-based heartbeat or watchdog timer.
|
|
96
|
+
*
|
|
97
|
+
* Replaces duplicated `heartbeatTimer` / `watchdogTimer` patterns:
|
|
98
|
+
* timer cleanup on re-start, clearInterval on stop.
|
|
99
|
+
*/
|
|
100
|
+
export class HeartbeatMonitor {
|
|
101
|
+
timer = null;
|
|
102
|
+
_lastResponseTime = 0;
|
|
103
|
+
get lastResponseTime() { return this._lastResponseTime; }
|
|
104
|
+
/** Record that a response was received (for watchdog stale detection). */
|
|
105
|
+
recordResponse() { this._lastResponseTime = Date.now(); }
|
|
106
|
+
/**
|
|
107
|
+
* Start a periodic callback. Clears any existing timer first.
|
|
108
|
+
* @param intervalMs Interval in milliseconds.
|
|
109
|
+
* @param onTick Callback invoked on each tick.
|
|
110
|
+
*/
|
|
111
|
+
start(intervalMs, onTick) {
|
|
112
|
+
this.stop();
|
|
113
|
+
this.timer = setInterval(() => {
|
|
114
|
+
Promise.resolve(onTick()).catch(() => { });
|
|
115
|
+
}, intervalMs);
|
|
116
|
+
}
|
|
117
|
+
/** Clear the heartbeat timer. */
|
|
118
|
+
stop() {
|
|
119
|
+
if (this.timer) {
|
|
120
|
+
clearInterval(this.timer);
|
|
121
|
+
this.timer = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Watchdog convenience: returns true if more than `staleMs` have elapsed
|
|
126
|
+
* since the last `recordResponse()` call.
|
|
127
|
+
*/
|
|
128
|
+
isStale(staleMs) {
|
|
129
|
+
return this._lastResponseTime > 0 && Date.now() - this._lastResponseTime > staleMs;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// ── StateManager ─────────────────────────────────────────────────
|
|
133
|
+
/**
|
|
134
|
+
* Manages a connection state variable with change-notification callback.
|
|
135
|
+
*
|
|
136
|
+
* Replaces duplicated `connectionState` / `channelState` + `stateChangeHandler`
|
|
137
|
+
* patterns across wework, clawbot, and workbuddy clients.
|
|
138
|
+
*/
|
|
139
|
+
export class StateManager {
|
|
140
|
+
_state;
|
|
141
|
+
onChange;
|
|
142
|
+
constructor(initialState) {
|
|
143
|
+
this._state = initialState;
|
|
144
|
+
}
|
|
145
|
+
get current() { return this._state; }
|
|
146
|
+
/** Register a callback invoked whenever the state changes. */
|
|
147
|
+
setOnChange(handler) {
|
|
148
|
+
this.onChange = handler;
|
|
149
|
+
}
|
|
150
|
+
/** Transition to a new state and notify the callback if it changed. */
|
|
151
|
+
set(newState) {
|
|
152
|
+
if (this._state === newState)
|
|
153
|
+
return;
|
|
154
|
+
this._state = newState;
|
|
155
|
+
this.onChange?.(newState);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified session-invalid detection — consolidates all session-expiry
|
|
3
|
+
* error patterns from codex, codebuddy, and opencode into one place.
|
|
4
|
+
*/
|
|
5
|
+
const SESSION_INVALID_PATTERNS = [
|
|
6
|
+
'No session found',
|
|
7
|
+
'No conversation found',
|
|
8
|
+
'Unable to find session',
|
|
9
|
+
'Session not found',
|
|
10
|
+
'Invalid session',
|
|
11
|
+
'Unable to resume',
|
|
12
|
+
'session not found',
|
|
13
|
+
'no sessions found',
|
|
14
|
+
'session expired',
|
|
15
|
+
'session corrupt',
|
|
16
|
+
];
|
|
17
|
+
export function isSessionInvalidMessage(msg) {
|
|
18
|
+
const lower = msg.toLowerCase();
|
|
19
|
+
return SESSION_INVALID_PATTERNS.some((p) => lower.includes(p.toLowerCase()));
|
|
20
|
+
}
|
package/dist/wework/client.js
CHANGED
|
@@ -10,26 +10,44 @@
|
|
|
10
10
|
import { WebSocket } from 'ws';
|
|
11
11
|
import { randomBytes } from 'node:crypto';
|
|
12
12
|
import { createLogger } from '../logger.js';
|
|
13
|
-
import {
|
|
13
|
+
import { isFatalReconnectError } from '../shared/reconnect.js';
|
|
14
|
+
import { ReconnectManager, HeartbeatMonitor, StateManager } from '../shared/connection-manager.js';
|
|
14
15
|
const log = createLogger('WeWork');
|
|
15
16
|
const DEFAULT_WS_URL = 'wss://openws.work.weixin.qq.com';
|
|
16
17
|
const HEARTBEAT_INTERVAL = 30000; // 30秒
|
|
17
18
|
const PONG_TIMEOUT = HEARTBEAT_INTERVAL * 3; // 90秒无任何服务端响应则判定连接死亡
|
|
18
|
-
|
|
19
|
-
// Global state
|
|
19
|
+
// Connection infrastructure (shared managers replace raw timers/counters)
|
|
20
20
|
let ws = null;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
let heartbeatTimer = null;
|
|
24
|
-
let reconnectAttempts = 0;
|
|
21
|
+
const stateManager = new StateManager('disconnected');
|
|
22
|
+
const heartbeatMonitor = new HeartbeatMonitor();
|
|
25
23
|
let shouldReconnect = false;
|
|
26
24
|
let isStopping = false;
|
|
27
|
-
let lastServerResponseTime = 0; // 上次收到服务端消息的时间
|
|
28
25
|
// Event handlers
|
|
29
26
|
let messageHandler = null;
|
|
30
|
-
let stateChangeHandler = null;
|
|
31
27
|
// Configuration
|
|
32
28
|
let config = null;
|
|
29
|
+
// Reconnect manager: exponential backoff 5s → 7.5s → 11s → ... max 60s
|
|
30
|
+
const reconnectManager = new ReconnectManager({
|
|
31
|
+
name: 'WeWork',
|
|
32
|
+
backoff: { mode: 'exponential', baseMs: 5000, maxMs: 60000, cap: 5 },
|
|
33
|
+
maxAttempts: 100,
|
|
34
|
+
onReconnect: async () => {
|
|
35
|
+
log.info('Reconnecting...');
|
|
36
|
+
try {
|
|
37
|
+
await connectWebSocket();
|
|
38
|
+
reconnectManager.setFatal(false); // 连上后恢复正常退避
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (isFatalReconnectError(err)) {
|
|
42
|
+
reconnectManager.setFatal(true);
|
|
43
|
+
log.warn('WeWork 致命错误(鉴权失败),转慢探测:', err);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
log.error('Reconnection failed:', err);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
33
51
|
/**
|
|
34
52
|
* Generate unique request ID
|
|
35
53
|
*/
|
|
@@ -40,7 +58,7 @@ function generateReqId() {
|
|
|
40
58
|
* Get current connection state
|
|
41
59
|
*/
|
|
42
60
|
export function getConnectionState() {
|
|
43
|
-
return
|
|
61
|
+
return stateManager.current;
|
|
44
62
|
}
|
|
45
63
|
/**
|
|
46
64
|
* 主动推送消息 (aibot_send_msg)
|
|
@@ -48,7 +66,7 @@ export function getConnectionState() {
|
|
|
48
66
|
* 注意:需用户曾与机器人对话后,才能向该会话主动推送
|
|
49
67
|
*/
|
|
50
68
|
export function sendProactiveMessage(chatId, content) {
|
|
51
|
-
if (!ws ||
|
|
69
|
+
if (!ws || stateManager.current !== 'connected') {
|
|
52
70
|
throw new Error('Cannot send proactive message: WebSocket not connected');
|
|
53
71
|
}
|
|
54
72
|
if (!chatId) {
|
|
@@ -77,7 +95,7 @@ export function sendProactiveMessage(chatId, content) {
|
|
|
77
95
|
* 长连接模式下必须用此方式回复,透传 req_id
|
|
78
96
|
*/
|
|
79
97
|
export function sendWebSocketReply(reqId, body) {
|
|
80
|
-
if (!ws ||
|
|
98
|
+
if (!ws || stateManager.current !== 'connected') {
|
|
81
99
|
throw new Error('Cannot send reply: WebSocket not connected');
|
|
82
100
|
}
|
|
83
101
|
if (!reqId) {
|
|
@@ -109,11 +127,13 @@ export async function initWeWork(cfg, eventHandler, onStateChange) {
|
|
|
109
127
|
websocketUrl: cfg.weworkWsUrl || DEFAULT_WS_URL,
|
|
110
128
|
};
|
|
111
129
|
messageHandler = eventHandler;
|
|
112
|
-
|
|
130
|
+
stateManager.setOnChange(onStateChange);
|
|
113
131
|
isStopping = false;
|
|
114
132
|
shouldReconnect = false;
|
|
133
|
+
reconnectManager.reset();
|
|
134
|
+
reconnectManager.resume();
|
|
115
135
|
log.info('Initializing WeWork client');
|
|
116
|
-
// 首次连接支持重试:单独启用企微时偶发 TLS
|
|
136
|
+
// 首次连接支持重试:单独启用企微时偶发 TLS 连接失败,加飞书后因初始化顺序有"预热"效果则稳定
|
|
117
137
|
const maxAttempts = 3;
|
|
118
138
|
const retryDelayMs = 1500;
|
|
119
139
|
let lastErr = null;
|
|
@@ -136,7 +156,7 @@ export async function initWeWork(cfg, eventHandler, onStateChange) {
|
|
|
136
156
|
* Connect to WeWork WebSocket server
|
|
137
157
|
*/
|
|
138
158
|
async function connectWebSocket() {
|
|
139
|
-
if (
|
|
159
|
+
if (stateManager.current === 'connecting') {
|
|
140
160
|
log.warn('WebSocket connection already in progress');
|
|
141
161
|
return;
|
|
142
162
|
}
|
|
@@ -154,16 +174,16 @@ async function connectWebSocket() {
|
|
|
154
174
|
}
|
|
155
175
|
ws = null;
|
|
156
176
|
}
|
|
157
|
-
|
|
177
|
+
stateManager.set('connecting');
|
|
158
178
|
const websocketUrl = config.websocketUrl || DEFAULT_WS_URL;
|
|
159
179
|
return new Promise((resolve, reject) => {
|
|
160
180
|
try {
|
|
161
181
|
ws = new WebSocket(websocketUrl);
|
|
162
182
|
ws.on('open', async () => {
|
|
163
183
|
log.info('WeWork WebSocket connected');
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
184
|
+
reconnectManager.reset();
|
|
185
|
+
stateManager.set('connected');
|
|
186
|
+
heartbeatMonitor.start(HEARTBEAT_INTERVAL, heartbeatTick);
|
|
167
187
|
// 发送认证订阅消息,并等待服务端确认(否则 aibot_send_msg 会报 846609 not subscribed)
|
|
168
188
|
try {
|
|
169
189
|
await sendSubscribeAndWaitAck(resolve, reject);
|
|
@@ -176,7 +196,7 @@ async function connectWebSocket() {
|
|
|
176
196
|
}
|
|
177
197
|
});
|
|
178
198
|
ws.on('message', async (data) => {
|
|
179
|
-
|
|
199
|
+
heartbeatMonitor.recordResponse();
|
|
180
200
|
try {
|
|
181
201
|
const message = JSON.parse(data.toString());
|
|
182
202
|
await handleMessage(message);
|
|
@@ -187,21 +207,21 @@ async function connectWebSocket() {
|
|
|
187
207
|
});
|
|
188
208
|
ws.on('error', (err) => {
|
|
189
209
|
log.error('WeWork WebSocket error:', err);
|
|
190
|
-
|
|
210
|
+
stateManager.set('error');
|
|
191
211
|
reject(err);
|
|
192
212
|
});
|
|
193
213
|
ws.on('close', () => {
|
|
194
214
|
log.info('WeWork WebSocket closed');
|
|
195
|
-
|
|
196
|
-
|
|
215
|
+
heartbeatMonitor.stop();
|
|
216
|
+
stateManager.set('disconnected');
|
|
197
217
|
if (!isStopping && shouldReconnect) {
|
|
198
|
-
|
|
218
|
+
reconnectManager.schedule();
|
|
199
219
|
}
|
|
200
220
|
});
|
|
201
221
|
}
|
|
202
222
|
catch (err) {
|
|
203
223
|
log.error('Error creating WebSocket connection:', err);
|
|
204
|
-
|
|
224
|
+
stateManager.set('error');
|
|
205
225
|
reject(err);
|
|
206
226
|
}
|
|
207
227
|
});
|
|
@@ -279,7 +299,7 @@ async function handleMessage(message) {
|
|
|
279
299
|
* Send message to WeWork
|
|
280
300
|
*/
|
|
281
301
|
export function sendMessage(message) {
|
|
282
|
-
if (!ws ||
|
|
302
|
+
if (!ws || stateManager.current !== 'connected') {
|
|
283
303
|
log.warn('Cannot send message: WebSocket not connected');
|
|
284
304
|
return;
|
|
285
305
|
}
|
|
@@ -319,108 +339,43 @@ export function sendStreamWithItems(reqId, streamId, content, finish, msgItem) {
|
|
|
319
339
|
});
|
|
320
340
|
}
|
|
321
341
|
/**
|
|
322
|
-
*
|
|
323
|
-
*/
|
|
324
|
-
function updateState(state) {
|
|
325
|
-
connectionState = state;
|
|
326
|
-
if (stateChangeHandler) {
|
|
327
|
-
stateChangeHandler(state);
|
|
328
|
-
}
|
|
329
|
-
log.debug('Connection state:', state);
|
|
330
|
-
}
|
|
331
|
-
/**
|
|
332
|
-
* Start heartbeat to keep connection alive
|
|
342
|
+
* Heartbeat tick: send ping and detect stale connections.
|
|
333
343
|
* 同时检测服务端是否响应,超时无响应则主动断开触发重连
|
|
334
344
|
*/
|
|
335
|
-
function
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
ws.removeAllListeners();
|
|
347
|
-
ws.close();
|
|
348
|
-
}
|
|
349
|
-
catch {
|
|
350
|
-
/* ignore */
|
|
351
|
-
}
|
|
352
|
-
ws = null;
|
|
353
|
-
updateState('disconnected');
|
|
354
|
-
scheduleReconnect();
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
const pingMessage = {
|
|
358
|
-
cmd: "ping" /* WeWorkCommand.PING */,
|
|
359
|
-
headers: {
|
|
360
|
-
req_id: generateReqId(),
|
|
361
|
-
},
|
|
362
|
-
body: {},
|
|
363
|
-
};
|
|
364
|
-
try {
|
|
365
|
-
ws.send(JSON.stringify(pingMessage));
|
|
366
|
-
log.debug('Sent ping');
|
|
367
|
-
}
|
|
368
|
-
catch (err) {
|
|
369
|
-
log.error('Error sending ping:', err);
|
|
370
|
-
}
|
|
345
|
+
function heartbeatTick() {
|
|
346
|
+
if (stateManager.current !== 'connected' || !ws)
|
|
347
|
+
return;
|
|
348
|
+
// 检测连接是否已死:长时间未收到任何服务端响应
|
|
349
|
+
if (heartbeatMonitor.isStale(PONG_TIMEOUT)) {
|
|
350
|
+
const elapsed = Date.now() - heartbeatMonitor.lastResponseTime;
|
|
351
|
+
log.warn(`No server response for ${Math.round(elapsed / 1000)}s, forcing reconnect`);
|
|
352
|
+
heartbeatMonitor.stop();
|
|
353
|
+
try {
|
|
354
|
+
ws.removeAllListeners();
|
|
355
|
+
ws.close();
|
|
371
356
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (heartbeatTimer) {
|
|
379
|
-
clearInterval(heartbeatTimer);
|
|
380
|
-
heartbeatTimer = null;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* Schedule reconnection attempt
|
|
385
|
-
* 超过 MAX_RECONNECT_ATTEMPTS 后自动重置计数器继续重试,避免永久断连
|
|
386
|
-
*/
|
|
387
|
-
/** 致命(鉴权)错误后转慢探测,避免紧密 hammer 网关 */
|
|
388
|
-
let fatalSlowProbe = false;
|
|
389
|
-
function scheduleReconnect() {
|
|
390
|
-
if (isStopping || !shouldReconnect) {
|
|
357
|
+
catch {
|
|
358
|
+
/* ignore */
|
|
359
|
+
}
|
|
360
|
+
ws = null;
|
|
361
|
+
stateManager.set('disconnected');
|
|
362
|
+
reconnectManager.schedule();
|
|
391
363
|
return;
|
|
392
364
|
}
|
|
393
|
-
|
|
394
|
-
|
|
365
|
+
const pingMessage = {
|
|
366
|
+
cmd: "ping" /* WeWorkCommand.PING */,
|
|
367
|
+
headers: {
|
|
368
|
+
req_id: generateReqId(),
|
|
369
|
+
},
|
|
370
|
+
body: {},
|
|
371
|
+
};
|
|
372
|
+
try {
|
|
373
|
+
ws.send(JSON.stringify(pingMessage));
|
|
374
|
+
log.debug('Sent ping');
|
|
395
375
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
log.warn(`Max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached, resetting counter and retrying at lower frequency`);
|
|
399
|
-
reconnectAttempts = 0;
|
|
376
|
+
catch (err) {
|
|
377
|
+
log.error('Error sending ping:', err);
|
|
400
378
|
}
|
|
401
|
-
// 逐步增加间隔,5s → 7.5s → 11s → ... 最大 60s;致命(鉴权)错误转慢探测
|
|
402
|
-
const backoff = fatalSlowProbe
|
|
403
|
-
? SLOW_PROBE_MS
|
|
404
|
-
: Math.min(5000 * Math.pow(1.5, Math.floor(reconnectAttempts / 5)), 60000);
|
|
405
|
-
const interval = jitteredDelay(Math.round(backoff));
|
|
406
|
-
reconnectTimer = setTimeout(async () => {
|
|
407
|
-
reconnectTimer = null;
|
|
408
|
-
reconnectAttempts++;
|
|
409
|
-
log.info(`Reconnecting... Attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS} (interval: ${interval}ms${fatalSlowProbe ? ', slow-probe' : ''})`);
|
|
410
|
-
try {
|
|
411
|
-
await connectWebSocket();
|
|
412
|
-
fatalSlowProbe = false; // 连上后恢复正常退避
|
|
413
|
-
}
|
|
414
|
-
catch (err) {
|
|
415
|
-
if (isFatalReconnectError(err)) {
|
|
416
|
-
fatalSlowProbe = true;
|
|
417
|
-
log.warn('WeWork 致命错误(鉴权失败),转慢探测:', err);
|
|
418
|
-
}
|
|
419
|
-
else {
|
|
420
|
-
log.error('Reconnection failed:', err);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}, interval);
|
|
424
379
|
}
|
|
425
380
|
/**
|
|
426
381
|
* Stop WeWork client
|
|
@@ -428,15 +383,12 @@ function scheduleReconnect() {
|
|
|
428
383
|
export function stopWeWork() {
|
|
429
384
|
isStopping = true;
|
|
430
385
|
shouldReconnect = false;
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
clearTimeout(reconnectTimer);
|
|
434
|
-
reconnectTimer = null;
|
|
435
|
-
}
|
|
386
|
+
heartbeatMonitor.stop();
|
|
387
|
+
reconnectManager.stop();
|
|
436
388
|
if (ws) {
|
|
437
389
|
ws.close();
|
|
438
390
|
ws = null;
|
|
439
391
|
}
|
|
440
|
-
|
|
392
|
+
stateManager.set('disconnected');
|
|
441
393
|
log.info('WeWork client stopped');
|
|
442
394
|
}
|