import type { OpenClawConfig } from "../../config/config.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js"; type ChannelSectionBase = { name?: string; accounts?: Record>; }; function channelHasAccounts(cfg: OpenClawConfig, channelKey: string): boolean { const channels = cfg.channels as Record | undefined; const base = channels?.[channelKey] as ChannelSectionBase | undefined; return Boolean(base?.accounts && Object.keys(base.accounts).length > 0); } function shouldStoreNameInAccounts(params: { cfg: OpenClawConfig; channelKey: string; accountId: string; alwaysUseAccounts?: boolean; }): boolean { if (params.alwaysUseAccounts) { return true; } if (params.accountId !== DEFAULT_ACCOUNT_ID) { return true; } return channelHasAccounts(params.cfg, params.channelKey); } export function applyAccountNameToChannelSection(params: { cfg: OpenClawConfig; channelKey: string; accountId: string; name?: string; alwaysUseAccounts?: boolean; }): OpenClawConfig { const trimmed = params.name?.trim(); if (!trimmed) { return params.cfg; } const accountId = normalizeAccountId(params.accountId); const channels = params.cfg.channels as Record | undefined; const baseConfig = channels?.[params.channelKey]; const base = typeof baseConfig === "object" && baseConfig ? (baseConfig as ChannelSectionBase) : undefined; const useAccounts = shouldStoreNameInAccounts({ cfg: params.cfg, channelKey: params.channelKey, accountId, alwaysUseAccounts: params.alwaysUseAccounts, }); if (!useAccounts && accountId === DEFAULT_ACCOUNT_ID) { const safeBase = base ?? {}; return { ...params.cfg, channels: { ...params.cfg.channels, [params.channelKey]: { ...safeBase, name: trimmed, }, }, } as OpenClawConfig; } const baseAccounts: Record> = base?.accounts ?? {}; const existingAccount = baseAccounts[accountId] ?? {}; const baseWithoutName = accountId === DEFAULT_ACCOUNT_ID ? (({ name: _ignored, ...rest }) => rest)(base ?? {}) : (base ?? {}); return { ...params.cfg, channels: { ...params.cfg.channels, [params.channelKey]: { ...baseWithoutName, accounts: { ...baseAccounts, [accountId]: { ...existingAccount, name: trimmed, }, }, }, }, } as OpenClawConfig; } export function migrateBaseNameToDefaultAccount(params: { cfg: OpenClawConfig; channelKey: string; alwaysUseAccounts?: boolean; }): OpenClawConfig { if (params.alwaysUseAccounts) { return params.cfg; } const channels = params.cfg.channels as Record | undefined; const base = channels?.[params.channelKey] as ChannelSectionBase | undefined; const baseName = base?.name?.trim(); if (!baseName) { return params.cfg; } const accounts: Record> = { ...base?.accounts, }; const defaultAccount = accounts[DEFAULT_ACCOUNT_ID] ?? {}; if (!defaultAccount.name) { accounts[DEFAULT_ACCOUNT_ID] = { ...defaultAccount, name: baseName }; } const { name: _ignored, ...rest } = base ?? {}; return { ...params.cfg, channels: { ...params.cfg.channels, [params.channelKey]: { ...rest, accounts, }, }, } as OpenClawConfig; } export function applySetupAccountConfigPatch(params: { cfg: OpenClawConfig; channelKey: string; accountId: string; patch: Record; }): OpenClawConfig { return patchScopedAccountConfig({ cfg: params.cfg, channelKey: params.channelKey, accountId: params.accountId, patch: params.patch, }); } export function patchScopedAccountConfig(params: { cfg: OpenClawConfig; channelKey: string; accountId: string; patch: Record; accountPatch?: Record; ensureChannelEnabled?: boolean; ensureAccountEnabled?: boolean; }): OpenClawConfig { const accountId = normalizeAccountId(params.accountId); const channels = params.cfg.channels as Record | undefined; const channelConfig = channels?.[params.channelKey]; const base = typeof channelConfig === "object" && channelConfig ? (channelConfig as Record & { accounts?: Record>; }) : undefined; const ensureChannelEnabled = params.ensureChannelEnabled ?? true; const ensureAccountEnabled = params.ensureAccountEnabled ?? ensureChannelEnabled; const patch = params.patch; const accountPatch = params.accountPatch ?? patch; if (accountId === DEFAULT_ACCOUNT_ID) { return { ...params.cfg, channels: { ...params.cfg.channels, [params.channelKey]: { ...base, ...(ensureChannelEnabled ? { enabled: true } : {}), ...patch, }, }, } as OpenClawConfig; } const accounts = base?.accounts ?? {}; const existingAccount = accounts[accountId] ?? {}; return { ...params.cfg, channels: { ...params.cfg.channels, [params.channelKey]: { ...base, ...(ensureChannelEnabled ? { enabled: true } : {}), accounts: { ...accounts, [accountId]: { ...existingAccount, ...(ensureAccountEnabled ? { enabled: typeof existingAccount.enabled === "boolean" ? existingAccount.enabled : true, } : {}), ...accountPatch, }, }, }, }, } as OpenClawConfig; } type ChannelSectionRecord = Record & { accounts?: Record>; }; const COMMON_SINGLE_ACCOUNT_KEYS_TO_MOVE = new Set([ "name", "token", "tokenFile", "botToken", "appToken", "account", "signalNumber", "authDir", "cliPath", "dbPath", "httpUrl", "httpHost", "httpPort", "webhookPath", "webhookUrl", "webhookSecret", "service", "region", "homeserver", "userId", "accessToken", "password", "deviceName", "url", "code", "dmPolicy", "allowFrom", "groupPolicy", "groupAllowFrom", "defaultTo", ]); const SINGLE_ACCOUNT_KEYS_TO_MOVE_BY_CHANNEL: Record> = { telegram: new Set(["streaming"]), }; export function shouldMoveSingleAccountChannelKey(params: { channelKey: string; key: string; }): boolean { if (COMMON_SINGLE_ACCOUNT_KEYS_TO_MOVE.has(params.key)) { return true; } return SINGLE_ACCOUNT_KEYS_TO_MOVE_BY_CHANNEL[params.channelKey]?.has(params.key) ?? false; } function cloneIfObject(value: T): T { if (value && typeof value === "object") { return structuredClone(value); } return value; } // When promoting a single-account channel config to multi-account, // move top-level account settings into accounts.default so the original // account keeps working without duplicate account values at channel root. export function moveSingleAccountChannelSectionToDefaultAccount(params: { cfg: OpenClawConfig; channelKey: string; }): OpenClawConfig { const channels = params.cfg.channels as Record | undefined; const baseConfig = channels?.[params.channelKey]; const base = typeof baseConfig === "object" && baseConfig ? (baseConfig as ChannelSectionRecord) : undefined; if (!base) { return params.cfg; } const accounts = base.accounts ?? {}; if (Object.keys(accounts).length > 0) { return params.cfg; } const keysToMove = Object.entries(base) .filter( ([key, value]) => key !== "accounts" && key !== "enabled" && value !== undefined && shouldMoveSingleAccountChannelKey({ channelKey: params.channelKey, key }), ) .map(([key]) => key); const defaultAccount: Record = {}; for (const key of keysToMove) { const value = base[key]; defaultAccount[key] = cloneIfObject(value); } const nextChannel: ChannelSectionRecord = { ...base }; for (const key of keysToMove) { delete nextChannel[key]; } return { ...params.cfg, channels: { ...params.cfg.channels, [params.channelKey]: { ...nextChannel, accounts: { ...accounts, [DEFAULT_ACCOUNT_ID]: defaultAccount, }, }, }, } as OpenClawConfig; }