| import { |
| promptSecretRefForOnboarding, |
| resolveSecretInputModeForEnvSelection, |
| } from "../../../commands/auth-choice.apply-helpers.js"; |
| import type { OpenClawConfig } from "../../../config/config.js"; |
| import type { DmPolicy, GroupPolicy } from "../../../config/types.js"; |
| import type { SecretInput } from "../../../config/types.secrets.js"; |
| import { promptAccountId as promptAccountIdSdk } from "../../../plugin-sdk/onboarding.js"; |
| 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, |
| patchScopedAccountConfig, |
| } from "../setup-helpers.js"; |
|
|
| export const promptAccountId: PromptAccountId = async (params: PromptAccountIdParams) => { |
| return await promptAccountIdSdk(params); |
| }; |
|
|
| export function addWildcardAllowFrom(allowFrom?: Array<string | number> | null): string[] { |
| const next = (allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean); |
| if (!next.includes("*")) { |
| next.push("*"); |
| } |
| return next; |
| } |
|
|
| export function mergeAllowFromEntries( |
| current: Array<string | number> | null | undefined, |
| additions: Array<string | number>, |
| ): string[] { |
| const merged = [...(current ?? []), ...additions].map((v) => String(v).trim()).filter(Boolean); |
| return [...new Set(merged)]; |
| } |
|
|
| export function splitOnboardingEntries(raw: string): string[] { |
| return raw |
| .split(/[\n,;]+/g) |
| .map((entry) => entry.trim()) |
| .filter(Boolean); |
| } |
|
|
| type ParsedOnboardingEntry = { value: string } | { error: string }; |
|
|
| export function parseOnboardingEntriesWithParser( |
| raw: string, |
| parseEntry: (entry: string) => ParsedOnboardingEntry, |
| ): { entries: string[]; error?: string } { |
| const parts = splitOnboardingEntries(String(raw ?? "")); |
| const entries: string[] = []; |
| for (const part of parts) { |
| const parsed = parseEntry(part); |
| if ("error" in parsed) { |
| return { entries: [], error: parsed.error }; |
| } |
| entries.push(parsed.value); |
| } |
| return { entries: normalizeAllowFromEntries(entries) }; |
| } |
|
|
| export function parseOnboardingEntriesAllowingWildcard( |
| raw: string, |
| parseEntry: (entry: string) => ParsedOnboardingEntry, |
| ): { entries: string[]; error?: string } { |
| return parseOnboardingEntriesWithParser(raw, (entry) => { |
| if (entry === "*") { |
| return { value: "*" }; |
| } |
| return parseEntry(entry); |
| }); |
| } |
|
|
| export function parseMentionOrPrefixedId(params: { |
| value: string; |
| mentionPattern: RegExp; |
| prefixPattern?: RegExp; |
| idPattern: RegExp; |
| normalizeId?: (id: string) => string; |
| }): string | null { |
| const trimmed = params.value.trim(); |
| if (!trimmed) { |
| return null; |
| } |
|
|
| const mentionMatch = trimmed.match(params.mentionPattern); |
| if (mentionMatch?.[1]) { |
| return params.normalizeId ? params.normalizeId(mentionMatch[1]) : mentionMatch[1]; |
| } |
|
|
| const stripped = params.prefixPattern ? trimmed.replace(params.prefixPattern, "") : trimmed; |
| if (!params.idPattern.test(stripped)) { |
| return null; |
| } |
|
|
| return params.normalizeId ? params.normalizeId(stripped) : stripped; |
| } |
|
|
| export function normalizeAllowFromEntries( |
| entries: Array<string | number>, |
| normalizeEntry?: (value: string) => string | null | undefined, |
| ): string[] { |
| const normalized = entries |
| .map((entry) => String(entry).trim()) |
| .filter(Boolean) |
| .map((entry) => { |
| if (entry === "*") { |
| return "*"; |
| } |
| if (!normalizeEntry) { |
| return entry; |
| } |
| const value = normalizeEntry(entry); |
| return typeof value === "string" ? value.trim() : ""; |
| }) |
| .filter(Boolean); |
| return [...new Set(normalized)]; |
| } |
|
|
| export function resolveOnboardingAccountId(params: { |
| accountId?: string; |
| defaultAccountId: string; |
| }): string { |
| return params.accountId?.trim() ? normalizeAccountId(params.accountId) : params.defaultAccountId; |
| } |
|
|
| export async function resolveAccountIdForConfigure(params: { |
| cfg: OpenClawConfig; |
| prompter: WizardPrompter; |
| label: string; |
| accountOverride?: string; |
| shouldPromptAccountIds: boolean; |
| listAccountIds: (cfg: OpenClawConfig) => string[]; |
| defaultAccountId: string; |
| }): Promise<string> { |
| const override = params.accountOverride?.trim(); |
| let accountId = override ? normalizeAccountId(override) : params.defaultAccountId; |
| if (params.shouldPromptAccountIds && !override) { |
| accountId = await promptAccountId({ |
| cfg: params.cfg, |
| prompter: params.prompter, |
| label: params.label, |
| currentId: accountId, |
| listAccountIds: params.listAccountIds, |
| defaultAccountId: params.defaultAccountId, |
| }); |
| } |
| return accountId; |
| } |
|
|
| export function setAccountAllowFromForChannel(params: { |
| cfg: OpenClawConfig; |
| channel: "imessage" | "signal"; |
| accountId: string; |
| allowFrom: string[]; |
| }): OpenClawConfig { |
| const { cfg, channel, accountId, allowFrom } = params; |
| return patchConfigForScopedAccount({ |
| cfg, |
| channel, |
| accountId, |
| patch: { allowFrom }, |
| ensureEnabled: false, |
| }); |
| } |
|
|
| function patchTopLevelChannelConfig(params: { |
| cfg: OpenClawConfig; |
| channel: string; |
| enabled?: boolean; |
| patch: Record<string, unknown>; |
| }): OpenClawConfig { |
| const channelConfig = |
| (params.cfg.channels?.[params.channel] as Record<string, unknown> | undefined) ?? {}; |
| return { |
| ...params.cfg, |
| channels: { |
| ...params.cfg.channels, |
| [params.channel]: { |
| ...channelConfig, |
| ...(params.enabled ? { enabled: true } : {}), |
| ...params.patch, |
| }, |
| }, |
| }; |
| } |
|
|
| export function setTopLevelChannelAllowFrom(params: { |
| cfg: OpenClawConfig; |
| channel: string; |
| allowFrom: string[]; |
| enabled?: boolean; |
| }): OpenClawConfig { |
| return patchTopLevelChannelConfig({ |
| cfg: params.cfg, |
| channel: params.channel, |
| enabled: params.enabled, |
| patch: { allowFrom: params.allowFrom }, |
| }); |
| } |
|
|
| export function setTopLevelChannelDmPolicyWithAllowFrom(params: { |
| cfg: OpenClawConfig; |
| channel: string; |
| dmPolicy: DmPolicy; |
| getAllowFrom?: (cfg: OpenClawConfig) => Array<string | number> | undefined; |
| }): OpenClawConfig { |
| const channelConfig = |
| (params.cfg.channels?.[params.channel] as Record<string, unknown> | undefined) ?? {}; |
| const existingAllowFrom = |
| params.getAllowFrom?.(params.cfg) ?? |
| (channelConfig.allowFrom as Array<string | number> | undefined) ?? |
| undefined; |
| const allowFrom = |
| params.dmPolicy === "open" ? addWildcardAllowFrom(existingAllowFrom) : undefined; |
| return patchTopLevelChannelConfig({ |
| cfg: params.cfg, |
| channel: params.channel, |
| patch: { |
| dmPolicy: params.dmPolicy, |
| ...(allowFrom ? { allowFrom } : {}), |
| }, |
| }); |
| } |
|
|
| export function setTopLevelChannelGroupPolicy(params: { |
| cfg: OpenClawConfig; |
| channel: string; |
| groupPolicy: GroupPolicy; |
| enabled?: boolean; |
| }): OpenClawConfig { |
| return patchTopLevelChannelConfig({ |
| cfg: params.cfg, |
| channel: params.channel, |
| enabled: params.enabled, |
| patch: { groupPolicy: params.groupPolicy }, |
| }); |
| } |
|
|
| export function setChannelDmPolicyWithAllowFrom(params: { |
| cfg: OpenClawConfig; |
| channel: "imessage" | "signal" | "telegram"; |
| dmPolicy: DmPolicy; |
| }): OpenClawConfig { |
| const { cfg, channel, dmPolicy } = params; |
| const allowFrom = |
| dmPolicy === "open" ? addWildcardAllowFrom(cfg.channels?.[channel]?.allowFrom) : undefined; |
| return { |
| ...cfg, |
| channels: { |
| ...cfg.channels, |
| [channel]: { |
| ...cfg.channels?.[channel], |
| dmPolicy, |
| ...(allowFrom ? { allowFrom } : {}), |
| }, |
| }, |
| }; |
| } |
|
|
| export function setLegacyChannelDmPolicyWithAllowFrom(params: { |
| cfg: OpenClawConfig; |
| channel: LegacyDmChannel; |
| dmPolicy: DmPolicy; |
| }): OpenClawConfig { |
| const channelConfig = (params.cfg.channels?.[params.channel] as |
| | { |
| allowFrom?: Array<string | number>; |
| dm?: { allowFrom?: Array<string | number> }; |
| } |
| | undefined) ?? { |
| allowFrom: undefined, |
| dm: undefined, |
| }; |
| const existingAllowFrom = channelConfig.allowFrom ?? channelConfig.dm?.allowFrom; |
| const allowFrom = |
| params.dmPolicy === "open" ? addWildcardAllowFrom(existingAllowFrom) : undefined; |
| return patchLegacyDmChannelConfig({ |
| cfg: params.cfg, |
| channel: params.channel, |
| patch: { |
| dmPolicy: params.dmPolicy, |
| ...(allowFrom ? { allowFrom } : {}), |
| }, |
| }); |
| } |
|
|
| export function setLegacyChannelAllowFrom(params: { |
| cfg: OpenClawConfig; |
| channel: LegacyDmChannel; |
| allowFrom: string[]; |
| }): OpenClawConfig { |
| return patchLegacyDmChannelConfig({ |
| cfg: params.cfg, |
| channel: params.channel, |
| patch: { allowFrom: params.allowFrom }, |
| }); |
| } |
|
|
| export function setAccountGroupPolicyForChannel(params: { |
| cfg: OpenClawConfig; |
| channel: "discord" | "slack"; |
| accountId: string; |
| groupPolicy: GroupPolicy; |
| }): OpenClawConfig { |
| return patchChannelConfigForAccount({ |
| cfg: params.cfg, |
| channel: params.channel, |
| accountId: params.accountId, |
| patch: { groupPolicy: params.groupPolicy }, |
| }); |
| } |
|
|
| type AccountScopedChannel = "discord" | "slack" | "telegram" | "imessage" | "signal"; |
| type LegacyDmChannel = "discord" | "slack"; |
|
|
| export function patchLegacyDmChannelConfig(params: { |
| cfg: OpenClawConfig; |
| channel: LegacyDmChannel; |
| patch: Record<string, unknown>; |
| }): OpenClawConfig { |
| const { cfg, channel, patch } = params; |
| const channelConfig = (cfg.channels?.[channel] as Record<string, unknown> | undefined) ?? {}; |
| const dmConfig = (channelConfig.dm as Record<string, unknown> | undefined) ?? {}; |
| return { |
| ...cfg, |
| channels: { |
| ...cfg.channels, |
| [channel]: { |
| ...channelConfig, |
| ...patch, |
| dm: { |
| ...dmConfig, |
| enabled: typeof dmConfig.enabled === "boolean" ? dmConfig.enabled : true, |
| }, |
| }, |
| }, |
| }; |
| } |
|
|
| export function setOnboardingChannelEnabled( |
| cfg: OpenClawConfig, |
| channel: AccountScopedChannel, |
| enabled: boolean, |
| ): OpenClawConfig { |
| const channelConfig = (cfg.channels?.[channel] as Record<string, unknown> | undefined) ?? {}; |
| return { |
| ...cfg, |
| channels: { |
| ...cfg.channels, |
| [channel]: { |
| ...channelConfig, |
| enabled, |
| }, |
| }, |
| }; |
| } |
|
|
| function patchConfigForScopedAccount(params: { |
| cfg: OpenClawConfig; |
| channel: AccountScopedChannel; |
| accountId: string; |
| patch: Record<string, unknown>; |
| ensureEnabled: boolean; |
| }): OpenClawConfig { |
| const { cfg, channel, accountId, patch, ensureEnabled } = params; |
| const seededCfg = |
| accountId === DEFAULT_ACCOUNT_ID |
| ? cfg |
| : moveSingleAccountChannelSectionToDefaultAccount({ |
| cfg, |
| channelKey: channel, |
| }); |
| return patchScopedAccountConfig({ |
| cfg: seededCfg, |
| channelKey: channel, |
| accountId, |
| patch, |
| ensureChannelEnabled: ensureEnabled, |
| ensureAccountEnabled: ensureEnabled, |
| }); |
| } |
|
|
| export function patchChannelConfigForAccount(params: { |
| cfg: OpenClawConfig; |
| channel: AccountScopedChannel; |
| accountId: string; |
| patch: Record<string, unknown>; |
| }): OpenClawConfig { |
| return patchConfigForScopedAccount({ |
| ...params, |
| ensureEnabled: true, |
| }); |
| } |
|
|
| export function applySingleTokenPromptResult(params: { |
| cfg: OpenClawConfig; |
| channel: "discord" | "telegram"; |
| accountId: string; |
| tokenPatchKey: "token" | "botToken"; |
| tokenResult: { |
| useEnv: boolean; |
| token: SecretInput | null; |
| }; |
| }): OpenClawConfig { |
| let next = params.cfg; |
| if (params.tokenResult.useEnv) { |
| next = patchChannelConfigForAccount({ |
| cfg: next, |
| channel: params.channel, |
| accountId: params.accountId, |
| patch: {}, |
| }); |
| } |
| if (params.tokenResult.token) { |
| next = patchChannelConfigForAccount({ |
| cfg: next, |
| channel: params.channel, |
| accountId: params.accountId, |
| patch: { [params.tokenPatchKey]: params.tokenResult.token }, |
| }); |
| } |
| return next; |
| } |
|
|
| export function buildSingleChannelSecretPromptState(params: { |
| accountConfigured: boolean; |
| hasConfigToken: boolean; |
| allowEnv: boolean; |
| envValue?: string; |
| }): { |
| accountConfigured: boolean; |
| hasConfigToken: boolean; |
| canUseEnv: boolean; |
| } { |
| return { |
| accountConfigured: params.accountConfigured, |
| hasConfigToken: params.hasConfigToken, |
| canUseEnv: params.allowEnv && Boolean(params.envValue?.trim()) && !params.hasConfigToken, |
| }; |
| } |
|
|
| export async function promptSingleChannelToken(params: { |
| prompter: Pick<WizardPrompter, "confirm" | "text">; |
| accountConfigured: boolean; |
| canUseEnv: boolean; |
| hasConfigToken: boolean; |
| envPrompt: string; |
| keepPrompt: string; |
| inputPrompt: string; |
| }): Promise<{ useEnv: boolean; token: string | null }> { |
| const promptToken = async (): Promise<string> => |
| String( |
| await params.prompter.text({ |
| message: params.inputPrompt, |
| validate: (value) => (value?.trim() ? undefined : "Required"), |
| }), |
| ).trim(); |
|
|
| if (params.canUseEnv) { |
| const keepEnv = await params.prompter.confirm({ |
| message: params.envPrompt, |
| initialValue: true, |
| }); |
| if (keepEnv) { |
| return { useEnv: true, token: null }; |
| } |
| return { useEnv: false, token: await promptToken() }; |
| } |
|
|
| if (params.hasConfigToken && params.accountConfigured) { |
| const keep = await params.prompter.confirm({ |
| message: params.keepPrompt, |
| initialValue: true, |
| }); |
| if (keep) { |
| return { useEnv: false, token: null }; |
| } |
| } |
|
|
| return { useEnv: false, token: await promptToken() }; |
| } |
|
|
| export type SingleChannelSecretInputPromptResult = |
| | { action: "keep" } |
| | { action: "use-env" } |
| | { action: "set"; value: SecretInput; resolvedValue: string }; |
|
|
| export async function runSingleChannelSecretStep(params: { |
| cfg: OpenClawConfig; |
| prompter: Pick<WizardPrompter, "confirm" | "text" | "select" | "note">; |
| providerHint: string; |
| credentialLabel: string; |
| secretInputMode?: "plaintext" | "ref"; |
| accountConfigured: boolean; |
| hasConfigToken: boolean; |
| allowEnv: boolean; |
| envValue?: string; |
| envPrompt: string; |
| keepPrompt: string; |
| inputPrompt: string; |
| preferredEnvVar?: string; |
| onMissingConfigured?: () => Promise<void>; |
| applyUseEnv?: (cfg: OpenClawConfig) => OpenClawConfig | Promise<OpenClawConfig>; |
| applySet?: ( |
| cfg: OpenClawConfig, |
| value: SecretInput, |
| resolvedValue: string, |
| ) => OpenClawConfig | Promise<OpenClawConfig>; |
| }): Promise<{ |
| cfg: OpenClawConfig; |
| action: SingleChannelSecretInputPromptResult["action"]; |
| resolvedValue?: string; |
| }> { |
| const promptState = buildSingleChannelSecretPromptState({ |
| accountConfigured: params.accountConfigured, |
| hasConfigToken: params.hasConfigToken, |
| allowEnv: params.allowEnv, |
| envValue: params.envValue, |
| }); |
|
|
| if (!promptState.accountConfigured && params.onMissingConfigured) { |
| await params.onMissingConfigured(); |
| } |
|
|
| const result = await promptSingleChannelSecretInput({ |
| cfg: params.cfg, |
| prompter: params.prompter, |
| providerHint: params.providerHint, |
| credentialLabel: params.credentialLabel, |
| secretInputMode: params.secretInputMode, |
| accountConfigured: promptState.accountConfigured, |
| canUseEnv: promptState.canUseEnv, |
| hasConfigToken: promptState.hasConfigToken, |
| envPrompt: params.envPrompt, |
| keepPrompt: params.keepPrompt, |
| inputPrompt: params.inputPrompt, |
| preferredEnvVar: params.preferredEnvVar, |
| }); |
|
|
| if (result.action === "use-env") { |
| return { |
| cfg: params.applyUseEnv ? await params.applyUseEnv(params.cfg) : params.cfg, |
| action: result.action, |
| resolvedValue: params.envValue?.trim() || undefined, |
| }; |
| } |
|
|
| if (result.action === "set") { |
| return { |
| cfg: params.applySet |
| ? await params.applySet(params.cfg, result.value, result.resolvedValue) |
| : params.cfg, |
| action: result.action, |
| resolvedValue: result.resolvedValue, |
| }; |
| } |
|
|
| return { |
| cfg: params.cfg, |
| action: result.action, |
| }; |
| } |
|
|
| export async function promptSingleChannelSecretInput(params: { |
| cfg: OpenClawConfig; |
| prompter: Pick<WizardPrompter, "confirm" | "text" | "select" | "note">; |
| providerHint: string; |
| credentialLabel: string; |
| secretInputMode?: "plaintext" | "ref"; |
| accountConfigured: boolean; |
| canUseEnv: boolean; |
| hasConfigToken: boolean; |
| envPrompt: string; |
| keepPrompt: string; |
| inputPrompt: string; |
| preferredEnvVar?: string; |
| }): Promise<SingleChannelSecretInputPromptResult> { |
| const selectedMode = await resolveSecretInputModeForEnvSelection({ |
| prompter: params.prompter as WizardPrompter, |
| explicitMode: params.secretInputMode, |
| copy: { |
| modeMessage: `How do you want to provide this ${params.credentialLabel}?`, |
| plaintextLabel: `Enter ${params.credentialLabel}`, |
| plaintextHint: "Stores the credential directly in OpenClaw config", |
| refLabel: "Use external secret provider", |
| refHint: "Stores a reference to env or configured external secret providers", |
| }, |
| }); |
|
|
| if (selectedMode === "plaintext") { |
| const plainResult = await promptSingleChannelToken({ |
| prompter: params.prompter, |
| accountConfigured: params.accountConfigured, |
| canUseEnv: params.canUseEnv, |
| hasConfigToken: params.hasConfigToken, |
| envPrompt: params.envPrompt, |
| keepPrompt: params.keepPrompt, |
| inputPrompt: params.inputPrompt, |
| }); |
| if (plainResult.useEnv) { |
| return { action: "use-env" }; |
| } |
| if (plainResult.token) { |
| return { action: "set", value: plainResult.token, resolvedValue: plainResult.token }; |
| } |
| return { action: "keep" }; |
| } |
|
|
| if (params.hasConfigToken && params.accountConfigured) { |
| const keep = await params.prompter.confirm({ |
| message: params.keepPrompt, |
| initialValue: true, |
| }); |
| if (keep) { |
| return { action: "keep" }; |
| } |
| } |
|
|
| const resolved = await promptSecretRefForOnboarding({ |
| provider: params.providerHint, |
| config: params.cfg, |
| prompter: params.prompter as WizardPrompter, |
| preferredEnvVar: params.preferredEnvVar, |
| copy: { |
| sourceMessage: `Where is this ${params.credentialLabel} stored?`, |
| envVarPlaceholder: params.preferredEnvVar ?? "OPENCLAW_SECRET", |
| envVarFormatError: |
| 'Use an env var name like "OPENCLAW_SECRET" (uppercase letters, numbers, underscores).', |
| noProvidersMessage: |
| "No file/exec secret providers are configured yet. Add one under secrets.providers, or select Environment variable.", |
| }, |
| }); |
| return { |
| action: "set", |
| value: resolved.ref, |
| resolvedValue: resolved.resolvedValue, |
| }; |
| } |
|
|
| type ParsedAllowFromResult = { entries: string[]; error?: string }; |
|
|
| export async function promptParsedAllowFromForScopedChannel(params: { |
| cfg: OpenClawConfig; |
| channel: "imessage" | "signal"; |
| accountId?: string; |
| defaultAccountId: string; |
| prompter: Pick<WizardPrompter, "note" | "text">; |
| noteTitle: string; |
| noteLines: string[]; |
| message: string; |
| placeholder: string; |
| parseEntries: (raw: string) => ParsedAllowFromResult; |
| getExistingAllowFrom: (params: { |
| cfg: OpenClawConfig; |
| accountId: string; |
| }) => Array<string | number>; |
| }): Promise<OpenClawConfig> { |
| const accountId = resolveOnboardingAccountId({ |
| accountId: params.accountId, |
| defaultAccountId: params.defaultAccountId, |
| }); |
| const existing = params.getExistingAllowFrom({ |
| cfg: params.cfg, |
| accountId, |
| }); |
| await params.prompter.note(params.noteLines.join("\n"), params.noteTitle); |
| const entry = await params.prompter.text({ |
| message: params.message, |
| placeholder: params.placeholder, |
| initialValue: existing[0] ? String(existing[0]) : undefined, |
| validate: (value) => { |
| const raw = String(value ?? "").trim(); |
| if (!raw) { |
| return "Required"; |
| } |
| return params.parseEntries(raw).error; |
| }, |
| }); |
| const parsed = params.parseEntries(String(entry)); |
| const unique = mergeAllowFromEntries(undefined, parsed.entries); |
| return setAccountAllowFromForChannel({ |
| cfg: params.cfg, |
| channel: params.channel, |
| accountId, |
| allowFrom: unique, |
| }); |
| } |
|
|
| export async function noteChannelLookupSummary(params: { |
| prompter: Pick<WizardPrompter, "note">; |
| label: string; |
| resolvedSections: Array<{ title: string; values: string[] }>; |
| unresolved?: string[]; |
| }): Promise<void> { |
| const lines: string[] = []; |
| for (const section of params.resolvedSections) { |
| if (section.values.length === 0) { |
| continue; |
| } |
| lines.push(`${section.title}: ${section.values.join(", ")}`); |
| } |
| if (params.unresolved && params.unresolved.length > 0) { |
| lines.push(`Unresolved (kept as typed): ${params.unresolved.join(", ")}`); |
| } |
| if (lines.length > 0) { |
| await params.prompter.note(lines.join("\n"), params.label); |
| } |
| } |
|
|
| export async function noteChannelLookupFailure(params: { |
| prompter: Pick<WizardPrompter, "note">; |
| label: string; |
| error: unknown; |
| }): Promise<void> { |
| await params.prompter.note( |
| `Channel lookup failed; keeping entries as typed. ${String(params.error)}`, |
| params.label, |
| ); |
| } |
|
|
| type AllowFromResolution = { |
| input: string; |
| resolved: boolean; |
| id?: string | null; |
| }; |
|
|
| export async function promptResolvedAllowFrom(params: { |
| prompter: WizardPrompter; |
| existing: Array<string | number>; |
| token?: string | null; |
| message: string; |
| placeholder: string; |
| label: string; |
| parseInputs: (value: string) => string[]; |
| parseId: (value: string) => string | null; |
| invalidWithoutTokenNote: string; |
| resolveEntries: (params: { token: string; entries: string[] }) => Promise<AllowFromResolution[]>; |
| }): Promise<string[]> { |
| while (true) { |
| const entry = await params.prompter.text({ |
| message: params.message, |
| placeholder: params.placeholder, |
| initialValue: params.existing[0] ? String(params.existing[0]) : undefined, |
| validate: (value) => (String(value ?? "").trim() ? undefined : "Required"), |
| }); |
| const parts = params.parseInputs(String(entry)); |
| if (!params.token) { |
| const ids = parts.map(params.parseId).filter(Boolean) as string[]; |
| if (ids.length !== parts.length) { |
| await params.prompter.note(params.invalidWithoutTokenNote, params.label); |
| continue; |
| } |
| return mergeAllowFromEntries(params.existing, ids); |
| } |
|
|
| const results = await params |
| .resolveEntries({ |
| token: params.token, |
| entries: parts, |
| }) |
| .catch(() => null); |
| if (!results) { |
| await params.prompter.note("Failed to resolve usernames. Try again.", params.label); |
| continue; |
| } |
| const unresolved = results.filter((res) => !res.resolved || !res.id); |
| if (unresolved.length > 0) { |
| await params.prompter.note( |
| `Could not resolve: ${unresolved.map((res) => res.input).join(", ")}`, |
| params.label, |
| ); |
| continue; |
| } |
| const ids = results.map((res) => res.id as string); |
| return mergeAllowFromEntries(params.existing, ids); |
| } |
| } |
|
|
| export async function promptLegacyChannelAllowFrom(params: { |
| cfg: OpenClawConfig; |
| channel: LegacyDmChannel; |
| prompter: WizardPrompter; |
| existing: Array<string | number>; |
| token?: string | null; |
| noteTitle: string; |
| noteLines: string[]; |
| message: string; |
| placeholder: string; |
| parseId: (value: string) => string | null; |
| invalidWithoutTokenNote: string; |
| resolveEntries: (params: { token: string; entries: string[] }) => Promise<AllowFromResolution[]>; |
| }): Promise<OpenClawConfig> { |
| await params.prompter.note(params.noteLines.join("\n"), params.noteTitle); |
| const unique = await promptResolvedAllowFrom({ |
| prompter: params.prompter, |
| existing: params.existing, |
| token: params.token, |
| message: params.message, |
| placeholder: params.placeholder, |
| label: params.noteTitle, |
| parseInputs: splitOnboardingEntries, |
| parseId: params.parseId, |
| invalidWithoutTokenNote: params.invalidWithoutTokenNote, |
| resolveEntries: params.resolveEntries, |
| }); |
| return setLegacyChannelAllowFrom({ |
| cfg: params.cfg, |
| channel: params.channel, |
| allowFrom: unique, |
| }); |
| } |
|
|