feat: 精简版本
This commit is contained in:
parent
f3c16c7609
commit
7e98a8630f
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,7 +3,6 @@ dist/
|
|||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
.env
|
||||||
.vscode/
|
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
# Vault - sensitive data
|
# Vault - sensitive data
|
||||||
|
|||||||
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"openclaw",
|
||||||
|
"vocechat"
|
||||||
|
],
|
||||||
|
"editor.formatOnSave": false
|
||||||
|
}
|
||||||
@ -1,5 +1,38 @@
|
|||||||
{
|
{
|
||||||
|
"id": "openclaw-vocechat",
|
||||||
"name": "openclaw-vocechat",
|
"name": "openclaw-vocechat",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "VoceChat channel plugin for OpenClaw",
|
||||||
|
"configSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"serverUrl": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"botApiToken": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"wsServerUrl": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uiHints": {
|
||||||
|
"serverUrl": {
|
||||||
|
"label": "VoceChat Server URL",
|
||||||
|
"placeholder": "https://your-vocechat-server.com"
|
||||||
|
},
|
||||||
|
"botApiToken": {
|
||||||
|
"sensitive": true,
|
||||||
|
"label": "Bot API Token",
|
||||||
|
"placeholder": "Your VoceChat Bot API token"
|
||||||
|
},
|
||||||
|
"wsServerUrl": {
|
||||||
|
"label": "VoceChat WebSocket Server URL",
|
||||||
|
"placeholder": "wss://your-vocechat-server.com/ws"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"openclaw": {
|
"openclaw": {
|
||||||
"extensions": ["./src/index.ts"],
|
"extensions": ["./src/index.ts"],
|
||||||
"channel": {
|
"channel": {
|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
"name": "openclaw-vocechat",
|
"name": "openclaw-vocechat",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "VoceChat channel plugin for OpenClaw",
|
"description": "VoceChat channel plugin for OpenClaw",
|
||||||
"main": "src/index.ts",
|
"type": "module",
|
||||||
|
"main": "./dist/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"dev": "tsc --watch"
|
"dev": "tsc --watch"
|
||||||
@ -13,9 +14,12 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"@types/ws": "^8.5.0",
|
"@types/ws": "^8.5.0",
|
||||||
|
"openclaw": "^2026.3.13",
|
||||||
"typescript": "^5.3.0"
|
"typescript": "^5.3.0"
|
||||||
},
|
},
|
||||||
"openclaw": {
|
"openclaw": {
|
||||||
"extensions": ["./src/index.ts"]
|
"extensions": [
|
||||||
|
"./src/index.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
5587
pnpm-lock.yaml
generated
Normal file
5587
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
5
pnpm-workspace.yaml
Normal file
5
pnpm-workspace.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
allowBuilds:
|
||||||
|
'@whiskeysockets/baileys': true
|
||||||
|
koffi: true
|
||||||
|
protobufjs: true
|
||||||
|
sharp: true
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import type { PluginApi, ChannelAccount } from 'openclaw/plugin-sdk/core';
|
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk/core';
|
||||||
import WebSocket from 'ws';
|
import WebSocket from 'ws';
|
||||||
|
|
||||||
|
import type { VoceChatAccount } from './types/index.js';
|
||||||
|
|
||||||
// VoceChat 消息类型定义(保留现有结构,待讨论后更新)
|
// VoceChat 消息类型定义(保留现有结构,待讨论后更新)
|
||||||
interface VoceChatMessage {
|
interface VoceChatMessage {
|
||||||
mid: number;
|
mid: number;
|
||||||
@ -19,13 +21,6 @@ interface VoceChatMessage {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// VoceChat 账号配置类型
|
|
||||||
interface VoceChatAccount extends ChannelAccount {
|
|
||||||
serverUrl: string;
|
|
||||||
apiKey: string;
|
|
||||||
botName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动入站消息监听
|
* 启动入站消息监听
|
||||||
*
|
*
|
||||||
@ -37,16 +32,16 @@ interface VoceChatAccount extends ChannelAccount {
|
|||||||
* 4. 解析 POST 推送的消息数据
|
* 4. 解析 POST 推送的消息数据
|
||||||
*/
|
*/
|
||||||
export async function startInbound(
|
export async function startInbound(
|
||||||
api: PluginApi,
|
api: OpenClawPluginApi,
|
||||||
account: VoceChatAccount,
|
account: VoceChatAccount,
|
||||||
onMessage: (message: any) => Promise<void>,
|
onMessage: (message: any) => Promise<void>,
|
||||||
onError: (error: Error) => void
|
onError: (error: Error) => void
|
||||||
): Promise<{ stop: () => void }> {
|
): Promise<{ stop: () => void }> {
|
||||||
const { serverUrl, apiKey } = account;
|
const { serverUrl, botApiToken } = account;
|
||||||
const accountId = account.accountId;
|
const accountId = account.accountId;
|
||||||
|
|
||||||
if (!serverUrl || !apiKey) {
|
if (!serverUrl || !botApiToken) {
|
||||||
throw new Error('VoceChat: serverUrl and apiKey are required');
|
throw new Error('VoceChat: serverUrl and botApiToken are required');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注意:当前使用 WebSocket 连接,后续应改为 Webhook
|
// 注意:当前使用 WebSocket 连接,后续应改为 Webhook
|
||||||
@ -57,7 +52,7 @@ export async function startInbound(
|
|||||||
|
|
||||||
const ws = new WebSocket(wsUrl, {
|
const ws = new WebSocket(wsUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${apiKey}`,
|
'Authorization': `Bearer ${botApiToken}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -94,12 +89,12 @@ export async function startInbound(
|
|||||||
|
|
||||||
await onMessage(message);
|
await onMessage(message);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
api.logger.error('VoceChat: Failed to process message', err);
|
api.logger.error('VoceChat: Failed to process message\n' + err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on('error', (err) => {
|
ws.on('error', (err) => {
|
||||||
api.logger.error(`VoceChat [${accountId}]: WebSocket error`, err);
|
api.logger.error(`VoceChat [${accountId}]: WebSocket error\n` + err);
|
||||||
onError(err);
|
onError(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
167
src/index.ts
167
src/index.ts
@ -1,17 +1,14 @@
|
|||||||
import type { PluginApi } from 'openclaw/plugin-sdk/core';
|
import type {
|
||||||
import { startInbound } from './inbound.js';
|
OpenClawPluginApi,
|
||||||
import { sendText, sendMarkdown, sendFile, replyToMessage } from './outbound.js';
|
OpenClawConfig,
|
||||||
|
} from "openclaw/plugin-sdk";
|
||||||
|
// import { startInbound } from './inbound.js';
|
||||||
|
import outbound from "./outbound.js";
|
||||||
|
|
||||||
// VoceChat 账号配置类型
|
import type { VoceChatAccount } from './types/index.js';
|
||||||
interface VoceChatAccount {
|
|
||||||
serverUrl: string;
|
|
||||||
apiKey: string;
|
|
||||||
botName?: string;
|
|
||||||
enabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function register(api: PluginApi) {
|
export default function register(api: OpenClawPluginApi) {
|
||||||
api.logger.info('VoceChat plugin loading...');
|
api.logger.info("VoceChat plugin loading...");
|
||||||
|
|
||||||
// 存储连接控制函数
|
// 存储连接控制函数
|
||||||
const connections = new Map<string, { stop: () => void }>();
|
const connections = new Map<string, { stop: () => void }>();
|
||||||
@ -19,157 +16,105 @@ export default function register(api: PluginApi) {
|
|||||||
// 注册 VoceChat 频道
|
// 注册 VoceChat 频道
|
||||||
api.registerChannel({
|
api.registerChannel({
|
||||||
plugin: {
|
plugin: {
|
||||||
id: 'vocechat',
|
id: "vocechat",
|
||||||
meta: {
|
meta: {
|
||||||
id: 'vocechat',
|
id: "vocechat",
|
||||||
label: 'VoceChat',
|
label: "VoceChat",
|
||||||
selectionLabel: 'VoceChat (API)',
|
selectionLabel: "VoceChat (API)",
|
||||||
docsPath: '/channels/vocechat',
|
docsPath: "/channels/vocechat",
|
||||||
blurb: 'VoceChat messaging channel.',
|
blurb: "VoceChat messaging channel.",
|
||||||
aliases: ['voce'],
|
aliases: ["voce"],
|
||||||
},
|
},
|
||||||
|
|
||||||
capabilities: {
|
capabilities: {
|
||||||
chatTypes: ['direct', 'group'],
|
chatTypes: ["direct", "group"],
|
||||||
media: {
|
media: false, // TODO: 后续再实现媒体消息支持
|
||||||
images: true,
|
|
||||||
files: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
config: {
|
config: {
|
||||||
listAccountIds: (cfg: any) => {
|
listAccountIds: (cfg: OpenClawConfig) => {
|
||||||
const accounts = cfg.channels?.vocechat?.accounts ?? {};
|
const accounts = cfg.channels?.vocechat?.accounts ?? {};
|
||||||
return Object.keys(accounts).filter(
|
return Object.keys(accounts).filter(
|
||||||
(id) => accounts[id]?.enabled !== false
|
(id) => accounts[id]?.enabled !== false,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
resolveAccount: (cfg: any, accountId?: string) => {
|
resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) => {
|
||||||
const account = cfg.channels?.vocechat?.accounts?.[accountId ?? 'default'];
|
const account =
|
||||||
|
cfg.channels?.vocechat?.accounts?.[accountId ?? "default"];
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return { accountId: accountId ?? 'default' };
|
return { accountId: accountId ?? "default" };
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
accountId: accountId ?? 'default',
|
accountId: accountId ?? "default",
|
||||||
...account,
|
...account,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
inspectAccount: (cfg: any, accountId?: string) => {
|
inspectAccount: (cfg: OpenClawConfig, accountId?: string | null) => {
|
||||||
const account = cfg.channels?.vocechat?.accounts?.[accountId ?? 'default'];
|
const account =
|
||||||
|
cfg.channels?.vocechat?.accounts?.[accountId ?? "default"];
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return {
|
return {
|
||||||
accountId: accountId ?? 'default',
|
accountId: accountId ?? "default",
|
||||||
enabled: false,
|
enabled: false,
|
||||||
configured: false
|
configured: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
accountId: accountId ?? 'default',
|
accountId: accountId ?? "default",
|
||||||
enabled: account.enabled !== false,
|
enabled: account.enabled !== false,
|
||||||
configured: !!account.serverUrl && !!account.apiKey,
|
configured:
|
||||||
|
!!account.serverUrl &&
|
||||||
|
!!account.botApiToken &&
|
||||||
|
!!account.wsServerUrl,
|
||||||
serverUrl: account.serverUrl,
|
serverUrl: account.serverUrl,
|
||||||
botName: account.botName,
|
wsServerUrl: account.wsServerUrl,
|
||||||
tokenStatus: account.apiKey ? 'available' : 'missing',
|
tokenStatus: account.botApiToken ? "available" : "missing",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// 入站消息处理
|
// 入站消息处理
|
||||||
inbound: {
|
// inbound: {
|
||||||
async start({ account, onMessage, onError }) {
|
// async start({ account, onMessage, onError }) {
|
||||||
const connection = await startInbound(
|
// const connection = await startInbound(
|
||||||
api,
|
// api,
|
||||||
account as VoceChatAccount,
|
// account as VoceChatAccount,
|
||||||
onMessage,
|
// onMessage,
|
||||||
onError
|
// onError,
|
||||||
);
|
// );
|
||||||
connections.set(account.accountId, connection);
|
// connections.set(account.accountId, connection);
|
||||||
return connection;
|
// return connection;
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
|
|
||||||
// 出站消息处理
|
// 出站消息处理
|
||||||
outbound: {
|
outbound,
|
||||||
deliveryMode: 'direct',
|
|
||||||
|
|
||||||
async sendText({ text, chat, account }) {
|
|
||||||
return sendText(api, text, chat, account as VoceChatAccount);
|
|
||||||
},
|
|
||||||
|
|
||||||
async sendMedia({ mediaUrl, chat, account, mimeType }) {
|
|
||||||
// TODO: 实现文件上传后再发送
|
|
||||||
// 1. 下载文件到本地
|
|
||||||
// 2. 调用 uploadFile 上传
|
|
||||||
// 3. 调用 sendFile 发送
|
|
||||||
api.logger.warn('VoceChat: sendMedia not fully implemented yet');
|
|
||||||
return { ok: false, error: 'Media sending not implemented' };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
security: {
|
|
||||||
dmPolicy: 'pairing',
|
|
||||||
},
|
|
||||||
|
|
||||||
status: {
|
|
||||||
async check({ account }) {
|
|
||||||
const { serverUrl, apiKey } = account as VoceChatAccount;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${serverUrl}/api/bot`, {
|
|
||||||
headers: {
|
|
||||||
'x-api-key': apiKey,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
status: 'connected',
|
|
||||||
details: `Bot is active, ${data.length || 0} channels`,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
status: 'error',
|
|
||||||
details: `API returned ${res.status}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
status: 'disconnected',
|
|
||||||
details: String(err),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 注册 CLI 命令
|
// 注册 CLI 命令
|
||||||
api.registerCli(({ program }) => {
|
api.registerCli(({ program }) => {
|
||||||
program
|
program
|
||||||
.command('vocechat:status')
|
.command("vocechat:status")
|
||||||
.description('Check VoceChat connection status')
|
.description("Check VoceChat connection status")
|
||||||
.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, conn] of connections) {
|
for (const [id] of connections) {
|
||||||
console.log(` - ${id}: active`);
|
console.log(` - ${id}: active`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 注册 Gateway RPC 方法
|
// 注册 Gateway RPC 方法
|
||||||
api.registerGatewayMethod('vocechat.status', ({ respond }) => {
|
api.registerGatewayMethod("vocechat.status", ({ respond }) => {
|
||||||
respond(true, {
|
respond(true, {
|
||||||
ok: true,
|
ok: true,
|
||||||
connections: connections.size,
|
connections: connections.size,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
api.logger.info('VoceChat plugin loaded successfully');
|
api.logger.info("VoceChat plugin loaded successfully");
|
||||||
}
|
}
|
||||||
221
src/outbound.ts
221
src/outbound.ts
@ -1,34 +1,29 @@
|
|||||||
import type { PluginApi, ChannelAccount, ChannelChat, OutboundResult } from 'openclaw/plugin-sdk/core';
|
import type {
|
||||||
|
OpenClawPluginApi,
|
||||||
// VoceChat 账号配置类型
|
OpenClawConfig,
|
||||||
interface VoceChatAccount extends ChannelAccount {
|
ChannelOutboundAdapter,
|
||||||
serverUrl: string;
|
ChannelOutboundContext,
|
||||||
apiKey: string;
|
} from "openclaw/plugin-sdk";
|
||||||
botName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送文本消息
|
* 发送文本消息
|
||||||
*/
|
*/
|
||||||
export async function sendText(
|
export async function sendTextToVoceChat(
|
||||||
api: PluginApi,
|
cfg: OpenClawConfig,
|
||||||
text: string,
|
to?: string | null,
|
||||||
chat: ChannelChat,
|
text?: string,
|
||||||
account: VoceChatAccount
|
) {
|
||||||
): Promise<OutboundResult> {
|
const { serverUrl, apiKey } = cfg.channels?.vocechat|| {};
|
||||||
const { serverUrl, apiKey } = account;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 根据聊天类型选择 API 端点
|
// 根据聊天类型选择 API 端点
|
||||||
const endpoint = chat.type === 'direct'
|
const endpoint = `${serverUrl}/api/bot/send_to_user/${to}`;
|
||||||
? `${serverUrl}/api/bot/send_to_user/${chat.id}`
|
|
||||||
: `${serverUrl}/api/bot/send_to_group/${chat.id}`;
|
|
||||||
|
|
||||||
const res = await fetch(endpoint, {
|
const res = await fetch(endpoint, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/plain',
|
"Content-Type": "text/plain",
|
||||||
'x-api-key': apiKey,
|
"x-api-key": apiKey,
|
||||||
},
|
},
|
||||||
body: text,
|
body: text,
|
||||||
});
|
});
|
||||||
@ -38,110 +33,29 @@ export async function sendText(
|
|||||||
throw new Error(`VoceChat API error: ${res.status} ${error}`);
|
throw new Error(`VoceChat API error: ${res.status} ${error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.text();
|
||||||
return { ok: true, messageId: String(data) };
|
return { ok: true, messageId: String(data) };
|
||||||
} catch (err) {
|
} catch (e) {}
|
||||||
api.logger.error('VoceChat: Failed to send text', err);
|
|
||||||
return { ok: false, error: String(err) };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送 Markdown 消息
|
* 发送 Markdown 消息
|
||||||
*/
|
*/
|
||||||
export async function sendMarkdown(
|
export async function sendMarkdown(
|
||||||
api: PluginApi,
|
cfg: OpenClawConfig,
|
||||||
markdown: string,
|
accountId?: string | null,
|
||||||
chat: ChannelChat,
|
text?: string,
|
||||||
account: VoceChatAccount
|
) {
|
||||||
): Promise<OutboundResult> {
|
const { serverUrl, apiKey } = cfg.channels?.vocechat || {};
|
||||||
const { serverUrl, apiKey } = account;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const endpoint = chat.type === 'direct'
|
const endpoint = `${serverUrl}/api/bot/send_to_user/${accountId}`;
|
||||||
? `${serverUrl}/api/bot/send_to_user/${chat.id}`
|
|
||||||
: `${serverUrl}/api/bot/send_to_group/${chat.id}`;
|
|
||||||
|
|
||||||
const res = await fetch(endpoint, {
|
const res = await fetch(endpoint, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/markdown',
|
"Content-Type": "text/markdown",
|
||||||
'x-api-key': apiKey,
|
"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,
|
body: text,
|
||||||
});
|
});
|
||||||
@ -151,58 +65,57 @@ export async function replyToMessage(
|
|||||||
throw new Error(`VoceChat API error: ${res.status} ${error}`);
|
throw new Error(`VoceChat API error: ${res.status} ${error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.text();
|
||||||
return { ok: true, messageId: String(data) };
|
return { ok: true, messageId: String(data) };
|
||||||
} catch (err) {
|
} catch (e) {}
|
||||||
api.logger.error('VoceChat: Failed to reply', err);
|
|
||||||
return { ok: false, error: String(err) };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传文件(用于发送文件消息前的准备)
|
* 回复特定消息
|
||||||
*/
|
*/
|
||||||
export async function uploadFile(
|
export async function replyToMessage(
|
||||||
api: PluginApi,
|
cfg: OpenClawConfig,
|
||||||
fileBuffer: Buffer,
|
accountId?: string | null,
|
||||||
fileName: string,
|
text?: string,
|
||||||
account: VoceChatAccount
|
mid?: string | number,
|
||||||
): Promise<{ ok: boolean; filePath?: string; error?: string }> {
|
) {
|
||||||
const { serverUrl, apiKey } = account;
|
const { serverUrl, apiKey } = cfg.channels?.vocechat || {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 准备上传
|
const endpoint = `${serverUrl}/api/bot/reply/${mid}`;
|
||||||
const prepareRes = await fetch(`${serverUrl}/api/bot/file/prepare`, {
|
|
||||||
method: 'POST',
|
const res = await fetch(endpoint, {
|
||||||
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "text/plain",
|
||||||
'x-api-key': apiKey,
|
"x-api-key": apiKey,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: text,
|
||||||
content_type: 'application/octet-stream',
|
|
||||||
file_name: fileName,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!prepareRes.ok) {
|
if (!res.ok) {
|
||||||
throw new Error('Failed to prepare file upload');
|
const error = await res.text();
|
||||||
|
throw new Error(`VoceChat API error: ${res.status} ${error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { upload_url, file_path } = await prepareRes.json();
|
const data = await res.text();
|
||||||
|
return { ok: true, messageId: String(data) };
|
||||||
// 2. 上传文件
|
} catch (err) {}
|
||||||
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) };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
deliveryMode: "gateway",
|
||||||
|
sendText({ text, cfg, accountId, to, replyToId }: ChannelOutboundContext) {
|
||||||
|
if (!text) return;
|
||||||
|
return sendTextToVoceChat(cfg, to, text)
|
||||||
|
.then((result) => {
|
||||||
|
if (result?.ok) {
|
||||||
|
return {
|
||||||
|
channel: "vocechat",
|
||||||
|
messageId: result.messageId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {});
|
||||||
|
},
|
||||||
|
} as ChannelOutboundAdapter;
|
||||||
|
|||||||
7
src/types/index.ts
Normal file
7
src/types/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// VoceChat 账号配置类型
|
||||||
|
export interface VoceChatAccount {
|
||||||
|
serverUrl: string;
|
||||||
|
botApiToken: string;
|
||||||
|
wsServerUrl: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user