@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 +54 -24
- package/package.json +1 -1
- package/src/websocket-client.ts +50 -30
package/index.ts
CHANGED
|
@@ -30,33 +30,63 @@ const eyeclawPlugin = {
|
|
|
30
30
|
serverUrl: rawConfig?.serverUrl || '',
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
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()
|
|
@@ -214,7 +216,7 @@ export class EyeClawWebSocketClient {
|
|
|
214
216
|
const decoder = new TextDecoder()
|
|
215
217
|
let buffer = ''
|
|
216
218
|
|
|
217
|
-
//
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
}
|
|
258
|
+
}
|
|
239
259
|
}
|
|
240
260
|
}
|
|
241
261
|
|