@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/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
- }