@xmoxmo/bncr 0.6.9 → 0.7.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/package.json +2 -1
- package/scripts/check-pack.mjs +1 -0
- package/scripts/selfcheck.mjs +12 -114
- package/scripts/version-policy.mjs +75 -0
- package/src/channel.ts +5 -0
- package/src/messaging/inbound/commands.ts +1 -1
- package/src/messaging/inbound/dispatch-prep.ts +1 -1
- package/src/messaging/inbound/dispatch.ts +9 -1
- package/src/openclaw/media-runtime.ts +114 -0
- package/src/plugin/channel-plugin-bridge-group.ts +1 -1
- package/src/plugin/channel-plugin-surface-group.ts +42 -6
- package/src/plugin/inbound-acceptance.ts +1 -1
- package/src/plugin/message-send.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmoxmo/bncr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "npx -y esbuild@0.28.0 index.ts --bundle --platform=node --format=esm --target=node22 --packages=external --outfile=dist/index.js",
|
|
29
29
|
"selfcheck": "node ./scripts/selfcheck.mjs",
|
|
30
|
+
"selfcheck:release": "node ./scripts/selfcheck.mjs --release",
|
|
30
31
|
"test": "node --import ./tests/register-ts-hooks.mjs --test ./tests/**/*.test.mjs",
|
|
31
32
|
"check-register-drift": "node ./scripts/check-register-drift.mjs",
|
|
32
33
|
"check-pack": "node ./scripts/check-pack.mjs",
|
package/scripts/check-pack.mjs
CHANGED
package/scripts/selfcheck.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { validateReleaseVersionPolicy, validateVersionSyntax } from './version-policy.mjs';
|
|
6
7
|
|
|
7
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
9
|
const __dirname = path.dirname(__filename);
|
|
@@ -10,6 +11,7 @@ const root = path.resolve(__dirname, '..');
|
|
|
10
11
|
const require = createRequire(import.meta.url);
|
|
11
12
|
|
|
12
13
|
const requiredRootFiles = ['index.ts', 'openclaw.plugin.json'];
|
|
14
|
+
const requiredScriptFiles = ['scripts/version-policy.mjs'];
|
|
13
15
|
|
|
14
16
|
const requiredSourceFiles = [
|
|
15
17
|
'src/bootstrap/channel-plugin-runtime.ts',
|
|
@@ -213,7 +215,7 @@ const requiredSourceFiles = [
|
|
|
213
215
|
'src/runtime/status-worker.ts',
|
|
214
216
|
];
|
|
215
217
|
|
|
216
|
-
const requiredFiles = [...requiredRootFiles, ...requiredSourceFiles];
|
|
218
|
+
const requiredFiles = [...requiredRootFiles, ...requiredScriptFiles, ...requiredSourceFiles];
|
|
217
219
|
|
|
218
220
|
const readPackageVersion = () => {
|
|
219
221
|
const pkgPath = path.join(root, 'package.json');
|
|
@@ -238,17 +240,17 @@ const readNpmLatestVersion = (packageName) => {
|
|
|
238
240
|
const requiredOpenClawSdkSubpaths = [
|
|
239
241
|
'openclaw/plugin-sdk/channel-outbound',
|
|
240
242
|
'openclaw/plugin-sdk/channel-ingress-runtime',
|
|
241
|
-
'openclaw/plugin-sdk/channel-message',
|
|
242
243
|
'openclaw/plugin-sdk/routing',
|
|
243
244
|
'openclaw/plugin-sdk/conversation-runtime',
|
|
244
245
|
'openclaw/plugin-sdk/session-store-runtime',
|
|
245
246
|
'openclaw/plugin-sdk/core',
|
|
246
247
|
'openclaw/plugin-sdk/json-store',
|
|
247
248
|
'openclaw/plugin-sdk/param-readers',
|
|
249
|
+
'openclaw/plugin-sdk/media-runtime',
|
|
248
250
|
'openclaw/plugin-sdk/security-runtime',
|
|
249
251
|
'openclaw/plugin-sdk/status-helpers',
|
|
250
252
|
'openclaw/plugin-sdk/tool-send',
|
|
251
|
-
'openclaw/plugin-sdk/command-
|
|
253
|
+
'openclaw/plugin-sdk/command-detection',
|
|
252
254
|
'openclaw/plugin-sdk/reply-history',
|
|
253
255
|
];
|
|
254
256
|
|
|
@@ -266,120 +268,14 @@ const resolveOpenClawSdkSubpaths = () => {
|
|
|
266
268
|
});
|
|
267
269
|
};
|
|
268
270
|
|
|
269
|
-
const validateVersionPolicy = (version, latestVersion) => {
|
|
270
|
-
const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
271
|
-
if (!match) {
|
|
272
|
-
return {
|
|
273
|
-
ok: false,
|
|
274
|
-
reason: 'version must be strict semver x.y.z',
|
|
275
|
-
version,
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const patch = Number.parseInt(match[3], 10);
|
|
280
|
-
if (patch > 9) {
|
|
281
|
-
return {
|
|
282
|
-
ok: false,
|
|
283
|
-
reason: 'patch version must stay within 0-9; bump minor instead',
|
|
284
|
-
version,
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Prevent jumping minor when current minor still has unused patch slots
|
|
289
|
-
if (latestVersion) {
|
|
290
|
-
const lm = latestVersion.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
291
|
-
if (lm) {
|
|
292
|
-
const vMajor = Number.parseInt(match[1], 10);
|
|
293
|
-
const vMinor = Number.parseInt(match[2], 10);
|
|
294
|
-
const lMajor = Number.parseInt(lm[1], 10);
|
|
295
|
-
const lMinor = Number.parseInt(lm[2], 10);
|
|
296
|
-
const lPatch = Number.parseInt(lm[3], 10);
|
|
297
|
-
|
|
298
|
-
// Version unchanged
|
|
299
|
-
if (vMajor === lMajor && vMinor === lMinor && patch === lPatch) {
|
|
300
|
-
return { ok: false, reason: `version unchanged: ${version}`, version };
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Downgrade: any component decreased
|
|
304
|
-
if (
|
|
305
|
-
vMajor < lMajor ||
|
|
306
|
-
(vMajor === lMajor && vMinor < lMinor) ||
|
|
307
|
-
(vMajor === lMajor && vMinor === lMinor && patch < lPatch)
|
|
308
|
-
) {
|
|
309
|
-
return { ok: false, reason: `downgrade from ${latestVersion} to ${version}`, version };
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Patch bump: same major and minor, patch must increment by exactly +1
|
|
313
|
-
if (vMajor === lMajor && vMinor === lMinor) {
|
|
314
|
-
if (patch === lPatch + 1) return { ok: true, version };
|
|
315
|
-
return {
|
|
316
|
-
ok: false,
|
|
317
|
-
reason: `patch jump from ${latestVersion} to ${version} (delta=${patch - lPatch}); expected ${lMajor}.${lMinor}.${lPatch + 1}`,
|
|
318
|
-
version,
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Minor bump: same major, minor + 1
|
|
323
|
-
if (vMajor === lMajor && vMinor === lMinor + 1) {
|
|
324
|
-
if (lPatch === 9 && patch === 0) return { ok: true, version };
|
|
325
|
-
if (lPatch !== 9)
|
|
326
|
-
return {
|
|
327
|
-
ok: false,
|
|
328
|
-
reason: `minor bumped from ${latestVersion} to ${version} but ${9 - lPatch} patch slots remain in ${lMajor}.${lMinor}; prefer ${lMajor}.${lMinor}.${lPatch + 1}`,
|
|
329
|
-
version,
|
|
330
|
-
};
|
|
331
|
-
return {
|
|
332
|
-
ok: false,
|
|
333
|
-
reason: `minor bump must reset patch to 0; got ${vMajor}.${vMinor}.${patch}`,
|
|
334
|
-
version,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Minor jumped by more than 1
|
|
339
|
-
if (vMajor === lMajor && vMinor > lMinor + 1) {
|
|
340
|
-
return {
|
|
341
|
-
ok: false,
|
|
342
|
-
reason: `minor jumped from ${latestVersion} to ${version}; expected ${lMajor}.${lMinor + 1}.0`,
|
|
343
|
-
version,
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Major bump: major + 1, requires previous minor fully exhausted
|
|
348
|
-
if (vMajor === lMajor + 1) {
|
|
349
|
-
if (lMinor === 9 && lPatch === 9 && vMinor === 0 && patch === 0)
|
|
350
|
-
return { ok: true, version };
|
|
351
|
-
if (lMinor !== 9 || lPatch !== 9)
|
|
352
|
-
return {
|
|
353
|
-
ok: false,
|
|
354
|
-
reason: `major bumped from ${latestVersion} to ${version} but previous major series not exhausted; expected ${lMajor + 1}.0.0 after ${lMajor}.9.9`,
|
|
355
|
-
version,
|
|
356
|
-
};
|
|
357
|
-
return {
|
|
358
|
-
ok: false,
|
|
359
|
-
reason: `major bump must reset to .0.0; got ${vMajor}.${vMinor}.${patch}`,
|
|
360
|
-
version,
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Major jumped by more than 1
|
|
365
|
-
if (vMajor > lMajor + 1) {
|
|
366
|
-
return {
|
|
367
|
-
ok: false,
|
|
368
|
-
reason: `major jumped from ${latestVersion} to ${version}; expected ${lMajor + 1}.0.0`,
|
|
369
|
-
version,
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return { ok: true, version };
|
|
376
|
-
};
|
|
377
|
-
|
|
378
271
|
const missing = requiredFiles.filter((rel) => !fs.existsSync(path.join(root, rel)));
|
|
379
272
|
const version = readPackageVersion();
|
|
380
273
|
const packageName = '@xmoxmo/bncr';
|
|
381
|
-
const
|
|
382
|
-
const
|
|
274
|
+
const releaseMode = process.argv.includes('--release');
|
|
275
|
+
const latestVersion = releaseMode ? readNpmLatestVersion(packageName) : null;
|
|
276
|
+
const versionPolicy = releaseMode
|
|
277
|
+
? validateReleaseVersionPolicy(version, latestVersion)
|
|
278
|
+
: validateVersionSyntax(version);
|
|
383
279
|
const sdkSubpaths = resolveOpenClawSdkSubpaths();
|
|
384
280
|
const missingSdkSubpaths = sdkSubpaths.filter((entry) => !entry.ok);
|
|
385
281
|
const result = {
|
|
@@ -388,6 +284,8 @@ const result = {
|
|
|
388
284
|
requiredCount: requiredFiles.length,
|
|
389
285
|
missing,
|
|
390
286
|
version,
|
|
287
|
+
versionMode: releaseMode ? 'release' : 'development',
|
|
288
|
+
latestVersion,
|
|
391
289
|
versionPolicy,
|
|
392
290
|
sdkSubpaths,
|
|
393
291
|
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export const validateVersionSyntax = (version) => {
|
|
2
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
3
|
+
if (!match) {
|
|
4
|
+
return {
|
|
5
|
+
ok: false,
|
|
6
|
+
reason: 'version must be strict semver x.y.z',
|
|
7
|
+
version,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const patch = Number.parseInt(match[3], 10);
|
|
12
|
+
if (patch > 9) {
|
|
13
|
+
return {
|
|
14
|
+
ok: false,
|
|
15
|
+
reason: 'patch version must stay within 0-9; bump minor instead',
|
|
16
|
+
version,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { ok: true, version };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const validateReleaseVersionPolicy = (version, latestVersion) => {
|
|
24
|
+
const versionPolicy = validateVersionSyntax(version);
|
|
25
|
+
if (!versionPolicy.ok) return versionPolicy;
|
|
26
|
+
|
|
27
|
+
if (!latestVersion) {
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
reason: 'cannot verify release version: npm latest version is unavailable',
|
|
31
|
+
version,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const latestMatch = latestVersion.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
36
|
+
if (!latestMatch) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
reason: `invalid npm latest version: ${latestVersion}`,
|
|
40
|
+
version,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const latestMajor = Number.parseInt(latestMatch[1], 10);
|
|
45
|
+
const latestMinor = Number.parseInt(latestMatch[2], 10);
|
|
46
|
+
const latestPatch = Number.parseInt(latestMatch[3], 10);
|
|
47
|
+
if (latestPatch > 9) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
reason: `invalid npm latest version: patch must stay within 0-9 (${latestVersion})`,
|
|
51
|
+
version,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const expected =
|
|
56
|
+
latestPatch < 9
|
|
57
|
+
? `${latestMajor}.${latestMinor}.${latestPatch + 1}`
|
|
58
|
+
: latestMinor < 9
|
|
59
|
+
? `${latestMajor}.${latestMinor + 1}.0`
|
|
60
|
+
: `${latestMajor + 1}.0.0`;
|
|
61
|
+
|
|
62
|
+
if (version !== expected) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
reason: `release version must be exactly one valid step after ${latestVersion}: expected ${expected}, got ${version}`,
|
|
66
|
+
version,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
mode: 'release',
|
|
73
|
+
version,
|
|
74
|
+
};
|
|
75
|
+
};
|
package/src/channel.ts
CHANGED
|
@@ -231,6 +231,10 @@ class BncrBridgeRuntime {
|
|
|
231
231
|
// Identity / lifecycle ----------------------------------------------------
|
|
232
232
|
private api: OpenClawPluginApi;
|
|
233
233
|
|
|
234
|
+
getApi(): OpenClawPluginApi {
|
|
235
|
+
return this.api;
|
|
236
|
+
}
|
|
237
|
+
|
|
234
238
|
private get runtimeApi(): OpenClawPluginApi {
|
|
235
239
|
return this.api;
|
|
236
240
|
}
|
|
@@ -3235,6 +3239,7 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
|
|
|
3235
3239
|
return createBncrChannelPluginSurfaceGroup({
|
|
3236
3240
|
channelId: CHANNEL_ID,
|
|
3237
3241
|
sceneRegistry: getBridge().getSceneRegistry(),
|
|
3242
|
+
getApi: () => getBridge().getApi(),
|
|
3238
3243
|
getMessageSendBridge: bridgeGroup.getMessageSendBridge,
|
|
3239
3244
|
getOutboundBridge: bridgeGroup.getOutboundBridge,
|
|
3240
3245
|
getMessagingBridge: bridgeGroup.getMessagingBridge,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasControlCommand } from 'openclaw/plugin-sdk/command-
|
|
1
|
+
import { hasControlCommand } from 'openclaw/plugin-sdk/command-detection';
|
|
2
2
|
import { emitBncrLogLine } from '../../core/logging.ts';
|
|
3
3
|
import { resolveBncrChannelPolicy } from '../../core/policy.ts';
|
|
4
4
|
import { formatDisplayScope } from '../../core/targets.ts';
|
|
@@ -271,7 +271,11 @@ export async function dispatchBncrInbound(params: {
|
|
|
271
271
|
mediaContentType: mediaContentType || mimeType,
|
|
272
272
|
historyLimit: resolvedHistoryLimit,
|
|
273
273
|
});
|
|
274
|
-
|
|
274
|
+
// A silent history flush repurposes the current turn as a context carrier
|
|
275
|
+
// and suppresses its outbound. Only a turn we would not reply to anyway
|
|
276
|
+
// may carry the flush; a reply-expected turn must reach the channel, and
|
|
277
|
+
// its backlog is flushed by the outbound path after the reply instead.
|
|
278
|
+
if (historyKey && !initialShouldDispatch && !forceSilentHistoryFlush) {
|
|
275
279
|
const entries = conversationHistories.get(historyKey);
|
|
276
280
|
if (entries && resolvedHistoryLimit > 0 && entries.length >= resolvedHistoryLimit) {
|
|
277
281
|
const sceneForHistory = sceneRegistry.get(buildSceneKey(parsed));
|
|
@@ -279,6 +283,10 @@ export async function dispatchBncrInbound(params: {
|
|
|
279
283
|
if (historyForce) {
|
|
280
284
|
silentHistoryFlush = true;
|
|
281
285
|
shouldDispatch = true;
|
|
286
|
+
emitBncrLogLine(
|
|
287
|
+
'info',
|
|
288
|
+
`history silent flush|key=${historyKey}|limit=${resolvedHistoryLimit}|entries=${entries.length}|carrier=${msgId ?? '-'}`,
|
|
289
|
+
);
|
|
282
290
|
}
|
|
283
291
|
}
|
|
284
292
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { canonicalizeBase64, estimateBase64DecodedBytes } from 'openclaw/plugin-sdk/media-runtime';
|
|
4
|
+
|
|
5
|
+
export const OPENCLAW_SEND_BUFFER_MEDIA_URL = 'buffer://message-send/attachment';
|
|
6
|
+
export const OPENCLAW_OUTBOUND_MEDIA_MAX_BYTES = 50 * 1024 * 1024;
|
|
3
7
|
|
|
4
8
|
type RuntimeMediaLoaded = {
|
|
5
9
|
buffer: Buffer;
|
|
@@ -39,6 +43,114 @@ type RuntimeApiHolder = {
|
|
|
39
43
|
|
|
40
44
|
export type OpenClawLoadedMedia = RuntimeMediaLoaded;
|
|
41
45
|
|
|
46
|
+
type OpenClawSendBufferDecodeResult = {
|
|
47
|
+
buffer: Buffer;
|
|
48
|
+
mimeType?: string;
|
|
49
|
+
fileName?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function normalizeOptionalString(value: string | undefined): string | undefined {
|
|
53
|
+
const normalized = String(value || '').trim();
|
|
54
|
+
return normalized || undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isPathInsideRoot(candidate: string, root: string): boolean {
|
|
58
|
+
const relative = path.relative(root, candidate);
|
|
59
|
+
return (
|
|
60
|
+
relative === '' ||
|
|
61
|
+
(relative !== '..' && !path.isAbsolute(relative) && !relative.startsWith(`..${path.sep}`))
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Translate a sandbox container workspace path into an authorized host path.
|
|
67
|
+
*
|
|
68
|
+
* `extra.path` bypasses host-side sandbox staging, so a path such as
|
|
69
|
+
* `/workspace/file.js` reaches the plugin unchanged. Resolve it only against
|
|
70
|
+
* roots supplied by the host and prefer the most specific matching root.
|
|
71
|
+
*/
|
|
72
|
+
function resolveContainerWorkspaceMediaPath(
|
|
73
|
+
mediaUrl: string,
|
|
74
|
+
localRoots?: readonly string[],
|
|
75
|
+
): string {
|
|
76
|
+
if (!localRoots?.length || !/^\/workspace(?:\/|$)/.test(mediaUrl)) {
|
|
77
|
+
return mediaUrl;
|
|
78
|
+
}
|
|
79
|
+
if (existsSync(mediaUrl)) {
|
|
80
|
+
return mediaUrl;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const relativePath = mediaUrl.slice('/workspace'.length).replace(/^\/+/, '');
|
|
84
|
+
if (!relativePath) {
|
|
85
|
+
return mediaUrl;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let bestMatch: { root: string; candidate: string } | undefined;
|
|
89
|
+
for (const root of localRoots) {
|
|
90
|
+
const resolvedRoot = path.resolve(root);
|
|
91
|
+
const candidate = path.resolve(resolvedRoot, relativePath);
|
|
92
|
+
if (!isPathInsideRoot(candidate, resolvedRoot) || !existsSync(candidate)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (!bestMatch || resolvedRoot.length > bestMatch.root.length) {
|
|
96
|
+
bestMatch = { root: resolvedRoot, candidate };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return bestMatch?.candidate ?? mediaUrl;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isOpenClawSendBufferMediaUrl(mediaUrl: string): boolean {
|
|
104
|
+
return String(mediaUrl || '').trim() === OPENCLAW_SEND_BUFFER_MEDIA_URL;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function decodeOpenClawSendBuffer(params: {
|
|
108
|
+
buffer: string;
|
|
109
|
+
contentType?: string;
|
|
110
|
+
mimeType?: string;
|
|
111
|
+
filename?: string;
|
|
112
|
+
fileName?: string;
|
|
113
|
+
maxBytes?: number;
|
|
114
|
+
}): OpenClawSendBufferDecodeResult {
|
|
115
|
+
const raw = String(params.buffer || '').trim();
|
|
116
|
+
const match = /^data:([^;,\s]+)(;(?!base64)[^,;\s]+)*;base64,(.*)$/is.exec(raw);
|
|
117
|
+
const encoded = match ? match[3] : raw;
|
|
118
|
+
if (!encoded.trim()) {
|
|
119
|
+
throw new Error('send buffer base64 decoded to empty buffer');
|
|
120
|
+
}
|
|
121
|
+
const canonical = canonicalizeBase64(encoded);
|
|
122
|
+
if (!canonical) {
|
|
123
|
+
throw new Error('send buffer has invalid base64 data');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const maxBytes = params.maxBytes ?? OPENCLAW_OUTBOUND_MEDIA_MAX_BYTES;
|
|
127
|
+
const estimatedBytes = estimateBase64DecodedBytes(canonical);
|
|
128
|
+
if (estimatedBytes > maxBytes) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`send buffer media too large: estimated ${estimatedBytes} bytes exceeds ${maxBytes} bytes`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const buffer = Buffer.from(canonical, 'base64');
|
|
135
|
+
if (!buffer.length) {
|
|
136
|
+
throw new Error('send buffer base64 decoded to empty buffer');
|
|
137
|
+
}
|
|
138
|
+
if (buffer.byteLength > maxBytes) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`send buffer media too large: decoded ${buffer.byteLength} bytes exceeds ${maxBytes} bytes`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
buffer,
|
|
146
|
+
mimeType:
|
|
147
|
+
normalizeOptionalString(params.contentType) ??
|
|
148
|
+
normalizeOptionalString(params.mimeType) ??
|
|
149
|
+
normalizeOptionalString(match?.[1]),
|
|
150
|
+
fileName: normalizeOptionalString(params.filename) ?? normalizeOptionalString(params.fileName),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
42
154
|
export function isOpenClawRemoteHttpMediaUrl(mediaUrl: string): boolean {
|
|
43
155
|
return /^https?:\/\//i.test(String(mediaUrl || '').trim());
|
|
44
156
|
}
|
|
@@ -50,6 +162,8 @@ export function isOpenClawRemoteHttpMediaUrl(mediaUrl: string): boolean {
|
|
|
50
162
|
*/
|
|
51
163
|
function resolveRelativeMediaPath(mediaUrl: string, localRoots?: readonly string[]): string {
|
|
52
164
|
if (!mediaUrl || !localRoots?.length) return mediaUrl;
|
|
165
|
+
const containerWorkspacePath = resolveContainerWorkspaceMediaPath(mediaUrl, localRoots);
|
|
166
|
+
if (containerWorkspacePath !== mediaUrl) return containerWorkspacePath;
|
|
53
167
|
if (path.isAbsolute(mediaUrl)) return mediaUrl;
|
|
54
168
|
// HTTP / file:// / data: / ~ paths are handled elsewhere
|
|
55
169
|
if (/^(https?|file|data):/i.test(mediaUrl) || mediaUrl.startsWith('~')) return mediaUrl;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChannelMessageSendResult } from 'openclaw/plugin-sdk/channel-
|
|
1
|
+
import type { ChannelMessageSendResult } from 'openclaw/plugin-sdk/channel-outbound';
|
|
2
2
|
import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
|
|
3
3
|
import type { BncrRecentOutboundEntry, BncrRoute } from '../core/types.ts';
|
|
4
4
|
import type { ReplyPayloadInput } from '../messaging/outbound/reply-enqueue.ts';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChannelMessageSendResult } from 'openclaw/plugin-sdk/channel-
|
|
1
|
+
import type { ChannelMessageSendResult } from 'openclaw/plugin-sdk/channel-outbound';
|
|
2
2
|
import { Type } from 'typebox';
|
|
3
3
|
import { listAccountIds, normalizeAccountId, resolveAccount } from '../core/accounts.ts';
|
|
4
4
|
import { BncrConfigSchema } from '../core/config-schema.ts';
|
|
@@ -6,6 +6,12 @@ import { resolveBncrChannelPolicy } from '../core/policy.ts';
|
|
|
6
6
|
import type { BncrRoute } from '../core/types.ts';
|
|
7
7
|
import type { ReplyPayloadInput } from '../messaging/outbound/reply-enqueue.ts';
|
|
8
8
|
import type { OutboundReplyTargetPolicy } from '../messaging/outbound/reply-target-policy.ts';
|
|
9
|
+
import {
|
|
10
|
+
decodeOpenClawSendBuffer,
|
|
11
|
+
isOpenClawSendBufferMediaUrl,
|
|
12
|
+
OPENCLAW_OUTBOUND_MEDIA_MAX_BYTES,
|
|
13
|
+
saveOpenClawChannelMediaBuffer,
|
|
14
|
+
} from '../openclaw/media-runtime.ts';
|
|
9
15
|
import type { OpenClawChannelToolSend, openClawJsonResult } from '../openclaw/sdk-helpers.ts';
|
|
10
16
|
import { readOpenClawStringParam } from '../openclaw/sdk-helpers.ts';
|
|
11
17
|
import type { BncrBridgeCallBridge } from './bridge-call.ts';
|
|
@@ -68,6 +74,7 @@ const BNCR_MESSAGE_TOOL_SCHEMA: BncrMessageToolSchemaContribution = {
|
|
|
68
74
|
export function createBncrChannelPluginSurfaceGroup(runtime: {
|
|
69
75
|
channelId: string;
|
|
70
76
|
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
77
|
+
getApi: () => Parameters<typeof saveOpenClawChannelMediaBuffer>[0];
|
|
71
78
|
getMessageSendBridge: () => {
|
|
72
79
|
channelMessageSendText: (
|
|
73
80
|
ctx: Record<string, unknown>,
|
|
@@ -222,20 +229,49 @@ export function createBncrChannelPluginSurfaceGroup(runtime: {
|
|
|
222
229
|
}
|
|
223
230
|
|
|
224
231
|
const to = readOpenClawStringParam(paramsObj, 'to', { required: true });
|
|
225
|
-
const
|
|
232
|
+
const rawBuffer = typeof paramsObj.buffer === 'string' ? paramsObj.buffer : '';
|
|
233
|
+
const rawMediaUrl =
|
|
226
234
|
(typeof paramsObj.mediaUrl === 'string' ? paramsObj.mediaUrl : '') ||
|
|
227
235
|
(typeof paramsObj.media === 'string' ? paramsObj.media : '') ||
|
|
228
236
|
(typeof paramsObj.path === 'string' ? paramsObj.path : '') ||
|
|
229
237
|
(typeof paramsObj.filePath === 'string' ? paramsObj.filePath : '');
|
|
238
|
+
const rawMediaUrls = paramsObj.mediaUrls;
|
|
239
|
+
const declaredMediaUrls = Array.isArray(rawMediaUrls)
|
|
240
|
+
? rawMediaUrls.filter((u): u is string => typeof u === 'string')
|
|
241
|
+
: undefined;
|
|
242
|
+
const hasExplicitMedia = [rawMediaUrl, ...(declaredMediaUrls ?? [])].some(
|
|
243
|
+
(mediaUrl) => Boolean(mediaUrl) && !isOpenClawSendBufferMediaUrl(mediaUrl),
|
|
244
|
+
);
|
|
245
|
+
let toolMediaUrl = rawMediaUrl;
|
|
246
|
+
let mediaUrls = declaredMediaUrls;
|
|
247
|
+
if (rawBuffer && !hasExplicitMedia) {
|
|
248
|
+
const decoded = decodeOpenClawSendBuffer({
|
|
249
|
+
buffer: rawBuffer,
|
|
250
|
+
contentType:
|
|
251
|
+
typeof paramsObj.contentType === 'string' ? paramsObj.contentType : undefined,
|
|
252
|
+
mimeType: typeof paramsObj.mimeType === 'string' ? paramsObj.mimeType : undefined,
|
|
253
|
+
filename: typeof paramsObj.filename === 'string' ? paramsObj.filename : undefined,
|
|
254
|
+
fileName: typeof paramsObj.fileName === 'string' ? paramsObj.fileName : undefined,
|
|
255
|
+
maxBytes: OPENCLAW_OUTBOUND_MEDIA_MAX_BYTES,
|
|
256
|
+
});
|
|
257
|
+
const saved = await saveOpenClawChannelMediaBuffer(
|
|
258
|
+
runtime.getApi(),
|
|
259
|
+
decoded.buffer,
|
|
260
|
+
decoded.mimeType,
|
|
261
|
+
'outbound',
|
|
262
|
+
OPENCLAW_OUTBOUND_MEDIA_MAX_BYTES,
|
|
263
|
+
decoded.fileName,
|
|
264
|
+
);
|
|
265
|
+
toolMediaUrl = saved.path;
|
|
266
|
+
mediaUrls = declaredMediaUrls?.map((mediaUrl) =>
|
|
267
|
+
isOpenClawSendBufferMediaUrl(mediaUrl) ? saved.path : mediaUrl,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
230
270
|
const rawExtra = paramsObj.extra;
|
|
231
271
|
const extra =
|
|
232
272
|
rawExtra && typeof rawExtra === 'object' && !Array.isArray(rawExtra)
|
|
233
273
|
? (rawExtra as Record<string, unknown>)
|
|
234
274
|
: undefined;
|
|
235
|
-
const rawMediaUrls = paramsObj.mediaUrls;
|
|
236
|
-
const mediaUrls = Array.isArray(rawMediaUrls)
|
|
237
|
-
? rawMediaUrls.filter((u): u is string => typeof u === 'string')
|
|
238
|
-
: undefined;
|
|
239
275
|
|
|
240
276
|
const toolMessage = readOpenClawStringParam(paramsObj, 'message', { allowEmpty: true }) ?? '';
|
|
241
277
|
const toolCaption = readOpenClawStringParam(paramsObj, 'caption', { allowEmpty: true }) ?? '';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasControlCommand } from 'openclaw/plugin-sdk/command-
|
|
1
|
+
import { hasControlCommand } from 'openclaw/plugin-sdk/command-detection';
|
|
2
2
|
import { resolveAccount } from '../core/accounts.ts';
|
|
3
3
|
import { checkBncrMessageGate } from '../messaging/inbound/gate.ts';
|
|
4
4
|
import {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChannelMessageSendResult } from 'openclaw/plugin-sdk/channel-
|
|
1
|
+
import type { ChannelMessageSendResult } from 'openclaw/plugin-sdk/channel-outbound';
|
|
2
2
|
import type { BncrChannelSendContext } from './channel-runtime-types.ts';
|
|
3
3
|
|
|
4
4
|
export type BncrMessageSendBridge = {
|