@eyeclaw/eyeclaw 2.0.14 → 2.2.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 CHANGED
@@ -1,31 +1,63 @@
1
+ /**
2
+ * EyeClaw SDK - OpenClaw Plugin
3
+ *
4
+ * 支持两种模式:
5
+ * 1. HTTP 端点:外部直接调用 POST /eyeclaw/chat,SSE 流式返回
6
+ * 2. WebSocket:连接 Rails 服务器,接收消息并流式返回
7
+ */
1
8
  import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
2
- import { emptyPluginConfigSchema } from 'openclaw/plugin-sdk'
3
- import { eyeclawPlugin, setRuntime } from './src/channel.js'
9
+ import { createHttpHandler, eyeclawConfigSchema } from './src/http-handler.js'
10
+ import { EyeClawWebSocketClient } from './src/websocket-client.js'
11
+ import type { EyeClawConfig } from './src/types.js'
4
12
 
5
13
  /**
6
- * EyeClaw SDK - OpenClaw Channel Plugin
7
- *
8
- * Connects local OpenClaw instance to EyeClaw platform via WebSocket.
14
+ * EyeClaw 插件
9
15
  */
10
- const plugin = {
16
+ const eyeclawPlugin = {
11
17
  id: 'eyeclaw',
12
18
  name: 'EyeClaw',
13
- description: 'EyeClaw platform channel plugin',
14
- configSchema: emptyPluginConfigSchema(),
19
+ description: 'EyeClaw 消息转发插件 - HTTP SSE 流式 + WebSocket 客户端',
20
+ configSchema: eyeclawConfigSchema,
15
21
 
16
22
  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)
23
+ const logger = api.logger
20
24
 
21
- // Register EyeClaw as a channel plugin
22
- api.registerChannel({ plugin: eyeclawPlugin })
25
+ // 解析配置
26
+ const rawConfig = api.config?.plugins?.entries?.eyeclaw?.config
27
+ const config: EyeClawConfig = {
28
+ sdkToken: rawConfig?.sdkToken || '',
29
+ botId: rawConfig?.botId || '',
30
+ serverUrl: rawConfig?.serverUrl || '',
31
+ }
32
+
33
+ // HTTP 处理器(SSE 流式)
34
+ api.registerHttpHandler(createHttpHandler(api, () => config))
35
+ logger.info('[EyeClaw] HTTP handler registered: /eyeclaw/*')
36
+
37
+ // WebSocket 客户端(连接 Rails 接收消息)
38
+ if (config.sdkToken && config.botId && config.serverUrl) {
39
+ const wsClient = new EyeClawWebSocketClient(api, config)
40
+ wsClient.start()
41
+ logger.info('[EyeClaw] WebSocket client starting...')
42
+
43
+ // 存储客户端引用,防止被垃圾回收
44
+ ;(global as any).__eyeclaw_ws = wsClient
45
+ } else {
46
+ logger.warn('[EyeClaw] WebSocket not started: missing config (sdkToken, botId, serverUrl)')
47
+ }
48
+
49
+ // 打印启动信息
50
+ const gatewayPort = api.config?.gateway?.port ?? 18789
51
+ console.log('')
52
+ console.log('╔═══════════════════════════════════════════════════════════════════════╗')
53
+ console.log('║ EyeClaw Plugin 已启动 ║')
54
+ console.log('╠═══════════════════════════════════════════════════════════════════════╣')
55
+ console.log(`║ HTTP 端点: POST http://127.0.0.1:${gatewayPort}/eyeclaw/chat ║`)
56
+ console.log(`║ WebSocket: ${config.serverUrl ? '已配置' : '未配置'} ║`)
57
+ console.log(`║ SDK Token: ${config.sdkToken ? config.sdkToken.substring(0, 8) + '...' : '未配置'} ║`)
58
+ console.log('╚═══════════════════════════════════════════════════════════════════════╝')
59
+ console.log('')
23
60
  },
24
61
  }
25
62
 
26
- export default plugin
27
-
28
- // Re-export for direct usage
29
- export { EyeClawClient } from './src/client.js'
30
- export * from './src/types.js'
31
- export { eyeclawPlugin } from './src/channel.js'
63
+ export default eyeclawPlugin
@@ -1,34 +1,21 @@
1
1
  {
2
2
  "id": "eyeclaw",
3
- "channels": ["eyeclaw"],
4
3
  "configSchema": {
5
4
  "type": "object",
6
- "additionalProperties": true,
7
5
  "properties": {
8
- "enabled": {
9
- "type": "boolean",
10
- "description": "Enable/disable the plugin"
11
- },
12
- "botId": {
13
- "type": ["string", "number"],
14
- "description": "Bot ID from EyeClaw platform"
15
- },
16
6
  "sdkToken": {
17
7
  "type": "string",
18
8
  "description": "SDK token for authentication"
19
9
  },
20
- "serverUrl": {
10
+ "botId": {
21
11
  "type": "string",
22
- "description": "EyeClaw server URL"
23
- },
24
- "reconnectInterval": {
25
- "type": "number",
26
- "description": "Reconnect interval in milliseconds"
12
+ "description": "Bot ID in EyeClaw Rails app"
27
13
  },
28
- "heartbeatInterval": {
29
- "type": "number",
30
- "description": "Heartbeat interval in milliseconds"
14
+ "serverUrl": {
15
+ "type": "string",
16
+ "description": "EyeClaw Rails server URL (e.g., http://localhost:3000)"
31
17
  }
32
- }
18
+ },
19
+ "required": ["sdkToken", "botId", "serverUrl"]
33
20
  }
34
21
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@eyeclaw/eyeclaw",
3
- "version": "2.0.14",
4
- "description": "EyeClaw channel plugin for OpenClaw - Connect your local OpenClaw instance to EyeClaw platform",
3
+ "version": "2.2.0",
4
+ "description": "EyeClaw plugin for OpenClaw - HTTP SSE streaming + WebSocket client",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
7
7
  "types": "./index.ts",
@@ -16,9 +16,9 @@
16
16
  "openclaw",
17
17
  "eyeclaw",
18
18
  "plugin",
19
- "channel",
20
- "mcp",
21
- "websocket"
19
+ "http",
20
+ "sse",
21
+ "streaming"
22
22
  ],
23
23
  "author": "EyeClaw Team",
24
24
  "license": "MIT",
@@ -35,24 +35,12 @@
35
35
  "extensions": [
36
36
  "./index.ts"
37
37
  ],
38
- "channel": {
39
- "id": "eyeclaw",
40
- "label": "EyeClaw",
41
- "selectionLabel": "EyeClaw Platform",
42
- "blurb": "Connect local OpenClaw to EyeClaw platform via WebSocket.",
43
- "order": 100
44
- },
45
38
  "install": {
46
39
  "npmSpec": "@eyeclaw/eyeclaw",
47
40
  "localPath": ".",
48
41
  "defaultChoice": "npm"
49
42
  }
50
43
  },
51
- "dependencies": {
52
- "ws": "^8.16.0",
53
- "@types/ws": "^8.5.10",
54
- "zod": "^3.22.4"
55
- },
56
44
  "peerDependencies": {
57
45
  "openclaw": ">=2026.2.3"
58
46
  },
package/src/cli.ts CHANGED
@@ -1,36 +1,34 @@
1
1
  #!/usr/bin/env node
2
- import chalk from 'chalk'
3
2
 
4
3
  /**
5
4
  * EyeClaw SDK CLI
6
5
  *
7
6
  * This file provides a minimal CLI interface for the plugin.
8
- * The actual installation is handled by: openclaw plugins install @eyeclaw/sdk
7
+ * The actual installation is handled by: openclaw plugins install @eyeclaw/eyeclaw
9
8
  */
10
9
 
11
10
  const USAGE = `
12
- ${chalk.bold.blue('EyeClaw SDK')} - Connect your local OpenClaw to EyeClaw platform
11
+ ${'Use chalk.bold.blue'}( 'EyeClaw SDK')} - Connect your local OpenClaw to EyeClaw platform
13
12
 
14
- ${chalk.bold('Installation:')}
15
- ${chalk.cyan('openclaw plugins install @eyeclaw/sdk')}
13
+ ${'Use chalk.bold'}( 'Installation:')}
14
+ ${'Use chalk.cyan'}( 'openclaw plugins install @eyeclaw/eyeclaw')}
16
15
 
17
- ${chalk.bold('Configuration:')}
18
- ${chalk.cyan('openclaw config set channels.eyeclaw.enabled true')}
19
- ${chalk.cyan('openclaw config set channels.eyeclaw.botId "your-bot-id"')}
20
- ${chalk.cyan('openclaw config set channels.eyeclaw.sdkToken "your-sdk-token"')}
21
- ${chalk.cyan('openclaw config set channels.eyeclaw.serverUrl "https://eyeclaw.io"')}
16
+ ${'Use chalk.bold'}( 'Configuration:')}
17
+ ${'Use chalk.cyan'}( 'openclaw config set plugins.eyeclaw.sdkToken "your-sdk-token"')}
22
18
 
23
- ${chalk.bold('Upgrade:')}
24
- ${chalk.cyan('openclaw plugins update eyeclaw')}
19
+ ${'Use chalk.bold'}( 'HTTP Endpoint:')}
20
+ ${'Use chalk.cyan'}( 'POST http://127.0.0.1:18789/eyeclaw/chat')}
21
+ Headers: Authorization: Bearer <sdkToken>
22
+ Body: { "message": "Hello" }
25
23
 
26
- ${chalk.bold('Uninstall:')}
27
- ${chalk.cyan('openclaw plugins uninstall eyeclaw')}
24
+ ${'Use chalk.bold'}( 'Upgrade:')}
25
+ ${'Use chalk.cyan'}( 'openclaw plugins update eyeclaw')}
28
26
 
29
- ${chalk.bold('Documentation:')}
30
- ${chalk.cyan('https://eyeclaw.io/docs')}
27
+ ${'Use chalk.bold'}( 'Uninstall:')}
28
+ ${'Use chalk.cyan'}( 'openclaw plugins uninstall eyeclaw')}
31
29
 
32
- ${chalk.bold('Support:')}
33
- ${chalk.cyan('https://github.com/eyeclaw/eyeclaw/issues')}
30
+ ${'Use chalk.bold'}( 'Documentation:')}
31
+ ${'Use chalk.cyan'}( 'https://eyeclaw.io/docs')}
34
32
  `
35
33
 
36
34
  function main() {
@@ -42,16 +40,14 @@ function main() {
42
40
  }
43
41
 
44
42
  if (args.includes('--version') || args.includes('-v')) {
45
- // Read version from package.json
46
43
  const pkg = require('../package.json')
47
44
  console.log(`v${pkg.version}`)
48
45
  process.exit(0)
49
46
  }
50
47
 
51
- // Default: show usage
52
48
  console.log(USAGE)
53
- console.log(chalk.yellow('ℹ This is an OpenClaw plugin. Please install it using:'))
54
- console.log(chalk.cyan(' openclaw plugins install @eyeclaw/eyeclaw\n'))
49
+ console.log('ℹ This is an OpenClaw plugin. Please install it using:')
50
+ console.log(' openclaw plugins install @eyeclaw/eyeclaw\n')
55
51
  }
56
52
 
57
53
  main()
@@ -0,0 +1,208 @@
1
+ /**
2
+ * EyeClaw SDK - OpenClaw HTTP Handler Plugin
3
+ *
4
+ * 直接注册 HTTP 端点,实现真正的 SSE 流式转发
5
+ */
6
+ import type { IncomingMessage, ServerResponse } from 'node:http'
7
+ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
8
+ import type { EyeClawConfig } from './types.js'
9
+
10
+ /**
11
+ * 读取 JSON 请求体
12
+ */
13
+ async function readJsonBody(req: IncomingMessage): Promise<any> {
14
+ const chunks: Buffer[] = []
15
+ return new Promise((resolve, reject) => {
16
+ req.on('data', (chunk) => chunks.push(chunk))
17
+ req.on('end', () => {
18
+ try {
19
+ const body = Buffer.concat(chunks).toString('utf-8')
20
+ resolve(body ? JSON.parse(body) : {})
21
+ } catch (e) {
22
+ reject(e)
23
+ }
24
+ })
25
+ req.on('error', (e) => reject(e))
26
+ })
27
+ }
28
+
29
+ /**
30
+ * 验证 Authorization 头
31
+ */
32
+ function verifyAuth(authHeader: string | string[] | undefined, expectedToken: string): boolean {
33
+ if (!expectedToken) return true
34
+ const header = Array.isArray(authHeader) ? authHeader[0] : authHeader
35
+ if (!header) return false
36
+ if (header.startsWith('Bearer ')) return header.slice(7) === expectedToken
37
+ return header === expectedToken
38
+ }
39
+
40
+ /**
41
+ * 格式化 SSE 响应
42
+ */
43
+ function formatSSE(event: string, data: any): string {
44
+ return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`
45
+ }
46
+
47
+ /**
48
+ * 创建 HTTP 处理器
49
+ */
50
+ export function createHttpHandler(api: OpenClawPluginApi, getConfig: () => EyeClawConfig) {
51
+ return async function handler(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
52
+ const url = new URL(req.url ?? '/', 'http://localhost')
53
+ if (!url.pathname.startsWith('/eyeclaw/')) return false
54
+
55
+ const logger = api.logger
56
+ const config = getConfig()
57
+
58
+ // 验证鉴权
59
+ const authHeader = req.headers.authorization
60
+ if (!verifyAuth(authHeader, config.sdkToken || '')) {
61
+ logger.warn('[EyeClaw] Unauthorized request')
62
+ res.statusCode = 401
63
+ res.setHeader('Content-Type', 'application/json')
64
+ res.end(JSON.stringify({ error: 'Unauthorized' }))
65
+ return true
66
+ }
67
+
68
+ if (req.method !== 'POST') {
69
+ res.statusCode = 405
70
+ res.setHeader('Allow', 'POST')
71
+ res.end('Method Not Allowed')
72
+ return true
73
+ }
74
+
75
+ try {
76
+ const body = await readJsonBody(req)
77
+ const { message, session_id, stream_id } = body
78
+
79
+ if (!message) {
80
+ res.statusCode = 400
81
+ res.setHeader('Content-Type', 'application/json')
82
+ res.end(JSON.stringify({ error: 'Missing required field: message' }))
83
+ return true
84
+ }
85
+
86
+ logger.info(`[EyeClaw] Chat: ${message.substring(0, 50)}...`)
87
+
88
+ // SSE 响应头
89
+ res.setHeader('Content-Type', 'text/event-stream')
90
+ res.setHeader('Cache-Control', 'no-cache')
91
+ res.setHeader('Connection', 'keep-alive')
92
+ res.setHeader('X-Accel-Buffering', 'no')
93
+
94
+ // 获取 Gateway 配置
95
+ const gatewayPort = api.config?.gateway?.port ?? 18789
96
+ const gatewayToken = api.config?.gateway?.auth?.token
97
+ const sessionKey = session_id ? `eyeclaw:${session_id}` : 'eyeclaw:default'
98
+
99
+ // 调用 OpenClaw
100
+ const openclawUrl = `http://127.0.0.1:${gatewayPort}/v1/chat/completions`
101
+ const openclawBody = {
102
+ model: 'openclaw:main',
103
+ stream: true,
104
+ messages: [{ role: 'user', content: message }],
105
+ user: sessionKey,
106
+ }
107
+
108
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' }
109
+ if (gatewayToken) headers['Authorization'] = `Bearer ${gatewayToken}`
110
+
111
+ logger.info(`[EyeClaw] Calling OpenClaw: ${openclawUrl}`)
112
+
113
+ const openclawResponse = await fetch(openclawUrl, {
114
+ method: 'POST',
115
+ headers,
116
+ body: JSON.stringify(openclawBody),
117
+ })
118
+
119
+ if (!openclawResponse.ok) {
120
+ const errorText = await openclawResponse.text()
121
+ throw new Error(`OpenClaw API error: ${openclawResponse.status} - ${errorText}`)
122
+ }
123
+
124
+ const reader = openclawResponse.body?.getReader()
125
+ if (!reader) throw new Error('No response body')
126
+
127
+ const decoder = new TextDecoder()
128
+ let buffer = ''
129
+ const currentStreamId = stream_id || Date.now().toString()
130
+
131
+ res.write(formatSSE('stream_start', { stream_id: currentStreamId }))
132
+
133
+ // 保活
134
+ const keepaliveInterval = setInterval(() => { try { res.write(': keepalive\n\n') } catch { clearInterval(keepaliveInterval) } }, 7000)
135
+
136
+ try {
137
+ while (true) {
138
+ const { done, value } = await reader.read()
139
+ if (done) break
140
+
141
+ buffer += decoder.decode(value, { stream: true })
142
+ const lines = buffer.split('\n')
143
+ buffer = lines.pop() || ''
144
+
145
+ for (const line of lines) {
146
+ const trimmed = line.trim()
147
+ if (!trimmed.startsWith('data: ')) continue
148
+ const data = trimmed.slice(6)
149
+ if (data === '[DONE]') continue
150
+
151
+ try {
152
+ const chunk = JSON.parse(data)
153
+ const content = chunk.choices?.[0]?.delta?.content
154
+ if (content) {
155
+ res.write(formatSSE('stream_chunk', { stream_id: currentStreamId, content }))
156
+ }
157
+ } catch { /* ignore */ }
158
+ }
159
+ }
160
+ } finally {
161
+ clearInterval(keepaliveInterval)
162
+ }
163
+
164
+ res.write(formatSSE('stream_end', { stream_id: currentStreamId }))
165
+ res.end()
166
+ logger.info(`[EyeClaw] Completed: ${currentStreamId}`)
167
+ return true
168
+
169
+ } catch (error) {
170
+ const errorMsg = error instanceof Error ? error.message : String(error)
171
+ logger.error(`[EyeClaw] Error: ${errorMsg}`)
172
+ res.write(formatSSE('stream_error', { error: errorMsg }))
173
+ res.end()
174
+ return true
175
+ }
176
+ }
177
+ }
178
+
179
+ /**
180
+ * 插件配置 Schema
181
+ */
182
+ export const eyeclawConfigSchema = {
183
+ safeParse: (value: unknown) => {
184
+ if (!value || typeof value !== 'object') {
185
+ return { success: false, errors: ['Expected config object'] }
186
+ }
187
+ const cfg = value as any
188
+ if (!cfg.sdkToken) {
189
+ return { success: false, errors: ['sdkToken is required'] }
190
+ }
191
+ if (!cfg.botId) {
192
+ return { success: false, errors: ['botId is required'] }
193
+ }
194
+ if (!cfg.serverUrl) {
195
+ return { success: false, errors: ['serverUrl is required'] }
196
+ }
197
+ return { success: true, data: cfg }
198
+ },
199
+ jsonSchema: {
200
+ type: 'object',
201
+ properties: {
202
+ sdkToken: { type: 'string', description: 'EyeClaw SDK Token' },
203
+ botId: { type: 'string', description: 'Bot ID in EyeClaw Rails app' },
204
+ serverUrl: { type: 'string', description: 'EyeClaw Rails server URL' },
205
+ },
206
+ required: ['sdkToken', 'botId', 'serverUrl'],
207
+ },
208
+ }
package/src/types.ts CHANGED
@@ -1,36 +1,9 @@
1
- // Type definitions for EyeClaw Channel Plugin
1
+ // Type definitions for EyeClaw Plugin
2
2
 
3
3
  export interface EyeClawConfig {
4
- enabled?: boolean
5
- botId: string | number
6
4
  sdkToken: string
7
- serverUrl?: string
8
- reconnectInterval?: number
9
- heartbeatInterval?: number
10
- }
11
-
12
- export interface ResolvedEyeClawAccount {
13
- accountId: string
14
- enabled: boolean
15
- configured: boolean
16
- name: string
17
- config?: EyeClawConfig
18
- }
19
-
20
- export interface PluginConfig {
21
- enabled: boolean
22
5
  botId: string
23
- sdkToken: string
24
6
  serverUrl: string
25
- reconnectInterval?: number
26
- heartbeatInterval?: number
27
- }
28
-
29
- export interface OpenClawContext {
30
- config: PluginConfig
31
- logger: Logger
32
- emit: (event: string, data: unknown) => void
33
- on: (event: string, handler: (data: unknown) => void) => void
34
7
  }
35
8
 
36
9
  export interface Logger {
@@ -40,14 +13,6 @@ export interface Logger {
40
13
  debug: (message: string) => void
41
14
  }
42
15
 
43
- export interface ChannelMessage {
44
- type: string
45
- content?: string
46
- role?: 'user' | 'assistant'
47
- timestamp?: string
48
- metadata?: Record<string, unknown>
49
- }
50
-
51
16
  export interface BotStatus {
52
17
  online: boolean
53
18
  status: string