Signal: lazy-load setup wizard surface
This commit is contained in:
parent
d663df7a74
commit
de6666b895
1
extensions/signal/src/channel.runtime.ts
Normal file
1
extensions/signal/src/channel.runtime.ts
Normal file
@ -0,0 +1 @@
|
||||
export { signalSetupWizard } from "./setup-surface.js";
|
||||
@ -28,7 +28,15 @@ import {
|
||||
} from "openclaw/plugin-sdk/signal";
|
||||
import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js";
|
||||
import { getSignalRuntime } from "./runtime.js";
|
||||
import { signalSetupAdapter, signalSetupWizard } from "./setup-surface.js";
|
||||
import { createSignalSetupWizardProxy, signalSetupAdapter } from "./setup-core.js";
|
||||
|
||||
async function loadSignalChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
}
|
||||
|
||||
const signalSetupWizard = createSignalSetupWizardProxy(async () => ({
|
||||
signalSetupWizard: (await loadSignalChannelRuntime()).signalSetupWizard,
|
||||
}));
|
||||
|
||||
const signalMessageActions: ChannelMessageActionAdapter = {
|
||||
listActions: (ctx) => getSignalRuntime().channel.signal.messageActions?.listActions?.(ctx) ?? [],
|
||||
|
||||
275
extensions/signal/src/setup-core.ts
Normal file
275
extensions/signal/src/setup-core.ts
Normal file
@ -0,0 +1,275 @@
|
||||
import type { ChannelOnboardingDmPolicy } from "../../../src/channels/plugins/onboarding-types.js";
|
||||
import {
|
||||
parseOnboardingEntriesAllowingWildcard,
|
||||
promptParsedAllowFromForScopedChannel,
|
||||
setChannelDmPolicyWithAllowFrom,
|
||||
setOnboardingChannelEnabled,
|
||||
} from "../../../src/channels/plugins/onboarding/helpers.js";
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
} from "../../../src/channels/plugins/setup-helpers.js";
|
||||
import type { ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||
import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js";
|
||||
import { formatCliCommand } from "../../../src/cli/command-format.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/session-key.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
import { normalizeE164 } from "../../../src/utils.js";
|
||||
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
||||
import {
|
||||
listSignalAccountIds,
|
||||
resolveDefaultSignalAccountId,
|
||||
resolveSignalAccount,
|
||||
} from "./accounts.js";
|
||||
|
||||
const channel = "signal" as const;
|
||||
const MIN_E164_DIGITS = 5;
|
||||
const MAX_E164_DIGITS = 15;
|
||||
const DIGITS_ONLY = /^\d+$/;
|
||||
const INVALID_SIGNAL_ACCOUNT_ERROR =
|
||||
"Invalid E.164 phone number (must start with + and country code, e.g. +15555550123)";
|
||||
|
||||
export function normalizeSignalAccountInput(value: string | null | undefined): string | null {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const normalized = normalizeE164(trimmed);
|
||||
const digits = normalized.slice(1);
|
||||
if (!DIGITS_ONLY.test(digits)) {
|
||||
return null;
|
||||
}
|
||||
if (digits.length < MIN_E164_DIGITS || digits.length > MAX_E164_DIGITS) {
|
||||
return null;
|
||||
}
|
||||
return `+${digits}`;
|
||||
}
|
||||
|
||||
function isUuidLike(value: string): boolean {
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
||||
}
|
||||
|
||||
export function parseSignalAllowFromEntries(raw: string): { entries: string[]; error?: string } {
|
||||
return parseOnboardingEntriesAllowingWildcard(raw, (entry) => {
|
||||
if (entry.toLowerCase().startsWith("uuid:")) {
|
||||
const id = entry.slice("uuid:".length).trim();
|
||||
if (!id) {
|
||||
return { error: "Invalid uuid entry" };
|
||||
}
|
||||
return { value: `uuid:${id}` };
|
||||
}
|
||||
if (isUuidLike(entry)) {
|
||||
return { value: `uuid:${entry}` };
|
||||
}
|
||||
const normalized = normalizeSignalAccountInput(entry);
|
||||
if (!normalized) {
|
||||
return { error: `Invalid entry: ${entry}` };
|
||||
}
|
||||
return { value: normalized };
|
||||
});
|
||||
}
|
||||
|
||||
function buildSignalSetupPatch(input: {
|
||||
signalNumber?: string;
|
||||
cliPath?: string;
|
||||
httpUrl?: string;
|
||||
httpHost?: string;
|
||||
httpPort?: string;
|
||||
}) {
|
||||
return {
|
||||
...(input.signalNumber ? { account: input.signalNumber } : {}),
|
||||
...(input.cliPath ? { cliPath: input.cliPath } : {}),
|
||||
...(input.httpUrl ? { httpUrl: input.httpUrl } : {}),
|
||||
...(input.httpHost ? { httpHost: input.httpHost } : {}),
|
||||
...(input.httpPort ? { httpPort: Number(input.httpPort) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function promptSignalAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
}): Promise<OpenClawConfig> {
|
||||
return promptParsedAllowFromForScopedChannel({
|
||||
cfg: params.cfg,
|
||||
channel,
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: resolveDefaultSignalAccountId(params.cfg),
|
||||
prompter: params.prompter,
|
||||
noteTitle: "Signal allowlist",
|
||||
noteLines: [
|
||||
"Allowlist Signal DMs by sender id.",
|
||||
"Examples:",
|
||||
"- +15555550123",
|
||||
"- uuid:123e4567-e89b-12d3-a456-426614174000",
|
||||
"Multiple entries: comma-separated.",
|
||||
`Docs: ${formatDocsLink("/signal", "signal")}`,
|
||||
],
|
||||
message: "Signal allowFrom (E.164 or uuid)",
|
||||
placeholder: "+15555550123, uuid:123e4567-e89b-12d3-a456-426614174000",
|
||||
parseEntries: parseSignalAllowFromEntries,
|
||||
getExistingAllowFrom: ({ cfg, accountId }) =>
|
||||
resolveSignalAccount({ cfg, accountId }).config.allowFrom ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
export const signalSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name,
|
||||
}),
|
||||
validateInput: ({ input }) => {
|
||||
if (
|
||||
!input.signalNumber &&
|
||||
!input.httpUrl &&
|
||||
!input.httpHost &&
|
||||
!input.httpPort &&
|
||||
!input.cliPath
|
||||
) {
|
||||
return "Signal requires --signal-number or --http-url/--http-host/--http-port/--cli-path.";
|
||||
}
|
||||
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,
|
||||
signal: {
|
||||
...next.channels?.signal,
|
||||
enabled: true,
|
||||
...buildSignalSetupPatch(input),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
signal: {
|
||||
...next.channels?.signal,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...next.channels?.signal?.accounts,
|
||||
[accountId]: {
|
||||
...next.channels?.signal?.accounts?.[accountId],
|
||||
enabled: true,
|
||||
...buildSignalSetupPatch(input),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export function createSignalSetupWizardProxy(
|
||||
loadWizard: () => Promise<{ signalSetupWizard: ChannelSetupWizard }>,
|
||||
) {
|
||||
const signalDmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "Signal",
|
||||
channel,
|
||||
policyKey: "channels.signal.dmPolicy",
|
||||
allowFromKey: "channels.signal.allowFrom",
|
||||
getCurrent: (cfg: OpenClawConfig) => cfg.channels?.signal?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg: OpenClawConfig, policy) =>
|
||||
setChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
dmPolicy: policy,
|
||||
}),
|
||||
promptAllowFrom: promptSignalAllowFrom,
|
||||
};
|
||||
|
||||
return {
|
||||
channel,
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs setup",
|
||||
configuredHint: "signal-cli found",
|
||||
unconfiguredHint: "signal-cli missing",
|
||||
configuredScore: 1,
|
||||
unconfiguredScore: 0,
|
||||
resolveConfigured: ({ cfg }) =>
|
||||
listSignalAccountIds(cfg).some(
|
||||
(accountId) => resolveSignalAccount({ cfg, accountId }).configured,
|
||||
),
|
||||
resolveStatusLines: async (params) =>
|
||||
(await loadWizard()).signalSetupWizard.status.resolveStatusLines?.(params) ?? [],
|
||||
resolveSelectionHint: async (params) =>
|
||||
await (await loadWizard()).signalSetupWizard.status.resolveSelectionHint?.(params),
|
||||
resolveQuickstartScore: async (params) =>
|
||||
await (await loadWizard()).signalSetupWizard.status.resolveQuickstartScore?.(params),
|
||||
},
|
||||
prepare: async (params) => await (await loadWizard()).signalSetupWizard.prepare?.(params),
|
||||
credentials: [],
|
||||
textInputs: [
|
||||
{
|
||||
inputKey: "cliPath",
|
||||
message: "signal-cli path",
|
||||
currentValue: ({ cfg, accountId, credentialValues }) =>
|
||||
(typeof credentialValues.cliPath === "string" ? credentialValues.cliPath : undefined) ??
|
||||
resolveSignalAccount({ cfg, accountId }).config.cliPath ??
|
||||
"signal-cli",
|
||||
initialValue: ({ cfg, accountId, credentialValues }) =>
|
||||
(typeof credentialValues.cliPath === "string" ? credentialValues.cliPath : undefined) ??
|
||||
resolveSignalAccount({ cfg, accountId }).config.cliPath ??
|
||||
"signal-cli",
|
||||
shouldPrompt: async (params) => {
|
||||
const input = (await loadWizard()).signalSetupWizard.textInputs?.find(
|
||||
(entry) => entry.inputKey === "cliPath",
|
||||
);
|
||||
return (await input?.shouldPrompt?.(params)) ?? false;
|
||||
},
|
||||
confirmCurrentValue: false,
|
||||
applyCurrentValue: true,
|
||||
helpTitle: "Signal",
|
||||
helpLines: [
|
||||
"signal-cli not found. Install it, then rerun this step or set channels.signal.cliPath.",
|
||||
],
|
||||
},
|
||||
{
|
||||
inputKey: "signalNumber",
|
||||
message: "Signal bot number (E.164)",
|
||||
currentValue: ({ cfg, accountId }) =>
|
||||
normalizeSignalAccountInput(resolveSignalAccount({ cfg, accountId }).config.account) ??
|
||||
undefined,
|
||||
keepPrompt: (value) => `Signal account set (${value}). Keep it?`,
|
||||
validate: ({ value }) =>
|
||||
normalizeSignalAccountInput(value) ? undefined : INVALID_SIGNAL_ACCOUNT_ERROR,
|
||||
normalizeValue: ({ value }) => normalizeSignalAccountInput(value) ?? value,
|
||||
},
|
||||
],
|
||||
completionNote: {
|
||||
title: "Signal next steps",
|
||||
lines: [
|
||||
'Link device with: signal-cli link -n "OpenClaw"',
|
||||
"Scan QR in Signal -> Linked Devices",
|
||||
`Then run: ${formatCliCommand("openclaw gateway call channels.status --params '{\"probe\":true}'")}`,
|
||||
`Docs: ${formatDocsLink("/signal", "signal")}`,
|
||||
],
|
||||
},
|
||||
dmPolicy: signalDmPolicy,
|
||||
disable: (cfg: OpenClawConfig) => setOnboardingChannelEnabled(cfg, channel, false),
|
||||
} satisfies ChannelSetupWizard;
|
||||
}
|
||||
@ -5,89 +5,29 @@ import {
|
||||
setChannelDmPolicyWithAllowFrom,
|
||||
setOnboardingChannelEnabled,
|
||||
} from "../../../src/channels/plugins/onboarding/helpers.js";
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
} from "../../../src/channels/plugins/setup-helpers.js";
|
||||
import { type ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||
import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js";
|
||||
import { formatCliCommand } from "../../../src/cli/command-format.js";
|
||||
import { detectBinary } from "../../../src/commands/onboard-helpers.js";
|
||||
import { installSignalCli } from "../../../src/commands/signal-install.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/session-key.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
import { normalizeE164 } from "../../../src/utils.js";
|
||||
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
||||
import {
|
||||
listSignalAccountIds,
|
||||
resolveDefaultSignalAccountId,
|
||||
resolveSignalAccount,
|
||||
} from "./accounts.js";
|
||||
import {
|
||||
normalizeSignalAccountInput,
|
||||
parseSignalAllowFromEntries,
|
||||
signalSetupAdapter,
|
||||
} from "./setup-core.js";
|
||||
|
||||
const channel = "signal" as const;
|
||||
const MIN_E164_DIGITS = 5;
|
||||
const MAX_E164_DIGITS = 15;
|
||||
const DIGITS_ONLY = /^\d+$/;
|
||||
const INVALID_SIGNAL_ACCOUNT_ERROR =
|
||||
"Invalid E.164 phone number (must start with + and country code, e.g. +15555550123)";
|
||||
|
||||
export function normalizeSignalAccountInput(value: string | null | undefined): string | null {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const normalized = normalizeE164(trimmed);
|
||||
const digits = normalized.slice(1);
|
||||
if (!DIGITS_ONLY.test(digits)) {
|
||||
return null;
|
||||
}
|
||||
if (digits.length < MIN_E164_DIGITS || digits.length > MAX_E164_DIGITS) {
|
||||
return null;
|
||||
}
|
||||
return `+${digits}`;
|
||||
}
|
||||
|
||||
function isUuidLike(value: string): boolean {
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
||||
}
|
||||
|
||||
export function parseSignalAllowFromEntries(raw: string): { entries: string[]; error?: string } {
|
||||
return parseOnboardingEntriesAllowingWildcard(raw, (entry) => {
|
||||
if (entry.toLowerCase().startsWith("uuid:")) {
|
||||
const id = entry.slice("uuid:".length).trim();
|
||||
if (!id) {
|
||||
return { error: "Invalid uuid entry" };
|
||||
}
|
||||
return { value: `uuid:${id}` };
|
||||
}
|
||||
if (isUuidLike(entry)) {
|
||||
return { value: `uuid:${entry}` };
|
||||
}
|
||||
const normalized = normalizeSignalAccountInput(entry);
|
||||
if (!normalized) {
|
||||
return { error: `Invalid entry: ${entry}` };
|
||||
}
|
||||
return { value: normalized };
|
||||
});
|
||||
}
|
||||
|
||||
function buildSignalSetupPatch(input: {
|
||||
signalNumber?: string;
|
||||
cliPath?: string;
|
||||
httpUrl?: string;
|
||||
httpHost?: string;
|
||||
httpPort?: string;
|
||||
}) {
|
||||
return {
|
||||
...(input.signalNumber ? { account: input.signalNumber } : {}),
|
||||
...(input.cliPath ? { cliPath: input.cliPath } : {}),
|
||||
...(input.httpUrl ? { httpUrl: input.httpUrl } : {}),
|
||||
...(input.httpHost ? { httpHost: input.httpHost } : {}),
|
||||
...(input.httpPort ? { httpPort: Number(input.httpPort) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function promptSignalAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: WizardPrompter;
|
||||
@ -131,75 +71,6 @@ const signalDmPolicy: ChannelOnboardingDmPolicy = {
|
||||
promptAllowFrom: promptSignalAllowFrom,
|
||||
};
|
||||
|
||||
export const signalSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name,
|
||||
}),
|
||||
validateInput: ({ input }) => {
|
||||
if (
|
||||
!input.signalNumber &&
|
||||
!input.httpUrl &&
|
||||
!input.httpHost &&
|
||||
!input.httpPort &&
|
||||
!input.cliPath
|
||||
) {
|
||||
return "Signal requires --signal-number or --http-url/--http-host/--http-port/--cli-path.";
|
||||
}
|
||||
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,
|
||||
signal: {
|
||||
...next.channels?.signal,
|
||||
enabled: true,
|
||||
...buildSignalSetupPatch(input),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
signal: {
|
||||
...next.channels?.signal,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...next.channels?.signal?.accounts,
|
||||
[accountId]: {
|
||||
...next.channels?.signal?.accounts?.[accountId],
|
||||
enabled: true,
|
||||
...buildSignalSetupPatch(input),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const signalSetupWizard: ChannelSetupWizard = {
|
||||
channel,
|
||||
status: {
|
||||
|
||||
@ -782,10 +782,8 @@ export {
|
||||
resolveSignalAccount,
|
||||
type ResolvedSignalAccount,
|
||||
} from "../../extensions/signal/src/accounts.js";
|
||||
export {
|
||||
signalSetupAdapter,
|
||||
signalSetupWizard,
|
||||
} from "../../extensions/signal/src/setup-surface.js";
|
||||
export { signalSetupWizard } from "../../extensions/signal/src/setup-surface.js";
|
||||
export { signalSetupAdapter } from "../../extensions/signal/src/setup-core.js";
|
||||
export {
|
||||
looksLikeSignalTargetId,
|
||||
normalizeSignalMessagingTarget,
|
||||
|
||||
@ -16,10 +16,8 @@ export {
|
||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||
resolveDefaultGroupPolicy,
|
||||
} from "../config/runtime-group-policy.js";
|
||||
export {
|
||||
signalSetupAdapter,
|
||||
signalSetupWizard,
|
||||
} from "../../extensions/signal/src/setup-surface.js";
|
||||
export { signalSetupWizard } from "../../extensions/signal/src/setup-surface.js";
|
||||
export { signalSetupAdapter } from "../../extensions/signal/src/setup-core.js";
|
||||
export { SignalConfigSchema } from "../config/zod-schema.providers-core.js";
|
||||
|
||||
export { normalizeE164 } from "../utils.js";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user