@ozaiya/openclaw-channel 0.7.1 → 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 +134 -19
- 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,15 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import fs from "node:fs/promises";
|
|
9
9
|
import path from "node:path";
|
|
10
|
-
|
|
11
|
-
let registerPluginHttpRoute;
|
|
12
|
-
try {
|
|
13
|
-
const sdk = await import("openclaw/plugin-sdk");
|
|
14
|
-
registerPluginHttpRoute = sdk.registerPluginHttpRoute;
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
// Older OpenClaw — will use socket.io fallback
|
|
18
|
-
}
|
|
10
|
+
import { registerPluginHttpRoute } from "openclaw/plugin-sdk/webhook-ingress";
|
|
19
11
|
import { unwrapGroupKey, decryptMessage, encryptMessage, wrapGroupKey } from "./crypto.js";
|
|
20
12
|
import { sendMessage, probeApi, fetchGroups, addMember, getUserPublicKeys, toggleReaction, editMessage, deleteMessage, pinMessage, unpinMessage, uploadFile, searchUsers, fetchLinkPreview, joinCall, leaveCall, } from "./api.js";
|
|
21
13
|
import { botCreateDirect, botCreateGroup } from "./botActions.js";
|
|
@@ -124,7 +116,7 @@ function resolveAccount(cfg, accountId) {
|
|
|
124
116
|
webhookSecret: accountConfig.webhookSecret ?? "",
|
|
125
117
|
apiBaseUrl,
|
|
126
118
|
webhookPath: accountConfig.webhookPath ?? `${DEFAULT_WEBHOOK_PATH}/${id}`,
|
|
127
|
-
stt: resolveOzaiyaSttConfig(ozaiya
|
|
119
|
+
stt: resolveOzaiyaSttConfig(ozaiya),
|
|
128
120
|
};
|
|
129
121
|
}
|
|
130
122
|
// Default account (top-level fields)
|
|
@@ -672,11 +664,14 @@ export const ozaiyaPlugin = {
|
|
|
672
664
|
else if (payload.event === "call.ended") {
|
|
673
665
|
await handleCallEnded(payload, botCtx);
|
|
674
666
|
}
|
|
667
|
+
else if (payload.event === "voice.llm_request") {
|
|
668
|
+
return handleVoiceLlmRequest(payload, botCtx);
|
|
669
|
+
}
|
|
675
670
|
},
|
|
676
671
|
});
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
672
|
+
let unregisterHttp = () => { };
|
|
673
|
+
if (typeof registerPluginHttpRoute === "function") {
|
|
674
|
+
unregisterHttp = registerPluginHttpRoute({
|
|
680
675
|
path: botAccount.webhookPath,
|
|
681
676
|
auth: "plugin",
|
|
682
677
|
replaceExisting: true,
|
|
@@ -684,8 +679,11 @@ export const ozaiyaPlugin = {
|
|
|
684
679
|
source: "ozaiya-gateway",
|
|
685
680
|
log: (msg) => ctx.log?.info(msg),
|
|
686
681
|
handler: webhookHandler,
|
|
687
|
-
})
|
|
688
|
-
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
ctx.log?.info(`[${botAccount.accountId}] registerPluginHttpRoute not available — webhook at ${botAccount.webhookPath} uses fallback`);
|
|
686
|
+
}
|
|
689
687
|
// Pre-fetch group keys for this bot
|
|
690
688
|
void fetchAndUnwrapGroupKeys(botAccount).then((count) => {
|
|
691
689
|
ctx.log?.info(`[${botAccount.accountId}] unwrapped ${count} group key(s)`);
|
|
@@ -748,6 +746,10 @@ export const ozaiyaPlugin = {
|
|
|
748
746
|
ctx.log?.warn?.(`[${botId}] Socket.io call.ended error: ${String(err)}`);
|
|
749
747
|
});
|
|
750
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
|
+
}
|
|
751
753
|
},
|
|
752
754
|
log: {
|
|
753
755
|
info: (msg) => ctx.log?.info(msg),
|
|
@@ -805,8 +807,9 @@ export const ozaiyaPlugin = {
|
|
|
805
807
|
}
|
|
806
808
|
},
|
|
807
809
|
});
|
|
808
|
-
|
|
809
|
-
|
|
810
|
+
let unregisterHttp = () => { };
|
|
811
|
+
if (typeof registerPluginHttpRoute === "function") {
|
|
812
|
+
unregisterHttp = registerPluginHttpRoute({
|
|
810
813
|
path: account.webhookPath,
|
|
811
814
|
auth: "plugin",
|
|
812
815
|
replaceExisting: true,
|
|
@@ -814,8 +817,11 @@ export const ozaiyaPlugin = {
|
|
|
814
817
|
source: "ozaiya-channel",
|
|
815
818
|
log: (msg) => ctx.log?.info(msg),
|
|
816
819
|
handler: staticWebhookHandler,
|
|
817
|
-
})
|
|
818
|
-
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
ctx.log?.info(`[${account.accountId}] registerPluginHttpRoute not available — webhook at ${account.webhookPath} uses fallback`);
|
|
824
|
+
}
|
|
819
825
|
ctx.log?.info(`[${account.accountId}] registered webhook at ${account.webhookPath}`);
|
|
820
826
|
// Block until abort
|
|
821
827
|
const stopHandler = () => {
|
|
@@ -1971,6 +1977,115 @@ ctx) {
|
|
|
1971
1977
|
},
|
|
1972
1978
|
});
|
|
1973
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
|
+
}
|
|
1974
2089
|
/**
|
|
1975
2090
|
* Handle call.ended webhook — disconnect the bot's VoiceCallSession.
|
|
1976
2091
|
*/
|