@xmoxmo/bncr 0.2.6 → 0.2.8

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.
Files changed (45) hide show
  1. package/README.md +7 -1
  2. package/index.ts +30 -15
  3. package/package.json +4 -3
  4. package/scripts/check-pack.mjs +77 -0
  5. package/scripts/selfcheck.mjs +10 -0
  6. package/src/channel.ts +398 -642
  7. package/src/core/extended-diagnostics.ts +10 -0
  8. package/src/core/file-ack.ts +9 -0
  9. package/src/core/file-transfer-payloads.ts +72 -0
  10. package/src/core/register-trace.ts +79 -0
  11. package/src/core/targets.ts +10 -1
  12. package/src/messaging/inbound/commands.ts +20 -10
  13. package/src/messaging/inbound/context-facts.ts +200 -0
  14. package/src/messaging/inbound/dispatch.ts +66 -14
  15. package/src/messaging/inbound/gate.ts +66 -26
  16. package/src/messaging/inbound/runtime-compat.ts +41 -0
  17. package/src/messaging/inbound/session-label.ts +7 -7
  18. package/src/messaging/outbound/durable-message-adapter.ts +107 -0
  19. package/src/messaging/outbound/durable-queue-adapter.ts +157 -0
  20. package/src/messaging/outbound/session-route.ts +2 -2
  21. package/src/openclaw/config-runtime.ts +52 -0
  22. package/src/openclaw/inbound-session-runtime.ts +94 -0
  23. package/src/openclaw/ingress-runtime.ts +35 -0
  24. package/src/openclaw/media-runtime.ts +73 -0
  25. package/src/openclaw/reply-runtime.ts +104 -0
  26. package/src/openclaw/routing-runtime.ts +48 -0
  27. package/src/openclaw/sdk-helpers.ts +20 -0
  28. package/src/openclaw/session-route-runtime.ts +15 -0
  29. package/src/plugin/capabilities.ts +8 -0
  30. package/src/plugin/config.ts +35 -0
  31. package/src/plugin/gateway-methods.ts +12 -0
  32. package/src/plugin/gateway-runtime.ts +11 -0
  33. package/src/plugin/message-policy.ts +4 -0
  34. package/src/plugin/message-send.ts +13 -0
  35. package/src/plugin/messaging.ts +142 -0
  36. package/src/plugin/meta.ts +10 -0
  37. package/src/plugin/outbound.ts +51 -0
  38. package/src/plugin/setup.ts +24 -0
  39. package/src/plugin/status.ts +38 -0
  40. package/src/runtime/log-dedupe.ts +56 -0
  41. package/src/runtime/outbound-ack-timeout.ts +96 -0
  42. package/src/runtime/outbound-flags.ts +81 -0
  43. package/src/runtime/outbox-transitions.ts +119 -0
  44. package/src/runtime/status-snapshots.ts +108 -0
  45. package/src/runtime/status-worker.ts +172 -0
package/src/channel.ts CHANGED
@@ -1,28 +1,17 @@
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,
22
12
  listAccountIds,
23
13
  normalizeAccountId,
24
14
  resolveAccount,
25
- resolveDefaultDisplayName,
26
15
  } from './core/accounts.ts';
27
16
  import { BncrConfigSchema } from './core/config-schema.ts';
28
17
  import {
@@ -77,14 +66,40 @@ import { buildDownlinkHealth as buildDownlinkHealthFromRuntime } from './core/do
77
66
  import { buildExtendedDiagnostics as buildExtendedDiagnosticsFromRuntime } from './core/extended-diagnostics.ts';
78
67
  import { observeLeaseState, matchesTransferOwner as matchesTransferOwnerFromRuntime } from './core/lease-state.ts';
79
68
  import { emitBncrLog, emitBncrLogLine } from './core/logging.ts';
69
+ import { buildFileAckKey } from './core/file-ack.ts';
70
+ import {
71
+ buildFileTransferAbortPayload,
72
+ buildFileTransferChunkPayload,
73
+ buildFileTransferCompletePayload,
74
+ buildFileTransferInitPayload,
75
+ } from './core/file-transfer-payloads.ts';
80
76
  import { resolveBncrChannelPolicy, resolveBncrConfigWarnings } from './core/policy.ts';
81
77
  import {
78
+ getOpenClawRuntimeConfig,
79
+ getOpenClawRuntimeConfigOrDefault,
80
+ } from './openclaw/config-runtime.ts';
81
+ import {
82
+ loadOpenClawWebMedia,
83
+ saveOpenClawChannelMediaBuffer,
84
+ type OpenClawLoadedMedia,
85
+ } from './openclaw/media-runtime.ts';
86
+ import { resolveOpenClawAgentRoute } from './openclaw/routing-runtime.ts';
87
+ import {
88
+ extractOpenClawToolSend,
89
+ openClawJsonResult,
90
+ readOpenClawBooleanParam,
91
+ readOpenClawJsonFileWithFallback,
92
+ readOpenClawStringParam,
93
+ writeOpenClawJsonFileAtomically,
94
+ } from './openclaw/sdk-helpers.ts';
95
+ import {
96
+ appendBoundedRegisterTrace,
97
+ buildRegisterDriftSnapshot,
98
+ buildRegisterTraceEntry,
82
99
  buildRegisterTraceSummary as buildRegisterTraceSummaryFromEntries,
83
- classifyRegisterTrace as classifyRegisterTraceFromStack,
84
100
  } from './core/register-trace.ts';
85
101
  import {
86
102
  buildAccountRuntimeSnapshot,
87
- buildAccountStatusSnapshot,
88
103
  buildIntegratedDiagnostics as buildIntegratedDiagnosticsFromRuntime,
89
104
  buildStatusHeadlineFromRuntime,
90
105
  buildStatusMetaFromRuntime,
@@ -92,11 +107,9 @@ import {
92
107
  import {
93
108
  buildCanonicalBncrSessionKey,
94
109
  formatDisplayScope,
95
- formatTargetDisplay,
96
110
  isLowerHex,
97
111
  normalizeInboundSessionKey,
98
112
  normalizeStoredSessionKey,
99
- parseExplicitTarget,
100
113
  parseRouteFromDisplayScope,
101
114
  parseRouteFromHexScope,
102
115
  parseRouteFromScope,
@@ -309,11 +322,48 @@ import {
309
322
  computeRetryRerouteDecision,
310
323
  } from './messaging/outbound/retry-policy.ts';
311
324
  import { sendBncrMedia, sendBncrText } from './messaging/outbound/send.ts';
312
- import { resolveBncrOutboundSessionRoute } from './messaging/outbound/session-route.ts';
325
+ import { buildBncrDurableQueuedResult } from './messaging/outbound/durable-queue-adapter.ts';
326
+ import { BNCR_CHANNEL_CAPABILITIES } from './plugin/capabilities.ts';
327
+ import { BNCR_CONFIG_SURFACE } from './plugin/config.ts';
328
+ import { createBncrGatewayRuntime } from './plugin/gateway-runtime.ts';
329
+ import { BNCR_GATEWAY_METHODS } from './plugin/gateway-methods.ts';
330
+ import { BNCR_CHANNEL_META } from './plugin/meta.ts';
331
+ import { createBncrMessagingSurface } from './plugin/messaging.ts';
332
+ import { createBncrMessageSend } from './plugin/message-send.ts';
333
+ import { BNCR_MESSAGE_RECEIVE_POLICY } from './plugin/message-policy.ts';
334
+ import { createBncrOutboundRuntime } from './plugin/outbound.ts';
335
+ import { BNCR_SETUP_SURFACE } from './plugin/setup.ts';
336
+ import { createBncrStatusSurface } from './plugin/status.ts';
337
+ import {
338
+ clearAllBncrStatusWorkers,
339
+ clearBncrStatusWorker,
340
+ startBncrStatusWorker,
341
+ stopBncrStatusWorker,
342
+ type ChannelAccountWorkerHandle,
343
+ } from './runtime/status-worker.ts';
344
+ import {
345
+ buildBncrRuntimeAckStrategy,
346
+ computeBncrRecommendedAckTimeoutMs,
347
+ computeBncrRecommendedAckTimeoutReason,
348
+ } from './runtime/outbound-ack-timeout.ts';
349
+ import {
350
+ buildBncrRuntimeFlags,
351
+ buildBncrRuntimeStatusInput,
352
+ resolveBncrOutboundAckRequired,
353
+ } from './runtime/outbound-flags.ts';
354
+ import { buildRuntimeStatusSnapshots } from './runtime/status-snapshots.ts';
313
355
  import {
314
- looksLikeBncrExplicitTarget,
315
- resolveBncrOutboundTarget,
316
- } from './messaging/outbound/target-resolver.ts';
356
+ applyBncrPushFailureDecisionToEntry,
357
+ applyBncrRetryRerouteDecisionToEntry,
358
+ buildBncrAckOkTelemetryPatch,
359
+ buildBncrAckRetryEntryPatch,
360
+ buildBncrOutboxFailureEntryPatch,
361
+ buildBncrOutboxPushSuccessEntryPatch,
362
+ } from './runtime/outbox-transitions.ts';
363
+ import {
364
+ pruneLogDedupeState as pruneLogDedupeStateFromRuntime,
365
+ shouldEmitDedupLog as shouldEmitDedupLogFromRuntime,
366
+ } from './runtime/log-dedupe.ts';
317
367
  const BRIDGE_VERSION = 2;
318
368
  const BNCR_PUSH_EVENT = 'plugin.bncr.push';
319
369
  const BNCR_FILE_INIT_EVENT = 'plugin.bncr.file.init';
@@ -339,8 +389,6 @@ const ADAPTIVE_ACK_TIMEOUT_LOG_THROTTLE_MS = 5 * 60 * 1000;
339
389
  const OUTBOUND_READY_TTL_MS = 30_000;
340
390
  const PREFERRED_OUTBOUND_TTL_MS = 12_000;
341
391
  const FILE_FORCE_CHUNK = true; // 统一走 WS 分块,保留 base64 仅作兜底
342
- const LOG_DEDUPE_STATE_TTL_MS = 10 * 60 * 1000;
343
- const LOG_DEDUPE_STATE_MAX_ENTRIES = 1_000;
344
392
  const FILE_INLINE_THRESHOLD = 5 * 1024 * 1024; // fallback 阈值(仅 FILE_FORCE_CHUNK=false 时生效)
345
393
  const FILE_CHUNK_SIZE = 256 * 1024; // 256KB
346
394
  const INBOUND_FILE_TRANSFER_MAX_BYTES = 50 * 1024 * 1024;
@@ -377,12 +425,6 @@ type FileSendTransferState = {
377
425
  error?: string;
378
426
  };
379
427
 
380
- type ChannelAccountWorkerHandle = {
381
- timer: NodeJS.Timeout;
382
- finish: (reason: string) => void;
383
- cleanupAbortListener?: () => void;
384
- };
385
-
386
428
  type FileRecvTransferState = {
387
429
  transferId: string;
388
430
  accountId: string;
@@ -481,20 +523,20 @@ function normalizeBncrSendParams(input: {
481
523
  accountId: string;
482
524
  }): NormalizedBncrSendParams {
483
525
  const paramsObj = isPlainObject(input.params) ? input.params : {};
484
- const to = readStringParam(paramsObj, 'to', { required: true });
526
+ const to = readOpenClawStringParam(paramsObj, 'to', { required: true });
485
527
  const resolvedAccountId = normalizeAccountId(
486
- readStringParam(paramsObj, 'accountId') ?? input.accountId,
528
+ readOpenClawStringParam(paramsObj, 'accountId') ?? input.accountId,
487
529
  );
488
530
 
489
- const message = readStringParam(paramsObj, 'message', { allowEmpty: true }) ?? '';
490
- const caption = readStringParam(paramsObj, 'caption', { allowEmpty: true }) ?? '';
531
+ const message = readOpenClawStringParam(paramsObj, 'message', { allowEmpty: true }) ?? '';
532
+ const caption = readOpenClawStringParam(paramsObj, 'caption', { allowEmpty: true }) ?? '';
491
533
  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;
534
+ readOpenClawStringParam(paramsObj, 'media', { trim: false }) ??
535
+ readOpenClawStringParam(paramsObj, 'path', { trim: false }) ??
536
+ readOpenClawStringParam(paramsObj, 'filePath', { trim: false }) ??
537
+ readOpenClawStringParam(paramsObj, 'mediaUrl', { trim: false });
538
+ const asVoice = readOpenClawBooleanParam(paramsObj, 'asVoice') ?? false;
539
+ const audioAsVoice = readOpenClawBooleanParam(paramsObj, 'audioAsVoice') ?? false;
498
540
 
499
541
  if (asVoice && !mediaUrl) throw new Error('send voice requires media path');
500
542
 
@@ -825,27 +867,17 @@ class BncrBridgeRuntime {
825
867
  }
826
868
 
827
869
  private pruneLogDedupeState(currentTime = now()) {
828
- for (const [key, entry] of this.logDedupeState.entries()) {
829
- if (currentTime - entry.at > LOG_DEDUPE_STATE_TTL_MS) {
830
- this.logDedupeState.delete(key);
831
- }
832
- }
833
-
834
- while (this.logDedupeState.size > LOG_DEDUPE_STATE_MAX_ENTRIES) {
835
- const oldestKey = this.logDedupeState.keys().next().value;
836
- if (!oldestKey) break;
837
- this.logDedupeState.delete(oldestKey);
838
- }
870
+ pruneLogDedupeStateFromRuntime(this.logDedupeState, currentTime);
839
871
  }
840
872
 
841
873
  private shouldEmitDedupLog(key: string, sig: string, windowMs = 5 * 60 * 1000) {
842
- const t = now();
843
- this.pruneLogDedupeState(t);
844
- const prev = this.logDedupeState.get(key) || null;
845
- if (prev && prev.sig === sig && t - prev.at < windowMs) return false;
846
- this.logDedupeState.set(key, { at: t, sig });
847
- this.pruneLogDedupeState(t);
848
- return true;
874
+ return shouldEmitDedupLogFromRuntime({
875
+ state: this.logDedupeState,
876
+ key,
877
+ sig,
878
+ nowMs: now(),
879
+ windowMs,
880
+ });
849
881
  }
850
882
 
851
883
  private logInfoDedup(
@@ -942,39 +974,64 @@ class BncrBridgeRuntime {
942
974
  );
943
975
  }
944
976
 
977
+ private buildStatusWorkerRuntime() {
978
+ return {
979
+ workers: this.channelAccountWorkers,
980
+ bridgeId: this.bridgeId,
981
+ hooks: {
982
+ isOnline: (accountId: string) => this.isOnline(accountId),
983
+ hasRecentInboundReachability: (accountId: string) =>
984
+ this.hasRecentInboundReachability(accountId),
985
+ getLastActivityAt: (accountId: string, previous: Record<string, any>) =>
986
+ this.lastActivityByAccount.get(accountId) ||
987
+ this.lastInboundByAccount.get(accountId) ||
988
+ this.lastOutboundByAccount.get(accountId) ||
989
+ previous?.lastEventAt ||
990
+ null,
991
+ getActiveConnectionKey: (accountId: string) =>
992
+ this.activeConnectionByAccount.get(accountId) || null,
993
+ getActiveConnections: (accountId: string) =>
994
+ Array.from(this.connections.values())
995
+ .filter((c) => c.accountId === accountId)
996
+ .map((c) => ({
997
+ connId: c.connId,
998
+ clientId: c.clientId,
999
+ inboundOnly: c.inboundOnly === true,
1000
+ outboundReady: c.outboundReady === true,
1001
+ preferredForOutbound: c.preferredForOutbound === true,
1002
+ })),
1003
+ buildStatusMeta: (accountId: string) => this.buildStatusMeta(accountId),
1004
+ logInfo: (scope: string | undefined, message: string, options?: { debugOnly?: boolean }) =>
1005
+ this.logInfo(scope, message, options),
1006
+ logInfoDedup: (
1007
+ scope: string | undefined,
1008
+ message: string,
1009
+ options: { key: string; sig: string; debugOnly?: boolean; windowMs?: number },
1010
+ ) => this.logInfoDedup(scope, message, options),
1011
+ },
1012
+ };
1013
+ }
1014
+
945
1015
  private clearChannelAccountWorker(accountId: string, reason: string) {
946
- const worker = this.channelAccountWorkers.get(accountId);
947
- if (!worker) return false;
948
- worker.finish(reason);
949
- this.logInfo(
950
- 'health',
951
- `status-worker cleared ${JSON.stringify({ bridge: this.bridgeId, accountId, reason })}`,
952
- { debugOnly: true },
953
- );
954
- return true;
1016
+ return clearBncrStatusWorker(this.buildStatusWorkerRuntime(), accountId, reason);
955
1017
  }
956
1018
 
957
1019
  private clearAllChannelAccountWorkers(reason: string) {
958
- for (const accountId of Array.from(this.channelAccountWorkers.keys())) {
959
- this.clearChannelAccountWorker(accountId, reason);
960
- }
1020
+ clearAllBncrStatusWorkers(this.buildStatusWorkerRuntime(), reason);
961
1021
  }
962
1022
 
963
1023
  private captureDriftSnapshot(
964
1024
  summary: ReturnType<BncrBridgeRuntime['buildRegisterTraceSummary']>,
965
1025
  ) {
966
- this.lastDriftSnapshot = {
1026
+ this.lastDriftSnapshot = buildRegisterDriftSnapshot({
967
1027
  capturedAt: now(),
968
1028
  registerCount: this.registerCount,
969
1029
  apiGeneration: this.apiGeneration,
970
- postWarmupRegisterCount: summary.postWarmupRegisterCount,
1030
+ summary,
971
1031
  apiInstanceId: this.lastApiInstanceId,
972
1032
  registryFingerprint: this.lastRegistryFingerprint,
973
- dominantBucket: summary.dominantBucket,
974
- sourceBuckets: { ...summary.sourceBuckets },
975
- traceWindowSize: this.registerTraceRecent.length,
976
- traceRecent: this.registerTraceRecent.map((trace) => ({ ...trace })),
977
- };
1033
+ traceRecent: this.registerTraceRecent,
1034
+ });
978
1035
  this.scheduleSave();
979
1036
  }
980
1037
 
@@ -1014,9 +1071,7 @@ class BncrBridgeRuntime {
1014
1071
  .map((line) => line.trim())
1015
1072
  .filter(Boolean)
1016
1073
  .join(' <- ');
1017
- const stackBucket = classifyRegisterTraceFromStack(stack);
1018
-
1019
- const trace = {
1074
+ const trace = buildRegisterTraceEntry({
1020
1075
  ts,
1021
1076
  bridgeId: this.bridgeId,
1022
1077
  gatewayPid: this.gatewayPid,
@@ -1028,11 +1083,8 @@ class BncrBridgeRuntime {
1028
1083
  source: this.pluginSource,
1029
1084
  pluginVersion: this.pluginVersion,
1030
1085
  stack,
1031
- stackBucket,
1032
- };
1033
- this.registerTraceRecent.push(trace);
1034
- if (this.registerTraceRecent.length > 12)
1035
- this.registerTraceRecent.splice(0, this.registerTraceRecent.length - 12);
1086
+ });
1087
+ appendBoundedRegisterTrace(this.registerTraceRecent, trace, 12);
1036
1088
 
1037
1089
  const summary = this.buildRegisterTraceSummary();
1038
1090
  if (summary.postWarmupRegisterCount > 0) this.captureDriftSnapshot(summary);
@@ -1135,11 +1187,29 @@ class BncrBridgeRuntime {
1135
1187
  return matchesTransferOwnerFromRuntime(params);
1136
1188
  }
1137
1189
 
1190
+ private buildRuntimeSurfaceDiagnostics() {
1191
+ const channelRuntime = (this.api as any)?.runtime?.channel;
1192
+ const surfaces = {
1193
+ inbound: Boolean(channelRuntime?.inbound),
1194
+ media: Boolean(channelRuntime?.media),
1195
+ reply: Boolean(channelRuntime?.reply),
1196
+ routing: Boolean(channelRuntime?.routing),
1197
+ session: Boolean(channelRuntime?.session),
1198
+ };
1199
+ return {
1200
+ channel: surfaces,
1201
+ missing: Object.entries(surfaces)
1202
+ .filter(([, present]) => !present)
1203
+ .map(([name]) => name),
1204
+ };
1205
+ }
1206
+
1138
1207
  private buildExtendedDiagnostics(accountId: string) {
1139
1208
  const acc = normalizeAccountId(accountId);
1140
1209
  const diagnostics = this.buildIntegratedDiagnostics(acc) as Record<string, any>;
1141
1210
  return buildExtendedDiagnosticsFromRuntime({
1142
1211
  diagnostics,
1212
+ runtimeSurface: this.buildRuntimeSurfaceDiagnostics(),
1143
1213
  register: {
1144
1214
  bridgeId: this.bridgeId,
1145
1215
  gatewayPid: this.gatewayPid,
@@ -1198,7 +1268,7 @@ class BncrBridgeRuntime {
1198
1268
  this.stopped = false;
1199
1269
  this.statePath = path.join(ctx.stateDir, 'bncr-bridge-state.json');
1200
1270
  try {
1201
- const cfg = this.api.runtime.config.current();
1271
+ const cfg = getOpenClawRuntimeConfig(this.api);
1202
1272
  this.initializeCanonicalAgentId(cfg);
1203
1273
  for (const warning of resolveBncrConfigWarnings(cfg?.channels?.[CHANNEL_ID] || {})) {
1204
1274
  this.logWarn('config', warning);
@@ -1292,7 +1362,7 @@ class BncrBridgeRuntime {
1292
1362
 
1293
1363
  private async refreshDebugFlagFromConfig(options?: { forceLog?: boolean }) {
1294
1364
  try {
1295
- const cfg = this.api.runtime.config.current();
1365
+ const cfg = getOpenClawRuntimeConfig(this.api);
1296
1366
  const raw = (cfg as any)?.channels?.[CHANNEL_ID]?.debug?.verbose;
1297
1367
  const next = typeof raw === 'boolean' ? raw : false;
1298
1368
  const changed = next !== BNCR_DEBUG_VERBOSE;
@@ -1316,7 +1386,7 @@ class BncrBridgeRuntime {
1316
1386
  channelId?: string;
1317
1387
  }): string | null {
1318
1388
  try {
1319
- const resolved = this.api.runtime.channel.routing.resolveAgentRoute({
1389
+ const resolved = resolveOpenClawAgentRoute(this.api, {
1320
1390
  cfg: args.cfg,
1321
1391
  channel: args.channelId || CHANNEL_ID,
1322
1392
  accountId: normalizeAccountId(args.accountId),
@@ -1440,7 +1510,7 @@ class BncrBridgeRuntime {
1440
1510
 
1441
1511
  private async loadState() {
1442
1512
  if (!this.statePath) return;
1443
- const loaded = await readJsonFileWithFallback(this.statePath, {
1513
+ const loaded = await readOpenClawJsonFileWithFallback(this.statePath, {
1444
1514
  outbox: [],
1445
1515
  deadLetter: [],
1446
1516
  sessionRoutes: [],
@@ -1709,7 +1779,7 @@ class BncrBridgeRuntime {
1709
1779
  : null,
1710
1780
  };
1711
1781
 
1712
- await writeJsonFileAtomically(this.statePath, data);
1782
+ await writeOpenClawJsonFileAtomically(this.statePath, data);
1713
1783
  }
1714
1784
 
1715
1785
  private resolveMessageAck(messageId: string, result: 'acked' | 'timeout' = 'acked') {
@@ -2721,19 +2791,18 @@ class BncrBridgeRuntime {
2721
2791
  outboundReady: true,
2722
2792
  preferredForOutbound: true,
2723
2793
  });
2724
- const ackAt = now();
2794
+ const telemetryPatch = buildBncrAckOkTelemetryPatch({
2795
+ entry: args.entry,
2796
+ ackAt: now(),
2797
+ defaultAckTimeoutMs: PUSH_ACK_TIMEOUT_MS,
2798
+ });
2799
+ const { ackAt, ackQueueLatencyMs, ackPushLatencyMs, lateAccepted } = telemetryPatch;
2725
2800
  this.lastAckOkByAccount.set(args.accountId, ackAt);
2726
- const ackQueueLatencyMs = Math.max(0, ackAt - finiteNumberOr(args.entry.createdAt, ackAt));
2727
- const ackPushLatencyMs =
2728
- typeof args.entry.lastPushAt === 'number'
2729
- ? Math.max(0, ackAt - args.entry.lastPushAt)
2730
- : null;
2731
2801
  this.lastAckQueueLatencyMsByAccount.set(args.accountId, ackQueueLatencyMs);
2732
2802
  if (typeof ackPushLatencyMs === 'number') {
2733
2803
  this.lastAckPushLatencyMsByAccount.set(args.accountId, ackPushLatencyMs);
2734
2804
  }
2735
- const lateAccepted = args.entry.awaitingRetryPush === true;
2736
- if (lateAccepted) {
2805
+ if (telemetryPatch.shouldResetAdaptiveAckRecovery) {
2737
2806
  this.adaptiveAckRecoveryOkCountByAccount.set(args.accountId, 0);
2738
2807
  this.lateAckOkCountByAccount.set(
2739
2808
  args.accountId,
@@ -2746,7 +2815,7 @@ class BncrBridgeRuntime {
2746
2815
  }
2747
2816
  args.entry.awaitingRetryPush = false;
2748
2817
  args.entry.lastError = undefined;
2749
- } else if (typeof ackPushLatencyMs === 'number' && ackPushLatencyMs <= PUSH_ACK_TIMEOUT_MS) {
2818
+ } else if (telemetryPatch.shouldIncrementAdaptiveAckRecovery) {
2750
2819
  this.adaptiveAckRecoveryOkCountByAccount.set(
2751
2820
  args.accountId,
2752
2821
  this.getCounter(this.adaptiveAckRecoveryOkCountByAccount, args.accountId) + 1,
@@ -2788,16 +2857,18 @@ class BncrBridgeRuntime {
2788
2857
  clientId?: string;
2789
2858
  error: string;
2790
2859
  }) {
2791
- args.entry.nextAttemptAt = now() + 1_000;
2792
- args.entry.lastError = args.error;
2793
- args.entry.awaitingRetryPush = true;
2794
- this.outbox.set(args.messageId, args.entry);
2860
+ const nextEntry = buildBncrAckRetryEntryPatch({
2861
+ entry: args.entry,
2862
+ error: args.error,
2863
+ nextAttemptAt: now() + 1_000,
2864
+ });
2865
+ this.outbox.set(args.messageId, nextEntry);
2795
2866
  this.scheduleSave();
2796
2867
  this.logOutboxAckSummary('outbox ack retry', {
2797
2868
  messageId: args.messageId,
2798
2869
  connId: args.connId,
2799
2870
  clientId: args.clientId,
2800
- err: args.entry.lastError,
2871
+ err: nextEntry.lastError,
2801
2872
  });
2802
2873
  }
2803
2874
 
@@ -2863,10 +2934,10 @@ class BncrBridgeRuntime {
2863
2934
  });
2864
2935
  }
2865
2936
 
2866
- private prepareInboundAcceptance(args: {
2937
+ private async prepareInboundAcceptance(args: {
2867
2938
  parsed: ReturnType<typeof parseBncrInboundParams>;
2868
2939
  canonicalAgentId: string;
2869
- }):
2940
+ }): Promise<
2870
2941
  | {
2871
2942
  ok: true;
2872
2943
  accountId: string;
@@ -2878,7 +2949,8 @@ class BncrBridgeRuntime {
2878
2949
  ok: false;
2879
2950
  status: boolean;
2880
2951
  payload: ReturnType<typeof buildInboundResponsePayload>;
2881
- } {
2952
+ }
2953
+ > {
2882
2954
  const { parsed, canonicalAgentId } = args;
2883
2955
  const {
2884
2956
  accountId,
@@ -2915,8 +2987,8 @@ class BncrBridgeRuntime {
2915
2987
  };
2916
2988
  }
2917
2989
 
2918
- const cfg = this.api.runtime.config.current();
2919
- const gate = checkBncrMessageGate({
2990
+ const cfg = getOpenClawRuntimeConfig(this.api);
2991
+ const gate = await checkBncrMessageGate({
2920
2992
  parsed,
2921
2993
  cfg,
2922
2994
  account: resolveAccount(cfg, accountId),
@@ -2944,7 +3016,7 @@ class BncrBridgeRuntime {
2944
3016
  taskKey: extracted.taskKey,
2945
3017
  text,
2946
3018
  extractedText: extracted.text,
2947
- resolveAgentRoute: (params) => this.api.runtime.channel.routing.resolveAgentRoute(params),
3019
+ resolveAgentRoute: (params) => resolveOpenClawAgentRoute(this.api, params),
2948
3020
  });
2949
3021
 
2950
3022
  return {
@@ -3015,8 +3087,12 @@ class BncrBridgeRuntime {
3015
3087
  entry: OutboxEntry;
3016
3088
  lastError: string;
3017
3089
  }) {
3018
- args.entry.lastError = args.lastError;
3019
- this.outbox.set(args.entry.messageId, args.entry);
3090
+ const nextEntry = buildBncrOutboxFailureEntryPatch({
3091
+ entry: args.entry,
3092
+ lastError: args.lastError,
3093
+ });
3094
+ Object.assign(args.entry, nextEntry);
3095
+ this.outbox.set(nextEntry.messageId, args.entry);
3020
3096
  }
3021
3097
 
3022
3098
  private recordOutboxPushFailure(args: {
@@ -3025,8 +3101,12 @@ class BncrBridgeRuntime {
3025
3101
  fallbackError: string;
3026
3102
  persist?: boolean;
3027
3103
  }) {
3028
- args.entry.lastError = asString((args.error as any)?.message || args.error || args.fallbackError);
3029
- this.outbox.set(args.entry.messageId, args.entry);
3104
+ const nextEntry = buildBncrOutboxFailureEntryPatch({
3105
+ entry: args.entry,
3106
+ lastError: asString((args.error as any)?.message || args.error || args.fallbackError),
3107
+ });
3108
+ Object.assign(args.entry, nextEntry);
3109
+ this.outbox.set(nextEntry.messageId, args.entry);
3030
3110
  if (args.persist) this.scheduleSave();
3031
3111
  }
3032
3112
 
@@ -3037,23 +3117,19 @@ class BncrBridgeRuntime {
3037
3117
  ownerClientId?: string;
3038
3118
  clearLastError?: boolean;
3039
3119
  }) {
3040
- const connIds = Array.from(args.connIds);
3041
- args.entry.lastPushAt = now();
3042
- args.entry.lastPushConnId =
3043
- args.ownerConnId || (connIds.length === 1 ? connIds[0] : undefined);
3044
- args.entry.lastPushClientId = args.ownerClientId;
3045
- args.entry.awaitingRetryPush = false;
3046
- if (!Array.isArray(args.entry.routeAttemptConnIds)) args.entry.routeAttemptConnIds = [];
3047
- if (
3048
- args.entry.lastPushConnId &&
3049
- !args.entry.routeAttemptConnIds.includes(args.entry.lastPushConnId)
3050
- ) {
3051
- args.entry.routeAttemptConnIds.push(args.entry.lastPushConnId);
3052
- }
3053
- if (args.clearLastError) args.entry.lastError = undefined;
3054
- this.outbox.set(args.entry.messageId, args.entry);
3055
- this.lastOutboundByAccount.set(args.entry.accountId, args.entry.lastPushAt);
3056
- this.markActivity(args.entry.accountId, args.entry.lastPushAt);
3120
+ const pushedAt = now();
3121
+ const nextEntry = buildBncrOutboxPushSuccessEntryPatch({
3122
+ entry: args.entry,
3123
+ connIds: args.connIds,
3124
+ pushedAt,
3125
+ ownerConnId: args.ownerConnId,
3126
+ ownerClientId: args.ownerClientId,
3127
+ clearLastError: args.clearLastError,
3128
+ });
3129
+ Object.assign(args.entry, nextEntry);
3130
+ this.outbox.set(nextEntry.messageId, args.entry);
3131
+ this.lastOutboundByAccount.set(nextEntry.accountId, pushedAt);
3132
+ this.markActivity(nextEntry.accountId, pushedAt);
3057
3133
  this.scheduleSave();
3058
3134
  }
3059
3135
 
@@ -3088,41 +3164,19 @@ class BncrBridgeRuntime {
3088
3164
  }
3089
3165
 
3090
3166
  private isOutboundAckRequired(accountId?: string) {
3091
- try {
3092
- const cfg = this.api.runtime.config.current();
3093
- const channelCfg = (cfg as any)?.channels?.[CHANNEL_ID];
3094
- const accountCfg =
3095
- accountId && channelCfg?.accounts && typeof channelCfg.accounts === 'object'
3096
- ? (channelCfg.accounts as Record<string, any>)[normalizeAccountId(accountId)]
3097
- : null;
3098
- const scoped = accountCfg?.outboundRequireAck;
3099
- const global = channelCfg?.outboundRequireAck;
3100
- if (typeof scoped === 'boolean') return scoped;
3101
- if (typeof global === 'boolean') return global;
3102
- return true;
3103
- } catch {
3104
- return true;
3105
- }
3167
+ return resolveBncrOutboundAckRequired({ api: this.api, accountId });
3106
3168
  }
3107
3169
 
3108
3170
  private buildRuntimeFlags(accountId?: string) {
3109
- let ackPolicySource: 'channel' | 'default' = 'default';
3110
- try {
3111
- const cfg = this.api.runtime.config.current();
3112
- const global = (cfg as any)?.channels?.[CHANNEL_ID]?.outboundRequireAck;
3113
- if (typeof global === 'boolean') ackPolicySource = 'channel';
3114
- } catch {
3115
- // keep default source
3116
- }
3117
- return {
3118
- outboundRequireAck: this.isOutboundAckRequired(accountId),
3119
- ackPolicySource,
3120
- messageAckTimeoutMs: this.resolveMessageAckTimeoutMs(accountId),
3171
+ return buildBncrRuntimeFlags({
3172
+ api: this.api,
3173
+ accountId,
3174
+ resolveMessageAckTimeoutMs: (acc?: string) => this.resolveMessageAckTimeoutMs(acc),
3121
3175
  adaptiveAckTimeoutEnabled: ADAPTIVE_ACK_TIMEOUT_DEFAULT_ENABLED,
3122
3176
  defaultMessageAckTimeoutMs: PUSH_ACK_TIMEOUT_MS,
3123
3177
  fileAckTimeoutMs: FILE_ACK_TIMEOUT_MS,
3124
3178
  debugVerbose: BNCR_DEBUG_VERBOSE,
3125
- };
3179
+ });
3126
3180
  }
3127
3181
 
3128
3182
  private async flushPushQueue(args?: {
@@ -3343,14 +3397,8 @@ class BncrBridgeRuntime {
3343
3397
  continue;
3344
3398
  }
3345
3399
 
3346
- entry.routeAttemptConnIds = decision.attemptedConnIds;
3347
- entry.fastReroutePending = decision.fastReroutePending;
3348
- entry.retryCount = decision.nextRetryCount;
3349
- entry.lastAttemptAt = decision.lastAttemptAt;
3350
- entry.nextAttemptAt = decision.nextAttemptAt;
3351
- entry.lastError = decision.lastError;
3352
- entry.routeAttemptRound = decision.routeAttemptRound;
3353
- this.outbox.set(entry.messageId, entry);
3400
+ const nextEntry = applyBncrRetryRerouteDecisionToEntry(entry, decision);
3401
+ this.outbox.set(entry.messageId, nextEntry);
3354
3402
  this.scheduleSave();
3355
3403
  if (requireAck) {
3356
3404
  this.lastAckTimeoutByAccount.set(acc, now());
@@ -3364,7 +3412,7 @@ class BncrBridgeRuntime {
3364
3412
  localNextDelay = updateMinOutboxDelay(localNextDelay, wait);
3365
3413
  this.logOutboxAckReroute({
3366
3414
  accountId: acc,
3367
- entry,
3415
+ entry: nextEntry,
3368
3416
  requireAck,
3369
3417
  currentConnId,
3370
3418
  availableConnIds,
@@ -3395,11 +3443,8 @@ class BncrBridgeRuntime {
3395
3443
  continue;
3396
3444
  }
3397
3445
 
3398
- entry.retryCount = decision.nextRetryCount;
3399
- entry.lastAttemptAt = decision.lastAttemptAt;
3400
- entry.nextAttemptAt = decision.nextAttemptAt;
3401
- entry.lastError = decision.lastError;
3402
- this.outbox.set(entry.messageId, entry);
3446
+ const nextEntry = applyBncrPushFailureDecisionToEntry(entry, decision);
3447
+ this.outbox.set(entry.messageId, nextEntry);
3403
3448
  this.scheduleSave();
3404
3449
 
3405
3450
  const wait = computeOutboxRetryWait(decision.nextAttemptAt, t);
@@ -3920,7 +3965,7 @@ class BncrBridgeRuntime {
3920
3965
  const canonicalAgentId =
3921
3966
  this.canonicalAgentId ||
3922
3967
  this.ensureCanonicalAgentId({
3923
- cfg: this.api.runtime.config?.get?.() || {},
3968
+ cfg: getOpenClawRuntimeConfigOrDefault(this.api, {}),
3924
3969
  accountId: acc,
3925
3970
  channelId: CHANNEL_ID,
3926
3971
  peer: { kind: 'direct', id: route.groupId === '0' ? route.userId : route.groupId },
@@ -3953,9 +3998,7 @@ class BncrBridgeRuntime {
3953
3998
  }
3954
3999
 
3955
4000
  private fileAckKey(transferId: string, stage: string, chunkIndex?: number): string {
3956
- const n = Number(chunkIndex);
3957
- const idx = Number.isInteger(n) && n >= 0 ? String(n) : '-';
3958
- return `${transferId}|${stage}|${idx}`;
4001
+ return buildFileAckKey({ transferId, stage, chunkIndex });
3959
4002
  }
3960
4003
 
3961
4004
  private fileAckOwnerInfo(transferId: string) {
@@ -4184,30 +4227,6 @@ class BncrBridgeRuntime {
4184
4227
  }
4185
4228
 
4186
4229
 
4187
- private buildRuntimeQueueSnapshot(accountId: string) {
4188
- const pending = Array.from(this.outbox.values()).filter((v) => v.accountId === accountId).length;
4189
- const deadLetter = this.deadLetter.filter((v) => v.accountId === accountId).length;
4190
- const sessionRoutesCount = Array.from(this.sessionRoutes.values()).filter(
4191
- (v) => v.accountId === accountId,
4192
- ).length;
4193
- return {
4194
- pending,
4195
- deadLetter,
4196
- sessionRoutesCount,
4197
- invalidOutboxSessionKeys: this.countInvalidOutboxSessionKeys(accountId),
4198
- legacyAccountResidue: this.countLegacyAccountResidue(accountId),
4199
- };
4200
- }
4201
-
4202
- private buildRuntimeEventCounters(accountId: string) {
4203
- return {
4204
- connectEvents: this.getCounter(this.connectEventsByAccount, accountId),
4205
- inboundEvents: this.getCounter(this.inboundEventsByAccount, accountId),
4206
- activityEvents: this.getCounter(this.activityEventsByAccount, accountId),
4207
- ackEvents: this.getCounter(this.ackEventsByAccount, accountId),
4208
- };
4209
- }
4210
-
4211
4230
  private computeRecommendedAckTimeoutReason(args: {
4212
4231
  lateAckOkCount: number;
4213
4232
  recentAckTimeoutCount: number;
@@ -4217,26 +4236,15 @@ class BncrBridgeRuntime {
4217
4236
  recommendedAckTimeoutMs?: number;
4218
4237
  nowMs?: number;
4219
4238
  }) {
4220
- if (args.recentAckTimeoutCount <= 0) return 'no-timeout-evidence';
4221
- if (args.lateAckOkCount <= 0) return 'no-late-ack-evidence';
4222
- if (typeof args.lastLateAckPushLatencyMs !== 'number') return 'missing-latency';
4223
- const lastLateAckOkAt = typeof args.lastLateAckOkAt === 'number' ? args.lastLateAckOkAt : null;
4224
- const nowMs = typeof args.nowMs === 'number' ? args.nowMs : now();
4225
- if (
4226
- typeof lastLateAckOkAt === 'number' &&
4227
- lastLateAckOkAt > 0 &&
4228
- nowMs - lastLateAckOkAt > ADAPTIVE_ACK_TIMEOUT_OBSERVATION_TTL_MS
4229
- ) {
4230
- return 'late-ack-expired';
4231
- }
4232
- if (
4233
- typeof args.adaptiveAckRecoveryOkCount === 'number' &&
4234
- args.adaptiveAckRecoveryOkCount >= ADAPTIVE_ACK_TIMEOUT_RECOVERY_OK_THRESHOLD
4235
- ) {
4236
- return 'recovered';
4237
- }
4238
- if (args.recommendedAckTimeoutMs === RECOMMENDED_ACK_TIMEOUT_MAX_MS) return 'capped-max';
4239
- return 'late-ack-observed';
4239
+ return computeBncrRecommendedAckTimeoutReason({
4240
+ ...args,
4241
+ nowMs: typeof args.nowMs === 'number' ? args.nowMs : now(),
4242
+ defaultAckTimeoutMs: PUSH_ACK_TIMEOUT_MS,
4243
+ minAckTimeoutMs: RECOMMENDED_ACK_TIMEOUT_MIN_MS,
4244
+ maxAckTimeoutMs: RECOMMENDED_ACK_TIMEOUT_MAX_MS,
4245
+ lateAckObservationTtlMs: ADAPTIVE_ACK_TIMEOUT_OBSERVATION_TTL_MS,
4246
+ recoveryOkThreshold: ADAPTIVE_ACK_TIMEOUT_RECOVERY_OK_THRESHOLD,
4247
+ });
4240
4248
  }
4241
4249
 
4242
4250
  private computeRecommendedAckTimeoutMs(args: {
@@ -4247,29 +4255,15 @@ class BncrBridgeRuntime {
4247
4255
  adaptiveAckRecoveryOkCount?: number;
4248
4256
  nowMs?: number;
4249
4257
  }) {
4250
- const lastLateAckOkAt = typeof args.lastLateAckOkAt === 'number' ? args.lastLateAckOkAt : null;
4251
- const nowMs = typeof args.nowMs === 'number' ? args.nowMs : now();
4252
- const lateAckExpired =
4253
- typeof lastLateAckOkAt === 'number' &&
4254
- lastLateAckOkAt > 0 &&
4255
- nowMs - lastLateAckOkAt > ADAPTIVE_ACK_TIMEOUT_OBSERVATION_TTL_MS;
4256
- const recovered =
4257
- typeof args.adaptiveAckRecoveryOkCount === 'number' &&
4258
- args.adaptiveAckRecoveryOkCount >= ADAPTIVE_ACK_TIMEOUT_RECOVERY_OK_THRESHOLD;
4259
- if (
4260
- args.lateAckOkCount <= 0 ||
4261
- args.recentAckTimeoutCount <= 0 ||
4262
- typeof args.lastLateAckPushLatencyMs !== 'number' ||
4263
- lateAckExpired ||
4264
- recovered
4265
- ) {
4266
- return PUSH_ACK_TIMEOUT_MS;
4267
- }
4268
- const recommended = Math.ceil(args.lastLateAckPushLatencyMs * 1.25);
4269
- return Math.min(
4270
- RECOMMENDED_ACK_TIMEOUT_MAX_MS,
4271
- Math.max(RECOMMENDED_ACK_TIMEOUT_MIN_MS, recommended),
4272
- );
4258
+ return computeBncrRecommendedAckTimeoutMs({
4259
+ ...args,
4260
+ nowMs: typeof args.nowMs === 'number' ? args.nowMs : now(),
4261
+ defaultAckTimeoutMs: PUSH_ACK_TIMEOUT_MS,
4262
+ minAckTimeoutMs: RECOMMENDED_ACK_TIMEOUT_MIN_MS,
4263
+ maxAckTimeoutMs: RECOMMENDED_ACK_TIMEOUT_MAX_MS,
4264
+ lateAckObservationTtlMs: ADAPTIVE_ACK_TIMEOUT_OBSERVATION_TTL_MS,
4265
+ recoveryOkThreshold: ADAPTIVE_ACK_TIMEOUT_RECOVERY_OK_THRESHOLD,
4266
+ });
4273
4267
  }
4274
4268
 
4275
4269
  private maybeLogAdaptiveAckTimeout(args: {
@@ -4399,44 +4393,41 @@ class BncrBridgeRuntime {
4399
4393
  }
4400
4394
 
4401
4395
  private buildRuntimeAckStrategy(ackObservability: Record<string, any>) {
4402
- const currentMs = finiteNumberOr(ackObservability.currentAckTimeoutMs, PUSH_ACK_TIMEOUT_MS);
4403
- const defaultMs = finiteNumberOr(ackObservability.defaultAckTimeoutMs, PUSH_ACK_TIMEOUT_MS);
4404
- const reason = asString(ackObservability.recommendedAckTimeoutReason || 'unknown') || 'unknown';
4405
- return {
4406
- mode: ackObservability.adaptiveAckTimeoutEnabled === true ? 'adaptive' : 'fixed',
4407
- currentMs,
4408
- defaultMs,
4409
- maxMs: RECOMMENDED_ACK_TIMEOUT_MAX_MS,
4410
- reason,
4411
- active: currentMs > defaultMs,
4412
- lastLateAckAgeMs: ackObservability.lastLateAckAgeMs ?? null,
4413
- lateAckObservationTtlMs: ackObservability.lateAckObservationTtlMs ?? null,
4414
- recovered: ackObservability.adaptiveAckRecovered === true,
4415
- };
4416
- }
4417
-
4418
- private buildRuntimeActivitySnapshot(accountId: string) {
4419
- return {
4420
- activeConnections: this.activeConnectionCount(accountId),
4421
- lastSession: this.lastSessionByAccount.get(accountId) || null,
4422
- lastActivityAt: this.lastActivityByAccount.get(accountId) || null,
4423
- lastInboundAt: this.lastInboundByAccount.get(accountId) || null,
4424
- lastOutboundAt: this.lastOutboundByAccount.get(accountId) || null,
4425
- };
4396
+ return buildBncrRuntimeAckStrategy({
4397
+ ackObservability,
4398
+ defaultAckTimeoutMs: PUSH_ACK_TIMEOUT_MS,
4399
+ maxAckTimeoutMs: RECOMMENDED_ACK_TIMEOUT_MAX_MS,
4400
+ });
4426
4401
  }
4427
4402
 
4428
4403
  private buildRuntimeStatusInput(accountId: string, overrides: { running?: boolean } = {}) {
4429
4404
  const acc = normalizeAccountId(accountId);
4430
- return {
4405
+ const snapshots = buildRuntimeStatusSnapshots({
4406
+ accountId: acc,
4407
+ outboxEntries: this.outbox.values(),
4408
+ deadLetterEntries: this.deadLetter,
4409
+ sessionRouteEntries: this.sessionRoutes.values(),
4410
+ countInvalidOutboxSessionKeys: (snapshotAccountId) =>
4411
+ this.countInvalidOutboxSessionKeys(snapshotAccountId),
4412
+ countLegacyAccountResidue: (snapshotAccountId) => this.countLegacyAccountResidue(snapshotAccountId),
4413
+ connectEventsByAccount: this.connectEventsByAccount,
4414
+ inboundEventsByAccount: this.inboundEventsByAccount,
4415
+ activityEventsByAccount: this.activityEventsByAccount,
4416
+ ackEventsByAccount: this.ackEventsByAccount,
4417
+ activeConnectionCount: (snapshotAccountId) => this.activeConnectionCount(snapshotAccountId),
4418
+ lastSessionByAccount: this.lastSessionByAccount,
4419
+ lastActivityByAccount: this.lastActivityByAccount,
4420
+ lastInboundByAccount: this.lastInboundByAccount,
4421
+ lastOutboundByAccount: this.lastOutboundByAccount,
4422
+ });
4423
+ return buildBncrRuntimeStatusInput({
4431
4424
  accountId: acc,
4432
4425
  connected: this.isOnline(acc),
4433
- ...this.buildRuntimeQueueSnapshot(acc),
4434
- ...this.buildRuntimeEventCounters(acc),
4435
- ...this.buildRuntimeActivitySnapshot(acc),
4426
+ ...snapshots,
4436
4427
  startedAt: this.startedAt,
4437
4428
  running: overrides.running,
4438
4429
  channelRoot: path.join(process.cwd(), 'plugins', 'bncr'),
4439
- };
4430
+ });
4440
4431
  }
4441
4432
 
4442
4433
  private buildStatusMeta(accountId: string) {
@@ -4568,7 +4559,7 @@ class BncrBridgeRuntime {
4568
4559
  mediaUrl: string,
4569
4560
  mediaLocalRoots?: readonly string[],
4570
4561
  ): Promise<{ mediaBase64: string; mimeType?: string; fileName?: string }> {
4571
- const loaded = await this.api.runtime.media.loadWebMedia(mediaUrl, {
4562
+ const loaded = await loadOpenClawWebMedia(this.api, mediaUrl, {
4572
4563
  localRoots: mediaLocalRoots,
4573
4564
  maxBytes: 20 * 1024 * 1024,
4574
4565
  });
@@ -4583,12 +4574,12 @@ class BncrBridgeRuntime {
4583
4574
  mediaUrl: string;
4584
4575
  mediaLocalRoots?: readonly string[];
4585
4576
  }): Promise<{
4586
- loaded: Awaited<ReturnType<OpenClawPluginApi['runtime']['media']['loadWebMedia']>>;
4577
+ loaded: OpenClawLoadedMedia;
4587
4578
  size: number;
4588
4579
  mimeType?: string;
4589
4580
  fileName: string;
4590
4581
  }> {
4591
- const loaded = await this.api.runtime.media.loadWebMedia(params.mediaUrl, {
4582
+ const loaded = await loadOpenClawWebMedia(this.api, params.mediaUrl, {
4592
4583
  localRoots: params.mediaLocalRoots,
4593
4584
  maxBytes: 50 * 1024 * 1024,
4594
4585
  });
@@ -4812,34 +4803,6 @@ class BncrBridgeRuntime {
4812
4803
  );
4813
4804
  }
4814
4805
 
4815
- private buildFileTransferInitPayload(args: {
4816
- transferId: string;
4817
- sessionKey: string;
4818
- route: BncrRoute;
4819
- fileName: string;
4820
- mimeType?: string;
4821
- fileSize: number;
4822
- chunkSize: number;
4823
- totalChunks: number;
4824
- fileSha256: string;
4825
- }) {
4826
- return {
4827
- transferId: args.transferId,
4828
- direction: 'oc2bncr' as const,
4829
- sessionKey: args.sessionKey,
4830
- platform: args.route.platform,
4831
- groupId: args.route.groupId,
4832
- userId: args.route.userId,
4833
- fileName: args.fileName,
4834
- mimeType: args.mimeType,
4835
- fileSize: args.fileSize,
4836
- chunkSize: args.chunkSize,
4837
- totalChunks: args.totalChunks,
4838
- fileSha256: args.fileSha256,
4839
- ts: now(),
4840
- };
4841
- }
4842
-
4843
4806
  private buildInitialFileSendTransferState(args: {
4844
4807
  transferId: string;
4845
4808
  accountId: string;
@@ -5030,7 +4993,7 @@ class BncrBridgeRuntime {
5030
4993
 
5031
4994
  ctx.broadcastToConnIds(
5032
4995
  BNCR_FILE_INIT_EVENT,
5033
- this.buildFileTransferInitPayload({
4996
+ buildFileTransferInitPayload({
5034
4997
  transferId,
5035
4998
  sessionKey: params.sessionKey,
5036
4999
  route: params.route,
@@ -5040,6 +5003,7 @@ class BncrBridgeRuntime {
5040
5003
  chunkSize,
5041
5004
  totalChunks,
5042
5005
  fileSha256,
5006
+ ts: now(),
5043
5007
  }),
5044
5008
  connIds,
5045
5009
  );
@@ -5056,7 +5020,7 @@ class BncrBridgeRuntime {
5056
5020
  for (let attempt = 1; attempt <= 3; attempt++) {
5057
5021
  ctx.broadcastToConnIds(
5058
5022
  BNCR_FILE_CHUNK_EVENT,
5059
- {
5023
+ buildFileTransferChunkPayload({
5060
5024
  transferId,
5061
5025
  chunkIndex: idx,
5062
5026
  offset: start,
@@ -5064,7 +5028,7 @@ class BncrBridgeRuntime {
5064
5028
  chunkSha256,
5065
5029
  base64: slice.toString('base64'),
5066
5030
  ts: now(),
5067
- },
5031
+ }),
5068
5032
  connIds,
5069
5033
  );
5070
5034
 
@@ -5112,11 +5076,11 @@ class BncrBridgeRuntime {
5112
5076
  this.fileSendTransfers.set(transferId, st);
5113
5077
  ctx.broadcastToConnIds(
5114
5078
  BNCR_FILE_ABORT_EVENT,
5115
- {
5079
+ buildFileTransferAbortPayload({
5116
5080
  transferId,
5117
5081
  reason: st.error,
5118
5082
  ts: now(),
5119
- },
5083
+ }),
5120
5084
  connIds,
5121
5085
  );
5122
5086
  throw new Error(st.error);
@@ -5125,10 +5089,10 @@ class BncrBridgeRuntime {
5125
5089
 
5126
5090
  ctx.broadcastToConnIds(
5127
5091
  BNCR_FILE_COMPLETE_EVENT,
5128
- {
5092
+ buildFileTransferCompletePayload({
5129
5093
  transferId,
5130
5094
  ts: now(),
5131
- },
5095
+ }),
5132
5096
  connIds,
5133
5097
  );
5134
5098
 
@@ -5408,7 +5372,7 @@ class BncrBridgeRuntime {
5408
5372
 
5409
5373
  handleDiagnostics = async ({ params, respond }: GatewayRequestHandlerOptions) => {
5410
5374
  const accountId = normalizeAccountId(asString(params?.accountId || ''));
5411
- const cfg = this.api.runtime.config.current();
5375
+ const cfg = getOpenClawRuntimeConfig(this.api);
5412
5376
  const runtime = this.getAccountRuntimeSnapshot(accountId);
5413
5377
  const diagnostics = this.buildExtendedDiagnostics(accountId);
5414
5378
 
@@ -5562,6 +5526,17 @@ class BncrBridgeRuntime {
5562
5526
  respond(false, { error: 'transfer not found' });
5563
5527
  return;
5564
5528
  }
5529
+ if (st.status === 'completed') {
5530
+ respond(true, {
5531
+ ok: true,
5532
+ transferId,
5533
+ status: 'completed',
5534
+ path: st.completedPath,
5535
+ ignored: true,
5536
+ terminal: true,
5537
+ });
5538
+ return;
5539
+ }
5565
5540
  if (chunkIndex >= st.totalChunks) {
5566
5541
  respond(false, { error: `chunkIndex out of range index=${chunkIndex} total=${st.totalChunks}` });
5567
5542
  return;
@@ -5703,7 +5678,8 @@ class BncrBridgeRuntime {
5703
5678
  throw new Error('file sha256 mismatch');
5704
5679
  }
5705
5680
 
5706
- const saved = await this.api.runtime.channel.media.saveMediaBuffer(
5681
+ const saved = await saveOpenClawChannelMediaBuffer(
5682
+ this.api,
5707
5683
  merged,
5708
5684
  st.mimeType,
5709
5685
  'inbound',
@@ -5764,6 +5740,17 @@ class BncrBridgeRuntime {
5764
5740
  respond(true, { ok: true, transferId, message: 'not-found' });
5765
5741
  return;
5766
5742
  }
5743
+ if (st.status === 'completed') {
5744
+ respond(true, {
5745
+ ok: true,
5746
+ transferId,
5747
+ status: 'completed',
5748
+ path: st.completedPath,
5749
+ ignored: true,
5750
+ terminal: true,
5751
+ });
5752
+ return;
5753
+ }
5767
5754
 
5768
5755
  const staleObserved = this.observeLease('file.abort', params ?? {});
5769
5756
  if (staleObserved.stale) {
@@ -5860,6 +5847,30 @@ class BncrBridgeRuntime {
5860
5847
  ? 'file.abort'
5861
5848
  : 'file.complete';
5862
5849
  const staleObserved = this.observeLease(staleKind, params ?? {});
5850
+ if (st?.status === 'completed' || st?.status === 'aborted') {
5851
+ respond(
5852
+ true,
5853
+ staleObserved.stale
5854
+ ? {
5855
+ ok: true,
5856
+ transferId,
5857
+ stage,
5858
+ state: st.status,
5859
+ stale: true,
5860
+ ignored: true,
5861
+ terminal: true,
5862
+ }
5863
+ : {
5864
+ ok: true,
5865
+ transferId,
5866
+ stage,
5867
+ state: st.status,
5868
+ ignored: true,
5869
+ terminal: true,
5870
+ },
5871
+ );
5872
+ return;
5873
+ }
5863
5874
  if (staleObserved.stale) {
5864
5875
  const sameConn = !!st?.ownerConnId && st.ownerConnId === connId;
5865
5876
  const sameClient =
@@ -6038,14 +6049,14 @@ class BncrBridgeRuntime {
6038
6049
  this.lastInboundAtGlobal = now();
6039
6050
  this.incrementCounter(this.inboundEventsByAccount, accountId);
6040
6051
 
6041
- const cfg = this.api.runtime.config.current();
6052
+ const cfg = getOpenClawRuntimeConfig(this.api);
6042
6053
  const canonicalAgentId = this.ensureCanonicalAgentId({
6043
6054
  cfg,
6044
6055
  accountId,
6045
6056
  peer,
6046
6057
  channelId: CHANNEL_ID,
6047
6058
  });
6048
- const acceptance = this.prepareInboundAcceptance({ parsed, canonicalAgentId });
6059
+ const acceptance = await this.prepareInboundAcceptance({ parsed, canonicalAgentId });
6049
6060
  if (!acceptance.ok) {
6050
6061
  respond(acceptance.status, acceptance.payload);
6051
6062
  return;
@@ -6116,126 +6127,11 @@ class BncrBridgeRuntime {
6116
6127
  };
6117
6128
 
6118
6129
  channelStartAccount = async (ctx: any) => {
6119
- const accountId = normalizeAccountId(ctx.accountId);
6120
- this.clearChannelAccountWorker(accountId, 'start-replace');
6121
-
6122
- const tick = () => {
6123
- const previous = ctx.getStatus?.() || {};
6124
- const onlineByConn = this.isOnline(accountId);
6125
- const recentInboundReachable = this.hasRecentInboundReachability(accountId);
6126
- const connected = onlineByConn || recentInboundReachable;
6127
- const lastActAt =
6128
- this.lastActivityByAccount.get(accountId) ||
6129
- this.lastInboundByAccount.get(accountId) ||
6130
- this.lastOutboundByAccount.get(accountId) ||
6131
- previous?.lastEventAt ||
6132
- null;
6133
- const healthSig = JSON.stringify({
6134
- bridge: this.bridgeId,
6135
- accountId,
6136
- connected,
6137
- onlineByConn,
6138
- recentInboundReachable,
6139
- activeConnectionKey: this.activeConnectionByAccount.get(accountId) || null,
6140
- activeConnections: Array.from(this.connections.values())
6141
- .filter((c) => c.accountId === accountId)
6142
- .map((c) => ({
6143
- connId: c.connId,
6144
- clientId: c.clientId,
6145
- inboundOnly: c.inboundOnly === true,
6146
- outboundReady: c.outboundReady === true,
6147
- preferredForOutbound: c.preferredForOutbound === true,
6148
- })),
6149
- });
6150
- const conns = Array.from(this.connections.values()).filter((c) => c.accountId === accountId).length;
6151
- this.logInfoDedup(
6152
- 'health',
6153
- `status-tick ${accountId}|changed|${connected ? 'linked' : 'configured'}|onlineByConn=${onlineByConn}|recentInboundReachable=${recentInboundReachable}|conns=${conns}`,
6154
- {
6155
- key: `health-status-tick:${accountId}`,
6156
- sig: healthSig,
6157
- },
6158
- );
6159
- this.logInfoDedup('health', `status-tick ${healthSig}`, {
6160
- key: `health-status-tick-debug:${accountId}`,
6161
- sig: healthSig,
6162
- debugOnly: true,
6163
- });
6164
-
6165
- ctx.setStatus?.({
6166
- ...previous,
6167
- accountId,
6168
- running: true,
6169
- connected,
6170
- lastEventAt: lastActAt,
6171
- // 状态映射:在线=linked,离线=configured
6172
- mode: connected ? 'linked' : 'configured',
6173
- lastError: previous?.lastError ?? null,
6174
- meta: this.buildStatusMeta(accountId),
6175
- });
6176
- };
6177
-
6178
- tick();
6179
- const timer = setInterval(tick, 5_000);
6180
- let worker!: ChannelAccountWorkerHandle;
6181
- const done = new Promise<void>((resolve) => {
6182
- let settled = false;
6183
- const finish = (reason: string) => {
6184
- if (settled) return;
6185
- settled = true;
6186
- const activeWorker = this.channelAccountWorkers.get(accountId);
6187
- if (activeWorker === worker) {
6188
- this.channelAccountWorkers.delete(accountId);
6189
- }
6190
- clearInterval(timer);
6191
- worker.cleanupAbortListener?.();
6192
- worker.cleanupAbortListener = undefined;
6193
- this.logInfo(
6194
- 'health',
6195
- `status-worker finished ${JSON.stringify({ bridge: this.bridgeId, accountId, reason })}`,
6196
- { debugOnly: true },
6197
- );
6198
- this.logInfo('health', `status-worker finished ${accountId}|${reason}`);
6199
- resolve();
6200
- };
6201
-
6202
- worker = { timer, finish };
6203
- this.channelAccountWorkers.set(accountId, worker);
6204
-
6205
- const onAbort = () => finish('abort');
6206
- const abortSignal = ctx.abortSignal;
6207
-
6208
- if (abortSignal?.aborted) {
6209
- onAbort();
6210
- return;
6211
- }
6212
-
6213
- abortSignal?.addEventListener?.('abort', onAbort, { once: true });
6214
- if (abortSignal?.removeEventListener) {
6215
- worker.cleanupAbortListener = () => abortSignal.removeEventListener('abort', onAbort);
6216
- }
6217
- });
6218
- await done;
6130
+ await startBncrStatusWorker(this.buildStatusWorkerRuntime(), ctx);
6219
6131
  };
6220
6132
 
6221
6133
  channelStopAccount = async (ctx: any) => {
6222
- const accountId = normalizeAccountId(ctx?.accountId);
6223
- const cleared = this.clearChannelAccountWorker(accountId, 'explicit-stop');
6224
- const previous = ctx?.getStatus?.() || {};
6225
- ctx?.setStatus?.({
6226
- ...previous,
6227
- accountId,
6228
- running: false,
6229
- restartPending: false,
6230
- lastStopAt: Date.now(),
6231
- meta: this.buildStatusMeta(accountId),
6232
- });
6233
- this.logInfo(
6234
- 'health',
6235
- `status-stop ${JSON.stringify({ bridge: this.bridgeId, accountId, cleared })}`,
6236
- { debugOnly: true },
6237
- );
6238
- this.logInfo('health', `status-stop ${accountId}|cleared=${cleared}`);
6134
+ await stopBncrStatusWorker(this.buildStatusWorkerRuntime(), ctx);
6239
6135
  };
6240
6136
 
6241
6137
  private logChannelSendEntry(args: {
@@ -6352,6 +6248,65 @@ class BncrBridgeRuntime {
6352
6248
  createMessageId: () => randomUUID(),
6353
6249
  });
6354
6250
  };
6251
+
6252
+ private async enqueueChannelMessageHandoff(ctx: any, payload: ReplyPayloadInput) {
6253
+ const accountId = normalizeAccountId(ctx.accountId);
6254
+ const to = asString(ctx.to || '').trim();
6255
+ const verified = this.resolveVerifiedTarget(to, accountId);
6256
+ this.rememberSessionRoute(verified.sessionKey, accountId, verified.route);
6257
+ const before = new Set(this.outbox.keys());
6258
+ await this.enqueueFromReply({
6259
+ accountId,
6260
+ sessionKey: verified.sessionKey,
6261
+ route: verified.route,
6262
+ payload,
6263
+ mediaLocalRoots: ctx.mediaLocalRoots,
6264
+ });
6265
+ const entries = Array.from(this.outbox.values()).filter((entry) => !before.has(entry.messageId));
6266
+ if (!entries.length) {
6267
+ throw new Error('bncr channel.message handoff did not enqueue an outbox entry');
6268
+ }
6269
+ return entries[entries.length - 1];
6270
+ }
6271
+
6272
+ channelMessageSendText = async (ctx: any) => {
6273
+ const entry = await this.enqueueChannelMessageHandoff(ctx, {
6274
+ text: asString(ctx.text || ''),
6275
+ kind: ctx?.kind,
6276
+ replyToId: this.resolveChannelSendReplyToId(ctx),
6277
+ });
6278
+ return buildBncrDurableQueuedResult({ entry });
6279
+ };
6280
+
6281
+ channelMessageSendMedia = async (ctx: any) => {
6282
+ const entry = await this.enqueueChannelMessageHandoff(ctx, {
6283
+ text: asString(ctx.text || ''),
6284
+ mediaUrl: asString(ctx.mediaUrl || ''),
6285
+ mediaUrls: Array.isArray(ctx?.mediaUrls) ? ctx.mediaUrls : undefined,
6286
+ asVoice: ctx?.asVoice === true,
6287
+ audioAsVoice: ctx?.audioAsVoice === true,
6288
+ kind: ctx?.kind,
6289
+ replyToId: this.resolveChannelSendReplyToId(ctx),
6290
+ });
6291
+ return buildBncrDurableQueuedResult({ entry });
6292
+ };
6293
+
6294
+ channelMessageSendPayload = async (ctx: any) => {
6295
+ const payload = ctx?.payload || {};
6296
+ if (!payload || typeof payload !== 'object') {
6297
+ throw new Error('bncr channel.message payload must be an object');
6298
+ }
6299
+ const entry = await this.enqueueChannelMessageHandoff(ctx, {
6300
+ text: asString(payload.text || payload.message || payload.caption || ''),
6301
+ mediaUrl: asString(payload.mediaUrl || ''),
6302
+ mediaUrls: Array.isArray(payload.mediaUrls) ? payload.mediaUrls : undefined,
6303
+ asVoice: payload.asVoice === true,
6304
+ audioAsVoice: payload.audioAsVoice === true,
6305
+ kind: payload.kind,
6306
+ replyToId: asString(payload.replyToId || ctx?.replyToId || ctx?.replyToMessageId || '').trim() || undefined,
6307
+ });
6308
+ return buildBncrDurableQueuedResult({ entry });
6309
+ };
6355
6310
  }
6356
6311
 
6357
6312
  export function createBncrBridge(api: OpenClawPluginApi) {
@@ -6387,7 +6342,7 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
6387
6342
  };
6388
6343
  },
6389
6344
  supportsAction: ({ action }) => action === 'send',
6390
- extractToolSend: ({ args }) => extractToolSend(args, 'sendMessage'),
6345
+ extractToolSend: ({ args }) => extractOpenClawToolSend(args, 'sendMessage'),
6391
6346
  handleAction: async ({ action, params, accountId, mediaLocalRoots }) => {
6392
6347
  if (action !== 'send')
6393
6348
  throw new Error(`Action ${action} is not supported for provider ${CHANNEL_ID}.`);
@@ -6425,226 +6380,27 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
6425
6380
  createMessageId: () => randomUUID(),
6426
6381
  });
6427
6382
 
6428
- return jsonResult({ ok: true, ...result });
6383
+ return openClawJsonResult({ ok: true, ...result });
6429
6384
  },
6430
6385
  };
6431
6386
 
6432
6387
  const plugin = {
6433
6388
  id: CHANNEL_ID,
6434
- meta: {
6435
- id: CHANNEL_ID,
6436
- label: 'Bncr',
6437
- selectionLabel: 'Bncr Client',
6438
- docsPath: '/channels/bncr',
6439
- blurb: 'Bncr Channel.',
6440
- aliases: ['bncr'],
6441
- },
6389
+ meta: BNCR_CHANNEL_META,
6442
6390
  actions: messageActions,
6443
- capabilities: {
6444
- chatTypes: ['direct'] as ChatType[],
6445
- media: true,
6446
- reply: true,
6447
- nativeCommands: true,
6448
- },
6449
- messaging: {
6450
- // 接收任意标签输入;不在 normalize 阶段做格式门槛,统一下沉到发送前验证。
6451
- normalizeTarget: (raw: string) => {
6452
- const input = asString(raw).trim();
6453
- return input || undefined;
6454
- },
6455
- parseExplicitTarget: ({ raw, accountId, cfg }: any) => {
6456
- const resolvedAccountId = normalizeAccountId(
6457
- asString(accountId || BNCR_DEFAULT_ACCOUNT_ID),
6458
- );
6459
- const runtimeBridge = getBridge();
6460
- const canonicalAgentId =
6461
- runtimeBridge.canonicalAgentId ||
6462
- runtimeBridge.ensureCanonicalAgentId({ cfg, accountId: resolvedAccountId });
6463
- return parseExplicitTarget(asString(raw).trim(), { canonicalAgentId });
6464
- },
6465
- formatTargetDisplay: ({ target }: any) => {
6466
- return formatTargetDisplay(target);
6467
- },
6468
- resolveSessionTarget: ({ id, accountId, cfg }: any) => {
6469
- const raw = asString(id).trim();
6470
- if (!raw) return undefined;
6471
- const resolvedAccountId = normalizeAccountId(
6472
- asString(accountId || BNCR_DEFAULT_ACCOUNT_ID),
6473
- );
6474
- const runtimeBridge = getBridge();
6475
- const canonicalAgentId =
6476
- runtimeBridge.canonicalAgentId ||
6477
- runtimeBridge.ensureCanonicalAgentId({ cfg, accountId: resolvedAccountId });
6478
-
6479
- let parsed = parseExplicitTarget(raw, { canonicalAgentId });
6480
- if (!parsed) {
6481
- const route = runtimeBridge.resolveRouteBySession(raw, resolvedAccountId);
6482
- if (route) {
6483
- parsed = parseExplicitTarget(formatDisplayScope(route), { canonicalAgentId });
6484
- }
6485
- }
6486
- return parsed?.displayScope || undefined;
6487
- },
6488
- resolveOutboundSessionRoute: (params: any) => {
6489
- const accountId = normalizeAccountId(
6490
- asString(params?.accountId || BNCR_DEFAULT_ACCOUNT_ID),
6491
- );
6492
- const runtimeBridge = getBridge();
6493
- const canonicalAgentId =
6494
- runtimeBridge.canonicalAgentId ||
6495
- runtimeBridge.ensureCanonicalAgentId({ cfg: params?.cfg, accountId });
6496
- return resolveBncrOutboundSessionRoute({
6497
- ...params,
6498
- canonicalAgentId,
6499
- resolveRouteBySession: (raw: string, acc: string) =>
6500
- runtimeBridge.resolveRouteBySession(raw, acc),
6501
- });
6502
- },
6503
- targetResolver: {
6504
- looksLikeId: (raw: string, normalized?: string) => {
6505
- return looksLikeBncrExplicitTarget(asString(normalized || raw).trim());
6506
- },
6507
- resolveTarget: async ({ accountId, input, normalized }) => {
6508
- const runtimeBridge = getBridge();
6509
- const resolved = resolveBncrOutboundTarget({
6510
- target: asString(normalized || input).trim(),
6511
- accountId: normalizeAccountId(asString(accountId || BNCR_DEFAULT_ACCOUNT_ID)),
6512
- resolveRouteBySession: (raw: string, acc: string) =>
6513
- runtimeBridge.resolveRouteBySession(raw, acc),
6514
- });
6515
- if (!resolved) return null;
6516
- return {
6517
- to: resolved.displayScope,
6518
- kind: resolved.kind,
6519
- display: resolved.displayScope,
6520
- source: 'normalized' as const,
6521
- };
6522
- },
6523
- hint: 'Standard to=Bncr:<platform>:<group>:<user> or Bncr:<platform>:<user>; sessionKey keeps existing strict/legacy compatibility, canonical sessionKey=agent:<agentId>:bncr:direct:<hex>',
6524
- },
6391
+ message: {
6392
+ receive: BNCR_MESSAGE_RECEIVE_POLICY,
6393
+ send: createBncrMessageSend(getBridge),
6525
6394
  },
6395
+ capabilities: BNCR_CHANNEL_CAPABILITIES,
6396
+ messaging: createBncrMessagingSurface(getBridge),
6526
6397
  configSchema: BncrConfigSchema,
6527
- config: {
6528
- listAccountIds,
6529
- resolveAccount,
6530
- setAccountEnabled: ({ cfg, accountId, enabled }: any) =>
6531
- setAccountEnabledInConfigSection({
6532
- cfg,
6533
- sectionKey: CHANNEL_ID,
6534
- accountId,
6535
- enabled,
6536
- allowTopLevel: true,
6537
- }),
6538
- isEnabled: (account: any, cfg: any) => {
6539
- const policy = resolveBncrChannelPolicy(cfg?.channels?.[CHANNEL_ID] || {});
6540
- return policy.enabled !== false && account?.enabled !== false;
6541
- },
6542
- isConfigured: () => true,
6543
- describeAccount: (account: any) => {
6544
- const displayName = resolveDefaultDisplayName(account?.name, account?.accountId);
6545
- return {
6546
- accountId: account.accountId,
6547
- name: displayName,
6548
- enabled: account.enabled !== false,
6549
- configured: true,
6550
- };
6551
- },
6552
- },
6553
- setup: {
6554
- applyAccountName: ({ cfg, accountId, name }: any) =>
6555
- applyAccountNameToChannelSection({
6556
- cfg,
6557
- channelKey: CHANNEL_ID,
6558
- accountId,
6559
- name,
6560
- alwaysUseAccounts: true,
6561
- }),
6562
- applyAccountConfig: ({ cfg, accountId }: any) => {
6563
- const next = { ...(cfg || {}) } as any;
6564
- next.channels = next.channels || {};
6565
- next.channels[CHANNEL_ID] = next.channels[CHANNEL_ID] || {};
6566
- next.channels[CHANNEL_ID].accounts = next.channels[CHANNEL_ID].accounts || {};
6567
- next.channels[CHANNEL_ID].accounts[accountId] = {
6568
- ...(next.channels[CHANNEL_ID].accounts[accountId] || {}),
6569
- enabled: true,
6570
- };
6571
- return next;
6572
- },
6573
- },
6574
- outbound: {
6575
- deliveryMode: 'gateway' as const,
6576
- sendText: async (ctx: any) => getBridge().channelSendText(ctx),
6577
- sendMedia: async (ctx: any) => getBridge().channelSendMedia(ctx),
6578
- replyAction: async (ctx: any) =>
6579
- sendBncrReplyAction({
6580
- accountId: normalizeAccountId(ctx?.accountId),
6581
- to: asString(ctx?.to || '').trim(),
6582
- text: asString(ctx?.text || ''),
6583
- replyToMessageId:
6584
- asString(ctx?.replyToId || ctx?.replyToMessageId || '').trim() || undefined,
6585
- sendText: async ({ accountId, to, text }) =>
6586
- getBridge().channelSendText({ accountId, to, text }),
6587
- }),
6588
- deleteAction: async (ctx: any) =>
6589
- deleteBncrMessageAction({
6590
- accountId: normalizeAccountId(ctx?.accountId),
6591
- targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
6592
- }),
6593
- reactAction: async (ctx: any) =>
6594
- reactBncrMessageAction({
6595
- accountId: normalizeAccountId(ctx?.accountId),
6596
- targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
6597
- emoji: asString(ctx?.emoji || '').trim(),
6598
- }),
6599
- editAction: async (ctx: any) =>
6600
- editBncrMessageAction({
6601
- accountId: normalizeAccountId(ctx?.accountId),
6602
- targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
6603
- text: asString(ctx?.text || ''),
6604
- }),
6605
- },
6606
- status: {
6607
- defaultRuntime: createDefaultChannelRuntimeState(BNCR_DEFAULT_ACCOUNT_ID, {
6608
- mode: 'ws-offline',
6609
- }),
6610
- buildChannelSummary: async ({ defaultAccountId }: any) => {
6611
- return getBridge().getChannelSummary(defaultAccountId || BNCR_DEFAULT_ACCOUNT_ID);
6612
- },
6613
- buildAccountSnapshot: async ({ account, runtime }: any) => {
6614
- const runtimeBridge = getBridge();
6615
- const rt = runtime || runtimeBridge.getAccountRuntimeSnapshot(account?.accountId);
6616
- return buildAccountStatusSnapshot({
6617
- account,
6618
- runtime: rt,
6619
- healthSummary: runtimeBridge.getStatusHeadline(account?.accountId),
6620
- // default 名不可隐藏时,统一展示稳定默认值
6621
- displayName: resolveDefaultDisplayName(account?.name, account?.accountId),
6622
- });
6623
- },
6624
- resolveAccountState: ({ enabled, configured, account, cfg, runtime }: any) => {
6625
- if (!enabled) return 'disabled';
6626
- const resolved = resolveAccount(cfg, account?.accountId);
6627
- if (!(resolved.enabled && configured)) return 'not configured';
6628
- const rt = runtime || getBridge().getAccountRuntimeSnapshot(account?.accountId);
6629
- return rt?.connected ? 'linked' : 'configured';
6630
- },
6631
- },
6632
- gatewayMethods: [
6633
- 'bncr.connect',
6634
- 'bncr.inbound',
6635
- 'bncr.activity',
6636
- 'bncr.ack',
6637
- 'bncr.diagnostics',
6638
- 'bncr.file.init',
6639
- 'bncr.file.chunk',
6640
- 'bncr.file.complete',
6641
- 'bncr.file.abort',
6642
- 'bncr.file.ack',
6643
- ],
6644
- gateway: {
6645
- startAccount: async (ctx: any) => getBridge().channelStartAccount(ctx),
6646
- stopAccount: async (ctx: any) => getBridge().channelStopAccount(ctx),
6647
- },
6398
+ config: BNCR_CONFIG_SURFACE,
6399
+ setup: BNCR_SETUP_SURFACE,
6400
+ outbound: createBncrOutboundRuntime(getBridge),
6401
+ status: createBncrStatusSurface(getBridge),
6402
+ gatewayMethods: BNCR_GATEWAY_METHODS,
6403
+ gateway: createBncrGatewayRuntime(getBridge),
6648
6404
  };
6649
6405
 
6650
6406
  return plugin;