@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
|
@@ -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
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -457,6 +457,123 @@ function validateLoginResponse(value) {
|
|
|
457
457
|
export function createWeixinApi({ fetchImpl = fetch } = {}) {
|
|
458
458
|
if (typeof fetchImpl !== 'function') throw new TypeError('fetchImpl must be a function');
|
|
459
459
|
|
|
460
|
+
async function sendArtifact({
|
|
461
|
+
baseUrl,
|
|
462
|
+
token,
|
|
463
|
+
toUserId,
|
|
464
|
+
file,
|
|
465
|
+
contextToken,
|
|
466
|
+
runId,
|
|
467
|
+
signal,
|
|
468
|
+
}, { mediaType, createItem }) {
|
|
469
|
+
const recipient = nonEmptyString(toUserId);
|
|
470
|
+
if (!recipient || !file || typeof file !== 'object'
|
|
471
|
+
|| typeof file.fileName !== 'string' || !file.fileName
|
|
472
|
+
|| !Buffer.isBuffer(file.bytes)) {
|
|
473
|
+
throw new TypeError('toUserId and a file are required');
|
|
474
|
+
}
|
|
475
|
+
signal?.throwIfAborted();
|
|
476
|
+
const fileKey = randomBytes(16).toString('hex');
|
|
477
|
+
const aesKey = randomBytes(16);
|
|
478
|
+
const rawMd5 = createHash('md5').update(file.bytes).digest('hex');
|
|
479
|
+
let upload;
|
|
480
|
+
try {
|
|
481
|
+
upload = await requestJson(fetchImpl, {
|
|
482
|
+
method: 'POST',
|
|
483
|
+
baseUrl,
|
|
484
|
+
endpoint: 'ilink/bot/getuploadurl',
|
|
485
|
+
token,
|
|
486
|
+
signal,
|
|
487
|
+
body: {
|
|
488
|
+
filekey: fileKey,
|
|
489
|
+
media_type: mediaType,
|
|
490
|
+
to_user_id: recipient,
|
|
491
|
+
rawsize: file.bytes.byteLength,
|
|
492
|
+
rawfilemd5: rawMd5,
|
|
493
|
+
filesize: aesEcbPaddedSize(file.bytes.byteLength),
|
|
494
|
+
no_need_thumb: true,
|
|
495
|
+
aeskey: aesKey.toString('hex'),
|
|
496
|
+
base_info: baseInfo(),
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
} catch (error) {
|
|
500
|
+
if (signal?.aborted) throw abortError(signal);
|
|
501
|
+
const status = Number(error?.status);
|
|
502
|
+
const fallback = error?.code === 'http-error' && status >= 400 && status < 500
|
|
503
|
+
? 'artifact-provider-rejected'
|
|
504
|
+
: 'artifact-provider-failed';
|
|
505
|
+
throw weixinArtifactError(error, { fallback });
|
|
506
|
+
}
|
|
507
|
+
const uploadRejection = rejectedProviderResponse(upload);
|
|
508
|
+
if (uploadRejection) {
|
|
509
|
+
throw weixinArtifactError(new WeixinApiError(
|
|
510
|
+
'upload-url-rejected',
|
|
511
|
+
'微信服务拒绝了文件上传请求。',
|
|
512
|
+
{ providerCode: uploadRejection },
|
|
513
|
+
));
|
|
514
|
+
}
|
|
515
|
+
const uploadUrl = weixinCdnUploadUrl(upload, fileKey);
|
|
516
|
+
const ciphertext = encryptWeixinUpload(file.bytes, aesKey);
|
|
517
|
+
let downloadParam;
|
|
518
|
+
try {
|
|
519
|
+
downloadParam = await uploadWeixinCdn(fetchImpl, uploadUrl, ciphertext, { signal });
|
|
520
|
+
} catch (error) {
|
|
521
|
+
if (signal?.aborted) throw abortError(signal);
|
|
522
|
+
const status = Number(error?.status);
|
|
523
|
+
const fallback = error?.code === 'upload-rejected' || (status >= 400 && status < 500)
|
|
524
|
+
? 'artifact-provider-rejected'
|
|
525
|
+
: 'artifact-provider-failed';
|
|
526
|
+
throw weixinArtifactError(error, { fallback });
|
|
527
|
+
}
|
|
528
|
+
signal?.throwIfAborted();
|
|
529
|
+
const deliverySeed = nonEmptyString(file.deliveryKey) ?? nonEmptyString(file.artifactId)
|
|
530
|
+
?? randomUUID();
|
|
531
|
+
const clientIdSeed = mediaType === 3 ? deliverySeed : `${deliverySeed}\u0000${mediaType}`;
|
|
532
|
+
const clientId = `dsh-weixin-${createHash('sha256')
|
|
533
|
+
.update(clientIdSeed)
|
|
534
|
+
.digest('hex')
|
|
535
|
+
.slice(0, 32)}`;
|
|
536
|
+
const media = {
|
|
537
|
+
encrypt_query_param: downloadParam,
|
|
538
|
+
aes_key: Buffer.from(aesKey.toString('hex')).toString('base64'),
|
|
539
|
+
encrypt_type: 1,
|
|
540
|
+
};
|
|
541
|
+
let response;
|
|
542
|
+
try {
|
|
543
|
+
response = await requestJson(fetchImpl, {
|
|
544
|
+
method: 'POST',
|
|
545
|
+
baseUrl,
|
|
546
|
+
endpoint: 'ilink/bot/sendmessage',
|
|
547
|
+
token,
|
|
548
|
+
signal,
|
|
549
|
+
body: {
|
|
550
|
+
msg: {
|
|
551
|
+
from_user_id: '',
|
|
552
|
+
to_user_id: recipient,
|
|
553
|
+
client_id: clientId,
|
|
554
|
+
message_type: 2,
|
|
555
|
+
message_state: 2,
|
|
556
|
+
item_list: [createItem({ file, media, ciphertextSize: ciphertext.byteLength })],
|
|
557
|
+
...(nonEmptyString(contextToken) ? { context_token: contextToken.trim() } : {}),
|
|
558
|
+
...(nonEmptyString(runId) ? { run_id: runId.trim() } : {}),
|
|
559
|
+
},
|
|
560
|
+
base_info: baseInfo(),
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
} catch (error) {
|
|
564
|
+
throw classifyWeixinFinalDeliveryError(error, signal);
|
|
565
|
+
}
|
|
566
|
+
const sendRejection = rejectedProviderResponse(response);
|
|
567
|
+
if (sendRejection) {
|
|
568
|
+
throw weixinArtifactError(new WeixinApiError(
|
|
569
|
+
'send-rejected',
|
|
570
|
+
'微信服务拒绝了文件消息。',
|
|
571
|
+
{ providerCode: sendRejection },
|
|
572
|
+
));
|
|
573
|
+
}
|
|
574
|
+
return { messageId: clientId };
|
|
575
|
+
}
|
|
576
|
+
|
|
460
577
|
return Object.freeze({
|
|
461
578
|
inboundImages(message) {
|
|
462
579
|
return extractWeixinImages(message, { fetchImpl });
|
|
@@ -549,115 +666,31 @@ export function createWeixinApi({ fetchImpl = fetch } = {}) {
|
|
|
549
666
|
return true;
|
|
550
667
|
},
|
|
551
668
|
|
|
552
|
-
async sendFile(
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const aesKey = randomBytes(16);
|
|
562
|
-
const rawMd5 = createHash('md5').update(file.bytes).digest('hex');
|
|
563
|
-
let upload;
|
|
564
|
-
try {
|
|
565
|
-
upload = await requestJson(fetchImpl, {
|
|
566
|
-
method: 'POST',
|
|
567
|
-
baseUrl,
|
|
568
|
-
endpoint: 'ilink/bot/getuploadurl',
|
|
569
|
-
token,
|
|
570
|
-
signal,
|
|
571
|
-
body: {
|
|
572
|
-
filekey: fileKey,
|
|
573
|
-
media_type: 3,
|
|
574
|
-
to_user_id: recipient,
|
|
575
|
-
rawsize: file.bytes.byteLength,
|
|
576
|
-
rawfilemd5: rawMd5,
|
|
577
|
-
filesize: aesEcbPaddedSize(file.bytes.byteLength),
|
|
578
|
-
no_need_thumb: true,
|
|
579
|
-
aeskey: aesKey.toString('hex'),
|
|
580
|
-
base_info: baseInfo(),
|
|
669
|
+
async sendFile(request) {
|
|
670
|
+
return sendArtifact(request, {
|
|
671
|
+
mediaType: 3,
|
|
672
|
+
createItem: ({ file, media }) => ({
|
|
673
|
+
type: 4,
|
|
674
|
+
file_item: {
|
|
675
|
+
media,
|
|
676
|
+
file_name: file.fileName,
|
|
677
|
+
len: String(file.bytes.byteLength),
|
|
581
678
|
},
|
|
582
|
-
})
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
'upload-url-rejected',
|
|
595
|
-
'微信服务拒绝了文件上传请求。',
|
|
596
|
-
{ providerCode: uploadRejection },
|
|
597
|
-
));
|
|
598
|
-
}
|
|
599
|
-
const uploadUrl = weixinCdnUploadUrl(upload, fileKey);
|
|
600
|
-
const ciphertext = encryptWeixinUpload(file.bytes, aesKey);
|
|
601
|
-
let downloadParam;
|
|
602
|
-
try {
|
|
603
|
-
downloadParam = await uploadWeixinCdn(fetchImpl, uploadUrl, ciphertext, { signal });
|
|
604
|
-
} catch (error) {
|
|
605
|
-
if (signal?.aborted) throw abortError(signal);
|
|
606
|
-
const status = Number(error?.status);
|
|
607
|
-
const fallback = error?.code === 'upload-rejected' || (status >= 400 && status < 500)
|
|
608
|
-
? 'artifact-provider-rejected'
|
|
609
|
-
: 'artifact-provider-failed';
|
|
610
|
-
throw weixinArtifactError(error, { fallback });
|
|
611
|
-
}
|
|
612
|
-
signal?.throwIfAborted();
|
|
613
|
-
const deliverySeed = nonEmptyString(file.deliveryKey) ?? nonEmptyString(file.artifactId)
|
|
614
|
-
?? randomUUID();
|
|
615
|
-
const clientId = `dsh-weixin-${createHash('sha256').update(deliverySeed).digest('hex').slice(0, 32)}`;
|
|
616
|
-
let response;
|
|
617
|
-
try {
|
|
618
|
-
response = await requestJson(fetchImpl, {
|
|
619
|
-
method: 'POST',
|
|
620
|
-
baseUrl,
|
|
621
|
-
endpoint: 'ilink/bot/sendmessage',
|
|
622
|
-
token,
|
|
623
|
-
signal,
|
|
624
|
-
body: {
|
|
625
|
-
msg: {
|
|
626
|
-
from_user_id: '',
|
|
627
|
-
to_user_id: recipient,
|
|
628
|
-
client_id: clientId,
|
|
629
|
-
message_type: 2,
|
|
630
|
-
message_state: 2,
|
|
631
|
-
item_list: [{
|
|
632
|
-
type: 4,
|
|
633
|
-
file_item: {
|
|
634
|
-
media: {
|
|
635
|
-
encrypt_query_param: downloadParam,
|
|
636
|
-
aes_key: Buffer.from(aesKey.toString('hex')).toString('base64'),
|
|
637
|
-
encrypt_type: 1,
|
|
638
|
-
},
|
|
639
|
-
file_name: file.fileName,
|
|
640
|
-
len: String(file.bytes.byteLength),
|
|
641
|
-
},
|
|
642
|
-
}],
|
|
643
|
-
...(nonEmptyString(contextToken) ? { context_token: contextToken.trim() } : {}),
|
|
644
|
-
...(nonEmptyString(runId) ? { run_id: runId.trim() } : {}),
|
|
645
|
-
},
|
|
646
|
-
base_info: baseInfo(),
|
|
679
|
+
}),
|
|
680
|
+
});
|
|
681
|
+
},
|
|
682
|
+
|
|
683
|
+
async sendImage(request) {
|
|
684
|
+
return sendArtifact(request, {
|
|
685
|
+
mediaType: 1,
|
|
686
|
+
createItem: ({ media, ciphertextSize }) => ({
|
|
687
|
+
type: 2,
|
|
688
|
+
image_item: {
|
|
689
|
+
media,
|
|
690
|
+
mid_size: ciphertextSize,
|
|
647
691
|
},
|
|
648
|
-
})
|
|
649
|
-
}
|
|
650
|
-
throw classifyWeixinFinalDeliveryError(error, signal);
|
|
651
|
-
}
|
|
652
|
-
const sendRejection = rejectedProviderResponse(response);
|
|
653
|
-
if (sendRejection) {
|
|
654
|
-
throw weixinArtifactError(new WeixinApiError(
|
|
655
|
-
'send-rejected',
|
|
656
|
-
'微信服务拒绝了文件消息。',
|
|
657
|
-
{ providerCode: sendRejection },
|
|
658
|
-
));
|
|
659
|
-
}
|
|
660
|
-
return { messageId: clientId };
|
|
692
|
+
}),
|
|
693
|
+
});
|
|
661
694
|
},
|
|
662
695
|
|
|
663
696
|
async notifyStart({ baseUrl, token, signal }) {
|
|
@@ -38,14 +38,9 @@ import {
|
|
|
38
38
|
prefetchInboundFiles,
|
|
39
39
|
} from '../shared/inbound-file.mjs';
|
|
40
40
|
import { rememberConnectionTestTarget } from '../shared/connection-test.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
|
providerMessageIdsFor,
|
|
50
45
|
} from '../shared/semantic/delivery.mjs';
|
|
51
46
|
|
|
@@ -869,73 +864,40 @@ export class WeixinHarnessBridge {
|
|
|
869
864
|
}
|
|
870
865
|
|
|
871
866
|
async #deliverArtifacts(toUserId, replyTo, artifacts, contextToken, runId, baseReceipt) {
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
);
|
|
908
|
-
let noticeSent = false;
|
|
909
|
-
const providerMessageIds = await this.#send(
|
|
910
|
-
toUserId,
|
|
911
|
-
artifactFailureText(artifact?.fileName, error),
|
|
912
|
-
contextToken,
|
|
913
|
-
runId,
|
|
914
|
-
).then((ids) => {
|
|
915
|
-
noticeSent = true;
|
|
916
|
-
return ids;
|
|
917
|
-
}).catch(() => []);
|
|
918
|
-
const failureReceipt = createArtifactFailureReceipt({
|
|
919
|
-
artifactId: artifact?.artifactId ?? 'unknown',
|
|
920
|
-
deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
|
|
921
|
-
error,
|
|
922
|
-
providerMessageIds,
|
|
923
|
-
});
|
|
924
|
-
receipts.push(failureReceipt);
|
|
925
|
-
if (noticeSent || failureReceipt.artifacts[0]?.outcome === 'unknown') userVisible = true;
|
|
926
|
-
} finally {
|
|
927
|
-
releaseOutboundArtifact(artifact);
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
const receipt = receipts.length === 0
|
|
931
|
-
? null
|
|
932
|
-
: receipts.length === 1
|
|
933
|
-
? receipts[0]
|
|
934
|
-
: mergeDeliveryReceipts({
|
|
935
|
-
deliveryId: replyTo,
|
|
936
|
-
presentation: baseReceipt ? 'weixin-text-and-files' : 'weixin-files',
|
|
937
|
-
receipts,
|
|
938
|
-
});
|
|
939
|
-
return { receipt, userVisible };
|
|
867
|
+
const sendArtifact = (method, file) => this.#api[method]({
|
|
868
|
+
baseUrl: this.#baseUrl,
|
|
869
|
+
token: this.#token,
|
|
870
|
+
toUserId,
|
|
871
|
+
file,
|
|
872
|
+
contextToken,
|
|
873
|
+
runId,
|
|
874
|
+
signal: this.#signal,
|
|
875
|
+
});
|
|
876
|
+
const delivery = await deliverOutboundArtifacts({
|
|
877
|
+
artifacts,
|
|
878
|
+
baseReceipt,
|
|
879
|
+
deliveryId: replyTo,
|
|
880
|
+
aggregatePresentation: baseReceipt ? 'weixin-text-and-files' : 'weixin-files',
|
|
881
|
+
channelKey: 'weixin',
|
|
882
|
+
signal: this.#signal,
|
|
883
|
+
sendImage: typeof this.#api.sendImage === 'function'
|
|
884
|
+
? (file) => sendArtifact('sendImage', file)
|
|
885
|
+
: undefined,
|
|
886
|
+
sendFile: typeof this.#api.sendFile === 'function'
|
|
887
|
+
? (file) => sendArtifact('sendFile', file)
|
|
888
|
+
: undefined,
|
|
889
|
+
sendFailureNotice: (artifact, error) => this.#send(
|
|
890
|
+
toUserId,
|
|
891
|
+
artifactFailureText(artifact?.fileName, error),
|
|
892
|
+
contextToken,
|
|
893
|
+
runId,
|
|
894
|
+
),
|
|
895
|
+
logger: this.#logger,
|
|
896
|
+
});
|
|
897
|
+
this.#status.artifactsSent = (this.#status.artifactsSent ?? 0)
|
|
898
|
+
+ delivery.artifactsSent;
|
|
899
|
+
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
|
|
900
|
+
+ delivery.artifactSendErrors;
|
|
901
|
+
return { receipt: delivery.receipt, userVisible: delivery.userVisible };
|
|
940
902
|
}
|
|
941
903
|
}
|