@ozaiya/openclaw-channel 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +23 -49
- package/dist/src/channel.js +183 -53
- package/dist/src/channel.js.map +1 -1
- package/dist/src/configSchema.d.ts +23 -49
- package/dist/src/configSchema.js +23 -31
- package/dist/src/configSchema.js.map +1 -1
- package/dist/src/gateway.js +31 -0
- package/dist/src/gateway.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/transcribeAudio.d.ts +3 -3
- package/dist/src/transcribeAudio.js +16 -153
- package/dist/src/transcribeAudio.js.map +1 -1
- package/dist/src/types.d.ts +21 -34
- package/dist/src/voiceCall.d.ts +1 -0
- package/dist/src/voiceCall.js +12 -3
- package/dist/src/voiceCall.js.map +1 -1
- package/dist/src/webhook.d.ts +6 -1
- package/dist/src/webhook.js +31 -7
- package/dist/src/webhook.js.map +1 -1
- package/package.json +1 -1
- package/types/openclaw-plugin-sdk.d.ts +13 -0
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,25 @@ declare const plugin: {
|
|
|
32
32
|
readonly webhookPath: {
|
|
33
33
|
readonly type: "string";
|
|
34
34
|
};
|
|
35
|
+
readonly webhookBase: {
|
|
36
|
+
readonly type: "string";
|
|
37
|
+
};
|
|
38
|
+
readonly botWebhookBases: {
|
|
39
|
+
readonly type: "object";
|
|
40
|
+
readonly additionalProperties: {
|
|
41
|
+
readonly type: "string";
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
readonly userToken: {
|
|
45
|
+
readonly type: "string";
|
|
46
|
+
};
|
|
47
|
+
readonly gatewayName: {
|
|
48
|
+
readonly type: "string";
|
|
49
|
+
};
|
|
50
|
+
readonly dmPolicy: {
|
|
51
|
+
readonly type: "string";
|
|
52
|
+
readonly enum: readonly ["allow", "deny", "allowlist"];
|
|
53
|
+
};
|
|
35
54
|
readonly accounts: {
|
|
36
55
|
readonly type: "object";
|
|
37
56
|
readonly additionalProperties: {
|
|
@@ -53,9 +72,6 @@ declare const plugin: {
|
|
|
53
72
|
readonly webhookPath: {
|
|
54
73
|
readonly type: "string";
|
|
55
74
|
};
|
|
56
|
-
readonly stt: {
|
|
57
|
-
readonly $ref: "#/$defs/stt";
|
|
58
|
-
};
|
|
59
75
|
};
|
|
60
76
|
};
|
|
61
77
|
};
|
|
@@ -70,7 +86,10 @@ declare const plugin: {
|
|
|
70
86
|
readonly properties: {
|
|
71
87
|
readonly mode: {
|
|
72
88
|
readonly type: "string";
|
|
73
|
-
readonly enum: readonly ["disabled", "
|
|
89
|
+
readonly enum: readonly ["disabled", "enabled"];
|
|
90
|
+
};
|
|
91
|
+
readonly url: {
|
|
92
|
+
readonly type: "string";
|
|
74
93
|
};
|
|
75
94
|
readonly timeoutMs: {
|
|
76
95
|
readonly type: "integer";
|
|
@@ -80,51 +99,6 @@ declare const plugin: {
|
|
|
80
99
|
readonly type: "integer";
|
|
81
100
|
readonly minimum: 1;
|
|
82
101
|
};
|
|
83
|
-
readonly openai: {
|
|
84
|
-
readonly type: "object";
|
|
85
|
-
readonly additionalProperties: false;
|
|
86
|
-
readonly properties: {
|
|
87
|
-
readonly apiKey: {
|
|
88
|
-
readonly type: "string";
|
|
89
|
-
};
|
|
90
|
-
readonly baseUrl: {
|
|
91
|
-
readonly type: "string";
|
|
92
|
-
};
|
|
93
|
-
readonly model: {
|
|
94
|
-
readonly type: "string";
|
|
95
|
-
};
|
|
96
|
-
readonly organization: {
|
|
97
|
-
readonly type: "string";
|
|
98
|
-
};
|
|
99
|
-
readonly project: {
|
|
100
|
-
readonly type: "string";
|
|
101
|
-
};
|
|
102
|
-
};
|
|
103
|
-
};
|
|
104
|
-
readonly localCommand: {
|
|
105
|
-
readonly type: "object";
|
|
106
|
-
readonly additionalProperties: false;
|
|
107
|
-
readonly properties: {
|
|
108
|
-
readonly command: {
|
|
109
|
-
readonly type: "string";
|
|
110
|
-
};
|
|
111
|
-
readonly args: {
|
|
112
|
-
readonly type: "array";
|
|
113
|
-
readonly items: {
|
|
114
|
-
readonly type: "string";
|
|
115
|
-
};
|
|
116
|
-
};
|
|
117
|
-
readonly cwd: {
|
|
118
|
-
readonly type: "string";
|
|
119
|
-
};
|
|
120
|
-
readonly env: {
|
|
121
|
-
readonly type: "object";
|
|
122
|
-
readonly additionalProperties: {
|
|
123
|
-
readonly type: "string";
|
|
124
|
-
};
|
|
125
|
-
};
|
|
126
|
-
};
|
|
127
|
-
};
|
|
128
102
|
};
|
|
129
103
|
};
|
|
130
104
|
};
|
package/dist/src/channel.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import fs from "node:fs/promises";
|
|
9
9
|
import path from "node:path";
|
|
10
|
-
import { registerPluginHttpRoute } from "openclaw/plugin-sdk";
|
|
10
|
+
import { registerPluginHttpRoute } from "openclaw/plugin-sdk/webhook-ingress";
|
|
11
11
|
import { unwrapGroupKey, decryptMessage, encryptMessage, wrapGroupKey } from "./crypto.js";
|
|
12
12
|
import { sendMessage, probeApi, fetchGroups, addMember, getUserPublicKeys, toggleReaction, editMessage, deleteMessage, pinMessage, unpinMessage, uploadFile, searchUsers, fetchLinkPreview, joinCall, leaveCall, } from "./api.js";
|
|
13
13
|
import { botCreateDirect, botCreateGroup } from "./botActions.js";
|
|
@@ -116,7 +116,7 @@ function resolveAccount(cfg, accountId) {
|
|
|
116
116
|
webhookSecret: accountConfig.webhookSecret ?? "",
|
|
117
117
|
apiBaseUrl,
|
|
118
118
|
webhookPath: accountConfig.webhookPath ?? `${DEFAULT_WEBHOOK_PATH}/${id}`,
|
|
119
|
-
stt: resolveOzaiyaSttConfig(ozaiya
|
|
119
|
+
stt: resolveOzaiyaSttConfig(ozaiya),
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
// Default account (top-level fields)
|
|
@@ -644,36 +644,46 @@ export const ozaiyaPlugin = {
|
|
|
644
644
|
// Stop existing handler if re-registering
|
|
645
645
|
botUnregisters.get(botAccount.accountId)?.();
|
|
646
646
|
recordState(botAccount.accountId, { running: true, lastStartAt: Date.now() });
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
auth: "plugin",
|
|
650
|
-
replaceExisting: true,
|
|
651
|
-
pluginId: "ozaiya",
|
|
652
|
-
source: "ozaiya-gateway",
|
|
647
|
+
const webhookHandler = createOzaiyaWebhookHandler({
|
|
648
|
+
webhookSecret: botAccount.webhookSecret,
|
|
653
649
|
log: (msg) => ctx.log?.info(msg),
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
}),
|
|
650
|
+
onEvent: async (payload) => {
|
|
651
|
+
const botCtx = { ...ctx, account: botAccount };
|
|
652
|
+
if (payload.event === "message.new") {
|
|
653
|
+
await handleInboundMessage(payload, botCtx);
|
|
654
|
+
}
|
|
655
|
+
else if (payload.event === "callback_query") {
|
|
656
|
+
await handleCallbackQuery(payload, botCtx);
|
|
657
|
+
}
|
|
658
|
+
else if (payload.event === "session.reset") {
|
|
659
|
+
await handleSessionReset(payload, botCtx);
|
|
660
|
+
}
|
|
661
|
+
else if (payload.event === "call.started") {
|
|
662
|
+
await handleCallStarted(payload, botCtx);
|
|
663
|
+
}
|
|
664
|
+
else if (payload.event === "call.ended") {
|
|
665
|
+
await handleCallEnded(payload, botCtx);
|
|
666
|
+
}
|
|
667
|
+
else if (payload.event === "voice.llm_request") {
|
|
668
|
+
return handleVoiceLlmRequest(payload, botCtx);
|
|
669
|
+
}
|
|
670
|
+
},
|
|
676
671
|
});
|
|
672
|
+
let unregisterHttp = () => { };
|
|
673
|
+
if (typeof registerPluginHttpRoute === "function") {
|
|
674
|
+
unregisterHttp = registerPluginHttpRoute({
|
|
675
|
+
path: botAccount.webhookPath,
|
|
676
|
+
auth: "plugin",
|
|
677
|
+
replaceExisting: true,
|
|
678
|
+
pluginId: "ozaiya",
|
|
679
|
+
source: "ozaiya-gateway",
|
|
680
|
+
log: (msg) => ctx.log?.info(msg),
|
|
681
|
+
handler: webhookHandler,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
ctx.log?.info(`[${botAccount.accountId}] registerPluginHttpRoute not available — webhook at ${botAccount.webhookPath} uses fallback`);
|
|
686
|
+
}
|
|
677
687
|
// Pre-fetch group keys for this bot
|
|
678
688
|
void fetchAndUnwrapGroupKeys(botAccount).then((count) => {
|
|
679
689
|
ctx.log?.info(`[${botAccount.accountId}] unwrapped ${count} group key(s)`);
|
|
@@ -736,6 +746,10 @@ export const ozaiyaPlugin = {
|
|
|
736
746
|
ctx.log?.warn?.(`[${botId}] Socket.io call.ended error: ${String(err)}`);
|
|
737
747
|
});
|
|
738
748
|
}
|
|
749
|
+
else if (payload.event === "voice.llm_request") {
|
|
750
|
+
// voice.llm_request requires HTTP streaming — not supported via socket.io fast-path
|
|
751
|
+
ctx.log?.warn?.(`[${botId}] voice.llm_request received via socket.io, ignoring (requires HTTP)`);
|
|
752
|
+
}
|
|
739
753
|
},
|
|
740
754
|
log: {
|
|
741
755
|
info: (msg) => ctx.log?.info(msg),
|
|
@@ -776,31 +790,38 @@ export const ozaiyaPlugin = {
|
|
|
776
790
|
const keyCount = await fetchAndUnwrapGroupKeys(account);
|
|
777
791
|
ctx.log?.info(`[${account.accountId}] unwrapped ${keyCount} group key(s)`);
|
|
778
792
|
// Register webhook HTTP handler
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
auth: "plugin",
|
|
782
|
-
replaceExisting: true,
|
|
783
|
-
pluginId: "ozaiya",
|
|
784
|
-
source: "ozaiya-channel",
|
|
793
|
+
const staticWebhookHandler = createOzaiyaWebhookHandler({
|
|
794
|
+
webhookSecret: account.webhookSecret,
|
|
785
795
|
log: (msg) => ctx.log?.info(msg),
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
await handleSessionReset(payload, ctx);
|
|
800
|
-
}
|
|
801
|
-
},
|
|
802
|
-
}),
|
|
796
|
+
onEvent: async (payload) => {
|
|
797
|
+
if (payload.event === "message.new") {
|
|
798
|
+
await handleInboundMessage(payload, ctx);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (payload.event === "callback_query") {
|
|
802
|
+
await handleCallbackQuery(payload, ctx);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
if (payload.event === "session.reset") {
|
|
806
|
+
await handleSessionReset(payload, ctx);
|
|
807
|
+
}
|
|
808
|
+
},
|
|
803
809
|
});
|
|
810
|
+
let unregisterHttp = () => { };
|
|
811
|
+
if (typeof registerPluginHttpRoute === "function") {
|
|
812
|
+
unregisterHttp = registerPluginHttpRoute({
|
|
813
|
+
path: account.webhookPath,
|
|
814
|
+
auth: "plugin",
|
|
815
|
+
replaceExisting: true,
|
|
816
|
+
pluginId: "ozaiya",
|
|
817
|
+
source: "ozaiya-channel",
|
|
818
|
+
log: (msg) => ctx.log?.info(msg),
|
|
819
|
+
handler: staticWebhookHandler,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
ctx.log?.info(`[${account.accountId}] registerPluginHttpRoute not available — webhook at ${account.webhookPath} uses fallback`);
|
|
824
|
+
}
|
|
804
825
|
ctx.log?.info(`[${account.accountId}] registered webhook at ${account.webhookPath}`);
|
|
805
826
|
// Block until abort
|
|
806
827
|
const stopHandler = () => {
|
|
@@ -1956,6 +1977,115 @@ ctx) {
|
|
|
1956
1977
|
},
|
|
1957
1978
|
});
|
|
1958
1979
|
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Handle voice.llm_request — Volcengine CustomLLM callback routed through Ozaiya server.
|
|
1982
|
+
* Dispatches the user's speech transcript to the OpenClaw agent and streams back
|
|
1983
|
+
* an OpenAI-compatible SSE response for Volcengine TTS.
|
|
1984
|
+
*/
|
|
1985
|
+
async function handleVoiceLlmRequest(payload,
|
|
1986
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1987
|
+
ctx) {
|
|
1988
|
+
const account = ctx.account;
|
|
1989
|
+
const runtime = getOzaiyaRuntime();
|
|
1990
|
+
const ch = runtime.channel;
|
|
1991
|
+
const lastUserMessage = payload.messages
|
|
1992
|
+
.filter((m) => m.role === "user")
|
|
1993
|
+
.pop()?.content ?? "";
|
|
1994
|
+
ctx.log?.info?.(`[${account.accountId}] voice.llm_request for call ${payload.callId}: "${lastUserMessage.slice(0, 80)}"`);
|
|
1995
|
+
// Resolve agent route
|
|
1996
|
+
const route = ch.routing.resolveAgentRoute({
|
|
1997
|
+
cfg: ctx.cfg,
|
|
1998
|
+
channel: "ozaiya",
|
|
1999
|
+
accountId: account.accountId,
|
|
2000
|
+
peer: { kind: "group", id: payload.groupId },
|
|
2001
|
+
});
|
|
2002
|
+
const fromAddress = `ozaiya:group:${payload.groupId}`;
|
|
2003
|
+
const conversationLabel = `group:${payload.groupId}`;
|
|
2004
|
+
const storePath = ch.session.resolveStorePath(undefined, { agentId: route.agentId });
|
|
2005
|
+
const previousTimestamp = ch.session.readSessionUpdatedAt({ storePath, sessionKey: route.sessionKey });
|
|
2006
|
+
const envelopeOptions = ch.reply.resolveEnvelopeFormatOptions(ctx.cfg);
|
|
2007
|
+
const voicePrompt = "[Voice Call — Doubao Engine] You are in a live voice call. Your response will be spoken aloud via TTS. " +
|
|
2008
|
+
"Rules: respond concisely (1-3 sentences), use natural spoken language, " +
|
|
2009
|
+
"never use markdown/code blocks/bullet lists/URLs/emojis. " +
|
|
2010
|
+
"Do not say \"sure\" or \"of course\" — just answer directly.";
|
|
2011
|
+
const bodyForAgent = `${voicePrompt}\n\n${lastUserMessage}`;
|
|
2012
|
+
const body = ch.reply.formatAgentEnvelope({
|
|
2013
|
+
channel: "Ozaiya",
|
|
2014
|
+
from: `Voice (${conversationLabel})`,
|
|
2015
|
+
timestamp: Date.now(),
|
|
2016
|
+
previousTimestamp,
|
|
2017
|
+
envelope: envelopeOptions,
|
|
2018
|
+
body: bodyForAgent,
|
|
2019
|
+
});
|
|
2020
|
+
const msgCtx = ch.reply.finalizeInboundContext({
|
|
2021
|
+
Body: body,
|
|
2022
|
+
BodyForAgent: bodyForAgent,
|
|
2023
|
+
RawBody: lastUserMessage,
|
|
2024
|
+
CommandBody: lastUserMessage,
|
|
2025
|
+
From: fromAddress,
|
|
2026
|
+
To: fromAddress,
|
|
2027
|
+
SessionKey: route.sessionKey,
|
|
2028
|
+
AccountId: route.accountId,
|
|
2029
|
+
ChatType: "group",
|
|
2030
|
+
ConversationLabel: conversationLabel,
|
|
2031
|
+
GroupSubject: payload.groupId,
|
|
2032
|
+
SenderId: "voice-caller",
|
|
2033
|
+
SenderName: "Voice Caller",
|
|
2034
|
+
Provider: "ozaiya",
|
|
2035
|
+
Surface: "ozaiya-voice",
|
|
2036
|
+
MessageSid: `voice-llm-${Date.now()}`,
|
|
2037
|
+
Timestamp: Date.now(),
|
|
2038
|
+
NumFiles: 0,
|
|
2039
|
+
NumMedia: 0,
|
|
2040
|
+
HasFiles: false,
|
|
2041
|
+
CommandAuthorized: true,
|
|
2042
|
+
OriginatingChannel: "ozaiya",
|
|
2043
|
+
OriginatingTo: fromAddress,
|
|
2044
|
+
});
|
|
2045
|
+
// Create a ReadableStream that produces OpenAI SSE chunks
|
|
2046
|
+
const responseId = `chatcmpl-${Date.now()}`;
|
|
2047
|
+
const encoder = new TextEncoder();
|
|
2048
|
+
const stream = new ReadableStream({
|
|
2049
|
+
async start(controller) {
|
|
2050
|
+
try {
|
|
2051
|
+
await ch.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
2052
|
+
ctx: msgCtx,
|
|
2053
|
+
cfg: ctx.cfg,
|
|
2054
|
+
dispatcherOptions: {
|
|
2055
|
+
deliver: async (replyPayload, _info) => {
|
|
2056
|
+
const text = replyPayload.text?.trim();
|
|
2057
|
+
if (!text)
|
|
2058
|
+
return;
|
|
2059
|
+
const chunk = JSON.stringify({
|
|
2060
|
+
id: responseId,
|
|
2061
|
+
object: "chat.completion.chunk",
|
|
2062
|
+
choices: [{ index: 0, delta: { content: text }, finish_reason: null }],
|
|
2063
|
+
});
|
|
2064
|
+
controller.enqueue(encoder.encode(`data: ${chunk}\n\n`));
|
|
2065
|
+
},
|
|
2066
|
+
onError: (err) => {
|
|
2067
|
+
ctx.log?.warn?.(`ozaiya: voice.llm_request dispatch error: ${String(err)}`);
|
|
2068
|
+
},
|
|
2069
|
+
},
|
|
2070
|
+
});
|
|
2071
|
+
// Send finish chunk
|
|
2072
|
+
const finishChunk = JSON.stringify({
|
|
2073
|
+
id: responseId,
|
|
2074
|
+
object: "chat.completion.chunk",
|
|
2075
|
+
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
|
2076
|
+
});
|
|
2077
|
+
controller.enqueue(encoder.encode(`data: ${finishChunk}\n\n`));
|
|
2078
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
2079
|
+
controller.close();
|
|
2080
|
+
}
|
|
2081
|
+
catch (err) {
|
|
2082
|
+
ctx.log?.warn?.(`ozaiya: voice.llm_request stream error: ${String(err)}`);
|
|
2083
|
+
controller.close();
|
|
2084
|
+
}
|
|
2085
|
+
},
|
|
2086
|
+
});
|
|
2087
|
+
return { contentType: "text/event-stream", body: stream };
|
|
2088
|
+
}
|
|
1959
2089
|
/**
|
|
1960
2090
|
* Handle call.ended webhook — disconnect the bot's VoiceCallSession.
|
|
1961
2091
|
*/
|