@openclaw/voice-call 2026.7.2-beta.7 → 2026.8.1-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/doctor-contract-api.js +10 -8
- package/dist/{guarded-json-api-DS9sZa3C.js → guarded-json-api-DEDwgf4J.js} +24 -46
- package/dist/index.js +308 -629
- package/dist/{mock-YsvBTX-7.js → mock-BCq0q36l.js} +2 -3
- package/dist/{plivo-vZ--Wqxv.js → plivo-DbI17uAc.js} +45 -8
- package/dist/{realtime-handler--i_OERv6.js → realtime-handler-uXR0QgVa.js} +364 -108
- package/dist/{response-generator-D-7ApVfT.js → response-generator-D-hkzo5Y.js} +13 -5
- package/dist/{runtime-entry-D0UJiLfi.js → runtime-entry-F13eJ4pp.js} +279 -172
- 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-sIS_WgnY.js} +3 -2
- package/dist/{twilio-D_D8KysU.js → twilio-CbeVrVAl.js} +20 -26
- package/dist/{webhook-replay-aaP79dI8.js → webhook-replay-FQrEGVSM.js} +20 -6
- package/openclaw.plugin.json +49 -4
- package/package.json +4 -4
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-F13eJ4pp.js";
|
|
2
2
|
export { createVoiceCallRuntime };
|
package/dist/setup-api.js
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import { asOptionalRecord, isRecord, readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
1
|
+
import { asFiniteNumber, asOptionalRecord, isRecord, readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
2
2
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
3
3
|
//#region extensions/voice-call/src/config-migration.ts
|
|
4
|
-
const asObject = asOptionalRecord;
|
|
5
|
-
const getString = readStringField;
|
|
6
|
-
/** Read finite numeric config values. */
|
|
7
|
-
function getNumber(obj, key) {
|
|
8
|
-
const value = obj?.[key];
|
|
9
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
10
|
-
}
|
|
11
4
|
/** Merge legacy provider-specific values into the canonical providers map. */
|
|
12
5
|
function mergeProviderConfig(providersValue, providerId, compatValues) {
|
|
13
|
-
if (Object.keys(compatValues).length === 0) return
|
|
14
|
-
const providers =
|
|
15
|
-
const existing =
|
|
6
|
+
if (Object.keys(compatValues).length === 0) return asOptionalRecord(providersValue);
|
|
7
|
+
const providers = asOptionalRecord(providersValue) ?? {};
|
|
8
|
+
const existing = asOptionalRecord(providers[providerId]) ?? {};
|
|
16
9
|
return {
|
|
17
10
|
...providers,
|
|
18
11
|
[providerId]: {
|
|
@@ -23,23 +16,23 @@ function mergeProviderConfig(providersValue, providerId, compatValues) {
|
|
|
23
16
|
}
|
|
24
17
|
/** Migrate legacy voice-call config input to the current canonical shape. */
|
|
25
18
|
function migrateVoiceCallLegacyConfigInput(params) {
|
|
26
|
-
const raw =
|
|
27
|
-
const realtime =
|
|
28
|
-
const realtimeAgentContext =
|
|
29
|
-
const twilio =
|
|
30
|
-
const streaming =
|
|
19
|
+
const raw = asOptionalRecord(params.value) ?? {};
|
|
20
|
+
const realtime = asOptionalRecord(raw.realtime);
|
|
21
|
+
const realtimeAgentContext = asOptionalRecord(realtime?.agentContext);
|
|
22
|
+
const twilio = asOptionalRecord(raw.twilio);
|
|
23
|
+
const streaming = asOptionalRecord(raw.streaming);
|
|
31
24
|
const configPathPrefix = params.configPathPrefix ?? "plugins.entries.voice-call.config";
|
|
32
25
|
const legacyStreamingOpenAICompat = {};
|
|
33
|
-
const streamingOpenAIApiKey =
|
|
26
|
+
const streamingOpenAIApiKey = readStringField(streaming, "openaiApiKey");
|
|
34
27
|
if (streamingOpenAIApiKey) legacyStreamingOpenAICompat.apiKey = streamingOpenAIApiKey;
|
|
35
|
-
const streamingSttModel =
|
|
28
|
+
const streamingSttModel = readStringField(streaming, "sttModel");
|
|
36
29
|
if (streamingSttModel) legacyStreamingOpenAICompat.model = streamingSttModel;
|
|
37
|
-
const streamingSilenceDurationMs =
|
|
30
|
+
const streamingSilenceDurationMs = asFiniteNumber(streaming?.silenceDurationMs);
|
|
38
31
|
if (streamingSilenceDurationMs !== void 0) legacyStreamingOpenAICompat.silenceDurationMs = streamingSilenceDurationMs;
|
|
39
|
-
const streamingVadThreshold =
|
|
32
|
+
const streamingVadThreshold = asFiniteNumber(streaming?.vadThreshold);
|
|
40
33
|
if (streamingVadThreshold !== void 0) legacyStreamingOpenAICompat.vadThreshold = streamingVadThreshold;
|
|
41
|
-
const streamingProvider =
|
|
42
|
-
const legacyStreamingProvider =
|
|
34
|
+
const streamingProvider = readStringField(streaming, "provider");
|
|
35
|
+
const legacyStreamingProvider = readStringField(streaming, "sttProvider");
|
|
43
36
|
const normalizedStreaming = streaming ? {
|
|
44
37
|
...streaming,
|
|
45
38
|
provider: streamingProvider ?? legacyStreamingProvider,
|
|
@@ -74,9 +67,9 @@ function migrateVoiceCallLegacyConfigInput(params) {
|
|
|
74
67
|
if (typeof streaming?.sttProvider === "string") changes.push(`Moved ${configPathPrefix}.streaming.sttProvider → ${configPathPrefix}.streaming.provider.`);
|
|
75
68
|
if (typeof streaming?.openaiApiKey === "string") changes.push(`Moved ${configPathPrefix}.streaming.openaiApiKey → ${configPathPrefix}.streaming.providers.openai.apiKey.`);
|
|
76
69
|
if (typeof streaming?.sttModel === "string") changes.push(`Moved ${configPathPrefix}.streaming.sttModel → ${configPathPrefix}.streaming.providers.openai.model.`);
|
|
77
|
-
if (
|
|
70
|
+
if (asFiniteNumber(streaming?.silenceDurationMs) !== void 0) changes.push(`Moved ${configPathPrefix}.streaming.silenceDurationMs → ${configPathPrefix}.streaming.providers.openai.silenceDurationMs.`);
|
|
78
71
|
else if (typeof streaming?.silenceDurationMs === "number") changes.push(`Removed invalid ${configPathPrefix}.streaming.silenceDurationMs.`);
|
|
79
|
-
if (
|
|
72
|
+
if (asFiniteNumber(streaming?.vadThreshold) !== void 0) changes.push(`Moved ${configPathPrefix}.streaming.vadThreshold → ${configPathPrefix}.streaming.providers.openai.vadThreshold.`);
|
|
80
73
|
else if (typeof streaming?.vadThreshold === "number") changes.push(`Removed invalid ${configPathPrefix}.streaming.vadThreshold.`);
|
|
81
74
|
if (realtimeAgentContext && Object.hasOwn(realtimeAgentContext, "includeSystemPrompt")) changes.push(`Removed ${configPathPrefix}.realtime.agentContext.includeSystemPrompt.`);
|
|
82
75
|
return {
|
|
@@ -130,6 +130,54 @@ const { setRuntime: setVoiceCallStateRuntime, tryGetRuntime: getOptionalVoiceCal
|
|
|
130
130
|
errorMessage: "Voice Call state runtime not initialized"
|
|
131
131
|
});
|
|
132
132
|
//#endregion
|
|
133
|
+
//#region extensions/voice-call/src/manager/replay-keys.ts
|
|
134
|
+
/** Match the existing voice-call webhook replay-cache cardinality. */
|
|
135
|
+
const MAX_MANAGER_REPLAY_KEYS = 1e4;
|
|
136
|
+
function pruneOldestSetEntries(keys, maxEntries) {
|
|
137
|
+
while (keys.size > maxEntries) {
|
|
138
|
+
const oldest = keys.values().next().value;
|
|
139
|
+
if (oldest === void 0) break;
|
|
140
|
+
keys.delete(oldest);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function pruneOldestMapEntries(keys, maxEntries) {
|
|
144
|
+
while (keys.size > maxEntries) {
|
|
145
|
+
const oldest = keys.keys().next().value;
|
|
146
|
+
if (oldest === void 0) break;
|
|
147
|
+
keys.delete(oldest);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** Remember one manager replay key without refreshing duplicate insertion order. */
|
|
151
|
+
function rememberManagerReplayKey(keys, key, maxEntries = MAX_MANAGER_REPLAY_KEYS) {
|
|
152
|
+
keys.add(key);
|
|
153
|
+
pruneOldestSetEntries(keys, maxEntries);
|
|
154
|
+
}
|
|
155
|
+
/** Append one per-call replay key and retain only the newest bounded suffix. */
|
|
156
|
+
function appendCallReplayKey(keys, key, maxEntries = 500) {
|
|
157
|
+
keys.push(key);
|
|
158
|
+
trimCallReplayKeys(keys, maxEntries);
|
|
159
|
+
}
|
|
160
|
+
/** Normalize restored or externally constructed call replay history in place. */
|
|
161
|
+
function trimCallReplayKeys(keys, maxEntries = 500) {
|
|
162
|
+
const overflow = keys.length - maxEntries;
|
|
163
|
+
if (overflow > 0) keys.splice(0, overflow);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Reserve one rejected provider call. The token prevents a stale failed
|
|
167
|
+
* hangup from deleting a newer reservation after bounded eviction and reuse.
|
|
168
|
+
*/
|
|
169
|
+
function reserveRejectedProviderCall(calls, providerCallId, maxEntries = MAX_MANAGER_REPLAY_KEYS) {
|
|
170
|
+
if (calls.has(providerCallId)) return;
|
|
171
|
+
const reservation = Symbol(providerCallId);
|
|
172
|
+
calls.set(providerCallId, reservation);
|
|
173
|
+
pruneOldestMapEntries(calls, maxEntries);
|
|
174
|
+
return reservation;
|
|
175
|
+
}
|
|
176
|
+
/** Release a rejected-call reservation only when the failing attempt still owns it. */
|
|
177
|
+
function releaseRejectedProviderCall(calls, providerCallId, reservation) {
|
|
178
|
+
if (calls.get(providerCallId) === reservation) calls.delete(providerCallId);
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
133
181
|
//#region extensions/voice-call/src/manager/store.ts
|
|
134
182
|
/** Plugin state namespace for call record event metadata. */
|
|
135
183
|
const CALL_RECORD_EVENTS_NAMESPACE = "call-record-events";
|
|
@@ -237,10 +285,18 @@ function countCallRecordChunks(call) {
|
|
|
237
285
|
}
|
|
238
286
|
/** Truncate oversized call records to fit the bounded plugin state chunk budget. */
|
|
239
287
|
function prepareVoiceCallRecordForStorage(call) {
|
|
240
|
-
|
|
241
|
-
|
|
288
|
+
let boundedCall = call;
|
|
289
|
+
if (call.processedEventIds.length > 500) {
|
|
290
|
+
boundedCall = {
|
|
291
|
+
...call,
|
|
292
|
+
processedEventIds: [...call.processedEventIds]
|
|
293
|
+
};
|
|
294
|
+
trimCallReplayKeys(boundedCall.processedEventIds);
|
|
295
|
+
}
|
|
296
|
+
if (countCallRecordChunks(boundedCall) <= 48) return boundedCall;
|
|
297
|
+
const transcriptEntries = boundedCall.transcript.length;
|
|
242
298
|
const metadata = {
|
|
243
|
-
...
|
|
299
|
+
...boundedCall.metadata,
|
|
244
300
|
voiceCallPersistence: {
|
|
245
301
|
transcriptTruncated: true,
|
|
246
302
|
originalTranscriptEntries: transcriptEntries
|
|
@@ -266,12 +322,12 @@ function prepareVoiceCallRecordForStorage(call) {
|
|
|
266
322
|
];
|
|
267
323
|
for (const candidateInput of candidateInputs) {
|
|
268
324
|
const candidate = CallRecordSchema.parse({
|
|
269
|
-
...
|
|
325
|
+
...boundedCall,
|
|
270
326
|
...candidateInput
|
|
271
327
|
});
|
|
272
328
|
if (countCallRecordChunks(candidate) <= 48) return candidate;
|
|
273
329
|
}
|
|
274
|
-
return
|
|
330
|
+
return boundedCall;
|
|
275
331
|
}
|
|
276
332
|
/** Register a serialized call record event and its chunks, then prune old events. */
|
|
277
333
|
function registerCallRecordEvent(stores, eventKey, call, order) {
|
|
@@ -356,17 +412,19 @@ function loadActiveCallsFromStore(storePath) {
|
|
|
356
412
|
if (calls.length === 0) return {
|
|
357
413
|
activeCalls: /* @__PURE__ */ new Map(),
|
|
358
414
|
providerCallIdMap: /* @__PURE__ */ new Map(),
|
|
359
|
-
processedEventIds: /* @__PURE__ */ new Set()
|
|
360
|
-
rejectedProviderCallIds: /* @__PURE__ */ new Set()
|
|
415
|
+
processedEventIds: /* @__PURE__ */ new Set()
|
|
361
416
|
};
|
|
362
417
|
const callMap = /* @__PURE__ */ new Map();
|
|
363
|
-
for (const call of calls)
|
|
418
|
+
for (const call of calls) {
|
|
419
|
+
callMap.delete(call.callId);
|
|
420
|
+
callMap.set(call.callId, call);
|
|
421
|
+
}
|
|
364
422
|
const activeCalls = /* @__PURE__ */ new Map();
|
|
365
423
|
const providerCallIdMap = /* @__PURE__ */ new Map();
|
|
366
424
|
const processedEventIds = /* @__PURE__ */ new Set();
|
|
367
|
-
const rejectedProviderCallIds = /* @__PURE__ */ new Set();
|
|
368
425
|
for (const [callId, call] of callMap) {
|
|
369
|
-
|
|
426
|
+
trimCallReplayKeys(call.processedEventIds);
|
|
427
|
+
for (const eventId of call.processedEventIds) rememberManagerReplayKey(processedEventIds, eventId);
|
|
370
428
|
if (TerminalStates.has(call.state)) continue;
|
|
371
429
|
activeCalls.set(callId, call);
|
|
372
430
|
if (call.providerCallId) providerCallIdMap.set(call.providerCallId, callId);
|
|
@@ -374,8 +432,7 @@ function loadActiveCallsFromStore(storePath) {
|
|
|
374
432
|
return {
|
|
375
433
|
activeCalls,
|
|
376
434
|
providerCallIdMap,
|
|
377
|
-
processedEventIds
|
|
378
|
-
rejectedProviderCallIds
|
|
435
|
+
processedEventIds
|
|
379
436
|
};
|
|
380
437
|
}
|
|
381
438
|
function readCallHistoryFromStore(storePath) {
|
|
@@ -407,4 +464,4 @@ function resolveDefaultVoiceCallStoreDir(env = process.env) {
|
|
|
407
464
|
return path.join(resolveStateDir(env), "voice-calls");
|
|
408
465
|
}
|
|
409
466
|
//#endregion
|
|
410
|
-
export {
|
|
467
|
+
export { releaseRejectedProviderCall as _, CALL_RECORD_EVENT_META_MAX_ENTRIES as a, setVoiceCallStateRuntime as b, buildVoiceCallLegacyJsonlEventKey as c, loadActiveCallsFromStore as d, parseVoiceCallRecordLine as f, appendCallReplayKey as g, resolveVoiceCallLegacyCallLogPath as h, CALL_RECORD_EVENT_CHUNKS_NAMESPACE as i, findCallMatchesInStore as l, prepareVoiceCallRecordForStorage as m, CALL_RECORD_CHUNK_MAX_ENTRIES as n, MAX_CALL_RECORD_EVENTS as o, persistCallRecord as p, CALL_RECORD_EVENTS_NAMESPACE as r, RAW_CALL_RECORD_CHUNK_BYTES as s, resolveDefaultVoiceCallStoreDir as t, getCallHistoryFromStore as u, rememberManagerReplayKey as v, TerminalStates as x, reserveRejectedProviderCall as y };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { s as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
1
|
+
import { s as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-DEDwgf4J.js";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
//#region extensions/voice-call/src/providers/telnyx.ts
|
|
4
4
|
function normalizeTelnyxDirection(direction) {
|
|
@@ -57,7 +57,8 @@ var TelnyxProvider = class {
|
|
|
57
57
|
ok: result.ok,
|
|
58
58
|
reason: result.reason,
|
|
59
59
|
isReplay: result.isReplay,
|
|
60
|
-
verifiedRequestKey: result.verifiedRequestKey
|
|
60
|
+
verifiedRequestKey: result.verifiedRequestKey,
|
|
61
|
+
releaseReplay: result.releaseReplay
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
3
|
import { n as requireSupportedTwilioApiHostname, r as resolveTwilioApiBaseUrl } from "./twilio-region-4fkgz3UG.js";
|
|
4
|
-
import { _ as escapeXml, a as normalizeProviderStatus, i as mapProviderStatusToEndReason, p as chunkAudio, r as isProviderStatusTerminal, s as getHeader, v as mapVoiceToPolly } from "./runtime-entry-
|
|
5
|
-
import { c as verifyTwilioWebhook, i as
|
|
4
|
+
import { _ as escapeXml, a as normalizeProviderStatus, i as mapProviderStatusToEndReason, p as chunkAudio, r as isProviderStatusTerminal, s as getHeader, v as mapVoiceToPolly } from "./runtime-entry-F13eJ4pp.js";
|
|
5
|
+
import { c as verifyTwilioWebhook, i as readVoiceCallProviderJsonResponse, n as cancelProviderResponseBody, r as readProviderErrorResponseSnippet, t as guardedJsonApiRequest } from "./guarded-json-api-DEDwgf4J.js";
|
|
6
6
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
7
7
|
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
|
|
8
8
|
import crypto from "node:crypto";
|
|
@@ -68,13 +68,7 @@ async function twilioApiRequest(params) {
|
|
|
68
68
|
const errorText = await readProviderErrorResponseSnippet(response);
|
|
69
69
|
throw new TwilioApiError(response.status, errorText);
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
if (!text) return;
|
|
73
|
-
try {
|
|
74
|
-
return JSON.parse(text);
|
|
75
|
-
} catch {
|
|
76
|
-
throw new Error("Twilio API returned malformed JSON.");
|
|
77
|
-
}
|
|
71
|
+
return await readVoiceCallProviderJsonResponse(response, "Twilio API returned malformed JSON.");
|
|
78
72
|
} finally {
|
|
79
73
|
await release();
|
|
80
74
|
}
|
|
@@ -138,7 +132,8 @@ function verifyTwilioProviderWebhook(params) {
|
|
|
138
132
|
ok: result.ok,
|
|
139
133
|
reason: result.reason,
|
|
140
134
|
isReplay: result.isReplay,
|
|
141
|
-
verifiedRequestKey: result.verifiedRequestKey
|
|
135
|
+
verifiedRequestKey: result.verifiedRequestKey,
|
|
136
|
+
releaseReplay: result.releaseReplay
|
|
142
137
|
};
|
|
143
138
|
}
|
|
144
139
|
//#endregion
|
|
@@ -172,17 +167,20 @@ var TwilioProvider = class TwilioProvider {
|
|
|
172
167
|
this.notifyCalls.delete(callId);
|
|
173
168
|
}
|
|
174
169
|
/**
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* This is used when we only have `providerCallId` (e.g. hangup).
|
|
170
|
+
* Release all process-local metadata owned by one Twilio call.
|
|
171
|
+
* Terminal webhooks can be replayed, so this must stay idempotent.
|
|
178
172
|
*/
|
|
179
|
-
|
|
173
|
+
releaseCallState(providerCallId, callId) {
|
|
180
174
|
const webhookUrl = this.callWebhookUrls.get(providerCallId);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
175
|
+
let resolvedCallId = callId;
|
|
176
|
+
if (!resolvedCallId && webhookUrl) try {
|
|
177
|
+
resolvedCallId = new URL(webhookUrl).searchParams.get("callId") || void 0;
|
|
178
|
+
} catch {}
|
|
179
|
+
if (resolvedCallId) this.deleteStoredTwiml(resolvedCallId);
|
|
180
|
+
this.callWebhookUrls.delete(providerCallId);
|
|
181
|
+
this.callStreamMap.delete(providerCallId);
|
|
185
182
|
this.streamAuthTokens.delete(providerCallId);
|
|
183
|
+
this.activeStreamCalls.delete(providerCallId);
|
|
186
184
|
}
|
|
187
185
|
constructor(config, options = {}) {
|
|
188
186
|
this.name = "twilio";
|
|
@@ -375,14 +373,13 @@ var TwilioProvider = class TwilioProvider {
|
|
|
375
373
|
};
|
|
376
374
|
const endReason = mapProviderStatusToEndReason(callStatus);
|
|
377
375
|
if (endReason) {
|
|
378
|
-
|
|
379
|
-
this.activeStreamCalls.delete(callSid);
|
|
380
|
-
if (callIdOverride) this.deleteStoredTwiml(callIdOverride);
|
|
381
|
-
return {
|
|
376
|
+
const event = {
|
|
382
377
|
...baseEvent,
|
|
383
378
|
type: "call.ended",
|
|
384
379
|
reason: endReason
|
|
385
380
|
};
|
|
381
|
+
this.releaseCallState(callSid, callIdOverride);
|
|
382
|
+
return event;
|
|
386
383
|
}
|
|
387
384
|
return null;
|
|
388
385
|
}
|
|
@@ -524,11 +521,8 @@ var TwilioProvider = class TwilioProvider {
|
|
|
524
521
|
* Hang up a call via Twilio API.
|
|
525
522
|
*/
|
|
526
523
|
async hangupCall(input) {
|
|
527
|
-
this.deleteStoredTwimlForProviderCall(input.providerCallId);
|
|
528
|
-
this.callWebhookUrls.delete(input.providerCallId);
|
|
529
|
-
this.streamAuthTokens.delete(input.providerCallId);
|
|
530
|
-
this.activeStreamCalls.delete(input.providerCallId);
|
|
531
524
|
await this.apiRequest(`/Calls/${input.providerCallId}.json`, { Status: "completed" }, { allowNotFound: true });
|
|
525
|
+
this.releaseCallState(input.providerCallId, input.callId);
|
|
532
526
|
}
|
|
533
527
|
/**
|
|
534
528
|
* Play TTS audio via Twilio.
|
|
@@ -10,23 +10,37 @@ function createWebhookReplayCache() {
|
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
function pruneWebhookReplayCache(cache, now) {
|
|
13
|
-
for (const [key,
|
|
13
|
+
for (const [key, reservation] of cache.seenUntil) if (!isFutureDateTimestampMs(reservation.expiresAt, { nowMs: now })) cache.seenUntil.delete(key);
|
|
14
14
|
while (cache.seenUntil.size > REPLAY_CACHE_MAX_ENTRIES) {
|
|
15
15
|
const oldest = cache.seenUntil.keys().next().value;
|
|
16
16
|
if (!oldest) break;
|
|
17
17
|
cache.seenUntil.delete(oldest);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
function
|
|
20
|
+
function reserveWebhookReplay(cache, replayKey) {
|
|
21
21
|
const now = Date.now();
|
|
22
22
|
cache.calls += 1;
|
|
23
23
|
if (cache.calls % REPLAY_CACHE_PRUNE_INTERVAL === 0) pruneWebhookReplayCache(cache, now);
|
|
24
24
|
const existing = cache.seenUntil.get(replayKey);
|
|
25
|
-
if (existing !== void 0 && isFutureDateTimestampMs(existing, { nowMs: now })) return
|
|
25
|
+
if (existing !== void 0 && isFutureDateTimestampMs(existing.expiresAt, { nowMs: now })) return {
|
|
26
|
+
isReplay: true,
|
|
27
|
+
verifiedRequestKey: replayKey
|
|
28
|
+
};
|
|
26
29
|
const expiresAt = resolveExpiresAtMsFromDurationMs(REPLAY_WINDOW_MS, { nowMs: now });
|
|
27
|
-
if (expiresAt
|
|
30
|
+
if (expiresAt === void 0) return {
|
|
31
|
+
isReplay: false,
|
|
32
|
+
verifiedRequestKey: replayKey
|
|
33
|
+
};
|
|
34
|
+
const reservation = { expiresAt };
|
|
35
|
+
cache.seenUntil.set(replayKey, reservation);
|
|
28
36
|
if (cache.seenUntil.size > REPLAY_CACHE_MAX_ENTRIES) pruneWebhookReplayCache(cache, now);
|
|
29
|
-
return
|
|
37
|
+
return {
|
|
38
|
+
isReplay: false,
|
|
39
|
+
verifiedRequestKey: replayKey,
|
|
40
|
+
releaseReplay: () => {
|
|
41
|
+
if (cache.seenUntil.get(replayKey) === reservation) cache.seenUntil.delete(replayKey);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
30
44
|
}
|
|
31
45
|
//#endregion
|
|
32
|
-
export {
|
|
46
|
+
export { reserveWebhookReplay as n, createWebhookReplayCache as t };
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "voice-call",
|
|
3
|
+
"doctorContract": {
|
|
4
|
+
"resolveSessionStoreAgentIds": true,
|
|
5
|
+
"stateMigrations": true
|
|
6
|
+
},
|
|
3
7
|
"name": "Voice Call",
|
|
4
8
|
"description": "OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls.",
|
|
5
9
|
"skills": ["./skills"],
|
|
@@ -224,6 +228,11 @@
|
|
|
224
228
|
"label": "Call Log Store Path",
|
|
225
229
|
"advanced": true
|
|
226
230
|
},
|
|
231
|
+
"agentId": {
|
|
232
|
+
"label": "Response Agent ID",
|
|
233
|
+
"help": "Agent workspace used for voice response generation. Defaults to \"main\".",
|
|
234
|
+
"advanced": true
|
|
235
|
+
},
|
|
227
236
|
"sessionScope": {
|
|
228
237
|
"label": "Session Scope",
|
|
229
238
|
"help": "Use per-phone to preserve caller memory across calls, or per-call to isolate every call into a fresh voice session."
|
|
@@ -703,9 +712,17 @@
|
|
|
703
712
|
"model": {
|
|
704
713
|
"type": "string"
|
|
705
714
|
},
|
|
706
|
-
"
|
|
715
|
+
"speakerVoice": {
|
|
707
716
|
"type": "string"
|
|
708
717
|
},
|
|
718
|
+
"voice": {
|
|
719
|
+
"type": "string",
|
|
720
|
+
"description": "Legacy alias for speakerVoice."
|
|
721
|
+
},
|
|
722
|
+
"voiceId": {
|
|
723
|
+
"type": "string",
|
|
724
|
+
"description": "Legacy alias for speakerVoice."
|
|
725
|
+
},
|
|
709
726
|
"speed": {
|
|
710
727
|
"type": "number",
|
|
711
728
|
"minimum": 0.25,
|
|
@@ -726,9 +743,17 @@
|
|
|
726
743
|
"baseUrl": {
|
|
727
744
|
"type": "string"
|
|
728
745
|
},
|
|
729
|
-
"
|
|
746
|
+
"speakerVoiceId": {
|
|
730
747
|
"type": "string"
|
|
731
748
|
},
|
|
749
|
+
"voice": {
|
|
750
|
+
"type": "string",
|
|
751
|
+
"description": "Legacy alias for speakerVoiceId."
|
|
752
|
+
},
|
|
753
|
+
"voiceId": {
|
|
754
|
+
"type": "string",
|
|
755
|
+
"description": "Legacy alias for speakerVoiceId."
|
|
756
|
+
},
|
|
732
757
|
"modelId": {
|
|
733
758
|
"type": "string"
|
|
734
759
|
},
|
|
@@ -782,9 +807,17 @@
|
|
|
782
807
|
"enabled": {
|
|
783
808
|
"type": "boolean"
|
|
784
809
|
},
|
|
785
|
-
"
|
|
810
|
+
"speakerVoice": {
|
|
786
811
|
"type": "string"
|
|
787
812
|
},
|
|
813
|
+
"voice": {
|
|
814
|
+
"type": "string",
|
|
815
|
+
"description": "Legacy alias for speakerVoice."
|
|
816
|
+
},
|
|
817
|
+
"voiceId": {
|
|
818
|
+
"type": "string",
|
|
819
|
+
"description": "Legacy alias for speakerVoice."
|
|
820
|
+
},
|
|
788
821
|
"lang": {
|
|
789
822
|
"type": "string"
|
|
790
823
|
},
|
|
@@ -820,9 +853,17 @@
|
|
|
820
853
|
"enabled": {
|
|
821
854
|
"type": "boolean"
|
|
822
855
|
},
|
|
823
|
-
"
|
|
856
|
+
"speakerVoice": {
|
|
824
857
|
"type": "string"
|
|
825
858
|
},
|
|
859
|
+
"voice": {
|
|
860
|
+
"type": "string",
|
|
861
|
+
"description": "Legacy alias for speakerVoice."
|
|
862
|
+
},
|
|
863
|
+
"voiceId": {
|
|
864
|
+
"type": "string",
|
|
865
|
+
"description": "Legacy alias for speakerVoice."
|
|
866
|
+
},
|
|
826
867
|
"lang": {
|
|
827
868
|
"type": "string"
|
|
828
869
|
},
|
|
@@ -879,6 +920,10 @@
|
|
|
879
920
|
"store": {
|
|
880
921
|
"type": "string"
|
|
881
922
|
},
|
|
923
|
+
"agentId": {
|
|
924
|
+
"type": "string",
|
|
925
|
+
"minLength": 1
|
|
926
|
+
},
|
|
882
927
|
"sessionScope": {
|
|
883
928
|
"type": "string",
|
|
884
929
|
"enum": ["per-phone", "per-call"]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/voice-call",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.8.1-beta.2",
|
|
4
4
|
"description": "OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"zod": "4.4.3"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
|
-
"openclaw": ">=2026.
|
|
16
|
+
"openclaw": ">=2026.8.1-beta.2"
|
|
17
17
|
},
|
|
18
18
|
"peerDependenciesMeta": {
|
|
19
19
|
"openclaw": {
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"minHostVersion": ">=2026.4.10"
|
|
31
31
|
},
|
|
32
32
|
"compat": {
|
|
33
|
-
"pluginApi": ">=2026.
|
|
33
|
+
"pluginApi": ">=2026.8.1-beta.2"
|
|
34
34
|
},
|
|
35
35
|
"build": {
|
|
36
|
-
"openclawVersion": "2026.
|
|
36
|
+
"openclawVersion": "2026.8.1-beta.2"
|
|
37
37
|
},
|
|
38
38
|
"release": {
|
|
39
39
|
"publishToClawHub": true,
|