@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 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: USAGE_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: { "Content-Type": "application/json" },
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) });
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laburen/openclaw-plugin-whatsapp-api",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "WhatsApp API channel plugin for OpenClaw",
6
6
  "main": "index.js",
@@ -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
- JOB_ID=$(openclaw cron list --json 2>/dev/null | jq -r ".jobs[] | select(.name == \"$JOB_NAME\") | .id")
23
- if [ -n "$JOB_ID" ]; then
24
- openclaw cron rm "$JOB_ID"
25
- echo "[$JOB_NAME] Cron job removed (id: $JOB_ID)."
26
- else
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
- # Check if already installed
33
- EXISTING=$(openclaw cron list --json 2>/dev/null | jq -r ".jobs[] | select(.name == \"$JOB_NAME\") | .id")
34
- if [ -n "$EXISTING" ]; then
35
- echo "[$JOB_NAME] Already installed (id: $EXISTING). Run with --uninstall first to reinstall."
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"