@openclaw/voice-call 2026.5.2 → 2026.5.3-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/api.js +2 -0
- package/dist/call-status-CXldV5o8.js +32 -0
- package/dist/cli-metadata.js +12 -0
- package/dist/config-7w04YpHh.js +548 -0
- package/dist/config-compat-B0me39_4.js +129 -0
- package/dist/guarded-json-api-Btx5EE4w.js +591 -0
- package/dist/http-headers-BrnxBasF.js +10 -0
- package/dist/index.js +1284 -0
- package/dist/mock-CeKvfVEd.js +135 -0
- package/dist/plivo-B-a7KFoT.js +393 -0
- package/dist/realtime-handler-B63CIDP2.js +325 -0
- package/dist/realtime-transcription.runtime-B2h70y2W.js +2 -0
- package/dist/realtime-voice.runtime-Bkh4nvLn.js +2 -0
- package/dist/response-generator-BrcmwDZU.js +182 -0
- package/dist/response-model-CyF5K80p.js +12 -0
- package/dist/runtime-api.js +6 -0
- package/dist/runtime-entry-88ytYAQa.js +3119 -0
- package/dist/runtime-entry.js +2 -0
- package/dist/setup-api.js +37 -0
- package/dist/telnyx-jjBE8boz.js +260 -0
- package/dist/twilio-1OqbcXLL.js +676 -0
- package/dist/voice-mapping-BYDGdWGx.js +40 -0
- package/package.json +14 -6
- package/api.ts +0 -16
- package/cli-metadata.ts +0 -10
- package/config-api.ts +0 -12
- package/index.test.ts +0 -943
- package/index.ts +0 -794
- package/runtime-api.ts +0 -20
- package/runtime-entry.ts +0 -1
- package/setup-api.ts +0 -47
- package/src/allowlist.test.ts +0 -18
- package/src/allowlist.ts +0 -19
- package/src/cli.ts +0 -845
- package/src/config-compat.test.ts +0 -120
- package/src/config-compat.ts +0 -227
- package/src/config.test.ts +0 -479
- package/src/config.ts +0 -808
- package/src/core-bridge.ts +0 -14
- package/src/deep-merge.test.ts +0 -40
- package/src/deep-merge.ts +0 -23
- package/src/gateway-continue-operation.ts +0 -200
- package/src/http-headers.test.ts +0 -16
- package/src/http-headers.ts +0 -15
- package/src/manager/context.ts +0 -42
- package/src/manager/events.test.ts +0 -581
- package/src/manager/events.ts +0 -288
- package/src/manager/lifecycle.ts +0 -53
- package/src/manager/lookup.test.ts +0 -52
- package/src/manager/lookup.ts +0 -35
- package/src/manager/outbound.test.ts +0 -528
- package/src/manager/outbound.ts +0 -486
- package/src/manager/state.ts +0 -48
- package/src/manager/store.ts +0 -106
- package/src/manager/timers.test.ts +0 -129
- package/src/manager/timers.ts +0 -113
- package/src/manager/twiml.test.ts +0 -13
- package/src/manager/twiml.ts +0 -17
- package/src/manager.closed-loop.test.ts +0 -236
- package/src/manager.inbound-allowlist.test.ts +0 -188
- package/src/manager.notify.test.ts +0 -377
- package/src/manager.restore.test.ts +0 -183
- package/src/manager.test-harness.ts +0 -127
- package/src/manager.ts +0 -392
- package/src/media-stream.test.ts +0 -768
- package/src/media-stream.ts +0 -708
- package/src/providers/base.ts +0 -97
- package/src/providers/mock.test.ts +0 -78
- package/src/providers/mock.ts +0 -185
- package/src/providers/plivo.test.ts +0 -93
- package/src/providers/plivo.ts +0 -601
- package/src/providers/shared/call-status.test.ts +0 -24
- package/src/providers/shared/call-status.ts +0 -24
- package/src/providers/shared/guarded-json-api.test.ts +0 -106
- package/src/providers/shared/guarded-json-api.ts +0 -42
- package/src/providers/telnyx.test.ts +0 -340
- package/src/providers/telnyx.ts +0 -394
- package/src/providers/twilio/api.test.ts +0 -145
- package/src/providers/twilio/api.ts +0 -93
- package/src/providers/twilio/twiml-policy.test.ts +0 -84
- package/src/providers/twilio/twiml-policy.ts +0 -87
- package/src/providers/twilio/webhook.ts +0 -34
- package/src/providers/twilio.test.ts +0 -591
- package/src/providers/twilio.ts +0 -861
- package/src/providers/twilio.types.ts +0 -17
- package/src/realtime-defaults.ts +0 -3
- package/src/realtime-fast-context.test.ts +0 -88
- package/src/realtime-fast-context.ts +0 -165
- package/src/realtime-transcription.runtime.ts +0 -4
- package/src/realtime-voice.runtime.ts +0 -5
- package/src/response-generator.test.ts +0 -321
- package/src/response-generator.ts +0 -318
- package/src/response-model.test.ts +0 -71
- package/src/response-model.ts +0 -23
- package/src/runtime.test.ts +0 -536
- package/src/runtime.ts +0 -510
- package/src/telephony-audio.test.ts +0 -61
- package/src/telephony-audio.ts +0 -12
- package/src/telephony-tts.test.ts +0 -196
- package/src/telephony-tts.ts +0 -235
- package/src/test-fixtures.ts +0 -73
- package/src/tts-provider-voice.test.ts +0 -34
- package/src/tts-provider-voice.ts +0 -21
- package/src/tunnel.test.ts +0 -166
- package/src/tunnel.ts +0 -314
- package/src/types.ts +0 -291
- package/src/utils.test.ts +0 -17
- package/src/utils.ts +0 -14
- package/src/voice-mapping.test.ts +0 -34
- package/src/voice-mapping.ts +0 -68
- package/src/webhook/realtime-handler.test.ts +0 -598
- package/src/webhook/realtime-handler.ts +0 -485
- package/src/webhook/stale-call-reaper.test.ts +0 -88
- package/src/webhook/stale-call-reaper.ts +0 -38
- package/src/webhook/tailscale.test.ts +0 -214
- package/src/webhook/tailscale.ts +0 -129
- package/src/webhook-exposure.test.ts +0 -33
- package/src/webhook-exposure.ts +0 -84
- package/src/webhook-security.test.ts +0 -770
- package/src/webhook-security.ts +0 -994
- package/src/webhook.hangup-once.lifecycle.test.ts +0 -135
- package/src/webhook.test.ts +0 -1470
- package/src/webhook.ts +0 -908
- package/src/webhook.types.ts +0 -5
- package/src/websocket-test-support.ts +0 -72
- package/tsconfig.json +0 -16
package/index.ts
DELETED
|
@@ -1,794 +0,0 @@
|
|
|
1
|
-
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
2
|
-
import { ErrorCodes, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
|
|
3
|
-
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
|
4
|
-
import { Type } from "typebox";
|
|
5
|
-
import {
|
|
6
|
-
definePluginEntry,
|
|
7
|
-
type GatewayRequestHandlerOptions,
|
|
8
|
-
type OpenClawPluginApi,
|
|
9
|
-
} from "./api.js";
|
|
10
|
-
import { createVoiceCallRuntime, type VoiceCallRuntime } from "./runtime-entry.js";
|
|
11
|
-
import { registerVoiceCallCli } from "./src/cli.js";
|
|
12
|
-
import {
|
|
13
|
-
formatVoiceCallLegacyConfigWarnings,
|
|
14
|
-
normalizeVoiceCallLegacyConfigInput,
|
|
15
|
-
parseVoiceCallPluginConfig,
|
|
16
|
-
} from "./src/config-compat.js";
|
|
17
|
-
import {
|
|
18
|
-
resolveVoiceCallConfig,
|
|
19
|
-
validateProviderConfig,
|
|
20
|
-
type VoiceCallConfig,
|
|
21
|
-
} from "./src/config.js";
|
|
22
|
-
import type { CoreConfig } from "./src/core-bridge.js";
|
|
23
|
-
import { createVoiceCallContinueOperationStore } from "./src/gateway-continue-operation.js";
|
|
24
|
-
|
|
25
|
-
const voiceCallConfigSchema = {
|
|
26
|
-
parse(value: unknown): VoiceCallConfig {
|
|
27
|
-
const normalized = normalizeVoiceCallLegacyConfigInput(value);
|
|
28
|
-
const enabled = typeof normalized.enabled === "boolean" ? normalized.enabled : true;
|
|
29
|
-
return parseVoiceCallPluginConfig({
|
|
30
|
-
...normalized,
|
|
31
|
-
enabled,
|
|
32
|
-
provider: normalized.provider ?? (enabled ? "mock" : undefined),
|
|
33
|
-
});
|
|
34
|
-
},
|
|
35
|
-
uiHints: {
|
|
36
|
-
provider: {
|
|
37
|
-
label: "Provider",
|
|
38
|
-
help: "Use twilio, telnyx, or mock for dev/no-network.",
|
|
39
|
-
},
|
|
40
|
-
fromNumber: { label: "From Number", placeholder: "+15550001234" },
|
|
41
|
-
toNumber: { label: "Default To Number", placeholder: "+15550001234" },
|
|
42
|
-
inboundPolicy: { label: "Inbound Policy" },
|
|
43
|
-
allowFrom: { label: "Inbound Allowlist" },
|
|
44
|
-
inboundGreeting: { label: "Inbound Greeting", advanced: true },
|
|
45
|
-
numbers: {
|
|
46
|
-
label: "Per-number Routing",
|
|
47
|
-
help: "Inbound overrides keyed by dialed E.164 number.",
|
|
48
|
-
advanced: true,
|
|
49
|
-
},
|
|
50
|
-
"telnyx.apiKey": { label: "Telnyx API Key", sensitive: true },
|
|
51
|
-
"telnyx.connectionId": { label: "Telnyx Connection ID" },
|
|
52
|
-
"telnyx.publicKey": { label: "Telnyx Public Key", sensitive: true },
|
|
53
|
-
"twilio.accountSid": { label: "Twilio Account SID" },
|
|
54
|
-
"twilio.authToken": { label: "Twilio Auth Token", sensitive: true },
|
|
55
|
-
"outbound.defaultMode": { label: "Default Call Mode" },
|
|
56
|
-
"outbound.notifyHangupDelaySec": {
|
|
57
|
-
label: "Notify Hangup Delay (sec)",
|
|
58
|
-
advanced: true,
|
|
59
|
-
},
|
|
60
|
-
"serve.port": { label: "Webhook Port" },
|
|
61
|
-
"serve.bind": { label: "Webhook Bind" },
|
|
62
|
-
"serve.path": { label: "Webhook Path" },
|
|
63
|
-
"tailscale.mode": { label: "Tailscale Mode", advanced: true },
|
|
64
|
-
"tailscale.path": { label: "Tailscale Path", advanced: true },
|
|
65
|
-
"tunnel.provider": { label: "Tunnel Provider", advanced: true },
|
|
66
|
-
"tunnel.ngrokAuthToken": {
|
|
67
|
-
label: "ngrok Auth Token",
|
|
68
|
-
sensitive: true,
|
|
69
|
-
advanced: true,
|
|
70
|
-
},
|
|
71
|
-
"tunnel.ngrokDomain": { label: "ngrok Domain", advanced: true },
|
|
72
|
-
"tunnel.allowNgrokFreeTierLoopbackBypass": {
|
|
73
|
-
label: "Allow ngrok Free Tier (Loopback Bypass)",
|
|
74
|
-
advanced: true,
|
|
75
|
-
},
|
|
76
|
-
"streaming.enabled": { label: "Enable Streaming", advanced: true },
|
|
77
|
-
"streaming.provider": {
|
|
78
|
-
label: "Streaming Provider",
|
|
79
|
-
help: "Uses the first registered realtime transcription provider when unset.",
|
|
80
|
-
advanced: true,
|
|
81
|
-
},
|
|
82
|
-
"streaming.providers": { label: "Streaming Provider Config", advanced: true },
|
|
83
|
-
"streaming.streamPath": { label: "Media Stream Path", advanced: true },
|
|
84
|
-
"realtime.enabled": { label: "Enable Realtime Voice", advanced: true },
|
|
85
|
-
"realtime.provider": {
|
|
86
|
-
label: "Realtime Voice Provider",
|
|
87
|
-
help: "Uses the first registered realtime voice provider when unset.",
|
|
88
|
-
advanced: true,
|
|
89
|
-
},
|
|
90
|
-
"realtime.streamPath": { label: "Realtime Stream Path", advanced: true },
|
|
91
|
-
"realtime.instructions": { label: "Realtime Instructions", advanced: true },
|
|
92
|
-
"realtime.toolPolicy": {
|
|
93
|
-
label: "Realtime Tool Policy",
|
|
94
|
-
help: "Controls the shared openclaw_agent_consult tool.",
|
|
95
|
-
advanced: true,
|
|
96
|
-
},
|
|
97
|
-
"realtime.fastContext.enabled": {
|
|
98
|
-
label: "Enable Fast Realtime Context",
|
|
99
|
-
help: "Searches memory/session context before the full consult agent.",
|
|
100
|
-
advanced: true,
|
|
101
|
-
},
|
|
102
|
-
"realtime.fastContext.timeoutMs": {
|
|
103
|
-
label: "Fast Context Timeout",
|
|
104
|
-
advanced: true,
|
|
105
|
-
},
|
|
106
|
-
"realtime.fastContext.maxResults": {
|
|
107
|
-
label: "Fast Context Result Limit",
|
|
108
|
-
advanced: true,
|
|
109
|
-
},
|
|
110
|
-
"realtime.fastContext.sources": {
|
|
111
|
-
label: "Fast Context Sources",
|
|
112
|
-
advanced: true,
|
|
113
|
-
},
|
|
114
|
-
"realtime.fastContext.fallbackToConsult": {
|
|
115
|
-
label: "Fallback To Full Consult",
|
|
116
|
-
advanced: true,
|
|
117
|
-
},
|
|
118
|
-
"realtime.providers": { label: "Realtime Provider Config", advanced: true },
|
|
119
|
-
"tts.provider": {
|
|
120
|
-
label: "TTS Provider Override",
|
|
121
|
-
help: "Deep-merges with messages.tts (Microsoft is ignored for calls).",
|
|
122
|
-
advanced: true,
|
|
123
|
-
},
|
|
124
|
-
"tts.providers": { label: "TTS Provider Config", advanced: true },
|
|
125
|
-
publicUrl: { label: "Public Webhook URL", advanced: true },
|
|
126
|
-
skipSignatureVerification: {
|
|
127
|
-
label: "Skip Signature Verification",
|
|
128
|
-
advanced: true,
|
|
129
|
-
},
|
|
130
|
-
store: { label: "Call Log Store Path", advanced: true },
|
|
131
|
-
agentId: {
|
|
132
|
-
label: "Response Agent ID",
|
|
133
|
-
help: 'Agent workspace used for voice response generation. Defaults to "main".',
|
|
134
|
-
advanced: true,
|
|
135
|
-
},
|
|
136
|
-
responseModel: {
|
|
137
|
-
label: "Response Model",
|
|
138
|
-
help: "Optional override. Falls back to the runtime default model when unset.",
|
|
139
|
-
advanced: true,
|
|
140
|
-
},
|
|
141
|
-
responseSystemPrompt: { label: "Response System Prompt", advanced: true },
|
|
142
|
-
responseTimeoutMs: { label: "Response Timeout (ms)", advanced: true },
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const VoiceCallToolSchema = Type.Union([
|
|
147
|
-
Type.Object({
|
|
148
|
-
action: Type.Literal("initiate_call"),
|
|
149
|
-
to: Type.Optional(Type.String({ description: "Call target" })),
|
|
150
|
-
message: Type.String({ description: "Intro message" }),
|
|
151
|
-
mode: Type.Optional(Type.Union([Type.Literal("notify"), Type.Literal("conversation")])),
|
|
152
|
-
dtmfSequence: Type.Optional(Type.String({ description: "DTMF digits to play before connect" })),
|
|
153
|
-
}),
|
|
154
|
-
Type.Object({
|
|
155
|
-
action: Type.Literal("continue_call"),
|
|
156
|
-
callId: Type.String({ description: "Call ID" }),
|
|
157
|
-
message: Type.String({ description: "Follow-up message" }),
|
|
158
|
-
}),
|
|
159
|
-
Type.Object({
|
|
160
|
-
action: Type.Literal("speak_to_user"),
|
|
161
|
-
callId: Type.String({ description: "Call ID" }),
|
|
162
|
-
message: Type.String({ description: "Message to speak" }),
|
|
163
|
-
}),
|
|
164
|
-
Type.Object({
|
|
165
|
-
action: Type.Literal("send_dtmf"),
|
|
166
|
-
callId: Type.String({ description: "Call ID" }),
|
|
167
|
-
digits: Type.String({ description: "DTMF digits to send" }),
|
|
168
|
-
}),
|
|
169
|
-
Type.Object({
|
|
170
|
-
action: Type.Literal("end_call"),
|
|
171
|
-
callId: Type.String({ description: "Call ID" }),
|
|
172
|
-
}),
|
|
173
|
-
Type.Object({
|
|
174
|
-
action: Type.Literal("get_status"),
|
|
175
|
-
callId: Type.String({ description: "Call ID" }),
|
|
176
|
-
}),
|
|
177
|
-
Type.Object({
|
|
178
|
-
mode: Type.Optional(Type.Union([Type.Literal("call"), Type.Literal("status")])),
|
|
179
|
-
to: Type.Optional(Type.String({ description: "Call target" })),
|
|
180
|
-
sid: Type.Optional(Type.String({ description: "Call SID" })),
|
|
181
|
-
message: Type.Optional(Type.String({ description: "Optional intro message" })),
|
|
182
|
-
dtmfSequence: Type.Optional(Type.String({ description: "DTMF digits to play before connect" })),
|
|
183
|
-
}),
|
|
184
|
-
]);
|
|
185
|
-
|
|
186
|
-
function asParamRecord(params: unknown): Record<string, unknown> {
|
|
187
|
-
return params && typeof params === "object" && !Array.isArray(params)
|
|
188
|
-
? (params as Record<string, unknown>)
|
|
189
|
-
: {};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function isCliOnlyProcess(): boolean {
|
|
193
|
-
return process.env.OPENCLAW_CLI === "1" && !process.argv.slice(2).includes("gateway");
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const VOICE_CALL_RUNTIME_KEY = Symbol.for("openclaw.voice-call.runtime");
|
|
197
|
-
const VOICE_CALL_RUNTIME_PROMISE_KEY = Symbol.for("openclaw.voice-call.runtimePromise");
|
|
198
|
-
const VOICE_CALL_RUNTIME_STOP_PROMISE_KEY = Symbol.for("openclaw.voice-call.runtimeStopPromise");
|
|
199
|
-
|
|
200
|
-
type VoiceCallRuntimeGlobalState = typeof globalThis & {
|
|
201
|
-
[VOICE_CALL_RUNTIME_KEY]?: VoiceCallRuntime | null;
|
|
202
|
-
[VOICE_CALL_RUNTIME_PROMISE_KEY]?: Promise<VoiceCallRuntime> | null;
|
|
203
|
-
[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY]?: Promise<void> | null;
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
function getVoiceCallRuntimeGlobalState(): VoiceCallRuntimeGlobalState {
|
|
207
|
-
const state = globalThis as VoiceCallRuntimeGlobalState;
|
|
208
|
-
state[VOICE_CALL_RUNTIME_KEY] ??= null;
|
|
209
|
-
state[VOICE_CALL_RUNTIME_PROMISE_KEY] ??= null;
|
|
210
|
-
state[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY] ??= null;
|
|
211
|
-
return state;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export default definePluginEntry({
|
|
215
|
-
id: "voice-call",
|
|
216
|
-
name: "Voice Call",
|
|
217
|
-
description: "Voice-call plugin with Telnyx/Twilio/Plivo providers",
|
|
218
|
-
configSchema: voiceCallConfigSchema,
|
|
219
|
-
register(api: OpenClawPluginApi) {
|
|
220
|
-
const config = resolveVoiceCallConfig(voiceCallConfigSchema.parse(api.pluginConfig));
|
|
221
|
-
const validation = validateProviderConfig(config);
|
|
222
|
-
|
|
223
|
-
if (api.pluginConfig && typeof api.pluginConfig === "object") {
|
|
224
|
-
for (const warning of formatVoiceCallLegacyConfigWarnings({
|
|
225
|
-
value: api.pluginConfig,
|
|
226
|
-
configPathPrefix: "plugins.entries.voice-call.config",
|
|
227
|
-
doctorFixCommand: "openclaw doctor --fix",
|
|
228
|
-
})) {
|
|
229
|
-
api.logger.warn(warning);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const runtimeState = getVoiceCallRuntimeGlobalState();
|
|
234
|
-
const continueOperationStore = createVoiceCallContinueOperationStore({
|
|
235
|
-
config,
|
|
236
|
-
coreConfig: api.config as CoreConfig,
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
const ensureRuntime = async (): Promise<VoiceCallRuntime> => {
|
|
240
|
-
if (!config.enabled) {
|
|
241
|
-
throw new Error("Voice call disabled in plugin config");
|
|
242
|
-
}
|
|
243
|
-
if (!validation.valid) {
|
|
244
|
-
throw new Error(validation.errors.join("; "));
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
while (true) {
|
|
248
|
-
if (runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY]) {
|
|
249
|
-
await runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY];
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const runtime = runtimeState[VOICE_CALL_RUNTIME_KEY];
|
|
254
|
-
if (runtime) {
|
|
255
|
-
return runtime;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
let runtimePromise = runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY];
|
|
259
|
-
if (!runtimePromise) {
|
|
260
|
-
runtimePromise = createVoiceCallRuntime({
|
|
261
|
-
config,
|
|
262
|
-
coreConfig: api.config as CoreConfig,
|
|
263
|
-
fullConfig: api.config,
|
|
264
|
-
agentRuntime: api.runtime.agent,
|
|
265
|
-
ttsRuntime: api.runtime.tts,
|
|
266
|
-
logger: api.logger,
|
|
267
|
-
});
|
|
268
|
-
runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] = runtimePromise;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
const createdRuntime = await runtimePromise;
|
|
273
|
-
if (runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY]) {
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
if (runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] !== runtimePromise) {
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
runtimeState[VOICE_CALL_RUNTIME_KEY] = createdRuntime;
|
|
280
|
-
return createdRuntime;
|
|
281
|
-
} catch (err) {
|
|
282
|
-
if (runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] === runtimePromise) {
|
|
283
|
-
// Reset shared state so the next call can retry instead of caching
|
|
284
|
-
// a rejected promise across plugin contexts. See: #32387, #58115.
|
|
285
|
-
runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] = null;
|
|
286
|
-
runtimeState[VOICE_CALL_RUNTIME_KEY] = null;
|
|
287
|
-
}
|
|
288
|
-
throw err;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
const respondError = (
|
|
294
|
-
respond: GatewayRequestHandlerOptions["respond"],
|
|
295
|
-
message: string,
|
|
296
|
-
code: (typeof ErrorCodes)[keyof typeof ErrorCodes] = ErrorCodes.UNAVAILABLE,
|
|
297
|
-
) => {
|
|
298
|
-
respond(false, undefined, errorShape(code, message));
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
const sendError = (respond: GatewayRequestHandlerOptions["respond"], err: unknown) => {
|
|
302
|
-
respondError(respond, formatErrorMessage(err));
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
const describeHistoricalCall = async (rt: VoiceCallRuntime, callId: string) => {
|
|
306
|
-
const history = await rt.manager.getCallHistory(100);
|
|
307
|
-
const call = history
|
|
308
|
-
.toReversed()
|
|
309
|
-
.find((candidate) => candidate.callId === callId || candidate.providerCallId === callId);
|
|
310
|
-
if (!call) {
|
|
311
|
-
return undefined;
|
|
312
|
-
}
|
|
313
|
-
const details = [
|
|
314
|
-
`last state=${call.state}`,
|
|
315
|
-
call.endReason ? `endReason=${call.endReason}` : undefined,
|
|
316
|
-
call.endedAt ? `endedAt=${new Date(call.endedAt).toISOString()}` : undefined,
|
|
317
|
-
].filter(Boolean);
|
|
318
|
-
return `call is not active (${details.join(", ")})`;
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
const resolveCallMessageRequest = async (params: GatewayRequestHandlerOptions["params"]) => {
|
|
322
|
-
const callId = normalizeOptionalString(params?.callId) ?? "";
|
|
323
|
-
const message = normalizeOptionalString(params?.message) ?? "";
|
|
324
|
-
if (!callId || !message) {
|
|
325
|
-
return { error: "callId and message required" } as const;
|
|
326
|
-
}
|
|
327
|
-
const rt = await ensureRuntime();
|
|
328
|
-
const activeCall = rt.manager.getCall(callId) ?? rt.manager.getCallByProviderCallId(callId);
|
|
329
|
-
if (activeCall) {
|
|
330
|
-
return { rt, callId: activeCall.callId, message } as const;
|
|
331
|
-
}
|
|
332
|
-
return { error: (await describeHistoricalCall(rt, callId)) ?? "Call not found" } as const;
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
const initiateCallAndRespond = async (params: {
|
|
336
|
-
rt: VoiceCallRuntime;
|
|
337
|
-
respond: GatewayRequestHandlerOptions["respond"];
|
|
338
|
-
to: string;
|
|
339
|
-
message?: string;
|
|
340
|
-
mode?: "notify" | "conversation";
|
|
341
|
-
dtmfSequence?: string;
|
|
342
|
-
}) => {
|
|
343
|
-
const result = await params.rt.manager.initiateCall(params.to, undefined, {
|
|
344
|
-
message: params.message,
|
|
345
|
-
mode: params.mode,
|
|
346
|
-
dtmfSequence: params.dtmfSequence,
|
|
347
|
-
});
|
|
348
|
-
if (!result.success) {
|
|
349
|
-
respondError(params.respond, result.error || "initiate failed");
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
params.respond(true, { callId: result.callId, initiated: true });
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
const respondToCallMessageAction = async (params: {
|
|
356
|
-
requestParams: GatewayRequestHandlerOptions["params"];
|
|
357
|
-
respond: GatewayRequestHandlerOptions["respond"];
|
|
358
|
-
action: (
|
|
359
|
-
request: Exclude<Awaited<ReturnType<typeof resolveCallMessageRequest>>, { error: string }>,
|
|
360
|
-
) => Promise<{
|
|
361
|
-
success: boolean;
|
|
362
|
-
error?: string;
|
|
363
|
-
transcript?: string;
|
|
364
|
-
}>;
|
|
365
|
-
failure: string;
|
|
366
|
-
includeTranscript?: boolean;
|
|
367
|
-
}) => {
|
|
368
|
-
const request = await resolveCallMessageRequest(params.requestParams);
|
|
369
|
-
if ("error" in request) {
|
|
370
|
-
respondError(
|
|
371
|
-
params.respond,
|
|
372
|
-
request.error ?? "callId and message required",
|
|
373
|
-
ErrorCodes.INVALID_REQUEST,
|
|
374
|
-
);
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
const result = await params.action(request);
|
|
378
|
-
if (!result.success) {
|
|
379
|
-
respondError(params.respond, result.error || params.failure);
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
params.respond(
|
|
383
|
-
true,
|
|
384
|
-
params.includeTranscript
|
|
385
|
-
? { success: true, transcript: result.transcript }
|
|
386
|
-
: { success: true },
|
|
387
|
-
);
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
api.registerGatewayMethod(
|
|
391
|
-
"voicecall.initiate",
|
|
392
|
-
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
393
|
-
try {
|
|
394
|
-
const message = normalizeOptionalString(params?.message) ?? "";
|
|
395
|
-
if (!message) {
|
|
396
|
-
respondError(respond, "message required", ErrorCodes.INVALID_REQUEST);
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
const rt = await ensureRuntime();
|
|
400
|
-
const to = normalizeOptionalString(params?.to) ?? rt.config.toNumber;
|
|
401
|
-
if (!to) {
|
|
402
|
-
respondError(respond, "to required", ErrorCodes.INVALID_REQUEST);
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
const mode =
|
|
406
|
-
params?.mode === "notify" || params?.mode === "conversation" ? params.mode : undefined;
|
|
407
|
-
await initiateCallAndRespond({
|
|
408
|
-
rt,
|
|
409
|
-
respond,
|
|
410
|
-
to,
|
|
411
|
-
message,
|
|
412
|
-
mode,
|
|
413
|
-
});
|
|
414
|
-
} catch (err) {
|
|
415
|
-
sendError(respond, err);
|
|
416
|
-
}
|
|
417
|
-
},
|
|
418
|
-
);
|
|
419
|
-
|
|
420
|
-
api.registerGatewayMethod(
|
|
421
|
-
"voicecall.continue",
|
|
422
|
-
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
423
|
-
try {
|
|
424
|
-
await respondToCallMessageAction({
|
|
425
|
-
requestParams: params,
|
|
426
|
-
respond,
|
|
427
|
-
action: (request) => request.rt.manager.continueCall(request.callId, request.message),
|
|
428
|
-
failure: "continue failed",
|
|
429
|
-
includeTranscript: true,
|
|
430
|
-
});
|
|
431
|
-
} catch (err) {
|
|
432
|
-
sendError(respond, err);
|
|
433
|
-
}
|
|
434
|
-
},
|
|
435
|
-
);
|
|
436
|
-
|
|
437
|
-
api.registerGatewayMethod(
|
|
438
|
-
"voicecall.continue.start",
|
|
439
|
-
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
440
|
-
try {
|
|
441
|
-
const request = await resolveCallMessageRequest(params);
|
|
442
|
-
if ("error" in request) {
|
|
443
|
-
respondError(
|
|
444
|
-
respond,
|
|
445
|
-
request.error ?? "callId and message required",
|
|
446
|
-
ErrorCodes.INVALID_REQUEST,
|
|
447
|
-
);
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
respond(true, continueOperationStore.start(request));
|
|
451
|
-
} catch (err) {
|
|
452
|
-
sendError(respond, err);
|
|
453
|
-
}
|
|
454
|
-
},
|
|
455
|
-
);
|
|
456
|
-
|
|
457
|
-
api.registerGatewayMethod(
|
|
458
|
-
"voicecall.continue.result",
|
|
459
|
-
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
460
|
-
try {
|
|
461
|
-
const operationId = normalizeOptionalString(params?.operationId) ?? "";
|
|
462
|
-
if (!operationId) {
|
|
463
|
-
respondError(respond, "operationId required", ErrorCodes.INVALID_REQUEST);
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
const operation = continueOperationStore.read(operationId);
|
|
467
|
-
if (!operation.ok) {
|
|
468
|
-
respondError(respond, operation.error, ErrorCodes.INVALID_REQUEST);
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
respond(true, operation.payload);
|
|
472
|
-
} catch (err) {
|
|
473
|
-
sendError(respond, err);
|
|
474
|
-
}
|
|
475
|
-
},
|
|
476
|
-
);
|
|
477
|
-
|
|
478
|
-
api.registerGatewayMethod(
|
|
479
|
-
"voicecall.speak",
|
|
480
|
-
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
481
|
-
try {
|
|
482
|
-
const request = await resolveCallMessageRequest(params);
|
|
483
|
-
if ("error" in request) {
|
|
484
|
-
respondError(
|
|
485
|
-
respond,
|
|
486
|
-
request.error ?? "callId and message required",
|
|
487
|
-
ErrorCodes.INVALID_REQUEST,
|
|
488
|
-
);
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
if (request.rt.config.realtime.enabled) {
|
|
492
|
-
const realtimeResult = request.rt.webhookServer.speakRealtime(
|
|
493
|
-
request.callId,
|
|
494
|
-
request.message,
|
|
495
|
-
);
|
|
496
|
-
if (realtimeResult.success) {
|
|
497
|
-
respond(true, { success: true });
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
const result = await request.rt.manager.speak(request.callId, request.message);
|
|
502
|
-
if (!result.success) {
|
|
503
|
-
respondError(respond, result.error || "speak failed");
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
respond(true, { success: true });
|
|
507
|
-
} catch (err) {
|
|
508
|
-
sendError(respond, err);
|
|
509
|
-
}
|
|
510
|
-
},
|
|
511
|
-
);
|
|
512
|
-
|
|
513
|
-
api.registerGatewayMethod(
|
|
514
|
-
"voicecall.dtmf",
|
|
515
|
-
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
516
|
-
try {
|
|
517
|
-
const callId = normalizeOptionalString(params?.callId) ?? "";
|
|
518
|
-
const digits = normalizeOptionalString(params?.digits) ?? "";
|
|
519
|
-
if (!callId || !digits) {
|
|
520
|
-
respondError(respond, "callId and digits required", ErrorCodes.INVALID_REQUEST);
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
const rt = await ensureRuntime();
|
|
524
|
-
const result = await rt.manager.sendDtmf(callId, digits);
|
|
525
|
-
if (!result.success) {
|
|
526
|
-
respondError(respond, result.error || "dtmf failed");
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
respond(true, { success: true });
|
|
530
|
-
} catch (err) {
|
|
531
|
-
sendError(respond, err);
|
|
532
|
-
}
|
|
533
|
-
},
|
|
534
|
-
);
|
|
535
|
-
|
|
536
|
-
api.registerGatewayMethod(
|
|
537
|
-
"voicecall.end",
|
|
538
|
-
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
539
|
-
try {
|
|
540
|
-
const callId = normalizeOptionalString(params?.callId) ?? "";
|
|
541
|
-
if (!callId) {
|
|
542
|
-
respondError(respond, "callId required", ErrorCodes.INVALID_REQUEST);
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
const rt = await ensureRuntime();
|
|
546
|
-
const result = await rt.manager.endCall(callId);
|
|
547
|
-
if (!result.success) {
|
|
548
|
-
respondError(respond, result.error || "end failed");
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
respond(true, { success: true });
|
|
552
|
-
} catch (err) {
|
|
553
|
-
sendError(respond, err);
|
|
554
|
-
}
|
|
555
|
-
},
|
|
556
|
-
);
|
|
557
|
-
|
|
558
|
-
api.registerGatewayMethod(
|
|
559
|
-
"voicecall.status",
|
|
560
|
-
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
561
|
-
try {
|
|
562
|
-
const raw =
|
|
563
|
-
normalizeOptionalString(params?.callId) ?? normalizeOptionalString(params?.sid) ?? "";
|
|
564
|
-
const rt = await ensureRuntime();
|
|
565
|
-
if (!raw) {
|
|
566
|
-
respond(true, { found: true, calls: rt.manager.getActiveCalls() });
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
const call = rt.manager.getCall(raw) || rt.manager.getCallByProviderCallId(raw);
|
|
570
|
-
if (!call) {
|
|
571
|
-
respond(true, { found: false });
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
respond(true, { found: true, call });
|
|
575
|
-
} catch (err) {
|
|
576
|
-
sendError(respond, err);
|
|
577
|
-
}
|
|
578
|
-
},
|
|
579
|
-
);
|
|
580
|
-
|
|
581
|
-
api.registerGatewayMethod(
|
|
582
|
-
"voicecall.start",
|
|
583
|
-
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
584
|
-
try {
|
|
585
|
-
const to = normalizeOptionalString(params?.to) ?? "";
|
|
586
|
-
const message = normalizeOptionalString(params?.message) ?? "";
|
|
587
|
-
const dtmfSequence = normalizeOptionalString(params?.dtmfSequence);
|
|
588
|
-
if (!to) {
|
|
589
|
-
respondError(respond, "to required", ErrorCodes.INVALID_REQUEST);
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
const rt = await ensureRuntime();
|
|
593
|
-
const mode =
|
|
594
|
-
params?.mode === "notify" || params?.mode === "conversation" ? params.mode : undefined;
|
|
595
|
-
await initiateCallAndRespond({
|
|
596
|
-
rt,
|
|
597
|
-
respond,
|
|
598
|
-
to,
|
|
599
|
-
message: message || undefined,
|
|
600
|
-
mode,
|
|
601
|
-
dtmfSequence,
|
|
602
|
-
});
|
|
603
|
-
} catch (err) {
|
|
604
|
-
sendError(respond, err);
|
|
605
|
-
}
|
|
606
|
-
},
|
|
607
|
-
);
|
|
608
|
-
|
|
609
|
-
api.registerTool({
|
|
610
|
-
name: "voice_call",
|
|
611
|
-
label: "Voice Call",
|
|
612
|
-
description: "Make phone calls and have voice conversations via the voice-call plugin.",
|
|
613
|
-
parameters: VoiceCallToolSchema,
|
|
614
|
-
async execute(_toolCallId, params) {
|
|
615
|
-
const rawParams = asParamRecord(params);
|
|
616
|
-
const json = (payload: unknown) => ({
|
|
617
|
-
content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
|
|
618
|
-
details: payload,
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
try {
|
|
622
|
-
const rt = await ensureRuntime();
|
|
623
|
-
|
|
624
|
-
if (typeof rawParams.action === "string") {
|
|
625
|
-
switch (rawParams.action) {
|
|
626
|
-
case "initiate_call": {
|
|
627
|
-
const message = normalizeOptionalString(rawParams.message) ?? "";
|
|
628
|
-
if (!message) {
|
|
629
|
-
throw new Error("message required");
|
|
630
|
-
}
|
|
631
|
-
const to = normalizeOptionalString(rawParams.to) ?? rt.config.toNumber;
|
|
632
|
-
if (!to) {
|
|
633
|
-
throw new Error("to required");
|
|
634
|
-
}
|
|
635
|
-
const result = await rt.manager.initiateCall(to, undefined, {
|
|
636
|
-
message,
|
|
637
|
-
dtmfSequence: normalizeOptionalString(rawParams.dtmfSequence),
|
|
638
|
-
mode:
|
|
639
|
-
rawParams.mode === "notify" || rawParams.mode === "conversation"
|
|
640
|
-
? rawParams.mode
|
|
641
|
-
: undefined,
|
|
642
|
-
});
|
|
643
|
-
if (!result.success) {
|
|
644
|
-
throw new Error(result.error || "initiate failed");
|
|
645
|
-
}
|
|
646
|
-
return json({ callId: result.callId, initiated: true });
|
|
647
|
-
}
|
|
648
|
-
case "continue_call": {
|
|
649
|
-
const callId = normalizeOptionalString(rawParams.callId) ?? "";
|
|
650
|
-
const message = normalizeOptionalString(rawParams.message) ?? "";
|
|
651
|
-
if (!callId || !message) {
|
|
652
|
-
throw new Error("callId and message required");
|
|
653
|
-
}
|
|
654
|
-
const result = await rt.manager.continueCall(callId, message);
|
|
655
|
-
if (!result.success) {
|
|
656
|
-
throw new Error(result.error || "continue failed");
|
|
657
|
-
}
|
|
658
|
-
return json({ success: true, transcript: result.transcript });
|
|
659
|
-
}
|
|
660
|
-
case "speak_to_user": {
|
|
661
|
-
const callId = normalizeOptionalString(rawParams.callId) ?? "";
|
|
662
|
-
const message = normalizeOptionalString(rawParams.message) ?? "";
|
|
663
|
-
if (!callId || !message) {
|
|
664
|
-
throw new Error("callId and message required");
|
|
665
|
-
}
|
|
666
|
-
const result = await rt.manager.speak(callId, message);
|
|
667
|
-
if (!result.success) {
|
|
668
|
-
throw new Error(result.error || "speak failed");
|
|
669
|
-
}
|
|
670
|
-
return json({ success: true });
|
|
671
|
-
}
|
|
672
|
-
case "send_dtmf": {
|
|
673
|
-
const callId = normalizeOptionalString(rawParams.callId) ?? "";
|
|
674
|
-
const digits = normalizeOptionalString(rawParams.digits) ?? "";
|
|
675
|
-
if (!callId || !digits) {
|
|
676
|
-
throw new Error("callId and digits required");
|
|
677
|
-
}
|
|
678
|
-
const result = await rt.manager.sendDtmf(callId, digits);
|
|
679
|
-
if (!result.success) {
|
|
680
|
-
throw new Error(result.error || "dtmf failed");
|
|
681
|
-
}
|
|
682
|
-
return json({ success: true });
|
|
683
|
-
}
|
|
684
|
-
case "end_call": {
|
|
685
|
-
const callId = normalizeOptionalString(rawParams.callId) ?? "";
|
|
686
|
-
if (!callId) {
|
|
687
|
-
throw new Error("callId required");
|
|
688
|
-
}
|
|
689
|
-
const result = await rt.manager.endCall(callId);
|
|
690
|
-
if (!result.success) {
|
|
691
|
-
throw new Error(result.error || "end failed");
|
|
692
|
-
}
|
|
693
|
-
return json({ success: true });
|
|
694
|
-
}
|
|
695
|
-
case "get_status": {
|
|
696
|
-
const callId = normalizeOptionalString(rawParams.callId) ?? "";
|
|
697
|
-
if (!callId) {
|
|
698
|
-
throw new Error("callId required");
|
|
699
|
-
}
|
|
700
|
-
const call =
|
|
701
|
-
rt.manager.getCall(callId) || rt.manager.getCallByProviderCallId(callId);
|
|
702
|
-
return json(call ? { found: true, call } : { found: false });
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
const mode = rawParams.mode ?? "call";
|
|
708
|
-
if (mode === "status") {
|
|
709
|
-
const sid = normalizeOptionalString(rawParams.sid) ?? "";
|
|
710
|
-
if (!sid) {
|
|
711
|
-
throw new Error("sid required for status");
|
|
712
|
-
}
|
|
713
|
-
const call = rt.manager.getCall(sid) || rt.manager.getCallByProviderCallId(sid);
|
|
714
|
-
return json(call ? { found: true, call } : { found: false });
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const to = normalizeOptionalString(rawParams.to) ?? rt.config.toNumber;
|
|
718
|
-
if (!to) {
|
|
719
|
-
throw new Error("to required for call");
|
|
720
|
-
}
|
|
721
|
-
const result = await rt.manager.initiateCall(to, undefined, {
|
|
722
|
-
dtmfSequence: normalizeOptionalString(rawParams.dtmfSequence),
|
|
723
|
-
message: normalizeOptionalString(rawParams.message),
|
|
724
|
-
});
|
|
725
|
-
if (!result.success) {
|
|
726
|
-
throw new Error(result.error || "initiate failed");
|
|
727
|
-
}
|
|
728
|
-
return json({ callId: result.callId, initiated: true });
|
|
729
|
-
} catch (err) {
|
|
730
|
-
return json({
|
|
731
|
-
error: formatErrorMessage(err),
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
},
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
api.registerCli(
|
|
738
|
-
({ program }) =>
|
|
739
|
-
registerVoiceCallCli({
|
|
740
|
-
program,
|
|
741
|
-
config,
|
|
742
|
-
ensureRuntime,
|
|
743
|
-
logger: api.logger,
|
|
744
|
-
}),
|
|
745
|
-
{ commands: ["voicecall"] },
|
|
746
|
-
);
|
|
747
|
-
|
|
748
|
-
api.registerService({
|
|
749
|
-
id: "voicecall",
|
|
750
|
-
start: () => {
|
|
751
|
-
if (isCliOnlyProcess()) {
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
754
|
-
if (!config.enabled) {
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
if (!validation.valid) {
|
|
758
|
-
api.logger.warn(
|
|
759
|
-
`[voice-call] Runtime not started; setup incomplete: ${validation.errors.join("; ")}`,
|
|
760
|
-
);
|
|
761
|
-
return;
|
|
762
|
-
}
|
|
763
|
-
void ensureRuntime().catch((err) => {
|
|
764
|
-
api.logger.error(`[voice-call] Failed to start runtime: ${formatErrorMessage(err)}`);
|
|
765
|
-
});
|
|
766
|
-
},
|
|
767
|
-
stop: async () => {
|
|
768
|
-
if (runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY]) {
|
|
769
|
-
await runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY];
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
const runtime = runtimeState[VOICE_CALL_RUNTIME_KEY];
|
|
773
|
-
const runtimePromise = runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY];
|
|
774
|
-
if (!runtime && !runtimePromise) {
|
|
775
|
-
return;
|
|
776
|
-
}
|
|
777
|
-
runtimeState[VOICE_CALL_RUNTIME_KEY] = null;
|
|
778
|
-
runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] = null;
|
|
779
|
-
const stopPromise = (async () => {
|
|
780
|
-
const rt = runtime ?? (await runtimePromise!);
|
|
781
|
-
await rt.stop();
|
|
782
|
-
})();
|
|
783
|
-
runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY] = stopPromise;
|
|
784
|
-
try {
|
|
785
|
-
await stopPromise;
|
|
786
|
-
} finally {
|
|
787
|
-
if (runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY] === stopPromise) {
|
|
788
|
-
runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY] = null;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
},
|
|
792
|
-
});
|
|
793
|
-
},
|
|
794
|
-
});
|