@openclaw/voice-call 2026.5.27 → 2026.5.28-beta.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/{config-cNGVtrwa.js → config-U-rgixyY.js} +0 -4
- package/dist/{config-compat-4gaIelu_.js → config-compat-CokN3Zzr.js} +23 -5
- package/dist/{guarded-json-api-Dkeawg_W.js → guarded-json-api-hXuXY1dk.js} +1 -1
- package/dist/index.js +11 -11
- package/dist/{plivo-CNtzf7Do.js → plivo-CVgE_V_c.js} +2 -2
- package/dist/{realtime-handler-NP8w71q9.js → realtime-handler-DQDCDHv2.js} +78 -84
- package/dist/{response-generator-D7HL2sFM.js → response-generator-C8EHsqMw.js} +5 -5
- package/dist/{runtime-entry-CxeSe0VA.js → runtime-entry-DwBgs2Sq.js} +178 -55
- package/dist/runtime-entry.js +1 -1
- package/dist/setup-api.js +1 -1
- package/dist/{telnyx-CJAhbDYn.js → telnyx-C8sgJugJ.js} +1 -1
- package/dist/{twilio-Dn84Eomh.js → twilio-CpBg6Ir5.js} +8 -3
- package/npm-shrinkwrap.json +3 -3
- package/openclaw.plugin.json +1 -8
- package/package.json +4 -4
|
@@ -218,8 +218,6 @@ const VoiceCallRealtimeAgentContextConfigSchema = z.object({
|
|
|
218
218
|
maxChars: z.number().int().positive().default(6e3),
|
|
219
219
|
/** Include configured agent identity fields. */
|
|
220
220
|
includeIdentity: z.boolean().default(true),
|
|
221
|
-
/** Include agents.defaults/list systemPromptOverride when configured. */
|
|
222
|
-
includeSystemPrompt: z.boolean().default(true),
|
|
223
221
|
/** Include selected workspace files such as SOUL.md and IDENTITY.md. */
|
|
224
222
|
includeWorkspaceFiles: z.boolean().default(true),
|
|
225
223
|
/** Workspace-relative files to include, bounded by maxChars. */
|
|
@@ -232,7 +230,6 @@ const VoiceCallRealtimeAgentContextConfigSchema = z.object({
|
|
|
232
230
|
enabled: false,
|
|
233
231
|
maxChars: 6e3,
|
|
234
232
|
includeIdentity: true,
|
|
235
|
-
includeSystemPrompt: true,
|
|
236
233
|
includeWorkspaceFiles: true,
|
|
237
234
|
files: [
|
|
238
235
|
"SOUL.md",
|
|
@@ -293,7 +290,6 @@ const VoiceCallRealtimeConfigSchema = z.object({
|
|
|
293
290
|
enabled: false,
|
|
294
291
|
maxChars: 6e3,
|
|
295
292
|
includeIdentity: true,
|
|
296
|
-
includeSystemPrompt: true,
|
|
297
293
|
includeWorkspaceFiles: true,
|
|
298
294
|
files: [
|
|
299
295
|
"SOUL.md",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as VoiceCallConfigSchema } from "./config-
|
|
1
|
+
import { t as VoiceCallConfigSchema } from "./config-U-rgixyY.js";
|
|
2
2
|
import { asOptionalRecord, readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
3
|
//#region extensions/voice-call/src/config-compat.ts
|
|
4
4
|
const VOICE_CALL_LEGACY_CONFIG_REMOVAL_VERSION = "2026.6.0";
|
|
@@ -6,7 +6,7 @@ const asObject = asOptionalRecord;
|
|
|
6
6
|
const getString = readStringField;
|
|
7
7
|
function getNumber(obj, key) {
|
|
8
8
|
const value = obj?.[key];
|
|
9
|
-
return typeof value === "number" ? value : void 0;
|
|
9
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
10
10
|
}
|
|
11
11
|
function mergeProviderConfig(providersValue, providerId, compatValues) {
|
|
12
12
|
if (Object.keys(compatValues).length === 0) return asObject(providersValue);
|
|
@@ -22,6 +22,7 @@ function mergeProviderConfig(providersValue, providerId, compatValues) {
|
|
|
22
22
|
}
|
|
23
23
|
function collectVoiceCallLegacyConfigIssues(value) {
|
|
24
24
|
const raw = asObject(value) ?? {};
|
|
25
|
+
const realtimeAgentContext = asObject(asObject(raw.realtime)?.agentContext);
|
|
25
26
|
const twilio = asObject(raw.twilio);
|
|
26
27
|
const streaming = asObject(raw.streaming);
|
|
27
28
|
const issues = [];
|
|
@@ -60,6 +61,11 @@ function collectVoiceCallLegacyConfigIssues(value) {
|
|
|
60
61
|
replacement: "streaming.providers.openai.vadThreshold",
|
|
61
62
|
message: "Move streaming.vadThreshold to streaming.providers.openai.vadThreshold."
|
|
62
63
|
});
|
|
64
|
+
if (realtimeAgentContext && Object.prototype.hasOwnProperty.call(realtimeAgentContext, "includeSystemPrompt")) issues.push({
|
|
65
|
+
path: "realtime.agentContext.includeSystemPrompt",
|
|
66
|
+
replacement: "realtime.agentContext",
|
|
67
|
+
message: "Remove realtime.agentContext.includeSystemPrompt; realtime context now uses the generated agent prompt."
|
|
68
|
+
});
|
|
63
69
|
return issues;
|
|
64
70
|
}
|
|
65
71
|
function formatVoiceCallLegacyConfigWarnings(params) {
|
|
@@ -69,6 +75,8 @@ function formatVoiceCallLegacyConfigWarnings(params) {
|
|
|
69
75
|
}
|
|
70
76
|
function migrateVoiceCallLegacyConfigInput(params) {
|
|
71
77
|
const raw = asObject(params.value) ?? {};
|
|
78
|
+
const realtime = asObject(raw.realtime);
|
|
79
|
+
const realtimeAgentContext = asObject(realtime?.agentContext);
|
|
72
80
|
const twilio = asObject(raw.twilio);
|
|
73
81
|
const streaming = asObject(raw.streaming);
|
|
74
82
|
const configPathPrefix = params.configPathPrefix ?? "plugins.entries.voice-call.config";
|
|
@@ -98,12 +106,19 @@ function migrateVoiceCallLegacyConfigInput(params) {
|
|
|
98
106
|
}
|
|
99
107
|
const normalizedTwilio = twilio ? { ...twilio } : void 0;
|
|
100
108
|
if (normalizedTwilio) delete normalizedTwilio.from;
|
|
109
|
+
const normalizedRealtimeAgentContext = realtimeAgentContext ? { ...realtimeAgentContext } : void 0;
|
|
110
|
+
if (normalizedRealtimeAgentContext) delete normalizedRealtimeAgentContext.includeSystemPrompt;
|
|
111
|
+
const normalizedRealtime = realtime ? {
|
|
112
|
+
...realtime,
|
|
113
|
+
agentContext: normalizedRealtimeAgentContext ?? realtime.agentContext
|
|
114
|
+
} : void 0;
|
|
101
115
|
const config = {
|
|
102
116
|
...raw,
|
|
103
117
|
provider: raw.provider === "log" ? "mock" : raw.provider,
|
|
104
118
|
fromNumber: raw.fromNumber ?? (typeof twilio?.from === "string" ? twilio.from : void 0),
|
|
105
119
|
twilio: normalizedTwilio,
|
|
106
|
-
streaming: normalizedStreaming
|
|
120
|
+
streaming: normalizedStreaming,
|
|
121
|
+
realtime: normalizedRealtime
|
|
107
122
|
};
|
|
108
123
|
const changes = [];
|
|
109
124
|
if (raw.provider === "log") changes.push(`Moved ${configPathPrefix}.provider "log" → "mock".`);
|
|
@@ -111,8 +126,11 @@ function migrateVoiceCallLegacyConfigInput(params) {
|
|
|
111
126
|
if (typeof streaming?.sttProvider === "string") changes.push(`Moved ${configPathPrefix}.streaming.sttProvider → ${configPathPrefix}.streaming.provider.`);
|
|
112
127
|
if (typeof streaming?.openaiApiKey === "string") changes.push(`Moved ${configPathPrefix}.streaming.openaiApiKey → ${configPathPrefix}.streaming.providers.openai.apiKey.`);
|
|
113
128
|
if (typeof streaming?.sttModel === "string") changes.push(`Moved ${configPathPrefix}.streaming.sttModel → ${configPathPrefix}.streaming.providers.openai.model.`);
|
|
114
|
-
if (
|
|
115
|
-
if (typeof streaming?.
|
|
129
|
+
if (getNumber(streaming, "silenceDurationMs") !== void 0) changes.push(`Moved ${configPathPrefix}.streaming.silenceDurationMs → ${configPathPrefix}.streaming.providers.openai.silenceDurationMs.`);
|
|
130
|
+
else if (typeof streaming?.silenceDurationMs === "number") changes.push(`Removed invalid ${configPathPrefix}.streaming.silenceDurationMs.`);
|
|
131
|
+
if (getNumber(streaming, "vadThreshold") !== void 0) changes.push(`Moved ${configPathPrefix}.streaming.vadThreshold → ${configPathPrefix}.streaming.providers.openai.vadThreshold.`);
|
|
132
|
+
else if (typeof streaming?.vadThreshold === "number") changes.push(`Removed invalid ${configPathPrefix}.streaming.vadThreshold.`);
|
|
133
|
+
if (realtimeAgentContext && Object.prototype.hasOwnProperty.call(realtimeAgentContext, "includeSystemPrompt")) changes.push(`Removed ${configPathPrefix}.realtime.agentContext.includeSystemPrompt.`);
|
|
116
134
|
return {
|
|
117
135
|
config,
|
|
118
136
|
changes,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import { a as getHeader } from "./runtime-entry-
|
|
3
|
+
import { a as getHeader } from "./runtime-entry-DwBgs2Sq.js";
|
|
4
4
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
5
5
|
import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
|
|
6
6
|
import { normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { definePluginEntry, sleep } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import { i as resolveVoiceCallConfig, s as validateProviderConfig } from "./config-
|
|
4
|
-
import { c as getTailscaleSelfInfo, l as setupTailscaleExposureRoute, o as resolveWebhookExposureStatus, p as resolveUserPath, s as cleanupTailscaleExposureRoute, t as createVoiceCallRuntime } from "./runtime-entry-
|
|
5
|
-
import { i as parseVoiceCallPluginConfig, r as normalizeVoiceCallLegacyConfigInput, t as formatVoiceCallLegacyConfigWarnings } from "./config-compat-
|
|
3
|
+
import { i as resolveVoiceCallConfig, s as validateProviderConfig } from "./config-U-rgixyY.js";
|
|
4
|
+
import { c as getTailscaleSelfInfo, l as setupTailscaleExposureRoute, o as resolveWebhookExposureStatus, p as resolveUserPath, s as cleanupTailscaleExposureRoute, t as createVoiceCallRuntime } from "./runtime-entry-DwBgs2Sq.js";
|
|
5
|
+
import { i as parseVoiceCallPluginConfig, r as normalizeVoiceCallLegacyConfigInput, t as formatVoiceCallLegacyConfigWarnings } from "./config-compat-CokN3Zzr.js";
|
|
6
6
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
7
7
|
import { ErrorCodes, callGatewayFromCli, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
|
|
8
8
|
import { isRecord, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
@@ -12,6 +12,7 @@ import os from "node:os";
|
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { randomUUID } from "node:crypto";
|
|
14
14
|
import { format } from "node:util";
|
|
15
|
+
import { MAX_TCP_PORT, parseStrictNonNegativeInteger } from "openclaw/plugin-sdk/number-runtime";
|
|
15
16
|
//#region extensions/voice-call/src/cli.ts
|
|
16
17
|
const VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS = 5e3;
|
|
17
18
|
const VOICE_CALL_GATEWAY_OPERATION_TIMEOUT_MS = 3e4;
|
|
@@ -26,8 +27,8 @@ function writeStdoutJson(value) {
|
|
|
26
27
|
}
|
|
27
28
|
function parseVoiceCallIntOption(raw, optionName, opts) {
|
|
28
29
|
const min = opts?.min ?? 0;
|
|
29
|
-
const parsed =
|
|
30
|
-
if (
|
|
30
|
+
const parsed = parseStrictNonNegativeInteger(raw?.trim() ?? "");
|
|
31
|
+
if (parsed === void 0 || parsed < min || opts?.max !== void 0 && parsed > opts.max) throw new Error(`Invalid numeric value for ${optionName}: ${raw ?? ""}`);
|
|
31
32
|
return parsed;
|
|
32
33
|
}
|
|
33
34
|
function isGatewayUnavailableForLocalFallback(err) {
|
|
@@ -457,7 +458,10 @@ function registerVoiceCallCli(params) {
|
|
|
457
458
|
});
|
|
458
459
|
root.command("expose").description("Enable/disable Tailscale serve/funnel for the webhook").option("--mode <mode>", "off | serve (tailnet) | funnel (public)", "funnel").option("--path <path>", "Tailscale path to expose (recommend matching serve.path)").option("--port <port>", "Local webhook port").option("--serve-path <path>", "Local webhook path").action(async (options) => {
|
|
459
460
|
const mode = resolveMode(options.mode ?? "funnel");
|
|
460
|
-
const servePort = parseVoiceCallIntOption(options.port ?? String(config.serve.port ?? 3334), "--port", {
|
|
461
|
+
const servePort = parseVoiceCallIntOption(options.port ?? String(config.serve.port ?? 3334), "--port", {
|
|
462
|
+
min: 1,
|
|
463
|
+
max: MAX_TCP_PORT
|
|
464
|
+
});
|
|
461
465
|
const servePath = options.servePath ?? config.serve.path ?? "/voice/webhook";
|
|
462
466
|
const tsPath = options.path ?? config.tailscale?.path ?? servePath;
|
|
463
467
|
const localUrl = `http://127.0.0.1:${servePort}`;
|
|
@@ -763,7 +767,7 @@ const voiceCallConfigSchema = {
|
|
|
763
767
|
},
|
|
764
768
|
"realtime.agentContext.enabled": {
|
|
765
769
|
label: "Enable Agent Voice Context",
|
|
766
|
-
help: "Injects a compact agent identity
|
|
770
|
+
help: "Injects a compact agent identity and workspace context capsule into realtime voice instructions.",
|
|
767
771
|
advanced: true
|
|
768
772
|
},
|
|
769
773
|
"realtime.agentContext.maxChars": {
|
|
@@ -774,10 +778,6 @@ const voiceCallConfigSchema = {
|
|
|
774
778
|
label: "Include Agent Identity",
|
|
775
779
|
advanced: true
|
|
776
780
|
},
|
|
777
|
-
"realtime.agentContext.includeSystemPrompt": {
|
|
778
|
-
label: "Include Agent System Prompt",
|
|
779
|
-
advanced: true
|
|
780
|
-
},
|
|
781
781
|
"realtime.agentContext.includeWorkspaceFiles": {
|
|
782
782
|
label: "Include Agent Workspace Files",
|
|
783
783
|
advanced: true
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as getHeader, m as escapeXml } from "./runtime-entry-
|
|
2
|
-
import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
1
|
+
import { a as getHeader, m as escapeXml } from "./runtime-entry-DwBgs2Sq.js";
|
|
2
|
+
import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-hXuXY1dk.js";
|
|
3
3
|
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
//#region extensions/voice-call/src/providers/plivo.ts
|
|
@@ -142,9 +142,9 @@ function decodeMulawSample(value) {
|
|
|
142
142
|
//#region extensions/voice-call/src/webhook/stream-frame-adapter.ts
|
|
143
143
|
function parseTimestampMs(value) {
|
|
144
144
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
145
|
-
if (typeof value === "string") {
|
|
146
|
-
const parsed = Number.
|
|
147
|
-
return Number.
|
|
145
|
+
if (typeof value === "string" && /^[+-]?\d+$/.test(value.trim())) {
|
|
146
|
+
const parsed = Number(value.trim());
|
|
147
|
+
return Number.isSafeInteger(parsed) ? parsed : void 0;
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
function tryParseJson(rawMessage) {
|
|
@@ -154,73 +154,96 @@ function tryParseJson(rawMessage) {
|
|
|
154
154
|
} catch {}
|
|
155
155
|
return null;
|
|
156
156
|
}
|
|
157
|
+
function readRecordField(record, field) {
|
|
158
|
+
const value = record[field];
|
|
159
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
160
|
+
}
|
|
157
161
|
function normalizeBase64ForCompare(value) {
|
|
158
162
|
return value.replace(/=+$/u, "").replace(/-/gu, "+").replace(/_/gu, "/");
|
|
159
163
|
}
|
|
160
164
|
function isValidBase64Payload(value) {
|
|
161
165
|
return normalizeBase64ForCompare(Buffer.from(value, "base64").toString("base64")) === normalizeBase64ForCompare(value);
|
|
162
166
|
}
|
|
167
|
+
function parseMediaFrame(msg) {
|
|
168
|
+
const mediaData = readRecordField(msg, "media");
|
|
169
|
+
const payload = typeof mediaData?.payload === "string" ? mediaData.payload : void 0;
|
|
170
|
+
if (!payload || !isValidBase64Payload(payload)) return { kind: "ignored" };
|
|
171
|
+
return {
|
|
172
|
+
kind: "media",
|
|
173
|
+
payloadBase64: payload,
|
|
174
|
+
timestampMs: parseTimestampMs(mediaData?.timestamp),
|
|
175
|
+
track: typeof mediaData?.track === "string" ? mediaData.track : void 0
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function parseMarkFrame(msg) {
|
|
179
|
+
const markData = readRecordField(msg, "mark");
|
|
180
|
+
return {
|
|
181
|
+
kind: "mark",
|
|
182
|
+
name: typeof markData?.name === "string" ? markData.name : void 0
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function parseCommonInboundFrame(event, msg) {
|
|
186
|
+
if (event === "media") return parseMediaFrame(msg);
|
|
187
|
+
if (event === "mark") return parseMarkFrame(msg);
|
|
188
|
+
if (event === "stop") return { kind: "stop" };
|
|
189
|
+
}
|
|
190
|
+
function parseProviderInboundFrame(rawMessage, parseStartFrame, parseExtraFrame) {
|
|
191
|
+
const msg = tryParseJson(rawMessage);
|
|
192
|
+
if (!msg) return { kind: "ignored" };
|
|
193
|
+
const event = msg.event;
|
|
194
|
+
if (event === "start") return parseStartFrame(msg) ?? { kind: "ignored" };
|
|
195
|
+
return parseCommonInboundFrame(event, msg) ?? parseExtraFrame?.(event, msg) ?? { kind: "ignored" };
|
|
196
|
+
}
|
|
197
|
+
function withOptionalStreamSid(streamSid) {
|
|
198
|
+
return streamSid === void 0 ? {} : { streamSid };
|
|
199
|
+
}
|
|
200
|
+
function serializeMediaFrame(payloadBase64, streamSid) {
|
|
201
|
+
return JSON.stringify({
|
|
202
|
+
event: "media",
|
|
203
|
+
...withOptionalStreamSid(streamSid),
|
|
204
|
+
media: { payload: payloadBase64 }
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function serializeClearFrame(streamSid) {
|
|
208
|
+
return JSON.stringify({
|
|
209
|
+
event: "clear",
|
|
210
|
+
...withOptionalStreamSid(streamSid)
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
function serializeMarkFrame(name, streamSid) {
|
|
214
|
+
return JSON.stringify({
|
|
215
|
+
event: "mark",
|
|
216
|
+
...withOptionalStreamSid(streamSid),
|
|
217
|
+
mark: { name }
|
|
218
|
+
});
|
|
219
|
+
}
|
|
163
220
|
var TwilioStreamFrameAdapter = class {
|
|
164
221
|
constructor() {
|
|
165
222
|
this.providerName = "twilio";
|
|
166
223
|
this.streamSid = "";
|
|
167
224
|
}
|
|
168
225
|
parseInbound(rawMessage) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const event = msg.event;
|
|
172
|
-
if (event === "start") {
|
|
173
|
-
const startData = typeof msg.start === "object" && msg.start !== null ? msg.start : void 0;
|
|
226
|
+
return parseProviderInboundFrame(rawMessage, (msg) => {
|
|
227
|
+
const startData = readRecordField(msg, "start");
|
|
174
228
|
const streamSid = typeof startData?.streamSid === "string" ? startData.streamSid : "";
|
|
175
229
|
const callSid = typeof startData?.callSid === "string" ? startData.callSid : "";
|
|
176
|
-
if (!streamSid || !callSid) return
|
|
230
|
+
if (!streamSid || !callSid) return;
|
|
177
231
|
this.streamSid = streamSid;
|
|
178
232
|
return {
|
|
179
233
|
kind: "start",
|
|
180
234
|
streamId: streamSid,
|
|
181
235
|
providerCallId: callSid
|
|
182
236
|
};
|
|
183
|
-
}
|
|
184
|
-
if (event === "media") {
|
|
185
|
-
const mediaData = typeof msg.media === "object" && msg.media !== null ? msg.media : void 0;
|
|
186
|
-
const payload = typeof mediaData?.payload === "string" ? mediaData.payload : void 0;
|
|
187
|
-
if (!payload || !isValidBase64Payload(payload)) return { kind: "ignored" };
|
|
188
|
-
return {
|
|
189
|
-
kind: "media",
|
|
190
|
-
payloadBase64: payload,
|
|
191
|
-
timestampMs: parseTimestampMs(mediaData?.timestamp),
|
|
192
|
-
track: typeof mediaData?.track === "string" ? mediaData.track : void 0
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
if (event === "mark") {
|
|
196
|
-
const markData = typeof msg.mark === "object" && msg.mark !== null ? msg.mark : void 0;
|
|
197
|
-
return {
|
|
198
|
-
kind: "mark",
|
|
199
|
-
name: typeof markData?.name === "string" ? markData.name : void 0
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
if (event === "stop") return { kind: "stop" };
|
|
203
|
-
return { kind: "ignored" };
|
|
237
|
+
});
|
|
204
238
|
}
|
|
205
239
|
serializeMedia(payloadBase64) {
|
|
206
|
-
return
|
|
207
|
-
event: "media",
|
|
208
|
-
streamSid: this.streamSid,
|
|
209
|
-
media: { payload: payloadBase64 }
|
|
210
|
-
});
|
|
240
|
+
return serializeMediaFrame(payloadBase64, this.streamSid);
|
|
211
241
|
}
|
|
212
242
|
serializeClear() {
|
|
213
|
-
return
|
|
214
|
-
event: "clear",
|
|
215
|
-
streamSid: this.streamSid
|
|
216
|
-
});
|
|
243
|
+
return serializeClearFrame(this.streamSid);
|
|
217
244
|
}
|
|
218
245
|
serializeMark(name) {
|
|
219
|
-
return
|
|
220
|
-
event: "mark",
|
|
221
|
-
streamSid: this.streamSid,
|
|
222
|
-
mark: { name }
|
|
223
|
-
});
|
|
246
|
+
return serializeMarkFrame(name, this.streamSid);
|
|
224
247
|
}
|
|
225
248
|
};
|
|
226
249
|
var TelnyxStreamFrameAdapter = class {
|
|
@@ -228,64 +251,35 @@ var TelnyxStreamFrameAdapter = class {
|
|
|
228
251
|
this.providerName = "telnyx";
|
|
229
252
|
}
|
|
230
253
|
parseInbound(rawMessage) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const topLevelStreamId = typeof msg.stream_id === "string" && msg.stream_id ? msg.stream_id : void 0;
|
|
235
|
-
if (event === "start") {
|
|
236
|
-
const startData = typeof msg.start === "object" && msg.start !== null ? msg.start : void 0;
|
|
254
|
+
return parseProviderInboundFrame(rawMessage, (msg) => {
|
|
255
|
+
const topLevelStreamId = typeof msg.stream_id === "string" && msg.stream_id ? msg.stream_id : void 0;
|
|
256
|
+
const startData = readRecordField(msg, "start");
|
|
237
257
|
const providerCallId = typeof startData?.call_control_id === "string" && startData.call_control_id ? startData.call_control_id : void 0;
|
|
238
|
-
if (!topLevelStreamId || !providerCallId) return
|
|
258
|
+
if (!topLevelStreamId || !providerCallId) return;
|
|
239
259
|
return {
|
|
240
260
|
kind: "start",
|
|
241
261
|
streamId: topLevelStreamId,
|
|
242
262
|
providerCallId
|
|
243
263
|
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
const payload = typeof mediaData?.payload === "string" ? mediaData.payload : void 0;
|
|
248
|
-
if (!payload || !isValidBase64Payload(payload)) return { kind: "ignored" };
|
|
249
|
-
return {
|
|
250
|
-
kind: "media",
|
|
251
|
-
payloadBase64: payload,
|
|
252
|
-
timestampMs: parseTimestampMs(mediaData?.timestamp),
|
|
253
|
-
track: typeof mediaData?.track === "string" ? mediaData.track : void 0
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
if (event === "mark") {
|
|
257
|
-
const markData = typeof msg.mark === "object" && msg.mark !== null ? msg.mark : void 0;
|
|
258
|
-
return {
|
|
259
|
-
kind: "mark",
|
|
260
|
-
name: typeof markData?.name === "string" ? markData.name : void 0
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
if (event === "stop") return { kind: "stop" };
|
|
264
|
-
if (event === "error") {
|
|
265
|
-
const errorData = typeof msg.payload === "object" && msg.payload !== null ? msg.payload : void 0;
|
|
264
|
+
}, (event, msg) => {
|
|
265
|
+
if (event !== "error") return;
|
|
266
|
+
const errorData = readRecordField(msg, "payload");
|
|
266
267
|
return {
|
|
267
268
|
kind: "error",
|
|
268
269
|
code: typeof errorData?.code === "string" || typeof errorData?.code === "number" ? String(errorData.code) : void 0,
|
|
269
270
|
title: typeof errorData?.title === "string" ? errorData.title : void 0,
|
|
270
271
|
detail: typeof errorData?.detail === "string" ? errorData.detail : void 0
|
|
271
272
|
};
|
|
272
|
-
}
|
|
273
|
-
return { kind: "ignored" };
|
|
273
|
+
});
|
|
274
274
|
}
|
|
275
275
|
serializeMedia(payloadBase64) {
|
|
276
|
-
return
|
|
277
|
-
event: "media",
|
|
278
|
-
media: { payload: payloadBase64 }
|
|
279
|
-
});
|
|
276
|
+
return serializeMediaFrame(payloadBase64);
|
|
280
277
|
}
|
|
281
278
|
serializeClear() {
|
|
282
|
-
return
|
|
279
|
+
return serializeClearFrame();
|
|
283
280
|
}
|
|
284
281
|
serializeMark(name) {
|
|
285
|
-
return
|
|
286
|
-
event: "mark",
|
|
287
|
-
mark: { name }
|
|
288
|
-
});
|
|
282
|
+
return serializeMarkFrame(name);
|
|
289
283
|
}
|
|
290
284
|
};
|
|
291
285
|
//#endregion
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { o as resolveVoiceCallSessionKey } from "./config-
|
|
2
|
-
import { f as resolveVoiceResponseModel } from "./runtime-entry-
|
|
1
|
+
import { o as resolveVoiceCallSessionKey } from "./config-U-rgixyY.js";
|
|
2
|
+
import { f as resolveVoiceResponseModel } from "./runtime-entry-DwBgs2Sq.js";
|
|
3
3
|
import { isRecord, normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import { applyModelOverrideToSessionEntry } from "openclaw/plugin-sdk/model-session-runtime";
|
|
6
6
|
//#region extensions/voice-call/src/response-generator.ts
|
|
7
7
|
/**
|
|
8
|
-
* Voice call response generator - uses the embedded
|
|
8
|
+
* Voice call response generator - uses the embedded OpenClaw agent for tool support.
|
|
9
9
|
* Routes voice responses through the same agent infrastructure as messaging.
|
|
10
10
|
*/
|
|
11
11
|
function readExplicitToolsAllow(value) {
|
|
@@ -92,7 +92,7 @@ function resolveVoiceSandboxSessionKey(agentId, sessionKey) {
|
|
|
92
92
|
return `agent:${agentId}:${trimmed}`;
|
|
93
93
|
}
|
|
94
94
|
/**
|
|
95
|
-
* Generate a voice response using the embedded
|
|
95
|
+
* Generate a voice response using the embedded OpenClaw agent with full tool support.
|
|
96
96
|
* Uses the same agent infrastructure as messaging for consistent behavior.
|
|
97
97
|
*/
|
|
98
98
|
async function generateVoiceResponse(params) {
|
|
@@ -168,7 +168,7 @@ async function generateVoiceResponse(params) {
|
|
|
168
168
|
const timeoutMs = voiceConfig.responseTimeoutMs ?? agentRuntime.resolveAgentTimeoutMs({ cfg });
|
|
169
169
|
const runId = `voice:${callId}:${Date.now()}`;
|
|
170
170
|
try {
|
|
171
|
-
const result = await agentRuntime.
|
|
171
|
+
const result = await agentRuntime.runEmbeddedAgent({
|
|
172
172
|
sessionId,
|
|
173
173
|
sessionKey: resolvedSessionKey,
|
|
174
174
|
sandboxSessionKey: resolveVoiceSandboxSessionKey(agentId, resolvedSessionKey),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isBlockedHostnameOrIp, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import { a as resolveVoiceCallEffectiveConfig, c as deepMergeDefined, i as resolveVoiceCallConfig, n as normalizeVoiceCallConfig, o as resolveVoiceCallSessionKey, r as resolveTwilioAuthToken, s as validateProviderConfig } from "./config-
|
|
3
|
+
import { a as resolveVoiceCallEffectiveConfig, c as deepMergeDefined, i as resolveVoiceCallConfig, n as normalizeVoiceCallConfig, o as resolveVoiceCallSessionKey, r as resolveTwilioAuthToken, s as validateProviderConfig } from "./config-U-rgixyY.js";
|
|
4
4
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
5
5
|
import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
|
|
6
6
|
import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
@@ -339,7 +339,7 @@ function findCall(params) {
|
|
|
339
339
|
function resolveProviderVoiceSetting(providerConfig) {
|
|
340
340
|
if (!providerConfig || typeof providerConfig !== "object") return;
|
|
341
341
|
const candidate = providerConfig;
|
|
342
|
-
return normalizeOptionalString(candidate.voice) ?? normalizeOptionalString(candidate.voiceId);
|
|
342
|
+
return normalizeOptionalString(candidate.speakerVoice) ?? normalizeOptionalString(candidate.speakerVoiceId) ?? normalizeOptionalString(candidate.voice) ?? normalizeOptionalString(candidate.voiceId);
|
|
343
343
|
}
|
|
344
344
|
function resolvePreferredTtsVoice(config) {
|
|
345
345
|
const providerId = config.tts?.provider;
|
|
@@ -1250,13 +1250,6 @@ var CallManager = class {
|
|
|
1250
1250
|
};
|
|
1251
1251
|
//#endregion
|
|
1252
1252
|
//#region extensions/voice-call/src/realtime-agent-context.ts
|
|
1253
|
-
function readAgentEntries(cfg) {
|
|
1254
|
-
const agents = cfg.agents;
|
|
1255
|
-
return Array.isArray(agents?.list) ? agents.list.filter((entry) => Boolean(entry && typeof entry === "object")) : [];
|
|
1256
|
-
}
|
|
1257
|
-
function resolveAgentSystemPromptOverride(cfg, agentId) {
|
|
1258
|
-
return normalizeOptionalString(readAgentEntries(cfg).find((candidate) => normalizeOptionalString(candidate.id) === agentId)?.systemPromptOverride) ?? normalizeOptionalString(cfg.agents?.defaults?.systemPromptOverride);
|
|
1259
|
-
}
|
|
1260
1253
|
function limitText(text, maxChars) {
|
|
1261
1254
|
if (text.length <= maxChars) return text;
|
|
1262
1255
|
return `${text.slice(0, Math.max(0, maxChars - 32)).trimEnd()}\n[truncated]`;
|
|
@@ -1301,10 +1294,6 @@ async function buildRealtimeVoiceInstructions(params) {
|
|
|
1301
1294
|
].filter(Boolean);
|
|
1302
1295
|
if (identityLines.length > 0) capsule.push(`Configured identity:\n${identityLines.join("\n")}`);
|
|
1303
1296
|
}
|
|
1304
|
-
if (contextConfig.includeSystemPrompt) {
|
|
1305
|
-
const systemPrompt = resolveAgentSystemPromptOverride(params.coreConfig, agentId);
|
|
1306
|
-
if (systemPrompt) capsule.push(`Configured system prompt override:\n${systemPrompt}`);
|
|
1307
|
-
}
|
|
1308
1297
|
if (contextConfig.includeWorkspaceFiles) {
|
|
1309
1298
|
const fileSections = await readWorkspaceVoiceContextFiles({
|
|
1310
1299
|
workspaceDir: params.agentRuntime.resolveAgentWorkspaceDir(params.coreConfig, agentId),
|
|
@@ -1464,15 +1453,58 @@ function collectTelephonyProviderConfigs(ttsConfig) {
|
|
|
1464
1453
|
return entries;
|
|
1465
1454
|
}
|
|
1466
1455
|
//#endregion
|
|
1456
|
+
//#region extensions/voice-call/src/bounded-child-output.ts
|
|
1457
|
+
const DEFAULT_MAX_OUTPUT_CHARS = 16384;
|
|
1458
|
+
function emptyBoundedChildOutput() {
|
|
1459
|
+
return {
|
|
1460
|
+
text: "",
|
|
1461
|
+
truncated: false
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
function appendBoundedChildOutput(current, chunk, maxChars = DEFAULT_MAX_OUTPUT_CHARS) {
|
|
1465
|
+
const appended = current.text + chunk;
|
|
1466
|
+
if (appended.length <= maxChars) return {
|
|
1467
|
+
text: appended,
|
|
1468
|
+
truncated: current.truncated
|
|
1469
|
+
};
|
|
1470
|
+
return {
|
|
1471
|
+
text: appended.slice(-maxChars),
|
|
1472
|
+
truncated: true
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
function formatBoundedChildOutput(output) {
|
|
1476
|
+
return output.truncated ? `[output truncated]\n${output.text}` : output.text;
|
|
1477
|
+
}
|
|
1478
|
+
//#endregion
|
|
1467
1479
|
//#region extensions/voice-call/src/webhook/tailscale.ts
|
|
1480
|
+
const TAILSCALE_COMMAND_STDOUT_MAX_BYTES = 4 * 1024 * 1024;
|
|
1481
|
+
function appendTailscaleCommandStdout(current, data, maxBytes = TAILSCALE_COMMAND_STDOUT_MAX_BYTES) {
|
|
1482
|
+
if (current.exceeded) return current;
|
|
1483
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
1484
|
+
const bytes = current.bytes + buffer.byteLength;
|
|
1485
|
+
if (bytes > maxBytes) return {
|
|
1486
|
+
bytes,
|
|
1487
|
+
exceeded: true,
|
|
1488
|
+
text: ""
|
|
1489
|
+
};
|
|
1490
|
+
return {
|
|
1491
|
+
bytes,
|
|
1492
|
+
exceeded: false,
|
|
1493
|
+
text: `${current.text}${buffer.toString("utf8")}`
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1468
1496
|
function runTailscaleCommand(args, timeoutMs = 2500) {
|
|
1469
1497
|
return new Promise((resolve) => {
|
|
1470
1498
|
const proc = spawn("tailscale", args, { stdio: [
|
|
1471
1499
|
"ignore",
|
|
1472
1500
|
"pipe",
|
|
1473
|
-
"
|
|
1501
|
+
"ignore"
|
|
1474
1502
|
] });
|
|
1475
|
-
let stdout =
|
|
1503
|
+
let stdout = {
|
|
1504
|
+
bytes: 0,
|
|
1505
|
+
exceeded: false,
|
|
1506
|
+
text: ""
|
|
1507
|
+
};
|
|
1476
1508
|
let settled = false;
|
|
1477
1509
|
let timer;
|
|
1478
1510
|
const finish = (result) => {
|
|
@@ -1482,7 +1514,14 @@ function runTailscaleCommand(args, timeoutMs = 2500) {
|
|
|
1482
1514
|
resolve(result);
|
|
1483
1515
|
};
|
|
1484
1516
|
proc.stdout.on("data", (data) => {
|
|
1485
|
-
stdout
|
|
1517
|
+
stdout = appendTailscaleCommandStdout(stdout, data);
|
|
1518
|
+
if (stdout.exceeded) {
|
|
1519
|
+
proc.kill("SIGKILL");
|
|
1520
|
+
finish({
|
|
1521
|
+
code: -1,
|
|
1522
|
+
stdout: ""
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1486
1525
|
});
|
|
1487
1526
|
timer = setTimeout(() => {
|
|
1488
1527
|
proc.kill("SIGKILL");
|
|
@@ -1500,13 +1539,17 @@ function runTailscaleCommand(args, timeoutMs = 2500) {
|
|
|
1500
1539
|
proc.on("close", (code) => {
|
|
1501
1540
|
finish({
|
|
1502
1541
|
code: code ?? -1,
|
|
1503
|
-
stdout
|
|
1542
|
+
stdout: stdout.text
|
|
1504
1543
|
});
|
|
1505
1544
|
});
|
|
1506
1545
|
});
|
|
1507
1546
|
}
|
|
1508
1547
|
async function getTailscaleSelfInfo() {
|
|
1509
|
-
const { code, stdout } = await runTailscaleCommand([
|
|
1548
|
+
const { code, stdout } = await runTailscaleCommand([
|
|
1549
|
+
"status",
|
|
1550
|
+
"--json",
|
|
1551
|
+
"--peers=false"
|
|
1552
|
+
]);
|
|
1510
1553
|
if (code !== 0) return null;
|
|
1511
1554
|
try {
|
|
1512
1555
|
const status = JSON.parse(stdout);
|
|
@@ -1569,6 +1612,7 @@ async function cleanupTailscaleExposure(config) {
|
|
|
1569
1612
|
}
|
|
1570
1613
|
//#endregion
|
|
1571
1614
|
//#region extensions/voice-call/src/tunnel.ts
|
|
1615
|
+
const NGROK_LOG_BUFFER_MAX_CHARS = 16384;
|
|
1572
1616
|
/**
|
|
1573
1617
|
* Start an ngrok tunnel to expose the local webhook server.
|
|
1574
1618
|
*
|
|
@@ -1635,9 +1679,9 @@ async function startNgrokTunnel(config) {
|
|
|
1635
1679
|
} catch {}
|
|
1636
1680
|
};
|
|
1637
1681
|
proc.stdout.on("data", (data) => {
|
|
1638
|
-
outputBuffer
|
|
1639
|
-
const lines = outputBuffer.split("\n");
|
|
1682
|
+
const lines = (outputBuffer + data.toString()).split("\n");
|
|
1640
1683
|
outputBuffer = lines.pop() || "";
|
|
1684
|
+
if (outputBuffer.length > NGROK_LOG_BUFFER_MAX_CHARS) outputBuffer = outputBuffer.slice(-16384);
|
|
1641
1685
|
for (const line of lines) if (line.trim()) processLine(line);
|
|
1642
1686
|
});
|
|
1643
1687
|
proc.stderr.on("data", (data) => {
|
|
@@ -1646,7 +1690,8 @@ async function startNgrokTunnel(config) {
|
|
|
1646
1690
|
if (!resolved) {
|
|
1647
1691
|
resolved = true;
|
|
1648
1692
|
clearTimeout(timeout);
|
|
1649
|
-
|
|
1693
|
+
const output = appendBoundedChildOutput(emptyBoundedChildOutput(), msg);
|
|
1694
|
+
reject(/* @__PURE__ */ new Error(`ngrok error: ${formatBoundedChildOutput(output)}`));
|
|
1650
1695
|
}
|
|
1651
1696
|
}
|
|
1652
1697
|
});
|
|
@@ -1676,17 +1721,20 @@ async function runNgrokCommand(args) {
|
|
|
1676
1721
|
"pipe",
|
|
1677
1722
|
"pipe"
|
|
1678
1723
|
] });
|
|
1679
|
-
let stdout =
|
|
1680
|
-
let stderr =
|
|
1724
|
+
let stdout = emptyBoundedChildOutput();
|
|
1725
|
+
let stderr = emptyBoundedChildOutput();
|
|
1681
1726
|
proc.stdout.on("data", (data) => {
|
|
1682
|
-
stdout
|
|
1727
|
+
stdout = appendBoundedChildOutput(stdout, data.toString());
|
|
1683
1728
|
});
|
|
1684
1729
|
proc.stderr.on("data", (data) => {
|
|
1685
|
-
stderr
|
|
1730
|
+
stderr = appendBoundedChildOutput(stderr, data.toString());
|
|
1686
1731
|
});
|
|
1687
1732
|
proc.on("close", (code) => {
|
|
1688
|
-
if (code === 0) resolve(stdout);
|
|
1689
|
-
else
|
|
1733
|
+
if (code === 0) resolve(stdout.text);
|
|
1734
|
+
else {
|
|
1735
|
+
const output = stderr.text ? stderr : stdout;
|
|
1736
|
+
reject(/* @__PURE__ */ new Error(`ngrok command failed: ${formatBoundedChildOutput(output)}`));
|
|
1737
|
+
}
|
|
1690
1738
|
});
|
|
1691
1739
|
proc.on("error", reject);
|
|
1692
1740
|
});
|
|
@@ -1712,10 +1760,18 @@ async function startTailscaleTunnel(config) {
|
|
|
1712
1760
|
"pipe",
|
|
1713
1761
|
"pipe"
|
|
1714
1762
|
] });
|
|
1763
|
+
let stdout = emptyBoundedChildOutput();
|
|
1764
|
+
let stderr = emptyBoundedChildOutput();
|
|
1715
1765
|
const timeout = setTimeout(() => {
|
|
1716
1766
|
proc.kill("SIGKILL");
|
|
1717
1767
|
reject(/* @__PURE__ */ new Error(`Tailscale ${config.mode} timed out`));
|
|
1718
1768
|
}, 1e4);
|
|
1769
|
+
proc.stdout.on("data", (data) => {
|
|
1770
|
+
stdout = appendBoundedChildOutput(stdout, data.toString());
|
|
1771
|
+
});
|
|
1772
|
+
proc.stderr.on("data", (data) => {
|
|
1773
|
+
stderr = appendBoundedChildOutput(stderr, data.toString());
|
|
1774
|
+
});
|
|
1719
1775
|
proc.on("close", (code) => {
|
|
1720
1776
|
clearTimeout(timeout);
|
|
1721
1777
|
if (code === 0) {
|
|
@@ -1728,7 +1784,11 @@ async function startTailscaleTunnel(config) {
|
|
|
1728
1784
|
await stopTailscaleTunnel(config.mode, path);
|
|
1729
1785
|
}
|
|
1730
1786
|
});
|
|
1731
|
-
} else
|
|
1787
|
+
} else {
|
|
1788
|
+
const output = stderr.text ? stderr : stdout;
|
|
1789
|
+
const detail = output.text ? `: ${formatBoundedChildOutput(output)}` : "";
|
|
1790
|
+
reject(/* @__PURE__ */ new Error(`Tailscale ${config.mode} failed with code ${code}${detail}`));
|
|
1791
|
+
}
|
|
1732
1792
|
});
|
|
1733
1793
|
proc.on("error", (err) => {
|
|
1734
1794
|
clearTimeout(timeout);
|
|
@@ -2528,7 +2588,7 @@ function loadRealtimeTranscriptionRuntime() {
|
|
|
2528
2588
|
return realtimeTranscriptionRuntimePromise;
|
|
2529
2589
|
}
|
|
2530
2590
|
function loadResponseGeneratorModule() {
|
|
2531
|
-
responseGeneratorModulePromise ??= import("./response-generator-
|
|
2591
|
+
responseGeneratorModulePromise ??= import("./response-generator-C8EHsqMw.js");
|
|
2532
2592
|
return responseGeneratorModulePromise;
|
|
2533
2593
|
}
|
|
2534
2594
|
function sanitizeTranscriptForLog(value) {
|
|
@@ -2595,6 +2655,23 @@ function buildRealtimeRejectedTwiML() {
|
|
|
2595
2655
|
body: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><Reject reason=\"rejected\" /></Response>"
|
|
2596
2656
|
};
|
|
2597
2657
|
}
|
|
2658
|
+
function buildTwilioReplayTwiML() {
|
|
2659
|
+
return {
|
|
2660
|
+
statusCode: 200,
|
|
2661
|
+
headers: { "Content-Type": "text/xml" },
|
|
2662
|
+
body: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response></Response>"
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
const WEBHOOK_REPLAY_RESPONSE_TTL_MS = 600 * 1e3;
|
|
2666
|
+
const WEBHOOK_REPLAY_RESPONSE_MAX_ENTRIES = 1e4;
|
|
2667
|
+
const WEBHOOK_REPLAY_RESPONSE_PRUNE_INTERVAL = 64;
|
|
2668
|
+
function cloneWebhookResponsePayload(payload) {
|
|
2669
|
+
return {
|
|
2670
|
+
statusCode: payload.statusCode,
|
|
2671
|
+
headers: payload.headers ? { ...payload.headers } : void 0,
|
|
2672
|
+
body: payload.body
|
|
2673
|
+
};
|
|
2674
|
+
}
|
|
2598
2675
|
/**
|
|
2599
2676
|
* HTTP server for receiving voice call webhooks from providers.
|
|
2600
2677
|
* Supports WebSocket upgrades for media streams when streaming is enabled.
|
|
@@ -2609,6 +2686,8 @@ var VoiceCallWebhookServer = class {
|
|
|
2609
2686
|
this.mediaStreamHandler = null;
|
|
2610
2687
|
this.pendingDisconnectHangups = /* @__PURE__ */ new Map();
|
|
2611
2688
|
this.realtimeHandler = null;
|
|
2689
|
+
this.replayResponses = /* @__PURE__ */ new Map();
|
|
2690
|
+
this.replayResponseCacheCalls = 0;
|
|
2612
2691
|
this.config = normalizeVoiceCallConfig(config);
|
|
2613
2692
|
this.manager = manager;
|
|
2614
2693
|
this.provider = provider;
|
|
@@ -2976,34 +3055,78 @@ var VoiceCallWebhookServer = class {
|
|
|
2976
3055
|
body: "Unauthorized"
|
|
2977
3056
|
};
|
|
2978
3057
|
}
|
|
2979
|
-
const
|
|
2980
|
-
if (
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
headers: { "Content-Type": "application/xml" },
|
|
2986
|
-
body: initialTwiML
|
|
2987
|
-
};
|
|
3058
|
+
const isReplay = Boolean(verification.isReplay);
|
|
3059
|
+
if (isReplay) {
|
|
3060
|
+
console.warn("[voice-call] Replay detected; skipping event side effects");
|
|
3061
|
+
if (this.provider.name === "twilio") return buildTwilioReplayTwiML();
|
|
3062
|
+
const cachedResponse = await this.getCachedReplayResponse(verification.verifiedRequestKey);
|
|
3063
|
+
if (cachedResponse) return cachedResponse;
|
|
2988
3064
|
}
|
|
2989
|
-
const
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
console.log(
|
|
2994
|
-
return
|
|
3065
|
+
const buildResponse = async () => {
|
|
3066
|
+
const initialTwiML = this.provider.consumeInitialTwiML?.(ctx);
|
|
3067
|
+
if (initialTwiML !== void 0 && initialTwiML !== null) {
|
|
3068
|
+
const params = new URLSearchParams(ctx.rawBody);
|
|
3069
|
+
console.log(`[voice-call] Serving provider initial TwiML before realtime handling (callSid=${params.get("CallSid") ?? "unknown"}, direction=${params.get("Direction") ?? "unknown"})`);
|
|
3070
|
+
return {
|
|
3071
|
+
statusCode: 200,
|
|
3072
|
+
headers: { "Content-Type": "application/xml" },
|
|
3073
|
+
body: initialTwiML
|
|
3074
|
+
};
|
|
2995
3075
|
}
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3076
|
+
const realtimeParams = this.getRealtimeTwimlParams(ctx);
|
|
3077
|
+
if (realtimeParams) {
|
|
3078
|
+
const direction = realtimeParams.get("Direction");
|
|
3079
|
+
if ((!direction || direction === "inbound") && !this.shouldAcceptRealtimeInboundRequest(realtimeParams)) {
|
|
3080
|
+
console.log("[voice-call] Realtime inbound call rejected before stream setup");
|
|
3081
|
+
return buildRealtimeRejectedTwiML();
|
|
3082
|
+
}
|
|
3083
|
+
console.log(`[voice-call] Serving realtime TwiML for Twilio call ${realtimeParams.get("CallSid") ?? "unknown"} (direction=${direction ?? "unknown"})`);
|
|
3084
|
+
return this.realtimeHandler.buildTwiMLPayload(req, realtimeParams);
|
|
3085
|
+
}
|
|
3086
|
+
const parsed = this.provider.parseWebhookEvent(ctx, { verifiedRequestKey: verification.verifiedRequestKey });
|
|
3087
|
+
if (!isReplay) this.processParsedEvents(parsed.events);
|
|
3088
|
+
return normalizeWebhookResponse(parsed);
|
|
3089
|
+
};
|
|
3090
|
+
if (isReplay) return await buildResponse();
|
|
3091
|
+
if (this.provider.name === "twilio") return await buildResponse();
|
|
3092
|
+
return await this.cacheReplayResponse(verification.verifiedRequestKey, buildResponse);
|
|
3003
3093
|
} finally {
|
|
3004
3094
|
this.webhookInFlightLimiter.release(inFlightKey);
|
|
3005
3095
|
}
|
|
3006
3096
|
}
|
|
3097
|
+
pruneReplayResponses(now) {
|
|
3098
|
+
for (const [key, entry] of this.replayResponses) if (entry.expiresAt <= now) this.replayResponses.delete(key);
|
|
3099
|
+
while (this.replayResponses.size > WEBHOOK_REPLAY_RESPONSE_MAX_ENTRIES) {
|
|
3100
|
+
const oldest = this.replayResponses.keys().next().value;
|
|
3101
|
+
if (!oldest) break;
|
|
3102
|
+
this.replayResponses.delete(oldest);
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
async getCachedReplayResponse(key) {
|
|
3106
|
+
const now = Date.now();
|
|
3107
|
+
const entry = this.replayResponses.get(key);
|
|
3108
|
+
if (!entry) return null;
|
|
3109
|
+
if (entry.expiresAt <= now) {
|
|
3110
|
+
this.replayResponses.delete(key);
|
|
3111
|
+
return null;
|
|
3112
|
+
}
|
|
3113
|
+
return cloneWebhookResponsePayload(await entry.response);
|
|
3114
|
+
}
|
|
3115
|
+
async cacheReplayResponse(key, buildResponse) {
|
|
3116
|
+
const now = Date.now();
|
|
3117
|
+
this.replayResponseCacheCalls += 1;
|
|
3118
|
+
if (this.replayResponseCacheCalls % WEBHOOK_REPLAY_RESPONSE_PRUNE_INTERVAL === 0) this.pruneReplayResponses(now);
|
|
3119
|
+
const response = buildResponse().then(cloneWebhookResponsePayload).catch((err) => {
|
|
3120
|
+
this.replayResponses.delete(key);
|
|
3121
|
+
throw err;
|
|
3122
|
+
});
|
|
3123
|
+
this.replayResponses.set(key, {
|
|
3124
|
+
expiresAt: now + WEBHOOK_REPLAY_RESPONSE_TTL_MS,
|
|
3125
|
+
response
|
|
3126
|
+
});
|
|
3127
|
+
if (this.replayResponses.size > WEBHOOK_REPLAY_RESPONSE_MAX_ENTRIES) this.pruneReplayResponses(now);
|
|
3128
|
+
return cloneWebhookResponsePayload(await response);
|
|
3129
|
+
}
|
|
3007
3130
|
verifyPreAuthWebhookHeaders(headers) {
|
|
3008
3131
|
if (this.config.skipSignatureVerification) return { ok: true };
|
|
3009
3132
|
switch (this.provider.name) {
|
|
@@ -3145,15 +3268,15 @@ let mockProviderPromise;
|
|
|
3145
3268
|
let realtimeVoiceRuntimePromise;
|
|
3146
3269
|
let realtimeHandlerPromise;
|
|
3147
3270
|
function loadTelnyxProvider() {
|
|
3148
|
-
telnyxProviderPromise ??= import("./telnyx-
|
|
3271
|
+
telnyxProviderPromise ??= import("./telnyx-C8sgJugJ.js");
|
|
3149
3272
|
return telnyxProviderPromise;
|
|
3150
3273
|
}
|
|
3151
3274
|
function loadTwilioProvider() {
|
|
3152
|
-
twilioProviderPromise ??= import("./twilio-
|
|
3275
|
+
twilioProviderPromise ??= import("./twilio-CpBg6Ir5.js");
|
|
3153
3276
|
return twilioProviderPromise;
|
|
3154
3277
|
}
|
|
3155
3278
|
function loadPlivoProvider() {
|
|
3156
|
-
plivoProviderPromise ??= import("./plivo-
|
|
3279
|
+
plivoProviderPromise ??= import("./plivo-CVgE_V_c.js");
|
|
3157
3280
|
return plivoProviderPromise;
|
|
3158
3281
|
}
|
|
3159
3282
|
function loadMockProvider() {
|
|
@@ -3165,7 +3288,7 @@ function loadRealtimeVoiceRuntime() {
|
|
|
3165
3288
|
return realtimeVoiceRuntimePromise;
|
|
3166
3289
|
}
|
|
3167
3290
|
function loadRealtimeHandler() {
|
|
3168
|
-
realtimeHandlerPromise ??= import("./realtime-handler-
|
|
3291
|
+
realtimeHandlerPromise ??= import("./realtime-handler-DQDCDHv2.js");
|
|
3169
3292
|
return realtimeHandlerPromise;
|
|
3170
3293
|
}
|
|
3171
3294
|
function resolveVoiceCallConsultSessionKey(call) {
|
package/dist/runtime-entry.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as createVoiceCallRuntime } from "./runtime-entry-
|
|
1
|
+
import { t as createVoiceCallRuntime } from "./runtime-entry-DwBgs2Sq.js";
|
|
2
2
|
export { createVoiceCallRuntime };
|
package/dist/setup-api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as migrateVoiceCallLegacyConfigInput } from "./config-compat-
|
|
1
|
+
import { n as migrateVoiceCallLegacyConfigInput } from "./config-compat-CokN3Zzr.js";
|
|
2
2
|
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
3
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
4
4
|
//#region extensions/voice-call/setup-api.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
1
|
+
import { i as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-hXuXY1dk.js";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
//#region extensions/voice-call/src/providers/telnyx.ts
|
|
4
4
|
function normalizeTelnyxDirection(direction) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import { a as getHeader, d as chunkAudio, h as mapVoiceToPolly, i as normalizeProviderStatus, m as escapeXml, n as isProviderStatusTerminal, r as mapProviderStatusToEndReason } from "./runtime-entry-
|
|
4
|
-
import { a as verifyTwilioWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
3
|
+
import { a as getHeader, d as chunkAudio, h as mapVoiceToPolly, i as normalizeProviderStatus, m as escapeXml, n as isProviderStatusTerminal, r as mapProviderStatusToEndReason } from "./runtime-entry-DwBgs2Sq.js";
|
|
4
|
+
import { a as verifyTwilioWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-hXuXY1dk.js";
|
|
5
5
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
6
6
|
import crypto from "node:crypto";
|
|
7
7
|
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
@@ -314,6 +314,11 @@ var TwilioProvider = class TwilioProvider {
|
|
|
314
314
|
if (direction === "inbound") return "inbound";
|
|
315
315
|
if (direction === "outbound-api" || direction === "outbound-dial") return "outbound";
|
|
316
316
|
}
|
|
317
|
+
static parseConfidence(value) {
|
|
318
|
+
const trimmed = value?.trim();
|
|
319
|
+
if (!trimmed || !/^\d+(?:\.\d+)?$/.test(trimmed)) return .9;
|
|
320
|
+
return Number(trimmed);
|
|
321
|
+
}
|
|
317
322
|
/**
|
|
318
323
|
* Convert Twilio webhook params to normalized event format.
|
|
319
324
|
*/
|
|
@@ -337,7 +342,7 @@ var TwilioProvider = class TwilioProvider {
|
|
|
337
342
|
type: "call.speech",
|
|
338
343
|
transcript: speechResult,
|
|
339
344
|
isFinal: true,
|
|
340
|
-
confidence:
|
|
345
|
+
confidence: TwilioProvider.parseConfidence(params.get("Confidence"))
|
|
341
346
|
};
|
|
342
347
|
const digits = params.get("Digits");
|
|
343
348
|
if (digits) return {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/voice-call",
|
|
3
|
-
"version": "2026.5.
|
|
3
|
+
"version": "2026.5.28-beta.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@openclaw/voice-call",
|
|
9
|
-
"version": "2026.5.
|
|
9
|
+
"version": "2026.5.28-beta.2",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"commander": "14.0.3",
|
|
12
12
|
"typebox": "1.1.38",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"zod": "4.4.3"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
|
-
"openclaw": ">=2026.5.
|
|
17
|
+
"openclaw": ">=2026.5.28-beta.2"
|
|
18
18
|
},
|
|
19
19
|
"peerDependenciesMeta": {
|
|
20
20
|
"openclaw": {
|
package/openclaw.plugin.json
CHANGED
|
@@ -188,7 +188,7 @@
|
|
|
188
188
|
},
|
|
189
189
|
"realtime.agentContext.enabled": {
|
|
190
190
|
"label": "Enable Agent Voice Context",
|
|
191
|
-
"help": "Injects a compact agent identity
|
|
191
|
+
"help": "Injects a compact agent identity and workspace context capsule into realtime voice instructions.",
|
|
192
192
|
"advanced": true
|
|
193
193
|
},
|
|
194
194
|
"realtime.agentContext.maxChars": {
|
|
@@ -199,10 +199,6 @@
|
|
|
199
199
|
"label": "Include Agent Identity",
|
|
200
200
|
"advanced": true
|
|
201
201
|
},
|
|
202
|
-
"realtime.agentContext.includeSystemPrompt": {
|
|
203
|
-
"label": "Include Agent System Prompt",
|
|
204
|
-
"advanced": true
|
|
205
|
-
},
|
|
206
202
|
"realtime.agentContext.includeWorkspaceFiles": {
|
|
207
203
|
"label": "Include Agent Workspace Files",
|
|
208
204
|
"advanced": true
|
|
@@ -617,9 +613,6 @@
|
|
|
617
613
|
"includeIdentity": {
|
|
618
614
|
"type": "boolean"
|
|
619
615
|
},
|
|
620
|
-
"includeSystemPrompt": {
|
|
621
|
-
"type": "boolean"
|
|
622
|
-
},
|
|
623
616
|
"includeWorkspaceFiles": {
|
|
624
617
|
"type": "boolean"
|
|
625
618
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/voice-call",
|
|
3
|
-
"version": "2026.5.
|
|
3
|
+
"version": "2026.5.28-beta.2",
|
|
4
4
|
"description": "OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"zod": "4.4.3"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
|
-
"openclaw": ">=2026.5.
|
|
17
|
+
"openclaw": ">=2026.5.28-beta.2"
|
|
18
18
|
},
|
|
19
19
|
"peerDependenciesMeta": {
|
|
20
20
|
"openclaw": {
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"minHostVersion": ">=2026.4.10"
|
|
32
32
|
},
|
|
33
33
|
"compat": {
|
|
34
|
-
"pluginApi": ">=2026.5.
|
|
34
|
+
"pluginApi": ">=2026.5.28-beta.2"
|
|
35
35
|
},
|
|
36
36
|
"build": {
|
|
37
|
-
"openclawVersion": "2026.5.
|
|
37
|
+
"openclawVersion": "2026.5.28-beta.2"
|
|
38
38
|
},
|
|
39
39
|
"release": {
|
|
40
40
|
"publishToClawHub": true,
|