@eyeclaw/eyeclaw 2.4.2 → 2.4.3
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/package.json +1 -1
- package/src/websocket-client.ts +124 -4
package/package.json
CHANGED
package/src/websocket-client.ts
CHANGED
|
@@ -39,6 +39,8 @@ export class EyeClawWebSocketClient {
|
|
|
39
39
|
private serverVersion = '' // 服务器版本(用于检测部署)
|
|
40
40
|
private consecutiveFailures = 0 // 连续失败次数
|
|
41
41
|
private lastCloseCode = 0 // 上次关闭码
|
|
42
|
+
private networkOnline = true // 网络在线状态
|
|
43
|
+
private networkCheckTimer: any = null // 网络检测定时器
|
|
42
44
|
|
|
43
45
|
// 🔥 ACK 机制:追踪已发送和已确认的 chunks
|
|
44
46
|
private sentChunks = 0 // 已发送的 chunks 数量
|
|
@@ -61,6 +63,9 @@ export class EyeClawWebSocketClient {
|
|
|
61
63
|
return
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
// 启动网络状态监控
|
|
67
|
+
this.startNetworkMonitoring()
|
|
68
|
+
|
|
64
69
|
const wsUrl = serverUrl.replace(/^http/, 'ws') + `/cable?sdk_token=${sdkToken}&bot_id=${botId}`
|
|
65
70
|
this.api.logger.info(`[EyeClaw] WebSocket connecting to: ${wsUrl}`)
|
|
66
71
|
|
|
@@ -88,8 +93,24 @@ export class EyeClawWebSocketClient {
|
|
|
88
93
|
this.handleMessage(event.data)
|
|
89
94
|
}
|
|
90
95
|
|
|
91
|
-
this.ws.onerror = (error) => {
|
|
92
|
-
|
|
96
|
+
this.ws.onerror = (error: any) => {
|
|
97
|
+
// 尝试提取更详细的错误信息
|
|
98
|
+
let errorMsg = 'Unknown WebSocket error'
|
|
99
|
+
|
|
100
|
+
// 浏览器环境
|
|
101
|
+
if (error && typeof error === 'object' && 'message' in error) {
|
|
102
|
+
errorMsg = (error as any).message || 'ErrorEvent without message'
|
|
103
|
+
if ((error as any).error) {
|
|
104
|
+
errorMsg += ` | Error: ${(error as any).error}`
|
|
105
|
+
}
|
|
106
|
+
} else if (error) {
|
|
107
|
+
// Node.js 环境或其他错误
|
|
108
|
+
errorMsg = String(error)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.api.logger.error(`[EyeClaw] WebSocket error: ${errorMsg}`)
|
|
112
|
+
// 网络错误也应该触发重连,而不是等待 onclose
|
|
113
|
+
// 这样可以更快地从网络中断中恢复
|
|
93
114
|
}
|
|
94
115
|
|
|
95
116
|
this.ws.onclose = () => {
|
|
@@ -122,20 +143,104 @@ export class EyeClawWebSocketClient {
|
|
|
122
143
|
this.api.logger.warn('[EyeClaw] Initial connection failed, increasing delay...')
|
|
123
144
|
}
|
|
124
145
|
|
|
146
|
+
// 网络断开也会导致 onclose 被调用(code 0)
|
|
147
|
+
// 检测是否是网络断开(code 0 且非部署场景)
|
|
148
|
+
if (closeCode === 0 && !this.deploymentDetected) {
|
|
149
|
+
this.api.logger.info('[EyeClaw] 🌐 Network disconnection detected, will retry with backoff')
|
|
150
|
+
}
|
|
151
|
+
|
|
125
152
|
this.scheduleReconnect()
|
|
126
153
|
}
|
|
127
154
|
|
|
128
155
|
} catch (error) {
|
|
129
156
|
this.api.logger.error(`[EyeClaw] WebSocket connection failed: ${error}`)
|
|
157
|
+
// 连接失败也应该触发重连
|
|
130
158
|
this.scheduleReconnect()
|
|
131
159
|
}
|
|
132
160
|
}
|
|
133
161
|
|
|
162
|
+
/**
|
|
163
|
+
* 启动网络状态监控
|
|
164
|
+
* 定期检测网络是否恢复,以便在网络中断后自动重连
|
|
165
|
+
*/
|
|
166
|
+
private startNetworkMonitoring() {
|
|
167
|
+
// 防止重复启动
|
|
168
|
+
if (this.networkCheckTimer) {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 每 5 秒检测一次网络状态
|
|
173
|
+
this.networkCheckTimer = setInterval(() => {
|
|
174
|
+
this.checkNetworkAndReconnectIfNeeded()
|
|
175
|
+
}, 5000)
|
|
176
|
+
|
|
177
|
+
// 尝试立即检测一次
|
|
178
|
+
this.checkNetworkAndReconnectIfNeeded()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 检测网络状态并在需要时触发重连
|
|
183
|
+
*/
|
|
184
|
+
private async checkNetworkAndReconnectIfNeeded() {
|
|
185
|
+
// 如果已经在重连中或者 WebSocket 已经连接,跳过
|
|
186
|
+
if (this.reconnecting || (this.ws && this.ws.readyState === WebSocket.OPEN)) {
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
// 尝试一个简单的 HTTP 请求来检测网络是否可用
|
|
192
|
+
const serverUrl = this.config.serverUrl.replace(/^ws/, 'http').replace(/^wss/, 'https')
|
|
193
|
+
const controller = new AbortController()
|
|
194
|
+
const timeoutId = setTimeout(() => controller.abort(), 3000)
|
|
195
|
+
|
|
196
|
+
const response = await fetch(`${serverUrl}/api/v1/health`, {
|
|
197
|
+
method: 'GET',
|
|
198
|
+
signal: controller.signal,
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
clearTimeout(timeoutId)
|
|
202
|
+
|
|
203
|
+
const wasOnline = this.networkOnline
|
|
204
|
+
this.networkOnline = response.ok
|
|
205
|
+
|
|
206
|
+
if (!wasOnline && this.networkOnline) {
|
|
207
|
+
// 网络从离线变为在线,触发重连
|
|
208
|
+
this.api.logger.info('[EyeClaw] 🌐 Network restored, triggering immediate reconnect')
|
|
209
|
+
this.deploymentDetected = true // 使用快速重连模式
|
|
210
|
+
this.scheduleReconnect()
|
|
211
|
+
} else if (this.networkOnline && !this.wasConnected) {
|
|
212
|
+
// 网络在线但未连接过,触发连接
|
|
213
|
+
this.api.logger.info('[EyeClaw] 🌐 Network available, connecting...')
|
|
214
|
+
this.scheduleReconnect()
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
// 网络请求失败,说明网络不可用
|
|
218
|
+
const wasOnline = this.networkOnline
|
|
219
|
+
this.networkOnline = false
|
|
220
|
+
|
|
221
|
+
if (wasOnline) {
|
|
222
|
+
this.api.logger.warn('[EyeClaw] 🌐 Network became unavailable')
|
|
223
|
+
}
|
|
224
|
+
// 网络不可用时,不触发重连,等待下次检测
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 停止网络状态监控
|
|
230
|
+
*/
|
|
231
|
+
private stopNetworkMonitoring() {
|
|
232
|
+
if (this.networkCheckTimer) {
|
|
233
|
+
clearInterval(this.networkCheckTimer)
|
|
234
|
+
this.networkCheckTimer = null
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
134
238
|
/**
|
|
135
239
|
* 停止 WebSocket 连接
|
|
136
240
|
*/
|
|
137
241
|
stop() {
|
|
138
242
|
this.stopPing()
|
|
243
|
+
this.stopNetworkMonitoring() // 停止网络监控
|
|
139
244
|
this.reconnecting = false
|
|
140
245
|
this.resetReconnectDelay()
|
|
141
246
|
|
|
@@ -679,9 +784,9 @@ export class EyeClawWebSocketClient {
|
|
|
679
784
|
return
|
|
680
785
|
}
|
|
681
786
|
|
|
682
|
-
// 🚀
|
|
787
|
+
// 🚀 部署恢复场景或网络恢复:立即重连(无延迟)
|
|
683
788
|
if (this.deploymentDetected) {
|
|
684
|
-
this.api.logger.info('[EyeClaw] ⚡ Deployment recovery
|
|
789
|
+
this.api.logger.info('[EyeClaw] ⚡ Deployment recovery or network restored: immediate reconnect')
|
|
685
790
|
this.reconnectTimer = setTimeout(() => {
|
|
686
791
|
this.reconnecting = false
|
|
687
792
|
this.start()
|
|
@@ -690,6 +795,21 @@ export class EyeClawWebSocketClient {
|
|
|
690
795
|
return
|
|
691
796
|
}
|
|
692
797
|
|
|
798
|
+
// 检测是否是网络断开导致的失败(code 0)
|
|
799
|
+
// 如果是网络断开,不要使用指数退避,而是使用固定间隔重试
|
|
800
|
+
if (this.lastCloseCode === 0) {
|
|
801
|
+
// 网络断开场景:每 3 秒尝试一次(有网络后能快速恢复)
|
|
802
|
+
const networkRetryDelay = 3000
|
|
803
|
+
this.reconnectAttempts++
|
|
804
|
+
this.api.logger.info(`[EyeClaw] 🌐 Network disconnection scenario: retrying in ${networkRetryDelay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
|
|
805
|
+
|
|
806
|
+
this.reconnectTimer = setTimeout(() => {
|
|
807
|
+
this.reconnecting = false
|
|
808
|
+
this.performHealthCheckAndReconnect()
|
|
809
|
+
}, networkRetryDelay)
|
|
810
|
+
return
|
|
811
|
+
}
|
|
812
|
+
|
|
693
813
|
const delay = this.calculateReconnectDelay()
|
|
694
814
|
this.reconnectAttempts++
|
|
695
815
|
this.api.logger.info(`[EyeClaw] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
|