@xmoxmo/bncr 0.6.8 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/scripts/check-pack.mjs +1 -0
- package/scripts/selfcheck.mjs +11 -112
- package/scripts/version-policy.mjs +75 -0
- package/src/channel.ts +12 -0
- package/src/messaging/outbound/session-route.ts +4 -6
- package/src/openclaw/media-runtime.ts +114 -0
- package/src/plugin/channel-plugin-bridge-group.ts +2 -8
- package/src/plugin/channel-plugin-surface-group.ts +43 -8
- package/src/plugin/messaging.ts +9 -32
- package/src/plugin/scene-registry.ts +14 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmoxmo/bncr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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');
|
|
@@ -245,6 +247,7 @@ const requiredOpenClawSdkSubpaths = [
|
|
|
245
247
|
'openclaw/plugin-sdk/core',
|
|
246
248
|
'openclaw/plugin-sdk/json-store',
|
|
247
249
|
'openclaw/plugin-sdk/param-readers',
|
|
250
|
+
'openclaw/plugin-sdk/media-runtime',
|
|
248
251
|
'openclaw/plugin-sdk/security-runtime',
|
|
249
252
|
'openclaw/plugin-sdk/status-helpers',
|
|
250
253
|
'openclaw/plugin-sdk/tool-send',
|
|
@@ -266,120 +269,14 @@ const resolveOpenClawSdkSubpaths = () => {
|
|
|
266
269
|
});
|
|
267
270
|
};
|
|
268
271
|
|
|
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
272
|
const missing = requiredFiles.filter((rel) => !fs.existsSync(path.join(root, rel)));
|
|
379
273
|
const version = readPackageVersion();
|
|
380
274
|
const packageName = '@xmoxmo/bncr';
|
|
381
|
-
const
|
|
382
|
-
const
|
|
275
|
+
const releaseMode = process.argv.includes('--release');
|
|
276
|
+
const latestVersion = releaseMode ? readNpmLatestVersion(packageName) : null;
|
|
277
|
+
const versionPolicy = releaseMode
|
|
278
|
+
? validateReleaseVersionPolicy(version, latestVersion)
|
|
279
|
+
: validateVersionSyntax(version);
|
|
383
280
|
const sdkSubpaths = resolveOpenClawSdkSubpaths();
|
|
384
281
|
const missingSdkSubpaths = sdkSubpaths.filter((entry) => !entry.ok);
|
|
385
282
|
const result = {
|
|
@@ -388,6 +285,8 @@ const result = {
|
|
|
388
285
|
requiredCount: requiredFiles.length,
|
|
389
286
|
missing,
|
|
390
287
|
version,
|
|
288
|
+
versionMode: releaseMode ? 'release' : 'development',
|
|
289
|
+
latestVersion,
|
|
391
290
|
versionPolicy,
|
|
392
291
|
sdkSubpaths,
|
|
393
292
|
};
|
|
@@ -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
|
@@ -201,6 +201,7 @@ import {
|
|
|
201
201
|
createBncrOutboxDiagnosticsHelpers,
|
|
202
202
|
createBncrRuntimeAckObservabilityBuilder,
|
|
203
203
|
} from './plugin/runtime-diagnostics-snapshot.ts';
|
|
204
|
+
import { buildRouteSceneKey } from './plugin/scene-registry.ts';
|
|
204
205
|
import { BNCR_SETUP_SURFACE } from './plugin/setup.ts';
|
|
205
206
|
import type { BncrHistoryShardRow } from './plugin/sqlite-state.ts';
|
|
206
207
|
import { createBncrStateTransientRuntimeGroup } from './plugin/state-transient-runtime-group.ts';
|
|
@@ -230,6 +231,10 @@ class BncrBridgeRuntime {
|
|
|
230
231
|
// Identity / lifecycle ----------------------------------------------------
|
|
231
232
|
private api: OpenClawPluginApi;
|
|
232
233
|
|
|
234
|
+
getApi(): OpenClawPluginApi {
|
|
235
|
+
return this.api;
|
|
236
|
+
}
|
|
237
|
+
|
|
233
238
|
private get runtimeApi(): OpenClawPluginApi {
|
|
234
239
|
return this.api;
|
|
235
240
|
}
|
|
@@ -309,6 +314,12 @@ class BncrBridgeRuntime {
|
|
|
309
314
|
getSceneRegistry(): Map<string, BncrSceneRecord> {
|
|
310
315
|
return this.sceneRegistry;
|
|
311
316
|
}
|
|
317
|
+
resolveOutboundAgentId(route: BncrRoute): string {
|
|
318
|
+
return (
|
|
319
|
+
asString(this.sceneRegistry.get(buildRouteSceneKey(route))?.agentId).trim() ||
|
|
320
|
+
this.defaultPublicAgentId()
|
|
321
|
+
);
|
|
322
|
+
}
|
|
312
323
|
private conversationHistories: BncrConversationHistoryMap = new Map();
|
|
313
324
|
private outboundReplayCache: BncrOutboundReplayCache = new Map();
|
|
314
325
|
private routeAliases = new Map<
|
|
@@ -3228,6 +3239,7 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
|
|
|
3228
3239
|
return createBncrChannelPluginSurfaceGroup({
|
|
3229
3240
|
channelId: CHANNEL_ID,
|
|
3230
3241
|
sceneRegistry: getBridge().getSceneRegistry(),
|
|
3242
|
+
getApi: () => getBridge().getApi(),
|
|
3231
3243
|
getMessageSendBridge: bridgeGroup.getMessageSendBridge,
|
|
3232
3244
|
getOutboundBridge: bridgeGroup.getOutboundBridge,
|
|
3233
3245
|
getMessagingBridge: bridgeGroup.getMessagingBridge,
|
|
@@ -27,12 +27,11 @@ type BncrOutboundSessionRoute = ChannelOutboundSessionRoute & BncrChannelRouteRe
|
|
|
27
27
|
type ResolveBncrOutboundSessionRouteParams = {
|
|
28
28
|
cfg: BncrChannelConfigRoot;
|
|
29
29
|
channel: string;
|
|
30
|
-
agentId: string;
|
|
31
30
|
accountId?: string;
|
|
32
31
|
target: string;
|
|
33
32
|
resolvedTarget?: { to?: string } | null;
|
|
34
33
|
threadId?: string | number | null;
|
|
35
|
-
|
|
34
|
+
resolveAgentId: (route: BncrRoute) => string;
|
|
36
35
|
resolveRouteBySession?: (raw: string, accountId: string) => BncrRoute | null;
|
|
37
36
|
};
|
|
38
37
|
|
|
@@ -76,15 +75,14 @@ export function resolveBncrOutboundSessionRoute(params: ResolveBncrOutboundSessi
|
|
|
76
75
|
|
|
77
76
|
if (!route) return null;
|
|
78
77
|
|
|
79
|
-
const
|
|
80
|
-
asString(params.canonicalAgentId).trim() || asString(params.agentId).trim() || 'main';
|
|
78
|
+
const agentId = asString(params.resolveAgentId(route)).trim() || 'public';
|
|
81
79
|
const peerId = routeScopeToHex(route);
|
|
82
|
-
const sessionKey = buildCanonicalBncrSessionKey(route,
|
|
80
|
+
const sessionKey = buildCanonicalBncrSessionKey(route, agentId);
|
|
83
81
|
const displayTo = formatDisplayScope(route);
|
|
84
82
|
|
|
85
83
|
const built = buildOpenClawChannelOutboundSessionRoute({
|
|
86
84
|
cfg: params.cfg,
|
|
87
|
-
agentId
|
|
85
|
+
agentId,
|
|
88
86
|
channel: params.channel,
|
|
89
87
|
accountId: params.accountId,
|
|
90
88
|
peer: {
|
|
@@ -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;
|
|
@@ -34,6 +34,7 @@ export type BncrChannelPluginBridge = {
|
|
|
34
34
|
channelId: string;
|
|
35
35
|
peer: { kind: 'direct'; id: string };
|
|
36
36
|
}) => string;
|
|
37
|
+
resolveOutboundAgentId: (route: BncrRoute) => string;
|
|
37
38
|
resolveRouteBySession: (raw: string, accountId: string) => BncrRoute | null;
|
|
38
39
|
getChannelSummary: (defaultAccountId: string) => Record<string, unknown>;
|
|
39
40
|
getAccountRuntimeSnapshot: (accountId: string) => BncrStatusRuntimeSnapshot;
|
|
@@ -90,14 +91,7 @@ export function createBncrChannelPluginBridgeGroup(runtime: {
|
|
|
90
91
|
const getMessagingBridge = () => {
|
|
91
92
|
const bridge = runtime.getBridge();
|
|
92
93
|
return {
|
|
93
|
-
|
|
94
|
-
ensureCanonicalAgentId: (params: { cfg: BncrChannelConfigRoot; accountId: string }) =>
|
|
95
|
-
bridge.ensureCanonicalAgentId({
|
|
96
|
-
cfg: params.cfg,
|
|
97
|
-
accountId: params.accountId,
|
|
98
|
-
channelId: runtime.channelId,
|
|
99
|
-
peer: { kind: 'direct', id: params.accountId },
|
|
100
|
-
}),
|
|
94
|
+
resolveOutboundAgentId: (route: BncrRoute) => bridge.resolveOutboundAgentId(route),
|
|
101
95
|
resolveRouteBySession: (raw: string, accountId: string) =>
|
|
102
96
|
bridge.resolveRouteBySession(raw, accountId),
|
|
103
97
|
};
|
|
@@ -3,13 +3,19 @@ import { Type } from 'typebox';
|
|
|
3
3
|
import { listAccountIds, normalizeAccountId, resolveAccount } from '../core/accounts.ts';
|
|
4
4
|
import { BncrConfigSchema } from '../core/config-schema.ts';
|
|
5
5
|
import { resolveBncrChannelPolicy } from '../core/policy.ts';
|
|
6
|
+
import type { BncrRoute } from '../core/types.ts';
|
|
6
7
|
import type { ReplyPayloadInput } from '../messaging/outbound/reply-enqueue.ts';
|
|
7
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';
|
|
8
15
|
import type { OpenClawChannelToolSend, openClawJsonResult } from '../openclaw/sdk-helpers.ts';
|
|
9
16
|
import { readOpenClawStringParam } from '../openclaw/sdk-helpers.ts';
|
|
10
17
|
import type { BncrBridgeCallBridge } from './bridge-call.ts';
|
|
11
18
|
import type {
|
|
12
|
-
BncrChannelConfigRoot,
|
|
13
19
|
BncrChannelSendContext,
|
|
14
20
|
BncrMessageToolSchemaContribution,
|
|
15
21
|
BncrSceneRecord,
|
|
@@ -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>,
|
|
@@ -81,8 +88,7 @@ export function createBncrChannelPluginSurfaceGroup(runtime: {
|
|
|
81
88
|
};
|
|
82
89
|
getOutboundBridge: () => BncrOutboundBridge;
|
|
83
90
|
getMessagingBridge: () => {
|
|
84
|
-
|
|
85
|
-
ensureCanonicalAgentId: (params: { cfg: BncrChannelConfigRoot; accountId: string }) => string;
|
|
91
|
+
resolveOutboundAgentId: (route: BncrRoute) => string;
|
|
86
92
|
resolveRouteBySession: (raw: string, accountId: string) => BncrVerifiedTarget['route'] | null;
|
|
87
93
|
};
|
|
88
94
|
getStatusBridge: () => {
|
|
@@ -223,20 +229,49 @@ export function createBncrChannelPluginSurfaceGroup(runtime: {
|
|
|
223
229
|
}
|
|
224
230
|
|
|
225
231
|
const to = readOpenClawStringParam(paramsObj, 'to', { required: true });
|
|
226
|
-
const
|
|
232
|
+
const rawBuffer = typeof paramsObj.buffer === 'string' ? paramsObj.buffer : '';
|
|
233
|
+
const rawMediaUrl =
|
|
227
234
|
(typeof paramsObj.mediaUrl === 'string' ? paramsObj.mediaUrl : '') ||
|
|
228
235
|
(typeof paramsObj.media === 'string' ? paramsObj.media : '') ||
|
|
229
236
|
(typeof paramsObj.path === 'string' ? paramsObj.path : '') ||
|
|
230
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
|
+
}
|
|
231
270
|
const rawExtra = paramsObj.extra;
|
|
232
271
|
const extra =
|
|
233
272
|
rawExtra && typeof rawExtra === 'object' && !Array.isArray(rawExtra)
|
|
234
273
|
? (rawExtra as Record<string, unknown>)
|
|
235
274
|
: undefined;
|
|
236
|
-
const rawMediaUrls = paramsObj.mediaUrls;
|
|
237
|
-
const mediaUrls = Array.isArray(rawMediaUrls)
|
|
238
|
-
? rawMediaUrls.filter((u): u is string => typeof u === 'string')
|
|
239
|
-
: undefined;
|
|
240
275
|
|
|
241
276
|
const toolMessage = readOpenClawStringParam(paramsObj, 'message', { allowEmpty: true }) ?? '';
|
|
242
277
|
const toolCaption = readOpenClawStringParam(paramsObj, 'caption', { allowEmpty: true }) ?? '';
|
package/src/plugin/messaging.ts
CHANGED
|
@@ -15,8 +15,7 @@ function formatBncrHumanDisplay(route: BncrRoute): string {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
type BncrMessagingRuntimeBridge = {
|
|
18
|
-
|
|
19
|
-
ensureCanonicalAgentId: (params: { cfg: BncrChannelConfigRoot; accountId: string }) => string;
|
|
18
|
+
resolveOutboundAgentId: (route: BncrRoute) => string;
|
|
20
19
|
resolveRouteBySession: (raw: string, accountId: string) => BncrRoute | null;
|
|
21
20
|
};
|
|
22
21
|
|
|
@@ -44,6 +43,7 @@ type BncrMessagingSessionTargetArgs = {
|
|
|
44
43
|
};
|
|
45
44
|
|
|
46
45
|
type BncrMessagingOutboundSessionRouteArgs = {
|
|
46
|
+
/** Host-reported owner. BNCR resolves ownership from its persisted scene registry instead. */
|
|
47
47
|
agentId: string;
|
|
48
48
|
target: string;
|
|
49
49
|
resolvedTarget?: { to?: string } | null;
|
|
@@ -99,21 +99,9 @@ function resolveMessagingAccountId(accountId: unknown) {
|
|
|
99
99
|
return normalizeAccountId(asSanitizedString(accountId || BNCR_DEFAULT_ACCOUNT_ID));
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
function
|
|
103
|
-
runtimeBridge: BncrMessagingRuntimeBridge,
|
|
104
|
-
cfg: BncrChannelConfigRoot,
|
|
105
|
-
accountId: string,
|
|
106
|
-
) {
|
|
107
|
-
return runtimeBridge.canonicalAgentId || runtimeBridge.ensureCanonicalAgentId({ cfg, accountId });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function createBncrMessagingExplicitTargetParser(
|
|
111
|
-
getBridge: () => BncrMessagingRuntimeBridge,
|
|
112
|
-
) {
|
|
102
|
+
export function createBncrMessagingExplicitTargetParser() {
|
|
113
103
|
return ({ raw }: BncrMessagingExplicitTargetArgs) => {
|
|
114
|
-
const
|
|
115
|
-
const canonicalAgentId = runtimeBridge.canonicalAgentId || 'main';
|
|
116
|
-
const parsed = parseExplicitTarget(asSanitizedString(raw).trim(), { canonicalAgentId });
|
|
104
|
+
const parsed = parseExplicitTarget(asSanitizedString(raw).trim());
|
|
117
105
|
if (!parsed) return null;
|
|
118
106
|
const chatType: ChatType = parsed.route?.groupId ? 'group' : 'direct';
|
|
119
107
|
return {
|
|
@@ -125,16 +113,12 @@ export function createBncrMessagingExplicitTargetParser(
|
|
|
125
113
|
};
|
|
126
114
|
}
|
|
127
115
|
|
|
128
|
-
export function createBncrMessagingSessionTargetResolver(
|
|
129
|
-
getBridge: () => BncrMessagingRuntimeBridge,
|
|
130
|
-
) {
|
|
116
|
+
export function createBncrMessagingSessionTargetResolver() {
|
|
131
117
|
return ({ id }: BncrMessagingSessionTargetArgs) => {
|
|
132
118
|
const raw = asSanitizedString(id).trim();
|
|
133
119
|
if (!raw) return undefined;
|
|
134
|
-
const runtimeBridge = getBridge();
|
|
135
|
-
const canonicalAgentId = runtimeBridge.canonicalAgentId || 'main';
|
|
136
120
|
|
|
137
|
-
const parsed = parseExplicitTarget(raw
|
|
121
|
+
const parsed = parseExplicitTarget(raw);
|
|
138
122
|
if (!parsed) {
|
|
139
123
|
return raw || undefined;
|
|
140
124
|
}
|
|
@@ -146,17 +130,10 @@ export function createBncrMessagingOutboundSessionRouteResolver(
|
|
|
146
130
|
getBridge: () => BncrMessagingRuntimeBridge,
|
|
147
131
|
) {
|
|
148
132
|
return (params: BncrMessagingOutboundSessionRouteArgs) => {
|
|
149
|
-
const accountId = resolveMessagingAccountId(params?.accountId);
|
|
150
133
|
const runtimeBridge = getBridge();
|
|
151
|
-
const canonicalAgentId = resolveMessagingCanonicalAgentId(
|
|
152
|
-
runtimeBridge,
|
|
153
|
-
params?.cfg,
|
|
154
|
-
accountId,
|
|
155
|
-
);
|
|
156
134
|
return resolveBncrOutboundSessionRoute({
|
|
157
135
|
channel: 'bncr',
|
|
158
136
|
cfg: params.cfg,
|
|
159
|
-
agentId: params.agentId,
|
|
160
137
|
accountId: params.accountId ?? undefined,
|
|
161
138
|
target: params.target,
|
|
162
139
|
resolvedTarget: params.resolvedTarget,
|
|
@@ -164,7 +141,7 @@ export function createBncrMessagingOutboundSessionRouteResolver(
|
|
|
164
141
|
params.threadId === null || params.threadId === undefined
|
|
165
142
|
? undefined
|
|
166
143
|
: asSanitizedString(params.threadId),
|
|
167
|
-
|
|
144
|
+
resolveAgentId: (route) => runtimeBridge.resolveOutboundAgentId(route),
|
|
168
145
|
resolveRouteBySession: (raw: string, acc: string) =>
|
|
169
146
|
runtimeBridge.resolveRouteBySession(raw, acc),
|
|
170
147
|
});
|
|
@@ -175,9 +152,9 @@ export function createBncrMessagingSurface(getBridge: () => BncrMessagingRuntime
|
|
|
175
152
|
return {
|
|
176
153
|
// 接收任意标签输入;不在 normalize 阶段做格式门槛,统一下沉到发送前验证。
|
|
177
154
|
normalizeTarget: normalizeBncrMessagingTarget,
|
|
178
|
-
parseExplicitTarget: createBncrMessagingExplicitTargetParser(
|
|
155
|
+
parseExplicitTarget: createBncrMessagingExplicitTargetParser(),
|
|
179
156
|
formatTargetDisplay: formatBncrMessagingTargetDisplay,
|
|
180
|
-
resolveSessionTarget: createBncrMessagingSessionTargetResolver(
|
|
157
|
+
resolveSessionTarget: createBncrMessagingSessionTargetResolver(),
|
|
181
158
|
resolveOutboundSessionRoute: createBncrMessagingOutboundSessionRouteResolver(getBridge),
|
|
182
159
|
targetResolver: createBncrMessagingTargetResolver(getBridge),
|
|
183
160
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { BncrRoute } from '../core/types.ts';
|
|
2
|
+
import { asString } from '../core/value-sanitize.ts';
|
|
1
3
|
import type {
|
|
2
4
|
BncrGroupReplyMode,
|
|
3
5
|
BncrSceneKind,
|
|
@@ -30,10 +32,19 @@ function defaultGroupReplyMode(kind: BncrSceneKind): BncrGroupReplyMode | undefi
|
|
|
30
32
|
return kind === 'group' ? 'admin' : undefined;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
export function buildRouteSceneKey(route: BncrRoute): string {
|
|
36
|
+
const platform = asString(route.platform).trim();
|
|
37
|
+
const groupId = asString(route.groupId).trim() || '0';
|
|
38
|
+
const userId = asString(route.userId).trim() || '0';
|
|
39
|
+
return groupId !== '0' ? `${platform}:${groupId}` : `${platform}:${userId}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
export function buildSceneKey(parsed: ParsedInbound): string {
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
:
|
|
43
|
+
return buildRouteSceneKey({
|
|
44
|
+
platform: parsed.platform,
|
|
45
|
+
groupId: parsed.groupId || '0',
|
|
46
|
+
userId: parsed.userId || '0',
|
|
47
|
+
});
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
function buildSceneRecord(args: {
|