mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 08:12:43 +00:00
Channels: move single-account config into accounts.default (#27334)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 50b5771808
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
committed by
GitHub
parent
da6a96ed33
commit
dfa0b5b4fc
@@ -554,6 +554,39 @@ describe("patchChannelConfigForAccount", () => {
|
||||
expect(next.channels?.slack?.accounts?.work?.appToken).toBe("new-app");
|
||||
});
|
||||
|
||||
it("moves single-account config into default account when patching non-default", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
telegram: {
|
||||
enabled: true,
|
||||
botToken: "legacy-token",
|
||||
allowFrom: ["100"],
|
||||
groupPolicy: "allowlist",
|
||||
streaming: "partial",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const next = patchChannelConfigForAccount({
|
||||
cfg,
|
||||
channel: "telegram",
|
||||
accountId: "work",
|
||||
patch: { botToken: "work-token" },
|
||||
});
|
||||
|
||||
expect(next.channels?.telegram?.accounts?.default).toEqual({
|
||||
botToken: "legacy-token",
|
||||
allowFrom: ["100"],
|
||||
groupPolicy: "allowlist",
|
||||
streaming: "partial",
|
||||
});
|
||||
expect(next.channels?.telegram?.botToken).toBeUndefined();
|
||||
expect(next.channels?.telegram?.allowFrom).toBeUndefined();
|
||||
expect(next.channels?.telegram?.groupPolicy).toBeUndefined();
|
||||
expect(next.channels?.telegram?.streaming).toBeUndefined();
|
||||
expect(next.channels?.telegram?.accounts?.work?.botToken).toBe("work-token");
|
||||
});
|
||||
|
||||
it("supports imessage/signal account-scoped channel patches", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { promptAccountId as promptAccountIdSdk } from "../../../plugin-sdk/onboa
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js";
|
||||
import type { WizardPrompter } from "../../../wizard/prompts.js";
|
||||
import type { PromptAccountId, PromptAccountIdParams } from "../onboarding-types.js";
|
||||
import { moveSingleAccountChannelSectionToDefaultAccount } from "../setup-helpers.js";
|
||||
|
||||
export const promptAccountId: PromptAccountId = async (params: PromptAccountIdParams) => {
|
||||
return await promptAccountIdSdk(params);
|
||||
@@ -282,13 +283,21 @@ function patchConfigForScopedAccount(params: {
|
||||
ensureEnabled: boolean;
|
||||
}): OpenClawConfig {
|
||||
const { cfg, channel, accountId, patch, ensureEnabled } = params;
|
||||
const channelConfig = (cfg.channels?.[channel] as Record<string, unknown> | undefined) ?? {};
|
||||
const seededCfg =
|
||||
accountId === DEFAULT_ACCOUNT_ID
|
||||
? cfg
|
||||
: moveSingleAccountChannelSectionToDefaultAccount({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
});
|
||||
const channelConfig =
|
||||
(seededCfg.channels?.[channel] as Record<string, unknown> | undefined) ?? {};
|
||||
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...cfg,
|
||||
...seededCfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
...seededCfg.channels,
|
||||
[channel]: {
|
||||
...channelConfig,
|
||||
...(ensureEnabled ? { enabled: true } : {}),
|
||||
@@ -303,9 +312,9 @@ function patchConfigForScopedAccount(params: {
|
||||
const existingAccount = accounts[accountId] ?? {};
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
...seededCfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
...seededCfg.channels,
|
||||
[channel]: {
|
||||
...channelConfig,
|
||||
...(ensureEnabled ? { enabled: true } : {}),
|
||||
|
||||
@@ -119,3 +119,115 @@ export function migrateBaseNameToDefaultAccount(params: {
|
||||
},
|
||||
} 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user