@openclaw/voice-call 2026.6.2-beta.1 → 2026.6.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/dist/{config-compat-BLdRz9GR.js → config-compat-BPPFhsJ7.js} +9 -1
- package/dist/{config-BKyRNKHF.js → config-jaA6vOZ2.js} +2 -0
- package/dist/doctor-contract-api.js +13 -1
- package/dist/{guarded-json-api-tyxHyrev.js → guarded-json-api-C5KNhYC-.js} +2 -1
- package/dist/index.js +5 -4
- package/dist/{plivo-DeSZeJ0S.js → plivo-29Dvun7P.js} +2 -2
- package/dist/{realtime-handler-BP6_qBNe.js → realtime-handler-ChN5De4P.js} +36 -0
- package/dist/{response-generator-D6UsHyfH.js → response-generator-DjsoOMEg.js} +2 -2
- package/dist/{runtime-entry-13O0Ki99.js → runtime-entry-GVY8iEzX.js} +52 -7
- package/dist/runtime-entry.js +1 -1
- package/dist/setup-api.js +3 -1
- package/dist/{store-XuAPgOJG.js → store-8M2m4Isq.js} +25 -0
- package/dist/{telnyx-68EYJwaz.js → telnyx-Gm8Z1pUt.js} +1 -1
- package/dist/{twilio-D0dUSGam.js → twilio-BuFmgABx.js} +10 -3
- package/npm-shrinkwrap.json +3 -3
- package/package.json +4 -4
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import { t as VoiceCallConfigSchema } from "./config-
|
|
1
|
+
import { t as VoiceCallConfigSchema } from "./config-jaA6vOZ2.js";
|
|
2
2
|
import { asOptionalRecord, readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
3
|
//#region extensions/voice-call/src/config-compat.ts
|
|
4
|
+
/** Version where legacy voice-call config shape support is removed. */
|
|
4
5
|
const VOICE_CALL_LEGACY_CONFIG_REMOVAL_VERSION = "2026.6.0";
|
|
5
6
|
const asObject = asOptionalRecord;
|
|
6
7
|
const getString = readStringField;
|
|
8
|
+
/** Read finite numeric config values. */
|
|
7
9
|
function getNumber(obj, key) {
|
|
8
10
|
const value = obj?.[key];
|
|
9
11
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
10
12
|
}
|
|
13
|
+
/** Merge legacy provider-specific values into the canonical providers map. */
|
|
11
14
|
function mergeProviderConfig(providersValue, providerId, compatValues) {
|
|
12
15
|
if (Object.keys(compatValues).length === 0) return asObject(providersValue);
|
|
13
16
|
const providers = asObject(providersValue) ?? {};
|
|
@@ -20,6 +23,7 @@ function mergeProviderConfig(providersValue, providerId, compatValues) {
|
|
|
20
23
|
}
|
|
21
24
|
};
|
|
22
25
|
}
|
|
26
|
+
/** Collect legacy voice-call config keys that should be migrated. */
|
|
23
27
|
function collectVoiceCallLegacyConfigIssues(value) {
|
|
24
28
|
const raw = asObject(value) ?? {};
|
|
25
29
|
const realtimeAgentContext = asObject(asObject(raw.realtime)?.agentContext);
|
|
@@ -68,11 +72,13 @@ function collectVoiceCallLegacyConfigIssues(value) {
|
|
|
68
72
|
});
|
|
69
73
|
return issues;
|
|
70
74
|
}
|
|
75
|
+
/** Format runtime warnings for legacy voice-call config keys. */
|
|
71
76
|
function formatVoiceCallLegacyConfigWarnings(params) {
|
|
72
77
|
const issues = collectVoiceCallLegacyConfigIssues(params.value);
|
|
73
78
|
if (issues.length === 0) return [];
|
|
74
79
|
return [`[voice-call] legacy config keys detected under ${params.configPathPrefix}; runtime loading will not rewrite them, and support for the legacy shape will be removed in ${VOICE_CALL_LEGACY_CONFIG_REMOVAL_VERSION}. Run "${params.doctorFixCommand}".`, ...issues.map((issue) => `[voice-call] ${params.configPathPrefix}.${issue.path}: ${issue.message}`)];
|
|
75
80
|
}
|
|
81
|
+
/** Migrate legacy voice-call config input to the current canonical shape. */
|
|
76
82
|
function migrateVoiceCallLegacyConfigInput(params) {
|
|
77
83
|
const raw = asObject(params.value) ?? {};
|
|
78
84
|
const realtime = asObject(raw.realtime);
|
|
@@ -137,9 +143,11 @@ function migrateVoiceCallLegacyConfigInput(params) {
|
|
|
137
143
|
issues
|
|
138
144
|
};
|
|
139
145
|
}
|
|
146
|
+
/** Normalize legacy voice-call config input without returning migration metadata. */
|
|
140
147
|
function normalizeVoiceCallLegacyConfigInput(value) {
|
|
141
148
|
return migrateVoiceCallLegacyConfigInput({ value }).config;
|
|
142
149
|
}
|
|
150
|
+
/** Parse voice-call plugin config after applying legacy normalization. */
|
|
143
151
|
function parseVoiceCallPluginConfig(value) {
|
|
144
152
|
return VoiceCallConfigSchema.parse(normalizeVoiceCallLegacyConfigInput(value));
|
|
145
153
|
}
|
|
@@ -10,6 +10,7 @@ const BLOCKED_MERGE_KEYS = new Set([
|
|
|
10
10
|
"prototype",
|
|
11
11
|
"constructor"
|
|
12
12
|
]);
|
|
13
|
+
/** Deep-merge plain objects, keeping base values when overrides are undefined. */
|
|
13
14
|
function deepMergeDefined(base, override) {
|
|
14
15
|
if (!isRecord(base) || !isRecord(override)) return override === void 0 ? base : override;
|
|
15
16
|
const result = { ...base };
|
|
@@ -22,6 +23,7 @@ function deepMergeDefined(base, override) {
|
|
|
22
23
|
}
|
|
23
24
|
//#endregion
|
|
24
25
|
//#region extensions/voice-call/src/realtime-defaults.ts
|
|
26
|
+
/** Baseline instructions that keep realtime calls brief and route deep work to agent consult. */
|
|
25
27
|
const DEFAULT_VOICE_CALL_REALTIME_INSTRUCTIONS = `You are OpenClaw's phone-call realtime voice interface. Keep spoken replies brief and natural. When a question needs deeper reasoning, current information, or tools, call ${REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME} before answering.`;
|
|
26
28
|
//#endregion
|
|
27
29
|
//#region extensions/voice-call/src/config.ts
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import { a as MAX_CALL_RECORD_EVENTS, f as prepareVoiceCallRecordForStorage, i as CALL_RECORD_EVENT_META_MAX_ENTRIES, n as CALL_RECORD_EVENTS_NAMESPACE, o as RAW_CALL_RECORD_CHUNK_BYTES, p as resolveVoiceCallLegacyCallLogPath, r as CALL_RECORD_EVENT_CHUNKS_NAMESPACE, s as buildVoiceCallLegacyJsonlEventKey, t as CALL_RECORD_CHUNK_MAX_ENTRIES, u as parseVoiceCallRecordLine } from "./store-
|
|
1
|
+
import { a as MAX_CALL_RECORD_EVENTS, f as prepareVoiceCallRecordForStorage, i as CALL_RECORD_EVENT_META_MAX_ENTRIES, n as CALL_RECORD_EVENTS_NAMESPACE, o as RAW_CALL_RECORD_CHUNK_BYTES, p as resolveVoiceCallLegacyCallLogPath, r as CALL_RECORD_EVENT_CHUNKS_NAMESPACE, s as buildVoiceCallLegacyJsonlEventKey, t as CALL_RECORD_CHUNK_MAX_ENTRIES, u as parseVoiceCallRecordLine } from "./store-8M2m4Isq.js";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
//#region extensions/voice-call/doctor-contract-api.ts
|
|
6
|
+
/** Resolve home from doctor env with OS fallback. */
|
|
6
7
|
function resolveHome(env) {
|
|
7
8
|
return env.HOME?.trim() || os.homedir();
|
|
8
9
|
}
|
|
10
|
+
/** Resolve config paths, including "~", against the doctor env home. */
|
|
9
11
|
function resolveUserPath(input, env) {
|
|
10
12
|
const trimmed = input.trim();
|
|
11
13
|
if (!trimmed) return trimmed;
|
|
12
14
|
if (trimmed.startsWith("~")) return path.resolve(trimmed.replace(/^~(?=$|[\\/])/, resolveHome(env)));
|
|
13
15
|
return path.resolve(trimmed);
|
|
14
16
|
}
|
|
17
|
+
/** Read the configured voice-call store path from either package id. */
|
|
15
18
|
function getVoiceCallConfigStore(config) {
|
|
16
19
|
for (const pluginId of ["voice-call", "@openclaw/voice-call"]) {
|
|
17
20
|
const rawConfig = config.plugins?.entries?.[pluginId]?.config;
|
|
@@ -21,11 +24,13 @@ function getVoiceCallConfigStore(config) {
|
|
|
21
24
|
}
|
|
22
25
|
return "";
|
|
23
26
|
}
|
|
27
|
+
/** Resolve the voice-call store path used by legacy and plugin-state call records. */
|
|
24
28
|
function resolveVoiceCallStorePath(params) {
|
|
25
29
|
const configuredStore = getVoiceCallConfigStore(params.config);
|
|
26
30
|
if (configuredStore) return resolveUserPath(configuredStore, params.env);
|
|
27
31
|
return path.join(resolveHome(params.env), ".openclaw", "voice-calls");
|
|
28
32
|
}
|
|
33
|
+
/** Return true when a path exists and is a file. */
|
|
29
34
|
async function fileExists(filePath) {
|
|
30
35
|
try {
|
|
31
36
|
return (await fs.stat(filePath)).isFile();
|
|
@@ -33,9 +38,11 @@ async function fileExists(filePath) {
|
|
|
33
38
|
return false;
|
|
34
39
|
}
|
|
35
40
|
}
|
|
41
|
+
/** Build the plugin state key for one migrated event chunk. */
|
|
36
42
|
function buildChunkKey(eventKey, index) {
|
|
37
43
|
return `${eventKey}:chunk:${String(index).padStart(4, "0")}`;
|
|
38
44
|
}
|
|
45
|
+
/** Chunk a prepared call record into bounded plugin state rows. */
|
|
39
46
|
function prepareChunks(call) {
|
|
40
47
|
const serialized = JSON.stringify(prepareVoiceCallRecordForStorage(call));
|
|
41
48
|
const buffer = Buffer.from(serialized, "utf8");
|
|
@@ -57,6 +64,7 @@ function prepareChunks(call) {
|
|
|
57
64
|
}
|
|
58
65
|
};
|
|
59
66
|
}
|
|
67
|
+
/** Read and prepare legacy JSONL call records, collecting line-level warnings. */
|
|
60
68
|
async function readLegacyCallRecords(filePath) {
|
|
61
69
|
let content;
|
|
62
70
|
try {
|
|
@@ -99,6 +107,7 @@ async function readLegacyCallRecords(filePath) {
|
|
|
99
107
|
warnings
|
|
100
108
|
};
|
|
101
109
|
}
|
|
110
|
+
/** Archive the legacy JSONL source after a complete migration. */
|
|
102
111
|
async function archiveLegacySource(params) {
|
|
103
112
|
const archivedPath = `${params.filePath}.migrated`;
|
|
104
113
|
if (await fileExists(archivedPath)) {
|
|
@@ -112,6 +121,7 @@ async function archiveLegacySource(params) {
|
|
|
112
121
|
params.warnings.push(`Failed archiving Voice Call call-log legacy source: ${String(err)}`);
|
|
113
122
|
}
|
|
114
123
|
}
|
|
124
|
+
/** Select newest missing records that fit remaining plugin state capacity. */
|
|
115
125
|
async function selectEntriesForImport(params) {
|
|
116
126
|
const existingEventKeys = new Set((await params.eventStore.entries()).map((entry) => entry.key));
|
|
117
127
|
const missingEntries = params.entries.filter((entry) => !existingEventKeys.has(entry.eventKey));
|
|
@@ -135,6 +145,7 @@ async function selectEntriesForImport(params) {
|
|
|
135
145
|
entries: selected.toReversed()
|
|
136
146
|
};
|
|
137
147
|
}
|
|
148
|
+
/** Import prepared legacy call records into plugin state. */
|
|
138
149
|
async function importLegacyCallRecords(params) {
|
|
139
150
|
const selected = await selectEntriesForImport(params);
|
|
140
151
|
let imported = 0;
|
|
@@ -151,6 +162,7 @@ async function importLegacyCallRecords(params) {
|
|
|
151
162
|
}
|
|
152
163
|
return imported;
|
|
153
164
|
}
|
|
165
|
+
/** Doctor migrations owned by the voice-call plugin. */
|
|
154
166
|
const stateMigrations = [{
|
|
155
167
|
id: "voice-call-calls-jsonl-to-plugin-state",
|
|
156
168
|
label: "Voice Call call log",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import { a as getHeader } from "./runtime-entry-
|
|
3
|
+
import { a as getHeader } from "./runtime-entry-GVY8iEzX.js";
|
|
4
4
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
5
5
|
import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
|
|
6
6
|
import { isFutureDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
|
|
@@ -561,6 +561,7 @@ function verifyPlivoWebhook(ctx, authToken, options) {
|
|
|
561
561
|
}
|
|
562
562
|
//#endregion
|
|
563
563
|
//#region extensions/voice-call/src/providers/shared/guarded-json-api.ts
|
|
564
|
+
/** Send a provider JSON request through the SSRF guard and parse bounded JSON responses. */
|
|
564
565
|
async function guardedJsonApiRequest(params) {
|
|
565
566
|
const { response, release } = await fetchWithSsrFGuard({
|
|
566
567
|
url: params.url,
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { definePluginEntry, sleep } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import { i as resolveVoiceCallConfig, s as validateProviderConfig } from "./config-
|
|
4
|
-
import { c as getTailscaleSelfInfo, l as setupTailscaleExposureRoute, o as resolveWebhookExposureStatus, p as resolveUserPath, s as cleanupTailscaleExposureRoute, t as createVoiceCallRuntime } from "./runtime-entry-
|
|
5
|
-
import { c as getCallHistoryFromStore, m as setVoiceCallStateRuntime } from "./store-
|
|
6
|
-
import { i as parseVoiceCallPluginConfig, r as normalizeVoiceCallLegacyConfigInput, t as formatVoiceCallLegacyConfigWarnings } from "./config-compat-
|
|
3
|
+
import { i as resolveVoiceCallConfig, s as validateProviderConfig } from "./config-jaA6vOZ2.js";
|
|
4
|
+
import { c as getTailscaleSelfInfo, l as setupTailscaleExposureRoute, o as resolveWebhookExposureStatus, p as resolveUserPath, s as cleanupTailscaleExposureRoute, t as createVoiceCallRuntime } from "./runtime-entry-GVY8iEzX.js";
|
|
5
|
+
import { c as getCallHistoryFromStore, m as setVoiceCallStateRuntime } from "./store-8M2m4Isq.js";
|
|
6
|
+
import { i as parseVoiceCallPluginConfig, r as normalizeVoiceCallLegacyConfigInput, t as formatVoiceCallLegacyConfigWarnings } from "./config-compat-BPPFhsJ7.js";
|
|
7
7
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
8
8
|
import { ErrorCodes, callGatewayFromCli, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
|
|
9
9
|
import { MAX_TCP_PORT, MAX_TIMER_TIMEOUT_MS, addTimerTimeoutGraceMs, clampTimerTimeoutMs, parseStrictNonNegativeInteger, resolveTimerTimeoutMs, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
|
@@ -539,6 +539,7 @@ function registerVoiceCallCli(params) {
|
|
|
539
539
|
//#region extensions/voice-call/src/gateway-continue-operation.ts
|
|
540
540
|
const VOICE_CALL_CONTINUE_OPERATION_BUFFER_MS = 3e4;
|
|
541
541
|
const VOICE_CALL_CONTINUE_OPERATION_CLEANUP_MS = 300 * 1e3;
|
|
542
|
+
/** Create a process-local operation store for gateway continue-call polling. */
|
|
542
543
|
function createVoiceCallContinueOperationStore(params) {
|
|
543
544
|
const operations = /* @__PURE__ */ new Map();
|
|
544
545
|
const resolvePollTimeoutMs = (rt) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as getHeader, m as escapeXml } from "./runtime-entry-
|
|
2
|
-
import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
1
|
+
import { a as getHeader, m as escapeXml } from "./runtime-entry-GVY8iEzX.js";
|
|
2
|
+
import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-C5KNhYC-.js";
|
|
3
3
|
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
//#region extensions/voice-call/src/providers/plivo.ts
|
|
@@ -16,6 +16,7 @@ const DEFAULT_MAX_QUEUED_AUDIO_BYTES = TELEPHONY_SAMPLE_RATE * 120;
|
|
|
16
16
|
const PCM16_MAX_AMPLITUDE = 32768;
|
|
17
17
|
const MULAW_LINEAR_SAMPLES = new Int16Array(256);
|
|
18
18
|
for (let i = 0; i < MULAW_LINEAR_SAMPLES.length; i += 1) MULAW_LINEAR_SAMPLES[i] = decodeMulawSample(i);
|
|
19
|
+
/** Paces outgoing mulaw audio frames at telephony cadence. */
|
|
19
20
|
var RealtimeAudioPacer = class {
|
|
20
21
|
constructor(params) {
|
|
21
22
|
this.params = params;
|
|
@@ -24,6 +25,7 @@ var RealtimeAudioPacer = class {
|
|
|
24
25
|
this.queuedAudioBytes = 0;
|
|
25
26
|
this.closed = false;
|
|
26
27
|
}
|
|
28
|
+
/** Queue mulaw audio and split it into 20ms-ish telephony chunks. */
|
|
27
29
|
sendAudio(muLaw) {
|
|
28
30
|
if (this.closed || muLaw.length === 0) return;
|
|
29
31
|
const maxQueuedAudioBytes = this.params.maxQueuedAudioBytes ?? DEFAULT_MAX_QUEUED_AUDIO_BYTES;
|
|
@@ -42,6 +44,7 @@ var RealtimeAudioPacer = class {
|
|
|
42
44
|
}
|
|
43
45
|
this.ensurePump();
|
|
44
46
|
}
|
|
47
|
+
/** Queue a provider mark frame after prior audio frames. */
|
|
45
48
|
sendMark(name) {
|
|
46
49
|
if (this.closed || !name) return;
|
|
47
50
|
this.queue.push({
|
|
@@ -50,6 +53,7 @@ var RealtimeAudioPacer = class {
|
|
|
50
53
|
});
|
|
51
54
|
this.ensurePump();
|
|
52
55
|
}
|
|
56
|
+
/** Clear queued audio and notify the provider stream. */
|
|
53
57
|
clearAudio() {
|
|
54
58
|
if (this.closed) return 0;
|
|
55
59
|
const clearedAudioBytes = this.queuedAudioBytes;
|
|
@@ -59,24 +63,29 @@ var RealtimeAudioPacer = class {
|
|
|
59
63
|
this.params.send(this.params.serializer.clear());
|
|
60
64
|
return clearedAudioBytes;
|
|
61
65
|
}
|
|
66
|
+
/** Stop sending and discard queued frames. */
|
|
62
67
|
close() {
|
|
63
68
|
this.closed = true;
|
|
64
69
|
this.clearTimer();
|
|
65
70
|
this.queue = [];
|
|
66
71
|
this.queuedAudioBytes = 0;
|
|
67
72
|
}
|
|
73
|
+
/** Clear the scheduled pump timer. */
|
|
68
74
|
clearTimer() {
|
|
69
75
|
if (!this.timer) return;
|
|
70
76
|
clearTimeout(this.timer);
|
|
71
77
|
this.timer = null;
|
|
72
78
|
}
|
|
79
|
+
/** Start the pump when queued work exists and no timer is active. */
|
|
73
80
|
ensurePump() {
|
|
74
81
|
if (!this.timer) this.pump();
|
|
75
82
|
}
|
|
83
|
+
/** Close the pacer and notify the caller about queued-audio backpressure. */
|
|
76
84
|
failBackpressure() {
|
|
77
85
|
this.close();
|
|
78
86
|
this.params.onBackpressure?.();
|
|
79
87
|
}
|
|
88
|
+
/** Send one queued item and schedule the next send based on audio duration. */
|
|
80
89
|
pump() {
|
|
81
90
|
this.timer = null;
|
|
82
91
|
if (this.closed) return;
|
|
@@ -97,6 +106,7 @@ var RealtimeAudioPacer = class {
|
|
|
97
106
|
if (this.queue.length > 0) this.timer = setTimeout(() => this.pump(), delayMs);
|
|
98
107
|
}
|
|
99
108
|
};
|
|
109
|
+
/** Calculate normalized RMS from mulaw bytes. */
|
|
100
110
|
function calculateMulawRms(muLaw) {
|
|
101
111
|
if (muLaw.length === 0) return 0;
|
|
102
112
|
let sum = 0;
|
|
@@ -106,6 +116,7 @@ function calculateMulawRms(muLaw) {
|
|
|
106
116
|
}
|
|
107
117
|
return Math.sqrt(sum / muLaw.length);
|
|
108
118
|
}
|
|
119
|
+
/** Detect likely speech start from consecutive loud mulaw chunks. */
|
|
109
120
|
var RealtimeMulawSpeechStartDetector = class {
|
|
110
121
|
constructor(params = {}) {
|
|
111
122
|
this.params = params;
|
|
@@ -113,6 +124,7 @@ var RealtimeMulawSpeechStartDetector = class {
|
|
|
113
124
|
this.quietChunks = DEFAULT_REQUIRED_QUIET_CHUNKS;
|
|
114
125
|
this.speaking = false;
|
|
115
126
|
}
|
|
127
|
+
/** Accept one mulaw chunk and return true only on transition into speaking. */
|
|
116
128
|
accept(muLaw) {
|
|
117
129
|
if (calculateMulawRms(muLaw) >= (this.params.rmsThreshold ?? DEFAULT_SPEECH_RMS_THRESHOLD)) {
|
|
118
130
|
this.quietChunks = 0;
|
|
@@ -131,6 +143,7 @@ var RealtimeMulawSpeechStartDetector = class {
|
|
|
131
143
|
return false;
|
|
132
144
|
}
|
|
133
145
|
};
|
|
146
|
+
/** Decode one G.711 mulaw byte to a linear PCM sample. */
|
|
134
147
|
function decodeMulawSample(value) {
|
|
135
148
|
const muLaw = ~value & 255;
|
|
136
149
|
const sign = muLaw & 128;
|
|
@@ -141,6 +154,7 @@ function decodeMulawSample(value) {
|
|
|
141
154
|
}
|
|
142
155
|
//#endregion
|
|
143
156
|
//#region extensions/voice-call/src/webhook/stream-frame-adapter.ts
|
|
157
|
+
/** Parse numeric timestamps sent as numbers or integer strings. */
|
|
144
158
|
function parseTimestampMs(value) {
|
|
145
159
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
146
160
|
if (typeof value === "string" && /^[+-]?\d+$/.test(value.trim())) {
|
|
@@ -148,6 +162,7 @@ function parseTimestampMs(value) {
|
|
|
148
162
|
return Number.isSafeInteger(parsed) ? parsed : void 0;
|
|
149
163
|
}
|
|
150
164
|
}
|
|
165
|
+
/** Parse a JSON object frame, returning null for invalid or non-object payloads. */
|
|
151
166
|
function tryParseJson(rawMessage) {
|
|
152
167
|
try {
|
|
153
168
|
const parsed = JSON.parse(rawMessage);
|
|
@@ -155,16 +170,20 @@ function tryParseJson(rawMessage) {
|
|
|
155
170
|
} catch {}
|
|
156
171
|
return null;
|
|
157
172
|
}
|
|
173
|
+
/** Read an object-valued field from a parsed frame. */
|
|
158
174
|
function readRecordField(record, field) {
|
|
159
175
|
const value = record[field];
|
|
160
176
|
return typeof value === "object" && value !== null ? value : void 0;
|
|
161
177
|
}
|
|
178
|
+
/** Normalize base64/base64url padding differences for validation. */
|
|
162
179
|
function normalizeBase64ForCompare(value) {
|
|
163
180
|
return value.replace(/=+$/u, "").replace(/-/gu, "+").replace(/_/gu, "/");
|
|
164
181
|
}
|
|
182
|
+
/** Return true when a payload round-trips as base64. */
|
|
165
183
|
function isValidBase64Payload(value) {
|
|
166
184
|
return normalizeBase64ForCompare(Buffer.from(value, "base64").toString("base64")) === normalizeBase64ForCompare(value);
|
|
167
185
|
}
|
|
186
|
+
/** Parse a common provider media frame. */
|
|
168
187
|
function parseMediaFrame(msg) {
|
|
169
188
|
const mediaData = readRecordField(msg, "media");
|
|
170
189
|
const payload = typeof mediaData?.payload === "string" ? mediaData.payload : void 0;
|
|
@@ -176,6 +195,7 @@ function parseMediaFrame(msg) {
|
|
|
176
195
|
track: typeof mediaData?.track === "string" ? mediaData.track : void 0
|
|
177
196
|
};
|
|
178
197
|
}
|
|
198
|
+
/** Parse a common provider mark frame. */
|
|
179
199
|
function parseMarkFrame(msg) {
|
|
180
200
|
const markData = readRecordField(msg, "mark");
|
|
181
201
|
return {
|
|
@@ -183,11 +203,13 @@ function parseMarkFrame(msg) {
|
|
|
183
203
|
name: typeof markData?.name === "string" ? markData.name : void 0
|
|
184
204
|
};
|
|
185
205
|
}
|
|
206
|
+
/** Parse common media, mark, and stop frames shared by supported providers. */
|
|
186
207
|
function parseCommonInboundFrame(event, msg) {
|
|
187
208
|
if (event === "media") return parseMediaFrame(msg);
|
|
188
209
|
if (event === "mark") return parseMarkFrame(msg);
|
|
189
210
|
if (event === "stop") return { kind: "stop" };
|
|
190
211
|
}
|
|
212
|
+
/** Parse one provider frame with provider-specific start/error hooks. */
|
|
191
213
|
function parseProviderInboundFrame(rawMessage, parseStartFrame, parseExtraFrame) {
|
|
192
214
|
const msg = tryParseJson(rawMessage);
|
|
193
215
|
if (!msg) return { kind: "ignored" };
|
|
@@ -195,9 +217,11 @@ function parseProviderInboundFrame(rawMessage, parseStartFrame, parseExtraFrame)
|
|
|
195
217
|
if (event === "start") return parseStartFrame(msg) ?? { kind: "ignored" };
|
|
196
218
|
return parseCommonInboundFrame(event, msg) ?? parseExtraFrame?.(event, msg) ?? { kind: "ignored" };
|
|
197
219
|
}
|
|
220
|
+
/** Include streamSid only when Twilio has already supplied one. */
|
|
198
221
|
function withOptionalStreamSid(streamSid) {
|
|
199
222
|
return streamSid === void 0 ? {} : { streamSid };
|
|
200
223
|
}
|
|
224
|
+
/** Serialize a provider media frame. */
|
|
201
225
|
function serializeMediaFrame(payloadBase64, streamSid) {
|
|
202
226
|
return JSON.stringify({
|
|
203
227
|
event: "media",
|
|
@@ -205,12 +229,14 @@ function serializeMediaFrame(payloadBase64, streamSid) {
|
|
|
205
229
|
media: { payload: payloadBase64 }
|
|
206
230
|
});
|
|
207
231
|
}
|
|
232
|
+
/** Serialize a provider clear frame. */
|
|
208
233
|
function serializeClearFrame(streamSid) {
|
|
209
234
|
return JSON.stringify({
|
|
210
235
|
event: "clear",
|
|
211
236
|
...withOptionalStreamSid(streamSid)
|
|
212
237
|
});
|
|
213
238
|
}
|
|
239
|
+
/** Serialize a provider mark frame. */
|
|
214
240
|
function serializeMarkFrame(name, streamSid) {
|
|
215
241
|
return JSON.stringify({
|
|
216
242
|
event: "mark",
|
|
@@ -218,11 +244,13 @@ function serializeMarkFrame(name, streamSid) {
|
|
|
218
244
|
mark: { name }
|
|
219
245
|
});
|
|
220
246
|
}
|
|
247
|
+
/** Twilio media stream adapter, retaining streamSid for outbound frames. */
|
|
221
248
|
var TwilioStreamFrameAdapter = class {
|
|
222
249
|
constructor() {
|
|
223
250
|
this.providerName = "twilio";
|
|
224
251
|
this.streamSid = "";
|
|
225
252
|
}
|
|
253
|
+
/** Parse one Twilio websocket message into a normalized frame. */
|
|
226
254
|
parseInbound(rawMessage) {
|
|
227
255
|
return parseProviderInboundFrame(rawMessage, (msg) => {
|
|
228
256
|
const startData = readRecordField(msg, "start");
|
|
@@ -237,20 +265,25 @@ var TwilioStreamFrameAdapter = class {
|
|
|
237
265
|
};
|
|
238
266
|
});
|
|
239
267
|
}
|
|
268
|
+
/** Serialize Twilio media with the active streamSid. */
|
|
240
269
|
serializeMedia(payloadBase64) {
|
|
241
270
|
return serializeMediaFrame(payloadBase64, this.streamSid);
|
|
242
271
|
}
|
|
272
|
+
/** Serialize Twilio clear with the active streamSid. */
|
|
243
273
|
serializeClear() {
|
|
244
274
|
return serializeClearFrame(this.streamSid);
|
|
245
275
|
}
|
|
276
|
+
/** Serialize Twilio mark with the active streamSid. */
|
|
246
277
|
serializeMark(name) {
|
|
247
278
|
return serializeMarkFrame(name, this.streamSid);
|
|
248
279
|
}
|
|
249
280
|
};
|
|
281
|
+
/** Telnyx media stream adapter. */
|
|
250
282
|
var TelnyxStreamFrameAdapter = class {
|
|
251
283
|
constructor() {
|
|
252
284
|
this.providerName = "telnyx";
|
|
253
285
|
}
|
|
286
|
+
/** Parse one Telnyx websocket message into a normalized frame. */
|
|
254
287
|
parseInbound(rawMessage) {
|
|
255
288
|
return parseProviderInboundFrame(rawMessage, (msg) => {
|
|
256
289
|
const topLevelStreamId = typeof msg.stream_id === "string" && msg.stream_id ? msg.stream_id : void 0;
|
|
@@ -273,12 +306,15 @@ var TelnyxStreamFrameAdapter = class {
|
|
|
273
306
|
};
|
|
274
307
|
});
|
|
275
308
|
}
|
|
309
|
+
/** Serialize Telnyx media. */
|
|
276
310
|
serializeMedia(payloadBase64) {
|
|
277
311
|
return serializeMediaFrame(payloadBase64);
|
|
278
312
|
}
|
|
313
|
+
/** Serialize Telnyx clear. */
|
|
279
314
|
serializeClear() {
|
|
280
315
|
return serializeClearFrame();
|
|
281
316
|
}
|
|
317
|
+
/** Serialize Telnyx mark. */
|
|
282
318
|
serializeMark(name) {
|
|
283
319
|
return serializeMarkFrame(name);
|
|
284
320
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { o as resolveVoiceCallSessionKey } from "./config-
|
|
2
|
-
import { f as resolveVoiceResponseModel } from "./runtime-entry-
|
|
1
|
+
import { o as resolveVoiceCallSessionKey } from "./config-jaA6vOZ2.js";
|
|
2
|
+
import { f as resolveVoiceResponseModel } from "./runtime-entry-GVY8iEzX.js";
|
|
3
3
|
import { isRecord, normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import { applyModelOverrideToSessionEntry } from "openclaw/plugin-sdk/model-session-runtime";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isBlockedHostnameOrIp, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import { a as resolveVoiceCallEffectiveConfig, c as deepMergeDefined, i as resolveVoiceCallConfig, n as normalizeVoiceCallConfig, o as resolveVoiceCallSessionKey, r as resolveTwilioAuthToken, s as validateProviderConfig } from "./config-
|
|
4
|
-
import { c as getCallHistoryFromStore, d as persistCallRecord, h as TerminalStates, l as loadActiveCallsFromStore, m as setVoiceCallStateRuntime } from "./store-
|
|
3
|
+
import { a as resolveVoiceCallEffectiveConfig, c as deepMergeDefined, i as resolveVoiceCallConfig, n as normalizeVoiceCallConfig, o as resolveVoiceCallSessionKey, r as resolveTwilioAuthToken, s as validateProviderConfig } from "./config-jaA6vOZ2.js";
|
|
4
|
+
import { c as getCallHistoryFromStore, d as persistCallRecord, h as TerminalStates, l as loadActiveCallsFromStore, m as setVoiceCallStateRuntime } from "./store-8M2m4Isq.js";
|
|
5
5
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
6
6
|
import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
|
|
7
7
|
import { MAX_TIMER_TIMEOUT_MS, asDateTimestampMs, resolveExpiresAtMsFromDurationMs, resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
|
|
@@ -20,10 +20,12 @@ import { resolveConfiguredCapabilityProvider } from "openclaw/plugin-sdk/provide
|
|
|
20
20
|
import { WEBHOOK_BODY_READ_DEFAULTS, createWebhookInFlightLimiter } from "openclaw/plugin-sdk/webhook-ingress";
|
|
21
21
|
import { WebSocket as WebSocket$1, WebSocketServer } from "ws";
|
|
22
22
|
//#region extensions/voice-call/src/allowlist.ts
|
|
23
|
+
/** Normalize a phone number to digits only. */
|
|
23
24
|
function normalizePhoneNumber(input) {
|
|
24
25
|
if (!input) return "";
|
|
25
26
|
return input.replace(/\D/g, "");
|
|
26
27
|
}
|
|
28
|
+
/** Return true when the normalized caller exactly matches an allowlist entry. */
|
|
27
29
|
function isAllowlistedCaller(normalizedFrom, allowFrom) {
|
|
28
30
|
if (!normalizedFrom) return false;
|
|
29
31
|
return (allowFrom ?? []).some((num) => {
|
|
@@ -66,16 +68,19 @@ function addTranscriptEntry(call, speaker, text) {
|
|
|
66
68
|
}
|
|
67
69
|
//#endregion
|
|
68
70
|
//#region extensions/voice-call/src/manager/timer-delays.ts
|
|
71
|
+
/** Convert seconds to a safe timeout delay in milliseconds. */
|
|
69
72
|
function resolveVoiceCallSecondsTimerDelayMs(seconds, minMs = 1) {
|
|
70
73
|
if (!Number.isFinite(seconds)) return resolveTimerTimeoutMs(MAX_TIMER_TIMEOUT_MS, MAX_TIMER_TIMEOUT_MS, minMs);
|
|
71
74
|
const timeoutMs = Math.floor(seconds * 1e3);
|
|
72
75
|
return resolveTimerTimeoutMs(Number.isFinite(timeoutMs) ? timeoutMs : MAX_TIMER_TIMEOUT_MS, minMs, minMs);
|
|
73
76
|
}
|
|
77
|
+
/** Normalize a millisecond timeout delay with fallback behavior. */
|
|
74
78
|
function resolveVoiceCallTimerDelayMs(timeoutMs, fallbackMs = 1) {
|
|
75
79
|
return resolveTimerTimeoutMs(timeoutMs, fallbackMs);
|
|
76
80
|
}
|
|
77
81
|
//#endregion
|
|
78
82
|
//#region extensions/voice-call/src/manager/timers.ts
|
|
83
|
+
/** Clear and forget the max-duration timer for a call. */
|
|
79
84
|
function clearMaxDurationTimer(ctx, callId) {
|
|
80
85
|
const timer = ctx.maxDurationTimers.get(callId);
|
|
81
86
|
if (timer) {
|
|
@@ -83,6 +88,7 @@ function clearMaxDurationTimer(ctx, callId) {
|
|
|
83
88
|
ctx.maxDurationTimers.delete(callId);
|
|
84
89
|
}
|
|
85
90
|
}
|
|
91
|
+
/** Start or replace the max-duration timer for a call. */
|
|
86
92
|
function startMaxDurationTimer(params) {
|
|
87
93
|
clearMaxDurationTimer(params.ctx, params.callId);
|
|
88
94
|
const maxDurationMs = params.timeoutMs === void 0 ? resolveVoiceCallSecondsTimerDelayMs(params.ctx.config.maxDurationSeconds) : resolveVoiceCallTimerDelayMs(params.timeoutMs);
|
|
@@ -101,18 +107,21 @@ function startMaxDurationTimer(params) {
|
|
|
101
107
|
}, maxDurationMs);
|
|
102
108
|
params.ctx.maxDurationTimers.set(params.callId, timer);
|
|
103
109
|
}
|
|
110
|
+
/** Clear and forget a pending final-transcript waiter. */
|
|
104
111
|
function clearTranscriptWaiter(ctx, callId) {
|
|
105
112
|
const waiter = ctx.transcriptWaiters.get(callId);
|
|
106
113
|
if (!waiter) return;
|
|
107
114
|
clearTimeout(waiter.timeout);
|
|
108
115
|
ctx.transcriptWaiters.delete(callId);
|
|
109
116
|
}
|
|
117
|
+
/** Reject a pending transcript waiter during call finalization or error paths. */
|
|
110
118
|
function rejectTranscriptWaiter(ctx, callId, reason) {
|
|
111
119
|
const waiter = ctx.transcriptWaiters.get(callId);
|
|
112
120
|
if (!waiter) return;
|
|
113
121
|
clearTranscriptWaiter(ctx, callId);
|
|
114
122
|
waiter.reject(new Error(reason));
|
|
115
123
|
}
|
|
124
|
+
/** Resolve a transcript waiter when the matching turn's final transcript arrives. */
|
|
116
125
|
function resolveTranscriptWaiter(ctx, callId, transcript, turnToken) {
|
|
117
126
|
const waiter = ctx.transcriptWaiters.get(callId);
|
|
118
127
|
if (!waiter) return false;
|
|
@@ -121,6 +130,7 @@ function resolveTranscriptWaiter(ctx, callId, transcript, turnToken) {
|
|
|
121
130
|
waiter.resolve(transcript);
|
|
122
131
|
return true;
|
|
123
132
|
}
|
|
133
|
+
/** Wait for the next final transcript for a call, optionally scoped to a turn token. */
|
|
124
134
|
function waitForFinalTranscript(ctx, callId, turnToken) {
|
|
125
135
|
if (ctx.transcriptWaiters.has(callId)) return Promise.reject(/* @__PURE__ */ new Error("Already waiting for transcript"));
|
|
126
136
|
const timeoutMs = resolveVoiceCallTimerDelayMs(ctx.config.transcriptTimeoutMs);
|
|
@@ -139,10 +149,12 @@ function waitForFinalTranscript(ctx, callId, turnToken) {
|
|
|
139
149
|
}
|
|
140
150
|
//#endregion
|
|
141
151
|
//#region extensions/voice-call/src/manager/lifecycle.ts
|
|
152
|
+
/** Remove a provider-call mapping only when it still points at this call. */
|
|
142
153
|
function removeProviderCallMapping(providerCallIdMap, call) {
|
|
143
154
|
if (!call.providerCallId) return;
|
|
144
155
|
if (providerCallIdMap.get(call.providerCallId) === call.callId) providerCallIdMap.delete(call.providerCallId);
|
|
145
156
|
}
|
|
157
|
+
/** Persist terminal state, clean timers/waiters, and remove active call indexes. */
|
|
146
158
|
function finalizeCall(params) {
|
|
147
159
|
const { ctx, call, endReason } = params;
|
|
148
160
|
call.endedAt = params.endedAt ?? Date.now();
|
|
@@ -156,11 +168,13 @@ function finalizeCall(params) {
|
|
|
156
168
|
}
|
|
157
169
|
//#endregion
|
|
158
170
|
//#region extensions/voice-call/src/manager/lookup.ts
|
|
171
|
+
/** Resolve an active call from provider call id with map lookup plus stale-map fallback scan. */
|
|
159
172
|
function getCallByProviderCallId(params) {
|
|
160
173
|
const callId = params.providerCallIdMap.get(params.providerCallId);
|
|
161
174
|
if (callId) return params.activeCalls.get(callId);
|
|
162
175
|
for (const call of params.activeCalls.values()) if (call.providerCallId === params.providerCallId) return call;
|
|
163
176
|
}
|
|
177
|
+
/** Resolve an active call by internal call id or provider call id. */
|
|
164
178
|
function findCall(params) {
|
|
165
179
|
const directCall = params.activeCalls.get(params.callIdOrProviderCallId);
|
|
166
180
|
if (directCall) return directCall;
|
|
@@ -172,11 +186,13 @@ function findCall(params) {
|
|
|
172
186
|
}
|
|
173
187
|
//#endregion
|
|
174
188
|
//#region extensions/voice-call/src/tts-provider-voice.ts
|
|
189
|
+
/** Read voice setting aliases from one provider-specific config block. */
|
|
175
190
|
function resolveProviderVoiceSetting(providerConfig) {
|
|
176
191
|
if (!providerConfig || typeof providerConfig !== "object") return;
|
|
177
192
|
const candidate = providerConfig;
|
|
178
193
|
return normalizeOptionalString(candidate.speakerVoice) ?? normalizeOptionalString(candidate.speakerVoiceId) ?? normalizeOptionalString(candidate.voice) ?? normalizeOptionalString(candidate.voiceId);
|
|
179
194
|
}
|
|
195
|
+
/** Resolve the active provider's preferred voice id/name from voice-call TTS config. */
|
|
180
196
|
function resolvePreferredTtsVoice(config) {
|
|
181
197
|
const providerId = config.tts?.provider;
|
|
182
198
|
if (!providerId) return;
|
|
@@ -219,6 +235,7 @@ function mapVoiceToPolly(voice) {
|
|
|
219
235
|
}
|
|
220
236
|
//#endregion
|
|
221
237
|
//#region extensions/voice-call/src/manager/twiml.ts
|
|
238
|
+
/** Generate TwiML that speaks one notification and hangs up. */
|
|
222
239
|
function generateNotifyTwiml(message, voice) {
|
|
223
240
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
224
241
|
<Response>
|
|
@@ -226,6 +243,7 @@ function generateNotifyTwiml(message, voice) {
|
|
|
226
243
|
<Hangup/>
|
|
227
244
|
</Response>`;
|
|
228
245
|
}
|
|
246
|
+
/** Generate TwiML that plays DTMF digits before redirecting to a webhook URL. */
|
|
229
247
|
function generateDtmfRedirectTwiml(digits, webhookUrl) {
|
|
230
248
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
231
249
|
<Response>
|
|
@@ -818,6 +836,7 @@ function processEvent(ctx, event) {
|
|
|
818
836
|
}
|
|
819
837
|
//#endregion
|
|
820
838
|
//#region extensions/voice-call/src/utils.ts
|
|
839
|
+
/** Resolve user input paths, including "~" against the current OS home. */
|
|
821
840
|
function resolveUserPath(input) {
|
|
822
841
|
const trimmed = input.trim();
|
|
823
842
|
if (!trimmed) return trimmed;
|
|
@@ -1089,10 +1108,12 @@ var CallManager = class {
|
|
|
1089
1108
|
};
|
|
1090
1109
|
//#endregion
|
|
1091
1110
|
//#region extensions/voice-call/src/realtime-agent-context.ts
|
|
1111
|
+
/** Limit injected context while preserving an explicit truncation marker. */
|
|
1092
1112
|
function limitText(text, maxChars) {
|
|
1093
1113
|
if (text.length <= maxChars) return text;
|
|
1094
1114
|
return `${text.slice(0, Math.max(0, maxChars - 32)).trimEnd()}\n[truncated]`;
|
|
1095
1115
|
}
|
|
1116
|
+
/** Read configured workspace context files through the safe workspace root. */
|
|
1096
1117
|
async function readWorkspaceVoiceContextFiles(params) {
|
|
1097
1118
|
const sections = [];
|
|
1098
1119
|
let remaining = params.maxChars;
|
|
@@ -1108,6 +1129,7 @@ async function readWorkspaceVoiceContextFiles(params) {
|
|
|
1108
1129
|
}
|
|
1109
1130
|
return sections;
|
|
1110
1131
|
}
|
|
1132
|
+
/** Build final realtime instructions from base instructions, consult policy, and fast context. */
|
|
1111
1133
|
async function buildRealtimeVoiceInstructions(params) {
|
|
1112
1134
|
const { config } = params;
|
|
1113
1135
|
const sections = [params.baseInstructions];
|
|
@@ -1146,6 +1168,7 @@ async function buildRealtimeVoiceInstructions(params) {
|
|
|
1146
1168
|
}
|
|
1147
1169
|
//#endregion
|
|
1148
1170
|
//#region extensions/voice-call/src/realtime-fast-context.ts
|
|
1171
|
+
/** Resolve fast-context consult data using caller-oriented labels. */
|
|
1149
1172
|
async function resolveRealtimeFastContextConsult(params) {
|
|
1150
1173
|
return await resolveRealtimeVoiceFastContextConsult({
|
|
1151
1174
|
...params,
|
|
@@ -1157,6 +1180,7 @@ async function resolveRealtimeFastContextConsult(params) {
|
|
|
1157
1180
|
}
|
|
1158
1181
|
//#endregion
|
|
1159
1182
|
//#region extensions/voice-call/src/response-model.ts
|
|
1183
|
+
/** Resolve provider/model fields from explicit voice config or agent defaults. */
|
|
1160
1184
|
function resolveVoiceResponseModel(params) {
|
|
1161
1185
|
const modelRef = params.voiceConfig.responseModel ?? `${params.agentRuntime.defaults.provider}/${params.agentRuntime.defaults.model}`;
|
|
1162
1186
|
const slashIndex = modelRef.indexOf("/");
|
|
@@ -1178,7 +1202,9 @@ function chunkAudio(audio, chunkSize = 160) {
|
|
|
1178
1202
|
}
|
|
1179
1203
|
//#endregion
|
|
1180
1204
|
//#region extensions/voice-call/src/telephony-tts.ts
|
|
1205
|
+
/** Default timeout for one telephony synthesis request. */
|
|
1181
1206
|
const TELEPHONY_DEFAULT_TTS_TIMEOUT_MS = 8e3;
|
|
1207
|
+
/** Create a TTS provider that honors voice-call overrides and converts PCM to mulaw. */
|
|
1182
1208
|
function createTelephonyTtsProvider(params) {
|
|
1183
1209
|
const { coreConfig, ttsOverride, runtime, logger } = params;
|
|
1184
1210
|
const mergedConfig = applyTtsOverride(coreConfig, ttsOverride);
|
|
@@ -1210,6 +1236,7 @@ function createTelephonyTtsProvider(params) {
|
|
|
1210
1236
|
}
|
|
1211
1237
|
};
|
|
1212
1238
|
}
|
|
1239
|
+
/** Apply voice-call TTS overrides to core config without mutating the original object. */
|
|
1213
1240
|
function applyTtsOverride(coreConfig, override) {
|
|
1214
1241
|
if (!override) return coreConfig;
|
|
1215
1242
|
const base = coreConfig.messages?.tts;
|
|
@@ -1223,12 +1250,14 @@ function applyTtsOverride(coreConfig, override) {
|
|
|
1223
1250
|
}
|
|
1224
1251
|
};
|
|
1225
1252
|
}
|
|
1253
|
+
/** Merge core and voice-call TTS config, keeping undefined override fields out. */
|
|
1226
1254
|
function mergeTtsConfig(base, override) {
|
|
1227
1255
|
if (!base && !override) return;
|
|
1228
1256
|
if (!override) return base;
|
|
1229
1257
|
if (!base) return override;
|
|
1230
1258
|
return deepMergeDefined(base, override);
|
|
1231
1259
|
}
|
|
1260
|
+
/** Resolve directive override policy for telephony synthesis. */
|
|
1232
1261
|
function resolveTelephonyModelOverridePolicy(overrides) {
|
|
1233
1262
|
if (!(overrides?.enabled ?? true)) return {
|
|
1234
1263
|
enabled: false,
|
|
@@ -1252,16 +1281,20 @@ function resolveTelephonyModelOverridePolicy(overrides) {
|
|
|
1252
1281
|
allowSeed: allow(overrides?.allowSeed)
|
|
1253
1282
|
};
|
|
1254
1283
|
}
|
|
1284
|
+
/** Read model override policy from TTS config when present. */
|
|
1255
1285
|
function readTelephonyModelOverrides(ttsConfig) {
|
|
1256
1286
|
const value = ttsConfig?.modelOverrides;
|
|
1257
1287
|
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1258
1288
|
}
|
|
1289
|
+
/** Normalize provider ids for config lookup. */
|
|
1259
1290
|
function normalizeProviderId(value) {
|
|
1260
1291
|
return typeof value === "string" ? value.trim().toLowerCase() || void 0 : void 0;
|
|
1261
1292
|
}
|
|
1293
|
+
/** Coerce provider config objects while rejecting arrays and primitives. */
|
|
1262
1294
|
function asProviderConfig(value) {
|
|
1263
1295
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1264
1296
|
}
|
|
1297
|
+
/** Collect named provider configs from canonical and legacy TTS config shapes. */
|
|
1265
1298
|
function collectTelephonyProviderConfigs(ttsConfig) {
|
|
1266
1299
|
if (!ttsConfig) return {};
|
|
1267
1300
|
const entries = {};
|
|
@@ -1294,12 +1327,14 @@ function collectTelephonyProviderConfigs(ttsConfig) {
|
|
|
1294
1327
|
//#endregion
|
|
1295
1328
|
//#region extensions/voice-call/src/bounded-child-output.ts
|
|
1296
1329
|
const DEFAULT_MAX_OUTPUT_CHARS = 16384;
|
|
1330
|
+
/** Create an empty bounded output buffer. */
|
|
1297
1331
|
function emptyBoundedChildOutput() {
|
|
1298
1332
|
return {
|
|
1299
1333
|
text: "",
|
|
1300
1334
|
truncated: false
|
|
1301
1335
|
};
|
|
1302
1336
|
}
|
|
1337
|
+
/** Append output while retaining the newest maxChars and recording truncation. */
|
|
1303
1338
|
function appendBoundedChildOutput(current, chunk, maxChars = DEFAULT_MAX_OUTPUT_CHARS) {
|
|
1304
1339
|
const appended = current.text + chunk;
|
|
1305
1340
|
if (appended.length <= maxChars) return {
|
|
@@ -1311,6 +1346,7 @@ function appendBoundedChildOutput(current, chunk, maxChars = DEFAULT_MAX_OUTPUT_
|
|
|
1311
1346
|
truncated: true
|
|
1312
1347
|
};
|
|
1313
1348
|
}
|
|
1349
|
+
/** Format captured output with a truncation marker when older text was dropped. */
|
|
1314
1350
|
function formatBoundedChildOutput(output) {
|
|
1315
1351
|
return output.truncated ? `[output truncated]\n${output.text}` : output.text;
|
|
1316
1352
|
}
|
|
@@ -1680,12 +1716,15 @@ async function startTunnel(config) {
|
|
|
1680
1716
|
}
|
|
1681
1717
|
//#endregion
|
|
1682
1718
|
//#region extensions/voice-call/src/webhook-exposure.ts
|
|
1719
|
+
/** Return true when a provider requires a public webhook URL or tunnel. */
|
|
1683
1720
|
function providerRequiresPublicWebhook(providerName) {
|
|
1684
1721
|
return providerName === "twilio" || providerName === "telnyx" || providerName === "plivo";
|
|
1685
1722
|
}
|
|
1723
|
+
/** Return true for localhost, private, or otherwise provider-unreachable hosts. */
|
|
1686
1724
|
function isLocalOnlyWebhookHost(hostname) {
|
|
1687
1725
|
return isBlockedHostnameOrIp(hostname);
|
|
1688
1726
|
}
|
|
1727
|
+
/** Return true when a webhook URL parses to a local/private host. */
|
|
1689
1728
|
function isProviderUnreachableWebhookUrl(webhookUrl) {
|
|
1690
1729
|
try {
|
|
1691
1730
|
return isLocalOnlyWebhookHost(new URL(webhookUrl).hostname);
|
|
@@ -1693,6 +1732,7 @@ function isProviderUnreachableWebhookUrl(webhookUrl) {
|
|
|
1693
1732
|
return false;
|
|
1694
1733
|
}
|
|
1695
1734
|
}
|
|
1735
|
+
/** Resolve a human-readable webhook exposure status for doctor/setup surfaces. */
|
|
1696
1736
|
function resolveWebhookExposureStatus(config) {
|
|
1697
1737
|
if (config.provider === "mock") return {
|
|
1698
1738
|
ok: true,
|
|
@@ -1729,6 +1769,7 @@ function resolveWebhookExposureStatus(config) {
|
|
|
1729
1769
|
}
|
|
1730
1770
|
//#endregion
|
|
1731
1771
|
//#region extensions/voice-call/src/http-headers.ts
|
|
1772
|
+
/** Return the first value for a header name regardless of caller casing. */
|
|
1732
1773
|
function getHeader(headers, name) {
|
|
1733
1774
|
const target = normalizeLowercaseStringOrEmpty(name);
|
|
1734
1775
|
const value = headers[target] ?? Object.entries(headers).find(([key]) => normalizeLowercaseStringOrEmpty(key) === target)?.[1];
|
|
@@ -2380,19 +2421,23 @@ const TERMINAL_PROVIDER_STATUS_TO_END_REASON = {
|
|
|
2380
2421
|
"no-answer": "no-answer",
|
|
2381
2422
|
canceled: "hangup-bot"
|
|
2382
2423
|
};
|
|
2424
|
+
/** Normalize provider status text, falling back to "unknown". */
|
|
2383
2425
|
function normalizeProviderStatus(status) {
|
|
2384
2426
|
const normalized = normalizeOptionalLowercaseString(status);
|
|
2385
2427
|
return normalized && normalized.length > 0 ? normalized : "unknown";
|
|
2386
2428
|
}
|
|
2429
|
+
/** Map terminal provider status strings to OpenClaw end reasons. */
|
|
2387
2430
|
function mapProviderStatusToEndReason(status) {
|
|
2388
2431
|
return TERMINAL_PROVIDER_STATUS_TO_END_REASON[normalizeProviderStatus(status)] ?? null;
|
|
2389
2432
|
}
|
|
2433
|
+
/** Return true when a provider status is terminal. */
|
|
2390
2434
|
function isProviderStatusTerminal(status) {
|
|
2391
2435
|
return mapProviderStatusToEndReason(status) !== null;
|
|
2392
2436
|
}
|
|
2393
2437
|
//#endregion
|
|
2394
2438
|
//#region extensions/voice-call/src/webhook/stale-call-reaper.ts
|
|
2395
2439
|
const CHECK_INTERVAL_MS = 3e4;
|
|
2440
|
+
/** Start a stale-call reaper and return its cleanup callback. */
|
|
2396
2441
|
function startStaleCallReaper(params) {
|
|
2397
2442
|
const maxAgeSeconds = params.staleCallReaperSeconds;
|
|
2398
2443
|
if (!maxAgeSeconds || maxAgeSeconds <= 0) return null;
|
|
@@ -2428,7 +2473,7 @@ function loadRealtimeTranscriptionRuntime() {
|
|
|
2428
2473
|
return realtimeTranscriptionRuntimePromise;
|
|
2429
2474
|
}
|
|
2430
2475
|
function loadResponseGeneratorModule() {
|
|
2431
|
-
responseGeneratorModulePromise ??= import("./response-generator-
|
|
2476
|
+
responseGeneratorModulePromise ??= import("./response-generator-DjsoOMEg.js");
|
|
2432
2477
|
return responseGeneratorModulePromise;
|
|
2433
2478
|
}
|
|
2434
2479
|
function sanitizeTranscriptForLog(value) {
|
|
@@ -3112,15 +3157,15 @@ let mockProviderPromise;
|
|
|
3112
3157
|
let realtimeVoiceRuntimePromise;
|
|
3113
3158
|
let realtimeHandlerPromise;
|
|
3114
3159
|
function loadTelnyxProvider() {
|
|
3115
|
-
telnyxProviderPromise ??= import("./telnyx-
|
|
3160
|
+
telnyxProviderPromise ??= import("./telnyx-Gm8Z1pUt.js");
|
|
3116
3161
|
return telnyxProviderPromise;
|
|
3117
3162
|
}
|
|
3118
3163
|
function loadTwilioProvider() {
|
|
3119
|
-
twilioProviderPromise ??= import("./twilio-
|
|
3164
|
+
twilioProviderPromise ??= import("./twilio-BuFmgABx.js");
|
|
3120
3165
|
return twilioProviderPromise;
|
|
3121
3166
|
}
|
|
3122
3167
|
function loadPlivoProvider() {
|
|
3123
|
-
plivoProviderPromise ??= import("./plivo-
|
|
3168
|
+
plivoProviderPromise ??= import("./plivo-29Dvun7P.js");
|
|
3124
3169
|
return plivoProviderPromise;
|
|
3125
3170
|
}
|
|
3126
3171
|
function loadMockProvider() {
|
|
@@ -3132,7 +3177,7 @@ function loadRealtimeVoiceRuntime() {
|
|
|
3132
3177
|
return realtimeVoiceRuntimePromise;
|
|
3133
3178
|
}
|
|
3134
3179
|
function loadRealtimeHandler() {
|
|
3135
|
-
realtimeHandlerPromise ??= import("./realtime-handler-
|
|
3180
|
+
realtimeHandlerPromise ??= import("./realtime-handler-ChN5De4P.js");
|
|
3136
3181
|
return realtimeHandlerPromise;
|
|
3137
3182
|
}
|
|
3138
3183
|
function resolveVoiceCallConsultSessionKey(call) {
|
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-GVY8iEzX.js";
|
|
2
2
|
export { createVoiceCallRuntime };
|
package/dist/setup-api.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { n as migrateVoiceCallLegacyConfigInput } from "./config-compat-
|
|
1
|
+
import { n as migrateVoiceCallLegacyConfigInput } from "./config-compat-BPPFhsJ7.js";
|
|
2
2
|
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
3
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
4
4
|
//#region extensions/voice-call/setup-api.ts
|
|
5
|
+
/** Migrate voice-call plugin config inside the full OpenClaw config object. */
|
|
5
6
|
function migrateVoiceCallPluginConfig(config) {
|
|
6
7
|
const rawVoiceCallConfig = config.plugins?.entries?.["voice-call"]?.config;
|
|
7
8
|
if (!isRecord(rawVoiceCallConfig)) return null;
|
|
@@ -25,6 +26,7 @@ function migrateVoiceCallPluginConfig(config) {
|
|
|
25
26
|
changes: migration.changes
|
|
26
27
|
};
|
|
27
28
|
}
|
|
29
|
+
/** Setup plugin entry that registers voice-call config migrations. */
|
|
28
30
|
var setup_api_default = definePluginEntry({
|
|
29
31
|
id: "voice-call",
|
|
30
32
|
name: "Voice Call Setup",
|
|
@@ -124,22 +124,30 @@ const { setRuntime: setVoiceCallStateRuntime, clearRuntime: clearVoiceCallStateR
|
|
|
124
124
|
});
|
|
125
125
|
//#endregion
|
|
126
126
|
//#region extensions/voice-call/src/manager/store.ts
|
|
127
|
+
/** Plugin state namespace for call record event metadata. */
|
|
127
128
|
const CALL_RECORD_EVENTS_NAMESPACE = "call-record-events";
|
|
129
|
+
/** Plugin state namespace for base64 call record event chunks. */
|
|
128
130
|
const CALL_RECORD_EVENT_CHUNKS_NAMESPACE = "call-record-event-chunks";
|
|
131
|
+
/** Maximum retained call record events. */
|
|
129
132
|
const MAX_CALL_RECORD_EVENTS = 1e3;
|
|
133
|
+
/** Extra metadata entries retained so pruning can safely trim oldest rows. */
|
|
130
134
|
const CALL_RECORD_EVENT_META_MAX_ENTRIES = 1100;
|
|
131
135
|
const CALL_RECORD_CHUNK_MAX_ENTRIES = 48048;
|
|
136
|
+
/** Raw UTF-8 bytes stored per call record chunk before base64 encoding. */
|
|
132
137
|
const RAW_CALL_RECORD_CHUNK_BYTES = 47 * 1024;
|
|
133
138
|
let callRecordEventSequence = 0;
|
|
139
|
+
/** Return the pre-SQLite JSONL call log path for migration/compat checks. */
|
|
134
140
|
function resolveVoiceCallLegacyCallLogPath(storePath) {
|
|
135
141
|
return path.join(storePath, "calls.jsonl");
|
|
136
142
|
}
|
|
143
|
+
/** Build env for plugin state stores rooted at the voice-call store path. */
|
|
137
144
|
function resolvePluginStateEnv(storePath) {
|
|
138
145
|
return {
|
|
139
146
|
...process.env,
|
|
140
147
|
OPENCLAW_STATE_DIR: storePath
|
|
141
148
|
};
|
|
142
149
|
}
|
|
150
|
+
/** Open the plugin state stores when the runtime is available. */
|
|
143
151
|
function createCallRecordStateStores(storePath) {
|
|
144
152
|
const runtime = getOptionalVoiceCallStateRuntime();
|
|
145
153
|
if (!runtime) return null;
|
|
@@ -157,6 +165,7 @@ function createCallRecordStateStores(storePath) {
|
|
|
157
165
|
})
|
|
158
166
|
};
|
|
159
167
|
}
|
|
168
|
+
/** Open call stores and log failures instead of breaking restore paths. */
|
|
160
169
|
function tryCreateCallRecordStateStores(storePath) {
|
|
161
170
|
try {
|
|
162
171
|
return createCallRecordStateStores(storePath);
|
|
@@ -165,12 +174,15 @@ function tryCreateCallRecordStateStores(storePath) {
|
|
|
165
174
|
return null;
|
|
166
175
|
}
|
|
167
176
|
}
|
|
177
|
+
/** Build the stable storage key for one chunk of an event. */
|
|
168
178
|
function buildChunkKey(eventKey, index) {
|
|
169
179
|
return `${eventKey}:chunk:${String(index).padStart(4, "0")}`;
|
|
170
180
|
}
|
|
181
|
+
/** Build a deterministic key for one legacy JSONL line. */
|
|
171
182
|
function buildVoiceCallLegacyJsonlEventKey(line, index) {
|
|
172
183
|
return `jsonl:${String(index).padStart(8, "0")}:${createHash("sha256").update(line).digest("hex")}`;
|
|
173
184
|
}
|
|
185
|
+
/** Allocate monotonic ordering metadata for newly persisted call records. */
|
|
174
186
|
function nextCallRecordOrder() {
|
|
175
187
|
const sequence = callRecordEventSequence;
|
|
176
188
|
callRecordEventSequence = (callRecordEventSequence + 1) % 1e6;
|
|
@@ -179,13 +191,16 @@ function nextCallRecordOrder() {
|
|
|
179
191
|
sequence
|
|
180
192
|
};
|
|
181
193
|
}
|
|
194
|
+
/** Build a unique event key that preserves timestamp and sequence ordering. */
|
|
182
195
|
function buildNewEventKey(order) {
|
|
183
196
|
return `event:${order.persistedAt.toString(36)}:${String(order.sequence).padStart(6, "0")}:${randomUUID()}`;
|
|
184
197
|
}
|
|
198
|
+
/** Recover the sequence segment from newer event keys. */
|
|
185
199
|
function parseEventKeySequence(key) {
|
|
186
200
|
const match = /^event:[^:]+:(\d+):/.exec(key);
|
|
187
201
|
return match ? Number.parseInt(match[1], 10) : 0;
|
|
188
202
|
}
|
|
203
|
+
/** Parse a stored call record line from v2 envelope or legacy raw-call JSON. */
|
|
189
204
|
function parseVoiceCallRecordLine(line, sequence = 0) {
|
|
190
205
|
if (!line.trim()) return null;
|
|
191
206
|
try {
|
|
@@ -209,9 +224,11 @@ function parseVoiceCallRecordLine(line, sequence = 0) {
|
|
|
209
224
|
return null;
|
|
210
225
|
}
|
|
211
226
|
}
|
|
227
|
+
/** Count storage chunks needed for a call record. */
|
|
212
228
|
function countCallRecordChunks(call) {
|
|
213
229
|
return Math.max(1, Math.ceil(Buffer.byteLength(JSON.stringify(call), "utf8") / RAW_CALL_RECORD_CHUNK_BYTES));
|
|
214
230
|
}
|
|
231
|
+
/** Truncate oversized call records to fit the bounded plugin state chunk budget. */
|
|
215
232
|
function prepareVoiceCallRecordForStorage(call) {
|
|
216
233
|
if (countCallRecordChunks(call) <= 48) return call;
|
|
217
234
|
const transcriptEntries = call.transcript.length;
|
|
@@ -249,6 +266,7 @@ function prepareVoiceCallRecordForStorage(call) {
|
|
|
249
266
|
}
|
|
250
267
|
return call;
|
|
251
268
|
}
|
|
269
|
+
/** Register a serialized call record event and its chunks, then prune old events. */
|
|
252
270
|
function registerCallRecordEvent(stores, eventKey, call, order) {
|
|
253
271
|
const serialized = JSON.stringify(prepareVoiceCallRecordForStorage(call));
|
|
254
272
|
const buffer = Buffer.from(serialized, "utf8");
|
|
@@ -269,18 +287,21 @@ function registerCallRecordEvent(stores, eventKey, call, order) {
|
|
|
269
287
|
});
|
|
270
288
|
pruneCallRecordEvents(stores);
|
|
271
289
|
}
|
|
290
|
+
/** Delete metadata and all chunk rows for one call record event. */
|
|
272
291
|
function deleteCallRecordEventRows(stores, eventKey) {
|
|
273
292
|
const meta = stores.events.lookup(eventKey);
|
|
274
293
|
stores.events.delete(eventKey);
|
|
275
294
|
if (!meta) return;
|
|
276
295
|
for (let index = 0; index < meta.chunkCount; index += 1) stores.chunks.delete(buildChunkKey(eventKey, index));
|
|
277
296
|
}
|
|
297
|
+
/** Keep only the newest bounded call record events. */
|
|
278
298
|
function pruneCallRecordEvents(stores) {
|
|
279
299
|
const rows = stores.events.entries();
|
|
280
300
|
if (rows.length <= 1e3) return;
|
|
281
301
|
const sorted = rows.toSorted((a, b) => a.createdAt - b.createdAt || a.key.localeCompare(b.key));
|
|
282
302
|
for (const row of sorted.slice(0, rows.length - MAX_CALL_RECORD_EVENTS)) deleteCallRecordEventRows(stores, row.key);
|
|
283
303
|
}
|
|
304
|
+
/** Read and reassemble one chunked call record event. */
|
|
284
305
|
function readCallRecordEvent(stores, eventKey) {
|
|
285
306
|
const meta = stores.events.lookup(eventKey);
|
|
286
307
|
if (!meta) return null;
|
|
@@ -292,6 +313,7 @@ function readCallRecordEvent(stores, eventKey) {
|
|
|
292
313
|
}
|
|
293
314
|
return parseVoiceCallRecordLine(Buffer.concat(chunks, meta.byteLength).toString("utf8"))?.call ?? null;
|
|
294
315
|
}
|
|
316
|
+
/** Read all persisted call records in stable persisted order. */
|
|
295
317
|
function readCallRecordEvents(stores) {
|
|
296
318
|
return stores.events.entries().toSorted((a, b) => a.createdAt - b.createdAt || a.key.localeCompare(b.key)).map((entry) => {
|
|
297
319
|
const call = readCallRecordEvent(stores, entry.key);
|
|
@@ -303,6 +325,7 @@ function readCallRecordEvents(stores) {
|
|
|
303
325
|
} : null;
|
|
304
326
|
}).filter((entry) => entry !== null).toSorted((a, b) => a.persistedAt - b.persistedAt || a.sequence - b.sequence || a.orderKey.localeCompare(b.orderKey)).map((entry) => entry.call);
|
|
305
327
|
}
|
|
328
|
+
/** Persist one call record event to plugin state. */
|
|
306
329
|
function persistCallRecord(storePath, call) {
|
|
307
330
|
try {
|
|
308
331
|
const stores = createCallRecordStateStores(storePath);
|
|
@@ -314,6 +337,7 @@ function persistCallRecord(storePath, call) {
|
|
|
314
337
|
throw err;
|
|
315
338
|
}
|
|
316
339
|
}
|
|
340
|
+
/** Restore nonterminal active calls and provider/event indexes from persisted records. */
|
|
317
341
|
function loadActiveCallsFromStore(storePath) {
|
|
318
342
|
const stores = tryCreateCallRecordStateStores(storePath);
|
|
319
343
|
let calls = [];
|
|
@@ -347,6 +371,7 @@ function loadActiveCallsFromStore(storePath) {
|
|
|
347
371
|
rejectedProviderCallIds
|
|
348
372
|
};
|
|
349
373
|
}
|
|
374
|
+
/** Return the newest persisted call history rows up to the requested limit. */
|
|
350
375
|
async function getCallHistoryFromStore(storePath, limit = 50) {
|
|
351
376
|
if (limit <= 0) return [];
|
|
352
377
|
const stores = tryCreateCallRecordStateStores(storePath);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
1
|
+
import { i as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-C5KNhYC-.js";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
//#region extensions/voice-call/src/providers/telnyx.ts
|
|
4
4
|
function normalizeTelnyxDirection(direction) {
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import { a as getHeader, d as chunkAudio, h as mapVoiceToPolly, i as normalizeProviderStatus, m as escapeXml, n as isProviderStatusTerminal, r as mapProviderStatusToEndReason } from "./runtime-entry-
|
|
4
|
-
import { a as verifyTwilioWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
3
|
+
import { a as getHeader, d as chunkAudio, h as mapVoiceToPolly, i as normalizeProviderStatus, m as escapeXml, n as isProviderStatusTerminal, r as mapProviderStatusToEndReason } from "./runtime-entry-GVY8iEzX.js";
|
|
4
|
+
import { a as verifyTwilioWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-C5KNhYC-.js";
|
|
5
5
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
6
6
|
import crypto from "node:crypto";
|
|
7
7
|
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
8
8
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
9
9
|
//#region extensions/voice-call/src/providers/twilio/api.ts
|
|
10
10
|
const TWILIO_API_TIMEOUT_MS = 3e4;
|
|
11
|
+
/** Parse Twilio JSON error responses without trusting response shape. */
|
|
11
12
|
function parseTwilioApiError(text) {
|
|
12
13
|
try {
|
|
13
14
|
const parsed = JSON.parse(text);
|
|
@@ -21,6 +22,7 @@ function parseTwilioApiError(text) {
|
|
|
21
22
|
return {};
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
/** Error thrown for non-2xx Twilio REST API responses. */
|
|
24
26
|
var TwilioApiError = class extends Error {
|
|
25
27
|
constructor(httpStatus, responseText) {
|
|
26
28
|
const parsed = parseTwilioApiError(responseText);
|
|
@@ -32,6 +34,7 @@ var TwilioApiError = class extends Error {
|
|
|
32
34
|
this.twilioCode = parsed.code;
|
|
33
35
|
}
|
|
34
36
|
};
|
|
37
|
+
/** POST a form-encoded Twilio REST API request through the SSRF guard. */
|
|
35
38
|
async function twilioApiRequest(params) {
|
|
36
39
|
const bodyParams = params.body instanceof URLSearchParams ? params.body : Object.entries(params.body).reduce((acc, [key, value]) => {
|
|
37
40
|
if (Array.isArray(value)) for (const entry of value) acc.append(key, entry);
|
|
@@ -71,9 +74,11 @@ async function twilioApiRequest(params) {
|
|
|
71
74
|
}
|
|
72
75
|
//#endregion
|
|
73
76
|
//#region extensions/voice-call/src/providers/twilio/twiml-policy.ts
|
|
77
|
+
/** Return true for Twilio outbound call directions. */
|
|
74
78
|
function isOutboundDirection(direction) {
|
|
75
79
|
return direction?.startsWith("outbound") ?? false;
|
|
76
80
|
}
|
|
81
|
+
/** Read the Twilio request fields needed by TwiML decision logic. */
|
|
77
82
|
function readTwimlRequestView(ctx) {
|
|
78
83
|
const params = new URLSearchParams(ctx.rawBody);
|
|
79
84
|
const type = normalizeOptionalString(ctx.query?.type);
|
|
@@ -86,6 +91,7 @@ function readTwimlRequestView(ctx) {
|
|
|
86
91
|
callIdFromQuery
|
|
87
92
|
};
|
|
88
93
|
}
|
|
94
|
+
/** Decide the TwiML response kind for a Twilio webhook request. */
|
|
89
95
|
function decideTwimlResponse(input) {
|
|
90
96
|
if (input.callIdFromQuery && !input.isStatusCallback) {
|
|
91
97
|
if (input.hasStoredTwiml) return {
|
|
@@ -109,6 +115,7 @@ function decideTwimlResponse(input) {
|
|
|
109
115
|
}
|
|
110
116
|
//#endregion
|
|
111
117
|
//#region extensions/voice-call/src/providers/twilio/webhook.ts
|
|
118
|
+
/** Verify a Twilio webhook and map SDK verification details to provider result fields. */
|
|
112
119
|
function verifyTwilioProviderWebhook(params) {
|
|
113
120
|
const result = verifyTwilioWebhook(params.ctx, params.authToken, {
|
|
114
121
|
publicUrl: params.currentPublicUrl || void 0,
|
|
@@ -206,6 +213,7 @@ var TwilioProvider = class TwilioProvider {
|
|
|
206
213
|
}
|
|
207
214
|
registerCallStream(callSid, streamSid) {
|
|
208
215
|
this.callStreamMap.set(callSid, streamSid);
|
|
216
|
+
this.activeStreamCalls.add(callSid);
|
|
209
217
|
}
|
|
210
218
|
hasRegisteredStream(callSid) {
|
|
211
219
|
return this.callStreamMap.has(callSid);
|
|
@@ -406,7 +414,6 @@ var TwilioProvider = class TwilioProvider {
|
|
|
406
414
|
canStream: Boolean(view.callSid && this.getStreamUrl())
|
|
407
415
|
});
|
|
408
416
|
if (decision.consumeStoredTwimlCallId) this.deleteStoredTwiml(decision.consumeStoredTwimlCallId);
|
|
409
|
-
if (decision.activateStreamCallSid) this.activeStreamCalls.add(decision.activateStreamCallSid);
|
|
410
417
|
switch (decision.kind) {
|
|
411
418
|
case "stored": return storedTwiml ?? TwilioProvider.EMPTY_TWIML;
|
|
412
419
|
case "queue": return TwilioProvider.QUEUE_TWIML;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/voice-call",
|
|
3
|
-
"version": "2026.6.
|
|
3
|
+
"version": "2026.6.5-beta.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@openclaw/voice-call",
|
|
9
|
-
"version": "2026.6.
|
|
9
|
+
"version": "2026.6.5-beta.2",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"commander": "14.0.3",
|
|
12
12
|
"typebox": "1.1.39",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"zod": "4.4.3"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
|
-
"openclaw": ">=2026.6.
|
|
17
|
+
"openclaw": ">=2026.6.5-beta.2"
|
|
18
18
|
},
|
|
19
19
|
"peerDependenciesMeta": {
|
|
20
20
|
"openclaw": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/voice-call",
|
|
3
|
-
"version": "2026.6.
|
|
3
|
+
"version": "2026.6.5-beta.2",
|
|
4
4
|
"description": "OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"zod": "4.4.3"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
|
-
"openclaw": ">=2026.6.
|
|
17
|
+
"openclaw": ">=2026.6.5-beta.2"
|
|
18
18
|
},
|
|
19
19
|
"peerDependenciesMeta": {
|
|
20
20
|
"openclaw": {
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"minHostVersion": ">=2026.4.10"
|
|
32
32
|
},
|
|
33
33
|
"compat": {
|
|
34
|
-
"pluginApi": ">=2026.6.
|
|
34
|
+
"pluginApi": ">=2026.6.5-beta.2"
|
|
35
35
|
},
|
|
36
36
|
"build": {
|
|
37
|
-
"openclawVersion": "2026.6.
|
|
37
|
+
"openclawVersion": "2026.6.5-beta.2"
|
|
38
38
|
},
|
|
39
39
|
"release": {
|
|
40
40
|
"publishToClawHub": true,
|