@laburen/openclaw-plugin-whatsapp-api 0.4.0 → 0.6.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/index.js +163 -5
- package/openclaw.plugin.json +8 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -419,6 +419,48 @@ async function sendWhatsAppApiMedia(params) {
|
|
|
419
419
|
}
|
|
420
420
|
});
|
|
421
421
|
}
|
|
422
|
+
/**
|
|
423
|
+
* Marks a message as read in the WhatsApp Cloud API.
|
|
424
|
+
*
|
|
425
|
+
* @param params.messageId - The WhatsApp message ID to mark as read
|
|
426
|
+
* @param params.account - Resolved account for Graph auth
|
|
427
|
+
* @returns Graph message id (usually just "unknown" or empty for read marks)
|
|
428
|
+
*/
|
|
429
|
+
async function sendWhatsAppApiReadMark(params) {
|
|
430
|
+
const { phoneNumberId, accessToken } = resolveGraphAuth(params.account);
|
|
431
|
+
return await sendWithRetry({
|
|
432
|
+
endpoint: `https://graph.facebook.com/${params.account.outboundApiVersion}/${phoneNumberId}/messages`,
|
|
433
|
+
account: params.account,
|
|
434
|
+
accessToken,
|
|
435
|
+
payload: {
|
|
436
|
+
messaging_product: "whatsapp",
|
|
437
|
+
status: "read",
|
|
438
|
+
message_id: params.messageId
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Shows/hides the typing indicator in the WhatsApp Cloud API.
|
|
444
|
+
*
|
|
445
|
+
* @param params.to - Recipient WhatsApp ID
|
|
446
|
+
* @param params.typing - `true` for "typing_on", `false` for "typing_off"
|
|
447
|
+
* @param params.account - Resolved account for Graph auth
|
|
448
|
+
* @returns Graph message id
|
|
449
|
+
*/
|
|
450
|
+
async function sendWhatsAppApiTypingIndicator(params) {
|
|
451
|
+
const { phoneNumberId, accessToken } = resolveGraphAuth(params.account);
|
|
452
|
+
return await sendWithRetry({
|
|
453
|
+
endpoint: `https://graph.facebook.com/${params.account.outboundApiVersion}/${phoneNumberId}/messages`,
|
|
454
|
+
account: params.account,
|
|
455
|
+
accessToken,
|
|
456
|
+
payload: {
|
|
457
|
+
messaging_product: "whatsapp",
|
|
458
|
+
recipient_type: "individual",
|
|
459
|
+
to: normalizeRecipient(params.to),
|
|
460
|
+
sender_action: params.typing ? "typing_on" : "typing_off"
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
}
|
|
422
464
|
async function sendWhatsAppApiLocation(params) {
|
|
423
465
|
const { phoneNumberId, accessToken } = resolveGraphAuth(params.account);
|
|
424
466
|
return await sendWithRetry({
|
|
@@ -1084,7 +1126,7 @@ async function handleWhatsAppApiWebhook(params) {
|
|
|
1084
1126
|
//#endregion
|
|
1085
1127
|
//#region src/channel/plugin.ts
|
|
1086
1128
|
const CHANNEL_ID = "whatsapp-api";
|
|
1087
|
-
const log$
|
|
1129
|
+
const log$4 = waLogger("channel");
|
|
1088
1130
|
const DEFAULT_ACCOUNT_ID = "default";
|
|
1089
1131
|
/**
|
|
1090
1132
|
* Resolves when `signal` aborts, or never if `signal` is undefined.
|
|
@@ -1153,7 +1195,7 @@ async function dispatchInboundToAgent(params) {
|
|
|
1153
1195
|
const mediaAttachment = await downloadInboundMediaAttachment({
|
|
1154
1196
|
account,
|
|
1155
1197
|
message,
|
|
1156
|
-
log: { warn: (msg) => log$
|
|
1198
|
+
log: { warn: (msg) => log$4.warn(msg) }
|
|
1157
1199
|
});
|
|
1158
1200
|
const replyApi = rt.channel.reply;
|
|
1159
1201
|
const ctxPayload = replyApi.finalizeInboundContext({
|
|
@@ -1181,9 +1223,33 @@ async function dispatchInboundToAgent(params) {
|
|
|
1181
1223
|
OriginatingTo: to
|
|
1182
1224
|
});
|
|
1183
1225
|
const dispatchFn = replyApi.dispatchReplyWithBufferedBlockDispatcher;
|
|
1226
|
+
sendWhatsAppApiReadMark({
|
|
1227
|
+
messageId: message.messageId,
|
|
1228
|
+
account
|
|
1229
|
+
}).catch((err) => log$4.warn("failed to mark read", {
|
|
1230
|
+
error: String(err),
|
|
1231
|
+
messageId: message.messageId
|
|
1232
|
+
}));
|
|
1184
1233
|
await dispatchFn({
|
|
1185
1234
|
ctx: ctxPayload,
|
|
1186
1235
|
cfg,
|
|
1236
|
+
typing: {
|
|
1237
|
+
start: async () => {
|
|
1238
|
+
await sendWhatsAppApiTypingIndicator({
|
|
1239
|
+
to: message.from,
|
|
1240
|
+
typing: true,
|
|
1241
|
+
account
|
|
1242
|
+
});
|
|
1243
|
+
},
|
|
1244
|
+
stop: async () => {
|
|
1245
|
+
await sendWhatsAppApiTypingIndicator({
|
|
1246
|
+
to: message.from,
|
|
1247
|
+
typing: false,
|
|
1248
|
+
account
|
|
1249
|
+
});
|
|
1250
|
+
},
|
|
1251
|
+
onStartError: (err) => log$4.warn("failed to start typing", { error: String(err) })
|
|
1252
|
+
},
|
|
1187
1253
|
dispatcherOptions: {
|
|
1188
1254
|
deliver: async (payload) => {
|
|
1189
1255
|
const account = resolveAccount(cfg, accountId);
|
|
@@ -1193,17 +1259,17 @@ async function dispatchInboundToAgent(params) {
|
|
|
1193
1259
|
account
|
|
1194
1260
|
});
|
|
1195
1261
|
if (sent.length === 0) {
|
|
1196
|
-
log$
|
|
1262
|
+
log$4.info("skipping outbound: reply payload empty", { messageId: message.messageId });
|
|
1197
1263
|
return;
|
|
1198
1264
|
}
|
|
1199
|
-
log$
|
|
1265
|
+
log$4.info(`sent ${sent.length} outbound message(s)`, {
|
|
1200
1266
|
to: message.from,
|
|
1201
1267
|
messageId: message.messageId
|
|
1202
1268
|
});
|
|
1203
1269
|
onOutbound?.();
|
|
1204
1270
|
},
|
|
1205
1271
|
onError: (err, info) => {
|
|
1206
|
-
log$
|
|
1272
|
+
log$4.error(`${info?.kind ?? "reply"} dispatch failed`, {
|
|
1207
1273
|
accountId,
|
|
1208
1274
|
messageId: message.messageId,
|
|
1209
1275
|
error: String(err)
|
|
@@ -1353,6 +1419,97 @@ function createWhatsAppApiChannel(api) {
|
|
|
1353
1419
|
};
|
|
1354
1420
|
}
|
|
1355
1421
|
//#endregion
|
|
1422
|
+
//#region src/hooks/llm-output/handlers/forward-usage.ts
|
|
1423
|
+
/**
|
|
1424
|
+
* SPDX-License-Identifier: MIT
|
|
1425
|
+
*
|
|
1426
|
+
* Forwards LLM token usage from the `llm_output` hook to a configured HTTP endpoint.
|
|
1427
|
+
*/
|
|
1428
|
+
const log$3 = waLogger("llm-output/forward-usage");
|
|
1429
|
+
/** Plugin extension config keys (see `openclaw.plugin.json`). */
|
|
1430
|
+
const LABUREN_WEB_APP_CONFIG_KEY = "LABUREN_WEB_APP_URL";
|
|
1431
|
+
const USER_PHONE_CONFIG_KEY = "userPhoneNumber";
|
|
1432
|
+
const USAGE_PATH = "/api/usage";
|
|
1433
|
+
const USAGE_FEATURE = "openclaw_run";
|
|
1434
|
+
const POST_TIMEOUT_MS = 3e3;
|
|
1435
|
+
function resolveUsageIngestUrl() {
|
|
1436
|
+
let baseRaw;
|
|
1437
|
+
try {
|
|
1438
|
+
baseRaw = getPluginApi().pluginConfig?.[LABUREN_WEB_APP_CONFIG_KEY];
|
|
1439
|
+
} catch {
|
|
1440
|
+
return null;
|
|
1441
|
+
}
|
|
1442
|
+
if (typeof baseRaw !== "string") return null;
|
|
1443
|
+
const base = baseRaw.trim().replace(/\/+$/, "");
|
|
1444
|
+
if (!base) return null;
|
|
1445
|
+
try {
|
|
1446
|
+
return new URL(USAGE_PATH, base).href;
|
|
1447
|
+
} catch {
|
|
1448
|
+
log$3.warn(`usage-forwarder: invalid ${LABUREN_WEB_APP_CONFIG_KEY} base URL`);
|
|
1449
|
+
return null;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
function resolveUserPhoneNumber() {
|
|
1453
|
+
let raw;
|
|
1454
|
+
try {
|
|
1455
|
+
raw = getPluginApi().pluginConfig?.[USER_PHONE_CONFIG_KEY];
|
|
1456
|
+
} catch {
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
if (typeof raw !== "string") return null;
|
|
1460
|
+
const phone = raw.trim();
|
|
1461
|
+
return phone.length > 0 ? phone : null;
|
|
1462
|
+
}
|
|
1463
|
+
async function handleForwardLlmUsage(event, ctx) {
|
|
1464
|
+
const usage = event.usage;
|
|
1465
|
+
if (!usage) return;
|
|
1466
|
+
const endpoint = resolveUsageIngestUrl();
|
|
1467
|
+
if (!endpoint) return;
|
|
1468
|
+
const userPhoneNumber = resolveUserPhoneNumber();
|
|
1469
|
+
if (!userPhoneNumber) return;
|
|
1470
|
+
const input = usage.input ?? 0;
|
|
1471
|
+
const output = usage.output ?? 0;
|
|
1472
|
+
const cacheRead = usage.cacheRead ?? 0;
|
|
1473
|
+
const cacheWrite = usage.cacheWrite ?? 0;
|
|
1474
|
+
const total = usage.total ?? input + output + cacheRead + cacheWrite;
|
|
1475
|
+
const body = {
|
|
1476
|
+
feature: USAGE_FEATURE,
|
|
1477
|
+
runId: event.runId,
|
|
1478
|
+
sessionId: event.sessionId,
|
|
1479
|
+
userPhoneNumber,
|
|
1480
|
+
sessionKey: ctx.sessionKey,
|
|
1481
|
+
agentId: ctx.agentId,
|
|
1482
|
+
provider: event.provider,
|
|
1483
|
+
model: event.model,
|
|
1484
|
+
usage: {
|
|
1485
|
+
input,
|
|
1486
|
+
output,
|
|
1487
|
+
cacheRead,
|
|
1488
|
+
cacheWrite,
|
|
1489
|
+
total
|
|
1490
|
+
},
|
|
1491
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1492
|
+
};
|
|
1493
|
+
try {
|
|
1494
|
+
const res = await fetch(endpoint, {
|
|
1495
|
+
method: "POST",
|
|
1496
|
+
headers: { "Content-Type": "application/json" },
|
|
1497
|
+
body: JSON.stringify(body),
|
|
1498
|
+
signal: AbortSignal.timeout(POST_TIMEOUT_MS)
|
|
1499
|
+
});
|
|
1500
|
+
if (!res.ok) log$3.warn(`usage-forwarder: HTTP ${res.status}`);
|
|
1501
|
+
} catch (err) {
|
|
1502
|
+
log$3.warn(`usage-forwarder: request failed (${String(err)})`);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
//#endregion
|
|
1506
|
+
//#region src/hooks/llm-output/register.ts
|
|
1507
|
+
function registerLlmOutputHook(api) {
|
|
1508
|
+
api.on("llm_output", async (event, ctx) => {
|
|
1509
|
+
await handleForwardLlmUsage(event, ctx);
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
//#endregion
|
|
1356
1513
|
//#region src/plugin/plugin-state-dir.ts
|
|
1357
1514
|
/**
|
|
1358
1515
|
* SPDX-License-Identifier: MIT
|
|
@@ -1444,6 +1601,7 @@ function registerMessageReceivedHook(api) {
|
|
|
1444
1601
|
*/
|
|
1445
1602
|
function registerAllPluginHooks(api) {
|
|
1446
1603
|
registerMessageReceivedHook(api);
|
|
1604
|
+
registerLlmOutputHook(api);
|
|
1447
1605
|
}
|
|
1448
1606
|
//#endregion
|
|
1449
1607
|
//#region src/services/context-window-check/register.ts
|
package/openclaw.plugin.json
CHANGED
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
"type": "object",
|
|
10
10
|
"additionalProperties": false,
|
|
11
11
|
"properties": {
|
|
12
|
+
"LABUREN_WEB_APP_URL": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "Laburen web app base URL (no trailing slash). LLM usage is POSTed to {base}/api/usage."
|
|
15
|
+
},
|
|
16
|
+
"userPhoneNumber": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "WhatsApp user phone (E.164 or your normalised form). Sent on each usage POST so the backend can resolve the user."
|
|
19
|
+
},
|
|
12
20
|
"defaults": {
|
|
13
21
|
"type": "object",
|
|
14
22
|
"additionalProperties": false,
|