diff --git a/extensions/discord/src/doctor-contract.ts b/extensions/discord/src/doctor-contract.ts index 5354f199e52..af277af338e 100644 --- a/extensions/discord/src/doctor-contract.ts +++ b/extensions/discord/src/doctor-contract.ts @@ -3,11 +3,7 @@ import type { ChannelDoctorLegacyConfigRule, } from "openclaw/plugin-sdk/channel-contract"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; -import { - asObjectRecord, - normalizeLegacyDmAliases, - normalizeLegacyStreamingAliases, -} from "openclaw/plugin-sdk/runtime-doctor"; +import { asObjectRecord, normalizeLegacyChannelAliases } from "openclaw/plugin-sdk/runtime-doctor"; import { resolveDiscordPreviewStreamMode } from "./preview-streaming.js"; const LEGACY_TTS_PROVIDER_KEYS = ["openai", "elevenlabs", "microsoft", "edge"] as const; @@ -137,77 +133,40 @@ export function normalizeCompatibilityConfig({ let changed = false; const shouldPromoteRootDmAllowFrom = !asObjectRecord(updated.accounts); - const dm = normalizeLegacyDmAliases({ - entry: updated, + const aliases = normalizeLegacyChannelAliases({ + entry: rawEntry, pathPrefix: "channels.discord", changes, - promoteAllowFrom: shouldPromoteRootDmAllowFrom, - }); - updated = dm.entry; - changed = changed || dm.changed; - - const streaming = normalizeLegacyStreamingAliases({ - entry: updated, - pathPrefix: "channels.discord", - changes, - resolvedMode: resolveDiscordPreviewStreamMode(updated), - includePreviewChunk: true, - }); - updated = streaming.entry; - changed = changed || streaming.changed; - - const rawAccounts = asObjectRecord(updated.accounts); - if (rawAccounts) { - let accountsChanged = false; - const accounts = { ...rawAccounts }; - for (const [accountId, rawAccount] of Object.entries(rawAccounts)) { - const account = asObjectRecord(rawAccount); - if (!account) { - continue; - } - let accountEntry = account; - let accountChanged = false; - const accountDm = normalizeLegacyDmAliases({ - entry: accountEntry, - pathPrefix: `channels.discord.accounts.${accountId}`, - changes, - }); - accountEntry = accountDm.entry; - accountChanged = accountDm.changed; - const accountStreaming = normalizeLegacyStreamingAliases({ - entry: accountEntry, - pathPrefix: `channels.discord.accounts.${accountId}`, - changes, - resolvedMode: resolveDiscordPreviewStreamMode(accountEntry), - includePreviewChunk: true, - }); - accountEntry = accountStreaming.entry; - accountChanged = accountChanged || accountStreaming.changed; - const accountVoice = asObjectRecord(accountEntry.voice); + normalizeDm: true, + rootDmPromoteAllowFrom: shouldPromoteRootDmAllowFrom, + normalizeAccountDm: true, + resolveStreamingOptions: (entry) => ({ + resolvedMode: resolveDiscordPreviewStreamMode(entry), + includePreviewChunk: true, + }), + normalizeAccountExtra: ({ account, pathPrefix }) => { + const accountVoice = asObjectRecord(account.voice); if ( - accountVoice && - migrateLegacyTtsConfig( + !accountVoice || + !migrateLegacyTtsConfig( asObjectRecord(accountVoice.tts), - `channels.discord.accounts.${accountId}.voice.tts`, + `${pathPrefix}.voice.tts`, changes, ) ) { - accountEntry = { - ...accountEntry, + return { entry: account, changed: false }; + } + return { + entry: { + ...account, voice: accountVoice, - }; - accountChanged = true; - } - if (accountChanged) { - accounts[accountId] = accountEntry; - accountsChanged = true; - } - } - if (accountsChanged) { - updated = { ...updated, accounts }; - changed = true; - } - } + }, + changed: true, + }; + }, + }); + updated = aliases.entry; + changed = aliases.changed; const voice = asObjectRecord(updated.voice); if ( diff --git a/extensions/slack/src/doctor-contract.ts b/extensions/slack/src/doctor-contract.ts index baaba34df63..237d38ce7f6 100644 --- a/extensions/slack/src/doctor-contract.ts +++ b/extensions/slack/src/doctor-contract.ts @@ -4,19 +4,13 @@ import type { } from "openclaw/plugin-sdk/channel-contract"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { + asObjectRecord, hasLegacyAccountStreamingAliases, hasLegacyStreamingAliases, - normalizeLegacyDmAliases, - normalizeLegacyStreamingAliases, + normalizeLegacyChannelAliases, } from "openclaw/plugin-sdk/runtime-doctor"; import { resolveSlackNativeStreaming, resolveSlackStreamingMode } from "./streaming-compat.js"; -function asObjectRecord(value: unknown): Record | null { - return value && typeof value === "object" && !Array.isArray(value) - ? (value as Record) - : null; -} - function hasLegacySlackStreamingAliases(value: unknown): boolean { return hasLegacyStreamingAliases(value, { includeNativeTransport: true }); } @@ -50,61 +44,19 @@ export function normalizeCompatibilityConfig({ let updated = rawEntry; let changed = false; - const dm = normalizeLegacyDmAliases({ - entry: updated, + const aliases = normalizeLegacyChannelAliases({ + entry: rawEntry, pathPrefix: "channels.slack", changes, + normalizeDm: true, + normalizeAccountDm: true, + resolveStreamingOptions: (entry) => ({ + resolvedMode: resolveSlackStreamingMode(entry), + resolvedNativeTransport: resolveSlackNativeStreaming(entry), + }), }); - updated = dm.entry; - changed = changed || dm.changed; - - const streaming = normalizeLegacyStreamingAliases({ - entry: updated, - pathPrefix: "channels.slack", - changes, - resolvedMode: resolveSlackStreamingMode(updated), - resolvedNativeTransport: resolveSlackNativeStreaming(updated), - }); - updated = streaming.entry; - changed = changed || streaming.changed; - - const rawAccounts = asObjectRecord(updated.accounts); - if (rawAccounts) { - let accountsChanged = false; - const accounts = { ...rawAccounts }; - for (const [accountId, rawAccount] of Object.entries(rawAccounts)) { - const account = asObjectRecord(rawAccount); - if (!account) { - continue; - } - let accountEntry = account; - let accountChanged = false; - const accountDm = normalizeLegacyDmAliases({ - entry: accountEntry, - pathPrefix: `channels.slack.accounts.${accountId}`, - changes, - }); - accountEntry = accountDm.entry; - accountChanged = accountDm.changed; - const accountStreaming = normalizeLegacyStreamingAliases({ - entry: accountEntry, - pathPrefix: `channels.slack.accounts.${accountId}`, - changes, - resolvedMode: resolveSlackStreamingMode(accountEntry), - resolvedNativeTransport: resolveSlackNativeStreaming(accountEntry), - }); - accountEntry = accountStreaming.entry; - accountChanged = accountChanged || accountStreaming.changed; - if (accountChanged) { - accounts[accountId] = accountEntry; - accountsChanged = true; - } - } - if (accountsChanged) { - updated = { ...updated, accounts }; - changed = true; - } - } + updated = aliases.entry; + changed = aliases.changed; if (!changed) { return { config: cfg, changes: [] }; diff --git a/extensions/telegram/src/doctor-contract.ts b/extensions/telegram/src/doctor-contract.ts index be3ef1d00c0..d01601ad76f 100644 --- a/extensions/telegram/src/doctor-contract.ts +++ b/extensions/telegram/src/doctor-contract.ts @@ -7,7 +7,7 @@ import { asObjectRecord, hasLegacyAccountStreamingAliases, hasLegacyStreamingAliases, - normalizeLegacyStreamingAliases, + normalizeLegacyChannelAliases, } from "openclaw/plugin-sdk/runtime-doctor"; import { resolveTelegramPreviewStreamMode } from "./preview-streaming.js"; @@ -93,42 +93,17 @@ export function normalizeCompatibilityConfig({ } } - const streaming = normalizeLegacyStreamingAliases({ + const aliases = normalizeLegacyChannelAliases({ entry: updated, pathPrefix: "channels.telegram", changes, - includePreviewChunk: true, - resolvedMode: resolveTelegramPreviewStreamMode(updated), + resolveStreamingOptions: (entry) => ({ + includePreviewChunk: true, + resolvedMode: resolveTelegramPreviewStreamMode(entry), + }), }); - updated = streaming.entry; - changed = changed || streaming.changed; - - const rawAccounts = asObjectRecord(updated.accounts); - if (rawAccounts) { - let accountsChanged = false; - const accounts = { ...rawAccounts }; - for (const [accountId, rawAccount] of Object.entries(rawAccounts)) { - const account = asObjectRecord(rawAccount); - if (!account) { - continue; - } - const accountStreaming = normalizeLegacyStreamingAliases({ - entry: account, - pathPrefix: `channels.telegram.accounts.${accountId}`, - changes, - includePreviewChunk: true, - resolvedMode: resolveTelegramPreviewStreamMode(account), - }); - if (accountStreaming.changed) { - accounts[accountId] = accountStreaming.entry; - accountsChanged = true; - } - } - if (accountsChanged) { - updated = { ...updated, accounts }; - changed = true; - } - } + updated = aliases.entry; + changed = changed || aliases.changed; if (!changed && changes.length === 0) { return { config: cfg, changes: [] }; diff --git a/src/config/channel-compat-normalization.ts b/src/config/channel-compat-normalization.ts index 0b664caf917..f8902e9a0de 100644 --- a/src/config/channel-compat-normalization.ts +++ b/src/config/channel-compat-normalization.ts @@ -1,10 +1,24 @@ import { normalizeStringEntries } from "../shared/string-normalization.js"; -type CompatMutationResult = { +export type CompatMutationResult = { entry: Record; changed: boolean; }; +export type LegacyStreamingAliasOptions = { + resolvedMode: string; + includePreviewChunk?: boolean; + resolvedNativeTransport?: unknown; + offModeLegacyNotice?: (pathPrefix: string) => string; +}; + +export type NormalizeLegacyChannelAccountParams = { + account: Record; + accountId: string; + pathPrefix: string; + changes: string[]; +}; + export function asObjectRecord(value: unknown): Record | null { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) @@ -120,15 +134,13 @@ export function normalizeLegacyDmAliases(params: { return { entry: updated, changed }; } -export function normalizeLegacyStreamingAliases(params: { - entry: Record; - pathPrefix: string; - changes: string[]; - resolvedMode: string; - includePreviewChunk?: boolean; - resolvedNativeTransport?: unknown; - offModeLegacyNotice?: (pathPrefix: string) => string; -}): CompatMutationResult { +export function normalizeLegacyStreamingAliases( + params: { + entry: Record; + pathPrefix: string; + changes: string[]; + } & LegacyStreamingAliasOptions, +): CompatMutationResult { const beforeStreaming = params.entry.streaming; const hadLegacyStreamMode = params.entry.streamMode !== undefined; const hasLegacyFlatFields = @@ -254,6 +266,98 @@ export function normalizeLegacyStreamingAliases(params: { return { entry: updated, changed }; } +export function normalizeLegacyChannelAliases(params: { + entry: Record; + pathPrefix: string; + changes: string[]; + normalizeDm?: boolean; + rootDmPromoteAllowFrom?: boolean; + normalizeAccountDm?: boolean; + resolveStreamingOptions: (entry: Record) => LegacyStreamingAliasOptions; + normalizeAccountExtra?: (params: NormalizeLegacyChannelAccountParams) => CompatMutationResult; +}): CompatMutationResult { + let updated = params.entry; + let changed = false; + + if (params.normalizeDm === true) { + const dm = normalizeLegacyDmAliases({ + entry: updated, + pathPrefix: params.pathPrefix, + changes: params.changes, + promoteAllowFrom: params.rootDmPromoteAllowFrom, + }); + updated = dm.entry; + changed = dm.changed; + } + + const streaming = normalizeLegacyStreamingAliases({ + entry: updated, + pathPrefix: params.pathPrefix, + changes: params.changes, + ...params.resolveStreamingOptions(updated), + }); + updated = streaming.entry; + changed = changed || streaming.changed; + + const rawAccounts = asObjectRecord(updated.accounts); + if (!rawAccounts) { + return { entry: updated, changed }; + } + + let accountsChanged = false; + const accounts = { ...rawAccounts }; + for (const [accountId, rawAccount] of Object.entries(rawAccounts)) { + const account = asObjectRecord(rawAccount); + if (!account) { + continue; + } + let accountEntry = account; + let accountChanged = false; + const accountPathPrefix = `${params.pathPrefix}.accounts.${accountId}`; + + if (params.normalizeAccountDm === true) { + const accountDm = normalizeLegacyDmAliases({ + entry: accountEntry, + pathPrefix: accountPathPrefix, + changes: params.changes, + }); + accountEntry = accountDm.entry; + accountChanged = accountDm.changed; + } + + const accountStreaming = normalizeLegacyStreamingAliases({ + entry: accountEntry, + pathPrefix: accountPathPrefix, + changes: params.changes, + ...params.resolveStreamingOptions(accountEntry), + }); + accountEntry = accountStreaming.entry; + accountChanged = accountChanged || accountStreaming.changed; + + const accountExtra = params.normalizeAccountExtra?.({ + account: accountEntry, + accountId, + pathPrefix: accountPathPrefix, + changes: params.changes, + }); + if (accountExtra) { + accountEntry = accountExtra.entry; + accountChanged = accountChanged || accountExtra.changed; + } + + if (accountChanged) { + accounts[accountId] = accountEntry; + accountsChanged = true; + } + } + if (accountsChanged) { + updated = { ...updated, accounts }; + changed = true; + } + + return { entry: updated, changed }; +} + export function hasLegacyStreamingAliases( value: unknown, options?: { includePreviewChunk?: boolean; includeNativeTransport?: boolean }, diff --git a/src/plugin-sdk/runtime-doctor.ts b/src/plugin-sdk/runtime-doctor.ts index 7abd77c3a6d..ae6e3df73fe 100644 --- a/src/plugin-sdk/runtime-doctor.ts +++ b/src/plugin-sdk/runtime-doctor.ts @@ -3,9 +3,15 @@ export { asObjectRecord, hasLegacyAccountStreamingAliases, hasLegacyStreamingAliases, + normalizeLegacyChannelAliases, normalizeLegacyDmAliases, normalizeLegacyStreamingAliases, } from "../config/channel-compat-normalization.js"; +export type { + CompatMutationResult, + LegacyStreamingAliasOptions, + NormalizeLegacyChannelAccountParams, +} from "../config/channel-compat-normalization.js"; export { detectPluginInstallPathIssue, formatPluginInstallPathIssue,