110 lines
2.9 KiB
TypeScript
110 lines
2.9 KiB
TypeScript
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk/core';
|
||
import WebSocket from 'ws';
|
||
|
||
import type { VoceChatAccount } from './types/index.js';
|
||
|
||
// 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;
|
||
}>;
|
||
}
|
||
|
||
/**
|
||
* 启动入站消息监听
|
||
*
|
||
* TODO: 当前使用 WebSocket 实现,待讨论 Webhook 方案后更新
|
||
* Webhook 方案需要:
|
||
* 1. 插件注册 HTTP 路由接收 Webhook 推送
|
||
* 2. 在 VoceChat 后台配置 Webhook URL
|
||
* 3. 处理 Webhook 的校验(GET 请求返回 200)
|
||
* 4. 解析 POST 推送的消息数据
|
||
*/
|
||
export async function startInbound(
|
||
api: OpenClawPluginApi,
|
||
account: VoceChatAccount,
|
||
onMessage: (message: any) => Promise<void>,
|
||
onError: (error: Error) => void
|
||
): Promise<{ stop: () => void }> {
|
||
const { serverUrl, botApiToken } = account;
|
||
const accountId = account.accountId;
|
||
|
||
if (!serverUrl || !botApiToken) {
|
||
throw new Error('VoceChat: serverUrl and botApiToken are required');
|
||
}
|
||
|
||
// 注意:当前使用 WebSocket 连接,后续应改为 Webhook
|
||
const wsUrl = serverUrl.replace(/^http/, 'ws') + '/ws';
|
||
|
||
api.logger.info(`VoceChat [${accountId}]: Connecting to ${wsUrl}`);
|
||
api.logger.warn(`VoceChat [${accountId}]: 当前使用 WebSocket,建议迁移到 Webhook 方案`);
|
||
|
||
const ws = new WebSocket(wsUrl, {
|
||
headers: {
|
||
'Authorization': `Bearer ${botApiToken}`,
|
||
},
|
||
});
|
||
|
||
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\n' + err);
|
||
}
|
||
});
|
||
|
||
ws.on('error', (err) => {
|
||
api.logger.error(`VoceChat [${accountId}]: WebSocket error\n` + err);
|
||
onError(err);
|
||
});
|
||
|
||
ws.on('close', () => {
|
||
api.logger.info(`VoceChat [${accountId}]: WebSocket closed`);
|
||
});
|
||
|
||
return {
|
||
stop: () => {
|
||
ws.close();
|
||
},
|
||
};
|
||
} |