@openclaw/voice-call 2026.2.3 → 2026.2.9
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/CHANGELOG.md +19 -1
- package/index.ts +146 -127
- package/package.json +1 -1
- package/src/cli.ts +1 -4
- package/src/response-generator.ts +1 -1
- package/src/runtime.ts +1 -1
- package/src/webhook.ts +31 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 2026.2.3
|
|
3
|
+
## 2026.2.6-3
|
|
4
|
+
|
|
5
|
+
### Changes
|
|
6
|
+
|
|
7
|
+
- Version alignment with core OpenClaw release numbers.
|
|
8
|
+
|
|
9
|
+
## 2026.2.6-2
|
|
10
|
+
|
|
11
|
+
### Changes
|
|
12
|
+
|
|
13
|
+
- Version alignment with core OpenClaw release numbers.
|
|
14
|
+
|
|
15
|
+
## 2026.2.6
|
|
16
|
+
|
|
17
|
+
### Changes
|
|
18
|
+
|
|
19
|
+
- Version alignment with core OpenClaw release numbers.
|
|
20
|
+
|
|
21
|
+
## 2026.2.4
|
|
4
22
|
|
|
5
23
|
### Changes
|
|
6
24
|
|
package/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GatewayRequestHandlerOptions, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
1
2
|
import { Type } from "@sinclair/typebox";
|
|
2
3
|
import type { CoreConfig } from "./src/core-bridge.js";
|
|
3
4
|
import { registerVoiceCallCli } from "./src/cli.js";
|
|
@@ -144,7 +145,7 @@ const voiceCallPlugin = {
|
|
|
144
145
|
name: "Voice Call",
|
|
145
146
|
description: "Voice-call plugin with Telnyx/Twilio/Plivo providers",
|
|
146
147
|
configSchema: voiceCallConfigSchema,
|
|
147
|
-
register(api) {
|
|
148
|
+
register(api: OpenClawPluginApi) {
|
|
148
149
|
const config = resolveVoiceCallConfig(voiceCallConfigSchema.parse(api.pluginConfig));
|
|
149
150
|
const validation = validateProviderConfig(config);
|
|
150
151
|
|
|
@@ -188,142 +189,160 @@ const voiceCallPlugin = {
|
|
|
188
189
|
respond(false, { error: err instanceof Error ? err.message : String(err) });
|
|
189
190
|
};
|
|
190
191
|
|
|
191
|
-
api.registerGatewayMethod(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
192
|
+
api.registerGatewayMethod(
|
|
193
|
+
"voicecall.initiate",
|
|
194
|
+
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
195
|
+
try {
|
|
196
|
+
const message = typeof params?.message === "string" ? params.message.trim() : "";
|
|
197
|
+
if (!message) {
|
|
198
|
+
respond(false, { error: "message required" });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const rt = await ensureRuntime();
|
|
202
|
+
const to =
|
|
203
|
+
typeof params?.to === "string" && params.to.trim()
|
|
204
|
+
? params.to.trim()
|
|
205
|
+
: rt.config.toNumber;
|
|
206
|
+
if (!to) {
|
|
207
|
+
respond(false, { error: "to required" });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const mode =
|
|
211
|
+
params?.mode === "notify" || params?.mode === "conversation" ? params.mode : undefined;
|
|
212
|
+
const result = await rt.manager.initiateCall(to, undefined, {
|
|
213
|
+
message,
|
|
214
|
+
mode,
|
|
215
|
+
});
|
|
216
|
+
if (!result.success) {
|
|
217
|
+
respond(false, { error: result.error || "initiate failed" });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
respond(true, { callId: result.callId, initiated: true });
|
|
221
|
+
} catch (err) {
|
|
222
|
+
sendError(respond, err);
|
|
216
223
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
sendError(respond, err);
|
|
220
|
-
}
|
|
221
|
-
});
|
|
224
|
+
},
|
|
225
|
+
);
|
|
222
226
|
|
|
223
|
-
api.registerGatewayMethod(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
227
|
+
api.registerGatewayMethod(
|
|
228
|
+
"voicecall.continue",
|
|
229
|
+
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
230
|
+
try {
|
|
231
|
+
const callId = typeof params?.callId === "string" ? params.callId.trim() : "";
|
|
232
|
+
const message = typeof params?.message === "string" ? params.message.trim() : "";
|
|
233
|
+
if (!callId || !message) {
|
|
234
|
+
respond(false, { error: "callId and message required" });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const rt = await ensureRuntime();
|
|
238
|
+
const result = await rt.manager.continueCall(callId, message);
|
|
239
|
+
if (!result.success) {
|
|
240
|
+
respond(false, { error: result.error || "continue failed" });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
respond(true, { success: true, transcript: result.transcript });
|
|
244
|
+
} catch (err) {
|
|
245
|
+
sendError(respond, err);
|
|
236
246
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
sendError(respond, err);
|
|
240
|
-
}
|
|
241
|
-
});
|
|
247
|
+
},
|
|
248
|
+
);
|
|
242
249
|
|
|
243
|
-
api.registerGatewayMethod(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
250
|
+
api.registerGatewayMethod(
|
|
251
|
+
"voicecall.speak",
|
|
252
|
+
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
253
|
+
try {
|
|
254
|
+
const callId = typeof params?.callId === "string" ? params.callId.trim() : "";
|
|
255
|
+
const message = typeof params?.message === "string" ? params.message.trim() : "";
|
|
256
|
+
if (!callId || !message) {
|
|
257
|
+
respond(false, { error: "callId and message required" });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const rt = await ensureRuntime();
|
|
261
|
+
const result = await rt.manager.speak(callId, message);
|
|
262
|
+
if (!result.success) {
|
|
263
|
+
respond(false, { error: result.error || "speak failed" });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
respond(true, { success: true });
|
|
267
|
+
} catch (err) {
|
|
268
|
+
sendError(respond, err);
|
|
256
269
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
sendError(respond, err);
|
|
260
|
-
}
|
|
261
|
-
});
|
|
270
|
+
},
|
|
271
|
+
);
|
|
262
272
|
|
|
263
|
-
api.registerGatewayMethod(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
273
|
+
api.registerGatewayMethod(
|
|
274
|
+
"voicecall.end",
|
|
275
|
+
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
276
|
+
try {
|
|
277
|
+
const callId = typeof params?.callId === "string" ? params.callId.trim() : "";
|
|
278
|
+
if (!callId) {
|
|
279
|
+
respond(false, { error: "callId required" });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const rt = await ensureRuntime();
|
|
283
|
+
const result = await rt.manager.endCall(callId);
|
|
284
|
+
if (!result.success) {
|
|
285
|
+
respond(false, { error: result.error || "end failed" });
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
respond(true, { success: true });
|
|
289
|
+
} catch (err) {
|
|
290
|
+
sendError(respond, err);
|
|
275
291
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
sendError(respond, err);
|
|
279
|
-
}
|
|
280
|
-
});
|
|
292
|
+
},
|
|
293
|
+
);
|
|
281
294
|
|
|
282
|
-
api.registerGatewayMethod(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
? params.
|
|
289
|
-
: ""
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
295
|
+
api.registerGatewayMethod(
|
|
296
|
+
"voicecall.status",
|
|
297
|
+
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
298
|
+
try {
|
|
299
|
+
const raw =
|
|
300
|
+
typeof params?.callId === "string"
|
|
301
|
+
? params.callId.trim()
|
|
302
|
+
: typeof params?.sid === "string"
|
|
303
|
+
? params.sid.trim()
|
|
304
|
+
: "";
|
|
305
|
+
if (!raw) {
|
|
306
|
+
respond(false, { error: "callId required" });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const rt = await ensureRuntime();
|
|
310
|
+
const call = rt.manager.getCall(raw) || rt.manager.getCallByProviderCallId(raw);
|
|
311
|
+
if (!call) {
|
|
312
|
+
respond(true, { found: false });
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
respond(true, { found: true, call });
|
|
316
|
+
} catch (err) {
|
|
317
|
+
sendError(respond, err);
|
|
299
318
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
sendError(respond, err);
|
|
303
|
-
}
|
|
304
|
-
});
|
|
319
|
+
},
|
|
320
|
+
);
|
|
305
321
|
|
|
306
|
-
api.registerGatewayMethod(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
322
|
+
api.registerGatewayMethod(
|
|
323
|
+
"voicecall.start",
|
|
324
|
+
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
325
|
+
try {
|
|
326
|
+
const to = typeof params?.to === "string" ? params.to.trim() : "";
|
|
327
|
+
const message = typeof params?.message === "string" ? params.message.trim() : "";
|
|
328
|
+
if (!to) {
|
|
329
|
+
respond(false, { error: "to required" });
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const rt = await ensureRuntime();
|
|
333
|
+
const result = await rt.manager.initiateCall(to, undefined, {
|
|
334
|
+
message: message || undefined,
|
|
335
|
+
});
|
|
336
|
+
if (!result.success) {
|
|
337
|
+
respond(false, { error: result.error || "initiate failed" });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
respond(true, { callId: result.callId, initiated: true });
|
|
341
|
+
} catch (err) {
|
|
342
|
+
sendError(respond, err);
|
|
321
343
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
sendError(respond, err);
|
|
325
|
-
}
|
|
326
|
-
});
|
|
344
|
+
},
|
|
345
|
+
);
|
|
327
346
|
|
|
328
347
|
api.registerTool({
|
|
329
348
|
name: "voice_call",
|
|
@@ -332,7 +351,7 @@ const voiceCallPlugin = {
|
|
|
332
351
|
parameters: VoiceCallToolSchema,
|
|
333
352
|
async execute(_toolCallId, params) {
|
|
334
353
|
const json = (payload: unknown) => ({
|
|
335
|
-
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
354
|
+
content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
|
|
336
355
|
details: payload,
|
|
337
356
|
});
|
|
338
357
|
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Command } from "commander";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { sleep } from "openclaw/plugin-sdk";
|
|
5
6
|
import type { VoiceCallConfig } from "./config.js";
|
|
6
7
|
import type { VoiceCallRuntime } from "./runtime.js";
|
|
7
8
|
import { resolveUserPath } from "./utils.js";
|
|
@@ -40,10 +41,6 @@ function resolveDefaultStorePath(config: VoiceCallConfig): string {
|
|
|
40
41
|
return path.join(base, "calls.jsonl");
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
function sleep(ms: number): Promise<void> {
|
|
44
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
44
|
export function registerVoiceCallCli(params: {
|
|
48
45
|
program: Command;
|
|
49
46
|
config: VoiceCallConfig;
|
|
@@ -146,7 +146,7 @@ export async function generateVoiceResponse(
|
|
|
146
146
|
|
|
147
147
|
const text = texts.join(" ") || null;
|
|
148
148
|
|
|
149
|
-
if (!text && result.meta
|
|
149
|
+
if (!text && result.meta?.aborted) {
|
|
150
150
|
return { text: null, error: "Response generation was aborted" };
|
|
151
151
|
}
|
|
152
152
|
|
package/src/runtime.ts
CHANGED
|
@@ -30,7 +30,7 @@ type Logger = {
|
|
|
30
30
|
info: (message: string) => void;
|
|
31
31
|
warn: (message: string) => void;
|
|
32
32
|
error: (message: string) => void;
|
|
33
|
-
debug
|
|
33
|
+
debug?: (message: string) => void;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
function isLoopbackBind(bind: string | undefined): boolean {
|
package/src/webhook.ts
CHANGED
|
@@ -296,23 +296,48 @@ export class VoiceCallWebhookServer {
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
/**
|
|
299
|
-
* Read request body as string.
|
|
299
|
+
* Read request body as string with timeout protection.
|
|
300
300
|
*/
|
|
301
|
-
private readBody(
|
|
301
|
+
private readBody(
|
|
302
|
+
req: http.IncomingMessage,
|
|
303
|
+
maxBytes: number,
|
|
304
|
+
timeoutMs = 30_000,
|
|
305
|
+
): Promise<string> {
|
|
302
306
|
return new Promise((resolve, reject) => {
|
|
307
|
+
let done = false;
|
|
308
|
+
const finish = (fn: () => void) => {
|
|
309
|
+
if (done) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
done = true;
|
|
313
|
+
clearTimeout(timer);
|
|
314
|
+
fn();
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const timer = setTimeout(() => {
|
|
318
|
+
finish(() => {
|
|
319
|
+
const err = new Error("Request body timeout");
|
|
320
|
+
req.destroy(err);
|
|
321
|
+
reject(err);
|
|
322
|
+
});
|
|
323
|
+
}, timeoutMs);
|
|
324
|
+
|
|
303
325
|
const chunks: Buffer[] = [];
|
|
304
326
|
let totalBytes = 0;
|
|
305
327
|
req.on("data", (chunk: Buffer) => {
|
|
306
328
|
totalBytes += chunk.length;
|
|
307
329
|
if (totalBytes > maxBytes) {
|
|
308
|
-
|
|
309
|
-
|
|
330
|
+
finish(() => {
|
|
331
|
+
req.destroy();
|
|
332
|
+
reject(new Error("PayloadTooLarge"));
|
|
333
|
+
});
|
|
310
334
|
return;
|
|
311
335
|
}
|
|
312
336
|
chunks.push(chunk);
|
|
313
337
|
});
|
|
314
|
-
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
315
|
-
req.on("error", reject);
|
|
338
|
+
req.on("end", () => finish(() => resolve(Buffer.concat(chunks).toString("utf-8"))));
|
|
339
|
+
req.on("error", (err) => finish(() => reject(err)));
|
|
340
|
+
req.on("close", () => finish(() => reject(new Error("Connection closed"))));
|
|
316
341
|
});
|
|
317
342
|
}
|
|
318
343
|
|