@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 +13 -0
- package/index.js +119 -7
- package/openclaw.plugin.json +12 -0
- package/package.json +1 -1
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
1262
|
+
log$4.info("skipping outbound: reply payload empty", { messageId: message.messageId });
|
|
1263
1263
|
return;
|
|
1264
1264
|
}
|
|
1265
|
-
log$
|
|
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$
|
|
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
|
package/openclaw.plugin.json
CHANGED
|
@@ -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,
|