@xmoxmo/bncr 0.5.5 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -9
- package/index.ts +1 -9
- package/package.json +1 -1
- package/scripts/check-pack.mjs +0 -6
- package/scripts/selfcheck.mjs +0 -7
- package/src/channel.ts +114 -37
- package/src/core/accounts.ts +1 -6
- package/src/core/connection-reachability.ts +1 -5
- package/src/core/dead-letter-diagnostics.ts +4 -10
- package/src/core/outbox-entry-builders.ts +38 -15
- package/src/core/outbox-file-transfer-failure.ts +5 -0
- package/src/core/outbox-file-transfer-guards.ts +6 -0
- package/src/core/outbox-file-transfer-prep.ts +8 -5
- package/src/core/outbox-file-transfer-success.ts +5 -0
- package/src/core/outbox-queue.ts +1 -7
- package/src/core/outbox-summary.ts +15 -27
- package/src/core/outbox-text-push-prep.ts +1 -36
- package/src/core/permissions.ts +1 -9
- package/src/core/persisted-outbox-entry.ts +54 -11
- package/src/core/policy.ts +1 -11
- package/src/core/register-trace.ts +2 -5
- package/src/core/status-meta.ts +1 -9
- package/src/core/status.ts +1 -21
- package/src/core/targets.ts +52 -70
- package/src/core/types.ts +1 -17
- package/src/core/value-sanitize.ts +38 -1
- package/src/messaging/inbound/commands.ts +2 -24
- package/src/messaging/inbound/dispatch-prep.ts +1 -1
- package/src/messaging/inbound/gate.ts +2 -31
- package/src/messaging/inbound/group-history.ts +0 -75
- package/src/messaging/inbound/native-command.ts +7 -3
- package/src/messaging/inbound/parse.ts +1 -31
- package/src/messaging/inbound/scene-admin.ts +181 -5
- package/src/messaging/outbound/diagnostics.ts +1 -6
- package/src/messaging/outbound/durable-queue-adapter.ts +17 -11
- package/src/messaging/outbound/marker-parser.ts +188 -0
- package/src/messaging/outbound/media-dedupe.ts +2 -6
- package/src/messaging/outbound/media.ts +6 -20
- package/src/messaging/outbound/normalize-outbound-send.ts +327 -0
- package/src/messaging/outbound/queue-selectors.ts +1 -5
- package/src/messaging/outbound/reply-enqueue-media.ts +4 -0
- package/src/messaging/outbound/reply-enqueue.ts +130 -57
- package/src/messaging/outbound/session-route.ts +6 -11
- package/src/messaging/outbound/target-resolver.ts +1 -6
- package/src/openclaw/routing-runtime.ts +0 -11
- package/src/openclaw/sdk-helpers.ts +0 -2
- package/src/plugin/bridge-file-transfer-push-facade.ts +8 -6
- package/src/plugin/bridge-media-facade.ts +1 -1
- package/src/plugin/channel-components.ts +0 -13
- package/src/plugin/channel-plugin-surface-group.ts +57 -40
- package/src/plugin/channel-runtime-builders-delivery.ts +3 -0
- package/src/plugin/channel-runtime-types.ts +10 -24
- package/src/plugin/channel-send.ts +241 -154
- package/src/plugin/channel-utils.ts +8 -29
- package/src/plugin/connection-handlers-helpers.ts +0 -15
- package/src/plugin/connection-handlers.ts +6 -5
- package/src/plugin/file-inbound-abort.ts +5 -0
- package/src/plugin/file-inbound-chunk.ts +5 -0
- package/src/plugin/file-inbound-complete.ts +5 -0
- package/src/plugin/file-transfer-orchestrator.ts +105 -3
- package/src/plugin/file-transfer-setup.ts +1 -1
- package/src/plugin/media-dedupe-runtime.ts +1 -6
- package/src/plugin/media-orchestrators-runtime-group.ts +2 -0
- package/src/plugin/message-send.ts +7 -8
- package/src/plugin/outbound.ts +6 -22
- package/src/plugin/outbox-drain-loop.ts +2 -2
- package/src/plugin/outbox-file-push-flow.ts +7 -7
- package/src/plugin/outbox-text-push-flow.ts +1 -1
- package/src/plugin/target-runtime.ts +1 -6
- package/src/runtime/outbound-ack-timeout.ts +1 -8
- package/src/runtime/outbox-transitions.ts +1 -4
- package/src/runtime/status-worker.ts +1 -9
- package/src/messaging/outbound/build-send-action.ts +0 -130
- package/src/messaging/outbound/durable-message-adapter.ts +0 -121
- package/src/messaging/outbound/send-params.ts +0 -81
- package/src/messaging/outbound/send.ts +0 -121
- package/src/openclaw/ingress-runtime.ts +0 -25
- package/src/plugin/channel-send-runtime-group.ts +0 -37
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildFileTransferInitPayload } from '../core/file-transfer-payloads.ts';
|
|
2
2
|
import type { BncrRoute, FileSendTransferState } from '../core/types.ts';
|
|
3
3
|
import { asSanitizedString, clampFiniteNumber } from '../core/value-sanitize.ts';
|
|
4
|
+
import { resolveOutboundFileName } from './channel-utils.ts';
|
|
4
5
|
import type { createBncrFileAckRuntime } from './file-ack-runtime.ts';
|
|
5
6
|
import {
|
|
6
7
|
abortChunkTransfer,
|
|
@@ -36,13 +37,13 @@ function getCompletedTransferPath(state?: FileSendTransferState): string | null
|
|
|
36
37
|
function buildBase64TransferResult(prepared: {
|
|
37
38
|
mimeType?: string;
|
|
38
39
|
fileName?: string;
|
|
39
|
-
|
|
40
|
+
base64?: string;
|
|
40
41
|
}) {
|
|
41
42
|
return {
|
|
42
43
|
mode: 'base64' as const,
|
|
43
44
|
mimeType: prepared.mimeType,
|
|
44
45
|
fileName: prepared.fileName,
|
|
45
|
-
|
|
46
|
+
base64: prepared.base64,
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -190,8 +191,96 @@ export function createBncrFileTransferOrchestrator(runtime: BncrFileTransferOrch
|
|
|
190
191
|
|
|
191
192
|
return buildChunkTransferResult({ mimeType, fileName, path: done.path });
|
|
192
193
|
};
|
|
194
|
+
const HTTP_URL_RE = /^https?:\/\//i;
|
|
195
|
+
|
|
196
|
+
/** Try HEAD for Content-Type; fall back to Range+sniff for magic bytes when unclear. */
|
|
197
|
+
async function resolveRemoteMediaType(url: string): Promise<{ mimeType?: string }> {
|
|
198
|
+
// 1) HEAD -> Content-Type
|
|
199
|
+
let headerType = '';
|
|
200
|
+
try {
|
|
201
|
+
const head = await fetch(url, { method: 'HEAD', signal: AbortSignal.timeout(10_000) });
|
|
202
|
+
if (head.ok) {
|
|
203
|
+
const raw = (head.headers.get('content-type') || '').trim().toLowerCase();
|
|
204
|
+
const parsed = raw.split(';')[0]?.trim() || '';
|
|
205
|
+
// Accept explicit types; reject octet-stream and generic application/*
|
|
206
|
+
if (parsed && parsed !== 'application/octet-stream') {
|
|
207
|
+
headerType = parsed;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
/* HEAD failed, fall through to Range sniff */
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (headerType) return { mimeType: headerType };
|
|
215
|
+
|
|
216
|
+
// 2) Range GET -> first 512 bytes -> magic byte sniff
|
|
217
|
+
try {
|
|
218
|
+
const range = await fetch(url, {
|
|
219
|
+
method: 'GET',
|
|
220
|
+
headers: { Range: 'bytes=0-511' },
|
|
221
|
+
signal: AbortSignal.timeout(10_000),
|
|
222
|
+
});
|
|
223
|
+
// Only accept 206 Partial Content; 200 (no Range support) or other statuses -> fall through
|
|
224
|
+
if (range.status !== 206) return {};
|
|
225
|
+
const buf = Buffer.from(await range.arrayBuffer());
|
|
226
|
+
if (buf.length < 4) return {};
|
|
227
|
+
|
|
228
|
+
const sniffed = sniffMimeFromMagic(buf);
|
|
229
|
+
if (!sniffed) return {};
|
|
230
|
+
return { mimeType: sniffed };
|
|
231
|
+
} catch {
|
|
232
|
+
return {};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Minimal magic-byte MIME sniffer for the first ~512 bytes. */
|
|
237
|
+
function sniffMimeFromMagic(buf: Buffer): string | undefined {
|
|
238
|
+
const eq = (off: number, ...bytes: number[]) =>
|
|
239
|
+
bytes.every((b, idx) => off + idx < buf.length && buf[off + idx] === b);
|
|
240
|
+
|
|
241
|
+
// JPEG: FF D8 FF
|
|
242
|
+
if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
|
|
243
|
+
return 'image/jpeg';
|
|
244
|
+
// PNG: 89 50 4E 47
|
|
245
|
+
if (eq(0, 0x89, 0x50, 0x4e, 0x47)) return 'image/png';
|
|
246
|
+
// GIF: 47 49 46 38
|
|
247
|
+
if (eq(0, 0x47, 0x49, 0x46, 0x38)) return 'image/gif';
|
|
248
|
+
// WEBP: RIFF .... WEBP
|
|
249
|
+
if (eq(0, 0x52, 0x49, 0x46, 0x46) && buf.length >= 12 && eq(8, 0x57, 0x45, 0x42, 0x50))
|
|
250
|
+
return 'image/webp';
|
|
251
|
+
// BMP: 42 4D
|
|
252
|
+
if (eq(0, 0x42, 0x4d)) return 'image/bmp';
|
|
253
|
+
// MP4 / MOV / M4A: ftyp box
|
|
254
|
+
if (eq(4, 0x66, 0x74, 0x79, 0x70) && buf.length >= 12) {
|
|
255
|
+
const brand = String.fromCharCode(buf[8], buf[9], buf[10], buf[11]);
|
|
256
|
+
if (brand === 'M4A ' || brand === 'mp42') return 'audio/mp4';
|
|
257
|
+
return 'video/mp4';
|
|
258
|
+
}
|
|
259
|
+
// MP3: ID3 tag
|
|
260
|
+
if (eq(0, 0x49, 0x44, 0x33)) return 'audio/mpeg';
|
|
261
|
+
// MPEG audio sync word
|
|
262
|
+
if (buf[0] === 0xff && (buf[1] & 0xe0) === 0xe0) return 'audio/mpeg';
|
|
263
|
+
// OGG: 4F 67 67 53
|
|
264
|
+
if (eq(0, 0x4f, 0x67, 0x67, 0x53)) return 'audio/ogg';
|
|
265
|
+
// WAV: RIFF .... WAVE
|
|
266
|
+
if (eq(0, 0x52, 0x49, 0x46, 0x46) && buf.length >= 12 && eq(8, 0x57, 0x41, 0x56, 0x45))
|
|
267
|
+
return 'audio/wav';
|
|
268
|
+
// FLAC: 66 4C 61 43
|
|
269
|
+
if (eq(0, 0x66, 0x4c, 0x61, 0x43)) return 'audio/flac';
|
|
270
|
+
// PDF: 25 50 44 46
|
|
271
|
+
if (eq(0, 0x25, 0x50, 0x44, 0x46)) return 'application/pdf';
|
|
272
|
+
// WebM/Matroska: 1A 45 DF A3
|
|
273
|
+
if (eq(0, 0x1a, 0x45, 0xdf, 0xa3)) {
|
|
274
|
+
for (let i = 0; i < Math.min(buf.length, 200); i++) {
|
|
275
|
+
if (buf[i] === 0xae) return 'video/webm';
|
|
276
|
+
}
|
|
277
|
+
return 'audio/webm';
|
|
278
|
+
}
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
193
281
|
|
|
194
282
|
const transferMediaToBncrClient = async (params: {
|
|
283
|
+
downloadMedia?: boolean;
|
|
195
284
|
accountId: string;
|
|
196
285
|
sessionKey: string;
|
|
197
286
|
route: BncrRoute;
|
|
@@ -201,9 +290,22 @@ export function createBncrFileTransferOrchestrator(runtime: BncrFileTransferOrch
|
|
|
201
290
|
mode: 'base64' | 'chunk';
|
|
202
291
|
mimeType?: string;
|
|
203
292
|
fileName?: string;
|
|
204
|
-
|
|
293
|
+
base64?: string;
|
|
205
294
|
path?: string;
|
|
206
295
|
}> => {
|
|
296
|
+
// HTTP URL: skip download/chunk, pass URL through to client
|
|
297
|
+
if (HTTP_URL_RE.test(params.mediaUrl) && !params.downloadMedia) {
|
|
298
|
+
const { mimeType } = await resolveRemoteMediaType(params.mediaUrl);
|
|
299
|
+
const fileName = resolveOutboundFileName({
|
|
300
|
+
mediaUrl: params.mediaUrl,
|
|
301
|
+
mimeType,
|
|
302
|
+
});
|
|
303
|
+
return {
|
|
304
|
+
mode: 'base64' as const,
|
|
305
|
+
mimeType,
|
|
306
|
+
fileName,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
207
309
|
const prepared = await runtime.prepareOutboundTransfer({
|
|
208
310
|
accountId: params.accountId,
|
|
209
311
|
sessionKey: params.sessionKey,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { asString } from '../core/value-sanitize.ts';
|
|
1
2
|
import {
|
|
2
3
|
buildMediaTextFallback,
|
|
3
4
|
type MediaDedupeCacheEntry,
|
|
@@ -5,12 +6,6 @@ import {
|
|
|
5
6
|
normalizeReplyToId,
|
|
6
7
|
} from '../messaging/outbound/media-dedupe.ts';
|
|
7
8
|
|
|
8
|
-
function asString(v: unknown, fallback = ''): string {
|
|
9
|
-
if (typeof v === 'string') return v;
|
|
10
|
-
if (v == null) return fallback;
|
|
11
|
-
return String(v);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
9
|
export function createBncrMediaDedupeRuntime(runtime: {
|
|
15
10
|
now: () => number;
|
|
16
11
|
recentMediaDedupeBySession: Map<string, Map<string, MediaDedupeCacheEntry>>;
|
|
@@ -53,6 +53,7 @@ function buildReplyMediaEntryHelpers(runtime: {
|
|
|
53
53
|
kind?: 'tool' | 'block' | 'final';
|
|
54
54
|
replyToId?: string;
|
|
55
55
|
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
56
|
+
downloadMedia?: boolean;
|
|
56
57
|
}) => OutboxEntry;
|
|
57
58
|
rememberRecentMediaSend: (args: {
|
|
58
59
|
sessionKey: string;
|
|
@@ -208,6 +209,7 @@ export function createBncrMediaOrchestratorsRuntimeGroup(runtime: {
|
|
|
208
209
|
kind?: 'tool' | 'block' | 'final';
|
|
209
210
|
replyToId?: string;
|
|
210
211
|
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
212
|
+
downloadMedia?: boolean;
|
|
211
213
|
}) => OutboxEntry;
|
|
212
214
|
rememberRecentMediaSend: (args: {
|
|
213
215
|
sessionKey: string;
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
import type { ChannelMessageSendResult } from 'openclaw/plugin-sdk/channel-message';
|
|
2
|
-
|
|
3
|
-
type MessageSendContext = Record<string, unknown>;
|
|
2
|
+
import type { BncrChannelSendContext } from './channel-runtime-types.ts';
|
|
4
3
|
|
|
5
4
|
export type BncrMessageSendBridge = {
|
|
6
5
|
channelMessageSendText: (
|
|
7
|
-
ctx:
|
|
6
|
+
ctx: BncrChannelSendContext,
|
|
8
7
|
) => ChannelMessageSendResult | Promise<ChannelMessageSendResult>;
|
|
9
8
|
channelMessageSendMedia: (
|
|
10
|
-
ctx:
|
|
9
|
+
ctx: BncrChannelSendContext,
|
|
11
10
|
) => ChannelMessageSendResult | Promise<ChannelMessageSendResult>;
|
|
12
11
|
channelMessageSendPayload: (
|
|
13
|
-
ctx:
|
|
12
|
+
ctx: BncrChannelSendContext,
|
|
14
13
|
) => ChannelMessageSendResult | Promise<ChannelMessageSendResult>;
|
|
15
14
|
};
|
|
16
15
|
|
|
17
16
|
export function createBncrMessageSend(getBridge: () => BncrMessageSendBridge) {
|
|
18
17
|
return {
|
|
19
|
-
text: async (ctx:
|
|
20
|
-
media: async (ctx:
|
|
21
|
-
payload: async (ctx:
|
|
18
|
+
text: async (ctx: BncrChannelSendContext) => getBridge().channelMessageSendText(ctx),
|
|
19
|
+
media: async (ctx: BncrChannelSendContext) => getBridge().channelMessageSendMedia(ctx),
|
|
20
|
+
payload: async (ctx: BncrChannelSendContext) => getBridge().channelMessageSendPayload(ctx),
|
|
22
21
|
};
|
|
23
22
|
}
|
package/src/plugin/outbound.ts
CHANGED
|
@@ -1,31 +1,15 @@
|
|
|
1
1
|
import { normalizeAccountId } from '../core/accounts.ts';
|
|
2
|
+
import { asString } from '../core/value-sanitize.ts';
|
|
2
3
|
import {
|
|
3
4
|
deleteBncrMessageAction,
|
|
4
5
|
editBncrMessageAction,
|
|
5
6
|
reactBncrMessageAction,
|
|
6
7
|
sendBncrReplyAction,
|
|
7
8
|
} from '../messaging/outbound/actions.ts';
|
|
8
|
-
|
|
9
|
-
function asString(v: unknown, fallback = ''): string {
|
|
10
|
-
return typeof v === 'string' ? v : v == null ? fallback : String(v);
|
|
11
|
-
}
|
|
9
|
+
import type { BncrChannelSendContext } from './channel-runtime-types.ts';
|
|
12
10
|
|
|
13
11
|
type BncrOutboundDeliveryResult = { channel: string; messageId: string; chatId: string };
|
|
14
12
|
|
|
15
|
-
type BncrOutboundSendContext = {
|
|
16
|
-
accountId?: string | null;
|
|
17
|
-
to?: string;
|
|
18
|
-
text?: string;
|
|
19
|
-
mediaUrl?: string;
|
|
20
|
-
type?: string;
|
|
21
|
-
kind?: string;
|
|
22
|
-
replyToId?: string | null;
|
|
23
|
-
replyToMessageId?: string | null;
|
|
24
|
-
asVoice?: boolean;
|
|
25
|
-
audioAsVoice?: boolean;
|
|
26
|
-
mediaLocalRoots?: readonly string[];
|
|
27
|
-
};
|
|
28
|
-
|
|
29
13
|
type BncrOutboundReplyActionContext = {
|
|
30
14
|
accountId?: string | null;
|
|
31
15
|
to?: string;
|
|
@@ -50,18 +34,18 @@ type BncrOutboundEditActionContext = BncrOutboundTargetActionContext & {
|
|
|
50
34
|
|
|
51
35
|
export type BncrOutboundBridge = {
|
|
52
36
|
channelSendText: (
|
|
53
|
-
ctx:
|
|
37
|
+
ctx: BncrChannelSendContext,
|
|
54
38
|
) => BncrOutboundDeliveryResult | Promise<BncrOutboundDeliveryResult>;
|
|
55
39
|
channelSendMedia: (
|
|
56
|
-
ctx:
|
|
40
|
+
ctx: BncrChannelSendContext,
|
|
57
41
|
) => BncrOutboundDeliveryResult | Promise<BncrOutboundDeliveryResult>;
|
|
58
42
|
};
|
|
59
43
|
|
|
60
44
|
export function createBncrOutboundRuntime(getBridge: () => BncrOutboundBridge) {
|
|
61
45
|
return {
|
|
62
46
|
deliveryMode: 'gateway' as const,
|
|
63
|
-
sendText: async (ctx:
|
|
64
|
-
sendMedia: async (ctx:
|
|
47
|
+
sendText: async (ctx: BncrChannelSendContext) => getBridge().channelSendText(ctx),
|
|
48
|
+
sendMedia: async (ctx: BncrChannelSendContext) => getBridge().channelSendMedia(ctx),
|
|
65
49
|
replyAction: async (ctx: BncrOutboundReplyActionContext) =>
|
|
66
50
|
sendBncrReplyAction({
|
|
67
51
|
accountId: normalizeAccountId(ctx?.accountId),
|
|
@@ -295,8 +295,8 @@ export function createBncrOutboxDrainLoop(
|
|
|
295
295
|
try {
|
|
296
296
|
pushed = await runtime.tryPushEntry(entry);
|
|
297
297
|
} catch (error) {
|
|
298
|
-
const
|
|
299
|
-
if (
|
|
298
|
+
const msg = runtime.isPlainObject(entry.payload?.message) ? entry.payload.message : null;
|
|
299
|
+
if (msg?.transferMode === 'media') {
|
|
300
300
|
runtime.handleFileTransferPushFailure({ entry, error });
|
|
301
301
|
} else {
|
|
302
302
|
runtime.handleTextPushFailure({ entry, error });
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { resolveFileTransferGuard } from '../core/outbox-file-transfer-guards.ts';
|
|
2
|
-
import {
|
|
2
|
+
import { prepareRouteSelection } from '../core/outbox-file-transfer-prep.ts';
|
|
3
3
|
import type { BncrConnection, OutboxEntry } from '../core/types.ts';
|
|
4
4
|
import { selectOutboxFileTransferRouteCandidates } from '../messaging/outbound/queue-selectors.ts';
|
|
5
5
|
|
|
6
6
|
export async function runBncrFileTransferOutboxPush(args: {
|
|
7
7
|
entry: OutboxEntry;
|
|
8
|
-
|
|
8
|
+
msg: Record<string, unknown>;
|
|
9
9
|
gatewayContext: unknown;
|
|
10
10
|
owner: BncrConnection | null;
|
|
11
11
|
resolvePushConnIds: (accountId: string) => Set<string>;
|
|
@@ -18,7 +18,7 @@ export async function runBncrFileTransferOutboxPush(args: {
|
|
|
18
18
|
}) => void;
|
|
19
19
|
pushFileTransferSuccessPath: (args: {
|
|
20
20
|
entry: OutboxEntry;
|
|
21
|
-
|
|
21
|
+
msg: Record<string, unknown>;
|
|
22
22
|
owner: BncrConnection | null;
|
|
23
23
|
connIds: Iterable<string>;
|
|
24
24
|
recentInboundReachable: boolean;
|
|
@@ -27,21 +27,21 @@ export async function runBncrFileTransferOutboxPush(args: {
|
|
|
27
27
|
}) => Promise<void>;
|
|
28
28
|
handleFileTransferPushFailure: (args: { entry: OutboxEntry; error: unknown }) => void;
|
|
29
29
|
}) {
|
|
30
|
-
const selection =
|
|
30
|
+
const selection = prepareRouteSelection({
|
|
31
31
|
entry: args.entry,
|
|
32
32
|
owner: args.owner,
|
|
33
33
|
resolvePushConnIds: args.resolvePushConnIds,
|
|
34
34
|
resolveRecentInboundConnIds: args.resolveRecentInboundConnIds,
|
|
35
35
|
hasRecentInboundReachability: args.hasRecentInboundReachability,
|
|
36
36
|
isRevalidatedAttemptedConn: args.isRevalidatedAttemptedConn,
|
|
37
|
-
selectOutboxFileTransferRouteCandidates,
|
|
37
|
+
selectRouteCandidates: selectOutboxFileTransferRouteCandidates,
|
|
38
38
|
});
|
|
39
39
|
const guard = resolveFileTransferGuard({
|
|
40
40
|
gatewayContext: args.gatewayContext,
|
|
41
41
|
entry: args.entry,
|
|
42
42
|
owner: args.owner,
|
|
43
43
|
routeSelection: selection,
|
|
44
|
-
mediaUrl: String(args.
|
|
44
|
+
mediaUrl: String(args.msg.mediaUrl || '').trim(),
|
|
45
45
|
});
|
|
46
46
|
if (!guard.ok) {
|
|
47
47
|
args.handleFileTransferPushGuardFailure({
|
|
@@ -54,7 +54,7 @@ export async function runBncrFileTransferOutboxPush(args: {
|
|
|
54
54
|
try {
|
|
55
55
|
await args.pushFileTransferSuccessPath({
|
|
56
56
|
entry: args.entry,
|
|
57
|
-
|
|
57
|
+
msg: args.msg,
|
|
58
58
|
owner: args.owner,
|
|
59
59
|
connIds: guard.connIds,
|
|
60
60
|
recentInboundReachable: guard.recentInboundReachable,
|
|
@@ -43,7 +43,7 @@ export async function runBncrTextOutboxPush(args: {
|
|
|
43
43
|
resolveRecentInboundConnIds: args.resolveRecentInboundConnIds,
|
|
44
44
|
hasRecentInboundReachability: args.hasRecentInboundReachability,
|
|
45
45
|
isRevalidatedAttemptedConn: args.isRevalidatedAttemptedConn,
|
|
46
|
-
selectOutboxRouteCandidates,
|
|
46
|
+
selectRouteCandidates: selectOutboxRouteCandidates,
|
|
47
47
|
});
|
|
48
48
|
const guard = resolveTextPushGuard({
|
|
49
49
|
gatewayContext: args.gatewayContext,
|
|
@@ -6,15 +6,10 @@ import {
|
|
|
6
6
|
routeKey,
|
|
7
7
|
} from '../core/targets.ts';
|
|
8
8
|
import type { BncrRoute } from '../core/types.ts';
|
|
9
|
+
import { asString } from '../core/value-sanitize.ts';
|
|
9
10
|
import { getOpenClawRuntimeConfigOrDefault } from '../openclaw/config-runtime.ts';
|
|
10
11
|
import type { BncrChannelConfigRoot } from './channel-runtime-types.ts';
|
|
11
12
|
|
|
12
|
-
function asString(v: unknown, fallback = ''): string {
|
|
13
|
-
if (typeof v === 'string') return v;
|
|
14
|
-
if (v == null) return fallback;
|
|
15
|
-
return String(v);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
13
|
export function createBncrTargetRuntime(runtime: {
|
|
19
14
|
api: Parameters<typeof getOpenClawRuntimeConfigOrDefault>[0];
|
|
20
15
|
channelId: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BncrAckObservability, BncrAckStrategy } from '../core/types.ts';
|
|
2
|
+
import { asString, finiteNumberOr } from '../core/value-sanitize.ts';
|
|
2
3
|
|
|
3
4
|
type ComputeBncrRecommendedAckTimeoutArgs = {
|
|
4
5
|
lateAckOkCount: number;
|
|
@@ -76,14 +77,6 @@ export function resolveBncrRuntimeAckTimeoutDecision(args: ComputeBncrRecommende
|
|
|
76
77
|
return { timeoutMs, reason };
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
function finiteNumberOr(value: unknown, fallback: number) {
|
|
80
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function asString(value: unknown, fallback = '') {
|
|
84
|
-
return typeof value === 'string' ? value : fallback;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
80
|
export function buildBncrRuntimeAckStrategy(args: {
|
|
88
81
|
ackObservability: BncrAckObservability;
|
|
89
82
|
defaultAckTimeoutMs: number;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OutboxEntry } from '../core/types.ts';
|
|
2
|
+
import { finiteNumberOr } from '../core/value-sanitize.ts';
|
|
2
3
|
import type {
|
|
3
4
|
PushFailureDecision,
|
|
4
5
|
RetryRerouteDecision,
|
|
@@ -83,10 +84,6 @@ export function buildBncrOutboxPushSuccessEntryPatch(args: {
|
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
function finiteNumberOr(value: unknown, fallback: number): number {
|
|
87
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
87
|
export function buildBncrAckOkTelemetryPatch(args: {
|
|
91
88
|
entry: OutboxEntry;
|
|
92
89
|
ackAt: number;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeAccountId } from '../core/accounts.ts';
|
|
2
2
|
import type { BncrDiagnosticsSummary } from '../core/types.ts';
|
|
3
|
+
import { finiteNumberOr, nonNegativeFiniteNumberOr } from '../core/value-sanitize.ts';
|
|
3
4
|
|
|
4
5
|
type StatusRuntimeMeta = {
|
|
5
6
|
pending?: number;
|
|
@@ -60,15 +61,6 @@ export function createHealthStatusLogState(): HealthStatusLogState {
|
|
|
60
61
|
return { emittedSig: null, pendingSig: null, pendingSince: 0 };
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
function finiteNumberOr(value: unknown, fallback: number): number {
|
|
64
|
-
const n = Number(value);
|
|
65
|
-
return Number.isFinite(n) ? n : fallback;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function nonNegativeFiniteNumberOr(value: unknown, fallback: number): number {
|
|
69
|
-
return Math.max(0, finiteNumberOr(value, fallback));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
64
|
export function updateHealthStatusLogState(args: {
|
|
73
65
|
state: HealthStatusLogState;
|
|
74
66
|
sig: string;
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
type MinimalBncrSendInput = {
|
|
4
|
-
channel?: string;
|
|
5
|
-
action?: string;
|
|
6
|
-
idempotencyKey?: string;
|
|
7
|
-
accountId?: string;
|
|
8
|
-
to?: string;
|
|
9
|
-
message?: string;
|
|
10
|
-
caption?: string;
|
|
11
|
-
path?: string;
|
|
12
|
-
media?: string;
|
|
13
|
-
filePath?: string;
|
|
14
|
-
mediaUrl?: string;
|
|
15
|
-
mediaUrls?: string[];
|
|
16
|
-
asVoice?: boolean;
|
|
17
|
-
audioAsVoice?: boolean;
|
|
18
|
-
params?: Record<string, unknown>;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
type BuiltBncrMessageAction = {
|
|
22
|
-
channel: string;
|
|
23
|
-
action: string;
|
|
24
|
-
idempotencyKey: string;
|
|
25
|
-
accountId?: string;
|
|
26
|
-
params: Record<string, unknown>;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
function asString(v: unknown, fallback = ''): string {
|
|
30
|
-
if (typeof v === 'string') return v;
|
|
31
|
-
if (v == null) return fallback;
|
|
32
|
-
return String(v);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
36
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function pickFirstString(...values: unknown[]): string | undefined {
|
|
40
|
-
for (const value of values) {
|
|
41
|
-
if (typeof value !== 'string') continue;
|
|
42
|
-
if (value.length === 0) return '';
|
|
43
|
-
return value;
|
|
44
|
-
}
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function pickFirstBoolean(...values: unknown[]): boolean | undefined {
|
|
49
|
-
for (const value of values) {
|
|
50
|
-
if (typeof value === 'boolean') return value;
|
|
51
|
-
}
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function buildBncrMessageAction(input: MinimalBncrSendInput): BuiltBncrMessageAction {
|
|
56
|
-
const paramsObj = isPlainObject(input.params) ? input.params : {};
|
|
57
|
-
|
|
58
|
-
const channel = asString(input.channel || 'bncr').trim() || 'bncr';
|
|
59
|
-
const action = asString(input.action || 'send').trim() || 'send';
|
|
60
|
-
const idempotencyKey = asString(input.idempotencyKey || '').trim() || `bncr-${randomUUID()}`;
|
|
61
|
-
const accountId =
|
|
62
|
-
asString(pickFirstString(paramsObj.accountId, input.accountId) || '').trim() || undefined;
|
|
63
|
-
|
|
64
|
-
const to = asString(pickFirstString(paramsObj.to, input.to) || '').trim();
|
|
65
|
-
if (!to) throw new Error('bncr send requires to');
|
|
66
|
-
|
|
67
|
-
const mediaPath = pickFirstString(
|
|
68
|
-
paramsObj.media,
|
|
69
|
-
paramsObj.path,
|
|
70
|
-
paramsObj.filePath,
|
|
71
|
-
paramsObj.mediaUrl,
|
|
72
|
-
input.media,
|
|
73
|
-
input.path,
|
|
74
|
-
input.filePath,
|
|
75
|
-
input.mediaUrl,
|
|
76
|
-
);
|
|
77
|
-
const rawMediaUrls = Array.isArray(paramsObj.mediaUrls)
|
|
78
|
-
? paramsObj.mediaUrls
|
|
79
|
-
: Array.isArray(input.mediaUrls)
|
|
80
|
-
? input.mediaUrls
|
|
81
|
-
: undefined;
|
|
82
|
-
const mediaUrls = rawMediaUrls?.map((value) => asString(value || '').trim()).filter(Boolean);
|
|
83
|
-
|
|
84
|
-
const message = pickFirstString(paramsObj.message, input.message) ?? '';
|
|
85
|
-
const explicitCaption = pickFirstString(paramsObj.caption, input.caption) ?? '';
|
|
86
|
-
const asVoice = pickFirstBoolean(paramsObj.asVoice, input.asVoice);
|
|
87
|
-
const audioAsVoice = pickFirstBoolean(paramsObj.audioAsVoice, input.audioAsVoice);
|
|
88
|
-
|
|
89
|
-
if ((asVoice === true || audioAsVoice === true) && !mediaPath && !mediaUrls?.length) {
|
|
90
|
-
throw new Error('bncr voice send requires media path');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const normalizedParams: Record<string, unknown> = {
|
|
94
|
-
...paramsObj,
|
|
95
|
-
to,
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
if (mediaPath) {
|
|
99
|
-
normalizedParams.path = mediaPath;
|
|
100
|
-
const finalCaption = explicitCaption || message;
|
|
101
|
-
if (finalCaption) normalizedParams.caption = finalCaption;
|
|
102
|
-
delete normalizedParams.message;
|
|
103
|
-
if (mediaUrls?.length) normalizedParams.mediaUrls = mediaUrls;
|
|
104
|
-
} else {
|
|
105
|
-
if (mediaUrls?.length) {
|
|
106
|
-
normalizedParams.mediaUrls = mediaUrls;
|
|
107
|
-
const finalCaption = explicitCaption || message;
|
|
108
|
-
if (finalCaption) normalizedParams.caption = finalCaption;
|
|
109
|
-
delete normalizedParams.message;
|
|
110
|
-
delete normalizedParams.path;
|
|
111
|
-
} else {
|
|
112
|
-
const finalMessage = message || explicitCaption;
|
|
113
|
-
if (!finalMessage.trim()) throw new Error('bncr send requires message or media');
|
|
114
|
-
normalizedParams.message = finalMessage;
|
|
115
|
-
delete normalizedParams.caption;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (asVoice === true) normalizedParams.asVoice = true;
|
|
120
|
-
if (audioAsVoice === true) normalizedParams.audioAsVoice = true;
|
|
121
|
-
if (accountId) normalizedParams.accountId = accountId;
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
channel,
|
|
125
|
-
action,
|
|
126
|
-
idempotencyKey,
|
|
127
|
-
...(accountId ? { accountId } : {}),
|
|
128
|
-
params: normalizedParams,
|
|
129
|
-
};
|
|
130
|
-
}
|