import type { OpenClawConfig } from "../../config/config.js"; import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js"; import type { WizardPrompter } from "../../wizard/prompts.js"; import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy, ChannelOnboardingStatus, ChannelOnboardingStatusContext, } from "./onboarding-types.js"; import { configureChannelAccessWithAllowlist } from "./onboarding/channel-access-configure.js"; import type { ChannelAccessPolicy } from "./onboarding/channel-access.js"; import { promptResolvedAllowFrom, resolveAccountIdForConfigure, runSingleChannelSecretStep, splitOnboardingEntries, } from "./onboarding/helpers.js"; import type { ChannelSetupInput } from "./types.core.js"; import type { ChannelPlugin } from "./types.js"; export type ChannelSetupWizardStatus = { configuredLabel: string; unconfiguredLabel: string; configuredHint?: string; unconfiguredHint?: string; configuredScore?: number; unconfiguredScore?: number; resolveConfigured: (params: { cfg: OpenClawConfig }) => boolean | Promise; }; export type ChannelSetupWizardCredentialState = { accountConfigured: boolean; hasConfiguredValue: boolean; resolvedValue?: string; envValue?: string; }; type ChannelSetupWizardCredentialValues = Partial>; export type ChannelSetupWizardNote = { title: string; lines: string[]; shouldShow?: (params: { cfg: OpenClawConfig; accountId: string; credentialValues: ChannelSetupWizardCredentialValues; }) => boolean | Promise; }; export type ChannelSetupWizardEnvShortcut = { prompt: string; preferredEnvVar?: string; isAvailable: (params: { cfg: OpenClawConfig; accountId: string }) => boolean; apply: (params: { cfg: OpenClawConfig; accountId: string; }) => OpenClawConfig | Promise; }; export type ChannelSetupWizardCredential = { inputKey: keyof ChannelSetupInput; providerHint: string; credentialLabel: string; preferredEnvVar?: string; helpTitle?: string; helpLines?: string[]; envPrompt: string; keepPrompt: string; inputPrompt: string; allowEnv?: (params: { cfg: OpenClawConfig; accountId: string }) => boolean; inspect: (params: { cfg: OpenClawConfig; accountId: string; }) => ChannelSetupWizardCredentialState; applyUseEnv?: (params: { cfg: OpenClawConfig; accountId: string; }) => OpenClawConfig | Promise; applySet?: (params: { cfg: OpenClawConfig; accountId: string; value: unknown; resolvedValue: string; }) => OpenClawConfig | Promise; }; export type ChannelSetupWizardAllowFromEntry = { input: string; resolved: boolean; id: string | null; }; export type ChannelSetupWizardAllowFrom = { helpTitle?: string; helpLines?: string[]; credentialInputKey?: keyof ChannelSetupInput; message: string; placeholder: string; invalidWithoutCredentialNote: string; parseInputs?: (raw: string) => string[]; parseId: (raw: string) => string | null; resolveEntries: (params: { cfg: OpenClawConfig; accountId: string; credentialValues: ChannelSetupWizardCredentialValues; entries: string[]; }) => Promise; apply: (params: { cfg: OpenClawConfig; accountId: string; allowFrom: string[]; }) => OpenClawConfig | Promise; }; export type ChannelSetupWizardGroupAccess = { label: string; placeholder: string; helpTitle?: string; helpLines?: string[]; currentPolicy: (params: { cfg: OpenClawConfig; accountId: string }) => ChannelAccessPolicy; currentEntries: (params: { cfg: OpenClawConfig; accountId: string }) => string[]; updatePrompt: (params: { cfg: OpenClawConfig; accountId: string }) => boolean; setPolicy: (params: { cfg: OpenClawConfig; accountId: string; policy: ChannelAccessPolicy; }) => OpenClawConfig; resolveAllowlist: (params: { cfg: OpenClawConfig; accountId: string; credentialValues: ChannelSetupWizardCredentialValues; entries: string[]; prompter: Pick; }) => Promise; applyAllowlist: (params: { cfg: OpenClawConfig; accountId: string; resolved: unknown; }) => OpenClawConfig; }; export type ChannelSetupWizard = { channel: string; status: ChannelSetupWizardStatus; introNote?: ChannelSetupWizardNote; envShortcut?: ChannelSetupWizardEnvShortcut; credentials: ChannelSetupWizardCredential[]; dmPolicy?: ChannelOnboardingDmPolicy; allowFrom?: ChannelSetupWizardAllowFrom; groupAccess?: ChannelSetupWizardGroupAccess; disable?: (cfg: OpenClawConfig) => OpenClawConfig; onAccountRecorded?: ChannelOnboardingAdapter["onAccountRecorded"]; }; type ChannelSetupWizardPlugin = Pick; async function buildStatus( plugin: ChannelSetupWizardPlugin, wizard: ChannelSetupWizard, ctx: ChannelOnboardingStatusContext, ): Promise { const configured = await wizard.status.resolveConfigured({ cfg: ctx.cfg }); return { channel: plugin.id, configured, statusLines: [ `${plugin.meta.label}: ${configured ? wizard.status.configuredLabel : wizard.status.unconfiguredLabel}`, ], selectionHint: configured ? wizard.status.configuredHint : wizard.status.unconfiguredHint, quickstartScore: configured ? wizard.status.configuredScore : wizard.status.unconfiguredScore, }; } function applySetupInput(params: { plugin: ChannelSetupWizardPlugin; cfg: OpenClawConfig; accountId: string; input: ChannelSetupInput; }) { const setup = params.plugin.setup; if (!setup?.applyAccountConfig) { throw new Error(`${params.plugin.id} does not support setup`); } const resolvedAccountId = setup.resolveAccountId?.({ cfg: params.cfg, accountId: params.accountId, input: params.input, }) ?? params.accountId; const validationError = setup.validateInput?.({ cfg: params.cfg, accountId: resolvedAccountId, input: params.input, }); if (validationError) { throw new Error(validationError); } let next = setup.applyAccountConfig({ cfg: params.cfg, accountId: resolvedAccountId, input: params.input, }); if (params.input.name?.trim() && setup.applyAccountName) { next = setup.applyAccountName({ cfg: next, accountId: resolvedAccountId, name: params.input.name, }); } return { cfg: next, accountId: resolvedAccountId, }; } function trimResolvedValue(value?: string): string | undefined { const trimmed = value?.trim(); return trimmed ? trimmed : undefined; } function collectCredentialValues(params: { wizard: ChannelSetupWizard; cfg: OpenClawConfig; accountId: string; }): ChannelSetupWizardCredentialValues { const values: ChannelSetupWizardCredentialValues = {}; for (const credential of params.wizard.credentials) { const resolvedValue = trimResolvedValue( credential.inspect({ cfg: params.cfg, accountId: params.accountId, }).resolvedValue, ); if (resolvedValue) { values[credential.inputKey] = resolvedValue; } } return values; } export function buildChannelOnboardingAdapterFromSetupWizard(params: { plugin: ChannelSetupWizardPlugin; wizard: ChannelSetupWizard; }): ChannelOnboardingAdapter { const { plugin, wizard } = params; return { channel: plugin.id, getStatus: async (ctx) => buildStatus(plugin, wizard, ctx), configure: async ({ cfg, prompter, options, accountOverrides, shouldPromptAccountIds, forceAllowFrom, }) => { const defaultAccountId = plugin.config.defaultAccountId?.(cfg) ?? plugin.config.listAccountIds(cfg)[0] ?? DEFAULT_ACCOUNT_ID; const accountId = await resolveAccountIdForConfigure({ cfg, prompter, label: plugin.meta.label, accountOverride: accountOverrides[plugin.id], shouldPromptAccountIds, listAccountIds: plugin.config.listAccountIds, defaultAccountId, }); let next = cfg; let credentialValues = collectCredentialValues({ wizard, cfg: next, accountId, }); let usedEnvShortcut = false; if (wizard.envShortcut?.isAvailable({ cfg: next, accountId })) { const useEnvShortcut = await prompter.confirm({ message: wizard.envShortcut.prompt, initialValue: true, }); if (useEnvShortcut) { next = await wizard.envShortcut.apply({ cfg: next, accountId }); credentialValues = collectCredentialValues({ wizard, cfg: next, accountId, }); usedEnvShortcut = true; } } const shouldShowIntro = !usedEnvShortcut && (wizard.introNote?.shouldShow ? await wizard.introNote.shouldShow({ cfg: next, accountId, credentialValues, }) : Boolean(wizard.introNote)); if (shouldShowIntro && wizard.introNote) { await prompter.note(wizard.introNote.lines.join("\n"), wizard.introNote.title); } if (!usedEnvShortcut) { for (const credential of wizard.credentials) { let credentialState = credential.inspect({ cfg: next, accountId }); let resolvedCredentialValue = trimResolvedValue(credentialState.resolvedValue); const allowEnv = credential.allowEnv?.({ cfg: next, accountId }) ?? false; const credentialResult = await runSingleChannelSecretStep({ cfg: next, prompter, providerHint: credential.providerHint, credentialLabel: credential.credentialLabel, secretInputMode: options?.secretInputMode, accountConfigured: credentialState.accountConfigured, hasConfigToken: credentialState.hasConfiguredValue, allowEnv, envValue: credentialState.envValue, envPrompt: credential.envPrompt, keepPrompt: credential.keepPrompt, inputPrompt: credential.inputPrompt, preferredEnvVar: credential.preferredEnvVar, onMissingConfigured: credential.helpLines && credential.helpLines.length > 0 ? async () => { await prompter.note( credential.helpLines!.join("\n"), credential.helpTitle ?? credential.credentialLabel, ); } : undefined, applyUseEnv: async (currentCfg) => credential.applyUseEnv ? await credential.applyUseEnv({ cfg: currentCfg, accountId, }) : applySetupInput({ plugin, cfg: currentCfg, accountId, input: { [credential.inputKey]: undefined, useEnv: true, }, }).cfg, applySet: async (currentCfg, value, resolvedValue) => { resolvedCredentialValue = resolvedValue; return credential.applySet ? await credential.applySet({ cfg: currentCfg, accountId, value, resolvedValue, }) : applySetupInput({ plugin, cfg: currentCfg, accountId, input: { [credential.inputKey]: value, useEnv: false, }, }).cfg; }, }); next = credentialResult.cfg; credentialState = credential.inspect({ cfg: next, accountId }); resolvedCredentialValue = trimResolvedValue(credentialResult.resolvedValue) || trimResolvedValue(credentialState.resolvedValue); if (resolvedCredentialValue) { credentialValues[credential.inputKey] = resolvedCredentialValue; } else { delete credentialValues[credential.inputKey]; } } } if (wizard.groupAccess) { const access = wizard.groupAccess; if (access.helpLines && access.helpLines.length > 0) { await prompter.note(access.helpLines.join("\n"), access.helpTitle ?? access.label); } next = await configureChannelAccessWithAllowlist({ cfg: next, prompter, label: access.label, currentPolicy: access.currentPolicy({ cfg: next, accountId }), currentEntries: access.currentEntries({ cfg: next, accountId }), placeholder: access.placeholder, updatePrompt: access.updatePrompt({ cfg: next, accountId }), setPolicy: (currentCfg, policy) => access.setPolicy({ cfg: currentCfg, accountId, policy, }), resolveAllowlist: async ({ cfg: currentCfg, entries }) => await access.resolveAllowlist({ cfg: currentCfg, accountId, credentialValues, entries, prompter, }), applyAllowlist: ({ cfg: currentCfg, resolved }) => access.applyAllowlist({ cfg: currentCfg, accountId, resolved, }), }); } if (forceAllowFrom && wizard.allowFrom) { const allowFrom = wizard.allowFrom; const allowFromCredentialValue = trimResolvedValue( credentialValues[allowFrom.credentialInputKey ?? wizard.credentials[0]?.inputKey], ); if (allowFrom.helpLines && allowFrom.helpLines.length > 0) { await prompter.note( allowFrom.helpLines.join("\n"), allowFrom.helpTitle ?? `${plugin.meta.label} allowlist`, ); } const existingAllowFrom = plugin.config.resolveAllowFrom?.({ cfg: next, accountId, }) ?? []; const unique = await promptResolvedAllowFrom({ prompter, existing: existingAllowFrom, token: allowFromCredentialValue, message: allowFrom.message, placeholder: allowFrom.placeholder, label: allowFrom.helpTitle ?? `${plugin.meta.label} allowlist`, parseInputs: allowFrom.parseInputs ?? splitOnboardingEntries, parseId: allowFrom.parseId, invalidWithoutTokenNote: allowFrom.invalidWithoutCredentialNote, resolveEntries: async ({ entries }) => allowFrom.resolveEntries({ cfg: next, accountId, credentialValues, entries, }), }); next = await allowFrom.apply({ cfg: next, accountId, allowFrom: unique, }); } return { cfg: next, accountId }; }, dmPolicy: wizard.dmPolicy, disable: wizard.disable, onAccountRecorded: wizard.onAccountRecorded, }; }