@xmoxmo/bncr 0.5.7 → 0.5.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/package.json +1 -1
- package/scripts/check-pack.mjs +0 -2
- package/scripts/selfcheck.mjs +0 -3
- package/src/bootstrap/runtime-loader.ts +1 -1
- package/src/channel.ts +49 -138
- package/src/core/connection-reachability.ts +1 -1
- package/src/core/logging.ts +1 -1
- package/src/core/outbox-entry-builders.ts +60 -82
- package/src/core/outbox-summary.ts +5 -39
- package/src/core/permissions.ts +1 -1
- package/src/core/register-trace.ts +2 -2
- package/src/messaging/inbound/parse.ts +1 -1
- package/src/messaging/outbound/diagnostics-debug-builders.ts +0 -15
- package/src/messaging/outbound/diagnostics.ts +0 -1
- package/src/messaging/outbound/marker-parser.ts +215 -0
- package/src/messaging/outbound/media-dedupe.ts +1 -4
- package/src/messaging/outbound/normalize-outbound-send.ts +9 -7
- package/src/messaging/outbound/reply-enqueue-media.ts +1 -184
- package/src/messaging/outbound/reply-enqueue.ts +249 -162
- package/src/openclaw/media-runtime.ts +1 -1
- package/src/plugin/bridge-media-facade.ts +0 -3
- package/src/plugin/bridge-outbox-facade.ts +0 -1
- package/src/plugin/channel-runtime-builders-delivery.ts +2 -362
- package/src/plugin/channel-runtime-builders-status.ts +0 -160
- package/src/plugin/channel-runtime-builders.ts +3 -17
- package/src/plugin/channel-runtime-constants.ts +1 -1
- package/src/plugin/channel-send.ts +5 -4
- package/src/plugin/file-transfer-orchestrator.ts +39 -2
- package/src/plugin/media-orchestrators-runtime-group.ts +20 -149
- package/src/plugin/messaging.ts +3 -3
- package/src/plugin/outbox-push.ts +1 -52
- package/src/plugin/outbox-unified-push-flow.ts +307 -0
- package/src/plugin/scene-registry.ts +1 -1
- package/src/runtime/log-dedupe.ts +3 -3
- package/src/runtime/outbound-ack-timeout.ts +2 -4
- package/src/runtime/status-snapshots.ts +2 -2
- package/src/runtime/status-worker.ts +2 -6
- package/src/core/outbox-text-push-prep.ts +0 -1
- package/src/plugin/bridge-file-transfer-push-facade.ts +0 -142
- package/src/plugin/outbox-file-push-flow.ts +0 -69
- package/src/plugin/outbox-text-push-flow.ts +0 -90
|
@@ -193,7 +193,38 @@ export function createBncrFileTransferOrchestrator(runtime: BncrFileTransferOrch
|
|
|
193
193
|
};
|
|
194
194
|
const HTTP_URL_RE = /^https?:\/\//i;
|
|
195
195
|
|
|
196
|
-
|
|
196
|
+
const EXT_TO_MIME: Record<string, string> = {
|
|
197
|
+
jpg: 'image/jpeg',
|
|
198
|
+
jpeg: 'image/jpeg',
|
|
199
|
+
png: 'image/png',
|
|
200
|
+
gif: 'image/gif',
|
|
201
|
+
webp: 'image/webp',
|
|
202
|
+
bmp: 'image/bmp',
|
|
203
|
+
heic: 'image/heic',
|
|
204
|
+
heif: 'image/heif',
|
|
205
|
+
mp4: 'video/mp4',
|
|
206
|
+
mov: 'video/quicktime',
|
|
207
|
+
mkv: 'video/x-matroska',
|
|
208
|
+
webm: 'video/webm',
|
|
209
|
+
avi: 'video/x-msvideo',
|
|
210
|
+
m4v: 'video/x-m4v',
|
|
211
|
+
mp3: 'audio/mpeg',
|
|
212
|
+
wav: 'audio/wav',
|
|
213
|
+
ogg: 'audio/ogg',
|
|
214
|
+
oga: 'audio/ogg',
|
|
215
|
+
opus: 'audio/opus',
|
|
216
|
+
m4a: 'audio/mp4',
|
|
217
|
+
aac: 'audio/aac',
|
|
218
|
+
flac: 'audio/flac',
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
function mimeFromUrlExt(url: string): string | undefined {
|
|
222
|
+
const clean = url.split(/[?#]/, 1)[0] || '';
|
|
223
|
+
const ext = clean.includes('.') ? clean.slice(clean.lastIndexOf('.') + 1).toLowerCase() : '';
|
|
224
|
+
return EXT_TO_MIME[ext];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Try HEAD for Content-Type; fall back to Range+sniff for magic bytes; then URL extension. */
|
|
197
228
|
async function resolveRemoteMediaType(url: string): Promise<{ mimeType?: string }> {
|
|
198
229
|
// 1) HEAD -> Content-Type
|
|
199
230
|
let headerType = '';
|
|
@@ -229,8 +260,14 @@ export function createBncrFileTransferOrchestrator(runtime: BncrFileTransferOrch
|
|
|
229
260
|
if (!sniffed) return {};
|
|
230
261
|
return { mimeType: sniffed };
|
|
231
262
|
} catch {
|
|
232
|
-
|
|
263
|
+
/* Range sniff failed, fall through to extension inference */
|
|
233
264
|
}
|
|
265
|
+
|
|
266
|
+
// 3) URL extension -> mime type (fallback when HEAD and Range both fail)
|
|
267
|
+
const extMime = mimeFromUrlExt(url);
|
|
268
|
+
if (extMime) return { mimeType: extMime };
|
|
269
|
+
|
|
270
|
+
return {};
|
|
234
271
|
}
|
|
235
272
|
|
|
236
273
|
/** Minimal magic-byte MIME sniffer for the first ~512 bytes. */
|
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
import type { BncrRoute, FileSendTransferState, OutboxEntry } from '../core/types.ts';
|
|
2
|
-
import { normalizeMessageText } from '../messaging/outbound/media-dedupe.ts';
|
|
3
2
|
import type {
|
|
4
3
|
NormalizedReplyPayload,
|
|
5
4
|
OutboundReplyTargetPolicy,
|
|
6
|
-
ReplyMediaEntriesParams,
|
|
7
5
|
ReplyPayloadInput,
|
|
8
6
|
} from '../messaging/outbound/reply-enqueue.ts';
|
|
9
7
|
import {
|
|
10
8
|
enqueueNormalizedReplyPayload,
|
|
11
|
-
enqueueReplyTextEntry,
|
|
12
9
|
normalizeReplyPayload,
|
|
13
10
|
} from '../messaging/outbound/reply-enqueue.ts';
|
|
14
|
-
import {
|
|
15
|
-
enqueueReplyMediaFallbackTextEntry,
|
|
16
|
-
enqueueReplyMediaFileTransferEntry,
|
|
17
|
-
enqueueSingleReplyMediaEntry,
|
|
18
|
-
} from '../messaging/outbound/reply-enqueue-media.ts';
|
|
19
11
|
import type { createBncrFileAckRuntime } from './file-ack-runtime.ts';
|
|
20
12
|
import { createBncrFileTransferOrchestrator } from './file-transfer-orchestrator.ts';
|
|
21
13
|
import type { createBncrFileTransferSetup } from './file-transfer-setup.ts';
|
|
@@ -27,92 +19,12 @@ type FileAckPayload = Awaited<
|
|
|
27
19
|
ReturnType<ReturnType<typeof createBncrFileAckRuntime>['waitForFileAck']>
|
|
28
20
|
>;
|
|
29
21
|
|
|
30
|
-
function
|
|
31
|
-
logInfo: (scope: string | undefined, message: string, options?: { debugOnly?: boolean }) => void;
|
|
32
|
-
enqueueOutbound: (entry: OutboxEntry) => void;
|
|
33
|
-
buildTextOutboxEntry: (args: {
|
|
34
|
-
accountId: string;
|
|
35
|
-
sessionKey: string;
|
|
36
|
-
route: BncrRoute;
|
|
37
|
-
text: string;
|
|
38
|
-
kind?: 'tool' | 'block' | 'final';
|
|
39
|
-
replyToId?: string;
|
|
40
|
-
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
41
|
-
}) => OutboxEntry;
|
|
42
|
-
buildFileTransferOutboxEntry: (args: {
|
|
43
|
-
accountId: string;
|
|
44
|
-
sessionKey: string;
|
|
45
|
-
route: BncrRoute;
|
|
46
|
-
mediaUrl: string;
|
|
47
|
-
mediaLocalRoots?: readonly string[];
|
|
48
|
-
text: string;
|
|
49
|
-
asVoice: boolean;
|
|
50
|
-
audioAsVoice: boolean;
|
|
51
|
-
type?: string;
|
|
52
|
-
extra?: Record<string, unknown>;
|
|
53
|
-
kind?: 'tool' | 'block' | 'final';
|
|
54
|
-
replyToId?: string;
|
|
55
|
-
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
56
|
-
downloadMedia?: boolean;
|
|
57
|
-
}) => OutboxEntry;
|
|
58
|
-
rememberRecentMediaSend: (args: {
|
|
59
|
-
sessionKey: string;
|
|
60
|
-
mediaUrl: string;
|
|
61
|
-
text: string;
|
|
62
|
-
replyToId: string;
|
|
63
|
-
createdAt: number;
|
|
64
|
-
}) => void;
|
|
65
|
-
}) {
|
|
66
|
-
return {
|
|
67
|
-
enqueueReplyMediaFallbackTextEntry: (
|
|
68
|
-
params: Parameters<typeof enqueueReplyMediaFallbackTextEntry>[0],
|
|
69
|
-
) =>
|
|
70
|
-
enqueueReplyMediaFallbackTextEntry(params, {
|
|
71
|
-
logInfo: runtime.logInfo,
|
|
72
|
-
enqueueOutbound: runtime.enqueueOutbound,
|
|
73
|
-
buildTextOutboxEntry: runtime.buildTextOutboxEntry,
|
|
74
|
-
}),
|
|
75
|
-
enqueueReplyMediaFileTransferEntry: (
|
|
76
|
-
params: Parameters<typeof enqueueReplyMediaFileTransferEntry>[0],
|
|
77
|
-
) =>
|
|
78
|
-
enqueueReplyMediaFileTransferEntry(params, {
|
|
79
|
-
enqueueOutbound: runtime.enqueueOutbound,
|
|
80
|
-
buildFileTransferOutboxEntry: runtime.buildFileTransferOutboxEntry,
|
|
81
|
-
rememberRecentMediaSend: runtime.rememberRecentMediaSend,
|
|
82
|
-
}),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function buildReplyTextEntryHelper(runtime: {
|
|
87
|
-
enqueueOutbound: (entry: OutboxEntry) => void;
|
|
88
|
-
buildTextOutboxEntry: (args: {
|
|
89
|
-
accountId: string;
|
|
90
|
-
sessionKey: string;
|
|
91
|
-
route: BncrRoute;
|
|
92
|
-
text: string;
|
|
93
|
-
kind?: 'tool' | 'block' | 'final';
|
|
94
|
-
replyToId?: string;
|
|
95
|
-
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
96
|
-
}) => OutboxEntry;
|
|
97
|
-
}) {
|
|
98
|
-
return (params: {
|
|
99
|
-
accountId: string;
|
|
100
|
-
sessionKey: string;
|
|
101
|
-
route: BncrRoute;
|
|
102
|
-
payload: NormalizedReplyPayload;
|
|
103
|
-
}) =>
|
|
104
|
-
enqueueReplyTextEntry(params, {
|
|
105
|
-
enqueueOutbound: runtime.enqueueOutbound,
|
|
106
|
-
buildTextOutboxEntry: runtime.buildTextOutboxEntry,
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function normalizeReplyOrchestratorPayload(args: {
|
|
22
|
+
async function normalizeReplyOrchestratorPayload(args: {
|
|
111
23
|
payload: ReplyPayloadInput;
|
|
112
24
|
asString: (value: unknown, fallback?: string) => string;
|
|
113
25
|
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
114
26
|
}) {
|
|
115
|
-
return normalizeReplyPayload(
|
|
27
|
+
return await normalizeReplyPayload(
|
|
116
28
|
args.payload,
|
|
117
29
|
{ asString: args.asString },
|
|
118
30
|
{
|
|
@@ -186,29 +98,21 @@ export function createBncrMediaOrchestratorsRuntimeGroup(runtime: {
|
|
|
186
98
|
payload: NormalizedReplyPayload;
|
|
187
99
|
}) => void;
|
|
188
100
|
enqueueOutbound: (entry: OutboxEntry) => void;
|
|
189
|
-
|
|
101
|
+
buildOutboxEntry: (args: {
|
|
190
102
|
accountId: string;
|
|
191
103
|
sessionKey: string;
|
|
192
104
|
route: BncrRoute;
|
|
193
|
-
text: string;
|
|
194
|
-
kind?: 'tool' | 'block' | 'final';
|
|
195
|
-
replyToId?: string;
|
|
196
|
-
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
197
|
-
}) => OutboxEntry;
|
|
198
|
-
buildFileTransferOutboxEntry: (args: {
|
|
199
|
-
accountId: string;
|
|
200
|
-
sessionKey: string;
|
|
201
|
-
route: BncrRoute;
|
|
202
|
-
mediaUrl: string;
|
|
203
|
-
mediaLocalRoots?: readonly string[];
|
|
204
|
-
text: string;
|
|
205
|
-
asVoice: boolean;
|
|
206
|
-
audioAsVoice: boolean;
|
|
207
105
|
type?: string;
|
|
208
|
-
|
|
106
|
+
msg: string;
|
|
209
107
|
kind?: 'tool' | 'block' | 'final';
|
|
210
108
|
replyToId?: string;
|
|
211
109
|
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
110
|
+
extra?: Record<string, unknown>;
|
|
111
|
+
transferMode?: 'text' | 'media';
|
|
112
|
+
mediaUrl?: string;
|
|
113
|
+
mediaLocalRoots?: readonly string[];
|
|
114
|
+
asVoice?: boolean;
|
|
115
|
+
audioAsVoice?: boolean;
|
|
212
116
|
downloadMedia?: boolean;
|
|
213
117
|
}) => OutboxEntry;
|
|
214
118
|
rememberRecentMediaSend: (args: {
|
|
@@ -244,40 +148,7 @@ export function createBncrMediaOrchestratorsRuntimeGroup(runtime: {
|
|
|
244
148
|
logFileTransferCompleteAck: runtime.logFileTransferCompleteAck,
|
|
245
149
|
});
|
|
246
150
|
|
|
247
|
-
const
|
|
248
|
-
const replyTextEntryHelper = buildReplyTextEntryHelper(runtime);
|
|
249
|
-
|
|
250
|
-
const enqueueReplyMediaEntries = (params: ReplyMediaEntriesParams) => {
|
|
251
|
-
let first = true;
|
|
252
|
-
const currentTime = runtime.now();
|
|
253
|
-
|
|
254
|
-
for (const mediaUrl of params.payload.mediaList) {
|
|
255
|
-
const normalizedText = normalizeMessageText(first ? params.payload.text : '');
|
|
256
|
-
const fallback = runtime.tryBuildMediaDedupeFallback({
|
|
257
|
-
sessionKey: params.sessionKey,
|
|
258
|
-
mediaUrl,
|
|
259
|
-
text: normalizedText,
|
|
260
|
-
replyToId: params.payload.replyToId,
|
|
261
|
-
currentTime,
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
enqueueSingleReplyMediaEntry(
|
|
265
|
-
{
|
|
266
|
-
params,
|
|
267
|
-
mediaUrl,
|
|
268
|
-
normalizedText,
|
|
269
|
-
text: first ? params.payload.text : '',
|
|
270
|
-
fallback,
|
|
271
|
-
currentTime,
|
|
272
|
-
},
|
|
273
|
-
replyMediaEntryHelpers,
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
first = false;
|
|
277
|
-
}
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
const enqueueFromReply = (params: {
|
|
151
|
+
const enqueueFromReply = async (params: {
|
|
281
152
|
accountId: string;
|
|
282
153
|
sessionKey: string;
|
|
283
154
|
route: BncrRoute;
|
|
@@ -286,7 +157,7 @@ export function createBncrMediaOrchestratorsRuntimeGroup(runtime: {
|
|
|
286
157
|
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
287
158
|
}) => {
|
|
288
159
|
const { accountId, sessionKey, route, payload, mediaLocalRoots, replyTargetPolicy } = params;
|
|
289
|
-
const normalized = normalizeReplyOrchestratorPayload({
|
|
160
|
+
const normalized = await normalizeReplyOrchestratorPayload({
|
|
290
161
|
payload,
|
|
291
162
|
asString: runtime.asString,
|
|
292
163
|
replyTargetPolicy,
|
|
@@ -302,19 +173,19 @@ export function createBncrMediaOrchestratorsRuntimeGroup(runtime: {
|
|
|
302
173
|
},
|
|
303
174
|
{
|
|
304
175
|
logEnqueueFromReply: runtime.logEnqueueFromReply,
|
|
305
|
-
|
|
306
|
-
|
|
176
|
+
enqueueOutbound: runtime.enqueueOutbound,
|
|
177
|
+
buildOutboxEntry: runtime.buildOutboxEntry,
|
|
178
|
+
tryBuildMediaDedupeFallback: runtime.tryBuildMediaDedupeFallback,
|
|
179
|
+
rememberRecentMediaSend: runtime.rememberRecentMediaSend,
|
|
180
|
+
logInfo: runtime.logInfo,
|
|
307
181
|
},
|
|
308
182
|
);
|
|
309
183
|
};
|
|
310
184
|
|
|
311
|
-
const replyMediaOrchestrator = {
|
|
312
|
-
enqueueFromReply,
|
|
313
|
-
enqueueReplyMediaEntries,
|
|
314
|
-
};
|
|
315
|
-
|
|
316
185
|
return {
|
|
317
186
|
fileTransferOrchestrator,
|
|
318
|
-
replyMediaOrchestrator
|
|
187
|
+
replyMediaOrchestrator: {
|
|
188
|
+
enqueueFromReply,
|
|
189
|
+
},
|
|
319
190
|
};
|
|
320
191
|
}
|
package/src/plugin/messaging.ts
CHANGED
|
@@ -67,12 +67,12 @@ function isDisplayTarget(
|
|
|
67
67
|
return Boolean(value && typeof value === 'object');
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
function normalizeBncrMessagingTarget(raw: string) {
|
|
71
71
|
const input = asSanitizedString(raw).trim();
|
|
72
72
|
return input || undefined;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
function formatBncrMessagingTargetDisplay({ target }: BncrMessagingTargetDisplayInput) {
|
|
76
76
|
if (typeof target === 'string') {
|
|
77
77
|
const parsed = parseExplicitTarget(asSanitizedString(target).trim());
|
|
78
78
|
return parsed?.route ? formatBncrHumanDisplay(parsed.route) : asSanitizedString(target).trim();
|
|
@@ -183,7 +183,7 @@ export function createBncrMessagingSurface(getBridge: () => BncrMessagingRuntime
|
|
|
183
183
|
};
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
function createBncrMessagingTargetResolver(getBridge: () => BncrMessagingRuntimeBridge) {
|
|
187
187
|
return {
|
|
188
188
|
looksLikeId: (raw: string, normalized?: string) => {
|
|
189
189
|
return looksLikeBncrExplicitTarget(asSanitizedString(normalized || raw).trim());
|
|
@@ -3,12 +3,7 @@ import {
|
|
|
3
3
|
resolveFileTransferFailureState,
|
|
4
4
|
} from '../core/outbox-file-transfer-failure.ts';
|
|
5
5
|
import { buildTextPushFailureArgs } from '../core/outbox-text-push-failure.ts';
|
|
6
|
-
|
|
7
|
-
buildTextPushBroadcastPayload,
|
|
8
|
-
buildTextPushOkArgs,
|
|
9
|
-
buildTextPushRouteSelectArgs,
|
|
10
|
-
buildTextPushSuccessArgs,
|
|
11
|
-
} from '../core/outbox-text-push-success.ts';
|
|
6
|
+
|
|
12
7
|
import type { BncrConnection, OutboxEntry } from '../core/types.ts';
|
|
13
8
|
import {
|
|
14
9
|
buildOutboxPushOkDebugInfo,
|
|
@@ -137,51 +132,6 @@ export function createBncrOutboxPush(runtime: BncrOutboxPushRuntime) {
|
|
|
137
132
|
);
|
|
138
133
|
};
|
|
139
134
|
|
|
140
|
-
const pushTextSuccessPath = (args: {
|
|
141
|
-
entry: OutboxEntry;
|
|
142
|
-
owner: BncrConnection | null;
|
|
143
|
-
connIds: Iterable<string>;
|
|
144
|
-
recentInboundReachable: boolean;
|
|
145
|
-
routeReason: string;
|
|
146
|
-
ownerConnId?: string;
|
|
147
|
-
}) => {
|
|
148
|
-
runtime.gatewayBroadcastToConnIds(
|
|
149
|
-
runtime.pushEvent,
|
|
150
|
-
buildTextPushBroadcastPayload({
|
|
151
|
-
payload: args.entry.payload,
|
|
152
|
-
messageId: args.entry.messageId,
|
|
153
|
-
}),
|
|
154
|
-
new Set(args.connIds),
|
|
155
|
-
);
|
|
156
|
-
logOutboxRouteSelect(
|
|
157
|
-
buildTextPushRouteSelectArgs({
|
|
158
|
-
entry: args.entry,
|
|
159
|
-
connIds: args.connIds,
|
|
160
|
-
routeReason: args.routeReason,
|
|
161
|
-
recentInboundReachable: args.recentInboundReachable,
|
|
162
|
-
owner: args.owner,
|
|
163
|
-
event: runtime.pushEvent,
|
|
164
|
-
}),
|
|
165
|
-
);
|
|
166
|
-
runtime.recordOutboxPushSuccess(
|
|
167
|
-
buildTextPushSuccessArgs({
|
|
168
|
-
entry: args.entry,
|
|
169
|
-
connIds: args.connIds,
|
|
170
|
-
ownerConnId: args.ownerConnId,
|
|
171
|
-
ownerClientId: args.ownerConnId ? args.owner?.clientId : undefined,
|
|
172
|
-
}),
|
|
173
|
-
);
|
|
174
|
-
logOutboxPushOkSummary(args.entry.messageId);
|
|
175
|
-
logOutboxPushOk(
|
|
176
|
-
buildTextPushOkArgs({
|
|
177
|
-
entry: args.entry,
|
|
178
|
-
connIds: args.connIds,
|
|
179
|
-
recentInboundReachable: args.recentInboundReachable,
|
|
180
|
-
event: runtime.pushEvent,
|
|
181
|
-
}),
|
|
182
|
-
);
|
|
183
|
-
};
|
|
184
|
-
|
|
185
135
|
const handleTextPushFailure = (args: { entry: OutboxEntry; error: unknown }) => {
|
|
186
136
|
runtime.recordOutboxPushFailure({
|
|
187
137
|
entry: args.entry,
|
|
@@ -253,7 +203,6 @@ export function createBncrOutboxPush(runtime: BncrOutboxPushRuntime) {
|
|
|
253
203
|
};
|
|
254
204
|
|
|
255
205
|
return {
|
|
256
|
-
pushTextSuccessPath,
|
|
257
206
|
handleTextPushFailure,
|
|
258
207
|
handleFileTransferPushFailure,
|
|
259
208
|
handleFileTransferPushGuardFailure,
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildFileTransferPushOkArgs,
|
|
3
|
+
buildFileTransferPushSuccessArgs,
|
|
4
|
+
} from '../core/outbox-file-transfer-bookkeeping.ts';
|
|
5
|
+
import { resolveFileTransferGuard } from '../core/outbox-file-transfer-guards.ts';
|
|
6
|
+
import {
|
|
7
|
+
prepareRouteSelection,
|
|
8
|
+
prepareRouteSelection as prepareTextPushRouteSelection,
|
|
9
|
+
} from '../core/outbox-file-transfer-prep.ts';
|
|
10
|
+
import {
|
|
11
|
+
buildFileTransferBroadcastPayload,
|
|
12
|
+
buildFileTransferRouteSelectArgs,
|
|
13
|
+
} from '../core/outbox-file-transfer-success.ts';
|
|
14
|
+
import { resolveTextPushGuard } from '../core/outbox-text-push-guards.ts';
|
|
15
|
+
import {
|
|
16
|
+
buildTextPushBroadcastPayload,
|
|
17
|
+
buildTextPushOkArgs,
|
|
18
|
+
buildTextPushRouteSelectArgs,
|
|
19
|
+
buildTextPushSuccessArgs,
|
|
20
|
+
} from '../core/outbox-text-push-success.ts';
|
|
21
|
+
import type { BncrConnection, BncrRoute, OutboxEntry } from '../core/types.ts';
|
|
22
|
+
import { isPlainObject } from '../core/value-sanitize.ts';
|
|
23
|
+
import {
|
|
24
|
+
selectOutboxFileTransferRouteCandidates,
|
|
25
|
+
selectOutboxRouteCandidates,
|
|
26
|
+
} from '../messaging/outbound/queue-selectors.ts';
|
|
27
|
+
|
|
28
|
+
type FileTransferGuardFailure = {
|
|
29
|
+
reason: 'media-url-missing' | 'no-gateway-context' | 'no-active-connection';
|
|
30
|
+
lastError: string;
|
|
31
|
+
recentInboundReachable?: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type RunBncrOutboxPushArgs = {
|
|
35
|
+
entry: OutboxEntry;
|
|
36
|
+
gatewayContext: unknown;
|
|
37
|
+
owner: BncrConnection | null;
|
|
38
|
+
resolvePushConnIds: (accountId: string) => Set<string>;
|
|
39
|
+
resolveRecentInboundConnIds: (accountId: string) => Set<string>;
|
|
40
|
+
hasRecentInboundReachability: (accountId: string) => boolean;
|
|
41
|
+
isRevalidatedAttemptedConn: (connId: string) => boolean;
|
|
42
|
+
// Route + guard failure
|
|
43
|
+
recordOutboxPrePushFailure: (args: {
|
|
44
|
+
entry: OutboxEntry;
|
|
45
|
+
lastError: string;
|
|
46
|
+
persist?: boolean;
|
|
47
|
+
}) => void;
|
|
48
|
+
logOutboxPushSkip: (args: {
|
|
49
|
+
messageId: string;
|
|
50
|
+
accountId: string;
|
|
51
|
+
reason: string;
|
|
52
|
+
recentInboundReachable?: boolean;
|
|
53
|
+
routeReason?: string;
|
|
54
|
+
connIds?: Iterable<string>;
|
|
55
|
+
ownerConnId?: string;
|
|
56
|
+
ownerClientId?: string;
|
|
57
|
+
}) => void;
|
|
58
|
+
handleFileTransferPushGuardFailure: (args: {
|
|
59
|
+
entry: OutboxEntry;
|
|
60
|
+
guard: FileTransferGuardFailure & { ok: false };
|
|
61
|
+
}) => void;
|
|
62
|
+
// Broadcast + bookkeeping (unified)
|
|
63
|
+
pushEvent: string;
|
|
64
|
+
gatewayBroadcastToConnIds: (
|
|
65
|
+
event: string,
|
|
66
|
+
payload: unknown,
|
|
67
|
+
connIds: ReadonlySet<string>,
|
|
68
|
+
) => void;
|
|
69
|
+
recordOutboxPushSuccess: (args: {
|
|
70
|
+
entry: OutboxEntry;
|
|
71
|
+
connIds: Iterable<string>;
|
|
72
|
+
ownerConnId?: string;
|
|
73
|
+
ownerClientId?: string;
|
|
74
|
+
}) => void;
|
|
75
|
+
logOutboxRouteSelect: (args: {
|
|
76
|
+
messageId: string;
|
|
77
|
+
accountId: string;
|
|
78
|
+
kind?: 'file-transfer';
|
|
79
|
+
routeReason: string;
|
|
80
|
+
connIds: Iterable<string>;
|
|
81
|
+
ownerConnId: string;
|
|
82
|
+
ownerClientId: string;
|
|
83
|
+
recentInboundReachable: boolean;
|
|
84
|
+
event: string;
|
|
85
|
+
}) => void;
|
|
86
|
+
logOutboxPushOkSummary: (messageId: string) => void;
|
|
87
|
+
logOutboundSummary: (entry: OutboxEntry) => void;
|
|
88
|
+
logOutboxPushOk: (args: {
|
|
89
|
+
messageId: string;
|
|
90
|
+
accountId: string;
|
|
91
|
+
kind?: 'file-transfer';
|
|
92
|
+
connIds: Iterable<string>;
|
|
93
|
+
ownerConnId: string;
|
|
94
|
+
ownerClientId: string;
|
|
95
|
+
recentInboundReachable: boolean;
|
|
96
|
+
event: string;
|
|
97
|
+
}) => void;
|
|
98
|
+
// Failure handlers
|
|
99
|
+
handleTextPushFailure: (args: { entry: OutboxEntry; error: unknown }) => void;
|
|
100
|
+
handleFileTransferPushFailure: (args: { entry: OutboxEntry; error: unknown }) => void;
|
|
101
|
+
// Media-specific (optional)
|
|
102
|
+
transferMediaToBncrClient?: (params: {
|
|
103
|
+
accountId: string;
|
|
104
|
+
sessionKey: string;
|
|
105
|
+
route: BncrRoute;
|
|
106
|
+
mediaUrl: string;
|
|
107
|
+
mediaLocalRoots?: readonly string[];
|
|
108
|
+
downloadMedia?: boolean;
|
|
109
|
+
}) => Promise<{
|
|
110
|
+
mode: 'base64' | 'chunk';
|
|
111
|
+
mimeType?: string;
|
|
112
|
+
fileName?: string;
|
|
113
|
+
base64?: string;
|
|
114
|
+
path?: string;
|
|
115
|
+
}>;
|
|
116
|
+
buildFileTransferOutboundFrame?: (params: {
|
|
117
|
+
entry: OutboxEntry;
|
|
118
|
+
msg: Record<string, unknown>;
|
|
119
|
+
media: { fileName?: string; mimeType?: string; path?: string; base64?: string; type?: string };
|
|
120
|
+
mediaUrl: string;
|
|
121
|
+
}) => Record<string, unknown>;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export async function runBncrOutboxPush(args: RunBncrOutboxPushArgs): Promise<boolean> {
|
|
125
|
+
const entry = args.entry;
|
|
126
|
+
const msg =
|
|
127
|
+
isPlainObject(entry.payload) &&
|
|
128
|
+
isPlainObject((entry.payload as Record<string, unknown>).message)
|
|
129
|
+
? ((entry.payload as Record<string, unknown>).message as Record<string, unknown>)
|
|
130
|
+
: null;
|
|
131
|
+
const transferMode = msg?.transferMode === 'media' ? 'media' : 'text';
|
|
132
|
+
|
|
133
|
+
// Route selection
|
|
134
|
+
const selection =
|
|
135
|
+
transferMode === 'media'
|
|
136
|
+
? prepareRouteSelection({
|
|
137
|
+
entry,
|
|
138
|
+
owner: args.owner,
|
|
139
|
+
resolvePushConnIds: args.resolvePushConnIds,
|
|
140
|
+
resolveRecentInboundConnIds: args.resolveRecentInboundConnIds,
|
|
141
|
+
hasRecentInboundReachability: args.hasRecentInboundReachability,
|
|
142
|
+
isRevalidatedAttemptedConn: args.isRevalidatedAttemptedConn,
|
|
143
|
+
selectRouteCandidates: selectOutboxFileTransferRouteCandidates,
|
|
144
|
+
})
|
|
145
|
+
: prepareTextPushRouteSelection({
|
|
146
|
+
entry,
|
|
147
|
+
owner: args.owner,
|
|
148
|
+
resolvePushConnIds: args.resolvePushConnIds,
|
|
149
|
+
resolveRecentInboundConnIds: args.resolveRecentInboundConnIds,
|
|
150
|
+
hasRecentInboundReachability: args.hasRecentInboundReachability,
|
|
151
|
+
isRevalidatedAttemptedConn: args.isRevalidatedAttemptedConn,
|
|
152
|
+
selectRouteCandidates: selectOutboxRouteCandidates,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Guard + dispatch
|
|
156
|
+
if (transferMode === 'media') {
|
|
157
|
+
const guard = resolveFileTransferGuard({
|
|
158
|
+
gatewayContext: args.gatewayContext,
|
|
159
|
+
entry,
|
|
160
|
+
owner: args.owner,
|
|
161
|
+
routeSelection: selection,
|
|
162
|
+
mediaUrl: String(msg!.mediaUrl || '').trim(),
|
|
163
|
+
});
|
|
164
|
+
if (!guard.ok) {
|
|
165
|
+
args.handleFileTransferPushGuardFailure({
|
|
166
|
+
entry,
|
|
167
|
+
guard: guard as FileTransferGuardFailure & { ok: false },
|
|
168
|
+
});
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Media success path (inline) ──
|
|
173
|
+
try {
|
|
174
|
+
const media = await args.transferMediaToBncrClient!({
|
|
175
|
+
accountId: entry.accountId,
|
|
176
|
+
sessionKey: entry.sessionKey,
|
|
177
|
+
route: entry.route,
|
|
178
|
+
mediaUrl: guard.mediaUrl,
|
|
179
|
+
mediaLocalRoots: Array.isArray(msg!.mediaLocalRoots)
|
|
180
|
+
? (msg!.mediaLocalRoots as readonly string[])
|
|
181
|
+
: undefined,
|
|
182
|
+
downloadMedia: msg!.downloadMedia as boolean | undefined,
|
|
183
|
+
});
|
|
184
|
+
const frame = args.buildFileTransferOutboundFrame!({
|
|
185
|
+
entry,
|
|
186
|
+
msg: msg!,
|
|
187
|
+
media,
|
|
188
|
+
mediaUrl: guard.mediaUrl,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
args.gatewayBroadcastToConnIds(
|
|
192
|
+
args.pushEvent,
|
|
193
|
+
buildFileTransferBroadcastPayload({ frame, messageId: entry.messageId }),
|
|
194
|
+
new Set(guard.connIds),
|
|
195
|
+
);
|
|
196
|
+
args.logOutboxRouteSelect(
|
|
197
|
+
buildFileTransferRouteSelectArgs({
|
|
198
|
+
entry,
|
|
199
|
+
connIds: guard.connIds,
|
|
200
|
+
routeReason: guard.routeReason,
|
|
201
|
+
recentInboundReachable: guard.recentInboundReachable,
|
|
202
|
+
owner: args.owner,
|
|
203
|
+
event: args.pushEvent,
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
args.recordOutboxPushSuccess(
|
|
207
|
+
buildFileTransferPushSuccessArgs({
|
|
208
|
+
entry,
|
|
209
|
+
connIds: guard.connIds,
|
|
210
|
+
owner: args.owner,
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
args.logOutboxPushOkSummary(entry.messageId);
|
|
214
|
+
// Sync entry type from the resolved frame
|
|
215
|
+
const frameMsg = frame?.message as Record<string, unknown> | undefined;
|
|
216
|
+
if (frameMsg?.type !== undefined) {
|
|
217
|
+
(msg! as Record<string, unknown>).type = frameMsg.type;
|
|
218
|
+
}
|
|
219
|
+
args.logOutboundSummary(entry);
|
|
220
|
+
args.logOutboxPushOk(
|
|
221
|
+
buildFileTransferPushOkArgs({
|
|
222
|
+
entry,
|
|
223
|
+
connIds: guard.connIds,
|
|
224
|
+
recentInboundReachable: guard.recentInboundReachable,
|
|
225
|
+
event: args.pushEvent,
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
return true;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
args.handleFileTransferPushFailure({ entry, error });
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Text ──
|
|
236
|
+
const textGuard = resolveTextPushGuard({
|
|
237
|
+
gatewayContext: args.gatewayContext,
|
|
238
|
+
entry,
|
|
239
|
+
routeSelection: selection,
|
|
240
|
+
});
|
|
241
|
+
if (!textGuard.ok) {
|
|
242
|
+
args.recordOutboxPrePushFailure({
|
|
243
|
+
entry,
|
|
244
|
+
lastError:
|
|
245
|
+
textGuard.reason === 'no-gateway-context'
|
|
246
|
+
? 'gateway context unavailable'
|
|
247
|
+
: 'no active bncr client',
|
|
248
|
+
persist: true,
|
|
249
|
+
});
|
|
250
|
+
args.logOutboxPushSkip({
|
|
251
|
+
messageId: entry.messageId,
|
|
252
|
+
accountId: entry.accountId,
|
|
253
|
+
reason: textGuard.reason,
|
|
254
|
+
recentInboundReachable:
|
|
255
|
+
textGuard.reason === 'no-active-connection' ? textGuard.recentInboundReachable : undefined,
|
|
256
|
+
routeReason: selection.routeReason,
|
|
257
|
+
connIds: selection.connIds,
|
|
258
|
+
ownerConnId: selection.ownerConnId,
|
|
259
|
+
ownerClientId: args.owner?.clientId,
|
|
260
|
+
});
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── Text success path (inline) ──
|
|
265
|
+
try {
|
|
266
|
+
args.gatewayBroadcastToConnIds(
|
|
267
|
+
args.pushEvent,
|
|
268
|
+
buildTextPushBroadcastPayload({
|
|
269
|
+
payload: entry.payload,
|
|
270
|
+
messageId: entry.messageId,
|
|
271
|
+
}),
|
|
272
|
+
new Set(textGuard.connIds),
|
|
273
|
+
);
|
|
274
|
+
args.logOutboxRouteSelect(
|
|
275
|
+
buildTextPushRouteSelectArgs({
|
|
276
|
+
entry,
|
|
277
|
+
connIds: textGuard.connIds,
|
|
278
|
+
routeReason: textGuard.routeReason,
|
|
279
|
+
recentInboundReachable: textGuard.recentInboundReachable,
|
|
280
|
+
owner: args.owner,
|
|
281
|
+
event: args.pushEvent,
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
args.recordOutboxPushSuccess(
|
|
285
|
+
buildTextPushSuccessArgs({
|
|
286
|
+
entry,
|
|
287
|
+
connIds: textGuard.connIds,
|
|
288
|
+
ownerConnId: textGuard.ownerConnId,
|
|
289
|
+
ownerClientId: textGuard.ownerConnId ? args.owner?.clientId : undefined,
|
|
290
|
+
}),
|
|
291
|
+
);
|
|
292
|
+
args.logOutboxPushOkSummary(entry.messageId);
|
|
293
|
+
args.logOutboundSummary(entry);
|
|
294
|
+
args.logOutboxPushOk(
|
|
295
|
+
buildTextPushOkArgs({
|
|
296
|
+
entry,
|
|
297
|
+
connIds: textGuard.connIds,
|
|
298
|
+
recentInboundReachable: textGuard.recentInboundReachable,
|
|
299
|
+
event: args.pushEvent,
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
return true;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
args.handleTextPushFailure({ entry, error });
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -36,7 +36,7 @@ export function buildSceneKey(parsed: ParsedInbound): string {
|
|
|
36
36
|
: `${parsed.platform}:${parsed.userId}`;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
function buildSceneRecord(args: {
|
|
40
40
|
parsed: ParsedInbound;
|
|
41
41
|
sceneKey: string;
|
|
42
42
|
kind: BncrSceneKind;
|