@eyeclaw/eyeclaw 2.4.2 → 2.4.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eyeclaw/eyeclaw",
3
- "version": "2.4.2",
3
+ "version": "2.4.4",
4
4
  "description": "EyeClaw plugin for OpenClaw - HTTP SSE streaming + WebSocket client",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -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
- this.api.logger.error(`[EyeClaw] WebSocket error: ${error}`)
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 = () => {
@@ -102,12 +123,19 @@ export class EyeClawWebSocketClient {
102
123
  this.stopPing()
103
124
  this.lastCloseCode = closeCode
104
125
 
126
+ // 保存之前的连接状态用于检测部署
127
+ const previouslyConnected = this.wasConnected
128
+ const lastConnected = this.lastConnectedAt
129
+
130
+ // 标记为未连接状态,防止死循环
131
+ this.wasConnected = false
132
+
105
133
  // 统计连续失败
106
134
  this.consecutiveFailures++
107
135
 
108
136
  // 检测是否是部署导致的断连(之前已成功连接过,且断开时间 > 5 秒)
109
137
  // 或者关闭码为 1000/1001(正常关闭)但之前已连接
110
- if (this.wasConnected && Date.now() - this.lastConnectedAt > 5000) {
138
+ if (previouslyConnected && Date.now() - lastConnected > 5000) {
111
139
  // 正常关闭或部署重启
112
140
  if (wasClean) {
113
141
  this.deploymentDetected = true
@@ -117,25 +145,114 @@ export class EyeClawWebSocketClient {
117
145
  this.deploymentDetected = true
118
146
  this.api.logger.info('[EyeClaw] 🔄 Server restart detected (unclean close), will reconnect immediately')
119
147
  }
120
- } else if (!this.wasConnected && !wasClean) {
148
+ } else if (!previouslyConnected && !wasClean) {
121
149
  // 首次连接失败,增加重试延迟
122
150
  this.api.logger.warn('[EyeClaw] Initial connection failed, increasing delay...')
123
151
  }
124
152
 
153
+ // 网络断开也会导致 onclose 被调用(code 0)
154
+ // 检测是否是网络断开(code 0 且非部署场景)
155
+ if (closeCode === 0 && !this.deploymentDetected) {
156
+ this.api.logger.info('[EyeClaw] 🌐 Network disconnection detected, will retry with backoff')
157
+ }
158
+
125
159
  this.scheduleReconnect()
126
160
  }
127
161
 
128
162
  } catch (error) {
129
163
  this.api.logger.error(`[EyeClaw] WebSocket connection failed: ${error}`)
164
+ // 连接失败也应该触发重连
130
165
  this.scheduleReconnect()
131
166
  }
132
167
  }
133
168
 
169
+ /**
170
+ * 启动网络状态监控
171
+ * 定期检测网络是否恢复,以便在网络中断后自动重连
172
+ */
173
+ private startNetworkMonitoring() {
174
+ // 防止重复启动
175
+ if (this.networkCheckTimer) {
176
+ return
177
+ }
178
+
179
+ // 每 5 秒检测一次网络状态
180
+ this.networkCheckTimer = setInterval(() => {
181
+ this.checkNetworkAndReconnectIfNeeded()
182
+ }, 5000)
183
+
184
+ // 尝试立即检测一次
185
+ this.checkNetworkAndReconnectIfNeeded()
186
+ }
187
+
188
+ /**
189
+ * 检测网络状态并在需要时触发重连
190
+ */
191
+ private async checkNetworkAndReconnectIfNeeded() {
192
+ // 如果 WebSocket 已经连接,跳过(不需要重连)
193
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
194
+ return
195
+ }
196
+
197
+ // 如果正在重连中,跳过
198
+ if (this.reconnecting) {
199
+ return
200
+ }
201
+
202
+ try {
203
+ // 尝试一个简单的 HTTP 请求来检测网络是否可用
204
+ const serverUrl = this.config.serverUrl.replace(/^ws/, 'http').replace(/^wss/, 'https')
205
+ const controller = new AbortController()
206
+ const timeoutId = setTimeout(() => controller.abort(), 3000)
207
+
208
+ const response = await fetch(`${serverUrl}/api/v1/health`, {
209
+ method: 'GET',
210
+ signal: controller.signal,
211
+ })
212
+
213
+ clearTimeout(timeoutId)
214
+
215
+ const wasOnline = this.networkOnline
216
+ this.networkOnline = response.ok
217
+
218
+ if (!wasOnline && this.networkOnline) {
219
+ // 网络从离线变为在线,触发重连
220
+ this.api.logger.info('[EyeClaw] 🌐 Network restored, triggering reconnect')
221
+ this.deploymentDetected = true // 使用快速重连模式
222
+ this.scheduleReconnect()
223
+ } else if (this.networkOnline && !this.wasConnected) {
224
+ // 网络在线但未连接过,触发连接
225
+ this.api.logger.info('[EyeClaw] 🌐 Network available, connecting...')
226
+ this.scheduleReconnect()
227
+ }
228
+ } catch (error) {
229
+ // 网络请求失败,说明网络不可用
230
+ const wasOnline = this.networkOnline
231
+ this.networkOnline = false
232
+
233
+ if (wasOnline) {
234
+ this.api.logger.warn('[EyeClaw] 🌐 Network became unavailable')
235
+ }
236
+ // 网络不可用时,不触发重连,等待下次检测
237
+ }
238
+ }
239
+
240
+ /**
241
+ * 停止网络状态监控
242
+ */
243
+ private stopNetworkMonitoring() {
244
+ if (this.networkCheckTimer) {
245
+ clearInterval(this.networkCheckTimer)
246
+ this.networkCheckTimer = null
247
+ }
248
+ }
249
+
134
250
  /**
135
251
  * 停止 WebSocket 连接
136
252
  */
137
253
  stop() {
138
254
  this.stopPing()
255
+ this.stopNetworkMonitoring() // 停止网络监控
139
256
  this.reconnecting = false
140
257
  this.resetReconnectDelay()
141
258
 
@@ -679,14 +796,37 @@ export class EyeClawWebSocketClient {
679
796
  return
680
797
  }
681
798
 
682
- // 🚀 部署恢复场景:立即重连(无延迟)
799
+ // 🚀 部署恢复场景或网络恢复:立即重连(无延迟)
800
+ // 但需要限制次数,防止无限立即重连失败导致死循环
683
801
  if (this.deploymentDetected) {
684
- this.api.logger.info('[EyeClaw] Deployment recovery mode: immediate reconnect')
802
+ // 如果连续 3 次立即重连都失败,说明可能是网络问题,不再使用立即重连
803
+ if (this.consecutiveFailures > 3) {
804
+ this.api.logger.warn('[EyeClaw] ⚠️ Too many immediate reconnects failed, switching to backoff mode')
805
+ this.deploymentDetected = false
806
+ // 继续使用正常重连逻辑
807
+ } else {
808
+ this.api.logger.info('[EyeClaw] ⚡ Deployment recovery or network restored: immediate reconnect')
809
+ this.reconnectTimer = setTimeout(() => {
810
+ this.reconnecting = false
811
+ this.start()
812
+ }, 100) // 100ms 延迟(给服务器一点启动时间)
813
+ this.deploymentDetected = false // 重置标志
814
+ return
815
+ }
816
+ }
817
+
818
+ // 检测是否是网络断开导致的失败(code 0)
819
+ // 如果是网络断开,不要使用指数退避,而是使用固定间隔重试
820
+ if (this.lastCloseCode === 0) {
821
+ // 网络断开场景:每 3 秒尝试一次(有网络后能快速恢复)
822
+ const networkRetryDelay = 3000
823
+ this.reconnectAttempts++
824
+ this.api.logger.info(`[EyeClaw] 🌐 Network disconnection scenario: retrying in ${networkRetryDelay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
825
+
685
826
  this.reconnectTimer = setTimeout(() => {
686
827
  this.reconnecting = false
687
- this.start()
688
- }, 100) // 100ms 延迟(给服务器一点启动时间)
689
- this.deploymentDetected = false // 重置标志
828
+ this.performHealthCheckAndReconnect()
829
+ }, networkRetryDelay)
690
830
  return
691
831
  }
692
832
 
@@ -704,9 +844,15 @@ export class EyeClawWebSocketClient {
704
844
  * 健康检查后重连
705
845
  */
706
846
  private async performHealthCheckAndReconnect() {
847
+ // 如果 WebSocket 已经连接,不需要重连
848
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
849
+ this.api.logger?.debug?.('[EyeClaw] ✅ WebSocket already connected, skipping reconnect')
850
+ return
851
+ }
852
+
707
853
  try {
708
854
  // 尝试 HTTP 健康检查
709
- const serverUrl = this.config.serverUrl.replace(/^http/, 'http') // 使用 http 而非 ws
855
+ const serverUrl = this.config.serverUrl.replace(/^ws/, 'http').replace(/^wss/, 'https')
710
856
  const healthUrl = `${serverUrl}/api/v1/health`
711
857
 
712
858
  const controller = new AbortController()
@@ -720,7 +866,7 @@ export class EyeClawWebSocketClient {
720
866
  clearTimeout(timeoutId)
721
867
 
722
868
  if (response.ok) {
723
- this.api.logger.info('[EyeClaw] ✅ Health check passed, proceeding with reconnect')
869
+ this.api.logger.info('[EyeClaw] ✅ Health check passed, connecting...')
724
870
  this.start()
725
871
  } else {
726
872
  this.api.logger.warn(`[EyeClaw] ⚠️ Health check failed (${response.status}), retrying soon...`)
@@ -731,9 +877,12 @@ export class EyeClawWebSocketClient {
731
877
  }, 2000)
732
878
  }
733
879
  } catch (error) {
734
- this.api.logger.warn(`[EyeClaw] ⚠️ Health check error: ${error}, proceeding with reconnect anyway`)
735
- // 即使健康检查失败也尝试重连
736
- this.start()
880
+ this.api.logger.warn(`[EyeClaw] ⚠️ Health check error: ${error}, retrying soon...`)
881
+ // 健康检查失败,稍后重试
882
+ this.reconnectTimer = setTimeout(() => {
883
+ this.reconnecting = false
884
+ this.scheduleReconnect()
885
+ }, 2000)
737
886
  }
738
887
  }
739
888