@eyeclaw/eyeclaw 2.2.4 → 2.3.0

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/index.ts CHANGED
@@ -30,33 +30,63 @@ const eyeclawPlugin = {
30
30
  serverUrl: rawConfig?.serverUrl || '',
31
31
  }
32
32
 
33
- // HTTP 处理器(SSE 流式)
34
- api.registerHttpHandler(createHttpHandler(api, () => config))
35
- logger.info('[EyeClaw] HTTP handler registered: /eyeclaw/*')
33
+ // 获取 Gateway 端口
34
+ const gatewayPort = api.config?.gateway?.port ?? 18789
36
35
 
37
- // WebSocket 客户端(连接 Rails 接收消息)
38
- if (config.sdkToken && config.botId && config.serverUrl) {
39
- const wsClient = new EyeClawWebSocketClient(api, config)
40
- wsClient.start()
41
- logger.info('[EyeClaw] WebSocket client starting...')
42
-
43
- // 存储客户端引用,防止被垃圾回收
44
- ;(global as any).__eyeclaw_ws = wsClient
45
- } else {
46
- logger.warn('[EyeClaw] WebSocket not started: missing config (sdkToken, botId, serverUrl)')
36
+ // 配置获取函数
37
+ const getConfig = () => config
38
+ const getState = () => ({
39
+ config,
40
+ gatewayPort,
41
+ })
42
+
43
+ // 1. 注册 HTTP 处理器(SSE 流式)
44
+ if (typeof api.registerHttpHandler === 'function') {
45
+ api.registerHttpHandler(createHttpHandler(api, getConfig))
46
+ logger.info('[EyeClaw] HTTP handler registered: /eyeclaw/*')
47
47
  }
48
48
 
49
- // 打印启动信息
50
- const gatewayPort = api.config?.gateway?.port ?? 18789
51
- console.log('')
52
- console.log('╔═══════════════════════════════════════════════════════════════════════╗')
53
- console.log('║ EyeClaw Plugin 已启动 ║')
54
- console.log('╠═══════════════════════════════════════════════════════════════════════╣')
55
- console.log(`║ HTTP 端点: POST http://127.0.0.1:${gatewayPort}/eyeclaw/chat ║`)
56
- console.log(`║ WebSocket: ${config.serverUrl ? '已配置' : '未配置'} ║`)
57
- console.log(`║ SDK Token: ${config.sdkToken ? config.sdkToken.substring(0, 8) + '...' : '未配置'} ║`)
58
- console.log('╚═══════════════════════════════════════════════════════════════════════╝')
59
- console.log('')
49
+ // 2. 注册 WebSocket 服务
50
+ if (typeof api.registerService === 'function') {
51
+ api.registerService({
52
+ id: 'eyeclaw-websocket',
53
+ start: () => {
54
+ if (!config.sdkToken || !config.botId || !config.serverUrl) {
55
+ logger.warn('[EyeClaw] WebSocket not started: missing config (sdkToken, botId, serverUrl)')
56
+ return
57
+ }
58
+
59
+ const wsClient = new EyeClawWebSocketClient(api, config, getState)
60
+ wsClient.start()
61
+
62
+ // 存储客户端引用,防止被垃圾回收
63
+ ;(global as any).__eyeclaw_ws = wsClient
64
+
65
+ // 打印启动信息
66
+ console.log('')
67
+ console.log('╔═══════════════════════════════════════════════════════════════════════╗')
68
+ console.log('║ EyeClaw Plugin 已启动 ║')
69
+ console.log('╠═══════════════════════════════════════════════════════════════════════╣')
70
+ console.log(`║ HTTP 端点: POST http://127.0.0.1:${gatewayPort}/eyeclaw/chat ║`)
71
+ console.log(`║ WebSocket: ${config.serverUrl ? '已配置' : '未配置'} ║`)
72
+ console.log(`║ SDK Token: ${config.sdkToken ? config.sdkToken.substring(0, 8) + '...' : '未配置'} ║`)
73
+ console.log('╚═══════════════════════════════════════════════════════════════════════╝')
74
+ console.log('')
75
+
76
+ logger.info('[EyeClaw] WebSocket service started')
77
+ },
78
+ stop: () => {
79
+ const wsClient = (global as any).__eyeclaw_ws
80
+ if (wsClient) {
81
+ wsClient.stop()
82
+ delete (global as any).__eyeclaw_ws
83
+ }
84
+ logger.info('[EyeClaw] WebSocket service stopped')
85
+ },
86
+ })
87
+ } else {
88
+ logger.warn('[EyeClaw] registerService API not available, WebSocket will not start')
89
+ }
60
90
  },
61
91
  }
62
92
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eyeclaw/eyeclaw",
3
- "version": "2.2.4",
3
+ "version": "2.3.0",
4
4
  "description": "EyeClaw plugin for OpenClaw - HTTP SSE streaming + WebSocket client",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -20,15 +20,17 @@ export class EyeClawWebSocketClient {
20
20
  private ws: WebSocket | null = null
21
21
  private api: OpenClawPluginApi
22
22
  private config: EyeClawConfig
23
+ private getState: () => any
23
24
  private reconnectAttempts = 0
24
25
  private maxReconnectAttempts = 5
25
26
  private reconnectDelay = 3000
26
27
  private subscribed = false
27
28
  private pingInterval: any = null
28
29
 
29
- constructor(api: OpenClawPluginApi, config: EyeClawConfig) {
30
+ constructor(api: OpenClawPluginApi, config: EyeClawConfig, getState: () => any) {
30
31
  this.api = api
31
32
  this.config = config
33
+ this.getState = getState
32
34
  }
33
35
 
34
36
  /**
@@ -174,38 +176,38 @@ export class EyeClawWebSocketClient {
174
176
 
175
177
  /**
176
178
  * 使用 OpenClaw API 处理消息(流式)
179
+ * 调用自己的 HTTP 端点 /eyeclaw/chat
177
180
  */
178
181
  private async processWithOpenClaw(message: string, sessionId?: string) {
179
- const gatewayPort = this.api.config?.gateway?.port ?? 18789
180
- const gatewayToken = this.api.config?.gateway?.auth?.token
181
-
182
- const openclawUrl = `http://127.0.0.1:${gatewayPort}/v1/chat/completions`
183
- const openclawBody = {
184
- model: 'openclaw:main',
185
- stream: true,
186
- messages: [{ role: 'user', content: message }],
187
- user: sessionId ? `eyeclaw:${sessionId}` : 'eyeclaw:ws',
182
+ const state = this.getState()
183
+ const gatewayPort = state.gatewayPort
184
+ const eyeclawUrl = `http://127.0.0.1:${gatewayPort}/eyeclaw/chat`
185
+
186
+ const requestBody = {
187
+ message,
188
+ session_id: sessionId,
188
189
  }
189
190
 
190
- const headers: Record<string, string> = { 'Content-Type': 'application/json' }
191
- if (gatewayToken) headers['Authorization'] = `Bearer ${gatewayToken}`
191
+ const headers: Record<string, string> = {
192
+ 'Content-Type': 'application/json',
193
+ 'Authorization': `Bearer ${this.config.sdkToken}`,
194
+ }
192
195
 
193
- this.api.logger.info(`[EyeClaw] Calling OpenClaw API: ${openclawUrl}`)
194
- this.api.logger.info(`[EyeClaw] Gateway port: ${gatewayPort}, Has token: ${!!gatewayToken}`)
196
+ this.api.logger.info(`[EyeClaw] Calling own HTTP endpoint: ${eyeclawUrl}`)
195
197
 
196
198
  try {
197
- const response = await fetch(openclawUrl, {
199
+ const response = await fetch(eyeclawUrl, {
198
200
  method: 'POST',
199
201
  headers,
200
- body: JSON.stringify(openclawBody),
202
+ body: JSON.stringify(requestBody),
201
203
  })
202
204
 
203
- this.api.logger.info(`[EyeClaw] OpenClaw response status: ${response.status}`)
205
+ this.api.logger.info(`[EyeClaw] HTTP response status: ${response.status}`)
204
206
 
205
207
  if (!response.ok) {
206
208
  const errorText = await response.text()
207
- this.api.logger.error(`[EyeClaw] OpenClaw API error details: status=${response.status}, body=${errorText}`)
208
- throw new Error(`OpenClaw API error: ${response.status} - ${errorText}`)
209
+ this.api.logger.error(`[EyeClaw] HTTP error: status=${response.status}, body=${errorText}`)
210
+ throw new Error(`HTTP error: ${response.status} - ${errorText}`)
209
211
  }
210
212
 
211
213
  const reader = response.body?.getReader()
@@ -214,7 +216,7 @@ export class EyeClawWebSocketClient {
214
216
  const decoder = new TextDecoder()
215
217
  let buffer = ''
216
218
 
217
- // 发送流式响应回 Rails
219
+ // 解析 SSE 流式响应
218
220
  while (true) {
219
221
  const { done, value } = await reader.read()
220
222
  if (done) break
@@ -225,17 +227,35 @@ export class EyeClawWebSocketClient {
225
227
 
226
228
  for (const line of lines) {
227
229
  const trimmed = line.trim()
228
- if (!trimmed.startsWith('data: ')) continue
229
- const data = trimmed.slice(6)
230
- if (data === '[DONE]') continue
231
-
232
- try {
233
- const chunk = JSON.parse(data)
234
- const content = chunk.choices?.[0]?.delta?.content
235
- if (content) {
236
- this.sendChunk(content, sessionId)
230
+
231
+ // 跳过空行和注释
232
+ if (!trimmed || trimmed.startsWith(':')) continue
233
+
234
+ // 解析 SSE 事件
235
+ if (trimmed.startsWith('event: ')) {
236
+ const eventType = trimmed.slice(7).trim()
237
+ continue
238
+ }
239
+
240
+ if (trimmed.startsWith('data: ')) {
241
+ const data = trimmed.slice(6)
242
+
243
+ try {
244
+ const eventData = JSON.parse(data)
245
+
246
+ // stream_chunk 事件:包含内容
247
+ if (eventData.content) {
248
+ this.sendChunk(eventData.content, sessionId)
249
+ }
250
+
251
+ // stream_end 事件:结束
252
+ if (eventData.stream_id && !eventData.content) {
253
+ // 流结束
254
+ }
255
+ } catch {
256
+ // 忽略无法解析的数据
237
257
  }
238
- } catch { /* ignore */ }
258
+ }
239
259
  }
240
260
  }
241
261