@openclaw/voice-call 2026.10.2 → 2026.10.5-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/README.md +118 -3
- package/dist/.setup/{guarded-json-api-x55yqHvW.mjs → carrier-api-BYKw_fsQ.mjs} +94 -90
- package/dist/.setup/{cli-Cn1leykM.mjs → cli-dyyK8Upw.mjs} +66 -84
- package/dist/.setup/{config-CX9J-053.mjs → config-CsrGuamE.mjs} +95 -69
- package/dist/.setup/{mock-eLv_Dc4o.mjs → mock-rySvj-G6.mjs} +12 -3
- package/dist/.setup/{plivo-CpPIENu4.mjs → plivo-COfD-Kuc.mjs} +35 -89
- package/dist/.setup/{realtime-handler-Do7Sz7Gh.mjs → realtime-handler-BdAuKAC2.mjs} +749 -464
- package/dist/.setup/{response-generator-DAhbXEta.mjs → response-generator-COu0_8-d.mjs} +14 -32
- package/dist/.setup/{runtime-entry-CbSKxSKk.mjs → runtime-entry-STjFmi_B.mjs} +1053 -446
- package/dist/.setup/{telnyx-DPilutCH.mjs → telnyx-B4P0peEe.mjs} +30 -61
- package/dist/.setup/{twilio-BWFNGyVx.mjs → twilio-BzAYgH7M.mjs} +75 -90
- package/dist/.setup/{voice-mapping-B_iAUkER.mjs → voice-mapping-1lzFmzOF.mjs} +0 -10
- package/dist/cli-output-mode.js +1 -0
- package/dist/doctor-contract-api.js +19 -23
- package/dist/index.js +212 -89
- package/dist/runtime-entry.js +1 -1
- package/dist/setup-api.js +7 -6
- package/openclaw.plugin.json +210 -2
- package/package.json +4 -4
|
@@ -2,13 +2,30 @@ import { TtsConfigSchema } from "../api.js";
|
|
|
2
2
|
import { t as TWILIO_REGIONS } from "./twilio-region-wBOx-8sz.mjs";
|
|
3
3
|
import { normalizeAgentId, parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
|
|
4
4
|
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, REALTIME_VOICE_AGENT_CONSULT_TOOL_POLICIES, resolveRealtimeVoiceAgentConsultTools } from "openclaw/plugin-sdk/realtime-voice";
|
|
5
|
+
import { z } from "zod";
|
|
5
6
|
import { mergeDeep } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
6
7
|
import { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
7
8
|
import { canonicalizeMainSessionAlias } from "openclaw/plugin-sdk/session-store-runtime";
|
|
8
9
|
import { resolveSpeechProviderApiKey } from "openclaw/plugin-sdk/speech-core";
|
|
9
10
|
import { normalizeWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
|
|
10
|
-
import { z } from "zod";
|
|
11
11
|
import { MAX_TCP_PORT } from "openclaw/plugin-sdk/number-runtime";
|
|
12
|
+
//#region extensions/voice-call/src/call-brief-schema.ts
|
|
13
|
+
const text = (max) => z.string().trim().min(1).max(max);
|
|
14
|
+
const CallBriefSchema = z.object({
|
|
15
|
+
task: text(2e3).optional(),
|
|
16
|
+
context: text(4e3).optional(),
|
|
17
|
+
language: text(100).optional(),
|
|
18
|
+
identity: z.union([text(500), z.object({
|
|
19
|
+
introduction: text(500),
|
|
20
|
+
disclose: z.enum(["volunteer", "when-asked"]).optional()
|
|
21
|
+
}).strict()]).optional(),
|
|
22
|
+
disclosures: z.array(text(500)).max(20).optional(),
|
|
23
|
+
approvals: text(2e3).optional(),
|
|
24
|
+
voicemailMessage: text(1e3).optional(),
|
|
25
|
+
successCriteria: text(1e3).optional(),
|
|
26
|
+
maxDurationSeconds: z.number().int().positive().optional()
|
|
27
|
+
}).strict().refine((brief) => JSON.stringify(brief).length <= 8e3, "Call brief exceeds 8000 characters");
|
|
28
|
+
//#endregion
|
|
12
29
|
//#region extensions/voice-call/src/allowlist.ts
|
|
13
30
|
function normalizePhoneNumber(input) {
|
|
14
31
|
return input?.replace(/\D/g, "") ?? "";
|
|
@@ -19,6 +36,51 @@ function isAllowlistedCaller(normalizedFrom, allowFrom) {
|
|
|
19
36
|
return (allowFrom ?? []).some((num) => normalizePhoneNumber(num) === normalizedFrom);
|
|
20
37
|
}
|
|
21
38
|
//#endregion
|
|
39
|
+
//#region extensions/voice-call/src/errand-config.ts
|
|
40
|
+
const CallReportsConfigSchema = z.object({
|
|
41
|
+
enabled: z.boolean().default(false),
|
|
42
|
+
includeTranscript: z.boolean().default(true),
|
|
43
|
+
summaryModel: z.string().trim().min(1).optional(),
|
|
44
|
+
inboundSessionKey: z.string().trim().min(1).optional()
|
|
45
|
+
}).strict().default({
|
|
46
|
+
enabled: false,
|
|
47
|
+
includeTranscript: true
|
|
48
|
+
});
|
|
49
|
+
const CallLiveConfigSchema = z.object({
|
|
50
|
+
transcript: z.boolean().default(false),
|
|
51
|
+
minIntervalMs: z.number().int().positive().default(5e3)
|
|
52
|
+
}).strict().default({
|
|
53
|
+
transcript: false,
|
|
54
|
+
minIntervalMs: 5e3
|
|
55
|
+
});
|
|
56
|
+
const CallCallbacksConfigSchema = z.object({
|
|
57
|
+
enabled: z.boolean().default(false),
|
|
58
|
+
windowMinutes: z.number().int().positive().default(60),
|
|
59
|
+
greeting: z.string().trim().min(1).optional(),
|
|
60
|
+
brief: CallBriefSchema.optional()
|
|
61
|
+
}).strict().default({
|
|
62
|
+
enabled: false,
|
|
63
|
+
windowMinutes: 60
|
|
64
|
+
}).describe("Accept recent outbound recipients as callbacks only when realtime voice is enabled.");
|
|
65
|
+
const DEFAULT_VOICEMAIL_HOLD_OPENING_MAX_MS = 3e4;
|
|
66
|
+
const CallVoicemailConfigSchema = z.object({
|
|
67
|
+
detection: z.enum(["off", "twilio"]).default("off"),
|
|
68
|
+
onMachine: z.enum(["leave-message", "hang-up"]).default("leave-message"),
|
|
69
|
+
holdOpeningMaxMs: z.number().int().positive().default(DEFAULT_VOICEMAIL_HOLD_OPENING_MAX_MS),
|
|
70
|
+
machineDetectionSpeechThresholdMs: z.number().int().min(1e3).max(6e3).default(6e3),
|
|
71
|
+
machineDetectionSpeechEndThresholdMs: z.number().int().min(500).max(5e3).default(1200),
|
|
72
|
+
machineDetectionSilenceTimeoutMs: z.number().int().min(2e3).max(1e4).default(5e3),
|
|
73
|
+
machineDetectionTimeoutMs: z.number().int().min(3e3).max(59e3).multipleOf(1e3).default(3e4)
|
|
74
|
+
}).strict().default({
|
|
75
|
+
detection: "off",
|
|
76
|
+
onMachine: "leave-message",
|
|
77
|
+
holdOpeningMaxMs: DEFAULT_VOICEMAIL_HOLD_OPENING_MAX_MS,
|
|
78
|
+
machineDetectionSpeechThresholdMs: 6e3,
|
|
79
|
+
machineDetectionSpeechEndThresholdMs: 1200,
|
|
80
|
+
machineDetectionSilenceTimeoutMs: 5e3,
|
|
81
|
+
machineDetectionTimeoutMs: 3e4
|
|
82
|
+
});
|
|
83
|
+
//#endregion
|
|
22
84
|
//#region extensions/voice-call/src/realtime-call-control.ts
|
|
23
85
|
/** Stable provider-facing tool name for ending the current phone call. */
|
|
24
86
|
const REALTIME_VOICE_END_CALL_TOOL_NAME = "openclaw_end_call";
|
|
@@ -103,11 +165,7 @@ const VoiceCallServeConfigSchema = z.object({
|
|
|
103
165
|
port: z.number().int().positive().default(3334),
|
|
104
166
|
bind: z.string().default("127.0.0.1"),
|
|
105
167
|
path: z.string().min(1).default("/voice/webhook")
|
|
106
|
-
}).strict()
|
|
107
|
-
port: 3334,
|
|
108
|
-
bind: "127.0.0.1",
|
|
109
|
-
path: "/voice/webhook"
|
|
110
|
-
});
|
|
168
|
+
}).strict();
|
|
111
169
|
const VoiceCallTunnelConfigSchema = z.object({
|
|
112
170
|
provider: z.enum([
|
|
113
171
|
"none",
|
|
@@ -121,10 +179,7 @@ const VoiceCallTunnelConfigSchema = z.object({
|
|
|
121
179
|
ngrokDomain: z.string().min(1).optional(),
|
|
122
180
|
/** Trust loopback forwarding for ngrok URL reconstruction; signatures remain mandatory. */
|
|
123
181
|
allowNgrokFreeTierLoopbackBypass: z.boolean().default(false)
|
|
124
|
-
}).strict()
|
|
125
|
-
provider: "none",
|
|
126
|
-
allowNgrokFreeTierLoopbackBypass: false
|
|
127
|
-
});
|
|
182
|
+
}).strict();
|
|
128
183
|
const VoiceCallWebhookSecurityConfigSchema = z.object({
|
|
129
184
|
/** Only these hosts are accepted from forwarding headers. */
|
|
130
185
|
allowedHosts: z.array(z.string().min(1)).default([]),
|
|
@@ -138,11 +193,7 @@ const VoiceCallWebhookSecurityConfigSchema = z.object({
|
|
|
138
193
|
* the remote IP matches one of these addresses.
|
|
139
194
|
*/
|
|
140
195
|
trustedProxyIPs: z.array(z.string().min(1)).default([])
|
|
141
|
-
}).strict()
|
|
142
|
-
allowedHosts: [],
|
|
143
|
-
trustForwardingHeaders: false,
|
|
144
|
-
trustedProxyIPs: []
|
|
145
|
-
});
|
|
196
|
+
}).strict();
|
|
146
197
|
const CallModeSchema = z.enum(["notify", "conversation"]);
|
|
147
198
|
const VoiceCallSessionScopeSchema = z.enum([
|
|
148
199
|
"per-phone",
|
|
@@ -153,10 +204,7 @@ const OutboundConfigSchema = z.object({
|
|
|
153
204
|
defaultMode: CallModeSchema.default("notify"),
|
|
154
205
|
/** Seconds to wait after TTS before auto-hangup in notify mode */
|
|
155
206
|
notifyHangupDelaySec: z.number().int().nonnegative().default(3)
|
|
156
|
-
}).strict()
|
|
157
|
-
defaultMode: "notify",
|
|
158
|
-
notifyHangupDelaySec: 3
|
|
159
|
-
});
|
|
207
|
+
}).strict();
|
|
160
208
|
const RealtimeToolSchema = z.object({
|
|
161
209
|
type: z.literal("function"),
|
|
162
210
|
name: z.string().min(1),
|
|
@@ -186,13 +234,7 @@ const VoiceCallRealtimeFastContextConfigSchema = z.object({
|
|
|
186
234
|
sources: z.array(VoiceCallRealtimeFastContextSourceSchema).min(1).default(["memory", "sessions"]),
|
|
187
235
|
/** Fall back to the full agent consult when fast context has no answer. */
|
|
188
236
|
fallbackToConsult: z.boolean().default(false)
|
|
189
|
-
}).strict()
|
|
190
|
-
enabled: false,
|
|
191
|
-
timeoutMs: 800,
|
|
192
|
-
maxResults: 3,
|
|
193
|
-
sources: ["memory", "sessions"],
|
|
194
|
-
fallbackToConsult: false
|
|
195
|
-
});
|
|
237
|
+
}).strict();
|
|
196
238
|
const VoiceCallRealtimeAgentContextConfigSchema = z.object({
|
|
197
239
|
/** Include configured identity and selected profile files alongside the always-on agent context. */
|
|
198
240
|
enabled: z.boolean().default(false),
|
|
@@ -207,17 +249,7 @@ const VoiceCallRealtimeAgentContextConfigSchema = z.object({
|
|
|
207
249
|
"IDENTITY.md",
|
|
208
250
|
"USER.md"
|
|
209
251
|
])
|
|
210
|
-
}).strict()
|
|
211
|
-
enabled: false,
|
|
212
|
-
maxChars: 6e3,
|
|
213
|
-
includeIdentity: true,
|
|
214
|
-
includeWorkspaceFiles: true,
|
|
215
|
-
files: [
|
|
216
|
-
"SOUL.md",
|
|
217
|
-
"IDENTITY.md",
|
|
218
|
-
"USER.md"
|
|
219
|
-
]
|
|
220
|
-
});
|
|
252
|
+
}).strict();
|
|
221
253
|
const VoiceCallRealtimeConsultThinkingLevelSchema = z.enum([
|
|
222
254
|
"off",
|
|
223
255
|
"minimal",
|
|
@@ -235,6 +267,8 @@ const VoiceCallRealtimeConfigSchema = z.object({
|
|
|
235
267
|
provider: z.string().min(1).optional(),
|
|
236
268
|
/** Optional override for the local WebSocket route path. */
|
|
237
269
|
streamPath: z.string().min(1).optional(),
|
|
270
|
+
/** End an active realtime call after this much speech inactivity. */
|
|
271
|
+
idleHangupMs: z.number().int().positive().optional(),
|
|
238
272
|
/** System instructions passed to the realtime provider. */
|
|
239
273
|
instructions: z.string().default(DEFAULT_VOICE_CALL_REALTIME_INSTRUCTIONS),
|
|
240
274
|
/** Tool policy for the shared OpenClaw agent consult tool. */
|
|
@@ -247,9 +281,9 @@ const VoiceCallRealtimeConfigSchema = z.object({
|
|
|
247
281
|
consultFastMode: z.boolean().optional(),
|
|
248
282
|
tools: z.array(RealtimeToolSchema).default([]),
|
|
249
283
|
/** Low-latency memory/session context for the consult tool. */
|
|
250
|
-
fastContext: VoiceCallRealtimeFastContextConfigSchema,
|
|
284
|
+
fastContext: VoiceCallRealtimeFastContextConfigSchema.default(VoiceCallRealtimeFastContextConfigSchema.parse({})),
|
|
251
285
|
/** Bounded agent persona/context injection for the fast realtime voice path. */
|
|
252
|
-
agentContext: VoiceCallRealtimeAgentContextConfigSchema,
|
|
286
|
+
agentContext: VoiceCallRealtimeAgentContextConfigSchema.default(VoiceCallRealtimeAgentContextConfigSchema.parse({})),
|
|
253
287
|
/** Provider-owned raw config blobs keyed by provider id. */
|
|
254
288
|
providers: VoiceCallProvidersConfigSchema
|
|
255
289
|
}).strict().default({
|
|
@@ -281,15 +315,7 @@ const VoiceCallStreamingConfigSchema = z.object({
|
|
|
281
315
|
maxPendingConnectionsPerIp: z.number().int().positive().default(4),
|
|
282
316
|
/** Hard cap for all open media stream sockets (pending + active). */
|
|
283
317
|
maxConnections: z.number().int().positive().default(128)
|
|
284
|
-
}).strict()
|
|
285
|
-
enabled: false,
|
|
286
|
-
streamPath: "/voice/stream",
|
|
287
|
-
providers: {},
|
|
288
|
-
preStartTimeoutMs: 5e3,
|
|
289
|
-
maxPendingConnections: 32,
|
|
290
|
-
maxPendingConnectionsPerIp: 4,
|
|
291
|
-
maxConnections: 128
|
|
292
|
-
});
|
|
318
|
+
}).strict();
|
|
293
319
|
const VoiceCallConfigSchema = z.object({
|
|
294
320
|
enabled: z.boolean().default(false),
|
|
295
321
|
provider: z.enum([
|
|
@@ -309,7 +335,12 @@ const VoiceCallConfigSchema = z.object({
|
|
|
309
335
|
inboundGreeting: z.string().optional(),
|
|
310
336
|
/** Per-dialed-number overrides for inbound calls. Keys are E.164 numbers. */
|
|
311
337
|
numbers: z.record(E164Schema, VoiceCallNumberRouteConfigSchema).default({}),
|
|
312
|
-
outbound: OutboundConfigSchema,
|
|
338
|
+
outbound: OutboundConfigSchema.default(OutboundConfigSchema.parse({})),
|
|
339
|
+
reports: CallReportsConfigSchema,
|
|
340
|
+
live: CallLiveConfigSchema,
|
|
341
|
+
callbacks: CallCallbacksConfigSchema,
|
|
342
|
+
voicemail: CallVoicemailConfigSchema,
|
|
343
|
+
/** Maximum call duration in seconds */
|
|
313
344
|
maxDurationSeconds: z.number().int().positive().default(300),
|
|
314
345
|
/**
|
|
315
346
|
* Maximum age of a call in seconds before it is automatically reaped.
|
|
@@ -322,12 +353,12 @@ const VoiceCallConfigSchema = z.object({
|
|
|
322
353
|
transcriptTimeoutMs: z.number().int().positive().default(18e4),
|
|
323
354
|
ringTimeoutMs: z.number().int().positive().default(3e4),
|
|
324
355
|
maxConcurrentCalls: z.number().int().positive().default(1),
|
|
325
|
-
serve: VoiceCallServeConfigSchema,
|
|
356
|
+
serve: VoiceCallServeConfigSchema.default(VoiceCallServeConfigSchema.parse({})),
|
|
326
357
|
/** @deprecated Prefer tunnel config. */
|
|
327
358
|
tailscale: VoiceCallTailscaleConfigSchema,
|
|
328
|
-
tunnel: VoiceCallTunnelConfigSchema,
|
|
329
|
-
webhookSecurity: VoiceCallWebhookSecurityConfigSchema,
|
|
330
|
-
streaming: VoiceCallStreamingConfigSchema,
|
|
359
|
+
tunnel: VoiceCallTunnelConfigSchema.default(VoiceCallTunnelConfigSchema.parse({})),
|
|
360
|
+
webhookSecurity: VoiceCallWebhookSecurityConfigSchema.default(VoiceCallWebhookSecurityConfigSchema.parse({})),
|
|
361
|
+
streaming: VoiceCallStreamingConfigSchema.default(VoiceCallStreamingConfigSchema.parse({})),
|
|
331
362
|
realtime: VoiceCallRealtimeConfigSchema,
|
|
332
363
|
/** Session memory scope for voice conversations. */
|
|
333
364
|
sessionScope: VoiceCallSessionScopeSchema.default("per-phone"),
|
|
@@ -386,7 +417,6 @@ function normalizeVoiceCallTtsConfig(defaults, overrides) {
|
|
|
386
417
|
}
|
|
387
418
|
function resolveVoiceCallNumberRouteKey(config, phone) {
|
|
388
419
|
const routes = config.numbers;
|
|
389
|
-
if (!routes) return;
|
|
390
420
|
if (phone && Object.hasOwn(routes, phone)) return phone;
|
|
391
421
|
const normalizedPhone = normalizePhoneNumber(phone);
|
|
392
422
|
if (!normalizedPhone) return;
|
|
@@ -409,18 +439,15 @@ function resolveVoiceCallEffectiveConfig(config, phoneOrRouteKey) {
|
|
|
409
439
|
config: {
|
|
410
440
|
...config,
|
|
411
441
|
...route,
|
|
412
|
-
tts: normalizeVoiceCallTtsConfig(config.tts, route.tts)
|
|
413
|
-
numbers: config.numbers
|
|
442
|
+
tts: normalizeVoiceCallTtsConfig(config.tts, route.tts)
|
|
414
443
|
}
|
|
415
444
|
};
|
|
416
445
|
}
|
|
417
446
|
function sanitizeVoiceCallProviderConfigs(value) {
|
|
418
|
-
|
|
419
|
-
return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== void 0));
|
|
447
|
+
return Object.fromEntries(Object.entries(value ?? {}).filter((entry) => entry[1] !== void 0));
|
|
420
448
|
}
|
|
421
449
|
function sanitizeVoiceCallNumberRoutes(value) {
|
|
422
|
-
|
|
423
|
-
return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== void 0).map(([key, route]) => [key, VoiceCallNumberRouteConfigSchema.parse(route)]));
|
|
450
|
+
return Object.fromEntries(Object.entries(value ?? {}).filter(([, route]) => route !== void 0).map(([key, route]) => [key, VoiceCallNumberRouteConfigSchema.parse(route)]));
|
|
424
451
|
}
|
|
425
452
|
function resolveTwilioAuthToken(config) {
|
|
426
453
|
return normalizeResolvedSecretInputString({
|
|
@@ -457,6 +484,10 @@ function normalizeVoiceCallConfig(config) {
|
|
|
457
484
|
...defaults.outbound,
|
|
458
485
|
...config.outbound
|
|
459
486
|
},
|
|
487
|
+
reports: CallReportsConfigSchema.parse(config.reports),
|
|
488
|
+
live: CallLiveConfigSchema.parse(config.live),
|
|
489
|
+
callbacks: CallCallbacksConfigSchema.parse(config.callbacks),
|
|
490
|
+
voicemail: CallVoicemailConfigSchema.parse(config.voicemail),
|
|
460
491
|
serve,
|
|
461
492
|
tailscale: {
|
|
462
493
|
...defaults.tailscale,
|
|
@@ -495,14 +526,9 @@ function normalizeVoiceCallConfig(config) {
|
|
|
495
526
|
}
|
|
496
527
|
function resolveVoiceCallSessionKey(params) {
|
|
497
528
|
const explicit = params.explicitSessionKey?.trim();
|
|
498
|
-
if (explicit) return resolveVoiceCallAgentSessionKey({
|
|
499
|
-
config: params.config,
|
|
500
|
-
sessionKey: explicit,
|
|
501
|
-
coreSession: params.coreSession
|
|
502
|
-
});
|
|
503
|
-
if (params.config.sessionScope === "main") return resolveVoiceCallAgentSessionKey({
|
|
529
|
+
if (explicit || params.config.sessionScope === "main") return resolveVoiceCallAgentSessionKey({
|
|
504
530
|
config: params.config,
|
|
505
|
-
sessionKey: "main",
|
|
531
|
+
sessionKey: explicit || "main",
|
|
506
532
|
coreSession: params.coreSession
|
|
507
533
|
});
|
|
508
534
|
const prefix = `agent:${normalizeAgentId(params.config.agentId)}:voice`;
|
|
@@ -554,7 +580,7 @@ function resolveVoiceCallConfig(config) {
|
|
|
554
580
|
resolved.tunnel.ngrokAuthToken = resolved.tunnel.ngrokAuthToken ?? resolveSpeechProviderApiKey(process.env.NGROK_AUTHTOKEN);
|
|
555
581
|
resolved.tunnel.ngrokDomain = resolved.tunnel.ngrokDomain ?? resolveSpeechProviderApiKey(process.env.NGROK_DOMAIN);
|
|
556
582
|
resolved.webhookSecurity.trustForwardingHeaders = resolved.webhookSecurity.trustForwardingHeaders ?? false;
|
|
557
|
-
return
|
|
583
|
+
return resolved;
|
|
558
584
|
}
|
|
559
585
|
function validateProviderConfig(config) {
|
|
560
586
|
const errors = [];
|
|
@@ -590,4 +616,4 @@ function validateProviderConfig(config) {
|
|
|
590
616
|
};
|
|
591
617
|
}
|
|
592
618
|
//#endregion
|
|
593
|
-
export { resolveVoiceCallEffectiveConfig as a, resolveVoiceCallSessionKey as c, REALTIME_VOICE_END_CALL_TOOL_NAME as d, resolveVoiceCallRealtimeTools as f, resolveVoiceCallConfig as i, resolveVoiceCallStreamExposurePaths as l,
|
|
619
|
+
export { resolveVoiceCallEffectiveConfig as a, resolveVoiceCallSessionKey as c, REALTIME_VOICE_END_CALL_TOOL_NAME as d, resolveVoiceCallRealtimeTools as f, CallBriefSchema as g, normalizePhoneNumber as h, resolveVoiceCallConfig as i, resolveVoiceCallStreamExposurePaths as l, isAllowlistedCaller as m, normalizeVoiceCallConfig as n, resolveVoiceCallNumberRouteKeyForCall as o, DEFAULT_VOICEMAIL_HOLD_OPENING_MAX_MS as p, resolveTwilioAuthToken as r, resolveVoiceCallPublicPathPrefix as s, VoiceCallConfigSchema as t, validateProviderConfig as u };
|
|
@@ -43,12 +43,15 @@ var MockProvider = class {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
normalizeEvent(evt) {
|
|
46
|
-
if (!evt.type || !evt.callId) return null;
|
|
46
|
+
if (!evt.type || typeof evt.callId !== "string" || !evt.callId) return null;
|
|
47
47
|
const base = {
|
|
48
48
|
id: evt.id ?? crypto.randomUUID(),
|
|
49
49
|
callId: evt.callId,
|
|
50
|
-
providerCallId: evt.providerCallId,
|
|
51
|
-
timestamp: evt.timestamp ?? Date.now()
|
|
50
|
+
providerCallId: typeof evt.providerCallId === "string" ? evt.providerCallId : void 0,
|
|
51
|
+
timestamp: evt.timestamp ?? Date.now(),
|
|
52
|
+
direction: evt.direction === "inbound" || evt.direction === "outbound" ? evt.direction : void 0,
|
|
53
|
+
from: typeof evt.from === "string" ? evt.from : void 0,
|
|
54
|
+
to: typeof evt.to === "string" ? evt.to : void 0
|
|
52
55
|
};
|
|
53
56
|
switch (evt.type) {
|
|
54
57
|
case "call.initiated":
|
|
@@ -58,6 +61,11 @@ var MockProvider = class {
|
|
|
58
61
|
...base,
|
|
59
62
|
type: evt.type
|
|
60
63
|
};
|
|
64
|
+
case "call.amd": return evt.answeredBy ? {
|
|
65
|
+
...base,
|
|
66
|
+
type: evt.type,
|
|
67
|
+
answeredBy: evt.answeredBy
|
|
68
|
+
} : null;
|
|
61
69
|
case "call.speaking": return {
|
|
62
70
|
...base,
|
|
63
71
|
type: evt.type,
|
|
@@ -111,6 +119,7 @@ var MockProvider = class {
|
|
|
111
119
|
}
|
|
112
120
|
async hangupCall(_input) {}
|
|
113
121
|
async playTts(_input) {}
|
|
122
|
+
async playMessageAndHangup(_input) {}
|
|
114
123
|
async sendDtmf(_input) {}
|
|
115
124
|
async startListening(_input) {}
|
|
116
125
|
async stopListening(_input) {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as escapeXml } from "./voice-mapping-
|
|
3
|
-
import {
|
|
1
|
+
import { c as getHeader } from "./runtime-entry-STjFmi_B.mjs";
|
|
2
|
+
import { t as escapeXml } from "./voice-mapping-1lzFmzOF.mjs";
|
|
3
|
+
import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as createCarrierApi } from "./carrier-api-BYKw_fsQ.mjs";
|
|
4
4
|
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
5
5
|
import crypto from "node:crypto";
|
|
6
6
|
//#region extensions/voice-call/src/providers/plivo.ts
|
|
@@ -40,27 +40,9 @@ var PlivoProvider = class PlivoProvider {
|
|
|
40
40
|
this.pendingListenByCallId = /* @__PURE__ */ new Map();
|
|
41
41
|
if (!config.authId) throw new Error("Plivo Auth ID is required");
|
|
42
42
|
if (!config.authToken) throw new Error("Plivo Auth Token is required");
|
|
43
|
-
this.authId = config.authId;
|
|
44
43
|
this.authToken = config.authToken;
|
|
45
|
-
this.baseUrl = `https://api.plivo.com/v1/Account/${this.authId}`;
|
|
46
|
-
this.apiHost = new URL(this.baseUrl).hostname;
|
|
47
44
|
this.options = options;
|
|
48
|
-
|
|
49
|
-
async apiRequest(params) {
|
|
50
|
-
const { method, endpoint, body, allowNotFound } = params;
|
|
51
|
-
return await guardedJsonApiRequest({
|
|
52
|
-
url: `${this.baseUrl}${endpoint}`,
|
|
53
|
-
method,
|
|
54
|
-
headers: {
|
|
55
|
-
Authorization: `Basic ${Buffer.from(`${this.authId}:${this.authToken}`).toString("base64")}`,
|
|
56
|
-
"Content-Type": "application/json"
|
|
57
|
-
},
|
|
58
|
-
body,
|
|
59
|
-
allowNotFound,
|
|
60
|
-
allowedHostnames: [this.apiHost],
|
|
61
|
-
auditContext: "voice-call.plivo.api",
|
|
62
|
-
errorPrefix: "Plivo API error"
|
|
63
|
-
});
|
|
45
|
+
this.api = createCarrierApi("Plivo", `https://api.plivo.com/v1/Account/${config.authId}`, `Basic ${Buffer.from(`${config.authId}:${config.authToken}`).toString("base64")}`);
|
|
64
46
|
}
|
|
65
47
|
verifyWebhook(ctx) {
|
|
66
48
|
const result = verifyPlivoWebhook(ctx, this.authToken, {
|
|
@@ -91,27 +73,22 @@ var PlivoProvider = class PlivoProvider {
|
|
|
91
73
|
const callId = normalizeOptionalString(ctx.query?.callId);
|
|
92
74
|
let event = null;
|
|
93
75
|
let providerResponseBody;
|
|
94
|
-
if (flow === "xml-speak") {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
actionUrl
|
|
102
|
-
}) : PlivoProvider.xmlSpeak(pending.text, pending.locale) : PlivoProvider.xmlKeepAlive();
|
|
103
|
-
} else if (flow === "xml-listen") {
|
|
104
|
-
const pending = callId ? this.pendingListenByCallId.get(callId) : void 0;
|
|
105
|
-
if (callId) this.pendingListenByCallId.delete(callId);
|
|
106
|
-
const actionUrl = this.buildActionUrl(ctx, callId);
|
|
76
|
+
if (flow === "xml-speak" || flow === "xml-listen") {
|
|
77
|
+
const speaks = flow === "xml-speak";
|
|
78
|
+
const pendingSpeak = speaks && callId ? this.pendingSpeakByCallId.get(callId) : void 0;
|
|
79
|
+
const pendingListen = !speaks && callId ? this.pendingListenByCallId.get(callId) : void 0;
|
|
80
|
+
const language = speaks ? pendingSpeak?.locale : pendingListen?.language;
|
|
81
|
+
if (callId) (speaks ? this.pendingSpeakByCallId : this.pendingListenByCallId).delete(callId);
|
|
82
|
+
const actionUrl = !speaks || pendingSpeak?.listenAfterPlayback && callId ? this.buildActionUrl(ctx, callId) : null;
|
|
107
83
|
providerResponseBody = actionUrl && callId ? PlivoProvider.xmlGetInputSpeech({
|
|
108
|
-
|
|
109
|
-
language
|
|
110
|
-
|
|
84
|
+
text: pendingSpeak?.text,
|
|
85
|
+
language,
|
|
86
|
+
actionUrl
|
|
87
|
+
}) : pendingSpeak ? PlivoProvider.xmlKeepAlive(` <Speak language="${escapeXml(language || "en-US")}">${escapeXml(pendingSpeak.text)}</Speak>\n`) : PlivoProvider.xmlKeepAlive();
|
|
111
88
|
} else {
|
|
112
89
|
const dedupeKey = options?.verifiedRequestKey ?? createPlivoRequestDedupeKey(ctx);
|
|
113
90
|
event = this.normalizeEvent(parsed, callId, dedupeKey);
|
|
114
|
-
providerResponseBody = flow === "answer" || flow === "getinput" ? PlivoProvider.xmlKeepAlive() :
|
|
91
|
+
providerResponseBody = flow === "answer" || flow === "getinput" ? PlivoProvider.xmlKeepAlive() : "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response></Response>";
|
|
115
92
|
}
|
|
116
93
|
return {
|
|
117
94
|
events: event ? [event] : [],
|
|
@@ -185,18 +162,14 @@ var PlivoProvider = class PlivoProvider {
|
|
|
185
162
|
const hangupUrl = new URL(webhookUrl);
|
|
186
163
|
hangupUrl.searchParams.set("flow", "hangup");
|
|
187
164
|
this.callIdToWebhookUrl.set(input.callId, input.webhookUrl);
|
|
188
|
-
const result = await this.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
hangup_url: hangupUrl.toString(),
|
|
197
|
-
hangup_method: "POST",
|
|
198
|
-
hangup_on_ring: this.options.ringTimeoutSec ?? 30
|
|
199
|
-
}
|
|
165
|
+
const result = await this.api.request("/Call/", {
|
|
166
|
+
from: PlivoProvider.normalizeNumber(input.from),
|
|
167
|
+
to: PlivoProvider.normalizeNumber(input.to),
|
|
168
|
+
answer_url: answerUrl.toString(),
|
|
169
|
+
answer_method: "POST",
|
|
170
|
+
hangup_url: hangupUrl.toString(),
|
|
171
|
+
hangup_method: "POST",
|
|
172
|
+
hangup_on_ring: this.options.ringTimeoutSec ?? 30
|
|
200
173
|
});
|
|
201
174
|
const requestUuid = Array.isArray(result.request_uuid) ? result.request_uuid[0] : result.request_uuid;
|
|
202
175
|
if (!requestUuid) throw new Error("Plivo call create returned no request_uuid");
|
|
@@ -207,14 +180,12 @@ var PlivoProvider = class PlivoProvider {
|
|
|
207
180
|
}
|
|
208
181
|
async hangupCall(input) {
|
|
209
182
|
const callUuid = this.requestUuidToCallUuid.get(input.providerCallId);
|
|
210
|
-
await this.
|
|
183
|
+
await this.api.request(`/Call/${callUuid || input.providerCallId}/`, void 0, {
|
|
211
184
|
method: "DELETE",
|
|
212
|
-
endpoint: `/Call/${callUuid || input.providerCallId}/`,
|
|
213
185
|
allowNotFound: true
|
|
214
186
|
});
|
|
215
|
-
if (!callUuid) await this.
|
|
187
|
+
if (!callUuid) await this.api.request(`/Request/${input.providerCallId}/`, void 0, {
|
|
216
188
|
method: "DELETE",
|
|
217
|
-
endpoint: `/Request/${input.providerCallId}/`,
|
|
218
189
|
allowNotFound: true
|
|
219
190
|
});
|
|
220
191
|
this.releaseCallState({
|
|
@@ -239,14 +210,10 @@ var PlivoProvider = class PlivoProvider {
|
|
|
239
210
|
transferUrl.searchParams.set("provider", "plivo");
|
|
240
211
|
transferUrl.searchParams.set("flow", params.flow);
|
|
241
212
|
transferUrl.searchParams.set("callId", params.callId);
|
|
242
|
-
await this.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
legs: "aleg",
|
|
247
|
-
aleg_url: transferUrl.toString(),
|
|
248
|
-
aleg_method: "POST"
|
|
249
|
-
}
|
|
213
|
+
await this.api.request(`/Call/${params.callUuid}/`, {
|
|
214
|
+
legs: "aleg",
|
|
215
|
+
aleg_url: transferUrl.toString(),
|
|
216
|
+
aleg_method: "POST"
|
|
250
217
|
});
|
|
251
218
|
}
|
|
252
219
|
async playTts(input) {
|
|
@@ -281,15 +248,7 @@ var PlivoProvider = class PlivoProvider {
|
|
|
281
248
|
"machine",
|
|
282
249
|
"hangup"
|
|
283
250
|
]);
|
|
284
|
-
return
|
|
285
|
-
url: `${this.baseUrl}/Call/${input.providerCallId}/`,
|
|
286
|
-
method: "GET",
|
|
287
|
-
headers: { Authorization: `Basic ${Buffer.from(`${this.authId}:${this.authToken}`).toString("base64")}` },
|
|
288
|
-
allowNotFound: true,
|
|
289
|
-
allowedHostnames: [this.apiHost],
|
|
290
|
-
auditContext: "plivo-get-call-status",
|
|
291
|
-
errorPrefix: "Plivo get call status error"
|
|
292
|
-
}), (data) => {
|
|
251
|
+
return this.api.getCallStatus(`/Call/${input.providerCallId}/`, (data) => {
|
|
293
252
|
const status = data.call_status ?? "unknown";
|
|
294
253
|
return {
|
|
295
254
|
status,
|
|
@@ -302,31 +261,18 @@ var PlivoProvider = class PlivoProvider {
|
|
|
302
261
|
if (normalizeLowercaseStringOrEmpty(trimmed).startsWith("sip:")) return trimmed;
|
|
303
262
|
return trimmed.replace(/[^\d+]/g, "");
|
|
304
263
|
}
|
|
305
|
-
static
|
|
306
|
-
return `<?xml version="1.0" encoding="UTF-8"?><Response></Response>`;
|
|
307
|
-
}
|
|
308
|
-
static xmlKeepAlive() {
|
|
309
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
310
|
-
<Response>
|
|
311
|
-
<Wait length="300" />
|
|
312
|
-
</Response>`;
|
|
313
|
-
}
|
|
314
|
-
static xmlSpeak(text, locale) {
|
|
264
|
+
static xmlKeepAlive(content = "") {
|
|
315
265
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
316
266
|
<Response>
|
|
317
|
-
<
|
|
318
|
-
<Wait length="300" />
|
|
267
|
+
${content} <Wait length="300" />
|
|
319
268
|
</Response>`;
|
|
320
269
|
}
|
|
321
270
|
static xmlGetInputSpeech(params) {
|
|
322
271
|
const language = params.language || "en-US";
|
|
323
272
|
const prompt = params.text === void 0 ? "" : ` <Speak language="${escapeXml(language)}">${escapeXml(params.text)}</Speak>\n`;
|
|
324
|
-
return
|
|
325
|
-
<Response>
|
|
326
|
-
<GetInput inputType="speech" method="POST" action="${escapeXml(params.actionUrl)}" language="${escapeXml(language)}" executionTimeout="30" speechEndTimeout="2" redirect="false">
|
|
273
|
+
return PlivoProvider.xmlKeepAlive(` <GetInput inputType="speech" method="POST" action="${escapeXml(params.actionUrl)}" language="${escapeXml(language)}" executionTimeout="30" speechEndTimeout="2" redirect="false">
|
|
327
274
|
${prompt} </GetInput>
|
|
328
|
-
|
|
329
|
-
</Response>`;
|
|
275
|
+
`);
|
|
330
276
|
}
|
|
331
277
|
buildActionUrl(ctx, callId) {
|
|
332
278
|
const base = this.baseWebhookUrlFromCtx(ctx);
|