@openclaw/voice-call 2026.9.1-beta.1 → 2026.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/api.js +2 -2
- package/dist/cli-DNq2qUU0.js +630 -0
- package/dist/config-DQwvsxeV.js +697 -0
- package/dist/doctor-contract-api.js +9 -6
- package/dist/{guarded-json-api-BmMfqKzQ.js → guarded-json-api-9je4i7qf.js} +10 -7
- package/dist/index.js +18 -582
- package/dist/{mock-Dv7KxYGt.js → mock-C57qj18W.js} +1 -1
- package/dist/{plivo-CNPyQgvw.js → plivo-BfKoYCMW.js} +2 -2
- package/dist/{realtime-handler-DYTBiNVr.js → realtime-handler-BYWqpZ6N.js} +67 -45
- package/dist/{response-generator-BCm6S1G6.js → response-generator-C0577tL-.js} +3 -3
- package/dist/runtime-api.js +2 -2
- package/dist/{runtime-entry-ptAKgGER.js → runtime-entry-gE2Yd4V8.js} +138 -766
- package/dist/runtime-entry.js +1 -1
- package/dist/setup-api.js +58 -38
- package/dist/{store-path-51_oC_xn.js → store-path-C_z9zKnA.js} +11 -21
- package/dist/{telnyx-DWpKHVeV.js → telnyx-rcjslHOP.js} +1 -1
- package/dist/{twilio-DUIVX0Eb.js → twilio-B100ALoj.js} +5 -4
- package/dist/{webhook-replay-FQrEGVSM.js → webhook-replay-CCQSoBNF.js} +1 -1
- package/node_modules/typebox/build/format/email.mjs +1 -1
- package/node_modules/typebox/build/format/idna/idn-hostname.mjs +19 -1
- package/node_modules/typebox/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +8 -6
package/dist/index.js
CHANGED
|
@@ -1,585 +1,17 @@
|
|
|
1
|
-
import { definePluginEntry
|
|
1
|
+
import { definePluginEntry } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
3
|
import { VOICE_CALL_CLI_DESCRIPTOR } from "./cli-output-mode.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { i as resolveVoiceCallConfig, t as VoiceCallConfigSchema, u as validateProviderConfig } from "./config-DQwvsxeV.js";
|
|
5
|
+
import { t as createVoiceCallRuntime } from "./runtime-entry-gE2Yd4V8.js";
|
|
6
6
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
7
|
-
import { ErrorCodes,
|
|
7
|
+
import { ErrorCodes, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
|
|
8
8
|
import { resolveGlobalSingleton } from "openclaw/plugin-sdk/global-singleton";
|
|
9
9
|
import { normalizeAgentId, parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
|
|
10
|
-
import { asNonArrayRecord, asOptionalRecord,
|
|
10
|
+
import { asNonArrayRecord, asOptionalRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
11
11
|
import { jsonResult } from "openclaw/plugin-sdk/tool-results";
|
|
12
12
|
import { Type } from "typebox";
|
|
13
|
-
import {
|
|
14
|
-
import fs from "node:fs";
|
|
13
|
+
import { resolveTimerTimeoutMs, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
|
15
14
|
import { randomUUID } from "node:crypto";
|
|
16
|
-
import path from "node:path";
|
|
17
|
-
import { StringDecoder } from "node:string_decoder";
|
|
18
|
-
import { format } from "node:util";
|
|
19
|
-
//#region extensions/voice-call/src/cli-command-io.ts
|
|
20
|
-
function writeCliLine(...values) {
|
|
21
|
-
process.stdout.write(`${format(...values)}\n`);
|
|
22
|
-
}
|
|
23
|
-
function writeCliJson(value) {
|
|
24
|
-
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
25
|
-
}
|
|
26
|
-
function parseCliInteger(raw, optionName, opts) {
|
|
27
|
-
const min = opts?.min ?? 0;
|
|
28
|
-
const parsed = parseStrictNonNegativeInteger(raw?.trim() ?? "");
|
|
29
|
-
if (parsed === void 0 || parsed < min || opts?.max !== void 0 && parsed > opts.max) throw new Error(`Invalid numeric value for ${optionName}: ${raw ?? ""}`);
|
|
30
|
-
return parsed;
|
|
31
|
-
}
|
|
32
|
-
//#endregion
|
|
33
|
-
//#region extensions/voice-call/src/cli-call-log.ts
|
|
34
|
-
function percentile(values, p) {
|
|
35
|
-
if (values.length === 0) return 0;
|
|
36
|
-
const sorted = [...values].toSorted((a, b) => a - b);
|
|
37
|
-
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(p / 100 * sorted.length) - 1))] ?? 0;
|
|
38
|
-
}
|
|
39
|
-
function summarizeSeries(values) {
|
|
40
|
-
if (values.length === 0) return {
|
|
41
|
-
count: 0,
|
|
42
|
-
minMs: 0,
|
|
43
|
-
maxMs: 0,
|
|
44
|
-
avgMs: 0,
|
|
45
|
-
p50Ms: 0,
|
|
46
|
-
p95Ms: 0
|
|
47
|
-
};
|
|
48
|
-
const minMs = values.reduce((min, value) => value < min ? value : min);
|
|
49
|
-
const maxMs = values.reduce((max, value) => value > max ? value : max);
|
|
50
|
-
const avgMs = values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
51
|
-
return {
|
|
52
|
-
count: values.length,
|
|
53
|
-
minMs,
|
|
54
|
-
maxMs,
|
|
55
|
-
avgMs,
|
|
56
|
-
p50Ms: percentile(values, 50),
|
|
57
|
-
p95Ms: percentile(values, 95)
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
function writeVoiceCallLatencySummary(calls) {
|
|
61
|
-
const turnLatencyMs = [];
|
|
62
|
-
const listenWaitMs = [];
|
|
63
|
-
for (const call of calls) {
|
|
64
|
-
const metadata = isRecord(call) && isRecord(call.metadata) ? call.metadata : void 0;
|
|
65
|
-
const latency = metadata?.lastTurnLatencyMs;
|
|
66
|
-
const listenWait = metadata?.lastTurnListenWaitMs;
|
|
67
|
-
if (typeof latency === "number" && Number.isFinite(latency)) turnLatencyMs.push(latency);
|
|
68
|
-
if (typeof listenWait === "number" && Number.isFinite(listenWait)) listenWaitMs.push(listenWait);
|
|
69
|
-
}
|
|
70
|
-
writeCliJson({
|
|
71
|
-
recordsScanned: calls.length,
|
|
72
|
-
turnLatency: summarizeSeries(turnLatencyMs),
|
|
73
|
-
listenWait: summarizeSeries(listenWaitMs)
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
function registerVoiceCallLogs(params) {
|
|
77
|
-
params.root.command("tail").description("Tail voice-call JSONL logs (prints new lines; useful during provider tests)").option("--file <path>", "Path to calls.jsonl", params.defaultFile).option("--since <n>", "Print last N lines first", "25").option("--poll <ms>", "Poll interval in ms", "250").action(async (options) => {
|
|
78
|
-
const file = options.file;
|
|
79
|
-
const since = parseCliInteger(options.since, "--since", { min: 0 });
|
|
80
|
-
const pollMs = parseCliInteger(options.poll, "--poll", { min: 50 });
|
|
81
|
-
const tailSqliteHistory = async (initialLimit) => {
|
|
82
|
-
params.ensureHistoryStateRuntime();
|
|
83
|
-
const seen = /* @__PURE__ */ new Set();
|
|
84
|
-
const printCall = (call) => {
|
|
85
|
-
const line = JSON.stringify(call);
|
|
86
|
-
if (!seen.has(line)) {
|
|
87
|
-
seen.add(line);
|
|
88
|
-
writeCliLine(line);
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
if (initialLimit > 0) for (const call of await getCallHistoryFromStore(path.dirname(file), initialLimit)) printCall(call);
|
|
92
|
-
for (;;) {
|
|
93
|
-
try {
|
|
94
|
-
for (const call of await getCallHistoryFromStore(path.dirname(file), 1e3)) printCall(call);
|
|
95
|
-
} catch {}
|
|
96
|
-
await sleep(pollMs);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
if (fs.existsSync(file) && path.basename(file) !== "calls.jsonl") {
|
|
100
|
-
const initial = fs.readFileSync(file);
|
|
101
|
-
let decoder = new StringDecoder("utf8");
|
|
102
|
-
const initialLines = decoder.write(initial).split("\n");
|
|
103
|
-
let pendingLine = initialLines.pop() ?? "";
|
|
104
|
-
const lines = initialLines.filter(Boolean);
|
|
105
|
-
for (const line of lines.slice(Math.max(0, lines.length - since))) writeCliLine(line);
|
|
106
|
-
let offset = initial.length;
|
|
107
|
-
let lastObservedSize = initial.length;
|
|
108
|
-
for (;;) {
|
|
109
|
-
try {
|
|
110
|
-
const stat = fs.statSync(file);
|
|
111
|
-
if (stat.size < lastObservedSize) {
|
|
112
|
-
offset = 0;
|
|
113
|
-
decoder = new StringDecoder("utf8");
|
|
114
|
-
pendingLine = "";
|
|
115
|
-
}
|
|
116
|
-
lastObservedSize = stat.size;
|
|
117
|
-
if (stat.size > offset) {
|
|
118
|
-
const fd = fs.openSync(file, "r");
|
|
119
|
-
try {
|
|
120
|
-
const buf = Buffer.alloc(stat.size - offset);
|
|
121
|
-
const bytesRead = fs.readSync(fd, buf, 0, buf.length, offset);
|
|
122
|
-
offset += bytesRead;
|
|
123
|
-
const text = decoder.write(buf.subarray(0, bytesRead));
|
|
124
|
-
const completeLines = `${pendingLine}${text}`.split("\n");
|
|
125
|
-
pendingLine = completeLines.pop() ?? "";
|
|
126
|
-
for (const line of completeLines.filter(Boolean)) writeCliLine(line);
|
|
127
|
-
} finally {
|
|
128
|
-
fs.closeSync(fd);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
} catch {}
|
|
132
|
-
await sleep(pollMs);
|
|
133
|
-
}
|
|
134
|
-
} else await tailSqliteHistory(since);
|
|
135
|
-
});
|
|
136
|
-
params.root.command("latency").description("Summarize turn latency metrics from voice-call JSONL logs").option("--file <path>", "Path to calls.jsonl", params.defaultFile).option("--last <n>", "Analyze last N records", "200").action(async (options) => {
|
|
137
|
-
const file = options.file;
|
|
138
|
-
const last = parseCliInteger(options.last, "--last", { min: 1 });
|
|
139
|
-
if (fs.existsSync(file) && path.basename(file) !== "calls.jsonl") writeVoiceCallLatencySummary(fs.readFileSync(file, "utf8").split("\n").filter(Boolean).slice(-last).map((line) => {
|
|
140
|
-
try {
|
|
141
|
-
const parsed = JSON.parse(line);
|
|
142
|
-
return (isRecord(parsed) ? parsed.call : void 0) ?? parsed;
|
|
143
|
-
} catch {
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
}).filter((call) => call !== null));
|
|
147
|
-
else {
|
|
148
|
-
params.ensureHistoryStateRuntime();
|
|
149
|
-
writeVoiceCallLatencySummary(await getCallHistoryFromStore(path.dirname(file), last));
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
//#endregion
|
|
154
|
-
//#region extensions/voice-call/src/cli-gateway-call.ts
|
|
155
|
-
const VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS = 5e3;
|
|
156
|
-
const VOICE_CALL_GATEWAY_OPERATION_TIMEOUT_MS = 3e4;
|
|
157
|
-
const VOICE_CALL_GATEWAY_TRANSCRIPT_BUFFER_MS = 1e4;
|
|
158
|
-
const VOICE_CALL_GATEWAY_POLL_INTERVAL_MS = 1e3;
|
|
159
|
-
function isGatewayUnavailableForLocalFallback(err) {
|
|
160
|
-
return isGatewayTransportError(err) && err.kind === "closed" && (err.code === void 0 || err.code === 1006);
|
|
161
|
-
}
|
|
162
|
-
function isGatewayCredentialFailure(err) {
|
|
163
|
-
return err instanceof Error && (err.name === "GatewayCredentialsRequiredError" || err.name === "GatewayExplicitAuthRequiredError" || err.name === "GatewaySecretRefUnavailableError");
|
|
164
|
-
}
|
|
165
|
-
function gatewayOperationalError(err) {
|
|
166
|
-
const message = formatErrorMessage(err);
|
|
167
|
-
const detail = (() => {
|
|
168
|
-
if (isGatewayClientRequestError(err)) return `Gateway responded but voicecall failed: ${message}\nThe running Gateway owns the voice-call runtime; check \`openclaw gateway status\` or restart it.`;
|
|
169
|
-
if (isGatewayCredentialFailure(err)) return `Gateway requires credentials: ${message}\nConfigure gateway.auth or pair this device with \`openclaw devices approve --latest\`.`;
|
|
170
|
-
if (isGatewayTransportError(err)) {
|
|
171
|
-
const url = err.connectionDetails.url;
|
|
172
|
-
if (err.kind === "timeout") return `Gateway at ${url} did not answer within ${err.timeoutMs === void 0 ? "the configured timeout" : `${err.timeoutMs}ms`}: ${message}\nIt may be starting or wedged; check \`openclaw gateway status\`.`;
|
|
173
|
-
return `Gateway connection at ${url} failed: ${message}\nCheck gateway.auth and \`openclaw gateway status\`, then retry.`;
|
|
174
|
-
}
|
|
175
|
-
return `Gateway voicecall request failed: ${message}\nCheck \`openclaw gateway status\`, then retry.`;
|
|
176
|
-
})();
|
|
177
|
-
return new Error(redactSensitiveUrlLikeString(detail));
|
|
178
|
-
}
|
|
179
|
-
function isUnknownMethod(err, method) {
|
|
180
|
-
return formatErrorMessage(err).includes(`unknown method: ${method}`);
|
|
181
|
-
}
|
|
182
|
-
async function callVoiceCallGateway(method, params, opts) {
|
|
183
|
-
try {
|
|
184
|
-
const timeoutMs = typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) ? Math.max(1, Math.ceil(opts.timeoutMs)) : VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS;
|
|
185
|
-
return {
|
|
186
|
-
ok: true,
|
|
187
|
-
payload: await callGatewayFromCli(method, {
|
|
188
|
-
json: true,
|
|
189
|
-
timeout: String(timeoutMs)
|
|
190
|
-
}, params, { progress: false })
|
|
191
|
-
};
|
|
192
|
-
} catch (err) {
|
|
193
|
-
if (isGatewayUnavailableForLocalFallback(err)) return {
|
|
194
|
-
ok: false,
|
|
195
|
-
error: err
|
|
196
|
-
};
|
|
197
|
-
throw gatewayOperationalError(err);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
function resolveOperationTimeout(config) {
|
|
201
|
-
return Math.max(VOICE_CALL_GATEWAY_OPERATION_TIMEOUT_MS, addTimerTimeoutGraceMs(config.ringTimeoutMs) ?? 1);
|
|
202
|
-
}
|
|
203
|
-
function resolveContinueTimeout(config) {
|
|
204
|
-
return clampTimerTimeoutMs(config.transcriptTimeoutMs + VOICE_CALL_GATEWAY_OPERATION_TIMEOUT_MS + VOICE_CALL_GATEWAY_TRANSCRIPT_BUFFER_MS) ?? 1;
|
|
205
|
-
}
|
|
206
|
-
function resolveVoiceCallDeadlineMs(timeoutMs, nowMs = Date.now()) {
|
|
207
|
-
return nowMs + (clampTimerTimeoutMs(timeoutMs) ?? MAX_TIMER_TIMEOUT_MS);
|
|
208
|
-
}
|
|
209
|
-
function readGatewayOperationId(payload) {
|
|
210
|
-
if (isRecord(payload) && typeof payload.operationId === "string" && payload.operationId) return payload.operationId;
|
|
211
|
-
throw new Error("voicecall gateway response missing operationId");
|
|
212
|
-
}
|
|
213
|
-
function readGatewayPollTimeoutMs(payload, fallbackTimeoutMs) {
|
|
214
|
-
if (isRecord(payload) && typeof payload.pollTimeoutMs === "number") return clampTimerTimeoutMs(payload.pollTimeoutMs) ?? fallbackTimeoutMs;
|
|
215
|
-
return fallbackTimeoutMs;
|
|
216
|
-
}
|
|
217
|
-
function readCompletedContinueResult(payload) {
|
|
218
|
-
if (!isRecord(payload)) throw new Error("voicecall gateway response missing operation status");
|
|
219
|
-
if (payload.status === "pending") return { status: "pending" };
|
|
220
|
-
if (payload.status === "failed") return {
|
|
221
|
-
status: "failed",
|
|
222
|
-
error: typeof payload.error === "string" ? payload.error : "continue failed"
|
|
223
|
-
};
|
|
224
|
-
if (payload.status === "completed") return {
|
|
225
|
-
status: "completed",
|
|
226
|
-
result: payload.result
|
|
227
|
-
};
|
|
228
|
-
throw new Error("voicecall gateway response has unknown operation status");
|
|
229
|
-
}
|
|
230
|
-
async function pollContinueGateway(payload, fallbackTimeoutMs) {
|
|
231
|
-
if (!isRecord(payload) || typeof payload.operationId !== "string") return payload;
|
|
232
|
-
const params = {
|
|
233
|
-
operationId: readGatewayOperationId(payload),
|
|
234
|
-
timeoutMs: readGatewayPollTimeoutMs(payload, fallbackTimeoutMs)
|
|
235
|
-
};
|
|
236
|
-
const deadlineMs = resolveVoiceCallDeadlineMs(params.timeoutMs);
|
|
237
|
-
for (;;) {
|
|
238
|
-
const remainingMs = deadlineMs - Date.now();
|
|
239
|
-
if (remainingMs <= 0) break;
|
|
240
|
-
const gateway = await callVoiceCallGateway("voicecall.continue.result", { operationId: params.operationId }, { timeoutMs: Math.min(VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS, remainingMs) });
|
|
241
|
-
if (!gateway.ok) throw new Error(`gateway unavailable while waiting for voicecall continue result: ${formatErrorMessage(gateway.error)}`);
|
|
242
|
-
const result = readCompletedContinueResult(gateway.payload);
|
|
243
|
-
if (result.status === "completed") return result.result;
|
|
244
|
-
if (result.status === "failed") throw new Error(result.error);
|
|
245
|
-
const sleepMs = Math.min(VOICE_CALL_GATEWAY_POLL_INTERVAL_MS, deadlineMs - Date.now());
|
|
246
|
-
if (sleepMs <= 0) break;
|
|
247
|
-
await sleep(sleepMs);
|
|
248
|
-
}
|
|
249
|
-
throw new Error("voicecall continue timed out waiting for gateway operation");
|
|
250
|
-
}
|
|
251
|
-
async function ensureStandaloneRuntime(params) {
|
|
252
|
-
try {
|
|
253
|
-
return await params.ensureRuntime();
|
|
254
|
-
} catch (err) {
|
|
255
|
-
if (err instanceof Error && "code" in err && err.code === "EADDRINUSE") throw new Error(`Voice-call webhook port ${params.config.serve.port} is already in use. A running Gateway probably already serves it; operational commands route through that Gateway. Check \`openclaw gateway status\` and retry.`, { cause: err });
|
|
256
|
-
throw err;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
async function runGatewayManagerCommand(params) {
|
|
260
|
-
const gateway = await params.gatewayCall();
|
|
261
|
-
if (gateway.ok) {
|
|
262
|
-
writeCliJson(params.resolveGatewayPayload ? await params.resolveGatewayPayload(gateway.payload) : gateway.payload);
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
const runtime = await ensureStandaloneRuntime(params);
|
|
266
|
-
const result = await params.managerFallback(runtime.manager);
|
|
267
|
-
if (!result.success) throw new Error(result.error || `${params.failureLabel} failed`);
|
|
268
|
-
writeCliJson(result);
|
|
269
|
-
}
|
|
270
|
-
function readGatewayCallId(payload, invalidCallIdMessage) {
|
|
271
|
-
if (isRecord(payload) && typeof payload.callId === "string") {
|
|
272
|
-
if (!invalidCallIdMessage || payload.callId) return payload.callId;
|
|
273
|
-
}
|
|
274
|
-
if (invalidCallIdMessage) throw new Error(invalidCallIdMessage);
|
|
275
|
-
if (isRecord(payload) && typeof payload.error === "string") throw new Error(payload.error);
|
|
276
|
-
throw new Error("voicecall gateway response missing callId");
|
|
277
|
-
}
|
|
278
|
-
async function initiateVoiceCall(params) {
|
|
279
|
-
const mode = params.mode === "notify" || params.mode === "conversation" ? params.mode : params.defaultMode;
|
|
280
|
-
const gateway = await callVoiceCallGateway(params.method, {
|
|
281
|
-
...params.to ? { to: params.to } : {},
|
|
282
|
-
...params.message ? { message: params.message } : {},
|
|
283
|
-
...mode ? { mode } : {}
|
|
284
|
-
}, { timeoutMs: resolveOperationTimeout(params.config) });
|
|
285
|
-
if (gateway.ok) return readGatewayCallId(gateway.payload, params.failureMessage);
|
|
286
|
-
const runtime = await ensureStandaloneRuntime(params);
|
|
287
|
-
const to = params.to ?? runtime.config.toNumber;
|
|
288
|
-
if (!to) throw new Error("Missing --to and no toNumber configured");
|
|
289
|
-
const result = await runtime.manager.initiateCall(to, void 0, {
|
|
290
|
-
message: params.message,
|
|
291
|
-
mode
|
|
292
|
-
});
|
|
293
|
-
if (!result.success) throw new Error(result.error || params.failureMessage || "initiate failed");
|
|
294
|
-
if (params.failureMessage && !result.callId) throw new Error(params.failureMessage);
|
|
295
|
-
return result.callId;
|
|
296
|
-
}
|
|
297
|
-
//#endregion
|
|
298
|
-
//#region extensions/voice-call/src/cli.ts
|
|
299
|
-
function resolveMode(input) {
|
|
300
|
-
const raw = normalizeOptionalLowercaseString(input) ?? "";
|
|
301
|
-
if (raw === "serve" || raw === "off") return raw;
|
|
302
|
-
return "funnel";
|
|
303
|
-
}
|
|
304
|
-
function resolveDefaultStorePath(config) {
|
|
305
|
-
const base = config.store?.trim() ? resolveUserPath(config.store) : resolveDefaultVoiceCallStoreDir();
|
|
306
|
-
return path.join(base, "calls.jsonl");
|
|
307
|
-
}
|
|
308
|
-
function buildSetupStatus(config) {
|
|
309
|
-
const validation = validateProviderConfig(config);
|
|
310
|
-
const webhookExposure = resolveWebhookExposureStatus(config);
|
|
311
|
-
const checks = [
|
|
312
|
-
{
|
|
313
|
-
id: "plugin-enabled",
|
|
314
|
-
ok: config.enabled,
|
|
315
|
-
message: config.enabled ? "Voice Call plugin is enabled" : "Enable plugins.entries.voice-call.enabled"
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
id: "provider",
|
|
319
|
-
ok: Boolean(config.provider),
|
|
320
|
-
message: config.provider ? `Provider configured: ${config.provider}` : "Set plugins.entries.voice-call.config.provider"
|
|
321
|
-
},
|
|
322
|
-
{
|
|
323
|
-
id: "provider-config",
|
|
324
|
-
ok: validation.valid,
|
|
325
|
-
message: validation.valid ? "Provider credentials/config look complete" : validation.errors.join("; ")
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
id: "webhook-exposure",
|
|
329
|
-
ok: webhookExposure.ok,
|
|
330
|
-
message: webhookExposure.message
|
|
331
|
-
},
|
|
332
|
-
{
|
|
333
|
-
id: "mode",
|
|
334
|
-
ok: !(config.streaming.enabled && config.realtime.enabled),
|
|
335
|
-
message: config.streaming.enabled && config.realtime.enabled ? "streaming.enabled and realtime.enabled cannot both be true" : config.realtime.enabled ? `Realtime voice enabled (${config.realtime.provider ?? "first registered provider"})` : config.streaming.enabled ? `Streaming transcription enabled (${config.streaming.provider ?? "first registered provider"})` : "Notify/conversation calls use normal TTS/STT flow"
|
|
336
|
-
}
|
|
337
|
-
];
|
|
338
|
-
return {
|
|
339
|
-
ok: checks.every((check) => check.ok),
|
|
340
|
-
checks
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
function writeSetupStatus(status) {
|
|
344
|
-
writeCliLine("Voice Call setup: %s", status.ok ? "OK" : "needs attention");
|
|
345
|
-
for (const check of status.checks) writeCliLine("%s %s: %s", check.ok ? "OK" : "FAIL", check.id, check.message);
|
|
346
|
-
}
|
|
347
|
-
function registerVoiceCallCli(params) {
|
|
348
|
-
const { program, config, ensureRuntime, stateRuntime } = params;
|
|
349
|
-
const ensureHistoryStateRuntime = () => {
|
|
350
|
-
if (stateRuntime) setVoiceCallStateRuntime({ state: stateRuntime });
|
|
351
|
-
};
|
|
352
|
-
const root = program.command("voicecall").description("Voice call utilities").addHelpText("after", () => `\nDocs: https://docs.openclaw.ai/cli/voicecall\n`);
|
|
353
|
-
root.command("setup").description("Show Voice Call provider and webhook setup status").option("--json", "Print machine-readable JSON").action((options) => {
|
|
354
|
-
const status = buildSetupStatus(config);
|
|
355
|
-
if (options.json) {
|
|
356
|
-
writeCliJson(status);
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
writeSetupStatus(status);
|
|
360
|
-
});
|
|
361
|
-
root.command("smoke").description("Check Voice Call readiness and optionally place a short outbound test call").option("-t, --to <phone>", "Phone number to call for a live smoke").option("--message <text>", "Message to speak during the smoke call", "OpenClaw voice call smoke test.").option("--mode <mode>", "Call mode: notify or conversation", "notify").option("--yes", "Actually place the live outbound call").option("--json", "Print machine-readable JSON").action(async (options) => {
|
|
362
|
-
const setup = buildSetupStatus(config);
|
|
363
|
-
if (!setup.ok) {
|
|
364
|
-
if (options.json) writeCliJson({
|
|
365
|
-
ok: false,
|
|
366
|
-
setup
|
|
367
|
-
});
|
|
368
|
-
else writeSetupStatus(setup);
|
|
369
|
-
process.exitCode = 1;
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
if (!options.to) {
|
|
373
|
-
if (options.json) writeCliJson({
|
|
374
|
-
ok: true,
|
|
375
|
-
setup,
|
|
376
|
-
liveCall: false
|
|
377
|
-
});
|
|
378
|
-
else {
|
|
379
|
-
writeSetupStatus(setup);
|
|
380
|
-
writeCliLine("live-call: skipped (pass --to and --yes to place one)");
|
|
381
|
-
}
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
if (!options.yes) {
|
|
385
|
-
if (options.json) writeCliJson({
|
|
386
|
-
ok: true,
|
|
387
|
-
setup,
|
|
388
|
-
liveCall: false,
|
|
389
|
-
wouldCall: options.to
|
|
390
|
-
});
|
|
391
|
-
else {
|
|
392
|
-
writeSetupStatus(setup);
|
|
393
|
-
writeCliLine("live-call: dry run for %s (add --yes to place it)", options.to);
|
|
394
|
-
}
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
const callId = await initiateVoiceCall({
|
|
398
|
-
ensureRuntime,
|
|
399
|
-
config,
|
|
400
|
-
method: "voicecall.start",
|
|
401
|
-
to: options.to,
|
|
402
|
-
message: options.message,
|
|
403
|
-
mode: options.mode,
|
|
404
|
-
defaultMode: "notify",
|
|
405
|
-
failureMessage: "smoke call failed"
|
|
406
|
-
});
|
|
407
|
-
if (options.json) {
|
|
408
|
-
writeCliJson({
|
|
409
|
-
ok: true,
|
|
410
|
-
setup,
|
|
411
|
-
liveCall: true,
|
|
412
|
-
callId
|
|
413
|
-
});
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
writeSetupStatus(setup);
|
|
417
|
-
writeCliLine("live-call: started %s", callId);
|
|
418
|
-
});
|
|
419
|
-
root.command("call").description("Initiate an outbound voice call").requiredOption("-m, --message <text>", "Message to speak when call connects").option("-t, --to <phone>", "Phone number to call (E.164 format, uses config toNumber if not set)").option("--mode <mode>", "Call mode: notify (hangup after message) or conversation (stay open)", "conversation").action(async (options) => {
|
|
420
|
-
writeCliJson({ callId: await initiateVoiceCall({
|
|
421
|
-
ensureRuntime,
|
|
422
|
-
config,
|
|
423
|
-
method: "voicecall.initiate",
|
|
424
|
-
to: options.to,
|
|
425
|
-
message: options.message,
|
|
426
|
-
mode: options.mode
|
|
427
|
-
}) });
|
|
428
|
-
});
|
|
429
|
-
root.command("start").description("Alias for voicecall call").requiredOption("--to <phone>", "Phone number to call").option("--message <text>", "Message to speak when call connects").option("--mode <mode>", "Call mode: notify (hangup after message) or conversation (stay open)", "conversation").action(async (options) => {
|
|
430
|
-
writeCliJson({ callId: await initiateVoiceCall({
|
|
431
|
-
ensureRuntime,
|
|
432
|
-
config,
|
|
433
|
-
method: "voicecall.start",
|
|
434
|
-
to: options.to,
|
|
435
|
-
message: options.message,
|
|
436
|
-
mode: options.mode
|
|
437
|
-
}) });
|
|
438
|
-
});
|
|
439
|
-
root.command("continue").description("Speak a message and wait for a response").requiredOption("--call-id <id>", "Call ID").requiredOption("--message <text>", "Message to speak").action(async (options) => {
|
|
440
|
-
const gatewayParams = {
|
|
441
|
-
callId: options.callId,
|
|
442
|
-
message: options.message
|
|
443
|
-
};
|
|
444
|
-
const continueTimeoutMs = resolveContinueTimeout(config);
|
|
445
|
-
await runGatewayManagerCommand({
|
|
446
|
-
config,
|
|
447
|
-
ensureRuntime,
|
|
448
|
-
gatewayCall: async () => {
|
|
449
|
-
try {
|
|
450
|
-
return await callVoiceCallGateway("voicecall.continue.start", gatewayParams, { timeoutMs: resolveOperationTimeout(config) });
|
|
451
|
-
} catch (err) {
|
|
452
|
-
if (!isUnknownMethod(err, "voicecall.continue.start")) throw err;
|
|
453
|
-
return callVoiceCallGateway("voicecall.continue", gatewayParams, { timeoutMs: continueTimeoutMs });
|
|
454
|
-
}
|
|
455
|
-
},
|
|
456
|
-
resolveGatewayPayload: (payload) => pollContinueGateway(payload, continueTimeoutMs),
|
|
457
|
-
managerFallback: (manager) => manager.continueCall(options.callId, options.message),
|
|
458
|
-
failureLabel: "continue"
|
|
459
|
-
});
|
|
460
|
-
});
|
|
461
|
-
root.command("speak").description("Speak a message without waiting for response").requiredOption("--call-id <id>", "Call ID").requiredOption("--message <text>", "Message to speak").action(async (options) => {
|
|
462
|
-
await runGatewayManagerCommand({
|
|
463
|
-
config,
|
|
464
|
-
ensureRuntime,
|
|
465
|
-
gatewayCall: () => callVoiceCallGateway("voicecall.speak", {
|
|
466
|
-
callId: options.callId,
|
|
467
|
-
message: options.message
|
|
468
|
-
}),
|
|
469
|
-
managerFallback: (manager) => manager.speak(options.callId, options.message),
|
|
470
|
-
failureLabel: "speak"
|
|
471
|
-
});
|
|
472
|
-
});
|
|
473
|
-
root.command("dtmf").description("Send DTMF digits to an active call").requiredOption("--call-id <id>", "Call ID").requiredOption("--digits <digits>", "DTMF digits").action(async (options) => {
|
|
474
|
-
await runGatewayManagerCommand({
|
|
475
|
-
config,
|
|
476
|
-
ensureRuntime,
|
|
477
|
-
gatewayCall: () => callVoiceCallGateway("voicecall.dtmf", {
|
|
478
|
-
callId: options.callId,
|
|
479
|
-
digits: options.digits
|
|
480
|
-
}),
|
|
481
|
-
managerFallback: (manager) => manager.sendDtmf(options.callId, options.digits),
|
|
482
|
-
failureLabel: "dtmf"
|
|
483
|
-
});
|
|
484
|
-
});
|
|
485
|
-
root.command("end").description("Hang up an active call").requiredOption("--call-id <id>", "Call ID").action(async (options) => {
|
|
486
|
-
await runGatewayManagerCommand({
|
|
487
|
-
config,
|
|
488
|
-
ensureRuntime,
|
|
489
|
-
gatewayCall: () => callVoiceCallGateway("voicecall.end", { callId: options.callId }),
|
|
490
|
-
managerFallback: (manager) => manager.endCall(options.callId),
|
|
491
|
-
failureLabel: "end"
|
|
492
|
-
});
|
|
493
|
-
});
|
|
494
|
-
root.command("status").description("Show call status").option("--call-id <id>", "Call ID").option("--json", "Print machine-readable JSON").action(async (options) => {
|
|
495
|
-
const gateway = await callVoiceCallGateway("voicecall.status", options.callId ? { callId: options.callId } : void 0);
|
|
496
|
-
if (gateway.ok) {
|
|
497
|
-
if (options.callId && isRecord(gateway.payload)) {
|
|
498
|
-
if (gateway.payload.found === true && "call" in gateway.payload) {
|
|
499
|
-
writeCliJson(gateway.payload.call);
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
if (gateway.payload.found === false) {
|
|
503
|
-
writeCliJson({ found: false });
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
writeCliJson(gateway.payload);
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
ensureHistoryStateRuntime();
|
|
511
|
-
const storePath = path.dirname(resolveDefaultStorePath(config));
|
|
512
|
-
if (options.callId) {
|
|
513
|
-
const persisted = await findCallMatchesInStore(storePath, options.callId);
|
|
514
|
-
writeCliJson(persisted.byCallId ?? persisted.byProviderCallId ?? { found: false });
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
writeCliJson({
|
|
518
|
-
found: true,
|
|
519
|
-
calls: Array.from(loadActiveCallsFromStore(storePath).activeCalls.values())
|
|
520
|
-
});
|
|
521
|
-
});
|
|
522
|
-
registerVoiceCallLogs({
|
|
523
|
-
root,
|
|
524
|
-
defaultFile: resolveDefaultStorePath(config),
|
|
525
|
-
ensureHistoryStateRuntime
|
|
526
|
-
});
|
|
527
|
-
root.command("expose").description("Enable/disable Tailscale serve/funnel for the webhook").option("--mode <mode>", "off | serve (tailnet) | funnel (public)", "funnel").option("--path <path>", "Tailscale path to expose (recommend matching serve.path)").option("--port <port>", "Local webhook port").option("--serve-path <path>", "Local webhook path").action(async (options) => {
|
|
528
|
-
const mode = resolveMode(options.mode ?? "funnel");
|
|
529
|
-
const servePort = parseCliInteger(options.port ?? String(config.serve.port ?? 3334), "--port", {
|
|
530
|
-
min: 1,
|
|
531
|
-
max: MAX_TCP_PORT
|
|
532
|
-
});
|
|
533
|
-
const servePath = options.servePath ?? config.serve.path ?? "/voice/webhook";
|
|
534
|
-
const tsPath = options.path ?? config.tailscale?.path ?? servePath;
|
|
535
|
-
const streamExposurePaths = resolveVoiceCallStreamExposurePaths(config, {
|
|
536
|
-
publicWebhookPath: tsPath,
|
|
537
|
-
localWebhookPath: servePath
|
|
538
|
-
});
|
|
539
|
-
const streamPaths = streamExposurePaths.map(({ publicPath }) => publicPath);
|
|
540
|
-
const localUrl = `http://127.0.0.1:${servePort}${servePath}`;
|
|
541
|
-
if (mode === "off") {
|
|
542
|
-
for (const exposurePath of [tsPath, ...streamPaths]) for (const tailscaleMode of ["serve", "funnel"]) await cleanupTailscaleExposureRoute({
|
|
543
|
-
mode: tailscaleMode,
|
|
544
|
-
port: config.tailscale.port,
|
|
545
|
-
path: exposurePath
|
|
546
|
-
});
|
|
547
|
-
writeCliJson({
|
|
548
|
-
ok: true,
|
|
549
|
-
mode: "off",
|
|
550
|
-
path: tsPath,
|
|
551
|
-
streamPaths
|
|
552
|
-
});
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
const publicUrl = await setupTailscaleExposureRoutes({
|
|
556
|
-
mode,
|
|
557
|
-
port: config.tailscale.port,
|
|
558
|
-
routes: [{
|
|
559
|
-
path: tsPath,
|
|
560
|
-
localUrl
|
|
561
|
-
}, ...streamExposurePaths.map(({ publicPath, localPath }) => ({
|
|
562
|
-
path: publicPath,
|
|
563
|
-
localUrl: `http://127.0.0.1:${servePort}${localPath}`
|
|
564
|
-
}))]
|
|
565
|
-
});
|
|
566
|
-
const tsInfo = publicUrl ? null : await getTailscaleSelfInfo();
|
|
567
|
-
const enableUrl = tsInfo?.nodeId ? `https://login.tailscale.com/f/${mode}?node=${tsInfo.nodeId}` : null;
|
|
568
|
-
writeCliJson({
|
|
569
|
-
ok: Boolean(publicUrl),
|
|
570
|
-
mode,
|
|
571
|
-
path: tsPath,
|
|
572
|
-
streamPaths,
|
|
573
|
-
localUrl,
|
|
574
|
-
publicUrl,
|
|
575
|
-
hint: publicUrl ? void 0 : {
|
|
576
|
-
note: "Tailscale serve/funnel may be disabled on this tailnet (or require admin enable).",
|
|
577
|
-
enableUrl
|
|
578
|
-
}
|
|
579
|
-
});
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
//#endregion
|
|
583
15
|
//#region extensions/voice-call/src/command-service.ts
|
|
584
16
|
var VoiceCallCommandInputError = class extends Error {};
|
|
585
17
|
function toVoiceCallStatus(call) {
|
|
@@ -702,7 +134,7 @@ function createVoiceCallCommandService(ensureRuntime) {
|
|
|
702
134
|
//#endregion
|
|
703
135
|
//#region extensions/voice-call/src/gateway-continue-operation.ts
|
|
704
136
|
const VOICE_CALL_CONTINUE_OPERATION_BUFFER_MS = 3e4;
|
|
705
|
-
const VOICE_CALL_CONTINUE_OPERATION_CLEANUP_MS =
|
|
137
|
+
const VOICE_CALL_CONTINUE_OPERATION_CLEANUP_MS = 3e5;
|
|
706
138
|
/** Create a process-local operation store for gateway continue-call polling. */
|
|
707
139
|
function createVoiceCallContinueOperationStore(params) {
|
|
708
140
|
const operations = /* @__PURE__ */ new Map();
|
|
@@ -1081,13 +513,17 @@ var voice_call_default = definePluginEntry({
|
|
|
1081
513
|
}
|
|
1082
514
|
}
|
|
1083
515
|
}));
|
|
1084
|
-
api.registerCli(({ program }) =>
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
516
|
+
api.registerCli(async ({ program }) => {
|
|
517
|
+
const { registerVoiceCallCli } = await import("./cli-DNq2qUU0.js");
|
|
518
|
+
registerVoiceCallCli({
|
|
519
|
+
program,
|
|
520
|
+
config,
|
|
521
|
+
coreConfig: api.config,
|
|
522
|
+
ensureRuntime,
|
|
523
|
+
stateRuntime: api.runtime.state,
|
|
524
|
+
logger: api.logger
|
|
525
|
+
});
|
|
526
|
+
}, {
|
|
1091
527
|
commands: ["voicecall"],
|
|
1092
528
|
descriptors: [VOICE_CALL_CLI_DESCRIPTOR]
|
|
1093
529
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as reserveWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-
|
|
1
|
+
import { n as reserveWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-CCQSoBNF.js";
|
|
2
2
|
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
//#region extensions/voice-call/src/providers/mock.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as reconstructWebhookUrl, o as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
1
|
+
import { s as getHeader, v as escapeXml } from "./runtime-entry-gE2Yd4V8.js";
|
|
2
|
+
import { a as reconstructWebhookUrl, o as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-9je4i7qf.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
|