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