@eyeclaw/eyeclaw 2.0.15 → 2.2.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 +51 -19
- package/openclaw.plugin.json +7 -20
- package/package.json +5 -17
- package/src/cli.ts +18 -22
- package/src/http-handler.ts +208 -0
- package/src/types.ts +1 -36
- package/src/websocket-client.ts +316 -0
- package/src/channel.ts +0 -325
- package/src/client.ts +0 -377
package/src/client.ts
DELETED
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
import WebSocket from 'ws'
|
|
2
|
-
import type { PluginConfig, Logger, ChannelMessage, BotStatus } from './types.js'
|
|
3
|
-
|
|
4
|
-
export class EyeClawClient {
|
|
5
|
-
private ws: WebSocket | null = null
|
|
6
|
-
private config: PluginConfig
|
|
7
|
-
private logger: Logger
|
|
8
|
-
private reconnectTimer: NodeJS.Timeout | null = null
|
|
9
|
-
private heartbeatTimer: NodeJS.Timeout | null = null
|
|
10
|
-
private sessionId: string | null = null
|
|
11
|
-
private connected = false
|
|
12
|
-
|
|
13
|
-
constructor(config: PluginConfig, logger: Logger) {
|
|
14
|
-
this.config = config
|
|
15
|
-
this.logger = logger
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async connect(): Promise<void> {
|
|
19
|
-
if (this.connected) {
|
|
20
|
-
this.logger.warn('Already connected to EyeClaw')
|
|
21
|
-
return
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Add sdk_token as query parameter for connection authentication
|
|
25
|
-
const wsUrl = this.config.serverUrl.replace(/^http/, 'ws') + '/cable?sdk_token=' + encodeURIComponent(this.config.sdkToken)
|
|
26
|
-
this.logger.info(`Connecting to EyeClaw: ${wsUrl.split('?')[0]}?sdk_token=***`)
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
this.ws = new WebSocket(wsUrl)
|
|
30
|
-
|
|
31
|
-
this.ws.on('open', () => this.handleOpen())
|
|
32
|
-
this.ws.on('message', (data) => this.handleMessage(data))
|
|
33
|
-
this.ws.on('error', (error) => this.handleError(error))
|
|
34
|
-
this.ws.on('close', () => this.handleClose())
|
|
35
|
-
} catch (error) {
|
|
36
|
-
this.logger.error(`Failed to create WebSocket connection: ${error}`)
|
|
37
|
-
this.scheduleReconnect()
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
private handleOpen(): void {
|
|
42
|
-
this.logger.info('WebSocket connected, subscribing to channels...')
|
|
43
|
-
this.connected = true
|
|
44
|
-
|
|
45
|
-
// Subscribe to BotChannel (for responses back to EyeClaw platform)
|
|
46
|
-
const subscribeBotChannel = {
|
|
47
|
-
command: 'subscribe',
|
|
48
|
-
identifier: JSON.stringify({
|
|
49
|
-
channel: 'BotChannel',
|
|
50
|
-
}),
|
|
51
|
-
}
|
|
52
|
-
this.send(subscribeBotChannel)
|
|
53
|
-
|
|
54
|
-
// Subscribe to bot_{id}_commands channel (for commands from Rails)
|
|
55
|
-
// This is where /sse/rokid broadcasts commands
|
|
56
|
-
const botId = this.config.botId
|
|
57
|
-
const subscribeCommands = {
|
|
58
|
-
command: 'subscribe',
|
|
59
|
-
identifier: JSON.stringify({
|
|
60
|
-
channel: `bot_${botId}_commands`,
|
|
61
|
-
}),
|
|
62
|
-
}
|
|
63
|
-
this.logger.info(`Subscribing to bot_${botId}_commands channel...`)
|
|
64
|
-
this.send(subscribeCommands)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
private handleMessage(data: WebSocket.Data): void {
|
|
68
|
-
try {
|
|
69
|
-
const rawMessage = data.toString()
|
|
70
|
-
const message = JSON.parse(rawMessage)
|
|
71
|
-
this.logger.debug(`Raw message: ${rawMessage.substring(0, 200)}`)
|
|
72
|
-
this.logger.debug(`Parsed message type: ${message.type}, identifier: ${message.identifier}`)
|
|
73
|
-
|
|
74
|
-
// ActionCable protocol messages
|
|
75
|
-
if (message.type === 'ping') {
|
|
76
|
-
// Respond to ping
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (message.type === 'welcome') {
|
|
81
|
-
this.logger.info('Received welcome message from ActionCable')
|
|
82
|
-
return
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (message.type === 'confirm_subscription') {
|
|
86
|
-
const identifier = message.identifier ? JSON.parse(message.identifier) : {}
|
|
87
|
-
this.logger.info(`✅ Subscribed to channel: ${identifier.channel || 'unknown'}`)
|
|
88
|
-
if (identifier.channel === 'BotChannel') {
|
|
89
|
-
this.startHeartbeat()
|
|
90
|
-
}
|
|
91
|
-
return
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Channel messages - handle both nested and direct formats
|
|
95
|
-
// BotChannel: message.message
|
|
96
|
-
// bot_X_commands: message.message or direct
|
|
97
|
-
const channelMessage = message.message || message
|
|
98
|
-
if (channelMessage && typeof channelMessage === 'object') {
|
|
99
|
-
this.handleChannelMessage(channelMessage)
|
|
100
|
-
}
|
|
101
|
-
} catch (error) {
|
|
102
|
-
this.logger.error(`Failed to parse message: ${error}`)
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
private handleChannelMessage(message: Record<string, unknown>): void {
|
|
107
|
-
const { type } = message
|
|
108
|
-
|
|
109
|
-
// Debug: log all message types
|
|
110
|
-
this.logger.debug(`📩 Channel message: ${JSON.stringify(message)}`)
|
|
111
|
-
|
|
112
|
-
switch (type) {
|
|
113
|
-
case 'connected':
|
|
114
|
-
this.sessionId = message.session_id as string
|
|
115
|
-
this.logger.info(`🎉 Bot connected! Session ID: ${this.sessionId}`)
|
|
116
|
-
break
|
|
117
|
-
|
|
118
|
-
case 'pong':
|
|
119
|
-
this.logger.debug('Received pong from server')
|
|
120
|
-
break
|
|
121
|
-
|
|
122
|
-
case 'status_response':
|
|
123
|
-
this.handleStatusResponse(message as unknown as BotStatus)
|
|
124
|
-
break
|
|
125
|
-
|
|
126
|
-
case 'command_received':
|
|
127
|
-
this.logger.info(`Command received by server: ${message.command}`)
|
|
128
|
-
break
|
|
129
|
-
|
|
130
|
-
case 'execute_command':
|
|
131
|
-
this.handleExecuteCommand(message)
|
|
132
|
-
break
|
|
133
|
-
|
|
134
|
-
case 'ping':
|
|
135
|
-
this.logger.debug('Received ping from dashboard')
|
|
136
|
-
// Send pong response
|
|
137
|
-
this.sendLog('info', '🏓 Pong! Bot is alive.')
|
|
138
|
-
break
|
|
139
|
-
|
|
140
|
-
case 'log':
|
|
141
|
-
this.logger.info(`[Server Log] ${message.level}: ${message.message}`)
|
|
142
|
-
break
|
|
143
|
-
|
|
144
|
-
case 'stream_chunk':
|
|
145
|
-
// Forward stream chunks to handler (will be handled by web UI)
|
|
146
|
-
const chunkPreview = typeof message.chunk === 'string' ? message.chunk.substring(0, 50) : String(message.chunk || '').substring(0, 50)
|
|
147
|
-
this.logger.debug(`Stream chunk: ${message.type} - ${chunkPreview}...`)
|
|
148
|
-
break
|
|
149
|
-
|
|
150
|
-
default:
|
|
151
|
-
this.logger.warn(`Unknown message type: ${type}`)
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private async handleExecuteCommand(message: Record<string, unknown>): Promise<void> {
|
|
156
|
-
const command = message.command as string
|
|
157
|
-
const params = (message.params as Record<string, unknown>) || {}
|
|
158
|
-
|
|
159
|
-
this.logger.info(`📥 Executing command: ${command}`)
|
|
160
|
-
|
|
161
|
-
switch (command) {
|
|
162
|
-
case 'chat': {
|
|
163
|
-
const userMessage = params.message as string
|
|
164
|
-
this.logger.info(`💬 Chat message: ${userMessage}`)
|
|
165
|
-
|
|
166
|
-
// Send user message acknowledgment
|
|
167
|
-
this.sendLog('info', `收到消息: ${userMessage}`)
|
|
168
|
-
|
|
169
|
-
// Call OpenClaw Agent via callback and wait for completion
|
|
170
|
-
await this.handleChatMessage(userMessage)
|
|
171
|
-
break
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
case 'ping': {
|
|
175
|
-
this.sendLog('info', '🏓 Pong! Bot is responding.')
|
|
176
|
-
break
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
case 'status': {
|
|
180
|
-
this.requestStatus()
|
|
181
|
-
break
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
case 'echo': {
|
|
185
|
-
const echoMessage = params.message as string
|
|
186
|
-
this.sendLog('info', `Echo: ${echoMessage}`)
|
|
187
|
-
break
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
case 'help': {
|
|
191
|
-
const helpMessage = [
|
|
192
|
-
'🤖 Available Commands:',
|
|
193
|
-
'• chat - Send a chat message',
|
|
194
|
-
'• ping - Test connection',
|
|
195
|
-
'• status - Get bot status',
|
|
196
|
-
'• echo - Echo a message',
|
|
197
|
-
'• help - Show this help',
|
|
198
|
-
].join('\n')
|
|
199
|
-
this.sendLog('info', helpMessage)
|
|
200
|
-
break
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
default:
|
|
204
|
-
this.logger.warn(`Unknown command: ${command}`)
|
|
205
|
-
this.sendLog('error', `❌ Unknown command: ${command}`)
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
private async handleChatMessage(userMessage: string): Promise<void> {
|
|
210
|
-
// This will be called by OpenClaw channel plugin via sendAgent
|
|
211
|
-
if (this.sendAgentCallback) {
|
|
212
|
-
this.logger.info('🤖 Calling OpenClaw Agent...')
|
|
213
|
-
await this.sendAgentCallback(userMessage)
|
|
214
|
-
this.logger.info('✅ OpenClaw Agent completed')
|
|
215
|
-
} else {
|
|
216
|
-
// Fallback: simple echo if not running in OpenClaw context
|
|
217
|
-
this.logger.warn('No OpenClaw Agent available, using echo mode')
|
|
218
|
-
this.sendLog('info', `💬 Echo: "${userMessage}" (OpenClaw Agent not connected)`)
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Callback to send message to OpenClaw Agent (injected by channel plugin)
|
|
223
|
-
private sendAgentCallback: ((message: string) => Promise<void>) | null = null
|
|
224
|
-
|
|
225
|
-
setSendAgentCallback(callback: (message: string) => Promise<void>): void {
|
|
226
|
-
this.sendAgentCallback = callback
|
|
227
|
-
this.logger.info('✅ OpenClaw Agent callback registered')
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
private handleStatusResponse(status: BotStatus): void {
|
|
231
|
-
this.logger.info(`Bot status: online=${status.online}, status=${status.status}, sessions=${status.active_sessions}, uptime=${Math.floor(status.uptime / 60)}m`)
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
private handleError(error: Error): void {
|
|
235
|
-
this.logger.error(`WebSocket error: ${error.message}`)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private handleClose(): void {
|
|
239
|
-
this.logger.warn('WebSocket connection closed')
|
|
240
|
-
this.connected = false
|
|
241
|
-
this.stopHeartbeat()
|
|
242
|
-
this.scheduleReconnect()
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
private scheduleReconnect(): void {
|
|
246
|
-
if (this.reconnectTimer) {
|
|
247
|
-
return
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const interval = this.config.reconnectInterval || 5000
|
|
251
|
-
this.logger.info(`Reconnecting in ${interval / 1000}s...`)
|
|
252
|
-
|
|
253
|
-
this.reconnectTimer = setTimeout(() => {
|
|
254
|
-
this.reconnectTimer = null
|
|
255
|
-
this.connect()
|
|
256
|
-
}, interval)
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
private startHeartbeat(): void {
|
|
260
|
-
const interval = this.config.heartbeatInterval || 30000
|
|
261
|
-
|
|
262
|
-
this.heartbeatTimer = setInterval(() => {
|
|
263
|
-
this.sendChannelMessage('ping', {})
|
|
264
|
-
}, interval)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
private stopHeartbeat(): void {
|
|
268
|
-
if (this.heartbeatTimer) {
|
|
269
|
-
clearInterval(this.heartbeatTimer)
|
|
270
|
-
this.heartbeatTimer = null
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
private send(message: Record<string, unknown>): void {
|
|
275
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
276
|
-
this.logger.error('Cannot send message: WebSocket not connected')
|
|
277
|
-
return
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
this.ws.send(JSON.stringify(message))
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
private sendChannelMessage(action: string, data: Record<string, unknown>): void {
|
|
284
|
-
const message = {
|
|
285
|
-
command: 'message',
|
|
286
|
-
identifier: JSON.stringify({
|
|
287
|
-
channel: 'BotChannel',
|
|
288
|
-
}),
|
|
289
|
-
data: JSON.stringify({
|
|
290
|
-
action,
|
|
291
|
-
...data,
|
|
292
|
-
}),
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
this.send(message)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
sendLog(level: string, message: string): void {
|
|
299
|
-
// Send to BotChannel
|
|
300
|
-
this.sendChannelMessage('log', {
|
|
301
|
-
level,
|
|
302
|
-
message,
|
|
303
|
-
timestamp: new Date().toISOString(),
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
// Also broadcast directly to bot_{id} stream as backup
|
|
307
|
-
this.send({
|
|
308
|
-
command: 'message',
|
|
309
|
-
identifier: JSON.stringify({
|
|
310
|
-
channel: `bot_${this.config.botId}`,
|
|
311
|
-
}),
|
|
312
|
-
data: JSON.stringify({
|
|
313
|
-
action: 'log',
|
|
314
|
-
level,
|
|
315
|
-
message,
|
|
316
|
-
timestamp: new Date().toISOString(),
|
|
317
|
-
}),
|
|
318
|
-
})
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
sendStreamChunk(type: string, streamId: string, chunk: string): void {
|
|
322
|
-
this.logger.info(`📤 Sending stream_chunk: type=${type}, streamId=${streamId}, chunk="${chunk.substring(0, 30)}..."`)
|
|
323
|
-
|
|
324
|
-
// Send to BotChannel which will broadcast to bot_{id} for Rails to receive
|
|
325
|
-
this.sendChannelMessage('stream_chunk', {
|
|
326
|
-
type,
|
|
327
|
-
stream_id: streamId,
|
|
328
|
-
chunk,
|
|
329
|
-
timestamp: new Date().toISOString(),
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
// Also broadcast directly to bot_{id} stream as backup (Rails expects stream_type)
|
|
333
|
-
this.logger.info(`📤 Also sending to bot_${this.config.botId} channel`)
|
|
334
|
-
this.send({
|
|
335
|
-
command: 'message',
|
|
336
|
-
identifier: JSON.stringify({
|
|
337
|
-
channel: `bot_${this.config.botId}`,
|
|
338
|
-
}),
|
|
339
|
-
data: JSON.stringify({
|
|
340
|
-
action: 'stream_chunk',
|
|
341
|
-
stream_type: type, // Rails expects 'stream_type'
|
|
342
|
-
stream_id: streamId,
|
|
343
|
-
chunk,
|
|
344
|
-
timestamp: new Date().toISOString(),
|
|
345
|
-
}),
|
|
346
|
-
})
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
sendCommandResult(command: string, result: unknown, error?: string): void {
|
|
350
|
-
this.sendChannelMessage('command_result', {
|
|
351
|
-
command,
|
|
352
|
-
result,
|
|
353
|
-
error,
|
|
354
|
-
timestamp: new Date().toISOString(),
|
|
355
|
-
})
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
requestStatus(): void {
|
|
359
|
-
this.sendChannelMessage('status', {})
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
disconnect(): void {
|
|
363
|
-
this.logger.info('Disconnecting from EyeClaw...')
|
|
364
|
-
this.connected = false
|
|
365
|
-
this.stopHeartbeat()
|
|
366
|
-
|
|
367
|
-
if (this.reconnectTimer) {
|
|
368
|
-
clearTimeout(this.reconnectTimer)
|
|
369
|
-
this.reconnectTimer = null
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (this.ws) {
|
|
373
|
-
this.ws.close()
|
|
374
|
-
this.ws = null
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|