openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
475 lines (474 loc) • 16.7 kB
JavaScript
import { i as normalizeLegacyChannelAliases, t as asObjectRecord } from "./runtime-doctor-Bf2V8rFY.js";
import { g as normalizeRealtimeVoiceActivationNamePrefix, p as isSupportedRealtimeVoiceActivationName } from "./realtime-voice-BRCFRGg7.js";
import { t as resolveDiscordPreviewStreamMode } from "./preview-streaming-B4v7xLda.js";
//#region extensions/discord/src/doctor-contract.ts
const LEGACY_TTS_PROVIDER_KEYS = [
"openai",
"elevenlabs",
"microsoft",
"edge"
];
function hasLegacyTtsProviderKeys(value) {
const tts = asObjectRecord(value);
if (!tts) return false;
return LEGACY_TTS_PROVIDER_KEYS.some((key) => Object.hasOwn(tts, key));
}
function hasLegacyDiscordAccountTtsProviderKeys(value) {
const accounts = asObjectRecord(value);
if (!accounts) return false;
return Object.values(accounts).some((accountValue) => {
return hasLegacyTtsProviderKeys(asObjectRecord(asObjectRecord(accountValue)?.voice)?.tts);
});
}
function hasLegacyDiscordGuildChannelAllowAlias(value) {
const guilds = asObjectRecord(asObjectRecord(value)?.guilds);
if (!guilds) return false;
return Object.values(guilds).some((guildValue) => {
const channels = asObjectRecord(asObjectRecord(guildValue)?.channels);
if (!channels) return false;
return Object.values(channels).some((channel) => Object.hasOwn(asObjectRecord(channel) ?? {}, "allow"));
});
}
function hasLegacyDiscordGuildChannelAgentId(value) {
const guilds = asObjectRecord(asObjectRecord(value)?.guilds);
if (!guilds) return false;
return Object.values(guilds).some((guildValue) => {
const channels = asObjectRecord(asObjectRecord(guildValue)?.channels);
if (!channels) return false;
return Object.values(channels).some((channel) => Object.hasOwn(asObjectRecord(channel) ?? {}, "agentId"));
});
}
function hasLegacyDiscordAccountGuildChannelAllowAlias(value) {
const accounts = asObjectRecord(value);
if (!accounts) return false;
return Object.values(accounts).some((account) => hasLegacyDiscordGuildChannelAllowAlias(account));
}
function hasLegacyDiscordAccountGuildChannelAgentId(value) {
const accounts = asObjectRecord(value);
if (!accounts) return false;
return Object.values(accounts).some((account) => hasLegacyDiscordGuildChannelAgentId(account));
}
function hasUnsupportedRealtimeWakeNamesInVoice(value) {
const wakeNames = asObjectRecord(asObjectRecord(value)?.realtime)?.wakeNames;
return Array.isArray(wakeNames) ? wakeNames.length === 0 || wakeNames.some((wakeName) => typeof wakeName === "string" && !isSupportedRealtimeVoiceActivationName(wakeName)) : false;
}
function hasUnsupportedDiscordRealtimeWakeNames(value) {
const entry = asObjectRecord(value);
if (!entry) return false;
return hasUnsupportedRealtimeWakeNamesInVoice(entry.voice);
}
function hasUnsupportedDiscordAccountRealtimeWakeNames(value) {
const accounts = asObjectRecord(value);
if (!accounts) return false;
return Object.values(accounts).some((account) => hasUnsupportedDiscordRealtimeWakeNames(account));
}
function mergeMissing(target, source) {
for (const [key, value] of Object.entries(source)) {
if (value === void 0) continue;
const existing = target[key];
if (existing === void 0) {
target[key] = value;
continue;
}
if (existing && typeof existing === "object" && !Array.isArray(existing) && value && typeof value === "object" && !Array.isArray(value)) mergeMissing(existing, value);
}
}
function getOrCreateTtsProviders(tts) {
const providers = asObjectRecord(tts.providers) ?? {};
tts.providers = providers;
return providers;
}
function mergeLegacyTtsProviderConfig(tts, legacyKey, providerId) {
const legacyValue = asObjectRecord(tts[legacyKey]);
if (!legacyValue) return false;
const providers = getOrCreateTtsProviders(tts);
const existing = asObjectRecord(providers[providerId]) ?? {};
const merged = structuredClone(existing);
mergeMissing(merged, legacyValue);
providers[providerId] = merged;
delete tts[legacyKey];
return true;
}
function migrateLegacyTtsConfig(tts, pathLabel, changes) {
if (!tts) return false;
let changed = false;
if (mergeLegacyTtsProviderConfig(tts, "openai", "openai")) {
changes.push(`Moved ${pathLabel}.openai → ${pathLabel}.providers.openai.`);
changed = true;
}
if (mergeLegacyTtsProviderConfig(tts, "elevenlabs", "elevenlabs")) {
changes.push(`Moved ${pathLabel}.elevenlabs → ${pathLabel}.providers.elevenlabs.`);
changed = true;
}
if (mergeLegacyTtsProviderConfig(tts, "microsoft", "microsoft")) {
changes.push(`Moved ${pathLabel}.microsoft → ${pathLabel}.providers.microsoft.`);
changed = true;
}
if (mergeLegacyTtsProviderConfig(tts, "edge", "microsoft")) {
changes.push(`Moved ${pathLabel}.edge → ${pathLabel}.providers.microsoft.`);
changed = true;
}
return changed;
}
function normalizeUnsupportedRealtimeWakeNames(entry, pathPrefix, changes) {
const voice = asObjectRecord(entry.voice);
const realtime = asObjectRecord(voice?.realtime);
const wakeNames = realtime?.wakeNames;
if (!voice || !realtime || !Array.isArray(wakeNames)) return {
entry,
changed: false
};
if (wakeNames.length === 0) {
const nextRealtime = { ...realtime };
delete nextRealtime.wakeNames;
changes.push(`Removed empty ${pathPrefix}.voice.realtime.wakeNames; unset wake names use the default agent/OpenClaw fallback.`);
return {
entry: {
...entry,
voice: {
...voice,
realtime: nextRealtime
}
},
changed: true
};
}
let normalized = 0;
let removed = 0;
const nextWakeNames = wakeNames.flatMap((wakeName) => {
if (typeof wakeName !== "string" || isSupportedRealtimeVoiceActivationName(wakeName)) return [wakeName];
const nextWakeName = normalizeRealtimeVoiceActivationNamePrefix(wakeName);
if (!nextWakeName) {
removed += 1;
return [];
}
normalized += 1;
return [nextWakeName];
});
if (normalized === 0 && removed === 0) return {
entry,
changed: false
};
const dedupedWakeNames = Array.from(new Set(nextWakeNames));
const nextRealtime = { ...realtime };
if (dedupedWakeNames.length > 0) nextRealtime.wakeNames = dedupedWakeNames;
else delete nextRealtime.wakeNames;
if (normalized > 0) changes.push(`Shortened ${normalized} unsupported ${pathPrefix}.voice.realtime.wakeNames entries to one or two words.`);
if (removed > 0) changes.push(`Removed ${removed} unsupported ${pathPrefix}.voice.realtime.wakeNames entries with no usable words.`);
return {
entry: {
...entry,
voice: {
...voice,
realtime: nextRealtime
}
},
changed: true
};
}
function normalizeDiscordGuildChannelAllowAliases(params) {
const guilds = asObjectRecord(params.entry.guilds);
if (!guilds) return {
entry: params.entry,
changed: false
};
let changed = false;
const nextGuilds = { ...guilds };
for (const [guildId, guildValue] of Object.entries(guilds)) {
const guild = asObjectRecord(guildValue);
const channels = asObjectRecord(guild?.channels);
if (!guild || !channels) continue;
let channelsChanged = false;
const nextChannels = { ...channels };
for (const [channelId, channelValue] of Object.entries(channels)) {
const channel = asObjectRecord(channelValue);
if (!channel || !Object.hasOwn(channel, "allow")) continue;
const nextChannel = { ...channel };
if (nextChannel.enabled === void 0) {
nextChannel.enabled = channel.allow;
params.changes.push(`Moved ${params.pathPrefix}.guilds.${guildId}.channels.${channelId}.allow → ${params.pathPrefix}.guilds.${guildId}.channels.${channelId}.enabled.`);
} else params.changes.push(`Removed ${params.pathPrefix}.guilds.${guildId}.channels.${channelId}.allow (${params.pathPrefix}.guilds.${guildId}.channels.${channelId}.enabled already set).`);
delete nextChannel.allow;
nextChannels[channelId] = nextChannel;
channelsChanged = true;
}
if (!channelsChanged) continue;
nextGuilds[guildId] = {
...guild,
channels: nextChannels
};
changed = true;
}
return changed ? {
entry: {
...params.entry,
guilds: nextGuilds
},
changed: true
} : {
entry: params.entry,
changed: false
};
}
function isDiscordChannelAgentBinding(value, match) {
const binding = asObjectRecord(value);
const bindingMatch = asObjectRecord(binding?.match);
const peer = asObjectRecord(bindingMatch?.peer);
if (!binding || !bindingMatch || !peer) return false;
return bindingMatch.channel === "discord" && bindingMatch.guildId === match.guildId && (match.accountId === void 0 || bindingMatch.accountId === match.accountId) && peer.kind === "channel" && peer.id === match.channelId;
}
function normalizeDiscordGuildChannelAgentIds(params) {
const guilds = asObjectRecord(params.entry.guilds);
if (!guilds) return {
entry: params.entry,
changed: false
};
const existingBindings = Array.isArray(params.cfg.bindings) ? params.cfg.bindings : [];
let changed = false;
const nextGuilds = { ...guilds };
for (const [guildId, guildValue] of Object.entries(guilds)) {
const guild = asObjectRecord(guildValue);
const channels = asObjectRecord(guild?.channels);
if (!guild || !channels) continue;
let channelsChanged = false;
const nextChannels = { ...channels };
for (const [channelId, channelValue] of Object.entries(channels)) {
const channel = asObjectRecord(channelValue);
if (!channel || !Object.hasOwn(channel, "agentId")) continue;
const nextChannel = { ...channel };
const rawAgentId = nextChannel.agentId;
delete nextChannel.agentId;
nextChannels[channelId] = nextChannel;
channelsChanged = true;
const path = `${params.pathPrefix}.guilds.${guildId}.channels.${channelId}.agentId`;
const agentId = typeof rawAgentId === "string" ? rawAgentId.trim() : "";
if (!agentId) {
params.changes.push(`Removed ${path}; configure top-level bindings[] for per-channel Discord agent routing.`);
continue;
}
const match = {
accountId: params.accountId,
guildId,
channelId
};
if (existingBindings.find((binding) => isDiscordChannelAgentBinding(binding, match))) {
params.changes.push(`Removed ${path}; a matching top-level bindings[] route already exists for Discord channel ${channelId}.`);
continue;
}
const bindingMatch = {
channel: "discord",
guildId,
peer: {
kind: "channel",
id: channelId
}
};
if (params.accountId) bindingMatch.accountId = params.accountId;
params.bindingsToAdd.push({
agentId,
match: bindingMatch
});
params.changes.push(`Moved ${path} → top-level bindings[] route for Discord channel ${channelId}.`);
}
if (!channelsChanged) continue;
nextGuilds[guildId] = {
...guild,
channels: nextChannels
};
changed = true;
}
return changed ? {
entry: {
...params.entry,
guilds: nextGuilds
},
changed: true
} : {
entry: params.entry,
changed: false
};
}
const legacyConfigRules = [
{
path: [
"channels",
"discord",
"voice",
"tts"
],
message: "channels.discord.voice.tts.<provider> keys (openai/elevenlabs/microsoft/edge) are legacy; use channels.discord.voice.tts.providers.<provider>. Run \"openclaw doctor --fix\".",
match: hasLegacyTtsProviderKeys
},
{
path: [
"channels",
"discord",
"accounts"
],
message: "channels.discord.accounts.<id>.voice.tts.<provider> keys (openai/elevenlabs/microsoft/edge) are legacy; use channels.discord.accounts.<id>.voice.tts.providers.<provider>. Run \"openclaw doctor --fix\".",
match: hasLegacyDiscordAccountTtsProviderKeys
},
{
path: ["channels", "discord"],
message: "channels.discord.guilds.<id>.channels.<id>.allow is legacy; use channels.discord.guilds.<id>.channels.<id>.enabled instead. Run \"openclaw doctor --fix\".",
match: hasLegacyDiscordGuildChannelAllowAlias
},
{
path: [
"channels",
"discord",
"accounts"
],
message: "channels.discord.accounts.<id>.guilds.<id>.channels.<id>.allow is legacy; use channels.discord.accounts.<id>.guilds.<id>.channels.<id>.enabled instead. Run \"openclaw doctor --fix\".",
match: hasLegacyDiscordAccountGuildChannelAllowAlias
},
{
path: ["channels", "discord"],
message: "channels.discord.guilds.<id>.channels.<id>.agentId is legacy; use top-level bindings[] for per-channel Discord agent routing. Run \"openclaw doctor --fix\".",
match: hasLegacyDiscordGuildChannelAgentId
},
{
path: [
"channels",
"discord",
"accounts"
],
message: "channels.discord.accounts.<id>.guilds.<id>.channels.<id>.agentId is legacy; use top-level bindings[] with match.accountId for per-channel Discord agent routing. Run \"openclaw doctor --fix\".",
match: hasLegacyDiscordAccountGuildChannelAgentId
},
{
path: ["channels", "discord"],
message: "channels.discord.voice.realtime.wakeNames entries longer than two words are unsupported; use one- or two-word activation names. Run \"openclaw doctor --fix\".",
match: hasUnsupportedDiscordRealtimeWakeNames
},
{
path: [
"channels",
"discord",
"accounts"
],
message: "channels.discord.accounts.<id>.voice.realtime.wakeNames entries longer than two words are unsupported; use one- or two-word activation names. Run \"openclaw doctor --fix\".",
match: hasUnsupportedDiscordAccountRealtimeWakeNames
}
];
function normalizeCompatibilityConfig({ cfg }) {
const rawEntry = asObjectRecord(cfg.channels?.discord);
if (!rawEntry) return {
config: cfg,
changes: []
};
const changes = [];
let updated;
let changed;
const bindingsToAdd = [];
const aliases = normalizeLegacyChannelAliases({
entry: rawEntry,
pathPrefix: "channels.discord",
changes,
normalizeDm: true,
normalizeAccountDm: true,
resolveStreamingOptions: (entry) => ({
resolvedMode: resolveDiscordPreviewStreamMode(entry),
includePreviewChunk: true
}),
normalizeAccountExtra: ({ account, pathPrefix }) => {
const accountVoice = asObjectRecord(account.voice);
if (!accountVoice || !migrateLegacyTtsConfig(asObjectRecord(accountVoice.tts), `${pathPrefix}.voice.tts`, changes)) return {
entry: account,
changed: false
};
return {
entry: {
...account,
voice: accountVoice
},
changed: true
};
}
});
updated = aliases.entry;
changed = aliases.changed;
const guildAliases = normalizeDiscordGuildChannelAllowAliases({
entry: updated,
pathPrefix: "channels.discord",
changes
});
updated = guildAliases.entry;
changed = changed || guildAliases.changed;
const channelAgentIds = normalizeDiscordGuildChannelAgentIds({
cfg,
entry: updated,
pathPrefix: "channels.discord",
changes,
bindingsToAdd
});
updated = channelAgentIds.entry;
changed = changed || channelAgentIds.changed;
const accounts = asObjectRecord(updated.accounts);
if (accounts) {
let accountsChanged = false;
const nextAccounts = { ...accounts };
for (const [accountId, accountValue] of Object.entries(accounts)) {
const account = asObjectRecord(accountValue);
if (!account) continue;
const normalized = normalizeDiscordGuildChannelAllowAliases({
entry: account,
pathPrefix: `channels.discord.accounts.${accountId}`,
changes
});
let nextAccount = normalized.entry;
let accountChanged = normalized.changed;
const normalizedAgentIds = normalizeDiscordGuildChannelAgentIds({
cfg,
entry: nextAccount,
pathPrefix: `channels.discord.accounts.${accountId}`,
accountId,
changes,
bindingsToAdd
});
nextAccount = normalizedAgentIds.entry;
accountChanged = accountChanged || normalizedAgentIds.changed;
const normalizedWakeNames = normalizeUnsupportedRealtimeWakeNames(nextAccount, `channels.discord.accounts.${accountId}`, changes);
nextAccount = normalizedWakeNames.entry;
accountChanged = accountChanged || normalizedWakeNames.changed;
if (!accountChanged) continue;
nextAccounts[accountId] = nextAccount;
accountsChanged = true;
}
if (accountsChanged) {
updated = {
...updated,
accounts: nextAccounts
};
changed = true;
}
}
const voice = asObjectRecord(updated.voice);
if (voice && migrateLegacyTtsConfig(asObjectRecord(voice.tts), "channels.discord.voice.tts", changes)) {
updated = {
...updated,
voice
};
changed = true;
}
const normalizedWakeNames = normalizeUnsupportedRealtimeWakeNames(updated, "channels.discord", changes);
updated = normalizedWakeNames.entry;
changed = changed || normalizedWakeNames.changed;
if (!changed) return {
config: cfg,
changes: []
};
return {
config: {
...cfg,
channels: {
...cfg.channels,
discord: updated
},
bindings: bindingsToAdd.length > 0 ? [...cfg.bindings ?? [], ...bindingsToAdd] : cfg.bindings
},
changes
};
}
//#endregion
export { normalizeCompatibilityConfig as n, legacyConfigRules as t };