@xmoxmo/bncr 0.5.6 → 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 +79 -43
- 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 +34 -18
- 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/parse.ts +1 -31
- package/src/messaging/inbound/scene-admin.ts +8 -8
- 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 +62 -28
- package/src/messaging/outbound/media-dedupe.ts +2 -6
- package/src/messaging/outbound/media.ts +1 -6
- 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.ts +121 -70
- 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 +6 -6
- package/src/plugin/bridge-media-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 +1 -0
- package/src/plugin/channel-runtime-types.ts +2 -24
- package/src/plugin/channel-send.ts +205 -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/media-dedupe-runtime.ts +1 -6
- package/src/plugin/message-send.ts +7 -8
- package/src/plugin/outbound.ts +6 -26
- 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 -114
- package/src/messaging/outbound/send.ts +0 -127
- package/src/openclaw/ingress-runtime.ts +0 -25
- package/src/plugin/channel-send-runtime-group.ts +0 -37
package/src/core/types.ts
CHANGED
|
@@ -21,8 +21,6 @@ export type BncrGatewayCapabilityFlags = {
|
|
|
21
21
|
inboundOnly: boolean;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
export type FileRecvTransferStatus = 'init' | 'transferring' | 'completed' | 'aborted';
|
|
25
|
-
|
|
26
24
|
type FileRecvTransferBase = {
|
|
27
25
|
transferId: string;
|
|
28
26
|
accountId: string;
|
|
@@ -105,21 +103,7 @@ export type OutboxEntry = {
|
|
|
105
103
|
accountId: string;
|
|
106
104
|
sessionKey: string;
|
|
107
105
|
route: BncrRoute;
|
|
108
|
-
payload: Record<string, unknown
|
|
109
|
-
_meta?: {
|
|
110
|
-
kind?: 'message' | 'file-transfer';
|
|
111
|
-
retryCount?: number;
|
|
112
|
-
nextAttemptAt?: number;
|
|
113
|
-
mediaUrl?: string;
|
|
114
|
-
mediaLocalRoots?: string[];
|
|
115
|
-
text?: string;
|
|
116
|
-
asVoice?: boolean;
|
|
117
|
-
audioAsVoice?: boolean;
|
|
118
|
-
replyToId?: string;
|
|
119
|
-
finalEvent?: string;
|
|
120
|
-
[key: string]: unknown;
|
|
121
|
-
};
|
|
122
|
-
};
|
|
106
|
+
payload: Record<string, unknown>;
|
|
123
107
|
createdAt: number;
|
|
124
108
|
retryCount: number;
|
|
125
109
|
nextAttemptAt: number;
|
|
@@ -1,15 +1,39 @@
|
|
|
1
|
-
export function
|
|
1
|
+
export function asString(value: unknown, fallback = ''): string {
|
|
2
2
|
if (typeof value === 'string') return value;
|
|
3
3
|
if (value == null) return fallback;
|
|
4
4
|
return String(value);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
/** Alias: same as asString, named for call-sites that want a sanitization signal. */
|
|
8
|
+
export const asSanitizedString = asString;
|
|
9
|
+
|
|
7
10
|
export function finiteNumberOr(value: unknown, fallback: number): number {
|
|
8
11
|
const n = Number(value);
|
|
9
12
|
return Number.isFinite(n) ? n : fallback;
|
|
10
13
|
}
|
|
11
14
|
|
|
15
|
+
export function now(): number {
|
|
16
|
+
return Date.now();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function asBoolean(value: unknown, fallback = false): boolean {
|
|
20
|
+
if (typeof value === 'boolean') return value;
|
|
21
|
+
if (typeof value === 'number') return value !== 0;
|
|
22
|
+
if (typeof value === 'string') {
|
|
23
|
+
const raw = value.trim().toLowerCase();
|
|
24
|
+
if (!raw) return fallback;
|
|
25
|
+
if (['true', '1', 'yes', 'y', 'on'].includes(raw)) return true;
|
|
26
|
+
if (['false', '0', 'no', 'n', 'off'].includes(raw)) return false;
|
|
27
|
+
}
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
32
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
12
35
|
export function finiteNumberOrNull(value: unknown): number | null {
|
|
36
|
+
if (value === null || value === undefined) return null;
|
|
13
37
|
const n = Number(value);
|
|
14
38
|
return Number.isFinite(n) ? n : null;
|
|
15
39
|
}
|
|
@@ -27,3 +51,16 @@ export function clampFiniteNumber(
|
|
|
27
51
|
const finite = finiteNumberOr(value, fallback);
|
|
28
52
|
return Math.max(min, Math.min(finite, max));
|
|
29
53
|
}
|
|
54
|
+
|
|
55
|
+
export function asStringArray(v: unknown): string[] {
|
|
56
|
+
if (Array.isArray(v)) {
|
|
57
|
+
return v.map((item) => asString(item).trim()).filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
if (typeof v === 'string') {
|
|
60
|
+
return v
|
|
61
|
+
.split(',')
|
|
62
|
+
.map((item) => item.trim())
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
}
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
@@ -7,10 +7,7 @@ import {
|
|
|
7
7
|
resolveBncrPinnedMainDmOwnerFromAllowlist,
|
|
8
8
|
} from '../../openclaw/inbound-session-runtime.ts';
|
|
9
9
|
import { dispatchOpenClawReplyWithBufferedBlockDispatcher } from '../../openclaw/reply-runtime.ts';
|
|
10
|
-
import {
|
|
11
|
-
type OpenClawResolvedAgentRoute,
|
|
12
|
-
resolveOpenClawAgentRoute,
|
|
13
|
-
} from '../../openclaw/routing-runtime.ts';
|
|
10
|
+
import { resolveOpenClawAgentRoute } from '../../openclaw/routing-runtime.ts';
|
|
14
11
|
import type {
|
|
15
12
|
BncrEnqueueFromReply,
|
|
16
13
|
BncrInboundApi,
|
|
@@ -18,7 +15,7 @@ import type {
|
|
|
18
15
|
BncrInboundLogger,
|
|
19
16
|
BncrRememberSessionRoute,
|
|
20
17
|
} from './contracts.ts';
|
|
21
|
-
import type
|
|
18
|
+
import { assertResolvedAgentRoute, type ParsedInbound } from './dispatch-prep.ts';
|
|
22
19
|
import { buildBncrInboundRecordUpdateLastRoute } from './last-route.ts';
|
|
23
20
|
import {
|
|
24
21
|
parseBncrNativeCommand,
|
|
@@ -50,25 +47,6 @@ import {
|
|
|
50
47
|
} from './session-label.ts';
|
|
51
48
|
import { createBncrSessionMetaTaskBarrier } from './session-meta-task.ts';
|
|
52
49
|
|
|
53
|
-
function assertResolvedAgentRoute(resolvedRoute: OpenClawResolvedAgentRoute): {
|
|
54
|
-
sessionKey: string;
|
|
55
|
-
agentId: string;
|
|
56
|
-
mainSessionKey?: string;
|
|
57
|
-
} {
|
|
58
|
-
const sessionKey =
|
|
59
|
-
typeof resolvedRoute.sessionKey === 'string' ? resolvedRoute.sessionKey.trim() : '';
|
|
60
|
-
const agentId = typeof resolvedRoute.agentId === 'string' ? resolvedRoute.agentId.trim() : '';
|
|
61
|
-
if (!sessionKey) throw new Error('OpenClaw resolveAgentRoute returned empty sessionKey');
|
|
62
|
-
if (!agentId) throw new Error('OpenClaw resolveAgentRoute returned empty agentId');
|
|
63
|
-
return {
|
|
64
|
-
sessionKey,
|
|
65
|
-
agentId,
|
|
66
|
-
...(typeof resolvedRoute.mainSessionKey === 'string' && resolvedRoute.mainSessionKey.trim()
|
|
67
|
-
? { mainSessionKey: resolvedRoute.mainSessionKey }
|
|
68
|
-
: {}),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
50
|
function buildBncrNativeCommandResolvedRoute(args: {
|
|
73
51
|
api: BncrInboundApi;
|
|
74
52
|
cfg: BncrInboundConfig;
|
|
@@ -95,7 +95,7 @@ const BNCR_GENERIC_MEDIA_TEXTS = new Set([
|
|
|
95
95
|
'[文件]',
|
|
96
96
|
]);
|
|
97
97
|
|
|
98
|
-
function assertResolvedAgentRoute(resolvedRoute: OpenClawResolvedAgentRoute): {
|
|
98
|
+
export function assertResolvedAgentRoute(resolvedRoute: OpenClawResolvedAgentRoute): {
|
|
99
99
|
sessionKey: string;
|
|
100
100
|
agentId: string;
|
|
101
101
|
mainSessionKey?: string;
|
|
@@ -6,36 +6,7 @@ export type BncrGateResult = { allowed: true } | { allowed: false; reason: strin
|
|
|
6
6
|
const REQUIRED_PROTOCOL_VERSION = 'scene-routing-v1';
|
|
7
7
|
const REQUIRED_CAPABILITY = 'scene-routing-v1';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
if (typeof v === 'string') return v;
|
|
11
|
-
if (v == null) return fallback;
|
|
12
|
-
return String(v);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function asBoolean(v: unknown, fallback = false): boolean {
|
|
16
|
-
if (typeof v === 'boolean') return v;
|
|
17
|
-
if (typeof v === 'number') return v !== 0;
|
|
18
|
-
if (typeof v === 'string') {
|
|
19
|
-
const raw = v.trim().toLowerCase();
|
|
20
|
-
if (!raw) return fallback;
|
|
21
|
-
if (['true', '1', 'yes', 'y', 'on'].includes(raw)) return true;
|
|
22
|
-
if (['false', '0', 'no', 'n', 'off'].includes(raw)) return false;
|
|
23
|
-
}
|
|
24
|
-
return fallback;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function asCapabilities(v: unknown): string[] {
|
|
28
|
-
if (Array.isArray(v)) {
|
|
29
|
-
return v.map((item) => asString(item).trim()).filter(Boolean);
|
|
30
|
-
}
|
|
31
|
-
if (typeof v === 'string') {
|
|
32
|
-
return v
|
|
33
|
-
.split(',')
|
|
34
|
-
.map((item) => item.trim())
|
|
35
|
-
.filter(Boolean);
|
|
36
|
-
}
|
|
37
|
-
return [];
|
|
38
|
-
}
|
|
9
|
+
import { asBoolean, asString, asStringArray } from '../../core/value-sanitize.ts';
|
|
39
10
|
|
|
40
11
|
export async function checkBncrMessageGate(params: {
|
|
41
12
|
parsed: BncrInboundParamsInput & {
|
|
@@ -57,7 +28,7 @@ export async function checkBncrMessageGate(params: {
|
|
|
57
28
|
}
|
|
58
29
|
|
|
59
30
|
const protocolVersion = asString(parsed?.protocolVersion || '').trim();
|
|
60
|
-
const capabilities =
|
|
31
|
+
const capabilities = asStringArray(parsed?.capabilities);
|
|
61
32
|
if (
|
|
62
33
|
protocolVersion !== REQUIRED_PROTOCOL_VERSION ||
|
|
63
34
|
!capabilities.includes(REQUIRED_CAPABILITY)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import {
|
|
3
2
|
createChannelHistoryWindow,
|
|
4
3
|
DEFAULT_GROUP_HISTORY_LIMIT,
|
|
@@ -245,80 +244,6 @@ export function collectBncrHistoryMediaFromEntries(args: {
|
|
|
245
244
|
}): HistoryMediaEntry[] {
|
|
246
245
|
return args.entries.flatMap((entry) => (Array.isArray(entry.media) ? entry.media : []));
|
|
247
246
|
}
|
|
248
|
-
|
|
249
|
-
function toBncrPromptMediaPath(value: string | undefined): string | undefined {
|
|
250
|
-
const normalized = String(value || '').trim();
|
|
251
|
-
if (!normalized) return undefined;
|
|
252
|
-
const slashNormalized = normalized.replace(/\\/g, '/');
|
|
253
|
-
if (!slashNormalized.includes('/media/inbound/')) return undefined;
|
|
254
|
-
const base = path.posix.basename(slashNormalized);
|
|
255
|
-
if (!base || base === '.' || base === '..') return undefined;
|
|
256
|
-
return `media://inbound/${encodeURIComponent(base)}`;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function buildHistoryWindowMediaSummary(
|
|
260
|
-
media: HistoryMediaEntry[] | undefined,
|
|
261
|
-
): string | undefined {
|
|
262
|
-
if (!Array.isArray(media) || media.length === 0) return undefined;
|
|
263
|
-
const kinds = media
|
|
264
|
-
.map((item) =>
|
|
265
|
-
String(item?.kind || '')
|
|
266
|
-
.trim()
|
|
267
|
-
.toLowerCase(),
|
|
268
|
-
)
|
|
269
|
-
.filter(Boolean);
|
|
270
|
-
const uniformKind = kinds[0] && kinds.every((item) => item === kinds[0]) ? kinds[0] : 'document';
|
|
271
|
-
const count = media.length;
|
|
272
|
-
if (uniformKind === 'image')
|
|
273
|
-
return count === 1 ? '<media:image>' : `<media:image> (${count} images)`;
|
|
274
|
-
if (uniformKind === 'video')
|
|
275
|
-
return count === 1 ? '<media:video>' : `<media:video> (${count} videos)`;
|
|
276
|
-
if (uniformKind === 'audio') {
|
|
277
|
-
return count === 1 ? '<media:audio>' : `<media:audio> (${count} audio attachments)`;
|
|
278
|
-
}
|
|
279
|
-
return count === 1 ? '<media:document>' : `<media:document> (${count} attachments)`;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function buildHistoryWindowMedias(media: HistoryMediaEntry[] | undefined) {
|
|
283
|
-
if (!Array.isArray(media) || media.length === 0) return [];
|
|
284
|
-
return media.flatMap((item) => {
|
|
285
|
-
const promptPath = toBncrPromptMediaPath(item?.path);
|
|
286
|
-
const payload = {
|
|
287
|
-
...(promptPath ? { path: promptPath } : {}),
|
|
288
|
-
...(item?.contentType ? { contentType: item.contentType } : {}),
|
|
289
|
-
...(item?.kind ? { kind: item.kind } : {}),
|
|
290
|
-
...(item?.messageId ? { messageId: item.messageId } : {}),
|
|
291
|
-
};
|
|
292
|
-
return Object.keys(payload).length > 0 ? [payload] : [];
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export function buildBncrHistoryWindowContextFromEntries(args: {
|
|
297
|
-
entries: readonly BncrHistoryEntry[];
|
|
298
|
-
channelId: string;
|
|
299
|
-
}) {
|
|
300
|
-
if (args.entries.length === 0) return undefined;
|
|
301
|
-
const messages = args.entries.map((entry) => ({
|
|
302
|
-
...(entry.messageId ? { messageId: entry.messageId } : {}),
|
|
303
|
-
sender: entry.sender,
|
|
304
|
-
senderId: entry.senderId,
|
|
305
|
-
...(typeof entry.timestamp === 'number' ? { timestampMs: entry.timestamp } : {}),
|
|
306
|
-
body: entry.body,
|
|
307
|
-
mediaSummary: buildHistoryWindowMediaSummary(entry.media),
|
|
308
|
-
medias: buildHistoryWindowMedias(entry.media),
|
|
309
|
-
}));
|
|
310
|
-
return {
|
|
311
|
-
label: 'Bncr history window',
|
|
312
|
-
source: args.channelId,
|
|
313
|
-
type: 'bncr.history_window',
|
|
314
|
-
payload: {
|
|
315
|
-
relation: 'before_current_message',
|
|
316
|
-
order: 'chronological',
|
|
317
|
-
messages,
|
|
318
|
-
},
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
247
|
export function clearBncrPendingGroupHistory(args: {
|
|
323
248
|
historyMap: BncrGroupHistoryMap;
|
|
324
249
|
parsed: ParsedInbound;
|
|
@@ -2,39 +2,9 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { normalizeAccountId } from '../../core/accounts.ts';
|
|
3
3
|
import { extractInlineTaskKey } from '../../core/targets.ts';
|
|
4
4
|
import type { BncrRoute } from '../../core/types.ts';
|
|
5
|
+
import { asBoolean, asString, asStringArray } from '../../core/value-sanitize.ts';
|
|
5
6
|
import type { BncrInboundMediaItem, BncrInboundParamsInput } from './contracts.ts';
|
|
6
7
|
|
|
7
|
-
function asString(v: unknown, fallback = ''): string {
|
|
8
|
-
if (typeof v === 'string') return v;
|
|
9
|
-
if (v == null) return fallback;
|
|
10
|
-
return String(v);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function asBoolean(v: unknown, fallback = false): boolean {
|
|
14
|
-
if (typeof v === 'boolean') return v;
|
|
15
|
-
if (typeof v === 'number') return v !== 0;
|
|
16
|
-
if (typeof v === 'string') {
|
|
17
|
-
const raw = v.trim().toLowerCase();
|
|
18
|
-
if (!raw) return fallback;
|
|
19
|
-
if (['true', '1', 'yes', 'y', 'on'].includes(raw)) return true;
|
|
20
|
-
if (['false', '0', 'no', 'n', 'off'].includes(raw)) return false;
|
|
21
|
-
}
|
|
22
|
-
return fallback;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function asStringArray(v: unknown): string[] {
|
|
26
|
-
if (Array.isArray(v)) {
|
|
27
|
-
return v.map((item) => asString(item).trim()).filter(Boolean);
|
|
28
|
-
}
|
|
29
|
-
if (typeof v === 'string') {
|
|
30
|
-
return v
|
|
31
|
-
.split(',')
|
|
32
|
-
.map((item) => item.trim())
|
|
33
|
-
.filter(Boolean);
|
|
34
|
-
}
|
|
35
|
-
return [];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
8
|
function asInboundMediaItems(params: BncrInboundParamsInput): BncrInboundMediaItem[] {
|
|
39
9
|
const normalized: BncrInboundMediaItem[] = [];
|
|
40
10
|
const rawList = Array.isArray(params?.mediaList) ? params.mediaList : [];
|
|
@@ -21,7 +21,7 @@ export type BncrSceneAdminCommand =
|
|
|
21
21
|
| { kind: 'history-force-set'; sceneKey: string; enabled: boolean | 'clear' }
|
|
22
22
|
| { kind: 'history-help' }
|
|
23
23
|
| { kind: 'download-media-get'; sceneKey?: string }
|
|
24
|
-
| { kind: 'download-media-set'; sceneKey: string; enabled: boolean }
|
|
24
|
+
| { kind: 'download-media-set'; sceneKey: string; enabled: boolean | undefined }
|
|
25
25
|
| { kind: 'download-media-global-get' }
|
|
26
26
|
| { kind: 'download-media-global-set'; enabled: boolean };
|
|
27
27
|
|
|
@@ -132,7 +132,7 @@ export function parseSceneAdminCommand(command: NativeCommand): ParsedSceneAdmin
|
|
|
132
132
|
return {
|
|
133
133
|
matched: true,
|
|
134
134
|
valid: true,
|
|
135
|
-
command: { kind: 'mode', sceneKey: '', mode: 'clear'
|
|
135
|
+
command: { kind: 'mode', sceneKey: '', mode: 'clear' },
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
138
|
if (args[0] === 'help') {
|
|
@@ -159,7 +159,7 @@ export function parseSceneAdminCommand(command: NativeCommand): ParsedSceneAdmin
|
|
|
159
159
|
return {
|
|
160
160
|
matched: true,
|
|
161
161
|
valid: true,
|
|
162
|
-
command: { kind: 'mode', sceneKey: args[1], mode: 'clear'
|
|
162
|
+
command: { kind: 'mode', sceneKey: args[1], mode: 'clear' },
|
|
163
163
|
};
|
|
164
164
|
}
|
|
165
165
|
if (GROUP_REPLY_MODES.has(args[0] as BncrGroupReplyMode) && args[1]) {
|
|
@@ -205,7 +205,7 @@ export function parseSceneAdminCommand(command: NativeCommand): ParsedSceneAdmin
|
|
|
205
205
|
return {
|
|
206
206
|
matched: true,
|
|
207
207
|
valid: true,
|
|
208
|
-
command: { kind: 'history-limit-set', sceneKey: '', limit: 'clear'
|
|
208
|
+
command: { kind: 'history-limit-set', sceneKey: '', limit: 'clear' },
|
|
209
209
|
};
|
|
210
210
|
}
|
|
211
211
|
const num = parseInt(args[0], 10);
|
|
@@ -227,7 +227,7 @@ export function parseSceneAdminCommand(command: NativeCommand): ParsedSceneAdmin
|
|
|
227
227
|
return {
|
|
228
228
|
matched: true,
|
|
229
229
|
valid: true,
|
|
230
|
-
command: { kind: 'history-limit-set', sceneKey: args[1], limit: 'clear'
|
|
230
|
+
command: { kind: 'history-limit-set', sceneKey: args[1], limit: 'clear' },
|
|
231
231
|
};
|
|
232
232
|
}
|
|
233
233
|
const num = parseInt(args[0], 10);
|
|
@@ -253,13 +253,13 @@ export function parseSceneAdminCommand(command: NativeCommand): ParsedSceneAdmin
|
|
|
253
253
|
return {
|
|
254
254
|
matched: true,
|
|
255
255
|
valid: true,
|
|
256
|
-
command: { kind: 'history-force-set', sceneKey: '', enabled: 'clear'
|
|
256
|
+
command: { kind: 'history-force-set', sceneKey: '', enabled: 'clear' },
|
|
257
257
|
};
|
|
258
258
|
}
|
|
259
259
|
return {
|
|
260
260
|
matched: true,
|
|
261
261
|
valid: true,
|
|
262
|
-
command: { kind: 'history-force-set', sceneKey: args[1], enabled: 'clear'
|
|
262
|
+
command: { kind: 'history-force-set', sceneKey: args[1], enabled: 'clear' },
|
|
263
263
|
};
|
|
264
264
|
}
|
|
265
265
|
if (args[0] === 'on' || args[0] === 'off') {
|
|
@@ -302,7 +302,7 @@ export function parseSceneAdminCommand(command: NativeCommand): ParsedSceneAdmin
|
|
|
302
302
|
command: {
|
|
303
303
|
kind: 'download-media-set',
|
|
304
304
|
sceneKey: args[1] || '',
|
|
305
|
-
enabled: undefined
|
|
305
|
+
enabled: undefined,
|
|
306
306
|
},
|
|
307
307
|
};
|
|
308
308
|
}
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
BncrOutboxQueueDiagnostics,
|
|
6
6
|
OutboxEntry,
|
|
7
7
|
} from '../../core/types.ts';
|
|
8
|
+
import { finiteNumberOrNull } from '../../core/value-sanitize.ts';
|
|
8
9
|
|
|
9
10
|
export {
|
|
10
11
|
buildFlushDebugInfo,
|
|
@@ -42,12 +43,6 @@ type OutboxIncidentSummaryInput = {
|
|
|
42
43
|
nowMs?: number;
|
|
43
44
|
};
|
|
44
45
|
|
|
45
|
-
function finiteNumberOrNull(value: unknown): number | null {
|
|
46
|
-
if (value == null) return null;
|
|
47
|
-
const n = Number(value);
|
|
48
|
-
return Number.isFinite(n) ? n : null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
46
|
function positiveAgeMs(nowMs: number, at: unknown): number | null {
|
|
52
47
|
const n = finiteNumberOrNull(at);
|
|
53
48
|
if (n === null || n < 0) return null;
|
|
@@ -8,7 +8,7 @@ export type BncrDurableQueuedReceipt = {
|
|
|
8
8
|
platformMessageId: string;
|
|
9
9
|
kind: 'text' | 'media' | 'voice' | 'unknown';
|
|
10
10
|
index: number;
|
|
11
|
-
threadId?: string;
|
|
11
|
+
threadId?: string | number | null;
|
|
12
12
|
replyToId?: string;
|
|
13
13
|
raw: {
|
|
14
14
|
channel: 'bncr';
|
|
@@ -20,7 +20,7 @@ export type BncrDurableQueuedReceipt = {
|
|
|
20
20
|
meta: BncrDurableQueuedReceiptMeta;
|
|
21
21
|
};
|
|
22
22
|
}>;
|
|
23
|
-
threadId?: string;
|
|
23
|
+
threadId?: string | number | null;
|
|
24
24
|
replyToId?: string;
|
|
25
25
|
sentAt: number;
|
|
26
26
|
raw: Array<{
|
|
@@ -66,8 +66,12 @@ export type BncrDurableQueuedResult = {
|
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
type OutboxPayload = OutboxEntry['payload'];
|
|
69
|
-
type
|
|
70
|
-
type
|
|
69
|
+
type OutboxMessagePayload = {
|
|
70
|
+
type?: unknown;
|
|
71
|
+
transferMode?: string;
|
|
72
|
+
asVoice?: boolean;
|
|
73
|
+
audioAsVoice?: boolean;
|
|
74
|
+
};
|
|
71
75
|
|
|
72
76
|
function getOutboxPayload(entry: OutboxEntry): OutboxPayload {
|
|
73
77
|
return entry.payload;
|
|
@@ -76,7 +80,7 @@ function getOutboxPayload(entry: OutboxEntry): OutboxPayload {
|
|
|
76
80
|
export function buildBncrDurableQueuedResult(args: {
|
|
77
81
|
entry: OutboxEntry;
|
|
78
82
|
index?: number;
|
|
79
|
-
threadId?: string;
|
|
83
|
+
threadId?: string | number | null;
|
|
80
84
|
replyToId?: string;
|
|
81
85
|
sentAt?: number;
|
|
82
86
|
}): BncrDurableQueuedResult {
|
|
@@ -145,20 +149,22 @@ function extractPayloadType(entry: OutboxEntry): string | undefined {
|
|
|
145
149
|
|
|
146
150
|
function extractReplyToId(entry: OutboxEntry): string | undefined {
|
|
147
151
|
const payload = getOutboxPayload(entry);
|
|
148
|
-
const
|
|
149
|
-
|
|
152
|
+
const message =
|
|
153
|
+
payload?.message && typeof payload.message === 'object' && !Array.isArray(payload.message)
|
|
154
|
+
? (payload.message as Record<string, unknown>)
|
|
155
|
+
: null;
|
|
156
|
+
const replyToId = payload?.replyToId ?? (message?.replyToId as string | undefined);
|
|
150
157
|
return typeof replyToId === 'string' ? replyToId : undefined;
|
|
151
158
|
}
|
|
152
159
|
|
|
153
160
|
function inferReceiptKind(entry: OutboxEntry): 'text' | 'media' | 'voice' | 'unknown' {
|
|
154
161
|
const payload = getOutboxPayload(entry);
|
|
155
|
-
const meta = payload?._meta as OutboxMeta | undefined;
|
|
156
162
|
const message =
|
|
157
|
-
payload
|
|
163
|
+
payload?.message && typeof payload.message === 'object'
|
|
158
164
|
? (payload.message as OutboxMessagePayload)
|
|
159
165
|
: null;
|
|
160
|
-
if (
|
|
161
|
-
if (
|
|
166
|
+
if (message?.transferMode === 'media') {
|
|
167
|
+
if (message?.asVoice === true || message?.audioAsVoice === true) return 'voice';
|
|
162
168
|
return 'media';
|
|
163
169
|
}
|
|
164
170
|
if (message?.type === 'text') return 'text';
|
|
@@ -6,29 +6,30 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Format: [BncrParam:{"key":"val","asVoice":true}]
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* - Complete marker whose JSON parses successfully → stripped, params merged.
|
|
10
|
+
* - Complete marker whose JSON fails to parse → left in text (not stripped)
|
|
11
|
+
* so the user can see and fix the bad payload.
|
|
12
|
+
* - Incomplete marker (no closing ]) → left in text, does NOT block later
|
|
13
|
+
* complete markers.
|
|
14
|
+
* - Multiple valid markers → all stripped, params merge (later same-key wins).
|
|
11
15
|
*/
|
|
12
16
|
|
|
13
17
|
const PREFIX = '[BncrParam:';
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const start = idx + PREFIX.length;
|
|
23
|
-
if (start >= text.length) return { cleanText: text, params: {} };
|
|
19
|
+
/**
|
|
20
|
+
* Locate the closing `]` of a complete marker starting at `prefixIdx`.
|
|
21
|
+
* Returns the index of the closing `]`, or -1 if incomplete.
|
|
22
|
+
*/
|
|
23
|
+
function findMarkerEnd(text: string, prefixIdx: number): number {
|
|
24
|
+
const start = prefixIdx + PREFIX.length;
|
|
25
|
+
if (start >= text.length) return -1;
|
|
24
26
|
|
|
25
27
|
// Scan character-by-character tracking brace/bracket depth so that
|
|
26
|
-
// ] inside JSON values (e.g. CDATA
|
|
28
|
+
// ] inside JSON values (e.g. CDATA / lyric tags) is not mistaken for
|
|
27
29
|
// the closing marker bracket.
|
|
28
30
|
let depth = 0;
|
|
29
31
|
let inString = false;
|
|
30
32
|
let esc = false;
|
|
31
|
-
let end = -1;
|
|
32
33
|
let started = false;
|
|
33
34
|
|
|
34
35
|
for (let i = start; i < text.length; i++) {
|
|
@@ -63,29 +64,62 @@ export function parseBncrMarker(text: string): {
|
|
|
63
64
|
if (depth === 0) {
|
|
64
65
|
// JSON content complete - expect ] to close the marker
|
|
65
66
|
if (i + 1 < text.length && text[i + 1] === ']') {
|
|
66
|
-
|
|
67
|
+
return i + 1;
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
+
return -1;
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
return -1;
|
|
76
|
+
}
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
export function parseBncrMarker(text: string): {
|
|
79
|
+
cleanText: string;
|
|
80
|
+
params: Record<string, unknown>;
|
|
81
|
+
} {
|
|
82
|
+
const params: Record<string, unknown> = {};
|
|
83
|
+
let result = text;
|
|
84
|
+
let searchFrom = 0;
|
|
85
|
+
|
|
86
|
+
while (true) {
|
|
87
|
+
const idx = result.indexOf(PREFIX, searchFrom);
|
|
88
|
+
if (idx === -1) break;
|
|
89
|
+
|
|
90
|
+
const end = findMarkerEnd(result, idx);
|
|
91
|
+
if (end === -1) {
|
|
92
|
+
// Incomplete marker (missing ]): leave in place,
|
|
93
|
+
// keep looking after this PREFIX so later valid markers are not blocked.
|
|
94
|
+
searchFrom = idx + PREFIX.length;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
78
97
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
98
|
+
const rawJson = result.slice(idx + PREFIX.length, end);
|
|
99
|
+
let parsedOk = false;
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(rawJson);
|
|
102
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
103
|
+
Object.assign(params, parsed as Record<string, unknown>);
|
|
104
|
+
parsedOk = true;
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// JSON parse failed — do NOT strip marker so the user can debug it
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (parsedOk) {
|
|
111
|
+
// Strip the complete valid marker
|
|
112
|
+
result = result.slice(0, idx) + result.slice(end + 1);
|
|
113
|
+
searchFrom = idx;
|
|
114
|
+
} else {
|
|
115
|
+
// Leave the invalid marker in place, continue after it
|
|
116
|
+
searchFrom = end + 1;
|
|
83
117
|
}
|
|
84
|
-
} catch {
|
|
85
|
-
// Invalid JSON — remove marker but don't apply params
|
|
86
118
|
}
|
|
87
119
|
|
|
88
|
-
|
|
120
|
+
// Only trim whitespace when markers were actually stripped (result changed).
|
|
121
|
+
// Without markers the original text is returned as-is for maximum fidelity.
|
|
122
|
+
const cleanText = result !== text ? result.replace(/ {2,}/g, ' ').trim() : result;
|
|
89
123
|
return { cleanText, params };
|
|
90
124
|
}
|
|
91
125
|
|
|
@@ -103,6 +137,7 @@ const MEDIA_CONSUMPTION_TYPES = new Set(['file', 'image', 'video', 'audio', 'voi
|
|
|
103
137
|
* based on whether its value is a recognized media type.
|
|
104
138
|
*/
|
|
105
139
|
const CONSUMPTION_FIELDS_NO_TYPE = new Set([
|
|
140
|
+
'ismedia',
|
|
106
141
|
'asVoice',
|
|
107
142
|
'audioAsVoice',
|
|
108
143
|
'kind',
|
|
@@ -110,8 +145,6 @@ const CONSUMPTION_FIELDS_NO_TYPE = new Set([
|
|
|
110
145
|
'downloadMedia',
|
|
111
146
|
]);
|
|
112
147
|
|
|
113
|
-
const CONSUMPTION_FIELDS = new Set([...CONSUMPTION_FIELDS_NO_TYPE]);
|
|
114
|
-
|
|
115
148
|
export function extractConsumptionFields(extra: Record<string, unknown> | undefined): {
|
|
116
149
|
consumed: Partial<{
|
|
117
150
|
asVoice: boolean;
|
|
@@ -120,6 +153,7 @@ export function extractConsumptionFields(extra: Record<string, unknown> | undefi
|
|
|
120
153
|
kind: string;
|
|
121
154
|
replyToId: string;
|
|
122
155
|
downloadMedia: boolean;
|
|
156
|
+
ismedia: boolean;
|
|
123
157
|
}>;
|
|
124
158
|
remaining: Record<string, unknown>;
|
|
125
159
|
} {
|
|
@@ -140,7 +174,7 @@ export function extractConsumptionFields(extra: Record<string, unknown> | undefi
|
|
|
140
174
|
}
|
|
141
175
|
continue;
|
|
142
176
|
}
|
|
143
|
-
if (
|
|
177
|
+
if (CONSUMPTION_FIELDS_NO_TYPE.has(key)) {
|
|
144
178
|
consumed[key] = value;
|
|
145
179
|
} else {
|
|
146
180
|
remaining[key] = value;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { asString } from '../../core/value-sanitize.ts';
|
|
2
|
+
|
|
1
3
|
export type MediaDedupeCacheEntry = {
|
|
2
4
|
mediaUrl: string;
|
|
3
5
|
text: string;
|
|
@@ -5,12 +7,6 @@ export type MediaDedupeCacheEntry = {
|
|
|
5
7
|
createdAt: number;
|
|
6
8
|
};
|
|
7
9
|
|
|
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
10
|
export function normalizeReplyToId(value: unknown): string {
|
|
15
11
|
return asString(value || '').trim();
|
|
16
12
|
}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
+
import { asString } from '../../core/value-sanitize.ts';
|
|
1
2
|
import { normalizeOutboundReplyToId } from './reply-target-policy.ts';
|
|
2
3
|
|
|
3
4
|
type BncrOutboundMessageType = 'text' | 'image' | 'video' | 'voice' | 'audio' | 'file';
|
|
4
5
|
|
|
5
|
-
function asString(v: unknown, fallback = ''): string {
|
|
6
|
-
if (typeof v === 'string') return v;
|
|
7
|
-
if (v == null) return fallback;
|
|
8
|
-
return String(v);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
6
|
function isBncrOutboundMessageType(value: string): value is BncrOutboundMessageType {
|
|
12
7
|
return (
|
|
13
8
|
value === 'text' ||
|