@eyeclaw/eyeclaw 2.0.8 → 2.0.10

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 -74
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.8",
3
+ "version": "2.0.10",
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
+ export function setRuntime(runtime: any) {
16
+ _runtime = runtime
17
+ }
18
+
19
+ export 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,99 +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}`)
188
-
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
- // No --json flag: use plain text streaming output
195
- ])
208
+ ctx.log?.info(`🤖 Processing message via OpenClaw dispatchReply: ${message}`)
196
209
 
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
- // Send text chunks as they arrive (real-time streaming)
209
- const lines = text.split('\n')
210
- for (const line of lines) {
211
- const trimmed = line.trim()
212
- if (trimmed && !trimmed.startsWith('{') && !trimmed.startsWith('[')) {
213
- // Skip JSON-like lines, send only plain text
214
- client.sendStreamChunk('stream_chunk', streamId, line + '\n')
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)
215
236
  }
216
- }
217
- } catch (error) {
218
- // Catch any errors in data processing to prevent WebSocket disconnect
219
- const errorMsg = error instanceof Error ? error.message : String(error)
220
- ctx.log?.error(`Error processing stdout data: ${errorMsg}`)
221
- }
222
- })
223
-
224
- // Handle stderr (errors)
225
- agentProcess.stderr?.on('data', (data: Buffer) => {
226
- const errorText = data.toString()
227
- ctx.log?.error(`Agent stderr: ${errorText}`)
228
- })
229
-
230
- // Wait for process to complete
231
- await new Promise<void>((resolve, reject) => {
232
- // Handle process completion
233
- agentProcess.on('close', (code: number) => {
234
- try {
235
- // Send stream_end event
236
- client.sendStreamChunk('stream_end', streamId, '')
237
237
 
238
- if (code === 0) {
239
- ctx.log?.info(`✅ Agent completed successfully`)
240
- resolve()
241
- } else {
242
- ctx.log?.error(`Agent exited with code ${code}`)
243
- client.sendLog('error', `❌ Agent error: process exited with code ${code}`)
244
- reject(new Error(`Agent exited with code ${code}`))
238
+ // 当主响应完成时记录
239
+ if (info.kind === 'final') {
240
+ ctx.log?.info('Main response complete')
245
241
  }
246
- } catch (error) {
247
- // Prevent errors in close handler from crashing
248
- const errorMsg = error instanceof Error ? error.message : String(error)
249
- ctx.log?.error(`Error in close handler: ${errorMsg}`)
250
- reject(error)
251
- }
252
- })
253
-
254
- // Handle process errors
255
- agentProcess.on('error', (error: Error) => {
256
- ctx.log?.error(`Failed to start agent process: ${error.message}`)
257
- client.sendStreamChunk('stream_error', streamId, error.message)
258
- client.sendLog('error', `❌ Failed to start agent: ${error.message}`)
259
- reject(error)
260
- })
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
+ },
261
248
  })
262
249
 
250
+ // 发送 stream_end
251
+ client.sendStreamChunk('stream_end', streamId, '')
252
+ ctx.log?.info(`✅ Message processed successfully`)
253
+
263
254
  } catch (error) {
264
255
  const errorMsg = error instanceof Error ? error.message : String(error)
265
- ctx.log?.error(`Failed to call OpenClaw Agent: ${errorMsg}`)
266
- // Send error notification but don't crash the WebSocket connection
256
+ ctx.log?.error(`Failed to process message: ${errorMsg}`)
257
+ // 发送错误通知
267
258
  try {
268
259
  client.sendStreamChunk('stream_error', streamId, errorMsg)
269
- client.sendLog('error', `❌ Agent error: ${errorMsg}`)
260
+ client.sendLog('error', `❌ Error: ${errorMsg}`)
270
261
  } catch (sendError) {
271
262
  ctx.log?.error(`Failed to send error notification: ${sendError}`)
272
263
  }