@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.
- package/README.md +7 -1
- package/index.ts +30 -15
- package/package.json +4 -3
- package/scripts/check-pack.mjs +77 -0
- package/scripts/selfcheck.mjs +10 -0
- package/src/channel.ts +398 -642
- package/src/core/extended-diagnostics.ts +10 -0
- package/src/core/file-ack.ts +9 -0
- package/src/core/file-transfer-payloads.ts +72 -0
- package/src/core/register-trace.ts +79 -0
- package/src/core/targets.ts +10 -1
- package/src/messaging/inbound/commands.ts +20 -10
- package/src/messaging/inbound/context-facts.ts +200 -0
- package/src/messaging/inbound/dispatch.ts +66 -14
- package/src/messaging/inbound/gate.ts +66 -26
- package/src/messaging/inbound/runtime-compat.ts +41 -0
- package/src/messaging/inbound/session-label.ts +7 -7
- package/src/messaging/outbound/durable-message-adapter.ts +107 -0
- package/src/messaging/outbound/durable-queue-adapter.ts +157 -0
- package/src/messaging/outbound/session-route.ts +2 -2
- package/src/openclaw/config-runtime.ts +52 -0
- package/src/openclaw/inbound-session-runtime.ts +94 -0
- package/src/openclaw/ingress-runtime.ts +35 -0
- package/src/openclaw/media-runtime.ts +73 -0
- package/src/openclaw/reply-runtime.ts +104 -0
- package/src/openclaw/routing-runtime.ts +48 -0
- package/src/openclaw/sdk-helpers.ts +20 -0
- package/src/openclaw/session-route-runtime.ts +15 -0
- package/src/plugin/capabilities.ts +8 -0
- package/src/plugin/config.ts +35 -0
- package/src/plugin/gateway-methods.ts +12 -0
- package/src/plugin/gateway-runtime.ts +11 -0
- package/src/plugin/message-policy.ts +4 -0
- package/src/plugin/message-send.ts +13 -0
- package/src/plugin/messaging.ts +142 -0
- package/src/plugin/meta.ts +10 -0
- package/src/plugin/outbound.ts +51 -0
- package/src/plugin/setup.ts +24 -0
- package/src/plugin/status.ts +38 -0
- package/src/runtime/log-dedupe.ts +56 -0
- package/src/runtime/outbound-ack-timeout.ts +96 -0
- package/src/runtime/outbound-flags.ts +81 -0
- package/src/runtime/outbox-transitions.ts +119 -0
- package/src/runtime/status-snapshots.ts +108 -0
- 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 {
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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 =
|
|
526
|
+
const to = readOpenClawStringParam(paramsObj, 'to', { required: true });
|
|
485
527
|
const resolvedAccountId = normalizeAccountId(
|
|
486
|
-
|
|
528
|
+
readOpenClawStringParam(paramsObj, 'accountId') ?? input.accountId,
|
|
487
529
|
);
|
|
488
530
|
|
|
489
|
-
const message =
|
|
490
|
-
const caption =
|
|
531
|
+
const message = readOpenClawStringParam(paramsObj, 'message', { allowEmpty: true }) ?? '';
|
|
532
|
+
const caption = readOpenClawStringParam(paramsObj, 'caption', { allowEmpty: true }) ?? '';
|
|
491
533
|
const mediaUrl =
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
const asVoice =
|
|
497
|
-
const audioAsVoice =
|
|
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
|
-
|
|
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
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1030
|
+
summary,
|
|
971
1031
|
apiInstanceId: this.lastApiInstanceId,
|
|
972
1032
|
registryFingerprint: this.lastRegistryFingerprint,
|
|
973
|
-
|
|
974
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
3019
|
-
|
|
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
|
-
|
|
3029
|
-
|
|
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
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
args.
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
)
|
|
3051
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
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
|
-
|
|
3347
|
-
entry.
|
|
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
|
-
|
|
3399
|
-
entry.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
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
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
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
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
|
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:
|
|
4577
|
+
loaded: OpenClawLoadedMedia;
|
|
4587
4578
|
size: number;
|
|
4588
4579
|
mimeType?: string;
|
|
4589
4580
|
fileName: string;
|
|
4590
4581
|
}> {
|
|
4591
|
-
const loaded = await this.api
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 })
|
|
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
|
|
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
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
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
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
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;
|