Initial commit: VoceChat plugin for OpenClaw
This commit is contained in:
commit
bbc862273e
8
.env.example
Normal file
8
.env.example
Normal 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
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.vscode/
|
||||
.idea/
|
||||
74
README.md
Normal file
74
README.md
Normal 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
20
openclaw.plugin.json
Normal 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
21
package.json
Normal 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
43
skills/vocechat/SKILL.md
Normal 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
318
src/index.ts
Normal 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
19
tsconfig.json
Normal 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"]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user