@xmanrui/dsh-im 1.3.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.
@@ -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 receipts = baseReceipt ? [baseReceipt] : [];
343
- let userVisible = Boolean(baseReceipt);
344
- for (const artifact of artifacts) {
345
- this.#signal?.throwIfAborted();
346
- try {
347
- if (typeof this.#bot.sendFile !== 'function') {
348
- const unavailable = new Error('Native file delivery is unavailable');
349
- unavailable.code = 'artifact-provider-unavailable';
350
- throw unavailable;
351
- }
352
- const file = await materializeOutboundArtifact(artifact, {
353
- signal: this.#signal,
354
- });
355
- this.#signal?.throwIfAborted();
356
- const result = await this.#bot.sendFile(target, file);
357
- receipts.push(createDeliveryReceipt({
358
- deliveryId: file.deliveryKey,
359
- presentation: `${this.#descriptor.key}-file`,
360
- providerMessageIds: providerMessageIdsFor(result),
361
- artifacts: [{ artifactId: file.artifactId, outcome: 'sent' }],
362
- }));
363
- userVisible = true;
364
- this.#status.artifactsSent = (this.#status.artifactsSent ?? 0) + 1;
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 = 'Telegram rejected the document.';
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 = 'Telegram denied permission to send the document.';
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 = 'The document exceeds Telegram\'s size limit.';
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 = 'Telegram rate-limited document delivery.';
53
+ summary = `Telegram rate-limited ${mediaLabel} delivery.`;
54
54
  } else if (providerCode >= 500 || status >= 500) {
55
55
  code = 'artifact-delivery-uncertain';
56
- summary = 'Telegram document delivery result is uncertain.';
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('Telegram document delivery result is uncertain', { cause });
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('A Telegram document is required');
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
- 'document',
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('sendDocument', payload, {
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,
@@ -31,15 +31,10 @@ import {
31
31
  inboundFileUserMessage,
32
32
  } from '../shared/inbound-file.mjs';
33
33
  import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
34
+ import { trackOutboundArtifactProviderPromise } from '../shared/semantic/artifact.mjs';
35
+ import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
34
36
  import {
35
- materializeOutboundArtifact,
36
- releaseOutboundArtifact,
37
- trackOutboundArtifactProviderPromise,
38
- } from '../shared/semantic/artifact.mjs';
39
- import {
40
- createArtifactFailureReceipt,
41
37
  createDeliveryReceipt,
42
- mergeDeliveryReceipts,
43
38
  } from '../shared/semantic/delivery.mjs';
44
39
 
45
40
  const DEFAULT_FILE_UPLOAD_TIMEOUT_MS = 120_000;
@@ -353,6 +348,71 @@ function wecomArtifactError(error, { dispatched = false } = {}) {
353
348
  return wrapped;
354
349
  }
355
350
 
351
+ async function sendWecomMedia(
352
+ client,
353
+ chatId,
354
+ file,
355
+ mediaType,
356
+ { signal, timeoutMs = DEFAULT_FILE_UPLOAD_TIMEOUT_MS } = {},
357
+ ) {
358
+ signal?.throwIfAborted();
359
+ if (typeof client?.uploadMedia !== 'function'
360
+ || typeof client?.sendMediaMessage !== 'function') {
361
+ const unavailable = new Error(`Enterprise WeChat ${mediaType} delivery is unavailable`);
362
+ unavailable.code = 'artifact-provider-unavailable';
363
+ throw unavailable;
364
+ }
365
+
366
+ const timeout = AbortSignal.timeout(timeoutMs);
367
+ const waitSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
368
+ let uploaded;
369
+ try {
370
+ const pending = client.uploadMedia(file.bytes, {
371
+ type: mediaType,
372
+ filename: file.fileName,
373
+ });
374
+ trackOutboundArtifactProviderPromise(file, pending);
375
+ uploaded = await waitWithSignal(pending, waitSignal);
376
+ } catch (error) {
377
+ if (signal?.aborted) throw abortReason(signal);
378
+ throw wecomArtifactError(error);
379
+ }
380
+
381
+ signal?.throwIfAborted();
382
+ const mediaId = nonEmptyString(uploaded?.media_id);
383
+ if (!mediaId) {
384
+ const rejected = new Error(`Enterprise WeChat ${mediaType} upload returned no media id`);
385
+ rejected.code = 'artifact-provider-rejected';
386
+ throw rejected;
387
+ }
388
+
389
+ let sent;
390
+ try {
391
+ const pending = client.sendMediaMessage(chatId, mediaType, mediaId);
392
+ trackOutboundArtifactProviderPromise(file, pending);
393
+ sent = await waitWithSignal(pending, waitSignal);
394
+ } catch (error) {
395
+ if (signal?.aborted) throw abortReason(signal);
396
+ throw wecomArtifactError(error, { dispatched: true });
397
+ }
398
+
399
+ signal?.throwIfAborted();
400
+ const providerCode = Number(sent?.body?.errcode ?? sent?.errcode);
401
+ if (Number.isFinite(providerCode) && providerCode !== 0) {
402
+ throw wecomArtifactError({ providerCode });
403
+ }
404
+ return sent;
405
+ }
406
+
407
+ /** Send one materialized artifact through Enterprise WeChat's native image message. */
408
+ export function sendWecomImage(client, chatId, file, options) {
409
+ return sendWecomMedia(client, chatId, file, 'image', options);
410
+ }
411
+
412
+ function sendWecomFile(client, chatId, file, options) {
413
+ return sendWecomMedia(client, chatId, file, 'file', options);
414
+ }
415
+
356
416
  function answerTextForDelivery(answer, artifacts) {
357
417
  if (typeof answer === 'string' && answer.trim()) return answer;
358
418
  return artifacts.length > 0 ? '结果文件已生成。' : '任务已完成,但没有生成可显示的文本。';
@@ -650,98 +710,35 @@ export class WecomHarnessBridge {
650
710
  if (artifacts.length === 0) {
651
711
  return { receipt: baseReceipt, failureNoticeVisible: false };
652
712
  }
653
- const receipts = baseReceipt ? [baseReceipt] : [];
654
- let failureNoticeVisible = false;
655
- for (const artifact of artifacts) {
656
- this.#signal?.throwIfAborted();
657
- try {
658
- if (typeof this.#client.uploadMedia !== 'function'
659
- || typeof this.#client.sendMediaMessage !== 'function') {
660
- const unavailable = new Error('Enterprise WeChat file delivery is unavailable');
661
- unavailable.code = 'artifact-provider-unavailable';
662
- throw unavailable;
663
- }
664
- const file = await materializeOutboundArtifact(artifact, {
665
- signal: this.#signal,
666
- });
667
- this.#signal?.throwIfAborted();
668
- const timeout = AbortSignal.timeout(this.#fileUploadTimeoutMs);
669
- const waitSignal = this.#signal ? AbortSignal.any([this.#signal, timeout]) : timeout;
670
- let uploaded;
671
- try {
672
- const pending = this.#client.uploadMedia(file.bytes, {
673
- type: 'file',
674
- filename: file.fileName,
675
- });
676
- trackOutboundArtifactProviderPromise(file, pending);
677
- uploaded = await waitWithSignal(pending, waitSignal);
678
- } catch (error) {
679
- if (this.#signal?.aborted) throw abortReason(this.#signal);
680
- throw wecomArtifactError(error);
681
- }
682
- this.#signal?.throwIfAborted();
683
- const mediaId = nonEmptyString(uploaded?.media_id);
684
- if (!mediaId) {
685
- const rejected = new Error('Enterprise WeChat upload returned no media id');
686
- rejected.code = 'artifact-provider-rejected';
687
- throw rejected;
688
- }
689
- let sent;
690
- try {
691
- const pending = this.#client.sendMediaMessage(chatId, 'file', mediaId);
692
- trackOutboundArtifactProviderPromise(file, pending);
693
- sent = await waitWithSignal(pending, waitSignal);
694
- } catch (error) {
695
- if (this.#signal?.aborted) throw abortReason(this.#signal);
696
- throw wecomArtifactError(error, { dispatched: true });
697
- }
698
- this.#signal?.throwIfAborted();
699
- const providerCode = Number(sent?.body?.errcode ?? sent?.errcode);
700
- if (Number.isFinite(providerCode) && providerCode !== 0) {
701
- throw wecomArtifactError({ providerCode });
702
- }
703
- const messageId = providerMessageId(sent);
704
- receipts.push(createDeliveryReceipt({
705
- deliveryId: file.deliveryKey,
706
- presentation: 'wecom-file',
707
- providerMessageIds: messageId ? [messageId] : [],
708
- artifacts: [{ artifactId: file.artifactId, outcome: 'sent' }],
709
- }));
710
- this.#status.artifactsSent = (this.#status.artifactsSent ?? 0) + 1;
711
- } catch (error) {
712
- if (this.#signal?.aborted) throw error;
713
- this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0) + 1;
714
- this.#logger.warn?.(
715
- `[dsh-im:wecom] result file delivery failed (${error?.code ?? 'unknown'})`,
716
- );
717
- let providerMessageIds = [];
718
- try {
719
- providerMessageIds = await this.#sendActive(
720
- chatId,
721
- artifactFailureText(artifact?.fileName, error),
722
- );
723
- failureNoticeVisible = true;
724
- } catch (noticeError) {
725
- if (this.#signal?.aborted) throw noticeError;
726
- this.#logger.warn?.('[dsh-im:wecom] unable to send the safe result-file failure notice');
727
- }
728
- receipts.push(createArtifactFailureReceipt({
729
- artifactId: artifact?.artifactId ?? 'unknown',
730
- deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
731
- error,
732
- providerMessageIds,
733
- }));
734
- } finally {
735
- releaseOutboundArtifact(artifact);
736
- }
737
- }
738
- return {
739
- receipt: mergeDeliveryReceipts({
740
- deliveryId: replyTo,
741
- presentation: baseReceipt ? 'wecom-text-and-files' : 'wecom-files',
742
- receipts,
713
+ const delivery = await deliverOutboundArtifacts({
714
+ artifacts,
715
+ baseReceipt,
716
+ deliveryId: replyTo,
717
+ aggregatePresentation: baseReceipt ? 'wecom-text-and-files' : 'wecom-files',
718
+ alwaysMerge: true,
719
+ channelKey: 'wecom',
720
+ signal: this.#signal,
721
+ sendImage: (file) => sendWecomImage(this.#client, chatId, file, {
722
+ signal: this.#signal,
723
+ timeoutMs: this.#fileUploadTimeoutMs,
743
724
  }),
744
- failureNoticeVisible,
725
+ sendFile: (file) => sendWecomFile(this.#client, chatId, file, {
726
+ signal: this.#signal,
727
+ timeoutMs: this.#fileUploadTimeoutMs,
728
+ }),
729
+ sendFailureNotice: (artifact, error) => this.#sendActive(
730
+ chatId,
731
+ artifactFailureText(artifact?.fileName, error),
732
+ ),
733
+ logger: this.#logger,
734
+ });
735
+ this.#status.artifactsSent = (this.#status.artifactsSent ?? 0)
736
+ + delivery.artifactsSent;
737
+ this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
738
+ + delivery.artifactSendErrors;
739
+ return {
740
+ receipt: delivery.receipt,
741
+ failureNoticeVisible: delivery.failureNoticeVisible,
745
742
  };
746
743
  }
747
744