@eyeclaw/eyeclaw 2.0.7 → 2.0.9

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.
Files changed (3) hide show
  1. package/index.ts +5 -1
  2. package/package.json +4 -2
  3. package/src/channel.ts +65 -104
package/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
2
2
  import { emptyPluginConfigSchema } from 'openclaw/plugin-sdk'
3
- import { eyeclawPlugin } from './src/channel.js'
3
+ import { eyeclawPlugin, setRuntime } from './src/channel.js'
4
4
 
5
5
  /**
6
6
  * EyeClaw SDK - OpenClaw Channel Plugin
@@ -14,6 +14,10 @@ const plugin = {
14
14
  configSchema: emptyPluginConfigSchema(),
15
15
 
16
16
  register(api: OpenClawPluginApi) {
17
+ // Save runtime for message processing (like WeCom plugin does)
18
+ // This allows channel.ts to access runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher
19
+ setRuntime(api.runtime)
20
+
17
21
  // Register EyeClaw as a channel plugin
18
22
  api.registerChannel({ plugin: eyeclawPlugin })
19
23
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eyeclaw/eyeclaw",
3
- "version": "2.0.7",
3
+ "version": "2.0.9",
4
4
  "description": "EyeClaw channel plugin for OpenClaw - Connect your local OpenClaw instance to EyeClaw platform",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -32,7 +32,9 @@
32
32
  },
33
33
  "homepage": "https://eyeclaw.io",
34
34
  "openclaw": {
35
- "extensions": ["./index.ts"],
35
+ "extensions": [
36
+ "./index.ts"
37
+ ],
36
38
  "channel": {
37
39
  "id": "eyeclaw",
38
40
  "label": "EyeClaw",
package/src/channel.ts CHANGED
@@ -6,6 +6,20 @@ import { EyeClawClient } from './client.js'
6
6
  // Active clients map (accountId -> client)
7
7
  const clients = new Map<string, EyeClawClient>()
8
8
 
9
+ // Store runtime for use in gateway.startAccount (set during plugin registration)
10
+ let _runtime: any = null
11
+
12
+ /**
13
+ * Set the plugin runtime (called during plugin registration)
14
+ */
15
+ function setRuntime(runtime: any) {
16
+ _runtime = runtime
17
+ }
18
+
19
+ function getRuntime() {
20
+ return _runtime
21
+ }
22
+
9
23
  /**
10
24
  * Resolve EyeClaw account configuration
11
25
  */
@@ -138,9 +152,9 @@ export const eyeclawPlugin: ChannelPlugin<ResolvedEyeClawAccount> = {
138
152
  probe,
139
153
  }),
140
154
  },
141
-
155
+
142
156
  gateway: {
143
- startAccount: async (ctx) => {
157
+ startAccount: async (ctx: any) => {
144
158
  const account = resolveEyeClawAccount(ctx.cfg, ctx.accountId)
145
159
 
146
160
  if (!account.configured || !account.config) {
@@ -174,129 +188,76 @@ export const eyeclawPlugin: ChannelPlugin<ResolvedEyeClawAccount> = {
174
188
  error: (msg: string) => ctx.log?.error(msg),
175
189
  }
176
190
 
191
+ // Get runtime from module-level storage (set during register)
192
+ const runtime = getRuntime()
193
+ if (!runtime) {
194
+ throw new Error('OpenClaw runtime not available - did you install the plugin correctly?')
195
+ }
196
+
177
197
  // Create and connect client
178
198
  const client = new EyeClawClient(clientConfig, logger)
179
199
  clients.set(ctx.accountId, client)
180
200
 
181
201
  // Register OpenClaw Agent callback for chat messages
202
+ // Use runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher for true streaming
182
203
  client.setSendAgentCallback(async (message: string) => {
183
- const { spawn } = await import('child_process')
184
204
  const streamId = Date.now().toString()
205
+ const streamKey = 'eyeclaw-web-chat'
185
206
 
186
207
  try {
187
- ctx.log?.info(`🤖 Sending message to OpenClaw Agent: ${message}`)
208
+ ctx.log?.info(`🤖 Processing message via OpenClaw dispatchReply: ${message}`)
188
209
 
189
- // Spawn openclaw agent process for streaming output
190
- const agentProcess = spawn('openclaw', [
191
- 'agent',
192
- '--session-id', 'eyeclaw-web-chat',
193
- '--message', message,
194
- '--json'
195
- ])
196
-
197
- let outputBuffer = ''
198
-
199
- // Send stream_start event
210
+ // 发送 stream_start
200
211
  client.sendStreamChunk('stream_start', streamId, '')
201
212
 
202
- // Handle stdout (streaming response)
203
- agentProcess.stdout?.on('data', (data: Buffer) => {
204
- try {
205
- const text = data.toString()
206
- outputBuffer += text
207
-
208
- // For streaming text output, send each chunk immediately
209
- // Try to parse as JSON first
210
- try {
211
- const parsed = JSON.parse(outputBuffer)
212
- // If we can parse the complete JSON, extract the text
213
- const response = parsed.result?.payloads?.[0]?.text
214
- if (response) {
215
- // Send the text in chunks
216
- const chunkSize = 50 // Send ~50 chars at a time
217
- for (let i = 0; i < response.length; i += chunkSize) {
218
- const chunk = response.substring(i, i + chunkSize)
219
- client.sendStreamChunk('stream_chunk', streamId, chunk)
220
- }
221
- }
222
- outputBuffer = '' // Clear buffer after successful parse
223
- } catch (e) {
224
- // Not valid JSON yet, check if we have complete lines to send
225
- const lines = outputBuffer.split('\n')
226
- // Keep last incomplete line in buffer
227
- if (lines.length > 1) {
228
- for (let i = 0; i < lines.length - 1; i++) {
229
- if (lines[i].trim()) {
230
- client.sendStreamChunk('stream_chunk', streamId, lines[i] + '\n')
231
- }
232
- }
233
- outputBuffer = lines[lines.length - 1]
234
- }
235
- }
236
- } catch (error) {
237
- // Catch any errors in data processing to prevent WebSocket disconnect
238
- const errorMsg = error instanceof Error ? error.message : String(error)
239
- ctx.log?.error(`Error processing stdout data: ${errorMsg}`)
240
- }
241
- })
242
-
243
- // Handle stderr (errors)
244
- agentProcess.stderr?.on('data', (data: Buffer) => {
245
- const errorText = data.toString()
246
- ctx.log?.error(`Agent stderr: ${errorText}`)
247
- })
248
-
249
- // Wait for process to complete
250
- await new Promise<void>((resolve, reject) => {
251
- // Handle process completion
252
- agentProcess.on('close', (code: number) => {
253
- try {
254
- // Send any remaining buffered content
255
- if (outputBuffer.trim()) {
256
- try {
257
- const parsed = JSON.parse(outputBuffer)
258
- const response = parsed.result?.payloads?.[0]?.text || outputBuffer.trim()
259
- client.sendStreamChunk('stream_chunk', streamId, response)
260
- } catch (e) {
261
- client.sendStreamChunk('stream_chunk', streamId, outputBuffer.trim())
262
- }
213
+ // 使用 OpenClaw dispatchReplyWithBufferedBlockDispatcher 实现真正的流式
214
+ // 这和 WeCom 插件使用的方式完全相同
215
+ // API 路径: runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher
216
+ await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
217
+ ctx: {
218
+ // Inbound message context
219
+ sessionKey: `eyeclaw:${streamKey}`,
220
+ messageKey: `eyeclaw:${streamKey}:${streamId}`,
221
+ peerKey: streamKey,
222
+ message: {
223
+ role: 'user',
224
+ content: message,
225
+ roleDetail: 'user',
226
+ },
227
+ },
228
+ cfg: ctx.cfg,
229
+ dispatcherOptions: {
230
+ // 流式回调 - LLM 每生成一块文本就实时调用
231
+ deliver: async (payload: any, info: any) => {
232
+ const text = payload.text || ''
233
+ if (text) {
234
+ ctx.log?.debug(`Delivering chunk: ${text.substring(0, 50)}...`)
235
+ client.sendStreamChunk('stream_chunk', streamId, text)
263
236
  }
264
237
 
265
- // Send stream_end event
266
- client.sendStreamChunk('stream_end', streamId, '')
267
-
268
- if (code === 0) {
269
- ctx.log?.info(`✅ Agent completed successfully`)
270
- resolve()
271
- } else {
272
- ctx.log?.error(`Agent exited with code ${code}`)
273
- client.sendLog('error', `❌ Agent error: process exited with code ${code}`)
274
- reject(new Error(`Agent exited with code ${code}`))
238
+ // 当主响应完成时记录
239
+ if (info.kind === 'final') {
240
+ ctx.log?.info('Main response complete')
275
241
  }
276
- } catch (error) {
277
- // Prevent errors in close handler from crashing
278
- const errorMsg = error instanceof Error ? error.message : String(error)
279
- ctx.log?.error(`Error in close handler: ${errorMsg}`)
280
- reject(error)
281
- }
282
- })
283
-
284
- // Handle process errors
285
- agentProcess.on('error', (error: Error) => {
286
- ctx.log?.error(`Failed to start agent process: ${error.message}`)
287
- client.sendStreamChunk('stream_error', streamId, error.message)
288
- client.sendLog('error', `❌ Failed to start agent: ${error.message}`)
289
- reject(error)
290
- })
242
+ },
243
+ onError: async (error: any, info: any) => {
244
+ ctx.log?.error(`Reply failed: ${error.message}`)
245
+ client.sendStreamChunk('stream_error', streamId, error.message)
246
+ },
247
+ },
291
248
  })
292
249
 
250
+ // 发送 stream_end
251
+ client.sendStreamChunk('stream_end', streamId, '')
252
+ ctx.log?.info(`✅ Message processed successfully`)
253
+
293
254
  } catch (error) {
294
255
  const errorMsg = error instanceof Error ? error.message : String(error)
295
- ctx.log?.error(`Failed to call OpenClaw Agent: ${errorMsg}`)
296
- // Send error notification but don't crash the WebSocket connection
256
+ ctx.log?.error(`Failed to process message: ${errorMsg}`)
257
+ // 发送错误通知
297
258
  try {
298
259
  client.sendStreamChunk('stream_error', streamId, errorMsg)
299
- client.sendLog('error', `❌ Agent error: ${errorMsg}`)
260
+ client.sendLog('error', `❌ Error: ${errorMsg}`)
300
261
  } catch (sendError) {
301
262
  ctx.log?.error(`Failed to send error notification: ${sendError}`)
302
263
  }