@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 +56 -29
- package/package.json +1 -1
- package/src/websocket-client.ts +64 -32
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
|
-
//
|
|
34
|
-
api.
|
|
35
|
-
logger.info('[EyeClaw] HTTP handler registered: /eyeclaw/*')
|
|
33
|
+
// 获取 Gateway 端口
|
|
34
|
+
const gatewayPort = api.config?.gateway?.port ?? 18789
|
|
36
35
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
package/src/websocket-client.ts
CHANGED
|
@@ -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
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
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> = {
|
|
191
|
-
|
|
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
|
|
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(
|
|
199
|
+
const response = await fetch(eyeclawUrl, {
|
|
198
200
|
method: 'POST',
|
|
199
201
|
headers,
|
|
200
|
-
body: JSON.stringify(
|
|
202
|
+
body: JSON.stringify(requestBody),
|
|
201
203
|
})
|
|
202
204
|
|
|
203
|
-
this.api.logger.info(`[EyeClaw]
|
|
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]
|
|
208
|
-
throw new Error(`
|
|
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
|
-
//
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
}
|
|
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)
|