- 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
208 lines
5.0 KiB
TypeScript
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) };
|
|
}
|
|
} |