@lofa199419/waha-v2 2026.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts ADDED
@@ -0,0 +1,92 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3
+ import { wahaV2Plugin } from "./src/channel.js";
4
+ import { setWahaV2Runtime } from "./src/runtime.js";
5
+ import { handleWahaV2WebhookRequest } from "./src/webhook.js";
6
+ import {
7
+ handleWahaV2QrRoute,
8
+ handleWahaV2RequestCodeRoute,
9
+ handleWahaV2StartRoute,
10
+ handleWahaV2StatusRoute,
11
+ handleWahaV2WaitRoute,
12
+ WAHA_V2_ROUTE_QR,
13
+ WAHA_V2_ROUTE_REQUEST_CODE,
14
+ WAHA_V2_ROUTE_START,
15
+ WAHA_V2_ROUTE_STATUS,
16
+ WAHA_V2_ROUTE_WAIT,
17
+ } from "./src/routes.js";
18
+ import { WAHA_V2_WEBHOOK_BASE } from "./src/types.js";
19
+
20
+ const plugin = {
21
+ id: "waha-v2",
22
+ name: "WAHA v2 (WhatsApp HTTP API)",
23
+ configSchema: emptyPluginConfigSchema(),
24
+ description:
25
+ "Independent WAHA WhatsApp HTTP API channel plugin. " +
26
+ "Uses waha-node for outbound; OpenClaw plugin-sdk for inbound, routing, and policies.",
27
+
28
+ register(api: OpenClawPluginApi) {
29
+ // Store the runtime and logger for use in the webhook handler, gateway, and routes.
30
+ setWahaV2Runtime(api.runtime, api.logger);
31
+
32
+ // Register the channel (config, gateway, outbound, status, etc.).
33
+ api.registerChannel({ plugin: wahaV2Plugin });
34
+
35
+ // Inbound webhook — matches both:
36
+ // /webhooks/waha-v2 (legacy, routes by session name in payload)
37
+ // /webhooks/waha-v2/{accountId} (preferred, unambiguous — accountId from path)
38
+ // Using registerHttpHandler so we can capture the dynamic {accountId} segment.
39
+ api.registerHttpHandler(async (req, res) => {
40
+ const url = req.url ?? "";
41
+ if (!url.startsWith(WAHA_V2_WEBHOOK_BASE)) return false;
42
+ // Extract optional accountId from the path suffix.
43
+ const suffix = url.slice(WAHA_V2_WEBHOOK_BASE.length).split("?")[0] ?? "";
44
+ const accountId = suffix.startsWith("/") ? suffix.slice(1) || undefined : undefined;
45
+ await handleWahaV2WebhookRequest(req, res, api.runtime.config.loadConfig(), accountId);
46
+ return true;
47
+ });
48
+
49
+ // Session management API routes — used by UI, CLI, and setup flows.
50
+ api.registerHttpRoute({
51
+ path: WAHA_V2_ROUTE_STATUS,
52
+ handler: async (req, res) => {
53
+ await handleWahaV2StatusRoute(req, res, api.runtime.config.loadConfig());
54
+ },
55
+ });
56
+
57
+ api.registerHttpRoute({
58
+ path: WAHA_V2_ROUTE_START,
59
+ handler: async (req, res) => {
60
+ await handleWahaV2StartRoute(req, res, api.runtime.config.loadConfig());
61
+ },
62
+ });
63
+
64
+ api.registerHttpRoute({
65
+ path: WAHA_V2_ROUTE_QR,
66
+ handler: async (req, res) => {
67
+ await handleWahaV2QrRoute(req, res, api.runtime.config.loadConfig());
68
+ },
69
+ });
70
+
71
+ api.registerHttpRoute({
72
+ path: WAHA_V2_ROUTE_REQUEST_CODE,
73
+ handler: async (req, res) => {
74
+ await handleWahaV2RequestCodeRoute(req, res, api.runtime.config.loadConfig());
75
+ },
76
+ });
77
+
78
+ api.registerHttpRoute({
79
+ path: WAHA_V2_ROUTE_WAIT,
80
+ handler: async (req, res) => {
81
+ await handleWahaV2WaitRoute(req, res, api.runtime.config.loadConfig());
82
+ },
83
+ });
84
+
85
+ api.logger.info(
86
+ `waha-v2: registered channel + webhook ${WAHA_V2_WEBHOOK_BASE}/{accountId} + ` +
87
+ `session API routes (status, start, qr, request-code, wait)`,
88
+ );
89
+ },
90
+ };
91
+
92
+ export default plugin;
Binary file
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "waha-v2",
3
+ "channels": ["waha-v2"],
4
+ "skills": ["skills/waha-v2"],
5
+ "configSchema": {
6
+ "type": "object",
7
+ "additionalProperties": true,
8
+ "properties": {}
9
+ }
10
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@lofa199419/waha-v2",
3
+ "version": "2026.3.2",
4
+ "private": false,
5
+ "description": "OpenClaw WAHA v2 channel plugin — independent WhatsApp HTTP API integration",
6
+ "type": "module",
7
+ "dependencies": {
8
+ "axios": "1.6.0",
9
+ "waha-node": "1.0.0"
10
+ },
11
+ "devDependencies": {
12
+ "openclaw": "workspace:*"
13
+ },
14
+ "openclaw": {
15
+ "extensions": [
16
+ "./index.ts"
17
+ ],
18
+ "channel": {
19
+ "id": "waha-v2",
20
+ "label": "WAHA",
21
+ "selectionLabel": "WAHA v2 (plugin)",
22
+ "detailLabel": "WAHA v2",
23
+ "docsPath": "/channels/whatsapp",
24
+ "docsLabel": "whatsapp",
25
+ "blurb": "Independent WAHA WhatsApp HTTP API plugin — no Baileys dependency.",
26
+ "order": 13,
27
+ "systemImage": "qrcode"
28
+ },
29
+ "install": {
30
+ "npmSpec": "@lofa199419/waha-v2",
31
+ "localPath": "extensions/waha-v2",
32
+ "defaultChoice": "npm"
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,183 @@
1
+ ---
2
+ name: waha-v2
3
+ description: "Full WhatsApp agent via WAHA HTTP API — send messages, read chat history, manage contacts, groups, status stories, and WA channels."
4
+ metadata: { "openclaw": { "emoji": "💬", "requires": { "config": ["channels.waha-v2"] } } }
5
+ allowed-tools: ["message"]
6
+ ---
7
+
8
+ # WAHA v2 — Full WhatsApp Agent
9
+
10
+ Use the `message` tool with `channel: "waha-v2"`. You have complete WhatsApp access: messaging, chat history, contacts, groups, status stories, and WhatsApp Channels.
11
+
12
+ ## Musts
13
+
14
+ - Always set `channel: "waha-v2"`.
15
+ - Phone numbers as JIDs: `"<digits>@c.us"` for DMs, `"<digits>@g.us"` for groups.
16
+ - Strip all non-digits before appending `@c.us` (e.g. `+1 (555) 000-1234` → `"15550001234@c.us"`).
17
+ - Never send streaming or partial text to WhatsApp — only send the final, complete reply.
18
+ - Multi-account: pass `accountId` when the user has more than one WAHA instance configured.
19
+
20
+ ## Session States
21
+
22
+ | State | Meaning |
23
+ |---|---|
24
+ | `WORKING` / `CONNECTED` | Ready — go ahead |
25
+ | `STOPPED` / `FAILED` | Call `start-session` first |
26
+ | `SCAN_QR_CODE` / `PAIRING` | Waiting for user to scan QR or enter code |
27
+ | `CONNECTING` | Starting up — wait briefly |
28
+
29
+ ---
30
+
31
+ ## Sending Messages
32
+
33
+ ### Text
34
+ ```json
35
+ { "action": "send", "channel": "waha-v2", "to": "15550001234@c.us", "message": "Hello!" }
36
+ ```
37
+
38
+ ### Media (image / video / audio / file)
39
+ ```json
40
+ { "action": "send", "channel": "waha-v2", "to": "15550001234@c.us", "media": "https://…/file.pdf", "message": "Your report" }
41
+ ```
42
+
43
+ ### Poll
44
+ ```json
45
+ { "action": "poll", "channel": "waha-v2", "to": "15550001234@c.us", "pollQuestion": "Which day?", "pollOption": ["Mon","Wed","Fri"], "pollMulti": false }
46
+ ```
47
+
48
+ ### Location
49
+ ```json
50
+ { "action": "send-location", "channel": "waha-v2", "to": "15550001234@c.us", "latitude": 37.77, "longitude": -122.41, "title": "SF Office" }
51
+ ```
52
+
53
+ ### Contact card
54
+ ```json
55
+ { "action": "send-contact", "channel": "waha-v2", "to": "15550001234@c.us", "contacts": [{ "vcard": "BEGIN:VCARD\nVERSION:3.0\nFN:Alice\nTEL:+14155559999\nEND:VCARD" }] }
56
+ ```
57
+
58
+ ### Forward a message
59
+ ```json
60
+ { "action": "forward-message", "channel": "waha-v2", "to": "15550001234@c.us", "messageId": "<id>" }
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Message Management
66
+
67
+ ```json
68
+ { "action": "react", "channel": "waha-v2", "to": "15550001234@c.us", "messageId": "<id>", "emoji": "👍" }
69
+ { "action": "send-seen", "channel": "waha-v2", "to": "15550001234@c.us" }
70
+ { "action": "edit", "channel": "waha-v2", "to": "15550001234@c.us", "messageId": "<id>", "text": "corrected" }
71
+ { "action": "delete", "channel": "waha-v2", "to": "15550001234@c.us", "messageId": "<id>" }
72
+ { "action": "pin", "channel": "waha-v2", "to": "15550001234@c.us", "messageId": "<id>" }
73
+ { "action": "unpin", "channel": "waha-v2", "to": "15550001234@c.us", "messageId": "<id>" }
74
+ { "action": "star-message", "channel": "waha-v2", "to": "15550001234@c.us", "messageId": "<id>" }
75
+ { "action": "unstar-message", "channel": "waha-v2", "to": "15550001234@c.us", "messageId": "<id>" }
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Reading Chat History
81
+
82
+ ```json
83
+ { "action": "list-chats", "channel": "waha-v2", "limit": 50 }
84
+ { "action": "get-chat-messages", "channel": "waha-v2", "to": "15550001234@c.us", "limit": 30 }
85
+ { "action": "get-message", "channel": "waha-v2", "to": "15550001234@c.us", "messageId": "<id>" }
86
+ ```
87
+
88
+ ## Chat Management
89
+
90
+ ```json
91
+ { "action": "archive-chat", "channel": "waha-v2", "to": "15550001234@c.us" }
92
+ { "action": "unarchive-chat", "channel": "waha-v2", "to": "15550001234@c.us" }
93
+ { "action": "delete-chat", "channel": "waha-v2", "to": "15550001234@c.us" }
94
+ { "action": "mark-chat-unread", "channel": "waha-v2", "to": "15550001234@c.us" }
95
+ ```
96
+
97
+ ---
98
+
99
+ ## Contacts
100
+
101
+ ```json
102
+ { "action": "list-contacts", "channel": "waha-v2", "limit": 100 }
103
+ { "action": "get-contact", "channel": "waha-v2", "contactId": "15550001234@c.us" }
104
+ { "action": "check-contact", "channel": "waha-v2", "phone": "15550001234" }
105
+ { "action": "block-contact", "channel": "waha-v2", "to": "15550001234@c.us" }
106
+ { "action": "unblock-contact", "channel": "waha-v2", "to": "15550001234@c.us" }
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Groups
112
+
113
+ ```json
114
+ { "action": "list-groups", "channel": "waha-v2" }
115
+ { "action": "get-group", "channel": "waha-v2", "groupId": "120363012345@g.us" }
116
+ { "action": "create-group", "channel": "waha-v2", "subject": "Team Sync", "participants": ["15550001234@c.us"] }
117
+ { "action": "get-group-participants", "channel": "waha-v2", "groupId": "120363012345@g.us" }
118
+ { "action": "add-group-participants", "channel": "waha-v2", "groupId": "120363012345@g.us", "participants": ["15550001234@c.us"] }
119
+ { "action": "remove-group-participants", "channel": "waha-v2", "groupId": "120363012345@g.us", "participants": ["15550001234@c.us"] }
120
+ { "action": "promote-group-admin", "channel": "waha-v2", "groupId": "120363012345@g.us", "participants": ["15550001234@c.us"] }
121
+ { "action": "demote-group-admin", "channel": "waha-v2", "groupId": "120363012345@g.us", "participants": ["15550001234@c.us"] }
122
+ { "action": "renameGroup", "channel": "waha-v2", "groupId": "120363012345@g.us", "name": "New Name" }
123
+ { "action": "update-group-description", "channel": "waha-v2", "groupId": "120363012345@g.us", "description": "Weekly standups" }
124
+ { "action": "get-group-invite-code", "channel": "waha-v2", "groupId": "120363012345@g.us" }
125
+ { "action": "revoke-group-invite-code", "channel": "waha-v2", "groupId": "120363012345@g.us" }
126
+ { "action": "leaveGroup", "channel": "waha-v2", "groupId": "120363012345@g.us" }
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Status / Stories
132
+
133
+ ```json
134
+ { "action": "send-status", "channel": "waha-v2", "text": "Good morning! 🌅" }
135
+ { "action": "delete-status", "channel": "waha-v2", "messageId": "<statusMessageId>" }
136
+ ```
137
+
138
+ ---
139
+
140
+ ## WhatsApp Channels (Newsletters)
141
+
142
+ ```json
143
+ { "action": "list-wa-channels", "channel": "waha-v2" }
144
+ { "action": "get-wa-channel", "channel": "waha-v2", "channelId": "<id>" }
145
+ { "action": "create-wa-channel", "channel": "waha-v2", "name": "My Channel", "description": "Daily updates" }
146
+ { "action": "get-wa-channel-messages", "channel": "waha-v2", "channelId": "<id>", "limit": 20 }
147
+ { "action": "delete-wa-channel", "channel": "waha-v2", "channelId": "<id>" }
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Session Management & Login Flow
153
+
154
+ ### Full QR login sequence
155
+ 1. `start-session` — if `alreadyConnected: true`, done.
156
+ 2. `get-qr` — returns `{ data }` base64 PNG; show to user to scan in WhatsApp → Linked Devices.
157
+ 3. User scans, then call `start-session` again to confirm `connected: true`.
158
+
159
+ ### Pairing code (no phone camera)
160
+ 1. `start-session`
161
+ 2. `request-code` with `phoneNumber: "15550001234"` — returns 8-digit code.
162
+ 3. User enters code in WhatsApp → Linked Devices → Link with phone number.
163
+ 4. `start-session` to confirm.
164
+
165
+ ```json
166
+ { "action": "logout-session", "channel": "waha-v2" }
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Multi-Account
172
+
173
+ ```json
174
+ { "action": "send", "channel": "waha-v2", "accountId": "secondary", "to": "15550001234@c.us", "message": "Hi" }
175
+ ```
176
+
177
+ Each account gets its own webhook: `{webhookUrl}/{accountId}`.
178
+
179
+ ---
180
+
181
+ ## WhatsApp Markdown
182
+
183
+ `*bold*` `_italic_` `~strikethrough~` `` `mono` `` — no HTML, no tables.
@@ -0,0 +1,117 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
2
+ import {
3
+ WAHA_V2_CHANNEL_ID,
4
+ WAHA_V2_DEFAULT_ACCOUNT_ID,
5
+ type ResolvedWahaV2Account,
6
+ type WahaV2AccountConfig,
7
+ type WahaV2RootConfig,
8
+ } from "./types.js";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Config extraction
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export function readWahaV2Root(cfg: OpenClawConfig): WahaV2RootConfig {
15
+ const channels = (cfg as Record<string, unknown>).channels;
16
+ if (!channels || typeof channels !== "object" || Array.isArray(channels)) {
17
+ return {};
18
+ }
19
+ const root = (channels as Record<string, unknown>)[WAHA_V2_CHANNEL_ID];
20
+ if (!root || typeof root !== "object" || Array.isArray(root)) {
21
+ return {};
22
+ }
23
+ return root as WahaV2RootConfig;
24
+ }
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Account list
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export function listWahaV2AccountIds(cfg: OpenClawConfig): string[] {
31
+ const root = readWahaV2Root(cfg);
32
+ const ids = Object.keys(root.accounts ?? {}).filter(Boolean);
33
+ // If no named accounts are configured, expose a single implicit "default" account.
34
+ return ids.length > 0 ? ids.toSorted((a, b) => a.localeCompare(b)) : [WAHA_V2_DEFAULT_ACCOUNT_ID];
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Account resolution — account-level fields override root-level
39
+ // ---------------------------------------------------------------------------
40
+
41
+ export function resolveWahaV2Account(
42
+ cfg: OpenClawConfig,
43
+ accountId?: string | null,
44
+ ): ResolvedWahaV2Account {
45
+ const resolvedId = accountId?.trim() || WAHA_V2_DEFAULT_ACCOUNT_ID;
46
+ const root = readWahaV2Root(cfg);
47
+ // Merge: root provides defaults; per-account config overrides.
48
+ const account: WahaV2AccountConfig =
49
+ resolvedId === WAHA_V2_DEFAULT_ACCOUNT_ID
50
+ ? root
51
+ : { ...root, ...(root.accounts?.[resolvedId] ?? {}) };
52
+
53
+ return {
54
+ accountId: resolvedId,
55
+ name: account.name?.trim() || undefined,
56
+ baseUrl: account.baseUrl?.trim() || "http://localhost:3000",
57
+ apiKey: account.apiKey?.trim() || "",
58
+ session: account.session?.trim() || "default",
59
+ webhookUrl: account.webhookUrl?.trim() || undefined,
60
+ enabled: account.enabled !== false,
61
+ dmPolicy: account.dmPolicy,
62
+ allowFrom: account.allowFrom,
63
+ };
64
+ }
65
+
66
+ /** Find the account whose session name matches the given WAHA session string. */
67
+ export function resolveWahaV2AccountBySession(
68
+ cfg: OpenClawConfig,
69
+ session: string,
70
+ ): ResolvedWahaV2Account | undefined {
71
+ for (const accountId of listWahaV2AccountIds(cfg)) {
72
+ const account = resolveWahaV2Account(cfg, accountId);
73
+ if (account.session === session.trim()) {
74
+ return account;
75
+ }
76
+ }
77
+ return undefined;
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Config mutation helpers (used by ChannelConfigAdapter)
82
+ // ---------------------------------------------------------------------------
83
+
84
+ export function setWahaV2ChannelConfig(
85
+ cfg: OpenClawConfig,
86
+ patch: Partial<WahaV2RootConfig>,
87
+ ): OpenClawConfig {
88
+ const base = cfg as Record<string, unknown>;
89
+ const channels = { ...((base.channels as Record<string, unknown>) ?? {}) };
90
+ const current = (channels[WAHA_V2_CHANNEL_ID] ?? {}) as Record<string, unknown>;
91
+ channels[WAHA_V2_CHANNEL_ID] = { ...current, ...patch };
92
+ return { ...base, channels } as OpenClawConfig;
93
+ }
94
+
95
+ export function setWahaV2AccountEnabled(
96
+ cfg: OpenClawConfig,
97
+ accountId: string,
98
+ enabled: boolean,
99
+ ): OpenClawConfig {
100
+ const root = readWahaV2Root(cfg);
101
+ if (accountId === WAHA_V2_DEFAULT_ACCOUNT_ID && !root.accounts?.[accountId]) {
102
+ // Single-account mode — set enabled on root.
103
+ return setWahaV2ChannelConfig(cfg, { ...root, enabled });
104
+ }
105
+ const accounts = { ...(root.accounts ?? {}), [accountId]: { ...(root.accounts?.[accountId] ?? {}), enabled } };
106
+ return setWahaV2ChannelConfig(cfg, { ...root, accounts });
107
+ }
108
+
109
+ export function deleteWahaV2Account(cfg: OpenClawConfig, accountId: string): OpenClawConfig {
110
+ const root = readWahaV2Root(cfg);
111
+ if (!root.accounts?.[accountId]) {
112
+ return cfg;
113
+ }
114
+ const accounts = { ...root.accounts };
115
+ delete accounts[accountId];
116
+ return setWahaV2ChannelConfig(cfg, { ...root, accounts: Object.keys(accounts).length > 0 ? accounts : undefined });
117
+ }