@openclaw-china/dingtalk 2026.3.16 → 2026.3.19

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.d.ts CHANGED
@@ -883,7 +883,8 @@ declare function sendMessageDingtalk(params: SendMessageParams): Promise<Dingtal
883
883
  *
884
884
  * 包含 Moltbot 核心 API,用于:
885
885
  * - 路由解析 (channel.routing.resolveAgentRoute)
886
- * - 消息分发 (channel.reply.dispatchReplyFromConfig)
886
+ * - 实时消息分发 (channel.reply.dispatchReplyWithDispatcher /
887
+ * channel.reply.dispatchReplyWithBufferedBlockDispatcher)
887
888
  * - 系统事件 (system.enqueueSystemEvent)
888
889
  */
889
890
  interface PluginRuntime {
@@ -926,6 +927,25 @@ interface PluginRuntime {
926
927
  }) => Promise<void>;
927
928
  };
928
929
  reply?: {
930
+ dispatchReplyWithDispatcher?: (params: {
931
+ ctx: unknown;
932
+ cfg: unknown;
933
+ dispatcherOptions: {
934
+ deliver: (payload: unknown, info?: {
935
+ kind?: string;
936
+ }) => Promise<void>;
937
+ onError?: (err: unknown, info: {
938
+ kind: string;
939
+ }) => void;
940
+ onSkip?: (payload: unknown, info: {
941
+ kind: string;
942
+ reason: string;
943
+ }) => void;
944
+ onReplyStart?: () => Promise<void> | void;
945
+ humanDelay?: unknown;
946
+ };
947
+ replyOptions?: unknown;
948
+ }) => Promise<unknown>;
929
949
  dispatchReplyFromConfig?: (params: {
930
950
  ctx: unknown;
931
951
  cfg: unknown;
package/dist/index.js CHANGED
@@ -654,8 +654,8 @@ function getErrorMap() {
654
654
 
655
655
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
656
656
  var makeIssue = (params) => {
657
- const { data, path: path5, errorMaps, issueData } = params;
658
- const fullPath = [...path5, ...issueData.path || []];
657
+ const { data, path: path6, errorMaps, issueData } = params;
658
+ const fullPath = [...path6, ...issueData.path || []];
659
659
  const fullIssue = {
660
660
  ...issueData,
661
661
  path: fullPath
@@ -771,11 +771,11 @@ var errorUtil;
771
771
 
772
772
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
773
773
  var ParseInputLazyPath = class {
774
- constructor(parent, value, path5, key) {
774
+ constructor(parent, value, path6, key) {
775
775
  this._cachedPath = [];
776
776
  this.parent = parent;
777
777
  this.data = value;
778
- this._path = path5;
778
+ this._path = path6;
779
779
  this._key = key;
780
780
  }
781
781
  get path() {
@@ -6378,6 +6378,7 @@ var CHANNEL_ORDER = [
6378
6378
  "wecom",
6379
6379
  "wecom-app",
6380
6380
  "wecom-kf",
6381
+ "wechat-mp",
6381
6382
  "feishu-china"
6382
6383
  ];
6383
6384
  var CHANNEL_DISPLAY_LABELS = {
@@ -6386,6 +6387,7 @@ var CHANNEL_DISPLAY_LABELS = {
6386
6387
  wecom: "WeCom\uFF08\u4F01\u4E1A\u5FAE\u4FE1-\u667A\u80FD\u673A\u5668\u4EBA\uFF09",
6387
6388
  "wecom-app": "WeCom App\uFF08\u81EA\u5EFA\u5E94\u7528-\u53EF\u63A5\u5165\u5FAE\u4FE1\uFF09",
6388
6389
  "wecom-kf": "WeCom KF\uFF08\u5FAE\u4FE1\u5BA2\u670D\uFF09",
6390
+ "wechat-mp": "WeChat MP\uFF08\u5FAE\u4FE1\u516C\u4F17\u53F7\uFF09",
6389
6391
  qqbot: "QQBot\uFF08QQ \u673A\u5668\u4EBA\uFF09"
6390
6392
  };
6391
6393
  var CHANNEL_GUIDE_LINKS = {
@@ -6394,6 +6396,7 @@ var CHANNEL_GUIDE_LINKS = {
6394
6396
  wecom: `${GUIDES_BASE}/wecom/configuration.md`,
6395
6397
  "wecom-app": `${GUIDES_BASE}/wecom-app/configuration.md`,
6396
6398
  "wecom-kf": "https://github.com/BytePioneer-AI/openclaw-china/blob/main/extensions/wecom-kf/README.md",
6399
+ "wechat-mp": `${GUIDES_BASE}/wechat-mp/configuration.md`,
6397
6400
  qqbot: `${GUIDES_BASE}/qqbot/configuration.md`
6398
6401
  };
6399
6402
  var CHINA_CLI_STATE_KEY = /* @__PURE__ */ Symbol.for("@openclaw-china/china-cli-state");
@@ -6603,7 +6606,9 @@ function isChannelConfigured(cfg, channelId) {
6603
6606
  case "wecom-app":
6604
6607
  return hasTokenPair(channelCfg);
6605
6608
  case "wecom-kf":
6606
- return hasNonEmptyString(channelCfg.corpId) && hasNonEmptyString(channelCfg.corpSecret) && hasNonEmptyString(channelCfg.token) && hasNonEmptyString(channelCfg.encodingAESKey);
6609
+ return hasNonEmptyString(channelCfg.corpId) && hasNonEmptyString(channelCfg.token) && hasNonEmptyString(channelCfg.encodingAESKey);
6610
+ case "wechat-mp":
6611
+ return hasNonEmptyString(channelCfg.appId) && hasNonEmptyString(channelCfg.token);
6607
6612
  default:
6608
6613
  return false;
6609
6614
  }
@@ -6864,6 +6869,15 @@ async function configureWecomKf(prompter, cfg) {
6864
6869
  section("\u914D\u7F6E WeCom KF\uFF08\u5FAE\u4FE1\u5BA2\u670D\uFF09");
6865
6870
  showGuideLink("wecom-kf");
6866
6871
  const existing = getChannelConfig(cfg, "wecom-kf");
6872
+ Ve(
6873
+ [
6874
+ "\u5411\u5BFC\u987A\u5E8F\uFF1AwebhookPath / token / encodingAESKey / corpId / open_kfid / corpSecret",
6875
+ "\u57FA\u7840\u5FC5\u586B\uFF1AcorpId / token / encodingAESKey / open_kfid",
6876
+ "corpSecret \u4F1A\u4F5C\u4E3A\u6700\u540E\u4E00\u4E2A\u53C2\u6570\u8BE2\u95EE\uFF1B\u9996\u6B21\u63A5\u5165\u53EF\u5148\u7559\u7A7A\uFF0C\u5F85\u56DE\u8C03 URL \u6821\u9A8C\u901A\u8FC7\u5E76\u70B9\u51FB\u201C\u5F00\u59CB\u4F7F\u7528\u201D\u540E\u518D\u8865",
6877
+ "webhookPath \u9ED8\u8BA4\u503C\uFF1A/wecom-kf"
6878
+ ].join("\n"),
6879
+ "\u53C2\u6570\u8BF4\u660E"
6880
+ );
6867
6881
  const webhookPath = await prompter.askText({
6868
6882
  label: "Webhook \u8DEF\u5F84\uFF08\u9ED8\u8BA4 /wecom-kf\uFF09",
6869
6883
  defaultValue: toTrimmedString2(existing.webhookPath) ?? "/wecom-kf",
@@ -6884,19 +6898,14 @@ async function configureWecomKf(prompter, cfg) {
6884
6898
  defaultValue: toTrimmedString2(existing.corpId),
6885
6899
  required: true
6886
6900
  });
6887
- const corpSecret = await prompter.askSecret({
6888
- label: "\u5FAE\u4FE1\u5BA2\u670D Secret",
6889
- existingValue: toTrimmedString2(existing.corpSecret),
6890
- required: true
6891
- });
6892
6901
  const openKfId = await prompter.askText({
6893
6902
  label: "open_kfid",
6894
6903
  defaultValue: toTrimmedString2(existing.openKfId),
6895
6904
  required: true
6896
6905
  });
6897
- const welcomeText = await prompter.askText({
6898
- label: "\u6B22\u8FCE\u8BED\uFF08\u53EF\u9009\uFF09",
6899
- defaultValue: toTrimmedString2(existing.welcomeText),
6906
+ const corpSecret = await prompter.askSecret({
6907
+ label: "\u5FAE\u4FE1\u5BA2\u670D Secret\uFF08\u6700\u540E\u586B\u5199\uFF1B\u9996\u6B21\u63A5\u5165\u53EF\u5148\u7559\u7A7A\uFF09",
6908
+ existingValue: toTrimmedString2(existing.corpSecret),
6900
6909
  required: false
6901
6910
  });
6902
6911
  return mergeChannelConfig(cfg, "wecom-kf", {
@@ -6904,8 +6913,72 @@ async function configureWecomKf(prompter, cfg) {
6904
6913
  token,
6905
6914
  encodingAESKey,
6906
6915
  corpId,
6907
- corpSecret,
6908
6916
  openKfId,
6917
+ corpSecret: corpSecret || void 0
6918
+ });
6919
+ }
6920
+ async function configureWechatMp(prompter, cfg) {
6921
+ section("\u914D\u7F6E WeChat MP\uFF08\u5FAE\u4FE1\u516C\u4F17\u53F7\uFF09");
6922
+ showGuideLink("wechat-mp");
6923
+ const existing = getChannelConfig(cfg, "wechat-mp");
6924
+ const webhookPath = await prompter.askText({
6925
+ label: "Webhook \u8DEF\u5F84\uFF08\u9ED8\u8BA4 /wechat-mp\uFF09",
6926
+ defaultValue: toTrimmedString2(existing.webhookPath) ?? "/wechat-mp",
6927
+ required: true
6928
+ });
6929
+ const appId = await prompter.askText({
6930
+ label: "\u516C\u4F17\u53F7 appId",
6931
+ defaultValue: toTrimmedString2(existing.appId),
6932
+ required: true
6933
+ });
6934
+ const appSecret = await prompter.askSecret({
6935
+ label: "\u516C\u4F17\u53F7 appSecret\uFF08\u4E3B\u52A8\u53D1\u9001\u9700\u8981\uFF09",
6936
+ existingValue: toTrimmedString2(existing.appSecret),
6937
+ required: false
6938
+ });
6939
+ const token = await prompter.askSecret({
6940
+ label: "\u670D\u52A1\u5668\u914D\u7F6E token",
6941
+ existingValue: toTrimmedString2(existing.token),
6942
+ required: true
6943
+ });
6944
+ const messageMode = await prompter.askSelect(
6945
+ "\u6D88\u606F\u52A0\u89E3\u5BC6\u6A21\u5F0F",
6946
+ [
6947
+ { value: "plain", label: "plain\uFF08\u660E\u6587\uFF09" },
6948
+ { value: "safe", label: "safe\uFF08\u5B89\u5168\u6A21\u5F0F\uFF09" },
6949
+ { value: "compat", label: "compat\uFF08\u517C\u5BB9\u6A21\u5F0F\uFF09" }
6950
+ ],
6951
+ toTrimmedString2(existing.messageMode) ?? "safe"
6952
+ );
6953
+ let encodingAESKey = toTrimmedString2(existing.encodingAESKey);
6954
+ if (messageMode !== "plain") {
6955
+ encodingAESKey = await prompter.askSecret({
6956
+ label: "EncodingAESKey\uFF08safe/compat \u5FC5\u586B\uFF09",
6957
+ existingValue: encodingAESKey,
6958
+ required: true
6959
+ });
6960
+ }
6961
+ const replyMode = await prompter.askSelect(
6962
+ "\u56DE\u590D\u6A21\u5F0F",
6963
+ [
6964
+ { value: "passive", label: "passive\uFF085 \u79D2\u5185\u88AB\u52A8\u56DE\u590D\uFF09" },
6965
+ { value: "active", label: "active\uFF08\u5BA2\u670D\u6D88\u606F\u4E3B\u52A8\u53D1\u9001\uFF09" }
6966
+ ],
6967
+ toTrimmedString2(existing.replyMode) ?? "passive"
6968
+ );
6969
+ const welcomeText = await prompter.askText({
6970
+ label: "\u6B22\u8FCE\u8BED\uFF08\u53EF\u9009\uFF09",
6971
+ defaultValue: toTrimmedString2(existing.welcomeText),
6972
+ required: false
6973
+ });
6974
+ return mergeChannelConfig(cfg, "wechat-mp", {
6975
+ webhookPath,
6976
+ appId,
6977
+ appSecret: appSecret || void 0,
6978
+ token,
6979
+ encodingAESKey: messageMode === "plain" ? void 0 : encodingAESKey,
6980
+ messageMode,
6981
+ replyMode,
6909
6982
  welcomeText: welcomeText || void 0
6910
6983
  });
6911
6984
  }
@@ -6967,6 +7040,8 @@ async function configureSingleChannel(channel, prompter, cfg) {
6967
7040
  return configureWecomApp(prompter, cfg);
6968
7041
  case "wecom-kf":
6969
7042
  return configureWecomKf(prompter, cfg);
7043
+ case "wechat-mp":
7044
+ return configureWechatMp(prompter, cfg);
6970
7045
  case "qqbot":
6971
7046
  return configureQQBot(prompter, cfg);
6972
7047
  default:
@@ -7108,6 +7183,7 @@ var SUPPORTED_CHANNELS = [
7108
7183
  "wecom",
7109
7184
  "wecom-app",
7110
7185
  "wecom-kf",
7186
+ "wechat-mp",
7111
7187
  "qqbot"
7112
7188
  ];
7113
7189
  var CHINA_INSTALL_HINT_SHOWN_KEY = /* @__PURE__ */ Symbol.for("@openclaw-china/china-install-hint-shown");
@@ -8251,6 +8327,52 @@ ${list}`;
8251
8327
 
8252
8328
  ${prompt}` : content;
8253
8329
  }
8330
+ function escapeRegExp(value) {
8331
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8332
+ }
8333
+ function finalizeReplyText(text) {
8334
+ return text.replace(/\n{3,}/g, "\n\n").trim();
8335
+ }
8336
+ function stripLocalMediaSyntaxFromText(text, mediaItems) {
8337
+ let result = text;
8338
+ for (const media of mediaItems) {
8339
+ const source = media.source?.trim();
8340
+ if (!source) continue;
8341
+ const escapedSource = escapeRegExp(source);
8342
+ const fileName = media.fileName ?? path.basename(media.localPath ?? source);
8343
+ if (media.type === "image") {
8344
+ const pattern = new RegExp(`\\[!\\[[^\\]]*\\]\\(${escapedSource}\\)\\]\\([^\\)]+\\)`, "g");
8345
+ result = result.replace(pattern, "");
8346
+ }
8347
+ if (media.sourceKind === "markdown") {
8348
+ if (media.type === "image") {
8349
+ const pattern = new RegExp(`!\\[[^\\]]*\\]\\(${escapedSource}\\)`, "g");
8350
+ result = result.replace(pattern, "");
8351
+ } else {
8352
+ const pattern = new RegExp(`\\[[^\\]]*\\]\\(${escapedSource}\\)`, "g");
8353
+ result = result.replace(pattern, `[\u6587\u4EF6: ${fileName}]`);
8354
+ }
8355
+ continue;
8356
+ }
8357
+ if (result.includes(source)) {
8358
+ const replacement = media.type === "image" ? "" : `[\u6587\u4EF6: ${fileName}]`;
8359
+ result = result.split(source).join(replacement);
8360
+ }
8361
+ }
8362
+ return finalizeReplyText(result);
8363
+ }
8364
+ function dedupeMediaUrls(values) {
8365
+ const mediaQueue = [];
8366
+ const seenMedia = /* @__PURE__ */ new Set();
8367
+ for (const value of values) {
8368
+ const trimmed = value?.trim();
8369
+ if (!trimmed) continue;
8370
+ if (seenMedia.has(trimmed)) continue;
8371
+ seenMedia.add(trimmed);
8372
+ mediaQueue.push(trimmed);
8373
+ }
8374
+ return mediaQueue;
8375
+ }
8254
8376
  function extractLocalMediaFromText(params) {
8255
8377
  const { text, logger } = params;
8256
8378
  const result = extractMediaFromText(text, {
@@ -8270,13 +8392,17 @@ function extractLocalMediaFromText(params) {
8270
8392
  parseBarePaths: true,
8271
8393
  parseMarkdownLinks: true
8272
8394
  });
8273
- const mediaUrls = result.all.filter((m) => m.isLocal && m.localPath).map((m) => m.localPath);
8274
- return { mediaUrls };
8395
+ const localMedia = result.all.filter((m) => m.isLocal && m.localPath);
8396
+ const mediaUrls = localMedia.map((m) => m.localPath);
8397
+ return {
8398
+ text: stripLocalMediaSyntaxFromText(text, localMedia),
8399
+ mediaUrls
8400
+ };
8275
8401
  }
8276
8402
  function extractMediaLinesFromText(params) {
8277
8403
  const { text, logger } = params;
8278
8404
  const result = extractMediaFromText(text, {
8279
- removeFromText: false,
8405
+ removeFromText: true,
8280
8406
  checkExists: true,
8281
8407
  existsSync: (p) => {
8282
8408
  const exists = fs3.existsSync(p);
@@ -8294,6 +8420,18 @@ function extractMediaLinesFromText(params) {
8294
8420
  const mediaUrls = result.all.map((m) => m.isLocal ? m.localPath ?? m.source : m.source).filter((m) => typeof m === "string" && m.trim().length > 0);
8295
8421
  return { text: result.text, mediaUrls };
8296
8422
  }
8423
+ function prepareDingtalkReplyContent(params) {
8424
+ const { text, logger } = params;
8425
+ const mediaLineResult = extractMediaLinesFromText({ text, logger });
8426
+ const localMediaResult = extractLocalMediaFromText({
8427
+ text: mediaLineResult.text,
8428
+ logger
8429
+ });
8430
+ return {
8431
+ text: localMediaResult.text,
8432
+ mediaUrls: dedupeMediaUrls([...mediaLineResult.mediaUrls, ...localMediaResult.mediaUrls])
8433
+ };
8434
+ }
8297
8435
  function resolveAudioRecognition(raw) {
8298
8436
  if (raw.msgtype !== "audio") return void 0;
8299
8437
  if (!raw.content) return void 0;
@@ -8313,12 +8451,12 @@ function resolveAudioRecognition(raw) {
8313
8451
  function resolveGatewayAuthFromConfigFile(logger) {
8314
8452
  try {
8315
8453
  const fs5 = __require("fs");
8316
- const path5 = __require("path");
8454
+ const path6 = __require("path");
8317
8455
  const os4 = __require("os");
8318
8456
  const home = os4.homedir();
8319
8457
  const candidates = [
8320
- path5.join(home, ".openclaw", "openclaw.json"),
8321
- path5.join(home, ".openclaw", "config.json")
8458
+ path6.join(home, ".openclaw", "openclaw.json"),
8459
+ path6.join(home, ".openclaw", "config.json")
8322
8460
  ];
8323
8461
  for (const filePath of candidates) {
8324
8462
  if (!fs5.existsSync(filePath)) continue;
@@ -8532,35 +8670,21 @@ async function handleAICardStreaming(params) {
8532
8670
  }
8533
8671
  const now = Date.now();
8534
8672
  if (!firstFrameSent || now - lastUpdateTime >= updateInterval) {
8535
- await streamAICard(card, accumulated, false);
8673
+ const previewText = prepareDingtalkReplyContent({ text: accumulated }).text;
8674
+ await streamAICard(card, previewText, false);
8536
8675
  lastUpdateTime = now;
8537
8676
  firstFrameSent = true;
8538
8677
  }
8539
8678
  }
8540
- await finishAICard(card, accumulated, (msg) => logger.debug(msg));
8541
- logger.info(`AI Card streaming completed with ${accumulated.length} chars`);
8542
- const { mediaUrls: mediaFromLines } = extractMediaLinesFromText({
8679
+ const preparedReply = prepareDingtalkReplyContent({
8543
8680
  text: accumulated,
8544
8681
  logger
8545
8682
  });
8546
- const { mediaUrls: localMediaFromText } = extractLocalMediaFromText({
8547
- text: accumulated,
8548
- logger
8549
- });
8550
- const mediaQueue = [];
8551
- const seenMedia = /* @__PURE__ */ new Set();
8552
- const addMedia = (value) => {
8553
- const trimmed = value?.trim();
8554
- if (!trimmed) return;
8555
- if (seenMedia.has(trimmed)) return;
8556
- seenMedia.add(trimmed);
8557
- mediaQueue.push(trimmed);
8558
- };
8559
- for (const url of mediaFromLines) addMedia(url);
8560
- for (const url of localMediaFromText) addMedia(url);
8561
- if (mediaQueue.length > 0) {
8562
- logger.debug(`[stream] sending ${mediaQueue.length} media attachments`);
8563
- for (const mediaUrl of mediaQueue) {
8683
+ await finishAICard(card, preparedReply.text, (msg) => logger.debug(msg));
8684
+ logger.info(`AI Card streaming completed with ${preparedReply.text.length} chars`);
8685
+ if (preparedReply.mediaUrls.length > 0) {
8686
+ logger.debug(`[stream] sending ${preparedReply.mediaUrls.length} media attachments`);
8687
+ for (const mediaUrl of preparedReply.mediaUrls) {
8564
8688
  try {
8565
8689
  await sendMediaDingtalk({
8566
8690
  cfg: dingtalkCfg,
@@ -8584,9 +8708,13 @@ async function handleAICardStreaming(params) {
8584
8708
  }
8585
8709
  try {
8586
8710
  const fallbackText = accumulated.trim() ? accumulated : formatStreamingInterruptMessage(err);
8711
+ const preparedReply = prepareDingtalkReplyContent({
8712
+ text: fallbackText,
8713
+ logger
8714
+ });
8587
8715
  const limit = dingtalkCfg.textChunkLimit ?? 4e3;
8588
- for (let i = 0; i < fallbackText.length; i += limit) {
8589
- const chunk = fallbackText.slice(i, i + limit);
8716
+ for (let i = 0; i < preparedReply.text.length; i += limit) {
8717
+ const chunk = preparedReply.text.slice(i, i + limit);
8590
8718
  await sendMessageDingtalk({
8591
8719
  cfg: dingtalkCfg,
8592
8720
  to: targetId,
@@ -8594,26 +8722,7 @@ async function handleAICardStreaming(params) {
8594
8722
  chatType
8595
8723
  });
8596
8724
  }
8597
- const { mediaUrls: mediaFromLines } = extractMediaLinesFromText({
8598
- text: fallbackText,
8599
- logger
8600
- });
8601
- const { mediaUrls: localMediaFromText } = extractLocalMediaFromText({
8602
- text: fallbackText,
8603
- logger
8604
- });
8605
- const mediaQueue = [];
8606
- const seenMedia = /* @__PURE__ */ new Set();
8607
- const addMedia = (value) => {
8608
- const trimmed = value?.trim();
8609
- if (!trimmed) return;
8610
- if (seenMedia.has(trimmed)) return;
8611
- seenMedia.add(trimmed);
8612
- mediaQueue.push(trimmed);
8613
- };
8614
- for (const url of mediaFromLines) addMedia(url);
8615
- for (const url of localMediaFromText) addMedia(url);
8616
- for (const mediaUrl of mediaQueue) {
8725
+ for (const mediaUrl of preparedReply.mediaUrls) {
8617
8726
  await sendMediaDingtalk({
8618
8727
  cfg: dingtalkCfg,
8619
8728
  to: targetId,
@@ -8748,12 +8857,8 @@ async function handleDingtalkMessage(params) {
8748
8857
  logger.debug("core.channel.routing.resolveAgentRoute not available, skipping dispatch");
8749
8858
  return;
8750
8859
  }
8751
- if (!replyApi?.dispatchReplyFromConfig) {
8752
- logger.debug("core.channel.reply.dispatchReplyFromConfig not available, skipping dispatch");
8753
- return;
8754
- }
8755
- if (!replyApi?.createReplyDispatcher && !replyApi?.createReplyDispatcherWithTyping) {
8756
- logger.debug("core.channel.reply dispatcher factory not available, skipping dispatch");
8860
+ if (!replyApi?.dispatchReplyWithDispatcher && !replyApi?.dispatchReplyWithBufferedBlockDispatcher) {
8861
+ logger.warn("core.channel.reply real-time dispatcher not available, skipping dispatch");
8757
8862
  return;
8758
8863
  }
8759
8864
  const resolveAgentRoute = routingApi.resolveAgentRoute;
@@ -9039,30 +9144,18 @@ async function handleDingtalkMessage(params) {
9039
9144
  };
9040
9145
  const payloadMediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
9041
9146
  const rawText = payload.text ?? "";
9042
- const { mediaUrls: mediaFromLines } = extractMediaLinesFromText({
9147
+ const preparedReply = prepareDingtalkReplyContent({
9043
9148
  text: rawText,
9044
9149
  logger
9045
9150
  });
9046
- const { mediaUrls: localMediaFromText } = extractLocalMediaFromText({
9047
- text: rawText,
9048
- logger
9049
- });
9050
- const mediaQueue = [];
9051
- const seenMedia = /* @__PURE__ */ new Set();
9052
- const addMedia = (value) => {
9053
- const trimmed = value?.trim();
9054
- if (!trimmed) return;
9055
- if (seenMedia.has(trimmed)) return;
9056
- seenMedia.add(trimmed);
9057
- mediaQueue.push(trimmed);
9058
- };
9059
- for (const url of payloadMediaUrls) addMedia(url);
9060
- for (const url of mediaFromLines) addMedia(url);
9061
- for (const url of localMediaFromText) addMedia(url);
9151
+ const mediaQueue = dedupeMediaUrls([
9152
+ ...payloadMediaUrls,
9153
+ ...preparedReply.mediaUrls
9154
+ ]);
9062
9155
  const converted = textApi?.convertMarkdownTables?.(
9063
- rawText,
9156
+ preparedReply.text,
9064
9157
  tableMode
9065
- ) ?? rawText;
9158
+ ) ?? preparedReply.text;
9066
9159
  const hasText = converted.trim().length > 0;
9067
9160
  if (hasText) {
9068
9161
  const chunks = textApi?.chunkTextWithMode && typeof textChunkLimitResolved === "number" && textChunkLimitResolved > 0 ? textApi.chunkTextWithMode(converted, textChunkLimitResolved, chunkMode) : [converted];
@@ -9089,18 +9182,20 @@ async function handleDingtalkMessage(params) {
9089
9182
  cfg,
9090
9183
  route?.agentId
9091
9184
  );
9092
- const createDispatcherWithTyping = replyApi?.createReplyDispatcherWithTyping;
9093
- const createDispatcher = replyApi?.createReplyDispatcher;
9094
9185
  const dispatchReplyWithDispatcher = replyApi?.dispatchReplyWithDispatcher;
9095
- if (dispatchReplyWithDispatcher) {
9186
+ const dispatchReplyWithBufferedBlockDispatcher = replyApi?.dispatchReplyWithBufferedBlockDispatcher;
9187
+ const streamingReplyOptions = chatType === "direct" ? {
9188
+ disableBlockStreaming: false
9189
+ } : void 0;
9190
+ const runRealtimeDispatch = async (mode, dispatchFn) => {
9096
9191
  logger.debug(
9097
- `[dispatch] direct=${JSON.stringify({
9192
+ `[dispatch] ${mode}=${JSON.stringify({
9098
9193
  sessionKey: route?.sessionKey,
9099
9194
  ...resolvedTargetMeta
9100
9195
  })}`
9101
9196
  );
9102
9197
  const deliveryState = { delivered: false, skippedNonSilent: 0 };
9103
- const result2 = await dispatchReplyWithDispatcher({
9198
+ const result = await dispatchFn({
9104
9199
  ctx: finalCtx,
9105
9200
  cfg,
9106
9201
  dispatcherOptions: {
@@ -9122,7 +9217,8 @@ async function handleDingtalkMessage(params) {
9122
9217
  onError: (err, info) => {
9123
9218
  logger.error(`${info.kind} reply failed: ${String(err)}`);
9124
9219
  }
9125
- }
9220
+ },
9221
+ replyOptions: streamingReplyOptions
9126
9222
  });
9127
9223
  if (!deliveryState.delivered && deliveryState.skippedNonSilent > 0) {
9128
9224
  await sendMessageDingtalk({
@@ -9133,63 +9229,21 @@ async function handleDingtalkMessage(params) {
9133
9229
  });
9134
9230
  longTaskNotice.markReplyDelivered();
9135
9231
  }
9136
- const counts2 = result2?.counts;
9137
- const queuedFinal2 = result2?.queuedFinal;
9232
+ const counts = result?.counts;
9233
+ const queuedFinal = result?.queuedFinal;
9138
9234
  logger.debug(
9139
- `dispatch complete (queuedFinal=${typeof queuedFinal2 === "boolean" ? queuedFinal2 : "unknown"}, replies=${counts2?.final ?? 0})`
9235
+ `dispatch complete (queuedFinal=${typeof queuedFinal === "boolean" ? queuedFinal : "unknown"}, replies=${counts?.final ?? 0})`
9140
9236
  );
9141
- return;
9142
- }
9143
- const dispatcherResult = createDispatcherWithTyping ? createDispatcherWithTyping({
9144
- deliver: async (payload, info) => {
9145
- await deliver(payload, info);
9146
- },
9147
- humanDelay,
9148
- onError: (err, info) => {
9149
- logger.error(`${info.kind} reply failed: ${String(err)}`);
9150
- }
9151
- }) : {
9152
- dispatcher: createDispatcher?.({
9153
- deliver: async (payload, info) => {
9154
- await deliver(payload, info);
9155
- },
9156
- humanDelay,
9157
- onError: (err, info) => {
9158
- logger.error(`${info.kind} reply failed: ${String(err)}`);
9159
- }
9160
- }),
9161
- replyOptions: {},
9162
- markDispatchIdle: () => void 0
9163
9237
  };
9164
- const dispatcher = dispatcherResult?.dispatcher;
9165
- if (!dispatcher) {
9166
- logger.debug("dispatcher not available, skipping dispatch");
9238
+ if (dispatchReplyWithDispatcher) {
9239
+ await runRealtimeDispatch("direct", dispatchReplyWithDispatcher);
9167
9240
  return;
9168
9241
  }
9169
- logger.debug(
9170
- `[dispatch] legacy=${JSON.stringify({
9171
- sessionKey: route?.sessionKey,
9172
- ...resolvedTargetMeta
9173
- })}`
9174
- );
9175
- const dispatchReplyFromConfig = replyApi?.dispatchReplyFromConfig;
9176
- if (!dispatchReplyFromConfig) {
9177
- logger.debug("dispatchReplyFromConfig not available");
9242
+ if (dispatchReplyWithBufferedBlockDispatcher) {
9243
+ await runRealtimeDispatch("buffered", dispatchReplyWithBufferedBlockDispatcher);
9178
9244
  return;
9179
9245
  }
9180
- const result = await dispatchReplyFromConfig({
9181
- ctx: finalCtx,
9182
- cfg,
9183
- dispatcher,
9184
- replyOptions: dispatcherResult?.replyOptions ?? {}
9185
- });
9186
- const markDispatchIdle = dispatcherResult?.markDispatchIdle;
9187
- markDispatchIdle?.();
9188
- const counts = result?.counts;
9189
- const queuedFinal = result?.queuedFinal;
9190
- logger.debug(
9191
- `dispatch complete (queuedFinal=${typeof queuedFinal === "boolean" ? queuedFinal : "unknown"}, replies=${counts?.final ?? 0})`
9192
- );
9246
+ logger.warn("no real-time reply dispatcher available after capability check");
9193
9247
  } catch (err) {
9194
9248
  longTaskNotice.dispose();
9195
9249
  throw err;
@@ -10043,14 +10097,37 @@ function resolveDingtalkAccount(params) {
10043
10097
  function canStoreDefaultAccountInAccounts(cfg) {
10044
10098
  return Boolean(cfg.channels?.dingtalk?.accounts?.[DEFAULT_ACCOUNT_ID]);
10045
10099
  }
10100
+ function asRecord(value) {
10101
+ return value && typeof value === "object" ? value : void 0;
10102
+ }
10103
+ function hasRealtimeReplyApi(reply) {
10104
+ return Boolean(
10105
+ reply?.dispatchReplyWithDispatcher || reply?.dispatchReplyWithBufferedBlockDispatcher
10106
+ );
10107
+ }
10046
10108
  function resolveRuntimeCandidate(params) {
10047
- const runtimeRecord = params.runtime && typeof params.runtime === "object" ? params.runtime : void 0;
10048
- const runtimeChannel = runtimeRecord?.channel && typeof runtimeRecord.channel === "object" ? runtimeRecord.channel : void 0;
10049
- const channelRuntime = params.channelRuntime && typeof params.channelRuntime === "object" ? params.channelRuntime : void 0;
10050
- const resolvedChannel = channelRuntime ?? (runtimeChannel?.routing || runtimeChannel?.reply || runtimeChannel?.session || runtimeChannel?.text ? runtimeChannel : void 0);
10051
- if (!resolvedChannel) {
10109
+ const runtimeRecord = asRecord(params.runtime);
10110
+ const runtimeChannel = asRecord(runtimeRecord?.channel);
10111
+ const channelRuntime = asRecord(params.channelRuntime);
10112
+ if (!runtimeRecord && !channelRuntime) {
10113
+ return void 0;
10114
+ }
10115
+ if (!runtimeChannel && !channelRuntime) {
10052
10116
  return runtimeRecord;
10053
10117
  }
10118
+ const resolvedChannel = {
10119
+ ...runtimeChannel ?? {},
10120
+ ...channelRuntime ?? {}
10121
+ };
10122
+ for (const key of ["routing", "reply", "session", "text"]) {
10123
+ const mergedSection = {
10124
+ ...asRecord(runtimeChannel?.[key]) ?? {},
10125
+ ...asRecord(channelRuntime?.[key]) ?? {}
10126
+ };
10127
+ if (Object.keys(mergedSection).length > 0) {
10128
+ resolvedChannel[key] = mergedSection;
10129
+ }
10130
+ }
10054
10131
  return {
10055
10132
  ...runtimeRecord ?? {},
10056
10133
  channel: resolvedChannel
@@ -10378,7 +10455,7 @@ var dingtalkPlugin = {
10378
10455
  channelRuntime: ctx.channelRuntime
10379
10456
  });
10380
10457
  const candidate = runtimeCandidate;
10381
- if (candidate?.channel?.routing?.resolveAgentRoute && candidate.channel?.reply?.dispatchReplyFromConfig) {
10458
+ if (candidate?.channel?.routing?.resolveAgentRoute && hasRealtimeReplyApi(candidate.channel?.reply)) {
10382
10459
  setDingtalkRuntime(runtimeCandidate);
10383
10460
  }
10384
10461
  return monitorDingtalkProvider({