@soimy/dingtalk 3.6.2 → 3.6.3

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.
@@ -182,6 +182,10 @@ function buildPersistedOutboundText(text: string, options: SendMessageOptions):
182
182
  return text;
183
183
  }
184
184
 
185
+ function shouldRouteSessionMediaViaProactive(mediaType?: string | null): mediaType is "voice" | "video" | "file" {
186
+ return mediaType === "voice" || mediaType === "video" || mediaType === "file";
187
+ }
188
+
185
189
  const DINGTALK_TEXT_CHUNK_LIMIT = 3800;
186
190
  const CARD_MEDIA_CONTROLLER_ATTACH_WAIT_MS = 150;
187
191
  const CARD_MEDIA_CONTROLLER_ATTACH_POLL_MS = 25;
@@ -726,55 +730,30 @@ export async function sendBySession(
726
730
  const token = await getAccessToken(config, options.log);
727
731
  const log = options.log || getLogger();
728
732
 
729
- // Session webhook supports native media messages; prefer that when media info is available.
733
+ // Keep session webhooks on text/markdown. Images can render through markdown
734
+ // media references; other media types are routed by sendMessage via OpenAPI.
730
735
  if (options.mediaPath && options.mediaType) {
731
- const uploadResult = await uploadMedia(config, options.mediaPath, options.mediaType, log, {
732
- mediaLocalRoots: options.mediaLocalRoots,
733
- });
734
- if (uploadResult) {
735
- const { mediaId, buffer, durationMs: uploadedDurationMs } = uploadResult;
736
- let body: any;
737
-
738
- if (options.mediaType === "image") {
739
- body = { msgtype: "image", image: { media_id: mediaId } };
740
- } else if (options.mediaType === "voice") {
741
- const durationMs = uploadedDurationMs
742
- ?? await getVoiceDurationMs(options.mediaPath, options.mediaType, log, { preReadBuffer: buffer });
743
- body = { msgtype: "voice", voice: { media_id: mediaId, duration: String(durationMs) } };
744
- log?.debug?.(
745
- `[DingTalk] Sending session voice message mediaId=${mediaId} durationMs=${durationMs}`,
746
- );
747
- } else if (options.mediaType === "video") {
748
- body = { msgtype: "video", video: { media_id: mediaId } };
749
- } else if (options.mediaType === "file") {
750
- body = { msgtype: "file", file: { media_id: mediaId } };
751
- }
752
-
753
- if (body) {
754
- const result = await axios({
755
- url: sessionWebhook,
756
- method: "POST",
757
- data: body,
758
- headers: { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json" },
759
- ...getProxyBypassOption(config),
760
- });
736
+ if (options.mediaType === "image") {
737
+ const uploadResult = await uploadMedia(config, options.mediaPath, options.mediaType, log, {
738
+ mediaLocalRoots: options.mediaLocalRoots,
739
+ });
740
+ if (uploadResult) {
741
+ const imageMarkdown = `![${path.basename(options.mediaPath)}](${uploadResult.mediaId})`;
742
+ text = text ? `${text}\n\n${imageMarkdown}` : imageMarkdown;
761
743
  log?.debug?.(
762
- `[DingTalk] Session webhook response msgtype=${body.msgtype} ${summarizeSessionWebhookResponse(result.data)}`,
744
+ `[DingTalk] Session webhook image will be delivered as markdown media reference mediaId=${uploadResult.mediaId}`,
763
745
  );
764
- ensureSessionWebhookBusinessSuccess(result.data, { msgtype: body.msgtype });
765
- const delivery = extractOutboundDeliveryMetadata(result.data);
766
- if (!delivery.messageId && !delivery.processQueryKey && !delivery.outTrackId) {
767
- log?.warn?.(
768
- `[DingTalk] Session webhook ${body.msgtype} response missing delivery metadata; ` +
769
- summarizeSessionWebhookResponse(result.data),
770
- );
771
- }
772
- return result.data;
746
+ } else {
747
+ const mediaHint = options.mediaUrl || options.mediaPath || options.filePath || "(媒体发送失败)";
748
+ text = `${text}\n\n📎 媒体发送失败,兜底链接/路径:${mediaHint}`.trim();
749
+ log?.warn?.("[DingTalk] Media upload failed, falling back to text description");
773
750
  }
774
751
  } else {
775
752
  const mediaHint = options.mediaUrl || options.mediaPath || options.filePath || "(媒体发送失败)";
776
- text = `${text}\n\n📎 媒体发送失败,兜底链接/路径:${mediaHint}`.trim();
777
- log?.warn?.("[DingTalk] Media upload failed, falling back to text description");
753
+ text = `${text}\n\n📎 当前会话无法直接发送 ${options.mediaType},兜底链接/路径:${mediaHint}`.trim();
754
+ log?.warn?.(
755
+ `[DingTalk] Session webhook does not support native ${options.mediaType} replies; falling back to text description`,
756
+ );
778
757
  }
779
758
  }
780
759
 
@@ -835,6 +814,51 @@ export async function sendMessage(
835
814
  const messageType = config.messageType || "markdown";
836
815
  const log = options.log || getLogger();
837
816
 
817
+ if (options.sessionWebhook && options.mediaPath && shouldRouteSessionMediaViaProactive(options.mediaType)) {
818
+ log?.debug?.(
819
+ `[DingTalk] Session webhook does not support ${options.mediaType} replies reliably; ` +
820
+ "using proactive media API instead",
821
+ );
822
+ const proactiveMediaResult = await sendProactiveMedia(
823
+ config,
824
+ conversationId,
825
+ options.mediaPath,
826
+ options.mediaType,
827
+ options,
828
+ );
829
+ if (!proactiveMediaResult.ok) {
830
+ log?.warn?.(
831
+ `[DingTalk] Proactive ${options.mediaType} reply failed; falling back to session markdown: ` +
832
+ (proactiveMediaResult.error || "unknown"),
833
+ );
834
+ const data = await sendBySession(config, options.sessionWebhook, text, options);
835
+ const delivery = extractOutboundDeliveryMetadata(data);
836
+ const messageId = delivery.messageId || delivery.processQueryKey || delivery.outTrackId;
837
+ const persistedText = buildPersistedOutboundText(text, options);
838
+ persistOutboundMessageContext({
839
+ storePath: options.storePath,
840
+ accountId: options.accountId,
841
+ conversationId: options.conversationId || conversationId,
842
+ text: persistedText,
843
+ messageType: "outbound-media",
844
+ quotedRef: options.quotedRef,
845
+ log,
846
+ ...DEFAULT_OUTBOUND_SENDER,
847
+ chatType: inferConversationChatType(options.conversationId || conversationId),
848
+ delivery: {
849
+ ...delivery,
850
+ kind: "session",
851
+ },
852
+ });
853
+ return { ok: true, data, messageId };
854
+ }
855
+ return {
856
+ ok: true,
857
+ data: proactiveMediaResult.data,
858
+ messageId: proactiveMediaResult.messageId,
859
+ };
860
+ }
861
+
838
862
  if (messageType === "card" && options.card && !options.forceMarkdown) {
839
863
  const card = options.card;
840
864
  if (isCardInTerminalState(card.state)) {
@@ -858,28 +882,6 @@ export async function sendMessage(
858
882
  }
859
883
  }
860
884
 
861
- if (options.sessionWebhook && options.mediaPath && options.mediaType === "voice") {
862
- log?.debug?.(
863
- "[DingTalk] Session webhook does not support voice replies reliably; " +
864
- "using proactive media API for this voice response",
865
- );
866
- const proactiveVoiceResult = await sendProactiveMedia(
867
- config,
868
- conversationId,
869
- options.mediaPath,
870
- options.mediaType,
871
- options,
872
- );
873
- if (!proactiveVoiceResult.ok) {
874
- return { ok: false, error: proactiveVoiceResult.error || "Voice reply send failed" };
875
- }
876
- return {
877
- ok: true,
878
- data: proactiveVoiceResult.data,
879
- messageId: proactiveVoiceResult.messageId,
880
- };
881
- }
882
-
883
885
  if (options.sessionWebhook) {
884
886
  const data = await sendBySession(config, options.sessionWebhook, text, options);
885
887
  const delivery = extractOutboundDeliveryMetadata(data);