@vama/openclaw 2026.5.5-1 → 2026.5.5-4
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 +202 -0
- package/dist/{api-C0vtNv5b.js → api-lhR0QgC_.js} +1 -1
- package/dist/api.js +2 -2
- package/dist/{channel-plugin-api-CcZ_y9pT.js → channel-plugin-api-YGbkWmVM.js} +38 -9
- package/dist/channel-plugin-api.js +1 -1
- package/dist/index.js +3 -3
- package/dist/{probe-B2hFOc2Y.js → monitor-CHFjRu2J.js} +393 -75
- package/package.json +13 -11
package/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# @vama/openclaw
|
|
2
|
+
|
|
3
|
+
Connect your own [OpenClaw](https://openclaw.ai) agent to **Vama** direct messages — the BotFather-style "bring your own claw" flow. This plugin adds a `vama` channel that talks to Vama's BotHub, so an agent you run appears natively in Vama DMs.
|
|
4
|
+
|
|
5
|
+
> **Full setup guide:** https://web.vama.com/connect-guide
|
|
6
|
+
|
|
7
|
+
## Install the channel
|
|
8
|
+
|
|
9
|
+
The Vama channel ships **bundled** in Vama-provided OpenClaw builds (for example, agents created from inside the Vama app), so if you use one of those you can skip straight to **Get a token** below.
|
|
10
|
+
|
|
11
|
+
If you run **stock OpenClaw** from upstream, install the Vama channel first. It is published to both npm and ClawHub; npm is the default:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
openclaw plugins install @vama/openclaw
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Prefer ClawHub? Use the `clawhub:` spec instead:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
openclaw plugins install clawhub:@vama/openclaw
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Either one pulls the published plugin so `vama` becomes available to `openclaw onboard` and the `channels.vama` config block below. The channel requires OpenClaw `>=2026.5.12`.
|
|
24
|
+
|
|
25
|
+
## Get a token (Bring your own claw)
|
|
26
|
+
|
|
27
|
+
If you run your own OpenClaw gateway and just want to connect it to Vama — the BotFather-style flow — mint a token straight from the Vama app:
|
|
28
|
+
|
|
29
|
+
1. On stock OpenClaw, install the channel first (see **Install the channel**). Vama-provided builds already bundle it.
|
|
30
|
+
2. In Vama, go to **Settings → Agents → Connect an agent** (the "Bring your own claw" page).
|
|
31
|
+
3. Optionally name the agent, then click **Create agent**. Vama shows a one-time **agent token** and **webhook secret**, plus a ready-to-paste `channels.vama` config block.
|
|
32
|
+
4. Copy the config into `~/.openclaw/openclaw.json` (see below).
|
|
33
|
+
5. Fill in `webhookUrl` with your gateway's public URL and start the gateway with `openclaw gateway run` — it registers the URL with BotHub automatically on every start (see **Receiving messages**).
|
|
34
|
+
|
|
35
|
+
Each token is shown **once**. If you lose one, use **Regenerate token** on that agent — the old token stops working immediately. **Delete agent** disconnects the claw and removes that agent from your Vama DMs.
|
|
36
|
+
|
|
37
|
+
> You can connect **as many agents as you like** — each **Create agent** provisions a new, independent agent with its own token. Run a separate OpenClaw gateway (or a separate `accounts` entry — see **Multi-account**) per agent.
|
|
38
|
+
|
|
39
|
+
## CLI onboarding (alternative)
|
|
40
|
+
|
|
41
|
+
Instead of minting a token in the app, you can auto-provision from the CLI:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
openclaw onboard
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Select **Vama** from the channel list. The wizard prompts for your Vama username, auto-provisions a bot via BotHub, and configures webhook settings. Then start the gateway:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
openclaw gateway run
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Manual configuration
|
|
54
|
+
|
|
55
|
+
Set the following in `~/.openclaw/openclaw.json`:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"channels": {
|
|
60
|
+
"vama": {
|
|
61
|
+
"enabled": true,
|
|
62
|
+
"botToken": "<bot_token from provisioning>",
|
|
63
|
+
"webhookSecret": "<webhook_secret from provisioning>",
|
|
64
|
+
"webhookUrl": "https://<your-public-host>/vama/events",
|
|
65
|
+
"webhookPort": 3001,
|
|
66
|
+
"webhookPath": "/vama/events",
|
|
67
|
+
"webhookHost": "127.0.0.1"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Receiving messages (webhook reachability)
|
|
74
|
+
|
|
75
|
+
BotHub delivers inbound messages to your gateway over an HTTP **webhook**, so BotHub must be able to reach your gateway's webhook listener from the internet. Two things make that work:
|
|
76
|
+
|
|
77
|
+
1. **A public HTTPS URL that forwards to the local listener.** The listener binds to `webhookHost`:`webhookPort``webhookPath` (default `127.0.0.1:3001/vama/events`). If your machine isn't directly reachable, any tunnel or reverse proxy works, e.g.:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
cloudflared tunnel --url http://localhost:3001
|
|
81
|
+
# prints something like https://random-words.trycloudflare.com
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
2. **Registering that URL with BotHub.** Set it as `channels.vama.webhookUrl` (include the `/vama/events` path). The gateway registers it with BotHub **automatically every time it starts** — no manual API calls. If your tunnel URL changes (quick tunnels are ephemeral), update `webhookUrl` and restart the gateway. For a set-and-forget setup, use a stable URL (named Cloudflare tunnel, Tailscale Funnel, or a reverse proxy on a domain you own).
|
|
85
|
+
|
|
86
|
+
The `openclaw onboard` wizard also prompts for this URL, registers it immediately, and saves it to `webhookUrl` for you.
|
|
87
|
+
|
|
88
|
+
Verify end-to-end with:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
openclaw channels status --probe
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The probe checks both that your token is valid **and** that a webhook URL is registered with BotHub. If registration is missing it fails with instructions — a bot in that state shows **"Awaiting claw"** in Vama and cannot receive messages.
|
|
95
|
+
|
|
96
|
+
### WebSocket delivery (no public URL needed)
|
|
97
|
+
|
|
98
|
+
BotHub can also deliver events over an **outbound WebSocket** (`GET /v1/bot/ws`) — the gateway dials out, so there's nothing to expose: no tunnel, no reverse proxy, works behind any NAT. The gateway probes for it automatically at startup (`transport` defaults to `"auto"`) and falls back to webhook delivery when the bot doesn't have WebSocket enabled server-side. When the socket is connected, BotHub prefers it and uses the registered webhook only as fallback. The probe also accepts a WebSocket-enabled bot without a registered webhook URL.
|
|
99
|
+
|
|
100
|
+
## Configuration reference
|
|
101
|
+
|
|
102
|
+
| Key | Type | Default | Description |
|
|
103
|
+
| ---------------- | ------- | ---------------- | --------------------------------------------------------------------------------------------------------- |
|
|
104
|
+
| `enabled` | boolean | `false` | Enable/disable the Vama channel |
|
|
105
|
+
| `botToken` | string | — | Bot authentication token from provisioning |
|
|
106
|
+
| `webhookSecret` | string | — | HMAC secret for webhook signature verification |
|
|
107
|
+
| `webhookUrl` | string | — | Public URL of your webhook listener. Auto-registered with BotHub at every gateway start. |
|
|
108
|
+
| `transport` | string | `"auto"` | Inbound delivery: `"auto"` (WebSocket when BotHub allows it, else webhook), `"webhook"`, or `"websocket"` |
|
|
109
|
+
| `webhookPort` | integer | `3001` | Local port for the webhook listener |
|
|
110
|
+
| `webhookPath` | string | `"/vama/events"` | URL path for webhook events |
|
|
111
|
+
| `webhookHost` | string | `"127.0.0.1"` | Bind address for the webhook listener |
|
|
112
|
+
| `dmPolicy` | string | `"open"` | DM access policy: `"open"`, `"pairing"`, or `"allowlist"` |
|
|
113
|
+
| `allowFrom` | array | `[]` | Vama user IDs allowed to message the bot |
|
|
114
|
+
| `textChunkLimit` | integer | `10000` | Max characters per outbound message |
|
|
115
|
+
| `bothubUrl` | string | _(canonical)_ | Override the BotHub API base URL. Only needed for self-hosted BotHub deployments. |
|
|
116
|
+
|
|
117
|
+
## Access control
|
|
118
|
+
|
|
119
|
+
Control who can message the bot with `channels.vama.dmPolicy`:
|
|
120
|
+
|
|
121
|
+
- **`open`** (default) — any Vama user can DM the bot.
|
|
122
|
+
- **`pairing`** — unknown users get a pairing code to request approval.
|
|
123
|
+
- **`allowlist`** — only users listed in `channels.vama.allowFrom` can DM the bot.
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"channels": {
|
|
128
|
+
"vama": {
|
|
129
|
+
"dmPolicy": "allowlist",
|
|
130
|
+
"allowFrom": ["user_alice", "user_bob"]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Webhook security
|
|
137
|
+
|
|
138
|
+
BotHub signs every webhook delivery with HMAC-SHA256. OpenClaw verifies the signature using the `webhookSecret` from provisioning.
|
|
139
|
+
|
|
140
|
+
Headers sent by BotHub:
|
|
141
|
+
|
|
142
|
+
- `X-BotHub-Signature` — `sha256=<hex HMAC>`
|
|
143
|
+
- `X-BotHub-Timestamp` — Unix seconds
|
|
144
|
+
- `X-BotHub-Event` — Event type (e.g. `message.create`)
|
|
145
|
+
- `X-BotHub-Delivery-ID` — Unique delivery identifier
|
|
146
|
+
|
|
147
|
+
Signatures older than 5 minutes are rejected to prevent replay attacks.
|
|
148
|
+
|
|
149
|
+
## Multi-account
|
|
150
|
+
|
|
151
|
+
For multiple bot accounts, use the `accounts` map:
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"channels": {
|
|
156
|
+
"vama": {
|
|
157
|
+
"enabled": true,
|
|
158
|
+
"bothubUrl": "https://bothub.example.com",
|
|
159
|
+
"accounts": {
|
|
160
|
+
"staging": {
|
|
161
|
+
"botToken": "<staging_token>",
|
|
162
|
+
"webhookSecret": "<staging_secret>",
|
|
163
|
+
"webhookPort": 3002,
|
|
164
|
+
"name": "Staging Bot"
|
|
165
|
+
},
|
|
166
|
+
"production": {
|
|
167
|
+
"botToken": "<prod_token>",
|
|
168
|
+
"webhookSecret": "<prod_secret>",
|
|
169
|
+
"webhookPort": 3003,
|
|
170
|
+
"name": "Production Bot"
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Named accounts inherit top-level settings (like `bothubUrl`) and can override them individually.
|
|
179
|
+
|
|
180
|
+
## Capabilities
|
|
181
|
+
|
|
182
|
+
| Feature | Supported |
|
|
183
|
+
| ----------------- | -------------------- |
|
|
184
|
+
| Direct messages | Yes |
|
|
185
|
+
| Threads (replies) | Yes |
|
|
186
|
+
| Media attachments | No (text-only in v1) |
|
|
187
|
+
| Reactions | No |
|
|
188
|
+
| Message editing | No |
|
|
189
|
+
| Groups/channels | No |
|
|
190
|
+
|
|
191
|
+
## Troubleshooting
|
|
192
|
+
|
|
193
|
+
- **Vama shows "Awaiting claw" even though the gateway is running**: no webhook URL is registered with BotHub. Set `channels.vama.webhookUrl` to your public URL and restart the gateway — it registers automatically. `openclaw channels status --probe` reports this state explicitly ("no webhook URL is registered with BotHub").
|
|
194
|
+
- **Bot not responding**: run `openclaw channels status --probe`. It verifies both the token and webhook registration. Also check the gateway log for `webhook registered with BotHub` (or a registration error) at startup.
|
|
195
|
+
- **Worked, then stopped after a tunnel restart**: ephemeral tunnel URLs change on restart. Update `webhookUrl` to the new URL and restart the gateway, or switch to a stable tunnel/domain.
|
|
196
|
+
- **Signature verification failed**: ensure `webhookSecret` matches the value from provisioning. Re-provision if needed.
|
|
197
|
+
- **Connection test fails during onboarding**: verify the `bothubUrl` is correct and reachable from your gateway host.
|
|
198
|
+
- **Messages dropped**: check gateway logs for `dmPolicy` blocks. If using `allowlist`, verify the sender's user ID is in `channels.vama.allowFrom`.
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
See the Vama OpenClaw distribution for license terms.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as dispatchStarted, i as dispatchEnded } from "./
|
|
1
|
+
import { a as dispatchStarted, i as dispatchEnded } from "./monitor-CHFjRu2J.js";
|
|
2
2
|
//#region extensions/vama/src/subagent-keepalive-hooks.ts
|
|
3
3
|
function registerVamaSubagentKeepaliveHooks(api) {
|
|
4
4
|
api.on("subagent_spawned", () => {
|
package/dist/api.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
import { t as registerVamaSubagentKeepaliveHooks } from "./api-
|
|
1
|
+
import { n as probeVama, r as sendMessageVama, t as monitorVamaProvider } from "./monitor-CHFjRu2J.js";
|
|
2
|
+
import { t as registerVamaSubagentKeepaliveHooks } from "./api-lhR0QgC_.js";
|
|
3
3
|
export { monitorVamaProvider, probeVama, registerVamaSubagentKeepaliveHooks, sendMessageVama };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as provisionBot, i as createBotHubClient, n as attachmentHintFromExtension } from "./client-AsD46gcK.js";
|
|
2
|
-
import { c as
|
|
2
|
+
import { c as buildBaseChannelStatusSummary, d as resolveDefaultVamaAccountId, f as resolveVamaAccount, l as createDefaultChannelRuntimeState, n as probeVama, o as DEFAULT_ACCOUNT_ID$1, r as sendMessageVama, s as PAIRING_APPROVED_MESSAGE, t as monitorVamaProvider, u as listVamaAccountIds } from "./monitor-CHFjRu2J.js";
|
|
3
3
|
import { t as getVamaRuntime } from "./runtime-w-1oL50p.js";
|
|
4
4
|
import { jsonResult, readStringOrNumberParam, readStringParam } from "openclaw/plugin-sdk/channel-actions";
|
|
5
5
|
import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
|
|
@@ -367,6 +367,15 @@ const vamaPlugin = {
|
|
|
367
367
|
minimum: 1
|
|
368
368
|
},
|
|
369
369
|
webhookHost: { type: "string" },
|
|
370
|
+
webhookUrl: { type: "string" },
|
|
371
|
+
transport: {
|
|
372
|
+
type: "string",
|
|
373
|
+
enum: [
|
|
374
|
+
"auto",
|
|
375
|
+
"webhook",
|
|
376
|
+
"websocket"
|
|
377
|
+
]
|
|
378
|
+
},
|
|
370
379
|
dmPolicy: {
|
|
371
380
|
type: "string",
|
|
372
381
|
enum: [
|
|
@@ -399,7 +408,16 @@ const vamaPlugin = {
|
|
|
399
408
|
type: "integer",
|
|
400
409
|
minimum: 1
|
|
401
410
|
},
|
|
402
|
-
webhookHost: { type: "string" }
|
|
411
|
+
webhookHost: { type: "string" },
|
|
412
|
+
webhookUrl: { type: "string" },
|
|
413
|
+
transport: {
|
|
414
|
+
type: "string",
|
|
415
|
+
enum: [
|
|
416
|
+
"auto",
|
|
417
|
+
"webhook",
|
|
418
|
+
"websocket"
|
|
419
|
+
]
|
|
420
|
+
}
|
|
403
421
|
}
|
|
404
422
|
}
|
|
405
423
|
}
|
|
@@ -625,13 +643,24 @@ const vamaPlugin = {
|
|
|
625
643
|
}
|
|
626
644
|
}
|
|
627
645
|
})).trim();
|
|
628
|
-
if (publicWebhookUrl)
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
646
|
+
if (publicWebhookUrl) {
|
|
647
|
+
next = {
|
|
648
|
+
...next,
|
|
649
|
+
channels: {
|
|
650
|
+
...next.channels,
|
|
651
|
+
vama: {
|
|
652
|
+
...next.channels?.vama,
|
|
653
|
+
webhookUrl: publicWebhookUrl
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
try {
|
|
658
|
+
await createBotHubClient(resolveVamaAccount({ cfg: next })).registerWebhook({ url: publicWebhookUrl });
|
|
659
|
+
await prompter.note(`Webhook registered: ${publicWebhookUrl}`, "Vama webhook registration");
|
|
660
|
+
} catch (err) {
|
|
661
|
+
await prompter.note(`Webhook registration failed: ${String(err)}\nThe gateway will retry automatically at startup (channels.vama.webhookUrl is saved).`, "Vama webhook registration");
|
|
662
|
+
}
|
|
663
|
+
} else await prompter.note("Skipped webhook registration. Once your gateway is publicly reachable, set channels.vama.webhookUrl to its public URL (e.g. https://your-host/vama/events) — it registers automatically at startup.", "Vama webhook registration");
|
|
635
664
|
const account = resolveVamaAccount({ cfg: next });
|
|
636
665
|
try {
|
|
637
666
|
const probe = await probeVama(account);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as vamaPlugin } from "./channel-plugin-api-
|
|
1
|
+
import { t as vamaPlugin } from "./channel-plugin-api-YGbkWmVM.js";
|
|
2
2
|
export { vamaPlugin };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { n as
|
|
1
|
+
import { n as probeVama, r as sendMessageVama, t as monitorVamaProvider } from "./monitor-CHFjRu2J.js";
|
|
2
2
|
import { n as setVamaRuntime } from "./runtime-w-1oL50p.js";
|
|
3
|
-
import { t as registerVamaSubagentKeepaliveHooks } from "./api-
|
|
4
|
-
import { t as vamaPlugin } from "./channel-plugin-api-
|
|
3
|
+
import { t as registerVamaSubagentKeepaliveHooks } from "./api-lhR0QgC_.js";
|
|
4
|
+
import { t as vamaPlugin } from "./channel-plugin-api-YGbkWmVM.js";
|
|
5
5
|
import "./runtime-api.js";
|
|
6
6
|
//#region extensions/vama/index.ts
|
|
7
7
|
const channelEntry = {
|
|
@@ -3,6 +3,11 @@ import { t as getVamaRuntime } from "./runtime-w-1oL50p.js";
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { promises } from "node:fs";
|
|
5
5
|
import * as http from "node:http";
|
|
6
|
+
import { DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { WebSocket, fetch } from "undici";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
|
|
6
11
|
import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
|
|
7
12
|
import { installRequestBodyLimitGuard } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
8
13
|
import { logTypingFailure } from "openclaw/plugin-sdk/channel-logging";
|
|
@@ -10,34 +15,8 @@ import { PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk/channel-plugin-com
|
|
|
10
15
|
import { createReplyPrefixContext, createTypingCallbacks } from "openclaw/plugin-sdk/channel-message";
|
|
11
16
|
import { createPersistentDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
|
|
12
17
|
import { buildBaseChannelStatusSummary, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
|
|
13
|
-
import { DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
14
|
-
import path from "node:path";
|
|
15
|
-
import { homedir } from "node:os";
|
|
16
|
-
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
|
|
17
18
|
import * as crypto from "node:crypto";
|
|
18
19
|
import { createHash } from "node:crypto";
|
|
19
|
-
//#region extensions/vama/src/host-pairing-access.ts
|
|
20
|
-
/** Scope pairing store operations to one channel/account pair for plugin-facing helpers. */
|
|
21
|
-
function createScopedPairingAccess(params) {
|
|
22
|
-
const resolvedAccountId = normalizeAccountId(params.accountId);
|
|
23
|
-
return {
|
|
24
|
-
accountId: resolvedAccountId,
|
|
25
|
-
readAllowFromStore: () => params.core.channel.pairing.readAllowFromStore({
|
|
26
|
-
channel: params.channel,
|
|
27
|
-
accountId: resolvedAccountId
|
|
28
|
-
}),
|
|
29
|
-
readStoreForDmPolicy: (provider, accountId) => params.core.channel.pairing.readAllowFromStore({
|
|
30
|
-
channel: provider,
|
|
31
|
-
accountId: normalizeAccountId(accountId)
|
|
32
|
-
}),
|
|
33
|
-
upsertPairingRequest: (input) => params.core.channel.pairing.upsertPairingRequest({
|
|
34
|
-
channel: params.channel,
|
|
35
|
-
accountId: resolvedAccountId,
|
|
36
|
-
...input
|
|
37
|
-
})
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
//#endregion
|
|
41
20
|
//#region extensions/vama/src/accounts.ts
|
|
42
21
|
function listConfiguredAccountIds(cfg) {
|
|
43
22
|
const accounts = (cfg.channels?.vama)?.accounts;
|
|
@@ -77,6 +56,9 @@ function resolveVamaAccount(params) {
|
|
|
77
56
|
const webhookSecret = merged.webhookSecret?.trim() || void 0;
|
|
78
57
|
const webhookSecretFile = merged.webhookSecretFile?.trim() || void 0;
|
|
79
58
|
const bothubUrl = merged.bothubUrl?.trim() || "https://bothub.vama.com";
|
|
59
|
+
const webhookUrl = merged.webhookUrl?.trim() || void 0;
|
|
60
|
+
const rawTransport = merged.transport?.trim();
|
|
61
|
+
const transport = rawTransport === "auto" || rawTransport === "webhook" || rawTransport === "websocket" ? rawTransport : void 0;
|
|
80
62
|
return {
|
|
81
63
|
accountId,
|
|
82
64
|
enabled,
|
|
@@ -86,10 +68,34 @@ function resolveVamaAccount(params) {
|
|
|
86
68
|
webhookSecret,
|
|
87
69
|
webhookSecretFile,
|
|
88
70
|
bothubUrl,
|
|
71
|
+
webhookUrl,
|
|
72
|
+
transport,
|
|
89
73
|
config: merged
|
|
90
74
|
};
|
|
91
75
|
}
|
|
92
76
|
//#endregion
|
|
77
|
+
//#region extensions/vama/src/host-pairing-access.ts
|
|
78
|
+
/** Scope pairing store operations to one channel/account pair for plugin-facing helpers. */
|
|
79
|
+
function createScopedPairingAccess(params) {
|
|
80
|
+
const resolvedAccountId = normalizeAccountId(params.accountId);
|
|
81
|
+
return {
|
|
82
|
+
accountId: resolvedAccountId,
|
|
83
|
+
readAllowFromStore: () => params.core.channel.pairing.readAllowFromStore({
|
|
84
|
+
channel: params.channel,
|
|
85
|
+
accountId: resolvedAccountId
|
|
86
|
+
}),
|
|
87
|
+
readStoreForDmPolicy: (provider, accountId) => params.core.channel.pairing.readAllowFromStore({
|
|
88
|
+
channel: provider,
|
|
89
|
+
accountId: normalizeAccountId(accountId)
|
|
90
|
+
}),
|
|
91
|
+
upsertPairingRequest: (input) => params.core.channel.pairing.upsertPairingRequest({
|
|
92
|
+
channel: params.channel,
|
|
93
|
+
accountId: resolvedAccountId,
|
|
94
|
+
...input
|
|
95
|
+
})
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
93
99
|
//#region extensions/vama/src/dedup.ts
|
|
94
100
|
const DEDUP_TTL_MS = 1440 * 60 * 1e3;
|
|
95
101
|
const MEMORY_MAX_SIZE = 1e3;
|
|
@@ -703,6 +709,335 @@ async function handleVamaMessage(params) {
|
|
|
703
709
|
}
|
|
704
710
|
}
|
|
705
711
|
//#endregion
|
|
712
|
+
//#region extensions/vama/src/ws.ts
|
|
713
|
+
/**
|
|
714
|
+
* WebSocket transport for inbound BotHub events.
|
|
715
|
+
*
|
|
716
|
+
* BotHub exposes `GET /v1/bot/ws` (Authorization: Bot <token>). When a bot
|
|
717
|
+
* has `websocket_enabled` set server-side, BotHub prefers delivering events
|
|
718
|
+
* over the live WebSocket and only falls back to the registered webhook when
|
|
719
|
+
* no connection is up. Frames are the exact same JSON envelopes as webhook
|
|
720
|
+
* POST bodies (`{type, timestamp, bot_id, data}`) — no HMAC headers, since
|
|
721
|
+
* authentication already happened at the upgrade.
|
|
722
|
+
*
|
|
723
|
+
* Why this exists: a connected WebSocket needs NO public URL — no tunnel,
|
|
724
|
+
* no reverse proxy, works behind any NAT. That makes it the easiest possible
|
|
725
|
+
* "bring your own claw" transport. The flag is off by default server-side,
|
|
726
|
+
* so this client is written to probe first and fall back to webhook-only
|
|
727
|
+
* silently — the day BotHub enables WebSocket for a bot, the very next
|
|
728
|
+
* gateway start picks it up with zero config changes.
|
|
729
|
+
*
|
|
730
|
+
* Server behavior this client is built against (BotHub `internal/ws/hub.go`):
|
|
731
|
+
* - server pings every 30s, expects a pong within 60s (undici auto-pongs)
|
|
732
|
+
* - client frames are ignored (bots send via REST) — we never send
|
|
733
|
+
* - a new connection for the same bot replaces the old one server-side
|
|
734
|
+
*/
|
|
735
|
+
/** Reconnect schedule. Index advances per consecutive failure, clamps at the tail. */
|
|
736
|
+
const DEFAULT_BACKOFF_MS = [
|
|
737
|
+
1e3,
|
|
738
|
+
2e3,
|
|
739
|
+
5e3,
|
|
740
|
+
1e4,
|
|
741
|
+
3e4
|
|
742
|
+
];
|
|
743
|
+
function resolveVamaWsUrl(bothubUrl) {
|
|
744
|
+
const base = new URL(bothubUrl);
|
|
745
|
+
base.protocol = base.protocol === "http:" ? "ws:" : "wss:";
|
|
746
|
+
base.pathname = `${base.pathname.replace(/\/$/, "")}/v1/bot/ws`;
|
|
747
|
+
base.search = "";
|
|
748
|
+
base.hash = "";
|
|
749
|
+
return base.toString();
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Plain authed GET against the WS endpoint to learn whether an upgrade can
|
|
753
|
+
* ever succeed, without burning a real upgrade attempt. The server checks
|
|
754
|
+
* token auth and `websocket_enabled` BEFORE upgrading, so a non-upgrade GET
|
|
755
|
+
* deterministically distinguishes "would work" (400/426 from the upgrader
|
|
756
|
+
* rejecting a plain GET) from "will never work" (403 disabled / 401 / 404).
|
|
757
|
+
*
|
|
758
|
+
* Throws on transport errors — callers treat those as retryable.
|
|
759
|
+
*/
|
|
760
|
+
async function checkVamaWebSocketSupport(account, deps = {}) {
|
|
761
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
762
|
+
const url = new URL(account.bothubUrl ?? "https://bothub.vama.com");
|
|
763
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}/v1/bot/ws`;
|
|
764
|
+
const res = await fetchImpl(url.toString(), {
|
|
765
|
+
method: "GET",
|
|
766
|
+
headers: { Authorization: `Bot ${account.botToken ?? ""}` }
|
|
767
|
+
});
|
|
768
|
+
const body = await res.text().catch(() => "");
|
|
769
|
+
if (res.status === 400 || res.status === 426) return { supported: true };
|
|
770
|
+
if (res.status === 403) {
|
|
771
|
+
let code;
|
|
772
|
+
try {
|
|
773
|
+
code = JSON.parse(body).error;
|
|
774
|
+
} catch {}
|
|
775
|
+
return {
|
|
776
|
+
supported: false,
|
|
777
|
+
reason: code === "websocket_disabled" ? "disabled" : "http_403"
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
if (res.status === 401) return {
|
|
781
|
+
supported: false,
|
|
782
|
+
reason: "unauthorized"
|
|
783
|
+
};
|
|
784
|
+
if (res.status === 404) return {
|
|
785
|
+
supported: false,
|
|
786
|
+
reason: "unsupported"
|
|
787
|
+
};
|
|
788
|
+
return {
|
|
789
|
+
supported: false,
|
|
790
|
+
reason: `http_${res.status}`
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
function sleep$1(ms, abortSignal) {
|
|
794
|
+
return new Promise((resolve) => {
|
|
795
|
+
if (ms <= 0 || abortSignal?.aborted) {
|
|
796
|
+
resolve();
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const timer = setTimeout(() => {
|
|
800
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
801
|
+
resolve();
|
|
802
|
+
}, ms);
|
|
803
|
+
const onAbort = () => {
|
|
804
|
+
clearTimeout(timer);
|
|
805
|
+
resolve();
|
|
806
|
+
};
|
|
807
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Starts the WebSocket transport loop. Never throws; the loop ends either on
|
|
812
|
+
* abort, or when the preflight rules WS out (caller stays on webhook
|
|
813
|
+
* delivery — BotHub falls back server-side automatically).
|
|
814
|
+
*/
|
|
815
|
+
function startVamaWebSocket(opts) {
|
|
816
|
+
const { account, onEvent, log, error } = opts;
|
|
817
|
+
const accountId = account.accountId;
|
|
818
|
+
const mode = opts.mode ?? "auto";
|
|
819
|
+
const backoff = opts.backoffMs ?? DEFAULT_BACKOFF_MS;
|
|
820
|
+
const WebSocketCtor = opts.deps?.webSocketCtor ?? WebSocket;
|
|
821
|
+
let state = "connecting";
|
|
822
|
+
let active;
|
|
823
|
+
const abort = new AbortController();
|
|
824
|
+
if (opts.abortSignal?.aborted) abort.abort();
|
|
825
|
+
else opts.abortSignal?.addEventListener("abort", () => abort.abort(), { once: true });
|
|
826
|
+
const aborted = () => abort.signal.aborted;
|
|
827
|
+
const connectOnce = () => new Promise((resolve) => {
|
|
828
|
+
const ws = new WebSocketCtor(resolveVamaWsUrl(account.bothubUrl ?? "https://bothub.vama.com"), { headers: { Authorization: `Bot ${account.botToken ?? ""}` } });
|
|
829
|
+
active = ws;
|
|
830
|
+
let opened = false;
|
|
831
|
+
let settled = false;
|
|
832
|
+
const settle = () => {
|
|
833
|
+
if (!settled) {
|
|
834
|
+
settled = true;
|
|
835
|
+
active = void 0;
|
|
836
|
+
resolve({ opened });
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
ws.addEventListener("open", () => {
|
|
840
|
+
opened = true;
|
|
841
|
+
state = "open";
|
|
842
|
+
log(`vama[${accountId}]: WebSocket connected to BotHub — events arrive in real time`);
|
|
843
|
+
});
|
|
844
|
+
ws.addEventListener("message", (ev) => {
|
|
845
|
+
let parsed;
|
|
846
|
+
try {
|
|
847
|
+
parsed = JSON.parse(String(ev.data));
|
|
848
|
+
} catch {
|
|
849
|
+
error(`vama[${accountId}]: ignoring malformed WebSocket frame`);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
try {
|
|
853
|
+
onEvent(parsed);
|
|
854
|
+
} catch (err) {
|
|
855
|
+
error(`vama[${accountId}]: WebSocket event handler error: ${String(err)}`);
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
ws.addEventListener("error", () => {});
|
|
859
|
+
ws.addEventListener("close", () => settle());
|
|
860
|
+
});
|
|
861
|
+
const loop = async () => {
|
|
862
|
+
let failures = 0;
|
|
863
|
+
while (!aborted()) {
|
|
864
|
+
let preflight;
|
|
865
|
+
try {
|
|
866
|
+
preflight = await checkVamaWebSocketSupport(account, opts.deps);
|
|
867
|
+
} catch (err) {
|
|
868
|
+
failures += 1;
|
|
869
|
+
const delay = backoff[Math.min(failures - 1, backoff.length - 1)];
|
|
870
|
+
if (failures === 1) log(`vama[${accountId}]: WebSocket preflight failed (${err instanceof Error ? err.message : String(err)}); retrying in ${Math.round(delay / 1e3)}s`);
|
|
871
|
+
await sleep$1(delay, abort.signal);
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (!preflight.supported) {
|
|
875
|
+
if (mode === "websocket") error(`vama[${accountId}]: transport is set to "websocket" but BotHub refused (${preflight.reason}). ` + (preflight.reason === "disabled" ? "WebSocket delivery is not enabled for this bot — falling back to webhook delivery. Remove transport or set it to \"auto\"." : "Falling back to webhook delivery."));
|
|
876
|
+
else if (preflight.reason !== "disabled") log(`vama[${accountId}]: WebSocket unavailable (${preflight.reason}); using webhook delivery`);
|
|
877
|
+
state = "fallback";
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
if (aborted()) break;
|
|
881
|
+
state = "connecting";
|
|
882
|
+
const { opened } = await connectOnce();
|
|
883
|
+
if (aborted()) break;
|
|
884
|
+
if (opened) {
|
|
885
|
+
failures = 0;
|
|
886
|
+
log(`vama[${accountId}]: WebSocket closed; reconnecting`);
|
|
887
|
+
} else failures += 1;
|
|
888
|
+
const delay = opened ? backoff[0] : backoff[Math.min(failures - 1, backoff.length - 1)];
|
|
889
|
+
await sleep$1(Math.round(delay * (.8 + Math.random() * .4)), abort.signal);
|
|
890
|
+
}
|
|
891
|
+
state = "stopped";
|
|
892
|
+
};
|
|
893
|
+
return {
|
|
894
|
+
state: () => state,
|
|
895
|
+
done: loop().catch((err) => {
|
|
896
|
+
state = "stopped";
|
|
897
|
+
error(`vama[${accountId}]: WebSocket transport loop crashed: ${String(err)}`);
|
|
898
|
+
}),
|
|
899
|
+
stop: () => {
|
|
900
|
+
abort.abort();
|
|
901
|
+
try {
|
|
902
|
+
active?.close();
|
|
903
|
+
} catch {}
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
//#endregion
|
|
908
|
+
//#region extensions/vama/src/probe.ts
|
|
909
|
+
const probeCache = /* @__PURE__ */ new Map();
|
|
910
|
+
const PROBE_CACHE_TTL_MS = 600 * 1e3;
|
|
911
|
+
const MAX_PROBE_CACHE_SIZE = 64;
|
|
912
|
+
async function probeVama(account) {
|
|
913
|
+
if (!account.botToken) return {
|
|
914
|
+
ok: false,
|
|
915
|
+
error: "missing credentials (botToken)"
|
|
916
|
+
};
|
|
917
|
+
const cacheKey = account.accountId;
|
|
918
|
+
const cached = probeCache.get(cacheKey);
|
|
919
|
+
if (cached && cached.expiresAt > Date.now()) return cached.result;
|
|
920
|
+
try {
|
|
921
|
+
const me = await createBotHubClient(account).getMe();
|
|
922
|
+
const botId = me.bot_id;
|
|
923
|
+
const registeredUrl = me.webhook_url?.trim() || void 0;
|
|
924
|
+
if (!registeredUrl) {
|
|
925
|
+
let wsEnabled = false;
|
|
926
|
+
if (account.transport !== "webhook") try {
|
|
927
|
+
wsEnabled = (await checkVamaWebSocketSupport(account)).supported;
|
|
928
|
+
} catch {}
|
|
929
|
+
if (!wsEnabled) return {
|
|
930
|
+
ok: false,
|
|
931
|
+
botId,
|
|
932
|
+
error: "bot token is valid, but no webhook URL is registered with BotHub — Vama cannot deliver messages to this gateway (the agent shows \"Awaiting claw\"). Set channels.vama.webhookUrl to your gateway's public URL (e.g. https://your-host/vama/events) and restart the gateway, or re-run `openclaw onboard`."
|
|
933
|
+
};
|
|
934
|
+
const wsResult = {
|
|
935
|
+
ok: true,
|
|
936
|
+
botId,
|
|
937
|
+
wsEnabled: true
|
|
938
|
+
};
|
|
939
|
+
probeCache.set(cacheKey, {
|
|
940
|
+
result: wsResult,
|
|
941
|
+
expiresAt: Date.now() + PROBE_CACHE_TTL_MS
|
|
942
|
+
});
|
|
943
|
+
return wsResult;
|
|
944
|
+
}
|
|
945
|
+
const result = {
|
|
946
|
+
ok: true,
|
|
947
|
+
botId,
|
|
948
|
+
webhookUrl: registeredUrl
|
|
949
|
+
};
|
|
950
|
+
probeCache.set(cacheKey, {
|
|
951
|
+
result,
|
|
952
|
+
expiresAt: Date.now() + PROBE_CACHE_TTL_MS
|
|
953
|
+
});
|
|
954
|
+
if (probeCache.size > MAX_PROBE_CACHE_SIZE) {
|
|
955
|
+
const oldest = probeCache.keys().next().value;
|
|
956
|
+
if (oldest !== void 0) probeCache.delete(oldest);
|
|
957
|
+
}
|
|
958
|
+
return result;
|
|
959
|
+
} catch (err) {
|
|
960
|
+
return {
|
|
961
|
+
ok: false,
|
|
962
|
+
error: err instanceof Error ? err.message : String(err)
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
function clearProbeCache() {
|
|
967
|
+
probeCache.clear();
|
|
968
|
+
}
|
|
969
|
+
//#endregion
|
|
970
|
+
//#region extensions/vama/src/register.ts
|
|
971
|
+
/**
|
|
972
|
+
* Outer retry schedule for startup webhook registration. The BotHub
|
|
973
|
+
* client already retries transport errors per call (fetchWithRetry);
|
|
974
|
+
* these delays cover application-level failures around gateway boot —
|
|
975
|
+
* e.g. the tunnel in front of the listener still coming up, or BotHub
|
|
976
|
+
* rejecting the URL while DNS for a freshly-minted tunnel propagates.
|
|
977
|
+
*/
|
|
978
|
+
const DEFAULT_RETRY_DELAYS_MS = [5e3, 15e3];
|
|
979
|
+
function sleep(ms, abortSignal) {
|
|
980
|
+
return new Promise((resolve) => {
|
|
981
|
+
if (abortSignal?.aborted) {
|
|
982
|
+
resolve();
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
const timer = setTimeout(() => {
|
|
986
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
987
|
+
resolve();
|
|
988
|
+
}, ms);
|
|
989
|
+
const onAbort = () => {
|
|
990
|
+
clearTimeout(timer);
|
|
991
|
+
resolve();
|
|
992
|
+
};
|
|
993
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Register `account.webhookUrl` with BotHub (PUT /v1/bot/webhook).
|
|
998
|
+
*
|
|
999
|
+
* Called fire-and-forget from the monitor once the local webhook server
|
|
1000
|
+
* is listening (ordering matters: BotHub replays buffered messages
|
|
1001
|
+
* synchronously on registration, so the listener must be up first).
|
|
1002
|
+
*
|
|
1003
|
+
* Never throws — a registration failure must not take down the channel
|
|
1004
|
+
* (outbound sends still work; the probe surfaces the broken inbound
|
|
1005
|
+
* path). Returns true when registration succeeded.
|
|
1006
|
+
*/
|
|
1007
|
+
async function registerVamaWebhook(opts) {
|
|
1008
|
+
const { account, log, error, abortSignal } = opts;
|
|
1009
|
+
const url = account.webhookUrl;
|
|
1010
|
+
if (!url) return false;
|
|
1011
|
+
const accountId = account.accountId;
|
|
1012
|
+
const webhookPath = account.config?.webhookPath ?? "/vama/events";
|
|
1013
|
+
try {
|
|
1014
|
+
const parsed = new URL(url);
|
|
1015
|
+
if (parsed.pathname !== webhookPath) log(`vama[${accountId}]: webhookUrl path "${parsed.pathname}" differs from webhookPath "${webhookPath}" — BotHub will POST to ${url}; make sure that forwards to the local listener path ${webhookPath}`);
|
|
1016
|
+
} catch {
|
|
1017
|
+
error(`vama[${accountId}]: webhookUrl is not a valid URL: ${url} — skipping registration`);
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
const delays = opts.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
|
|
1021
|
+
let lastErr;
|
|
1022
|
+
for (let attempt = 0; attempt <= delays.length; attempt++) {
|
|
1023
|
+
if (abortSignal?.aborted) return false;
|
|
1024
|
+
try {
|
|
1025
|
+
await createBotHubClient(account).registerWebhook({ url });
|
|
1026
|
+
log(`vama[${accountId}]: webhook registered with BotHub: ${url}`);
|
|
1027
|
+
clearProbeCache();
|
|
1028
|
+
return true;
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
lastErr = err;
|
|
1031
|
+
if (attempt < delays.length) {
|
|
1032
|
+
log(`vama[${accountId}]: webhook registration attempt ${attempt + 1} failed (${err instanceof Error ? err.message : String(err)}); retrying in ${Math.round(delays[attempt] / 1e3)}s`);
|
|
1033
|
+
await sleep(delays[attempt], abortSignal);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
error(`vama[${accountId}]: webhook registration failed: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}. BotHub cannot deliver messages until this succeeds (the agent shows "Awaiting claw" in Vama). Check that ${url} is a public HTTPS URL that forwards to the local listener, then restart the gateway.`);
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
//#endregion
|
|
706
1041
|
//#region extensions/vama/src/webhook.ts
|
|
707
1042
|
const MAX_TIMESTAMP_AGE_S = 300;
|
|
708
1043
|
/**
|
|
@@ -820,6 +1155,20 @@ async function monitorVamaProvider(opts = {}) {
|
|
|
820
1155
|
const webhookPath = account.config?.webhookPath ?? "/vama/events";
|
|
821
1156
|
const host = account.config?.webhookHost ?? "127.0.0.1";
|
|
822
1157
|
log(`vama[${accountId}]: starting webhook server on ${host}:${port}, path ${webhookPath}...`);
|
|
1158
|
+
const dispatchEvent = (event, replayId) => {
|
|
1159
|
+
if (event.type === "message.create") {
|
|
1160
|
+
const messageEvent = event.data;
|
|
1161
|
+
handleVamaMessage({
|
|
1162
|
+
cfg,
|
|
1163
|
+
event: messageEvent,
|
|
1164
|
+
runtime: opts.runtime,
|
|
1165
|
+
accountId,
|
|
1166
|
+
replayId
|
|
1167
|
+
}).catch((err) => {
|
|
1168
|
+
error(`vama[${accountId}]: error handling message: ${String(err)}`);
|
|
1169
|
+
});
|
|
1170
|
+
} else log(`vama[${accountId}]: ignoring event type=${event.type}`);
|
|
1171
|
+
};
|
|
823
1172
|
const server = http.createServer();
|
|
824
1173
|
server.on("request", (req, res) => {
|
|
825
1174
|
if (isRateLimited(`${accountId}:${req.socket.remoteAddress ?? "unknown"}`, Date.now())) {
|
|
@@ -875,28 +1224,17 @@ async function monitorVamaProvider(opts = {}) {
|
|
|
875
1224
|
error(`vama[${accountId}]: failed to parse webhook body`);
|
|
876
1225
|
return;
|
|
877
1226
|
}
|
|
878
|
-
const eventType = event.type;
|
|
879
|
-
const deliveryId = req.headers["x-bothub-delivery-id"];
|
|
880
1227
|
const replayId = req.headers["x-replay-id"];
|
|
881
|
-
|
|
882
|
-
const messageEvent = event.data;
|
|
883
|
-
handleVamaMessage({
|
|
884
|
-
cfg,
|
|
885
|
-
event: messageEvent,
|
|
886
|
-
runtime: opts.runtime,
|
|
887
|
-
accountId,
|
|
888
|
-
replayId
|
|
889
|
-
}).catch((err) => {
|
|
890
|
-
error(`vama[${accountId}]: error handling message: ${String(err)}`);
|
|
891
|
-
});
|
|
892
|
-
} else log(`vama[${accountId}]: ignoring event type=${eventType} delivery=${deliveryId}`);
|
|
1228
|
+
dispatchEvent(event, replayId);
|
|
893
1229
|
}).catch((err) => {
|
|
894
1230
|
if (!guard.isTripped()) error(`vama[${accountId}]: webhook handler error: ${String(err)}`);
|
|
895
1231
|
guard.dispose();
|
|
896
1232
|
});
|
|
897
1233
|
});
|
|
1234
|
+
let wsHandle;
|
|
898
1235
|
return new Promise((resolve, reject) => {
|
|
899
1236
|
const cleanup = () => {
|
|
1237
|
+
wsHandle?.stop();
|
|
900
1238
|
server.close();
|
|
901
1239
|
};
|
|
902
1240
|
const handleAbort = () => {
|
|
@@ -912,6 +1250,20 @@ async function monitorVamaProvider(opts = {}) {
|
|
|
912
1250
|
opts.abortSignal?.addEventListener("abort", handleAbort, { once: true });
|
|
913
1251
|
server.listen(port, host, () => {
|
|
914
1252
|
log(`vama[${accountId}]: webhook server listening on ${host}:${port}`);
|
|
1253
|
+
registerVamaWebhook({
|
|
1254
|
+
account,
|
|
1255
|
+
log,
|
|
1256
|
+
error,
|
|
1257
|
+
abortSignal: opts.abortSignal
|
|
1258
|
+
});
|
|
1259
|
+
if (account.transport !== "webhook") wsHandle = startVamaWebSocket({
|
|
1260
|
+
account,
|
|
1261
|
+
onEvent: (event) => dispatchEvent(event),
|
|
1262
|
+
log,
|
|
1263
|
+
error,
|
|
1264
|
+
abortSignal: opts.abortSignal,
|
|
1265
|
+
mode: account.transport === "websocket" ? "websocket" : "auto"
|
|
1266
|
+
});
|
|
915
1267
|
});
|
|
916
1268
|
server.on("error", (err) => {
|
|
917
1269
|
error(`vama[${accountId}]: webhook server error: ${err}`);
|
|
@@ -922,38 +1274,4 @@ async function monitorVamaProvider(opts = {}) {
|
|
|
922
1274
|
});
|
|
923
1275
|
}
|
|
924
1276
|
//#endregion
|
|
925
|
-
|
|
926
|
-
const probeCache = /* @__PURE__ */ new Map();
|
|
927
|
-
const PROBE_CACHE_TTL_MS = 600 * 1e3;
|
|
928
|
-
const MAX_PROBE_CACHE_SIZE = 64;
|
|
929
|
-
async function probeVama(account) {
|
|
930
|
-
if (!account.botToken) return {
|
|
931
|
-
ok: false,
|
|
932
|
-
error: "missing credentials (botToken)"
|
|
933
|
-
};
|
|
934
|
-
const cacheKey = account.accountId;
|
|
935
|
-
const cached = probeCache.get(cacheKey);
|
|
936
|
-
if (cached && cached.expiresAt > Date.now()) return cached.result;
|
|
937
|
-
try {
|
|
938
|
-
const result = {
|
|
939
|
-
ok: true,
|
|
940
|
-
botId: (await createBotHubClient(account).getMe()).bot_id
|
|
941
|
-
};
|
|
942
|
-
probeCache.set(cacheKey, {
|
|
943
|
-
result,
|
|
944
|
-
expiresAt: Date.now() + PROBE_CACHE_TTL_MS
|
|
945
|
-
});
|
|
946
|
-
if (probeCache.size > MAX_PROBE_CACHE_SIZE) {
|
|
947
|
-
const oldest = probeCache.keys().next().value;
|
|
948
|
-
if (oldest !== void 0) probeCache.delete(oldest);
|
|
949
|
-
}
|
|
950
|
-
return result;
|
|
951
|
-
} catch (err) {
|
|
952
|
-
return {
|
|
953
|
-
ok: false,
|
|
954
|
-
error: err instanceof Error ? err.message : String(err)
|
|
955
|
-
};
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
//#endregion
|
|
959
|
-
export { dispatchStarted as a, resolveVamaAccount as c, buildBaseChannelStatusSummary as d, createDefaultChannelRuntimeState as f, dispatchEnded as i, DEFAULT_ACCOUNT_ID$1 as l, monitorVamaProvider as n, listVamaAccountIds as o, sendMessageVama as r, resolveDefaultVamaAccountId as s, probeVama as t, PAIRING_APPROVED_MESSAGE as u };
|
|
1277
|
+
export { dispatchStarted as a, buildBaseChannelStatusSummary as c, resolveDefaultVamaAccountId as d, resolveVamaAccount as f, dispatchEnded as i, createDefaultChannelRuntimeState as l, probeVama as n, DEFAULT_ACCOUNT_ID$1 as o, sendMessageVama as r, PAIRING_APPROVED_MESSAGE as s, monitorVamaProvider as t, listVamaAccountIds as u };
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vama/openclaw",
|
|
3
|
-
"version": "2026.5.5-
|
|
3
|
+
"version": "2026.5.5-4",
|
|
4
4
|
"description": "OpenClaw Vama channel plugin via BotHub",
|
|
5
|
+
"homepage": "https://web.vama.com/connect-guide",
|
|
5
6
|
"repository": {
|
|
6
7
|
"type": "git",
|
|
7
8
|
"url": "https://github.com/VamaSingapore/openclaw"
|
|
@@ -10,14 +11,6 @@
|
|
|
10
11
|
"dependencies": {
|
|
11
12
|
"undici": "8.2.0"
|
|
12
13
|
},
|
|
13
|
-
"peerDependencies": {
|
|
14
|
-
"openclaw": ">=2026.5.12"
|
|
15
|
-
},
|
|
16
|
-
"peerDependenciesMeta": {
|
|
17
|
-
"openclaw": {
|
|
18
|
-
"optional": true
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
14
|
"openclaw": {
|
|
22
15
|
"extensions": [
|
|
23
16
|
"./index.ts"
|
|
@@ -53,6 +46,15 @@
|
|
|
53
46
|
},
|
|
54
47
|
"files": [
|
|
55
48
|
"dist/**",
|
|
56
|
-
"openclaw.plugin.json"
|
|
57
|
-
|
|
49
|
+
"openclaw.plugin.json",
|
|
50
|
+
"README.md"
|
|
51
|
+
],
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"openclaw": ">=2026.5.12"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"openclaw": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
59
|
+
}
|
|
58
60
|
}
|