@xmoxmo/bncr 0.5.7 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/check-pack.mjs +0 -2
- package/scripts/selfcheck.mjs +0 -3
- package/src/bootstrap/runtime-loader.ts +1 -1
- package/src/channel.ts +49 -138
- package/src/core/connection-reachability.ts +1 -1
- package/src/core/logging.ts +1 -1
- package/src/core/outbox-entry-builders.ts +60 -82
- package/src/core/outbox-summary.ts +5 -39
- package/src/core/permissions.ts +1 -1
- package/src/core/register-trace.ts +2 -2
- package/src/messaging/inbound/parse.ts +1 -1
- package/src/messaging/outbound/diagnostics-debug-builders.ts +0 -15
- package/src/messaging/outbound/diagnostics.ts +0 -1
- package/src/messaging/outbound/marker-parser.ts +215 -0
- package/src/messaging/outbound/media-dedupe.ts +1 -4
- package/src/messaging/outbound/normalize-outbound-send.ts +9 -7
- package/src/messaging/outbound/reply-enqueue-media.ts +1 -184
- package/src/messaging/outbound/reply-enqueue.ts +249 -162
- package/src/openclaw/media-runtime.ts +1 -1
- package/src/plugin/bridge-media-facade.ts +0 -3
- package/src/plugin/bridge-outbox-facade.ts +0 -1
- package/src/plugin/channel-runtime-builders-delivery.ts +2 -362
- package/src/plugin/channel-runtime-builders-status.ts +0 -160
- package/src/plugin/channel-runtime-builders.ts +3 -17
- package/src/plugin/channel-runtime-constants.ts +1 -1
- package/src/plugin/channel-send.ts +5 -4
- package/src/plugin/file-transfer-orchestrator.ts +39 -2
- package/src/plugin/media-orchestrators-runtime-group.ts +20 -149
- package/src/plugin/messaging.ts +3 -3
- package/src/plugin/outbox-push.ts +1 -52
- package/src/plugin/outbox-unified-push-flow.ts +307 -0
- package/src/plugin/scene-registry.ts +1 -1
- package/src/runtime/log-dedupe.ts +3 -3
- package/src/runtime/outbound-ack-timeout.ts +2 -4
- package/src/runtime/status-snapshots.ts +2 -2
- package/src/runtime/status-worker.ts +2 -6
- package/src/core/outbox-text-push-prep.ts +0 -1
- package/src/plugin/bridge-file-transfer-push-facade.ts +0 -142
- package/src/plugin/outbox-file-push-flow.ts +0 -69
- package/src/plugin/outbox-text-push-flow.ts +0 -90
|
@@ -12,8 +12,13 @@
|
|
|
12
12
|
* - Incomplete marker (no closing ]) → left in text, does NOT block later
|
|
13
13
|
* complete markers.
|
|
14
14
|
* - Multiple valid markers → all stripped, params merge (later same-key wins).
|
|
15
|
+
* - When path/paths/mediaUrl/mediaUrls are present, probe for reachability.
|
|
16
|
+
* Unreachable single paths keep the marker visible in the output text.
|
|
17
|
+
* Unreachable entries in path arrays are dropped with warnings logged.
|
|
15
18
|
*/
|
|
16
19
|
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
|
|
17
22
|
const PREFIX = '[BncrParam:';
|
|
18
23
|
|
|
19
24
|
/**
|
|
@@ -186,3 +191,213 @@ export function extractConsumptionFields(extra: Record<string, unknown> | undefi
|
|
|
186
191
|
remaining,
|
|
187
192
|
};
|
|
188
193
|
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Media path probing
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
const PROBE_TIMEOUT_MS = 5_000;
|
|
200
|
+
const MEDIA_PATH_KEYS = new Set(['path', 'paths', 'mediaUrl', 'mediaUrls']);
|
|
201
|
+
|
|
202
|
+
function hasMediaPathParams(params: Record<string, unknown>): boolean {
|
|
203
|
+
for (const key of MEDIA_PATH_KEYS) {
|
|
204
|
+
if (key in params) return true;
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Check whether a single media path (HTTP URL or local file) is reachable.
|
|
211
|
+
*
|
|
212
|
+
* - HTTP/HTTPS URLs → HEAD request with short timeout (5 s), follows redirects.
|
|
213
|
+
* - Local files → fs.existsSync.
|
|
214
|
+
*
|
|
215
|
+
* Returns `{ ok, reason }` where `reason` describes the failure when `ok` is false.
|
|
216
|
+
*/
|
|
217
|
+
async function probeMediaPath(raw: string): Promise<{
|
|
218
|
+
ok: boolean;
|
|
219
|
+
reason?: string;
|
|
220
|
+
}> {
|
|
221
|
+
const path = String(raw || '').trim();
|
|
222
|
+
if (!path) return { ok: false, reason: 'empty path' };
|
|
223
|
+
|
|
224
|
+
// HTTP(S) URL — HEAD probe
|
|
225
|
+
if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
226
|
+
try {
|
|
227
|
+
const controller = new AbortController();
|
|
228
|
+
const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
229
|
+
const response = await fetch(path, {
|
|
230
|
+
method: 'HEAD',
|
|
231
|
+
signal: controller.signal,
|
|
232
|
+
redirect: 'follow',
|
|
233
|
+
});
|
|
234
|
+
clearTimeout(timer);
|
|
235
|
+
if (response.ok || response.status === 206) return { ok: true };
|
|
236
|
+
return { ok: false, reason: `HTTP ${response.status} ${response.statusText}` };
|
|
237
|
+
} catch (err: unknown) {
|
|
238
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
239
|
+
return { ok: false, reason };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Local file
|
|
244
|
+
try {
|
|
245
|
+
if (fs.existsSync(path)) return { ok: true };
|
|
246
|
+
return { ok: false, reason: 'file not found' };
|
|
247
|
+
} catch {
|
|
248
|
+
return { ok: false, reason: 'unreadable path' };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* After JSON marker parsing, probe any media paths found in the params.
|
|
254
|
+
*
|
|
255
|
+
* - Single path (`path` / `mediaUrl`): if unreachable, keeps the marker in text
|
|
256
|
+
* (returns allValid=false, warnings populated).
|
|
257
|
+
* - Path arrays (`paths` / `mediaUrls`): unreachable entries are removed; if at
|
|
258
|
+
* least one entry is valid the marker is still stripped. If ALL entries are
|
|
259
|
+
* invalid the marker is kept.
|
|
260
|
+
*
|
|
261
|
+
* Returns `{ params, warnings, allValid }`:
|
|
262
|
+
* - `params` — (potentially filtered) params with invalid array entries removed.
|
|
263
|
+
* - `warnings` — human-readable warning strings for each unreachable path.
|
|
264
|
+
* - `allValid` — false when a single path or all array entries are unreachable.
|
|
265
|
+
*/
|
|
266
|
+
async function probeAndResolveMarkerPaths(params: Record<string, unknown>): Promise<{
|
|
267
|
+
params: Record<string, unknown>;
|
|
268
|
+
warnings: string[];
|
|
269
|
+
allValid: boolean;
|
|
270
|
+
}> {
|
|
271
|
+
const warnings: string[] = [];
|
|
272
|
+
if (!hasMediaPathParams(params)) return { params, warnings, allValid: true };
|
|
273
|
+
|
|
274
|
+
// Single path / mediaUrl
|
|
275
|
+
const singlePath = String(params.path ?? params.mediaUrl ?? '').trim();
|
|
276
|
+
if (singlePath) {
|
|
277
|
+
const result = await probeMediaPath(singlePath);
|
|
278
|
+
if (!result.ok) {
|
|
279
|
+
warnings.push(`path ${singlePath} unreachable: ${result.reason}`);
|
|
280
|
+
return { params, warnings, allValid: false };
|
|
281
|
+
}
|
|
282
|
+
// Path confirmed reachable — signal downstream that media is validated
|
|
283
|
+
// Only set ismedia:true when user hasn't explicitly opted out.
|
|
284
|
+
if (!('ismedia' in params && params.ismedia === false)) {
|
|
285
|
+
params = { ...params, ismedia: true };
|
|
286
|
+
}
|
|
287
|
+
return { params, warnings, allValid: true };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Path arrays — paths / mediaUrls
|
|
291
|
+
const arraySource = (params.paths ?? params.mediaUrls) as unknown;
|
|
292
|
+
if (Array.isArray(arraySource) && arraySource.length > 0) {
|
|
293
|
+
const entries = arraySource.filter(
|
|
294
|
+
(v): v is string => typeof v === 'string' && v.trim().length > 0,
|
|
295
|
+
);
|
|
296
|
+
if (entries.length === 0) return { params, warnings, allValid: true };
|
|
297
|
+
|
|
298
|
+
const results = await Promise.all(entries.map((p) => probeMediaPath(p)));
|
|
299
|
+
const valid: string[] = [];
|
|
300
|
+
for (let i = 0; i < entries.length; i++) {
|
|
301
|
+
if (results[i].ok) {
|
|
302
|
+
valid.push(entries[i]);
|
|
303
|
+
} else {
|
|
304
|
+
warnings.push(`path ${entries[i]} unreachable: ${results[i].reason}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (valid.length === 0) {
|
|
309
|
+
// All invalid → keep marker
|
|
310
|
+
return { params, warnings, allValid: false };
|
|
311
|
+
}
|
|
312
|
+
// At least one valid → strip marker, replace with filtered entries.
|
|
313
|
+
// Only set ismedia=true when user hasn't explicitly opted out.
|
|
314
|
+
const setMedia = !('ismedia' in params && params.ismedia === false);
|
|
315
|
+
params = { ...params, ...(setMedia ? { ismedia: true } : {}), paths: valid, mediaUrls: valid };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return { params, warnings, allValid: true };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Parse text markers AND probe media paths in one async step.
|
|
323
|
+
*
|
|
324
|
+
/**
|
|
325
|
+
* Unified marker strip decision: the single place that decides whether to strip
|
|
326
|
+
* a [BncrParam:...] marker from the text or leave it in place.
|
|
327
|
+
*
|
|
328
|
+
* - Markers that JSON parse successfully AND whose media paths (if any) are
|
|
329
|
+
* reachable → stripped from text, params extracted and merged.
|
|
330
|
+
* - Markers whose paths are unreachable (single path OR all array entries) →
|
|
331
|
+
* kept in text, NOT stripped, NOT extracted.
|
|
332
|
+
* - Markers without path fields → stripped as normal (existing behavior).
|
|
333
|
+
* - Markers that fail JSON parse → kept in text (existing behavior).
|
|
334
|
+
* - Multiple markers → each evaluated independently, later markers' params
|
|
335
|
+
* overwrite earlier ones when the same key appears.
|
|
336
|
+
*
|
|
337
|
+
* This function IS the single decision point. Downstream consumers of the
|
|
338
|
+
* returned text never re-parse markers — they trust the decision made here.
|
|
339
|
+
*/
|
|
340
|
+
export async function resolveMarkerStripDecision(text: string): Promise<{
|
|
341
|
+
cleanText: string;
|
|
342
|
+
params: Record<string, unknown>;
|
|
343
|
+
warnings: string[];
|
|
344
|
+
}> {
|
|
345
|
+
const params: Record<string, unknown> = {};
|
|
346
|
+
const warnings: string[] = [];
|
|
347
|
+
let result = text;
|
|
348
|
+
let searchFrom = 0;
|
|
349
|
+
const hadMarkers = text.includes(PREFIX);
|
|
350
|
+
|
|
351
|
+
if (!hadMarkers) return { cleanText: text, params, warnings };
|
|
352
|
+
|
|
353
|
+
while (true) {
|
|
354
|
+
const idx = result.indexOf(PREFIX, searchFrom);
|
|
355
|
+
if (idx === -1) break;
|
|
356
|
+
|
|
357
|
+
const end = findMarkerEnd(result, idx);
|
|
358
|
+
if (end === -1) {
|
|
359
|
+
searchFrom = idx + PREFIX.length;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const rawJson = result.slice(idx + PREFIX.length, end);
|
|
364
|
+
let parsed: Record<string, unknown> | null = null;
|
|
365
|
+
try {
|
|
366
|
+
const obj = JSON.parse(rawJson);
|
|
367
|
+
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
368
|
+
parsed = obj as Record<string, unknown>;
|
|
369
|
+
}
|
|
370
|
+
} catch {
|
|
371
|
+
// JSON parse failed → keep marker in text, continue scanning after it
|
|
372
|
+
searchFrom = end + 1;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// JSON parsed but is not a plain object → keep marker in text
|
|
377
|
+
if (!parsed) {
|
|
378
|
+
searchFrom = end + 1;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Marker has path fields → probe before deciding to strip
|
|
383
|
+
if (hasMediaPathParams(parsed)) {
|
|
384
|
+
const probeResult = await probeAndResolveMarkerPaths(parsed);
|
|
385
|
+
if (probeResult.warnings.length > 0) warnings.push(...probeResult.warnings);
|
|
386
|
+
if (!probeResult.allValid) {
|
|
387
|
+
// Path(s) unreachable → keep marker in text, do NOT extract any params
|
|
388
|
+
searchFrom = end + 1;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
// Paths valid → strip, merge params (probeResult.params includes ismedia:true + filtered arrays)
|
|
392
|
+
Object.assign(params, probeResult.params);
|
|
393
|
+
} else {
|
|
394
|
+
// No path fields → strip marker and merge params unconditionally
|
|
395
|
+
Object.assign(params, parsed);
|
|
396
|
+
}
|
|
397
|
+
result = result.slice(0, idx) + result.slice(end + 1);
|
|
398
|
+
searchFrom = idx;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const cleanText = result !== text ? result.replace(/ {2,}/g, ' ').trim() : result;
|
|
402
|
+
return { cleanText, params, warnings };
|
|
403
|
+
}
|
|
@@ -15,10 +15,7 @@ export function normalizeMessageText(value: unknown): string {
|
|
|
15
15
|
return asString(value || '').trim();
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
currentReplyToId: string,
|
|
20
|
-
previousReplyToId: string,
|
|
21
|
-
): boolean {
|
|
18
|
+
function shouldTreatReplyToAsSame(currentReplyToId: string, previousReplyToId: string): boolean {
|
|
22
19
|
if (!currentReplyToId || !previousReplyToId) return true;
|
|
23
20
|
return currentReplyToId === previousReplyToId;
|
|
24
21
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { asString, isPlainObject } from '../../core/value-sanitize.ts';
|
|
15
|
-
import { extractConsumptionFields,
|
|
15
|
+
import { extractConsumptionFields, resolveMarkerStripDecision } from './marker-parser.ts';
|
|
16
16
|
|
|
17
17
|
const MEDIA_SOURCE_EXTRA_KEYS = ['path', 'paths', 'mediaUrl', 'mediaUrls'] as const;
|
|
18
18
|
|
|
@@ -104,13 +104,13 @@ function buildBranchExtra(args: {
|
|
|
104
104
|
* Parse markers from one or more text fields and merge extras.
|
|
105
105
|
* Marker params always override host/params extra and host control flags.
|
|
106
106
|
*/
|
|
107
|
-
export function mergeMarkerAndHostFields(args: {
|
|
107
|
+
export async function mergeMarkerAndHostFields(args: {
|
|
108
108
|
texts: string[];
|
|
109
109
|
hostExtra?: Record<string, unknown>;
|
|
110
110
|
forceDocument?: boolean;
|
|
111
111
|
gifPlayback?: boolean;
|
|
112
112
|
silent?: boolean;
|
|
113
|
-
}): {
|
|
113
|
+
}): Promise<{
|
|
114
114
|
cleanTexts: string[];
|
|
115
115
|
markerParams: Record<string, unknown>;
|
|
116
116
|
consumed: Partial<{
|
|
@@ -124,12 +124,12 @@ export function mergeMarkerAndHostFields(args: {
|
|
|
124
124
|
}>;
|
|
125
125
|
remaining: Record<string, unknown>;
|
|
126
126
|
markerHasMsg: boolean;
|
|
127
|
-
} {
|
|
127
|
+
}> {
|
|
128
128
|
const markerParams: Record<string, unknown> = {};
|
|
129
129
|
const cleanTexts: string[] = [];
|
|
130
130
|
|
|
131
131
|
for (const raw of args.texts) {
|
|
132
|
-
const { cleanText, params } =
|
|
132
|
+
const { cleanText, params } = await resolveMarkerStripDecision(raw);
|
|
133
133
|
cleanTexts.push(cleanText);
|
|
134
134
|
Object.assign(markerParams, params);
|
|
135
135
|
}
|
|
@@ -222,13 +222,15 @@ function resolveDispatchMediaSources(args: {
|
|
|
222
222
|
* Normalize any outbound send input into a single dispatch shape.
|
|
223
223
|
* Callers only need to branch on `hasMedia` after this.
|
|
224
224
|
*/
|
|
225
|
-
export function normalizeOutboundSend(
|
|
225
|
+
export async function normalizeOutboundSend(
|
|
226
|
+
input: UnifiedOutboundSendInput,
|
|
227
|
+
): Promise<NormalizedOutboundSend> {
|
|
226
228
|
const rawText = asString(input.text ?? input.message ?? '', '');
|
|
227
229
|
const rawCaption = asString(input.caption ?? '', '');
|
|
228
230
|
// Prefer text/message; also parse caption so marker in either field works.
|
|
229
231
|
const textsToParse = rawCaption && rawCaption !== rawText ? [rawText, rawCaption] : [rawText];
|
|
230
232
|
|
|
231
|
-
const { cleanTexts, consumed, remaining, markerHasMsg } = mergeMarkerAndHostFields({
|
|
233
|
+
const { cleanTexts, consumed, remaining, markerHasMsg } = await mergeMarkerAndHostFields({
|
|
232
234
|
texts: textsToParse,
|
|
233
235
|
hostExtra: isPlainObject(input.extra) ? input.extra : undefined,
|
|
234
236
|
forceDocument: input.forceDocument,
|
|
@@ -1,187 +1,4 @@
|
|
|
1
|
-
import {
|
|
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;
|