@vama/openclaw 2026.5.5-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-C0vtNv5b.js +12 -0
- package/dist/api.js +3 -0
- package/dist/channel-plugin-api-CcZ_y9pT.js +700 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/client-AsD46gcK.js +367 -0
- package/dist/index.js +55 -0
- package/dist/probe-B2hFOc2Y.js +959 -0
- package/dist/runtime-api.js +2 -0
- package/dist/runtime-w-1oL50p.js +11 -0
- package/openclaw.plugin.json +148 -0
- package/package.json +58 -0
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
import { a as provisionBot, i as createBotHubClient, n as attachmentHintFromExtension } from "./client-AsD46gcK.js";
|
|
2
|
+
import { c as resolveVamaAccount, d as buildBaseChannelStatusSummary, f as createDefaultChannelRuntimeState, l as DEFAULT_ACCOUNT_ID$1, n as monitorVamaProvider, o as listVamaAccountIds, r as sendMessageVama, s as resolveDefaultVamaAccountId, t as probeVama, u as PAIRING_APPROVED_MESSAGE } from "./probe-B2hFOc2Y.js";
|
|
3
|
+
import { t as getVamaRuntime } from "./runtime-w-1oL50p.js";
|
|
4
|
+
import { jsonResult, readStringOrNumberParam, readStringParam } from "openclaw/plugin-sdk/channel-actions";
|
|
5
|
+
import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
|
|
6
|
+
import { DEFAULT_ACCOUNT_ID, addWildcardAllowFrom, createStandardChannelSetupStatus, formatDocsLink, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
|
|
7
|
+
//#region extensions/vama/src/channel-actions.ts
|
|
8
|
+
/**
|
|
9
|
+
* Channel-actions adapter for the shared `message` agent tool.
|
|
10
|
+
*
|
|
11
|
+
* Vama bots previously had exactly one outbound-media mechanism: emit a
|
|
12
|
+
* `MEDIA:/path/to/file` line inside the assistant reply text, which
|
|
13
|
+
* `reply-dispatcher.ts` parses out and uploads via `client.sendFile`. That
|
|
14
|
+
* path still works (see `reply-dispatcher.ts:parseInlineMediaTargets`) and
|
|
15
|
+
* remains the default for normal turn replies — but it is brittle:
|
|
16
|
+
*
|
|
17
|
+
* - The model has to remember the exact format (line-start `MEDIA:` + path)
|
|
18
|
+
* - Code-fenced or quoted variants don't match the line-anchored regex
|
|
19
|
+
* - Long-context decay degrades format-directive following more than
|
|
20
|
+
* tool-call adherence on every frontier model
|
|
21
|
+
* - There is no schema validation; mistakes silently become text replies
|
|
22
|
+
*
|
|
23
|
+
* Registering a `ChannelMessageActionAdapter` here mirrors the pattern used
|
|
24
|
+
* by every other multimedia channel in openclaw (telegram, discord,
|
|
25
|
+
* matrix, etc.). It exposes a structured `message({action: "send", to:
|
|
26
|
+
* "<channelId>", media: "/path/to/file.html", caption: "..."})` tool call.
|
|
27
|
+
* The platform's `message-action-discovery` layer picks this up and
|
|
28
|
+
* publishes the right schema fragments on the agent's `message` tool. The
|
|
29
|
+
* model now has two paths:
|
|
30
|
+
*
|
|
31
|
+
* 1. (Preferred) Call the `message` tool with `media`/`path`/`filePath`
|
|
32
|
+
* — schema-validated, returns a typed `{messageId, channelId}` tool
|
|
33
|
+
* result the model can read.
|
|
34
|
+
* 2. (Fallback) Emit `MEDIA:/path` in plain text — `reply-dispatcher`
|
|
35
|
+
* still parses it.
|
|
36
|
+
*
|
|
37
|
+
* Both paths converge on the same `client.sendFile` underneath and the
|
|
38
|
+
* same BotHub `POST /v1/bot/messages/file` endpoint. Existing dedup,
|
|
39
|
+
* filename preservation, and attachment-hint inference are unchanged.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* Extension-only file-extension helper. Duplicated from `outbound.ts` and
|
|
43
|
+
* `reply-dispatcher.ts` rather than factored out — those copies are
|
|
44
|
+
* intentionally narrow (basename-of-path slice, no normalisation) and we
|
|
45
|
+
* want this adapter to behave identically. If a future refactor centralises
|
|
46
|
+
* extension parsing in the plugin-SDK, all three sites can collapse.
|
|
47
|
+
*/
|
|
48
|
+
function extensionOf$1(path) {
|
|
49
|
+
const idx = path.lastIndexOf(".");
|
|
50
|
+
if (idx < 0 || idx === path.length - 1) return "";
|
|
51
|
+
if (idx < Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"))) return "";
|
|
52
|
+
return path.slice(idx + 1).toLowerCase();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolve a media argument (typically the `media`, `path`, or `filePath`
|
|
56
|
+
* field of the tool call) to a local filesystem path that
|
|
57
|
+
* `BotHubClient.sendFile` can read. Mirrors `localPathFromMediaUrl` in
|
|
58
|
+
* `outbound.ts` and `reply-dispatcher.ts` so all three Vama outbound
|
|
59
|
+
* paths agree on which inputs count as "uploadable from disk".
|
|
60
|
+
*
|
|
61
|
+
* Remote URLs (`http://` / `https://`) return `null` — the outbound side
|
|
62
|
+
* doesn't fetch remote bytes yet (no SSRF guard, no allowlist), so the
|
|
63
|
+
* caller falls back to sending the URL inline as text. That matches the
|
|
64
|
+
* fallback `outbound.ts:sendMedia` already implements.
|
|
65
|
+
*/
|
|
66
|
+
function localPathFromMediaArg(raw) {
|
|
67
|
+
if (raw.startsWith("file://")) return raw.slice(7);
|
|
68
|
+
if (raw.startsWith("/") || raw.startsWith("./")) return raw;
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function describeVamaMessageTool({ cfg, accountId }) {
|
|
72
|
+
if (!resolveVamaAccount({
|
|
73
|
+
cfg,
|
|
74
|
+
accountId: accountId ?? void 0
|
|
75
|
+
}).configured) return {
|
|
76
|
+
actions: [],
|
|
77
|
+
capabilities: [],
|
|
78
|
+
schema: null
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
actions: ["send"],
|
|
82
|
+
capabilities: [],
|
|
83
|
+
schema: null
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Read the channel/conversation target from the tool params. The model can
|
|
88
|
+
* supply any of `to` / `channelId` / `target` (the last is the routing
|
|
89
|
+
* schema's canonical name). Accept all three so a structured caller can
|
|
90
|
+
* use whichever maps best to their context.
|
|
91
|
+
*/
|
|
92
|
+
function readVamaTarget(params) {
|
|
93
|
+
const value = readStringOrNumberParam(params, "to") ?? readStringOrNumberParam(params, "channelId") ?? readStringOrNumberParam(params, "target", { required: true });
|
|
94
|
+
if (!value) throw new Error("Vama send: target channel id required (use `to` or `channelId`)");
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Read the media argument the model supplied. The shared message tool
|
|
99
|
+
* exposes three optional fields that can carry an attachment path: `media`
|
|
100
|
+
* is openclaw's canonical name, `path` and `filePath` are the
|
|
101
|
+
* historical aliases. Accept all three so we don't second-guess which
|
|
102
|
+
* name the model picks.
|
|
103
|
+
*/
|
|
104
|
+
function readVamaMediaArg(params) {
|
|
105
|
+
return readStringParam(params, "media") ?? readStringParam(params, "path") ?? readStringParam(params, "filePath");
|
|
106
|
+
}
|
|
107
|
+
function readVamaCaption(params) {
|
|
108
|
+
return readStringParam(params, "caption") ?? readStringParam(params, "message");
|
|
109
|
+
}
|
|
110
|
+
function readVamaParentId(params) {
|
|
111
|
+
return readStringOrNumberParam(params, "replyTo") ?? readStringOrNumberParam(params, "replyToMessageId") ?? readStringOrNumberParam(params, "parentId");
|
|
112
|
+
}
|
|
113
|
+
const vamaMessageActions = {
|
|
114
|
+
describeMessageTool: describeVamaMessageTool,
|
|
115
|
+
resolveExecutionMode: () => "gateway",
|
|
116
|
+
extractToolSend: ({ args }) => extractToolSend(args, "send"),
|
|
117
|
+
handleAction: async ({ action, params, cfg, accountId }) => {
|
|
118
|
+
if (action !== "send") throw new Error(`Unsupported Vama action: ${action}`);
|
|
119
|
+
const account = resolveVamaAccount({
|
|
120
|
+
cfg,
|
|
121
|
+
accountId: accountId ?? void 0
|
|
122
|
+
});
|
|
123
|
+
if (!account.configured) throw new Error(`Vama account "${account.accountId}" not configured`);
|
|
124
|
+
const to = readVamaTarget(params);
|
|
125
|
+
const mediaArg = readVamaMediaArg(params);
|
|
126
|
+
const caption = readVamaCaption(params);
|
|
127
|
+
const parentId = readVamaParentId(params);
|
|
128
|
+
if (!mediaArg) {
|
|
129
|
+
const result = await sendMessageVama({
|
|
130
|
+
cfg,
|
|
131
|
+
to,
|
|
132
|
+
text: caption ?? "",
|
|
133
|
+
accountId: account.accountId,
|
|
134
|
+
parentId
|
|
135
|
+
});
|
|
136
|
+
return jsonResult({
|
|
137
|
+
ok: true,
|
|
138
|
+
messageId: result.messageId,
|
|
139
|
+
channelId: result.channelId
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const localPath = localPathFromMediaArg(mediaArg);
|
|
143
|
+
if (!localPath) {
|
|
144
|
+
const result = await sendMessageVama({
|
|
145
|
+
cfg,
|
|
146
|
+
to,
|
|
147
|
+
text: caption?.trim() ? `${caption.trim()}\n${mediaArg}` : mediaArg,
|
|
148
|
+
accountId: account.accountId,
|
|
149
|
+
parentId
|
|
150
|
+
});
|
|
151
|
+
return jsonResult({
|
|
152
|
+
ok: true,
|
|
153
|
+
messageId: result.messageId,
|
|
154
|
+
channelId: result.channelId,
|
|
155
|
+
warning: "remote_media_unsupported_sent_as_link"
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const result = await createBotHubClient(account).sendFile({
|
|
159
|
+
channelId: to,
|
|
160
|
+
path: localPath,
|
|
161
|
+
caption: caption?.trim() || void 0,
|
|
162
|
+
attachmentType: attachmentHintFromExtension(extensionOf$1(localPath)),
|
|
163
|
+
parentId
|
|
164
|
+
});
|
|
165
|
+
return jsonResult({
|
|
166
|
+
ok: true,
|
|
167
|
+
messageId: result.message_id,
|
|
168
|
+
channelId: result.channel_id
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region extensions/vama/src/outbound.ts
|
|
174
|
+
/**
|
|
175
|
+
* Resolve a media URL/path to a local filesystem path. The reply-dispatcher
|
|
176
|
+
* normalises agent output through `splitMediaFromOutput` before reaching
|
|
177
|
+
* the outbound adapter, so URLs are usually already absolute paths to
|
|
178
|
+
* artefacts under the agent workspace (e.g.
|
|
179
|
+
* `/home/app/.openclaw/workspace/output/foo.pdf`). Remote URLs (`http*`)
|
|
180
|
+
* are not supported on the outbound side yet — we'd have to download them
|
|
181
|
+
* first, which raises an SSRF question we'd rather defer until there's a
|
|
182
|
+
* real producer. Returning `null` here causes the adapter to fall back to
|
|
183
|
+
* sending the URL as plain text, preserving the legacy behaviour.
|
|
184
|
+
*/
|
|
185
|
+
function localPathFromMediaUrl(mediaUrl) {
|
|
186
|
+
if (mediaUrl.startsWith("file://")) return mediaUrl.slice(7);
|
|
187
|
+
if (mediaUrl.startsWith("/") || mediaUrl.startsWith("./")) return mediaUrl;
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
function extensionOf(path) {
|
|
191
|
+
const idx = path.lastIndexOf(".");
|
|
192
|
+
if (idx < 0 || idx === path.length - 1) return "";
|
|
193
|
+
if (idx < Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"))) return "";
|
|
194
|
+
return path.slice(idx + 1).toLowerCase();
|
|
195
|
+
}
|
|
196
|
+
const vamaOutbound = {
|
|
197
|
+
deliveryMode: "direct",
|
|
198
|
+
chunker: (text, limit) => getVamaRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
199
|
+
chunkerMode: "markdown",
|
|
200
|
+
textChunkLimit: 1e4,
|
|
201
|
+
sendText: async ({ cfg, to, text, accountId }) => {
|
|
202
|
+
const result = await sendMessageVama({
|
|
203
|
+
cfg,
|
|
204
|
+
to,
|
|
205
|
+
text,
|
|
206
|
+
accountId: accountId ?? void 0
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
channel: "vama",
|
|
210
|
+
messageId: result.messageId,
|
|
211
|
+
channelId: result.channelId
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
|
|
215
|
+
const account = resolveVamaAccount({
|
|
216
|
+
cfg,
|
|
217
|
+
accountId: accountId ?? void 0
|
|
218
|
+
});
|
|
219
|
+
if (!account.configured) throw new Error(`Vama account "${account.accountId}" not configured`);
|
|
220
|
+
if (!mediaUrl) {
|
|
221
|
+
const result = await sendMessageVama({
|
|
222
|
+
cfg,
|
|
223
|
+
to,
|
|
224
|
+
text: text ?? "",
|
|
225
|
+
accountId: accountId ?? void 0
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
channel: "vama",
|
|
229
|
+
messageId: result.messageId,
|
|
230
|
+
channelId: result.channelId
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const localPath = localPathFromMediaUrl(mediaUrl);
|
|
234
|
+
if (!localPath) {
|
|
235
|
+
const result = await sendMessageVama({
|
|
236
|
+
cfg,
|
|
237
|
+
to,
|
|
238
|
+
text: text && text.trim().length > 0 ? `${text.trim()}\n${mediaUrl}` : mediaUrl,
|
|
239
|
+
accountId: accountId ?? void 0
|
|
240
|
+
});
|
|
241
|
+
return {
|
|
242
|
+
channel: "vama",
|
|
243
|
+
messageId: result.messageId,
|
|
244
|
+
channelId: result.channelId
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const result = await createBotHubClient(account).sendFile({
|
|
248
|
+
channelId: to,
|
|
249
|
+
path: localPath,
|
|
250
|
+
caption: text?.trim() || void 0,
|
|
251
|
+
attachmentType: attachmentHintFromExtension(extensionOf(localPath))
|
|
252
|
+
});
|
|
253
|
+
return {
|
|
254
|
+
channel: "vama",
|
|
255
|
+
messageId: result.message_id,
|
|
256
|
+
channelId: result.channel_id
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region extensions/vama/src/setup-surface.ts
|
|
262
|
+
const channel = "vama";
|
|
263
|
+
function setVamaDmPolicy(cfg, dmPolicy) {
|
|
264
|
+
const allowFrom = dmPolicy === "open" ? addWildcardAllowFrom(cfg.channels?.vama?.allowFrom)?.map((entry) => entry) : void 0;
|
|
265
|
+
return {
|
|
266
|
+
...cfg,
|
|
267
|
+
channels: {
|
|
268
|
+
...cfg.channels,
|
|
269
|
+
vama: {
|
|
270
|
+
...cfg.channels?.vama,
|
|
271
|
+
dmPolicy,
|
|
272
|
+
...allowFrom ? { allowFrom } : {}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function setVamaAllowFrom(cfg, allowFrom) {
|
|
278
|
+
return {
|
|
279
|
+
...cfg,
|
|
280
|
+
channels: {
|
|
281
|
+
...cfg.channels,
|
|
282
|
+
vama: {
|
|
283
|
+
...cfg.channels?.vama,
|
|
284
|
+
allowFrom
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function parseAllowFromInput(raw) {
|
|
290
|
+
return raw.split(/[\n,;]+/g).map((entry) => entry.trim()).filter(Boolean);
|
|
291
|
+
}
|
|
292
|
+
async function promptVamaAllowFrom(params) {
|
|
293
|
+
const existing = params.cfg.channels?.vama?.allowFrom ?? [];
|
|
294
|
+
await params.prompter.note(["Allowlist Vama DMs by user ID.", "Enter one or more Vama user IDs, separated by commas."].join("\n"), "Vama allowlist");
|
|
295
|
+
while (true) {
|
|
296
|
+
const parts = parseAllowFromInput(await params.prompter.text({
|
|
297
|
+
message: "Vama allowFrom (user IDs)",
|
|
298
|
+
placeholder: "user_xxxxx, user_yyyyy",
|
|
299
|
+
initialValue: existing[0] ? String(existing[0]) : void 0,
|
|
300
|
+
validate: (value) => (value ?? "").trim() ? void 0 : "Required"
|
|
301
|
+
}));
|
|
302
|
+
if (parts.length === 0) {
|
|
303
|
+
await params.prompter.note("Enter at least one user.", "Vama allowlist");
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const unique = [...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...parts])];
|
|
307
|
+
return setVamaAllowFrom(params.cfg, unique);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const vamaDmPolicy = {
|
|
311
|
+
label: "Vama",
|
|
312
|
+
channel,
|
|
313
|
+
policyKey: "channels.vama.dmPolicy",
|
|
314
|
+
allowFromKey: "channels.vama.allowFrom",
|
|
315
|
+
getCurrent: (cfg) => (cfg.channels?.vama)?.dmPolicy ?? "open",
|
|
316
|
+
setPolicy: (cfg, policy) => setVamaDmPolicy(cfg, policy),
|
|
317
|
+
promptAllowFrom: promptVamaAllowFrom
|
|
318
|
+
};
|
|
319
|
+
const vamaPlugin = {
|
|
320
|
+
id: "vama",
|
|
321
|
+
meta: {
|
|
322
|
+
id: "vama",
|
|
323
|
+
label: "Vama",
|
|
324
|
+
selectionLabel: "Vama",
|
|
325
|
+
docsPath: "/channels/vama",
|
|
326
|
+
docsLabel: "vama",
|
|
327
|
+
blurb: "Vama chat integration via BotHub.",
|
|
328
|
+
order: 80
|
|
329
|
+
},
|
|
330
|
+
pairing: {
|
|
331
|
+
idLabel: "vamaUserId",
|
|
332
|
+
normalizeAllowEntry: (entry) => entry.trim(),
|
|
333
|
+
notifyApproval: async ({ cfg, id }) => {
|
|
334
|
+
const account = resolveVamaAccount({ cfg });
|
|
335
|
+
if (!account.configured) return;
|
|
336
|
+
try {
|
|
337
|
+
const { createBotHubClient } = await import("./client-AsD46gcK.js").then((n) => n.r);
|
|
338
|
+
await createBotHubClient(account).sendMessage({
|
|
339
|
+
userId: id,
|
|
340
|
+
text: PAIRING_APPROVED_MESSAGE
|
|
341
|
+
});
|
|
342
|
+
} catch {}
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
capabilities: {
|
|
346
|
+
chatTypes: ["direct"],
|
|
347
|
+
polls: false,
|
|
348
|
+
threads: true,
|
|
349
|
+
media: true,
|
|
350
|
+
reactions: false,
|
|
351
|
+
edit: false,
|
|
352
|
+
reply: true
|
|
353
|
+
},
|
|
354
|
+
reload: { configPrefixes: ["channels.vama"] },
|
|
355
|
+
configSchema: { schema: {
|
|
356
|
+
type: "object",
|
|
357
|
+
additionalProperties: false,
|
|
358
|
+
properties: {
|
|
359
|
+
enabled: { type: "boolean" },
|
|
360
|
+
botToken: { type: "string" },
|
|
361
|
+
webhookSecret: { type: "string" },
|
|
362
|
+
webhookSecretFile: { type: "string" },
|
|
363
|
+
bothubUrl: { type: "string" },
|
|
364
|
+
webhookPath: { type: "string" },
|
|
365
|
+
webhookPort: {
|
|
366
|
+
type: "integer",
|
|
367
|
+
minimum: 1
|
|
368
|
+
},
|
|
369
|
+
webhookHost: { type: "string" },
|
|
370
|
+
dmPolicy: {
|
|
371
|
+
type: "string",
|
|
372
|
+
enum: [
|
|
373
|
+
"open",
|
|
374
|
+
"pairing",
|
|
375
|
+
"allowlist"
|
|
376
|
+
]
|
|
377
|
+
},
|
|
378
|
+
allowFrom: {
|
|
379
|
+
type: "array",
|
|
380
|
+
items: { oneOf: [{ type: "string" }, { type: "number" }] }
|
|
381
|
+
},
|
|
382
|
+
textChunkLimit: {
|
|
383
|
+
type: "integer",
|
|
384
|
+
minimum: 1
|
|
385
|
+
},
|
|
386
|
+
accounts: {
|
|
387
|
+
type: "object",
|
|
388
|
+
additionalProperties: {
|
|
389
|
+
type: "object",
|
|
390
|
+
properties: {
|
|
391
|
+
enabled: { type: "boolean" },
|
|
392
|
+
name: { type: "string" },
|
|
393
|
+
botToken: { type: "string" },
|
|
394
|
+
webhookSecret: { type: "string" },
|
|
395
|
+
webhookSecretFile: { type: "string" },
|
|
396
|
+
bothubUrl: { type: "string" },
|
|
397
|
+
webhookPath: { type: "string" },
|
|
398
|
+
webhookPort: {
|
|
399
|
+
type: "integer",
|
|
400
|
+
minimum: 1
|
|
401
|
+
},
|
|
402
|
+
webhookHost: { type: "string" }
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
} },
|
|
408
|
+
config: {
|
|
409
|
+
listAccountIds: (cfg) => listVamaAccountIds(cfg),
|
|
410
|
+
resolveAccount: (cfg, accountId) => resolveVamaAccount({
|
|
411
|
+
cfg,
|
|
412
|
+
accountId
|
|
413
|
+
}),
|
|
414
|
+
defaultAccountId: (cfg) => resolveDefaultVamaAccountId(cfg),
|
|
415
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
|
416
|
+
if (accountId === DEFAULT_ACCOUNT_ID$1) return {
|
|
417
|
+
...cfg,
|
|
418
|
+
channels: {
|
|
419
|
+
...cfg.channels,
|
|
420
|
+
vama: {
|
|
421
|
+
...cfg.channels?.vama,
|
|
422
|
+
enabled
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
const vamaCfg = cfg.channels?.vama;
|
|
427
|
+
return {
|
|
428
|
+
...cfg,
|
|
429
|
+
channels: {
|
|
430
|
+
...cfg.channels,
|
|
431
|
+
vama: {
|
|
432
|
+
...vamaCfg,
|
|
433
|
+
accounts: {
|
|
434
|
+
...vamaCfg?.accounts,
|
|
435
|
+
[accountId]: {
|
|
436
|
+
...vamaCfg?.accounts?.[accountId],
|
|
437
|
+
enabled
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
},
|
|
444
|
+
deleteAccount: ({ cfg, accountId }) => {
|
|
445
|
+
if (accountId === DEFAULT_ACCOUNT_ID$1) {
|
|
446
|
+
const next = { ...cfg };
|
|
447
|
+
const nextChannels = { ...cfg.channels };
|
|
448
|
+
delete nextChannels.vama;
|
|
449
|
+
if (Object.keys(nextChannels).length > 0) next.channels = nextChannels;
|
|
450
|
+
else delete next.channels;
|
|
451
|
+
return next;
|
|
452
|
+
}
|
|
453
|
+
const vamaCfg = cfg.channels?.vama;
|
|
454
|
+
const accounts = { ...vamaCfg?.accounts };
|
|
455
|
+
delete accounts[accountId];
|
|
456
|
+
return {
|
|
457
|
+
...cfg,
|
|
458
|
+
channels: {
|
|
459
|
+
...cfg.channels,
|
|
460
|
+
vama: {
|
|
461
|
+
...vamaCfg,
|
|
462
|
+
accounts: Object.keys(accounts).length > 0 ? accounts : void 0
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
},
|
|
467
|
+
isConfigured: (account) => account.configured,
|
|
468
|
+
describeAccount: (account) => ({
|
|
469
|
+
accountId: account.accountId,
|
|
470
|
+
enabled: account.enabled,
|
|
471
|
+
configured: account.configured,
|
|
472
|
+
name: account.name,
|
|
473
|
+
bothubUrl: account.bothubUrl
|
|
474
|
+
}),
|
|
475
|
+
resolveAllowFrom: ({ cfg, accountId }) => {
|
|
476
|
+
return (resolveVamaAccount({
|
|
477
|
+
cfg,
|
|
478
|
+
accountId
|
|
479
|
+
}).config?.allowFrom ?? []).map((entry) => String(entry));
|
|
480
|
+
},
|
|
481
|
+
formatAllowFrom: ({ allowFrom }) => allowFrom.map((entry) => String(entry).trim()).filter(Boolean)
|
|
482
|
+
},
|
|
483
|
+
security: { collectWarnings: ({ cfg, accountId }) => {
|
|
484
|
+
const account = resolveVamaAccount({
|
|
485
|
+
cfg,
|
|
486
|
+
accountId
|
|
487
|
+
});
|
|
488
|
+
if ((account.config?.dmPolicy ?? "open") !== "open") return [];
|
|
489
|
+
return [`- Vama[${account.accountId}]: dmPolicy="open" allows any user to DM the bot. Set channels.vama.dmPolicy="allowlist" + channels.vama.allowFrom to restrict senders.`];
|
|
490
|
+
} },
|
|
491
|
+
setup: {
|
|
492
|
+
resolveAccountId: () => DEFAULT_ACCOUNT_ID$1,
|
|
493
|
+
applyAccountConfig: ({ cfg, accountId }) => {
|
|
494
|
+
if (!accountId || accountId === DEFAULT_ACCOUNT_ID$1) return {
|
|
495
|
+
...cfg,
|
|
496
|
+
channels: {
|
|
497
|
+
...cfg.channels,
|
|
498
|
+
vama: {
|
|
499
|
+
...cfg.channels?.vama,
|
|
500
|
+
enabled: true
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
const vamaCfg = cfg.channels?.vama;
|
|
505
|
+
return {
|
|
506
|
+
...cfg,
|
|
507
|
+
channels: {
|
|
508
|
+
...cfg.channels,
|
|
509
|
+
vama: {
|
|
510
|
+
...vamaCfg,
|
|
511
|
+
accounts: {
|
|
512
|
+
...vamaCfg?.accounts,
|
|
513
|
+
[accountId]: {
|
|
514
|
+
...vamaCfg?.accounts?.[accountId],
|
|
515
|
+
enabled: true
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
setupWizard: {
|
|
524
|
+
channel,
|
|
525
|
+
status: createStandardChannelSetupStatus({
|
|
526
|
+
channelLabel: "Vama",
|
|
527
|
+
configuredLabel: "configured",
|
|
528
|
+
unconfiguredLabel: "needs BotHub credentials",
|
|
529
|
+
configuredHint: "configured",
|
|
530
|
+
unconfiguredHint: "needs credentials",
|
|
531
|
+
configuredScore: 2,
|
|
532
|
+
unconfiguredScore: 0,
|
|
533
|
+
includeStatusLine: true,
|
|
534
|
+
resolveConfigured: ({ cfg }) => resolveVamaAccount({ cfg }).configured
|
|
535
|
+
}),
|
|
536
|
+
introNote: {
|
|
537
|
+
title: "Vama setup",
|
|
538
|
+
lines: [
|
|
539
|
+
"Vama needs BotHub credentials.",
|
|
540
|
+
"You will be prompted for your Vama username to auto-provision a bot.",
|
|
541
|
+
`Docs: ${formatDocsLink("/channels/vama", "channels/vama")}`
|
|
542
|
+
],
|
|
543
|
+
shouldShow: ({ cfg }) => !resolveVamaAccount({ cfg }).configured
|
|
544
|
+
},
|
|
545
|
+
resolveAccountIdForConfigure: () => DEFAULT_ACCOUNT_ID,
|
|
546
|
+
resolveShouldPromptAccountIds: () => false,
|
|
547
|
+
credentials: [],
|
|
548
|
+
finalize: async ({ cfg, prompter }) => {
|
|
549
|
+
const vamaCfg = cfg.channels?.vama;
|
|
550
|
+
const hasCredentials = Boolean(vamaCfg?.botToken?.trim());
|
|
551
|
+
let next = cfg;
|
|
552
|
+
if (hasCredentials) {
|
|
553
|
+
if (await prompter.confirm({
|
|
554
|
+
message: "Vama credentials already configured. Keep them?",
|
|
555
|
+
initialValue: true
|
|
556
|
+
})) {
|
|
557
|
+
next = {
|
|
558
|
+
...next,
|
|
559
|
+
channels: {
|
|
560
|
+
...next.channels,
|
|
561
|
+
vama: {
|
|
562
|
+
...next.channels?.vama,
|
|
563
|
+
enabled: true
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
return { cfg: next };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const bothubUrl = vamaCfg?.bothubUrl?.trim() || "https://bothub.vama.com";
|
|
571
|
+
const username = (await prompter.text({
|
|
572
|
+
message: "Your Vama username (for bot provisioning)",
|
|
573
|
+
validate: (value) => (value ?? "").trim() ? void 0 : "Required"
|
|
574
|
+
})).trim();
|
|
575
|
+
await prompter.note("Provisioning bot via BotHub...", "Vama setup");
|
|
576
|
+
let botToken;
|
|
577
|
+
let webhookSecret;
|
|
578
|
+
try {
|
|
579
|
+
const result = await provisionBot({
|
|
580
|
+
bothubUrl,
|
|
581
|
+
targetUsername: username
|
|
582
|
+
});
|
|
583
|
+
botToken = result.bot_token;
|
|
584
|
+
webhookSecret = result.webhook_secret;
|
|
585
|
+
const status = result.created ? "New bot created" : "Existing bot re-provisioned";
|
|
586
|
+
await prompter.note(`${status} (ID: ${result.bot_id})`, "Vama provisioning");
|
|
587
|
+
} catch (err) {
|
|
588
|
+
await prompter.note(`Provisioning failed: ${String(err)}\nYou can manually set botToken and webhookSecret in the config.`, "Vama provisioning error");
|
|
589
|
+
return { cfg: next };
|
|
590
|
+
}
|
|
591
|
+
const webhookPort = Number(await prompter.text({
|
|
592
|
+
message: "Webhook port (for receiving messages)",
|
|
593
|
+
initialValue: String(vamaCfg?.webhookPort ?? 3001),
|
|
594
|
+
validate: (value) => {
|
|
595
|
+
const n = Number(value);
|
|
596
|
+
if (Number.isNaN(n) || n < 1 || n > 65535) return "Must be a valid port (1-65535)";
|
|
597
|
+
}
|
|
598
|
+
}));
|
|
599
|
+
next = {
|
|
600
|
+
...next,
|
|
601
|
+
channels: {
|
|
602
|
+
...next.channels,
|
|
603
|
+
vama: {
|
|
604
|
+
...next.channels?.vama,
|
|
605
|
+
enabled: true,
|
|
606
|
+
botToken,
|
|
607
|
+
webhookSecret,
|
|
608
|
+
bothubUrl,
|
|
609
|
+
webhookPort
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
const publicWebhookUrl = (await prompter.text({
|
|
614
|
+
message: "Public webhook URL for BotHub to send events (leave blank to skip)",
|
|
615
|
+
placeholder: "https://your-gateway-host.example.com/vama/events",
|
|
616
|
+
initialValue: "",
|
|
617
|
+
validate: (value) => {
|
|
618
|
+
const v = (value ?? "").trim();
|
|
619
|
+
if (!v) return;
|
|
620
|
+
try {
|
|
621
|
+
new URL(v);
|
|
622
|
+
return;
|
|
623
|
+
} catch {
|
|
624
|
+
return "Must be a valid URL (or leave blank to skip)";
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
})).trim();
|
|
628
|
+
if (publicWebhookUrl) try {
|
|
629
|
+
await createBotHubClient(resolveVamaAccount({ cfg: next })).registerWebhook({ url: publicWebhookUrl });
|
|
630
|
+
await prompter.note(`Webhook registered: ${publicWebhookUrl}`, "Vama webhook registration");
|
|
631
|
+
} catch (err) {
|
|
632
|
+
await prompter.note(`Webhook registration failed: ${String(err)}\nYou can register later via the BotHub API.`, "Vama webhook registration");
|
|
633
|
+
}
|
|
634
|
+
else await prompter.note("Skipped webhook registration. Run the setup wizard again once your gateway is publicly reachable to register a webhook URL.", "Vama webhook registration");
|
|
635
|
+
const account = resolveVamaAccount({ cfg: next });
|
|
636
|
+
try {
|
|
637
|
+
const probe = await probeVama(account);
|
|
638
|
+
if (probe.ok) await prompter.note(`Connected successfully (bot ${probe.botId ?? "unknown"})`, "Vama connection test");
|
|
639
|
+
else await prompter.note(`Connection test failed: ${probe.error ?? "unknown error"}`, "Vama connection test");
|
|
640
|
+
} catch (err) {
|
|
641
|
+
await prompter.note(`Connection test failed: ${String(err)}`, "Vama connection test");
|
|
642
|
+
}
|
|
643
|
+
return { cfg: next };
|
|
644
|
+
},
|
|
645
|
+
completionNote: {
|
|
646
|
+
title: "Vama next steps",
|
|
647
|
+
lines: [
|
|
648
|
+
"Next: restart gateway and verify status.",
|
|
649
|
+
"Command: openclaw channels status --probe",
|
|
650
|
+
`Docs: ${formatDocsLink("/channels/vama", "channels/vama")}`
|
|
651
|
+
]
|
|
652
|
+
},
|
|
653
|
+
dmPolicy: vamaDmPolicy,
|
|
654
|
+
disable: (cfg) => setSetupChannelEnabled(cfg, channel, false)
|
|
655
|
+
},
|
|
656
|
+
outbound: vamaOutbound,
|
|
657
|
+
actions: vamaMessageActions,
|
|
658
|
+
status: {
|
|
659
|
+
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID$1, { port: null }),
|
|
660
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
661
|
+
...buildBaseChannelStatusSummary(snapshot),
|
|
662
|
+
port: snapshot.port ?? null,
|
|
663
|
+
probe: snapshot.probe,
|
|
664
|
+
lastProbeAt: snapshot.lastProbeAt ?? null
|
|
665
|
+
}),
|
|
666
|
+
probeAccount: async ({ account }) => await probeVama(account),
|
|
667
|
+
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
|
668
|
+
accountId: account.accountId,
|
|
669
|
+
enabled: account.enabled,
|
|
670
|
+
configured: account.configured,
|
|
671
|
+
name: account.name,
|
|
672
|
+
bothubUrl: account.bothubUrl,
|
|
673
|
+
running: runtime?.running ?? false,
|
|
674
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
675
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
676
|
+
lastError: runtime?.lastError ?? null,
|
|
677
|
+
port: runtime?.port ?? null,
|
|
678
|
+
probe
|
|
679
|
+
})
|
|
680
|
+
},
|
|
681
|
+
gateway: { startAccount: async (ctx) => {
|
|
682
|
+
const port = resolveVamaAccount({
|
|
683
|
+
cfg: ctx.cfg,
|
|
684
|
+
accountId: ctx.accountId
|
|
685
|
+
}).config?.webhookPort ?? null;
|
|
686
|
+
ctx.setStatus({
|
|
687
|
+
accountId: ctx.accountId,
|
|
688
|
+
port
|
|
689
|
+
});
|
|
690
|
+
ctx.log?.info(`starting vama[${ctx.accountId}]`);
|
|
691
|
+
return monitorVamaProvider({
|
|
692
|
+
config: ctx.cfg,
|
|
693
|
+
runtime: ctx.runtime,
|
|
694
|
+
abortSignal: ctx.abortSignal,
|
|
695
|
+
accountId: ctx.accountId
|
|
696
|
+
});
|
|
697
|
+
} }
|
|
698
|
+
};
|
|
699
|
+
//#endregion
|
|
700
|
+
export { vamaPlugin as t };
|