1376 lines
40 KiB
TypeScript
1376 lines
40 KiB
TypeScript
import type { OpenClawConfig } from "../../config/config.js";
|
|
import type { DmPolicy, GroupPolicy } from "../../config/types.js";
|
|
import type { SecretInput } from "../../config/types.secrets.js";
|
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
|
import type { WizardPrompter } from "../../wizard/prompts.js";
|
|
import {
|
|
moveSingleAccountChannelSectionToDefaultAccount,
|
|
patchScopedAccountConfig,
|
|
} from "./setup-helpers.js";
|
|
import type {
|
|
ChannelSetupDmPolicy,
|
|
PromptAccountId,
|
|
PromptAccountIdParams,
|
|
} from "./setup-wizard-types.js";
|
|
import type { ChannelSetupWizard, ChannelSetupWizardAllowFromEntry } from "./setup-wizard.js";
|
|
|
|
let providerAuthInputPromise:
|
|
| Promise<typeof import("../../plugins/provider-auth-input.js")>
|
|
| undefined;
|
|
|
|
function loadProviderAuthInput() {
|
|
providerAuthInputPromise ??= import("../../plugins/provider-auth-input.js");
|
|
return providerAuthInputPromise;
|
|
}
|
|
|
|
export const promptAccountId: PromptAccountId = async (params: PromptAccountIdParams) => {
|
|
const existingIds = params.listAccountIds(params.cfg);
|
|
const initial = params.currentId?.trim() || params.defaultAccountId || DEFAULT_ACCOUNT_ID;
|
|
const choice = await params.prompter.select({
|
|
message: `${params.label} account`,
|
|
options: [
|
|
...existingIds.map((id) => ({
|
|
value: id,
|
|
label: id === DEFAULT_ACCOUNT_ID ? "default (primary)" : id,
|
|
})),
|
|
{ value: "__new__", label: "Add a new account" },
|
|
],
|
|
initialValue: initial,
|
|
});
|
|
|
|
if (choice !== "__new__") {
|
|
return normalizeAccountId(choice);
|
|
}
|
|
|
|
const entered = await params.prompter.text({
|
|
message: `New ${params.label} account id`,
|
|
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
});
|
|
const normalized = normalizeAccountId(String(entered));
|
|
if (String(entered).trim() !== normalized) {
|
|
await params.prompter.note(
|
|
`Normalized account id to "${normalized}".`,
|
|
`${params.label} account`,
|
|
);
|
|
}
|
|
return normalized;
|
|
};
|
|
|
|
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 splitSetupEntries(raw: string): string[] {
|
|
return raw
|
|
.split(/[\n,;]+/g)
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
type ParsedSetupEntry = { value: string } | { error: string };
|
|
|
|
export function parseSetupEntriesWithParser(
|
|
raw: string,
|
|
parseEntry: (entry: string) => ParsedSetupEntry,
|
|
): { entries: string[]; error?: string } {
|
|
const parts = splitSetupEntries(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 parseSetupEntriesAllowingWildcard(
|
|
raw: string,
|
|
parseEntry: (entry: string) => ParsedSetupEntry,
|
|
): { entries: string[]; error?: string } {
|
|
return parseSetupEntriesWithParser(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 resolveSetupAccountId(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,
|
|
});
|
|
}
|
|
|
|
export function patchTopLevelChannelConfigSection(params: {
|
|
cfg: OpenClawConfig;
|
|
channel: string;
|
|
enabled?: boolean;
|
|
clearFields?: string[];
|
|
patch: Record<string, unknown>;
|
|
}): OpenClawConfig {
|
|
const channelConfig = {
|
|
...(params.cfg.channels?.[params.channel] as Record<string, unknown> | undefined),
|
|
};
|
|
for (const field of params.clearFields ?? []) {
|
|
delete channelConfig[field];
|
|
}
|
|
return {
|
|
...params.cfg,
|
|
channels: {
|
|
...params.cfg.channels,
|
|
[params.channel]: {
|
|
...channelConfig,
|
|
...(params.enabled ? { enabled: true } : {}),
|
|
...params.patch,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
export function patchNestedChannelConfigSection(params: {
|
|
cfg: OpenClawConfig;
|
|
channel: string;
|
|
section: string;
|
|
enabled?: boolean;
|
|
clearFields?: string[];
|
|
patch: Record<string, unknown>;
|
|
}): OpenClawConfig {
|
|
const channelConfig = {
|
|
...(params.cfg.channels?.[params.channel] as Record<string, unknown> | undefined),
|
|
};
|
|
const sectionConfig = {
|
|
...(channelConfig[params.section] as Record<string, unknown> | undefined),
|
|
};
|
|
for (const field of params.clearFields ?? []) {
|
|
delete sectionConfig[field];
|
|
}
|
|
return {
|
|
...params.cfg,
|
|
channels: {
|
|
...params.cfg.channels,
|
|
[params.channel]: {
|
|
...channelConfig,
|
|
...(params.enabled ? { enabled: true } : {}),
|
|
[params.section]: {
|
|
...sectionConfig,
|
|
...params.patch,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
export function setTopLevelChannelAllowFrom(params: {
|
|
cfg: OpenClawConfig;
|
|
channel: string;
|
|
allowFrom: string[];
|
|
enabled?: boolean;
|
|
}): OpenClawConfig {
|
|
return patchTopLevelChannelConfigSection({
|
|
cfg: params.cfg,
|
|
channel: params.channel,
|
|
enabled: params.enabled,
|
|
patch: { allowFrom: params.allowFrom },
|
|
});
|
|
}
|
|
|
|
export function setNestedChannelAllowFrom(params: {
|
|
cfg: OpenClawConfig;
|
|
channel: string;
|
|
section: string;
|
|
allowFrom: string[];
|
|
enabled?: boolean;
|
|
}): OpenClawConfig {
|
|
return patchNestedChannelConfigSection({
|
|
cfg: params.cfg,
|
|
channel: params.channel,
|
|
section: params.section,
|
|
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 patchTopLevelChannelConfigSection({
|
|
cfg: params.cfg,
|
|
channel: params.channel,
|
|
patch: {
|
|
dmPolicy: params.dmPolicy,
|
|
...(allowFrom ? { allowFrom } : {}),
|
|
},
|
|
});
|
|
}
|
|
|
|
export function setNestedChannelDmPolicyWithAllowFrom(params: {
|
|
cfg: OpenClawConfig;
|
|
channel: string;
|
|
section: string;
|
|
dmPolicy: DmPolicy;
|
|
getAllowFrom?: (cfg: OpenClawConfig) => Array<string | number> | undefined;
|
|
enabled?: boolean;
|
|
}): OpenClawConfig {
|
|
const channelConfig =
|
|
(params.cfg.channels?.[params.channel] as Record<string, unknown> | undefined) ?? {};
|
|
const sectionConfig =
|
|
(channelConfig[params.section] as Record<string, unknown> | undefined) ?? {};
|
|
const existingAllowFrom =
|
|
params.getAllowFrom?.(params.cfg) ??
|
|
(sectionConfig.allowFrom as Array<string | number> | undefined) ??
|
|
undefined;
|
|
const allowFrom =
|
|
params.dmPolicy === "open" ? addWildcardAllowFrom(existingAllowFrom) : undefined;
|
|
return patchNestedChannelConfigSection({
|
|
cfg: params.cfg,
|
|
channel: params.channel,
|
|
section: params.section,
|
|
enabled: params.enabled,
|
|
patch: {
|
|
policy: params.dmPolicy,
|
|
...(allowFrom ? { allowFrom } : {}),
|
|
},
|
|
});
|
|
}
|
|
|
|
export function setTopLevelChannelGroupPolicy(params: {
|
|
cfg: OpenClawConfig;
|
|
channel: string;
|
|
groupPolicy: GroupPolicy;
|
|
enabled?: boolean;
|
|
}): OpenClawConfig {
|
|
return patchTopLevelChannelConfigSection({
|
|
cfg: params.cfg,
|
|
channel: params.channel,
|
|
enabled: params.enabled,
|
|
patch: { groupPolicy: params.groupPolicy },
|
|
});
|
|
}
|
|
|
|
export function createTopLevelChannelDmPolicy(params: {
|
|
label: string;
|
|
channel: string;
|
|
policyKey: string;
|
|
allowFromKey: string;
|
|
getCurrent: (cfg: OpenClawConfig) => DmPolicy;
|
|
promptAllowFrom?: ChannelSetupDmPolicy["promptAllowFrom"];
|
|
getAllowFrom?: (cfg: OpenClawConfig) => Array<string | number> | undefined;
|
|
}): ChannelSetupDmPolicy {
|
|
const setPolicy = createTopLevelChannelDmPolicySetter({
|
|
channel: params.channel,
|
|
getAllowFrom: params.getAllowFrom,
|
|
});
|
|
return {
|
|
label: params.label,
|
|
channel: params.channel,
|
|
policyKey: params.policyKey,
|
|
allowFromKey: params.allowFromKey,
|
|
getCurrent: params.getCurrent,
|
|
setPolicy,
|
|
...(params.promptAllowFrom ? { promptAllowFrom: params.promptAllowFrom } : {}),
|
|
};
|
|
}
|
|
|
|
export function createNestedChannelDmPolicy(params: {
|
|
label: string;
|
|
channel: string;
|
|
section: string;
|
|
policyKey: string;
|
|
allowFromKey: string;
|
|
getCurrent: (cfg: OpenClawConfig) => DmPolicy;
|
|
promptAllowFrom?: ChannelSetupDmPolicy["promptAllowFrom"];
|
|
getAllowFrom?: (cfg: OpenClawConfig) => Array<string | number> | undefined;
|
|
enabled?: boolean;
|
|
}): ChannelSetupDmPolicy {
|
|
const setPolicy = createNestedChannelDmPolicySetter({
|
|
channel: params.channel,
|
|
section: params.section,
|
|
getAllowFrom: params.getAllowFrom,
|
|
enabled: params.enabled,
|
|
});
|
|
return {
|
|
label: params.label,
|
|
channel: params.channel,
|
|
policyKey: params.policyKey,
|
|
allowFromKey: params.allowFromKey,
|
|
getCurrent: params.getCurrent,
|
|
setPolicy,
|
|
...(params.promptAllowFrom ? { promptAllowFrom: params.promptAllowFrom } : {}),
|
|
};
|
|
}
|
|
|
|
export function createTopLevelChannelDmPolicySetter(params: {
|
|
channel: string;
|
|
getAllowFrom?: (cfg: OpenClawConfig) => Array<string | number> | undefined;
|
|
}): (cfg: OpenClawConfig, dmPolicy: DmPolicy) => OpenClawConfig {
|
|
return (cfg, dmPolicy) =>
|
|
setTopLevelChannelDmPolicyWithAllowFrom({
|
|
cfg,
|
|
channel: params.channel,
|
|
dmPolicy,
|
|
getAllowFrom: params.getAllowFrom,
|
|
});
|
|
}
|
|
|
|
export function createNestedChannelDmPolicySetter(params: {
|
|
channel: string;
|
|
section: string;
|
|
getAllowFrom?: (cfg: OpenClawConfig) => Array<string | number> | undefined;
|
|
enabled?: boolean;
|
|
}): (cfg: OpenClawConfig, dmPolicy: DmPolicy) => OpenClawConfig {
|
|
return (cfg, dmPolicy) =>
|
|
setNestedChannelDmPolicyWithAllowFrom({
|
|
cfg,
|
|
channel: params.channel,
|
|
section: params.section,
|
|
dmPolicy,
|
|
getAllowFrom: params.getAllowFrom,
|
|
enabled: params.enabled,
|
|
});
|
|
}
|
|
|
|
export function createTopLevelChannelAllowFromSetter(params: {
|
|
channel: string;
|
|
enabled?: boolean;
|
|
}): (cfg: OpenClawConfig, allowFrom: string[]) => OpenClawConfig {
|
|
return (cfg, allowFrom) =>
|
|
setTopLevelChannelAllowFrom({
|
|
cfg,
|
|
channel: params.channel,
|
|
allowFrom,
|
|
enabled: params.enabled,
|
|
});
|
|
}
|
|
|
|
export function createNestedChannelAllowFromSetter(params: {
|
|
channel: string;
|
|
section: string;
|
|
enabled?: boolean;
|
|
}): (cfg: OpenClawConfig, allowFrom: string[]) => OpenClawConfig {
|
|
return (cfg, allowFrom) =>
|
|
setNestedChannelAllowFrom({
|
|
cfg,
|
|
channel: params.channel,
|
|
section: params.section,
|
|
allowFrom,
|
|
enabled: params.enabled,
|
|
});
|
|
}
|
|
|
|
export function createTopLevelChannelGroupPolicySetter(params: {
|
|
channel: string;
|
|
enabled?: boolean;
|
|
}): (cfg: OpenClawConfig, groupPolicy: "open" | "allowlist" | "disabled") => OpenClawConfig {
|
|
return (cfg, groupPolicy) =>
|
|
setTopLevelChannelGroupPolicy({
|
|
cfg,
|
|
channel: params.channel,
|
|
groupPolicy,
|
|
enabled: params.enabled,
|
|
});
|
|
}
|
|
|
|
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 },
|
|
});
|
|
}
|
|
|
|
export function setAccountDmAllowFromForChannel(params: {
|
|
cfg: OpenClawConfig;
|
|
channel: "discord" | "slack";
|
|
accountId: string;
|
|
allowFrom: string[];
|
|
}): OpenClawConfig {
|
|
return patchChannelConfigForAccount({
|
|
cfg: params.cfg,
|
|
channel: params.channel,
|
|
accountId: params.accountId,
|
|
patch: { dmPolicy: "allowlist", allowFrom: params.allowFrom },
|
|
});
|
|
}
|
|
|
|
export function createLegacyCompatChannelDmPolicy(params: {
|
|
label: string;
|
|
channel: LegacyDmChannel;
|
|
promptAllowFrom?: ChannelSetupDmPolicy["promptAllowFrom"];
|
|
}): ChannelSetupDmPolicy {
|
|
return {
|
|
label: params.label,
|
|
channel: params.channel,
|
|
policyKey: `channels.${params.channel}.dmPolicy`,
|
|
allowFromKey: `channels.${params.channel}.allowFrom`,
|
|
getCurrent: (cfg) =>
|
|
(
|
|
cfg.channels?.[params.channel] as
|
|
| {
|
|
dmPolicy?: DmPolicy;
|
|
dm?: { policy?: DmPolicy };
|
|
}
|
|
| undefined
|
|
)?.dmPolicy ??
|
|
(
|
|
cfg.channels?.[params.channel] as
|
|
| {
|
|
dmPolicy?: DmPolicy;
|
|
dm?: { policy?: DmPolicy };
|
|
}
|
|
| undefined
|
|
)?.dm?.policy ??
|
|
"pairing",
|
|
setPolicy: (cfg, policy) =>
|
|
setLegacyChannelDmPolicyWithAllowFrom({
|
|
cfg,
|
|
channel: params.channel,
|
|
dmPolicy: policy,
|
|
}),
|
|
...(params.promptAllowFrom ? { promptAllowFrom: params.promptAllowFrom } : {}),
|
|
};
|
|
}
|
|
|
|
export async function resolveGroupAllowlistWithLookupNotes<TResolved>(params: {
|
|
label: string;
|
|
prompter: Pick<WizardPrompter, "note">;
|
|
entries: string[];
|
|
fallback: TResolved;
|
|
resolve: () => Promise<TResolved>;
|
|
}): Promise<TResolved> {
|
|
try {
|
|
return await params.resolve();
|
|
} catch (error) {
|
|
await noteChannelLookupFailure({
|
|
prompter: params.prompter,
|
|
label: params.label,
|
|
error,
|
|
});
|
|
await noteChannelLookupSummary({
|
|
prompter: params.prompter,
|
|
label: params.label,
|
|
resolvedSections: [],
|
|
unresolved: params.entries,
|
|
});
|
|
return params.fallback;
|
|
}
|
|
}
|
|
|
|
export function createAccountScopedAllowFromSection(params: {
|
|
channel: "discord" | "slack";
|
|
credentialInputKey?: NonNullable<ChannelSetupWizard["allowFrom"]>["credentialInputKey"];
|
|
helpTitle?: string;
|
|
helpLines?: string[];
|
|
message: string;
|
|
placeholder: string;
|
|
invalidWithoutCredentialNote: string;
|
|
parseId: NonNullable<NonNullable<ChannelSetupWizard["allowFrom"]>["parseId"]>;
|
|
resolveEntries: NonNullable<NonNullable<ChannelSetupWizard["allowFrom"]>["resolveEntries"]>;
|
|
}): NonNullable<ChannelSetupWizard["allowFrom"]> {
|
|
return {
|
|
...(params.helpTitle ? { helpTitle: params.helpTitle } : {}),
|
|
...(params.helpLines ? { helpLines: params.helpLines } : {}),
|
|
...(params.credentialInputKey ? { credentialInputKey: params.credentialInputKey } : {}),
|
|
message: params.message,
|
|
placeholder: params.placeholder,
|
|
invalidWithoutCredentialNote: params.invalidWithoutCredentialNote,
|
|
parseId: params.parseId,
|
|
resolveEntries: params.resolveEntries,
|
|
apply: ({ cfg, accountId, allowFrom }) =>
|
|
setAccountDmAllowFromForChannel({
|
|
cfg,
|
|
channel: params.channel,
|
|
accountId,
|
|
allowFrom,
|
|
}),
|
|
};
|
|
}
|
|
|
|
export function createAccountScopedGroupAccessSection<TResolved>(params: {
|
|
channel: "discord" | "slack";
|
|
label: string;
|
|
placeholder: string;
|
|
helpTitle?: string;
|
|
helpLines?: string[];
|
|
skipAllowlistEntries?: boolean;
|
|
currentPolicy: NonNullable<ChannelSetupWizard["groupAccess"]>["currentPolicy"];
|
|
currentEntries: NonNullable<ChannelSetupWizard["groupAccess"]>["currentEntries"];
|
|
updatePrompt: NonNullable<ChannelSetupWizard["groupAccess"]>["updatePrompt"];
|
|
resolveAllowlist?: NonNullable<
|
|
NonNullable<ChannelSetupWizard["groupAccess"]>["resolveAllowlist"]
|
|
>;
|
|
fallbackResolved: (entries: string[]) => TResolved;
|
|
applyAllowlist: (params: {
|
|
cfg: OpenClawConfig;
|
|
accountId: string;
|
|
resolved: TResolved;
|
|
}) => OpenClawConfig;
|
|
}): NonNullable<ChannelSetupWizard["groupAccess"]> {
|
|
return {
|
|
label: params.label,
|
|
placeholder: params.placeholder,
|
|
...(params.helpTitle ? { helpTitle: params.helpTitle } : {}),
|
|
...(params.helpLines ? { helpLines: params.helpLines } : {}),
|
|
...(params.skipAllowlistEntries ? { skipAllowlistEntries: true } : {}),
|
|
currentPolicy: params.currentPolicy,
|
|
currentEntries: params.currentEntries,
|
|
updatePrompt: params.updatePrompt,
|
|
setPolicy: ({ cfg, accountId, policy }) =>
|
|
setAccountGroupPolicyForChannel({
|
|
cfg,
|
|
channel: params.channel,
|
|
accountId,
|
|
groupPolicy: policy,
|
|
}),
|
|
...(params.resolveAllowlist
|
|
? {
|
|
resolveAllowlist: ({ cfg, accountId, credentialValues, entries, prompter }) =>
|
|
resolveGroupAllowlistWithLookupNotes({
|
|
label: params.label,
|
|
prompter,
|
|
entries,
|
|
fallback: params.fallbackResolved(entries),
|
|
resolve: async () =>
|
|
await params.resolveAllowlist!({
|
|
cfg,
|
|
accountId,
|
|
credentialValues,
|
|
entries,
|
|
prompter,
|
|
}),
|
|
}),
|
|
}
|
|
: {}),
|
|
applyAllowlist: ({ cfg, accountId, resolved }) =>
|
|
params.applyAllowlist({
|
|
cfg,
|
|
accountId,
|
|
resolved: resolved as TResolved,
|
|
}),
|
|
};
|
|
}
|
|
|
|
type AccountScopedChannel =
|
|
| "bluebubbles"
|
|
| "discord"
|
|
| "imessage"
|
|
| "line"
|
|
| "signal"
|
|
| "slack"
|
|
| "telegram";
|
|
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 setSetupChannelEnabled(
|
|
cfg: OpenClawConfig,
|
|
channel: string,
|
|
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 { promptSecretRefForSetup, resolveSecretInputModeForEnvSelection } =
|
|
await loadProviderAuthInput();
|
|
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 promptSecretRefForSetup({
|
|
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 promptParsedAllowFromForAccount<TConfig extends OpenClawConfig>(params: {
|
|
cfg: TConfig;
|
|
accountId?: string;
|
|
defaultAccountId: string;
|
|
prompter: Pick<WizardPrompter, "note" | "text">;
|
|
noteTitle: string;
|
|
noteLines: string[];
|
|
message: string;
|
|
placeholder: string;
|
|
parseEntries: (raw: string) => ParsedAllowFromResult;
|
|
getExistingAllowFrom: (params: { cfg: TConfig; accountId: string }) => Array<string | number>;
|
|
mergeEntries?: (params: { existing: Array<string | number>; parsed: string[] }) => string[];
|
|
applyAllowFrom: (params: {
|
|
cfg: TConfig;
|
|
accountId: string;
|
|
allowFrom: string[];
|
|
}) => TConfig | Promise<TConfig>;
|
|
}): Promise<TConfig> {
|
|
const accountId = resolveSetupAccountId({
|
|
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 =
|
|
params.mergeEntries?.({
|
|
existing,
|
|
parsed: parsed.entries,
|
|
}) ?? mergeAllowFromEntries(undefined, parsed.entries);
|
|
return await params.applyAllowFrom({
|
|
cfg: params.cfg,
|
|
accountId,
|
|
allowFrom: unique,
|
|
});
|
|
}
|
|
|
|
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> {
|
|
return await promptParsedAllowFromForAccount({
|
|
cfg: params.cfg,
|
|
accountId: params.accountId,
|
|
defaultAccountId: params.defaultAccountId,
|
|
prompter: params.prompter,
|
|
noteTitle: params.noteTitle,
|
|
noteLines: params.noteLines,
|
|
message: params.message,
|
|
placeholder: params.placeholder,
|
|
parseEntries: params.parseEntries,
|
|
getExistingAllowFrom: params.getExistingAllowFrom,
|
|
applyAllowFrom: ({ cfg, accountId, allowFrom }) =>
|
|
setAccountAllowFromForChannel({
|
|
cfg,
|
|
channel: params.channel,
|
|
accountId,
|
|
allowFrom,
|
|
}),
|
|
});
|
|
}
|
|
|
|
export function resolveParsedAllowFromEntries(params: {
|
|
entries: string[];
|
|
parseId: (raw: string) => string | null;
|
|
}): ChannelSetupWizardAllowFromEntry[] {
|
|
return params.entries.map((entry) => {
|
|
const id = params.parseId(entry);
|
|
return {
|
|
input: entry,
|
|
resolved: Boolean(id),
|
|
id,
|
|
};
|
|
});
|
|
}
|
|
|
|
export function createAllowFromSection(params: {
|
|
helpTitle?: string;
|
|
helpLines?: string[];
|
|
credentialInputKey?: NonNullable<ChannelSetupWizard["allowFrom"]>["credentialInputKey"];
|
|
message: string;
|
|
placeholder: string;
|
|
invalidWithoutCredentialNote: string;
|
|
parseInputs?: NonNullable<NonNullable<ChannelSetupWizard["allowFrom"]>["parseInputs"]>;
|
|
parseId: NonNullable<NonNullable<ChannelSetupWizard["allowFrom"]>["parseId"]>;
|
|
resolveEntries?: NonNullable<NonNullable<ChannelSetupWizard["allowFrom"]>["resolveEntries"]>;
|
|
apply: NonNullable<NonNullable<ChannelSetupWizard["allowFrom"]>["apply"]>;
|
|
}): NonNullable<ChannelSetupWizard["allowFrom"]> {
|
|
return {
|
|
...(params.helpTitle ? { helpTitle: params.helpTitle } : {}),
|
|
...(params.helpLines ? { helpLines: params.helpLines } : {}),
|
|
...(params.credentialInputKey ? { credentialInputKey: params.credentialInputKey } : {}),
|
|
message: params.message,
|
|
placeholder: params.placeholder,
|
|
invalidWithoutCredentialNote: params.invalidWithoutCredentialNote,
|
|
...(params.parseInputs ? { parseInputs: params.parseInputs } : {}),
|
|
parseId: params.parseId,
|
|
resolveEntries:
|
|
params.resolveEntries ??
|
|
(async ({ entries }) => resolveParsedAllowFromEntries({ entries, parseId: params.parseId })),
|
|
apply: params.apply,
|
|
};
|
|
}
|
|
|
|
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 resolveEntriesWithOptionalToken<TResult>(params: {
|
|
token?: string | null;
|
|
entries: string[];
|
|
buildWithoutToken: (input: string) => TResult;
|
|
resolveEntries: (params: { token: string; entries: string[] }) => Promise<TResult[]>;
|
|
}): Promise<TResult[]> {
|
|
const token = params.token?.trim();
|
|
if (!token) {
|
|
return params.entries.map(params.buildWithoutToken);
|
|
}
|
|
return await params.resolveEntries({
|
|
token,
|
|
entries: params.entries,
|
|
});
|
|
}
|
|
|
|
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: splitSetupEntries,
|
|
parseId: params.parseId,
|
|
invalidWithoutTokenNote: params.invalidWithoutTokenNote,
|
|
resolveEntries: params.resolveEntries,
|
|
});
|
|
return setLegacyChannelAllowFrom({
|
|
cfg: params.cfg,
|
|
channel: params.channel,
|
|
allowFrom: unique,
|
|
});
|
|
}
|
|
|
|
export async function promptLegacyChannelAllowFromForAccount<TAccount>(params: {
|
|
cfg: OpenClawConfig;
|
|
channel: LegacyDmChannel;
|
|
prompter: WizardPrompter;
|
|
accountId?: string;
|
|
defaultAccountId: string;
|
|
resolveAccount: (cfg: OpenClawConfig, accountId: string) => TAccount;
|
|
resolveExisting: (account: TAccount, cfg: OpenClawConfig) => Array<string | number>;
|
|
resolveToken: (account: TAccount) => string | null | undefined;
|
|
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> {
|
|
const accountId = resolveSetupAccountId({
|
|
accountId: params.accountId,
|
|
defaultAccountId: params.defaultAccountId,
|
|
});
|
|
const account = params.resolveAccount(params.cfg, accountId);
|
|
return await promptLegacyChannelAllowFrom({
|
|
cfg: params.cfg,
|
|
channel: params.channel,
|
|
prompter: params.prompter,
|
|
existing: params.resolveExisting(account, params.cfg),
|
|
token: params.resolveToken(account),
|
|
noteTitle: params.noteTitle,
|
|
noteLines: params.noteLines,
|
|
message: params.message,
|
|
placeholder: params.placeholder,
|
|
parseId: params.parseId,
|
|
invalidWithoutTokenNote: params.invalidWithoutTokenNote,
|
|
resolveEntries: params.resolveEntries,
|
|
});
|
|
}
|