@eyeclaw/eyeclaw 2.2.5 → 2.3.1

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
@@ -25,41 +25,68 @@ const eyeclawPlugin = {
25
25
  // 解析配置
26
26
  const rawConfig = api.config?.plugins?.entries?.eyeclaw?.config
27
27
  const config: EyeClawConfig = {
28
- sdkToken: rawConfig?.sdkToken || '',
28
+ sdkToken: rawConfig?.sdkToken as string || '',
29
29
  botId: rawConfig?.botId ? String(rawConfig.botId) : '',
30
- serverUrl: rawConfig?.serverUrl || '',
30
+ serverUrl: rawConfig?.serverUrl as string || '',
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
- // 延迟启动 WebSocket,避免在插件加载/更新时阻塞
40
- setTimeout(() => {
41
- const wsClient = new EyeClawWebSocketClient(api, config)
42
- wsClient.start()
43
- logger.info('[EyeClaw] WebSocket client starting...')
44
-
45
- // 存储客户端引用,防止被垃圾回收
46
- ;(global as any).__eyeclaw_ws = wsClient
47
- }, 2000) // 延迟 2 秒启动
48
- } else {
49
- 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/*')
50
47
  }
51
48
 
52
- // 打印启动信息
53
- const gatewayPort = api.config?.gateway?.port ?? 18789
54
- console.log('')
55
- console.log('╔═══════════════════════════════════════════════════════════════════════╗')
56
- console.log('║ EyeClaw Plugin 已启动 ║')
57
- console.log('╠═══════════════════════════════════════════════════════════════════════╣')
58
- console.log(`║ HTTP 端点: POST http://127.0.0.1:${gatewayPort}/eyeclaw/chat ║`)
59
- console.log(`║ WebSocket: ${config.serverUrl ? '已配置' : '未配置'} ║`)
60
- console.log(`║ SDK Token: ${config.sdkToken ? config.sdkToken.substring(0, 8) + '...' : '未配置'} ║`)
61
- console.log('╚═══════════════════════════════════════════════════════════════════════╝')
62
- 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
+ }
63
90
  },
64
91
  }
65
92
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eyeclaw/eyeclaw",
3
- "version": "2.2.5",
3
+ "version": "2.3.1",
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()
@@ -213,8 +215,9 @@ export class EyeClawWebSocketClient {
213
215
 
214
216
  const decoder = new TextDecoder()
215
217
  let buffer = ''
218
+ let currentEvent = ''
216
219
 
217
- // 发送流式响应回 Rails
220
+ // 解析 SSE 流式响应
218
221
  while (true) {
219
222
  const { done, value } = await reader.read()
220
223
  if (done) break
@@ -225,22 +228,51 @@ export class EyeClawWebSocketClient {
225
228
 
226
229
  for (const line of lines) {
227
230
  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)
231
+
232
+ // 跳过空行和注释
233
+ if (!trimmed || trimmed.startsWith(':')) {
234
+ // 空行表示事件结束,重置 currentEvent
235
+ if (!trimmed) {
236
+ currentEvent = ''
237
+ }
238
+ continue
239
+ }
240
+
241
+ // 解析 SSE 事件类型
242
+ if (trimmed.startsWith('event: ')) {
243
+ currentEvent = trimmed.slice(7).trim()
244
+ continue
245
+ }
246
+
247
+ // 解析 SSE 数据
248
+ if (trimmed.startsWith('data: ')) {
249
+ const data = trimmed.slice(6)
250
+
251
+ try {
252
+ const eventData = JSON.parse(data)
253
+
254
+ // stream_chunk 事件:发送内容
255
+ if (currentEvent === 'stream_chunk' && eventData.content) {
256
+ this.sendChunk(eventData.content, sessionId)
257
+ }
258
+
259
+ // stream_end 事件:流结束(由 HTTP handler 发送)
260
+ if (currentEvent === 'stream_end') {
261
+ this.api.logger.info(`[EyeClaw] Stream ended: ${eventData.stream_id}`)
262
+ }
263
+
264
+ // stream_error 事件:错误
265
+ if (currentEvent === 'stream_error') {
266
+ this.api.logger.error(`[EyeClaw] Stream error: ${eventData.error}`)
267
+ }
268
+ } catch (e) {
269
+ this.api.logger.warn(`[EyeClaw] Failed to parse SSE data: ${data}`)
237
270
  }
238
- } catch { /* ignore */ }
271
+ }
239
272
  }
240
273
  }
241
274
 
242
- // 发送完成信号
243
- this.sendMessage('stream_end', { session_id: sessionId })
275
+ this.api.logger.info(`[EyeClaw] Stream processing completed for session: ${sessionId}`)
244
276
 
245
277
  } catch (error) {
246
278
  const errorMsg = error instanceof Error ? error.message : String(error)