Files
Moltbot/src/channels/plugins/setup-helpers.ts
Gustavo Madeira Santana dfa0b5b4fc Channels: move single-account config into accounts.default (#27334)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 50b57718085368d302680ec93fab67f5ed6140a4
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-26 04:06:03 -05:00

234 lines
6.1 KiB
TypeScript

import type { OpenClawConfig } from "../../config/config.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
type ChannelSectionBase = {
name?: string;
accounts?: Record<string, Record<string, unknown>>;
};
function channelHasAccounts(cfg: OpenClawConfig, channelKey: string): boolean {
const channels = cfg.channels as Record<string, unknown> | 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<string, unknown> | 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<string, Record<string, unknown>> = 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<string, unknown> | undefined;
const base = channels?.[params.channelKey] as ChannelSectionBase | undefined;
const baseName = base?.name?.trim();
if (!baseName) {
return params.cfg;
}
const accounts: Record<string, Record<string, unknown>> = {
...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;
}
type ChannelSectionRecord = Record<string, unknown> & {
accounts?: Record<string, Record<string, unknown>>;
};
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<string, ReadonlySet<string>> = {
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<T>(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<string, unknown> | 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<string, unknown> = {};
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;
}