import {
REALTIME_VOICE_AGENT_CONSULT_TOOL_POLICIES,
type RealtimeVoiceAgentConsultToolPolicy,
} from "openclaw/plugin-sdk/realtime-voice"; import { z } from "openclaw/plugin-sdk/zod"; import { TtsAutoSchema, TtsConfigSchema, TtsModeSchema, TtsProviderSchema } from "../api.js"; import { deepMergeDefined } from "./deep-merge.js"; import { DEFAULT_VOICE_CALL_REALTIME_INSTRUCTIONS } from "./realtime-defaults.js";
export { DEFAULT_VOICE_CALL_REALTIME_INSTRUCTIONS } from "./realtime-defaults.js";
// ----------------------------------------------------------------------------- // Phone Number Validation // -----------------------------------------------------------------------------
/** * E.164 phone number format: +[country code][number] * Examples use 555 prefix (reserved for fictional numbers)
*/
export const E164Schema = z
.string()
.regex(/^\+[1-9]\d{1,14}$/, "Expected E.164 format, e.g. +15550001234");
export const VoiceCallWebhookSecurityConfigSchema = z
.object({ /** * Allowed hostnames for webhook URL reconstruction. * Only these hosts are accepted from forwarding headers.
*/
allowedHosts: z.array(z.string().min(1)).default([]), /** * Trust X-Forwarded-* headers without a hostname allowlist. * WARNING: Only enable if you trust your proxy configuration.
*/
trustForwardingHeaders: z.boolean().default(false), /** * Trusted proxy IP addresses. Forwarded headers are only trusted when * the remote IP matches one of these addresses.
*/
trustedProxyIPs: z.array(z.string().min(1)).default([]),
})
.strict()
.default({ allowedHosts: [], trustForwardingHeaders: false, trustedProxyIPs: [] });
export type WebhookSecurityConfig = z.infer<typeof VoiceCallWebhookSecurityConfigSchema>;
/** * Call mode determines how outbound calls behave: * - "notify": Deliver message and auto-hangup after delay (one-way notification) * - "conversation": Stay open for back-and-forth until explicit end or timeout
*/
export const CallModeSchema = z.enum(["notify", "conversation"]);
export type CallMode = z.infer<typeof CallModeSchema>;
export const OutboundConfigSchema = z
.object({ /** Default call mode for outbound calls */
defaultMode: CallModeSchema.default("notify"), /** Seconds to wait after TTS before auto-hangup in notify mode */
notifyHangupDelaySec: z.number().int().nonnegative().default(3),
})
.strict()
.default({ defaultMode: "notify", notifyHangupDelaySec: 3 });
export type OutboundConfig = z.infer<typeof OutboundConfigSchema>;
export const VoiceCallStreamingConfigSchema = z
.object({ /** Enable real-time audio streaming (requires WebSocket support) */
enabled: z.boolean().default(false), /** Provider id from registered realtime transcription providers. */
provider: z.string().min(1).optional(), /** WebSocket path for media stream connections */
streamPath: z.string().min(1).default("/voice/stream"), /** Provider-owned raw config blobs keyed by provider id. */
providers: VoiceCallStreamingProvidersConfigSchema, /** * Close unauthenticated media stream sockets if no valid `start` frame arrives in time. * Protects against pre-auth idle connection hold attacks.
*/
preStartTimeoutMs: z.number().int().positive().default(5000), /** Maximum number of concurrently pending (pre-start) media stream sockets. */
maxPendingConnections: z.number().int().positive().default(32), /** Maximum pending media stream sockets per source IP. */
maxPendingConnectionsPerIp: z.number().int().positive().default(4), /** Hard cap for all open media stream sockets (pending + active). */
maxConnections: z.number().int().positive().default(128),
})
.strict()
.default({
enabled: false,
streamPath: "/voice/stream",
providers: {},
preStartTimeoutMs: 5000,
maxPendingConnections: 32,
maxPendingConnectionsPerIp: 4,
maxConnections: 128,
});
export type VoiceCallStreamingConfig = z.infer<typeof VoiceCallStreamingConfigSchema>;
// ----------------------------------------------------------------------------- // Main Voice Call Configuration // -----------------------------------------------------------------------------
/** Maximum call duration in seconds */
maxDurationSeconds: z.number().int().positive().default(300),
/** * Maximum age of a call in seconds before it is automatically reaped. * Catches calls stuck before answer (for example, local mock calls that * never receive provider webhooks). Set to 0 to disable.
*/
staleCallReaperSeconds: z.number().int().nonnegative().default(120),
/** Silence timeout for end-of-speech detection (ms) */
silenceTimeoutMs: z.number().int().positive().default(800),
/** Timeout for user transcript (ms) */
transcriptTimeoutMs: z.number().int().positive().default(180000),
/** Ring timeout for outbound calls (ms) */
ringTimeoutMs: z.number().int().positive().default(30000),
/** Maximum concurrent calls */
maxConcurrentCalls: z.number().int().positive().default(1),
/** Webhook server configuration */
serve: VoiceCallServeConfigSchema,
/** * Resolves the configuration by merging environment variables into missing fields. * Returns a new configuration object with environment variables applied.
*/
export function resolveVoiceCallConfig(config: VoiceCallConfigInput): VoiceCallConfig { const resolved = normalizeVoiceCallConfig(config);
/** * Validate that the configuration has all required fields for the selected provider.
*/
export function validateProviderConfig(config: VoiceCallConfig): {
valid: boolean;
errors: string[];
} { const errors: string[] = [];
if (!config.provider) {
errors.push("plugins.entries.voice-call.config.provider is required");
}
if (!config.fromNumber && config.provider !== "mock") {
errors.push(
config.provider === "twilio"
? "plugins.entries.voice-call.config.fromNumber is required (or set TWILIO_FROM_NUMBER env)"
: "plugins.entries.voice-call.config.fromNumber is required",
);
}
if (config.provider === "telnyx") { if (!config.telnyx?.apiKey) {
errors.push( "plugins.entries.voice-call.config.telnyx.apiKey is required (or set TELNYX_API_KEY env)",
);
} if (!config.telnyx?.connectionId) {
errors.push( "plugins.entries.voice-call.config.telnyx.connectionId is required (or set TELNYX_CONNECTION_ID env)",
);
} if (!config.skipSignatureVerification && !config.telnyx?.publicKey) {
errors.push( "plugins.entries.voice-call.config.telnyx.publicKey is required (or set TELNYX_PUBLIC_KEY env)",
);
}
}
if (config.provider === "twilio") { if (!config.twilio?.accountSid) {
errors.push( "plugins.entries.voice-call.config.twilio.accountSid is required (or set TWILIO_ACCOUNT_SID env)",
);
} if (!config.twilio?.authToken) {
errors.push( "plugins.entries.voice-call.config.twilio.authToken is required (or set TWILIO_AUTH_TOKEN env)",
);
}
}
if (config.provider === "plivo") { if (!config.plivo?.authId) {
errors.push( "plugins.entries.voice-call.config.plivo.authId is required (or set PLIVO_AUTH_ID env)",
);
} if (!config.plivo?.authToken) {
errors.push( "plugins.entries.voice-call.config.plivo.authToken is required (or set PLIVO_AUTH_TOKEN env)",
);
}
}
if (config.realtime.enabled && config.inboundPolicy === "disabled") {
errors.push( 'plugins.entries.voice-call.config.inboundPolicy must not be "disabled" when realtime.enabled is true',
);
}
if (config.realtime.enabled && config.streaming.enabled) {
errors.push( "plugins.entries.voice-call.config.realtime.enabled and plugins.entries.voice-call.config.streaming.enabled cannot both be true",
);
}
if (config.realtime.enabled && config.provider && config.provider !== "twilio") {
errors.push( 'plugins.entries.voice-call.config.provider must be "twilio" when realtime.enabled is true',
);
}
return { valid: errors.length === 0, errors };
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.13 Sekunden
(vorverarbeitet am 2026-06-09)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.