@openclaw/voice-call 2026.7.2-beta.7 → 2026.8.1-beta.3
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 +4 -3
- package/dist/doctor-contract-api.js +11 -8
- package/dist/{guarded-json-api-DS9sZa3C.js → guarded-json-api-j4OEG0fR.js} +24 -46
- package/dist/index.js +644 -913
- package/dist/{mock-YsvBTX-7.js → mock-Dv7KxYGt.js} +5 -4
- package/dist/{plivo-vZ--Wqxv.js → plivo-BbVnahnQ.js} +45 -8
- package/dist/{realtime-handler--i_OERv6.js → realtime-handler-BaMEKbmJ.js} +579 -209
- package/dist/{response-generator-D-7ApVfT.js → response-generator-tjyX4tDq.js} +70 -15
- package/dist/{runtime-entry-D0UJiLfi.js → runtime-entry-DK8kPCGC.js} +687 -355
- package/dist/runtime-entry.js +1 -1
- package/dist/setup-api.js +17 -24
- package/dist/{store-path-nYL-yM0S.js → store-path-51_oC_xn.js} +70 -13
- package/dist/{telnyx-Dx6cfLvV.js → telnyx-DO8dFQcb.js} +14 -9
- package/dist/{twilio-D_D8KysU.js → twilio-JDt_1nlz.js} +58 -51
- package/dist/{webhook-replay-aaP79dI8.js → webhook-replay-FQrEGVSM.js} +20 -6
- package/openclaw.plugin.json +69 -6
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ Put under `plugins.entries.voice-call.config`:
|
|
|
38
38
|
provider: "twilio", // or "telnyx" | "plivo" | "mock"
|
|
39
39
|
fromNumber: "+15550001234",
|
|
40
40
|
toNumber: "+15550005678",
|
|
41
|
-
sessionScope: "per-phone", // or "per-call"
|
|
41
|
+
sessionScope: "per-phone", // or "per-call" | "main"
|
|
42
42
|
|
|
43
43
|
twilio: {
|
|
44
44
|
accountSid: "ACxxxxxxxx",
|
|
@@ -67,7 +67,7 @@ Put under `plugins.entries.voice-call.config`:
|
|
|
67
67
|
// Public exposure (pick one):
|
|
68
68
|
// publicUrl: "https://example.ngrok.app/voice/webhook",
|
|
69
69
|
// tunnel: { provider: "ngrok" },
|
|
70
|
-
// tailscale: { mode: "funnel", path: "/voice/webhook" }
|
|
70
|
+
// tailscale: { mode: "funnel", port: 8443, path: "/voice/webhook" }
|
|
71
71
|
|
|
72
72
|
outbound: {
|
|
73
73
|
defaultMode: "notify", // or "conversation"
|
|
@@ -98,13 +98,14 @@ Put under `plugins.entries.voice-call.config`:
|
|
|
98
98
|
Notes:
|
|
99
99
|
|
|
100
100
|
- Twilio/Telnyx/Plivo require a **publicly reachable** webhook URL.
|
|
101
|
+
- `tailscale.port` defaults to `443` and owns the external HTTPS port for both legacy `tailscale.mode` and unified Tailscale tunnel providers. Funnel supports `443`, `8443`, or `10000`; Serve accepts any valid TCP port.
|
|
101
102
|
- Twilio defaults to US1. For a non-US Region, set `twilio.region` to `ie1` or `au1` and use credentials created in that Region; see [Twilio's regional REST API guide](https://www.twilio.com/docs/global-infrastructure/using-the-twilio-rest-api-in-a-non-us-region).
|
|
102
103
|
- `mock` is a local dev provider (no network calls).
|
|
103
104
|
- Telnyx requires `telnyx.publicKey` (or `TELNYX_PUBLIC_KEY`) unless `skipSignatureVerification` is true.
|
|
104
105
|
- Runtime accepts canonical config only. If older configs still use `provider: "log"`, `twilio.from`, or legacy `streaming.*` OpenAI keys, run `openclaw doctor --fix` to rewrite them.
|
|
105
106
|
- advanced webhook, streaming, and tunnel notes: `https://docs.openclaw.ai/plugins/voice-call`
|
|
106
107
|
- `responseModel` is optional. When unset, voice responses use the runtime default model.
|
|
107
|
-
- `sessionScope` defaults to `per-phone`, preserving caller memory across calls. Use `per-call` for reception, booking, IVR, and bridge flows where each carrier call should start fresh.
|
|
108
|
+
- `sessionScope` defaults to `per-phone`, preserving caller memory across calls. Use `per-call` for reception, booking, IVR, and bridge flows where each carrier call should start fresh. Use `main` to share the configured agent's main session, honoring core `session.mainKey` and global scope.
|
|
108
109
|
- `realtime.consultThinkingLevel` is optional. When set, it overrides the thinking level used by the model behind realtime `openclaw_agent_consult` calls.
|
|
109
110
|
- `realtime.consultFastMode` is optional. When set, it toggles fast mode for realtime `openclaw_agent_consult` calls.
|
|
110
111
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { a as CALL_RECORD_EVENT_META_MAX_ENTRIES, c as buildVoiceCallLegacyJsonlEventKey, f as parseVoiceCallRecordLine, h as resolveVoiceCallLegacyCallLogPath, i as CALL_RECORD_EVENT_CHUNKS_NAMESPACE, m as prepareVoiceCallRecordForStorage, n as CALL_RECORD_CHUNK_MAX_ENTRIES, o as MAX_CALL_RECORD_EVENTS, r as CALL_RECORD_EVENTS_NAMESPACE, s as RAW_CALL_RECORD_CHUNK_BYTES, t as resolveDefaultVoiceCallStoreDir } from "./store-path-
|
|
1
|
+
import { a as CALL_RECORD_EVENT_META_MAX_ENTRIES, c as buildVoiceCallLegacyJsonlEventKey, f as parseVoiceCallRecordLine, h as resolveVoiceCallLegacyCallLogPath, i as CALL_RECORD_EVENT_CHUNKS_NAMESPACE, m as prepareVoiceCallRecordForStorage, n as CALL_RECORD_CHUNK_MAX_ENTRIES, o as MAX_CALL_RECORD_EVENTS, r as CALL_RECORD_EVENTS_NAMESPACE, s as RAW_CALL_RECORD_CHUNK_BYTES, t as resolveDefaultVoiceCallStoreDir } from "./store-path-51_oC_xn.js";
|
|
2
2
|
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
|
3
|
+
import { asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import os from "node:os";
|
|
5
6
|
import fs from "node:fs/promises";
|
|
6
|
-
import { archiveLegacyStateSource
|
|
7
|
+
import { archiveLegacyStateSource } from "openclaw/plugin-sdk/runtime-doctor-migrations";
|
|
7
8
|
//#region extensions/voice-call/doctor-contract-api.ts
|
|
8
9
|
/** Resolve home from doctor env with OS fallback. */
|
|
9
10
|
function resolveHome(env) {
|
|
@@ -26,21 +27,18 @@ function getVoiceCallConfigStore(config) {
|
|
|
26
27
|
}
|
|
27
28
|
return "";
|
|
28
29
|
}
|
|
29
|
-
function asRecord(value) {
|
|
30
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
31
|
-
}
|
|
32
30
|
/** Return Voice Call agents whose templated core session stores need migration. */
|
|
33
31
|
function resolveSessionStoreAgentIds(params) {
|
|
34
32
|
const agentIds = /* @__PURE__ */ new Set();
|
|
35
33
|
for (const pluginId of ["voice-call", "@openclaw/voice-call"]) {
|
|
36
34
|
const entry = params.cfg.plugins?.entries?.[pluginId];
|
|
37
35
|
if (!entry) continue;
|
|
38
|
-
const config = entry.config === void 0 ? {} :
|
|
36
|
+
const config = entry.config === void 0 ? {} : asOptionalRecord(entry.config);
|
|
39
37
|
if (!config) continue;
|
|
40
38
|
agentIds.add(normalizeAgentId(typeof config.agentId === "string" ? config.agentId : void 0));
|
|
41
|
-
const numbers =
|
|
39
|
+
const numbers = asOptionalRecord(config.numbers);
|
|
42
40
|
for (const route of Object.values(numbers ?? {})) {
|
|
43
|
-
const agentId =
|
|
41
|
+
const agentId = asOptionalRecord(route)?.agentId;
|
|
44
42
|
if (typeof agentId === "string") agentIds.add(normalizeAgentId(agentId));
|
|
45
43
|
}
|
|
46
44
|
}
|
|
@@ -61,7 +59,10 @@ function resolveVoiceCallStateDatabaseEnv(params) {
|
|
|
61
59
|
function describeVoiceCallSchemaMigration(migration) {
|
|
62
60
|
switch (migration.kind) {
|
|
63
61
|
case "agent-databases-composite-primary-key": return "agent database registry primary key -> agent_id,path";
|
|
62
|
+
case "agent-databases-relative-paths-v9": return "agent database registry paths -> state-relative paths";
|
|
64
63
|
case "audit-events-v2": return "audit event ledger -> versioned message lifecycle schema";
|
|
64
|
+
case "commitments-retirement-v7": return "retired commitments storage -> removed table and indexes";
|
|
65
|
+
case "worker-placement-execution-mode-v8": return "cloud worker placements -> execution-mode claims";
|
|
65
66
|
case "operator-approvals-system-agent": return "operator approvals -> OpenClaw system changes";
|
|
66
67
|
case "session-watch-cursor-provenance-v4": return "session watch cursors -> provenance column";
|
|
67
68
|
case "strict-tables-v3": return "tables -> SQLite STRICT typing";
|
|
@@ -185,12 +186,14 @@ const stateMigrations = [{
|
|
|
185
186
|
id: "voice-call-calls-jsonl-to-plugin-state",
|
|
186
187
|
label: "Voice Call call log",
|
|
187
188
|
async detectLegacyState(params) {
|
|
189
|
+
const { detectOpenClawStateDatabaseSchemaMigrations } = await import("openclaw/plugin-sdk/doctor-repair-runtime");
|
|
188
190
|
const { entries } = await readLegacyCallRecords(resolveVoiceCallLegacyCallLogPath(resolveVoiceCallStorePath(params)));
|
|
189
191
|
const schemaMigrations = detectOpenClawStateDatabaseSchemaMigrations({ env: resolveVoiceCallStateDatabaseEnv(params) });
|
|
190
192
|
if (entries.length === 0 && schemaMigrations.length === 0) return null;
|
|
191
193
|
return { preview: [...schemaMigrations.map((migration) => `- Voice Call SQLite schema: ${describeVoiceCallSchemaMigration(migration)}`), ...entries.length > 0 ? [`- Voice Call call log: ${entries.length} ${entries.length === 1 ? "record" : "records"} -> plugin state (${CALL_RECORD_EVENTS_NAMESPACE})`] : []] };
|
|
192
194
|
},
|
|
193
195
|
async migrateLegacyState(params) {
|
|
196
|
+
const { detectOpenClawStateDatabaseSchemaMigrations, repairOpenClawStateDatabaseSchema } = await import("openclaw/plugin-sdk/doctor-repair-runtime");
|
|
194
197
|
const changes = [];
|
|
195
198
|
const warnings = [];
|
|
196
199
|
const filePath = resolveVoiceCallLegacyCallLogPath(resolveVoiceCallStorePath(params));
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import { n as normalizeProxyIp, s as getHeader } from "./runtime-entry-
|
|
4
|
-
import { n as
|
|
3
|
+
import { n as normalizeProxyIp, s as getHeader } from "./runtime-entry-DK8kPCGC.js";
|
|
4
|
+
import { n as reserveWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-FQrEGVSM.js";
|
|
5
5
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
6
6
|
import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
|
|
7
7
|
import { normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
8
8
|
import crypto from "node:crypto";
|
|
9
|
-
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
9
|
+
import { redactSensitiveText, safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
10
10
|
import { isIP } from "node:net";
|
|
11
11
|
import { readResponseTextPrefix, readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
|
|
12
12
|
//#region extensions/voice-call/src/webhook-security.ts
|
|
@@ -256,8 +256,7 @@ function verifyTelnyxWebhook(ctx, publicKey, options) {
|
|
|
256
256
|
return {
|
|
257
257
|
ok: true,
|
|
258
258
|
reason: "verification skipped (dev mode)",
|
|
259
|
-
|
|
260
|
-
verifiedRequestKey: replayKey
|
|
259
|
+
...reserveWebhookReplay(telnyxReplayCache, replayKey)
|
|
261
260
|
};
|
|
262
261
|
}
|
|
263
262
|
if (!publicKey) return {
|
|
@@ -293,8 +292,7 @@ function verifyTelnyxWebhook(ctx, publicKey, options) {
|
|
|
293
292
|
const replayKey = `telnyx:${sha256Hex(`${timestamp}\n${canonicalSignature}\n${ctx.rawBody}`)}`;
|
|
294
293
|
return {
|
|
295
294
|
ok: true,
|
|
296
|
-
|
|
297
|
-
verifiedRequestKey: replayKey
|
|
295
|
+
...reserveWebhookReplay(telnyxReplayCache, replayKey)
|
|
298
296
|
};
|
|
299
297
|
} catch (err) {
|
|
300
298
|
return {
|
|
@@ -312,8 +310,7 @@ function verifyTwilioWebhook(ctx, authToken, options) {
|
|
|
312
310
|
return {
|
|
313
311
|
ok: true,
|
|
314
312
|
reason: "verification skipped (dev mode)",
|
|
315
|
-
|
|
316
|
-
verifiedRequestKey: replayKey
|
|
313
|
+
...reserveWebhookReplay(twilioReplayCache, replayKey)
|
|
317
314
|
};
|
|
318
315
|
}
|
|
319
316
|
const signature = getHeader(ctx.headers, "x-twilio-signature");
|
|
@@ -339,8 +336,7 @@ function verifyTwilioWebhook(ctx, authToken, options) {
|
|
|
339
336
|
return {
|
|
340
337
|
ok: true,
|
|
341
338
|
verificationUrl,
|
|
342
|
-
|
|
343
|
-
verifiedRequestKey: replayKey
|
|
339
|
+
...reserveWebhookReplay(twilioReplayCache, replayKey)
|
|
344
340
|
};
|
|
345
341
|
}
|
|
346
342
|
const variants = /* @__PURE__ */ new Set();
|
|
@@ -363,8 +359,7 @@ function verifyTwilioWebhook(ctx, authToken, options) {
|
|
|
363
359
|
return {
|
|
364
360
|
ok: true,
|
|
365
361
|
verificationUrl: candidateUrl,
|
|
366
|
-
|
|
367
|
-
verifiedRequestKey: replayKey
|
|
362
|
+
...reserveWebhookReplay(twilioReplayCache, replayKey)
|
|
368
363
|
};
|
|
369
364
|
}
|
|
370
365
|
const isNgrokFreeTier = verificationUrl.includes(".ngrok-free.app") || verificationUrl.includes(".ngrok.io");
|
|
@@ -456,8 +451,7 @@ function verifyPlivoWebhook(ctx, authToken, options) {
|
|
|
456
451
|
return {
|
|
457
452
|
ok: true,
|
|
458
453
|
reason: "verification skipped (dev mode)",
|
|
459
|
-
|
|
460
|
-
verifiedRequestKey: replayKey
|
|
454
|
+
...reserveWebhookReplay(plivoReplayCache, replayKey)
|
|
461
455
|
};
|
|
462
456
|
}
|
|
463
457
|
const signatureV3 = getHeader(ctx.headers, "x-plivo-signature-v3");
|
|
@@ -507,13 +501,11 @@ function verifyPlivoWebhook(ctx, authToken, options) {
|
|
|
507
501
|
postParams,
|
|
508
502
|
nonce: nonceV3
|
|
509
503
|
});
|
|
510
|
-
const isReplay = markWebhookReplay(plivoReplayCache, replayKey);
|
|
511
504
|
return {
|
|
512
505
|
ok: true,
|
|
513
506
|
version: "v3",
|
|
514
507
|
verificationUrl,
|
|
515
|
-
|
|
516
|
-
verifiedRequestKey: replayKey
|
|
508
|
+
...reserveWebhookReplay(plivoReplayCache, replayKey)
|
|
517
509
|
};
|
|
518
510
|
}
|
|
519
511
|
if (signatureV2 && nonceV2) {
|
|
@@ -529,13 +521,11 @@ function verifyPlivoWebhook(ctx, authToken, options) {
|
|
|
529
521
|
reason: "Invalid Plivo V2 signature"
|
|
530
522
|
};
|
|
531
523
|
const replayKey = createPlivoV2ReplayKey(verificationUrl, nonceV2);
|
|
532
|
-
const isReplay = markWebhookReplay(plivoReplayCache, replayKey);
|
|
533
524
|
return {
|
|
534
525
|
ok: true,
|
|
535
526
|
version: "v2",
|
|
536
527
|
verificationUrl,
|
|
537
|
-
|
|
538
|
-
verifiedRequestKey: replayKey
|
|
528
|
+
...reserveWebhookReplay(plivoReplayCache, replayKey)
|
|
539
529
|
};
|
|
540
530
|
}
|
|
541
531
|
return {
|
|
@@ -555,26 +545,20 @@ async function cancelProviderResponseBody(response) {
|
|
|
555
545
|
function appendTruncatedSuffix(text) {
|
|
556
546
|
return `${text.trimEnd()}${TRUNCATED_SUFFIX}`;
|
|
557
547
|
}
|
|
558
|
-
async function
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
548
|
+
async function readVoiceCallProviderJsonResponse(response, malformedJsonMessage) {
|
|
549
|
+
const body = await readResponseWithLimit(response, PROVIDER_JSON_RESPONSE_MAX_BYTES, { onOverflow: ({ size, maxBytes }) => /* @__PURE__ */ new Error(`provider response body too large: ${size} bytes (limit: ${maxBytes} bytes)`) });
|
|
550
|
+
if (body.byteLength === 0) return;
|
|
551
|
+
try {
|
|
552
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(body);
|
|
553
|
+
return JSON.parse(text);
|
|
554
|
+
} catch (cause) {
|
|
555
|
+
throw new Error(malformedJsonMessage, { cause });
|
|
562
556
|
}
|
|
563
|
-
const body = await readResponseWithLimit(params.response, params.maxBytes, { onOverflow: ({ size, maxBytes }) => /* @__PURE__ */ new Error(`provider response body too large: ${size} bytes (limit: ${maxBytes} bytes)`) });
|
|
564
|
-
return new TextDecoder().decode(body);
|
|
565
|
-
}
|
|
566
|
-
async function readProviderJsonResponseText(response) {
|
|
567
|
-
return await readProviderResponseTextWithLimit({
|
|
568
|
-
response,
|
|
569
|
-
maxBytes: PROVIDER_JSON_RESPONSE_MAX_BYTES
|
|
570
|
-
});
|
|
571
557
|
}
|
|
572
558
|
async function readProviderErrorResponseSnippet(response) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
truncateOnLimit: true
|
|
577
|
-
});
|
|
559
|
+
const prefix = await readResponseTextPrefix(response, PROVIDER_ERROR_RESPONSE_MAX_BYTES);
|
|
560
|
+
const text = redactSensitiveText(prefix.text, { mode: "tools" });
|
|
561
|
+
return prefix.truncated ? appendTruncatedSuffix(text) : text;
|
|
578
562
|
}
|
|
579
563
|
//#endregion
|
|
580
564
|
//#region extensions/voice-call/src/providers/shared/guarded-json-api.ts
|
|
@@ -601,16 +585,10 @@ async function guardedJsonApiRequest(params) {
|
|
|
601
585
|
const errorText = await readProviderErrorResponseSnippet(response);
|
|
602
586
|
throw new Error(`${params.errorPrefix}: ${response.status} ${errorText}`);
|
|
603
587
|
}
|
|
604
|
-
|
|
605
|
-
if (!text) return;
|
|
606
|
-
try {
|
|
607
|
-
return JSON.parse(text);
|
|
608
|
-
} catch {
|
|
609
|
-
throw new Error(`${params.errorPrefix}: malformed JSON response`);
|
|
610
|
-
}
|
|
588
|
+
return await readVoiceCallProviderJsonResponse(response, `${params.errorPrefix}: malformed JSON response`);
|
|
611
589
|
} finally {
|
|
612
590
|
await release();
|
|
613
591
|
}
|
|
614
592
|
}
|
|
615
593
|
//#endregion
|
|
616
|
-
export { reconstructWebhookUrl as a, verifyTwilioWebhook as c,
|
|
594
|
+
export { reconstructWebhookUrl as a, verifyTwilioWebhook as c, readVoiceCallProviderJsonResponse as i, cancelProviderResponseBody as n, verifyPlivoWebhook as o, readProviderErrorResponseSnippet as r, verifyTelnyxWebhook as s, guardedJsonApiRequest as t };
|