@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 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", "openai", "local-command"];
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
  };
@@ -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, accountConfig),
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 unregisterHttp = registerPluginHttpRoute({
648
- path: botAccount.webhookPath,
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
- handler: createOzaiyaWebhookHandler({
655
- webhookSecret: botAccount.webhookSecret,
656
- log: (msg) => ctx.log?.info(msg),
657
- onEvent: async (payload) => {
658
- const botCtx = { ...ctx, account: botAccount };
659
- if (payload.event === "message.new") {
660
- await handleInboundMessage(payload, botCtx);
661
- }
662
- else if (payload.event === "callback_query") {
663
- await handleCallbackQuery(payload, botCtx);
664
- }
665
- else if (payload.event === "session.reset") {
666
- await handleSessionReset(payload, botCtx);
667
- }
668
- else if (payload.event === "call.started") {
669
- await handleCallStarted(payload, botCtx);
670
- }
671
- else if (payload.event === "call.ended") {
672
- await handleCallEnded(payload, botCtx);
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 unregisterHttp = registerPluginHttpRoute({
780
- path: account.webhookPath,
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
- handler: createOzaiyaWebhookHandler({
787
- webhookSecret: account.webhookSecret,
788
- log: (msg) => ctx.log?.info(msg),
789
- onEvent: async (payload) => {
790
- if (payload.event === "message.new") {
791
- await handleInboundMessage(payload, ctx);
792
- return;
793
- }
794
- if (payload.event === "callback_query") {
795
- await handleCallbackQuery(payload, ctx);
796
- return;
797
- }
798
- if (payload.event === "session.reset") {
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
  */