From 79078f6a70bcf0eac8153544f13a74fb6ab710e2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 17 Mar 2026 05:32:11 +0000 Subject: [PATCH] refactor(setup): share env-aware patched adapters --- extensions/discord/src/setup-core.ts | 77 ++----------------- extensions/slack/src/setup-core.ts | 86 +++------------------- extensions/telegram/src/setup-core.ts | 24 ++---- src/channels/plugins/setup-helpers.test.ts | 34 +++++++++ src/channels/plugins/setup-helpers.ts | 29 ++++++++ src/plugin-sdk/setup.ts | 1 + 6 files changed, 89 insertions(+), 162 deletions(-) diff --git a/extensions/discord/src/setup-core.ts b/extensions/discord/src/setup-core.ts index 7f4c1be29d3..4b807f10a65 100644 --- a/extensions/discord/src/setup-core.ts +++ b/extensions/discord/src/setup-core.ts @@ -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; diff --git a/extensions/slack/src/setup-core.ts b/extensions/slack/src/setup-core.ts index 234cb6dc3c8..af71e5edc52 100644 --- a/extensions/slack/src/setup-core.ts +++ b/extensions/slack/src/setup-core.ts @@ -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; diff --git a/extensions/telegram/src/setup-core.ts b/extensions/telegram/src/setup-core.ts index 2791f7d2fbc..542fffc0500 100644 --- a/extensions/telegram/src/setup-core.ts +++ b/extensions/telegram/src/setup-core.ts @@ -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 } : {}, }); diff --git a/src/channels/plugins/setup-helpers.test.ts b/src/channels/plugins/setup-helpers.test.ts index 2040271f540..f81de4fe4ed 100644 --- a/src/channels/plugins/setup-helpers.test.ts +++ b/src/channels/plugins/setup-helpers.test.ts @@ -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({ diff --git a/src/channels/plugins/setup-helpers.ts b/src/channels/plugins/setup-helpers.ts index cfbd58a8d4e..e27f13e383a 100644 --- a/src/channels/plugins/setup-helpers.ts +++ b/src/channels/plugins/setup-helpers.ts @@ -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; +}): 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; diff --git a/src/plugin-sdk/setup.ts b/src/plugin-sdk/setup.ts index bd4e5283c97..065fbfeed9c 100644 --- a/src/plugin-sdk/setup.ts +++ b/src/plugin-sdk/setup.ts @@ -24,6 +24,7 @@ export { normalizeE164, pathExists } from "../utils.js"; export { applyAccountNameToChannelSection, applySetupAccountConfigPatch, + createEnvPatchedAccountSetupAdapter, createPatchedAccountSetupAdapter, migrateBaseNameToDefaultAccount, patchScopedAccountConfig,