Initial commit: VoceChat plugin for OpenClaw

This commit is contained in:
root 2026-03-13 19:14:17 +08:00
commit bbc862273e
8 changed files with 510 additions and 0 deletions

8
.env.example Normal file
View File

@ -0,0 +1,8 @@
# VoceChat Plugin Environment Variables
Optional environment variables:
- `VOCECHAT_SERVER_URL` - Default server URL
- `VOCECHAT_API_KEY` - Default API key
These can be used instead of or to override config file settings.

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
node_modules/
dist/
*.log
.DS_Store
.env
.vscode/
.idea/

74
README.md Normal file
View File

@ -0,0 +1,74 @@
# OpenClaw VoceChat Plugin
VoceChat channel plugin for OpenClaw.
## Features
- ✅ WebSocket 实时消息接收
- ✅ 支持私聊和群聊
- ✅ 支持文本、图片、文件消息
- ✅ 配对模式安全控制
- ✅ 多账号支持
## Installation
### From Git
```bash
git clone <your-repo-url> ~/.openclaw/extensions/vocechat
cd ~/.openclaw/extensions/vocechat
pnpm install
```
### Enable Plugin
```bash
openclaw plugins enable vocechat
```
## Configuration
Edit `~/.openclaw/openclaw.json`:
```json5
{
channels: {
vocechat: {
enabled: true,
dmPolicy: "pairing", // "pairing" | "allowlist" | "open"
accounts: {
default: {
serverUrl: "https://your-vocechat-server.com",
apiKey: "your-api-key",
botName: "OpenClaw Bot",
},
},
},
},
}
```
## Usage
1. Start the gateway:
```bash
openclaw gateway
```
2. Send a message to your VoceChat bot
3. Approve pairing (if dmPolicy is "pairing"):
```bash
openclaw pairing approve vocechat <CODE>
```
## Development
```bash
pnpm install
pnpm dev
```
## License
MIT

20
openclaw.plugin.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "openclaw-vocechat",
"openclaw": {
"extensions": ["./src/index.ts"],
"channel": {
"id": "vocechat",
"label": "VoceChat",
"selectionLabel": "VoceChat (self-hosted)",
"docsPath": "/channels/vocechat",
"docsLabel": "vocechat",
"blurb": "Self-hosted chat via VoceChat API.",
"order": 70,
"aliases": ["voce"]
},
"install": {
"localPath": "extensions/vocechat",
"defaultChoice": "local"
}
}
}

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "openclaw-vocechat",
"version": "1.0.0",
"description": "VoceChat channel plugin for OpenClaw",
"main": "src/index.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"dependencies": {
"ws": "^8.16.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/ws": "^8.5.0",
"typescript": "^5.3.0"
},
"openclaw": {
"extensions": ["./src/index.ts"]
}
}

43
skills/vocechat/SKILL.md Normal file
View File

@ -0,0 +1,43 @@
---
name: vocechat
description: VoceChat channel integration for OpenClaw
---
# VoceChat Skill
This skill provides integration with VoceChat self-hosted messaging platform.
## Usage
When users want to:
- Send messages via VoceChat
- Check VoceChat connection status
- Configure VoceChat settings
Use the `vocechat` channel tools.
## Configuration
Configure in `~/.openclaw/openclaw.json`:
```json5
{
channels: {
vocechat: {
enabled: true,
dmPolicy: "pairing",
accounts: {
default: {
serverUrl: "https://your-vocechat-server.com",
apiKey: "your-api-key",
botName: "OpenClaw Bot",
},
},
},
},
}
```
## Commands
- `/vocechat:status` - Check connection status

318
src/index.ts Normal file
View File

@ -0,0 +1,318 @@
import type { PluginApi } from 'openclaw/plugin-sdk/core';
import WebSocket from 'ws';
// VoceChat 消息类型定义
interface VoceChatMessage {
mid: number;
messageId: string;
fromUid: number;
fromName?: string;
channelId: number;
channelType: 'direct' | 'group';
content: string;
createdAt: number;
contentType?: 'text' | 'image' | 'file';
attachments?: Array<{
name: string;
url: string;
size: number;
}>;
}
// VoceChat 配置类型
interface VoceChatAccount {
serverUrl: string;
apiKey: string;
botName?: string;
enabled?: boolean;
}
export default function register(api: PluginApi) {
api.logger.info('VoceChat plugin loading...');
// 存储 WebSocket 连接
const connections = new Map<string, WebSocket>();
// 注册 VoceChat 频道
api.registerChannel({
plugin: {
id: 'vocechat',
meta: {
id: 'vocechat',
label: 'VoceChat',
selectionLabel: 'VoceChat (API)',
docsPath: '/channels/vocechat',
blurb: 'VoceChat messaging channel.',
aliases: ['voce'],
},
capabilities: {
chatTypes: ['direct', 'group'],
media: {
images: true,
files: true,
},
},
config: {
listAccountIds: (cfg: any) => {
const accounts = cfg.channels?.vocechat?.accounts ?? {};
return Object.keys(accounts).filter(
(id) => accounts[id]?.enabled !== false
);
},
resolveAccount: (cfg: any, accountId?: string) => {
const account = cfg.channels?.vocechat?.accounts?.[accountId ?? 'default'];
if (!account) {
return { accountId: accountId ?? 'default' };
}
return {
accountId: accountId ?? 'default',
...account,
};
},
// 只读检查(用于状态显示)
inspectAccount: (cfg: any, accountId?: string) => {
const account = cfg.channels?.vocechat?.accounts?.[accountId ?? 'default'];
if (!account) {
return {
accountId: accountId ?? 'default',
enabled: false,
configured: false
};
}
return {
accountId: accountId ?? 'default',
enabled: account.enabled !== false,
configured: !!account.serverUrl && !!account.apiKey,
serverUrl: account.serverUrl,
botName: account.botName,
tokenStatus: account.apiKey ? 'available' : 'missing',
};
},
},
// 入站消息处理
inbound: {
async start({ account, onMessage, onError }) {
const { serverUrl, apiKey } = account as VoceChatAccount;
const accountId = account.accountId;
if (!serverUrl || !apiKey) {
throw new Error('VoceChat: serverUrl and apiKey are required');
}
// 构建 WebSocket URL
const wsUrl = serverUrl.replace(/^http/, 'ws') + '/ws';
api.logger.info(`VoceChat [${accountId}]: Connecting to ${wsUrl}`);
const ws = new WebSocket(wsUrl, {
headers: {
'Authorization': `Bearer ${apiKey}`,
},
});
connections.set(accountId, ws);
ws.on('open', () => {
api.logger.info(`VoceChat [${accountId}]: WebSocket connected`);
});
ws.on('message', async (data: WebSocket.Data) => {
try {
const event = JSON.parse(data.toString());
// 只处理消息事件
if (event.type !== 'message' || !event.data) {
return;
}
const msg: VoceChatMessage = event.data;
// 转换为 OpenClaw 标准消息格式
const message = {
id: msg.messageId || String(msg.mid),
text: msg.content,
sender: {
id: String(msg.fromUid),
name: msg.fromName || `User-${msg.fromUid}`,
},
chat: {
id: String(msg.channelId),
type: msg.channelType,
},
timestamp: msg.createdAt,
attachments: msg.attachments,
};
await onMessage(message);
} catch (err) {
api.logger.error('VoceChat: Failed to process message', err);
}
});
ws.on('error', (err) => {
api.logger.error(`VoceChat [${accountId}]: WebSocket error`, err);
onError(err);
});
ws.on('close', () => {
api.logger.info(`VoceChat [${accountId}]: WebSocket closed`);
connections.delete(accountId);
});
return {
stop: () => {
ws.close();
connections.delete(accountId);
},
};
},
},
// 出站消息处理
outbound: {
deliveryMode: 'direct',
async sendText({ text, chat, account }) {
const { serverUrl, apiKey } = account as VoceChatAccount;
try {
const res = await fetch(`${serverUrl}/api/message`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
target: chat.id,
content: text,
contentType: 'text',
}),
});
if (!res.ok) {
const error = await res.text();
throw new Error(`VoceChat API error: ${res.status} ${error}`);
}
return { ok: true };
} catch (err) {
api.logger.error('VoceChat: Failed to send text', err);
return { ok: false, error: String(err) };
}
},
async sendMedia({ mediaUrl, chat, account, mimeType }) {
const { serverUrl, apiKey } = account as VoceChatAccount;
try {
// 先上传文件获取 URL
const uploadRes = await fetch(`${serverUrl}/api/file`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
url: mediaUrl,
}),
});
if (!uploadRes.ok) {
throw new Error('Failed to upload media');
}
const { url: fileUrl } = await uploadRes.json();
// 发送文件消息
const res = await fetch(`${serverUrl}/api/message`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
target: chat.id,
content: fileUrl,
contentType: mimeType?.startsWith('image/') ? 'image' : 'file',
}),
});
return { ok: res.ok };
} catch (err) {
api.logger.error('VoceChat: Failed to send media', err);
return { ok: false, error: String(err) };
}
},
},
// 安全策略
security: {
dmPolicy: 'pairing', // 默认配对模式
},
// 状态检查
status: {
async check({ account }) {
const { serverUrl, apiKey } = account as VoceChatAccount;
try {
const res = await fetch(`${serverUrl}/api/user/me`, {
headers: {
'Authorization': `Bearer ${apiKey}`,
},
});
if (res.ok) {
const user = await res.json();
return {
ok: true,
status: 'connected',
details: `Logged in as ${user.name || user.uid}`,
};
} else {
return {
ok: false,
status: 'error',
details: `API returned ${res.status}`,
};
}
} catch (err) {
return {
ok: false,
status: 'disconnected',
details: String(err),
};
}
},
},
},
});
// 注册 CLI 命令
api.registerCli(({ program }) => {
program
.command('vocechat:status')
.description('Check VoceChat connection status')
.action(async () => {
console.log('VoceChat plugin status:');
console.log(` Active connections: ${connections.size}`);
for (const [id, ws] of connections) {
console.log(` - ${id}: ${ws.readyState === WebSocket.OPEN ? 'connected' : 'disconnected'}`);
}
});
});
// 注册 Gateway RPC 方法
api.registerGatewayMethod('vocechat.status', ({ respond }) => {
respond(true, {
ok: true,
connections: connections.size,
});
});
api.logger.info('VoceChat plugin loaded successfully');
}

19
tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}