@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -540,6 +540,93 @@ export function createDingtalkApi({
540
540
  return true;
541
541
  }
542
542
 
543
+ async function sendArtifact({
544
+ clientId,
545
+ clientSecret,
546
+ target,
547
+ file,
548
+ signal,
549
+ }, { uploadType, messageKey, createMessageParams }) {
550
+ if (!file || typeof file !== 'object'
551
+ || typeof file.fileName !== 'string' || !file.fileName
552
+ || !Buffer.isBuffer(file.bytes)) {
553
+ throw new TypeError('A DingTalk file is required');
554
+ }
555
+ const normalizedTarget = normalizeFileTarget(target);
556
+ let token;
557
+ try {
558
+ token = await accessToken({ clientId, clientSecret, signal });
559
+ } catch (error) {
560
+ if (signal?.aborted) throw abortError(signal);
561
+ const status = Number(error?.status);
562
+ const fallback = error?.code === 'http-error' && status >= 400 && status < 500
563
+ ? 'artifact-provider-rejected'
564
+ : 'artifact-provider-failed';
565
+ throw dingtalkArtifactError(error, { fallback });
566
+ }
567
+ const uploadUrl = new URL('media/upload', DINGTALK_REGISTRATION_BASE_URL);
568
+ uploadUrl.searchParams.set('access_token', token);
569
+ uploadUrl.searchParams.set('type', uploadType);
570
+ const form = new FormData();
571
+ form.append(
572
+ 'media',
573
+ new Blob([file.bytes], { type: file.mediaType ?? 'application/octet-stream' }),
574
+ file.fileName,
575
+ );
576
+ let uploaded;
577
+ try {
578
+ uploaded = await requestMultipart(fetchImpl, uploadUrl, { body: form, signal });
579
+ } catch (error) {
580
+ if (signal?.aborted) throw abortError(signal);
581
+ const status = Number(error?.status);
582
+ const fallback = error?.code === 'http-error' && status >= 400 && status < 500
583
+ ? 'artifact-provider-rejected'
584
+ : 'artifact-provider-failed';
585
+ throw dingtalkArtifactError(error, { fallback });
586
+ }
587
+ const uploadRejection = rejectedProviderResponse(uploaded);
588
+ const mediaId = nonEmptyString(uploaded?.media_id);
589
+ if (uploadRejection || !mediaId) {
590
+ throw dingtalkArtifactError(new DingtalkApiError(
591
+ 'upload-rejected',
592
+ '钉钉服务拒绝了文件上传。',
593
+ { providerCode: uploadRejection ?? 'missing-media-id' },
594
+ ));
595
+ }
596
+ signal?.throwIfAborted();
597
+ const messageBody = {
598
+ robotCode: normalizedTarget.robotCode,
599
+ msgKey: messageKey,
600
+ msgParam: JSON.stringify(createMessageParams(mediaId, file)),
601
+ ...(normalizedTarget.type === 'group'
602
+ ? { openConversationId: normalizedTarget.openConversationId }
603
+ : { userIds: [normalizedTarget.userId] }),
604
+ };
605
+ const pathname = normalizedTarget.type === 'group'
606
+ ? 'v1.0/robot/groupMessages/send'
607
+ : 'v1.0/robot/oToMessages/batchSend';
608
+ let response;
609
+ try {
610
+ response = await requestJson(fetchImpl, endpoint(apiBase, pathname), {
611
+ body: messageBody,
612
+ headers: { 'x-acs-dingtalk-access-token': token },
613
+ signal,
614
+ action: '文件消息发送',
615
+ });
616
+ } catch (error) {
617
+ throw classifyDingtalkFinalDeliveryError(error, signal);
618
+ }
619
+ const sendRejection = rejectedProviderResponse(response);
620
+ if (sendRejection) {
621
+ throw dingtalkArtifactError(new DingtalkApiError(
622
+ 'send-rejected',
623
+ '钉钉服务拒绝了文件消息。',
624
+ { providerCode: sendRejection },
625
+ ));
626
+ }
627
+ return response;
628
+ }
629
+
543
630
  return Object.freeze({
544
631
  async beginRegistration({ signal } = {}) {
545
632
  const initialized = assertRegistrationOk(await requestJson(
@@ -842,89 +929,24 @@ export function createDingtalkApi({
842
929
  return true;
843
930
  },
844
931
 
845
- async sendFile({ clientId, clientSecret, target, file, signal }) {
846
- if (!file || typeof file !== 'object'
847
- || typeof file.fileName !== 'string' || !file.fileName
848
- || !Buffer.isBuffer(file.bytes)) {
849
- throw new TypeError('A DingTalk file is required');
850
- }
851
- const normalizedTarget = normalizeFileTarget(target);
852
- const fileType = dingtalkFileType(file.fileName);
853
- let token;
854
- try {
855
- token = await accessToken({ clientId, clientSecret, signal });
856
- } catch (error) {
857
- if (signal?.aborted) throw abortError(signal);
858
- const status = Number(error?.status);
859
- const fallback = error?.code === 'http-error' && status >= 400 && status < 500
860
- ? 'artifact-provider-rejected'
861
- : 'artifact-provider-failed';
862
- throw dingtalkArtifactError(error, { fallback });
863
- }
864
- const uploadUrl = new URL('media/upload', DINGTALK_REGISTRATION_BASE_URL);
865
- uploadUrl.searchParams.set('access_token', token);
866
- uploadUrl.searchParams.set('type', 'file');
867
- const form = new FormData();
868
- form.append(
869
- 'media',
870
- new Blob([file.bytes], { type: file.mediaType ?? 'application/octet-stream' }),
871
- file.fileName,
872
- );
873
- let uploaded;
874
- try {
875
- uploaded = await requestMultipart(fetchImpl, uploadUrl, { body: form, signal });
876
- } catch (error) {
877
- if (signal?.aborted) throw abortError(signal);
878
- const status = Number(error?.status);
879
- const fallback = error?.code === 'http-error' && status >= 400 && status < 500
880
- ? 'artifact-provider-rejected'
881
- : 'artifact-provider-failed';
882
- throw dingtalkArtifactError(error, { fallback });
883
- }
884
- const uploadRejection = rejectedProviderResponse(uploaded);
885
- if (uploadRejection || !nonEmptyString(uploaded?.media_id)) {
886
- throw dingtalkArtifactError(new DingtalkApiError(
887
- 'upload-rejected',
888
- '钉钉服务拒绝了文件上传。',
889
- { providerCode: uploadRejection ?? 'missing-media-id' },
890
- ));
891
- }
892
- signal?.throwIfAborted();
893
- const messageBody = {
894
- robotCode: normalizedTarget.robotCode,
895
- msgKey: 'sampleFile',
896
- msgParam: JSON.stringify({
897
- mediaId: uploaded.media_id,
932
+ async sendFile(request) {
933
+ return sendArtifact(request, {
934
+ uploadType: 'file',
935
+ messageKey: 'sampleFile',
936
+ createMessageParams: (mediaId, file) => ({
937
+ mediaId,
898
938
  fileName: file.fileName,
899
- fileType,
939
+ fileType: dingtalkFileType(file.fileName),
900
940
  }),
901
- ...(normalizedTarget.type === 'group'
902
- ? { openConversationId: normalizedTarget.openConversationId }
903
- : { userIds: [normalizedTarget.userId] }),
904
- };
905
- const pathname = normalizedTarget.type === 'group'
906
- ? 'v1.0/robot/groupMessages/send'
907
- : 'v1.0/robot/oToMessages/batchSend';
908
- let response;
909
- try {
910
- response = await requestJson(fetchImpl, endpoint(apiBase, pathname), {
911
- body: messageBody,
912
- headers: { 'x-acs-dingtalk-access-token': token },
913
- signal,
914
- action: '文件消息发送',
915
- });
916
- } catch (error) {
917
- throw classifyDingtalkFinalDeliveryError(error, signal);
918
- }
919
- const sendRejection = rejectedProviderResponse(response);
920
- if (sendRejection) {
921
- throw dingtalkArtifactError(new DingtalkApiError(
922
- 'send-rejected',
923
- '钉钉服务拒绝了文件消息。',
924
- { providerCode: sendRejection },
925
- ));
926
- }
927
- return response;
941
+ });
942
+ },
943
+
944
+ async sendImage(request) {
945
+ return sendArtifact(request, {
946
+ uploadType: 'image',
947
+ messageKey: 'sampleImageMsg',
948
+ createMessageParams: (mediaId) => ({ photoURL: mediaId }),
949
+ });
928
950
  },
929
951
 
930
952
  clearAccessToken(clientId) {
@@ -35,14 +35,9 @@ import {
35
35
  prefetchInboundFiles,
36
36
  } from '../shared/inbound-file.mjs';
37
37
  import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
38
+ import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
38
39
  import {
39
- materializeOutboundArtifact,
40
- releaseOutboundArtifact,
41
- } from '../shared/semantic/artifact.mjs';
42
- import {
43
- createArtifactFailureReceipt,
44
40
  createDeliveryReceipt,
45
- mergeDeliveryReceipts,
46
41
  providerMessageIdsFor,
47
42
  } from '../shared/semantic/delivery.mjs';
48
43
 
@@ -1084,70 +1079,39 @@ export class DingtalkHarnessBridge {
1084
1079
  }
1085
1080
 
1086
1081
  async #deliverArtifacts(target, sessionWebhook, replyTo, artifacts, baseReceipt) {
1087
- const receipts = baseReceipt ? [baseReceipt] : [];
1088
- let userVisible = Boolean(baseReceipt);
1089
- for (const artifact of artifacts) {
1090
- this.#signal?.throwIfAborted();
1091
- try {
1092
- if (typeof this.#api.sendFile !== 'function') {
1093
- const unavailable = new Error('DingTalk file delivery is unavailable');
1094
- unavailable.code = 'artifact-provider-unavailable';
1095
- throw unavailable;
1096
- }
1097
- const file = await materializeOutboundArtifact(artifact, {
1098
- signal: this.#signal,
1099
- });
1100
- const result = await this.#api.sendFile({
1101
- clientId: this.#clientId,
1102
- clientSecret: this.#clientSecret,
1103
- target,
1104
- file,
1105
- signal: this.#signal,
1106
- });
1107
- receipts.push(createDeliveryReceipt({
1108
- deliveryId: file.deliveryKey,
1109
- presentation: 'dingtalk-file',
1110
- providerMessageIds: dingtalkFileProviderIds(result),
1111
- artifacts: [{ artifactId: file.artifactId, outcome: 'sent' }],
1112
- }));
1113
- userVisible = true;
1114
- this.#status.artifactsSent = (this.#status.artifactsSent ?? 0) + 1;
1115
- } catch (error) {
1116
- if (this.#signal?.aborted) throw error;
1117
- this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0) + 1;
1118
- this.#logger.warn?.(
1119
- `[dsh-dingtalk] result file delivery failed (${error?.code ?? 'unknown'})`,
1120
- );
1121
- let noticeSent = false;
1122
- const providerMessageIds = await this.#send(
1123
- sessionWebhook,
1124
- artifactFailureText(artifact?.fileName, error),
1125
- ).then((ids) => {
1126
- noticeSent = true;
1127
- return ids;
1128
- }).catch(() => []);
1129
- const failureReceipt = createArtifactFailureReceipt({
1130
- artifactId: artifact?.artifactId ?? 'unknown',
1131
- deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
1132
- error,
1133
- providerMessageIds,
1134
- });
1135
- receipts.push(failureReceipt);
1136
- if (noticeSent || failureReceipt.artifacts[0]?.outcome === 'unknown') userVisible = true;
1137
- } finally {
1138
- releaseOutboundArtifact(artifact);
1139
- }
1140
- }
1141
- const receipt = receipts.length === 0
1142
- ? null
1143
- : receipts.length === 1
1144
- ? receipts[0]
1145
- : mergeDeliveryReceipts({
1146
- deliveryId: replyTo,
1147
- presentation: baseReceipt ? 'dingtalk-text-and-files' : 'dingtalk-files',
1148
- receipts,
1149
- });
1150
- return { receipt, userVisible };
1082
+ const sendArtifact = async (method, file) => dingtalkFileProviderIds(
1083
+ await this.#api[method]({
1084
+ clientId: this.#clientId,
1085
+ clientSecret: this.#clientSecret,
1086
+ target,
1087
+ file,
1088
+ signal: this.#signal,
1089
+ }),
1090
+ );
1091
+ const delivery = await deliverOutboundArtifacts({
1092
+ artifacts,
1093
+ baseReceipt,
1094
+ deliveryId: replyTo,
1095
+ aggregatePresentation: baseReceipt ? 'dingtalk-text-and-files' : 'dingtalk-files',
1096
+ channelKey: 'dingtalk',
1097
+ signal: this.#signal,
1098
+ sendImage: typeof this.#api.sendImage === 'function'
1099
+ ? (file) => sendArtifact('sendImage', file)
1100
+ : undefined,
1101
+ sendFile: typeof this.#api.sendFile === 'function'
1102
+ ? (file) => sendArtifact('sendFile', file)
1103
+ : undefined,
1104
+ sendFailureNotice: (artifact, error) => this.#send(
1105
+ sessionWebhook,
1106
+ artifactFailureText(artifact?.fileName, error),
1107
+ ),
1108
+ logger: this.#logger,
1109
+ });
1110
+ this.#status.artifactsSent = (this.#status.artifactsSent ?? 0)
1111
+ + delivery.artifactsSent;
1112
+ this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
1113
+ + delivery.artifactSendErrors;
1114
+ return { receipt: delivery.receipt, userVisible: delivery.userVisible };
1151
1115
  }
1152
1116
  }
1153
1117
 
@@ -38,14 +38,9 @@ import {
38
38
  } from '../shared/preset-command.mjs';
39
39
  import { runWorkspaceCommand, resolveSessionListWorkspace, workspacePathSnapshot } from '../shared/workspace-command.mjs';
40
40
  import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
41
+ import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
41
42
  import {
42
- materializeOutboundArtifact,
43
- releaseOutboundArtifact,
44
- } from '../shared/semantic/artifact.mjs';
45
- import {
46
- createArtifactFailureReceipt,
47
43
  createDeliveryReceipt,
48
- mergeDeliveryReceipts,
49
44
  } from '../shared/semantic/delivery.mjs';
50
45
  import {
51
46
  MENU_PAGE_SIZE,
@@ -1641,64 +1636,50 @@ export class FeishuHarnessBridge {
1641
1636
  }
1642
1637
 
1643
1638
  async #deliverArtifacts(chatId, replyTo, artifacts = [], baseReceipt) {
1644
- const receipts = baseReceipt ? [baseReceipt] : [];
1645
- let failureNoticeVisible = false;
1646
- for (const artifact of artifacts) {
1647
- this.#signal?.throwIfAborted();
1648
- try {
1649
- if (typeof this.#channel?.sendFile !== 'function') {
1650
- const unavailable = new Error('Feishu file delivery is unavailable');
1651
- unavailable.code = 'artifact-provider-unavailable';
1652
- throw unavailable;
1653
- }
1654
- const file = await materializeOutboundArtifact(artifact, {
1655
- signal: this.#signal,
1656
- });
1657
- receipts.push(await this.#channel.sendFile(chatId, file, {
1658
- replyTo,
1659
- signal: this.#signal,
1660
- }));
1661
- this.#status.artifactsSent = (this.#status.artifactsSent ?? 0) + 1;
1662
- } catch (error) {
1663
- if (this.#signal?.aborted) throw error;
1664
- this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0) + 1;
1665
- this.#logger.warn?.(
1666
- `[dsh-feishu] result file delivery failed (${error?.code ?? 'unknown'})`,
1667
- );
1668
- let noticeMessageId = null;
1669
- try {
1670
- noticeMessageId = await this.#send(chatId, artifactFailureText(artifact?.fileName, error));
1671
- failureNoticeVisible = true;
1672
- } catch {
1673
- this.#logger.warn?.('[dsh-feishu] unable to send the safe result-file failure notice');
1674
- }
1675
- receipts.push(createArtifactFailureReceipt({
1676
- artifactId: artifact?.artifactId ?? 'unknown',
1677
- deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
1678
- error,
1679
- providerMessageIds: noticeMessageId ? [noticeMessageId] : [],
1680
- }));
1681
- } finally {
1682
- releaseOutboundArtifact(artifact);
1683
- }
1684
- }
1685
- if (receipts.length === 0) {
1639
+ const delivery = await deliverOutboundArtifacts({
1640
+ artifacts,
1641
+ baseReceipt,
1642
+ deliveryId: baseReceipt?.deliveryId ?? artifacts[0]?.deliveryKey ?? replyTo,
1643
+ aggregatePresentation: baseReceipt ? 'feishu-text-and-files' : 'feishu-files',
1644
+ channelKey: 'feishu',
1645
+ signal: this.#signal,
1646
+ sendImage: typeof this.#channel?.sendImage === 'function'
1647
+ ? (file) => this.#channel.sendImage(chatId, file, {
1648
+ replyTo,
1649
+ signal: this.#signal,
1650
+ })
1651
+ : undefined,
1652
+ sendFile: typeof this.#channel?.sendFile === 'function'
1653
+ ? (file) => this.#channel.sendFile(chatId, file, {
1654
+ replyTo,
1655
+ signal: this.#signal,
1656
+ })
1657
+ : undefined,
1658
+ sendFailureNotice: async (artifact, error) => ({
1659
+ messageId: await this.#send(
1660
+ chatId,
1661
+ artifactFailureText(artifact?.fileName, error),
1662
+ ),
1663
+ }),
1664
+ logger: this.#logger,
1665
+ });
1666
+ this.#status.artifactsSent = (this.#status.artifactsSent ?? 0)
1667
+ + delivery.artifactsSent;
1668
+ this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
1669
+ + delivery.artifactSendErrors;
1670
+ if (!delivery.receipt) {
1686
1671
  return {
1687
1672
  receipt: createDeliveryReceipt({
1688
1673
  deliveryId: replyTo,
1689
1674
  presentation: 'feishu-files',
1690
1675
  }),
1691
- failureNoticeVisible,
1676
+ failureNoticeVisible: delivery.failureNoticeVisible,
1692
1677
  };
1693
1678
  }
1694
- const receipt = receipts.length === 1
1695
- ? receipts[0]
1696
- : mergeDeliveryReceipts({
1697
- deliveryId: baseReceipt?.deliveryId ?? artifacts[0]?.deliveryKey ?? replyTo,
1698
- presentation: baseReceipt ? 'feishu-text-and-files' : 'feishu-files',
1699
- receipts,
1700
- });
1701
- return { receipt, failureNoticeVisible };
1679
+ return {
1680
+ receipt: delivery.receipt,
1681
+ failureNoticeVisible: delivery.failureNoticeVisible,
1682
+ };
1702
1683
  }
1703
1684
 
1704
1685
  async #answerWithStream(event, key, message) {
@@ -114,9 +114,10 @@ function waitForFileOperation(operation, { signal, timeoutMs, stage }) {
114
114
  });
115
115
  }
116
116
 
117
- function deliveryUuid(file, chatId) {
117
+ function deliveryUuid(file, chatId, messageType) {
118
+ const seed = `${file.deliveryKey}\u0000${chatId}`;
118
119
  const digest = createHash('sha256')
119
- .update(`${file.deliveryKey}\u0000${chatId}`)
120
+ .update(messageType === 'file' ? seed : `${seed}\u0000${messageType}`)
120
121
  .digest('hex')
121
122
  .slice(0, 40);
122
123
  return `dshim_${digest}`;
@@ -230,6 +231,29 @@ export class VerifiedFeishuChannel {
230
231
  }
231
232
 
232
233
  async sendFile(chatId, file, { replyTo, signal } = {}) {
234
+ return this.#sendArtifact(chatId, file, {
235
+ replyTo,
236
+ signal,
237
+ messageType: 'file',
238
+ presentation: 'feishu-file',
239
+ });
240
+ }
241
+
242
+ async sendImage(chatId, file, { replyTo, signal } = {}) {
243
+ return this.#sendArtifact(chatId, file, {
244
+ replyTo,
245
+ signal,
246
+ messageType: 'image',
247
+ presentation: 'feishu-image',
248
+ });
249
+ }
250
+
251
+ async #sendArtifact(chatId, file, {
252
+ replyTo,
253
+ signal,
254
+ messageType,
255
+ presentation,
256
+ }) {
233
257
  signal?.throwIfAborted();
234
258
  if (typeof chatId !== 'string' || !chatId) throw new TypeError('chatId is required');
235
259
  if (!file || typeof file !== 'object'
@@ -243,13 +267,20 @@ export class VerifiedFeishuChannel {
243
267
  try {
244
268
  uploaded = await waitForFileOperation((operationSignal) => {
245
269
  operationSignal.throwIfAborted();
246
- const pending = this.#client.im.v1.file.create({
247
- data: {
248
- file_type: 'stream',
249
- file_name: file.fileName,
250
- file: file.bytes,
251
- },
252
- });
270
+ const pending = messageType === 'image'
271
+ ? this.#client.im.v1.image.create({
272
+ data: {
273
+ image_type: 'message',
274
+ image: file.bytes,
275
+ },
276
+ })
277
+ : this.#client.im.v1.file.create({
278
+ data: {
279
+ file_type: 'stream',
280
+ file_name: file.fileName,
281
+ file: file.bytes,
282
+ },
283
+ });
253
284
  trackOutboundArtifactProviderPromise(file, pending);
254
285
  return pending;
255
286
  }, {
@@ -262,21 +293,25 @@ export class VerifiedFeishuChannel {
262
293
  throw fileDeliveryError('upload', error);
263
294
  }
264
295
  signal?.throwIfAborted();
265
- const fileKey = uploaded?.file_key;
266
- if (typeof fileKey !== 'string' || !fileKey) {
296
+ const resourceKey = messageType === 'image'
297
+ ? uploaded?.image_key ?? uploaded?.data?.image_key
298
+ : uploaded?.file_key ?? uploaded?.data?.file_key;
299
+ if (typeof resourceKey !== 'string' || !resourceKey) {
267
300
  throw fileDeliveryError('upload', undefined, uploaded?.code);
268
301
  }
269
302
 
270
- const uuid = deliveryUuid(file, chatId);
271
- const content = JSON.stringify({ file_key: fileKey });
303
+ const uuid = deliveryUuid(file, chatId, messageType);
304
+ const content = JSON.stringify(messageType === 'image'
305
+ ? { image_key: resourceKey }
306
+ : { file_key: resourceKey });
272
307
  const request = replyTo
273
308
  ? {
274
309
  path: { message_id: replyTo },
275
- data: { msg_type: 'file', content, uuid },
310
+ data: { msg_type: messageType, content, uuid },
276
311
  }
277
312
  : {
278
313
  params: { receive_id_type: 'chat_id' },
279
- data: { receive_id: chatId, msg_type: 'file', content, uuid },
314
+ data: { receive_id: chatId, msg_type: messageType, content, uuid },
280
315
  };
281
316
  const send = () => {
282
317
  const pending = replyTo
@@ -300,7 +335,7 @@ export class VerifiedFeishuChannel {
300
335
  operationSignal.throwIfAborted();
301
336
 
302
337
  // Feishu documents 230049 as an uncertain asynchronous send result.
303
- // Reuse the same file_key and UUID once so the provider can deduplicate.
338
+ // Reuse the same resource key and UUID once so the provider can deduplicate.
304
339
  if (Number(result?.code) === 230049) {
305
340
  result = await send();
306
341
  operationSignal.throwIfAborted();
@@ -324,7 +359,7 @@ export class VerifiedFeishuChannel {
324
359
  }
325
360
  return createDeliveryReceipt({
326
361
  deliveryId: file.deliveryKey,
327
- presentation: 'feishu-file',
362
+ presentation,
328
363
  providerMessageIds: [messageId],
329
364
  artifacts: [{
330
365
  artifactId: file.artifactId,