@openclaw/voice-call 2026.3.13 → 2026.5.2-beta.1
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 +27 -5
- package/api.ts +16 -0
- package/cli-metadata.ts +10 -0
- package/config-api.ts +12 -0
- package/index.test.ts +943 -0
- package/index.ts +379 -149
- package/openclaw.plugin.json +384 -157
- package/package.json +35 -5
- package/runtime-api.ts +20 -0
- package/runtime-entry.ts +1 -0
- package/setup-api.ts +47 -0
- package/src/allowlist.test.ts +18 -0
- package/src/cli.ts +533 -68
- package/src/config-compat.test.ts +120 -0
- package/src/config-compat.ts +227 -0
- package/src/config.test.ts +273 -12
- package/src/config.ts +355 -72
- package/src/core-bridge.ts +2 -147
- package/src/deep-merge.test.ts +40 -0
- package/src/gateway-continue-operation.ts +200 -0
- package/src/http-headers.ts +6 -3
- package/src/manager/context.ts +6 -5
- package/src/manager/events.test.ts +243 -19
- package/src/manager/events.ts +61 -31
- package/src/manager/lifecycle.ts +53 -0
- package/src/manager/lookup.test.ts +52 -0
- package/src/manager/outbound.test.ts +528 -0
- package/src/manager/outbound.ts +163 -57
- package/src/manager/store.ts +18 -6
- package/src/manager/timers.test.ts +129 -0
- package/src/manager/timers.ts +4 -3
- package/src/manager/twiml.test.ts +13 -0
- package/src/manager/twiml.ts +8 -0
- package/src/manager.closed-loop.test.ts +30 -12
- package/src/manager.inbound-allowlist.test.ts +77 -10
- package/src/manager.notify.test.ts +344 -20
- package/src/manager.restore.test.ts +95 -8
- package/src/manager.test-harness.ts +8 -6
- package/src/manager.ts +79 -5
- package/src/media-stream.test.ts +578 -81
- package/src/media-stream.ts +235 -54
- package/src/providers/base.ts +19 -0
- package/src/providers/mock.ts +7 -1
- package/src/providers/plivo.test.ts +50 -6
- package/src/providers/plivo.ts +14 -6
- package/src/providers/shared/call-status.ts +2 -1
- package/src/providers/shared/guarded-json-api.test.ts +106 -0
- package/src/providers/shared/guarded-json-api.ts +1 -1
- package/src/providers/telnyx.test.ts +178 -6
- package/src/providers/telnyx.ts +40 -3
- package/src/providers/twilio/api.test.ts +145 -0
- package/src/providers/twilio/api.ts +67 -16
- package/src/providers/twilio/twiml-policy.ts +6 -10
- package/src/providers/twilio/webhook.ts +1 -1
- package/src/providers/twilio.test.ts +425 -25
- package/src/providers/twilio.ts +230 -77
- package/src/providers/twilio.types.ts +17 -0
- package/src/realtime-defaults.ts +3 -0
- package/src/realtime-fast-context.test.ts +88 -0
- package/src/realtime-fast-context.ts +165 -0
- package/src/realtime-transcription.runtime.ts +4 -0
- package/src/realtime-voice.runtime.ts +5 -0
- package/src/response-generator.test.ts +321 -0
- package/src/response-generator.ts +213 -53
- package/src/response-model.test.ts +71 -0
- package/src/response-model.ts +23 -0
- package/src/runtime.test.ts +429 -0
- package/src/runtime.ts +270 -24
- package/src/telephony-audio.test.ts +61 -0
- package/src/telephony-audio.ts +1 -79
- package/src/telephony-tts.test.ts +133 -12
- package/src/telephony-tts.ts +155 -2
- package/src/test-fixtures.ts +28 -7
- package/src/tts-provider-voice.test.ts +34 -0
- package/src/tts-provider-voice.ts +21 -0
- package/src/tunnel.test.ts +166 -0
- package/src/tunnel.ts +1 -1
- package/src/types.ts +24 -37
- package/src/utils.test.ts +17 -0
- package/src/voice-mapping.test.ts +34 -0
- package/src/voice-mapping.ts +3 -2
- package/src/webhook/realtime-handler.test.ts +598 -0
- package/src/webhook/realtime-handler.ts +485 -0
- package/src/webhook/stale-call-reaper.test.ts +88 -0
- package/src/webhook/stale-call-reaper.ts +5 -0
- package/src/webhook/tailscale.test.ts +214 -0
- package/src/webhook/tailscale.ts +19 -5
- package/src/webhook-exposure.test.ts +33 -0
- package/src/webhook-exposure.ts +84 -0
- package/src/webhook-security.test.ts +172 -21
- package/src/webhook-security.ts +43 -29
- package/src/webhook.hangup-once.lifecycle.test.ts +135 -0
- package/src/webhook.test.ts +1145 -27
- package/src/webhook.ts +523 -102
- package/src/webhook.types.ts +5 -0
- package/src/websocket-test-support.ts +72 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -121
- package/src/providers/index.ts +0 -10
- package/src/providers/stt-openai-realtime.test.ts +0 -42
- package/src/providers/stt-openai-realtime.ts +0 -311
- package/src/providers/tts-openai.test.ts +0 -43
- package/src/providers/tts-openai.ts +0 -221
package/src/types.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
1
|
+
import { z } from "openclaw/plugin-sdk/zod";
|
|
2
2
|
import type { CallMode } from "./config.js";
|
|
3
3
|
|
|
4
4
|
// -----------------------------------------------------------------------------
|
|
5
5
|
// Provider Identifiers
|
|
6
6
|
// -----------------------------------------------------------------------------
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
const ProviderNameSchema = z.enum(["telnyx", "twilio", "plivo", "mock"]);
|
|
9
9
|
export type ProviderName = z.infer<typeof ProviderNameSchema>;
|
|
10
10
|
|
|
11
11
|
// -----------------------------------------------------------------------------
|
|
@@ -16,13 +16,13 @@ export type ProviderName = z.infer<typeof ProviderNameSchema>;
|
|
|
16
16
|
export type CallId = string;
|
|
17
17
|
|
|
18
18
|
/** Provider-specific call identifier */
|
|
19
|
-
|
|
19
|
+
type ProviderCallId = string;
|
|
20
20
|
|
|
21
21
|
// -----------------------------------------------------------------------------
|
|
22
22
|
// Call Lifecycle States
|
|
23
23
|
// -----------------------------------------------------------------------------
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
const CallStateSchema = z.enum([
|
|
26
26
|
// Non-terminal states
|
|
27
27
|
"initiated",
|
|
28
28
|
"ringing",
|
|
@@ -55,7 +55,7 @@ export const TerminalStates = new Set<CallState>([
|
|
|
55
55
|
"voicemail",
|
|
56
56
|
]);
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
const EndReasonSchema = z.enum([
|
|
59
59
|
"completed",
|
|
60
60
|
"hangup-user",
|
|
61
61
|
"hangup-bot",
|
|
@@ -87,7 +87,7 @@ const BaseEventSchema = z.object({
|
|
|
87
87
|
to: z.string().optional(),
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
const NormalizedEventSchema = z.discriminatedUnion("type", [
|
|
91
91
|
BaseEventSchema.extend({
|
|
92
92
|
type: z.literal("call.initiated"),
|
|
93
93
|
}),
|
|
@@ -134,14 +134,13 @@ export type NormalizedEvent = z.infer<typeof NormalizedEventSchema>;
|
|
|
134
134
|
// Call Direction
|
|
135
135
|
// -----------------------------------------------------------------------------
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
export type CallDirection = z.infer<typeof CallDirectionSchema>;
|
|
137
|
+
const CallDirectionSchema = z.enum(["outbound", "inbound"]);
|
|
139
138
|
|
|
140
139
|
// -----------------------------------------------------------------------------
|
|
141
140
|
// Call Record
|
|
142
141
|
// -----------------------------------------------------------------------------
|
|
143
142
|
|
|
144
|
-
|
|
143
|
+
const TranscriptEntrySchema = z.object({
|
|
145
144
|
timestamp: z.number(),
|
|
146
145
|
speaker: z.enum(["bot", "user"]),
|
|
147
146
|
text: z.string(),
|
|
@@ -212,8 +211,10 @@ export type InitiateCallInput = {
|
|
|
212
211
|
to: string;
|
|
213
212
|
webhookUrl: string;
|
|
214
213
|
clientState?: Record<string, string>;
|
|
215
|
-
/** Inline TwiML to execute
|
|
214
|
+
/** Inline TwiML to execute without fetching webhook TwiML. */
|
|
216
215
|
inlineTwiml?: string;
|
|
216
|
+
/** TwiML to serve once before normal webhook-driven call handling resumes. */
|
|
217
|
+
preConnectTwiml?: string;
|
|
217
218
|
};
|
|
218
219
|
|
|
219
220
|
export type InitiateCallResult = {
|
|
@@ -227,6 +228,11 @@ export type HangupCallInput = {
|
|
|
227
228
|
reason: EndReason;
|
|
228
229
|
};
|
|
229
230
|
|
|
231
|
+
export type AnswerCallInput = {
|
|
232
|
+
callId: CallId;
|
|
233
|
+
providerCallId: ProviderCallId;
|
|
234
|
+
};
|
|
235
|
+
|
|
230
236
|
export type PlayTtsInput = {
|
|
231
237
|
callId: CallId;
|
|
232
238
|
providerCallId: ProviderCallId;
|
|
@@ -235,6 +241,12 @@ export type PlayTtsInput = {
|
|
|
235
241
|
locale?: string;
|
|
236
242
|
};
|
|
237
243
|
|
|
244
|
+
export type SendDtmfInput = {
|
|
245
|
+
callId: CallId;
|
|
246
|
+
providerCallId: ProviderCallId;
|
|
247
|
+
digits: string;
|
|
248
|
+
};
|
|
249
|
+
|
|
238
250
|
export type StartListeningInput = {
|
|
239
251
|
callId: CallId;
|
|
240
252
|
providerCallId: ProviderCallId;
|
|
@@ -274,31 +286,6 @@ export type OutboundCallOptions = {
|
|
|
274
286
|
message?: string;
|
|
275
287
|
/** Call mode (overrides config default) */
|
|
276
288
|
mode?: CallMode;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
// -----------------------------------------------------------------------------
|
|
280
|
-
// Tool Result Types
|
|
281
|
-
// -----------------------------------------------------------------------------
|
|
282
|
-
|
|
283
|
-
export type InitiateCallToolResult = {
|
|
284
|
-
success: boolean;
|
|
285
|
-
callId?: string;
|
|
286
|
-
status?: "initiated" | "queued" | "no-answer" | "busy" | "failed";
|
|
287
|
-
error?: string;
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
export type ContinueCallToolResult = {
|
|
291
|
-
success: boolean;
|
|
292
|
-
transcript?: string;
|
|
293
|
-
error?: string;
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
export type SpeakToUserToolResult = {
|
|
297
|
-
success: boolean;
|
|
298
|
-
error?: string;
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
export type EndCallToolResult = {
|
|
302
|
-
success: boolean;
|
|
303
|
-
error?: string;
|
|
289
|
+
/** DTMF digits to send after the call is connected */
|
|
290
|
+
dtmfSequence?: string;
|
|
304
291
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { resolveUserPath } from "./utils.js";
|
|
5
|
+
|
|
6
|
+
describe("resolveUserPath", () => {
|
|
7
|
+
it("returns trimmed empty input unchanged", () => {
|
|
8
|
+
expect(resolveUserPath(" ")).toBe("");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("expands tildes and resolves relative paths", () => {
|
|
12
|
+
expect(resolveUserPath("~/voice-call/config.json")).toBe(
|
|
13
|
+
path.resolve(os.homedir(), "voice-call/config.json"),
|
|
14
|
+
);
|
|
15
|
+
expect(resolveUserPath("./voice-call")).toBe(path.resolve("./voice-call"));
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_POLLY_VOICE,
|
|
4
|
+
escapeXml,
|
|
5
|
+
getOpenAiVoiceNames,
|
|
6
|
+
isOpenAiVoice,
|
|
7
|
+
mapVoiceToPolly,
|
|
8
|
+
} from "./voice-mapping.js";
|
|
9
|
+
|
|
10
|
+
describe("voice mapping", () => {
|
|
11
|
+
it("escapes xml-special characters", () => {
|
|
12
|
+
expect(escapeXml(`5 < 6 & "quote" 'apostrophe' > 4`)).toBe(
|
|
13
|
+
"5 < 6 & "quote" 'apostrophe' > 4",
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("maps openai voices, passes through provider voices, and falls back to default", () => {
|
|
18
|
+
expect(mapVoiceToPolly("alloy")).toBe("Polly.Joanna");
|
|
19
|
+
expect(mapVoiceToPolly("ECHO")).toBe("Polly.Matthew");
|
|
20
|
+
expect(mapVoiceToPolly("Polly.Brian")).toBe("Polly.Brian");
|
|
21
|
+
expect(mapVoiceToPolly("Google.en-US-Standard-C")).toBe("Google.en-US-Standard-C");
|
|
22
|
+
expect(mapVoiceToPolly("unknown")).toBe(DEFAULT_POLLY_VOICE);
|
|
23
|
+
expect(mapVoiceToPolly(undefined)).toBe(DEFAULT_POLLY_VOICE);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("detects known openai voices and lists them", () => {
|
|
27
|
+
expect(isOpenAiVoice("nova")).toBe(true);
|
|
28
|
+
expect(isOpenAiVoice("NOVA")).toBe(true);
|
|
29
|
+
expect(isOpenAiVoice("Polly.Joanna")).toBe(false);
|
|
30
|
+
expect(getOpenAiVoiceNames()).toEqual(
|
|
31
|
+
expect.arrayContaining(["alloy", "echo", "fable", "nova", "onyx", "shimmer"]),
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
});
|
package/src/voice-mapping.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Voice mapping and XML utilities for voice call providers.
|
|
3
3
|
*/
|
|
4
|
+
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Escape XML special characters for TwiML and other XML responses.
|
|
@@ -49,14 +50,14 @@ export function mapVoiceToPolly(voice: string | undefined): string {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
// Map OpenAI voices to Polly equivalents
|
|
52
|
-
return OPENAI_TO_POLLY_MAP[voice
|
|
53
|
+
return OPENAI_TO_POLLY_MAP[normalizeLowercaseStringOrEmpty(voice)] || DEFAULT_POLLY_VOICE;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
57
|
* Check if a voice name is a known OpenAI voice.
|
|
57
58
|
*/
|
|
58
59
|
export function isOpenAiVoice(voice: string): boolean {
|
|
59
|
-
return voice
|
|
60
|
+
return normalizeLowercaseStringOrEmpty(voice) in OPENAI_TO_POLLY_MAP;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
/**
|