diff --git a/extensions/voice-call/src/config.ts b/extensions/voice-call/src/config.ts index 9721cf380..2d1494c78 100644 --- a/extensions/voice-call/src/config.ts +++ b/extensions/voice-call/src/config.ts @@ -5,6 +5,7 @@ import { TtsProviderSchema, } from "openclaw/plugin-sdk/voice-call"; import { z } from "zod"; +import { deepMergeDefined } from "./deep-merge.js"; // ----------------------------------------------------------------------------- // Phone Number Validation @@ -376,45 +377,7 @@ function normalizeVoiceCallTtsConfig( return undefined; } - return TtsConfigSchema.parse({ - ...(defaults ?? {}), - ...(overrides ?? {}), - modelOverrides: - defaults?.modelOverrides || overrides?.modelOverrides - ? { - ...(defaults?.modelOverrides ?? {}), - ...(overrides?.modelOverrides ?? {}), - } - : undefined, - elevenlabs: - defaults?.elevenlabs || overrides?.elevenlabs - ? { - ...(defaults?.elevenlabs ?? {}), - ...(overrides?.elevenlabs ?? {}), - voiceSettings: - defaults?.elevenlabs?.voiceSettings || overrides?.elevenlabs?.voiceSettings - ? { - ...(defaults?.elevenlabs?.voiceSettings ?? {}), - ...(overrides?.elevenlabs?.voiceSettings ?? {}), - } - : undefined, - } - : undefined, - openai: - defaults?.openai || overrides?.openai - ? { - ...(defaults?.openai ?? {}), - ...(overrides?.openai ?? {}), - } - : undefined, - edge: - defaults?.edge || overrides?.edge - ? { - ...(defaults?.edge ?? {}), - ...(overrides?.edge ?? {}), - } - : undefined, - }); + return TtsConfigSchema.parse(deepMergeDefined(defaults ?? {}, overrides ?? {})); } export function normalizeVoiceCallConfig(config: VoiceCallConfigInput): VoiceCallConfig { diff --git a/extensions/voice-call/src/deep-merge.ts b/extensions/voice-call/src/deep-merge.ts new file mode 100644 index 000000000..b889ec14e --- /dev/null +++ b/extensions/voice-call/src/deep-merge.ts @@ -0,0 +1,23 @@ +const BLOCKED_MERGE_KEYS = new Set(["__proto__", "prototype", "constructor"]); + +export function deepMergeDefined(base: unknown, override: unknown): unknown { + if (!isPlainObject(base) || !isPlainObject(override)) { + return override === undefined ? base : override; + } + + const result: Record = { ...base }; + for (const [key, value] of Object.entries(override)) { + if (BLOCKED_MERGE_KEYS.has(key) || value === undefined) { + continue; + } + + const existing = result[key]; + result[key] = key in result ? deepMergeDefined(existing, value) : value; + } + + return result; +} + +function isPlainObject(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} diff --git a/extensions/voice-call/src/telephony-tts.ts b/extensions/voice-call/src/telephony-tts.ts index da8e5f71a..f753a69f1 100644 --- a/extensions/voice-call/src/telephony-tts.ts +++ b/extensions/voice-call/src/telephony-tts.ts @@ -1,5 +1,6 @@ import type { VoiceCallTtsConfig } from "./config.js"; import type { CoreConfig } from "./core-bridge.js"; +import { deepMergeDefined } from "./deep-merge.js"; import { convertPcmToMulaw8k } from "./telephony-audio.js"; export type TelephonyTtsRuntime = { @@ -20,8 +21,6 @@ export type TelephonyTtsProvider = { synthesizeForTelephony: (text: string) => Promise; }; -const BLOCKED_MERGE_KEYS = new Set(["__proto__", "prototype", "constructor"]); - export function createTelephonyTtsProvider(params: { coreConfig: CoreConfig; ttsOverride?: VoiceCallTtsConfig; @@ -79,28 +78,5 @@ function mergeTtsConfig( if (!base) { return override; } - return deepMerge(base, override); -} - -function deepMerge(base: T, override: T): T { - if (!isPlainObject(base) || !isPlainObject(override)) { - return override; - } - const result: Record = { ...base }; - for (const [key, value] of Object.entries(override)) { - if (BLOCKED_MERGE_KEYS.has(key) || value === undefined) { - continue; - } - const existing = (base as Record)[key]; - if (isPlainObject(existing) && isPlainObject(value)) { - result[key] = deepMerge(existing, value); - } else { - result[key] = value; - } - } - return result as T; -} - -function isPlainObject(value: unknown): value is Record { - return Boolean(value) && typeof value === "object" && !Array.isArray(value); + return deepMergeDefined(base, override) as VoiceCallTtsConfig; }