Add client and plugin specifications
This commit is contained in:
633
docs/CLIENT_SPEC.md
Normal file
633
docs/CLIENT_SPEC.md
Normal file
@@ -0,0 +1,633 @@
|
|||||||
|
# WebSocket Client Specification
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document describes how to build a **client application** that connects to the OpenClaw WebSocket Channel plugin.
|
||||||
|
|
||||||
|
**Server URL:** `ws://your-openclaw-host:8765/ws` (configurable)
|
||||||
|
**Authentication:** API Key via `X-API-Key` header or `?key=` query parameter
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### JavaScript/Node.js Example
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import WebSocket from 'ws';
|
||||||
|
|
||||||
|
const ws = new WebSocket('ws://localhost:8765/ws?key=YOUR_API_KEY');
|
||||||
|
|
||||||
|
ws.on('open', () => {
|
||||||
|
console.log('Connected to OpenClaw!');
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', (data) => {
|
||||||
|
const message = JSON.parse(data.toString());
|
||||||
|
console.log('Received:', message);
|
||||||
|
|
||||||
|
switch (message.type) {
|
||||||
|
case 'handshake':
|
||||||
|
console.log('Server capabilities:', message.capabilities);
|
||||||
|
break;
|
||||||
|
case 'message':
|
||||||
|
console.log('Bot says:', message.content.text);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send a message to the bot
|
||||||
|
function sendMessage(text) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'message',
|
||||||
|
from: 'user-123', // Your user ID
|
||||||
|
content: {
|
||||||
|
type: 'text',
|
||||||
|
text: text
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage('Hello, OpenClaw!');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
import websocket
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
|
||||||
|
def on_message(ws, message):
|
||||||
|
data = json.loads(message)
|
||||||
|
print(f"Received: {data}")
|
||||||
|
|
||||||
|
if data['type'] == 'handshake':
|
||||||
|
print(f"Capabilities: {data['capabilities']}")
|
||||||
|
elif data['type'] == 'message':
|
||||||
|
print(f"Bot: {data['content']['text']}")
|
||||||
|
|
||||||
|
def on_open(ws):
|
||||||
|
print("Connected!")
|
||||||
|
# Send a test message
|
||||||
|
ws.send(json.dumps({
|
||||||
|
"type": "message",
|
||||||
|
"from": "user-123",
|
||||||
|
"content": {"type": "text", "text": "Hello!"}
|
||||||
|
}))
|
||||||
|
|
||||||
|
def on_error(ws, error):
|
||||||
|
print(f"Error: {error}")
|
||||||
|
|
||||||
|
def on_close(ws, close_status_code, close_msg):
|
||||||
|
print("Disconnected")
|
||||||
|
|
||||||
|
# Connect
|
||||||
|
ws = websocket.WebSocketApp(
|
||||||
|
"ws://localhost:8765/ws?key=YOUR_API_KEY",
|
||||||
|
on_message=on_message,
|
||||||
|
on_open=on_open,
|
||||||
|
on_error=on_error,
|
||||||
|
on_close=on_close
|
||||||
|
)
|
||||||
|
|
||||||
|
ws.run_forever()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Connection
|
||||||
|
|
||||||
|
### URL Format
|
||||||
|
|
||||||
|
```
|
||||||
|
ws://<host>:<port>/<path>?key=<api_key>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Default values:**
|
||||||
|
- Host: `localhost`
|
||||||
|
- Port: `8765`
|
||||||
|
- Path: `/ws`
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
Provide API key in one of two ways:
|
||||||
|
|
||||||
|
**1. Query Parameter (recommended for WebSocket):**
|
||||||
|
```
|
||||||
|
ws://localhost:8765/ws?key=sk_test_abc123
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. HTTP Header (during upgrade):**
|
||||||
|
```javascript
|
||||||
|
const ws = new WebSocket('ws://localhost:8765/ws', {
|
||||||
|
headers: {
|
||||||
|
'X-API-Key': 'sk_test_abc123'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection Response
|
||||||
|
|
||||||
|
**Success:** Connection established, server sends handshake message.
|
||||||
|
|
||||||
|
**Failure:** Connection closed with error code:
|
||||||
|
- `1008` - Invalid/missing API key
|
||||||
|
- `1003` - No accounts configured
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Message Protocol
|
||||||
|
|
||||||
|
### Server → Client
|
||||||
|
|
||||||
|
#### 1. Handshake (sent immediately on connect)
|
||||||
|
|
||||||
|
Sent once when connection is established.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "handshake",
|
||||||
|
"accountId": "default",
|
||||||
|
"capabilities": {
|
||||||
|
"text": true,
|
||||||
|
"images": false,
|
||||||
|
"files": false,
|
||||||
|
"voice": false,
|
||||||
|
"reactions": false,
|
||||||
|
"threads": false,
|
||||||
|
"typing": false
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"serverName": "Production",
|
||||||
|
"region": "eu-west"
|
||||||
|
},
|
||||||
|
"timestamp": 1703275200000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
- `capabilities` - What this account supports
|
||||||
|
- `metadata` - Custom metadata from config
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. Text Message
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"to": "user-123",
|
||||||
|
"content": {
|
||||||
|
"type": "text",
|
||||||
|
"text": "Hello! How can I help?"
|
||||||
|
},
|
||||||
|
"timestamp": 1703275201000,
|
||||||
|
"messageId": "msg_abc123",
|
||||||
|
"threadId": "thread_xyz" // optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. Image Message
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"to": "user-123",
|
||||||
|
"content": {
|
||||||
|
"type": "image",
|
||||||
|
"url": "https://example.com/image.png",
|
||||||
|
"alt": "Description of image"
|
||||||
|
},
|
||||||
|
"timestamp": 1703275202000,
|
||||||
|
"messageId": "msg_def456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4. File Message
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"to": "user-123",
|
||||||
|
"content": {
|
||||||
|
"type": "file",
|
||||||
|
"url": "https://example.com/document.pdf",
|
||||||
|
"name": "document.pdf",
|
||||||
|
"size": 1024567
|
||||||
|
},
|
||||||
|
"timestamp": 1703275203000,
|
||||||
|
"messageId": "msg_ghi789"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 5. Voice Message
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"to": "user-123",
|
||||||
|
"content": {
|
||||||
|
"type": "voice",
|
||||||
|
"url": "https://example.com/audio.ogg",
|
||||||
|
"duration": 45.2
|
||||||
|
},
|
||||||
|
"timestamp": 1703275204000,
|
||||||
|
"messageId": "msg_jkl012"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 6. Typing Indicator
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "typing",
|
||||||
|
"to": "user-123",
|
||||||
|
"isTyping": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When `isTyping: false` — bot stopped typing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 7. Pairing Code Request
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "pairing_code",
|
||||||
|
"code": "ABC123",
|
||||||
|
"target": "user-456",
|
||||||
|
"message": "Send this code to verify your identity:"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Used for DM pairing flow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Client → Server
|
||||||
|
|
||||||
|
#### 1. Text Message
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"from": "user-123",
|
||||||
|
"content": {
|
||||||
|
"type": "text",
|
||||||
|
"text": "Hello, bot!"
|
||||||
|
},
|
||||||
|
"threadId": "thread_xyz", // optional, for threaded conversations
|
||||||
|
"timestamp": 1703275200000 // optional, server will add if missing
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Required fields:**
|
||||||
|
- `type` - Must be `"message"`
|
||||||
|
- `from` - Your user ID (must be in account's `allowFrom` list if DM policy is `allowlist`)
|
||||||
|
- `content.type` - Message type (`"text"`, `"image"`, `"file"`, `"voice"`)
|
||||||
|
- `content.text` - For text messages
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. Image Message
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"from": "user-123",
|
||||||
|
"content": {
|
||||||
|
"type": "image",
|
||||||
|
"url": "https://example.com/user-image.png",
|
||||||
|
"alt": "User uploaded image"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Only works if account has `capabilities.images: true`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. File Message
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"from": "user-123",
|
||||||
|
"content": {
|
||||||
|
"type": "file",
|
||||||
|
"url": "https://example.com/user-file.pdf",
|
||||||
|
"name": "my-document.pdf"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Only works if account has `capabilities.files: true`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4. Voice Message
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"from": "user-123",
|
||||||
|
"content": {
|
||||||
|
"type": "voice",
|
||||||
|
"url": "https://example.com/user-voice.ogg",
|
||||||
|
"duration": 30.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Only works if account has `capabilities.voice: true`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
The server advertises capabilities during handshake. **Only send message types that are enabled:**
|
||||||
|
|
||||||
|
| Capability | Can Send | Can Receive |
|
||||||
|
|------------|----------|-------------|
|
||||||
|
| `text: true` | ✅ Text messages | ✅ Text messages |
|
||||||
|
| `images: true` | ✅ Image messages | ✅ Image messages |
|
||||||
|
| `files: true` | ✅ File messages | ✅ File messages |
|
||||||
|
| `voice: true` | ✅ Voice messages | ✅ Voice messages |
|
||||||
|
| `reactions: true` | ❌ (future) | ❌ (future) |
|
||||||
|
| `threads: true` | ✅ With `threadId` | ✅ With `threadId` |
|
||||||
|
| `typing: true` | ❌ (future) | ✅ Typing indicators |
|
||||||
|
|
||||||
|
**Important:** If you send a message type that's not enabled, the server will log a warning and drop the message.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DM Security
|
||||||
|
|
||||||
|
The server enforces DM policies per account:
|
||||||
|
|
||||||
|
### `allowlist` (default)
|
||||||
|
- Only users in `allowFrom` list can send messages
|
||||||
|
- Check handshake metadata or config for allowed users
|
||||||
|
|
||||||
|
### `open`
|
||||||
|
- Any user can send messages
|
||||||
|
- No restrictions
|
||||||
|
|
||||||
|
### `closed`
|
||||||
|
- No DMs allowed
|
||||||
|
- All messages rejected
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Threading
|
||||||
|
|
||||||
|
If `capabilities.threads: true`, you can use threaded conversations:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"from": "user-123",
|
||||||
|
"content": {"type": "text", "text": "Reply in thread"},
|
||||||
|
"threadId": "unique-thread-id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Thread ID rules:**
|
||||||
|
- Must be consistent for related messages
|
||||||
|
- Can be any string (UUID, conversation ID, etc.)
|
||||||
|
- Server uses this to group messages
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Invalid JSON
|
||||||
|
Server responds with error message:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "error",
|
||||||
|
"error": "Invalid message format"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unsupported Message Type
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "error",
|
||||||
|
"error": "Unsupported message type: xyz"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Capability Disabled
|
||||||
|
Message is silently dropped (check server logs).
|
||||||
|
|
||||||
|
### Connection Lost
|
||||||
|
Implement reconnection logic:
|
||||||
|
```javascript
|
||||||
|
ws.on('close', () => {
|
||||||
|
console.log('Disconnected, reconnecting...');
|
||||||
|
setTimeout(() => connect(), 5000);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Example: Chat Client
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import WebSocket from 'ws';
|
||||||
|
|
||||||
|
class OpenClawClient {
|
||||||
|
constructor(url, apiKey, userId) {
|
||||||
|
this.url = url;
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
this.userId = userId;
|
||||||
|
this.ws = null;
|
||||||
|
this.capabilities = null;
|
||||||
|
this.messageHandlers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const wsUrl = `${this.url}?key=${this.apiKey}`;
|
||||||
|
this.ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
this.ws.on('open', () => {
|
||||||
|
console.log('Connected to OpenClaw');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('message', (data) => {
|
||||||
|
const message = JSON.parse(data.toString());
|
||||||
|
this.handleMessage(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('error', (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('close', () => {
|
||||||
|
console.log('Disconnected, reconnecting in 5s...');
|
||||||
|
setTimeout(() => this.connect(), 5000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessage(message) {
|
||||||
|
switch (message.type) {
|
||||||
|
case 'handshake':
|
||||||
|
this.capabilities = message.capabilities;
|
||||||
|
console.log('Server capabilities:', message.capabilities);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'message':
|
||||||
|
// Bot sent a message
|
||||||
|
this.messageHandlers.forEach(h => h(message));
|
||||||
|
if (message.content.type === 'text') {
|
||||||
|
console.log('Bot:', message.content.text);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'typing':
|
||||||
|
if (message.isTyping) {
|
||||||
|
console.log('Bot is typing...');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'error':
|
||||||
|
console.error('Server error:', message.error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage(handler) {
|
||||||
|
this.messageHandlers.push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
send(text) {
|
||||||
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||||
|
throw new Error('Not connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.send(JSON.stringify({
|
||||||
|
type: 'message',
|
||||||
|
from: this.userId,
|
||||||
|
content: {
|
||||||
|
type: 'text',
|
||||||
|
text: text
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const client = new OpenClawClient(
|
||||||
|
'ws://localhost:8765/ws',
|
||||||
|
'YOUR_API_KEY',
|
||||||
|
'user-123'
|
||||||
|
);
|
||||||
|
|
||||||
|
client.connect().then(() => {
|
||||||
|
client.onMessage((msg) => {
|
||||||
|
console.log('Received:', msg.content.text);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.send('Hello, OpenClaw!');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Using wscat
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install
|
||||||
|
npm install -g wscat
|
||||||
|
|
||||||
|
# Connect
|
||||||
|
wscat -c "ws://localhost:8765/ws?key=YOUR_API_KEY"
|
||||||
|
|
||||||
|
# Send message
|
||||||
|
> {"type":"message","from":"user-123","content":{"type":"text","text":"Hello!"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Python websocket-client
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install websocket-client
|
||||||
|
python test_client.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Example
|
||||||
|
|
||||||
|
Server-side config (`openclaw.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"channels": {
|
||||||
|
"websocket": {
|
||||||
|
"port": 8765,
|
||||||
|
"path": "/ws",
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"accountId": "my-app",
|
||||||
|
"apiKey": "sk_test_abc123",
|
||||||
|
"allowFrom": ["user-123", "user-456"],
|
||||||
|
"dmPolicy": "allowlist",
|
||||||
|
"capabilities": {
|
||||||
|
"text": true,
|
||||||
|
"images": true,
|
||||||
|
"files": false,
|
||||||
|
"voice": false,
|
||||||
|
"reactions": false,
|
||||||
|
"threads": true,
|
||||||
|
"typing": true
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"appName": "My Chat App",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
1. **Use HTTPS/WSS in production** - Always use secure WebSocket (`wss://`) in production
|
||||||
|
2. **Rotate API keys** - Change API keys periodically
|
||||||
|
3. **Validate user IDs** - Ensure `from` field matches expected users
|
||||||
|
4. **Rate limiting** - Implement client-side rate limiting
|
||||||
|
5. **Error handling** - Don't expose internal errors to users
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
- OpenClaw Docs: https://docs.openclaw.ai
|
||||||
|
- Discord: https://discord.com/invite/clawd
|
||||||
|
- GitHub: https://github.com/openclaw/openclaw
|
||||||
400
docs/PLUGIN_SPEC.md
Executable file
400
docs/PLUGIN_SPEC.md
Executable file
@@ -0,0 +1,400 @@
|
|||||||
|
# WebSocket Channel Plugin Specification
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Build an OpenClaw channel plugin that enables custom messaging platforms to connect via WebSocket. The plugin provides a flexible, configurable foundation where each channel instance can define its own capabilities and requirements.
|
||||||
|
|
||||||
|
**Plugin Name:** `openclaw-websocket-channel`
|
||||||
|
**Plugin ID:** `websocket-channel`
|
||||||
|
**Channel ID:** `websocket`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Options
|
||||||
|
|
||||||
|
### Option A: WebSocket Server Inside Gateway (Recommended)
|
||||||
|
|
||||||
|
The WebSocket server runs as part of the OpenClaw Gateway process.
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Single deployment unit — no separate service to manage
|
||||||
|
- Direct access to OpenClaw runtime APIs (TTS, STT, subagents)
|
||||||
|
- Lower latency — no network hop between gateway and websocket server
|
||||||
|
- Unified logging and diagnostics
|
||||||
|
- Easier authentication via gateway's existing auth system
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- Gateway must expose WebSocket port publicly (or via reverse proxy)
|
||||||
|
- All WebSocket connections terminate on the gateway host
|
||||||
|
- Less isolation — buggy WebSocket code could affect gateway stability
|
||||||
|
|
||||||
|
**Best for:** Most use cases, especially when you control the gateway deployment.
|
||||||
|
|
||||||
|
### Option B: Separate WebSocket Service
|
||||||
|
|
||||||
|
A standalone WebSocket service connects to OpenClaw via HTTP routes or a sidecar pattern.
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Isolated failure domain — WebSocket issues don't crash gateway
|
||||||
|
- Can scale WebSocket connections independently
|
||||||
|
- Easier to deploy WebSocket server closer to clients (edge)
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- More complex deployment (two services to manage)
|
||||||
|
- Need secure channel between WebSocket service and Gateway
|
||||||
|
- Additional latency for message routing
|
||||||
|
- Must implement custom bridging logic
|
||||||
|
|
||||||
|
**Best for:** High-scale deployments or when WebSocket server must run in a different network zone.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
**Use Option A** (WebSocket server inside Gateway) for this plugin. It's simpler, more integrated, and matches the pattern used by other channel plugins (Discord, Telegram, etc.).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Requirements
|
||||||
|
|
||||||
|
### 1. Package Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
extensions/websocket-channel/
|
||||||
|
├── package.json # openclaw.channel metadata
|
||||||
|
├── openclaw.plugin.json # Manifest with config schema
|
||||||
|
├── index.ts # defineChannelPluginEntry
|
||||||
|
├── setup-entry.ts # defineSetupPluginEntry
|
||||||
|
└── src/
|
||||||
|
├── channel.ts # WebSocketChannelPlugin
|
||||||
|
├── websocket-server.ts # WebSocket server implementation
|
||||||
|
├── client.ts # WebSocket client connection handler
|
||||||
|
├── auth.ts # API key authentication
|
||||||
|
├── config.ts # Channel configuration types
|
||||||
|
└── channel.test.ts # Tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Manifest (`openclaw.plugin.json`)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "websocket-channel",
|
||||||
|
"kind": "channel",
|
||||||
|
"channels": ["websocket"],
|
||||||
|
"name": "WebSocket Channel",
|
||||||
|
"description": "Custom WebSocket-based messaging channel for OpenClaw",
|
||||||
|
"configSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"websocket": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"port": { "type": "number", "default": 8765 },
|
||||||
|
"path": { "type": "string", "default": "/ws" },
|
||||||
|
"accounts": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"accountId": { "type": "string" },
|
||||||
|
"apiKey": { "type": "string" },
|
||||||
|
"allowFrom": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"dmPolicy": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["allowlist", "open", "closed"],
|
||||||
|
"default": "allowlist"
|
||||||
|
},
|
||||||
|
"capabilities": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"text": { "type": "boolean", "default": true },
|
||||||
|
"images": { "type": "boolean", "default": false },
|
||||||
|
"files": { "type": "boolean", "default": false },
|
||||||
|
"voice": { "type": "boolean", "default": false },
|
||||||
|
"reactions": { "type": "boolean", "default": false },
|
||||||
|
"threads": { "type": "boolean", "default": false },
|
||||||
|
"typing": { "type": "boolean", "default": false }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Custom metadata exposed to clients"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["accountId", "apiKey"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["accounts"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["websocket"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Channel Plugin Implementation
|
||||||
|
|
||||||
|
#### Core Interface
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
createChatChannelPlugin,
|
||||||
|
createChannelPluginBase,
|
||||||
|
} from "openclaw/plugin-sdk/core";
|
||||||
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
||||||
|
|
||||||
|
type WebSocketAccount = {
|
||||||
|
accountId: string | null;
|
||||||
|
apiKey: string;
|
||||||
|
allowFrom: string[];
|
||||||
|
dmPolicy: string;
|
||||||
|
capabilities: ChannelCapabilities;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChannelCapabilities = {
|
||||||
|
text: boolean;
|
||||||
|
images: boolean;
|
||||||
|
files: boolean;
|
||||||
|
voice: boolean;
|
||||||
|
reactions: boolean;
|
||||||
|
threads: boolean;
|
||||||
|
typing: boolean;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### WebSocket Server
|
||||||
|
|
||||||
|
- Use `ws` package (or native Bun WebSocket if available)
|
||||||
|
- Listen on configurable port/path from config
|
||||||
|
- Support multiple accounts (multi-tenant)
|
||||||
|
- Authenticate connections via `X-API-Key` header or query param
|
||||||
|
|
||||||
|
#### Message Protocol
|
||||||
|
|
||||||
|
Define a JSON message format for WebSocket communication:
|
||||||
|
|
||||||
|
**Client → Server (inbound):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"accountId": "account-123",
|
||||||
|
"from": "user-456",
|
||||||
|
"content": {
|
||||||
|
"type": "text",
|
||||||
|
"text": "Hello!"
|
||||||
|
},
|
||||||
|
"timestamp": 1703275200000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Server → Client (outbound):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"to": "user-456",
|
||||||
|
"content": {
|
||||||
|
"type": "text",
|
||||||
|
"text": "Hi there!"
|
||||||
|
},
|
||||||
|
"timestamp": 1703275201000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Server → Client (capabilities advertisement):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "handshake",
|
||||||
|
"accountId": "account-123",
|
||||||
|
"capabilities": {
|
||||||
|
"text": true,
|
||||||
|
"images": true,
|
||||||
|
"files": false,
|
||||||
|
"voice": false,
|
||||||
|
"reactions": true,
|
||||||
|
"threads": true,
|
||||||
|
"typing": true
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"serverVersion": "1.0.0",
|
||||||
|
"customField": "value"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Authentication
|
||||||
|
|
||||||
|
- API key passed via `X-API-Key` header or `?key=` query param
|
||||||
|
- Validate against configured accounts
|
||||||
|
- Reject connections with invalid/missing keys
|
||||||
|
- Log authentication failures
|
||||||
|
|
||||||
|
#### DM Security
|
||||||
|
|
||||||
|
- Implement `security.dm` adapter
|
||||||
|
- Support `allowlist`, `open`, and `closed` policies
|
||||||
|
- Per-account allowFrom lists
|
||||||
|
|
||||||
|
#### Pairing
|
||||||
|
|
||||||
|
- Implement text-based pairing flow
|
||||||
|
- Send pairing code via WebSocket to client
|
||||||
|
- Client must return code to complete pairing
|
||||||
|
|
||||||
|
#### Threading
|
||||||
|
|
||||||
|
- Support both `reply` and `thread` modes
|
||||||
|
- Configurable per account or fixed
|
||||||
|
|
||||||
|
#### Outbound Messaging
|
||||||
|
|
||||||
|
- `sendText`: Send text messages via WebSocket
|
||||||
|
- `sendMedia`: Send file paths (client must fetch)
|
||||||
|
- Support typing indicators if capability enabled
|
||||||
|
|
||||||
|
### 4. HTTP Route Registration
|
||||||
|
|
||||||
|
Register WebSocket upgrade route in Gateway:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
registerFull(api) {
|
||||||
|
api.registerHttpRoute({
|
||||||
|
path: "/ws",
|
||||||
|
auth: "plugin",
|
||||||
|
handler: async (req, res) => {
|
||||||
|
// Handle WebSocket upgrade
|
||||||
|
// Delegate to websocket-server.ts
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Capabilities Advertisement
|
||||||
|
|
||||||
|
On connection handshake, send capabilities to client:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function sendHandshake(ws, account) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: "handshake",
|
||||||
|
accountId: account.accountId,
|
||||||
|
capabilities: account.capabilities,
|
||||||
|
metadata: account.metadata || {}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows clients to adapt their UI/behavior based on supported features.
|
||||||
|
|
||||||
|
### 6. Configuration Examples
|
||||||
|
|
||||||
|
#### Minimal Config
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
"channels": {
|
||||||
|
"websocket": {
|
||||||
|
"port": 8765,
|
||||||
|
"path": "/ws",
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"accountId": "default",
|
||||||
|
"apiKey": "sk_test_abc123",
|
||||||
|
"allowFrom": ["user-1", "user-2"],
|
||||||
|
"dmPolicy": "allowlist",
|
||||||
|
"capabilities": {
|
||||||
|
"text": true,
|
||||||
|
"images": false,
|
||||||
|
"files": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Full-Featured Config
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
"channels": {
|
||||||
|
"websocket": {
|
||||||
|
"port": 8765,
|
||||||
|
"path": "/ws",
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"accountId": "production",
|
||||||
|
"apiKey": "sk_prod_xyz789",
|
||||||
|
"allowFrom": [],
|
||||||
|
"dmPolicy": "open",
|
||||||
|
"capabilities": {
|
||||||
|
"text": true,
|
||||||
|
"images": true,
|
||||||
|
"files": true,
|
||||||
|
"voice": true,
|
||||||
|
"reactions": true,
|
||||||
|
"threads": true,
|
||||||
|
"typing": true
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"serverName": "Production WS",
|
||||||
|
"region": "eu-west"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Testing Requirements
|
||||||
|
|
||||||
|
- Unit tests for account resolution
|
||||||
|
- Unit tests for capability validation
|
||||||
|
- Integration tests for WebSocket handshake
|
||||||
|
- Integration tests for message send/receive
|
||||||
|
- Tests for authentication failures
|
||||||
|
- Tests for DM policy enforcement
|
||||||
|
|
||||||
|
### 8. Documentation
|
||||||
|
|
||||||
|
Create `/channels/websocket.md` in OpenClaw docs with:
|
||||||
|
- Setup guide
|
||||||
|
- Configuration reference
|
||||||
|
- Message protocol specification
|
||||||
|
- Client implementation examples (JavaScript/Python)
|
||||||
|
- Security best practices
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
- [ ] Create package structure
|
||||||
|
- [ ] Write manifest with config schema
|
||||||
|
- [ ] Implement WebSocket server
|
||||||
|
- [ ] Implement authentication (API keys)
|
||||||
|
- [ ] Implement message protocol
|
||||||
|
- [ ] Implement capabilities advertisement
|
||||||
|
- [ ] Implement DM security
|
||||||
|
- [ ] Implement pairing flow
|
||||||
|
- [ ] Implement threading
|
||||||
|
- [ ] Implement outbound messaging
|
||||||
|
- [ ] Write tests
|
||||||
|
- [ ] Write documentation
|
||||||
|
- [ ] Test with sample client
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes for Developer
|
||||||
|
|
||||||
|
- Follow patterns from `extensions/discord` and `extensions/telegram`
|
||||||
|
- Use `createChatChannelPlugin` builder for cleaner code
|
||||||
|
- Keep WebSocket protocol simple and extensible
|
||||||
|
- Log all connection events for debugging
|
||||||
|
- Handle reconnection gracefully
|
||||||
|
- Support multiple concurrent connections per account
|
||||||
|
- Consider rate limiting per connection
|
||||||
Reference in New Issue
Block a user