@eyeclaw/eyeclaw 1.0.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/LICENSE +21 -0
- package/README.md +222 -0
- package/index.ts +82 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +65 -0
- package/src/cli.ts +57 -0
- package/src/client.ts +226 -0
- package/src/types.ts +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EyeClaw Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# @eyeclaw/eyeclaw
|
|
2
|
+
|
|
3
|
+
EyeClaw channel plugin for [OpenClaw](https://github.com/openclaw/openclaw) - Connect your local OpenClaw instance to the EyeClaw platform.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openclaw plugins install @eyeclaw/eyeclaw
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Windows Troubleshooting
|
|
12
|
+
|
|
13
|
+
If `openclaw plugins install` fails with `spawn npm ENOENT`, install manually:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# 1. Download the package
|
|
17
|
+
curl -O https://registry.npmjs.org/@eyeclaw/eyeclaw/-/eyeclaw-1.0.0.tgz
|
|
18
|
+
|
|
19
|
+
# 2. Install from local file
|
|
20
|
+
openclaw plugins install ./eyeclaw-1.0.0.tgz
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
### 1. Create a Bot on EyeClaw
|
|
26
|
+
|
|
27
|
+
1. Sign up at [https://eyeclaw.io](https://eyeclaw.io)
|
|
28
|
+
2. Create a new bot in your dashboard
|
|
29
|
+
3. Copy the Bot ID and SDK Token
|
|
30
|
+
|
|
31
|
+
### 2. Configure OpenClaw
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
openclaw config set channels.eyeclaw.enabled true
|
|
35
|
+
openclaw config set channels.eyeclaw.botId "your-bot-id"
|
|
36
|
+
openclaw config set channels.eyeclaw.sdkToken "your-sdk-token"
|
|
37
|
+
openclaw config set channels.eyeclaw.serverUrl "https://eyeclaw.io"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 3. Start OpenClaw
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
openclaw start
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
You should see the connection message:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
✅ Successfully subscribed to BotChannel
|
|
50
|
+
🎉 Bot connected! Session ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Configuration Options
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
channels:
|
|
57
|
+
eyeclaw:
|
|
58
|
+
enabled: true
|
|
59
|
+
botId: "1"
|
|
60
|
+
sdkToken: "your-sdk-token-here"
|
|
61
|
+
serverUrl: "https://eyeclaw.io" # or self-hosted URL
|
|
62
|
+
reconnectInterval: 5000 # milliseconds (default: 5000)
|
|
63
|
+
heartbeatInterval: 30000 # milliseconds (default: 30000)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Option | Type | Default | Description |
|
|
67
|
+
|--------|------|---------|-------------|
|
|
68
|
+
| `enabled` | boolean | `false` | Enable/disable the plugin |
|
|
69
|
+
| `botId` | string | - | Bot ID from EyeClaw dashboard |
|
|
70
|
+
| `sdkToken` | string | - | SDK Token for authentication |
|
|
71
|
+
| `serverUrl` | string | `https://eyeclaw.io` | EyeClaw server URL |
|
|
72
|
+
| `reconnectInterval` | number | `5000` | Reconnect interval in ms |
|
|
73
|
+
| `heartbeatInterval` | number | `30000` | Heartbeat interval in ms |
|
|
74
|
+
|
|
75
|
+
## Features
|
|
76
|
+
|
|
77
|
+
- **WebSocket Connection**: Real-time bidirectional communication with EyeClaw platform
|
|
78
|
+
- **Auto-Reconnect**: Automatically reconnects on connection loss
|
|
79
|
+
- **Heartbeat**: Keeps connection alive with periodic ping/pong
|
|
80
|
+
- **Real-time Monitoring**: View bot status, logs, and sessions in EyeClaw dashboard
|
|
81
|
+
- **MCP Integration**: Expose your bot as MCP plugin for platforms like Coze, Claude Desktop
|
|
82
|
+
- **Session Management**: Track connection sessions with uptime and activity logs
|
|
83
|
+
|
|
84
|
+
## MCP Plugin Integration
|
|
85
|
+
|
|
86
|
+
Your bot automatically provides an MCP plugin URL that can be used with AI platforms:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
https://eyeclaw.io/mcp/stream/your-bot-id?api_key=your-api-key
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Supported Platforms
|
|
93
|
+
|
|
94
|
+
- **Coze**: Add as custom MCP plugin
|
|
95
|
+
- **Claude Desktop**: Add to `claude_desktop_config.json`
|
|
96
|
+
- **Other MCP-compatible platforms**: Use the stream URL
|
|
97
|
+
|
|
98
|
+
## Upgrade
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
openclaw plugins update eyeclaw
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Uninstall
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
openclaw plugins uninstall eyeclaw
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Troubleshooting
|
|
111
|
+
|
|
112
|
+
### Bot cannot connect
|
|
113
|
+
|
|
114
|
+
1. Check your internet connection
|
|
115
|
+
2. Verify `botId` and `sdkToken` are correct
|
|
116
|
+
3. Ensure `serverUrl` is accessible
|
|
117
|
+
4. Check OpenClaw logs: `openclaw logs`
|
|
118
|
+
|
|
119
|
+
### Connection drops frequently
|
|
120
|
+
|
|
121
|
+
1. Check firewall settings
|
|
122
|
+
2. Verify network stability
|
|
123
|
+
3. Try increasing `heartbeatInterval`:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
openclaw config set channels.eyeclaw.heartbeatInterval 60000
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### "Unauthorized" error
|
|
130
|
+
|
|
131
|
+
Your SDK token may be invalid or expired. Regenerate it in the EyeClaw dashboard:
|
|
132
|
+
|
|
133
|
+
1. Go to your bot settings
|
|
134
|
+
2. Click "Regenerate SDK Token"
|
|
135
|
+
3. Update OpenClaw config with the new token
|
|
136
|
+
|
|
137
|
+
## Architecture
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
┌─────────────────┐ WebSocket ┌──────────────────┐
|
|
141
|
+
│ Local OpenClaw │◄────────────────────────►│ EyeClaw Platform │
|
|
142
|
+
│ Instance │ ActionCable/BotChannel │ (Rails) │
|
|
143
|
+
└─────────────────┘ └──────────────────┘
|
|
144
|
+
│ │
|
|
145
|
+
│ │
|
|
146
|
+
▼ ▼
|
|
147
|
+
Execute commands Real-time dashboard
|
|
148
|
+
Process requests Status monitoring
|
|
149
|
+
Run tools Activity logs
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### How it Works
|
|
153
|
+
|
|
154
|
+
1. **Plugin Initialization**: When OpenClaw starts, the plugin connects to EyeClaw via WebSocket
|
|
155
|
+
2. **Authentication**: Uses SDK Token to authenticate with BotChannel
|
|
156
|
+
3. **Session Creation**: Server creates a session and assigns a session ID
|
|
157
|
+
4. **Heartbeat**: Plugin sends periodic pings to keep connection alive
|
|
158
|
+
5. **Event Forwarding**: OpenClaw events (messages, tool execution, errors) are sent to EyeClaw
|
|
159
|
+
6. **Dashboard Updates**: EyeClaw dashboard shows real-time status and logs
|
|
160
|
+
|
|
161
|
+
## Development
|
|
162
|
+
|
|
163
|
+
### Build from Source
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
cd sdk
|
|
167
|
+
npm install
|
|
168
|
+
npm run build
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Link Locally
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
npm link
|
|
175
|
+
openclaw plugins install /path/to/eyeclaw/sdk
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Run TypeScript in Watch Mode
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
npm run watch
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## API Reference
|
|
185
|
+
|
|
186
|
+
### EyeClawClient
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import { EyeClawClient } from '@eyeclaw/eyeclaw'
|
|
190
|
+
|
|
191
|
+
const client = new EyeClawClient(config, logger)
|
|
192
|
+
|
|
193
|
+
// Connect to EyeClaw
|
|
194
|
+
await client.connect()
|
|
195
|
+
|
|
196
|
+
// Send log message
|
|
197
|
+
client.sendLog('info', 'Hello from OpenClaw')
|
|
198
|
+
|
|
199
|
+
// Send command result
|
|
200
|
+
client.sendCommandResult('execute_tool', { result: 'success' })
|
|
201
|
+
|
|
202
|
+
// Request bot status
|
|
203
|
+
client.requestStatus()
|
|
204
|
+
|
|
205
|
+
// Disconnect
|
|
206
|
+
client.disconnect()
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Support
|
|
210
|
+
|
|
211
|
+
- **Documentation**: [https://eyeclaw.io/docs](https://eyeclaw.io/docs)
|
|
212
|
+
- **Issues**: [https://github.com/eyeclaw/eyeclaw/issues](https://github.com/eyeclaw/eyeclaw/issues)
|
|
213
|
+
- **Discord**: [https://discord.gg/eyeclaw](https://discord.gg/eyeclaw)
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
MIT © EyeClaw Team
|
|
218
|
+
|
|
219
|
+
## Related Projects
|
|
220
|
+
|
|
221
|
+
- [OpenClaw](https://github.com/openclaw/openclaw) - The AI assistant framework
|
|
222
|
+
- [clawdbot-feishu](https://github.com/m1heng/clawdbot-feishu) - Feishu/Lark channel plugin (inspiration for this plugin)
|
package/index.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
|
|
2
|
+
import { emptyPluginConfigSchema } from 'openclaw/plugin-sdk'
|
|
3
|
+
import { EyeClawClient } from './src/client.js'
|
|
4
|
+
import type { PluginConfig } from './src/types.js'
|
|
5
|
+
|
|
6
|
+
let client: EyeClawClient | null = null
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* EyeClaw SDK - OpenClaw Channel Plugin
|
|
10
|
+
*
|
|
11
|
+
* Connects local OpenClaw instance to EyeClaw platform via WebSocket.
|
|
12
|
+
*/
|
|
13
|
+
const plugin = {
|
|
14
|
+
id: 'eyeclaw',
|
|
15
|
+
name: 'EyeClaw',
|
|
16
|
+
description: 'EyeClaw platform channel plugin',
|
|
17
|
+
configSchema: emptyPluginConfigSchema(),
|
|
18
|
+
|
|
19
|
+
register(api: OpenClawPluginApi) {
|
|
20
|
+
const runtime = api.runtime
|
|
21
|
+
const logger = runtime.logging.getChildLogger({ plugin: 'eyeclaw' })
|
|
22
|
+
const config = runtime.config as any
|
|
23
|
+
|
|
24
|
+
// Get EyeClaw config from channels.eyeclaw
|
|
25
|
+
const eyeclawConfig: PluginConfig = config?.channels?.eyeclaw || {}
|
|
26
|
+
|
|
27
|
+
// Check if enabled
|
|
28
|
+
if (eyeclawConfig.enabled === false) {
|
|
29
|
+
logger.info('Plugin disabled in config')
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Validate required fields
|
|
34
|
+
if (!eyeclawConfig.botId || !eyeclawConfig.sdkToken) {
|
|
35
|
+
logger.warn('botId and sdkToken are required')
|
|
36
|
+
logger.warn('Configure with: openclaw config set channels.eyeclaw.botId "YOUR_BOT_ID"')
|
|
37
|
+
logger.warn('Configure with: openclaw config set channels.eyeclaw.sdkToken "YOUR_SDK_TOKEN"')
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Set defaults
|
|
42
|
+
eyeclawConfig.serverUrl = eyeclawConfig.serverUrl || 'http://localhost:3000'
|
|
43
|
+
eyeclawConfig.reconnectInterval = eyeclawConfig.reconnectInterval || 5000
|
|
44
|
+
eyeclawConfig.heartbeatInterval = eyeclawConfig.heartbeatInterval || 30000
|
|
45
|
+
|
|
46
|
+
logger.info('🦞 Starting EyeClaw SDK...', { botId: eyeclawConfig.botId, serverUrl: eyeclawConfig.serverUrl })
|
|
47
|
+
|
|
48
|
+
// Create logger adapter for client (simple console logger)
|
|
49
|
+
const clientLogger = {
|
|
50
|
+
debug: (msg: string) => logger.debug(msg),
|
|
51
|
+
info: (msg: string) => logger.info(msg),
|
|
52
|
+
warn: (msg: string) => logger.warn(msg),
|
|
53
|
+
error: (msg: string) => logger.error(msg),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Create and connect WebSocket client
|
|
57
|
+
client = new EyeClawClient(eyeclawConfig, clientLogger)
|
|
58
|
+
|
|
59
|
+
client.connect().then(() => {
|
|
60
|
+
logger.info('✅ Successfully connected to EyeClaw platform')
|
|
61
|
+
}).catch((error: Error) => {
|
|
62
|
+
logger.error('Failed to connect to EyeClaw', { error: error.message })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Handle shutdown
|
|
66
|
+
const shutdown = () => {
|
|
67
|
+
if (client) {
|
|
68
|
+
logger.info('🛑 Shutting down EyeClaw SDK...')
|
|
69
|
+
client.disconnect()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
process.on('SIGINT', shutdown)
|
|
74
|
+
process.on('SIGTERM', shutdown)
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default plugin
|
|
79
|
+
|
|
80
|
+
// Re-export for direct usage
|
|
81
|
+
export { EyeClawClient } from './src/client.js'
|
|
82
|
+
export * from './src/types.js'
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@eyeclaw/eyeclaw",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "EyeClaw channel plugin for OpenClaw - Connect your local OpenClaw instance to EyeClaw platform",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.ts",
|
|
7
|
+
"types": "./index.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"index.ts",
|
|
10
|
+
"src",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"openclaw.plugin.json"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"openclaw",
|
|
17
|
+
"eyeclaw",
|
|
18
|
+
"plugin",
|
|
19
|
+
"channel",
|
|
20
|
+
"mcp",
|
|
21
|
+
"websocket"
|
|
22
|
+
],
|
|
23
|
+
"author": "EyeClaw Team",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/eyeclaw/eyeclaw.git",
|
|
28
|
+
"directory": "sdk"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/eyeclaw/eyeclaw/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://eyeclaw.io",
|
|
34
|
+
"openclaw": {
|
|
35
|
+
"extensions": ["./index.ts"],
|
|
36
|
+
"channel": {
|
|
37
|
+
"id": "eyeclaw",
|
|
38
|
+
"label": "EyeClaw",
|
|
39
|
+
"selectionLabel": "EyeClaw Platform",
|
|
40
|
+
"blurb": "Connect local OpenClaw to EyeClaw platform via WebSocket.",
|
|
41
|
+
"order": 100
|
|
42
|
+
},
|
|
43
|
+
"install": {
|
|
44
|
+
"npmSpec": "@eyeclaw/eyeclaw",
|
|
45
|
+
"localPath": ".",
|
|
46
|
+
"defaultChoice": "npm"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"ws": "^8.16.0",
|
|
51
|
+
"@types/ws": "^8.5.10",
|
|
52
|
+
"zod": "^3.22.4"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"openclaw": ">=2026.2.3"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"typescript": "^5.3.3",
|
|
59
|
+
"@types/node": "^20.11.0",
|
|
60
|
+
"openclaw": ">=2026.2.3"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=18.0.0"
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* EyeClaw SDK CLI
|
|
6
|
+
*
|
|
7
|
+
* This file provides a minimal CLI interface for the plugin.
|
|
8
|
+
* The actual installation is handled by: openclaw plugins install @eyeclaw/sdk
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const USAGE = `
|
|
12
|
+
${chalk.bold.blue('EyeClaw SDK')} - Connect your local OpenClaw to EyeClaw platform
|
|
13
|
+
|
|
14
|
+
${chalk.bold('Installation:')}
|
|
15
|
+
${chalk.cyan('openclaw plugins install @eyeclaw/sdk')}
|
|
16
|
+
|
|
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"')}
|
|
22
|
+
|
|
23
|
+
${chalk.bold('Upgrade:')}
|
|
24
|
+
${chalk.cyan('openclaw plugins update eyeclaw')}
|
|
25
|
+
|
|
26
|
+
${chalk.bold('Uninstall:')}
|
|
27
|
+
${chalk.cyan('openclaw plugins uninstall eyeclaw')}
|
|
28
|
+
|
|
29
|
+
${chalk.bold('Documentation:')}
|
|
30
|
+
${chalk.cyan('https://eyeclaw.io/docs')}
|
|
31
|
+
|
|
32
|
+
${chalk.bold('Support:')}
|
|
33
|
+
${chalk.cyan('https://github.com/eyeclaw/eyeclaw/issues')}
|
|
34
|
+
`
|
|
35
|
+
|
|
36
|
+
function main() {
|
|
37
|
+
const args = process.argv.slice(2)
|
|
38
|
+
|
|
39
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
40
|
+
console.log(USAGE)
|
|
41
|
+
process.exit(0)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
45
|
+
// Read version from package.json
|
|
46
|
+
const pkg = require('../package.json')
|
|
47
|
+
console.log(`v${pkg.version}`)
|
|
48
|
+
process.exit(0)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Default: show usage
|
|
52
|
+
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/sdk\n'))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
main()
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
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 BotChannel...')
|
|
43
|
+
this.connected = true
|
|
44
|
+
|
|
45
|
+
// Subscribe to BotChannel (token already passed in connection URL)
|
|
46
|
+
const subscribeMessage = {
|
|
47
|
+
command: 'subscribe',
|
|
48
|
+
identifier: JSON.stringify({
|
|
49
|
+
channel: 'BotChannel',
|
|
50
|
+
}),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.send(subscribeMessage)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private handleMessage(data: WebSocket.Data): void {
|
|
57
|
+
try {
|
|
58
|
+
const message = JSON.parse(data.toString())
|
|
59
|
+
this.logger.debug('Received message:', message)
|
|
60
|
+
|
|
61
|
+
// ActionCable protocol messages
|
|
62
|
+
if (message.type === 'ping') {
|
|
63
|
+
// Respond to ping
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (message.type === 'welcome') {
|
|
68
|
+
this.logger.info('Received welcome message from ActionCable')
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (message.type === 'confirm_subscription') {
|
|
73
|
+
this.logger.info('✅ Successfully subscribed to BotChannel')
|
|
74
|
+
this.startHeartbeat()
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Channel messages
|
|
79
|
+
if (message.message) {
|
|
80
|
+
this.handleChannelMessage(message.message)
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
this.logger.error('Failed to parse message:', error)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private handleChannelMessage(message: Record<string, unknown>): void {
|
|
88
|
+
const { type } = message
|
|
89
|
+
|
|
90
|
+
switch (type) {
|
|
91
|
+
case 'connected':
|
|
92
|
+
this.sessionId = message.session_id as string
|
|
93
|
+
this.logger.info(`🎉 Bot connected! Session ID: ${this.sessionId}`)
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
case 'pong':
|
|
97
|
+
this.logger.debug('Received pong from server')
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
case 'status_response':
|
|
101
|
+
this.handleStatusResponse(message as unknown as BotStatus)
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
case 'command_received':
|
|
105
|
+
this.logger.info('Command received by server:', message.command)
|
|
106
|
+
break
|
|
107
|
+
|
|
108
|
+
case 'log':
|
|
109
|
+
this.logger.info(`[Server Log] ${message.level}: ${message.message}`)
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
default:
|
|
113
|
+
this.logger.warn('Unknown message type:', type)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private handleStatusResponse(status: BotStatus): void {
|
|
118
|
+
this.logger.info('Bot status:', {
|
|
119
|
+
online: status.online,
|
|
120
|
+
status: status.status,
|
|
121
|
+
sessions: status.active_sessions,
|
|
122
|
+
uptime: `${Math.floor(status.uptime / 60)}m`,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private handleError(error: Error): void {
|
|
127
|
+
this.logger.error('WebSocket error:', error.message)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private handleClose(): void {
|
|
131
|
+
this.logger.warn('WebSocket connection closed')
|
|
132
|
+
this.connected = false
|
|
133
|
+
this.stopHeartbeat()
|
|
134
|
+
this.scheduleReconnect()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private scheduleReconnect(): void {
|
|
138
|
+
if (this.reconnectTimer) {
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const interval = this.config.reconnectInterval || 5000
|
|
143
|
+
this.logger.info(`Reconnecting in ${interval / 1000}s...`)
|
|
144
|
+
|
|
145
|
+
this.reconnectTimer = setTimeout(() => {
|
|
146
|
+
this.reconnectTimer = null
|
|
147
|
+
this.connect()
|
|
148
|
+
}, interval)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private startHeartbeat(): void {
|
|
152
|
+
const interval = this.config.heartbeatInterval || 30000
|
|
153
|
+
|
|
154
|
+
this.heartbeatTimer = setInterval(() => {
|
|
155
|
+
this.sendChannelMessage('ping', {})
|
|
156
|
+
}, interval)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private stopHeartbeat(): void {
|
|
160
|
+
if (this.heartbeatTimer) {
|
|
161
|
+
clearInterval(this.heartbeatTimer)
|
|
162
|
+
this.heartbeatTimer = null
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private send(message: Record<string, unknown>): void {
|
|
167
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
168
|
+
this.logger.error('Cannot send message: WebSocket not connected')
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.ws.send(JSON.stringify(message))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private sendChannelMessage(action: string, data: Record<string, unknown>): void {
|
|
176
|
+
const message = {
|
|
177
|
+
command: 'message',
|
|
178
|
+
identifier: JSON.stringify({
|
|
179
|
+
channel: 'BotChannel',
|
|
180
|
+
}),
|
|
181
|
+
data: JSON.stringify({
|
|
182
|
+
action,
|
|
183
|
+
...data,
|
|
184
|
+
}),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.send(message)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
sendLog(level: string, message: string): void {
|
|
191
|
+
this.sendChannelMessage('log', {
|
|
192
|
+
level,
|
|
193
|
+
message,
|
|
194
|
+
timestamp: new Date().toISOString(),
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
sendCommandResult(command: string, result: unknown, error?: string): void {
|
|
199
|
+
this.sendChannelMessage('command_result', {
|
|
200
|
+
command,
|
|
201
|
+
result,
|
|
202
|
+
error,
|
|
203
|
+
timestamp: new Date().toISOString(),
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
requestStatus(): void {
|
|
208
|
+
this.sendChannelMessage('status', {})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
disconnect(): void {
|
|
212
|
+
this.logger.info('Disconnecting from EyeClaw...')
|
|
213
|
+
this.connected = false
|
|
214
|
+
this.stopHeartbeat()
|
|
215
|
+
|
|
216
|
+
if (this.reconnectTimer) {
|
|
217
|
+
clearTimeout(this.reconnectTimer)
|
|
218
|
+
this.reconnectTimer = null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (this.ws) {
|
|
222
|
+
this.ws.close()
|
|
223
|
+
this.ws = null
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Type definitions for OpenClaw plugin system
|
|
2
|
+
// Reference: clawdbot-feishu architecture
|
|
3
|
+
|
|
4
|
+
export interface PluginConfig {
|
|
5
|
+
enabled: boolean
|
|
6
|
+
botId: string
|
|
7
|
+
sdkToken: string
|
|
8
|
+
serverUrl: string
|
|
9
|
+
reconnectInterval?: number
|
|
10
|
+
heartbeatInterval?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface OpenClawContext {
|
|
14
|
+
config: PluginConfig
|
|
15
|
+
logger: Logger
|
|
16
|
+
emit: (event: string, data: unknown) => void
|
|
17
|
+
on: (event: string, handler: (data: unknown) => void) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Logger {
|
|
21
|
+
info: (message: string) => void
|
|
22
|
+
warn: (message: string) => void
|
|
23
|
+
error: (message: string) => void
|
|
24
|
+
debug: (message: string) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ChannelMessage {
|
|
28
|
+
type: string
|
|
29
|
+
content?: string
|
|
30
|
+
role?: 'user' | 'assistant'
|
|
31
|
+
timestamp?: string
|
|
32
|
+
metadata?: Record<string, unknown>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BotStatus {
|
|
36
|
+
online: boolean
|
|
37
|
+
status: string
|
|
38
|
+
active_sessions: number
|
|
39
|
+
total_sessions: number
|
|
40
|
+
uptime: number
|
|
41
|
+
}
|