@xmoxmo/bncr 0.5.6 → 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/dist/index.js +1 -9
- package/index.ts +1 -9
- package/package.json +1 -1
- package/scripts/check-pack.mjs +0 -8
- package/scripts/selfcheck.mjs +0 -10
- package/src/bootstrap/runtime-loader.ts +1 -1
- package/src/channel.ts +118 -171
- package/src/core/accounts.ts +1 -6
- package/src/core/connection-reachability.ts +2 -6
- package/src/core/dead-letter-diagnostics.ts +4 -10
- package/src/core/logging.ts +1 -1
- package/src/core/outbox-entry-builders.ts +78 -84
- 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 +10 -56
- package/src/core/permissions.ts +2 -10
- package/src/core/persisted-outbox-entry.ts +54 -11
- package/src/core/policy.ts +1 -11
- package/src/core/register-trace.ts +4 -7
- 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/parse.ts +2 -32
- package/src/messaging/inbound/scene-admin.ts +8 -8
- package/src/messaging/outbound/diagnostics-debug-builders.ts +0 -15
- package/src/messaging/outbound/diagnostics.ts +1 -7
- package/src/messaging/outbound/durable-queue-adapter.ts +17 -11
- package/src/messaging/outbound/marker-parser.ts +277 -28
- package/src/messaging/outbound/media-dedupe.ts +3 -10
- package/src/messaging/outbound/media.ts +1 -6
- package/src/messaging/outbound/normalize-outbound-send.ts +329 -0
- package/src/messaging/outbound/queue-selectors.ts +1 -5
- package/src/messaging/outbound/reply-enqueue-media.ts +1 -184
- package/src/messaging/outbound/reply-enqueue.ts +313 -175
- package/src/messaging/outbound/session-route.ts +6 -11
- package/src/messaging/outbound/target-resolver.ts +1 -6
- package/src/openclaw/media-runtime.ts +1 -1
- package/src/openclaw/routing-runtime.ts +0 -11
- package/src/openclaw/sdk-helpers.ts +0 -2
- package/src/plugin/bridge-media-facade.ts +0 -4
- package/src/plugin/bridge-outbox-facade.ts +0 -1
- package/src/plugin/channel-components.ts +0 -13
- package/src/plugin/channel-plugin-surface-group.ts +52 -65
- package/src/plugin/channel-runtime-builders-delivery.ts +3 -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-runtime-types.ts +2 -24
- package/src/plugin/channel-send.ts +206 -221
- 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 +39 -2
- package/src/plugin/media-dedupe-runtime.ts +1 -6
- package/src/plugin/media-orchestrators-runtime-group.ts +20 -149
- package/src/plugin/message-send.ts +7 -8
- package/src/plugin/messaging.ts +3 -3
- package/src/plugin/outbound.ts +6 -26
- package/src/plugin/outbox-drain-loop.ts +2 -2
- 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/plugin/target-runtime.ts +1 -6
- package/src/runtime/log-dedupe.ts +3 -3
- package/src/runtime/outbound-ack-timeout.ts +3 -12
- package/src/runtime/outbox-transitions.ts +1 -4
- package/src/runtime/status-snapshots.ts +2 -2
- package/src/runtime/status-worker.ts +3 -15
- package/src/core/outbox-text-push-prep.ts +0 -36
- 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 -114
- package/src/messaging/outbound/send.ts +0 -127
- package/src/openclaw/ingress-runtime.ts +0 -25
- package/src/plugin/bridge-file-transfer-push-facade.ts +0 -142
- package/src/plugin/channel-send-runtime-group.ts +0 -37
- package/src/plugin/outbox-file-push-flow.ts +0 -69
- package/src/plugin/outbox-text-push-flow.ts +0 -90
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
BncrDeadLetterEntrySummary,
|
|
6
6
|
OutboxEntry,
|
|
7
7
|
} from './types.ts';
|
|
8
|
+
import { asString } from './value-sanitize.ts';
|
|
8
9
|
|
|
9
10
|
export type DeadLetterTopReason = { reason: string; count: number };
|
|
10
11
|
|
|
@@ -15,12 +16,6 @@ export type BuildDeadLetterDiagnosticsOptions = {
|
|
|
15
16
|
cappedAt: number;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
function asString(value: unknown, fallback = ''): string {
|
|
19
|
-
if (typeof value === 'string') return value;
|
|
20
|
-
if (value == null) return fallback;
|
|
21
|
-
return String(value);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
19
|
function asPayloadMessage(payload: OutboxEntry['payload']): {
|
|
25
20
|
msg?: string;
|
|
26
21
|
type?: string;
|
|
@@ -87,15 +82,14 @@ export function parseDeadLetterOlderThan(raw: unknown): number | null {
|
|
|
87
82
|
}
|
|
88
83
|
|
|
89
84
|
export function summarizeDeadLetterEntry(entry: OutboxEntry) {
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const text = asString(meta.text || msg.msg || '');
|
|
85
|
+
const msg = asPayloadMessage(entry.payload) || {};
|
|
86
|
+
const text = asString(msg.msg || '');
|
|
93
87
|
return {
|
|
94
88
|
messageId: entry.messageId,
|
|
95
89
|
accountId: entry.accountId,
|
|
96
90
|
sessionKey: entry.sessionKey,
|
|
97
91
|
route: formatDisplayScope(entry.route),
|
|
98
|
-
kind: asString(
|
|
92
|
+
kind: asString(msg.type || 'message'),
|
|
99
93
|
createdAt: Number.isFinite(Number(entry.createdAt)) ? Number(entry.createdAt) : null,
|
|
100
94
|
retryCount: Number.isFinite(Number(entry.retryCount)) ? Number(entry.retryCount) : 0,
|
|
101
95
|
lastError: entry.lastError || null,
|
package/src/core/logging.ts
CHANGED
|
@@ -117,7 +117,7 @@ function sanitizeDebugValue(key: string, value: unknown): unknown {
|
|
|
117
117
|
return value;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
function sanitizeBncrDebugPayload(payload: Record<string, unknown>) {
|
|
121
121
|
return sanitizeDebugPayload(payload);
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -2,28 +2,77 @@ import type { OutboundReplyTargetPolicy } from '../messaging/outbound/reply-targ
|
|
|
2
2
|
import { normalizeOutboundReplyToId } from '../messaging/outbound/reply-target-policy.ts';
|
|
3
3
|
import type { BncrRoute, OutboxEntry } from './types.ts';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/** Strip empty/falsy values from extra so they don't override existing message defaults. */
|
|
6
|
+
function sanitizeExtraSpread(extra: Record<string, unknown>): Record<string, unknown> {
|
|
7
|
+
const out: Record<string, unknown> = {};
|
|
8
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
9
|
+
if (value === '' || value === null || value === undefined) continue;
|
|
10
|
+
out[key] = value;
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type BuildOutboxEntryParams = {
|
|
6
16
|
createMessageId: () => string;
|
|
7
17
|
now: () => number;
|
|
8
18
|
normalizeAccountId: (accountId?: string | null) => string;
|
|
9
|
-
pushEvent: string;
|
|
10
19
|
accountId: string;
|
|
11
20
|
sessionKey: string;
|
|
12
21
|
route: BncrRoute;
|
|
13
|
-
|
|
14
|
-
mediaLocalRoots?: readonly string[];
|
|
15
|
-
text: string;
|
|
16
|
-
asVoice?: boolean;
|
|
17
|
-
audioAsVoice?: boolean;
|
|
22
|
+
/** For text sends this is 'text'; for media it may be undefined (inferred downstream). */
|
|
18
23
|
type?: string;
|
|
19
|
-
|
|
24
|
+
/** Message body (text content for text sends, caption for media sends). */
|
|
25
|
+
msg: string;
|
|
20
26
|
kind?: 'tool' | 'block' | 'final';
|
|
21
27
|
replyToId?: string;
|
|
22
28
|
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
29
|
+
extra?: Record<string, unknown>;
|
|
30
|
+
/** Media-only fields. When transferMode === 'media' these shape the frame. */
|
|
31
|
+
transferMode?: 'text' | 'media';
|
|
32
|
+
mediaUrl?: string;
|
|
33
|
+
mediaLocalRoots?: readonly string[];
|
|
34
|
+
asVoice?: boolean;
|
|
35
|
+
audioAsVoice?: boolean;
|
|
23
36
|
downloadMedia?: boolean;
|
|
24
|
-
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Unified outbox entry builder.
|
|
41
|
+
*
|
|
42
|
+
* Text and media sends share the same outbox lifecycle (queue, drain, retry).
|
|
43
|
+
* The only structural difference is that media entries carry extra media fields
|
|
44
|
+
* and set `transferMode: 'media'` so the push layer knows to process media.
|
|
45
|
+
*
|
|
46
|
+
* - **Text**: `{ type: 'text', transferMode: undefined, no media fields }`
|
|
47
|
+
* - **Media**: `{ type: args.type || undefined, transferMode: 'media', ...media fields }`
|
|
48
|
+
*/
|
|
49
|
+
export function buildOutboxEntry(args: BuildOutboxEntryParams): OutboxEntry {
|
|
25
50
|
const messageId = args.createMessageId();
|
|
26
51
|
const createdAt = args.now();
|
|
52
|
+
const isMedia = args.transferMode === 'media';
|
|
53
|
+
|
|
54
|
+
const message: Record<string, unknown> = {
|
|
55
|
+
platform: args.route.platform,
|
|
56
|
+
groupId: args.route.groupId,
|
|
57
|
+
userId: args.route.userId,
|
|
58
|
+
type: isMedia ? args.type : 'text',
|
|
59
|
+
kind: args.kind,
|
|
60
|
+
msg: args.msg,
|
|
61
|
+
path: '',
|
|
62
|
+
base64: '',
|
|
63
|
+
fileName: '',
|
|
64
|
+
...(args.extra ? sanitizeExtraSpread(args.extra) : {}),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (isMedia) {
|
|
68
|
+
message.mediaUrl = args.mediaUrl || '';
|
|
69
|
+
if (args.mediaLocalRoots) message.mediaLocalRoots = Array.from(args.mediaLocalRoots);
|
|
70
|
+
message.asVoice = args.asVoice === true;
|
|
71
|
+
message.audioAsVoice = args.audioAsVoice === true;
|
|
72
|
+
message.downloadMedia = args.downloadMedia;
|
|
73
|
+
message.transferMode = 'media';
|
|
74
|
+
}
|
|
75
|
+
|
|
27
76
|
return {
|
|
28
77
|
messageId,
|
|
29
78
|
accountId: args.normalizeAccountId(args.accountId),
|
|
@@ -31,86 +80,31 @@ export function buildFileTransferOutboxEntry(args: {
|
|
|
31
80
|
route: args.route,
|
|
32
81
|
payload: {
|
|
33
82
|
type: 'message.outbound',
|
|
83
|
+
messageId,
|
|
84
|
+
idempotencyKey: messageId,
|
|
34
85
|
sessionKey: args.sessionKey,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
messageKind: args.kind,
|
|
86
|
+
replyToId:
|
|
87
|
+
normalizeOutboundReplyToId({
|
|
88
|
+
kind: args.kind,
|
|
89
|
+
replyToId: args.replyToId,
|
|
90
|
+
replyTargetPolicy: args.replyTargetPolicy,
|
|
91
|
+
}) || undefined,
|
|
92
|
+
message: message as {
|
|
93
|
+
platform: string;
|
|
94
|
+
groupId: string;
|
|
95
|
+
userId: string;
|
|
96
|
+
type?: string;
|
|
97
|
+
kind?: 'tool' | 'block' | 'final';
|
|
98
|
+
msg: string;
|
|
99
|
+
path: string;
|
|
100
|
+
base64: string;
|
|
101
|
+
fileName: string;
|
|
102
|
+
[key: string]: unknown;
|
|
53
103
|
},
|
|
104
|
+
ts: createdAt,
|
|
54
105
|
},
|
|
55
106
|
createdAt,
|
|
56
107
|
retryCount: 0,
|
|
57
108
|
nextAttemptAt: createdAt,
|
|
58
109
|
};
|
|
59
110
|
}
|
|
60
|
-
|
|
61
|
-
export function buildTextOutboxEntry(args: {
|
|
62
|
-
createMessageId: () => string;
|
|
63
|
-
now: () => number;
|
|
64
|
-
normalizeAccountId: (accountId?: string | null) => string;
|
|
65
|
-
normalizeReplyToId: (value?: string | null) => string;
|
|
66
|
-
accountId: string;
|
|
67
|
-
extra?: Record<string, unknown>;
|
|
68
|
-
sessionKey: string;
|
|
69
|
-
route: BncrRoute;
|
|
70
|
-
text: string;
|
|
71
|
-
kind?: 'tool' | 'block' | 'final';
|
|
72
|
-
replyToId?: string;
|
|
73
|
-
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
74
|
-
downloadMedia?: boolean;
|
|
75
|
-
}): OutboxEntry {
|
|
76
|
-
const messageId = args.createMessageId();
|
|
77
|
-
const createdAt = args.now();
|
|
78
|
-
const frame = {
|
|
79
|
-
type: 'message.outbound',
|
|
80
|
-
messageId,
|
|
81
|
-
idempotencyKey: messageId,
|
|
82
|
-
sessionKey: args.sessionKey,
|
|
83
|
-
replyToId:
|
|
84
|
-
normalizeOutboundReplyToId({
|
|
85
|
-
kind: args.kind,
|
|
86
|
-
replyToId: args.replyToId,
|
|
87
|
-
replyTargetPolicy: args.replyTargetPolicy,
|
|
88
|
-
}) || undefined,
|
|
89
|
-
message: {
|
|
90
|
-
platform: args.route.platform,
|
|
91
|
-
groupId: args.route.groupId,
|
|
92
|
-
userId: args.route.userId,
|
|
93
|
-
type: 'text',
|
|
94
|
-
kind: args.kind,
|
|
95
|
-
msg: args.text,
|
|
96
|
-
path: '',
|
|
97
|
-
base64: '',
|
|
98
|
-
fileName: '',
|
|
99
|
-
// Extra fields from marker/pipeline spread for adapter-specific data
|
|
100
|
-
// (e.g. type="appmsg", msg="<appmsg>..." from [BncrParam:...])
|
|
101
|
-
...(args.extra ? { ...args.extra } : {}),
|
|
102
|
-
},
|
|
103
|
-
ts: createdAt,
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
messageId,
|
|
108
|
-
accountId: args.normalizeAccountId(args.accountId),
|
|
109
|
-
sessionKey: args.sessionKey,
|
|
110
|
-
route: args.route,
|
|
111
|
-
payload: frame,
|
|
112
|
-
createdAt,
|
|
113
|
-
retryCount: 0,
|
|
114
|
-
nextAttemptAt: createdAt,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Intentionally separate from outbox-text-push-failure.ts.
|
|
3
|
+
* File transfer failure adds retryable-state resolution logic
|
|
4
|
+
* that text push doesn't need (no chunk-level retry semantics).
|
|
5
|
+
*/
|
|
1
6
|
import { buildPushFailureArgs } from './outbox-push-args.ts';
|
|
2
7
|
import type { OutboxEntry } from './types.ts';
|
|
3
8
|
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Intentionally separate from outbox-text-push-guards.ts.
|
|
3
|
+
* File transfer guard has distinct output type (includes owner/mediaUrl)
|
|
4
|
+
* and extra mediaUrl validation that text push doesn't need.
|
|
5
|
+
* Merging would add unnecessary conditional branches.
|
|
6
|
+
*/
|
|
1
7
|
import type { BncrConnection, OutboxEntry } from './types.ts';
|
|
2
8
|
|
|
3
9
|
export type FileTransferGuardResult =
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
import type { BncrConnection, OutboxEntry } from './types.ts';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type PushRouteSelectionArgs<C> = {
|
|
4
4
|
entry: OutboxEntry;
|
|
5
5
|
owner: BncrConnection | null;
|
|
6
6
|
resolvePushConnIds: (accountId: string) => Iterable<string>;
|
|
7
7
|
resolveRecentInboundConnIds: (accountId: string) => Iterable<string>;
|
|
8
8
|
hasRecentInboundReachability: (accountId: string) => boolean;
|
|
9
9
|
isRevalidatedAttemptedConn: (connId: string) => boolean;
|
|
10
|
-
|
|
10
|
+
selectRouteCandidates: (args: {
|
|
11
11
|
routeCandidates: Iterable<string>;
|
|
12
12
|
attemptedConnIds: string[];
|
|
13
13
|
recentInboundConnIds: Iterable<string>;
|
|
14
14
|
ownerConnId?: string;
|
|
15
15
|
recentInboundReachable: boolean;
|
|
16
16
|
isRevalidatedAttemptedConn: (connId: string) => boolean;
|
|
17
|
-
}) =>
|
|
18
|
-
}
|
|
17
|
+
}) => C;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Shared route selection prep for both text and file push flows. */
|
|
21
|
+
export function prepareRouteSelection<C>(args: PushRouteSelectionArgs<C>): C {
|
|
19
22
|
const attemptedConnIds = Array.isArray(args.entry.routeAttemptConnIds)
|
|
20
23
|
? args.entry.routeAttemptConnIds.filter((v): v is string => typeof v === 'string' && !!v)
|
|
21
24
|
: [];
|
|
22
25
|
|
|
23
|
-
return args.
|
|
26
|
+
return args.selectRouteCandidates({
|
|
24
27
|
routeCandidates: args.resolvePushConnIds(args.entry.accountId),
|
|
25
28
|
attemptedConnIds,
|
|
26
29
|
recentInboundConnIds: args.resolveRecentInboundConnIds(args.entry.accountId),
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Intentionally separate from outbox-text-push-success.ts.
|
|
3
|
+
* File transfer success uses kind:'file-transfer' tag and different
|
|
4
|
+
* argument shapes (owner vs connId). Separate callers, separate files.
|
|
5
|
+
*/
|
|
1
6
|
import { buildPushBroadcastPayload, buildPushRouteSelectArgs } from './outbox-push-args.ts';
|
|
2
7
|
import type { BncrConnection, OutboxEntry } from './types.ts';
|
|
3
8
|
|
package/src/core/outbox-queue.ts
CHANGED
|
@@ -50,13 +50,7 @@ export function collectDueOutboxEntries(args: {
|
|
|
50
50
|
nextAttemptAt: args.now + args.backoffMs(nextAttempt),
|
|
51
51
|
};
|
|
52
52
|
updatedEntries.push(updatedEntry);
|
|
53
|
-
duePayloads.push(
|
|
54
|
-
...updatedEntry.payload,
|
|
55
|
-
_meta: {
|
|
56
|
-
retryCount: updatedEntry.retryCount,
|
|
57
|
-
nextAttemptAt: updatedEntry.nextAttemptAt,
|
|
58
|
-
},
|
|
59
|
-
});
|
|
53
|
+
duePayloads.push(updatedEntry.payload);
|
|
60
54
|
|
|
61
55
|
if (duePayloads.length >= args.maxBatch) break;
|
|
62
56
|
}
|
|
@@ -1,33 +1,6 @@
|
|
|
1
1
|
import type { OutboxEntry } from './types.ts';
|
|
2
2
|
|
|
3
|
-
type
|
|
4
|
-
kind?: string;
|
|
5
|
-
asVoice?: boolean;
|
|
6
|
-
audioAsVoice?: boolean;
|
|
7
|
-
type?: unknown;
|
|
8
|
-
mediaUrl?: unknown;
|
|
9
|
-
text?: unknown;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
type OutboxSummaryMessage = {
|
|
13
|
-
type?: unknown;
|
|
14
|
-
msg?: unknown;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
type OutboxSummaryPayload = {
|
|
18
|
-
type?: unknown;
|
|
19
|
-
message?: OutboxSummaryMessage;
|
|
20
|
-
_meta?: OutboxSummaryMeta;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
function inferMediaTypeFromUrl(raw: string): 'image' | 'video' | 'audio' | 'file' {
|
|
24
|
-
const clean = String(raw || '').split(/[?#]/, 1)[0] || '';
|
|
25
|
-
const ext = clean.includes('.') ? clean.slice(clean.lastIndexOf('.') + 1).toLowerCase() : '';
|
|
26
|
-
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'heic', 'heif'].includes(ext)) return 'image';
|
|
27
|
-
if (['mp4', 'mov', 'mkv', 'webm', 'avi', 'm4v'].includes(ext)) return 'video';
|
|
28
|
-
if (['mp3', 'wav', 'ogg', 'oga', 'opus', 'm4a', 'aac', 'flac'].includes(ext)) return 'audio';
|
|
29
|
-
return 'file';
|
|
30
|
-
}
|
|
3
|
+
type OutboxSummaryMessage = { type?: unknown; msg?: unknown; mediaUrl?: unknown };
|
|
31
4
|
|
|
32
5
|
function filenameFromUrl(raw: string): string {
|
|
33
6
|
const clean = String(raw || '').split(/[?#]/, 1)[0] || '';
|
|
@@ -40,34 +13,15 @@ function filenameFromUrl(raw: string): string {
|
|
|
40
13
|
}
|
|
41
14
|
}
|
|
42
15
|
|
|
43
|
-
function summarizeOutboxType(
|
|
44
|
-
|
|
45
|
-
msg: OutboxSummaryMessage,
|
|
46
|
-
asString: (value: unknown) => string,
|
|
47
|
-
) {
|
|
48
|
-
const directType = asString(msg.type || '');
|
|
49
|
-
if (directType) return directType;
|
|
50
|
-
|
|
51
|
-
const meta = payload?._meta || {};
|
|
52
|
-
if (meta.kind === 'file-transfer') {
|
|
53
|
-
if (meta.asVoice === true || meta.audioAsVoice === true) return 'voice';
|
|
54
|
-
const hintedType = asString(meta.type || '').trim();
|
|
55
|
-
if (hintedType) return hintedType;
|
|
56
|
-
return inferMediaTypeFromUrl(asString(meta.mediaUrl || ''));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return asString(payload?.type || 'unknown');
|
|
16
|
+
function summarizeOutboxType(msg: OutboxSummaryMessage, asString: (value: unknown) => string) {
|
|
17
|
+
return asString(msg.type || '') || 'unknown';
|
|
60
18
|
}
|
|
61
19
|
|
|
62
|
-
function summarizeOutboxText(
|
|
63
|
-
|
|
64
|
-
msg: OutboxSummaryMessage,
|
|
65
|
-
asString: (value: unknown) => string,
|
|
66
|
-
) {
|
|
67
|
-
const text = asString(msg.msg || payload?._meta?.text || '').trim();
|
|
20
|
+
function summarizeOutboxText(msg: OutboxSummaryMessage, asString: (value: unknown) => string) {
|
|
21
|
+
const text = asString(msg.msg || '').trim();
|
|
68
22
|
if (text) return text;
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
23
|
+
const mediaUrl = asString(msg.mediaUrl || '');
|
|
24
|
+
if (mediaUrl) return filenameFromUrl(mediaUrl);
|
|
71
25
|
return '';
|
|
72
26
|
}
|
|
73
27
|
|
|
@@ -77,10 +31,10 @@ export function summarizeOutboxEntry(args: {
|
|
|
77
31
|
formatDisplayScope: (route: OutboxEntry['route']) => string;
|
|
78
32
|
summarizeTextPreview: (raw: string, limit?: number) => string;
|
|
79
33
|
}) {
|
|
80
|
-
const payload = args.entry.payload as
|
|
34
|
+
const payload = args.entry.payload as { message?: OutboxSummaryMessage } | undefined;
|
|
81
35
|
const msg = payload?.message || {};
|
|
82
|
-
const type = summarizeOutboxType(
|
|
83
|
-
const text = summarizeOutboxText(
|
|
36
|
+
const type = summarizeOutboxType(msg, args.asString);
|
|
37
|
+
const text = summarizeOutboxText(msg, args.asString);
|
|
84
38
|
const preview = args.summarizeTextPreview(text);
|
|
85
39
|
return [type, args.formatDisplayScope(args.entry.route), preview].join('|');
|
|
86
40
|
}
|
package/src/core/permissions.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
if (typeof v === 'string') return v;
|
|
3
|
-
if (v == null) return fallback;
|
|
4
|
-
return String(v);
|
|
5
|
-
}
|
|
1
|
+
import { asString } from '../core/value-sanitize.ts';
|
|
6
2
|
|
|
7
3
|
type BncrElevatedConfigRoot = {
|
|
8
4
|
tools?: {
|
|
@@ -14,21 +10,18 @@ type BncrElevatedConfigRoot = {
|
|
|
14
10
|
};
|
|
15
11
|
};
|
|
16
12
|
};
|
|
17
|
-
|
|
18
|
-
export function getBncrElevatedConfig(rootCfg: BncrElevatedConfigRoot | null | undefined) {
|
|
13
|
+
function getBncrElevatedConfig(rootCfg: BncrElevatedConfigRoot | null | undefined) {
|
|
19
14
|
const elevated = rootCfg?.tools?.elevated || {};
|
|
20
15
|
const allowFrom = elevated?.allowFrom || {};
|
|
21
16
|
const bncrRules = Array.isArray(allowFrom?.bncr)
|
|
22
17
|
? allowFrom.bncr.map((x: unknown) => asString(x).trim()).filter(Boolean)
|
|
23
18
|
: [];
|
|
24
|
-
|
|
25
19
|
return {
|
|
26
20
|
enabled: elevated?.enabled === true,
|
|
27
21
|
bncrAllowed: bncrRules.length > 0,
|
|
28
22
|
bncrRules,
|
|
29
23
|
};
|
|
30
24
|
}
|
|
31
|
-
|
|
32
25
|
export function buildBncrPermissionSummary(rootCfg: BncrElevatedConfigRoot | null | undefined) {
|
|
33
26
|
const elevated = getBncrElevatedConfig(rootCfg);
|
|
34
27
|
return {
|
|
@@ -40,5 +33,4 @@ export function buildBncrPermissionSummary(rootCfg: BncrElevatedConfigRoot | nul
|
|
|
40
33
|
: 'bncr elevated not explicitly allowed',
|
|
41
34
|
};
|
|
42
35
|
}
|
|
43
|
-
|
|
44
36
|
export type BncrPermissionSummary = ReturnType<typeof buildBncrPermissionSummary>;
|
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
import { normalizeAccountId } from './accounts.ts';
|
|
2
2
|
import { normalizeStoredSessionKey, parseRouteLike } from './targets.ts';
|
|
3
3
|
import type { OutboxEntry } from './types.ts';
|
|
4
|
+
import { asString, finiteNumberOr } from './value-sanitize.ts';
|
|
4
5
|
|
|
5
6
|
type PersistedOutboxEntryInput = Partial<OutboxEntry> & {
|
|
6
7
|
payload?: Record<string, unknown>;
|
|
7
8
|
};
|
|
8
9
|
|
|
9
|
-
function asString(v: unknown, fallback = ''): string {
|
|
10
|
-
if (typeof v === 'string') return v;
|
|
11
|
-
if (v == null) return fallback;
|
|
12
|
-
return String(v);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function finiteNumberOr(value: unknown, fallback: number): number {
|
|
16
|
-
const n = Number(value);
|
|
17
|
-
return Number.isFinite(n) ? n : fallback;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
10
|
function optionalFiniteNumber(value: unknown): number | undefined {
|
|
21
11
|
if (value == null || value === '') return undefined;
|
|
22
12
|
const n = Number(value);
|
|
@@ -43,6 +33,59 @@ export function normalizePersistedOutboxEntry(args: {
|
|
|
43
33
|
payload.groupId = route.groupId;
|
|
44
34
|
payload.userId = route.userId;
|
|
45
35
|
|
|
36
|
+
// Migrate old _meta format entries to the new message format.
|
|
37
|
+
// Old entries persisted before the _meta→message refactor carry media
|
|
38
|
+
// metadata in payload._meta. New code reads from payload.message.
|
|
39
|
+
const oldMeta = payload._meta as Record<string, unknown> | undefined;
|
|
40
|
+
if (oldMeta && !payload.message) {
|
|
41
|
+
const oldKind = asString(oldMeta.kind);
|
|
42
|
+
if (oldKind === 'file-transfer') {
|
|
43
|
+
// Move replyToId from _meta to payload top level
|
|
44
|
+
const replyToId = asString(oldMeta.replyToId || '').trim() || undefined;
|
|
45
|
+
if (replyToId) payload.replyToId = replyToId;
|
|
46
|
+
|
|
47
|
+
// Build new message object from old _meta fields
|
|
48
|
+
const msg: Record<string, unknown> = {
|
|
49
|
+
type: (oldMeta.type as string) || 'file',
|
|
50
|
+
msg: oldMeta.text || '',
|
|
51
|
+
mediaUrl: oldMeta.mediaUrl,
|
|
52
|
+
mediaLocalRoots: oldMeta.mediaLocalRoots,
|
|
53
|
+
asVoice: oldMeta.asVoice === true,
|
|
54
|
+
audioAsVoice: oldMeta.audioAsVoice === true,
|
|
55
|
+
downloadMedia: oldMeta.downloadMedia === true,
|
|
56
|
+
kind: oldMeta.messageKind,
|
|
57
|
+
transferMode: 'media',
|
|
58
|
+
path: '',
|
|
59
|
+
base64: '',
|
|
60
|
+
fileName: '',
|
|
61
|
+
};
|
|
62
|
+
// Strip consumed _meta keys, pass through remaining as extra
|
|
63
|
+
const consumed = new Set([
|
|
64
|
+
'kind',
|
|
65
|
+
'mediaUrl',
|
|
66
|
+
'mediaLocalRoots',
|
|
67
|
+
'text',
|
|
68
|
+
'asVoice',
|
|
69
|
+
'audioAsVoice',
|
|
70
|
+
'replyToId',
|
|
71
|
+
'finalEvent',
|
|
72
|
+
'type',
|
|
73
|
+
'downloadMedia',
|
|
74
|
+
'messageKind',
|
|
75
|
+
'retryCount',
|
|
76
|
+
'nextAttemptAt',
|
|
77
|
+
]);
|
|
78
|
+
for (const [key, value] of Object.entries(oldMeta)) {
|
|
79
|
+
if (!consumed.has(key)) {
|
|
80
|
+
msg[key] = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
payload.message = msg;
|
|
84
|
+
}
|
|
85
|
+
// Remove old _meta after migration
|
|
86
|
+
delete payload._meta;
|
|
87
|
+
}
|
|
88
|
+
|
|
46
89
|
return {
|
|
47
90
|
...entry,
|
|
48
91
|
messageId: asString(entry.messageId).trim(),
|
package/src/core/policy.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
if (typeof v === 'string') return v;
|
|
3
|
-
if (v == null) return fallback;
|
|
4
|
-
return String(v);
|
|
5
|
-
}
|
|
1
|
+
import { asBoolean, asString } from './value-sanitize.ts';
|
|
6
2
|
|
|
7
3
|
export type BncrChannelPolicyConfig = {
|
|
8
4
|
enabled?: boolean;
|
|
@@ -21,12 +17,6 @@ function asList(v: unknown): string[] {
|
|
|
21
17
|
return v.map((x) => asString(x).trim()).filter(Boolean);
|
|
22
18
|
}
|
|
23
19
|
|
|
24
|
-
function asBoolean(v: unknown, fallback = false): boolean {
|
|
25
|
-
if (typeof v === 'boolean') return v;
|
|
26
|
-
if (v == null) return fallback;
|
|
27
|
-
return String(v).trim().toLowerCase() === 'true';
|
|
28
|
-
}
|
|
29
|
-
|
|
30
20
|
function normalizeDmPolicy(value: unknown): BncrDmPolicy {
|
|
31
21
|
const normalized = asString(value || 'open')
|
|
32
22
|
.trim()
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { finiteNumberOr } from './value-sanitize.ts';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
const n = Number(value);
|
|
5
|
-
return Number.isFinite(n) ? n : fallback;
|
|
6
|
-
}
|
|
3
|
+
const DEFAULT_REGISTER_WARMUP_WINDOW_MS = 30_000;
|
|
7
4
|
|
|
8
5
|
export type RegisterTraceEntry = {
|
|
9
6
|
ts: number;
|
|
@@ -46,7 +43,7 @@ export type RegisterDriftSnapshot = {
|
|
|
46
43
|
traceRecent: Array<Record<string, unknown>>;
|
|
47
44
|
};
|
|
48
45
|
|
|
49
|
-
|
|
46
|
+
function classifyRegisterTrace(stack: string) {
|
|
50
47
|
if (
|
|
51
48
|
stack.includes('prepareSecretsRuntimeSnapshot') ||
|
|
52
49
|
stack.includes('resolveRuntimeWebTools') ||
|
|
@@ -69,7 +66,7 @@ export function classifyRegisterTrace(stack: string) {
|
|
|
69
66
|
return 'other';
|
|
70
67
|
}
|
|
71
68
|
|
|
72
|
-
|
|
69
|
+
function dominantRegisterBucket(sourceBuckets: Record<string, number>) {
|
|
73
70
|
let winner: string | null = null;
|
|
74
71
|
let winnerCount = -1;
|
|
75
72
|
for (const [bucket, count] of Object.entries(sourceBuckets)) {
|
package/src/core/status-meta.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PendingAdmission } from './types.ts';
|
|
2
|
+
import { finiteNumberOrNull, now } from './value-sanitize.ts';
|
|
2
3
|
|
|
3
4
|
type RuntimeStatusMetaDisplayInput = {
|
|
4
5
|
pendingAdmissions?: PendingAdmission[];
|
|
@@ -8,15 +9,6 @@ type RuntimeStatusMetaDisplayInput = {
|
|
|
8
9
|
lastOutboundAt?: number | null;
|
|
9
10
|
};
|
|
10
11
|
|
|
11
|
-
function now() {
|
|
12
|
-
return Date.now();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function finiteNumberOrNull(value: unknown): number | null {
|
|
16
|
-
const n = Number(value);
|
|
17
|
-
return Number.isFinite(n) ? n : null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
12
|
function formatPendingAdmissionScope(route: unknown): string | null {
|
|
21
13
|
if (!route || typeof route !== 'object') return null;
|
|
22
14
|
const record = route as Record<string, unknown>;
|
package/src/core/status.ts
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { buildRuntimeStatusMetaDisplay } from './status-meta.ts';
|
|
4
4
|
import type { BncrDiagnosticsSummary, BncrRuntimeLastSession, PendingAdmission } from './types.ts';
|
|
5
|
+
import { finiteNumberOr, nonNegativeFiniteNumberOr, now } from './value-sanitize.ts';
|
|
5
6
|
|
|
6
7
|
type RuntimeStatusInput = {
|
|
7
8
|
accountId: string;
|
|
@@ -27,19 +28,6 @@ type RuntimeStatusInput = {
|
|
|
27
28
|
channelRoot?: string;
|
|
28
29
|
};
|
|
29
30
|
|
|
30
|
-
function now() {
|
|
31
|
-
return Date.now();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function finiteNumberOr(value: unknown, fallback: number): number {
|
|
35
|
-
const n = Number(value);
|
|
36
|
-
return Number.isFinite(n) ? n : fallback;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function nonNegativeFiniteNumberOr(value: unknown, fallback: number): number {
|
|
40
|
-
return Math.max(0, finiteNumberOr(value, fallback));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
31
|
export function buildIntegratedDiagnostics(input: RuntimeStatusInput): BncrDiagnosticsSummary {
|
|
44
32
|
const root = input.channelRoot || path.join(process.cwd(), 'plugins', 'bncr');
|
|
45
33
|
const pluginIndexExists = fs.existsSync(path.join(root, 'index.ts'));
|
|
@@ -230,11 +218,3 @@ export function buildAccountStatusSnapshot(input: {
|
|
|
230
218
|
diagnostics,
|
|
231
219
|
};
|
|
232
220
|
}
|
|
233
|
-
|
|
234
|
-
export function buildChannelSummaryFromRuntime(input: RuntimeStatusInput) {
|
|
235
|
-
const headline = buildStatusHeadlineFromRuntime(input);
|
|
236
|
-
return {
|
|
237
|
-
linked: input.connected,
|
|
238
|
-
self: { e164: headline },
|
|
239
|
-
};
|
|
240
|
-
}
|