@ozaiya/openclaw-channel 0.7.1 → 0.7.3

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,15 +7,7 @@
7
7
  */
8
8
  import fs from "node:fs/promises";
9
9
  import path from "node:path";
10
- // registerPluginHttpRoute may not exist in older OpenClaw versions (< 2026.3.28)
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, accountConfig),
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
- // registerPluginHttpRoute is optional older OpenClaw versions use socket.io fallback
678
- const unregisterHttp = registerPluginHttpRoute
679
- ? registerPluginHttpRoute({
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
- : (() => { ctx.log?.info(`[${botAccount.accountId}] registerPluginHttpRoute not available, using socket.io only`); return () => { }; })();
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
- const unregisterHttp = registerPluginHttpRoute
809
- ? registerPluginHttpRoute({
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
- : (() => { ctx.log?.info(`[${account.accountId}] registerPluginHttpRoute not available, using socket.io only`); return () => { }; })();
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
  */