iMessage: lazy-load setup wizard surface

This commit is contained in:
Vincent Koc
2026-03-15 18:52:22 -07:00
parent 399b6f745a
commit 413d2ff3da
6 changed files with 254 additions and 116 deletions

View File

@@ -0,0 +1 @@
export { imessageSetupWizard } from "./setup-surface.js";

View File

@@ -28,10 +28,18 @@ import {
import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js";
import { buildPassiveProbedChannelStatusSummary } from "../../shared/channel-status-summary.js";
import { getIMessageRuntime } from "./runtime.js";
import { imessageSetupAdapter, imessageSetupWizard } from "./setup-surface.js";
import { createIMessageSetupWizardProxy, imessageSetupAdapter } from "./setup-core.js";
const meta = getChatChannelMeta("imessage");
async function loadIMessageChannelRuntime() {
return await import("./channel.runtime.js");
}
const imessageSetupWizard = createIMessageSetupWizardProxy(async () => ({
imessageSetupWizard: (await loadIMessageChannelRuntime()).imessageSetupWizard,
}));
type IMessageSendFn = ReturnType<
typeof getIMessageRuntime
>["channel"]["imessage"]["sendMessageIMessage"];

View File

@@ -0,0 +1,236 @@
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 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 type { WizardPrompter } from "../../../src/wizard/prompts.js";
import {
listIMessageAccountIds,
resolveDefaultIMessageAccountId,
resolveIMessageAccount,
} from "./accounts.js";
import { normalizeIMessageHandle } from "./targets.js";
const channel = "imessage" as const;
export function parseIMessageAllowFromEntries(raw: string): { entries: string[]; error?: string } {
return parseOnboardingEntriesAllowingWildcard(raw, (entry) => {
const lower = entry.toLowerCase();
if (lower.startsWith("chat_id:")) {
const id = entry.slice("chat_id:".length).trim();
if (!/^\d+$/.test(id)) {
return { error: `Invalid chat_id: ${entry}` };
}
return { value: entry };
}
if (lower.startsWith("chat_guid:")) {
if (!entry.slice("chat_guid:".length).trim()) {
return { error: "Invalid chat_guid entry" };
}
return { value: entry };
}
if (lower.startsWith("chat_identifier:")) {
if (!entry.slice("chat_identifier:".length).trim()) {
return { error: "Invalid chat_identifier entry" };
}
return { value: entry };
}
if (!normalizeIMessageHandle(entry)) {
return { error: `Invalid handle: ${entry}` };
}
return { value: entry };
});
}
function buildIMessageSetupPatch(input: {
cliPath?: string;
dbPath?: string;
service?: "imessage" | "sms" | "auto";
region?: string;
}) {
return {
...(input.cliPath ? { cliPath: input.cliPath } : {}),
...(input.dbPath ? { dbPath: input.dbPath } : {}),
...(input.service ? { service: input.service } : {}),
...(input.region ? { region: input.region } : {}),
};
}
async function promptIMessageAllowFrom(params: {
cfg: OpenClawConfig;
prompter: WizardPrompter;
accountId?: string;
}): Promise<OpenClawConfig> {
return promptParsedAllowFromForScopedChannel({
cfg: params.cfg,
channel,
accountId: params.accountId,
defaultAccountId: resolveDefaultIMessageAccountId(params.cfg),
prompter: params.prompter,
noteTitle: "iMessage allowlist",
noteLines: [
"Allowlist iMessage DMs by handle or chat target.",
"Examples:",
"- +15555550123",
"- user@example.com",
"- chat_id:123",
"- chat_guid:... or chat_identifier:...",
"Multiple entries: comma-separated.",
`Docs: ${formatDocsLink("/imessage", "imessage")}`,
],
message: "iMessage allowFrom (handle or chat_id)",
placeholder: "+15555550123, user@example.com, chat_id:123",
parseEntries: parseIMessageAllowFromEntries,
getExistingAllowFrom: ({ cfg, accountId }) =>
resolveIMessageAccount({ cfg, accountId }).config.allowFrom ?? [],
});
}
export const imessageSetupAdapter: ChannelSetupAdapter = {
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
applyAccountName: ({ cfg, accountId, name }) =>
applyAccountNameToChannelSection({
cfg,
channelKey: channel,
accountId,
name,
}),
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,
imessage: {
...next.channels?.imessage,
enabled: true,
...buildIMessageSetupPatch(input),
},
},
};
}
return {
...next,
channels: {
...next.channels,
imessage: {
...next.channels?.imessage,
enabled: true,
accounts: {
...next.channels?.imessage?.accounts,
[accountId]: {
...next.channels?.imessage?.accounts?.[accountId],
enabled: true,
...buildIMessageSetupPatch(input),
},
},
},
},
};
},
};
export function createIMessageSetupWizardProxy(
loadWizard: () => Promise<{ imessageSetupWizard: ChannelSetupWizard }>,
) {
const imessageDmPolicy: ChannelOnboardingDmPolicy = {
label: "iMessage",
channel,
policyKey: "channels.imessage.dmPolicy",
allowFromKey: "channels.imessage.allowFrom",
getCurrent: (cfg: OpenClawConfig) => cfg.channels?.imessage?.dmPolicy ?? "pairing",
setPolicy: (cfg: OpenClawConfig, policy) =>
setChannelDmPolicyWithAllowFrom({
cfg,
channel,
dmPolicy: policy,
}),
promptAllowFrom: promptIMessageAllowFrom,
};
return {
channel,
status: {
configuredLabel: "configured",
unconfiguredLabel: "needs setup",
configuredHint: "imsg found",
unconfiguredHint: "imsg missing",
configuredScore: 1,
unconfiguredScore: 0,
resolveConfigured: ({ cfg }) =>
listIMessageAccountIds(cfg).some((accountId) => {
const account = resolveIMessageAccount({ cfg, accountId });
return Boolean(
account.config.cliPath ||
account.config.dbPath ||
account.config.allowFrom ||
account.config.service ||
account.config.region,
);
}),
resolveStatusLines: async (params) =>
(await loadWizard()).imessageSetupWizard.status.resolveStatusLines?.(params) ?? [],
resolveSelectionHint: async (params) =>
await (await loadWizard()).imessageSetupWizard.status.resolveSelectionHint?.(params),
resolveQuickstartScore: async (params) =>
await (await loadWizard()).imessageSetupWizard.status.resolveQuickstartScore?.(params),
},
credentials: [],
textInputs: [
{
inputKey: "cliPath",
message: "imsg CLI path",
initialValue: ({ cfg, accountId }) =>
resolveIMessageAccount({ cfg, accountId }).config.cliPath ?? "imsg",
currentValue: ({ cfg, accountId }) =>
resolveIMessageAccount({ cfg, accountId }).config.cliPath ?? "imsg",
shouldPrompt: async (params) => {
const input = (await loadWizard()).imessageSetupWizard.textInputs?.find(
(entry) => entry.inputKey === "cliPath",
);
return (await input?.shouldPrompt?.(params)) ?? false;
},
confirmCurrentValue: false,
applyCurrentValue: true,
helpTitle: "iMessage",
helpLines: ["imsg CLI path required to enable iMessage."],
},
],
completionNote: {
title: "iMessage next steps",
lines: [
"This is still a work in progress.",
"Ensure OpenClaw has Full Disk Access to Messages DB.",
"Grant Automation permission for Messages when prompted.",
"List chats with: imsg chats --limit 20",
`Docs: ${formatDocsLink("/imessage", "imessage")}`,
],
},
dmPolicy: imessageDmPolicy,
disable: (cfg: OpenClawConfig) => setOnboardingChannelEnabled(cfg, channel, false),
} satisfies ChannelSetupWizard;
}

View File

@@ -5,15 +5,10 @@ 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 { detectBinary } from "../../../src/commands/onboard-helpers.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 type { WizardPrompter } from "../../../src/wizard/prompts.js";
import {
@@ -21,53 +16,10 @@ import {
resolveDefaultIMessageAccountId,
resolveIMessageAccount,
} from "./accounts.js";
import { normalizeIMessageHandle } from "./targets.js";
import { imessageSetupAdapter, parseIMessageAllowFromEntries } from "./setup-core.js";
const channel = "imessage" as const;
export function parseIMessageAllowFromEntries(raw: string): { entries: string[]; error?: string } {
return parseOnboardingEntriesAllowingWildcard(raw, (entry) => {
const lower = entry.toLowerCase();
if (lower.startsWith("chat_id:")) {
const id = entry.slice("chat_id:".length).trim();
if (!/^\d+$/.test(id)) {
return { error: `Invalid chat_id: ${entry}` };
}
return { value: entry };
}
if (lower.startsWith("chat_guid:")) {
if (!entry.slice("chat_guid:".length).trim()) {
return { error: "Invalid chat_guid entry" };
}
return { value: entry };
}
if (lower.startsWith("chat_identifier:")) {
if (!entry.slice("chat_identifier:".length).trim()) {
return { error: "Invalid chat_identifier entry" };
}
return { value: entry };
}
if (!normalizeIMessageHandle(entry)) {
return { error: `Invalid handle: ${entry}` };
}
return { value: entry };
});
}
function buildIMessageSetupPatch(input: {
cliPath?: string;
dbPath?: string;
service?: "imessage" | "sms" | "auto";
region?: string;
}) {
return {
...(input.cliPath ? { cliPath: input.cliPath } : {}),
...(input.dbPath ? { dbPath: input.dbPath } : {}),
...(input.service ? { service: input.service } : {}),
...(input.region ? { region: input.region } : {}),
};
}
async function promptIMessageAllowFrom(params: {
cfg: OpenClawConfig;
prompter: WizardPrompter;
@@ -113,63 +65,6 @@ const imessageDmPolicy: ChannelOnboardingDmPolicy = {
promptAllowFrom: promptIMessageAllowFrom,
};
export const imessageSetupAdapter: ChannelSetupAdapter = {
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
applyAccountName: ({ cfg, accountId, name }) =>
applyAccountNameToChannelSection({
cfg,
channelKey: channel,
accountId,
name,
}),
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,
imessage: {
...next.channels?.imessage,
enabled: true,
...buildIMessageSetupPatch(input),
},
},
};
}
return {
...next,
channels: {
...next.channels,
imessage: {
...next.channels?.imessage,
enabled: true,
accounts: {
...next.channels?.imessage?.accounts,
[accountId]: {
...next.channels?.imessage?.accounts?.[accountId],
enabled: true,
...buildIMessageSetupPatch(input),
},
},
},
},
};
},
};
export const imessageSetupWizard: ChannelSetupWizard = {
channel,
status: {
@@ -236,3 +131,5 @@ export const imessageSetupWizard: ChannelSetupWizard = {
dmPolicy: imessageDmPolicy,
disable: (cfg) => setOnboardingChannelEnabled(cfg, channel, false),
};
export { imessageSetupAdapter, parseIMessageAllowFromEntries };

View File

@@ -24,10 +24,8 @@ export {
resolveIMessageGroupRequireMention,
resolveIMessageGroupToolPolicy,
} from "../channels/plugins/group-mentions.js";
export {
imessageSetupAdapter,
imessageSetupWizard,
} from "../../extensions/imessage/src/setup-surface.js";
export { imessageSetupWizard } from "../../extensions/imessage/src/setup-surface.js";
export { imessageSetupAdapter } from "../../extensions/imessage/src/setup-core.js";
export { IMessageConfigSchema } from "../config/zod-schema.providers-core.js";
export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js";

View File

@@ -706,10 +706,8 @@ export {
resolveIMessageAccount,
type ResolvedIMessageAccount,
} from "../../extensions/imessage/src/accounts.js";
export {
imessageSetupAdapter,
imessageSetupWizard,
} from "../../extensions/imessage/src/setup-surface.js";
export { imessageSetupWizard } from "../../extensions/imessage/src/setup-surface.js";
export { imessageSetupAdapter } from "../../extensions/imessage/src/setup-core.js";
export {
looksLikeIMessageTargetId,
normalizeIMessageMessagingTarget,