@laburen/openclaw-plugin-whatsapp-api 0.6.0 → 0.7.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 +13 -0
- package/index.js +48 -3
- package/openclaw.plugin.json +4 -0
- package/package.json +1 -1
- package/src/scripts/setup.sh +34 -11
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
|
@@ -1429,9 +1429,29 @@ const log$3 = waLogger("llm-output/forward-usage");
|
|
|
1429
1429
|
/** Plugin extension config keys (see `openclaw.plugin.json`). */
|
|
1430
1430
|
const LABUREN_WEB_APP_CONFIG_KEY = "LABUREN_WEB_APP_URL";
|
|
1431
1431
|
const USER_PHONE_CONFIG_KEY = "userPhoneNumber";
|
|
1432
|
+
const LABUREN_API_KEY_CONFIG_KEY = "laburenApiKey";
|
|
1432
1433
|
const USAGE_PATH = "/api/usage";
|
|
1433
|
-
const USAGE_FEATURE = "openclaw_run";
|
|
1434
1434
|
const POST_TIMEOUT_MS = 3e3;
|
|
1435
|
+
/**
|
|
1436
|
+
* OpenClaw classifies each run by `trigger`. Only `user` (and `manual` admin
|
|
1437
|
+
* actions) should be billed as real product usage. Background runs initiated
|
|
1438
|
+
* by the runtime (`heartbeat`) or by scheduled jobs (`cron`, `memory`,
|
|
1439
|
+
* `overflow`) must be reported under a separate feature so the backend can
|
|
1440
|
+
* apply a different billing policy.
|
|
1441
|
+
*
|
|
1442
|
+
* Without this mapping every heartbeat and every duplicated wa-api cron tick
|
|
1443
|
+
* (which both fire every 30m) was charged to the user as `openclaw_run`,
|
|
1444
|
+
* draining credits without any real interaction.
|
|
1445
|
+
*/
|
|
1446
|
+
function resolveFeatureFromTrigger(trigger) {
|
|
1447
|
+
switch (trigger) {
|
|
1448
|
+
case "heartbeat":
|
|
1449
|
+
case "cron":
|
|
1450
|
+
case "memory":
|
|
1451
|
+
case "overflow": return "heartbeat_run";
|
|
1452
|
+
default: return "openclaw_run";
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1435
1455
|
function resolveUsageIngestUrl() {
|
|
1436
1456
|
let baseRaw;
|
|
1437
1457
|
try {
|
|
@@ -1460,6 +1480,17 @@ function resolveUserPhoneNumber() {
|
|
|
1460
1480
|
const phone = raw.trim();
|
|
1461
1481
|
return phone.length > 0 ? phone : null;
|
|
1462
1482
|
}
|
|
1483
|
+
function resolveLaburenApiKey() {
|
|
1484
|
+
let raw;
|
|
1485
|
+
try {
|
|
1486
|
+
raw = getPluginApi().pluginConfig?.[LABUREN_API_KEY_CONFIG_KEY];
|
|
1487
|
+
} catch {
|
|
1488
|
+
return null;
|
|
1489
|
+
}
|
|
1490
|
+
if (typeof raw !== "string") return null;
|
|
1491
|
+
const key = raw.trim();
|
|
1492
|
+
return key.length > 0 ? key : null;
|
|
1493
|
+
}
|
|
1463
1494
|
async function handleForwardLlmUsage(event, ctx) {
|
|
1464
1495
|
const usage = event.usage;
|
|
1465
1496
|
if (!usage) return;
|
|
@@ -1467,13 +1498,18 @@ async function handleForwardLlmUsage(event, ctx) {
|
|
|
1467
1498
|
if (!endpoint) return;
|
|
1468
1499
|
const userPhoneNumber = resolveUserPhoneNumber();
|
|
1469
1500
|
if (!userPhoneNumber) return;
|
|
1501
|
+
const laburenApiKey = resolveLaburenApiKey();
|
|
1502
|
+
if (!laburenApiKey) {
|
|
1503
|
+
log$3.warn(`usage-forwarder: missing ${LABUREN_API_KEY_CONFIG_KEY} (required for authenticated /api/usage calls)`);
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1470
1506
|
const input = usage.input ?? 0;
|
|
1471
1507
|
const output = usage.output ?? 0;
|
|
1472
1508
|
const cacheRead = usage.cacheRead ?? 0;
|
|
1473
1509
|
const cacheWrite = usage.cacheWrite ?? 0;
|
|
1474
1510
|
const total = usage.total ?? input + output + cacheRead + cacheWrite;
|
|
1475
1511
|
const body = {
|
|
1476
|
-
feature:
|
|
1512
|
+
feature: resolveFeatureFromTrigger(ctx.trigger),
|
|
1477
1513
|
runId: event.runId,
|
|
1478
1514
|
sessionId: event.sessionId,
|
|
1479
1515
|
userPhoneNumber,
|
|
@@ -1493,7 +1529,10 @@ async function handleForwardLlmUsage(event, ctx) {
|
|
|
1493
1529
|
try {
|
|
1494
1530
|
const res = await fetch(endpoint, {
|
|
1495
1531
|
method: "POST",
|
|
1496
|
-
headers: {
|
|
1532
|
+
headers: {
|
|
1533
|
+
"Content-Type": "application/json",
|
|
1534
|
+
Authorization: `Bearer ${laburenApiKey}`
|
|
1535
|
+
},
|
|
1497
1536
|
body: JSON.stringify(body),
|
|
1498
1537
|
signal: AbortSignal.timeout(POST_TIMEOUT_MS)
|
|
1499
1538
|
});
|
|
@@ -1662,6 +1701,12 @@ const plugin = {
|
|
|
1662
1701
|
id: "whatsapp-api",
|
|
1663
1702
|
name: "WhatsApp API",
|
|
1664
1703
|
description: "WhatsApp API channel plugin with inbound webhook and direct Meta outbound",
|
|
1704
|
+
/**
|
|
1705
|
+
* Called by OpenClaw when the plugin loads: publishes the channel, wires the
|
|
1706
|
+
* global plugin API holder, then registers pipeline hooks and plugin services.
|
|
1707
|
+
*
|
|
1708
|
+
* @param api - Plugin API injected by the host (config, runtime, registration helpers)
|
|
1709
|
+
*/
|
|
1665
1710
|
register(api) {
|
|
1666
1711
|
setPluginApi(api);
|
|
1667
1712
|
api.registerChannel({ plugin: createWhatsAppApiChannel(api) });
|
package/openclaw.plugin.json
CHANGED
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
"type": "string",
|
|
18
18
|
"description": "WhatsApp user phone (E.164 or your normalised form). Sent on each usage POST so the backend can resolve the user."
|
|
19
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
|
+
},
|
|
20
24
|
"defaults": {
|
|
21
25
|
"type": "object",
|
|
22
26
|
"additionalProperties": false,
|
package/package.json
CHANGED
package/src/scripts/setup.sh
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
# Run once per OpenClaw instance.
|
|
4
4
|
#
|
|
5
5
|
# Usage:
|
|
6
|
-
# bash setup.sh → installs the cron
|
|
7
|
-
# bash setup.sh --uninstall → removes the cron
|
|
6
|
+
# bash setup.sh → installs the cron (cleans up duplicates first)
|
|
7
|
+
# bash setup.sh --uninstall → removes all instances of the cron
|
|
8
8
|
|
|
9
9
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
10
|
CHECK_SCRIPT="$SCRIPT_DIR/check.sh"
|
|
@@ -18,22 +18,45 @@ for arg in "$@"; do
|
|
|
18
18
|
esac
|
|
19
19
|
done
|
|
20
20
|
|
|
21
|
+
# List all job IDs that match $JOB_NAME. Uses `awk` instead of `jq` because the
|
|
22
|
+
# openclaw container image does not include jq (the previous version of this
|
|
23
|
+
# script silently failed the existence check, which caused duplicate crons to
|
|
24
|
+
# accumulate on every plugin boot — one debit per duplicate, per tick).
|
|
25
|
+
list_existing_ids() {
|
|
26
|
+
openclaw cron list 2>/dev/null \
|
|
27
|
+
| awk -v name="$JOB_NAME" 'index($0, name) {print $1}'
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
if [ "$UNINSTALL" = "true" ]; then
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
REMOVED=0
|
|
32
|
+
while read -r id; do
|
|
33
|
+
[ -z "$id" ] && continue
|
|
34
|
+
if openclaw cron rm "$id" >/dev/null 2>&1; then
|
|
35
|
+
echo "[$JOB_NAME] Cron job removed (id: $id)."
|
|
36
|
+
REMOVED=$((REMOVED + 1))
|
|
37
|
+
fi
|
|
38
|
+
done < <(list_existing_ids)
|
|
39
|
+
if [ "$REMOVED" -eq 0 ]; then
|
|
27
40
|
echo "[$JOB_NAME] No cron job found to remove."
|
|
28
41
|
fi
|
|
29
42
|
exit 0
|
|
30
43
|
fi
|
|
31
44
|
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
45
|
+
# Count existing instances. If zero → install. If one → idempotent skip.
|
|
46
|
+
# If more than one → cleanup duplicates and reinstall one (self-heal).
|
|
47
|
+
EXISTING_IDS=$(list_existing_ids)
|
|
48
|
+
EXISTING_COUNT=$(printf '%s\n' "$EXISTING_IDS" | awk 'NF' | wc -l)
|
|
49
|
+
|
|
50
|
+
if [ "$EXISTING_COUNT" -eq 1 ]; then
|
|
51
|
+
echo "[$JOB_NAME] Already installed (id: $EXISTING_IDS). Skipping."
|
|
36
52
|
exit 0
|
|
53
|
+
elif [ "$EXISTING_COUNT" -gt 1 ]; then
|
|
54
|
+
echo "[$JOB_NAME] Found $EXISTING_COUNT duplicate instance(s). Cleaning up before reinstall..."
|
|
55
|
+
while read -r id; do
|
|
56
|
+
[ -z "$id" ] && continue
|
|
57
|
+
openclaw cron rm "$id" >/dev/null 2>&1 \
|
|
58
|
+
&& echo "[$JOB_NAME] Removed duplicate id: $id"
|
|
59
|
+
done < <(printf '%s\n' "$EXISTING_IDS")
|
|
37
60
|
fi
|
|
38
61
|
|
|
39
62
|
chmod +x "$CHECK_SCRIPT"
|