Skip to content

Commit 46cd698

Browse files
committed
fix(plugins): mirror tts provider selection
1 parent 68fb619 commit 46cd698

3 files changed

Lines changed: 217 additions & 112 deletions

File tree

src/plugins/channel-plugin-ids.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -617,11 +617,34 @@ describe("resolveGatewayStartupPluginIds", () => {
617617
} as OpenClawConfig,
618618
["browser", "microsoft", "memory-core"],
619619
],
620+
[
621+
"includes active persona speech providers at startup",
622+
{
623+
channels: {},
624+
messages: {
625+
tts: {
626+
persona: "narrator",
627+
personas: {
628+
narrator: {
629+
label: "Narrator",
630+
provider: "microsoft",
631+
},
632+
},
633+
},
634+
},
635+
} as OpenClawConfig,
636+
["browser", "microsoft", "memory-core"],
637+
],
620638
[
621639
"honors disabled speech provider config blocks at startup",
622640
{
623641
channels: {},
624-
messages: { tts: { providers: { microsoft: { enabled: false } } } },
642+
messages: {
643+
tts: {
644+
provider: "microsoft",
645+
providers: { microsoft: { enabled: false } },
646+
},
647+
},
625648
} as OpenClawConfig,
626649
["browser", "memory-core"],
627650
],

src/plugins/gateway-startup-plugin-ids.ts

Lines changed: 4 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
1414
import { hasExplicitChannelConfig } from "./channel-presence-policy.js";
1515
import { collectPluginConfigContractMatches } from "./config-contracts.js";
1616
import { resolveEffectivePluginActivationState } from "./config-state.js";
17+
import {
18+
collectConfiguredSpeechProviderIds,
19+
normalizeConfiguredSpeechProviderIdForStartup,
20+
} from "./gateway-startup-speech-providers.js";
1721
import type { InstalledPluginIndexRecord } from "./installed-plugin-index.js";
1822
import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js";
1923
import {
@@ -161,117 +165,6 @@ function hasConfiguredActivationPath(params: {
161165
);
162166
}
163167

164-
const TTS_PROVIDER_CONFIG_RESERVED_KEYS = new Set([
165-
"auto",
166-
"enabled",
167-
"maxTextLength",
168-
"mode",
169-
"modelOverrides",
170-
"persona",
171-
"personas",
172-
"prefsPath",
173-
"provider",
174-
"providers",
175-
"summaryModel",
176-
"timeoutMs",
177-
]);
178-
179-
function normalizeConfiguredSpeechProviderIdForStartup(value: unknown): string | undefined {
180-
if (typeof value !== "string") {
181-
return undefined;
182-
}
183-
const normalized = normalizeOptionalLowercaseString(value);
184-
if (!normalized) {
185-
return undefined;
186-
}
187-
return normalized === "edge" ? "microsoft" : normalized;
188-
}
189-
190-
function addConfiguredTtsProviderIds(target: Set<string>, value: unknown): void {
191-
if (!isRecord(value)) {
192-
return;
193-
}
194-
const directProvider = normalizeConfiguredSpeechProviderIdForStartup(value.provider);
195-
if (directProvider) {
196-
target.add(directProvider);
197-
}
198-
if (isRecord(value.providers)) {
199-
for (const [providerId, providerConfig] of Object.entries(value.providers)) {
200-
if (!isConfigActivationValueEnabled(providerConfig)) {
201-
continue;
202-
}
203-
const normalized = normalizeConfiguredSpeechProviderIdForStartup(providerId);
204-
if (normalized) {
205-
target.add(normalized);
206-
}
207-
}
208-
}
209-
for (const [key, providerConfig] of Object.entries(value)) {
210-
if (TTS_PROVIDER_CONFIG_RESERVED_KEYS.has(key) || !isRecord(providerConfig)) {
211-
continue;
212-
}
213-
if (!isConfigActivationValueEnabled(providerConfig)) {
214-
continue;
215-
}
216-
const normalized = normalizeConfiguredSpeechProviderIdForStartup(key);
217-
if (normalized) {
218-
target.add(normalized);
219-
}
220-
}
221-
}
222-
223-
function collectConfiguredSpeechProviderIds(config: OpenClawConfig): ReadonlySet<string> {
224-
const configured = new Set<string>();
225-
addConfiguredTtsProviderIds(configured, config.messages?.tts);
226-
227-
const agents = config.agents;
228-
if (isRecord(agents)) {
229-
if (Array.isArray(agents.list)) {
230-
for (const agent of agents.list) {
231-
if (isRecord(agent)) {
232-
addConfiguredTtsProviderIds(configured, agent.tts);
233-
}
234-
}
235-
}
236-
}
237-
238-
const channels = config.channels;
239-
if (isRecord(channels)) {
240-
for (const channelConfig of Object.values(channels)) {
241-
if (!isRecord(channelConfig)) {
242-
continue;
243-
}
244-
addConfiguredTtsProviderIds(configured, channelConfig.tts);
245-
if (isRecord(channelConfig.voice)) {
246-
addConfiguredTtsProviderIds(configured, channelConfig.voice.tts);
247-
}
248-
if (isRecord(channelConfig.accounts)) {
249-
for (const accountConfig of Object.values(channelConfig.accounts)) {
250-
if (!isRecord(accountConfig)) {
251-
continue;
252-
}
253-
addConfiguredTtsProviderIds(configured, accountConfig.tts);
254-
if (isRecord(accountConfig.voice)) {
255-
addConfiguredTtsProviderIds(configured, accountConfig.voice.tts);
256-
}
257-
}
258-
}
259-
}
260-
}
261-
262-
const pluginEntries = config.plugins?.entries;
263-
if (isRecord(pluginEntries)) {
264-
for (const entry of Object.values(pluginEntries)) {
265-
if (!isRecord(entry) || !isRecord(entry.config)) {
266-
continue;
267-
}
268-
addConfiguredTtsProviderIds(configured, entry.config.tts);
269-
}
270-
}
271-
272-
return configured;
273-
}
274-
275168
function manifestOwnsConfiguredSpeechProvider(params: {
276169
manifest: PluginManifestRecord | undefined;
277170
configuredSpeechProviderIds: ReadonlySet<string>;
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import type { OpenClawConfig } from "../config/types.openclaw.js";
2+
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
3+
4+
const TTS_PROVIDER_CONFIG_RESERVED_KEYS = new Set([
5+
"auto",
6+
"enabled",
7+
"maxTextLength",
8+
"mode",
9+
"modelOverrides",
10+
"persona",
11+
"personas",
12+
"prefsPath",
13+
"provider",
14+
"providers",
15+
"summaryModel",
16+
"timeoutMs",
17+
]);
18+
19+
function isRecord(value: unknown): value is Record<string, unknown> {
20+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
21+
}
22+
23+
function isConfigActivationValueEnabled(value: unknown): boolean {
24+
if (value === false) {
25+
return false;
26+
}
27+
if (isRecord(value) && value.enabled === false) {
28+
return false;
29+
}
30+
return true;
31+
}
32+
33+
export function normalizeConfiguredSpeechProviderIdForStartup(value: unknown): string | undefined {
34+
if (typeof value !== "string") {
35+
return undefined;
36+
}
37+
const normalized = normalizeOptionalLowercaseString(value);
38+
if (!normalized) {
39+
return undefined;
40+
}
41+
return normalized === "edge" ? "microsoft" : normalized;
42+
}
43+
44+
function resolveProviderConfigActivation(
45+
ttsConfig: Record<string, unknown>,
46+
providerId: string,
47+
): boolean | undefined {
48+
let fromProviders: boolean | undefined;
49+
if (isRecord(ttsConfig.providers)) {
50+
for (const [key, providerConfig] of Object.entries(ttsConfig.providers)) {
51+
if (normalizeConfiguredSpeechProviderIdForStartup(key) === providerId) {
52+
fromProviders = isConfigActivationValueEnabled(providerConfig);
53+
}
54+
}
55+
}
56+
if (fromProviders !== undefined) {
57+
return fromProviders;
58+
}
59+
60+
for (const [key, providerConfig] of Object.entries(ttsConfig)) {
61+
if (TTS_PROVIDER_CONFIG_RESERVED_KEYS.has(key) || !isRecord(providerConfig)) {
62+
continue;
63+
}
64+
if (normalizeConfiguredSpeechProviderIdForStartup(key) === providerId) {
65+
return isConfigActivationValueEnabled(providerConfig);
66+
}
67+
}
68+
return undefined;
69+
}
70+
71+
function addProviderIfEnabled(
72+
target: Set<string>,
73+
ttsConfig: Record<string, unknown>,
74+
providerId: unknown,
75+
): void {
76+
const normalized = normalizeConfiguredSpeechProviderIdForStartup(providerId);
77+
if (!normalized) {
78+
return;
79+
}
80+
if (resolveProviderConfigActivation(ttsConfig, normalized) !== false) {
81+
target.add(normalized);
82+
}
83+
}
84+
85+
function findActivePersona(
86+
ttsConfig: Record<string, unknown>,
87+
): Record<string, unknown> | undefined {
88+
const personaId = normalizeOptionalLowercaseString(
89+
typeof ttsConfig.persona === "string" ? ttsConfig.persona : undefined,
90+
);
91+
if (!personaId || !isRecord(ttsConfig.personas)) {
92+
return undefined;
93+
}
94+
for (const [id, persona] of Object.entries(ttsConfig.personas)) {
95+
if (normalizeOptionalLowercaseString(id) === personaId && isRecord(persona)) {
96+
return persona;
97+
}
98+
}
99+
return undefined;
100+
}
101+
102+
function addActivePersonaProvider(target: Set<string>, ttsConfig: Record<string, unknown>): void {
103+
const persona = findActivePersona(ttsConfig);
104+
if (!persona) {
105+
return;
106+
}
107+
const provider = normalizeConfiguredSpeechProviderIdForStartup(persona.provider);
108+
if (!provider) {
109+
return;
110+
}
111+
const rootActivation = resolveProviderConfigActivation(ttsConfig, provider);
112+
const personaActivation = resolveProviderConfigActivation(persona, provider);
113+
if ((personaActivation ?? rootActivation) !== false) {
114+
target.add(provider);
115+
}
116+
}
117+
118+
function addConfiguredTtsProviderIds(target: Set<string>, value: unknown): void {
119+
if (!isRecord(value)) {
120+
return;
121+
}
122+
addProviderIfEnabled(target, value, value.provider);
123+
addActivePersonaProvider(target, value);
124+
125+
if (isRecord(value.providers)) {
126+
for (const [providerId, providerConfig] of Object.entries(value.providers)) {
127+
if (isConfigActivationValueEnabled(providerConfig)) {
128+
addProviderIfEnabled(target, value, providerId);
129+
}
130+
}
131+
}
132+
for (const [key, providerConfig] of Object.entries(value)) {
133+
if (TTS_PROVIDER_CONFIG_RESERVED_KEYS.has(key) || !isRecord(providerConfig)) {
134+
continue;
135+
}
136+
if (isConfigActivationValueEnabled(providerConfig)) {
137+
addProviderIfEnabled(target, value, key);
138+
}
139+
}
140+
}
141+
142+
export function collectConfiguredSpeechProviderIds(config: OpenClawConfig): ReadonlySet<string> {
143+
const configured = new Set<string>();
144+
addConfiguredTtsProviderIds(configured, config.messages?.tts);
145+
146+
const agents = config.agents;
147+
if (isRecord(agents) && Array.isArray(agents.list)) {
148+
for (const agent of agents.list) {
149+
if (isRecord(agent)) {
150+
addConfiguredTtsProviderIds(configured, agent.tts);
151+
}
152+
}
153+
}
154+
155+
const channels = config.channels;
156+
if (isRecord(channels)) {
157+
for (const channelConfig of Object.values(channels)) {
158+
if (!isRecord(channelConfig)) {
159+
continue;
160+
}
161+
addConfiguredTtsProviderIds(configured, channelConfig.tts);
162+
if (isRecord(channelConfig.voice)) {
163+
addConfiguredTtsProviderIds(configured, channelConfig.voice.tts);
164+
}
165+
if (isRecord(channelConfig.accounts)) {
166+
for (const accountConfig of Object.values(channelConfig.accounts)) {
167+
if (!isRecord(accountConfig)) {
168+
continue;
169+
}
170+
addConfiguredTtsProviderIds(configured, accountConfig.tts);
171+
if (isRecord(accountConfig.voice)) {
172+
addConfiguredTtsProviderIds(configured, accountConfig.voice.tts);
173+
}
174+
}
175+
}
176+
}
177+
}
178+
179+
const pluginEntries = config.plugins?.entries;
180+
if (isRecord(pluginEntries)) {
181+
for (const entry of Object.values(pluginEntries)) {
182+
if (isRecord(entry) && isRecord(entry.config)) {
183+
addConfiguredTtsProviderIds(configured, entry.config.tts);
184+
}
185+
}
186+
}
187+
188+
return configured;
189+
}

0 commit comments

Comments
 (0)