@laburen/openclaw-plugin-whatsapp-api 0.5.0 → 0.6.1

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 CHANGED
@@ -120,6 +120,19 @@ Responses: **`403`** if the shared secret does not match, **`400`** if the paylo
120
120
 
121
121
  Per-account fields can override phone id, token, retries, etc. under `channels["whatsapp-api"].accounts.<accountId>`.
122
122
 
123
+ ## LLM Usage Forwarding (`llm_output`) (opcional)
124
+
125
+ Este plugin puede reenviar el usage de tokens cuando OpenClaw emite el hook `llm_output`. Cuando está configurado, hace un `POST` a `{LABUREN_WEB_APP_URL}/api/usage` y envía `Authorization: Bearer <laburenApiKey>`.
126
+
127
+ Para activarlo, configura estas keys del entry del plugin (no del channel):
128
+
129
+ ```bash
130
+ openclaw config set plugins.entries.whatsapp-api.LABUREN_WEB_APP_URL "https://tu-laburen.example"
131
+ openclaw config set plugins.entries.whatsapp-api.userPhoneNumber "+1234567890"
132
+ openclaw config set plugins.entries.whatsapp-api.laburenApiKey "tu-api-key"
133
+ openclaw gateway restart
134
+ ```
135
+
123
136
  ## Notes
124
137
 
125
138
  - Outbound mode is **direct-to-Meta** only (no third-party relay in this plugin).
package/index.js CHANGED
@@ -1126,7 +1126,7 @@ async function handleWhatsAppApiWebhook(params) {
1126
1126
  //#endregion
1127
1127
  //#region src/channel/plugin.ts
1128
1128
  const CHANNEL_ID = "whatsapp-api";
1129
- const log$3 = waLogger("channel");
1129
+ const log$4 = waLogger("channel");
1130
1130
  const DEFAULT_ACCOUNT_ID = "default";
1131
1131
  /**
1132
1132
  * Resolves when `signal` aborts, or never if `signal` is undefined.
@@ -1195,7 +1195,7 @@ async function dispatchInboundToAgent(params) {
1195
1195
  const mediaAttachment = await downloadInboundMediaAttachment({
1196
1196
  account,
1197
1197
  message,
1198
- log: { warn: (msg) => log$3.warn(msg) }
1198
+ log: { warn: (msg) => log$4.warn(msg) }
1199
1199
  });
1200
1200
  const replyApi = rt.channel.reply;
1201
1201
  const ctxPayload = replyApi.finalizeInboundContext({
@@ -1226,7 +1226,7 @@ async function dispatchInboundToAgent(params) {
1226
1226
  sendWhatsAppApiReadMark({
1227
1227
  messageId: message.messageId,
1228
1228
  account
1229
- }).catch((err) => log$3.warn("failed to mark read", {
1229
+ }).catch((err) => log$4.warn("failed to mark read", {
1230
1230
  error: String(err),
1231
1231
  messageId: message.messageId
1232
1232
  }));
@@ -1248,7 +1248,7 @@ async function dispatchInboundToAgent(params) {
1248
1248
  account
1249
1249
  });
1250
1250
  },
1251
- onStartError: (err) => log$3.warn("failed to start typing", { error: String(err) })
1251
+ onStartError: (err) => log$4.warn("failed to start typing", { error: String(err) })
1252
1252
  },
1253
1253
  dispatcherOptions: {
1254
1254
  deliver: async (payload) => {
@@ -1259,17 +1259,17 @@ async function dispatchInboundToAgent(params) {
1259
1259
  account
1260
1260
  });
1261
1261
  if (sent.length === 0) {
1262
- log$3.info("skipping outbound: reply payload empty", { messageId: message.messageId });
1262
+ log$4.info("skipping outbound: reply payload empty", { messageId: message.messageId });
1263
1263
  return;
1264
1264
  }
1265
- log$3.info(`sent ${sent.length} outbound message(s)`, {
1265
+ log$4.info(`sent ${sent.length} outbound message(s)`, {
1266
1266
  to: message.from,
1267
1267
  messageId: message.messageId
1268
1268
  });
1269
1269
  onOutbound?.();
1270
1270
  },
1271
1271
  onError: (err, info) => {
1272
- log$3.error(`${info?.kind ?? "reply"} dispatch failed`, {
1272
+ log$4.error(`${info?.kind ?? "reply"} dispatch failed`, {
1273
1273
  accountId,
1274
1274
  messageId: message.messageId,
1275
1275
  error: String(err)
@@ -1419,6 +1419,117 @@ function createWhatsAppApiChannel(api) {
1419
1419
  };
1420
1420
  }
1421
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 LABUREN_API_KEY_CONFIG_KEY = "laburenApiKey";
1433
+ const USAGE_PATH = "/api/usage";
1434
+ const USAGE_FEATURE = "openclaw_run";
1435
+ const POST_TIMEOUT_MS = 3e3;
1436
+ function resolveUsageIngestUrl() {
1437
+ let baseRaw;
1438
+ try {
1439
+ baseRaw = getPluginApi().pluginConfig?.[LABUREN_WEB_APP_CONFIG_KEY];
1440
+ } catch {
1441
+ return null;
1442
+ }
1443
+ if (typeof baseRaw !== "string") return null;
1444
+ const base = baseRaw.trim().replace(/\/+$/, "");
1445
+ if (!base) return null;
1446
+ try {
1447
+ return new URL(USAGE_PATH, base).href;
1448
+ } catch {
1449
+ log$3.warn(`usage-forwarder: invalid ${LABUREN_WEB_APP_CONFIG_KEY} base URL`);
1450
+ return null;
1451
+ }
1452
+ }
1453
+ function resolveUserPhoneNumber() {
1454
+ let raw;
1455
+ try {
1456
+ raw = getPluginApi().pluginConfig?.[USER_PHONE_CONFIG_KEY];
1457
+ } catch {
1458
+ return null;
1459
+ }
1460
+ if (typeof raw !== "string") return null;
1461
+ const phone = raw.trim();
1462
+ return phone.length > 0 ? phone : null;
1463
+ }
1464
+ function resolveLaburenApiKey() {
1465
+ let raw;
1466
+ try {
1467
+ raw = getPluginApi().pluginConfig?.[LABUREN_API_KEY_CONFIG_KEY];
1468
+ } catch {
1469
+ return null;
1470
+ }
1471
+ if (typeof raw !== "string") return null;
1472
+ const key = raw.trim();
1473
+ return key.length > 0 ? key : null;
1474
+ }
1475
+ async function handleForwardLlmUsage(event, ctx) {
1476
+ const usage = event.usage;
1477
+ if (!usage) return;
1478
+ const endpoint = resolveUsageIngestUrl();
1479
+ if (!endpoint) return;
1480
+ const userPhoneNumber = resolveUserPhoneNumber();
1481
+ if (!userPhoneNumber) return;
1482
+ const laburenApiKey = resolveLaburenApiKey();
1483
+ if (!laburenApiKey) {
1484
+ log$3.warn(`usage-forwarder: missing ${LABUREN_API_KEY_CONFIG_KEY} (required for authenticated /api/usage calls)`);
1485
+ return;
1486
+ }
1487
+ const input = usage.input ?? 0;
1488
+ const output = usage.output ?? 0;
1489
+ const cacheRead = usage.cacheRead ?? 0;
1490
+ const cacheWrite = usage.cacheWrite ?? 0;
1491
+ const total = usage.total ?? input + output + cacheRead + cacheWrite;
1492
+ const body = {
1493
+ feature: USAGE_FEATURE,
1494
+ runId: event.runId,
1495
+ sessionId: event.sessionId,
1496
+ userPhoneNumber,
1497
+ sessionKey: ctx.sessionKey,
1498
+ agentId: ctx.agentId,
1499
+ provider: event.provider,
1500
+ model: event.model,
1501
+ usage: {
1502
+ input,
1503
+ output,
1504
+ cacheRead,
1505
+ cacheWrite,
1506
+ total
1507
+ },
1508
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
1509
+ };
1510
+ try {
1511
+ const res = await fetch(endpoint, {
1512
+ method: "POST",
1513
+ headers: {
1514
+ "Content-Type": "application/json",
1515
+ Authorization: `Bearer ${laburenApiKey}`
1516
+ },
1517
+ body: JSON.stringify(body),
1518
+ signal: AbortSignal.timeout(POST_TIMEOUT_MS)
1519
+ });
1520
+ if (!res.ok) log$3.warn(`usage-forwarder: HTTP ${res.status}`);
1521
+ } catch (err) {
1522
+ log$3.warn(`usage-forwarder: request failed (${String(err)})`);
1523
+ }
1524
+ }
1525
+ //#endregion
1526
+ //#region src/hooks/llm-output/register.ts
1527
+ function registerLlmOutputHook(api) {
1528
+ api.on("llm_output", async (event, ctx) => {
1529
+ await handleForwardLlmUsage(event, ctx);
1530
+ });
1531
+ }
1532
+ //#endregion
1422
1533
  //#region src/plugin/plugin-state-dir.ts
1423
1534
  /**
1424
1535
  * SPDX-License-Identifier: MIT
@@ -1510,6 +1621,7 @@ function registerMessageReceivedHook(api) {
1510
1621
  */
1511
1622
  function registerAllPluginHooks(api) {
1512
1623
  registerMessageReceivedHook(api);
1624
+ registerLlmOutputHook(api);
1513
1625
  }
1514
1626
  //#endregion
1515
1627
  //#region src/services/context-window-check/register.ts
@@ -9,6 +9,18 @@
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
+ },
20
+ "laburenApiKey": {
21
+ "type": "string",
22
+ "description": "Bearer token for the Laburen usage ingest API (sent as `Authorization: Bearer <laburenApiKey>` on POST {base}/api/usage)."
23
+ },
12
24
  "defaults": {
13
25
  "type": "object",
14
26
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laburen/openclaw-plugin-whatsapp-api",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "description": "WhatsApp API channel plugin for OpenClaw",
6
6
  "main": "index.js",