@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.
Files changed (90) hide show
  1. package/dist/index.js +1 -9
  2. package/index.ts +1 -9
  3. package/package.json +1 -1
  4. package/scripts/check-pack.mjs +0 -8
  5. package/scripts/selfcheck.mjs +0 -10
  6. package/src/bootstrap/runtime-loader.ts +1 -1
  7. package/src/channel.ts +118 -171
  8. package/src/core/accounts.ts +1 -6
  9. package/src/core/connection-reachability.ts +2 -6
  10. package/src/core/dead-letter-diagnostics.ts +4 -10
  11. package/src/core/logging.ts +1 -1
  12. package/src/core/outbox-entry-builders.ts +78 -84
  13. package/src/core/outbox-file-transfer-failure.ts +5 -0
  14. package/src/core/outbox-file-transfer-guards.ts +6 -0
  15. package/src/core/outbox-file-transfer-prep.ts +8 -5
  16. package/src/core/outbox-file-transfer-success.ts +5 -0
  17. package/src/core/outbox-queue.ts +1 -7
  18. package/src/core/outbox-summary.ts +10 -56
  19. package/src/core/permissions.ts +2 -10
  20. package/src/core/persisted-outbox-entry.ts +54 -11
  21. package/src/core/policy.ts +1 -11
  22. package/src/core/register-trace.ts +4 -7
  23. package/src/core/status-meta.ts +1 -9
  24. package/src/core/status.ts +1 -21
  25. package/src/core/targets.ts +52 -70
  26. package/src/core/types.ts +1 -17
  27. package/src/core/value-sanitize.ts +38 -1
  28. package/src/messaging/inbound/commands.ts +2 -24
  29. package/src/messaging/inbound/dispatch-prep.ts +1 -1
  30. package/src/messaging/inbound/gate.ts +2 -31
  31. package/src/messaging/inbound/group-history.ts +0 -75
  32. package/src/messaging/inbound/parse.ts +2 -32
  33. package/src/messaging/inbound/scene-admin.ts +8 -8
  34. package/src/messaging/outbound/diagnostics-debug-builders.ts +0 -15
  35. package/src/messaging/outbound/diagnostics.ts +1 -7
  36. package/src/messaging/outbound/durable-queue-adapter.ts +17 -11
  37. package/src/messaging/outbound/marker-parser.ts +277 -28
  38. package/src/messaging/outbound/media-dedupe.ts +3 -10
  39. package/src/messaging/outbound/media.ts +1 -6
  40. package/src/messaging/outbound/normalize-outbound-send.ts +329 -0
  41. package/src/messaging/outbound/queue-selectors.ts +1 -5
  42. package/src/messaging/outbound/reply-enqueue-media.ts +1 -184
  43. package/src/messaging/outbound/reply-enqueue.ts +313 -175
  44. package/src/messaging/outbound/session-route.ts +6 -11
  45. package/src/messaging/outbound/target-resolver.ts +1 -6
  46. package/src/openclaw/media-runtime.ts +1 -1
  47. package/src/openclaw/routing-runtime.ts +0 -11
  48. package/src/openclaw/sdk-helpers.ts +0 -2
  49. package/src/plugin/bridge-media-facade.ts +0 -4
  50. package/src/plugin/bridge-outbox-facade.ts +0 -1
  51. package/src/plugin/channel-components.ts +0 -13
  52. package/src/plugin/channel-plugin-surface-group.ts +52 -65
  53. package/src/plugin/channel-runtime-builders-delivery.ts +3 -362
  54. package/src/plugin/channel-runtime-builders-status.ts +0 -160
  55. package/src/plugin/channel-runtime-builders.ts +3 -17
  56. package/src/plugin/channel-runtime-constants.ts +1 -1
  57. package/src/plugin/channel-runtime-types.ts +2 -24
  58. package/src/plugin/channel-send.ts +206 -221
  59. package/src/plugin/channel-utils.ts +8 -29
  60. package/src/plugin/connection-handlers-helpers.ts +0 -15
  61. package/src/plugin/connection-handlers.ts +6 -5
  62. package/src/plugin/file-inbound-abort.ts +5 -0
  63. package/src/plugin/file-inbound-chunk.ts +5 -0
  64. package/src/plugin/file-inbound-complete.ts +5 -0
  65. package/src/plugin/file-transfer-orchestrator.ts +39 -2
  66. package/src/plugin/media-dedupe-runtime.ts +1 -6
  67. package/src/plugin/media-orchestrators-runtime-group.ts +20 -149
  68. package/src/plugin/message-send.ts +7 -8
  69. package/src/plugin/messaging.ts +3 -3
  70. package/src/plugin/outbound.ts +6 -26
  71. package/src/plugin/outbox-drain-loop.ts +2 -2
  72. package/src/plugin/outbox-push.ts +1 -52
  73. package/src/plugin/outbox-unified-push-flow.ts +307 -0
  74. package/src/plugin/scene-registry.ts +1 -1
  75. package/src/plugin/target-runtime.ts +1 -6
  76. package/src/runtime/log-dedupe.ts +3 -3
  77. package/src/runtime/outbound-ack-timeout.ts +3 -12
  78. package/src/runtime/outbox-transitions.ts +1 -4
  79. package/src/runtime/status-snapshots.ts +2 -2
  80. package/src/runtime/status-worker.ts +3 -15
  81. package/src/core/outbox-text-push-prep.ts +0 -36
  82. package/src/messaging/outbound/build-send-action.ts +0 -130
  83. package/src/messaging/outbound/durable-message-adapter.ts +0 -121
  84. package/src/messaging/outbound/send-params.ts +0 -114
  85. package/src/messaging/outbound/send.ts +0 -127
  86. package/src/openclaw/ingress-runtime.ts +0 -25
  87. package/src/plugin/bridge-file-transfer-push-facade.ts +0 -142
  88. package/src/plugin/channel-send-runtime-group.ts +0 -37
  89. package/src/plugin/outbox-file-push-flow.ts +0 -69
  90. package/src/plugin/outbox-text-push-flow.ts +0 -90
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Unified outbound send normalization.
3
+ *
4
+ * All send entry points (tool action, channel.send, channel.message)
5
+ * fold into one shape first, then branch media vs text.
6
+ *
7
+ * Priority for control fields: marker/text-block > host/params extra > direct params.
8
+ * Marker-specified type is always preserved (in normalized.type or re-injected into extra
9
+ * for text branch) and never silently downgraded.
10
+ *
11
+ * Marker path/paths are media evidence unless ismedia is explicitly false.
12
+ */
13
+
14
+ import { asString, isPlainObject } from '../../core/value-sanitize.ts';
15
+ import { extractConsumptionFields, resolveMarkerStripDecision } from './marker-parser.ts';
16
+
17
+ const MEDIA_SOURCE_EXTRA_KEYS = ['path', 'paths', 'mediaUrl', 'mediaUrls'] as const;
18
+
19
+ export type UnifiedOutboundSendInput = {
20
+ /** Primary text body (channel text / tool message). */
21
+ text?: string;
22
+ message?: string;
23
+ caption?: string;
24
+ mediaUrl?: string;
25
+ media?: string;
26
+ path?: string;
27
+ filePath?: string;
28
+ mediaUrls?: unknown;
29
+ asVoice: boolean;
30
+ audioAsVoice: boolean;
31
+ downloadMedia?: boolean;
32
+ type?: string;
33
+ kind?: string;
34
+ replyToId?: string;
35
+ replyToMessageId?: string;
36
+ extra?: Record<string, unknown>;
37
+ forceDocument?: boolean;
38
+ gifPlayback?: boolean;
39
+ silent?: boolean;
40
+ };
41
+
42
+ export type NormalizedOutboundSend = {
43
+ markerHasMsg?: boolean;
44
+ text: string;
45
+ mediaUrl?: string;
46
+ mediaUrls?: string[];
47
+ asVoice: boolean;
48
+ audioAsVoice: boolean;
49
+ downloadMedia?: boolean;
50
+ type?: string;
51
+ hasMedia?: boolean;
52
+ extra?: Record<string, unknown>;
53
+ kind?: 'tool' | 'block' | 'final';
54
+ replyToId?: string;
55
+ };
56
+
57
+ function asTrimmedStringList(value: unknown): string[] | undefined {
58
+ if (!Array.isArray(value)) return undefined;
59
+ const list = value.map((v) => (typeof v === 'string' ? v.trim() : '')).filter(Boolean);
60
+ return list.length > 0 ? list : undefined;
61
+ }
62
+
63
+ function normalizeReplyKind(value: unknown): 'tool' | 'block' | 'final' | undefined {
64
+ return value === 'tool' || value === 'block' || value === 'final' ? value : undefined;
65
+ }
66
+
67
+ /** Strip media-source keys so they cannot override resolved media frame fields. */
68
+ function stripMediaSourceFieldsFromExtra(
69
+ extra: Record<string, unknown> | undefined,
70
+ ): Record<string, unknown> | undefined {
71
+ if (!extra) return undefined;
72
+ const next: Record<string, unknown> = { ...extra };
73
+ for (const key of MEDIA_SOURCE_EXTRA_KEYS) {
74
+ delete next[key];
75
+ }
76
+ return Object.keys(next).length > 0 ? next : undefined;
77
+ }
78
+
79
+ /**
80
+ * Build extra for the chosen branch.
81
+ * - media: strip media-source keys (path already resolved into mediaUrl)
82
+ * - text: re-inject consumed media type for adapter passthrough when present
83
+ */
84
+ function buildBranchExtra(args: {
85
+ hasMedia?: boolean;
86
+ extra?: Record<string, unknown>;
87
+ consumedType?: string;
88
+ }): Record<string, unknown> | undefined {
89
+ if (args.hasMedia) {
90
+ return stripMediaSourceFieldsFromExtra(args.extra);
91
+ }
92
+ const textExtra =
93
+ args.consumedType && args.extra
94
+ ? { ...args.extra, type: args.consumedType }
95
+ : args.consumedType
96
+ ? { type: args.consumedType }
97
+ : args.extra
98
+ ? { ...args.extra }
99
+ : undefined;
100
+ return textExtra && Object.keys(textExtra).length > 0 ? textExtra : undefined;
101
+ }
102
+
103
+ /**
104
+ * Parse markers from one or more text fields and merge extras.
105
+ * Marker params always override host/params extra and host control flags.
106
+ */
107
+ export async function mergeMarkerAndHostFields(args: {
108
+ texts: string[];
109
+ hostExtra?: Record<string, unknown>;
110
+ forceDocument?: boolean;
111
+ gifPlayback?: boolean;
112
+ silent?: boolean;
113
+ }): Promise<{
114
+ cleanTexts: string[];
115
+ markerParams: Record<string, unknown>;
116
+ consumed: Partial<{
117
+ asVoice: boolean;
118
+ audioAsVoice: boolean;
119
+ type: string;
120
+ kind: string;
121
+ replyToId: string;
122
+ downloadMedia: boolean;
123
+ ismedia: boolean;
124
+ }>;
125
+ remaining: Record<string, unknown>;
126
+ markerHasMsg: boolean;
127
+ }> {
128
+ const markerParams: Record<string, unknown> = {};
129
+ const cleanTexts: string[] = [];
130
+
131
+ for (const raw of args.texts) {
132
+ const { cleanText, params } = await resolveMarkerStripDecision(raw);
133
+ cleanTexts.push(cleanText);
134
+ Object.assign(markerParams, params);
135
+ }
136
+
137
+ const merged: Record<string, unknown> = {
138
+ ...(isPlainObject(args.hostExtra) ? { ...args.hostExtra } : {}),
139
+ ...markerParams,
140
+ };
141
+ if (args.forceDocument === true) merged.forceDocument = true;
142
+ if (args.gifPlayback === true) merged.gifPlayback = true;
143
+ if (args.silent === true) merged.silent = true;
144
+
145
+ // Marker wins over host force* when marker also set them (already via Object.assign order)
146
+
147
+ const { consumed, remaining } = extractConsumptionFields(
148
+ Object.keys(merged).length > 0 ? merged : undefined,
149
+ );
150
+
151
+ return {
152
+ cleanTexts,
153
+ markerParams,
154
+ consumed,
155
+ remaining,
156
+ markerHasMsg: typeof markerParams.msg === 'string',
157
+ };
158
+ }
159
+
160
+ function resolveHostMediaUrl(input: UnifiedOutboundSendInput): string {
161
+ return (
162
+ (typeof input.media === 'string' ? input.media : '') ||
163
+ (typeof input.path === 'string' ? input.path : '') ||
164
+ (typeof input.filePath === 'string' ? input.filePath : '') ||
165
+ (typeof input.mediaUrl === 'string' ? input.mediaUrl : '')
166
+ );
167
+ }
168
+
169
+ /**
170
+ * Resolve media sources after marker merge.
171
+ * Host mediaUrl/mediaUrls first; marker path/paths next.
172
+ * Marker path/paths are media evidence unless ismedia is explicitly false.
173
+ */
174
+ function resolveDispatchMediaSources(args: {
175
+ hostMediaUrl?: string;
176
+ hostMediaUrls?: unknown;
177
+ remainingExtra: Record<string, unknown>;
178
+ consumedType?: string;
179
+ /** Tri-state: true=force, false=skip (ismedia explicitly false), undefined=default (path is media evidence). */
180
+ forceMedia?: boolean;
181
+ }): {
182
+ mediaUrl?: string;
183
+ mediaUrls?: string[];
184
+ hasMedia?: boolean;
185
+ } {
186
+ const hostMediaUrl = asString(args.hostMediaUrl || '').trim();
187
+ const markerPath = asString(
188
+ (args.remainingExtra.path as string) || (args.remainingExtra.mediaUrl as string) || '',
189
+ ).trim();
190
+ const markerPaths =
191
+ asTrimmedStringList(args.remainingExtra.paths) ||
192
+ asTrimmedStringList(args.remainingExtra.mediaUrls);
193
+ const useMarkerPath = Boolean(markerPath) && args.forceMedia !== false;
194
+
195
+ const singleFromHost = hostMediaUrl || undefined;
196
+ const multiFromHost = asTrimmedStringList(args.hostMediaUrls);
197
+ const mediaUrls = multiFromHost || markerPaths;
198
+ let mediaUrl = singleFromHost || (useMarkerPath ? markerPath : undefined);
199
+
200
+ // If mediaUrl is already in mediaUrls, drop single to avoid double-send.
201
+ if (mediaUrl && mediaUrls?.includes(mediaUrl)) {
202
+ mediaUrl = undefined;
203
+ } else if (mediaUrl && mediaUrls?.length) {
204
+ // Keep both: single + multi as mediaUrls list with single first
205
+ const merged = [mediaUrl, ...mediaUrls.filter((u) => u !== mediaUrl)];
206
+ return {
207
+ mediaUrl: undefined,
208
+ mediaUrls: merged,
209
+ hasMedia: true,
210
+ };
211
+ }
212
+
213
+ const hasMedia = Boolean(mediaUrl || mediaUrls?.length) || args.forceMedia === true;
214
+ return {
215
+ mediaUrl,
216
+ mediaUrls,
217
+ hasMedia,
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Normalize any outbound send input into a single dispatch shape.
223
+ * Callers only need to branch on `hasMedia` after this.
224
+ */
225
+ export async function normalizeOutboundSend(
226
+ input: UnifiedOutboundSendInput,
227
+ ): Promise<NormalizedOutboundSend> {
228
+ const rawText = asString(input.text ?? input.message ?? '', '');
229
+ const rawCaption = asString(input.caption ?? '', '');
230
+ // Prefer text/message; also parse caption so marker in either field works.
231
+ const textsToParse = rawCaption && rawCaption !== rawText ? [rawText, rawCaption] : [rawText];
232
+
233
+ const { cleanTexts, consumed, remaining, markerHasMsg } = await mergeMarkerAndHostFields({
234
+ texts: textsToParse,
235
+ hostExtra: isPlainObject(input.extra) ? input.extra : undefined,
236
+ forceDocument: input.forceDocument,
237
+ gifPlayback: input.gifPlayback,
238
+ silent: input.silent,
239
+ });
240
+
241
+ const cleanText = cleanTexts[0] || '';
242
+ const cleanCaption = cleanTexts.length > 1 ? cleanTexts[1] || '' : '';
243
+
244
+ const forceMedia = consumed.ismedia;
245
+ const consumedType = typeof consumed.type === 'string' ? consumed.type : undefined;
246
+ const hostType = asString(input.type || '').trim() || undefined;
247
+
248
+ const media = resolveDispatchMediaSources({
249
+ hostMediaUrl: resolveHostMediaUrl(input),
250
+ hostMediaUrls: input.mediaUrls,
251
+ remainingExtra: remaining,
252
+ consumedType,
253
+ forceMedia,
254
+ });
255
+
256
+ const asVoice = typeof consumed.asVoice === 'boolean' ? consumed.asVoice : input.asVoice === true;
257
+ const audioAsVoice =
258
+ typeof consumed.audioAsVoice === 'boolean'
259
+ ? consumed.audioAsVoice
260
+ : input.audioAsVoice === true;
261
+ // Tri-state: marker boolean wins; otherwise host value; else undefined (scene cascade).
262
+ const downloadMedia =
263
+ typeof consumed.downloadMedia === 'boolean' ? consumed.downloadMedia : input.downloadMedia;
264
+ const type = consumedType ?? hostType;
265
+
266
+ // For media sends: text becomes caption (prefer caption field when both exist).
267
+ // For text sends: prefer message/text, fall back to caption.
268
+ const body = media.hasMedia ? cleanCaption || cleanText : cleanText || cleanCaption;
269
+
270
+ const kind = normalizeReplyKind(consumed.kind) ?? normalizeReplyKind(input.kind) ?? undefined;
271
+ const replyToId =
272
+ asString(consumed.replyToId || input.replyToId || input.replyToMessageId || '').trim() ||
273
+ undefined;
274
+
275
+ const extra = buildBranchExtra({
276
+ hasMedia: media.hasMedia,
277
+ extra: Object.keys(remaining).length > 0 ? remaining : undefined,
278
+ consumedType,
279
+ });
280
+
281
+ return {
282
+ text: body,
283
+ markerHasMsg,
284
+ mediaUrl: media.mediaUrl,
285
+ mediaUrls: media.mediaUrls,
286
+ asVoice,
287
+ audioAsVoice,
288
+ ...(downloadMedia !== undefined ? { downloadMedia } : {}),
289
+ ...(type ? { type } : {}),
290
+ hasMedia: media.hasMedia,
291
+ ...(extra ? { extra } : {}),
292
+ ...(kind ? { kind } : {}),
293
+ ...(replyToId ? { replyToId } : {}),
294
+ };
295
+ }
296
+
297
+ /**
298
+ * Given an already-normalized output, produce the full list of sends.
299
+ *
300
+ * When the marker supplies a `msg` field that will override the clean text
301
+ * body (non-empty `text` different from `extra.msg`), the list includes
302
+ * the clean text as a standalone text-only entry first, followed by the
303
+ * main payload. Otherwise the list contains just the single entry.
304
+ */
305
+ export function buildNormalizedSends(normalized: NormalizedOutboundSend): NormalizedOutboundSend[] {
306
+ if (
307
+ normalized.markerHasMsg &&
308
+ normalized.text &&
309
+ typeof normalized.extra?.msg === 'string' &&
310
+ normalized.extra.msg !== normalized.text
311
+ ) {
312
+ return [
313
+ // Pre-send: clean text only, no media, no extra
314
+ {
315
+ text: normalized.text,
316
+ markerHasMsg: true,
317
+ hasMedia: false,
318
+ asVoice: false,
319
+ audioAsVoice: false,
320
+ kind: normalized.kind,
321
+ replyToId: normalized.replyToId,
322
+ },
323
+ // Main send: as-is (adapter uses extra.msg as body)
324
+ normalized,
325
+ ];
326
+ }
327
+
328
+ return [normalized];
329
+ }
@@ -1,4 +1,5 @@
1
1
  import type { BncrConnection, OutboxEntry } from '../../core/types.ts';
2
+ import { finiteNumberOr } from '../../core/value-sanitize.ts';
2
3
 
3
4
  export type OutboxRouteSelection = {
4
5
  connIds: string[];
@@ -25,11 +26,6 @@ export type OutboxFileTransferRouteSelection = {
25
26
  ownerConnId?: string;
26
27
  };
27
28
 
28
- function finiteNumberOr(value: unknown, fallback: number): number {
29
- const n = Number(value);
30
- return Number.isFinite(n) ? n : fallback;
31
- }
32
-
33
29
  export function computeOutboxRetryWait(nextAttemptAt: number, nowMs: number): number {
34
30
  return Math.max(0, finiteNumberOr(nextAttemptAt, 0) - finiteNumberOr(nowMs, 0));
35
31
  }
@@ -1,187 +1,4 @@
1
- import { buildBncrDebugJsonMessage } from '../../core/logging.ts';
2
- import type { BncrRoute, OutboxEntry } from '../../core/types.ts';
3
- import { buildReplyMediaFallbackDebugInfo } from './diagnostics.ts';
4
- import type {
5
- EnqueueSingleReplyMediaEntryParams,
6
- NormalizedReplyPayload,
7
- ReplyMediaEntriesParams,
8
- ReplyMediaFallbackTextEntryParams,
9
- ReplyMediaFileTransferParams,
10
- } from './reply-enqueue.ts';
11
- import type { OutboundReplyTargetPolicy } from './reply-target-policy.ts';
12
-
13
- export function enqueueReplyMediaFallbackTextEntry(
14
- params: ReplyMediaFallbackTextEntryParams,
15
- helpers: {
16
- logInfo: (
17
- scope: string | undefined,
18
- message: string,
19
- options?: { debugOnly?: boolean },
20
- ) => void;
21
- enqueueOutbound: (entry: OutboxEntry) => void;
22
- buildTextOutboxEntry: (args: {
23
- accountId: string;
24
- sessionKey: string;
25
- route: BncrRoute;
26
- text: string;
27
- extra?: Record<string, unknown>;
28
- kind?: 'tool' | 'block' | 'final';
29
- replyToId?: string;
30
- replyTargetPolicy?: OutboundReplyTargetPolicy;
31
- }) => OutboxEntry;
32
- },
33
- ): void {
34
- helpers.logInfo(
35
- 'outbound',
36
- buildBncrDebugJsonMessage('media-dedupe-hit', buildReplyMediaFallbackDebugInfo(params)),
37
- { debugOnly: true },
38
- );
39
- helpers.enqueueOutbound(
40
- helpers.buildTextOutboxEntry({
41
- accountId: params.accountId,
42
- sessionKey: params.sessionKey,
43
- route: params.route,
44
- text: params.fallback.text,
45
- kind: params.kind,
46
- replyToId: params.replyToId || undefined,
47
- replyTargetPolicy: params.replyTargetPolicy,
48
- }),
49
- );
50
- }
51
-
52
- export function enqueueReplyMediaFileTransferEntry(
53
- params: ReplyMediaFileTransferParams,
54
- helpers: {
55
- enqueueOutbound: (entry: OutboxEntry) => void;
56
- buildFileTransferOutboxEntry: (args: {
57
- accountId: string;
58
- sessionKey: string;
59
- route: BncrRoute;
60
- mediaUrl: string;
61
- mediaLocalRoots?: readonly string[];
62
- text: string;
63
- asVoice: boolean;
64
- audioAsVoice: boolean;
65
- downloadMedia?: boolean;
66
- type?: string;
67
- extra?: Record<string, unknown>;
68
- kind?: 'tool' | 'block' | 'final';
69
- replyToId?: string;
70
- replyTargetPolicy?: OutboundReplyTargetPolicy;
71
- }) => OutboxEntry;
72
- rememberRecentMediaSend: (args: {
73
- sessionKey: string;
74
- mediaUrl: string;
75
- text: string;
76
- replyToId: string;
77
- createdAt: number;
78
- }) => void;
79
- },
80
- ): void {
81
- helpers.enqueueOutbound(
82
- helpers.buildFileTransferOutboxEntry({
83
- accountId: params.accountId,
84
- sessionKey: params.sessionKey,
85
- route: params.route,
86
- mediaUrl: params.mediaUrl,
87
- mediaLocalRoots: params.mediaLocalRoots,
88
- text: params.text,
89
- asVoice: params.asVoice,
90
- audioAsVoice: params.audioAsVoice,
91
- downloadMedia: params.downloadMedia,
92
- type: params.type,
93
- extra: params.extra,
94
- kind: params.kind,
95
- replyToId: params.replyToId || undefined,
96
- replyTargetPolicy: params.replyTargetPolicy,
97
- }),
98
- );
99
- helpers.rememberRecentMediaSend({
100
- sessionKey: params.sessionKey,
101
- mediaUrl: params.mediaUrl,
102
- text: params.normalizedText,
103
- replyToId: params.replyToId,
104
- createdAt: params.createdAt,
105
- });
106
- }
107
-
108
- export function enqueueSingleReplyMediaEntry(
109
- params: EnqueueSingleReplyMediaEntryParams,
110
- helpers: {
111
- enqueueReplyMediaFallbackTextEntry: (params: ReplyMediaFallbackTextEntryParams) => void;
112
- enqueueReplyMediaFileTransferEntry: (params: ReplyMediaFileTransferParams) => void;
113
- },
114
- ): void {
115
- if (params.fallback !== null) {
116
- helpers.enqueueReplyMediaFallbackTextEntry({
117
- accountId: params.params.accountId,
118
- sessionKey: params.params.sessionKey,
119
- route: params.params.route,
120
- mediaUrl: params.mediaUrl,
121
- kind: params.params.payload.kind,
122
- replyToId: params.params.payload.replyToId,
123
- replyTargetPolicy: params.params.payload.replyTargetPolicy,
124
- fallback: params.fallback,
125
- });
126
- return;
127
- }
128
-
129
- helpers.enqueueReplyMediaFileTransferEntry({
130
- accountId: params.params.accountId,
131
- sessionKey: params.params.sessionKey,
132
- route: params.params.route,
133
- mediaUrl: params.mediaUrl,
134
- mediaLocalRoots: params.params.mediaLocalRoots,
135
- text: params.text,
136
- normalizedText: params.normalizedText,
137
- asVoice: params.params.payload.asVoice,
138
- audioAsVoice: params.params.payload.audioAsVoice,
139
- downloadMedia: params.params.payload.downloadMedia,
140
- type: params.params.payload.type,
141
- extra: params.params.payload.extra,
142
- kind: params.params.payload.kind,
143
- replyToId: params.params.payload.replyToId,
144
- replyTargetPolicy: params.params.payload.replyTargetPolicy,
145
- createdAt: params.currentTime,
146
- });
147
- }
148
-
149
- export function enqueueReplyMediaEntries(
150
- params: ReplyMediaEntriesParams,
151
- helpers: {
152
- now: () => number;
153
- normalizeMessageText: (text: string) => string;
154
- tryBuildMediaDedupeFallback: (args: {
155
- sessionKey: string;
156
- mediaUrl: string;
157
- text: string;
158
- replyToId: string;
159
- currentTime: number;
160
- }) => { text: string; reason: string } | null;
161
- enqueueSingleReplyMediaEntry: (params: EnqueueSingleReplyMediaEntryParams) => void;
162
- },
163
- ): void {
164
- const currentTime = helpers.now();
165
- const normalizedText = helpers.normalizeMessageText(params.payload.text);
166
-
167
- for (const mediaUrl of params.payload.mediaList) {
168
- const fallback = helpers.tryBuildMediaDedupeFallback({
169
- sessionKey: params.sessionKey,
170
- mediaUrl,
171
- text: normalizedText,
172
- replyToId: params.payload.replyToId,
173
- currentTime,
174
- });
175
- helpers.enqueueSingleReplyMediaEntry({
176
- params,
177
- mediaUrl,
178
- normalizedText,
179
- text: params.payload.text,
180
- fallback,
181
- currentTime,
182
- });
183
- }
184
- }
1
+ import type { NormalizedReplyPayload } from './reply-enqueue.ts';
185
2
 
186
3
  export function hasReplyMediaEntries(payload: NormalizedReplyPayload) {
187
4
  return payload.mediaList.length > 0;