@openclaw/voice-call 2026.8.1-beta.2 → 2026.8.1-beta.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/README.md +4 -3
- package/dist/doctor-contract-api.js +1 -0
- package/dist/{guarded-json-api-DEDwgf4J.js → guarded-json-api-j4OEG0fR.js} +1 -1
- package/dist/index.js +390 -338
- package/dist/{mock-BCq0q36l.js → mock-Dv7KxYGt.js} +3 -1
- package/dist/{plivo-DbI17uAc.js → plivo-BbVnahnQ.js} +2 -2
- package/dist/{realtime-handler-uXR0QgVa.js → realtime-handler-BaMEKbmJ.js} +239 -125
- package/dist/{response-generator-D-hkzo5Y.js → response-generator-tjyX4tDq.js} +59 -12
- package/dist/{runtime-entry-F13eJ4pp.js → runtime-entry-DK8kPCGC.js} +422 -197
- package/dist/runtime-entry.js +1 -1
- package/dist/{telnyx-sIS_WgnY.js → telnyx-DO8dFQcb.js} +12 -8
- package/dist/{twilio-CbeVrVAl.js → twilio-JDt_1nlz.js} +40 -27
- package/openclaw.plugin.json +20 -2
- package/package.json +4 -4
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-DK8kPCGC.js";
|
|
2
2
|
export { createVoiceCallRuntime };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { s as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
1
|
+
import { s as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-j4OEG0fR.js";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
//#region extensions/voice-call/src/providers/telnyx.ts
|
|
4
4
|
function normalizeTelnyxDirection(direction) {
|
|
@@ -122,13 +122,17 @@ var TelnyxProvider = class {
|
|
|
122
122
|
type: "call.speaking",
|
|
123
123
|
text: data.payload?.text || ""
|
|
124
124
|
};
|
|
125
|
-
case "call.transcription":
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
125
|
+
case "call.transcription": {
|
|
126
|
+
const transcript = data.payload?.transcription_data?.transcript ?? data.payload?.transcription ?? "";
|
|
127
|
+
if (!transcript.trim()) return null;
|
|
128
|
+
return {
|
|
129
|
+
...baseEvent,
|
|
130
|
+
type: "call.speech",
|
|
131
|
+
transcript,
|
|
132
|
+
isFinal: data.payload?.transcription_data?.is_final ?? data.payload?.is_final ?? true,
|
|
133
|
+
confidence: data.payload?.transcription_data?.confidence ?? data.payload?.confidence
|
|
134
|
+
};
|
|
135
|
+
}
|
|
132
136
|
case "call.hangup": return {
|
|
133
137
|
...baseEvent,
|
|
134
138
|
type: "call.ended",
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
3
|
import { n as requireSupportedTwilioApiHostname, r as resolveTwilioApiBaseUrl } from "./twilio-region-4fkgz3UG.js";
|
|
4
|
-
import { _ as escapeXml, a as normalizeProviderStatus, i as mapProviderStatusToEndReason, p as chunkAudio, r as isProviderStatusTerminal, s as getHeader, v as mapVoiceToPolly } from "./runtime-entry-
|
|
5
|
-
import { c as verifyTwilioWebhook, i as readVoiceCallProviderJsonResponse, n as cancelProviderResponseBody, r as readProviderErrorResponseSnippet, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
4
|
+
import { _ as escapeXml, a as normalizeProviderStatus, i as mapProviderStatusToEndReason, p as chunkAudio, r as isProviderStatusTerminal, s as getHeader, v as mapVoiceToPolly } from "./runtime-entry-DK8kPCGC.js";
|
|
5
|
+
import { c as verifyTwilioWebhook, i as readVoiceCallProviderJsonResponse, n as cancelProviderResponseBody, r as readProviderErrorResponseSnippet, t as guardedJsonApiRequest } from "./guarded-json-api-j4OEG0fR.js";
|
|
6
6
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
7
7
|
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
|
|
8
8
|
import crypto from "node:crypto";
|
|
9
9
|
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
10
|
-
import {
|
|
10
|
+
import { retryAsync } from "openclaw/plugin-sdk/retry-runtime";
|
|
11
11
|
//#region extensions/voice-call/src/providers/twilio/api.ts
|
|
12
12
|
const TWILIO_API_TIMEOUT_MS = 3e4;
|
|
13
13
|
/** Parse Twilio JSON error responses without trusting response shape. */
|
|
@@ -188,6 +188,7 @@ var TwilioProvider = class TwilioProvider {
|
|
|
188
188
|
this.currentPublicUrl = null;
|
|
189
189
|
this.ttsProvider = null;
|
|
190
190
|
this.mediaStreamHandler = null;
|
|
191
|
+
this.playbackMarkSequence = 0;
|
|
191
192
|
this.callStreamMap = /* @__PURE__ */ new Map();
|
|
192
193
|
this.streamAuthTokens = /* @__PURE__ */ new Map();
|
|
193
194
|
this.twimlStorage = /* @__PURE__ */ new Map();
|
|
@@ -261,15 +262,16 @@ var TwilioProvider = class TwilioProvider {
|
|
|
261
262
|
});
|
|
262
263
|
}
|
|
263
264
|
async updateLiveCallTwiml(providerCallId, twiml, operation) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
265
|
+
await retryAsync(() => this.apiRequest(`/Calls/${providerCallId}.json`, { Twiml: twiml }), {
|
|
266
|
+
attempts: TWILIO_CALL_UPDATE_RETRY_DELAYS_MS.length + 1,
|
|
267
|
+
minDelayMs: 0,
|
|
268
|
+
shouldRetry: isTwilioCallNotInProgressError,
|
|
269
|
+
delayMs: ({ attempt }) => TWILIO_CALL_UPDATE_RETRY_DELAYS_MS[attempt - 1] ?? 0,
|
|
270
|
+
onRetry: ({ delayMs }) => {
|
|
271
|
+
console.warn(`[voice-call] Twilio ${operation} update hit call state race (21220); retrying in ${delayMs}ms`);
|
|
272
|
+
},
|
|
273
|
+
sleep: (delayMs) => sleepWithAbort(delayMs)
|
|
274
|
+
});
|
|
273
275
|
}
|
|
274
276
|
/**
|
|
275
277
|
* Verify Twilio webhook signature using HMAC-SHA1.
|
|
@@ -301,6 +303,7 @@ var TwilioProvider = class TwilioProvider {
|
|
|
301
303
|
dedupeKey,
|
|
302
304
|
turnToken: turnTokenFromQuery
|
|
303
305
|
});
|
|
306
|
+
if (event?.direction === "inbound" && event.type !== "call.ended" && event.providerCallId && this.currentPublicUrl && !this.callWebhookUrls.has(event.providerCallId)) this.callWebhookUrls.set(event.providerCallId, this.currentPublicUrl);
|
|
304
307
|
const twiml = this.generateTwimlResponse(ctx);
|
|
305
308
|
return {
|
|
306
309
|
events: event ? [event] : [],
|
|
@@ -345,7 +348,7 @@ var TwilioProvider = class TwilioProvider {
|
|
|
345
348
|
to: params.get("To") || void 0
|
|
346
349
|
};
|
|
347
350
|
const speechResult = params.get("SpeechResult");
|
|
348
|
-
if (speechResult) return {
|
|
351
|
+
if (speechResult?.trim()) return {
|
|
349
352
|
...baseEvent,
|
|
350
353
|
type: "call.speech",
|
|
351
354
|
transcript: speechResult,
|
|
@@ -587,10 +590,6 @@ var TwilioProvider = class TwilioProvider {
|
|
|
587
590
|
const raw = handler.sendAudio(streamSid, audio);
|
|
588
591
|
return normalizeSendResult(raw);
|
|
589
592
|
};
|
|
590
|
-
const sendPlaybackMark = (name) => {
|
|
591
|
-
const raw = handler.sendMark(streamSid, name);
|
|
592
|
-
return normalizeSendResult(raw);
|
|
593
|
-
};
|
|
594
593
|
await handler.queueTts(streamSid, async (signal) => {
|
|
595
594
|
const sendKeepAlive = () => {
|
|
596
595
|
sendAudioChunk(SILENCE_CHUNK);
|
|
@@ -601,6 +600,7 @@ var TwilioProvider = class TwilioProvider {
|
|
|
601
600
|
}, CHUNK_DELAY_MS);
|
|
602
601
|
let muLawAudio;
|
|
603
602
|
let synthTimeout = null;
|
|
603
|
+
let removeAbortListener = () => {};
|
|
604
604
|
const synthTimeoutMs = ttsProvider.synthesisTimeoutMs;
|
|
605
605
|
try {
|
|
606
606
|
const synthPromise = ttsProvider.synthesizeForTelephony(text);
|
|
@@ -609,10 +609,23 @@ var TwilioProvider = class TwilioProvider {
|
|
|
609
609
|
reject(/* @__PURE__ */ new Error(`Telephony TTS synthesis timed out after ${synthTimeoutMs}ms`));
|
|
610
610
|
}, synthTimeoutMs);
|
|
611
611
|
});
|
|
612
|
-
|
|
612
|
+
const abortPromise = new Promise((_, reject) => {
|
|
613
|
+
const onAbort = () => {
|
|
614
|
+
reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("Telephony TTS synthesis aborted"));
|
|
615
|
+
};
|
|
616
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
617
|
+
removeAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
618
|
+
if (signal.aborted) onAbort();
|
|
619
|
+
});
|
|
620
|
+
muLawAudio = await Promise.race([
|
|
621
|
+
synthPromise,
|
|
622
|
+
timeoutPromise,
|
|
623
|
+
abortPromise
|
|
624
|
+
]);
|
|
613
625
|
} finally {
|
|
614
626
|
if (synthTimeout) clearTimeout(synthTimeout);
|
|
615
627
|
clearInterval(keepAlive);
|
|
628
|
+
removeAbortListener();
|
|
616
629
|
}
|
|
617
630
|
if (muLawAudio.length === 0) throw new Error("Telephony TTS produced no audio");
|
|
618
631
|
let chunkAttempts = 0;
|
|
@@ -621,7 +634,11 @@ var TwilioProvider = class TwilioProvider {
|
|
|
621
634
|
for (const chunk of chunkAudio(muLawAudio, CHUNK_SIZE)) {
|
|
622
635
|
if (signal.aborted) break;
|
|
623
636
|
chunkAttempts += 1;
|
|
624
|
-
if (sendAudioChunk(chunk).sent)
|
|
637
|
+
if (!sendAudioChunk(chunk).sent) {
|
|
638
|
+
handler.clearAudio(streamSid);
|
|
639
|
+
throw new Error(`Telephony stream playback failed: audio chunk ${chunkAttempts} not delivered`);
|
|
640
|
+
}
|
|
641
|
+
chunkDelivered += 1;
|
|
625
642
|
const waitMs = nextChunkDueAt - Date.now();
|
|
626
643
|
if (waitMs > 0) try {
|
|
627
644
|
await sleepWithAbort(Math.ceil(waitMs), signal);
|
|
@@ -632,14 +649,10 @@ var TwilioProvider = class TwilioProvider {
|
|
|
632
649
|
nextChunkDueAt += CHUNK_DELAY_MS;
|
|
633
650
|
if (signal.aborted) break;
|
|
634
651
|
}
|
|
635
|
-
|
|
636
|
-
if (
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
if (chunkDelivered === 0) failures.push("no audio chunks delivered");
|
|
640
|
-
if (!markSent) failures.push("completion mark not delivered");
|
|
641
|
-
throw new Error(`Telephony stream playback failed: ${failures.join("; ")}`);
|
|
642
|
-
}
|
|
652
|
+
if (signal.aborted) return;
|
|
653
|
+
if (chunkAttempts === 0 || chunkDelivered !== chunkAttempts) throw new Error("Telephony stream playback failed: incomplete audio delivery");
|
|
654
|
+
const markName = `tts-${Date.now()}-${++this.playbackMarkSequence}`;
|
|
655
|
+
await handler.sendMarkAndWait(streamSid, markName, muLawAudio.length / 8, signal);
|
|
643
656
|
});
|
|
644
657
|
}
|
|
645
658
|
/**
|
package/openclaw.plugin.json
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
},
|
|
7
7
|
"name": "Voice Call",
|
|
8
8
|
"description": "OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls.",
|
|
9
|
+
"cliCommands": [
|
|
10
|
+
{
|
|
11
|
+
"name": "voicecall",
|
|
12
|
+
"description": "Voice call utilities",
|
|
13
|
+
"hasSubcommands": true
|
|
14
|
+
}
|
|
15
|
+
],
|
|
9
16
|
"skills": ["./skills"],
|
|
10
17
|
"commandAliases": [{ "name": "voicecall" }],
|
|
11
18
|
"activation": {
|
|
@@ -85,6 +92,11 @@
|
|
|
85
92
|
"label": "Tailscale Mode",
|
|
86
93
|
"advanced": true
|
|
87
94
|
},
|
|
95
|
+
"tailscale.port": {
|
|
96
|
+
"label": "Tailscale HTTPS Port",
|
|
97
|
+
"help": "External HTTPS port. Funnel supports 443, 8443, or 10000; Serve accepts any valid port.",
|
|
98
|
+
"advanced": true
|
|
99
|
+
},
|
|
88
100
|
"tailscale.path": {
|
|
89
101
|
"label": "Tailscale Path",
|
|
90
102
|
"advanced": true
|
|
@@ -235,7 +247,7 @@
|
|
|
235
247
|
},
|
|
236
248
|
"sessionScope": {
|
|
237
249
|
"label": "Session Scope",
|
|
238
|
-
"help": "Use per-phone to preserve caller memory
|
|
250
|
+
"help": "Use per-phone to preserve caller memory, per-call to isolate every call, or main to share the configured agent's main session."
|
|
239
251
|
},
|
|
240
252
|
"responseModel": {
|
|
241
253
|
"label": "Response Model",
|
|
@@ -421,6 +433,12 @@
|
|
|
421
433
|
"type": "string",
|
|
422
434
|
"enum": ["off", "serve", "funnel"]
|
|
423
435
|
},
|
|
436
|
+
"port": {
|
|
437
|
+
"type": "integer",
|
|
438
|
+
"minimum": 1,
|
|
439
|
+
"maximum": 65535,
|
|
440
|
+
"default": 443
|
|
441
|
+
},
|
|
424
442
|
"path": {
|
|
425
443
|
"type": "string"
|
|
426
444
|
}
|
|
@@ -926,7 +944,7 @@
|
|
|
926
944
|
},
|
|
927
945
|
"sessionScope": {
|
|
928
946
|
"type": "string",
|
|
929
|
-
"enum": ["per-phone", "per-call"]
|
|
947
|
+
"enum": ["per-phone", "per-call", "main"]
|
|
930
948
|
},
|
|
931
949
|
"responseModel": {
|
|
932
950
|
"type": "string"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/voice-call",
|
|
3
|
-
"version": "2026.8.1-beta.
|
|
3
|
+
"version": "2026.8.1-beta.3",
|
|
4
4
|
"description": "OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"zod": "4.4.3"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
|
-
"openclaw": ">=2026.8.1-beta.
|
|
16
|
+
"openclaw": ">=2026.8.1-beta.3"
|
|
17
17
|
},
|
|
18
18
|
"peerDependenciesMeta": {
|
|
19
19
|
"openclaw": {
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"minHostVersion": ">=2026.4.10"
|
|
31
31
|
},
|
|
32
32
|
"compat": {
|
|
33
|
-
"pluginApi": ">=2026.8.1-beta.
|
|
33
|
+
"pluginApi": ">=2026.8.1-beta.3"
|
|
34
34
|
},
|
|
35
35
|
"build": {
|
|
36
|
-
"openclawVersion": "2026.8.1-beta.
|
|
36
|
+
"openclawVersion": "2026.8.1-beta.3"
|
|
37
37
|
},
|
|
38
38
|
"release": {
|
|
39
39
|
"publishToClawHub": true,
|