diff --git a/src/channels/plugins/setup-wizard.ts b/src/channels/plugins/setup-wizard.ts index e19c2b57ee6..cb446a1bc76 100644 --- a/src/channels/plugins/setup-wizard.ts +++ b/src/channels/plugins/setup-wizard.ts @@ -3,6 +3,7 @@ import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js"; import type { WizardPrompter } from "../../wizard/prompts.js"; import type { ChannelOnboardingAdapter, + ChannelOnboardingConfigureContext, ChannelOnboardingDmPolicy, ChannelOnboardingStatus, ChannelOnboardingStatusContext, @@ -26,6 +27,18 @@ export type ChannelSetupWizardStatus = { configuredScore?: number; unconfiguredScore?: number; resolveConfigured: (params: { cfg: OpenClawConfig }) => boolean | Promise; + resolveStatusLines?: (params: { + cfg: OpenClawConfig; + configured: boolean; + }) => string[] | Promise; + resolveSelectionHint?: (params: { + cfg: OpenClawConfig; + configured: boolean; + }) => string | undefined | Promise; + resolveQuickstartScore?: (params: { + cfg: OpenClawConfig; + configured: boolean; + }) => number | undefined | Promise; }; export type ChannelSetupWizardCredentialState = { @@ -84,6 +97,51 @@ export type ChannelSetupWizardCredential = { }) => OpenClawConfig | Promise; }; +export type ChannelSetupWizardTextInput = { + inputKey: keyof ChannelSetupInput; + message: string; + placeholder?: string; + required?: boolean; + helpTitle?: string; + helpLines?: string[]; + confirmCurrentValue?: boolean; + keepPrompt?: string | ((value: string) => string); + currentValue?: (params: { + cfg: OpenClawConfig; + accountId: string; + credentialValues: ChannelSetupWizardCredentialValues; + }) => string | undefined | Promise; + initialValue?: (params: { + cfg: OpenClawConfig; + accountId: string; + credentialValues: ChannelSetupWizardCredentialValues; + }) => string | undefined | Promise; + shouldPrompt?: (params: { + cfg: OpenClawConfig; + accountId: string; + credentialValues: ChannelSetupWizardCredentialValues; + currentValue?: string; + }) => boolean | Promise; + applyCurrentValue?: boolean; + validate?: (params: { + value: string; + cfg: OpenClawConfig; + accountId: string; + credentialValues: ChannelSetupWizardCredentialValues; + }) => string | undefined; + normalizeValue?: (params: { + value: string; + cfg: OpenClawConfig; + accountId: string; + credentialValues: ChannelSetupWizardCredentialValues; + }) => string; + applySet?: (params: { + cfg: OpenClawConfig; + accountId: string; + value: string; + }) => OpenClawConfig | Promise; +}; + export type ChannelSetupWizardAllowFromEntry = { input: string; resolved: boolean; @@ -139,12 +197,33 @@ export type ChannelSetupWizardGroupAccess = { }) => OpenClawConfig; }; +export type ChannelSetupWizardPrepare = (params: { + cfg: OpenClawConfig; + accountId: string; + credentialValues: ChannelSetupWizardCredentialValues; + runtime: ChannelOnboardingConfigureContext["runtime"]; + prompter: WizardPrompter; + options?: ChannelOnboardingConfigureContext["options"]; +}) => + | { + cfg?: OpenClawConfig; + credentialValues?: ChannelSetupWizardCredentialValues; + } + | void + | Promise<{ + cfg?: OpenClawConfig; + credentialValues?: ChannelSetupWizardCredentialValues; + } | void>; + export type ChannelSetupWizard = { channel: string; status: ChannelSetupWizardStatus; introNote?: ChannelSetupWizardNote; envShortcut?: ChannelSetupWizardEnvShortcut; + prepare?: ChannelSetupWizardPrepare; credentials: ChannelSetupWizardCredential[]; + textInputs?: ChannelSetupWizardTextInput[]; + completionNote?: ChannelSetupWizardNote; dmPolicy?: ChannelOnboardingDmPolicy; allowFrom?: ChannelSetupWizardAllowFrom; groupAccess?: ChannelSetupWizardGroupAccess; @@ -160,14 +239,28 @@ async function buildStatus( ctx: ChannelOnboardingStatusContext, ): Promise { const configured = await wizard.status.resolveConfigured({ cfg: ctx.cfg }); + const statusLines = (await wizard.status.resolveStatusLines?.({ + cfg: ctx.cfg, + configured, + })) ?? [ + `${plugin.meta.label}: ${configured ? wizard.status.configuredLabel : wizard.status.unconfiguredLabel}`, + ]; + const selectionHint = + (await wizard.status.resolveSelectionHint?.({ + cfg: ctx.cfg, + configured, + })) ?? (configured ? wizard.status.configuredHint : wizard.status.unconfiguredHint); + const quickstartScore = + (await wizard.status.resolveQuickstartScore?.({ + cfg: ctx.cfg, + configured, + })) ?? (configured ? wizard.status.configuredScore : wizard.status.unconfiguredScore); 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, + statusLines, + selectionHint, + quickstartScore, }; } @@ -238,6 +331,29 @@ function collectCredentialValues(params: { return values; } +async function applyWizardTextInputValue(params: { + plugin: ChannelSetupWizardPlugin; + input: ChannelSetupWizardTextInput; + cfg: OpenClawConfig; + accountId: string; + value: string; +}) { + return params.input.applySet + ? await params.input.applySet({ + cfg: params.cfg, + accountId: params.accountId, + value: params.value, + }) + : applySetupInput({ + plugin: params.plugin, + cfg: params.cfg, + accountId: params.accountId, + input: { + [params.input.inputKey]: params.value, + }, + }).cfg; +} + export function buildChannelOnboardingAdapterFromSetupWizard(params: { plugin: ChannelSetupWizardPlugin; wizard: ChannelSetupWizard; @@ -248,6 +364,7 @@ export function buildChannelOnboardingAdapterFromSetupWizard(params: { getStatus: async (ctx) => buildStatus(plugin, wizard, ctx), configure: async ({ cfg, + runtime, prompter, options, accountOverrides, @@ -305,6 +422,26 @@ export function buildChannelOnboardingAdapterFromSetupWizard(params: { await prompter.note(wizard.introNote.lines.join("\n"), wizard.introNote.title); } + if (wizard.prepare) { + const prepared = await wizard.prepare({ + cfg: next, + accountId, + credentialValues, + runtime, + prompter, + options, + }); + if (prepared?.cfg) { + next = prepared.cfg; + } + if (prepared?.credentialValues) { + credentialValues = { + ...credentialValues, + ...prepared.credentialValues, + }; + } + } + if (!usedEnvShortcut) { for (const credential of wizard.credentials) { let credentialState = credential.inspect({ cfg: next, accountId }); @@ -383,6 +520,129 @@ export function buildChannelOnboardingAdapterFromSetupWizard(params: { } } + for (const textInput of wizard.textInputs ?? []) { + let currentValue = trimResolvedValue( + typeof credentialValues[textInput.inputKey] === "string" + ? credentialValues[textInput.inputKey] + : undefined, + ); + if (!currentValue && textInput.currentValue) { + currentValue = trimResolvedValue( + await textInput.currentValue({ + cfg: next, + accountId, + credentialValues, + }), + ); + } + const shouldPrompt = textInput.shouldPrompt + ? await textInput.shouldPrompt({ + cfg: next, + accountId, + credentialValues, + currentValue, + }) + : true; + + if (!shouldPrompt) { + if (currentValue) { + credentialValues[textInput.inputKey] = currentValue; + if (textInput.applyCurrentValue) { + next = await applyWizardTextInputValue({ + plugin, + input: textInput, + cfg: next, + accountId, + value: currentValue, + }); + } + } + continue; + } + + if (textInput.helpLines && textInput.helpLines.length > 0) { + await prompter.note( + textInput.helpLines.join("\n"), + textInput.helpTitle ?? textInput.message, + ); + } + + if (currentValue && textInput.confirmCurrentValue !== false) { + const keep = await prompter.confirm({ + message: + typeof textInput.keepPrompt === "function" + ? textInput.keepPrompt(currentValue) + : (textInput.keepPrompt ?? `${textInput.message} set (${currentValue}). Keep it?`), + initialValue: true, + }); + if (keep) { + credentialValues[textInput.inputKey] = currentValue; + if (textInput.applyCurrentValue) { + next = await applyWizardTextInputValue({ + plugin, + input: textInput, + cfg: next, + accountId, + value: currentValue, + }); + } + continue; + } + } + + const initialValue = trimResolvedValue( + (await textInput.initialValue?.({ + cfg: next, + accountId, + credentialValues, + })) ?? currentValue, + ); + const rawValue = String( + await prompter.text({ + message: textInput.message, + initialValue, + placeholder: textInput.placeholder, + validate: (value) => { + const trimmed = String(value ?? "").trim(); + if (!trimmed && textInput.required !== false) { + return "Required"; + } + return textInput.validate?.({ + value: trimmed, + cfg: next, + accountId, + credentialValues, + }); + }, + }), + ); + const trimmedValue = rawValue.trim(); + if (!trimmedValue && textInput.required === false) { + delete credentialValues[textInput.inputKey]; + continue; + } + const normalizedValue = trimResolvedValue( + textInput.normalizeValue?.({ + value: trimmedValue, + cfg: next, + accountId, + credentialValues, + }) ?? trimmedValue, + ); + if (!normalizedValue) { + delete credentialValues[textInput.inputKey]; + continue; + } + next = await applyWizardTextInputValue({ + plugin, + input: textInput, + cfg: next, + accountId, + value: normalizedValue, + }); + credentialValues[textInput.inputKey] = normalizedValue; + } + if (wizard.groupAccess) { const access = wizard.groupAccess; if (access.helpLines && access.helpLines.length > 0) { @@ -460,6 +720,19 @@ export function buildChannelOnboardingAdapterFromSetupWizard(params: { }); } + const shouldShowCompletionNote = + wizard.completionNote && + (wizard.completionNote.shouldShow + ? await wizard.completionNote.shouldShow({ + cfg: next, + accountId, + credentialValues, + }) + : true); + if (shouldShowCompletionNote && wizard.completionNote) { + await prompter.note(wizard.completionNote.lines.join("\n"), wizard.completionNote.title); + } + return { cfg: next, accountId }; }, dmPolicy: wizard.dmPolicy,