@xmanrui/dsh-im 1.4.0 → 1.5.0
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/README.en.md +2 -2
- package/README.md +2 -2
- package/lib/index.js +165 -165
- package/package.json +1 -1
- package/src/channels/dingtalk/dingtalk-api.mjs +103 -81
- package/src/channels/dingtalk/dingtalk-bridge.mjs +34 -70
- package/src/channels/feishu/bridge.mjs +38 -57
- package/src/channels/feishu/feishu-channel.mjs +52 -17
- package/src/channels/qq/qq-bridge.mjs +92 -78
- package/src/channels/shared/semantic/artifact-delivery.mjs +170 -0
- package/src/channels/shared/semantic/artifact.mjs +2 -2
- package/src/channels/shared/semantic/delivery.mjs +2 -0
- package/src/channels/shared/text-harness-bridge.mjs +24 -73
- package/src/channels/telegram/telegram-api.mjs +39 -13
- package/src/channels/telegram/telegram-runtime.mjs +10 -0
- package/src/channels/wecom/wecom-bridge.mjs +95 -98
- package/src/channels/weixin/weixin-api.mjs +140 -107
- package/src/channels/weixin/weixin-bridge.mjs +36 -74
- package/src/channels/whatsapp/whatsapp-runtime.mjs +42 -9
|
@@ -32,14 +32,11 @@ import {
|
|
|
32
32
|
prefetchInboundFiles,
|
|
33
33
|
} from '../shared/inbound-file.mjs';
|
|
34
34
|
import {
|
|
35
|
-
materializeOutboundArtifact,
|
|
36
|
-
releaseOutboundArtifact,
|
|
37
35
|
trackOutboundArtifactProviderPromise,
|
|
38
36
|
} from '../shared/semantic/artifact.mjs';
|
|
37
|
+
import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
|
|
39
38
|
import {
|
|
40
|
-
createArtifactFailureReceipt,
|
|
41
39
|
createDeliveryReceipt,
|
|
42
|
-
mergeDeliveryReceipts,
|
|
43
40
|
providerMessageIdsFor,
|
|
44
41
|
} from '../shared/semantic/delivery.mjs';
|
|
45
42
|
import { sendMarkdownReply } from './markdown-reply.mjs';
|
|
@@ -210,7 +207,7 @@ function qqArtifactError(error, { dispatched = false } = {}) {
|
|
|
210
207
|
if (status === 401 || status === 403) wrapped.code = 'artifact-permission-required';
|
|
211
208
|
else if (status === 413) wrapped.code = 'artifact-too-large';
|
|
212
209
|
else if (status === 429) wrapped.code = 'artifact-rate-limited';
|
|
213
|
-
else if (
|
|
210
|
+
else if ([400, 404, 405, 406, 410, 415, 422].includes(status)) {
|
|
214
211
|
wrapped.code = 'artifact-provider-rejected';
|
|
215
212
|
} else {
|
|
216
213
|
wrapped.code = dispatched ? 'artifact-delivery-uncertain' : 'artifact-provider-failed';
|
|
@@ -245,6 +242,68 @@ function waitWithSignal(promise, signal) {
|
|
|
245
242
|
});
|
|
246
243
|
}
|
|
247
244
|
|
|
245
|
+
/** Send one materialized artifact through QQ's native image message. */
|
|
246
|
+
export async function sendQqImage(
|
|
247
|
+
bot,
|
|
248
|
+
target,
|
|
249
|
+
file,
|
|
250
|
+
{ signal, timeoutMs = DEFAULT_FILE_UPLOAD_TIMEOUT_MS } = {},
|
|
251
|
+
) {
|
|
252
|
+
signal?.throwIfAborted();
|
|
253
|
+
if (typeof bot?.sendImage !== 'function') {
|
|
254
|
+
const unavailable = new Error('QQ image delivery is unavailable');
|
|
255
|
+
unavailable.code = 'artifact-provider-unavailable';
|
|
256
|
+
throw unavailable;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
261
|
+
const waitSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
262
|
+
const pending = bot.sendImage(
|
|
263
|
+
target,
|
|
264
|
+
{ buffer: file.bytes },
|
|
265
|
+
{ onProgress: () => signal?.throwIfAborted() },
|
|
266
|
+
);
|
|
267
|
+
trackOutboundArtifactProviderPromise(file, pending);
|
|
268
|
+
return await waitWithSignal(pending, waitSignal);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
271
|
+
throw qqArtifactError(error, { dispatched: true });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function sendQqFile(
|
|
276
|
+
bot,
|
|
277
|
+
target,
|
|
278
|
+
file,
|
|
279
|
+
{ signal, timeoutMs = DEFAULT_FILE_UPLOAD_TIMEOUT_MS } = {},
|
|
280
|
+
) {
|
|
281
|
+
signal?.throwIfAborted();
|
|
282
|
+
if (typeof bot?.sendFile !== 'function') {
|
|
283
|
+
const unavailable = new Error('QQ file delivery is unavailable');
|
|
284
|
+
unavailable.code = 'artifact-provider-unavailable';
|
|
285
|
+
throw unavailable;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
290
|
+
const waitSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
291
|
+
const pending = bot.sendFile(
|
|
292
|
+
target,
|
|
293
|
+
{ buffer: file.bytes },
|
|
294
|
+
{
|
|
295
|
+
fileName: file.fileName,
|
|
296
|
+
onProgress: () => signal?.throwIfAborted(),
|
|
297
|
+
},
|
|
298
|
+
);
|
|
299
|
+
trackOutboundArtifactProviderPromise(file, pending);
|
|
300
|
+
return await waitWithSignal(pending, waitSignal);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
303
|
+
throw qqArtifactError(error, { dispatched: true });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
248
307
|
function canClaimInteractionReply(message, pending) {
|
|
249
308
|
return pending.questions[pending.index]
|
|
250
309
|
&& nonEmptyString(message?.senderId) === pending.actor
|
|
@@ -491,80 +550,35 @@ export class QqHarnessBridge {
|
|
|
491
550
|
if (artifacts.length === 0) {
|
|
492
551
|
return { receipt: baseReceipt, failureNoticeVisible: false };
|
|
493
552
|
}
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
this.#signal
|
|
508
|
-
|
|
509
|
-
try {
|
|
510
|
-
const timeout = AbortSignal.timeout(this.#fileUploadTimeoutMs);
|
|
511
|
-
const waitSignal = this.#signal ? AbortSignal.any([this.#signal, timeout]) : timeout;
|
|
512
|
-
const pending = this.#bot.sendFile(
|
|
513
|
-
target,
|
|
514
|
-
{ buffer: file.bytes },
|
|
515
|
-
{
|
|
516
|
-
fileName: file.fileName,
|
|
517
|
-
onProgress: () => this.#signal?.throwIfAborted(),
|
|
518
|
-
},
|
|
519
|
-
);
|
|
520
|
-
trackOutboundArtifactProviderPromise(file, pending);
|
|
521
|
-
result = await waitWithSignal(pending, waitSignal);
|
|
522
|
-
} catch (error) {
|
|
523
|
-
if (this.#signal?.aborted) throw abortReason(this.#signal);
|
|
524
|
-
throw qqArtifactError(error, { dispatched: true });
|
|
525
|
-
}
|
|
526
|
-
this.#signal?.throwIfAborted();
|
|
527
|
-
const messageId = nonEmptyString(result?.message?.id);
|
|
528
|
-
receipts.push(createDeliveryReceipt({
|
|
529
|
-
deliveryId: file.deliveryKey,
|
|
530
|
-
presentation: 'qq-file',
|
|
531
|
-
providerMessageIds: messageId ? [messageId] : [],
|
|
532
|
-
artifacts: [{ artifactId: file.artifactId, outcome: 'sent' }],
|
|
533
|
-
}));
|
|
534
|
-
this.#status.artifactsSent = (this.#status.artifactsSent ?? 0) + 1;
|
|
535
|
-
} catch (rawError) {
|
|
536
|
-
if (this.#signal?.aborted) throw rawError;
|
|
537
|
-
const error = qqArtifactError(rawError);
|
|
538
|
-
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0) + 1;
|
|
539
|
-
this.#logger.warn?.(
|
|
540
|
-
`[dsh-im:qq] result file delivery failed (${error?.code ?? error?.name ?? 'unknown'})`,
|
|
541
|
-
);
|
|
542
|
-
let providerMessageIds = [];
|
|
543
|
-
try {
|
|
544
|
-
const notice = await this.#bot.sendText(target, artifactFailureText(artifact?.fileName, error));
|
|
545
|
-
failureNoticeVisible = true;
|
|
546
|
-
providerMessageIds = providerMessageIdsFor(notice);
|
|
547
|
-
} catch (noticeError) {
|
|
548
|
-
if (this.#signal?.aborted) throw noticeError;
|
|
549
|
-
this.#logger.warn?.('[dsh-im:qq] unable to send the safe result-file failure notice');
|
|
550
|
-
}
|
|
551
|
-
receipts.push(createArtifactFailureReceipt({
|
|
552
|
-
artifactId: artifact?.artifactId ?? 'unknown',
|
|
553
|
-
deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
|
|
554
|
-
error,
|
|
555
|
-
providerMessageIds,
|
|
556
|
-
}));
|
|
557
|
-
} finally {
|
|
558
|
-
releaseOutboundArtifact(artifact);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
return {
|
|
562
|
-
receipt: mergeDeliveryReceipts({
|
|
563
|
-
deliveryId: replyTo,
|
|
564
|
-
presentation: baseReceipt ? 'qq-text-and-files' : 'qq-files',
|
|
565
|
-
receipts,
|
|
553
|
+
const delivery = await deliverOutboundArtifacts({
|
|
554
|
+
artifacts,
|
|
555
|
+
baseReceipt,
|
|
556
|
+
deliveryId: replyTo,
|
|
557
|
+
aggregatePresentation: baseReceipt ? 'qq-text-and-files' : 'qq-files',
|
|
558
|
+
alwaysMerge: true,
|
|
559
|
+
channelKey: 'qq',
|
|
560
|
+
signal: this.#signal,
|
|
561
|
+
sendImage: (file) => sendQqImage(this.#bot, target, file, {
|
|
562
|
+
signal: this.#signal,
|
|
563
|
+
timeoutMs: this.#fileUploadTimeoutMs,
|
|
564
|
+
}),
|
|
565
|
+
sendFile: (file) => sendQqFile(this.#bot, target, file, {
|
|
566
|
+
signal: this.#signal,
|
|
567
|
+
timeoutMs: this.#fileUploadTimeoutMs,
|
|
566
568
|
}),
|
|
567
|
-
|
|
569
|
+
sendFailureNotice: (artifact, error) => this.#bot.sendText(
|
|
570
|
+
target,
|
|
571
|
+
artifactFailureText(artifact?.fileName, error),
|
|
572
|
+
),
|
|
573
|
+
logger: this.#logger,
|
|
574
|
+
});
|
|
575
|
+
this.#status.artifactsSent = (this.#status.artifactsSent ?? 0)
|
|
576
|
+
+ delivery.artifactsSent;
|
|
577
|
+
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
|
|
578
|
+
+ delivery.artifactSendErrors;
|
|
579
|
+
return {
|
|
580
|
+
receipt: delivery.receipt,
|
|
581
|
+
failureNoticeVisible: delivery.failureNoticeVisible,
|
|
568
582
|
};
|
|
569
583
|
}
|
|
570
584
|
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
materializeOutboundArtifact,
|
|
3
|
+
releaseOutboundArtifact,
|
|
4
|
+
} from './artifact.mjs';
|
|
5
|
+
import {
|
|
6
|
+
createArtifactFailureReceipt,
|
|
7
|
+
createDeliveryReceipt,
|
|
8
|
+
mergeDeliveryReceipts,
|
|
9
|
+
providerMessageIdsFor,
|
|
10
|
+
} from './delivery.mjs';
|
|
11
|
+
|
|
12
|
+
function unavailableError() {
|
|
13
|
+
const error = new Error('Native file delivery is unavailable');
|
|
14
|
+
error.code = 'artifact-provider-unavailable';
|
|
15
|
+
return error;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isAbort(error, signal) {
|
|
19
|
+
return signal?.aborted
|
|
20
|
+
|| error?.name === 'AbortError'
|
|
21
|
+
|| error?.code === 'ABORT_ERR';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function providerIds(value) {
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
return [...new Set(value
|
|
27
|
+
.filter((candidate) => (
|
|
28
|
+
(typeof candidate === 'string' && candidate.trim())
|
|
29
|
+
|| Number.isSafeInteger(candidate)
|
|
30
|
+
))
|
|
31
|
+
.map(String))];
|
|
32
|
+
}
|
|
33
|
+
return providerMessageIdsFor(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function sendMaterializedArtifact(file, {
|
|
37
|
+
sendFile,
|
|
38
|
+
sendImage,
|
|
39
|
+
signal,
|
|
40
|
+
}) {
|
|
41
|
+
if (file.mediaType?.startsWith('image/') && typeof sendImage === 'function') {
|
|
42
|
+
try {
|
|
43
|
+
return {
|
|
44
|
+
presentation: 'image',
|
|
45
|
+
result: await sendImage(file),
|
|
46
|
+
};
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (isAbort(error, signal) || error?.code === 'artifact-delivery-uncertain') {
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
signal?.throwIfAborted();
|
|
54
|
+
if (typeof sendFile !== 'function') throw unavailableError();
|
|
55
|
+
return {
|
|
56
|
+
presentation: 'file',
|
|
57
|
+
result: await sendFile(file),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Deliver registered artifacts with one shared image-first policy while keeping
|
|
63
|
+
* provider protocol details inside the channel-supplied send closures.
|
|
64
|
+
*/
|
|
65
|
+
export async function deliverOutboundArtifacts({
|
|
66
|
+
artifacts = [],
|
|
67
|
+
baseReceipt = null,
|
|
68
|
+
deliveryId,
|
|
69
|
+
aggregatePresentation,
|
|
70
|
+
alwaysMerge = false,
|
|
71
|
+
channelKey,
|
|
72
|
+
signal,
|
|
73
|
+
sendFile,
|
|
74
|
+
sendImage,
|
|
75
|
+
sendFailureNotice,
|
|
76
|
+
logger,
|
|
77
|
+
}) {
|
|
78
|
+
const receipts = baseReceipt ? [baseReceipt] : [];
|
|
79
|
+
let userVisible = Boolean(baseReceipt);
|
|
80
|
+
let failureNoticeVisible = false;
|
|
81
|
+
let artifactsSent = 0;
|
|
82
|
+
let artifactSendErrors = 0;
|
|
83
|
+
|
|
84
|
+
let artifactIndex = 0;
|
|
85
|
+
try {
|
|
86
|
+
while (artifactIndex < artifacts.length) {
|
|
87
|
+
const artifact = artifacts[artifactIndex];
|
|
88
|
+
artifactIndex += 1;
|
|
89
|
+
try {
|
|
90
|
+
signal?.throwIfAborted();
|
|
91
|
+
const file = await materializeOutboundArtifact(artifact, { signal });
|
|
92
|
+
signal?.throwIfAborted();
|
|
93
|
+
const sent = await sendMaterializedArtifact(file, {
|
|
94
|
+
sendFile,
|
|
95
|
+
sendImage,
|
|
96
|
+
signal,
|
|
97
|
+
});
|
|
98
|
+
signal?.throwIfAborted();
|
|
99
|
+
receipts.push(createDeliveryReceipt({
|
|
100
|
+
deliveryId: file.deliveryKey,
|
|
101
|
+
presentation: `${channelKey}-${sent.presentation}`,
|
|
102
|
+
providerMessageIds: providerIds(sent.result),
|
|
103
|
+
artifacts: [{ artifactId: file.artifactId, outcome: 'sent' }],
|
|
104
|
+
}));
|
|
105
|
+
artifactsSent += 1;
|
|
106
|
+
userVisible = true;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (isAbort(error, signal)) throw error;
|
|
109
|
+
artifactSendErrors += 1;
|
|
110
|
+
logger?.warn?.(
|
|
111
|
+
`[dsh-im:${channelKey}] result artifact delivery failed (${error?.code ?? 'unknown'})`,
|
|
112
|
+
);
|
|
113
|
+
let messageIds = [];
|
|
114
|
+
if (typeof sendFailureNotice === 'function') {
|
|
115
|
+
try {
|
|
116
|
+
signal?.throwIfAborted();
|
|
117
|
+
const notice = await sendFailureNotice(artifact, error);
|
|
118
|
+
signal?.throwIfAborted();
|
|
119
|
+
messageIds = providerIds(notice);
|
|
120
|
+
failureNoticeVisible = true;
|
|
121
|
+
} catch (noticeError) {
|
|
122
|
+
if (isAbort(noticeError, signal)) throw noticeError;
|
|
123
|
+
logger?.warn?.(
|
|
124
|
+
`[dsh-im:${channelKey}] unable to send the safe artifact failure notice`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const failureReceipt = createArtifactFailureReceipt({
|
|
129
|
+
artifactId: artifact?.artifactId ?? 'unknown',
|
|
130
|
+
deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
|
|
131
|
+
error,
|
|
132
|
+
providerMessageIds: messageIds,
|
|
133
|
+
});
|
|
134
|
+
receipts.push(failureReceipt);
|
|
135
|
+
if (failureNoticeVisible || failureReceipt.artifacts[0]?.outcome === 'unknown') {
|
|
136
|
+
userVisible = true;
|
|
137
|
+
}
|
|
138
|
+
} finally {
|
|
139
|
+
releaseOutboundArtifact(artifact);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} finally {
|
|
143
|
+
while (artifactIndex < artifacts.length) {
|
|
144
|
+
releaseOutboundArtifact(artifacts[artifactIndex]);
|
|
145
|
+
artifactIndex += 1;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let receipt = null;
|
|
150
|
+
if (receipts.length === 1 && !alwaysMerge) {
|
|
151
|
+
[receipt] = receipts;
|
|
152
|
+
} else if (receipts.length > 0) {
|
|
153
|
+
receipt = mergeDeliveryReceipts({
|
|
154
|
+
deliveryId: deliveryId
|
|
155
|
+
?? baseReceipt?.deliveryId
|
|
156
|
+
?? artifacts[0]?.deliveryKey,
|
|
157
|
+
presentation: aggregatePresentation
|
|
158
|
+
?? `${channelKey}-${baseReceipt ? 'text-and-files' : 'files'}`,
|
|
159
|
+
receipts,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
receipt,
|
|
165
|
+
userVisible,
|
|
166
|
+
failureNoticeVisible,
|
|
167
|
+
artifactsSent,
|
|
168
|
+
artifactSendErrors,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -576,7 +576,7 @@ export function createOutboundArtifactTool({ registry = outboundArtifactRegistry
|
|
|
576
576
|
};
|
|
577
577
|
const definition = Object.freeze({
|
|
578
578
|
name: OUTBOUND_ARTIFACT_TOOL,
|
|
579
|
-
description: 'Send a readable file to the user through the current conversation. Existing and newly created files are both valid.',
|
|
579
|
+
description: 'Send a readable file or generated image to the user through the current conversation. Existing and newly created files are both valid.',
|
|
580
580
|
parameters: {
|
|
581
581
|
type: 'object',
|
|
582
582
|
additionalProperties: false,
|
|
@@ -654,7 +654,7 @@ export function installOutboundArtifactTool(ctx, { registry = outboundArtifactRe
|
|
|
654
654
|
ctx.systemPrompt.section({
|
|
655
655
|
name: 'dsh-im:return-file',
|
|
656
656
|
order: 115,
|
|
657
|
-
text: `When the user asks to receive a file, call ${OUTBOUND_ARTIFACT_TOOL} with its path. Existing files can be sent directly; do not recreate or rename a file solely for delivery.`,
|
|
657
|
+
text: `When the user asks to receive a file or generated image, call ${OUTBOUND_ARTIFACT_TOOL} with its path. Existing files can be sent directly; do not recreate or rename a file solely for delivery.`,
|
|
658
658
|
});
|
|
659
659
|
return true;
|
|
660
660
|
}
|
|
@@ -34,6 +34,8 @@ export function providerMessageIdsFor(value) {
|
|
|
34
34
|
value.message?.ts,
|
|
35
35
|
value.key?.id,
|
|
36
36
|
value.data?.message_id,
|
|
37
|
+
value.body?.msgid,
|
|
38
|
+
value.body?.message_id,
|
|
37
39
|
];
|
|
38
40
|
const id = candidates.find((candidate) => (
|
|
39
41
|
(typeof candidate === 'string' && candidate.trim())
|
|
@@ -32,14 +32,9 @@ import {
|
|
|
32
32
|
harnessQuestionText,
|
|
33
33
|
validHarnessQuestion,
|
|
34
34
|
} from './harness-question.mjs';
|
|
35
|
+
import { deliverOutboundArtifacts } from './semantic/artifact-delivery.mjs';
|
|
35
36
|
import {
|
|
36
|
-
materializeOutboundArtifact,
|
|
37
|
-
releaseOutboundArtifact,
|
|
38
|
-
} from './semantic/artifact.mjs';
|
|
39
|
-
import {
|
|
40
|
-
createArtifactFailureReceipt,
|
|
41
37
|
createDeliveryReceipt,
|
|
42
|
-
mergeDeliveryReceipts,
|
|
43
38
|
providerMessageIdsFor,
|
|
44
39
|
} from './semantic/delivery.mjs';
|
|
45
40
|
|
|
@@ -339,73 +334,29 @@ export class TextHarnessBridge {
|
|
|
339
334
|
}
|
|
340
335
|
|
|
341
336
|
async #deliverArtifacts(target, replyTo, artifacts = [], baseReceipt) {
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
} catch (error) {
|
|
366
|
-
if (this.#signal?.aborted) throw error;
|
|
367
|
-
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0) + 1;
|
|
368
|
-
this.#logger.warn?.(
|
|
369
|
-
`[dsh-im:${this.#descriptor.key}] result file delivery failed (${error?.code ?? 'unknown'})`,
|
|
370
|
-
);
|
|
371
|
-
let providerMessageIds = [];
|
|
372
|
-
let noticeSent = false;
|
|
373
|
-
try {
|
|
374
|
-
const notice = await this.#bot.sendText(
|
|
375
|
-
target,
|
|
376
|
-
artifactFailureText(artifact?.fileName, error, this.#descriptor),
|
|
377
|
-
);
|
|
378
|
-
providerMessageIds = providerMessageIdsFor(notice);
|
|
379
|
-
noticeSent = true;
|
|
380
|
-
} catch {
|
|
381
|
-
this.#logger.warn?.(
|
|
382
|
-
`[dsh-im:${this.#descriptor.key}] unable to send the safe result-file failure notice`,
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
const failureReceipt = createArtifactFailureReceipt({
|
|
386
|
-
artifactId: artifact?.artifactId ?? 'unknown',
|
|
387
|
-
deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
|
|
388
|
-
error,
|
|
389
|
-
providerMessageIds,
|
|
390
|
-
});
|
|
391
|
-
receipts.push(failureReceipt);
|
|
392
|
-
if (noticeSent || failureReceipt.artifacts[0]?.outcome === 'unknown') userVisible = true;
|
|
393
|
-
} finally {
|
|
394
|
-
releaseOutboundArtifact(artifact);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
const receipt = receipts.length === 0
|
|
398
|
-
? null
|
|
399
|
-
: receipts.length === 1
|
|
400
|
-
? receipts[0]
|
|
401
|
-
: mergeDeliveryReceipts({
|
|
402
|
-
deliveryId: replyTo,
|
|
403
|
-
presentation: baseReceipt
|
|
404
|
-
? `${this.#descriptor.key}-text-and-files`
|
|
405
|
-
: `${this.#descriptor.key}-files`,
|
|
406
|
-
receipts,
|
|
407
|
-
});
|
|
408
|
-
return { receipt, userVisible };
|
|
337
|
+
const delivery = await deliverOutboundArtifacts({
|
|
338
|
+
artifacts,
|
|
339
|
+
baseReceipt,
|
|
340
|
+
deliveryId: replyTo,
|
|
341
|
+
channelKey: this.#descriptor.key,
|
|
342
|
+
signal: this.#signal,
|
|
343
|
+
sendImage: typeof this.#bot.sendImage === 'function'
|
|
344
|
+
? (file) => this.#bot.sendImage(target, file)
|
|
345
|
+
: undefined,
|
|
346
|
+
sendFile: typeof this.#bot.sendFile === 'function'
|
|
347
|
+
? (file) => this.#bot.sendFile(target, file)
|
|
348
|
+
: undefined,
|
|
349
|
+
sendFailureNotice: (artifact, error) => this.#bot.sendText(
|
|
350
|
+
target,
|
|
351
|
+
artifactFailureText(artifact?.fileName, error, this.#descriptor),
|
|
352
|
+
),
|
|
353
|
+
logger: this.#logger,
|
|
354
|
+
});
|
|
355
|
+
this.#status.artifactsSent = (this.#status.artifactsSent ?? 0)
|
|
356
|
+
+ delivery.artifactsSent;
|
|
357
|
+
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
|
|
358
|
+
+ delivery.artifactSendErrors;
|
|
359
|
+
return { receipt: delivery.receipt, userVisible: delivery.userVisible };
|
|
409
360
|
}
|
|
410
361
|
|
|
411
362
|
async #process(message, messageId, senderId, conversationKey, {
|
|
@@ -35,33 +35,33 @@ function preserveProviderMetadata(target, source) {
|
|
|
35
35
|
return target;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function telegramArtifactProviderError(cause) {
|
|
38
|
+
function telegramArtifactProviderError(cause, mediaLabel = 'document') {
|
|
39
39
|
const providerCode = Number(cause?.providerCode);
|
|
40
40
|
const status = Number(cause?.status);
|
|
41
41
|
const message = cleanString(cause?.message) ?? '';
|
|
42
42
|
let code = 'artifact-provider-rejected';
|
|
43
|
-
let summary =
|
|
43
|
+
let summary = `Telegram rejected the ${mediaLabel}.`;
|
|
44
44
|
if (providerCode === 401 || providerCode === 403 || status === 401 || status === 403) {
|
|
45
45
|
code = 'artifact-permission-required';
|
|
46
|
-
summary =
|
|
46
|
+
summary = `Telegram denied permission to send the ${mediaLabel}.`;
|
|
47
47
|
} else if (providerCode === 413 || status === 413
|
|
48
48
|
|| /(?:file|request|entity).{0,20}(?:too (?:big|large)|size limit)|too (?:big|large)/i.test(message)) {
|
|
49
49
|
code = 'artifact-too-large';
|
|
50
|
-
summary =
|
|
50
|
+
summary = `The ${mediaLabel} exceeds Telegram's size limit.`;
|
|
51
51
|
} else if (providerCode === 429 || status === 429) {
|
|
52
52
|
code = 'artifact-rate-limited';
|
|
53
|
-
summary =
|
|
53
|
+
summary = `Telegram rate-limited ${mediaLabel} delivery.`;
|
|
54
54
|
} else if (providerCode >= 500 || status >= 500) {
|
|
55
55
|
code = 'artifact-delivery-uncertain';
|
|
56
|
-
summary =
|
|
56
|
+
summary = `Telegram ${mediaLabel} delivery result is uncertain.`;
|
|
57
57
|
}
|
|
58
58
|
const error = new Error(summary, { cause });
|
|
59
59
|
error.code = code;
|
|
60
60
|
return preserveProviderMetadata(error, cause);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function uncertainTelegramDelivery(cause) {
|
|
64
|
-
const error = new Error(
|
|
63
|
+
function uncertainTelegramDelivery(cause, mediaLabel = 'document') {
|
|
64
|
+
const error = new Error(`Telegram ${mediaLabel} delivery result is uncertain`, { cause });
|
|
65
65
|
error.code = 'artifact-delivery-uncertain';
|
|
66
66
|
return preserveProviderMetadata(error, cause);
|
|
67
67
|
}
|
|
@@ -184,15 +184,41 @@ export class TelegramApi {
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
async sendDocument({ chatId, file, replyToMessageId, messageThreadId, signal }) {
|
|
187
|
+
return this.#sendArtifact('sendDocument', 'document', 'document', {
|
|
188
|
+
chatId,
|
|
189
|
+
file,
|
|
190
|
+
replyToMessageId,
|
|
191
|
+
messageThreadId,
|
|
192
|
+
signal,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async sendPhoto({ chatId, file, replyToMessageId, messageThreadId, signal }) {
|
|
197
|
+
return this.#sendArtifact('sendPhoto', 'photo', 'photo', {
|
|
198
|
+
chatId,
|
|
199
|
+
file,
|
|
200
|
+
replyToMessageId,
|
|
201
|
+
messageThreadId,
|
|
202
|
+
signal,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async #sendArtifact(method, fieldName, mediaLabel, {
|
|
207
|
+
chatId,
|
|
208
|
+
file,
|
|
209
|
+
replyToMessageId,
|
|
210
|
+
messageThreadId,
|
|
211
|
+
signal,
|
|
212
|
+
}) {
|
|
187
213
|
if (!file || typeof file !== 'object'
|
|
188
214
|
|| typeof file.fileName !== 'string' || !file.fileName
|
|
189
215
|
|| !Buffer.isBuffer(file.bytes)) {
|
|
190
|
-
throw new TypeError(
|
|
216
|
+
throw new TypeError(`A Telegram ${mediaLabel} is required`);
|
|
191
217
|
}
|
|
192
218
|
const payload = new FormData();
|
|
193
219
|
payload.append('chat_id', String(chatId));
|
|
194
220
|
payload.append(
|
|
195
|
-
|
|
221
|
+
fieldName,
|
|
196
222
|
new Blob([file.bytes], { type: file.mediaType ?? 'application/octet-stream' }),
|
|
197
223
|
file.fileName,
|
|
198
224
|
);
|
|
@@ -206,7 +232,7 @@ export class TelegramApi {
|
|
|
206
232
|
if (signal?.aborted) throw abortReason(signal);
|
|
207
233
|
const uploadSignal = requestSignal(signal, this.#fileUploadTimeoutMs);
|
|
208
234
|
try {
|
|
209
|
-
return await this.#call(
|
|
235
|
+
return await this.#call(method, payload, {
|
|
210
236
|
signal: uploadSignal,
|
|
211
237
|
timeoutMs: this.#fileUploadTimeoutMs,
|
|
212
238
|
multipart: true,
|
|
@@ -214,9 +240,9 @@ export class TelegramApi {
|
|
|
214
240
|
} catch (error) {
|
|
215
241
|
if (signal?.aborted) throw abortReason(signal);
|
|
216
242
|
if (error?.code?.startsWith?.('telegram-')) {
|
|
217
|
-
throw telegramArtifactProviderError(error);
|
|
243
|
+
throw telegramArtifactProviderError(error, mediaLabel);
|
|
218
244
|
}
|
|
219
|
-
throw uncertainTelegramDelivery(error);
|
|
245
|
+
throw uncertainTelegramDelivery(error, mediaLabel);
|
|
220
246
|
}
|
|
221
247
|
}
|
|
222
248
|
|
|
@@ -211,6 +211,16 @@ export class TelegramBotClient {
|
|
|
211
211
|
});
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
sendImage(target, file) {
|
|
215
|
+
return this.#api.sendPhoto({
|
|
216
|
+
chatId: target.chatId,
|
|
217
|
+
file,
|
|
218
|
+
replyToMessageId: target.replyToMessageId,
|
|
219
|
+
messageThreadId: target.messageThreadId,
|
|
220
|
+
signal: this.#signal,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
214
224
|
async openStream(target) {
|
|
215
225
|
const stream = createEditableMessageStream({
|
|
216
226
|
limit: 4_000,
|