@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/README.en.md +3 -3
- package/README.md +3 -3
- package/lib/index.js +168 -158
- 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/markdown-reply.mjs +176 -0
- package/src/channels/qq/qq-bridge.mjs +136 -105
- package/src/channels/shared/harness-client.mjs +66 -13
- 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
package/package.json
CHANGED
|
@@ -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(
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
: '
|
|
908
|
-
|
|
909
|
-
|
|
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
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
)
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
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
|
-
|
|
1695
|
-
|
|
1696
|
-
:
|
|
1697
|
-
|
|
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(`${
|
|
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 =
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
266
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
362
|
+
presentation,
|
|
328
363
|
providerMessageIds: [messageId],
|
|
329
364
|
artifacts: [{
|
|
330
365
|
artifactId: file.artifactId,
|