@lofa199419/waha-v2 2.1.0
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/README.md +50 -0
- package/bin/wa +20 -0
- package/bin/wa-adv +9 -0
- package/bin/waha-advanced-entrypoint +12 -0
- package/bin/waha-cli +11 -0
- package/bin/waha_cli.py +549 -0
- package/index.ts +109 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +59 -0
- package/scripts/install-openclaw-extension.mjs +106 -0
- package/skills/waha-v2/SKILL.md +143 -0
- package/skills/waha-v2-onboarding/SKILL.md +146 -0
- package/src/accounts.ts +133 -0
- package/src/channel.ts +823 -0
- package/src/client.ts +585 -0
- package/src/config-schema.ts +342 -0
- package/src/deliver.ts +70 -0
- package/src/gateway.ts +170 -0
- package/src/login.ts +64 -0
- package/src/outbound.ts +84 -0
- package/src/probe.ts +30 -0
- package/src/routes.ts +260 -0
- package/src/runtime.ts +56 -0
- package/src/types.ts +195 -0
- package/src/webhook.ts +841 -0
package/src/outbound.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { ChannelOutboundAdapter, PollInput } from "openclaw/plugin-sdk";
|
|
2
|
+
import { loadWebMedia } from "openclaw/plugin-sdk";
|
|
3
|
+
import { resolveWahaV2Account } from "./accounts.js";
|
|
4
|
+
import { requireWahaV2Client } from "./runtime.js";
|
|
5
|
+
import { WAHA_V2_CHANNEL_ID } from "./types.js";
|
|
6
|
+
|
|
7
|
+
// WhatsApp has a 4096 character limit per message.
|
|
8
|
+
const TEXT_CHUNK_LIMIT = 4096;
|
|
9
|
+
|
|
10
|
+
function resolveMessageId(id: string | undefined, serialized: string | undefined): string {
|
|
11
|
+
return id ?? serialized ?? "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const wahaV2Outbound: ChannelOutboundAdapter = {
|
|
15
|
+
deliveryMode: "gateway",
|
|
16
|
+
chunkerMode: "text",
|
|
17
|
+
textChunkLimit: TEXT_CHUNK_LIMIT,
|
|
18
|
+
pollMaxOptions: 12,
|
|
19
|
+
|
|
20
|
+
async sendText({ cfg, to, text, accountId }) {
|
|
21
|
+
const account = resolveWahaV2Account(cfg, accountId);
|
|
22
|
+
const client = requireWahaV2Client(account.accountId);
|
|
23
|
+
const result = await client.sendText(account.session, to, text);
|
|
24
|
+
return {
|
|
25
|
+
channel: WAHA_V2_CHANNEL_ID,
|
|
26
|
+
messageId: resolveMessageId(result.id, result._serialized),
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
async sendMedia({ cfg, to, text, mediaUrl, accountId }) {
|
|
31
|
+
if (!mediaUrl) {
|
|
32
|
+
throw new Error("waha-v2: mediaUrl is required for media sends");
|
|
33
|
+
}
|
|
34
|
+
const account = resolveWahaV2Account(cfg, accountId);
|
|
35
|
+
const client = requireWahaV2Client(account.accountId);
|
|
36
|
+
|
|
37
|
+
// Fetch media from the URL provided by the agent framework.
|
|
38
|
+
const media = await loadWebMedia(mediaUrl);
|
|
39
|
+
const contentType =
|
|
40
|
+
(media.contentType ?? "application/octet-stream").split(";")[0]?.trim() ??
|
|
41
|
+
"application/octet-stream";
|
|
42
|
+
const filename = media.fileName ?? "file";
|
|
43
|
+
const file = {
|
|
44
|
+
data: media.buffer.toString("base64"),
|
|
45
|
+
mimetype: contentType,
|
|
46
|
+
filename,
|
|
47
|
+
};
|
|
48
|
+
const caption = text?.trim() || undefined;
|
|
49
|
+
|
|
50
|
+
let result;
|
|
51
|
+
if (contentType.startsWith("image/")) {
|
|
52
|
+
result = await client.sendImage(account.session, to, file, caption);
|
|
53
|
+
} else if (contentType.startsWith("video/")) {
|
|
54
|
+
result = await client.sendVideo(account.session, to, file, caption);
|
|
55
|
+
} else if (
|
|
56
|
+
contentType.startsWith("audio/") ||
|
|
57
|
+
filename.endsWith(".ogg") ||
|
|
58
|
+
filename.endsWith(".opus")
|
|
59
|
+
) {
|
|
60
|
+
// Audio/voice notes use the WAHA sendVoice endpoint.
|
|
61
|
+
result = await client.sendVoice(account.session, to, file);
|
|
62
|
+
} else {
|
|
63
|
+
result = await client.sendFile(account.session, to, file, caption);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
channel: WAHA_V2_CHANNEL_ID,
|
|
68
|
+
messageId: resolveMessageId(result.id, result._serialized),
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
async sendPoll({ cfg, to, poll, accountId }) {
|
|
73
|
+
const account = resolveWahaV2Account(cfg, accountId);
|
|
74
|
+
const client = requireWahaV2Client(account.accountId);
|
|
75
|
+
const p = poll as PollInput;
|
|
76
|
+
const result = await client.sendPoll(account.session, to, {
|
|
77
|
+
name: p.question,
|
|
78
|
+
options: p.options,
|
|
79
|
+
// WAHA treats maxSelections > 1 as multi-answer; OpenClaw uses maxSelections.
|
|
80
|
+
multipleAnswers: (p.maxSelections ?? 1) > 1,
|
|
81
|
+
});
|
|
82
|
+
return { messageId: resolveMessageId(result.id, result._serialized) };
|
|
83
|
+
},
|
|
84
|
+
};
|
package/src/probe.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { WahaV2Client } from "./client.js";
|
|
2
|
+
import type { WahaV2ProbeResult } from "./types.js";
|
|
3
|
+
|
|
4
|
+
const WAHA_CONNECTED_STATUSES = new Set(["WORKING", "CONNECTED", "AUTHENTICATED"]);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Probe a WAHA session's live connectivity status.
|
|
8
|
+
* Uses the sessions list endpoint and checks for the target session name.
|
|
9
|
+
*/
|
|
10
|
+
export async function probeWahaV2Session(
|
|
11
|
+
client: WahaV2Client,
|
|
12
|
+
session: string,
|
|
13
|
+
): Promise<WahaV2ProbeResult> {
|
|
14
|
+
try {
|
|
15
|
+
const sessions = await client.listSessions(true);
|
|
16
|
+
const target = session.trim().toLowerCase();
|
|
17
|
+
const found = sessions.find(
|
|
18
|
+
(s) =>
|
|
19
|
+
String(s.name ?? "")
|
|
20
|
+
.trim()
|
|
21
|
+
.toLowerCase() === target,
|
|
22
|
+
);
|
|
23
|
+
const status = String(found?.status ?? "")
|
|
24
|
+
.trim()
|
|
25
|
+
.toUpperCase();
|
|
26
|
+
return { ok: WAHA_CONNECTED_STATUSES.has(status), status: found?.status };
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return { ok: false, error: String(err) };
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/routes.ts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { readJsonBodyWithLimit } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
4
|
+
import { resolveWahaV2Account } from "./accounts.js";
|
|
5
|
+
import { acquireLoginLock, releaseLoginLock, waitForWahaV2Connected } from "./login.js";
|
|
6
|
+
import { probeWahaV2Session } from "./probe.js";
|
|
7
|
+
import { getWahaV2Client, getWahaV2Logger } from "./runtime.js";
|
|
8
|
+
import { WAHA_V2_DEFAULT_ACCOUNT_ID, type WahaV2ProbeResult } from "./types.js";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Route path constants — imported by index.ts when registering routes
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export const WAHA_V2_API_BASE = "/plugins/waha-v2/api" as const;
|
|
15
|
+
export const WAHA_V2_ROUTE_STATUS = `${WAHA_V2_API_BASE}/status` as const;
|
|
16
|
+
export const WAHA_V2_ROUTE_START = `${WAHA_V2_API_BASE}/start` as const;
|
|
17
|
+
export const WAHA_V2_ROUTE_QR = `${WAHA_V2_API_BASE}/qr` as const;
|
|
18
|
+
export const WAHA_V2_ROUTE_REQUEST_CODE = `${WAHA_V2_API_BASE}/request-code` as const;
|
|
19
|
+
export const WAHA_V2_ROUTE_WAIT = `${WAHA_V2_API_BASE}/wait` as const;
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Internal helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
function parseUrl(req: IncomingMessage): URL {
|
|
26
|
+
return new URL(req.url ?? "/", "http://localhost");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function jsonOk(res: ServerResponse, data: unknown): void {
|
|
30
|
+
res.statusCode = 200;
|
|
31
|
+
res.setHeader("Content-Type", "application/json");
|
|
32
|
+
res.end(JSON.stringify(data));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function jsonError(res: ServerResponse, code: number, message: string): void {
|
|
36
|
+
res.statusCode = code;
|
|
37
|
+
res.setHeader("Content-Type", "application/json");
|
|
38
|
+
res.end(JSON.stringify({ ok: false, error: message }));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveAccountIdFromRequest(url: URL, body?: Record<string, unknown>): string {
|
|
42
|
+
return String(url.searchParams.get("accountId") ?? body?.accountId ?? WAHA_V2_DEFAULT_ACCOUNT_ID);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// GET /plugins/waha-v2/api/status
|
|
47
|
+
// Returns the live session status for an account.
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
export async function handleWahaV2StatusRoute(
|
|
51
|
+
req: IncomingMessage,
|
|
52
|
+
res: ServerResponse,
|
|
53
|
+
cfg: OpenClawConfig,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
const url = parseUrl(req);
|
|
56
|
+
const accountId = resolveAccountIdFromRequest(url);
|
|
57
|
+
const account = resolveWahaV2Account(cfg, accountId);
|
|
58
|
+
const client = getWahaV2Client(account.accountId);
|
|
59
|
+
if (!client) {
|
|
60
|
+
return jsonError(res, 503, `gateway not running for account "${accountId}"`);
|
|
61
|
+
}
|
|
62
|
+
const probe = await probeWahaV2Session(client, account.session).catch(
|
|
63
|
+
(err): WahaV2ProbeResult => ({
|
|
64
|
+
ok: false,
|
|
65
|
+
error: String(err),
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
jsonOk(res, {
|
|
69
|
+
ok: true,
|
|
70
|
+
accountId,
|
|
71
|
+
session: account.session,
|
|
72
|
+
connected: probe.ok,
|
|
73
|
+
status: probe.status ?? null,
|
|
74
|
+
error: probe.error ?? null,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// POST /plugins/waha-v2/api/start
|
|
80
|
+
// Creates (if needed) and starts the WAHA session for an account.
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
export async function handleWahaV2StartRoute(
|
|
84
|
+
req: IncomingMessage,
|
|
85
|
+
res: ServerResponse,
|
|
86
|
+
cfg: OpenClawConfig,
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
if (req.method !== "POST") {
|
|
89
|
+
return jsonError(res, 405, "Method Not Allowed");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const body = await readJsonBodyWithLimit(req, {
|
|
93
|
+
maxBytes: 64 * 1024,
|
|
94
|
+
timeoutMs: 5_000,
|
|
95
|
+
emptyObjectOnEmpty: true,
|
|
96
|
+
});
|
|
97
|
+
if (!body.ok) {
|
|
98
|
+
return jsonError(res, 400, body.error ?? "Bad Request");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const url = parseUrl(req);
|
|
102
|
+
const payload = body.value as Record<string, unknown>;
|
|
103
|
+
const accountId = resolveAccountIdFromRequest(url, payload);
|
|
104
|
+
const account = resolveWahaV2Account(cfg, accountId);
|
|
105
|
+
const client = getWahaV2Client(account.accountId);
|
|
106
|
+
if (!client) {
|
|
107
|
+
return jsonError(res, 503, `gateway not running for account "${accountId}"`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
acquireLoginLock(accountId);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
return jsonError(res, 409, String(err));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
// Try starting the existing session; if that fails, create it first (auto-starts).
|
|
118
|
+
await client.startSession(account.session).catch(async () => {
|
|
119
|
+
await client.createSession(account.session);
|
|
120
|
+
});
|
|
121
|
+
const probe = await probeWahaV2Session(client, account.session).catch(() => ({ ok: false }));
|
|
122
|
+
jsonOk(res, {
|
|
123
|
+
ok: true,
|
|
124
|
+
accountId,
|
|
125
|
+
session: account.session,
|
|
126
|
+
started: true,
|
|
127
|
+
connected: probe.ok,
|
|
128
|
+
});
|
|
129
|
+
} catch (err) {
|
|
130
|
+
getWahaV2Logger().warn(`waha-v2 start error for "${accountId}": ${String(err)}`);
|
|
131
|
+
jsonError(res, 500, String(err));
|
|
132
|
+
} finally {
|
|
133
|
+
releaseLoginLock(accountId);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// GET /plugins/waha-v2/api/qr
|
|
139
|
+
// Returns the QR code (base64) for scanning in WhatsApp.
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
export async function handleWahaV2QrRoute(
|
|
143
|
+
req: IncomingMessage,
|
|
144
|
+
res: ServerResponse,
|
|
145
|
+
cfg: OpenClawConfig,
|
|
146
|
+
): Promise<void> {
|
|
147
|
+
const url = parseUrl(req);
|
|
148
|
+
const accountId = resolveAccountIdFromRequest(url);
|
|
149
|
+
const account = resolveWahaV2Account(cfg, accountId);
|
|
150
|
+
const client = getWahaV2Client(account.accountId);
|
|
151
|
+
if (!client) {
|
|
152
|
+
return jsonError(res, 503, `gateway not running for account "${accountId}"`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const qr = await client.getQr(account.session);
|
|
157
|
+
if (!qr?.data) {
|
|
158
|
+
return jsonError(
|
|
159
|
+
res,
|
|
160
|
+
404,
|
|
161
|
+
"QR not available — session may already be connected, not started, or not in QR pairing mode",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
jsonOk(res, { ok: true, accountId, session: account.session, data: qr.data });
|
|
165
|
+
} catch (err) {
|
|
166
|
+
getWahaV2Logger().warn(`waha-v2 qr error for "${accountId}": ${String(err)}`);
|
|
167
|
+
jsonError(res, 500, String(err));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// POST /plugins/waha-v2/api/request-code
|
|
173
|
+
// Requests a pairing code (auth code) for the given phone number.
|
|
174
|
+
// Body: { accountId?: string, phoneNumber: string }
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
export async function handleWahaV2RequestCodeRoute(
|
|
178
|
+
req: IncomingMessage,
|
|
179
|
+
res: ServerResponse,
|
|
180
|
+
cfg: OpenClawConfig,
|
|
181
|
+
): Promise<void> {
|
|
182
|
+
if (req.method !== "POST") {
|
|
183
|
+
return jsonError(res, 405, "Method Not Allowed");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const body = await readJsonBodyWithLimit(req, {
|
|
187
|
+
maxBytes: 64 * 1024,
|
|
188
|
+
timeoutMs: 5_000,
|
|
189
|
+
emptyObjectOnEmpty: true,
|
|
190
|
+
});
|
|
191
|
+
if (!body.ok) {
|
|
192
|
+
return jsonError(res, 400, body.error ?? "Bad Request");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const url = parseUrl(req);
|
|
196
|
+
const payload = body.value as Record<string, unknown>;
|
|
197
|
+
const accountId = resolveAccountIdFromRequest(url, payload);
|
|
198
|
+
const phoneNumber = String(payload.phoneNumber ?? "").trim();
|
|
199
|
+
if (!phoneNumber) {
|
|
200
|
+
return jsonError(res, 400, "phoneNumber is required");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const account = resolveWahaV2Account(cfg, accountId);
|
|
204
|
+
const client = getWahaV2Client(account.accountId);
|
|
205
|
+
if (!client) {
|
|
206
|
+
return jsonError(res, 503, `gateway not running for account "${accountId}"`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
acquireLoginLock(accountId);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
return jsonError(res, 409, String(err));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const result = await client.requestCode(account.session, phoneNumber);
|
|
217
|
+
jsonOk(res, {
|
|
218
|
+
ok: true,
|
|
219
|
+
accountId,
|
|
220
|
+
session: account.session,
|
|
221
|
+
code: result?.code ?? null,
|
|
222
|
+
});
|
|
223
|
+
} catch (err) {
|
|
224
|
+
getWahaV2Logger().warn(`waha-v2 request-code error for "${accountId}": ${String(err)}`);
|
|
225
|
+
jsonError(res, 500, String(err));
|
|
226
|
+
} finally {
|
|
227
|
+
releaseLoginLock(accountId);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// GET /plugins/waha-v2/api/wait
|
|
233
|
+
// Long-polls until the session is connected (or timeout). Max 120 s.
|
|
234
|
+
// Query params: accountId?, timeout? (ms, default 60000, max 120000)
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
export async function handleWahaV2WaitRoute(
|
|
238
|
+
req: IncomingMessage,
|
|
239
|
+
res: ServerResponse,
|
|
240
|
+
cfg: OpenClawConfig,
|
|
241
|
+
): Promise<void> {
|
|
242
|
+
const url = parseUrl(req);
|
|
243
|
+
const accountId = resolveAccountIdFromRequest(url);
|
|
244
|
+
const timeoutMs = Math.min(Number(url.searchParams.get("timeout") ?? 60_000), 120_000);
|
|
245
|
+
|
|
246
|
+
const account = resolveWahaV2Account(cfg, accountId);
|
|
247
|
+
const client = getWahaV2Client(account.accountId);
|
|
248
|
+
if (!client) {
|
|
249
|
+
return jsonError(res, 503, `gateway not running for account "${accountId}"`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const result = await waitForWahaV2Connected(client, account.session, timeoutMs);
|
|
253
|
+
jsonOk(res, {
|
|
254
|
+
ok: result.ok,
|
|
255
|
+
accountId,
|
|
256
|
+
session: account.session,
|
|
257
|
+
connected: result.ok,
|
|
258
|
+
status: result.status ?? null,
|
|
259
|
+
});
|
|
260
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { PluginRuntime, RuntimeLogger } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { WahaV2Client } from "./client.js";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Plugin-level runtime + logger (set once at plugin registration)
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
let pluginRuntime: PluginRuntime | null = null;
|
|
9
|
+
let pluginLogger: RuntimeLogger | null = null;
|
|
10
|
+
|
|
11
|
+
export function setWahaV2Runtime(runtime: PluginRuntime, logger: RuntimeLogger): void {
|
|
12
|
+
pluginRuntime = runtime;
|
|
13
|
+
pluginLogger = logger;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getWahaV2Runtime(): PluginRuntime {
|
|
17
|
+
if (!pluginRuntime) {
|
|
18
|
+
throw new Error("waha-v2: runtime not initialized");
|
|
19
|
+
}
|
|
20
|
+
return pluginRuntime;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getWahaV2Logger(): RuntimeLogger {
|
|
24
|
+
if (!pluginLogger) {
|
|
25
|
+
throw new Error("waha-v2: logger not initialized");
|
|
26
|
+
}
|
|
27
|
+
return pluginLogger;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Per-account client map (populated by gateway.startAccount)
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
const accountClients = new Map<string, WahaV2Client>();
|
|
35
|
+
|
|
36
|
+
export function setWahaV2Client(accountId: string, client: WahaV2Client): void {
|
|
37
|
+
accountClients.set(accountId, client);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function removeWahaV2Client(accountId: string): void {
|
|
41
|
+
accountClients.delete(accountId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getWahaV2Client(accountId: string): WahaV2Client | undefined {
|
|
45
|
+
return accountClients.get(accountId);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function requireWahaV2Client(accountId: string): WahaV2Client {
|
|
49
|
+
const client = accountClients.get(accountId);
|
|
50
|
+
if (!client) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`waha-v2: no active client for account "${accountId}" — gateway may not be running`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return client;
|
|
56
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Config types — mirrors cfg.channels["waha-v2"] in OpenClaw config file
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Typing indicator and message-chunking config for the WAHA v2 channel.
|
|
7
|
+
* All fields are optional; defaults produce natural human-like behaviour.
|
|
8
|
+
*/
|
|
9
|
+
export type WahaV2TypingConfig = {
|
|
10
|
+
/** Show "typing…" indicator while the agent composes a reply. Default: true */
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
/** Split long replies into multiple messages at paragraph breaks. Default: true */
|
|
13
|
+
chunking?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Simulated typing speed in characters per second, used to compute
|
|
16
|
+
* the per-chunk delay: delay = clamp(length / charsPerSecond * 1000, min, max).
|
|
17
|
+
* Default: 12 (≈ 2-3 words/s).
|
|
18
|
+
*/
|
|
19
|
+
charsPerSecond?: number;
|
|
20
|
+
/** Maximum characters per message chunk before splitting further. Default: 1500 */
|
|
21
|
+
maxChunkLength?: number;
|
|
22
|
+
/** Emit detailed delivery/typing timing logs for troubleshooting. Default: false */
|
|
23
|
+
debug?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type WahaV2LabelRule = {
|
|
27
|
+
/** Label name or ID to match (case-insensitive for name). */
|
|
28
|
+
match: string;
|
|
29
|
+
/** Instruction injected into BodyForAgent when the rule matches. */
|
|
30
|
+
instruction: string;
|
|
31
|
+
/** Match by label `name` (default) or `id`. */
|
|
32
|
+
by?: "name" | "id";
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type WahaV2LabelRoutingConfig = {
|
|
36
|
+
/** Enables label-based instruction injection for inbound messages. */
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
/** Fallback instruction when no label rule matches. */
|
|
39
|
+
defaultInstruction?: string;
|
|
40
|
+
/** Ordered list of matching rules; first match wins. */
|
|
41
|
+
rules?: WahaV2LabelRule[];
|
|
42
|
+
/** Cache TTL (seconds) for chat label lookups. Default: 120. */
|
|
43
|
+
cacheTtlSec?: number;
|
|
44
|
+
/** Auto-assign personal label when message appears non-business. */
|
|
45
|
+
autoAssignPersonal?: boolean;
|
|
46
|
+
/** Target label name or id used for non-business auto-assignment. Default: "personal". */
|
|
47
|
+
personalLabel?: string;
|
|
48
|
+
/** Labels that pause bot replies while present on the chat. */
|
|
49
|
+
pauseLabels?: string[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Per-group action enable/disable flags for the WAHA v2 channel.
|
|
54
|
+
* Each key controls a category of agent actions; defaults to `true` (enabled).
|
|
55
|
+
*/
|
|
56
|
+
export type WahaV2ActionConfig = {
|
|
57
|
+
/** start/stop/QR/logout session management actions. */
|
|
58
|
+
sessionManagement?: boolean;
|
|
59
|
+
/** Message-level actions: react, seen, location, contact, forward, star, edit, delete, pin. */
|
|
60
|
+
messaging?: boolean;
|
|
61
|
+
/** Chat list/history/archive/delete actions. */
|
|
62
|
+
chats?: boolean;
|
|
63
|
+
/** Contact lookup/block/unblock actions. */
|
|
64
|
+
contacts?: boolean;
|
|
65
|
+
/** Group create/manage/participant/admin actions. */
|
|
66
|
+
groups?: boolean;
|
|
67
|
+
/** WhatsApp Status (Stories) send/delete actions. */
|
|
68
|
+
status?: boolean;
|
|
69
|
+
/** WhatsApp Channels (Newsletters) actions. */
|
|
70
|
+
waChannels?: boolean;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type WahaV2AccountConfig = {
|
|
74
|
+
enabled?: boolean;
|
|
75
|
+
name?: string;
|
|
76
|
+
baseUrl?: string;
|
|
77
|
+
apiKey?: string;
|
|
78
|
+
session?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Full public URL that WAHA should POST inbound events to.
|
|
81
|
+
* Example: "https://your-openclaw-host/webhooks/waha-v2"
|
|
82
|
+
* If set, the gateway registers it with the WAHA session on startup.
|
|
83
|
+
*/
|
|
84
|
+
webhookUrl?: string;
|
|
85
|
+
dmPolicy?: string;
|
|
86
|
+
groupPolicy?: string;
|
|
87
|
+
debounceMs?: number;
|
|
88
|
+
allowFrom?: string[];
|
|
89
|
+
allowGroups?: string[];
|
|
90
|
+
/** Pause the bot for a chat when the WhatsApp owner sends any message in it. */
|
|
91
|
+
pauseOnOwnerMessage?: boolean;
|
|
92
|
+
/** Owner-authored message texts that pause the bot immediately, e.g. ["//"]. */
|
|
93
|
+
ownerPauseWords?: string[];
|
|
94
|
+
/** Owner-authored message texts that resume a manually paused chat, e.g. ["///"]. */
|
|
95
|
+
ownerResumeWords?: string[];
|
|
96
|
+
actions?: WahaV2ActionConfig;
|
|
97
|
+
typing?: WahaV2TypingConfig;
|
|
98
|
+
labelRouting?: WahaV2LabelRoutingConfig;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export type WahaV2RootConfig = WahaV2AccountConfig & {
|
|
102
|
+
accounts?: Record<string, WahaV2AccountConfig>;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const WAHA_V2_CHANNEL_ID = "waha-v2" as const;
|
|
106
|
+
export const WAHA_V2_DEFAULT_ACCOUNT_ID = "default" as const;
|
|
107
|
+
/**
|
|
108
|
+
* Base path for inbound WAHA webhooks.
|
|
109
|
+
* Per-account paths are: `${WAHA_V2_WEBHOOK_BASE}/${accountId}`
|
|
110
|
+
* e.g. /webhooks/waha-v2/personal, /webhooks/waha-v2/business
|
|
111
|
+
*/
|
|
112
|
+
export const WAHA_V2_WEBHOOK_BASE = "/webhooks/waha-v2" as const;
|
|
113
|
+
/** @deprecated Use WAHA_V2_WEBHOOK_BASE */
|
|
114
|
+
export const WAHA_V2_WEBHOOK_PATH = WAHA_V2_WEBHOOK_BASE;
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Resolved account — what the channel plugin operates on at runtime
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
export type ResolvedWahaV2Account = {
|
|
121
|
+
accountId: string;
|
|
122
|
+
name?: string;
|
|
123
|
+
baseUrl: string;
|
|
124
|
+
apiKey: string;
|
|
125
|
+
session: string;
|
|
126
|
+
/** Full public URL for WAHA to POST inbound events to. Optional — user must set this. */
|
|
127
|
+
webhookUrl?: string;
|
|
128
|
+
enabled: boolean;
|
|
129
|
+
dmPolicy?: string;
|
|
130
|
+
groupPolicy?: string;
|
|
131
|
+
debounceMs?: number;
|
|
132
|
+
allowFrom?: string[];
|
|
133
|
+
allowGroups?: string[];
|
|
134
|
+
pauseOnOwnerMessage?: boolean;
|
|
135
|
+
ownerPauseWords?: string[];
|
|
136
|
+
ownerResumeWords?: string[];
|
|
137
|
+
typing?: WahaV2TypingConfig;
|
|
138
|
+
labelRouting?: WahaV2LabelRoutingConfig;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// WAHA webhook event payload — structure common across WEBJS / NOWEB / GOWS
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
export type WahaV2MediaInfo = {
|
|
146
|
+
url?: string;
|
|
147
|
+
mimetype?: string;
|
|
148
|
+
filename?: string;
|
|
149
|
+
/** Base64-encoded data (some engines embed it directly). */
|
|
150
|
+
data?: string;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export type WahaV2WebhookPayload = {
|
|
154
|
+
id?: string;
|
|
155
|
+
from?: string;
|
|
156
|
+
to?: string;
|
|
157
|
+
body?: string;
|
|
158
|
+
type?: string;
|
|
159
|
+
fromMe?: boolean;
|
|
160
|
+
isGroupMsg?: boolean;
|
|
161
|
+
hasMedia?: boolean;
|
|
162
|
+
/** Can be a plain string or WEBJS-style `{ _serialized: "..." }` object. */
|
|
163
|
+
chatId?: string | { _serialized?: string };
|
|
164
|
+
timestamp?: number;
|
|
165
|
+
/** Present when type is image / video / audio / document / ptt. */
|
|
166
|
+
media?: WahaV2MediaInfo;
|
|
167
|
+
/** Some engines embed media info under _data. */
|
|
168
|
+
_data?: { media?: WahaV2MediaInfo; body?: string } & Record<string, unknown>;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export type WahaV2WebhookEvent = {
|
|
172
|
+
event: string;
|
|
173
|
+
session: string;
|
|
174
|
+
payload?: WahaV2WebhookPayload;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Typed API responses (replaces waha-node's pervasive `any`)
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
export type WahaV2MessageResult = {
|
|
182
|
+
id?: string;
|
|
183
|
+
_serialized?: string;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export type WahaV2SessionInfo = {
|
|
187
|
+
name?: string;
|
|
188
|
+
status?: string;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export type WahaV2ProbeResult = {
|
|
192
|
+
ok: boolean;
|
|
193
|
+
status?: string;
|
|
194
|
+
error?: string;
|
|
195
|
+
};
|