diff --git a/extensions/whatsapp/setup-entry.test.ts b/extensions/whatsapp/setup-entry.test.ts index a9585137119..b30cc33cc7c 100644 --- a/extensions/whatsapp/setup-entry.test.ts +++ b/extensions/whatsapp/setup-entry.test.ts @@ -12,4 +12,10 @@ describe("whatsapp setup entry", () => { expect(setupEntry.kind).toBe("bundled-channel-setup-entry"); expect(whatsappSetupPlugin.id).toBe("whatsapp"); }); + + it("loads the delegated setup wizard without importing runtime dependencies", async () => { + const { whatsappSetupWizard } = await import("./src/setup-surface.js"); + + expect(whatsappSetupWizard.channel).toBe("whatsapp"); + }); }); diff --git a/extensions/whatsapp/src/setup-finalize.ts b/extensions/whatsapp/src/setup-finalize.ts index edd9eed17d0..651b6d69d99 100644 --- a/extensions/whatsapp/src/setup-finalize.ts +++ b/extensions/whatsapp/src/setup-finalize.ts @@ -15,7 +15,6 @@ import { resolveWhatsAppAccount, resolveWhatsAppAuthDir, } from "./accounts.js"; -import { loginWeb } from "./login.js"; import { whatsappSetupAdapter } from "./setup-core.js"; type SetupPrompter = Parameters>[0]["prompter"]; @@ -424,6 +423,7 @@ export async function finalizeWhatsAppSetup(params: { }); if (wantsLink) { try { + const { loginWeb } = await import("./login.js"); await loginWeb(false, undefined, params.runtime, accountId); } catch (error) { params.runtime.error(`WhatsApp login failed: ${String(error)}`); diff --git a/extensions/whatsapp/src/shared.ts b/extensions/whatsapp/src/shared.ts index 5fc4e02d5eb..79c1872a33a 100644 --- a/extensions/whatsapp/src/shared.ts +++ b/extensions/whatsapp/src/shared.ts @@ -94,8 +94,12 @@ export async function loadWhatsAppChannelRuntime() { return await import("./channel.runtime.js"); } +async function loadWhatsAppSetupSurface() { + return await import("./setup-surface.js"); +} + export const whatsappSetupWizardProxy = createWhatsAppSetupWizardProxy( - async () => (await loadWhatsAppChannelRuntime()).whatsappSetupWizard, + async () => (await loadWhatsAppSetupSurface()).whatsappSetupWizard, ); const whatsappConfigAdapter = createScopedChannelConfigAdapter({ diff --git a/scripts/e2e/bundled-channel-runtime-deps-docker.sh b/scripts/e2e/bundled-channel-runtime-deps-docker.sh index 6af8237d263..ee116614c41 100644 --- a/scripts/e2e/bundled-channel-runtime-deps-docker.sh +++ b/scripts/e2e/bundled-channel-runtime-deps-docker.sh @@ -709,6 +709,102 @@ for channel in "${!SETUP_ENTRY_DEP_SENTINELS[@]}"; do fi done +echo "Running packaged guided WhatsApp setup; runtime deps should be staged before finalize..." +OPENCLAW_PACKAGE_ROOT="$root" node --input-type=module - <<'NODE' +import path from "node:path"; +import { readdir } from "node:fs/promises"; +import { pathToFileURL } from "node:url"; + +const root = process.env.OPENCLAW_PACKAGE_ROOT; +if (!root) { + throw new Error("missing OPENCLAW_PACKAGE_ROOT"); +} +const distDir = path.join(root, "dist"); +const onboardChannelFiles = (await readdir(distDir)) + .filter((entry) => /^onboard-channels-.*\.js$/.test(entry)) + .sort(); +let setupChannels; +for (const entry of onboardChannelFiles) { + const module = await import(pathToFileURL(path.join(distDir, entry))); + if (typeof module.setupChannels === "function") { + setupChannels = module.setupChannels; + break; + } +} +if (!setupChannels) { + throw new Error( + `could not find packaged setupChannels export in ${JSON.stringify(onboardChannelFiles)}`, + ); +} + +let channelSelectCount = 0; +const notes = []; +const prompter = { + intro: async () => {}, + outro: async () => {}, + note: async (body, title) => { + notes.push({ title, body }); + }, + confirm: async ({ message, initialValue }) => { + if (message === "Link WhatsApp now (QR)?") { + return false; + } + return initialValue ?? true; + }, + select: async ({ message }) => { + if (message === "Select a channel") { + channelSelectCount += 1; + return channelSelectCount === 1 ? "whatsapp" : "__done__"; + } + if (message === "WhatsApp phone setup") { + return "separate"; + } + if (message === "WhatsApp DM policy") { + return "disabled"; + } + throw new Error(`unexpected select prompt: ${message}`); + }, + multiselect: async ({ message }) => { + throw new Error(`unexpected multiselect prompt: ${message}`); + }, + text: async ({ message }) => { + throw new Error(`unexpected text prompt: ${message}`); + }, +}; +const runtime = { + log: (message) => console.log(message), + error: (message) => console.error(message), +}; + +const result = await setupChannels( + { plugins: { enabled: true } }, + runtime, + prompter, + { + deferStatusUntilSelection: true, + skipConfirm: true, + skipStatusNote: true, + skipDmPolicyPrompt: true, + initialSelection: ["whatsapp"], + }, +); + +if (!result.channels?.whatsapp) { + throw new Error(`WhatsApp setup did not write channel config: ${JSON.stringify(result)}`); +} +console.log("packaged guided WhatsApp setup completed"); +NODE + +if [ -e "$root/dist/extensions/whatsapp/node_modules/@whiskeysockets/baileys/package.json" ]; then + echo "expected guided WhatsApp setup deps to be installed externally, not into bundled plugin tree" >&2 + exit 1 +fi +if ! find "$OPENCLAW_PLUGIN_STAGE_DIR" -maxdepth 12 -path "*/node_modules/@whiskeysockets/baileys/package.json" -type f | grep -q .; then + echo "guided WhatsApp setup did not stage @whiskeysockets/baileys before finalize" >&2 + find "$OPENCLAW_PLUGIN_STAGE_DIR" -maxdepth 12 -type f | sort | head -160 >&2 || true + exit 1 +fi + echo "Configuring setup-entry channels; doctor should now install bundled runtime deps externally..." node - <<'NODE' const fs = require("node:fs"); diff --git a/src/commands/channel-setup/plugin-install.ts b/src/commands/channel-setup/plugin-install.ts index cf13b81abf3..293441a4fee 100644 --- a/src/commands/channel-setup/plugin-install.ts +++ b/src/commands/channel-setup/plugin-install.ts @@ -72,6 +72,7 @@ function loadChannelSetupPluginRegistry(params: { onlyPluginIds?: string[]; activate?: boolean; installRuntimeDeps?: boolean; + forceSetupOnlyChannelPlugins?: boolean; }): PluginRegistry { clearPluginDiscoveryCache(); const autoEnabled = applyPluginAutoEnable({ config: params.cfg, env: process.env }); @@ -89,7 +90,8 @@ function loadChannelSetupPluginRegistry(params: { logger: createPluginLoaderLogger(log), onlyPluginIds: params.onlyPluginIds, includeSetupOnlyChannelPlugins: true, - forceSetupOnlyChannelPlugins: params.installRuntimeDeps === false, + forceSetupOnlyChannelPlugins: + params.forceSetupOnlyChannelPlugins ?? params.installRuntimeDeps === false, activate: params.activate, installBundledRuntimeDeps: params.installRuntimeDeps !== false, }); @@ -160,6 +162,7 @@ export function loadChannelSetupPluginRegistrySnapshotForChannel(params: { pluginId?: string; workspaceDir?: string; installRuntimeDeps?: boolean; + forceSetupOnlyChannelPlugins?: boolean; }): PluginRegistry { const scopedPluginId = resolveScopedChannelPluginId({ cfg: params.cfg, diff --git a/src/flows/channel-setup.test.ts b/src/flows/channel-setup.test.ts index ce9779a0730..6f958836ad6 100644 --- a/src/flows/channel-setup.test.ts +++ b/src/flows/channel-setup.test.ts @@ -461,12 +461,23 @@ describe("setupChannels workspace shadow exclusion", () => { }, ); - expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledTimes(1); - expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledWith( + expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledTimes(2); + expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenNthCalledWith( + 1, expect.objectContaining({ channel: "external-chat", pluginId: "external-chat", workspaceDir: "/tmp/openclaw-workspace", + installRuntimeDeps: false, + }), + ); + expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + channel: "external-chat", + workspaceDir: "/tmp/openclaw-workspace", + forceSetupOnlyChannelPlugins: true, + installRuntimeDeps: true, }), ); expect(getChannelSetupPlugin).not.toHaveBeenCalled(); diff --git a/src/flows/channel-setup.ts b/src/flows/channel-setup.ts index 65e00541f4a..65a36d51e8f 100644 --- a/src/flows/channel-setup.ts +++ b/src/flows/channel-setup.ts @@ -163,9 +163,14 @@ export async function setupChannels( const loadScopedChannelPlugin = async ( channel: ChannelChoice, pluginId?: string, + setup?: { + installRuntimeDeps?: boolean; + forceReload?: boolean; + forceSetupOnlyChannelPlugins?: boolean; + }, ): Promise => { const existing = getVisibleChannelPlugin(channel); - if (existing) { + if (existing && setup?.forceReload !== true) { return existing; } const snapshot = loadChannelSetupPluginRegistrySnapshotForChannel({ @@ -174,7 +179,8 @@ export async function setupChannels( channel, ...(pluginId ? { pluginId } : {}), workspaceDir: resolveWorkspaceDir(), - installRuntimeDeps: false, + installRuntimeDeps: setup?.installRuntimeDeps ?? false, + forceSetupOnlyChannelPlugins: setup?.forceSetupOnlyChannelPlugins, }); const plugin = snapshot.channelSetups.find((entry) => entry.plugin.id === channel)?.plugin ?? @@ -401,6 +407,13 @@ export async function setupChannels( }; const configureChannel = async (channel: ChannelChoice) => { + if (scopedPluginsById.has(channel)) { + await loadScopedChannelPlugin(channel, undefined, { + forceReload: true, + forceSetupOnlyChannelPlugins: true, + installRuntimeDeps: true, + }); + } const adapter = getVisibleSetupFlowAdapter(channel); if (!adapter) { await prompter.note(`${channel} does not support guided setup yet.`, "Channel setup"); diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 09c121859ec..58be2de0014 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -2441,7 +2441,9 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi manifestRecord.setupSource ) { const setupRegistration = resolveSetupChannelRegistration(mod, { - installRuntimeDeps: shouldInstallBundledRuntimeDeps && enableState.enabled, + installRuntimeDeps: + shouldInstallBundledRuntimeDeps && + (enableState.enabled || forceSetupOnlyChannelPlugins), }); if (setupRegistration.loadError) { recordPluginError({