openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
331 lines (330 loc) • 12.9 kB
JavaScript
import { a as normalizeLowercaseStringOrEmpty } from "./string-coerce-mnp54Vah.js";
import { t as getActiveRuntimePluginRegistry } from "./active-runtime-registry-PnN7H8vW.js";
import { n as resolvePluginCapabilityProvider, r as resolvePluginCapabilityProviders } from "./capability-provider-runtime-kNmuYvcj.js";
import { n as normalizeCapabilityProviderId, t as buildCapabilityProviderMaps } from "./provider-registry-shared-Djj3mWcp.js";
//#region src/tts/provider-registry-core.ts
/** Normalize user/provider IDs into the canonical speech provider ID shape. */
function normalizeSpeechProviderId(providerId) {
return normalizeCapabilityProviderId(providerId);
}
/** Create a registry facade with canonical listing, alias lookup, and ID canonicalization. */
function createSpeechProviderRegistry(resolver) {
const buildResolvedProviderMaps = (cfg) => buildCapabilityProviderMaps(resolver.listProviders(cfg));
const listProviders = (cfg) => [...buildResolvedProviderMaps(cfg).canonical.values()];
const getProvider = (providerId, cfg) => {
const normalized = normalizeSpeechProviderId(providerId);
if (!normalized) return;
return resolver.getProvider(normalized, cfg) ?? buildResolvedProviderMaps(cfg).aliases.get(normalized);
};
const canonicalizeProviderId = (providerId, cfg) => {
const normalized = normalizeSpeechProviderId(providerId);
if (!normalized) return;
return getProvider(normalized, cfg)?.id ?? normalized;
};
return {
canonicalizeSpeechProviderId: canonicalizeProviderId,
getSpeechProvider: getProvider,
listSpeechProviders: listProviders
};
}
//#endregion
//#region src/tts/provider-registry.ts
/** Resolve speech providers from configured plugin capabilities. */
function resolveSpeechProviderPluginEntries(cfg) {
return resolvePluginCapabilityProviders({
key: "speechProviders",
cfg
});
}
function resolveLoadedSpeechProviderPluginEntries() {
return (getActiveRuntimePluginRegistry()?.speechProviders ?? []).map((entry) => entry.provider);
}
/** Config-aware registry used by setup/status/runtime paths before plugins are loaded. */
const defaultSpeechProviderRegistry = createSpeechProviderRegistry({
getProvider: (providerId, cfg) => resolvePluginCapabilityProvider({
key: "speechProviders",
providerId,
cfg
}),
listProviders: resolveSpeechProviderPluginEntries
});
/** Loaded-only registry for runtime paths that must not rediscover plugin manifests. */
const loadedSpeechProviderRegistry = createSpeechProviderRegistry({
getProvider: (providerId) => resolveLoadedSpeechProviderPluginEntries().find((provider) => {
if (provider.id === providerId) return true;
return provider.aliases?.includes(providerId) ?? false;
}),
listProviders: () => resolveLoadedSpeechProviderPluginEntries()
});
/** List configured speech providers using manifest/capability discovery. */
const listSpeechProviders = defaultSpeechProviderRegistry.listSpeechProviders;
/** List currently loaded speech providers from the active runtime registry. */
const listLoadedSpeechProviders = loadedSpeechProviderRegistry.listSpeechProviders;
/** Resolve a configured speech provider by canonical ID or alias. */
const getSpeechProvider = defaultSpeechProviderRegistry.getSpeechProvider;
/** Resolve an input provider ID or alias to the provider's canonical ID. */
const canonicalizeSpeechProviderId = defaultSpeechProviderRegistry.canonicalizeSpeechProviderId;
//#endregion
//#region src/tts/directives.ts
function buildProviderOrder(left, right) {
const leftOrder = left.autoSelectOrder ?? Number.MAX_SAFE_INTEGER;
const rightOrder = right.autoSelectOrder ?? Number.MAX_SAFE_INTEGER;
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
return left.id.localeCompare(right.id);
}
function resolveDirectiveProviders(options) {
if (options?.providers) return [...options.providers].toSorted(buildProviderOrder);
return listSpeechProviders(options?.cfg).toSorted(buildProviderOrder);
}
function resolveDirectiveProviderConfig(provider, options) {
return options?.providerConfigs?.[provider.id];
}
function prioritizeProvider(providers, providerId) {
if (!providerId) return [...providers];
const preferredProvider = resolveDirectiveProvider(providers, providerId);
if (!preferredProvider) return [...providers];
return [preferredProvider, ...providers.filter((provider) => provider.id !== preferredProvider.id)];
}
function resolveDirectiveProvider(providers, providerId) {
const normalized = normalizeLowercaseStringOrEmpty(providerId);
if (!normalized) return;
return providers.find((provider) => provider.id === normalized || provider.aliases?.some((alias) => normalizeLowercaseStringOrEmpty(alias) === normalized));
}
function parseGenericSpeakerDirective(params) {
if (!params.policy.allowVoice) return;
switch (params.key) {
case "speakervoice":
case "speaker_voice": return {
...params.currentOverrides,
speakerVoice: params.value,
voice: params.value,
voiceName: params.value
};
case "speakervoiceid":
case "speaker_voice_id": return {
...params.currentOverrides,
speakerVoiceId: params.value,
voiceId: params.value
};
default: return;
}
}
function collectMarkdownCodeRanges(text) {
const ranges = [];
const addMatches = (regex) => {
for (const match of text.matchAll(regex)) {
if (match.index == null) continue;
ranges.push({
start: match.index,
end: match.index + match[0].length
});
}
};
addMatches(/```[\s\S]*?```/g);
addMatches(/~~~[\s\S]*?~~~/g);
addMatches(/^(?: {4}|\t).*(?:\n|$)/gm);
addMatches(/`+[^`\n]*`+/g);
return ranges.toSorted((left, right) => left.start - right.start);
}
function isInsideRange(index, ranges) {
return ranges.some((range) => index >= range.start && index < range.end);
}
function replaceOutsideMarkdownCode(text, regex, replace) {
const codeRanges = collectMarkdownCodeRanges(text);
return text.replace(regex, (...args) => {
const match = String(args[0]);
const offset = args.at(-2);
if (typeof offset === "number" && isInsideRange(offset, codeRanges)) return match;
return replace(match, args.slice(1, -2).map((capture) => String(capture)));
});
}
function normalizeTtsTagBody(body) {
return body.trim().replace(/\s+/g, "").toLowerCase();
}
function classifyTtsTag(body) {
const normalized = normalizeTtsTagBody(body);
if (normalized === "tts:text") return "hidden-open";
if (normalized === "/tts:text") return "hidden-close";
if (normalized === "tts" || normalized.startsWith("tts:") || normalized === "/tts" || normalized.startsWith("/tts:")) return "tts";
return "other";
}
/** Create an incremental cleaner for hiding [[tts:*]] directive text while streaming. */
function createTtsDirectiveTextStreamCleaner() {
let pending = "";
let insideHiddenTextBlock = false;
return {
push(text) {
const input = pending + text;
pending = "";
let output = "";
let index = 0;
while (index < input.length) {
const tagStart = input.indexOf("[[", index);
if (tagStart === -1) {
if (!insideHiddenTextBlock) output += input.slice(index);
break;
}
if (!insideHiddenTextBlock) output += input.slice(index, tagStart);
const tagEnd = input.indexOf("]]", tagStart + 2);
if (tagEnd === -1) {
pending = input.slice(tagStart);
break;
}
const rawTag = input.slice(tagStart, tagEnd + 2);
const tag = classifyTtsTag(input.slice(tagStart + 2, tagEnd));
if (tag === "hidden-open") insideHiddenTextBlock = true;
else if (tag === "hidden-close") insideHiddenTextBlock = false;
else if (tag === "other" && !insideHiddenTextBlock) output += rawTag;
index = tagEnd + 2;
}
return output;
},
flush() {
const tail = pending;
pending = "";
return insideHiddenTextBlock ? "" : tail;
},
hasBufferedDirectiveText() {
return pending.length > 0 || insideHiddenTextBlock;
}
};
}
/** Parse TTS directives from final message text, leaving markdown code spans unchanged. */
function parseTtsDirectives(text, policy, options) {
if (!policy.enabled) return {
cleanedText: text,
overrides: {},
warnings: [],
hasDirective: false
};
if (!/\[\[\s*\/?\s*tts(?:\s*:|\s*\]\])/iu.test(text)) return {
cleanedText: text,
overrides: {},
warnings: [],
hasDirective: false
};
let providers;
const getProviders = () => {
providers ??= resolveDirectiveProviders(options);
return providers;
};
const overrides = {};
const warnings = [];
let cleanedText = text;
let hasDirective = false;
cleanedText = replaceOutsideMarkdownCode(cleanedText, /\[\[\s*tts\s*:\s*text\s*\]\]([\s\S]*?)\[\[\s*\/\s*tts\s*:\s*text\s*\]\]/gi, (_match, [inner = ""]) => {
hasDirective = true;
if (policy.allowText && overrides.ttsText == null) overrides.ttsText = inner.trim();
return "";
});
cleanedText = replaceOutsideMarkdownCode(cleanedText, /\[\[\s*tts\s*\]\]([\s\S]*?)\[\[\s*\/\s*tts\s*\]\]/gi, (_match, [inner = ""]) => {
hasDirective = true;
const visible = inner.trim();
if (policy.allowText && overrides.ttsText == null) overrides.ttsText = visible;
return visible;
});
cleanedText = replaceOutsideMarkdownCode(cleanedText, /\[\[\s*tts\s*:\s*([^\]]+)\]\]/gi, (_match, [body = ""]) => {
hasDirective = true;
const tokens = body.split(/\s+/).filter(Boolean);
let declaredProviderId;
if (policy.allowProvider) for (const token of tokens) {
const eqIndex = token.indexOf("=");
if (eqIndex === -1) continue;
const rawKey = token.slice(0, eqIndex).trim();
if (!rawKey || normalizeLowercaseStringOrEmpty(rawKey) !== "provider") continue;
const rawValue = token.slice(eqIndex + 1).trim();
if (!rawValue) continue;
const providerId = normalizeLowercaseStringOrEmpty(rawValue);
if (!providerId) {
warnings.push("invalid provider id");
continue;
}
declaredProviderId = providerId;
overrides.provider = providerId;
}
let directiveProviders;
const getDirectiveProviders = () => {
if (directiveProviders) return directiveProviders;
if (declaredProviderId) {
const declaredProvider = resolveDirectiveProvider(getProviders(), declaredProviderId);
if (!declaredProvider) {
warnings.push(`unknown provider "${declaredProviderId}"`);
directiveProviders = [];
return directiveProviders;
}
directiveProviders = [declaredProvider];
return directiveProviders;
}
directiveProviders = prioritizeProvider(getProviders(), normalizeLowercaseStringOrEmpty(options?.preferredProviderId));
return directiveProviders;
};
for (const token of tokens) {
const eqIndex = token.indexOf("=");
if (eqIndex === -1) continue;
const rawKey = token.slice(0, eqIndex).trim();
const rawValue = token.slice(eqIndex + 1).trim();
if (!rawKey || !rawValue) continue;
const key = normalizeLowercaseStringOrEmpty(rawKey);
if (key === "provider") continue;
let handled = false;
const directiveProvidersLocal = getDirectiveProviders();
for (const provider of directiveProvidersLocal) {
const genericSpeakerOverrides = parseGenericSpeakerDirective({
key,
value: rawValue,
policy,
currentOverrides: overrides.providerOverrides?.[provider.id]
});
if (genericSpeakerOverrides) {
overrides.providerOverrides = {
...overrides.providerOverrides,
[provider.id]: {
...overrides.providerOverrides?.[provider.id],
...genericSpeakerOverrides
}
};
handled = true;
break;
}
const parsed = provider.parseDirectiveToken?.({
key,
value: rawValue,
policy,
selectedProvider: declaredProviderId ? provider.id : void 0,
providerConfig: resolveDirectiveProviderConfig(provider, options),
currentOverrides: overrides.providerOverrides?.[provider.id]
});
if (!parsed?.handled) continue;
if (parsed.overrides) overrides.providerOverrides = {
...overrides.providerOverrides,
[provider.id]: {
...overrides.providerOverrides?.[provider.id],
...parsed.overrides
}
};
if (parsed.warnings?.length) warnings.push(...parsed.warnings);
handled = true;
break;
}
if (!handled && declaredProviderId && directiveProvidersLocal.length > 0) warnings.push(`unsupported ${declaredProviderId} directive key "${key}"`);
}
return "";
});
cleanedText = replaceOutsideMarkdownCode(cleanedText, /\[\[\s*tts\s*\]\]/gi, () => {
hasDirective = true;
return "";
});
cleanedText = replaceOutsideMarkdownCode(cleanedText, /\[\[\s*\/\s*tts(?:\s*:\s*[^\]]*)?\]\]/gi, () => {
hasDirective = true;
return "";
});
return {
cleanedText,
ttsText: overrides.ttsText,
hasDirective,
overrides,
warnings
};
}
//#endregion
export { listLoadedSpeechProviders as a, getSpeechProvider as i, parseTtsDirectives as n, listSpeechProviders as o, canonicalizeSpeechProviderId as r, normalizeSpeechProviderId as s, createTtsDirectiveTextStreamCleaner as t };