Skip to content

Commit a979721

Browse files
committed
fix: stage WhatsApp runtime deps before setup login
1 parent 9b18ae8 commit a979721

8 files changed

Lines changed: 143 additions & 8 deletions

File tree

extensions/whatsapp/setup-entry.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,10 @@ describe("whatsapp setup entry", () => {
1111
expect(setupEntry.kind).toBe("bundled-channel-setup-entry");
1212
expect(setupEntry.loadSetupPlugin({ installRuntimeDeps: false }).id).toBe("whatsapp");
1313
});
14+
15+
it("loads the delegated setup wizard without importing runtime dependencies", async () => {
16+
const { whatsappSetupWizard } = await import("./src/setup-surface.js");
17+
18+
expect(whatsappSetupWizard.channel).toBe("whatsapp");
19+
});
1420
});

extensions/whatsapp/src/setup-finalize.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
resolveWhatsAppAccount,
1616
resolveWhatsAppAuthDir,
1717
} from "./accounts.js";
18-
import { loginWeb } from "./login.js";
1918
import { whatsappSetupAdapter } from "./setup-core.js";
2019

2120
type SetupPrompter = Parameters<NonNullable<ChannelSetupWizard["finalize"]>>[0]["prompter"];
@@ -424,6 +423,7 @@ export async function finalizeWhatsAppSetup(params: {
424423
});
425424
if (wantsLink) {
426425
try {
426+
const { loginWeb } = await import("./login.js");
427427
await loginWeb(false, undefined, params.runtime, accountId);
428428
} catch (error) {
429429
params.runtime.error(`WhatsApp login failed: ${String(error)}`);

extensions/whatsapp/src/shared.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,12 @@ export async function loadWhatsAppChannelRuntime() {
9494
return await import("./channel.runtime.js");
9595
}
9696

97+
async function loadWhatsAppSetupSurface() {
98+
return await import("./setup-surface.js");
99+
}
100+
97101
export const whatsappSetupWizardProxy = createWhatsAppSetupWizardProxy(
98-
async () => (await loadWhatsAppChannelRuntime()).whatsappSetupWizard,
102+
async () => (await loadWhatsAppSetupSurface()).whatsappSetupWizard,
99103
);
100104

101105
const whatsappConfigAdapter = createScopedChannelConfigAdapter<ResolvedWhatsAppAccount>({

scripts/e2e/bundled-channel-runtime-deps-docker.sh

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,102 @@ for channel in "${!SETUP_ENTRY_DEP_SENTINELS[@]}"; do
717717
fi
718718
done
719719
720+
echo "Running packaged guided WhatsApp setup; runtime deps should be staged before finalize..."
721+
OPENCLAW_PACKAGE_ROOT="$root" node --input-type=module - <<'NODE'
722+
import path from "node:path";
723+
import { readdir } from "node:fs/promises";
724+
import { pathToFileURL } from "node:url";
725+
726+
const root = process.env.OPENCLAW_PACKAGE_ROOT;
727+
if (!root) {
728+
throw new Error("missing OPENCLAW_PACKAGE_ROOT");
729+
}
730+
const distDir = path.join(root, "dist");
731+
const onboardChannelFiles = (await readdir(distDir))
732+
.filter((entry) => /^onboard-channels-.*\.js$/.test(entry))
733+
.sort();
734+
let setupChannels;
735+
for (const entry of onboardChannelFiles) {
736+
const module = await import(pathToFileURL(path.join(distDir, entry)));
737+
if (typeof module.setupChannels === "function") {
738+
setupChannels = module.setupChannels;
739+
break;
740+
}
741+
}
742+
if (!setupChannels) {
743+
throw new Error(
744+
`could not find packaged setupChannels export in ${JSON.stringify(onboardChannelFiles)}`,
745+
);
746+
}
747+
748+
let channelSelectCount = 0;
749+
const notes = [];
750+
const prompter = {
751+
intro: async () => {},
752+
outro: async () => {},
753+
note: async (body, title) => {
754+
notes.push({ title, body });
755+
},
756+
confirm: async ({ message, initialValue }) => {
757+
if (message === "Link WhatsApp now (QR)?") {
758+
return false;
759+
}
760+
return initialValue ?? true;
761+
},
762+
select: async ({ message }) => {
763+
if (message === "Select a channel") {
764+
channelSelectCount += 1;
765+
return channelSelectCount === 1 ? "whatsapp" : "__done__";
766+
}
767+
if (message === "WhatsApp phone setup") {
768+
return "separate";
769+
}
770+
if (message === "WhatsApp DM policy") {
771+
return "disabled";
772+
}
773+
throw new Error(`unexpected select prompt: ${message}`);
774+
},
775+
multiselect: async ({ message }) => {
776+
throw new Error(`unexpected multiselect prompt: ${message}`);
777+
},
778+
text: async ({ message }) => {
779+
throw new Error(`unexpected text prompt: ${message}`);
780+
},
781+
};
782+
const runtime = {
783+
log: (message) => console.log(message),
784+
error: (message) => console.error(message),
785+
};
786+
787+
const result = await setupChannels(
788+
{ plugins: { enabled: true } },
789+
runtime,
790+
prompter,
791+
{
792+
deferStatusUntilSelection: true,
793+
skipConfirm: true,
794+
skipStatusNote: true,
795+
skipDmPolicyPrompt: true,
796+
initialSelection: ["whatsapp"],
797+
},
798+
);
799+
800+
if (!result.channels?.whatsapp) {
801+
throw new Error(`WhatsApp setup did not write channel config: ${JSON.stringify(result)}`);
802+
}
803+
console.log("packaged guided WhatsApp setup completed");
804+
NODE
805+
806+
if [ -e "$root/dist/extensions/whatsapp/node_modules/@whiskeysockets/baileys/package.json" ]; then
807+
echo "expected guided WhatsApp setup deps to be installed externally, not into bundled plugin tree" >&2
808+
exit 1
809+
fi
810+
if ! find "$OPENCLAW_PLUGIN_STAGE_DIR" -maxdepth 12 -path "*/node_modules/@whiskeysockets/baileys/package.json" -type f | grep -q .; then
811+
echo "guided WhatsApp setup did not stage @whiskeysockets/baileys before finalize" >&2
812+
find "$OPENCLAW_PLUGIN_STAGE_DIR" -maxdepth 12 -type f | sort | head -160 >&2 || true
813+
exit 1
814+
fi
815+
720816
echo "Configuring setup-entry channels; doctor should now install bundled runtime deps externally..."
721817
node - <<'NODE'
722818
const fs = require("node:fs");

src/commands/channel-setup/plugin-install.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ function loadChannelSetupPluginRegistry(params: {
7272
onlyPluginIds?: string[];
7373
activate?: boolean;
7474
installRuntimeDeps?: boolean;
75+
forceSetupOnlyChannelPlugins?: boolean;
7576
}): PluginRegistry {
7677
clearPluginDiscoveryCache();
7778
const autoEnabled = applyPluginAutoEnable({ config: params.cfg, env: process.env });
@@ -89,7 +90,8 @@ function loadChannelSetupPluginRegistry(params: {
8990
logger: createPluginLoaderLogger(log),
9091
onlyPluginIds: params.onlyPluginIds,
9192
includeSetupOnlyChannelPlugins: true,
92-
forceSetupOnlyChannelPlugins: params.installRuntimeDeps === false,
93+
forceSetupOnlyChannelPlugins:
94+
params.forceSetupOnlyChannelPlugins ?? params.installRuntimeDeps === false,
9395
activate: params.activate,
9496
installBundledRuntimeDeps: params.installRuntimeDeps !== false,
9597
});
@@ -160,6 +162,7 @@ export function loadChannelSetupPluginRegistrySnapshotForChannel(params: {
160162
pluginId?: string;
161163
workspaceDir?: string;
162164
installRuntimeDeps?: boolean;
165+
forceSetupOnlyChannelPlugins?: boolean;
163166
}): PluginRegistry {
164167
const scopedPluginId = resolveScopedChannelPluginId({
165168
cfg: params.cfg,

src/flows/channel-setup.test.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,12 +461,23 @@ describe("setupChannels workspace shadow exclusion", () => {
461461
},
462462
);
463463

464-
expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledTimes(1);
465-
expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledWith(
464+
expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledTimes(2);
465+
expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenNthCalledWith(
466+
1,
466467
expect.objectContaining({
467468
channel: "external-chat",
468469
pluginId: "external-chat",
469470
workspaceDir: "/tmp/openclaw-workspace",
471+
installRuntimeDeps: false,
472+
}),
473+
);
474+
expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenNthCalledWith(
475+
2,
476+
expect.objectContaining({
477+
channel: "external-chat",
478+
workspaceDir: "/tmp/openclaw-workspace",
479+
forceSetupOnlyChannelPlugins: true,
480+
installRuntimeDeps: true,
470481
}),
471482
);
472483
expect(getChannelSetupPlugin).not.toHaveBeenCalled();

src/flows/channel-setup.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,14 @@ export async function setupChannels(
163163
const loadScopedChannelPlugin = async (
164164
channel: ChannelChoice,
165165
pluginId?: string,
166+
setup?: {
167+
installRuntimeDeps?: boolean;
168+
forceReload?: boolean;
169+
forceSetupOnlyChannelPlugins?: boolean;
170+
},
166171
): Promise<ChannelSetupPlugin | undefined> => {
167172
const existing = getVisibleChannelPlugin(channel);
168-
if (existing) {
173+
if (existing && setup?.forceReload !== true) {
169174
return existing;
170175
}
171176
const snapshot = loadChannelSetupPluginRegistrySnapshotForChannel({
@@ -174,7 +179,8 @@ export async function setupChannels(
174179
channel,
175180
...(pluginId ? { pluginId } : {}),
176181
workspaceDir: resolveWorkspaceDir(),
177-
installRuntimeDeps: false,
182+
installRuntimeDeps: setup?.installRuntimeDeps ?? false,
183+
forceSetupOnlyChannelPlugins: setup?.forceSetupOnlyChannelPlugins,
178184
});
179185
const plugin =
180186
snapshot.channelSetups.find((entry) => entry.plugin.id === channel)?.plugin ??
@@ -401,6 +407,13 @@ export async function setupChannels(
401407
};
402408

403409
const configureChannel = async (channel: ChannelChoice) => {
410+
if (scopedPluginsById.has(channel)) {
411+
await loadScopedChannelPlugin(channel, undefined, {
412+
forceReload: true,
413+
forceSetupOnlyChannelPlugins: true,
414+
installRuntimeDeps: true,
415+
});
416+
}
404417
const adapter = getVisibleSetupFlowAdapter(channel);
405418
if (!adapter) {
406419
await prompter.note(`${channel} does not support guided setup yet.`, "Channel setup");

src/plugins/loader.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2448,7 +2448,9 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
24482448
manifestRecord.setupSource
24492449
) {
24502450
const setupRegistration = resolveSetupChannelRegistration(mod, {
2451-
installRuntimeDeps: shouldInstallBundledRuntimeDeps && enableState.enabled,
2451+
installRuntimeDeps:
2452+
shouldInstallBundledRuntimeDeps &&
2453+
(enableState.enabled || forceSetupOnlyChannelPlugins),
24522454
});
24532455
if (setupRegistration.loadError) {
24542456
recordPluginError({

0 commit comments

Comments
 (0)