@openclaw/voice-call 2026.1.29
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/CHANGELOG.md +78 -0
- package/README.md +135 -0
- package/index.ts +497 -0
- package/openclaw.plugin.json +601 -0
- package/package.json +16 -0
- package/src/cli.ts +312 -0
- package/src/config.test.ts +204 -0
- package/src/config.ts +502 -0
- package/src/core-bridge.ts +198 -0
- package/src/manager/context.ts +21 -0
- package/src/manager/events.ts +177 -0
- package/src/manager/lookup.ts +33 -0
- package/src/manager/outbound.ts +248 -0
- package/src/manager/state.ts +50 -0
- package/src/manager/store.ts +88 -0
- package/src/manager/timers.ts +86 -0
- package/src/manager/twiml.ts +9 -0
- package/src/manager.test.ts +108 -0
- package/src/manager.ts +888 -0
- package/src/media-stream.test.ts +97 -0
- package/src/media-stream.ts +393 -0
- package/src/providers/base.ts +67 -0
- package/src/providers/index.ts +10 -0
- package/src/providers/mock.ts +168 -0
- package/src/providers/plivo.test.ts +28 -0
- package/src/providers/plivo.ts +504 -0
- package/src/providers/stt-openai-realtime.ts +311 -0
- package/src/providers/telnyx.ts +364 -0
- package/src/providers/tts-openai.ts +264 -0
- package/src/providers/twilio/api.ts +45 -0
- package/src/providers/twilio/webhook.ts +30 -0
- package/src/providers/twilio.test.ts +64 -0
- package/src/providers/twilio.ts +595 -0
- package/src/response-generator.ts +171 -0
- package/src/runtime.ts +217 -0
- package/src/telephony-audio.ts +88 -0
- package/src/telephony-tts.ts +95 -0
- package/src/tunnel.ts +331 -0
- package/src/types.ts +273 -0
- package/src/utils.ts +12 -0
- package/src/voice-mapping.ts +65 -0
- package/src/webhook-security.test.ts +260 -0
- package/src/webhook-security.ts +469 -0
- package/src/webhook.ts +491 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { escapeXml } from "../voice-mapping.js";
|
|
2
|
+
|
|
3
|
+
export function generateNotifyTwiml(message: string, voice: string): string {
|
|
4
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
5
|
+
<Response>
|
|
6
|
+
<Say voice="${voice}">${escapeXml(message)}</Say>
|
|
7
|
+
<Hangup/>
|
|
8
|
+
</Response>`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { VoiceCallConfigSchema } from "./config.js";
|
|
7
|
+
import { CallManager } from "./manager.js";
|
|
8
|
+
import type {
|
|
9
|
+
HangupCallInput,
|
|
10
|
+
InitiateCallInput,
|
|
11
|
+
InitiateCallResult,
|
|
12
|
+
PlayTtsInput,
|
|
13
|
+
ProviderWebhookParseResult,
|
|
14
|
+
StartListeningInput,
|
|
15
|
+
StopListeningInput,
|
|
16
|
+
WebhookContext,
|
|
17
|
+
WebhookVerificationResult,
|
|
18
|
+
} from "./types.js";
|
|
19
|
+
import type { VoiceCallProvider } from "./providers/base.js";
|
|
20
|
+
|
|
21
|
+
class FakeProvider implements VoiceCallProvider {
|
|
22
|
+
readonly name = "plivo" as const;
|
|
23
|
+
readonly playTtsCalls: PlayTtsInput[] = [];
|
|
24
|
+
|
|
25
|
+
verifyWebhook(_ctx: WebhookContext): WebhookVerificationResult {
|
|
26
|
+
return { ok: true };
|
|
27
|
+
}
|
|
28
|
+
parseWebhookEvent(_ctx: WebhookContext): ProviderWebhookParseResult {
|
|
29
|
+
return { events: [], statusCode: 200 };
|
|
30
|
+
}
|
|
31
|
+
async initiateCall(_input: InitiateCallInput): Promise<InitiateCallResult> {
|
|
32
|
+
return { providerCallId: "request-uuid", status: "initiated" };
|
|
33
|
+
}
|
|
34
|
+
async hangupCall(_input: HangupCallInput): Promise<void> {}
|
|
35
|
+
async playTts(input: PlayTtsInput): Promise<void> {
|
|
36
|
+
this.playTtsCalls.push(input);
|
|
37
|
+
}
|
|
38
|
+
async startListening(_input: StartListeningInput): Promise<void> {}
|
|
39
|
+
async stopListening(_input: StopListeningInput): Promise<void> {}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("CallManager", () => {
|
|
43
|
+
it("upgrades providerCallId mapping when provider ID changes", async () => {
|
|
44
|
+
const config = VoiceCallConfigSchema.parse({
|
|
45
|
+
enabled: true,
|
|
46
|
+
provider: "plivo",
|
|
47
|
+
fromNumber: "+15550000000",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const storePath = path.join(os.tmpdir(), `openclaw-voice-call-test-${Date.now()}`);
|
|
51
|
+
const manager = new CallManager(config, storePath);
|
|
52
|
+
manager.initialize(new FakeProvider(), "https://example.com/voice/webhook");
|
|
53
|
+
|
|
54
|
+
const { callId, success, error } = await manager.initiateCall("+15550000001");
|
|
55
|
+
expect(success).toBe(true);
|
|
56
|
+
expect(error).toBeUndefined();
|
|
57
|
+
|
|
58
|
+
// The provider returned a request UUID as the initial providerCallId.
|
|
59
|
+
expect(manager.getCall(callId)?.providerCallId).toBe("request-uuid");
|
|
60
|
+
expect(manager.getCallByProviderCallId("request-uuid")?.callId).toBe(callId);
|
|
61
|
+
|
|
62
|
+
// Provider later reports the actual call UUID.
|
|
63
|
+
manager.processEvent({
|
|
64
|
+
id: "evt-1",
|
|
65
|
+
type: "call.answered",
|
|
66
|
+
callId,
|
|
67
|
+
providerCallId: "call-uuid",
|
|
68
|
+
timestamp: Date.now(),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(manager.getCall(callId)?.providerCallId).toBe("call-uuid");
|
|
72
|
+
expect(manager.getCallByProviderCallId("call-uuid")?.callId).toBe(callId);
|
|
73
|
+
expect(manager.getCallByProviderCallId("request-uuid")).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("speaks initial message on answered for notify mode (non-Twilio)", async () => {
|
|
77
|
+
const config = VoiceCallConfigSchema.parse({
|
|
78
|
+
enabled: true,
|
|
79
|
+
provider: "plivo",
|
|
80
|
+
fromNumber: "+15550000000",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const storePath = path.join(os.tmpdir(), `openclaw-voice-call-test-${Date.now()}`);
|
|
84
|
+
const provider = new FakeProvider();
|
|
85
|
+
const manager = new CallManager(config, storePath);
|
|
86
|
+
manager.initialize(provider, "https://example.com/voice/webhook");
|
|
87
|
+
|
|
88
|
+
const { callId, success } = await manager.initiateCall(
|
|
89
|
+
"+15550000002",
|
|
90
|
+
undefined,
|
|
91
|
+
{ message: "Hello there", mode: "notify" },
|
|
92
|
+
);
|
|
93
|
+
expect(success).toBe(true);
|
|
94
|
+
|
|
95
|
+
manager.processEvent({
|
|
96
|
+
id: "evt-2",
|
|
97
|
+
type: "call.answered",
|
|
98
|
+
callId,
|
|
99
|
+
providerCallId: "call-uuid",
|
|
100
|
+
timestamp: Date.now(),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
104
|
+
|
|
105
|
+
expect(provider.playTtsCalls).toHaveLength(1);
|
|
106
|
+
expect(provider.playTtsCalls[0]?.text).toBe("Hello there");
|
|
107
|
+
});
|
|
108
|
+
});
|