build(whatsapp): externalize whatsapp plugin

This commit is contained in:
Peter Steinberger 2026-05-13 10:58:24 +01:00
parent 49ccd4e080
commit 85f9276624
No known key found for this signature in database
18 changed files with 81 additions and 80 deletions

View file

@ -53,7 +53,7 @@ Docs: https://docs.openclaw.ai
- Scrub streamable MCP redirect headers [AI]. (#80906) Thanks @pgondhi987.
- fix(memory-wiki): require admin scope for ingest [AI]. (#80897) Thanks @pgondhi987.
- memory-wiki: require write scope for Obsidian search [AI]. (#80904) Thanks @pgondhi987.
- WhatsApp/install: allow Baileys' pinned libsignal git subdependency under pnpm 11 so source installs and local checks can complete.
- WhatsApp: externalize the channel as a ClawHub/npm plugin outside the core npm runtime bundle, and bump Baileys to `7.0.0-rc11` so libsignal resolves from the registry instead of a GitHub tarball.
- Build: skip copied metadata for bundled plugins that are excluded from build entries, preventing update/status rebuilds from advertising missing QQ Bot runtime files. (#80925)
- Control UI/sessions: nest subagent sessions under their parent session in the session picker dropdown using a visual `└─ ` prefix, making the parent-child relationship clear. Fixes #77628. (#78623) Thanks @chinar-amrutkar.
- fix(config): reject auto-managed meta.lastTouched\* paths in config set/unset (#80856). Thanks @ai-hpc

View file

@ -16,8 +16,8 @@ Text is supported everywhere; media and reactions vary by channel.
- Slack multi-person DMs route as group chats, so group policy, mention
behavior, and group-session rules apply to MPIM conversations.
- WhatsApp setup is install-on-demand: onboarding can show the setup flow before
the plugin package is installed, and the Gateway loads the WhatsApp runtime
only when the channel is actually active.
the plugin package is installed, and the Gateway loads the external
ClawHub/npm plugin only when the channel is actually active.
## Supported channels

View file

@ -14,27 +14,19 @@ Status: production-ready via WhatsApp Web (Baileys). Gateway owns linked session
- `openclaw channels login --channel whatsapp` also offers the install flow when
the plugin is not present yet.
- Dev channel + git checkout: defaults to the local plugin path.
- Stable/Beta: uses the npm package `@openclaw/whatsapp` on the current official
release tag.
- Stable/Beta: installs the official `@openclaw/whatsapp` plugin from ClawHub
first, with npm as the fallback.
- The WhatsApp runtime is distributed outside the core OpenClaw npm package
because its Baileys dependency chain uses GPL-licensed `libsignal`.
Manual install stays available:
```bash
openclaw plugins install @openclaw/whatsapp
openclaw plugins install clawhub:@openclaw/whatsapp
```
Use the bare package to follow the current official release tag. Pin an exact
version only when you need a reproducible install.
On Windows, the WhatsApp plugin needs Git on `PATH` during npm install because
one of its Baileys/libsignal dependencies is fetched from a git URL. Install
Git for Windows, then restart the shell and rerun the install:
```powershell
winget install --id Git.Git -e
```
Portable Git also works if its `bin` directory is on `PATH`.
Use the bare npm package (`@openclaw/whatsapp`) only when you need the registry
fallback. Pin an exact version only when you need a reproducible install.
<CardGroup cols={3}>
<Card title="Pairing" icon="link" href="/channels/pairing">

View file

@ -166,7 +166,7 @@ uninstall, and publishing commands.
| [tlon](/plugins/reference/tlon) | Adds the Tlon channel surface for sending and receiving OpenClaw messages. | `@openclaw/tlon`<br />npm; ClawHub | channels: tlon; contracts: tools; skills |
| [twitch](/plugins/reference/twitch) | Adds the Twitch channel surface for sending and receiving OpenClaw messages. | `@openclaw/twitch`<br />npm; ClawHub | channels: twitch |
| [voice-call](/plugins/reference/voice-call) | Adds agent-callable tools. | `@openclaw/voice-call`<br />npm; ClawHub | contracts: tools |
| [whatsapp](/plugins/reference/whatsapp) | Adds the WhatsApp channel surface for sending and receiving OpenClaw messages. | `@openclaw/whatsapp`<br />npm; ClawHub | channels: whatsapp |
| [whatsapp](/plugins/reference/whatsapp) | Adds the WhatsApp channel surface for sending and receiving OpenClaw messages. | `@openclaw/whatsapp`<br />ClawHub: `clawhub:@openclaw/whatsapp`; npm | channels: whatsapp |
| [zalo](/plugins/reference/zalo) | Adds the Zalo channel surface for sending and receiving OpenClaw messages. | `@openclaw/zalo`<br />npm; ClawHub | channels: zalo |
| [zalouser](/plugins/reference/zalouser) | Adds the Zalo Personal channel surface for sending and receiving OpenClaw messages. | `@openclaw/zalouser`<br />npm; ClawHub | channels: zalouser; contracts: tools |

View file

@ -128,7 +128,7 @@ pnpm plugins:inventory:gen
| [vydra](/plugins/reference/vydra) | Adds Vydra model provider support to OpenClaw. | `@openclaw/vydra-provider`<br />included in OpenClaw | providers: vydra; contracts: imageGenerationProviders, speechProviders, videoGenerationProviders |
| [web-readability](/plugins/reference/web-readability) | Extract readable article content from local HTML web fetch responses. | `@openclaw/web-readability-plugin`<br />included in OpenClaw | contracts: webContentExtractors |
| [webhooks](/plugins/reference/webhooks) | Authenticated inbound webhooks that bind external automation to OpenClaw TaskFlows. | `@openclaw/webhooks`<br />included in OpenClaw | plugin |
| [whatsapp](/plugins/reference/whatsapp) | Adds the WhatsApp channel surface for sending and receiving OpenClaw messages. | `@openclaw/whatsapp`<br />npm; ClawHub | channels: whatsapp |
| [whatsapp](/plugins/reference/whatsapp) | Adds the WhatsApp channel surface for sending and receiving OpenClaw messages. | `@openclaw/whatsapp`<br />ClawHub: `clawhub:@openclaw/whatsapp`; npm | channels: whatsapp |
| [xai](/plugins/reference/xai) | Adds xAI model provider support to OpenClaw. | `@openclaw/xai-plugin`<br />included in OpenClaw | providers: xai; contracts: imageGenerationProviders, mediaUnderstandingProviders, realtimeTranscriptionProviders, speechProviders, tools, videoGenerationProviders, webSearchProviders |
| [xiaomi](/plugins/reference/xiaomi) | Adds Xiaomi model provider support to OpenClaw. | `@openclaw/xiaomi-provider`<br />included in OpenClaw | providers: xiaomi; contracts: speechProviders |
| [zai](/plugins/reference/zai) | Adds Z.AI model provider support to OpenClaw. | `@openclaw/zai-provider`<br />included in OpenClaw | providers: zai; contracts: mediaUnderstandingProviders |

View file

@ -12,22 +12,12 @@ Adds the WhatsApp channel surface for sending and receiving OpenClaw messages.
## Distribution
- Package: `@openclaw/whatsapp`
- Install route: npm; ClawHub
- Install route: ClawHub: `clawhub:@openclaw/whatsapp`; npm
## Surface
channels: whatsapp
## Windows install note
On Windows, the WhatsApp plugin needs Git on `PATH` during npm install because one of its Baileys/libsignal dependencies is fetched from a git URL. Install Git for Windows, then restart the shell and rerun the install:
```powershell
winget install --id Git.Git -e
```
Portable Git also works if its `bin` directory is on `PATH`.
## Related docs
- [whatsapp](/channels/whatsapp)

View file

@ -8,7 +8,7 @@
},
"type": "module",
"dependencies": {
"baileys": "7.0.0-rc10",
"baileys": "7.0.0-rc11",
"https-proxy-agent": "9.0.0",
"jimp": "1.6.1",
"typebox": "1.1.38",
@ -56,8 +56,9 @@
]
},
"install": {
"clawhubSpec": "clawhub:@openclaw/whatsapp",
"npmSpec": "@openclaw/whatsapp",
"defaultChoice": "npm",
"defaultChoice": "clawhub",
"minHostVersion": ">=2026.4.25"
},
"compat": {

39
pnpm-lock.yaml generated
View file

@ -35,7 +35,7 @@ packageExtensionsChecksum: sha256-oc/FAHkBR844HBfph1RZWyRMHHBpIFya25tyv5SGf6s=
patchedDependencies:
'@agentclientprotocol/claude-agent-acp@0.33.1': 3995624bb834cc60fea1461c7ef33f1fcdd8fb58b8f43f2f1490bc689f6e1be2
baileys@7.0.0-rc10: a9aea1790d2c65b1ae543c77faca4119bbfb91ee3b6ca6c38d1cad4f5702ada2
baileys@7.0.0-rc11: a9aea1790d2c65b1ae543c77faca4119bbfb91ee3b6ca6c38d1cad4f5702ada2
importers:
@ -1656,8 +1656,8 @@ importers:
extensions/whatsapp:
dependencies:
baileys:
specifier: 7.0.0-rc10
version: 7.0.0-rc10(patch_hash=a9aea1790d2c65b1ae543c77faca4119bbfb91ee3b6ca6c38d1cad4f5702ada2)(audio-decode@2.2.3)(jimp@1.6.1)(sharp@0.34.5)
specifier: 7.0.0-rc11
version: 7.0.0-rc11(patch_hash=a9aea1790d2c65b1ae543c77faca4119bbfb91ee3b6ca6c38d1cad4f5702ada2)(audio-decode@2.2.3)(jimp@1.6.1)(sharp@0.34.5)
https-proxy-agent:
specifier: 9.0.0
version: 9.0.0
@ -4602,10 +4602,6 @@ packages:
'@wasm-audio-decoders/opus-ml@0.0.2':
resolution: {integrity: sha512-58rWEqDGg+CKCyEeKm2KoxxSwTWtHh/NLTW9ObR4K8CGF6VwuuGudEI1CtniS/oSRmL1nJq/eh8MKARiluw4DQ==}
'@whiskeysockets/libsignal-node@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67':
resolution: {gitHosted: true, tarball: https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67}
version: 2.0.1
'@zed-industries/codex-acp-darwin-arm64@0.14.0':
resolution: {integrity: sha512-FWjHKlNJTZmXWM/2/GAQAg0WJEjPlfxEQJgvfxuzK1xZh81CDg9U6uSgZ1ggBkkN2bOoCnOupvGAPOmBoL5m2Q==}
cpu: [arm64]
@ -4811,8 +4807,8 @@ packages:
bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
baileys@7.0.0-rc10:
resolution: {integrity: sha512-B1ShVyha4iZP7aNyz4QsXFceExzkvLFgISQGaDGHQaYFxIdNqoen+xKmlo1iWEsaexvQORAvUK44VicRi1T94g==}
baileys@7.0.0-rc11:
resolution: {integrity: sha512-yi7An4f5DmShjb75L8eSttrA6HLy7PYoP3XIQBmvQ5yH8vVZWBXZDogDxfGQQL6N98ELRwznG7puhjyLEa5CQg==}
engines: {node: '>=20.0.0'}
peerDependencies:
audio-decode: ^2.1.3
@ -6022,6 +6018,9 @@ packages:
resolution: {integrity: sha512-LrQfPUeTW7MXbMvT62moEMnpMTuj9TO3lqjCeLKjM975PJ4Alrl/43f2tlDX7xOsNptKgH4LSNGwIbXwEkLg4g==}
engines: {node: '>=22.0.0'}
libsignal@6.0.0:
resolution: {integrity: sha512-d/5V3YFtDljbFMufz4ncyUYGYhJl+vzAe+c2EFFBQ6bz1h8Q3IOMEGXYMzlibU60I+e8GagMMpji18iez3P1hA==}
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
@ -7757,8 +7756,8 @@ packages:
resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
engines: {node: '>=20'}
whatsapp-rust-bridge@0.5.3:
resolution: {integrity: sha512-Xb3GAgtWQQJ30oI4a4pjM4+YUeli9CMLTwTIewUrb+AJMFElIkiT5uo+j1Zhc+amiV0Jj+LfX76c/EEZirJbGA==}
whatsapp-rust-bridge@0.5.4:
resolution: {integrity: sha512-yYO1qSs0Fe7tGtnxOFHomocUD6IZtoAgmA4oDFyGIRZ67D3QZk3w7swA6XXFXNQngiyrg2k7tul6IrM3eUFh7A==}
whatwg-mimetype@5.0.0:
resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
@ -11297,11 +11296,6 @@ snapshots:
dependencies:
'@wasm-audio-decoders/common': 9.0.7
'@whiskeysockets/libsignal-node@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67':
dependencies:
curve25519-js: 0.0.4
protobufjs: 7.5.6
'@zed-industries/codex-acp-darwin-arm64@0.14.0':
optional: true
@ -11485,19 +11479,19 @@ snapshots:
bail@2.0.2: {}
baileys@7.0.0-rc10(patch_hash=a9aea1790d2c65b1ae543c77faca4119bbfb91ee3b6ca6c38d1cad4f5702ada2)(audio-decode@2.2.3)(jimp@1.6.1)(sharp@0.34.5):
baileys@7.0.0-rc11(patch_hash=a9aea1790d2c65b1ae543c77faca4119bbfb91ee3b6ca6c38d1cad4f5702ada2)(audio-decode@2.2.3)(jimp@1.6.1)(sharp@0.34.5):
dependencies:
'@cacheable/node-cache': 1.7.6
'@hapi/boom': 9.1.4
async-mutex: 0.5.0
libsignal: '@whiskeysockets/libsignal-node@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67'
libsignal: 6.0.0
lru-cache: 11.3.6
music-metadata: 11.12.3
p-queue: 9.2.0
pino: 9.14.0
protobufjs: 7.5.6
sharp: 0.34.5
whatsapp-rust-bridge: 0.5.3
whatsapp-rust-bridge: 0.5.4
ws: 8.20.0
optionalDependencies:
audio-decode: 2.2.3
@ -12881,6 +12875,11 @@ snapshots:
kysely@0.29.0: {}
libsignal@6.0.0:
dependencies:
curve25519-js: 0.0.4
protobufjs: 7.5.6
lie@3.3.0:
dependencies:
immediate: 3.0.6
@ -14867,7 +14866,7 @@ snapshots:
webidl-conversions@8.0.1: {}
whatsapp-rust-bridge@0.5.3: {}
whatsapp-rust-bridge@0.5.4: {}
whatwg-mimetype@5.0.0: {}

View file

@ -12,7 +12,9 @@ minimumReleaseAgeExclude:
- "@agentclientprotocol/sdk"
- "axios"
- "basic-ftp"
- "baileys@7.0.0-rc11"
- "hono"
- "libsignal@6.0.0"
- "openclaw"
- "protobufjs"
- "vite"
@ -71,7 +73,6 @@ allowBuilds:
"@tloncorp/api": true
"@tloncorp/tlon-skill": true
baileys: true
"@whiskeysockets/libsignal-node": true
authenticate-pam: true
"@discordjs/opus": false
esbuild: true
@ -92,5 +93,5 @@ peerDependencyRules:
"prism-media>opusscript": "^0.0.8 || ^0.1.1"
patchedDependencies:
"baileys@7.0.0-rc10": "patches/baileys@7.0.0-rc10.patch"
"baileys@7.0.0-rc11": "patches/baileys@7.0.0-rc11.patch"
"@agentclientprotocol/claude-agent-acp@0.33.1": "patches/@agentclientprotocol__claude-agent-acp@0.33.1.patch"

View file

@ -28,20 +28,7 @@ const PLUGIN_DOC_ALIASES = new Map([
["tavily", "/tools/tavily"],
["tokenjuice", "/tools/tokenjuice"],
]);
const PLUGIN_REFERENCE_EXTRA_SECTIONS = new Map([
[
"whatsapp",
`## Windows install note
On Windows, the WhatsApp plugin needs Git on \`PATH\` during npm install because one of its Baileys/libsignal dependencies is fetched from a git URL. Install Git for Windows, then restart the shell and rerun the install:
\`\`\`powershell
winget install --id Git.Git -e
\`\`\`
Portable Git also works if its \`bin\` directory is on \`PATH\`.`,
],
]);
const PLUGIN_REFERENCE_EXTRA_SECTIONS = new Map([]);
function readJson(relativePath) {
return JSON.parse(fs.readFileSync(path.join(ROOT, relativePath), "utf8"));

View file

@ -9,7 +9,7 @@ import { shouldBuildBundledCluster } from "./optional-bundled-clusters.mjs";
const TOP_LEVEL_PUBLIC_SURFACE_EXTENSIONS = new Set([".ts", ".js", ".mts", ".cts", ".mjs", ".cjs"]);
export const NON_PACKAGED_BUNDLED_PLUGIN_DIRS = new Set(["qa-channel", "qa-lab", "qa-matrix"]);
const EXCLUDED_CORE_BUNDLED_PLUGIN_DIRS = new Set(["qqbot"]);
const EXCLUDED_CORE_BUNDLED_PLUGIN_DIRS = new Set(["qqbot", "whatsapp"]);
const toPosixPath = (value) => value.replaceAll("\\", "/");
function readBundledPluginPackageJson(packageJsonPath) {

View file

@ -475,8 +475,9 @@
"systemImage": "message"
},
"install": {
"clawhubSpec": "clawhub:@openclaw/whatsapp",
"npmSpec": "@openclaw/whatsapp",
"defaultChoice": "npm",
"defaultChoice": "clawhub",
"minHostVersion": ">=2026.4.25"
}
}

View file

@ -414,7 +414,7 @@ describe("copyBundledPluginMetadata", () => {
expectedExists: false,
},
{
name: "still bundles previously released optional plugins without the opt-in env",
name: "removes externalized optional plugin metadata from the core dist",
pluginId: "whatsapp",
packageName: "@openclaw/whatsapp",
packageOpenClaw: {
@ -422,7 +422,7 @@ describe("copyBundledPluginMetadata", () => {
install: { npmSpec: "@openclaw/whatsapp" },
},
env: {},
expectedExists: true,
expectedExists: false,
},
] as const)("$name", ({ pluginId, packageName, packageOpenClaw, env, expectedExists }) => {
const repoRoot = makeRepoRoot(`openclaw-bundled-plugin-${pluginId}-`);

View file

@ -43,6 +43,31 @@ describe("resolveMissingOfficialExternalChannelPluginRepairHint", () => {
});
});
it("prefers the ClawHub install hint for externalized WhatsApp", () => {
mocks.resolveConfiguredChannelPresencePolicy.mockReturnValue([
{
channelId: "whatsapp",
sources: ["explicit-config"],
effective: false,
pluginIds: [],
blockedReasons: ["no-channel-owner"],
},
]);
expect(
resolveMissingOfficialExternalChannelPluginRepairHint({
config: { channels: { whatsapp: { enabled: true } } },
channelId: "whatsapp",
}),
).toMatchObject({
pluginId: "whatsapp",
channelId: "whatsapp",
label: "WhatsApp",
installSpec: "clawhub:@openclaw/whatsapp",
installCommand: "openclaw plugins install clawhub:@openclaw/whatsapp",
});
});
it("does not return install hints for policy-blocked official external channel owners", () => {
mocks.resolveConfiguredChannelPresencePolicy.mockReturnValue([
{

View file

@ -85,9 +85,10 @@ describe("buildOfficialChannelCatalog", () => {
blurb: "works with your own number; recommend a separate phone + eSIM.",
},
install: {
clawhubSpec: "clawhub:@openclaw/whatsapp",
npmSpec: "@openclaw/whatsapp",
localPath: bundledPluginRoot("whatsapp"),
defaultChoice: "npm",
defaultChoice: "clawhub",
},
release: {
publishToNpm: true,
@ -195,8 +196,9 @@ describe("buildOfficialChannelCatalog", () => {
systemImage: "message",
},
install: {
clawhubSpec: "clawhub:@openclaw/whatsapp",
npmSpec: "@openclaw/whatsapp",
defaultChoice: "npm",
defaultChoice: "clawhub",
minHostVersion: ">=2026.4.25",
},
});
@ -325,8 +327,9 @@ describe("buildOfficialChannelCatalog", () => {
systemImage: "message",
},
install: {
clawhubSpec: "clawhub:@openclaw/whatsapp",
npmSpec: "@openclaw/whatsapp",
defaultChoice: "npm",
defaultChoice: "clawhub",
minHostVersion: ">=2026.4.25",
},
});

View file

@ -104,8 +104,10 @@ describe("bundled plugin build entries", () => {
expectSomePrefixMatch(Object.keys(entries), `extensions/${pluginId}/`);
expectNoPrefixMatches(artifacts, `dist/extensions/${pluginId}/`);
}
expectNoPrefixMatches(Object.keys(entries), "extensions/qqbot/");
expectNoPrefixMatches(artifacts, "dist/extensions/qqbot/");
for (const pluginId of ["qqbot", "whatsapp"]) {
expectNoPrefixMatches(Object.keys(entries), `extensions/${pluginId}/`);
expectNoPrefixMatches(artifacts, `dist/extensions/${pluginId}/`);
}
});
it("keeps bundled channel secret contracts on packed top-level sidecars", () => {

View file

@ -221,7 +221,7 @@ describe("bundled plugin postinstall", () => {
expect(warn).not.toHaveBeenCalled();
});
it("patches the Baileys rc10 upload helper dispatcher guard", async () => {
it("patches the Baileys upload helper dispatcher guard", async () => {
const packageRoot = await createTempDirAsync("openclaw-baileys-postinstall-");
const mediaFile = await writeBaileysMediaFile(
packageRoot,
@ -266,7 +266,7 @@ describe("bundled plugin postinstall", () => {
expect(patchedText).not.toContain(" dispatcher: agent,");
});
it("recognizes already patched Baileys rc10 upload helpers", async () => {
it("recognizes already patched Baileys upload helpers", async () => {
const packageRoot = await createTempDirAsync("openclaw-baileys-postinstall-");
await writeBaileysMediaFile(
packageRoot,