root f3c16c7609 refactor: split inbound/outbound into separate files
- Extract inbound message handling to inbound.ts (WebSocket, pending Webhook migration)
- Extract outbound message handling to outbound.ts with updated VoceChat API
- Update authentication to use x-api-key header
- Add support for text, markdown, file, and reply messages
- Add file upload helper functions
2026-03-13 20:31:23 +08:00

208 lines
5.0 KiB
TypeScript

import type { PluginApi, ChannelAccount, ChannelChat, OutboundResult } from 'openclaw/plugin-sdk/core';
// VoceChat 账号配置类型
interface VoceChatAccount extends ChannelAccount {
serverUrl: string;
apiKey: string;
botName?: string;
}
/**
* 发送文本消息
*/
export async function sendText(
api: PluginApi,
text: string,
chat: ChannelChat,
account: VoceChatAccount
): Promise<OutboundResult> {
const { serverUrl, apiKey } = account;
try {
// 根据聊天类型选择 API 端点
const endpoint = chat.type === 'direct'
? `${serverUrl}/api/bot/send_to_user/${chat.id}`
: `${serverUrl}/api/bot/send_to_group/${chat.id}`;
const res = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
'x-api-key': apiKey,
},
body: text,
});
if (!res.ok) {
const error = await res.text();
throw new Error(`VoceChat API error: ${res.status} ${error}`);
}
const data = await res.json();
return { ok: true, messageId: String(data) };
} catch (err) {
api.logger.error('VoceChat: Failed to send text', err);
return { ok: false, error: String(err) };
}
}
/**
* 发送 Markdown 消息
*/
export async function sendMarkdown(
api: PluginApi,
markdown: string,
chat: ChannelChat,
account: VoceChatAccount
): Promise<OutboundResult> {
const { serverUrl, apiKey } = account;
try {
const endpoint = chat.type === 'direct'
? `${serverUrl}/api/bot/send_to_user/${chat.id}`
: `${serverUrl}/api/bot/send_to_group/${chat.id}`;
const res = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'text/markdown',
'x-api-key': apiKey,
},
body: markdown,
});
if (!res.ok) {
const error = await res.text();
throw new Error(`VoceChat API error: ${res.status} ${error}`);
}
const data = await res.json();
return { ok: true, messageId: String(data) };
} catch (err) {
api.logger.error('VoceChat: Failed to send markdown', err);
return { ok: false, error: String(err) };
}
}
/**
* 发送文件消息
* 注意:需要先上传文件获取 file_path
*/
export async function sendFile(
api: PluginApi,
filePath: string,
chat: ChannelChat,
account: VoceChatAccount
): Promise<OutboundResult> {
const { serverUrl, apiKey } = account;
try {
const endpoint = chat.type === 'direct'
? `${serverUrl}/api/bot/send_to_user/${chat.id}`
: `${serverUrl}/api/bot/send_to_group/${chat.id}`;
const res = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'vocechat/file',
'x-api-key': apiKey,
},
body: JSON.stringify({ path: filePath }),
});
if (!res.ok) {
const error = await res.text();
throw new Error(`VoceChat API error: ${res.status} ${error}`);
}
const data = await res.json();
return { ok: true, messageId: String(data) };
} catch (err) {
api.logger.error('VoceChat: Failed to send file', err);
return { ok: false, error: String(err) };
}
}
/**
* 回复特定消息
*/
export async function replyToMessage(
api: PluginApi,
text: string,
mid: string,
account: VoceChatAccount
): Promise<OutboundResult> {
const { serverUrl, apiKey } = account;
try {
const endpoint = `${serverUrl}/api/bot/reply/${mid}`;
const res = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
'x-api-key': apiKey,
},
body: text,
});
if (!res.ok) {
const error = await res.text();
throw new Error(`VoceChat API error: ${res.status} ${error}`);
}
const data = await res.json();
return { ok: true, messageId: String(data) };
} catch (err) {
api.logger.error('VoceChat: Failed to reply', err);
return { ok: false, error: String(err) };
}
}
/**
* 上传文件(用于发送文件消息前的准备)
*/
export async function uploadFile(
api: PluginApi,
fileBuffer: Buffer,
fileName: string,
account: VoceChatAccount
): Promise<{ ok: boolean; filePath?: string; error?: string }> {
const { serverUrl, apiKey } = account;
try {
// 1. 准备上传
const prepareRes = await fetch(`${serverUrl}/api/bot/file/prepare`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
},
body: JSON.stringify({
content_type: 'application/octet-stream',
file_name: fileName,
}),
});
if (!prepareRes.ok) {
throw new Error('Failed to prepare file upload');
}
const { upload_url, file_path } = await prepareRes.json();
// 2. 上传文件
const uploadRes = await fetch(upload_url, {
method: 'PUT',
body: fileBuffer,
});
if (!uploadRes.ok) {
throw new Error('Failed to upload file');
}
return { ok: true, filePath: file_path };
} catch (err) {
api.logger.error('VoceChat: Failed to upload file', err);
return { ok: false, error: String(err) };
}
}