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
This commit is contained in:
parent
bbc862273e
commit
f3c16c7609
5
.gitignore
vendored
5
.gitignore
vendored
@ -5,3 +5,8 @@ dist/
|
|||||||
.env
|
.env
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# Vault - sensitive data
|
||||||
|
.vault/
|
||||||
|
*.secret
|
||||||
|
*.key
|
||||||
115
src/inbound.ts
Normal file
115
src/inbound.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import type { PluginApi, ChannelAccount } 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 extends ChannelAccount {
|
||||||
|
serverUrl: string;
|
||||||
|
apiKey: string;
|
||||||
|
botName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动入站消息监听
|
||||||
|
*
|
||||||
|
* TODO: 当前使用 WebSocket 实现,待讨论 Webhook 方案后更新
|
||||||
|
* Webhook 方案需要:
|
||||||
|
* 1. 插件注册 HTTP 路由接收 Webhook 推送
|
||||||
|
* 2. 在 VoceChat 后台配置 Webhook URL
|
||||||
|
* 3. 处理 Webhook 的校验(GET 请求返回 200)
|
||||||
|
* 4. 解析 POST 推送的消息数据
|
||||||
|
*/
|
||||||
|
export async function startInbound(
|
||||||
|
api: PluginApi,
|
||||||
|
account: VoceChatAccount,
|
||||||
|
onMessage: (message: any) => Promise<void>,
|
||||||
|
onError: (error: Error) => void
|
||||||
|
): Promise<{ stop: () => void }> {
|
||||||
|
const { serverUrl, apiKey } = account;
|
||||||
|
const accountId = account.accountId;
|
||||||
|
|
||||||
|
if (!serverUrl || !apiKey) {
|
||||||
|
throw new Error('VoceChat: serverUrl and apiKey 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 ${apiKey}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
stop: () => {
|
||||||
|
ws.close();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
197
src/index.ts
197
src/index.ts
@ -1,25 +1,8 @@
|
|||||||
import type { PluginApi } from 'openclaw/plugin-sdk/core';
|
import type { PluginApi } from 'openclaw/plugin-sdk/core';
|
||||||
import WebSocket from 'ws';
|
import { startInbound } from './inbound.js';
|
||||||
|
import { sendText, sendMarkdown, sendFile, replyToMessage } from './outbound.js';
|
||||||
|
|
||||||
// VoceChat 消息类型定义
|
// 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 {
|
interface VoceChatAccount {
|
||||||
serverUrl: string;
|
serverUrl: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
@ -30,8 +13,8 @@ interface VoceChatAccount {
|
|||||||
export default function register(api: PluginApi) {
|
export default function register(api: PluginApi) {
|
||||||
api.logger.info('VoceChat plugin loading...');
|
api.logger.info('VoceChat plugin loading...');
|
||||||
|
|
||||||
// 存储 WebSocket 连接
|
// 存储连接控制函数
|
||||||
const connections = new Map<string, WebSocket>();
|
const connections = new Map<string, { stop: () => void }>();
|
||||||
|
|
||||||
// 注册 VoceChat 频道
|
// 注册 VoceChat 频道
|
||||||
api.registerChannel({
|
api.registerChannel({
|
||||||
@ -73,7 +56,6 @@ export default function register(api: PluginApi) {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
// 只读检查(用于状态显示)
|
|
||||||
inspectAccount: (cfg: any, accountId?: string) => {
|
inspectAccount: (cfg: any, accountId?: string) => {
|
||||||
const account = cfg.channels?.vocechat?.accounts?.[accountId ?? 'default'];
|
const account = cfg.channels?.vocechat?.accounts?.[accountId ?? 'default'];
|
||||||
if (!account) {
|
if (!account) {
|
||||||
@ -97,79 +79,14 @@ export default function register(api: PluginApi) {
|
|||||||
// 入站消息处理
|
// 入站消息处理
|
||||||
inbound: {
|
inbound: {
|
||||||
async start({ account, onMessage, onError }) {
|
async start({ account, onMessage, onError }) {
|
||||||
const { serverUrl, apiKey } = account as VoceChatAccount;
|
const connection = await startInbound(
|
||||||
const accountId = account.accountId;
|
api,
|
||||||
|
account as VoceChatAccount,
|
||||||
if (!serverUrl || !apiKey) {
|
onMessage,
|
||||||
throw new Error('VoceChat: serverUrl and apiKey are required');
|
onError
|
||||||
}
|
);
|
||||||
|
connections.set(account.accountId, connection);
|
||||||
// 构建 WebSocket URL
|
return connection;
|
||||||
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);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -178,100 +95,40 @@ export default function register(api: PluginApi) {
|
|||||||
deliveryMode: 'direct',
|
deliveryMode: 'direct',
|
||||||
|
|
||||||
async sendText({ text, chat, account }) {
|
async sendText({ text, chat, account }) {
|
||||||
const { serverUrl, apiKey } = account as VoceChatAccount;
|
return sendText(api, text, chat, 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 }) {
|
async sendMedia({ mediaUrl, chat, account, mimeType }) {
|
||||||
const { serverUrl, apiKey } = account as VoceChatAccount;
|
// TODO: 实现文件上传后再发送
|
||||||
|
// 1. 下载文件到本地
|
||||||
try {
|
// 2. 调用 uploadFile 上传
|
||||||
// 先上传文件获取 URL
|
// 3. 调用 sendFile 发送
|
||||||
const uploadRes = await fetch(`${serverUrl}/api/file`, {
|
api.logger.warn('VoceChat: sendMedia not fully implemented yet');
|
||||||
method: 'POST',
|
return { ok: false, error: 'Media sending not implemented' };
|
||||||
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: {
|
security: {
|
||||||
dmPolicy: 'pairing', // 默认配对模式
|
dmPolicy: 'pairing',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 状态检查
|
|
||||||
status: {
|
status: {
|
||||||
async check({ account }) {
|
async check({ account }) {
|
||||||
const { serverUrl, apiKey } = account as VoceChatAccount;
|
const { serverUrl, apiKey } = account as VoceChatAccount;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${serverUrl}/api/user/me`, {
|
const res = await fetch(`${serverUrl}/api/bot`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${apiKey}`,
|
'x-api-key': apiKey,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const user = await res.json();
|
const data = await res.json();
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 'connected',
|
status: 'connected',
|
||||||
details: `Logged in as ${user.name || user.uid}`,
|
details: `Bot is active, ${data.length || 0} channels`,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
@ -300,8 +157,8 @@ export default function register(api: PluginApi) {
|
|||||||
.action(async () => {
|
.action(async () => {
|
||||||
console.log('VoceChat plugin status:');
|
console.log('VoceChat plugin status:');
|
||||||
console.log(` Active connections: ${connections.size}`);
|
console.log(` Active connections: ${connections.size}`);
|
||||||
for (const [id, ws] of connections) {
|
for (const [id, conn] of connections) {
|
||||||
console.log(` - ${id}: ${ws.readyState === WebSocket.OPEN ? 'connected' : 'disconnected'}`);
|
console.log(` - ${id}: active`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
208
src/outbound.ts
Normal file
208
src/outbound.ts
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
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) };
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user