@xmoxmo/bncr 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,7 +34,7 @@ openclaw plugins update bncr
34
34
  openclaw gateway restart
35
35
  ```
36
36
 
37
- > 兼容范围:`openclaw >= 2026.5.3-1`
37
+ > 兼容范围:`openclaw >= 2026.5.27`
38
38
  >
39
39
  > 如果你是从精确版本升级,或本地安装记录仍钉在旧版本,也可以显式执行:
40
40
  >
@@ -95,6 +95,10 @@ bncr 当前采用两层模型:
95
95
  - 在 OpenClaw 内部按正式 `channel plugin` 建模
96
96
  - 负责入站解析、消息分发、出站适配、状态与治理
97
97
 
98
+ 出站可靠投递的边界:bncr 后面仍有自己的服务框架和 outbox / ACK / retry / deadLetter 体系。对 OpenClaw 宿主来说,消息成功交给 bncr 插件并进入 bncr 自管 outbox,即表示频道 handoff 完成;这不等价于客户端 ACK 或目标平台最终送达。后续可靠投递由 bncr 自身负责。
99
+
100
+ 当前已注册生产 `channel.message` 作为 bncr 的频道专用 handoff adapter:`text` / `media` / `payload` 会转换为 bncr outbox entry。原有通用 `message.send` / `channel.actions.send` 发送能力继续保留;`channel.message` 是频道专用入口,不替代通用发送入口。该 adapter 仍不启用 `durableFinal`;进入 outbox 后的客户端 ACK、目标平台送达、retry、deadLetter 继续由 bncr 服务框架负责。
101
+
98
102
  当前代码结构:
99
103
 
100
104
  ```text
@@ -246,6 +250,7 @@ npm root -g
246
250
  cd plugins/bncr
247
251
  npm test
248
252
  npm run selfcheck
253
+ npm run check-pack
249
254
  npm pack
250
255
  ```
251
256
 
@@ -253,6 +258,7 @@ npm pack
253
258
 
254
259
  - `npm test`:跑回归测试
255
260
  - `npm run selfcheck`:检查插件骨架是否完整
261
+ - `npm run check-pack`:执行 `npm pack --dry-run --json`,确认发布包包含关键入口与 OpenClaw adapter 文件
256
262
  - `npm pack`:确认当前版本可正常打包
257
263
  - `npm run check-register-drift -- --duration-sec 300 --interval-sec 15`:静置采样 `bncr.diagnostics`,观察 `registerCount / apiGeneration / postWarmupRegisterCount` 是否在 warmup 后继续增长
258
264
 
package/index.ts CHANGED
@@ -5,6 +5,10 @@ import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { BncrConfigSchema } from './src/core/config-schema.ts';
7
7
  import { emitBncrLogLine } from './src/core/logging.ts';
8
+ import {
9
+ getOpenClawRuntimeConfig,
10
+ mutateOpenClawRuntimeConfigFile,
11
+ } from './src/openclaw/config-runtime.ts';
8
12
 
9
13
  const pluginFile = fileURLToPath(import.meta.url);
10
14
  const pluginDir = path.dirname(pluginFile);
@@ -660,35 +664,46 @@ const registerBncrCli = (api: OpenClawPluginApi & { registerCli?: (...args: any[
660
664
  'Seed minimal channels.bncr config (adds enabled=true and allowTool=false only when missing)',
661
665
  )
662
666
  .action(async () => {
663
- const cfg = api.runtime.config.current() as Record<string, unknown>;
664
- const next = structuredClone(cfg);
665
- if (!isPlainObject(next.channels)) next.channels = {};
666
-
667
- const existing = isPlainObject(next.channels.bncr) ? next.channels.bncr : {};
668
- const bncrCfg: Record<string, unknown> = { ...existing };
667
+ const cfg = getOpenClawRuntimeConfig(api) as Record<string, unknown>;
668
+ const channels = isPlainObject(cfg.channels) ? cfg.channels : {};
669
+ const existing = isPlainObject(channels.bncr) ? channels.bncr : {};
669
670
  const added: string[] = [];
670
671
 
671
- if (bncrCfg.enabled === undefined) {
672
- bncrCfg.enabled = true;
672
+ if (existing.enabled === undefined) {
673
673
  added.push('enabled=true');
674
674
  }
675
675
 
676
- if (bncrCfg.allowTool === undefined) {
677
- bncrCfg.allowTool = false;
676
+ if (existing.allowTool === undefined) {
678
677
  added.push('allowTool=false');
679
678
  }
680
679
 
681
- next.channels.bncr = bncrCfg;
682
-
683
680
  if (added.length === 0) {
684
681
  console.log('Minimal bncr config already present. No changes made.');
685
682
  return;
686
683
  }
687
684
 
688
- await api.runtime.config.writeConfigFile(next);
685
+ await mutateOpenClawRuntimeConfigFile(api, {
686
+ afterWrite: { mode: 'auto' },
687
+ mutate(draft: Record<string, unknown>) {
688
+ if (!isPlainObject(draft.channels)) draft.channels = {};
689
+ const draftChannels = draft.channels as Record<string, unknown>;
690
+ const draftExisting = isPlainObject(draftChannels.bncr) ? draftChannels.bncr : {};
691
+ const draftBncrCfg: Record<string, unknown> = { ...draftExisting };
692
+
693
+ if (draftBncrCfg.enabled === undefined) {
694
+ draftBncrCfg.enabled = true;
695
+ }
696
+
697
+ if (draftBncrCfg.allowTool === undefined) {
698
+ draftBncrCfg.allowTool = false;
699
+ }
700
+
701
+ draftChannels.bncr = draftBncrCfg;
702
+ },
703
+ });
689
704
  console.log('Seeded minimal bncr config at channels.bncr.');
690
705
  console.log(`Added missing fields: ${added.join(', ')}`);
691
- console.log('Restart the gateway to apply changes.');
706
+ console.log('Gateway will apply the config using the host afterWrite policy.');
692
707
  });
693
708
  },
694
709
  { commands: ['bncr'] },
@@ -801,7 +816,7 @@ const plugin = {
801
816
 
802
817
  const resolveDebug = async () => {
803
818
  try {
804
- const cfg = api.runtime.config.current();
819
+ const cfg = getOpenClawRuntimeConfig(api);
805
820
  return Boolean((cfg as any)?.channels?.bncr?.debug?.verbose);
806
821
  } catch {
807
822
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmoxmo/bncr",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,17 +27,18 @@
27
27
  "selfcheck": "node ./scripts/selfcheck.mjs",
28
28
  "test": "node --import ./tests/register-ts-hooks.mjs --test ./tests/*.test.mjs",
29
29
  "check-register-drift": "node ./scripts/check-register-drift.mjs",
30
+ "check-pack": "node ./scripts/check-pack.mjs",
30
31
  "format:check": "biome format --check .",
31
32
  "format": "biome format --write .",
32
33
  "lint": "biome lint .",
33
34
  "check": "biome check ."
34
35
  },
35
36
  "peerDependencies": {
36
- "openclaw": ">=2026.5.3-1"
37
+ "openclaw": ">=2026.5.27"
37
38
  },
38
39
  "devDependencies": {
39
40
  "@biomejs/biome": "^1.9.4",
40
- "openclaw": ">=2026.5.3-1"
41
+ "openclaw": ">=2026.5.27"
41
42
  },
42
43
  "openclaw": {
43
44
  "extensions": [
@@ -0,0 +1,61 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const root = path.resolve(__dirname, '..');
9
+
10
+ const requiredPackFiles = [
11
+ 'README.md',
12
+ 'index.ts',
13
+ 'openclaw.plugin.json',
14
+ 'package.json',
15
+ 'scripts/selfcheck.mjs',
16
+ 'scripts/check-register-drift.mjs',
17
+ 'src/channel.ts',
18
+ 'src/messaging/outbound/durable-message-adapter.ts',
19
+ 'src/messaging/outbound/durable-queue-adapter.ts',
20
+ 'src/openclaw/config-runtime.ts',
21
+ 'src/openclaw/inbound-session-runtime.ts',
22
+ 'src/openclaw/ingress-runtime.ts',
23
+ 'src/openclaw/media-runtime.ts',
24
+ 'src/openclaw/reply-runtime.ts',
25
+ 'src/openclaw/routing-runtime.ts',
26
+ 'src/openclaw/sdk-helpers.ts',
27
+ 'src/openclaw/session-route-runtime.ts',
28
+ ];
29
+
30
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
31
+ const output = execFileSync('npm', ['pack', '--dry-run', '--json'], {
32
+ cwd: root,
33
+ encoding: 'utf8',
34
+ stdio: ['ignore', 'pipe', 'pipe'],
35
+ });
36
+ const [pack] = JSON.parse(output);
37
+ const packedFiles = new Set((pack?.files ?? []).map((file) => file.path));
38
+ const missing = requiredPackFiles.filter((file) => !packedFiles.has(file));
39
+ const channelSource = fs.readFileSync(path.join(root, 'src/channel.ts'), 'utf8');
40
+ const channelMessageChecks = {
41
+ registered: channelSource.includes('message: {'),
42
+ text: channelSource.includes('channelMessageSendText'),
43
+ media: channelSource.includes('channelMessageSendMedia'),
44
+ payload: channelSource.includes('channelMessageSendPayload'),
45
+ manualAck: channelSource.includes("defaultAckPolicy: 'manual'"),
46
+ genericActionsPreserved: channelSource.includes('actions: messageActions'),
47
+ noDurableFinal: !channelSource.includes('durableFinal:'),
48
+ };
49
+ const channelMessageOk = Object.values(channelMessageChecks).every(Boolean);
50
+
51
+ const result = {
52
+ ok: missing.length === 0 && pkg.peerDependencies?.openclaw === '>=2026.5.27' && channelMessageOk,
53
+ package: pack?.id,
54
+ entryCount: pack?.entryCount,
55
+ missing,
56
+ openclaw: pkg.peerDependencies?.openclaw,
57
+ channelMessageChecks,
58
+ };
59
+
60
+ console.log(JSON.stringify(result, null, 2));
61
+ if (!result.ok) process.exit(1);
@@ -24,6 +24,16 @@ const requiredFiles = [
24
24
  'src/messaging/outbound/send.ts',
25
25
  'src/messaging/outbound/media.ts',
26
26
  'src/messaging/outbound/actions.ts',
27
+ 'src/messaging/outbound/durable-message-adapter.ts',
28
+ 'src/messaging/outbound/durable-queue-adapter.ts',
29
+ 'src/openclaw/config-runtime.ts',
30
+ 'src/openclaw/inbound-session-runtime.ts',
31
+ 'src/openclaw/ingress-runtime.ts',
32
+ 'src/openclaw/media-runtime.ts',
33
+ 'src/openclaw/reply-runtime.ts',
34
+ 'src/openclaw/routing-runtime.ts',
35
+ 'src/openclaw/sdk-helpers.ts',
36
+ 'src/openclaw/session-route-runtime.ts',
27
37
  ];
28
38
 
29
39
  const readPackageVersion = () => {
package/src/channel.ts CHANGED
@@ -1,21 +1,11 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { readBooleanParam } from 'openclaw/plugin-sdk/boolean-param';
5
4
  import type {
6
5
  GatewayRequestHandlerOptions,
7
6
  OpenClawPluginApi,
8
7
  OpenClawPluginServiceContext,
9
8
  } from 'openclaw/plugin-sdk/core';
10
- import {
11
- applyAccountNameToChannelSection,
12
- jsonResult,
13
- setAccountEnabledInConfigSection,
14
- } from 'openclaw/plugin-sdk/core';
15
- import { readJsonFileWithFallback, writeJsonFileAtomically } from 'openclaw/plugin-sdk/json-store';
16
- import { readStringParam } from 'openclaw/plugin-sdk/param-readers';
17
- import { createDefaultChannelRuntimeState } from 'openclaw/plugin-sdk/status-helpers';
18
- import { extractToolSend } from 'openclaw/plugin-sdk/tool-send';
19
9
  import {
20
10
  BNCR_DEFAULT_ACCOUNT_ID,
21
11
  CHANNEL_ID,
@@ -78,6 +68,27 @@ import { buildExtendedDiagnostics as buildExtendedDiagnosticsFromRuntime } from
78
68
  import { observeLeaseState, matchesTransferOwner as matchesTransferOwnerFromRuntime } from './core/lease-state.ts';
79
69
  import { emitBncrLog, emitBncrLogLine } from './core/logging.ts';
80
70
  import { resolveBncrChannelPolicy, resolveBncrConfigWarnings } from './core/policy.ts';
71
+ import {
72
+ getOpenClawRuntimeConfig,
73
+ getOpenClawRuntimeConfigOrDefault,
74
+ } from './openclaw/config-runtime.ts';
75
+ import {
76
+ loadOpenClawWebMedia,
77
+ saveOpenClawChannelMediaBuffer,
78
+ type OpenClawLoadedMedia,
79
+ } from './openclaw/media-runtime.ts';
80
+ import { resolveOpenClawAgentRoute } from './openclaw/routing-runtime.ts';
81
+ import {
82
+ applyOpenClawAccountNameToChannelSection,
83
+ createOpenClawDefaultChannelRuntimeState,
84
+ extractOpenClawToolSend,
85
+ openClawJsonResult,
86
+ readOpenClawBooleanParam,
87
+ readOpenClawJsonFileWithFallback,
88
+ readOpenClawStringParam,
89
+ setOpenClawAccountEnabledInConfigSection,
90
+ writeOpenClawJsonFileAtomically,
91
+ } from './openclaw/sdk-helpers.ts';
81
92
  import {
82
93
  buildRegisterTraceSummary as buildRegisterTraceSummaryFromEntries,
83
94
  classifyRegisterTrace as classifyRegisterTraceFromStack,
@@ -309,6 +320,7 @@ import {
309
320
  computeRetryRerouteDecision,
310
321
  } from './messaging/outbound/retry-policy.ts';
311
322
  import { sendBncrMedia, sendBncrText } from './messaging/outbound/send.ts';
323
+ import { buildBncrDurableQueuedResult } from './messaging/outbound/durable-queue-adapter.ts';
312
324
  import { resolveBncrOutboundSessionRoute } from './messaging/outbound/session-route.ts';
313
325
  import {
314
326
  looksLikeBncrExplicitTarget,
@@ -481,20 +493,20 @@ function normalizeBncrSendParams(input: {
481
493
  accountId: string;
482
494
  }): NormalizedBncrSendParams {
483
495
  const paramsObj = isPlainObject(input.params) ? input.params : {};
484
- const to = readStringParam(paramsObj, 'to', { required: true });
496
+ const to = readOpenClawStringParam(paramsObj, 'to', { required: true });
485
497
  const resolvedAccountId = normalizeAccountId(
486
- readStringParam(paramsObj, 'accountId') ?? input.accountId,
498
+ readOpenClawStringParam(paramsObj, 'accountId') ?? input.accountId,
487
499
  );
488
500
 
489
- const message = readStringParam(paramsObj, 'message', { allowEmpty: true }) ?? '';
490
- const caption = readStringParam(paramsObj, 'caption', { allowEmpty: true }) ?? '';
501
+ const message = readOpenClawStringParam(paramsObj, 'message', { allowEmpty: true }) ?? '';
502
+ const caption = readOpenClawStringParam(paramsObj, 'caption', { allowEmpty: true }) ?? '';
491
503
  const mediaUrl =
492
- readStringParam(paramsObj, 'media', { trim: false }) ??
493
- readStringParam(paramsObj, 'path', { trim: false }) ??
494
- readStringParam(paramsObj, 'filePath', { trim: false }) ??
495
- readStringParam(paramsObj, 'mediaUrl', { trim: false });
496
- const asVoice = readBooleanParam(paramsObj, 'asVoice') ?? false;
497
- const audioAsVoice = readBooleanParam(paramsObj, 'audioAsVoice') ?? false;
504
+ readOpenClawStringParam(paramsObj, 'media', { trim: false }) ??
505
+ readOpenClawStringParam(paramsObj, 'path', { trim: false }) ??
506
+ readOpenClawStringParam(paramsObj, 'filePath', { trim: false }) ??
507
+ readOpenClawStringParam(paramsObj, 'mediaUrl', { trim: false });
508
+ const asVoice = readOpenClawBooleanParam(paramsObj, 'asVoice') ?? false;
509
+ const audioAsVoice = readOpenClawBooleanParam(paramsObj, 'audioAsVoice') ?? false;
498
510
 
499
511
  if (asVoice && !mediaUrl) throw new Error('send voice requires media path');
500
512
 
@@ -1198,7 +1210,7 @@ class BncrBridgeRuntime {
1198
1210
  this.stopped = false;
1199
1211
  this.statePath = path.join(ctx.stateDir, 'bncr-bridge-state.json');
1200
1212
  try {
1201
- const cfg = this.api.runtime.config.current();
1213
+ const cfg = getOpenClawRuntimeConfig(this.api);
1202
1214
  this.initializeCanonicalAgentId(cfg);
1203
1215
  for (const warning of resolveBncrConfigWarnings(cfg?.channels?.[CHANNEL_ID] || {})) {
1204
1216
  this.logWarn('config', warning);
@@ -1292,7 +1304,7 @@ class BncrBridgeRuntime {
1292
1304
 
1293
1305
  private async refreshDebugFlagFromConfig(options?: { forceLog?: boolean }) {
1294
1306
  try {
1295
- const cfg = this.api.runtime.config.current();
1307
+ const cfg = getOpenClawRuntimeConfig(this.api);
1296
1308
  const raw = (cfg as any)?.channels?.[CHANNEL_ID]?.debug?.verbose;
1297
1309
  const next = typeof raw === 'boolean' ? raw : false;
1298
1310
  const changed = next !== BNCR_DEBUG_VERBOSE;
@@ -1316,7 +1328,7 @@ class BncrBridgeRuntime {
1316
1328
  channelId?: string;
1317
1329
  }): string | null {
1318
1330
  try {
1319
- const resolved = this.api.runtime.channel.routing.resolveAgentRoute({
1331
+ const resolved = resolveOpenClawAgentRoute(this.api, {
1320
1332
  cfg: args.cfg,
1321
1333
  channel: args.channelId || CHANNEL_ID,
1322
1334
  accountId: normalizeAccountId(args.accountId),
@@ -1440,7 +1452,7 @@ class BncrBridgeRuntime {
1440
1452
 
1441
1453
  private async loadState() {
1442
1454
  if (!this.statePath) return;
1443
- const loaded = await readJsonFileWithFallback(this.statePath, {
1455
+ const loaded = await readOpenClawJsonFileWithFallback(this.statePath, {
1444
1456
  outbox: [],
1445
1457
  deadLetter: [],
1446
1458
  sessionRoutes: [],
@@ -1709,7 +1721,7 @@ class BncrBridgeRuntime {
1709
1721
  : null,
1710
1722
  };
1711
1723
 
1712
- await writeJsonFileAtomically(this.statePath, data);
1724
+ await writeOpenClawJsonFileAtomically(this.statePath, data);
1713
1725
  }
1714
1726
 
1715
1727
  private resolveMessageAck(messageId: string, result: 'acked' | 'timeout' = 'acked') {
@@ -2863,10 +2875,10 @@ class BncrBridgeRuntime {
2863
2875
  });
2864
2876
  }
2865
2877
 
2866
- private prepareInboundAcceptance(args: {
2878
+ private async prepareInboundAcceptance(args: {
2867
2879
  parsed: ReturnType<typeof parseBncrInboundParams>;
2868
2880
  canonicalAgentId: string;
2869
- }):
2881
+ }): Promise<
2870
2882
  | {
2871
2883
  ok: true;
2872
2884
  accountId: string;
@@ -2878,7 +2890,8 @@ class BncrBridgeRuntime {
2878
2890
  ok: false;
2879
2891
  status: boolean;
2880
2892
  payload: ReturnType<typeof buildInboundResponsePayload>;
2881
- } {
2893
+ }
2894
+ > {
2882
2895
  const { parsed, canonicalAgentId } = args;
2883
2896
  const {
2884
2897
  accountId,
@@ -2915,8 +2928,8 @@ class BncrBridgeRuntime {
2915
2928
  };
2916
2929
  }
2917
2930
 
2918
- const cfg = this.api.runtime.config.current();
2919
- const gate = checkBncrMessageGate({
2931
+ const cfg = getOpenClawRuntimeConfig(this.api);
2932
+ const gate = await checkBncrMessageGate({
2920
2933
  parsed,
2921
2934
  cfg,
2922
2935
  account: resolveAccount(cfg, accountId),
@@ -2944,7 +2957,7 @@ class BncrBridgeRuntime {
2944
2957
  taskKey: extracted.taskKey,
2945
2958
  text,
2946
2959
  extractedText: extracted.text,
2947
- resolveAgentRoute: (params) => this.api.runtime.channel.routing.resolveAgentRoute(params),
2960
+ resolveAgentRoute: (params) => resolveOpenClawAgentRoute(this.api, params),
2948
2961
  });
2949
2962
 
2950
2963
  return {
@@ -3089,7 +3102,7 @@ class BncrBridgeRuntime {
3089
3102
 
3090
3103
  private isOutboundAckRequired(accountId?: string) {
3091
3104
  try {
3092
- const cfg = this.api.runtime.config.current();
3105
+ const cfg = getOpenClawRuntimeConfig(this.api);
3093
3106
  const channelCfg = (cfg as any)?.channels?.[CHANNEL_ID];
3094
3107
  const accountCfg =
3095
3108
  accountId && channelCfg?.accounts && typeof channelCfg.accounts === 'object'
@@ -3108,7 +3121,7 @@ class BncrBridgeRuntime {
3108
3121
  private buildRuntimeFlags(accountId?: string) {
3109
3122
  let ackPolicySource: 'channel' | 'default' = 'default';
3110
3123
  try {
3111
- const cfg = this.api.runtime.config.current();
3124
+ const cfg = getOpenClawRuntimeConfig(this.api);
3112
3125
  const global = (cfg as any)?.channels?.[CHANNEL_ID]?.outboundRequireAck;
3113
3126
  if (typeof global === 'boolean') ackPolicySource = 'channel';
3114
3127
  } catch {
@@ -3920,7 +3933,7 @@ class BncrBridgeRuntime {
3920
3933
  const canonicalAgentId =
3921
3934
  this.canonicalAgentId ||
3922
3935
  this.ensureCanonicalAgentId({
3923
- cfg: this.api.runtime.config?.get?.() || {},
3936
+ cfg: getOpenClawRuntimeConfigOrDefault(this.api, {}),
3924
3937
  accountId: acc,
3925
3938
  channelId: CHANNEL_ID,
3926
3939
  peer: { kind: 'direct', id: route.groupId === '0' ? route.userId : route.groupId },
@@ -4568,7 +4581,7 @@ class BncrBridgeRuntime {
4568
4581
  mediaUrl: string,
4569
4582
  mediaLocalRoots?: readonly string[],
4570
4583
  ): Promise<{ mediaBase64: string; mimeType?: string; fileName?: string }> {
4571
- const loaded = await this.api.runtime.media.loadWebMedia(mediaUrl, {
4584
+ const loaded = await loadOpenClawWebMedia(this.api, mediaUrl, {
4572
4585
  localRoots: mediaLocalRoots,
4573
4586
  maxBytes: 20 * 1024 * 1024,
4574
4587
  });
@@ -4583,12 +4596,12 @@ class BncrBridgeRuntime {
4583
4596
  mediaUrl: string;
4584
4597
  mediaLocalRoots?: readonly string[];
4585
4598
  }): Promise<{
4586
- loaded: Awaited<ReturnType<OpenClawPluginApi['runtime']['media']['loadWebMedia']>>;
4599
+ loaded: OpenClawLoadedMedia;
4587
4600
  size: number;
4588
4601
  mimeType?: string;
4589
4602
  fileName: string;
4590
4603
  }> {
4591
- const loaded = await this.api.runtime.media.loadWebMedia(params.mediaUrl, {
4604
+ const loaded = await loadOpenClawWebMedia(this.api, params.mediaUrl, {
4592
4605
  localRoots: params.mediaLocalRoots,
4593
4606
  maxBytes: 50 * 1024 * 1024,
4594
4607
  });
@@ -5408,7 +5421,7 @@ class BncrBridgeRuntime {
5408
5421
 
5409
5422
  handleDiagnostics = async ({ params, respond }: GatewayRequestHandlerOptions) => {
5410
5423
  const accountId = normalizeAccountId(asString(params?.accountId || ''));
5411
- const cfg = this.api.runtime.config.current();
5424
+ const cfg = getOpenClawRuntimeConfig(this.api);
5412
5425
  const runtime = this.getAccountRuntimeSnapshot(accountId);
5413
5426
  const diagnostics = this.buildExtendedDiagnostics(accountId);
5414
5427
 
@@ -5703,7 +5716,8 @@ class BncrBridgeRuntime {
5703
5716
  throw new Error('file sha256 mismatch');
5704
5717
  }
5705
5718
 
5706
- const saved = await this.api.runtime.channel.media.saveMediaBuffer(
5719
+ const saved = await saveOpenClawChannelMediaBuffer(
5720
+ this.api,
5707
5721
  merged,
5708
5722
  st.mimeType,
5709
5723
  'inbound',
@@ -6038,14 +6052,14 @@ class BncrBridgeRuntime {
6038
6052
  this.lastInboundAtGlobal = now();
6039
6053
  this.incrementCounter(this.inboundEventsByAccount, accountId);
6040
6054
 
6041
- const cfg = this.api.runtime.config.current();
6055
+ const cfg = getOpenClawRuntimeConfig(this.api);
6042
6056
  const canonicalAgentId = this.ensureCanonicalAgentId({
6043
6057
  cfg,
6044
6058
  accountId,
6045
6059
  peer,
6046
6060
  channelId: CHANNEL_ID,
6047
6061
  });
6048
- const acceptance = this.prepareInboundAcceptance({ parsed, canonicalAgentId });
6062
+ const acceptance = await this.prepareInboundAcceptance({ parsed, canonicalAgentId });
6049
6063
  if (!acceptance.ok) {
6050
6064
  respond(acceptance.status, acceptance.payload);
6051
6065
  return;
@@ -6352,6 +6366,65 @@ class BncrBridgeRuntime {
6352
6366
  createMessageId: () => randomUUID(),
6353
6367
  });
6354
6368
  };
6369
+
6370
+ private async enqueueChannelMessageHandoff(ctx: any, payload: ReplyPayloadInput) {
6371
+ const accountId = normalizeAccountId(ctx.accountId);
6372
+ const to = asString(ctx.to || '').trim();
6373
+ const verified = this.resolveVerifiedTarget(to, accountId);
6374
+ this.rememberSessionRoute(verified.sessionKey, accountId, verified.route);
6375
+ const before = new Set(this.outbox.keys());
6376
+ await this.enqueueFromReply({
6377
+ accountId,
6378
+ sessionKey: verified.sessionKey,
6379
+ route: verified.route,
6380
+ payload,
6381
+ mediaLocalRoots: ctx.mediaLocalRoots,
6382
+ });
6383
+ const entries = Array.from(this.outbox.values()).filter((entry) => !before.has(entry.messageId));
6384
+ if (!entries.length) {
6385
+ throw new Error('bncr channel.message handoff did not enqueue an outbox entry');
6386
+ }
6387
+ return entries[entries.length - 1];
6388
+ }
6389
+
6390
+ channelMessageSendText = async (ctx: any) => {
6391
+ const entry = await this.enqueueChannelMessageHandoff(ctx, {
6392
+ text: asString(ctx.text || ''),
6393
+ kind: ctx?.kind,
6394
+ replyToId: this.resolveChannelSendReplyToId(ctx),
6395
+ });
6396
+ return buildBncrDurableQueuedResult({ entry });
6397
+ };
6398
+
6399
+ channelMessageSendMedia = async (ctx: any) => {
6400
+ const entry = await this.enqueueChannelMessageHandoff(ctx, {
6401
+ text: asString(ctx.text || ''),
6402
+ mediaUrl: asString(ctx.mediaUrl || ''),
6403
+ mediaUrls: Array.isArray(ctx?.mediaUrls) ? ctx.mediaUrls : undefined,
6404
+ asVoice: ctx?.asVoice === true,
6405
+ audioAsVoice: ctx?.audioAsVoice === true,
6406
+ kind: ctx?.kind,
6407
+ replyToId: this.resolveChannelSendReplyToId(ctx),
6408
+ });
6409
+ return buildBncrDurableQueuedResult({ entry });
6410
+ };
6411
+
6412
+ channelMessageSendPayload = async (ctx: any) => {
6413
+ const payload = ctx?.payload || {};
6414
+ if (!payload || typeof payload !== 'object') {
6415
+ throw new Error('bncr channel.message payload must be an object');
6416
+ }
6417
+ const entry = await this.enqueueChannelMessageHandoff(ctx, {
6418
+ text: asString(payload.text || payload.message || payload.caption || ''),
6419
+ mediaUrl: asString(payload.mediaUrl || ''),
6420
+ mediaUrls: Array.isArray(payload.mediaUrls) ? payload.mediaUrls : undefined,
6421
+ asVoice: payload.asVoice === true,
6422
+ audioAsVoice: payload.audioAsVoice === true,
6423
+ kind: payload.kind,
6424
+ replyToId: asString(payload.replyToId || ctx?.replyToId || ctx?.replyToMessageId || '').trim() || undefined,
6425
+ });
6426
+ return buildBncrDurableQueuedResult({ entry });
6427
+ };
6355
6428
  }
6356
6429
 
6357
6430
  export function createBncrBridge(api: OpenClawPluginApi) {
@@ -6387,7 +6460,7 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
6387
6460
  };
6388
6461
  },
6389
6462
  supportsAction: ({ action }) => action === 'send',
6390
- extractToolSend: ({ args }) => extractToolSend(args, 'sendMessage'),
6463
+ extractToolSend: ({ args }) => extractOpenClawToolSend(args, 'sendMessage'),
6391
6464
  handleAction: async ({ action, params, accountId, mediaLocalRoots }) => {
6392
6465
  if (action !== 'send')
6393
6466
  throw new Error(`Action ${action} is not supported for provider ${CHANNEL_ID}.`);
@@ -6425,7 +6498,7 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
6425
6498
  createMessageId: () => randomUUID(),
6426
6499
  });
6427
6500
 
6428
- return jsonResult({ ok: true, ...result });
6501
+ return openClawJsonResult({ ok: true, ...result });
6429
6502
  },
6430
6503
  };
6431
6504
 
@@ -6440,6 +6513,17 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
6440
6513
  aliases: ['bncr'],
6441
6514
  },
6442
6515
  actions: messageActions,
6516
+ message: {
6517
+ receive: {
6518
+ defaultAckPolicy: 'manual' as const,
6519
+ supportedAckPolicies: ['manual'] as const,
6520
+ },
6521
+ send: {
6522
+ text: async (ctx: any) => getBridge().channelMessageSendText(ctx),
6523
+ media: async (ctx: any) => getBridge().channelMessageSendMedia(ctx),
6524
+ payload: async (ctx: any) => getBridge().channelMessageSendPayload(ctx),
6525
+ },
6526
+ },
6443
6527
  capabilities: {
6444
6528
  chatTypes: ['direct'] as ChatType[],
6445
6529
  media: true,
@@ -6528,7 +6612,7 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
6528
6612
  listAccountIds,
6529
6613
  resolveAccount,
6530
6614
  setAccountEnabled: ({ cfg, accountId, enabled }: any) =>
6531
- setAccountEnabledInConfigSection({
6615
+ setOpenClawAccountEnabledInConfigSection({
6532
6616
  cfg,
6533
6617
  sectionKey: CHANNEL_ID,
6534
6618
  accountId,
@@ -6552,7 +6636,7 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
6552
6636
  },
6553
6637
  setup: {
6554
6638
  applyAccountName: ({ cfg, accountId, name }: any) =>
6555
- applyAccountNameToChannelSection({
6639
+ applyOpenClawAccountNameToChannelSection({
6556
6640
  cfg,
6557
6641
  channelKey: CHANNEL_ID,
6558
6642
  accountId,
@@ -6604,7 +6688,7 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
6604
6688
  }),
6605
6689
  },
6606
6690
  status: {
6607
- defaultRuntime: createDefaultChannelRuntimeState(BNCR_DEFAULT_ACCOUNT_ID, {
6691
+ defaultRuntime: createOpenClawDefaultChannelRuntimeState(BNCR_DEFAULT_ACCOUNT_ID, {
6608
6692
  mode: 'ws-offline',
6609
6693
  }),
6610
6694
  buildChannelSummary: async ({ defaultAccountId }: any) => {
@@ -51,8 +51,17 @@ function parseRouteFromStandardDisplayScope(scope: string): BncrRoute | null {
51
51
  return null;
52
52
  }
53
53
 
54
- export function parseRouteFromDisplayScope(scope: string): BncrRoute | null {
54
+ function normalizeDisplayScopePrefix(scope: string): string {
55
55
  const raw = asString(scope).trim();
56
+ if (!raw) return '';
57
+ if (raw.startsWith('Bncr:')) return raw;
58
+ if (/^bncr[:-]/i.test(raw)) return raw;
59
+ if (!parseRouteFromStandardDisplayScope(raw)) return raw;
60
+ return `Bncr:${raw}`;
61
+ }
62
+
63
+ export function parseRouteFromDisplayScope(scope: string): BncrRoute | null {
64
+ const raw = normalizeDisplayScopePrefix(scope);
56
65
  if (!raw) return null;
57
66
 
58
67
  const payload = raw.match(/^Bncr:(.+)$/)?.[1];