refactor(setup): share env-aware patched adapters

This commit is contained in:
Peter Steinberger
2026-03-17 05:32:11 +00:00
parent 3486bff7d5
commit 79078f6a70
6 changed files with 89 additions and 162 deletions

View File

@@ -1,10 +1,7 @@
import type { DiscordGuildEntry } from "openclaw/plugin-sdk/config-runtime";
import {
applyAccountNameToChannelSection,
createPatchedAccountSetupAdapter,
DEFAULT_ACCOUNT_ID,
migrateBaseNameToDefaultAccount,
normalizeAccountId,
createEnvPatchedAccountSetupAdapter,
noteChannelLookupFailure,
noteChannelLookupSummary,
parseMentionOrPrefixedId,
@@ -74,71 +71,13 @@ export function parseDiscordAllowFromId(value: string): string | null {
});
}
export const discordSetupAdapter: ChannelSetupAdapter = {
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
applyAccountName: ({ cfg, accountId, name }) =>
applyAccountNameToChannelSection({
cfg,
channelKey: channel,
accountId,
name,
}),
validateInput: ({ accountId, input }) => {
if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
return "DISCORD_BOT_TOKEN can only be used for the default account.";
}
if (!input.useEnv && !input.token) {
return "Discord requires token (or --use-env).";
}
return null;
},
applyAccountConfig: ({ cfg, accountId, input }) => {
const namedConfig = applyAccountNameToChannelSection({
cfg,
channelKey: channel,
accountId,
name: input.name,
});
const next =
accountId !== DEFAULT_ACCOUNT_ID
? migrateBaseNameToDefaultAccount({
cfg: namedConfig,
channelKey: channel,
})
: namedConfig;
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...next,
channels: {
...next.channels,
discord: {
...next.channels?.discord,
enabled: true,
...(input.useEnv ? {} : input.token ? { token: input.token } : {}),
},
},
};
}
return {
...next,
channels: {
...next.channels,
discord: {
...next.channels?.discord,
enabled: true,
accounts: {
...next.channels?.discord?.accounts,
[accountId]: {
...next.channels?.discord?.accounts?.[accountId],
enabled: true,
...(input.token ? { token: input.token } : {}),
},
},
},
},
};
},
};
export const discordSetupAdapter: ChannelSetupAdapter = createEnvPatchedAccountSetupAdapter({
channelKey: channel,
defaultAccountOnlyEnvError: "DISCORD_BOT_TOKEN can only be used for the default account.",
missingCredentialError: "Discord requires token (or --use-env).",
hasCredentials: (input) => Boolean(input.token),
buildPatch: (input) => (input.token ? { token: input.token } : {}),
});
export function createDiscordSetupWizardBase(handlers: {
promptAllowFrom: NonNullable<ChannelSetupDmPolicy["promptAllowFrom"]>;

View File

@@ -1,11 +1,8 @@
import {
applyAccountNameToChannelSection,
createAllowlistSetupWizardProxy,
createPatchedAccountSetupAdapter,
DEFAULT_ACCOUNT_ID,
createEnvPatchedAccountSetupAdapter,
hasConfiguredSecretInput,
migrateBaseNameToDefaultAccount,
normalizeAccountId,
type OpenClawConfig,
noteChannelLookupFailure,
noteChannelLookupSummary,
@@ -95,77 +92,16 @@ function createSlackTokenCredential(params: {
};
}
export const slackSetupAdapter: ChannelSetupAdapter = {
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
applyAccountName: ({ cfg, accountId, name }) =>
applyAccountNameToChannelSection({
cfg,
channelKey: channel,
accountId,
name,
}),
validateInput: ({ accountId, input }) => {
if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
return "Slack env tokens can only be used for the default account.";
}
if (!input.useEnv && (!input.botToken || !input.appToken)) {
return "Slack requires --bot-token and --app-token (or --use-env).";
}
return null;
},
applyAccountConfig: ({ cfg, accountId, input }) => {
const namedConfig = applyAccountNameToChannelSection({
cfg,
channelKey: channel,
accountId,
name: input.name,
});
const next =
accountId !== DEFAULT_ACCOUNT_ID
? migrateBaseNameToDefaultAccount({
cfg: namedConfig,
channelKey: channel,
})
: namedConfig;
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...next,
channels: {
...next.channels,
slack: {
...next.channels?.slack,
enabled: true,
...(input.useEnv
? {}
: {
...(input.botToken ? { botToken: input.botToken } : {}),
...(input.appToken ? { appToken: input.appToken } : {}),
}),
},
},
};
}
return {
...next,
channels: {
...next.channels,
slack: {
...next.channels?.slack,
enabled: true,
accounts: {
...next.channels?.slack?.accounts,
[accountId]: {
...next.channels?.slack?.accounts?.[accountId],
enabled: true,
...(input.botToken ? { botToken: input.botToken } : {}),
...(input.appToken ? { appToken: input.appToken } : {}),
},
},
},
},
};
},
};
export const slackSetupAdapter: ChannelSetupAdapter = createEnvPatchedAccountSetupAdapter({
channelKey: channel,
defaultAccountOnlyEnvError: "Slack env tokens can only be used for the default account.",
missingCredentialError: "Slack requires --bot-token and --app-token (or --use-env).",
hasCredentials: (input) => Boolean(input.botToken && input.appToken),
buildPatch: (input) => ({
...(input.botToken ? { botToken: input.botToken } : {}),
...(input.appToken ? { appToken: input.appToken } : {}),
}),
});
export function createSlackSetupWizardBase(handlers: {
promptAllowFrom: NonNullable<ChannelSetupDmPolicy["promptAllowFrom"]>;

View File

@@ -1,5 +1,5 @@
import {
createPatchedAccountSetupAdapter,
createEnvPatchedAccountSetupAdapter,
DEFAULT_ACCOUNT_ID,
patchChannelConfigForAccount,
promptResolvedAllowFrom,
@@ -107,23 +107,11 @@ export async function promptTelegramAllowFromForAccount(params: {
});
}
export const telegramSetupAdapter: ChannelSetupAdapter = createPatchedAccountSetupAdapter({
export const telegramSetupAdapter: ChannelSetupAdapter = createEnvPatchedAccountSetupAdapter({
channelKey: channel,
validateInput: ({ accountId, input }) => {
if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
return "TELEGRAM_BOT_TOKEN can only be used for the default account.";
}
if (!input.useEnv && !input.token && !input.tokenFile) {
return "Telegram requires token or --token-file (or --use-env).";
}
return null;
},
defaultAccountOnlyEnvError: "TELEGRAM_BOT_TOKEN can only be used for the default account.",
missingCredentialError: "Telegram requires token or --token-file (or --use-env).",
hasCredentials: (input) => Boolean(input.token || input.tokenFile),
buildPatch: (input) =>
input.useEnv
? {}
: input.tokenFile
? { tokenFile: input.tokenFile }
: input.token
? { botToken: input.token }
: {},
input.tokenFile ? { tokenFile: input.tokenFile } : input.token ? { botToken: input.token } : {},
});

View File

@@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../../config/config.js";
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
import {
applySetupAccountConfigPatch,
createEnvPatchedAccountSetupAdapter,
createPatchedAccountSetupAdapter,
prepareScopedSetupConfig,
} from "./setup-helpers.js";
@@ -162,6 +163,39 @@ describe("createPatchedAccountSetupAdapter", () => {
});
});
describe("createEnvPatchedAccountSetupAdapter", () => {
it("rejects env mode for named accounts and requires credentials otherwise", () => {
const adapter = createEnvPatchedAccountSetupAdapter({
channelKey: "telegram",
defaultAccountOnlyEnvError: "env only on default",
missingCredentialError: "token required",
hasCredentials: (input) => Boolean(input.token || input.tokenFile),
buildPatch: (input) => ({ token: input.token }),
});
expect(
adapter.validateInput?.({
accountId: "work",
input: { useEnv: true },
}),
).toBe("env only on default");
expect(
adapter.validateInput?.({
accountId: DEFAULT_ACCOUNT_ID,
input: {},
}),
).toBe("token required");
expect(
adapter.validateInput?.({
accountId: DEFAULT_ACCOUNT_ID,
input: { token: "tok" },
}),
).toBeNull();
});
});
describe("prepareScopedSetupConfig", () => {
it("stores the name and migrates it for named accounts when requested", () => {
const next = prepareScopedSetupConfig({

View File

@@ -204,6 +204,35 @@ export function createPatchedAccountSetupAdapter(params: {
};
}
export function createEnvPatchedAccountSetupAdapter(params: {
channelKey: string;
alwaysUseAccounts?: boolean;
ensureChannelEnabled?: boolean;
ensureAccountEnabled?: boolean;
defaultAccountOnlyEnvError: string;
missingCredentialError: string;
hasCredentials: (input: ChannelSetupInput) => boolean;
validateInput?: ChannelSetupAdapter["validateInput"];
buildPatch: (input: ChannelSetupInput) => Record<string, unknown>;
}): ChannelSetupAdapter {
return createPatchedAccountSetupAdapter({
channelKey: params.channelKey,
alwaysUseAccounts: params.alwaysUseAccounts,
ensureChannelEnabled: params.ensureChannelEnabled,
ensureAccountEnabled: params.ensureAccountEnabled,
validateInput: (inputParams) => {
if (inputParams.input.useEnv && inputParams.accountId !== DEFAULT_ACCOUNT_ID) {
return params.defaultAccountOnlyEnvError;
}
if (!inputParams.input.useEnv && !params.hasCredentials(inputParams.input)) {
return params.missingCredentialError;
}
return params.validateInput?.(inputParams) ?? null;
},
buildPatch: params.buildPatch,
});
}
export function patchScopedAccountConfig(params: {
cfg: OpenClawConfig;
channelKey: string;

View File

@@ -24,6 +24,7 @@ export { normalizeE164, pathExists } from "../utils.js";
export {
applyAccountNameToChannelSection,
applySetupAccountConfigPatch,
createEnvPatchedAccountSetupAdapter,
createPatchedAccountSetupAdapter,
migrateBaseNameToDefaultAccount,
patchScopedAccountConfig,