refactor: untangle whatsapp runtime boundary

This commit is contained in:
Peter Steinberger
2026-03-19 03:12:16 +00:00
parent 510f4276b5
commit 30a94dfd3b
33 changed files with 848 additions and 474 deletions

View File

@@ -0,0 +1,339 @@
import fs from "node:fs";
import path from "node:path";
import { createJiti } from "jiti";
import { resolveWhatsAppHeartbeatRecipients } from "../../channels/plugins/whatsapp-heartbeat.js";
import { loadConfig } from "../../config/config.js";
import {
getDefaultLocalRoots as getDefaultLocalRootsImpl,
loadWebMedia as loadWebMediaImpl,
loadWebMediaRaw as loadWebMediaRawImpl,
optimizeImageToJpeg as optimizeImageToJpegImpl,
} from "../../media/web-media.js";
import { loadPluginManifestRegistry } from "../manifest-registry.js";
import {
buildPluginLoaderJitiOptions,
resolvePluginSdkAliasFile,
resolvePluginSdkScopedAliasMap,
shouldPreferNativeJiti,
} from "../sdk-alias.js";
const WHATSAPP_PLUGIN_ID = "whatsapp";
type WhatsAppLightModule = typeof import("../../../extensions/whatsapp/light-runtime-api.js");
type WhatsAppHeavyModule = typeof import("../../../extensions/whatsapp/runtime-api.js");
type WhatsAppPluginRecord = {
origin: string;
rootDir?: string;
source: string;
};
let cachedHeavyModulePath: string | null = null;
let cachedHeavyModule: WhatsAppHeavyModule | null = null;
let cachedLightModulePath: string | null = null;
let cachedLightModule: WhatsAppLightModule | null = null;
const jitiLoaders = new Map<boolean, ReturnType<typeof createJiti>>();
function readConfigSafely() {
try {
return loadConfig();
} catch {
return {};
}
}
function resolveWhatsAppPluginRecord(): WhatsAppPluginRecord {
const manifestRegistry = loadPluginManifestRegistry({
config: readConfigSafely(),
cache: true,
});
const record = manifestRegistry.plugins.find((plugin) => plugin.id === WHATSAPP_PLUGIN_ID);
if (!record?.source) {
throw new Error(
`WhatsApp plugin runtime is unavailable: missing plugin '${WHATSAPP_PLUGIN_ID}'`,
);
}
return {
origin: record.origin,
rootDir: record.rootDir,
source: record.source,
};
}
function resolveWhatsAppRuntimeModulePath(
record: WhatsAppPluginRecord,
entryBaseName: "light-runtime-api" | "runtime-api",
): string {
const candidates = [
path.join(path.dirname(record.source), `${entryBaseName}.js`),
path.join(path.dirname(record.source), `${entryBaseName}.ts`),
...(record.rootDir
? [
path.join(record.rootDir, `${entryBaseName}.js`),
path.join(record.rootDir, `${entryBaseName}.ts`),
]
: []),
];
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
throw new Error(
`WhatsApp plugin runtime is unavailable: missing ${entryBaseName} for plugin '${WHATSAPP_PLUGIN_ID}'`,
);
}
function getJiti(modulePath: string) {
const tryNative = shouldPreferNativeJiti(modulePath);
const cached = jitiLoaders.get(tryNative);
if (cached) {
return cached;
}
const pluginSdkAlias = resolvePluginSdkAliasFile({
srcFile: "root-alias.cjs",
distFile: "root-alias.cjs",
modulePath: modulePath,
});
const aliasMap = {
...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}),
...resolvePluginSdkScopedAliasMap({ modulePath }),
};
const loader = createJiti(import.meta.url, {
...buildPluginLoaderJitiOptions(aliasMap),
tryNative,
});
jitiLoaders.set(tryNative, loader);
return loader;
}
function loadWithJiti<TModule>(modulePath: string): TModule {
return getJiti(modulePath)(modulePath) as TModule;
}
function loadCurrentHeavyModuleSync(): WhatsAppHeavyModule {
const modulePath = resolveWhatsAppRuntimeModulePath(resolveWhatsAppPluginRecord(), "runtime-api");
return loadWithJiti<WhatsAppHeavyModule>(modulePath);
}
function loadWhatsAppLightModule(): WhatsAppLightModule {
const modulePath = resolveWhatsAppRuntimeModulePath(
resolveWhatsAppPluginRecord(),
"light-runtime-api",
);
if (cachedLightModule && cachedLightModulePath === modulePath) {
return cachedLightModule;
}
const loaded = loadWithJiti<WhatsAppLightModule>(modulePath);
cachedLightModulePath = modulePath;
cachedLightModule = loaded;
return loaded;
}
async function loadWhatsAppHeavyModule(): Promise<WhatsAppHeavyModule> {
const record = resolveWhatsAppPluginRecord();
const modulePath = resolveWhatsAppRuntimeModulePath(record, "runtime-api");
if (cachedHeavyModule && cachedHeavyModulePath === modulePath) {
return cachedHeavyModule;
}
const loaded = loadWithJiti<WhatsAppHeavyModule>(modulePath);
cachedHeavyModulePath = modulePath;
cachedHeavyModule = loaded;
return loaded;
}
function getLightExport<K extends keyof WhatsAppLightModule>(
exportName: K,
): NonNullable<WhatsAppLightModule[K]> {
const loaded = loadWhatsAppLightModule();
const value = loaded[exportName];
if (value == null) {
throw new Error(`WhatsApp plugin runtime is missing export '${String(exportName)}'`);
}
return value as NonNullable<WhatsAppLightModule[K]>;
}
async function getHeavyExport<K extends keyof WhatsAppHeavyModule>(
exportName: K,
): Promise<NonNullable<WhatsAppHeavyModule[K]>> {
const loaded = await loadWhatsAppHeavyModule();
const value = loaded[exportName];
if (value == null) {
throw new Error(`WhatsApp plugin runtime is missing export '${String(exportName)}'`);
}
return value as NonNullable<WhatsAppHeavyModule[K]>;
}
export function getActiveWebListener(
...args: Parameters<WhatsAppLightModule["getActiveWebListener"]>
): ReturnType<WhatsAppLightModule["getActiveWebListener"]> {
return getLightExport("getActiveWebListener")(...args);
}
export function getWebAuthAgeMs(
...args: Parameters<WhatsAppLightModule["getWebAuthAgeMs"]>
): ReturnType<WhatsAppLightModule["getWebAuthAgeMs"]> {
return getLightExport("getWebAuthAgeMs")(...args);
}
export function logWebSelfId(
...args: Parameters<WhatsAppLightModule["logWebSelfId"]>
): ReturnType<WhatsAppLightModule["logWebSelfId"]> {
return getLightExport("logWebSelfId")(...args);
}
export function loginWeb(
...args: Parameters<WhatsAppHeavyModule["loginWeb"]>
): ReturnType<WhatsAppHeavyModule["loginWeb"]> {
return loadWhatsAppHeavyModule().then((loaded) => loaded.loginWeb(...args));
}
export function logoutWeb(
...args: Parameters<WhatsAppLightModule["logoutWeb"]>
): ReturnType<WhatsAppLightModule["logoutWeb"]> {
return getLightExport("logoutWeb")(...args);
}
export function readWebSelfId(
...args: Parameters<WhatsAppLightModule["readWebSelfId"]>
): ReturnType<WhatsAppLightModule["readWebSelfId"]> {
return getLightExport("readWebSelfId")(...args);
}
export function webAuthExists(
...args: Parameters<WhatsAppLightModule["webAuthExists"]>
): ReturnType<WhatsAppLightModule["webAuthExists"]> {
return getLightExport("webAuthExists")(...args);
}
export function sendMessageWhatsApp(
...args: Parameters<WhatsAppHeavyModule["sendMessageWhatsApp"]>
): ReturnType<WhatsAppHeavyModule["sendMessageWhatsApp"]> {
return loadWhatsAppHeavyModule().then((loaded) => loaded.sendMessageWhatsApp(...args));
}
export function sendPollWhatsApp(
...args: Parameters<WhatsAppHeavyModule["sendPollWhatsApp"]>
): ReturnType<WhatsAppHeavyModule["sendPollWhatsApp"]> {
return loadWhatsAppHeavyModule().then((loaded) => loaded.sendPollWhatsApp(...args));
}
export function sendReactionWhatsApp(
...args: Parameters<WhatsAppHeavyModule["sendReactionWhatsApp"]>
): ReturnType<WhatsAppHeavyModule["sendReactionWhatsApp"]> {
return loadWhatsAppHeavyModule().then((loaded) => loaded.sendReactionWhatsApp(...args));
}
export function createRuntimeWhatsAppLoginTool(
...args: Parameters<WhatsAppLightModule["createWhatsAppLoginTool"]>
): ReturnType<WhatsAppLightModule["createWhatsAppLoginTool"]> {
return getLightExport("createWhatsAppLoginTool")(...args);
}
export function createWaSocket(
...args: Parameters<WhatsAppHeavyModule["createWaSocket"]>
): ReturnType<WhatsAppHeavyModule["createWaSocket"]> {
return loadWhatsAppHeavyModule().then((loaded) => loaded.createWaSocket(...args));
}
export function formatError(
...args: Parameters<WhatsAppLightModule["formatError"]>
): ReturnType<WhatsAppLightModule["formatError"]> {
return getLightExport("formatError")(...args);
}
export function getStatusCode(
...args: Parameters<WhatsAppLightModule["getStatusCode"]>
): ReturnType<WhatsAppLightModule["getStatusCode"]> {
return getLightExport("getStatusCode")(...args);
}
export function pickWebChannel(
...args: Parameters<WhatsAppLightModule["pickWebChannel"]>
): ReturnType<WhatsAppLightModule["pickWebChannel"]> {
return getLightExport("pickWebChannel")(...args);
}
export function resolveWaWebAuthDir(): WhatsAppLightModule["WA_WEB_AUTH_DIR"] {
return getLightExport("WA_WEB_AUTH_DIR");
}
export async function handleWhatsAppAction(
...args: Parameters<WhatsAppHeavyModule["handleWhatsAppAction"]>
): ReturnType<WhatsAppHeavyModule["handleWhatsAppAction"]> {
return (await getHeavyExport("handleWhatsAppAction"))(...args);
}
export async function loadWebMedia(
...args: Parameters<typeof loadWebMediaImpl>
): ReturnType<typeof loadWebMediaImpl> {
return await loadWebMediaImpl(...args);
}
export async function loadWebMediaRaw(
...args: Parameters<typeof loadWebMediaRawImpl>
): ReturnType<typeof loadWebMediaRawImpl> {
return await loadWebMediaRawImpl(...args);
}
export function monitorWebChannel(
...args: Parameters<WhatsAppHeavyModule["monitorWebChannel"]>
): ReturnType<WhatsAppHeavyModule["monitorWebChannel"]> {
return loadWhatsAppHeavyModule().then((loaded) => loaded.monitorWebChannel(...args));
}
export async function monitorWebInbox(
...args: Parameters<WhatsAppHeavyModule["monitorWebInbox"]>
): ReturnType<WhatsAppHeavyModule["monitorWebInbox"]> {
return (await getHeavyExport("monitorWebInbox"))(...args);
}
export async function optimizeImageToJpeg(
...args: Parameters<typeof optimizeImageToJpegImpl>
): ReturnType<typeof optimizeImageToJpegImpl> {
return await optimizeImageToJpegImpl(...args);
}
export async function runWebHeartbeatOnce(
...args: Parameters<WhatsAppHeavyModule["runWebHeartbeatOnce"]>
): ReturnType<WhatsAppHeavyModule["runWebHeartbeatOnce"]> {
return (await getHeavyExport("runWebHeartbeatOnce"))(...args);
}
export async function startWebLoginWithQr(
...args: Parameters<WhatsAppHeavyModule["startWebLoginWithQr"]>
): ReturnType<WhatsAppHeavyModule["startWebLoginWithQr"]> {
return (await getHeavyExport("startWebLoginWithQr"))(...args);
}
export async function waitForWaConnection(
...args: Parameters<WhatsAppHeavyModule["waitForWaConnection"]>
): ReturnType<WhatsAppHeavyModule["waitForWaConnection"]> {
return (await getHeavyExport("waitForWaConnection"))(...args);
}
export async function waitForWebLogin(
...args: Parameters<WhatsAppHeavyModule["waitForWebLogin"]>
): ReturnType<WhatsAppHeavyModule["waitForWebLogin"]> {
return (await getHeavyExport("waitForWebLogin"))(...args);
}
export const extractMediaPlaceholder = (
...args: Parameters<WhatsAppHeavyModule["extractMediaPlaceholder"]>
) => loadCurrentHeavyModuleSync().extractMediaPlaceholder(...args);
export const extractText = (...args: Parameters<WhatsAppHeavyModule["extractText"]>) =>
loadCurrentHeavyModuleSync().extractText(...args);
export function getDefaultLocalRoots(
...args: Parameters<typeof getDefaultLocalRootsImpl>
): ReturnType<typeof getDefaultLocalRootsImpl> {
return getDefaultLocalRootsImpl(...args);
}
export function resolveHeartbeatRecipients(
...args: Parameters<typeof resolveWhatsAppHeartbeatRecipients>
): ReturnType<typeof resolveWhatsAppHeartbeatRecipients> {
return resolveWhatsAppHeartbeatRecipients(...args);
}

View File

@@ -1 +1 @@
export { createWhatsAppLoginTool as createRuntimeWhatsAppLoginTool } from "openclaw/plugin-sdk/whatsapp";
export { createRuntimeWhatsAppLoginTool } from "./runtime-whatsapp-boundary.js";

View File

@@ -1,4 +1,4 @@
import { loginWeb as loginWebImpl } from "openclaw/plugin-sdk/whatsapp";
import { loginWeb as loginWebImpl } from "./runtime-whatsapp-boundary.js";
import type { PluginRuntime } from "./types.js";
type RuntimeWhatsAppLogin = Pick<PluginRuntime["channel"]["whatsapp"], "loginWeb">;

View File

@@ -1,7 +1,7 @@
import {
sendMessageWhatsApp as sendMessageWhatsAppImpl,
sendPollWhatsApp as sendPollWhatsAppImpl,
} from "openclaw/plugin-sdk/whatsapp";
} from "./runtime-whatsapp-boundary.js";
import type { PluginRuntime } from "./types.js";
type RuntimeWhatsAppOutbound = Pick<

View File

@@ -1,90 +1,21 @@
import { getActiveWebListener } from "openclaw/plugin-sdk/whatsapp";
import {
createRuntimeWhatsAppLoginTool,
getActiveWebListener,
getWebAuthAgeMs,
handleWhatsAppAction,
logWebSelfId,
loginWeb,
logoutWeb,
monitorWebChannel,
readWebSelfId,
sendMessageWhatsApp,
sendPollWhatsApp,
startWebLoginWithQr,
waitForWebLogin,
webAuthExists,
} from "openclaw/plugin-sdk/whatsapp";
import {
createLazyRuntimeMethodBinder,
createLazyRuntimeSurface,
} from "../../shared/lazy-runtime.js";
import { createRuntimeWhatsAppLoginTool } from "./runtime-whatsapp-login-tool.js";
} from "./runtime-whatsapp-boundary.js";
import type { PluginRuntime } from "./types.js";
const loadWebOutbound = createLazyRuntimeSurface(
() => import("./runtime-whatsapp-outbound.runtime.js"),
({ runtimeWhatsAppOutbound }) => runtimeWhatsAppOutbound,
);
const loadWebLogin = createLazyRuntimeSurface(
() => import("./runtime-whatsapp-login.runtime.js"),
({ runtimeWhatsAppLogin }) => runtimeWhatsAppLogin,
);
const bindWhatsAppOutboundMethod = createLazyRuntimeMethodBinder(loadWebOutbound);
const bindWhatsAppLoginMethod = createLazyRuntimeMethodBinder(loadWebLogin);
const sendMessageWhatsAppLazy = bindWhatsAppOutboundMethod(
(runtimeWhatsAppOutbound) => runtimeWhatsAppOutbound.sendMessageWhatsApp,
);
const sendPollWhatsAppLazy = bindWhatsAppOutboundMethod(
(runtimeWhatsAppOutbound) => runtimeWhatsAppOutbound.sendPollWhatsApp,
);
const loginWebLazy = bindWhatsAppLoginMethod(
(runtimeWhatsAppLogin) => runtimeWhatsAppLogin.loginWeb,
);
const startWebLoginWithQrLazy: PluginRuntime["channel"]["whatsapp"]["startWebLoginWithQr"] = async (
...args
) => {
const { startWebLoginWithQr } = await loadWebLoginQr();
return startWebLoginWithQr(...args);
};
const waitForWebLoginLazy: PluginRuntime["channel"]["whatsapp"]["waitForWebLogin"] = async (
...args
) => {
const { waitForWebLogin } = await loadWebLoginQr();
return waitForWebLogin(...args);
};
const monitorWebChannelLazy: PluginRuntime["channel"]["whatsapp"]["monitorWebChannel"] = async (
...args
) => {
const { monitorWebChannel } = await loadWebChannel();
return monitorWebChannel(...args);
};
const handleWhatsAppActionLazy: PluginRuntime["channel"]["whatsapp"]["handleWhatsAppAction"] =
async (...args) => {
const { handleWhatsAppAction } = await loadWhatsAppActions();
return handleWhatsAppAction(...args);
};
let webLoginQrPromise: Promise<typeof import("openclaw/plugin-sdk/whatsapp-login-qr")> | null =
null;
let webChannelPromise: Promise<typeof import("../../channels/web/index.js")> | null = null;
let whatsappActionsPromise: Promise<
typeof import("openclaw/plugin-sdk/whatsapp-action-runtime")
> | null = null;
function loadWebLoginQr() {
webLoginQrPromise ??= import("openclaw/plugin-sdk/whatsapp-login-qr");
return webLoginQrPromise;
}
function loadWebChannel() {
webChannelPromise ??= import("../../channels/web/index.js");
return webChannelPromise;
}
function loadWhatsAppActions() {
whatsappActionsPromise ??= import("openclaw/plugin-sdk/whatsapp-action-runtime");
return whatsappActionsPromise;
}
export function createRuntimeWhatsApp(): PluginRuntime["channel"]["whatsapp"] {
return {
getActiveWebListener,
@@ -93,13 +24,13 @@ export function createRuntimeWhatsApp(): PluginRuntime["channel"]["whatsapp"] {
logWebSelfId,
readWebSelfId,
webAuthExists,
sendMessageWhatsApp: sendMessageWhatsAppLazy,
sendPollWhatsApp: sendPollWhatsAppLazy,
loginWeb: loginWebLazy,
startWebLoginWithQr: startWebLoginWithQrLazy,
waitForWebLogin: waitForWebLoginLazy,
monitorWebChannel: monitorWebChannelLazy,
handleWhatsAppAction: handleWhatsAppActionLazy,
sendMessageWhatsApp,
sendPollWhatsApp,
loginWeb,
startWebLoginWithQr,
waitForWebLogin,
monitorWebChannel,
handleWhatsAppAction,
createLoginTool: createRuntimeWhatsAppLoginTool,
};
}

View File

@@ -205,19 +205,19 @@ export type PluginRuntimeChannel = {
sendMessageIMessage: typeof import("../../../extensions/imessage/runtime-api.js").sendMessageIMessage;
};
whatsapp: {
getActiveWebListener: typeof import("openclaw/plugin-sdk/whatsapp").getActiveWebListener;
getWebAuthAgeMs: typeof import("openclaw/plugin-sdk/whatsapp").getWebAuthAgeMs;
logoutWeb: typeof import("openclaw/plugin-sdk/whatsapp").logoutWeb;
logWebSelfId: typeof import("openclaw/plugin-sdk/whatsapp").logWebSelfId;
readWebSelfId: typeof import("openclaw/plugin-sdk/whatsapp").readWebSelfId;
webAuthExists: typeof import("openclaw/plugin-sdk/whatsapp").webAuthExists;
sendMessageWhatsApp: typeof import("openclaw/plugin-sdk/whatsapp").sendMessageWhatsApp;
sendPollWhatsApp: typeof import("openclaw/plugin-sdk/whatsapp").sendPollWhatsApp;
loginWeb: typeof import("openclaw/plugin-sdk/whatsapp").loginWeb;
startWebLoginWithQr: typeof import("openclaw/plugin-sdk/whatsapp-login-qr").startWebLoginWithQr;
waitForWebLogin: typeof import("openclaw/plugin-sdk/whatsapp-login-qr").waitForWebLogin;
monitorWebChannel: typeof import("../../channels/web/index.js").monitorWebChannel;
handleWhatsAppAction: typeof import("openclaw/plugin-sdk/whatsapp-action-runtime").handleWhatsAppAction;
getActiveWebListener: typeof import("./runtime-whatsapp-boundary.js").getActiveWebListener;
getWebAuthAgeMs: typeof import("./runtime-whatsapp-boundary.js").getWebAuthAgeMs;
logoutWeb: typeof import("./runtime-whatsapp-boundary.js").logoutWeb;
logWebSelfId: typeof import("./runtime-whatsapp-boundary.js").logWebSelfId;
readWebSelfId: typeof import("./runtime-whatsapp-boundary.js").readWebSelfId;
webAuthExists: typeof import("./runtime-whatsapp-boundary.js").webAuthExists;
sendMessageWhatsApp: typeof import("./runtime-whatsapp-boundary.js").sendMessageWhatsApp;
sendPollWhatsApp: typeof import("./runtime-whatsapp-boundary.js").sendPollWhatsApp;
loginWeb: typeof import("./runtime-whatsapp-boundary.js").loginWeb;
startWebLoginWithQr: typeof import("./runtime-whatsapp-boundary.js").startWebLoginWithQr;
waitForWebLogin: typeof import("./runtime-whatsapp-boundary.js").waitForWebLogin;
monitorWebChannel: typeof import("./runtime-whatsapp-boundary.js").monitorWebChannel;
handleWhatsAppAction: typeof import("./runtime-whatsapp-boundary.js").handleWhatsAppAction;
createLoginTool: typeof import("./runtime-whatsapp-login-tool.js").createRuntimeWhatsAppLoginTool;
};
line: {

View File

@@ -39,7 +39,7 @@ export type PluginRuntimeCore = {
formatNativeDependencyHint: typeof import("./native-deps.js").formatNativeDependencyHint;
};
media: {
loadWebMedia: typeof import("../../../extensions/whatsapp/runtime-api.js").loadWebMedia;
loadWebMedia: typeof import("../../media/web-media.js").loadWebMedia;
detectMime: typeof import("../../media/mime.js").detectMime;
mediaKindFromMime: typeof import("../../media/constants.js").mediaKindFromMime;
isVoiceCompatibleAudio: typeof import("../../media/audio.js").isVoiceCompatibleAudio;