@eyeclaw/eyeclaw 2.3.10 → 2.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eyeclaw/eyeclaw",
3
- "version": "2.3.10",
3
+ "version": "2.3.12",
4
4
  "description": "EyeClaw plugin for OpenClaw - HTTP SSE streaming + WebSocket client",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -27,6 +27,7 @@ export class EyeClawWebSocketClient {
27
27
  private subscribed = false
28
28
  private pingInterval: any = null
29
29
  private chunkSequence = 0 // 每个会话的 chunk 序号
30
+ private accumulatedContent = '' // 累积完整内容用于兜底
30
31
 
31
32
  constructor(api: OpenClawPluginApi, config: EyeClawConfig, getState: () => any) {
32
33
  this.api = api
@@ -104,9 +105,9 @@ export class EyeClawWebSocketClient {
104
105
  return
105
106
  }
106
107
 
107
- // Ping/pong (协议级别的 ping,直接响应 pong)
108
+ // Ping/pong (WebSocket 协议级别的 ping 由浏览器自动响应,无需手动处理)
108
109
  if (message.type === 'ping') {
109
- this.send({ type: 'pong' })
110
+ this.api.logger.debug('[EyeClaw] Received protocol-level ping (auto-handled by WebSocket)')
110
111
  return
111
112
  }
112
113
 
@@ -244,6 +245,9 @@ export class EyeClawWebSocketClient {
244
245
  if (done) {
245
246
  // 流结束,通知 Rails
246
247
  this.sendMessage('stream_end', { session_id: sessionId })
248
+
249
+ // 发送 stream_summary 用于兜底机制
250
+ this.sendStreamSummary(sessionId)
247
251
  break
248
252
  }
249
253
 
@@ -322,6 +326,10 @@ export class EyeClawWebSocketClient {
322
326
  private sendChunk(content: string, sessionId?: string) {
323
327
  const timestamp = new Date().toISOString();
324
328
  const sequence = this.chunkSequence++;
329
+
330
+ // 累积完整内容用于兜底
331
+ this.accumulatedContent += content;
332
+
325
333
  this.api.logger.info(`[EyeClaw] [${timestamp}] Sending chunk #${sequence} to Rails: "${content}"`);
326
334
  this.sendMessage('stream_chunk', {
327
335
  content,
@@ -329,6 +337,41 @@ export class EyeClawWebSocketClient {
329
337
  sequence, // 添加序号
330
338
  })
331
339
  }
340
+
341
+ /**
342
+ * 发送 stream_summary 用于兜底机制
343
+ * 告诉 Rails 完整内容是什么,以便检测丢包并补偿
344
+ */
345
+ private sendStreamSummary(sessionId?: string) {
346
+ // 计算内容 hash
347
+ const contentHash = this.hashCode(this.accumulatedContent);
348
+
349
+ this.api.logger.info(`[EyeClaw] Sending stream_summary: chunks=${this.chunkSequence}, content_len=${this.accumulatedContent.length}, hash=${contentHash}`);
350
+
351
+ this.sendMessage('stream_summary', {
352
+ session_id: sessionId,
353
+ total_content: this.accumulatedContent,
354
+ total_chunks: this.chunkSequence,
355
+ content_hash: contentHash,
356
+ })
357
+
358
+ // 重置累积内容(为下一个会话做准备)
359
+ this.accumulatedContent = '';
360
+ this.chunkSequence = 0;
361
+ }
362
+
363
+ /**
364
+ * 简单 hash 函数
365
+ */
366
+ private hashCode(str: string): string {
367
+ let hash = 0;
368
+ for (let i = 0; i < str.length; i++) {
369
+ const char = str.charCodeAt(i);
370
+ hash = ((hash << 5) - hash) + char;
371
+ hash = hash & hash; // Convert to 32bit integer
372
+ }
373
+ return hash.toString(16);
374
+ }
332
375
 
333
376
  /**
334
377
  * 发送消息到 Rails(带 channel identifier)
@@ -351,7 +394,7 @@ export class EyeClawWebSocketClient {
351
394
  */
352
395
  private startPing() {
353
396
  this.pingInterval = setInterval(() => {
354
- // 调用 Rails BotChannel 的 ping action
397
+ // 调用 Rails BotChannel 的 ping 方法(使用 ActionCable 标准协议)
355
398
  const channelIdentifier = JSON.stringify({
356
399
  channel: 'BotChannel',
357
400
  bot_id: this.config.botId,
@@ -362,6 +405,7 @@ export class EyeClawWebSocketClient {
362
405
  identifier: channelIdentifier,
363
406
  data: JSON.stringify({
364
407
  action: 'ping',
408
+ timestamp: new Date().toISOString(),
365
409
  }),
366
410
  })
367
411
  }, 60000) // 60秒心跳一次