@xmoxmo/bncr 0.2.1 → 0.2.3
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 +6 -2
- package/index.ts +474 -87
- package/openclaw.plugin.json +59 -0
- package/package.json +5 -5
- package/src/channel.ts +1279 -198
- package/src/core/config-schema.ts +6 -0
- package/src/core/types.ts +18 -1
- package/src/messaging/outbound/build-send-action.ts +115 -0
package/src/channel.ts
CHANGED
|
@@ -73,8 +73,13 @@ import {
|
|
|
73
73
|
resolveBncrOutboundTarget,
|
|
74
74
|
} from './messaging/outbound/target-resolver.ts';
|
|
75
75
|
const BRIDGE_VERSION = 2;
|
|
76
|
-
const BNCR_PUSH_EVENT = 'bncr.push';
|
|
76
|
+
const BNCR_PUSH_EVENT = 'plugin.bncr.push';
|
|
77
|
+
const BNCR_FILE_INIT_EVENT = 'plugin.bncr.file.init';
|
|
78
|
+
const BNCR_FILE_CHUNK_EVENT = 'plugin.bncr.file.chunk';
|
|
79
|
+
const BNCR_FILE_COMPLETE_EVENT = 'plugin.bncr.file.complete';
|
|
80
|
+
const BNCR_FILE_ABORT_EVENT = 'plugin.bncr.file.abort';
|
|
77
81
|
const CONNECT_TTL_MS = 120_000;
|
|
82
|
+
const RECENT_INBOUND_SEND_WINDOW_MS = 60_000;
|
|
78
83
|
const MAX_RETRY = 10;
|
|
79
84
|
const PUSH_DRAIN_INTERVAL_MS = 500;
|
|
80
85
|
const PUSH_ACK_TIMEOUT_MS = 30_000;
|
|
@@ -103,6 +108,8 @@ type FileSendTransferState = {
|
|
|
103
108
|
status: 'init' | 'transferring' | 'completed' | 'aborted';
|
|
104
109
|
ackedChunks: Set<number>;
|
|
105
110
|
failedChunks: Map<number, string>;
|
|
111
|
+
ownerConnId?: string;
|
|
112
|
+
ownerClientId?: string;
|
|
106
113
|
completedPath?: string;
|
|
107
114
|
error?: string;
|
|
108
115
|
};
|
|
@@ -122,10 +129,18 @@ type FileRecvTransferState = {
|
|
|
122
129
|
status: 'init' | 'transferring' | 'completed' | 'aborted';
|
|
123
130
|
bufferByChunk: Map<number, Buffer>;
|
|
124
131
|
receivedChunks: Set<number>;
|
|
132
|
+
ownerConnId?: string;
|
|
133
|
+
ownerClientId?: string;
|
|
125
134
|
completedPath?: string;
|
|
126
135
|
error?: string;
|
|
127
136
|
};
|
|
128
137
|
|
|
138
|
+
type FileAckPayloadState = {
|
|
139
|
+
payload: Record<string, unknown>;
|
|
140
|
+
ok: boolean;
|
|
141
|
+
at: number;
|
|
142
|
+
};
|
|
143
|
+
|
|
129
144
|
type ChatType = 'direct' | 'group' | (string & {});
|
|
130
145
|
|
|
131
146
|
type ChannelMessageActionAdapter = {
|
|
@@ -181,6 +196,56 @@ type PersistedState = {
|
|
|
181
196
|
} | null;
|
|
182
197
|
};
|
|
183
198
|
|
|
199
|
+
type NormalizedBncrSendParams = {
|
|
200
|
+
to: string;
|
|
201
|
+
accountId: string;
|
|
202
|
+
message: string;
|
|
203
|
+
caption: string;
|
|
204
|
+
mediaUrl?: string;
|
|
205
|
+
asVoice: boolean;
|
|
206
|
+
audioAsVoice: boolean;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
function normalizeBncrSendParams(input: {
|
|
210
|
+
params: unknown;
|
|
211
|
+
accountId: string;
|
|
212
|
+
}): NormalizedBncrSendParams {
|
|
213
|
+
const paramsObj = isPlainObject(input.params) ? input.params : {};
|
|
214
|
+
const to = readStringParam(paramsObj, 'to', { required: true });
|
|
215
|
+
const resolvedAccountId = normalizeAccountId(
|
|
216
|
+
readStringParam(paramsObj, 'accountId') ?? input.accountId,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const message = readStringParam(paramsObj, 'message', { allowEmpty: true }) ?? '';
|
|
220
|
+
const caption = readStringParam(paramsObj, 'caption', { allowEmpty: true }) ?? '';
|
|
221
|
+
const mediaUrl =
|
|
222
|
+
readStringParam(paramsObj, 'media', { trim: false }) ??
|
|
223
|
+
readStringParam(paramsObj, 'path', { trim: false }) ??
|
|
224
|
+
readStringParam(paramsObj, 'filePath', { trim: false }) ??
|
|
225
|
+
readStringParam(paramsObj, 'mediaUrl', { trim: false });
|
|
226
|
+
const asVoice = readBooleanParam(paramsObj, 'asVoice') ?? false;
|
|
227
|
+
const audioAsVoice = readBooleanParam(paramsObj, 'audioAsVoice') ?? false;
|
|
228
|
+
|
|
229
|
+
if (asVoice && !mediaUrl) throw new Error('send voice requires media path');
|
|
230
|
+
|
|
231
|
+
const normalizedMessage = mediaUrl ? '' : message || caption || '';
|
|
232
|
+
const normalizedCaption = mediaUrl ? caption || message || '' : '';
|
|
233
|
+
|
|
234
|
+
if (!normalizedMessage.trim() && !normalizedCaption.trim() && !mediaUrl) {
|
|
235
|
+
throw new Error('send requires message or media');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
to,
|
|
240
|
+
accountId: resolvedAccountId,
|
|
241
|
+
message: normalizedMessage,
|
|
242
|
+
caption: normalizedCaption,
|
|
243
|
+
mediaUrl: mediaUrl || undefined,
|
|
244
|
+
asVoice,
|
|
245
|
+
audioAsVoice,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
184
249
|
function now() {
|
|
185
250
|
return Date.now();
|
|
186
251
|
}
|
|
@@ -191,6 +256,10 @@ function asString(v: unknown, fallback = ''): string {
|
|
|
191
256
|
return String(v);
|
|
192
257
|
}
|
|
193
258
|
|
|
259
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
260
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
261
|
+
}
|
|
262
|
+
|
|
194
263
|
function backoffMs(retryCount: number): number {
|
|
195
264
|
// 1s,2s,4s,8s... capped by retry count checks
|
|
196
265
|
return Math.max(1_000, 1_000 * 2 ** Math.max(0, retryCount - 1));
|
|
@@ -340,6 +409,7 @@ class BncrBridgeRuntime {
|
|
|
340
409
|
private lastActivityByAccount = new Map<string, number>();
|
|
341
410
|
private lastInboundByAccount = new Map<string, number>();
|
|
342
411
|
private lastOutboundByAccount = new Map<string, number>();
|
|
412
|
+
private channelAccountTimers = new Map<string, NodeJS.Timeout>();
|
|
343
413
|
private canonicalAgentId: string | null = null;
|
|
344
414
|
private canonicalAgentSource: 'startup' | 'runtime' | 'fallback-main' | null = null;
|
|
345
415
|
private canonicalAgentResolvedAt: number | null = null;
|
|
@@ -354,7 +424,13 @@ class BncrBridgeRuntime {
|
|
|
354
424
|
private saveTimer: NodeJS.Timeout | null = null;
|
|
355
425
|
private pushTimer: NodeJS.Timeout | null = null;
|
|
356
426
|
private pushDrainRunningAccounts = new Set<string>();
|
|
357
|
-
private
|
|
427
|
+
private messageAckWaiters = new Map<
|
|
428
|
+
string,
|
|
429
|
+
{
|
|
430
|
+
resolve: (result: 'acked' | 'timeout') => void;
|
|
431
|
+
timer: NodeJS.Timeout;
|
|
432
|
+
}
|
|
433
|
+
>();
|
|
358
434
|
private gatewayContext: GatewayRequestHandlerOptions['context'] | null = null;
|
|
359
435
|
|
|
360
436
|
// 文件互传状态(V1:尽力而为,重连不续传)
|
|
@@ -368,6 +444,7 @@ class BncrBridgeRuntime {
|
|
|
368
444
|
timer: NodeJS.Timeout;
|
|
369
445
|
}
|
|
370
446
|
>();
|
|
447
|
+
private earlyFileAcks = new Map<string, FileAckPayloadState>();
|
|
371
448
|
|
|
372
449
|
constructor(api: OpenClawPluginApi) {
|
|
373
450
|
this.api = api;
|
|
@@ -426,6 +503,19 @@ class BncrBridgeRuntime {
|
|
|
426
503
|
this.logInfo('outbound', [type, this.summarizeScope(entry.route), preview].join('|'));
|
|
427
504
|
}
|
|
428
505
|
|
|
506
|
+
private clearChannelAccountWorker(accountId: string, reason: string) {
|
|
507
|
+
const timer = this.channelAccountTimers.get(accountId);
|
|
508
|
+
if (!timer) return false;
|
|
509
|
+
clearInterval(timer);
|
|
510
|
+
this.channelAccountTimers.delete(accountId);
|
|
511
|
+
this.logInfo(
|
|
512
|
+
'health',
|
|
513
|
+
`status-worker cleared ${JSON.stringify({ bridge: this.bridgeId, accountId, reason })}`,
|
|
514
|
+
{ debugOnly: true },
|
|
515
|
+
);
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
|
|
429
519
|
private classifyRegisterTrace(stack: string) {
|
|
430
520
|
if (
|
|
431
521
|
stack.includes('prepareSecretsRuntimeSnapshot') ||
|
|
@@ -660,6 +750,45 @@ class BncrBridgeRuntime {
|
|
|
660
750
|
return { stale: true, reason: 'mismatch' as const };
|
|
661
751
|
}
|
|
662
752
|
|
|
753
|
+
private shouldIgnoreStaleEvent(params: {
|
|
754
|
+
kind:
|
|
755
|
+
| 'inbound'
|
|
756
|
+
| 'activity'
|
|
757
|
+
| 'ack'
|
|
758
|
+
| 'file.init'
|
|
759
|
+
| 'file.chunk'
|
|
760
|
+
| 'file.complete'
|
|
761
|
+
| 'file.abort';
|
|
762
|
+
payload: { leaseId?: string; connectionEpoch?: number };
|
|
763
|
+
accountId: string;
|
|
764
|
+
connId: string;
|
|
765
|
+
clientId?: string;
|
|
766
|
+
}) {
|
|
767
|
+
const observed = this.observeLease(params.kind, params.payload);
|
|
768
|
+
if (!observed.stale) return false;
|
|
769
|
+
this.logWarn(
|
|
770
|
+
'stale',
|
|
771
|
+
`ignore kind=${params.kind} accountId=${params.accountId} connId=${params.connId} clientId=${params.clientId || '-'} reason=${observed.reason}`,
|
|
772
|
+
{ debugOnly: true },
|
|
773
|
+
);
|
|
774
|
+
return true;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private matchesTransferOwner(params: {
|
|
778
|
+
ownerConnId?: string;
|
|
779
|
+
ownerClientId?: string;
|
|
780
|
+
connId: string;
|
|
781
|
+
clientId?: string;
|
|
782
|
+
}) {
|
|
783
|
+
const sameConn = !!params.ownerConnId && params.ownerConnId === params.connId;
|
|
784
|
+
const sameClient =
|
|
785
|
+
!params.ownerConnId &&
|
|
786
|
+
!!params.ownerClientId &&
|
|
787
|
+
!!params.clientId &&
|
|
788
|
+
params.ownerClientId === params.clientId;
|
|
789
|
+
return sameConn || sameClient;
|
|
790
|
+
}
|
|
791
|
+
|
|
663
792
|
private buildExtendedDiagnostics(accountId: string) {
|
|
664
793
|
const diagnostics = this.buildIntegratedDiagnostics(accountId) as Record<string, any>;
|
|
665
794
|
return {
|
|
@@ -722,7 +851,7 @@ class BncrBridgeRuntime {
|
|
|
722
851
|
this.statePath = path.join(ctx.stateDir, 'bncr-bridge-state.json');
|
|
723
852
|
await this.loadState();
|
|
724
853
|
try {
|
|
725
|
-
const cfg =
|
|
854
|
+
const cfg = this.api.runtime.config.current();
|
|
726
855
|
this.initializeCanonicalAgentId(cfg);
|
|
727
856
|
} catch {
|
|
728
857
|
// ignore startup canonical agent initialization errors
|
|
@@ -750,6 +879,26 @@ class BncrBridgeRuntime {
|
|
|
750
879
|
this.logInfo('debug', 'service stopped', { debugOnly: true });
|
|
751
880
|
};
|
|
752
881
|
|
|
882
|
+
shutdown() {
|
|
883
|
+
if (this.saveTimer) {
|
|
884
|
+
clearTimeout(this.saveTimer);
|
|
885
|
+
this.saveTimer = null;
|
|
886
|
+
}
|
|
887
|
+
if (this.pushTimer) {
|
|
888
|
+
clearTimeout(this.pushTimer);
|
|
889
|
+
this.pushTimer = null;
|
|
890
|
+
}
|
|
891
|
+
for (const waiter of this.messageAckWaiters.values()) {
|
|
892
|
+
clearTimeout(waiter.timer);
|
|
893
|
+
}
|
|
894
|
+
this.messageAckWaiters.clear();
|
|
895
|
+
for (const waiter of this.fileAckWaiters.values()) {
|
|
896
|
+
clearTimeout(waiter.timer);
|
|
897
|
+
}
|
|
898
|
+
this.fileAckWaiters.clear();
|
|
899
|
+
this.earlyFileAcks.clear();
|
|
900
|
+
}
|
|
901
|
+
|
|
753
902
|
private scheduleSave() {
|
|
754
903
|
if (this.saveTimer) return;
|
|
755
904
|
this.saveTimer = setTimeout(() => {
|
|
@@ -769,7 +918,7 @@ class BncrBridgeRuntime {
|
|
|
769
918
|
|
|
770
919
|
private async refreshDebugFlagFromConfig(options?: { forceLog?: boolean }) {
|
|
771
920
|
try {
|
|
772
|
-
const cfg =
|
|
921
|
+
const cfg = this.api.runtime.config.current();
|
|
773
922
|
const raw = (cfg as any)?.channels?.[CHANNEL_ID]?.debug?.verbose;
|
|
774
923
|
const next = typeof raw === 'boolean' ? raw : false;
|
|
775
924
|
const changed = next !== BNCR_DEBUG_VERBOSE;
|
|
@@ -1170,18 +1319,32 @@ class BncrBridgeRuntime {
|
|
|
1170
1319
|
await writeJsonFileAtomically(this.statePath, data);
|
|
1171
1320
|
}
|
|
1172
1321
|
|
|
1173
|
-
private
|
|
1174
|
-
const key =
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1322
|
+
private resolveMessageAck(messageId: string, result: 'acked' | 'timeout' = 'acked') {
|
|
1323
|
+
const key = asString(messageId).trim();
|
|
1324
|
+
if (!key) return false;
|
|
1325
|
+
const waiter = this.messageAckWaiters.get(key);
|
|
1326
|
+
if (!waiter) return false;
|
|
1327
|
+
this.messageAckWaiters.delete(key);
|
|
1328
|
+
clearTimeout(waiter.timer);
|
|
1329
|
+
waiter.resolve(result);
|
|
1330
|
+
return true;
|
|
1179
1331
|
}
|
|
1180
1332
|
|
|
1181
1333
|
private rememberGatewayContext(context: GatewayRequestHandlerOptions['context']) {
|
|
1182
1334
|
if (context) this.gatewayContext = context;
|
|
1183
1335
|
}
|
|
1184
1336
|
|
|
1337
|
+
private resolveOutboxPushOwner(accountId: string): BncrConnection | null {
|
|
1338
|
+
const acc = normalizeAccountId(accountId);
|
|
1339
|
+
const t = now();
|
|
1340
|
+
const primaryKey = this.activeConnectionByAccount.get(acc);
|
|
1341
|
+
if (!primaryKey) return null;
|
|
1342
|
+
const primary = this.connections.get(primaryKey);
|
|
1343
|
+
if (!primary?.connId) return null;
|
|
1344
|
+
if (t - primary.lastSeenAt > CONNECT_TTL_MS) return null;
|
|
1345
|
+
return primary;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1185
1348
|
private resolvePushConnIds(accountId: string): Set<string> {
|
|
1186
1349
|
const acc = normalizeAccountId(accountId);
|
|
1187
1350
|
const t = now();
|
|
@@ -1207,7 +1370,280 @@ class BncrBridgeRuntime {
|
|
|
1207
1370
|
return connIds;
|
|
1208
1371
|
}
|
|
1209
1372
|
|
|
1210
|
-
private
|
|
1373
|
+
private hasRecentInboundReachability(accountId: string): boolean {
|
|
1374
|
+
const acc = normalizeAccountId(accountId);
|
|
1375
|
+
const t = now();
|
|
1376
|
+
const lastInboundAt = this.lastInboundByAccount.get(acc) || 0;
|
|
1377
|
+
const lastActivityAt = this.lastActivityByAccount.get(acc) || 0;
|
|
1378
|
+
const lastReachableAt = Math.max(lastInboundAt, lastActivityAt);
|
|
1379
|
+
return lastReachableAt > 0 && t - lastReachableAt <= RECENT_INBOUND_SEND_WINDOW_MS;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
private resolveRecentInboundConnIds(accountId: string): Set<string> {
|
|
1383
|
+
const acc = normalizeAccountId(accountId);
|
|
1384
|
+
const t = now();
|
|
1385
|
+
const connIds = new Set<string>();
|
|
1386
|
+
if (!this.hasRecentInboundReachability(acc)) return connIds;
|
|
1387
|
+
|
|
1388
|
+
for (const c of this.connections.values()) {
|
|
1389
|
+
if (c.accountId !== acc) continue;
|
|
1390
|
+
if (!c.connId) continue;
|
|
1391
|
+
if (t - c.lastSeenAt > CONNECT_TTL_MS * 2) continue;
|
|
1392
|
+
connIds.add(c.connId);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
return connIds;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
private isRecentlyReachableConn(accountId: string, connId?: string, clientId?: string): boolean {
|
|
1399
|
+
const acc = normalizeAccountId(accountId);
|
|
1400
|
+
const cid = asString(connId || '').trim();
|
|
1401
|
+
const client = asString(clientId || '').trim() || undefined;
|
|
1402
|
+
if (!cid) return false;
|
|
1403
|
+
|
|
1404
|
+
const recentConnIds = this.resolveRecentInboundConnIds(acc);
|
|
1405
|
+
if (recentConnIds.has(cid)) return true;
|
|
1406
|
+
|
|
1407
|
+
const activeKey = this.activeConnectionByAccount.get(acc);
|
|
1408
|
+
if (!activeKey) return false;
|
|
1409
|
+
const active = this.connections.get(activeKey);
|
|
1410
|
+
if (!active?.connId) return false;
|
|
1411
|
+
if (active.connId !== cid) return false;
|
|
1412
|
+
if (client && active.clientId && active.clientId !== client) return false;
|
|
1413
|
+
return true;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
private tryAdoptTransferOwner(args: {
|
|
1417
|
+
accountId: string;
|
|
1418
|
+
transfer:
|
|
1419
|
+
| FileSendTransferState
|
|
1420
|
+
| FileRecvTransferState
|
|
1421
|
+
| undefined;
|
|
1422
|
+
connId: string;
|
|
1423
|
+
clientId?: string;
|
|
1424
|
+
}): boolean {
|
|
1425
|
+
const { accountId, transfer, connId, clientId } = args;
|
|
1426
|
+
if (!transfer) return false;
|
|
1427
|
+
if (!this.hasRecentInboundReachability(accountId)) return false;
|
|
1428
|
+
if (!this.isRecentlyReachableConn(accountId, connId, clientId)) return false;
|
|
1429
|
+
|
|
1430
|
+
transfer.ownerConnId = connId;
|
|
1431
|
+
transfer.ownerClientId = asString(clientId || '').trim() || undefined;
|
|
1432
|
+
return true;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
private isRetryableFileTransferError(error: unknown): boolean {
|
|
1436
|
+
const msg = asString((error as any)?.message || error || '')
|
|
1437
|
+
.trim()
|
|
1438
|
+
.toLowerCase();
|
|
1439
|
+
if (!msg) return true;
|
|
1440
|
+
|
|
1441
|
+
const retryableMarkers = [
|
|
1442
|
+
'gateway context unavailable',
|
|
1443
|
+
'no active bncr client for file chunk transfer',
|
|
1444
|
+
'chunk ack timeout',
|
|
1445
|
+
'complete ack timeout',
|
|
1446
|
+
'transfer state missing',
|
|
1447
|
+
'transfer aborted',
|
|
1448
|
+
'temporarily unavailable',
|
|
1449
|
+
'timeout',
|
|
1450
|
+
'econn',
|
|
1451
|
+
'socket',
|
|
1452
|
+
'network',
|
|
1453
|
+
];
|
|
1454
|
+
|
|
1455
|
+
return retryableMarkers.some((marker) => msg.includes(marker));
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
private buildFileTransferOutboxEntry(params: {
|
|
1459
|
+
accountId: string;
|
|
1460
|
+
sessionKey: string;
|
|
1461
|
+
route: BncrRoute;
|
|
1462
|
+
mediaUrl: string;
|
|
1463
|
+
mediaLocalRoots?: readonly string[];
|
|
1464
|
+
text?: string;
|
|
1465
|
+
asVoice?: boolean;
|
|
1466
|
+
audioAsVoice?: boolean;
|
|
1467
|
+
kind?: 'tool' | 'block' | 'final';
|
|
1468
|
+
replyToId?: string;
|
|
1469
|
+
}): OutboxEntry {
|
|
1470
|
+
const messageId = randomUUID();
|
|
1471
|
+
return {
|
|
1472
|
+
messageId,
|
|
1473
|
+
accountId: normalizeAccountId(params.accountId),
|
|
1474
|
+
sessionKey: params.sessionKey,
|
|
1475
|
+
route: params.route,
|
|
1476
|
+
payload: {
|
|
1477
|
+
type: 'message.outbound',
|
|
1478
|
+
sessionKey: params.sessionKey,
|
|
1479
|
+
_meta: {
|
|
1480
|
+
kind: 'file-transfer',
|
|
1481
|
+
mediaUrl: params.mediaUrl,
|
|
1482
|
+
mediaLocalRoots: params.mediaLocalRoots ? Array.from(params.mediaLocalRoots) : undefined,
|
|
1483
|
+
text: asString(params.text || ''),
|
|
1484
|
+
asVoice: params.asVoice === true,
|
|
1485
|
+
audioAsVoice: params.audioAsVoice === true,
|
|
1486
|
+
finalEvent: BNCR_PUSH_EVENT,
|
|
1487
|
+
replyToId: asString(params.replyToId || '').trim() || undefined,
|
|
1488
|
+
messageKind: params.kind,
|
|
1489
|
+
},
|
|
1490
|
+
},
|
|
1491
|
+
createdAt: now(),
|
|
1492
|
+
retryCount: 0,
|
|
1493
|
+
nextAttemptAt: now(),
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
private async tryPushEntry(entry: OutboxEntry): Promise<boolean> {
|
|
1498
|
+
const meta = isPlainObject(entry.payload?._meta) ? entry.payload._meta : null;
|
|
1499
|
+
if (meta?.kind === 'file-transfer') {
|
|
1500
|
+
const ctx = this.gatewayContext;
|
|
1501
|
+
if (!ctx) {
|
|
1502
|
+
entry.lastError = 'gateway context unavailable';
|
|
1503
|
+
this.outbox.set(entry.messageId, entry);
|
|
1504
|
+
this.logInfo(
|
|
1505
|
+
'outbox',
|
|
1506
|
+
`push-skip ${JSON.stringify({
|
|
1507
|
+
messageId: entry.messageId,
|
|
1508
|
+
accountId: entry.accountId,
|
|
1509
|
+
kind: 'file-transfer',
|
|
1510
|
+
reason: 'no-gateway-context',
|
|
1511
|
+
})}`,
|
|
1512
|
+
{ debugOnly: true },
|
|
1513
|
+
);
|
|
1514
|
+
return false;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
const owner = this.resolveOutboxPushOwner(entry.accountId);
|
|
1518
|
+
let connIds = owner?.connId
|
|
1519
|
+
? new Set([owner.connId])
|
|
1520
|
+
: this.resolvePushConnIds(entry.accountId);
|
|
1521
|
+
const recentInboundReachable = this.hasRecentInboundReachability(entry.accountId);
|
|
1522
|
+
if (!connIds.size && recentInboundReachable) {
|
|
1523
|
+
connIds = this.resolveRecentInboundConnIds(entry.accountId);
|
|
1524
|
+
}
|
|
1525
|
+
if (!connIds.size) {
|
|
1526
|
+
entry.lastError = 'no active bncr client for file chunk transfer';
|
|
1527
|
+
this.outbox.set(entry.messageId, entry);
|
|
1528
|
+
this.logInfo(
|
|
1529
|
+
'outbox',
|
|
1530
|
+
`push-skip ${JSON.stringify({
|
|
1531
|
+
messageId: entry.messageId,
|
|
1532
|
+
accountId: entry.accountId,
|
|
1533
|
+
kind: 'file-transfer',
|
|
1534
|
+
reason: 'no-active-connection',
|
|
1535
|
+
recentInboundReachable,
|
|
1536
|
+
})}`,
|
|
1537
|
+
{ debugOnly: true },
|
|
1538
|
+
);
|
|
1539
|
+
return false;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
const mediaUrl = asString(meta.mediaUrl || '').trim();
|
|
1543
|
+
if (!mediaUrl) {
|
|
1544
|
+
entry.lastError = 'file transfer mediaUrl missing';
|
|
1545
|
+
this.outbox.set(entry.messageId, entry);
|
|
1546
|
+
this.logInfo(
|
|
1547
|
+
'outbox',
|
|
1548
|
+
`push-fail ${JSON.stringify({
|
|
1549
|
+
messageId: entry.messageId,
|
|
1550
|
+
accountId: entry.accountId,
|
|
1551
|
+
kind: 'file-transfer',
|
|
1552
|
+
error: entry.lastError,
|
|
1553
|
+
})}`,
|
|
1554
|
+
{ debugOnly: true },
|
|
1555
|
+
);
|
|
1556
|
+
return false;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
try {
|
|
1560
|
+
const media = await this.transferMediaToBncrClient({
|
|
1561
|
+
accountId: entry.accountId,
|
|
1562
|
+
sessionKey: entry.sessionKey,
|
|
1563
|
+
route: entry.route,
|
|
1564
|
+
mediaUrl,
|
|
1565
|
+
mediaLocalRoots: Array.isArray(meta.mediaLocalRoots)
|
|
1566
|
+
? meta.mediaLocalRoots.filter((v): v is string => typeof v === 'string')
|
|
1567
|
+
: undefined,
|
|
1568
|
+
});
|
|
1569
|
+
const wantsVoice = meta.asVoice === true || meta.audioAsVoice === true;
|
|
1570
|
+
const frame = buildBncrMediaOutboundFrame({
|
|
1571
|
+
messageId: entry.messageId,
|
|
1572
|
+
sessionKey: entry.sessionKey,
|
|
1573
|
+
route: entry.route,
|
|
1574
|
+
media,
|
|
1575
|
+
mediaUrl,
|
|
1576
|
+
mediaMsg: asString(meta.text || ''),
|
|
1577
|
+
fileName: resolveOutboundFileName({
|
|
1578
|
+
mediaUrl,
|
|
1579
|
+
fileName: media.fileName,
|
|
1580
|
+
mimeType: media.mimeType,
|
|
1581
|
+
}),
|
|
1582
|
+
hintedType: wantsVoice ? 'voice' : undefined,
|
|
1583
|
+
kind:
|
|
1584
|
+
meta.messageKind === 'tool' ||
|
|
1585
|
+
meta.messageKind === 'block' ||
|
|
1586
|
+
meta.messageKind === 'final'
|
|
1587
|
+
? meta.messageKind
|
|
1588
|
+
: undefined,
|
|
1589
|
+
replyToId: asString(meta.replyToId || '').trim() || undefined,
|
|
1590
|
+
now: now(),
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
ctx.broadcastToConnIds(
|
|
1594
|
+
BNCR_PUSH_EVENT,
|
|
1595
|
+
{
|
|
1596
|
+
...frame,
|
|
1597
|
+
idempotencyKey: entry.messageId,
|
|
1598
|
+
},
|
|
1599
|
+
connIds,
|
|
1600
|
+
);
|
|
1601
|
+
entry.lastPushAt = now();
|
|
1602
|
+
entry.lastPushConnId =
|
|
1603
|
+
owner?.connId || (connIds.size === 1 ? Array.from(connIds)[0] : undefined);
|
|
1604
|
+
entry.lastPushClientId = owner?.clientId;
|
|
1605
|
+
entry.lastError = undefined;
|
|
1606
|
+
this.outbox.set(entry.messageId, entry);
|
|
1607
|
+
this.lastOutboundByAccount.set(entry.accountId, entry.lastPushAt);
|
|
1608
|
+
this.markActivity(entry.accountId, entry.lastPushAt);
|
|
1609
|
+
this.scheduleSave();
|
|
1610
|
+
this.logInfo(
|
|
1611
|
+
'outbox',
|
|
1612
|
+
`push-ok ${JSON.stringify({
|
|
1613
|
+
messageId: entry.messageId,
|
|
1614
|
+
accountId: entry.accountId,
|
|
1615
|
+
kind: 'file-transfer',
|
|
1616
|
+
connIds: Array.from(connIds),
|
|
1617
|
+
ownerConnId: entry.lastPushConnId || '',
|
|
1618
|
+
ownerClientId: entry.lastPushClientId || '',
|
|
1619
|
+
recentInboundReachable,
|
|
1620
|
+
event: BNCR_PUSH_EVENT,
|
|
1621
|
+
})}`,
|
|
1622
|
+
{ debugOnly: true },
|
|
1623
|
+
);
|
|
1624
|
+
return true;
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
entry.lastError = asString((error as any)?.message || error || 'file-transfer-error');
|
|
1627
|
+
this.outbox.set(entry.messageId, entry);
|
|
1628
|
+
this.scheduleSave();
|
|
1629
|
+
this.logInfo(
|
|
1630
|
+
'outbox',
|
|
1631
|
+
`push-fail ${JSON.stringify({
|
|
1632
|
+
messageId: entry.messageId,
|
|
1633
|
+
accountId: entry.accountId,
|
|
1634
|
+
kind: 'file-transfer',
|
|
1635
|
+
retryable: this.isRetryableFileTransferError(error),
|
|
1636
|
+
error: entry.lastError,
|
|
1637
|
+
})}`,
|
|
1638
|
+
{ debugOnly: true },
|
|
1639
|
+
);
|
|
1640
|
+
if (!this.isRetryableFileTransferError(error)) {
|
|
1641
|
+
this.moveToDeadLetter(entry, entry.lastError || 'file-transfer-failed');
|
|
1642
|
+
}
|
|
1643
|
+
return false;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1211
1647
|
const ctx = this.gatewayContext;
|
|
1212
1648
|
if (!ctx) {
|
|
1213
1649
|
this.logInfo(
|
|
@@ -1222,7 +1658,14 @@ class BncrBridgeRuntime {
|
|
|
1222
1658
|
return false;
|
|
1223
1659
|
}
|
|
1224
1660
|
|
|
1225
|
-
const
|
|
1661
|
+
const owner = this.resolveOutboxPushOwner(entry.accountId);
|
|
1662
|
+
let connIds = owner?.connId
|
|
1663
|
+
? new Set([owner.connId])
|
|
1664
|
+
: this.resolvePushConnIds(entry.accountId);
|
|
1665
|
+
const recentInboundReachable = this.hasRecentInboundReachability(entry.accountId);
|
|
1666
|
+
if (!connIds.size && recentInboundReachable) {
|
|
1667
|
+
connIds = this.resolveRecentInboundConnIds(entry.accountId);
|
|
1668
|
+
}
|
|
1226
1669
|
if (!connIds.size) {
|
|
1227
1670
|
this.logInfo(
|
|
1228
1671
|
'outbox',
|
|
@@ -1230,6 +1673,7 @@ class BncrBridgeRuntime {
|
|
|
1230
1673
|
messageId: entry.messageId,
|
|
1231
1674
|
accountId: entry.accountId,
|
|
1232
1675
|
reason: 'no-active-connection',
|
|
1676
|
+
recentInboundReachable,
|
|
1233
1677
|
})}`,
|
|
1234
1678
|
{ debugOnly: true },
|
|
1235
1679
|
);
|
|
@@ -1243,18 +1687,26 @@ class BncrBridgeRuntime {
|
|
|
1243
1687
|
};
|
|
1244
1688
|
|
|
1245
1689
|
ctx.broadcastToConnIds(BNCR_PUSH_EVENT, payload, connIds);
|
|
1690
|
+
entry.lastPushAt = now();
|
|
1691
|
+
entry.lastPushConnId =
|
|
1692
|
+
owner?.connId || (connIds.size === 1 ? Array.from(connIds)[0] : undefined);
|
|
1693
|
+
entry.lastPushClientId = owner?.clientId;
|
|
1694
|
+
this.outbox.set(entry.messageId, entry);
|
|
1246
1695
|
this.logInfo(
|
|
1247
1696
|
'outbox',
|
|
1248
1697
|
`push-ok ${JSON.stringify({
|
|
1249
1698
|
messageId: entry.messageId,
|
|
1250
1699
|
accountId: entry.accountId,
|
|
1251
1700
|
connIds: Array.from(connIds),
|
|
1701
|
+
ownerConnId: entry.lastPushConnId || '',
|
|
1702
|
+
ownerClientId: entry.lastPushClientId || '',
|
|
1703
|
+
recentInboundReachable,
|
|
1252
1704
|
event: BNCR_PUSH_EVENT,
|
|
1253
1705
|
})}`,
|
|
1254
1706
|
{ debugOnly: true },
|
|
1255
1707
|
);
|
|
1256
|
-
this.lastOutboundByAccount.set(entry.accountId,
|
|
1257
|
-
this.markActivity(entry.accountId);
|
|
1708
|
+
this.lastOutboundByAccount.set(entry.accountId, entry.lastPushAt);
|
|
1709
|
+
this.markActivity(entry.accountId, entry.lastPushAt);
|
|
1258
1710
|
this.scheduleSave();
|
|
1259
1711
|
return true;
|
|
1260
1712
|
} catch (error) {
|
|
@@ -1282,6 +1734,42 @@ class BncrBridgeRuntime {
|
|
|
1282
1734
|
}, delay);
|
|
1283
1735
|
}
|
|
1284
1736
|
|
|
1737
|
+
private isOutboundAckRequired(accountId?: string) {
|
|
1738
|
+
try {
|
|
1739
|
+
const cfg = this.api.runtime.config.current();
|
|
1740
|
+
const channelCfg = (cfg as any)?.channels?.[CHANNEL_ID];
|
|
1741
|
+
const accountCfg =
|
|
1742
|
+
accountId && channelCfg?.accounts && typeof channelCfg.accounts === 'object'
|
|
1743
|
+
? (channelCfg.accounts as Record<string, any>)[normalizeAccountId(accountId)]
|
|
1744
|
+
: null;
|
|
1745
|
+
const scoped = accountCfg?.outboundRequireAck;
|
|
1746
|
+
const global = channelCfg?.outboundRequireAck;
|
|
1747
|
+
if (typeof scoped === 'boolean') return scoped;
|
|
1748
|
+
if (typeof global === 'boolean') return global;
|
|
1749
|
+
return true;
|
|
1750
|
+
} catch {
|
|
1751
|
+
return true;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
private buildRuntimeFlags(accountId?: string) {
|
|
1756
|
+
let ackPolicySource: 'channel' | 'default' = 'default';
|
|
1757
|
+
try {
|
|
1758
|
+
const cfg = this.api.runtime.config.current();
|
|
1759
|
+
const global = (cfg as any)?.channels?.[CHANNEL_ID]?.outboundRequireAck;
|
|
1760
|
+
if (typeof global === 'boolean') ackPolicySource = 'channel';
|
|
1761
|
+
} catch {
|
|
1762
|
+
// keep default source
|
|
1763
|
+
}
|
|
1764
|
+
return {
|
|
1765
|
+
outboundRequireAck: this.isOutboundAckRequired(accountId),
|
|
1766
|
+
ackPolicySource,
|
|
1767
|
+
messageAckTimeoutMs: PUSH_ACK_TIMEOUT_MS,
|
|
1768
|
+
fileAckTimeoutMs: FILE_ACK_TIMEOUT_MS,
|
|
1769
|
+
debugVerbose: BNCR_DEBUG_VERBOSE,
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1285
1773
|
private async flushPushQueue(accountId?: string): Promise<void> {
|
|
1286
1774
|
const filterAcc = accountId ? normalizeAccountId(accountId) : null;
|
|
1287
1775
|
const targetAccounts = filterAcc
|
|
@@ -1307,12 +1795,14 @@ class BncrBridgeRuntime {
|
|
|
1307
1795
|
for (const acc of targetAccounts) {
|
|
1308
1796
|
if (!acc || this.pushDrainRunningAccounts.has(acc)) continue;
|
|
1309
1797
|
const online = this.isOnline(acc);
|
|
1798
|
+
const recentInboundReachable = this.hasRecentInboundReachability(acc);
|
|
1310
1799
|
this.logInfo(
|
|
1311
1800
|
'outbox',
|
|
1312
1801
|
`online ${JSON.stringify({
|
|
1313
1802
|
bridge: this.bridgeId,
|
|
1314
1803
|
accountId: acc,
|
|
1315
1804
|
online,
|
|
1805
|
+
recentInboundReachable,
|
|
1316
1806
|
connections: Array.from(this.connections.values()).map((c) => ({
|
|
1317
1807
|
accountId: c.accountId,
|
|
1318
1808
|
connId: c.connId,
|
|
@@ -1341,26 +1831,48 @@ class BncrBridgeRuntime {
|
|
|
1341
1831
|
break;
|
|
1342
1832
|
}
|
|
1343
1833
|
|
|
1344
|
-
const onlineNow = this.isOnline(acc);
|
|
1345
|
-
const pushed = this.tryPushEntry(entry);
|
|
1834
|
+
const onlineNow = this.isOnline(acc) || this.hasRecentInboundReachability(acc);
|
|
1835
|
+
const pushed = await this.tryPushEntry(entry);
|
|
1346
1836
|
if (pushed) {
|
|
1347
|
-
|
|
1348
|
-
|
|
1837
|
+
const requireAck = this.isOutboundAckRequired(acc);
|
|
1838
|
+
let ackResult: 'acked' | 'timeout' = requireAck ? 'timeout' : 'acked';
|
|
1839
|
+
if (onlineNow && requireAck) {
|
|
1840
|
+
ackResult = await this.waitForMessageAck(entry.messageId, PUSH_ACK_TIMEOUT_MS);
|
|
1349
1841
|
}
|
|
1350
1842
|
|
|
1843
|
+
this.logInfo(
|
|
1844
|
+
'outbox',
|
|
1845
|
+
`ack ${JSON.stringify({
|
|
1846
|
+
messageId: entry.messageId,
|
|
1847
|
+
accountId: entry.accountId,
|
|
1848
|
+
requireAck,
|
|
1849
|
+
ackResult,
|
|
1850
|
+
onlineNow,
|
|
1851
|
+
})}`,
|
|
1852
|
+
{ debugOnly: true },
|
|
1853
|
+
);
|
|
1854
|
+
|
|
1351
1855
|
if (!this.outbox.has(entry.messageId)) {
|
|
1352
1856
|
await this.sleepMs(PUSH_DRAIN_INTERVAL_MS);
|
|
1353
1857
|
continue;
|
|
1354
1858
|
}
|
|
1355
1859
|
|
|
1860
|
+
if (onlineNow && (!requireAck || ackResult !== 'timeout')) {
|
|
1861
|
+
await this.sleepMs(PUSH_DRAIN_INTERVAL_MS);
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1356
1865
|
entry.retryCount += 1;
|
|
1357
1866
|
entry.lastAttemptAt = now();
|
|
1358
1867
|
if (entry.retryCount > MAX_RETRY) {
|
|
1359
|
-
this.moveToDeadLetter(
|
|
1868
|
+
this.moveToDeadLetter(
|
|
1869
|
+
entry,
|
|
1870
|
+
entry.lastError || (requireAck ? 'push-ack-timeout' : 'push-delivery-unconfirmed'),
|
|
1871
|
+
);
|
|
1360
1872
|
continue;
|
|
1361
1873
|
}
|
|
1362
1874
|
entry.nextAttemptAt = now() + backoffMs(entry.retryCount);
|
|
1363
|
-
entry.lastError =
|
|
1875
|
+
entry.lastError = requireAck ? 'push-ack-timeout' : 'push-delivery-unconfirmed';
|
|
1364
1876
|
this.outbox.set(entry.messageId, entry);
|
|
1365
1877
|
this.scheduleSave();
|
|
1366
1878
|
|
|
@@ -1370,6 +1882,11 @@ class BncrBridgeRuntime {
|
|
|
1370
1882
|
break;
|
|
1371
1883
|
}
|
|
1372
1884
|
|
|
1885
|
+
if (!this.outbox.has(entry.messageId)) {
|
|
1886
|
+
await this.sleepMs(PUSH_DRAIN_INTERVAL_MS);
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1373
1890
|
const nextAttempt = entry.retryCount + 1;
|
|
1374
1891
|
if (nextAttempt > MAX_RETRY) {
|
|
1375
1892
|
this.moveToDeadLetter(entry, entry.lastError || 'push-retry-limit');
|
|
@@ -1400,29 +1917,18 @@ class BncrBridgeRuntime {
|
|
|
1400
1917
|
if (globalNextDelay != null) this.schedulePushDrain(globalNextDelay);
|
|
1401
1918
|
}
|
|
1402
1919
|
|
|
1403
|
-
private async
|
|
1404
|
-
const key =
|
|
1920
|
+
private async waitForMessageAck(messageId: string, waitMs: number): Promise<'acked' | 'timeout'> {
|
|
1921
|
+
const key = asString(messageId).trim();
|
|
1405
1922
|
const timeoutMs = Math.max(0, Math.min(waitMs, 25_000));
|
|
1406
|
-
if (!timeoutMs) return;
|
|
1923
|
+
if (!key || !timeoutMs) return 'timeout';
|
|
1407
1924
|
|
|
1408
|
-
await new Promise<
|
|
1925
|
+
return await new Promise<'acked' | 'timeout'>((resolve) => {
|
|
1409
1926
|
const timer = setTimeout(() => {
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
key,
|
|
1413
|
-
arr.filter((fn) => fn !== done),
|
|
1414
|
-
);
|
|
1415
|
-
resolve();
|
|
1927
|
+
this.messageAckWaiters.delete(key);
|
|
1928
|
+
resolve('timeout');
|
|
1416
1929
|
}, timeoutMs);
|
|
1417
1930
|
|
|
1418
|
-
|
|
1419
|
-
clearTimeout(timer);
|
|
1420
|
-
resolve();
|
|
1421
|
-
};
|
|
1422
|
-
|
|
1423
|
-
const arr = this.waiters.get(key) || [];
|
|
1424
|
-
arr.push(done);
|
|
1425
|
-
this.waiters.set(key, arr);
|
|
1931
|
+
this.messageAckWaiters.set(key, { resolve, timer });
|
|
1426
1932
|
});
|
|
1427
1933
|
}
|
|
1428
1934
|
|
|
@@ -1473,6 +1979,9 @@ class BncrBridgeRuntime {
|
|
|
1473
1979
|
for (const [id, st] of this.fileRecvTransfers.entries()) {
|
|
1474
1980
|
if (t - st.startedAt > FILE_TRANSFER_KEEP_MS) this.fileRecvTransfers.delete(id);
|
|
1475
1981
|
}
|
|
1982
|
+
for (const [key, ack] of this.earlyFileAcks.entries()) {
|
|
1983
|
+
if (t - ack.at > FILE_TRANSFER_ACK_TTL_MS) this.earlyFileAcks.delete(key);
|
|
1984
|
+
}
|
|
1476
1985
|
}
|
|
1477
1986
|
|
|
1478
1987
|
private markSeen(accountId: string, connId: string, clientId?: string) {
|
|
@@ -1482,6 +1991,8 @@ class BncrBridgeRuntime {
|
|
|
1482
1991
|
const key = this.connectionKey(acc, clientId);
|
|
1483
1992
|
const t = now();
|
|
1484
1993
|
const prev = this.connections.get(key);
|
|
1994
|
+
const previousActiveKey = this.activeConnectionByAccount.get(acc) || null;
|
|
1995
|
+
const previousActiveConn = previousActiveKey ? this.connections.get(previousActiveKey) || null : null;
|
|
1485
1996
|
|
|
1486
1997
|
const nextConn: BncrConnection = {
|
|
1487
1998
|
accountId: acc,
|
|
@@ -1508,6 +2019,27 @@ class BncrBridgeRuntime {
|
|
|
1508
2019
|
const current = this.activeConnectionByAccount.get(acc);
|
|
1509
2020
|
if (!current) {
|
|
1510
2021
|
this.activeConnectionByAccount.set(acc, key);
|
|
2022
|
+
this.logInfo(
|
|
2023
|
+
'connection',
|
|
2024
|
+
`seen:promote ${JSON.stringify({
|
|
2025
|
+
bridge: this.bridgeId,
|
|
2026
|
+
accountId: acc,
|
|
2027
|
+
reason: 'no-current-active',
|
|
2028
|
+
previousActiveKey,
|
|
2029
|
+
previousActiveConn,
|
|
2030
|
+
nextActiveKey: key,
|
|
2031
|
+
nextActiveConn: nextConn,
|
|
2032
|
+
activeConnections: Array.from(this.connections.values())
|
|
2033
|
+
.filter((c) => c.accountId === acc)
|
|
2034
|
+
.map((c) => ({
|
|
2035
|
+
connId: c.connId,
|
|
2036
|
+
clientId: c.clientId,
|
|
2037
|
+
connectedAt: c.connectedAt,
|
|
2038
|
+
lastSeenAt: c.lastSeenAt,
|
|
2039
|
+
})),
|
|
2040
|
+
})}`,
|
|
2041
|
+
{ debugOnly: true },
|
|
2042
|
+
);
|
|
1511
2043
|
return;
|
|
1512
2044
|
}
|
|
1513
2045
|
|
|
@@ -1518,6 +2050,31 @@ class BncrBridgeRuntime {
|
|
|
1518
2050
|
nextConn.connectedAt >= curConn.connectedAt
|
|
1519
2051
|
) {
|
|
1520
2052
|
this.activeConnectionByAccount.set(acc, key);
|
|
2053
|
+
this.logInfo(
|
|
2054
|
+
'connection',
|
|
2055
|
+
`seen:promote ${JSON.stringify({
|
|
2056
|
+
bridge: this.bridgeId,
|
|
2057
|
+
accountId: acc,
|
|
2058
|
+
reason: !curConn
|
|
2059
|
+
? 'current-missing'
|
|
2060
|
+
: t - curConn.lastSeenAt > CONNECT_TTL_MS
|
|
2061
|
+
? 'current-stale'
|
|
2062
|
+
: 'newer-or-equal-connectedAt',
|
|
2063
|
+
previousActiveKey,
|
|
2064
|
+
previousActiveConn,
|
|
2065
|
+
nextActiveKey: key,
|
|
2066
|
+
nextActiveConn: nextConn,
|
|
2067
|
+
activeConnections: Array.from(this.connections.values())
|
|
2068
|
+
.filter((c) => c.accountId === acc)
|
|
2069
|
+
.map((c) => ({
|
|
2070
|
+
connId: c.connId,
|
|
2071
|
+
clientId: c.clientId,
|
|
2072
|
+
connectedAt: c.connectedAt,
|
|
2073
|
+
lastSeenAt: c.lastSeenAt,
|
|
2074
|
+
})),
|
|
2075
|
+
})}`,
|
|
2076
|
+
{ debugOnly: true },
|
|
2077
|
+
);
|
|
1521
2078
|
}
|
|
1522
2079
|
}
|
|
1523
2080
|
|
|
@@ -1681,9 +2238,61 @@ class BncrBridgeRuntime {
|
|
|
1681
2238
|
Math.min(Number(params.timeoutMs || FILE_ACK_TIMEOUT_MS), 120_000),
|
|
1682
2239
|
);
|
|
1683
2240
|
|
|
2241
|
+
const cached = this.earlyFileAcks.get(key);
|
|
2242
|
+
if (cached) {
|
|
2243
|
+
this.earlyFileAcks.delete(key);
|
|
2244
|
+
this.logInfo(
|
|
2245
|
+
'file-ack-cache-hit',
|
|
2246
|
+
JSON.stringify({
|
|
2247
|
+
bridge: this.bridgeId,
|
|
2248
|
+
transferId,
|
|
2249
|
+
stage,
|
|
2250
|
+
chunkIndex:
|
|
2251
|
+
Number.isFinite(Number(params.chunkIndex)) ? Number(params.chunkIndex) : undefined,
|
|
2252
|
+
key,
|
|
2253
|
+
ok: cached.ok,
|
|
2254
|
+
payload: cached.payload,
|
|
2255
|
+
}),
|
|
2256
|
+
{ debugOnly: true },
|
|
2257
|
+
);
|
|
2258
|
+
if (cached.ok) return Promise.resolve(cached.payload);
|
|
2259
|
+
return Promise.reject(
|
|
2260
|
+
new Error(
|
|
2261
|
+
asString(cached.payload?.errorMessage || cached.payload?.error || 'file ack failed'),
|
|
2262
|
+
),
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
this.logInfo(
|
|
2267
|
+
'file-ack-wait',
|
|
2268
|
+
JSON.stringify({
|
|
2269
|
+
bridge: this.bridgeId,
|
|
2270
|
+
transferId,
|
|
2271
|
+
stage,
|
|
2272
|
+
chunkIndex:
|
|
2273
|
+
Number.isFinite(Number(params.chunkIndex)) ? Number(params.chunkIndex) : undefined,
|
|
2274
|
+
key,
|
|
2275
|
+
timeoutMs,
|
|
2276
|
+
}),
|
|
2277
|
+
{ debugOnly: true },
|
|
2278
|
+
);
|
|
2279
|
+
|
|
1684
2280
|
return new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
1685
2281
|
const timer = setTimeout(() => {
|
|
1686
2282
|
this.fileAckWaiters.delete(key);
|
|
2283
|
+
this.logWarn(
|
|
2284
|
+
'file-ack-timeout',
|
|
2285
|
+
JSON.stringify({
|
|
2286
|
+
bridge: this.bridgeId,
|
|
2287
|
+
transferId,
|
|
2288
|
+
stage,
|
|
2289
|
+
chunkIndex:
|
|
2290
|
+
Number.isFinite(Number(params.chunkIndex)) ? Number(params.chunkIndex) : undefined,
|
|
2291
|
+
key,
|
|
2292
|
+
timeoutMs,
|
|
2293
|
+
}),
|
|
2294
|
+
{ debugOnly: true },
|
|
2295
|
+
);
|
|
1687
2296
|
reject(new Error(`file ack timeout: ${key}`));
|
|
1688
2297
|
}, timeoutMs);
|
|
1689
2298
|
this.fileAckWaiters.set(key, { resolve, reject, timer });
|
|
@@ -1701,9 +2310,45 @@ class BncrBridgeRuntime {
|
|
|
1701
2310
|
const stage = asString(params.stage).trim();
|
|
1702
2311
|
const key = this.fileAckKey(transferId, stage, params.chunkIndex);
|
|
1703
2312
|
const waiter = this.fileAckWaiters.get(key);
|
|
1704
|
-
if (!waiter)
|
|
2313
|
+
if (!waiter) {
|
|
2314
|
+
this.earlyFileAcks.set(key, {
|
|
2315
|
+
payload: params.payload,
|
|
2316
|
+
ok: params.ok,
|
|
2317
|
+
at: now(),
|
|
2318
|
+
});
|
|
2319
|
+
this.logInfo(
|
|
2320
|
+
'file-ack-early-cache',
|
|
2321
|
+
JSON.stringify({
|
|
2322
|
+
bridge: this.bridgeId,
|
|
2323
|
+
transferId,
|
|
2324
|
+
stage,
|
|
2325
|
+
chunkIndex:
|
|
2326
|
+
Number.isFinite(Number(params.chunkIndex)) ? Number(params.chunkIndex) : undefined,
|
|
2327
|
+
key,
|
|
2328
|
+
ok: params.ok,
|
|
2329
|
+
payload: params.payload,
|
|
2330
|
+
cached: true,
|
|
2331
|
+
}),
|
|
2332
|
+
{ debugOnly: true },
|
|
2333
|
+
);
|
|
2334
|
+
return false;
|
|
2335
|
+
}
|
|
1705
2336
|
this.fileAckWaiters.delete(key);
|
|
1706
2337
|
clearTimeout(waiter.timer);
|
|
2338
|
+
this.logInfo(
|
|
2339
|
+
'file-ack-resolve',
|
|
2340
|
+
JSON.stringify({
|
|
2341
|
+
bridge: this.bridgeId,
|
|
2342
|
+
transferId,
|
|
2343
|
+
stage,
|
|
2344
|
+
chunkIndex:
|
|
2345
|
+
Number.isFinite(Number(params.chunkIndex)) ? Number(params.chunkIndex) : undefined,
|
|
2346
|
+
key,
|
|
2347
|
+
ok: params.ok,
|
|
2348
|
+
payload: params.payload,
|
|
2349
|
+
}),
|
|
2350
|
+
{ debugOnly: true },
|
|
2351
|
+
);
|
|
1707
2352
|
if (params.ok) waiter.resolve(params.payload);
|
|
1708
2353
|
else
|
|
1709
2354
|
waiter.reject(
|
|
@@ -1723,7 +2368,17 @@ class BncrBridgeRuntime {
|
|
|
1723
2368
|
if (!connIds.size || !this.gatewayContext) {
|
|
1724
2369
|
throw new Error(`no active bncr connection for account=${accountId}`);
|
|
1725
2370
|
}
|
|
1726
|
-
|
|
2371
|
+
const normalizedEvent =
|
|
2372
|
+
event === 'bncr.file.init'
|
|
2373
|
+
? BNCR_FILE_INIT_EVENT
|
|
2374
|
+
: event === 'bncr.file.chunk'
|
|
2375
|
+
? BNCR_FILE_CHUNK_EVENT
|
|
2376
|
+
: event === 'bncr.file.complete'
|
|
2377
|
+
? BNCR_FILE_COMPLETE_EVENT
|
|
2378
|
+
: event === 'bncr.file.abort'
|
|
2379
|
+
? BNCR_FILE_ABORT_EVENT
|
|
2380
|
+
: event;
|
|
2381
|
+
this.gatewayContext.broadcastToConnIds(normalizedEvent, payload, connIds);
|
|
1727
2382
|
}
|
|
1728
2383
|
|
|
1729
2384
|
private resolveInboundFileType(mimeType: string, fileName: string): string {
|
|
@@ -1890,7 +2545,6 @@ class BncrBridgeRuntime {
|
|
|
1890
2545
|
this.logOutboundSummary(entry);
|
|
1891
2546
|
this.outbox.set(entry.messageId, entry);
|
|
1892
2547
|
this.scheduleSave();
|
|
1893
|
-
this.wakeAccountWaiters(entry.accountId);
|
|
1894
2548
|
this.flushPushQueue(entry.accountId);
|
|
1895
2549
|
}
|
|
1896
2550
|
|
|
@@ -1902,6 +2556,7 @@ class BncrBridgeRuntime {
|
|
|
1902
2556
|
this.deadLetter.push(dead);
|
|
1903
2557
|
if (this.deadLetter.length > 1000) this.deadLetter = this.deadLetter.slice(-1000);
|
|
1904
2558
|
this.outbox.delete(entry.messageId);
|
|
2559
|
+
this.resolveMessageAck(entry.messageId, 'timeout');
|
|
1905
2560
|
this.scheduleSave();
|
|
1906
2561
|
}
|
|
1907
2562
|
|
|
@@ -2067,9 +2722,46 @@ class BncrBridgeRuntime {
|
|
|
2067
2722
|
}
|
|
2068
2723
|
|
|
2069
2724
|
const ctx = this.gatewayContext;
|
|
2725
|
+
const owner = this.resolveOutboxPushOwner(params.accountId);
|
|
2726
|
+
const recentInboundReachable = this.hasRecentInboundReachability(params.accountId);
|
|
2727
|
+
const directConnIds = this.resolvePushConnIds(params.accountId);
|
|
2728
|
+
const recentConnIds = recentInboundReachable
|
|
2729
|
+
? this.resolveRecentInboundConnIds(params.accountId)
|
|
2730
|
+
: new Set<string>();
|
|
2731
|
+
const accountId = normalizeAccountId(params.accountId);
|
|
2732
|
+
const activeConnectionKey = this.activeConnectionByAccount.get(accountId) || null;
|
|
2733
|
+
const accountConnections = Array.from(this.connections.values())
|
|
2734
|
+
.filter((c) => c.accountId === accountId)
|
|
2735
|
+
.map((c) => ({
|
|
2736
|
+
connId: c.connId,
|
|
2737
|
+
clientId: c.clientId,
|
|
2738
|
+
connectedAt: c.connectedAt,
|
|
2739
|
+
lastSeenAt: c.lastSeenAt,
|
|
2740
|
+
}));
|
|
2741
|
+
this.logInfo(
|
|
2742
|
+
'file-chunk-diag',
|
|
2743
|
+
JSON.stringify({
|
|
2744
|
+
bridge: this.bridgeId,
|
|
2745
|
+
accountId,
|
|
2746
|
+
sessionKey: params.sessionKey,
|
|
2747
|
+
mediaUrl: params.mediaUrl,
|
|
2748
|
+
hasGatewayContext: Boolean(ctx),
|
|
2749
|
+
activeConnectionKey,
|
|
2750
|
+
ownerConnId: owner?.connId || null,
|
|
2751
|
+
ownerClientId: owner?.clientId || null,
|
|
2752
|
+
directConnIds: Array.from(directConnIds),
|
|
2753
|
+
recentInboundReachable,
|
|
2754
|
+
recentConnIds: Array.from(recentConnIds),
|
|
2755
|
+
accountConnections,
|
|
2756
|
+
}),
|
|
2757
|
+
{ debugOnly: true },
|
|
2758
|
+
);
|
|
2070
2759
|
if (!ctx) throw new Error('gateway context unavailable');
|
|
2071
2760
|
|
|
2072
|
-
|
|
2761
|
+
let connIds = directConnIds;
|
|
2762
|
+
if (!connIds.size && recentInboundReachable) {
|
|
2763
|
+
connIds = recentConnIds;
|
|
2764
|
+
}
|
|
2073
2765
|
if (!connIds.size) throw new Error('no active bncr client for file chunk transfer');
|
|
2074
2766
|
|
|
2075
2767
|
const transferId = randomUUID();
|
|
@@ -2077,6 +2769,26 @@ class BncrBridgeRuntime {
|
|
|
2077
2769
|
const totalChunks = Math.ceil(size / chunkSize);
|
|
2078
2770
|
const fileSha256 = createHash('sha256').update(loaded.buffer).digest('hex');
|
|
2079
2771
|
|
|
2772
|
+
this.logInfo(
|
|
2773
|
+
'file-transfer-start',
|
|
2774
|
+
JSON.stringify({
|
|
2775
|
+
bridge: this.bridgeId,
|
|
2776
|
+
transferId,
|
|
2777
|
+
accountId,
|
|
2778
|
+
sessionKey: params.sessionKey,
|
|
2779
|
+
mediaUrl: params.mediaUrl,
|
|
2780
|
+
fileName,
|
|
2781
|
+
mimeType,
|
|
2782
|
+
fileSize: size,
|
|
2783
|
+
chunkSize,
|
|
2784
|
+
totalChunks,
|
|
2785
|
+
connIds: Array.from(connIds),
|
|
2786
|
+
ownerConnId: owner?.connId || null,
|
|
2787
|
+
ownerClientId: owner?.clientId || null,
|
|
2788
|
+
}),
|
|
2789
|
+
{ debugOnly: true },
|
|
2790
|
+
);
|
|
2791
|
+
|
|
2080
2792
|
const st: FileSendTransferState = {
|
|
2081
2793
|
transferId,
|
|
2082
2794
|
accountId: normalizeAccountId(params.accountId),
|
|
@@ -2092,11 +2804,13 @@ class BncrBridgeRuntime {
|
|
|
2092
2804
|
status: 'init',
|
|
2093
2805
|
ackedChunks: new Set(),
|
|
2094
2806
|
failedChunks: new Map(),
|
|
2807
|
+
ownerConnId: owner?.connId,
|
|
2808
|
+
ownerClientId: owner?.clientId,
|
|
2095
2809
|
};
|
|
2096
2810
|
this.fileSendTransfers.set(transferId, st);
|
|
2097
2811
|
|
|
2098
2812
|
ctx.broadcastToConnIds(
|
|
2099
|
-
|
|
2813
|
+
BNCR_FILE_INIT_EVENT,
|
|
2100
2814
|
{
|
|
2101
2815
|
transferId,
|
|
2102
2816
|
direction: 'oc2bncr',
|
|
@@ -2126,7 +2840,7 @@ class BncrBridgeRuntime {
|
|
|
2126
2840
|
let lastErr: unknown = null;
|
|
2127
2841
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
2128
2842
|
ctx.broadcastToConnIds(
|
|
2129
|
-
|
|
2843
|
+
BNCR_FILE_CHUNK_EVENT,
|
|
2130
2844
|
{
|
|
2131
2845
|
transferId,
|
|
2132
2846
|
chunkIndex: idx,
|
|
@@ -2139,16 +2853,54 @@ class BncrBridgeRuntime {
|
|
|
2139
2853
|
connIds,
|
|
2140
2854
|
);
|
|
2141
2855
|
|
|
2856
|
+
this.logInfo(
|
|
2857
|
+
'file-transfer-chunk-send',
|
|
2858
|
+
JSON.stringify({
|
|
2859
|
+
bridge: this.bridgeId,
|
|
2860
|
+
transferId,
|
|
2861
|
+
accountId,
|
|
2862
|
+
chunkIndex: idx,
|
|
2863
|
+
attempt,
|
|
2864
|
+
offset: start,
|
|
2865
|
+
size: slice.byteLength,
|
|
2866
|
+
connIds: Array.from(connIds),
|
|
2867
|
+
}),
|
|
2868
|
+
{ debugOnly: true },
|
|
2869
|
+
);
|
|
2870
|
+
|
|
2142
2871
|
try {
|
|
2143
2872
|
await this.waitChunkAck({
|
|
2144
2873
|
transferId,
|
|
2145
2874
|
chunkIndex: idx,
|
|
2146
2875
|
timeoutMs: FILE_TRANSFER_ACK_TTL_MS,
|
|
2147
2876
|
});
|
|
2877
|
+
this.logInfo(
|
|
2878
|
+
'file-transfer-chunk-ack',
|
|
2879
|
+
JSON.stringify({
|
|
2880
|
+
bridge: this.bridgeId,
|
|
2881
|
+
transferId,
|
|
2882
|
+
accountId,
|
|
2883
|
+
chunkIndex: idx,
|
|
2884
|
+
attempt,
|
|
2885
|
+
}),
|
|
2886
|
+
{ debugOnly: true },
|
|
2887
|
+
);
|
|
2148
2888
|
ok = true;
|
|
2149
2889
|
break;
|
|
2150
2890
|
} catch (err) {
|
|
2151
2891
|
lastErr = err;
|
|
2892
|
+
this.logWarn(
|
|
2893
|
+
'file-transfer-chunk-ack-fail',
|
|
2894
|
+
JSON.stringify({
|
|
2895
|
+
bridge: this.bridgeId,
|
|
2896
|
+
transferId,
|
|
2897
|
+
accountId,
|
|
2898
|
+
chunkIndex: idx,
|
|
2899
|
+
attempt,
|
|
2900
|
+
error: asString((err as Error)?.message || err),
|
|
2901
|
+
}),
|
|
2902
|
+
{ debugOnly: true },
|
|
2903
|
+
);
|
|
2152
2904
|
await this.sleepMs(150 * attempt);
|
|
2153
2905
|
}
|
|
2154
2906
|
}
|
|
@@ -2158,7 +2910,7 @@ class BncrBridgeRuntime {
|
|
|
2158
2910
|
st.error = String((lastErr as any)?.message || lastErr || `chunk-${idx}-failed`);
|
|
2159
2911
|
this.fileSendTransfers.set(transferId, st);
|
|
2160
2912
|
ctx.broadcastToConnIds(
|
|
2161
|
-
|
|
2913
|
+
BNCR_FILE_ABORT_EVENT,
|
|
2162
2914
|
{
|
|
2163
2915
|
transferId,
|
|
2164
2916
|
reason: st.error,
|
|
@@ -2171,7 +2923,7 @@ class BncrBridgeRuntime {
|
|
|
2171
2923
|
}
|
|
2172
2924
|
|
|
2173
2925
|
ctx.broadcastToConnIds(
|
|
2174
|
-
|
|
2926
|
+
BNCR_FILE_COMPLETE_EVENT,
|
|
2175
2927
|
{
|
|
2176
2928
|
transferId,
|
|
2177
2929
|
ts: now(),
|
|
@@ -2179,8 +2931,30 @@ class BncrBridgeRuntime {
|
|
|
2179
2931
|
connIds,
|
|
2180
2932
|
);
|
|
2181
2933
|
|
|
2934
|
+
this.logInfo(
|
|
2935
|
+
'file-transfer-complete-send',
|
|
2936
|
+
JSON.stringify({
|
|
2937
|
+
bridge: this.bridgeId,
|
|
2938
|
+
transferId,
|
|
2939
|
+
accountId,
|
|
2940
|
+
connIds: Array.from(connIds),
|
|
2941
|
+
}),
|
|
2942
|
+
{ debugOnly: true },
|
|
2943
|
+
);
|
|
2944
|
+
|
|
2182
2945
|
const done = await this.waitCompleteAck({ transferId, timeoutMs: 60_000 });
|
|
2183
2946
|
|
|
2947
|
+
this.logInfo(
|
|
2948
|
+
'file-transfer-complete-ack',
|
|
2949
|
+
JSON.stringify({
|
|
2950
|
+
bridge: this.bridgeId,
|
|
2951
|
+
transferId,
|
|
2952
|
+
accountId,
|
|
2953
|
+
payload: done,
|
|
2954
|
+
}),
|
|
2955
|
+
{ debugOnly: true },
|
|
2956
|
+
);
|
|
2957
|
+
|
|
2184
2958
|
return {
|
|
2185
2959
|
mode: 'chunk',
|
|
2186
2960
|
mimeType,
|
|
@@ -2215,44 +2989,20 @@ class BncrBridgeRuntime {
|
|
|
2215
2989
|
if (mediaList.length > 0) {
|
|
2216
2990
|
let first = true;
|
|
2217
2991
|
for (const mediaUrl of mediaList) {
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
mediaLocalRoots,
|
|
2224
|
-
});
|
|
2225
|
-
const messageId = randomUUID();
|
|
2226
|
-
const mediaMsg = first ? asString(payload.text || '') : '';
|
|
2227
|
-
const wantsVoice = payload.asVoice === true || payload.audioAsVoice === true;
|
|
2228
|
-
const frame = buildBncrMediaOutboundFrame({
|
|
2229
|
-
messageId,
|
|
2230
|
-
sessionKey,
|
|
2231
|
-
route,
|
|
2232
|
-
media,
|
|
2233
|
-
mediaUrl,
|
|
2234
|
-
mediaMsg,
|
|
2235
|
-
fileName: resolveOutboundFileName({
|
|
2992
|
+
this.enqueueOutbound(
|
|
2993
|
+
this.buildFileTransferOutboxEntry({
|
|
2994
|
+
accountId,
|
|
2995
|
+
sessionKey,
|
|
2996
|
+
route,
|
|
2236
2997
|
mediaUrl,
|
|
2237
|
-
|
|
2238
|
-
|
|
2998
|
+
mediaLocalRoots,
|
|
2999
|
+
text: first ? asString(payload.text || '') : '',
|
|
3000
|
+
asVoice: payload.asVoice,
|
|
3001
|
+
audioAsVoice: payload.audioAsVoice,
|
|
3002
|
+
kind: payload.kind,
|
|
3003
|
+
replyToId: asString(payload.replyToId || '').trim() || undefined,
|
|
2239
3004
|
}),
|
|
2240
|
-
|
|
2241
|
-
kind: payload.kind,
|
|
2242
|
-
replyToId: asString(payload.replyToId || '').trim() || undefined,
|
|
2243
|
-
now: now(),
|
|
2244
|
-
});
|
|
2245
|
-
|
|
2246
|
-
this.enqueueOutbound({
|
|
2247
|
-
messageId,
|
|
2248
|
-
accountId: normalizeAccountId(accountId),
|
|
2249
|
-
sessionKey,
|
|
2250
|
-
route,
|
|
2251
|
-
payload: frame,
|
|
2252
|
-
createdAt: now(),
|
|
2253
|
-
retryCount: 0,
|
|
2254
|
-
nextAttemptAt: now(),
|
|
2255
|
-
});
|
|
3005
|
+
);
|
|
2256
3006
|
first = false;
|
|
2257
3007
|
}
|
|
2258
3008
|
return;
|
|
@@ -2329,6 +3079,11 @@ class BncrBridgeRuntime {
|
|
|
2329
3079
|
pending: Array.from(this.outbox.values()).filter((v) => v.accountId === accountId).length,
|
|
2330
3080
|
deadLetter: this.deadLetter.filter((v) => v.accountId === accountId).length,
|
|
2331
3081
|
diagnostics: this.buildExtendedDiagnostics(accountId),
|
|
3082
|
+
runtimeFlags: this.buildRuntimeFlags(accountId),
|
|
3083
|
+
waiters: {
|
|
3084
|
+
messageAck: this.messageAckWaiters.size,
|
|
3085
|
+
fileAck: this.fileAckWaiters.size,
|
|
3086
|
+
},
|
|
2332
3087
|
leaseId: lease.leaseId,
|
|
2333
3088
|
connectionEpoch: lease.connectionEpoch,
|
|
2334
3089
|
protocolVersion: 2,
|
|
@@ -2347,13 +3102,9 @@ class BncrBridgeRuntime {
|
|
|
2347
3102
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
2348
3103
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
2349
3104
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
2350
|
-
this.rememberGatewayContext(context);
|
|
2351
|
-
this.markSeen(accountId, connId, clientId);
|
|
2352
|
-
this.observeLease('ack', params ?? {});
|
|
2353
|
-
this.lastAckAtGlobal = now();
|
|
2354
|
-
this.incrementCounter(this.ackEventsByAccount, accountId);
|
|
2355
|
-
|
|
2356
3105
|
const messageId = asString(params?.messageId || '').trim();
|
|
3106
|
+
const staleObserved = this.observeLease('ack', params ?? {});
|
|
3107
|
+
|
|
2357
3108
|
this.logInfo(
|
|
2358
3109
|
'outbox',
|
|
2359
3110
|
`ack ${JSON.stringify({
|
|
@@ -2362,6 +3113,7 @@ class BncrBridgeRuntime {
|
|
|
2362
3113
|
ok: params?.ok !== false,
|
|
2363
3114
|
fatal: params?.fatal === true,
|
|
2364
3115
|
error: asString(params?.error || ''),
|
|
3116
|
+
stale: staleObserved.stale,
|
|
2365
3117
|
})}`,
|
|
2366
3118
|
{ debugOnly: true },
|
|
2367
3119
|
);
|
|
@@ -2372,7 +3124,7 @@ class BncrBridgeRuntime {
|
|
|
2372
3124
|
|
|
2373
3125
|
const entry = this.outbox.get(messageId);
|
|
2374
3126
|
if (!entry) {
|
|
2375
|
-
respond(true, { ok: true, message: 'already-acked-or-missing' });
|
|
3127
|
+
respond(true, { ok: true, message: 'already-acked-or-missing', stale: staleObserved.stale });
|
|
2376
3128
|
return;
|
|
2377
3129
|
}
|
|
2378
3130
|
|
|
@@ -2381,20 +3133,52 @@ class BncrBridgeRuntime {
|
|
|
2381
3133
|
return;
|
|
2382
3134
|
}
|
|
2383
3135
|
|
|
3136
|
+
if (staleObserved.stale) {
|
|
3137
|
+
const sameConn = !!entry.lastPushConnId && entry.lastPushConnId === connId;
|
|
3138
|
+
const sameClient =
|
|
3139
|
+
!entry.lastPushConnId &&
|
|
3140
|
+
!!entry.lastPushClientId &&
|
|
3141
|
+
!!clientId &&
|
|
3142
|
+
entry.lastPushClientId === clientId;
|
|
3143
|
+
if (!(sameConn || sameClient)) {
|
|
3144
|
+
this.logWarn(
|
|
3145
|
+
'stale',
|
|
3146
|
+
`ignore kind=ack accountId=${accountId} connId=${connId} clientId=${clientId || '-'} messageId=${messageId} reason=owner-mismatch lastPushConnId=${entry.lastPushConnId || '-'} lastPushClientId=${entry.lastPushClientId || '-'}`,
|
|
3147
|
+
{ debugOnly: true },
|
|
3148
|
+
);
|
|
3149
|
+
respond(true, { ok: true, stale: true, ignored: true });
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
} else {
|
|
3153
|
+
this.rememberGatewayContext(context);
|
|
3154
|
+
this.markSeen(accountId, connId, clientId);
|
|
3155
|
+
}
|
|
3156
|
+
this.lastAckAtGlobal = now();
|
|
3157
|
+
this.incrementCounter(this.ackEventsByAccount, accountId);
|
|
3158
|
+
|
|
2384
3159
|
const ok = params?.ok !== false;
|
|
2385
3160
|
const fatal = params?.fatal === true;
|
|
2386
3161
|
|
|
2387
3162
|
if (ok) {
|
|
2388
3163
|
this.outbox.delete(messageId);
|
|
2389
3164
|
this.scheduleSave();
|
|
2390
|
-
this.
|
|
2391
|
-
respond(
|
|
3165
|
+
this.resolveMessageAck(messageId, 'acked');
|
|
3166
|
+
respond(
|
|
3167
|
+
true,
|
|
3168
|
+
staleObserved.stale ? { ok: true, stale: true, staleAccepted: true } : { ok: true },
|
|
3169
|
+
);
|
|
3170
|
+
this.flushPushQueue(accountId);
|
|
2392
3171
|
return;
|
|
2393
3172
|
}
|
|
2394
3173
|
|
|
2395
3174
|
if (fatal) {
|
|
2396
3175
|
this.moveToDeadLetter(entry, asString(params?.error || 'fatal-ack'));
|
|
2397
|
-
respond(
|
|
3176
|
+
respond(
|
|
3177
|
+
true,
|
|
3178
|
+
staleObserved.stale
|
|
3179
|
+
? { ok: true, movedToDeadLetter: true, stale: true, staleAccepted: true }
|
|
3180
|
+
: { ok: true, movedToDeadLetter: true },
|
|
3181
|
+
);
|
|
2398
3182
|
return;
|
|
2399
3183
|
}
|
|
2400
3184
|
|
|
@@ -2403,7 +3187,12 @@ class BncrBridgeRuntime {
|
|
|
2403
3187
|
this.outbox.set(messageId, entry);
|
|
2404
3188
|
this.scheduleSave();
|
|
2405
3189
|
|
|
2406
|
-
respond(
|
|
3190
|
+
respond(
|
|
3191
|
+
true,
|
|
3192
|
+
staleObserved.stale
|
|
3193
|
+
? { ok: true, willRetry: true, stale: true, staleAccepted: true }
|
|
3194
|
+
: { ok: true, willRetry: true },
|
|
3195
|
+
);
|
|
2407
3196
|
};
|
|
2408
3197
|
|
|
2409
3198
|
handleActivity = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
|
|
@@ -2411,7 +3200,18 @@ class BncrBridgeRuntime {
|
|
|
2411
3200
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
2412
3201
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
2413
3202
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
2414
|
-
|
|
3203
|
+
if (
|
|
3204
|
+
this.shouldIgnoreStaleEvent({
|
|
3205
|
+
kind: 'activity',
|
|
3206
|
+
payload: params ?? {},
|
|
3207
|
+
accountId,
|
|
3208
|
+
connId,
|
|
3209
|
+
clientId,
|
|
3210
|
+
})
|
|
3211
|
+
) {
|
|
3212
|
+
respond(true, { accountId, ok: true, event: 'activity', stale: true, ignored: true });
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
2415
3215
|
this.lastActivityAtGlobal = now();
|
|
2416
3216
|
this.logInfo(
|
|
2417
3217
|
'activity',
|
|
@@ -2439,11 +3239,12 @@ class BncrBridgeRuntime {
|
|
|
2439
3239
|
deadLetter: this.deadLetter.filter((v) => v.accountId === accountId).length,
|
|
2440
3240
|
now: now(),
|
|
2441
3241
|
});
|
|
3242
|
+
this.flushPushQueue(accountId);
|
|
2442
3243
|
};
|
|
2443
3244
|
|
|
2444
3245
|
handleDiagnostics = async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
2445
3246
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
2446
|
-
const cfg =
|
|
3247
|
+
const cfg = this.api.runtime.config.current();
|
|
2447
3248
|
const runtime = this.getAccountRuntimeSnapshot(accountId);
|
|
2448
3249
|
const diagnostics = this.buildExtendedDiagnostics(accountId);
|
|
2449
3250
|
const permissions = buildBncrPermissionSummary(cfg ?? {});
|
|
@@ -2468,6 +3269,11 @@ class BncrBridgeRuntime {
|
|
|
2468
3269
|
accountId,
|
|
2469
3270
|
runtime,
|
|
2470
3271
|
diagnostics,
|
|
3272
|
+
runtimeFlags: this.buildRuntimeFlags(accountId),
|
|
3273
|
+
waiters: {
|
|
3274
|
+
messageAck: this.messageAckWaiters.size,
|
|
3275
|
+
fileAck: this.fileAckWaiters.size,
|
|
3276
|
+
},
|
|
2471
3277
|
permissions,
|
|
2472
3278
|
probe,
|
|
2473
3279
|
now: now(),
|
|
@@ -2478,9 +3284,20 @@ class BncrBridgeRuntime {
|
|
|
2478
3284
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
2479
3285
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
2480
3286
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
3287
|
+
if (
|
|
3288
|
+
this.shouldIgnoreStaleEvent({
|
|
3289
|
+
kind: 'file.init',
|
|
3290
|
+
payload: params ?? {},
|
|
3291
|
+
accountId,
|
|
3292
|
+
connId,
|
|
3293
|
+
clientId,
|
|
3294
|
+
})
|
|
3295
|
+
) {
|
|
3296
|
+
respond(true, { ok: true, stale: true, ignored: true });
|
|
3297
|
+
return;
|
|
3298
|
+
}
|
|
2481
3299
|
this.rememberGatewayContext(context);
|
|
2482
3300
|
this.markSeen(accountId, connId, clientId);
|
|
2483
|
-
this.observeLease('file.init', params ?? {});
|
|
2484
3301
|
this.markActivity(accountId);
|
|
2485
3302
|
|
|
2486
3303
|
const transferId = asString(params?.transferId || '').trim();
|
|
@@ -2536,6 +3353,8 @@ class BncrBridgeRuntime {
|
|
|
2536
3353
|
status: 'init',
|
|
2537
3354
|
bufferByChunk: new Map(),
|
|
2538
3355
|
receivedChunks: new Set(),
|
|
3356
|
+
ownerConnId: connId,
|
|
3357
|
+
ownerClientId: clientId,
|
|
2539
3358
|
});
|
|
2540
3359
|
|
|
2541
3360
|
respond(true, {
|
|
@@ -2549,10 +3368,6 @@ class BncrBridgeRuntime {
|
|
|
2549
3368
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
2550
3369
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
2551
3370
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
2552
|
-
this.rememberGatewayContext(context);
|
|
2553
|
-
this.markSeen(accountId, connId, clientId);
|
|
2554
|
-
this.observeLease('file.chunk', params ?? {});
|
|
2555
|
-
this.markActivity(accountId);
|
|
2556
3371
|
|
|
2557
3372
|
const transferId = asString(params?.transferId || '').trim();
|
|
2558
3373
|
const chunkIndex = Number(params?.chunkIndex ?? -1);
|
|
@@ -2572,6 +3387,30 @@ class BncrBridgeRuntime {
|
|
|
2572
3387
|
return;
|
|
2573
3388
|
}
|
|
2574
3389
|
|
|
3390
|
+
const staleObserved = this.observeLease('file.chunk', params ?? {});
|
|
3391
|
+
if (staleObserved.stale) {
|
|
3392
|
+
if (
|
|
3393
|
+
!this.matchesTransferOwner({
|
|
3394
|
+
ownerConnId: st.ownerConnId,
|
|
3395
|
+
ownerClientId: st.ownerClientId,
|
|
3396
|
+
connId,
|
|
3397
|
+
clientId,
|
|
3398
|
+
})
|
|
3399
|
+
) {
|
|
3400
|
+
this.logWarn(
|
|
3401
|
+
'stale',
|
|
3402
|
+
`ignore kind=file.chunk accountId=${accountId} connId=${connId} clientId=${clientId || '-'} transferId=${transferId} reason=owner-mismatch ownerConnId=${st.ownerConnId || '-'} ownerClientId=${st.ownerClientId || '-'}`,
|
|
3403
|
+
{ debugOnly: true },
|
|
3404
|
+
);
|
|
3405
|
+
respond(true, { ok: true, stale: true, ignored: true });
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
} else {
|
|
3409
|
+
this.rememberGatewayContext(context);
|
|
3410
|
+
this.markSeen(accountId, connId, clientId);
|
|
3411
|
+
this.markActivity(accountId);
|
|
3412
|
+
}
|
|
3413
|
+
|
|
2575
3414
|
try {
|
|
2576
3415
|
const buf = Buffer.from(base64, 'base64');
|
|
2577
3416
|
if (size > 0 && buf.length !== size) {
|
|
@@ -2586,14 +3425,28 @@ class BncrBridgeRuntime {
|
|
|
2586
3425
|
st.status = 'transferring';
|
|
2587
3426
|
this.fileRecvTransfers.set(transferId, st);
|
|
2588
3427
|
|
|
2589
|
-
respond(
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
3428
|
+
respond(
|
|
3429
|
+
true,
|
|
3430
|
+
staleObserved.stale
|
|
3431
|
+
? {
|
|
3432
|
+
ok: true,
|
|
3433
|
+
transferId,
|
|
3434
|
+
chunkIndex,
|
|
3435
|
+
offset,
|
|
3436
|
+
received: st.receivedChunks.size,
|
|
3437
|
+
totalChunks: st.totalChunks,
|
|
3438
|
+
stale: true,
|
|
3439
|
+
staleAccepted: true,
|
|
3440
|
+
}
|
|
3441
|
+
: {
|
|
3442
|
+
ok: true,
|
|
3443
|
+
transferId,
|
|
3444
|
+
chunkIndex,
|
|
3445
|
+
offset,
|
|
3446
|
+
received: st.receivedChunks.size,
|
|
3447
|
+
totalChunks: st.totalChunks,
|
|
3448
|
+
},
|
|
3449
|
+
);
|
|
2597
3450
|
} catch (error) {
|
|
2598
3451
|
respond(false, { error: String((error as any)?.message || error || 'chunk invalid') });
|
|
2599
3452
|
}
|
|
@@ -2608,10 +3461,6 @@ class BncrBridgeRuntime {
|
|
|
2608
3461
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
2609
3462
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
2610
3463
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
2611
|
-
this.rememberGatewayContext(context);
|
|
2612
|
-
this.markSeen(accountId, connId, clientId);
|
|
2613
|
-
this.observeLease('file.complete', params ?? {});
|
|
2614
|
-
this.markActivity(accountId);
|
|
2615
3464
|
|
|
2616
3465
|
const transferId = asString(params?.transferId || '').trim();
|
|
2617
3466
|
if (!transferId) {
|
|
@@ -2625,6 +3474,30 @@ class BncrBridgeRuntime {
|
|
|
2625
3474
|
return;
|
|
2626
3475
|
}
|
|
2627
3476
|
|
|
3477
|
+
const staleObserved = this.observeLease('file.complete', params ?? {});
|
|
3478
|
+
if (staleObserved.stale) {
|
|
3479
|
+
if (
|
|
3480
|
+
!this.matchesTransferOwner({
|
|
3481
|
+
ownerConnId: st.ownerConnId,
|
|
3482
|
+
ownerClientId: st.ownerClientId,
|
|
3483
|
+
connId,
|
|
3484
|
+
clientId,
|
|
3485
|
+
})
|
|
3486
|
+
) {
|
|
3487
|
+
this.logWarn(
|
|
3488
|
+
'stale',
|
|
3489
|
+
`ignore kind=file.complete accountId=${accountId} connId=${connId} clientId=${clientId || '-'} transferId=${transferId} reason=owner-mismatch ownerConnId=${st.ownerConnId || '-'} ownerClientId=${st.ownerClientId || '-'}`,
|
|
3490
|
+
{ debugOnly: true },
|
|
3491
|
+
);
|
|
3492
|
+
respond(true, { ok: true, stale: true, ignored: true });
|
|
3493
|
+
return;
|
|
3494
|
+
}
|
|
3495
|
+
} else {
|
|
3496
|
+
this.rememberGatewayContext(context);
|
|
3497
|
+
this.markSeen(accountId, connId, clientId);
|
|
3498
|
+
this.markActivity(accountId);
|
|
3499
|
+
}
|
|
3500
|
+
|
|
2628
3501
|
try {
|
|
2629
3502
|
if (st.receivedChunks.size < st.totalChunks) {
|
|
2630
3503
|
throw new Error(
|
|
@@ -2655,15 +3528,30 @@ class BncrBridgeRuntime {
|
|
|
2655
3528
|
st.status = 'completed';
|
|
2656
3529
|
this.fileRecvTransfers.set(transferId, st);
|
|
2657
3530
|
|
|
2658
|
-
respond(
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
3531
|
+
respond(
|
|
3532
|
+
true,
|
|
3533
|
+
staleObserved.stale
|
|
3534
|
+
? {
|
|
3535
|
+
ok: true,
|
|
3536
|
+
transferId,
|
|
3537
|
+
path: saved.path,
|
|
3538
|
+
size: merged.length,
|
|
3539
|
+
fileName: st.fileName,
|
|
3540
|
+
mimeType: st.mimeType,
|
|
3541
|
+
fileSha256: digest,
|
|
3542
|
+
stale: true,
|
|
3543
|
+
staleAccepted: true,
|
|
3544
|
+
}
|
|
3545
|
+
: {
|
|
3546
|
+
ok: true,
|
|
3547
|
+
transferId,
|
|
3548
|
+
path: saved.path,
|
|
3549
|
+
size: merged.length,
|
|
3550
|
+
fileName: st.fileName,
|
|
3551
|
+
mimeType: st.mimeType,
|
|
3552
|
+
fileSha256: digest,
|
|
3553
|
+
},
|
|
3554
|
+
);
|
|
2667
3555
|
} catch (error) {
|
|
2668
3556
|
st.status = 'aborted';
|
|
2669
3557
|
st.error = String((error as any)?.message || error || 'complete failed');
|
|
@@ -2676,10 +3564,6 @@ class BncrBridgeRuntime {
|
|
|
2676
3564
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
2677
3565
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
2678
3566
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
2679
|
-
this.rememberGatewayContext(context);
|
|
2680
|
-
this.markSeen(accountId, connId, clientId);
|
|
2681
|
-
this.observeLease('file.abort', params ?? {});
|
|
2682
|
-
this.markActivity(accountId);
|
|
2683
3567
|
|
|
2684
3568
|
const transferId = asString(params?.transferId || '').trim();
|
|
2685
3569
|
if (!transferId) {
|
|
@@ -2693,36 +3577,122 @@ class BncrBridgeRuntime {
|
|
|
2693
3577
|
return;
|
|
2694
3578
|
}
|
|
2695
3579
|
|
|
3580
|
+
const staleObserved = this.observeLease('file.abort', params ?? {});
|
|
3581
|
+
if (staleObserved.stale) {
|
|
3582
|
+
if (
|
|
3583
|
+
!this.matchesTransferOwner({
|
|
3584
|
+
ownerConnId: st.ownerConnId,
|
|
3585
|
+
ownerClientId: st.ownerClientId,
|
|
3586
|
+
connId,
|
|
3587
|
+
clientId,
|
|
3588
|
+
})
|
|
3589
|
+
) {
|
|
3590
|
+
this.logWarn(
|
|
3591
|
+
'stale',
|
|
3592
|
+
`ignore kind=file.abort accountId=${accountId} connId=${connId} clientId=${clientId || '-'} transferId=${transferId} reason=owner-mismatch ownerConnId=${st.ownerConnId || '-'} ownerClientId=${st.ownerClientId || '-'}`,
|
|
3593
|
+
{ debugOnly: true },
|
|
3594
|
+
);
|
|
3595
|
+
respond(true, { ok: true, stale: true, ignored: true });
|
|
3596
|
+
return;
|
|
3597
|
+
}
|
|
3598
|
+
} else {
|
|
3599
|
+
this.rememberGatewayContext(context);
|
|
3600
|
+
this.markSeen(accountId, connId, clientId);
|
|
3601
|
+
this.markActivity(accountId);
|
|
3602
|
+
}
|
|
3603
|
+
|
|
2696
3604
|
st.status = 'aborted';
|
|
2697
3605
|
st.error = asString(params?.reason || 'aborted');
|
|
2698
3606
|
this.fileRecvTransfers.set(transferId, st);
|
|
2699
3607
|
|
|
2700
|
-
respond(
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
3608
|
+
respond(
|
|
3609
|
+
true,
|
|
3610
|
+
staleObserved.stale
|
|
3611
|
+
? {
|
|
3612
|
+
ok: true,
|
|
3613
|
+
transferId,
|
|
3614
|
+
status: 'aborted',
|
|
3615
|
+
stale: true,
|
|
3616
|
+
staleAccepted: true,
|
|
3617
|
+
}
|
|
3618
|
+
: {
|
|
3619
|
+
ok: true,
|
|
3620
|
+
transferId,
|
|
3621
|
+
status: 'aborted',
|
|
3622
|
+
},
|
|
3623
|
+
);
|
|
2705
3624
|
};
|
|
2706
3625
|
|
|
2707
3626
|
handleFileAck = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
|
|
2708
3627
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
2709
3628
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
2710
3629
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
2711
|
-
this.rememberGatewayContext(context);
|
|
2712
|
-
this.markSeen(accountId, connId, clientId);
|
|
2713
|
-
this.markActivity(accountId);
|
|
2714
3630
|
|
|
2715
3631
|
const transferId = asString(params?.transferId || '').trim();
|
|
2716
3632
|
const stage = asString(params?.stage || '').trim();
|
|
2717
3633
|
const ok = params?.ok !== false;
|
|
2718
3634
|
const chunkIndex = Number(params?.chunkIndex ?? -1);
|
|
2719
3635
|
|
|
3636
|
+
this.logInfo(
|
|
3637
|
+
'file-ack-inbound',
|
|
3638
|
+
JSON.stringify({
|
|
3639
|
+
bridge: this.bridgeId,
|
|
3640
|
+
accountId,
|
|
3641
|
+
connId,
|
|
3642
|
+
clientId: clientId || null,
|
|
3643
|
+
transferId,
|
|
3644
|
+
stage,
|
|
3645
|
+
ok,
|
|
3646
|
+
chunkIndex: chunkIndex >= 0 ? chunkIndex : undefined,
|
|
3647
|
+
errorCode: asString(params?.errorCode || ''),
|
|
3648
|
+
errorMessage: asString(params?.errorMessage || ''),
|
|
3649
|
+
path: asString(params?.path || '').trim(),
|
|
3650
|
+
}),
|
|
3651
|
+
{ debugOnly: true },
|
|
3652
|
+
);
|
|
3653
|
+
|
|
2720
3654
|
if (!transferId || !stage) {
|
|
2721
3655
|
respond(false, { error: 'transferId/stage required' });
|
|
2722
3656
|
return;
|
|
2723
3657
|
}
|
|
2724
3658
|
|
|
2725
3659
|
const st = this.fileSendTransfers.get(transferId);
|
|
3660
|
+
const staleKind =
|
|
3661
|
+
stage === 'init'
|
|
3662
|
+
? 'file.init'
|
|
3663
|
+
: stage === 'chunk'
|
|
3664
|
+
? 'file.chunk'
|
|
3665
|
+
: stage === 'abort'
|
|
3666
|
+
? 'file.abort'
|
|
3667
|
+
: 'file.complete';
|
|
3668
|
+
const staleObserved = this.observeLease(staleKind, params ?? {});
|
|
3669
|
+
if (staleObserved.stale) {
|
|
3670
|
+
const sameConn = !!st?.ownerConnId && st.ownerConnId === connId;
|
|
3671
|
+
const sameClient =
|
|
3672
|
+
!st?.ownerConnId && !!st?.ownerClientId && !!clientId && st.ownerClientId === clientId;
|
|
3673
|
+
const adopted =
|
|
3674
|
+
!(sameConn || sameClient) &&
|
|
3675
|
+
this.tryAdoptTransferOwner({
|
|
3676
|
+
accountId,
|
|
3677
|
+
transfer: st,
|
|
3678
|
+
connId,
|
|
3679
|
+
clientId,
|
|
3680
|
+
});
|
|
3681
|
+
if (!(sameConn || sameClient || adopted)) {
|
|
3682
|
+
this.logWarn(
|
|
3683
|
+
'stale',
|
|
3684
|
+
`ignore kind=file.ack accountId=${accountId} connId=${connId} clientId=${clientId || '-'} transferId=${transferId} stage=${stage} reason=owner-mismatch ownerConnId=${st?.ownerConnId || '-'} ownerClientId=${st?.ownerClientId || '-'}`,
|
|
3685
|
+
{ debugOnly: true },
|
|
3686
|
+
);
|
|
3687
|
+
respond(true, { ok: true, stale: true, ignored: true });
|
|
3688
|
+
return;
|
|
3689
|
+
}
|
|
3690
|
+
} else {
|
|
3691
|
+
this.rememberGatewayContext(context);
|
|
3692
|
+
this.markSeen(accountId, connId, clientId);
|
|
3693
|
+
this.markActivity(accountId);
|
|
3694
|
+
}
|
|
3695
|
+
|
|
2726
3696
|
if (st) {
|
|
2727
3697
|
if (!ok) {
|
|
2728
3698
|
const code = asString(params?.errorCode || 'ACK_FAILED');
|
|
@@ -2759,12 +3729,24 @@ class BncrBridgeRuntime {
|
|
|
2759
3729
|
ok,
|
|
2760
3730
|
});
|
|
2761
3731
|
|
|
2762
|
-
respond(
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
3732
|
+
respond(
|
|
3733
|
+
true,
|
|
3734
|
+
staleObserved.stale
|
|
3735
|
+
? {
|
|
3736
|
+
ok: true,
|
|
3737
|
+
transferId,
|
|
3738
|
+
stage,
|
|
3739
|
+
state: st?.status || 'late',
|
|
3740
|
+
stale: true,
|
|
3741
|
+
staleAccepted: true,
|
|
3742
|
+
}
|
|
3743
|
+
: {
|
|
3744
|
+
ok: true,
|
|
3745
|
+
transferId,
|
|
3746
|
+
stage,
|
|
3747
|
+
state: st?.status || 'late',
|
|
3748
|
+
},
|
|
3749
|
+
);
|
|
2768
3750
|
};
|
|
2769
3751
|
|
|
2770
3752
|
handleInbound = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
|
|
@@ -2790,10 +3772,49 @@ class BncrBridgeRuntime {
|
|
|
2790
3772
|
} = parsed;
|
|
2791
3773
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
2792
3774
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
3775
|
+
if (
|
|
3776
|
+
this.shouldIgnoreStaleEvent({
|
|
3777
|
+
kind: 'inbound',
|
|
3778
|
+
payload: params ?? {},
|
|
3779
|
+
accountId,
|
|
3780
|
+
connId,
|
|
3781
|
+
clientId,
|
|
3782
|
+
})
|
|
3783
|
+
) {
|
|
3784
|
+
respond(true, {
|
|
3785
|
+
accepted: false,
|
|
3786
|
+
stale: true,
|
|
3787
|
+
ignored: true,
|
|
3788
|
+
accountId,
|
|
3789
|
+
msgId: msgId ?? null,
|
|
3790
|
+
});
|
|
3791
|
+
return;
|
|
3792
|
+
}
|
|
2793
3793
|
this.rememberGatewayContext(context);
|
|
2794
3794
|
this.markSeen(accountId, connId, clientId);
|
|
2795
|
-
this.observeLease('inbound', params ?? {});
|
|
2796
3795
|
this.markActivity(accountId);
|
|
3796
|
+
this.logInfo(
|
|
3797
|
+
'inbound',
|
|
3798
|
+
`lifecycle ${JSON.stringify({
|
|
3799
|
+
stage: 'accepted',
|
|
3800
|
+
bridge: this.bridgeId,
|
|
3801
|
+
accountId,
|
|
3802
|
+
connId,
|
|
3803
|
+
clientId,
|
|
3804
|
+
onlineAfterSeen: this.isOnline(accountId),
|
|
3805
|
+
recentInboundReachable: this.hasRecentInboundReachability(accountId),
|
|
3806
|
+
activeConnectionKey: this.activeConnectionByAccount.get(accountId) || null,
|
|
3807
|
+
activeConnections: Array.from(this.connections.values())
|
|
3808
|
+
.filter((c) => c.accountId === accountId)
|
|
3809
|
+
.map((c) => ({
|
|
3810
|
+
connId: c.connId,
|
|
3811
|
+
clientId: c.clientId,
|
|
3812
|
+
connectedAt: c.connectedAt,
|
|
3813
|
+
lastSeenAt: c.lastSeenAt,
|
|
3814
|
+
})),
|
|
3815
|
+
})}`,
|
|
3816
|
+
{ debugOnly: true },
|
|
3817
|
+
);
|
|
2797
3818
|
this.lastInboundAtGlobal = now();
|
|
2798
3819
|
this.incrementCounter(this.inboundEventsByAccount, accountId);
|
|
2799
3820
|
|
|
@@ -2811,7 +3832,7 @@ class BncrBridgeRuntime {
|
|
|
2811
3832
|
return;
|
|
2812
3833
|
}
|
|
2813
3834
|
|
|
2814
|
-
const cfg =
|
|
3835
|
+
const cfg = this.api.runtime.config.current();
|
|
2815
3836
|
const gate = checkBncrMessageGate({
|
|
2816
3837
|
parsed,
|
|
2817
3838
|
cfg,
|
|
@@ -2876,6 +3897,7 @@ class BncrBridgeRuntime {
|
|
|
2876
3897
|
msgId: msgId ?? null,
|
|
2877
3898
|
taskKey: extracted.taskKey ?? null,
|
|
2878
3899
|
});
|
|
3900
|
+
this.flushPushQueue(accountId);
|
|
2879
3901
|
|
|
2880
3902
|
void dispatchBncrInbound({
|
|
2881
3903
|
api: this.api,
|
|
@@ -2902,11 +3924,43 @@ class BncrBridgeRuntime {
|
|
|
2902
3924
|
|
|
2903
3925
|
channelStartAccount = async (ctx: any) => {
|
|
2904
3926
|
const accountId = normalizeAccountId(ctx.accountId);
|
|
3927
|
+
this.clearChannelAccountWorker(accountId, 'start-replace');
|
|
2905
3928
|
|
|
2906
3929
|
const tick = () => {
|
|
2907
|
-
const connected = this.isOnline(accountId);
|
|
2908
3930
|
const previous = ctx.getStatus?.() || {};
|
|
2909
|
-
const
|
|
3931
|
+
const onlineByConn = this.isOnline(accountId);
|
|
3932
|
+
const recentInboundReachable = this.hasRecentInboundReachability(accountId);
|
|
3933
|
+
const connected = onlineByConn || recentInboundReachable;
|
|
3934
|
+
const lastActAt =
|
|
3935
|
+
this.lastActivityByAccount.get(accountId) ||
|
|
3936
|
+
this.lastInboundByAccount.get(accountId) ||
|
|
3937
|
+
this.lastOutboundByAccount.get(accountId) ||
|
|
3938
|
+
previous?.lastEventAt ||
|
|
3939
|
+
null;
|
|
3940
|
+
this.logInfo(
|
|
3941
|
+
'health',
|
|
3942
|
+
`status-tick ${JSON.stringify({
|
|
3943
|
+
bridge: this.bridgeId,
|
|
3944
|
+
accountId,
|
|
3945
|
+
connected,
|
|
3946
|
+
onlineByConn,
|
|
3947
|
+
recentInboundReachable,
|
|
3948
|
+
lastActivityAt: this.lastActivityByAccount.get(accountId) || null,
|
|
3949
|
+
lastInboundAt: this.lastInboundByAccount.get(accountId) || null,
|
|
3950
|
+
lastOutboundAt: this.lastOutboundByAccount.get(accountId) || null,
|
|
3951
|
+
chosenLastEventAt: lastActAt,
|
|
3952
|
+
activeConnectionKey: this.activeConnectionByAccount.get(accountId) || null,
|
|
3953
|
+
activeConnections: Array.from(this.connections.values())
|
|
3954
|
+
.filter((c) => c.accountId === accountId)
|
|
3955
|
+
.map((c) => ({
|
|
3956
|
+
connId: c.connId,
|
|
3957
|
+
clientId: c.clientId,
|
|
3958
|
+
connectedAt: c.connectedAt,
|
|
3959
|
+
lastSeenAt: c.lastSeenAt,
|
|
3960
|
+
})),
|
|
3961
|
+
})}`,
|
|
3962
|
+
{ debugOnly: true },
|
|
3963
|
+
);
|
|
2910
3964
|
|
|
2911
3965
|
ctx.setStatus?.({
|
|
2912
3966
|
...previous,
|
|
@@ -2923,13 +3977,30 @@ class BncrBridgeRuntime {
|
|
|
2923
3977
|
|
|
2924
3978
|
tick();
|
|
2925
3979
|
const timer = setInterval(tick, 5_000);
|
|
3980
|
+
this.channelAccountTimers.set(accountId, timer);
|
|
2926
3981
|
|
|
2927
3982
|
await new Promise<void>((resolve) => {
|
|
2928
|
-
|
|
2929
|
-
|
|
3983
|
+
let settled = false;
|
|
3984
|
+
const finish = (reason: string) => {
|
|
3985
|
+
if (settled) return;
|
|
3986
|
+
settled = true;
|
|
3987
|
+
const activeTimer = this.channelAccountTimers.get(accountId);
|
|
3988
|
+
if (activeTimer === timer) {
|
|
3989
|
+
clearInterval(timer);
|
|
3990
|
+
this.channelAccountTimers.delete(accountId);
|
|
3991
|
+
} else {
|
|
3992
|
+
clearInterval(timer);
|
|
3993
|
+
}
|
|
3994
|
+
this.logInfo(
|
|
3995
|
+
'health',
|
|
3996
|
+
`status-worker finished ${JSON.stringify({ bridge: this.bridgeId, accountId, reason })}`,
|
|
3997
|
+
{ debugOnly: true },
|
|
3998
|
+
);
|
|
2930
3999
|
resolve();
|
|
2931
4000
|
};
|
|
2932
4001
|
|
|
4002
|
+
const onAbort = () => finish('abort');
|
|
4003
|
+
|
|
2933
4004
|
if (ctx.abortSignal?.aborted) {
|
|
2934
4005
|
onAbort();
|
|
2935
4006
|
return;
|
|
@@ -2939,8 +4010,23 @@ class BncrBridgeRuntime {
|
|
|
2939
4010
|
});
|
|
2940
4011
|
};
|
|
2941
4012
|
|
|
2942
|
-
channelStopAccount = async (
|
|
2943
|
-
|
|
4013
|
+
channelStopAccount = async (ctx: any) => {
|
|
4014
|
+
const accountId = normalizeAccountId(ctx?.accountId);
|
|
4015
|
+
const cleared = this.clearChannelAccountWorker(accountId, 'explicit-stop');
|
|
4016
|
+
const previous = ctx?.getStatus?.() || {};
|
|
4017
|
+
ctx?.setStatus?.({
|
|
4018
|
+
...previous,
|
|
4019
|
+
accountId,
|
|
4020
|
+
running: false,
|
|
4021
|
+
restartPending: false,
|
|
4022
|
+
lastStopAt: Date.now(),
|
|
4023
|
+
meta: this.buildStatusMeta(accountId),
|
|
4024
|
+
});
|
|
4025
|
+
this.logInfo(
|
|
4026
|
+
'health',
|
|
4027
|
+
`status-stop ${JSON.stringify({ bridge: this.bridgeId, accountId, cleared })}`,
|
|
4028
|
+
{ debugOnly: true },
|
|
4029
|
+
);
|
|
2944
4030
|
};
|
|
2945
4031
|
|
|
2946
4032
|
channelSendText = async (ctx: any) => {
|
|
@@ -3034,7 +4120,7 @@ export function createBncrBridge(api: OpenClawPluginApi) {
|
|
|
3034
4120
|
return new BncrBridgeRuntime(api);
|
|
3035
4121
|
}
|
|
3036
4122
|
|
|
3037
|
-
export function createBncrChannelPlugin(
|
|
4123
|
+
export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
|
|
3038
4124
|
const messageActions: ChannelMessageActionAdapter = {
|
|
3039
4125
|
describeMessageTool: ({ cfg }) => {
|
|
3040
4126
|
const channelCfg = cfg?.channels?.[CHANNEL_ID];
|
|
@@ -3046,9 +4132,10 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
3046
4132
|
(accountId) => resolveAccount(cfg, accountId).enabled !== false,
|
|
3047
4133
|
);
|
|
3048
4134
|
|
|
4135
|
+
const runtimeBridge = getBridge();
|
|
3049
4136
|
const hasConnectedRuntime = listAccountIds(cfg).some((accountId) => {
|
|
3050
4137
|
const resolved = resolveAccount(cfg, accountId);
|
|
3051
|
-
const runtime =
|
|
4138
|
+
const runtime = runtimeBridge.getAccountRuntimeSnapshot(resolved.accountId);
|
|
3052
4139
|
return Boolean(runtime?.connected);
|
|
3053
4140
|
});
|
|
3054
4141
|
|
|
@@ -3066,49 +4153,37 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
3066
4153
|
handleAction: async ({ action, params, accountId, mediaLocalRoots }) => {
|
|
3067
4154
|
if (action !== 'send')
|
|
3068
4155
|
throw new Error(`Action ${action} is not supported for provider ${CHANNEL_ID}.`);
|
|
3069
|
-
const
|
|
3070
|
-
const message = readStringParam(params, 'message', { allowEmpty: true }) ?? '';
|
|
3071
|
-
const caption = readStringParam(params, 'caption', { allowEmpty: true }) ?? '';
|
|
3072
|
-
const content = message || caption || '';
|
|
3073
|
-
const mediaUrl =
|
|
3074
|
-
readStringParam(params, 'media', { trim: false }) ??
|
|
3075
|
-
readStringParam(params, 'path', { trim: false }) ??
|
|
3076
|
-
readStringParam(params, 'filePath', { trim: false }) ??
|
|
3077
|
-
readStringParam(params, 'mediaUrl', { trim: false });
|
|
3078
|
-
const asVoice = readBooleanParam(params, 'asVoice') ?? false;
|
|
3079
|
-
const audioAsVoice = readBooleanParam(params, 'audioAsVoice') ?? false;
|
|
3080
|
-
const resolvedAccountId = normalizeAccountId(
|
|
3081
|
-
readStringParam(params, 'accountId') ?? accountId,
|
|
3082
|
-
);
|
|
3083
|
-
|
|
3084
|
-
if (!content.trim() && !mediaUrl) throw new Error('send requires text or media');
|
|
4156
|
+
const normalized = normalizeBncrSendParams({ params, accountId });
|
|
3085
4157
|
|
|
3086
|
-
const
|
|
4158
|
+
const runtimeBridge = getBridge();
|
|
4159
|
+
const result = normalized.mediaUrl
|
|
3087
4160
|
? await sendBncrMedia({
|
|
3088
4161
|
channelId: CHANNEL_ID,
|
|
3089
|
-
accountId:
|
|
3090
|
-
to,
|
|
3091
|
-
text:
|
|
3092
|
-
mediaUrl,
|
|
3093
|
-
asVoice,
|
|
3094
|
-
audioAsVoice,
|
|
4162
|
+
accountId: normalized.accountId,
|
|
4163
|
+
to: normalized.to,
|
|
4164
|
+
text: normalized.caption,
|
|
4165
|
+
mediaUrl: normalized.mediaUrl,
|
|
4166
|
+
asVoice: normalized.asVoice,
|
|
4167
|
+
audioAsVoice: normalized.audioAsVoice,
|
|
3095
4168
|
mediaLocalRoots,
|
|
3096
|
-
resolveVerifiedTarget: (to, accountId) =>
|
|
4169
|
+
resolveVerifiedTarget: (to, accountId) =>
|
|
4170
|
+
runtimeBridge.resolveVerifiedTarget(to, accountId),
|
|
3097
4171
|
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
3098
|
-
|
|
3099
|
-
enqueueFromReply: (args) =>
|
|
4172
|
+
runtimeBridge.rememberSessionRoute(sessionKey, accountId, route),
|
|
4173
|
+
enqueueFromReply: (args) => runtimeBridge.enqueueFromReply(args as any),
|
|
3100
4174
|
createMessageId: () => randomUUID(),
|
|
3101
4175
|
})
|
|
3102
4176
|
: await sendBncrText({
|
|
3103
4177
|
channelId: CHANNEL_ID,
|
|
3104
|
-
accountId:
|
|
3105
|
-
to,
|
|
3106
|
-
text:
|
|
4178
|
+
accountId: normalized.accountId,
|
|
4179
|
+
to: normalized.to,
|
|
4180
|
+
text: normalized.message,
|
|
3107
4181
|
mediaLocalRoots,
|
|
3108
|
-
resolveVerifiedTarget: (to, accountId) =>
|
|
4182
|
+
resolveVerifiedTarget: (to, accountId) =>
|
|
4183
|
+
runtimeBridge.resolveVerifiedTarget(to, accountId),
|
|
3109
4184
|
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
3110
|
-
|
|
3111
|
-
enqueueFromReply: (args) =>
|
|
4185
|
+
runtimeBridge.rememberSessionRoute(sessionKey, accountId, route),
|
|
4186
|
+
enqueueFromReply: (args) => runtimeBridge.enqueueFromReply(args as any),
|
|
3112
4187
|
createMessageId: () => randomUUID(),
|
|
3113
4188
|
});
|
|
3114
4189
|
|
|
@@ -3143,9 +4218,10 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
3143
4218
|
const resolvedAccountId = normalizeAccountId(
|
|
3144
4219
|
asString(accountId || BNCR_DEFAULT_ACCOUNT_ID),
|
|
3145
4220
|
);
|
|
4221
|
+
const runtimeBridge = getBridge();
|
|
3146
4222
|
const canonicalAgentId =
|
|
3147
|
-
|
|
3148
|
-
|
|
4223
|
+
runtimeBridge.canonicalAgentId ||
|
|
4224
|
+
runtimeBridge.ensureCanonicalAgentId({ cfg, accountId: resolvedAccountId });
|
|
3149
4225
|
return parseExplicitTarget(asString(raw).trim(), { canonicalAgentId });
|
|
3150
4226
|
},
|
|
3151
4227
|
formatTargetDisplay: ({ target }: any) => {
|
|
@@ -3157,13 +4233,14 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
3157
4233
|
const resolvedAccountId = normalizeAccountId(
|
|
3158
4234
|
asString(accountId || BNCR_DEFAULT_ACCOUNT_ID),
|
|
3159
4235
|
);
|
|
4236
|
+
const runtimeBridge = getBridge();
|
|
3160
4237
|
const canonicalAgentId =
|
|
3161
|
-
|
|
3162
|
-
|
|
4238
|
+
runtimeBridge.canonicalAgentId ||
|
|
4239
|
+
runtimeBridge.ensureCanonicalAgentId({ cfg, accountId: resolvedAccountId });
|
|
3163
4240
|
|
|
3164
4241
|
let parsed = parseExplicitTarget(raw, { canonicalAgentId });
|
|
3165
4242
|
if (!parsed) {
|
|
3166
|
-
const route =
|
|
4243
|
+
const route = runtimeBridge.resolveRouteBySession(raw, resolvedAccountId);
|
|
3167
4244
|
if (route) {
|
|
3168
4245
|
parsed = parseExplicitTarget(formatDisplayScope(route), { canonicalAgentId });
|
|
3169
4246
|
}
|
|
@@ -3174,13 +4251,15 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
3174
4251
|
const accountId = normalizeAccountId(
|
|
3175
4252
|
asString(params?.accountId || BNCR_DEFAULT_ACCOUNT_ID),
|
|
3176
4253
|
);
|
|
4254
|
+
const runtimeBridge = getBridge();
|
|
3177
4255
|
const canonicalAgentId =
|
|
3178
|
-
|
|
4256
|
+
runtimeBridge.canonicalAgentId ||
|
|
4257
|
+
runtimeBridge.ensureCanonicalAgentId({ cfg: params?.cfg, accountId });
|
|
3179
4258
|
return resolveBncrOutboundSessionRoute({
|
|
3180
4259
|
...params,
|
|
3181
4260
|
canonicalAgentId,
|
|
3182
4261
|
resolveRouteBySession: (raw: string, acc: string) =>
|
|
3183
|
-
|
|
4262
|
+
runtimeBridge.resolveRouteBySession(raw, acc),
|
|
3184
4263
|
});
|
|
3185
4264
|
},
|
|
3186
4265
|
targetResolver: {
|
|
@@ -3188,11 +4267,12 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
3188
4267
|
return looksLikeBncrExplicitTarget(asString(normalized || raw).trim());
|
|
3189
4268
|
},
|
|
3190
4269
|
resolveTarget: async ({ accountId, input, normalized }) => {
|
|
4270
|
+
const runtimeBridge = getBridge();
|
|
3191
4271
|
const resolved = resolveBncrOutboundTarget({
|
|
3192
4272
|
target: asString(normalized || input).trim(),
|
|
3193
4273
|
accountId: normalizeAccountId(asString(accountId || BNCR_DEFAULT_ACCOUNT_ID)),
|
|
3194
4274
|
resolveRouteBySession: (raw: string, acc: string) =>
|
|
3195
|
-
|
|
4275
|
+
runtimeBridge.resolveRouteBySession(raw, acc),
|
|
3196
4276
|
});
|
|
3197
4277
|
if (!resolved) return null;
|
|
3198
4278
|
return {
|
|
@@ -3255,8 +4335,8 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
3255
4335
|
},
|
|
3256
4336
|
outbound: {
|
|
3257
4337
|
deliveryMode: 'gateway' as const,
|
|
3258
|
-
sendText:
|
|
3259
|
-
sendMedia:
|
|
4338
|
+
sendText: async (ctx: any) => getBridge().channelSendText(ctx),
|
|
4339
|
+
sendMedia: async (ctx: any) => getBridge().channelSendMedia(ctx),
|
|
3260
4340
|
replyAction: async (ctx: any) =>
|
|
3261
4341
|
sendBncrReplyAction({
|
|
3262
4342
|
accountId: normalizeAccountId(ctx?.accountId),
|
|
@@ -3265,7 +4345,7 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
3265
4345
|
replyToMessageId:
|
|
3266
4346
|
asString(ctx?.replyToId || ctx?.replyToMessageId || '').trim() || undefined,
|
|
3267
4347
|
sendText: async ({ accountId, to, text }) =>
|
|
3268
|
-
|
|
4348
|
+
getBridge().channelSendText({ accountId, to, text }),
|
|
3269
4349
|
}),
|
|
3270
4350
|
deleteAction: async (ctx: any) =>
|
|
3271
4351
|
deleteBncrMessageAction({
|
|
@@ -3290,10 +4370,11 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
3290
4370
|
mode: 'ws-offline',
|
|
3291
4371
|
}),
|
|
3292
4372
|
buildChannelSummary: async ({ defaultAccountId }: any) => {
|
|
3293
|
-
return
|
|
4373
|
+
return getBridge().getChannelSummary(defaultAccountId || BNCR_DEFAULT_ACCOUNT_ID);
|
|
3294
4374
|
},
|
|
3295
4375
|
buildAccountSnapshot: async ({ account, runtime }: any) => {
|
|
3296
|
-
const
|
|
4376
|
+
const runtimeBridge = getBridge();
|
|
4377
|
+
const rt = runtime || runtimeBridge.getAccountRuntimeSnapshot(account?.accountId);
|
|
3297
4378
|
const meta = rt?.meta || {};
|
|
3298
4379
|
|
|
3299
4380
|
const pending = Number(rt?.pending ?? meta.pending ?? 0);
|
|
@@ -3328,7 +4409,7 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
3328
4409
|
mode: normalizedMode,
|
|
3329
4410
|
pending,
|
|
3330
4411
|
deadLetter,
|
|
3331
|
-
healthSummary:
|
|
4412
|
+
healthSummary: runtimeBridge.getStatusHeadline(account?.accountId),
|
|
3332
4413
|
lastSessionKey,
|
|
3333
4414
|
lastSessionScope,
|
|
3334
4415
|
lastSessionAt,
|
|
@@ -3346,7 +4427,7 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
3346
4427
|
if (!enabled) return 'disabled';
|
|
3347
4428
|
const resolved = resolveAccount(cfg, account?.accountId);
|
|
3348
4429
|
if (!(resolved.enabled && configured)) return 'not configured';
|
|
3349
|
-
const rt = runtime ||
|
|
4430
|
+
const rt = runtime || getBridge().getAccountRuntimeSnapshot(account?.accountId);
|
|
3350
4431
|
return rt?.connected ? 'linked' : 'configured';
|
|
3351
4432
|
},
|
|
3352
4433
|
},
|
|
@@ -3363,8 +4444,8 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
3363
4444
|
'bncr.file.ack',
|
|
3364
4445
|
],
|
|
3365
4446
|
gateway: {
|
|
3366
|
-
startAccount:
|
|
3367
|
-
stopAccount:
|
|
4447
|
+
startAccount: async (ctx: any) => getBridge().channelStartAccount(ctx),
|
|
4448
|
+
stopAccount: async (ctx: any) => getBridge().channelStopAccount(ctx),
|
|
3368
4449
|
},
|
|
3369
4450
|
};
|
|
3370
4451
|
|