@vama/openclaw 2026.5.5-4 → 2026.5.5-5
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/dist/{api-lhR0QgC_.js → api-DftFHS2s.js} +2 -1
- package/dist/api.js +3 -2
- package/dist/{channel-plugin-api-YGbkWmVM.js → channel-plugin-api-KLKTLQFV.js} +67 -6
- package/dist/channel-plugin-api.js +1 -1
- package/dist/cli-F0a1J9Rj.js +150 -0
- package/dist/{client-AsD46gcK.js → client-BzhfASX8.js} +61 -1
- package/dist/connect-code-Bbp9J6TH.js +49 -0
- package/dist/index.js +23 -5
- package/dist/{monitor-CHFjRu2J.js → monitor-DLgOtdLQ.js} +5 -320
- package/dist/probe-Cf1xor23.js +319 -0
- package/dist/runtime-api.js +1 -1
- package/openclaw.plugin.json +21 -0
- package/package.json +1 -1
- /package/dist/{runtime-w-1oL50p.js → runtime-Cq09Y5cd.js} +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import "./probe-Cf1xor23.js";
|
|
2
|
+
import { i as dispatchStarted, r as dispatchEnded } from "./monitor-DLgOtdLQ.js";
|
|
2
3
|
//#region extensions/vama/src/subagent-keepalive-hooks.ts
|
|
3
4
|
function registerVamaSubagentKeepaliveHooks(api) {
|
|
4
5
|
api.on("subagent_spawned", () => {
|
package/dist/api.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import { n as probeVama
|
|
2
|
-
import { t as
|
|
1
|
+
import { n as probeVama } from "./probe-Cf1xor23.js";
|
|
2
|
+
import { n as sendMessageVama, t as monitorVamaProvider } from "./monitor-DLgOtdLQ.js";
|
|
3
|
+
import { t as registerVamaSubagentKeepaliveHooks } from "./api-DftFHS2s.js";
|
|
3
4
|
export { monitorVamaProvider, probeVama, registerVamaSubagentKeepaliveHooks, sendMessageVama };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import {
|
|
3
|
-
import { t as
|
|
1
|
+
import { a as exchangeConnectCode, i as createBotHubClient, n as attachmentHintFromExtension, o as provisionBot } from "./client-BzhfASX8.js";
|
|
2
|
+
import { a as resolveDefaultVamaAccountId, i as listVamaAccountIds, n as probeVama, o as resolveVamaAccount } from "./probe-Cf1xor23.js";
|
|
3
|
+
import { a as DEFAULT_ACCOUNT_ID$1, c as createDefaultChannelRuntimeState, n as sendMessageVama, o as PAIRING_APPROVED_MESSAGE, s as buildBaseChannelStatusSummary, t as monitorVamaProvider } from "./monitor-DLgOtdLQ.js";
|
|
4
|
+
import { t as getVamaRuntime } from "./runtime-Cq09Y5cd.js";
|
|
5
|
+
import { t as parseVamaConnectCode } from "./connect-code-Bbp9J6TH.js";
|
|
4
6
|
import { jsonResult, readStringOrNumberParam, readStringParam } from "openclaw/plugin-sdk/channel-actions";
|
|
5
7
|
import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
|
|
6
8
|
import { DEFAULT_ACCOUNT_ID, addWildcardAllowFrom, createStandardChannelSetupStatus, formatDocsLink, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
|
|
@@ -334,7 +336,7 @@ const vamaPlugin = {
|
|
|
334
336
|
const account = resolveVamaAccount({ cfg });
|
|
335
337
|
if (!account.configured) return;
|
|
336
338
|
try {
|
|
337
|
-
const { createBotHubClient } = await import("./client-
|
|
339
|
+
const { createBotHubClient } = await import("./client-BzhfASX8.js").then((n) => n.r);
|
|
338
340
|
await createBotHubClient(account).sendMessage({
|
|
339
341
|
userId: id,
|
|
340
342
|
text: PAIRING_APPROVED_MESSAGE
|
|
@@ -554,8 +556,8 @@ const vamaPlugin = {
|
|
|
554
556
|
introNote: {
|
|
555
557
|
title: "Vama setup",
|
|
556
558
|
lines: [
|
|
557
|
-
"Vama
|
|
558
|
-
"
|
|
559
|
+
"Easiest: paste a connect code from the Vama app (Settings → AI Agents).",
|
|
560
|
+
"Alternative: provision a bot by Vama username.",
|
|
559
561
|
`Docs: ${formatDocsLink("/channels/vama", "channels/vama")}`
|
|
560
562
|
],
|
|
561
563
|
shouldShow: ({ cfg }) => !resolveVamaAccount({ cfg }).configured
|
|
@@ -585,6 +587,65 @@ const vamaPlugin = {
|
|
|
585
587
|
return { cfg: next };
|
|
586
588
|
}
|
|
587
589
|
}
|
|
590
|
+
if (await prompter.select({
|
|
591
|
+
message: "How do you want to connect to Vama?",
|
|
592
|
+
options: [{
|
|
593
|
+
value: "code",
|
|
594
|
+
label: "Paste a connect code (recommended)",
|
|
595
|
+
hint: "Vama app → Settings → AI Agents → Connect your own — no tunnel needed"
|
|
596
|
+
}, {
|
|
597
|
+
value: "username",
|
|
598
|
+
label: "Provision by Vama username",
|
|
599
|
+
hint: "creates/reuses a bot for your account; webhook URL needed later"
|
|
600
|
+
}],
|
|
601
|
+
initialValue: "code"
|
|
602
|
+
}) === "code") {
|
|
603
|
+
const parsedCode = parseVamaConnectCode((await prompter.text({
|
|
604
|
+
message: "Connect code (vmc1_… or 64-char hex)",
|
|
605
|
+
validate: (value) => {
|
|
606
|
+
try {
|
|
607
|
+
parseVamaConnectCode(value ?? "");
|
|
608
|
+
return;
|
|
609
|
+
} catch (err) {
|
|
610
|
+
return err instanceof Error ? err.message : String(err);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
})).trim());
|
|
614
|
+
const codeBothubUrl = parsedCode.bothubUrl || vamaCfg?.bothubUrl?.trim() || "https://bothub.vama.com";
|
|
615
|
+
await prompter.note("Exchanging connect code with BotHub...", "Vama setup");
|
|
616
|
+
try {
|
|
617
|
+
const exchanged = await exchangeConnectCode({
|
|
618
|
+
bothubUrl: codeBothubUrl,
|
|
619
|
+
code: parsedCode.code
|
|
620
|
+
});
|
|
621
|
+
next = {
|
|
622
|
+
...next,
|
|
623
|
+
channels: {
|
|
624
|
+
...next.channels,
|
|
625
|
+
vama: {
|
|
626
|
+
...next.channels?.vama,
|
|
627
|
+
enabled: true,
|
|
628
|
+
botToken: exchanged.bot_token,
|
|
629
|
+
webhookSecret: exchanged.webhook_secret,
|
|
630
|
+
...codeBothubUrl !== "https://bothub.vama.com" ? { bothubUrl: codeBothubUrl } : {}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
const label = exchanged.display_name || exchanged.username || exchanged.bot_id;
|
|
635
|
+
await prompter.note(exchanged.websocket_enabled ? `Connected as ${label}. WebSocket delivery is enabled — no tunnel or public URL needed.` : `Connected as ${label}. This bot uses webhook delivery — set channels.vama.webhookUrl once your gateway is publicly reachable.`, "Vama connect");
|
|
636
|
+
} catch (err) {
|
|
637
|
+
await prompter.note(`Connect code exchange failed: ${err instanceof Error ? err.message : String(err)}\nMint a fresh code in Vama settings, or re-run setup and choose username provisioning.`, "Vama connect error");
|
|
638
|
+
return { cfg: next };
|
|
639
|
+
}
|
|
640
|
+
const account = resolveVamaAccount({ cfg: next });
|
|
641
|
+
try {
|
|
642
|
+
const probe = await probeVama(account);
|
|
643
|
+
await prompter.note(probe.ok ? `Connected successfully (bot ${probe.botId ?? "unknown"})` : `Connection test failed: ${probe.error ?? "unknown error"}`, "Vama connection test");
|
|
644
|
+
} catch (err) {
|
|
645
|
+
await prompter.note(`Connection test failed: ${String(err)}`, "Vama connection test");
|
|
646
|
+
}
|
|
647
|
+
return { cfg: next };
|
|
648
|
+
}
|
|
588
649
|
const bothubUrl = vamaCfg?.bothubUrl?.trim() || "https://bothub.vama.com";
|
|
589
650
|
const username = (await prompter.text({
|
|
590
651
|
message: "Your Vama username (for bot provisioning)",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as vamaPlugin } from "./channel-plugin-api-
|
|
1
|
+
import { t as vamaPlugin } from "./channel-plugin-api-KLKTLQFV.js";
|
|
2
2
|
export { vamaPlugin };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { a as exchangeConnectCode } from "./client-BzhfASX8.js";
|
|
2
|
+
import { n as probeVama, o as resolveVamaAccount, t as clearProbeCache } from "./probe-Cf1xor23.js";
|
|
3
|
+
import { t as getVamaRuntime } from "./runtime-Cq09Y5cd.js";
|
|
4
|
+
import { t as parseVamaConnectCode } from "./connect-code-Bbp9J6TH.js";
|
|
5
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
6
|
+
//#region extensions/vama/src/cli.ts
|
|
7
|
+
/**
|
|
8
|
+
* Apply freshly exchanged credentials to the config. Default account writes
|
|
9
|
+
* top-level under channels.vama (matching the setup wizard); named accounts
|
|
10
|
+
* write under channels.vama.accounts.<id>. Existing unrelated keys
|
|
11
|
+
* (dmPolicy, webhookPort, …) are preserved.
|
|
12
|
+
*/
|
|
13
|
+
function applyVamaConnectCredentials(params) {
|
|
14
|
+
const { cfg, accountId } = params;
|
|
15
|
+
const vama = cfg.channels?.vama ?? {};
|
|
16
|
+
const credentialPatch = {
|
|
17
|
+
enabled: true,
|
|
18
|
+
botToken: params.botToken,
|
|
19
|
+
webhookSecret: params.webhookSecret
|
|
20
|
+
};
|
|
21
|
+
if (params.bothubUrl !== "https://bothub.vama.com") credentialPatch.bothubUrl = params.bothubUrl;
|
|
22
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
23
|
+
const next = {
|
|
24
|
+
...vama,
|
|
25
|
+
...credentialPatch
|
|
26
|
+
};
|
|
27
|
+
if (params.bothubUrl === "https://bothub.vama.com") delete next.bothubUrl;
|
|
28
|
+
return {
|
|
29
|
+
...cfg,
|
|
30
|
+
channels: {
|
|
31
|
+
...cfg.channels,
|
|
32
|
+
vama: {
|
|
33
|
+
...next,
|
|
34
|
+
enabled: true
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const nextAccount = {
|
|
40
|
+
...vama.accounts?.[accountId] ?? {},
|
|
41
|
+
...credentialPatch
|
|
42
|
+
};
|
|
43
|
+
if (params.bothubUrl === "https://bothub.vama.com") delete nextAccount.bothubUrl;
|
|
44
|
+
return {
|
|
45
|
+
...cfg,
|
|
46
|
+
channels: {
|
|
47
|
+
...cfg.channels,
|
|
48
|
+
vama: {
|
|
49
|
+
...vama,
|
|
50
|
+
enabled: true,
|
|
51
|
+
accounts: {
|
|
52
|
+
...vama.accounts,
|
|
53
|
+
[accountId]: nextAccount
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `openclaw vama connect <code>` — the whole BYO onboarding in one command:
|
|
61
|
+
* exchange the code for credentials, write the config, verify with a live
|
|
62
|
+
* probe. WebSocket-enabled bots (the default for codes minted from Vama
|
|
63
|
+
* settings) need no tunnel, no public URL, no webhook registration.
|
|
64
|
+
*/
|
|
65
|
+
async function runVamaConnect(params, deps = {}) {
|
|
66
|
+
const exchange = deps.exchange ?? exchangeConnectCode;
|
|
67
|
+
const probe = deps.probe ?? probeVama;
|
|
68
|
+
const log = deps.log ?? ((msg) => console.log(msg));
|
|
69
|
+
const parsed = parseVamaConnectCode(params.rawCode);
|
|
70
|
+
const runtime = getVamaRuntime();
|
|
71
|
+
const cfg = runtime.config.current();
|
|
72
|
+
const accountId = normalizeAccountId(params.account?.trim() || DEFAULT_ACCOUNT_ID);
|
|
73
|
+
const existingUrl = (cfg.channels?.vama ?? {}).bothubUrl?.trim();
|
|
74
|
+
const bothubUrl = (params.bothubUrlFlag?.trim() || parsed.bothubUrl || existingUrl || "https://bothub.vama.com").replace(/\/+$/, "");
|
|
75
|
+
log(`Exchanging connect code with ${bothubUrl}…`);
|
|
76
|
+
const exchanged = await exchange({
|
|
77
|
+
bothubUrl,
|
|
78
|
+
code: parsed.code
|
|
79
|
+
});
|
|
80
|
+
const updated = applyVamaConnectCredentials({
|
|
81
|
+
cfg,
|
|
82
|
+
accountId,
|
|
83
|
+
botToken: exchanged.bot_token,
|
|
84
|
+
webhookSecret: exchanged.webhook_secret,
|
|
85
|
+
bothubUrl
|
|
86
|
+
});
|
|
87
|
+
await runtime.config.replaceConfigFile({
|
|
88
|
+
nextConfig: updated,
|
|
89
|
+
afterWrite: { mode: "auto" }
|
|
90
|
+
});
|
|
91
|
+
clearProbeCache();
|
|
92
|
+
const result = {
|
|
93
|
+
accountId,
|
|
94
|
+
botId: exchanged.bot_id,
|
|
95
|
+
username: exchanged.username,
|
|
96
|
+
displayName: exchanged.display_name,
|
|
97
|
+
websocketEnabled: exchanged.websocket_enabled === true,
|
|
98
|
+
bothubUrl,
|
|
99
|
+
created: exchanged.created
|
|
100
|
+
};
|
|
101
|
+
try {
|
|
102
|
+
const probed = await probe(resolveVamaAccount({
|
|
103
|
+
cfg: updated,
|
|
104
|
+
accountId
|
|
105
|
+
}));
|
|
106
|
+
result.probeOk = probed.ok;
|
|
107
|
+
if (!probed.ok) result.probeError = probed.error;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
result.probeOk = false;
|
|
110
|
+
result.probeError = err instanceof Error ? err.message : String(err);
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
function printConnectResult(result) {
|
|
115
|
+
const botLabel = result.displayName || result.username || result.botId;
|
|
116
|
+
console.log("");
|
|
117
|
+
console.log(`✔ Connected to Vama as ${botLabel}${result.username ? ` (@${result.username})` : ""}`);
|
|
118
|
+
if (result.websocketEnabled) console.log(" Transport: WebSocket — no tunnel or public URL needed.");
|
|
119
|
+
else console.log(" Transport: webhook — set channels.vama.webhookUrl to a public URL (or mint a new code from Vama settings to enable WebSocket).");
|
|
120
|
+
if (result.probeOk === false && result.probeError) {
|
|
121
|
+
console.log(` Verification warning: ${result.probeError}`);
|
|
122
|
+
console.log(" Re-check later with: openclaw channels status --probe");
|
|
123
|
+
}
|
|
124
|
+
console.log("");
|
|
125
|
+
console.log(" If your gateway is running it reloads automatically; otherwise start it:");
|
|
126
|
+
console.log(" openclaw gateway");
|
|
127
|
+
}
|
|
128
|
+
function registerVamaCli({ program }) {
|
|
129
|
+
program.command("vama").description("Vama channel tools (connect codes, account state)").command("connect").description("Connect this OpenClaw to Vama with a one-time code from Vama settings (Settings → AI Agents)").argument("<code>", "connect code (vmc1_… token or 64-char hex)").option("--bothub-url <url>", "BotHub endpoint override (defaults to the URL embedded in the code)").option("--account <id>", "Vama account id to write credentials to", DEFAULT_ACCOUNT_ID).option("--json", "print the result as JSON", false).action(async (code, opts) => {
|
|
130
|
+
try {
|
|
131
|
+
const result = await runVamaConnect({
|
|
132
|
+
rawCode: code,
|
|
133
|
+
bothubUrlFlag: opts.bothubUrl,
|
|
134
|
+
account: opts.account
|
|
135
|
+
}, { log: opts.json ? () => {} : void 0 });
|
|
136
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
137
|
+
else printConnectResult(result);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
140
|
+
if (opts.json) console.log(JSON.stringify({
|
|
141
|
+
ok: false,
|
|
142
|
+
error: message
|
|
143
|
+
}, null, 2));
|
|
144
|
+
else console.error(`✖ Connect failed: ${message}`);
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
export { registerVamaCli };
|
|
@@ -19,6 +19,7 @@ var client_exports = /* @__PURE__ */ __exportAll({
|
|
|
19
19
|
FetchWithRetryError: () => FetchWithRetryError,
|
|
20
20
|
attachmentHintFromExtension: () => attachmentHintFromExtension,
|
|
21
21
|
createBotHubClient: () => createBotHubClient,
|
|
22
|
+
exchangeConnectCode: () => exchangeConnectCode,
|
|
22
23
|
mimeFromExtension: () => mimeFromExtension,
|
|
23
24
|
provisionBot: () => provisionBot
|
|
24
25
|
});
|
|
@@ -312,6 +313,65 @@ function createBotHubClient(account) {
|
|
|
312
313
|
};
|
|
313
314
|
}
|
|
314
315
|
/**
|
|
316
|
+
* Exchange a single-use connect code for bot credentials
|
|
317
|
+
* (POST /v1/provision/exchange — unauthenticated; the code IS the
|
|
318
|
+
* credential). BotHub consumes the code atomically: a second attempt with
|
|
319
|
+
* the same code fails with `invalid_code`, so callers should treat errors
|
|
320
|
+
* as "mint a fresh code in Vama settings", not retry-the-same-code.
|
|
321
|
+
*/
|
|
322
|
+
async function exchangeConnectCode(params) {
|
|
323
|
+
const baseUrl = params.bothubUrl.replace(/\/+$/, "");
|
|
324
|
+
const path = "/v1/provision/exchange";
|
|
325
|
+
const start = Date.now();
|
|
326
|
+
let resPair;
|
|
327
|
+
try {
|
|
328
|
+
resPair = await fetchWithRetry(`${baseUrl}${path}`, {
|
|
329
|
+
method: "POST",
|
|
330
|
+
headers: { "Content-Type": "application/json" },
|
|
331
|
+
body: JSON.stringify({ code: params.code })
|
|
332
|
+
});
|
|
333
|
+
} catch (err) {
|
|
334
|
+
logBothubCall({
|
|
335
|
+
method: "POST",
|
|
336
|
+
path,
|
|
337
|
+
start,
|
|
338
|
+
status: null,
|
|
339
|
+
attemptsUsed: attemptsUsedFromError(err),
|
|
340
|
+
errorMsg: unwrapFetchErrorMsg(err)
|
|
341
|
+
});
|
|
342
|
+
throw err;
|
|
343
|
+
}
|
|
344
|
+
const { res, attemptsUsed } = resPair;
|
|
345
|
+
if (!res.ok) {
|
|
346
|
+
const text = await res.text().catch(() => "");
|
|
347
|
+
logBothubCall({
|
|
348
|
+
method: "POST",
|
|
349
|
+
path,
|
|
350
|
+
start,
|
|
351
|
+
status: res.status,
|
|
352
|
+
attemptsUsed,
|
|
353
|
+
bodyText: text
|
|
354
|
+
});
|
|
355
|
+
let errorCode = "";
|
|
356
|
+
let message = text;
|
|
357
|
+
try {
|
|
358
|
+
const parsed = JSON.parse(text);
|
|
359
|
+
errorCode = parsed.error ?? "";
|
|
360
|
+
message = parsed.message ?? text;
|
|
361
|
+
} catch {}
|
|
362
|
+
if (errorCode === "invalid_code") throw new Error("Connect code is invalid, expired, or already used. Mint a fresh one in Vama settings and paste the new command.");
|
|
363
|
+
throw new Error(`BotHub exchange failed (${res.status}): ${message}`);
|
|
364
|
+
}
|
|
365
|
+
logBothubCall({
|
|
366
|
+
method: "POST",
|
|
367
|
+
path,
|
|
368
|
+
start,
|
|
369
|
+
status: res.status,
|
|
370
|
+
attemptsUsed
|
|
371
|
+
});
|
|
372
|
+
return await res.json();
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
315
375
|
* Provision a new bot via the BotHub provisioning API (unauthenticated).
|
|
316
376
|
*/
|
|
317
377
|
async function provisionBot(params) {
|
|
@@ -364,4 +424,4 @@ async function provisionBot(params) {
|
|
|
364
424
|
return await res.json();
|
|
365
425
|
}
|
|
366
426
|
//#endregion
|
|
367
|
-
export {
|
|
427
|
+
export { exchangeConnectCode as a, createBotHubClient as i, attachmentHintFromExtension as n, provisionBot as o, client_exports as r, BOTHUB_DEFAULT_URL as t };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const HEX_CODE_RE = /^[0-9a-f]{64}$/i;
|
|
2
|
+
function parseWrappedCode(raw) {
|
|
3
|
+
const payload = raw.slice(5);
|
|
4
|
+
let decoded;
|
|
5
|
+
try {
|
|
6
|
+
decoded = Buffer.from(payload, "base64url").toString("utf8");
|
|
7
|
+
} catch {
|
|
8
|
+
throw new Error("Invalid connect code: not valid base64url");
|
|
9
|
+
}
|
|
10
|
+
let parsed;
|
|
11
|
+
try {
|
|
12
|
+
parsed = JSON.parse(decoded);
|
|
13
|
+
} catch {
|
|
14
|
+
throw new Error("Invalid connect code: wrapper payload is not JSON");
|
|
15
|
+
}
|
|
16
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Invalid connect code: wrapper payload is not an object");
|
|
17
|
+
const { u, c } = parsed;
|
|
18
|
+
if (typeof c !== "string" || !HEX_CODE_RE.test(c)) throw new Error("Invalid connect code: missing or malformed exchange code");
|
|
19
|
+
let bothubUrl;
|
|
20
|
+
if (u !== void 0) {
|
|
21
|
+
if (typeof u !== "string") throw new Error("Invalid connect code: BotHub URL must be a string");
|
|
22
|
+
let parsedUrl;
|
|
23
|
+
try {
|
|
24
|
+
parsedUrl = new URL(u);
|
|
25
|
+
} catch {
|
|
26
|
+
throw new Error(`Invalid connect code: BotHub URL is not a valid URL: ${u}`);
|
|
27
|
+
}
|
|
28
|
+
if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") throw new Error(`Invalid connect code: BotHub URL must be http(s): ${u}`);
|
|
29
|
+
bothubUrl = u.replace(/\/+$/, "");
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
code: c.toLowerCase(),
|
|
33
|
+
bothubUrl
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parse a connect code as pasted by the user. Accepts the `vmc1_` wrapped
|
|
38
|
+
* form (URL + code in one token) and the bare 64-hex form. Throws a
|
|
39
|
+
* user-presentable Error for anything else.
|
|
40
|
+
*/
|
|
41
|
+
function parseVamaConnectCode(raw) {
|
|
42
|
+
const trimmed = raw.trim();
|
|
43
|
+
if (!trimmed) throw new Error("Connect code is required");
|
|
44
|
+
if (trimmed.startsWith("vmc1_")) return parseWrappedCode(trimmed);
|
|
45
|
+
if (HEX_CODE_RE.test(trimmed)) return { code: trimmed.toLowerCase() };
|
|
46
|
+
throw new Error("Invalid connect code: expected a vmc1_… token or a 64-character hex code (copy the full command from Vama settings — codes expire after 15 minutes)");
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { parseVamaConnectCode as t };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
|
-
import { n as probeVama
|
|
2
|
-
import { n as
|
|
3
|
-
import {
|
|
4
|
-
import { t as
|
|
1
|
+
import { n as probeVama } from "./probe-Cf1xor23.js";
|
|
2
|
+
import { n as sendMessageVama, t as monitorVamaProvider } from "./monitor-DLgOtdLQ.js";
|
|
3
|
+
import { n as setVamaRuntime } from "./runtime-Cq09Y5cd.js";
|
|
4
|
+
import { t as registerVamaSubagentKeepaliveHooks } from "./api-DftFHS2s.js";
|
|
5
|
+
import { t as vamaPlugin } from "./channel-plugin-api-KLKTLQFV.js";
|
|
5
6
|
import "./runtime-api.js";
|
|
7
|
+
//#region extensions/vama/src/cli-metadata.ts
|
|
8
|
+
function registerVamaCliMetadata(api) {
|
|
9
|
+
api.registerCli(async ({ program }) => {
|
|
10
|
+
const { registerVamaCli } = await import("./cli-F0a1J9Rj.js");
|
|
11
|
+
registerVamaCli({ program });
|
|
12
|
+
}, { descriptors: [{
|
|
13
|
+
name: "vama",
|
|
14
|
+
description: "Connect this OpenClaw to Vama and manage the Vama channel",
|
|
15
|
+
hasSubcommands: true
|
|
16
|
+
}] });
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
6
19
|
//#region extensions/vama/index.ts
|
|
7
20
|
const channelEntry = {
|
|
8
21
|
kind: "bundled-channel-entry",
|
|
@@ -41,10 +54,15 @@ const channelEntry = {
|
|
|
41
54
|
} }
|
|
42
55
|
},
|
|
43
56
|
register(api) {
|
|
44
|
-
|
|
57
|
+
const mode = api.registrationMode;
|
|
58
|
+
if (mode === "cli-metadata") {
|
|
59
|
+
registerVamaCliMetadata(api);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
45
62
|
setVamaRuntime(api.runtime);
|
|
46
63
|
api.registerChannel({ plugin: vamaPlugin });
|
|
47
64
|
registerVamaSubagentKeepaliveHooks(api);
|
|
65
|
+
if (mode === "discovery" || mode === "full") registerVamaCliMetadata(api);
|
|
48
66
|
},
|
|
49
67
|
loadChannelPlugin: () => vamaPlugin,
|
|
50
68
|
setChannelRuntime: (runtime) => {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { i as createBotHubClient, n as attachmentHintFromExtension } from "./client-
|
|
2
|
-
import { t as
|
|
1
|
+
import { i as createBotHubClient, n as attachmentHintFromExtension } from "./client-BzhfASX8.js";
|
|
2
|
+
import { o as resolveVamaAccount, r as startVamaWebSocket, t as clearProbeCache } from "./probe-Cf1xor23.js";
|
|
3
|
+
import { t as getVamaRuntime } from "./runtime-Cq09Y5cd.js";
|
|
3
4
|
import * as fs from "node:fs";
|
|
4
5
|
import { promises } from "node:fs";
|
|
5
6
|
import * as http from "node:http";
|
|
6
|
-
import { DEFAULT_ACCOUNT_ID
|
|
7
|
+
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
7
8
|
import path from "node:path";
|
|
8
|
-
import { WebSocket, fetch } from "undici";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
|
|
11
11
|
import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
|
|
@@ -17,63 +17,6 @@ import { createPersistentDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
|
|
|
17
17
|
import { buildBaseChannelStatusSummary, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
|
|
18
18
|
import * as crypto from "node:crypto";
|
|
19
19
|
import { createHash } from "node:crypto";
|
|
20
|
-
//#region extensions/vama/src/accounts.ts
|
|
21
|
-
function listConfiguredAccountIds(cfg) {
|
|
22
|
-
const accounts = (cfg.channels?.vama)?.accounts;
|
|
23
|
-
if (!accounts || typeof accounts !== "object") return [];
|
|
24
|
-
return Object.keys(accounts).filter(Boolean);
|
|
25
|
-
}
|
|
26
|
-
function listVamaAccountIds(cfg) {
|
|
27
|
-
const ids = listConfiguredAccountIds(cfg);
|
|
28
|
-
if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
|
|
29
|
-
return [...ids].toSorted((a, b) => a.localeCompare(b));
|
|
30
|
-
}
|
|
31
|
-
function resolveDefaultVamaAccountId(cfg) {
|
|
32
|
-
const ids = listVamaAccountIds(cfg);
|
|
33
|
-
if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
|
|
34
|
-
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
35
|
-
}
|
|
36
|
-
function resolveAccountConfig(cfg, accountId) {
|
|
37
|
-
const accounts = (cfg.channels?.vama)?.accounts;
|
|
38
|
-
if (!accounts || typeof accounts !== "object") return;
|
|
39
|
-
return accounts[accountId];
|
|
40
|
-
}
|
|
41
|
-
function mergeVamaAccountConfig(cfg, accountId) {
|
|
42
|
-
const { accounts: _ignored, ...base } = cfg.channels?.vama ?? {};
|
|
43
|
-
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
|
44
|
-
return {
|
|
45
|
-
...base,
|
|
46
|
-
...account
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function resolveVamaAccount(params) {
|
|
50
|
-
const accountId = normalizeAccountId(params.accountId);
|
|
51
|
-
const baseEnabled = (params.cfg.channels?.vama)?.enabled !== false;
|
|
52
|
-
const merged = mergeVamaAccountConfig(params.cfg, accountId);
|
|
53
|
-
const accountEnabled = merged.enabled !== false;
|
|
54
|
-
const enabled = baseEnabled && accountEnabled;
|
|
55
|
-
const botToken = merged.botToken?.trim() || void 0;
|
|
56
|
-
const webhookSecret = merged.webhookSecret?.trim() || void 0;
|
|
57
|
-
const webhookSecretFile = merged.webhookSecretFile?.trim() || void 0;
|
|
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;
|
|
62
|
-
return {
|
|
63
|
-
accountId,
|
|
64
|
-
enabled,
|
|
65
|
-
configured: Boolean(botToken),
|
|
66
|
-
name: merged.name?.trim() || void 0,
|
|
67
|
-
botToken,
|
|
68
|
-
webhookSecret,
|
|
69
|
-
webhookSecretFile,
|
|
70
|
-
bothubUrl,
|
|
71
|
-
webhookUrl,
|
|
72
|
-
transport,
|
|
73
|
-
config: merged
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
//#endregion
|
|
77
20
|
//#region extensions/vama/src/host-pairing-access.ts
|
|
78
21
|
/** Scope pairing store operations to one channel/account pair for plugin-facing helpers. */
|
|
79
22
|
function createScopedPairingAccess(params) {
|
|
@@ -709,264 +652,6 @@ async function handleVamaMessage(params) {
|
|
|
709
652
|
}
|
|
710
653
|
}
|
|
711
654
|
//#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
655
|
//#region extensions/vama/src/register.ts
|
|
971
656
|
/**
|
|
972
657
|
* Outer retry schedule for startup webhook registration. The BotHub
|
|
@@ -1274,4 +959,4 @@ async function monitorVamaProvider(opts = {}) {
|
|
|
1274
959
|
});
|
|
1275
960
|
}
|
|
1276
961
|
//#endregion
|
|
1277
|
-
export {
|
|
962
|
+
export { DEFAULT_ACCOUNT_ID$1 as a, createDefaultChannelRuntimeState as c, dispatchStarted as i, sendMessageVama as n, PAIRING_APPROVED_MESSAGE as o, dispatchEnded as r, buildBaseChannelStatusSummary as s, monitorVamaProvider as t };
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { i as createBotHubClient } from "./client-BzhfASX8.js";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
3
|
+
import { WebSocket, fetch } from "undici";
|
|
4
|
+
//#region extensions/vama/src/accounts.ts
|
|
5
|
+
function listConfiguredAccountIds(cfg) {
|
|
6
|
+
const accounts = (cfg.channels?.vama)?.accounts;
|
|
7
|
+
if (!accounts || typeof accounts !== "object") return [];
|
|
8
|
+
return Object.keys(accounts).filter(Boolean);
|
|
9
|
+
}
|
|
10
|
+
function listVamaAccountIds(cfg) {
|
|
11
|
+
const ids = listConfiguredAccountIds(cfg);
|
|
12
|
+
if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
|
|
13
|
+
return [...ids].toSorted((a, b) => a.localeCompare(b));
|
|
14
|
+
}
|
|
15
|
+
function resolveDefaultVamaAccountId(cfg) {
|
|
16
|
+
const ids = listVamaAccountIds(cfg);
|
|
17
|
+
if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
|
|
18
|
+
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
19
|
+
}
|
|
20
|
+
function resolveAccountConfig(cfg, accountId) {
|
|
21
|
+
const accounts = (cfg.channels?.vama)?.accounts;
|
|
22
|
+
if (!accounts || typeof accounts !== "object") return;
|
|
23
|
+
return accounts[accountId];
|
|
24
|
+
}
|
|
25
|
+
function mergeVamaAccountConfig(cfg, accountId) {
|
|
26
|
+
const { accounts: _ignored, ...base } = cfg.channels?.vama ?? {};
|
|
27
|
+
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
|
28
|
+
return {
|
|
29
|
+
...base,
|
|
30
|
+
...account
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function resolveVamaAccount(params) {
|
|
34
|
+
const accountId = normalizeAccountId(params.accountId);
|
|
35
|
+
const baseEnabled = (params.cfg.channels?.vama)?.enabled !== false;
|
|
36
|
+
const merged = mergeVamaAccountConfig(params.cfg, accountId);
|
|
37
|
+
const accountEnabled = merged.enabled !== false;
|
|
38
|
+
const enabled = baseEnabled && accountEnabled;
|
|
39
|
+
const botToken = merged.botToken?.trim() || void 0;
|
|
40
|
+
const webhookSecret = merged.webhookSecret?.trim() || void 0;
|
|
41
|
+
const webhookSecretFile = merged.webhookSecretFile?.trim() || void 0;
|
|
42
|
+
const bothubUrl = merged.bothubUrl?.trim() || "https://bothub.vama.com";
|
|
43
|
+
const webhookUrl = merged.webhookUrl?.trim() || void 0;
|
|
44
|
+
const rawTransport = merged.transport?.trim();
|
|
45
|
+
const transport = rawTransport === "auto" || rawTransport === "webhook" || rawTransport === "websocket" ? rawTransport : void 0;
|
|
46
|
+
return {
|
|
47
|
+
accountId,
|
|
48
|
+
enabled,
|
|
49
|
+
configured: Boolean(botToken),
|
|
50
|
+
name: merged.name?.trim() || void 0,
|
|
51
|
+
botToken,
|
|
52
|
+
webhookSecret,
|
|
53
|
+
webhookSecretFile,
|
|
54
|
+
bothubUrl,
|
|
55
|
+
webhookUrl,
|
|
56
|
+
transport,
|
|
57
|
+
config: merged
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region extensions/vama/src/ws.ts
|
|
62
|
+
/**
|
|
63
|
+
* WebSocket transport for inbound BotHub events.
|
|
64
|
+
*
|
|
65
|
+
* BotHub exposes `GET /v1/bot/ws` (Authorization: Bot <token>). When a bot
|
|
66
|
+
* has `websocket_enabled` set server-side, BotHub prefers delivering events
|
|
67
|
+
* over the live WebSocket and only falls back to the registered webhook when
|
|
68
|
+
* no connection is up. Frames are the exact same JSON envelopes as webhook
|
|
69
|
+
* POST bodies (`{type, timestamp, bot_id, data}`) — no HMAC headers, since
|
|
70
|
+
* authentication already happened at the upgrade.
|
|
71
|
+
*
|
|
72
|
+
* Why this exists: a connected WebSocket needs NO public URL — no tunnel,
|
|
73
|
+
* no reverse proxy, works behind any NAT. That makes it the easiest possible
|
|
74
|
+
* "bring your own claw" transport. The flag is off by default server-side,
|
|
75
|
+
* so this client is written to probe first and fall back to webhook-only
|
|
76
|
+
* silently — the day BotHub enables WebSocket for a bot, the very next
|
|
77
|
+
* gateway start picks it up with zero config changes.
|
|
78
|
+
*
|
|
79
|
+
* Server behavior this client is built against (BotHub `internal/ws/hub.go`):
|
|
80
|
+
* - server pings every 30s, expects a pong within 60s (undici auto-pongs)
|
|
81
|
+
* - client frames are ignored (bots send via REST) — we never send
|
|
82
|
+
* - a new connection for the same bot replaces the old one server-side
|
|
83
|
+
*/
|
|
84
|
+
/** Reconnect schedule. Index advances per consecutive failure, clamps at the tail. */
|
|
85
|
+
const DEFAULT_BACKOFF_MS = [
|
|
86
|
+
1e3,
|
|
87
|
+
2e3,
|
|
88
|
+
5e3,
|
|
89
|
+
1e4,
|
|
90
|
+
3e4
|
|
91
|
+
];
|
|
92
|
+
function resolveVamaWsUrl(bothubUrl) {
|
|
93
|
+
const base = new URL(bothubUrl);
|
|
94
|
+
base.protocol = base.protocol === "http:" ? "ws:" : "wss:";
|
|
95
|
+
base.pathname = `${base.pathname.replace(/\/$/, "")}/v1/bot/ws`;
|
|
96
|
+
base.search = "";
|
|
97
|
+
base.hash = "";
|
|
98
|
+
return base.toString();
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Plain authed GET against the WS endpoint to learn whether an upgrade can
|
|
102
|
+
* ever succeed, without burning a real upgrade attempt. The server checks
|
|
103
|
+
* token auth and `websocket_enabled` BEFORE upgrading, so a non-upgrade GET
|
|
104
|
+
* deterministically distinguishes "would work" (400/426 from the upgrader
|
|
105
|
+
* rejecting a plain GET) from "will never work" (403 disabled / 401 / 404).
|
|
106
|
+
*
|
|
107
|
+
* Throws on transport errors — callers treat those as retryable.
|
|
108
|
+
*/
|
|
109
|
+
async function checkVamaWebSocketSupport(account, deps = {}) {
|
|
110
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
111
|
+
const url = new URL(account.bothubUrl ?? "https://bothub.vama.com");
|
|
112
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}/v1/bot/ws`;
|
|
113
|
+
const res = await fetchImpl(url.toString(), {
|
|
114
|
+
method: "GET",
|
|
115
|
+
headers: { Authorization: `Bot ${account.botToken ?? ""}` }
|
|
116
|
+
});
|
|
117
|
+
const body = await res.text().catch(() => "");
|
|
118
|
+
if (res.status === 400 || res.status === 426) return { supported: true };
|
|
119
|
+
if (res.status === 403) {
|
|
120
|
+
let code;
|
|
121
|
+
try {
|
|
122
|
+
code = JSON.parse(body).error;
|
|
123
|
+
} catch {}
|
|
124
|
+
return {
|
|
125
|
+
supported: false,
|
|
126
|
+
reason: code === "websocket_disabled" ? "disabled" : "http_403"
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (res.status === 401) return {
|
|
130
|
+
supported: false,
|
|
131
|
+
reason: "unauthorized"
|
|
132
|
+
};
|
|
133
|
+
if (res.status === 404) return {
|
|
134
|
+
supported: false,
|
|
135
|
+
reason: "unsupported"
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
supported: false,
|
|
139
|
+
reason: `http_${res.status}`
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function sleep(ms, abortSignal) {
|
|
143
|
+
return new Promise((resolve) => {
|
|
144
|
+
if (ms <= 0 || abortSignal?.aborted) {
|
|
145
|
+
resolve();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const timer = setTimeout(() => {
|
|
149
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
150
|
+
resolve();
|
|
151
|
+
}, ms);
|
|
152
|
+
const onAbort = () => {
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
resolve();
|
|
155
|
+
};
|
|
156
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Starts the WebSocket transport loop. Never throws; the loop ends either on
|
|
161
|
+
* abort, or when the preflight rules WS out (caller stays on webhook
|
|
162
|
+
* delivery — BotHub falls back server-side automatically).
|
|
163
|
+
*/
|
|
164
|
+
function startVamaWebSocket(opts) {
|
|
165
|
+
const { account, onEvent, log, error } = opts;
|
|
166
|
+
const accountId = account.accountId;
|
|
167
|
+
const mode = opts.mode ?? "auto";
|
|
168
|
+
const backoff = opts.backoffMs ?? DEFAULT_BACKOFF_MS;
|
|
169
|
+
const WebSocketCtor = opts.deps?.webSocketCtor ?? WebSocket;
|
|
170
|
+
let state = "connecting";
|
|
171
|
+
let active;
|
|
172
|
+
const abort = new AbortController();
|
|
173
|
+
if (opts.abortSignal?.aborted) abort.abort();
|
|
174
|
+
else opts.abortSignal?.addEventListener("abort", () => abort.abort(), { once: true });
|
|
175
|
+
const aborted = () => abort.signal.aborted;
|
|
176
|
+
const connectOnce = () => new Promise((resolve) => {
|
|
177
|
+
const ws = new WebSocketCtor(resolveVamaWsUrl(account.bothubUrl ?? "https://bothub.vama.com"), { headers: { Authorization: `Bot ${account.botToken ?? ""}` } });
|
|
178
|
+
active = ws;
|
|
179
|
+
let opened = false;
|
|
180
|
+
let settled = false;
|
|
181
|
+
const settle = () => {
|
|
182
|
+
if (!settled) {
|
|
183
|
+
settled = true;
|
|
184
|
+
active = void 0;
|
|
185
|
+
resolve({ opened });
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
ws.addEventListener("open", () => {
|
|
189
|
+
opened = true;
|
|
190
|
+
state = "open";
|
|
191
|
+
log(`vama[${accountId}]: WebSocket connected to BotHub — events arrive in real time`);
|
|
192
|
+
});
|
|
193
|
+
ws.addEventListener("message", (ev) => {
|
|
194
|
+
let parsed;
|
|
195
|
+
try {
|
|
196
|
+
parsed = JSON.parse(String(ev.data));
|
|
197
|
+
} catch {
|
|
198
|
+
error(`vama[${accountId}]: ignoring malformed WebSocket frame`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
onEvent(parsed);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
error(`vama[${accountId}]: WebSocket event handler error: ${String(err)}`);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
ws.addEventListener("error", () => {});
|
|
208
|
+
ws.addEventListener("close", () => settle());
|
|
209
|
+
});
|
|
210
|
+
const loop = async () => {
|
|
211
|
+
let failures = 0;
|
|
212
|
+
while (!aborted()) {
|
|
213
|
+
let preflight;
|
|
214
|
+
try {
|
|
215
|
+
preflight = await checkVamaWebSocketSupport(account, opts.deps);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
failures += 1;
|
|
218
|
+
const delay = backoff[Math.min(failures - 1, backoff.length - 1)];
|
|
219
|
+
if (failures === 1) log(`vama[${accountId}]: WebSocket preflight failed (${err instanceof Error ? err.message : String(err)}); retrying in ${Math.round(delay / 1e3)}s`);
|
|
220
|
+
await sleep(delay, abort.signal);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (!preflight.supported) {
|
|
224
|
+
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."));
|
|
225
|
+
else if (preflight.reason !== "disabled") log(`vama[${accountId}]: WebSocket unavailable (${preflight.reason}); using webhook delivery`);
|
|
226
|
+
state = "fallback";
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (aborted()) break;
|
|
230
|
+
state = "connecting";
|
|
231
|
+
const { opened } = await connectOnce();
|
|
232
|
+
if (aborted()) break;
|
|
233
|
+
if (opened) {
|
|
234
|
+
failures = 0;
|
|
235
|
+
log(`vama[${accountId}]: WebSocket closed; reconnecting`);
|
|
236
|
+
} else failures += 1;
|
|
237
|
+
const delay = opened ? backoff[0] : backoff[Math.min(failures - 1, backoff.length - 1)];
|
|
238
|
+
await sleep(Math.round(delay * (.8 + Math.random() * .4)), abort.signal);
|
|
239
|
+
}
|
|
240
|
+
state = "stopped";
|
|
241
|
+
};
|
|
242
|
+
return {
|
|
243
|
+
state: () => state,
|
|
244
|
+
done: loop().catch((err) => {
|
|
245
|
+
state = "stopped";
|
|
246
|
+
error(`vama[${accountId}]: WebSocket transport loop crashed: ${String(err)}`);
|
|
247
|
+
}),
|
|
248
|
+
stop: () => {
|
|
249
|
+
abort.abort();
|
|
250
|
+
try {
|
|
251
|
+
active?.close();
|
|
252
|
+
} catch {}
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region extensions/vama/src/probe.ts
|
|
258
|
+
const probeCache = /* @__PURE__ */ new Map();
|
|
259
|
+
const PROBE_CACHE_TTL_MS = 600 * 1e3;
|
|
260
|
+
const MAX_PROBE_CACHE_SIZE = 64;
|
|
261
|
+
async function probeVama(account) {
|
|
262
|
+
if (!account.botToken) return {
|
|
263
|
+
ok: false,
|
|
264
|
+
error: "missing credentials (botToken)"
|
|
265
|
+
};
|
|
266
|
+
const cacheKey = account.accountId;
|
|
267
|
+
const cached = probeCache.get(cacheKey);
|
|
268
|
+
if (cached && cached.expiresAt > Date.now()) return cached.result;
|
|
269
|
+
try {
|
|
270
|
+
const me = await createBotHubClient(account).getMe();
|
|
271
|
+
const botId = me.bot_id;
|
|
272
|
+
const registeredUrl = me.webhook_url?.trim() || void 0;
|
|
273
|
+
if (!registeredUrl) {
|
|
274
|
+
let wsEnabled = false;
|
|
275
|
+
if (account.transport !== "webhook") try {
|
|
276
|
+
wsEnabled = (await checkVamaWebSocketSupport(account)).supported;
|
|
277
|
+
} catch {}
|
|
278
|
+
if (!wsEnabled) return {
|
|
279
|
+
ok: false,
|
|
280
|
+
botId,
|
|
281
|
+
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`."
|
|
282
|
+
};
|
|
283
|
+
const wsResult = {
|
|
284
|
+
ok: true,
|
|
285
|
+
botId,
|
|
286
|
+
wsEnabled: true
|
|
287
|
+
};
|
|
288
|
+
probeCache.set(cacheKey, {
|
|
289
|
+
result: wsResult,
|
|
290
|
+
expiresAt: Date.now() + PROBE_CACHE_TTL_MS
|
|
291
|
+
});
|
|
292
|
+
return wsResult;
|
|
293
|
+
}
|
|
294
|
+
const result = {
|
|
295
|
+
ok: true,
|
|
296
|
+
botId,
|
|
297
|
+
webhookUrl: registeredUrl
|
|
298
|
+
};
|
|
299
|
+
probeCache.set(cacheKey, {
|
|
300
|
+
result,
|
|
301
|
+
expiresAt: Date.now() + PROBE_CACHE_TTL_MS
|
|
302
|
+
});
|
|
303
|
+
if (probeCache.size > MAX_PROBE_CACHE_SIZE) {
|
|
304
|
+
const oldest = probeCache.keys().next().value;
|
|
305
|
+
if (oldest !== void 0) probeCache.delete(oldest);
|
|
306
|
+
}
|
|
307
|
+
return result;
|
|
308
|
+
} catch (err) {
|
|
309
|
+
return {
|
|
310
|
+
ok: false,
|
|
311
|
+
error: err instanceof Error ? err.message : String(err)
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function clearProbeCache() {
|
|
316
|
+
probeCache.clear();
|
|
317
|
+
}
|
|
318
|
+
//#endregion
|
|
319
|
+
export { resolveDefaultVamaAccountId as a, listVamaAccountIds as i, probeVama as n, resolveVamaAccount as o, startVamaWebSocket as r, clearProbeCache as t };
|
package/dist/runtime-api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as setVamaRuntime } from "./runtime-
|
|
1
|
+
import { n as setVamaRuntime } from "./runtime-Cq09Y5cd.js";
|
|
2
2
|
export { setVamaRuntime };
|
package/openclaw.plugin.json
CHANGED
|
@@ -37,6 +37,15 @@
|
|
|
37
37
|
"type": "string",
|
|
38
38
|
"description": "BotHub base URL. Leave blank to use the default."
|
|
39
39
|
},
|
|
40
|
+
"webhookUrl": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "Public URL BotHub should deliver webhook events to. Registered automatically on every gateway start when set."
|
|
43
|
+
},
|
|
44
|
+
"transport": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"enum": ["auto", "webhook", "websocket"],
|
|
47
|
+
"description": "Inbound event transport. auto (default) probes for WebSocket delivery and falls back to webhook; websocket requires it; webhook never attempts it."
|
|
48
|
+
},
|
|
40
49
|
"webhookPath": {
|
|
41
50
|
"type": "string",
|
|
42
51
|
"description": "HTTP path the gateway serves for inbound BotHub webhook events."
|
|
@@ -80,6 +89,8 @@
|
|
|
80
89
|
"webhookSecret": { "type": "string" },
|
|
81
90
|
"webhookSecretFile": { "type": "string" },
|
|
82
91
|
"bothubUrl": { "type": "string" },
|
|
92
|
+
"webhookUrl": { "type": "string" },
|
|
93
|
+
"transport": { "type": "string", "enum": ["auto", "webhook", "websocket"] },
|
|
83
94
|
"webhookPath": { "type": "string" },
|
|
84
95
|
"webhookPort": { "type": "integer", "minimum": 1, "maximum": 65535 },
|
|
85
96
|
"webhookHost": { "type": "string" }
|
|
@@ -115,6 +126,16 @@
|
|
|
115
126
|
"label": "BotHub URL",
|
|
116
127
|
"help": "Leave blank to use the default Vama BotHub endpoint."
|
|
117
128
|
},
|
|
129
|
+
"webhookUrl": {
|
|
130
|
+
"label": "Public webhook URL",
|
|
131
|
+
"help": "Only needed for webhook transport. WebSocket-connected bots need no public URL.",
|
|
132
|
+
"advanced": true
|
|
133
|
+
},
|
|
134
|
+
"transport": {
|
|
135
|
+
"label": "Transport",
|
|
136
|
+
"help": "auto picks WebSocket when BotHub allows it, else webhook.",
|
|
137
|
+
"advanced": true
|
|
138
|
+
},
|
|
118
139
|
"webhookPath": {
|
|
119
140
|
"label": "Webhook path",
|
|
120
141
|
"advanced": true
|
package/package.json
CHANGED
|
File without changes
|