@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.
package/dist/index.js CHANGED
@@ -1814,7 +1814,7 @@ async function updateCardVariables(outTrackId, params, token, config) {
1814
1814
  {
1815
1815
  outTrackId,
1816
1816
  cardData: { cardParamMap: stringMap },
1817
- cardUpdateOptions: { updateCardDataByKey: true, updatePrivateDataByKey: true }
1817
+ cardUpdateOptions: { updateCardDataByKey: true }
1818
1818
  },
1819
1819
  {
1820
1820
  headers: {
@@ -2614,6 +2614,19 @@ async function createAICard(config, conversationId, log, options = {}) {
2614
2614
  upsertPendingCard(aiCardInstance, options.storePath, log);
2615
2615
  }
2616
2616
  clearAICardDegrade(accountId, log);
2617
+ try {
2618
+ await putAICardStreamingField(aiCardInstance, template.contentKey, "", false, log, {
2619
+ suppressDegrade: true
2620
+ });
2621
+ aiCardInstance.state = AICardStatus.INPUTING;
2622
+ if (shouldPersistPending) {
2623
+ upsertPendingCard(aiCardInstance, options.storePath, log);
2624
+ }
2625
+ } catch (kickErr) {
2626
+ log?.debug?.(
2627
+ `[DingTalk][AICard] Non-critical: failed to kick card into streaming mode: ${kickErr.message}`
2628
+ );
2629
+ }
2617
2630
  return aiCardInstance;
2618
2631
  } catch (err) {
2619
2632
  log?.error?.(`[DingTalk][AICard] Create failed: ${err.message}`);
@@ -2712,13 +2725,13 @@ async function clearAICardStreamingContent(card, log) {
2712
2725
  log?.debug?.(`[DingTalk][AICard] Non-critical: failed to clear streaming content: ${message}`);
2713
2726
  }
2714
2727
  }
2715
- async function finalizeAICardStreamingLifecycleIfNeeded(card, content, log) {
2728
+ async function finalizeAICardStreamingLifecycleIfNeeded(card, log) {
2716
2729
  if (!card.streamLifecycleOpened) {
2717
2730
  return;
2718
2731
  }
2719
2732
  const template = DINGTALK_CARD_TEMPLATE;
2720
2733
  try {
2721
- await putAICardStreamingField(card, template.streamingKey, content, true, log, {
2734
+ await putAICardStreamingField(card, template.streamingKey, "", true, log, {
2722
2735
  suppressDegrade: true
2723
2736
  });
2724
2737
  } catch (err) {
@@ -2734,7 +2747,7 @@ async function commitAICardBlocks(card, options, log) {
2734
2747
  return;
2735
2748
  }
2736
2749
  await ensureFreshToken(card, log);
2737
- await finalizeAICardStreamingLifecycleIfNeeded(card, options.content, log);
2750
+ await finalizeAICardStreamingLifecycleIfNeeded(card, log);
2738
2751
  const template = DINGTALK_CARD_TEMPLATE;
2739
2752
  const updates = {
2740
2753
  [template.blockListKey]: options.blockListJson,
@@ -2797,7 +2810,7 @@ async function finalizeStoppedAICard(card, options, log) {
2797
2810
  await ensureFreshToken(card, log);
2798
2811
  const template = DINGTALK_CARD_TEMPLATE;
2799
2812
  const payload = buildStoppedCardFinalizePayload(options);
2800
- await finalizeAICardStreamingLifecycleIfNeeded(card, payload.content, log);
2813
+ await finalizeAICardStreamingLifecycleIfNeeded(card, log);
2801
2814
  try {
2802
2815
  await updateCardVariables(
2803
2816
  card.outTrackId || card.cardInstanceId,
@@ -4475,7 +4488,7 @@ function stripLeadingInvisibleChars(value) {
4475
4488
 
4476
4489
  // src/inbound-handler.ts
4477
4490
  import fs6 from "node:fs";
4478
- import * as path9 from "node:path";
4491
+ import * as path10 from "node:path";
4479
4492
  import { isAbortRequestText, isBtwRequestText } from "openclaw/plugin-sdk/reply-runtime";
4480
4493
  import { parseInlineDirectives } from "openclaw/plugin-sdk/text-runtime";
4481
4494
 
@@ -6716,7 +6729,7 @@ function isMarkdownTableSeparator(line) {
6716
6729
  return false;
6717
6730
  }
6718
6731
  const cells = normalized.replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
6719
- return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
6732
+ return cells.length > 0 && cells.every((cell) => /^:?-{1,}:?$/.test(cell));
6720
6733
  }
6721
6734
  function isMarkdownTableRow(line) {
6722
6735
  const trimmed = line.trim();
@@ -6725,9 +6738,58 @@ function isMarkdownTableRow(line) {
6725
6738
  function parseMarkdownTableRow(line) {
6726
6739
  return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
6727
6740
  }
6728
- function renderMarkdownTable(lines) {
6729
- const rows = lines.map(parseMarkdownTableRow).filter((cells) => cells.length > 0);
6730
- return rows.map((cells) => cells.join(" | ")).join(" \n");
6741
+ function parseSeparatorAlignment(cell) {
6742
+ const trimmed = cell.trim();
6743
+ const hasLeftColon = trimmed.startsWith(":");
6744
+ const hasRightColon = trimmed.endsWith(":");
6745
+ if (hasLeftColon && hasRightColon) {
6746
+ return "center";
6747
+ }
6748
+ if (hasRightColon) {
6749
+ return "right";
6750
+ }
6751
+ if (hasLeftColon) {
6752
+ return "left";
6753
+ }
6754
+ return "center";
6755
+ }
6756
+ function buildSeparatorRow(alignments) {
6757
+ const cells = alignments.map((align) => {
6758
+ if (align === "left") {
6759
+ return ":---";
6760
+ }
6761
+ if (align === "right") {
6762
+ return "---:";
6763
+ }
6764
+ return ":---:";
6765
+ });
6766
+ return "|" + cells.join("|") + "|";
6767
+ }
6768
+ function renderMarkdownTable(headerLine, separatorLine, dataLines) {
6769
+ const headerCells = parseMarkdownTableRow(headerLine);
6770
+ const separatorCells = parseMarkdownTableRow(separatorLine);
6771
+ const dataRows = dataLines.map(parseMarkdownTableRow).filter((cells) => cells.length > 0);
6772
+ if (headerCells.length === 0) {
6773
+ return "";
6774
+ }
6775
+ const colCount = Math.max(
6776
+ headerCells.length,
6777
+ separatorCells.length,
6778
+ ...dataRows.map((cells) => cells.length)
6779
+ );
6780
+ const alignments = [];
6781
+ for (let i = 0; i < colCount; i++) {
6782
+ const sepCell = separatorCells[i] || "";
6783
+ alignments.push(parseSeparatorAlignment(sepCell));
6784
+ }
6785
+ const separator = buildSeparatorRow(alignments);
6786
+ const allRows = [headerCells, ...dataRows];
6787
+ const rendered = allRows.map((cells) => {
6788
+ const padded = cells.length < colCount ? [...cells, ...Array(colCount - cells.length).fill("")] : cells;
6789
+ return "|" + padded.join("|") + "|";
6790
+ });
6791
+ rendered.splice(1, 0, separator);
6792
+ return rendered.join("\n");
6731
6793
  }
6732
6794
  function convertMarkdownTablesToPlainText(text) {
6733
6795
  const lines = text.split("\n");
@@ -6743,13 +6805,20 @@ function convertMarkdownTablesToPlainText(text) {
6743
6805
  continue;
6744
6806
  }
6745
6807
  if (!inCodeFence && index + 1 < lines.length && isMarkdownTableRow(line) && isMarkdownTableSeparator(lines[index + 1] || "")) {
6746
- const tableLines = [line];
6808
+ const headerLine = line;
6809
+ const separatorLine = lines[index + 1] || "";
6810
+ const dataLines = [];
6747
6811
  index += 2;
6748
6812
  while (index < lines.length && isMarkdownTableRow(lines[index] || "")) {
6749
- tableLines.push(lines[index] || "");
6813
+ dataLines.push(lines[index] || "");
6750
6814
  index += 1;
6751
6815
  }
6752
- output.push(renderMarkdownTable(tableLines));
6816
+ const renderedTable = renderMarkdownTable(headerLine, separatorLine, dataLines);
6817
+ const lastOutput = output[output.length - 1];
6818
+ if (lastOutput !== void 0 && lastOutput.trim() !== "") {
6819
+ output.push("");
6820
+ }
6821
+ output.push(renderedTable);
6753
6822
  continue;
6754
6823
  }
6755
6824
  output.push(line);
@@ -7247,6 +7316,9 @@ function buildPersistedOutboundText(text, options) {
7247
7316
  }
7248
7317
  return text;
7249
7318
  }
7319
+ function shouldRouteSessionMediaViaProactive(mediaType) {
7320
+ return mediaType === "voice" || mediaType === "video" || mediaType === "file";
7321
+ }
7250
7322
  var DINGTALK_TEXT_CHUNK_LIMIT = 3800;
7251
7323
  var CARD_MEDIA_CONTROLLER_ATTACH_WAIT_MS = 150;
7252
7324
  var CARD_MEDIA_CONTROLLER_ATTACH_POLL_MS = 25;
@@ -7643,51 +7715,33 @@ async function sendBySession(config, sessionWebhook, text, options = {}) {
7643
7715
  const token = await getAccessToken(config, options.log);
7644
7716
  const log = options.log || getLogger();
7645
7717
  if (options.mediaPath && options.mediaType) {
7646
- const uploadResult = await uploadMedia2(config, options.mediaPath, options.mediaType, log, {
7647
- mediaLocalRoots: options.mediaLocalRoots
7648
- });
7649
- if (uploadResult) {
7650
- const { mediaId, buffer, durationMs: uploadedDurationMs } = uploadResult;
7651
- let body;
7652
- if (options.mediaType === "image") {
7653
- body = { msgtype: "image", image: { media_id: mediaId } };
7654
- } else if (options.mediaType === "voice") {
7655
- const durationMs = uploadedDurationMs ?? await getVoiceDurationMs(options.mediaPath, options.mediaType, log, { preReadBuffer: buffer });
7656
- body = { msgtype: "voice", voice: { media_id: mediaId, duration: String(durationMs) } };
7657
- log?.debug?.(
7658
- `[DingTalk] Sending session voice message mediaId=${mediaId} durationMs=${durationMs}`
7659
- );
7660
- } else if (options.mediaType === "video") {
7661
- body = { msgtype: "video", video: { media_id: mediaId } };
7662
- } else if (options.mediaType === "file") {
7663
- body = { msgtype: "file", file: { media_id: mediaId } };
7664
- }
7665
- if (body) {
7666
- const result = await http_client_default({
7667
- url: sessionWebhook,
7668
- method: "POST",
7669
- data: body,
7670
- headers: { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json" },
7671
- ...getProxyBypassOption(config)
7672
- });
7718
+ if (options.mediaType === "image") {
7719
+ const uploadResult = await uploadMedia2(config, options.mediaPath, options.mediaType, log, {
7720
+ mediaLocalRoots: options.mediaLocalRoots
7721
+ });
7722
+ if (uploadResult) {
7723
+ const imageMarkdown = `![${path7.basename(options.mediaPath)}](${uploadResult.mediaId})`;
7724
+ text = text ? `${text}
7725
+
7726
+ ${imageMarkdown}` : imageMarkdown;
7673
7727
  log?.debug?.(
7674
- `[DingTalk] Session webhook response msgtype=${body.msgtype} ${summarizeSessionWebhookResponse(result.data)}`
7728
+ `[DingTalk] Session webhook image will be delivered as markdown media reference mediaId=${uploadResult.mediaId}`
7675
7729
  );
7676
- ensureSessionWebhookBusinessSuccess(result.data, { msgtype: body.msgtype });
7677
- const delivery = extractOutboundDeliveryMetadata(result.data);
7678
- if (!delivery.messageId && !delivery.processQueryKey && !delivery.outTrackId) {
7679
- log?.warn?.(
7680
- `[DingTalk] Session webhook ${body.msgtype} response missing delivery metadata; ` + summarizeSessionWebhookResponse(result.data)
7681
- );
7682
- }
7683
- return result.data;
7730
+ } else {
7731
+ const mediaHint = options.mediaUrl || options.mediaPath || options.filePath || "(\u5A92\u4F53\u53D1\u9001\u5931\u8D25)";
7732
+ text = `${text}
7733
+
7734
+ \u{1F4CE} \u5A92\u4F53\u53D1\u9001\u5931\u8D25\uFF0C\u515C\u5E95\u94FE\u63A5/\u8DEF\u5F84\uFF1A${mediaHint}`.trim();
7735
+ log?.warn?.("[DingTalk] Media upload failed, falling back to text description");
7684
7736
  }
7685
7737
  } else {
7686
7738
  const mediaHint = options.mediaUrl || options.mediaPath || options.filePath || "(\u5A92\u4F53\u53D1\u9001\u5931\u8D25)";
7687
7739
  text = `${text}
7688
7740
 
7689
- \u{1F4CE} \u5A92\u4F53\u53D1\u9001\u5931\u8D25\uFF0C\u515C\u5E95\u94FE\u63A5/\u8DEF\u5F84\uFF1A${mediaHint}`.trim();
7690
- log?.warn?.("[DingTalk] Media upload failed, falling back to text description");
7741
+ \u{1F4CE} \u5F53\u524D\u4F1A\u8BDD\u65E0\u6CD5\u76F4\u63A5\u53D1\u9001 ${options.mediaType}\uFF0C\u515C\u5E95\u94FE\u63A5/\u8DEF\u5F84\uFF1A${mediaHint}`.trim();
7742
+ log?.warn?.(
7743
+ `[DingTalk] Session webhook does not support native ${options.mediaType} replies; falling back to text description`
7744
+ );
7691
7745
  }
7692
7746
  }
7693
7747
  const textWithUploadedLocalImages = await replaceMarkdownLocalImages({
@@ -7733,6 +7787,48 @@ async function sendMessage(config, conversationId, text, options = {}) {
7733
7787
  try {
7734
7788
  const messageType = config.messageType || "markdown";
7735
7789
  const log = options.log || getLogger();
7790
+ if (options.sessionWebhook && options.mediaPath && shouldRouteSessionMediaViaProactive(options.mediaType)) {
7791
+ log?.debug?.(
7792
+ `[DingTalk] Session webhook does not support ${options.mediaType} replies reliably; using proactive media API instead`
7793
+ );
7794
+ const proactiveMediaResult = await sendProactiveMedia(
7795
+ config,
7796
+ conversationId,
7797
+ options.mediaPath,
7798
+ options.mediaType,
7799
+ options
7800
+ );
7801
+ if (!proactiveMediaResult.ok) {
7802
+ log?.warn?.(
7803
+ `[DingTalk] Proactive ${options.mediaType} reply failed; falling back to session markdown: ` + (proactiveMediaResult.error || "unknown")
7804
+ );
7805
+ const data = await sendBySession(config, options.sessionWebhook, text, options);
7806
+ const delivery2 = extractOutboundDeliveryMetadata(data);
7807
+ const messageId2 = delivery2.messageId || delivery2.processQueryKey || delivery2.outTrackId;
7808
+ const persistedText = buildPersistedOutboundText(text, options);
7809
+ persistOutboundMessageContext({
7810
+ storePath: options.storePath,
7811
+ accountId: options.accountId,
7812
+ conversationId: options.conversationId || conversationId,
7813
+ text: persistedText,
7814
+ messageType: "outbound-media",
7815
+ quotedRef: options.quotedRef,
7816
+ log,
7817
+ ...DEFAULT_OUTBOUND_SENDER,
7818
+ chatType: inferConversationChatType(options.conversationId || conversationId),
7819
+ delivery: {
7820
+ ...delivery2,
7821
+ kind: "session"
7822
+ }
7823
+ });
7824
+ return { ok: true, data, messageId: messageId2 };
7825
+ }
7826
+ return {
7827
+ ok: true,
7828
+ data: proactiveMediaResult.data,
7829
+ messageId: proactiveMediaResult.messageId
7830
+ };
7831
+ }
7736
7832
  if (messageType === "card" && options.card && !options.forceMarkdown) {
7737
7833
  const card = options.card;
7738
7834
  if (isCardInTerminalState(card.state)) {
@@ -7754,26 +7850,6 @@ async function sendMessage(config, conversationId, text, options = {}) {
7754
7850
  };
7755
7851
  }
7756
7852
  }
7757
- if (options.sessionWebhook && options.mediaPath && options.mediaType === "voice") {
7758
- log?.debug?.(
7759
- "[DingTalk] Session webhook does not support voice replies reliably; using proactive media API for this voice response"
7760
- );
7761
- const proactiveVoiceResult = await sendProactiveMedia(
7762
- config,
7763
- conversationId,
7764
- options.mediaPath,
7765
- options.mediaType,
7766
- options
7767
- );
7768
- if (!proactiveVoiceResult.ok) {
7769
- return { ok: false, error: proactiveVoiceResult.error || "Voice reply send failed" };
7770
- }
7771
- return {
7772
- ok: true,
7773
- data: proactiveVoiceResult.data,
7774
- messageId: proactiveVoiceResult.messageId
7775
- };
7776
- }
7777
7853
  if (options.sessionWebhook) {
7778
7854
  const data = await sendBySession(config, options.sessionWebhook, text, options);
7779
7855
  const delivery2 = extractOutboundDeliveryMetadata(data);
@@ -9061,8 +9137,6 @@ function createCardDraftController(params) {
9061
9137
  if (stopped || failed) {
9062
9138
  return;
9063
9139
  }
9064
- await contentLoop.flush();
9065
- await contentLoop.waitForInFlight();
9066
9140
  if (hasStreamingContent) {
9067
9141
  await clearStreamingContentFromCard();
9068
9142
  }
@@ -9265,6 +9339,7 @@ function createCardDraftController(params) {
9265
9339
  discardCurrentAnswer();
9266
9340
  } else {
9267
9341
  sealCurrentAnswer();
9342
+ queueRender();
9268
9343
  }
9269
9344
  await beginBoundaryFlush();
9270
9345
  return;
@@ -9482,7 +9557,6 @@ function createCardReplyStrategy(ctx) {
9482
9557
  };
9483
9558
  const { mode, usedDeprecatedCardRealTimeStream } = resolveCardStreamingMode(config);
9484
9559
  const streamAnswerLive = mode === "answer" || mode === "all";
9485
- const renderAnswerBlocksLive = mode === "all";
9486
9560
  const streamThinkingLive = mode === "all";
9487
9561
  let lifecycleState = "open";
9488
9562
  const shouldAcceptAnswerSnapshot = () => lifecycleState === "open";
@@ -9616,7 +9690,10 @@ function createCardReplyStrategy(ctx) {
9616
9690
  finalTextForFallback = normalized.answerText;
9617
9691
  return;
9618
9692
  }
9619
- await controller.updateAnswer(normalized.answerText);
9693
+ await controller.updateAnswer(normalized.answerText, {
9694
+ stream: streamAnswerLive,
9695
+ renderBlocks: !streamAnswerLive
9696
+ });
9620
9697
  }
9621
9698
  };
9622
9699
  const rewriteLocalMarkdownImagesToPlaceholders = (text) => {
@@ -9652,7 +9729,8 @@ function createCardReplyStrategy(ctx) {
9652
9729
  }
9653
9730
  await controller.updateAnswer(answerSnapshot, {
9654
9731
  stream: streamAnswerLive,
9655
- renderBlocks: renderAnswerBlocksLive
9732
+ // Active answer previews live in the content field; blockList is committed at boundaries/finalize.
9733
+ renderBlocks: false
9656
9734
  });
9657
9735
  };
9658
9736
  const applySplitTextToTimeline = async (text, options = {}) => {
@@ -9712,6 +9790,11 @@ function createCardReplyStrategy(ctx) {
9712
9790
  // Card mode keeps runtime block streaming disabled, but still consumes
9713
9791
  // reasoning blocks through explicit callbacks and delivery metadata.
9714
9792
  disableBlockStreaming: ctx.disableBlockStreaming ?? true,
9793
+ // DingTalk card mode owns the visible reply surface. In group chats,
9794
+ // OpenClaw defaults source replies to message-tool-only; override that
9795
+ // so final replies are delivered into this card instead of spawning a
9796
+ // separate visible message/card via the message tool.
9797
+ sourceReplyDeliveryMode: "automatic",
9715
9798
  onAssistantMessageStart: async () => {
9716
9799
  if (isLifecycleSealed() || isStopRequested?.()) {
9717
9800
  return;
@@ -9951,6 +10034,7 @@ function createCardReplyStrategy(ctx) {
9951
10034
  }
9952
10035
  try {
9953
10036
  await flushPendingReasoning();
10037
+ await controller.clearStreamingContent?.();
9954
10038
  await controller.flush();
9955
10039
  await controller.waitForInFlight();
9956
10040
  const fallbackAnswer = finalTextForFallback || (sawFinalDelivery ? EMPTY_FINAL_REPLY : void 0);
@@ -10066,6 +10150,7 @@ function createCardReplyStrategy(ctx) {
10066
10150
  }
10067
10151
 
10068
10152
  // src/reply-strategy-markdown.ts
10153
+ import path8 from "node:path";
10069
10154
  var EMPTY_FINAL_FALLBACK_TEXT = "\u2705 Done";
10070
10155
  function renderQuotedSegment(text) {
10071
10156
  return text.split("\n").map((line) => line.length > 0 ? `> ${line}` : ">").join("\n");
@@ -10102,6 +10187,10 @@ function computeSharedPrefixTail(previous, next) {
10102
10187
  const suffix = current.slice(sharedPrefixLength);
10103
10188
  return suffix.trim() ? suffix : "";
10104
10189
  }
10190
+ function renderMarkdownImage(mediaPath) {
10191
+ const filename = path8.basename(mediaPath) || "image";
10192
+ return `![${filename}](${mediaPath})`;
10193
+ }
10105
10194
  function createMarkdownReplyStrategy(ctx) {
10106
10195
  let finalText;
10107
10196
  let activeAnswerText = "";
@@ -10125,7 +10214,7 @@ function createMarkdownReplyStrategy(ctx) {
10125
10214
  }
10126
10215
  sentVisibleContent = true;
10127
10216
  };
10128
- const emitAnswerSuffix = async (text) => {
10217
+ const prepareAnswerSuffix = (text) => {
10129
10218
  const current = typeof text === "string" ? text : "";
10130
10219
  if (current.length > 0) {
10131
10220
  activeAnswerText = current;
@@ -10133,37 +10222,118 @@ function createMarkdownReplyStrategy(ctx) {
10133
10222
  }
10134
10223
  const suffix = computeIncrementalSuffix(lastSentAnswerText, current);
10135
10224
  if (suffix) {
10136
- await sendMarkdownSegment(suffix);
10137
- lastSentAnswerText = current;
10138
- return;
10225
+ return {
10226
+ text: suffix,
10227
+ markSent: () => {
10228
+ lastSentAnswerText = current;
10229
+ }
10230
+ };
10139
10231
  }
10140
10232
  if (current.trim() && lastSentAnswerText && !current.startsWith(lastSentAnswerText)) {
10141
10233
  const suffix2 = computeSharedPrefixTail(lastSentAnswerText, current);
10142
10234
  ctx.log?.warn?.(
10143
10235
  `[DingTalk][Markdown] answer prefix drift detected; falling back to shared-prefix tail prevLen=${lastSentAnswerText.length} currentLen=${current.length}`
10144
10236
  );
10145
- lastSentAnswerText = "";
10146
10237
  if (suffix2) {
10147
- await sendMarkdownSegment(suffix2);
10148
- lastSentAnswerText = current;
10149
- return;
10238
+ return {
10239
+ text: suffix2,
10240
+ markSent: () => {
10241
+ lastSentAnswerText = current;
10242
+ }
10243
+ };
10244
+ }
10245
+ return {
10246
+ text: current,
10247
+ markSent: () => {
10248
+ lastSentAnswerText = current;
10249
+ }
10250
+ };
10251
+ }
10252
+ return null;
10253
+ };
10254
+ const emitAnswerSuffix = async (text) => {
10255
+ const suffix = prepareAnswerSuffix(text);
10256
+ if (suffix) {
10257
+ await sendMarkdownSegment(suffix.text);
10258
+ suffix.markSent();
10259
+ }
10260
+ };
10261
+ const prepareMarkdownImageAttachments = async (mediaUrls) => {
10262
+ const imageMarkdown = [];
10263
+ const passthroughMediaUrls = [];
10264
+ const cleanups = [];
10265
+ for (const rawMediaUrl of mediaUrls) {
10266
+ const preparedMedia = await prepareMediaInput(
10267
+ rawMediaUrl,
10268
+ ctx.log,
10269
+ ctx.config.mediaUrlAllowlist
10270
+ );
10271
+ const actualMediaPath = preparedMedia.cleanup ? preparedMedia.path : resolveRelativePath(preparedMedia.path);
10272
+ const mediaType = resolveOutboundMediaType({
10273
+ mediaPath: actualMediaPath,
10274
+ asVoice: false
10275
+ });
10276
+ if (mediaType === "image") {
10277
+ imageMarkdown.push(renderMarkdownImage(actualMediaPath));
10278
+ if (preparedMedia.cleanup) {
10279
+ cleanups.push(preparedMedia.cleanup);
10280
+ }
10281
+ } else {
10282
+ await preparedMedia.cleanup?.();
10283
+ passthroughMediaUrls.push(rawMediaUrl);
10150
10284
  }
10151
- await sendMarkdownSegment(current);
10152
- lastSentAnswerText = current;
10153
10285
  }
10286
+ return { imageMarkdown, passthroughMediaUrls, cleanups };
10154
10287
  };
10155
10288
  return {
10156
10289
  getReplyOptions() {
10157
10290
  return {
10158
- disableBlockStreaming: ctx.disableBlockStreaming === true
10291
+ disableBlockStreaming: ctx.disableBlockStreaming === true,
10292
+ // DingTalk markdown/sessionWebhook mode owns the visible reply surface.
10293
+ // Keep runtime final replies on this strategy even when group chats
10294
+ // default source replies to message-tool-only.
10295
+ sourceReplyDeliveryMode: "automatic"
10159
10296
  };
10160
10297
  },
10161
10298
  async deliver(payload) {
10299
+ let answerTextSentWithImages = false;
10300
+ let toolTextSentWithImages = false;
10162
10301
  if (payload.mediaUrls.length > 0) {
10163
- await ctx.deliverMedia(payload.mediaUrls, { audioAsVoice: payload.audioAsVoice });
10164
- sentVisibleContent = true;
10302
+ const prepared = payload.audioAsVoice === true ? {
10303
+ imageMarkdown: [],
10304
+ passthroughMediaUrls: payload.mediaUrls,
10305
+ cleanups: []
10306
+ } : await prepareMarkdownImageAttachments(payload.mediaUrls);
10307
+ try {
10308
+ if (prepared.passthroughMediaUrls.length > 0) {
10309
+ await ctx.deliverMedia(prepared.passthroughMediaUrls, {
10310
+ audioAsVoice: payload.audioAsVoice
10311
+ });
10312
+ sentVisibleContent = true;
10313
+ }
10314
+ if (prepared.imageMarkdown.length > 0) {
10315
+ const answerSuffix = payload.kind === "block" || payload.kind === "final" ? prepareAnswerSuffix(payload.text) : typeof payload.text === "string" ? { text: renderQuotedSegment(payload.text), markSent: () => {
10316
+ } } : null;
10317
+ const markdownParts = [answerSuffix?.text || "", ...prepared.imageMarkdown].filter(
10318
+ (part) => part.trim().length > 0
10319
+ );
10320
+ if (markdownParts.length > 0) {
10321
+ await sendMarkdownSegment(markdownParts.join("\n\n"));
10322
+ answerSuffix?.markSent();
10323
+ }
10324
+ answerTextSentWithImages = payload.kind === "block" || payload.kind === "final";
10325
+ toolTextSentWithImages = payload.kind === "tool";
10326
+ }
10327
+ } finally {
10328
+ for (const cleanup of prepared.cleanups) {
10329
+ await cleanup();
10330
+ }
10331
+ }
10165
10332
  }
10166
10333
  if (payload.kind === "tool") {
10334
+ if (toolTextSentWithImages) {
10335
+ return;
10336
+ }
10167
10337
  const text = typeof payload.text === "string" ? payload.text : "";
10168
10338
  if (!text.trim()) {
10169
10339
  return;
@@ -10171,7 +10341,7 @@ function createMarkdownReplyStrategy(ctx) {
10171
10341
  await sendMarkdownSegment(renderQuotedSegment(text));
10172
10342
  return;
10173
10343
  }
10174
- if ((payload.kind === "block" || payload.kind === "final") && typeof payload.text === "string") {
10344
+ if ((payload.kind === "block" || payload.kind === "final") && typeof payload.text === "string" && !answerTextSentWithImages) {
10175
10345
  await emitAnswerSuffix(payload.text);
10176
10346
  }
10177
10347
  },
@@ -10495,12 +10665,12 @@ async function dispatchSubAgents(params) {
10495
10665
 
10496
10666
  // src/targeting/group-members-store.ts
10497
10667
  import * as fs5 from "node:fs";
10498
- import * as path8 from "node:path";
10668
+ import * as path9 from "node:path";
10499
10669
  var GROUP_MEMBERS_NAMESPACE = "members.group-roster";
10500
10670
  function groupMembersFilePath(storePath, groupId) {
10501
- const dir = path8.join(path8.dirname(storePath), "dingtalk-members");
10671
+ const dir = path9.join(path9.dirname(storePath), "dingtalk-members");
10502
10672
  const safeId = groupId.replace(/\+/g, "-").replace(/\//g, "_");
10503
- return path8.join(dir, `${safeId}.json`);
10673
+ return path9.join(dir, `${safeId}.json`);
10504
10674
  }
10505
10675
  function readLegacyRoster(storePath, groupId) {
10506
10676
  const filePath = groupMembersFilePath(storePath, groupId);
@@ -11581,7 +11751,7 @@ async function handleDingTalkMessage(params) {
11581
11751
  if (!hasPathLikeShape) {
11582
11752
  return void 0;
11583
11753
  }
11584
- return STANDALONE_MEDIA_PATH_EXTENSIONS.has(path9.extname(trimmed).toLowerCase()) ? trimmed : void 0;
11754
+ return STANDALONE_MEDIA_PATH_EXTENSIONS.has(path10.extname(trimmed).toLowerCase()) ? trimmed : void 0;
11585
11755
  }, extractSharedAudioAsVoice = function(payload, inlineReplyPayload) {
11586
11756
  const richPayload = payload;
11587
11757
  const sharedValue = parseBooleanLike(richPayload.audioAsVoice);
@@ -13025,14 +13195,14 @@ var RegistrationError = class extends Error {
13025
13195
  function asString(value) {
13026
13196
  return typeof value === "string" ? value : "";
13027
13197
  }
13028
- async function apiPost(path10, payload) {
13029
- const url = `${REGISTRATION_BASE_URL}${path10}`;
13198
+ async function apiPost(path11, payload) {
13199
+ const url = `${REGISTRATION_BASE_URL}${path11}`;
13030
13200
  const resp = await http_client_default.post(url, payload, { timeout: 15e3 });
13031
13201
  const data = resp.data;
13032
13202
  const errcode = data.errcode;
13033
13203
  if (errcode !== void 0 && errcode !== 0) {
13034
13204
  const errmsg = asString(data.errmsg) || "unknown error";
13035
- throw new RegistrationError(`API error [${path10}]: ${errmsg} (errcode=${typeof errcode === "number" ? errcode : asString(errcode)})`);
13205
+ throw new RegistrationError(`API error [${path11}]: ${errmsg} (errcode=${typeof errcode === "number" ? errcode : asString(errcode)})`);
13036
13206
  }
13037
13207
  return data;
13038
13208
  }