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
234 lines
6.1 KiB
TypeScript
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;
|
|
}
|