@openclaw-china/dingtalk 2026.3.18 → 2026.3.20

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,89 @@ 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
+ let activeDeliveryMode;
6970
+ if (replyMode === "active") {
6971
+ activeDeliveryMode = await prompter.askSelect(
6972
+ "\u4E3B\u52A8\u53D1\u9001\u6A21\u5F0F\uFF08activeDeliveryMode\uFF09",
6973
+ [
6974
+ { value: "split", label: "split\uFF08\u9010\u5757\u53D1\u9001\uFF0C\u63A8\u8350\uFF09" },
6975
+ { value: "merged", label: "merged\uFF08\u5408\u5E76\u540E\u5355\u6B21\u53D1\u9001\uFF09" }
6976
+ ],
6977
+ toTrimmedString2(existing.activeDeliveryMode) ?? "split"
6978
+ );
6979
+ }
6980
+ const renderMarkdown = await prompter.askConfirm(
6981
+ "\u542F\u7528 Markdown \u6E32\u67D3\uFF08\u63A8\u8350\u5F00\u542F\uFF09",
6982
+ toBoolean(existing.renderMarkdown, true)
6983
+ );
6984
+ const welcomeText = await prompter.askText({
6985
+ label: "\u6B22\u8FCE\u8BED\uFF08\u53EF\u9009\uFF09",
6986
+ defaultValue: toTrimmedString2(existing.welcomeText),
6987
+ required: false
6988
+ });
6989
+ return mergeChannelConfig(cfg, "wechat-mp", {
6990
+ webhookPath,
6991
+ appId,
6992
+ appSecret: appSecret || void 0,
6993
+ token,
6994
+ encodingAESKey: messageMode === "plain" ? void 0 : encodingAESKey,
6995
+ messageMode,
6996
+ replyMode,
6997
+ activeDeliveryMode,
6998
+ renderMarkdown,
6909
6999
  welcomeText: welcomeText || void 0
6910
7000
  });
6911
7001
  }
@@ -6967,6 +7057,8 @@ async function configureSingleChannel(channel, prompter, cfg) {
6967
7057
  return configureWecomApp(prompter, cfg);
6968
7058
  case "wecom-kf":
6969
7059
  return configureWecomKf(prompter, cfg);
7060
+ case "wechat-mp":
7061
+ return configureWechatMp(prompter, cfg);
6970
7062
  case "qqbot":
6971
7063
  return configureQQBot(prompter, cfg);
6972
7064
  default:
@@ -7108,6 +7200,7 @@ var SUPPORTED_CHANNELS = [
7108
7200
  "wecom",
7109
7201
  "wecom-app",
7110
7202
  "wecom-kf",
7203
+ "wechat-mp",
7111
7204
  "qqbot"
7112
7205
  ];
7113
7206
  var CHINA_INSTALL_HINT_SHOWN_KEY = /* @__PURE__ */ Symbol.for("@openclaw-china/china-install-hint-shown");
@@ -8251,6 +8344,52 @@ ${list}`;
8251
8344
 
8252
8345
  ${prompt}` : content;
8253
8346
  }
8347
+ function escapeRegExp(value) {
8348
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8349
+ }
8350
+ function finalizeReplyText(text) {
8351
+ return text.replace(/\n{3,}/g, "\n\n").trim();
8352
+ }
8353
+ function stripLocalMediaSyntaxFromText(text, mediaItems) {
8354
+ let result = text;
8355
+ for (const media of mediaItems) {
8356
+ const source = media.source?.trim();
8357
+ if (!source) continue;
8358
+ const escapedSource = escapeRegExp(source);
8359
+ const fileName = media.fileName ?? path.basename(media.localPath ?? source);
8360
+ if (media.type === "image") {
8361
+ const pattern = new RegExp(`\\[!\\[[^\\]]*\\]\\(${escapedSource}\\)\\]\\([^\\)]+\\)`, "g");
8362
+ result = result.replace(pattern, "");
8363
+ }
8364
+ if (media.sourceKind === "markdown") {
8365
+ if (media.type === "image") {
8366
+ const pattern = new RegExp(`!\\[[^\\]]*\\]\\(${escapedSource}\\)`, "g");
8367
+ result = result.replace(pattern, "");
8368
+ } else {
8369
+ const pattern = new RegExp(`\\[[^\\]]*\\]\\(${escapedSource}\\)`, "g");
8370
+ result = result.replace(pattern, `[\u6587\u4EF6: ${fileName}]`);
8371
+ }
8372
+ continue;
8373
+ }
8374
+ if (result.includes(source)) {
8375
+ const replacement = media.type === "image" ? "" : `[\u6587\u4EF6: ${fileName}]`;
8376
+ result = result.split(source).join(replacement);
8377
+ }
8378
+ }
8379
+ return finalizeReplyText(result);
8380
+ }
8381
+ function dedupeMediaUrls(values) {
8382
+ const mediaQueue = [];
8383
+ const seenMedia = /* @__PURE__ */ new Set();
8384
+ for (const value of values) {
8385
+ const trimmed = value?.trim();
8386
+ if (!trimmed) continue;
8387
+ if (seenMedia.has(trimmed)) continue;
8388
+ seenMedia.add(trimmed);
8389
+ mediaQueue.push(trimmed);
8390
+ }
8391
+ return mediaQueue;
8392
+ }
8254
8393
  function extractLocalMediaFromText(params) {
8255
8394
  const { text, logger } = params;
8256
8395
  const result = extractMediaFromText(text, {
@@ -8270,13 +8409,17 @@ function extractLocalMediaFromText(params) {
8270
8409
  parseBarePaths: true,
8271
8410
  parseMarkdownLinks: true
8272
8411
  });
8273
- const mediaUrls = result.all.filter((m) => m.isLocal && m.localPath).map((m) => m.localPath);
8274
- return { mediaUrls };
8412
+ const localMedia = result.all.filter((m) => m.isLocal && m.localPath);
8413
+ const mediaUrls = localMedia.map((m) => m.localPath);
8414
+ return {
8415
+ text: stripLocalMediaSyntaxFromText(text, localMedia),
8416
+ mediaUrls
8417
+ };
8275
8418
  }
8276
8419
  function extractMediaLinesFromText(params) {
8277
8420
  const { text, logger } = params;
8278
8421
  const result = extractMediaFromText(text, {
8279
- removeFromText: false,
8422
+ removeFromText: true,
8280
8423
  checkExists: true,
8281
8424
  existsSync: (p) => {
8282
8425
  const exists = fs3.existsSync(p);
@@ -8294,6 +8437,18 @@ function extractMediaLinesFromText(params) {
8294
8437
  const mediaUrls = result.all.map((m) => m.isLocal ? m.localPath ?? m.source : m.source).filter((m) => typeof m === "string" && m.trim().length > 0);
8295
8438
  return { text: result.text, mediaUrls };
8296
8439
  }
8440
+ function prepareDingtalkReplyContent(params) {
8441
+ const { text, logger } = params;
8442
+ const mediaLineResult = extractMediaLinesFromText({ text, logger });
8443
+ const localMediaResult = extractLocalMediaFromText({
8444
+ text: mediaLineResult.text,
8445
+ logger
8446
+ });
8447
+ return {
8448
+ text: localMediaResult.text,
8449
+ mediaUrls: dedupeMediaUrls([...mediaLineResult.mediaUrls, ...localMediaResult.mediaUrls])
8450
+ };
8451
+ }
8297
8452
  function resolveAudioRecognition(raw) {
8298
8453
  if (raw.msgtype !== "audio") return void 0;
8299
8454
  if (!raw.content) return void 0;
@@ -8313,12 +8468,12 @@ function resolveAudioRecognition(raw) {
8313
8468
  function resolveGatewayAuthFromConfigFile(logger) {
8314
8469
  try {
8315
8470
  const fs5 = __require("fs");
8316
- const path5 = __require("path");
8471
+ const path6 = __require("path");
8317
8472
  const os4 = __require("os");
8318
8473
  const home = os4.homedir();
8319
8474
  const candidates = [
8320
- path5.join(home, ".openclaw", "openclaw.json"),
8321
- path5.join(home, ".openclaw", "config.json")
8475
+ path6.join(home, ".openclaw", "openclaw.json"),
8476
+ path6.join(home, ".openclaw", "config.json")
8322
8477
  ];
8323
8478
  for (const filePath of candidates) {
8324
8479
  if (!fs5.existsSync(filePath)) continue;
@@ -8347,7 +8502,8 @@ function resolveGatewayRequestParams(runtime2, dingtalkCfg, logger) {
8347
8502
  const gatewayUrl = typeof gateway?.url === "string" ? gateway.url : `http://127.0.0.1:${gatewayPort}/v1/chat/completions`;
8348
8503
  const authToken = dingtalkCfg.gatewayToken ?? dingtalkCfg.gatewayPassword ?? gateway?.auth?.token ?? gateway?.authToken ?? gateway?.token ?? process.env.OPENCLAW_GATEWAY_TOKEN ?? process.env.OPENCLAW_GATEWAY_PASSWORD ?? resolveGatewayAuthFromConfigFile(logger);
8349
8504
  const headers = {
8350
- "Content-Type": "application/json"
8505
+ "Content-Type": "application/json",
8506
+ "x-openclaw-message-channel": "dingtalk"
8351
8507
  };
8352
8508
  if (typeof authToken === "string" && authToken.trim()) {
8353
8509
  headers["Authorization"] = `Bearer ${authToken}`;
@@ -8370,12 +8526,14 @@ async function* streamFromGateway(params) {
8370
8526
  logger.debug(`[gateway] streaming via ${gatewayUrl}, session=${sessionKey}`);
8371
8527
  const response = await fetch(gatewayUrl, {
8372
8528
  method: "POST",
8373
- headers,
8529
+ headers: {
8530
+ ...headers,
8531
+ "x-openclaw-session-key": sessionKey
8532
+ },
8374
8533
  body: JSON.stringify({
8375
8534
  model: "default",
8376
8535
  messages: [{ role: "user", content: userContent }],
8377
- stream: true,
8378
- user: sessionKey
8536
+ stream: true
8379
8537
  }),
8380
8538
  signal: abortSignal
8381
8539
  });
@@ -8532,35 +8690,21 @@ async function handleAICardStreaming(params) {
8532
8690
  }
8533
8691
  const now = Date.now();
8534
8692
  if (!firstFrameSent || now - lastUpdateTime >= updateInterval) {
8535
- await streamAICard(card, accumulated, false);
8693
+ const previewText = prepareDingtalkReplyContent({ text: accumulated }).text;
8694
+ await streamAICard(card, previewText, false);
8536
8695
  lastUpdateTime = now;
8537
8696
  firstFrameSent = true;
8538
8697
  }
8539
8698
  }
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({
8543
- text: accumulated,
8544
- logger
8545
- });
8546
- const { mediaUrls: localMediaFromText } = extractLocalMediaFromText({
8699
+ const preparedReply = prepareDingtalkReplyContent({
8547
8700
  text: accumulated,
8548
8701
  logger
8549
8702
  });
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) {
8703
+ await finishAICard(card, preparedReply.text, (msg) => logger.debug(msg));
8704
+ logger.info(`AI Card streaming completed with ${preparedReply.text.length} chars`);
8705
+ if (preparedReply.mediaUrls.length > 0) {
8706
+ logger.debug(`[stream] sending ${preparedReply.mediaUrls.length} media attachments`);
8707
+ for (const mediaUrl of preparedReply.mediaUrls) {
8564
8708
  try {
8565
8709
  await sendMediaDingtalk({
8566
8710
  cfg: dingtalkCfg,
@@ -8584,9 +8728,13 @@ async function handleAICardStreaming(params) {
8584
8728
  }
8585
8729
  try {
8586
8730
  const fallbackText = accumulated.trim() ? accumulated : formatStreamingInterruptMessage(err);
8731
+ const preparedReply = prepareDingtalkReplyContent({
8732
+ text: fallbackText,
8733
+ logger
8734
+ });
8587
8735
  const limit = dingtalkCfg.textChunkLimit ?? 4e3;
8588
- for (let i = 0; i < fallbackText.length; i += limit) {
8589
- const chunk = fallbackText.slice(i, i + limit);
8736
+ for (let i = 0; i < preparedReply.text.length; i += limit) {
8737
+ const chunk = preparedReply.text.slice(i, i + limit);
8590
8738
  await sendMessageDingtalk({
8591
8739
  cfg: dingtalkCfg,
8592
8740
  to: targetId,
@@ -8594,26 +8742,7 @@ async function handleAICardStreaming(params) {
8594
8742
  chatType
8595
8743
  });
8596
8744
  }
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) {
8745
+ for (const mediaUrl of preparedReply.mediaUrls) {
8617
8746
  await sendMediaDingtalk({
8618
8747
  cfg: dingtalkCfg,
8619
8748
  to: targetId,
@@ -8748,12 +8877,8 @@ async function handleDingtalkMessage(params) {
8748
8877
  logger.debug("core.channel.routing.resolveAgentRoute not available, skipping dispatch");
8749
8878
  return;
8750
8879
  }
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");
8880
+ if (!replyApi?.dispatchReplyWithDispatcher && !replyApi?.dispatchReplyWithBufferedBlockDispatcher) {
8881
+ logger.warn("core.channel.reply real-time dispatcher not available, skipping dispatch");
8757
8882
  return;
8758
8883
  }
8759
8884
  const resolveAgentRoute = routingApi.resolveAgentRoute;
@@ -9039,30 +9164,18 @@ async function handleDingtalkMessage(params) {
9039
9164
  };
9040
9165
  const payloadMediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
9041
9166
  const rawText = payload.text ?? "";
9042
- const { mediaUrls: mediaFromLines } = extractMediaLinesFromText({
9167
+ const preparedReply = prepareDingtalkReplyContent({
9043
9168
  text: rawText,
9044
9169
  logger
9045
9170
  });
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);
9171
+ const mediaQueue = dedupeMediaUrls([
9172
+ ...payloadMediaUrls,
9173
+ ...preparedReply.mediaUrls
9174
+ ]);
9062
9175
  const converted = textApi?.convertMarkdownTables?.(
9063
- rawText,
9176
+ preparedReply.text,
9064
9177
  tableMode
9065
- ) ?? rawText;
9178
+ ) ?? preparedReply.text;
9066
9179
  const hasText = converted.trim().length > 0;
9067
9180
  if (hasText) {
9068
9181
  const chunks = textApi?.chunkTextWithMode && typeof textChunkLimitResolved === "number" && textChunkLimitResolved > 0 ? textApi.chunkTextWithMode(converted, textChunkLimitResolved, chunkMode) : [converted];
@@ -9089,18 +9202,20 @@ async function handleDingtalkMessage(params) {
9089
9202
  cfg,
9090
9203
  route?.agentId
9091
9204
  );
9092
- const createDispatcherWithTyping = replyApi?.createReplyDispatcherWithTyping;
9093
- const createDispatcher = replyApi?.createReplyDispatcher;
9094
9205
  const dispatchReplyWithDispatcher = replyApi?.dispatchReplyWithDispatcher;
9095
- if (dispatchReplyWithDispatcher) {
9206
+ const dispatchReplyWithBufferedBlockDispatcher = replyApi?.dispatchReplyWithBufferedBlockDispatcher;
9207
+ const streamingReplyOptions = chatType === "direct" ? {
9208
+ disableBlockStreaming: false
9209
+ } : void 0;
9210
+ const runRealtimeDispatch = async (mode, dispatchFn) => {
9096
9211
  logger.debug(
9097
- `[dispatch] direct=${JSON.stringify({
9212
+ `[dispatch] ${mode}=${JSON.stringify({
9098
9213
  sessionKey: route?.sessionKey,
9099
9214
  ...resolvedTargetMeta
9100
9215
  })}`
9101
9216
  );
9102
9217
  const deliveryState = { delivered: false, skippedNonSilent: 0 };
9103
- const result2 = await dispatchReplyWithDispatcher({
9218
+ const result = await dispatchFn({
9104
9219
  ctx: finalCtx,
9105
9220
  cfg,
9106
9221
  dispatcherOptions: {
@@ -9122,7 +9237,8 @@ async function handleDingtalkMessage(params) {
9122
9237
  onError: (err, info) => {
9123
9238
  logger.error(`${info.kind} reply failed: ${String(err)}`);
9124
9239
  }
9125
- }
9240
+ },
9241
+ replyOptions: streamingReplyOptions
9126
9242
  });
9127
9243
  if (!deliveryState.delivered && deliveryState.skippedNonSilent > 0) {
9128
9244
  await sendMessageDingtalk({
@@ -9133,63 +9249,21 @@ async function handleDingtalkMessage(params) {
9133
9249
  });
9134
9250
  longTaskNotice.markReplyDelivered();
9135
9251
  }
9136
- const counts2 = result2?.counts;
9137
- const queuedFinal2 = result2?.queuedFinal;
9252
+ const counts = result?.counts;
9253
+ const queuedFinal = result?.queuedFinal;
9138
9254
  logger.debug(
9139
- `dispatch complete (queuedFinal=${typeof queuedFinal2 === "boolean" ? queuedFinal2 : "unknown"}, replies=${counts2?.final ?? 0})`
9255
+ `dispatch complete (queuedFinal=${typeof queuedFinal === "boolean" ? queuedFinal : "unknown"}, replies=${counts?.final ?? 0})`
9140
9256
  );
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
9257
  };
9164
- const dispatcher = dispatcherResult?.dispatcher;
9165
- if (!dispatcher) {
9166
- logger.debug("dispatcher not available, skipping dispatch");
9258
+ if (dispatchReplyWithDispatcher) {
9259
+ await runRealtimeDispatch("direct", dispatchReplyWithDispatcher);
9167
9260
  return;
9168
9261
  }
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");
9262
+ if (dispatchReplyWithBufferedBlockDispatcher) {
9263
+ await runRealtimeDispatch("buffered", dispatchReplyWithBufferedBlockDispatcher);
9178
9264
  return;
9179
9265
  }
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
- );
9266
+ logger.warn("no real-time reply dispatcher available after capability check");
9193
9267
  } catch (err) {
9194
9268
  longTaskNotice.dispose();
9195
9269
  throw err;
@@ -10043,14 +10117,37 @@ function resolveDingtalkAccount(params) {
10043
10117
  function canStoreDefaultAccountInAccounts(cfg) {
10044
10118
  return Boolean(cfg.channels?.dingtalk?.accounts?.[DEFAULT_ACCOUNT_ID]);
10045
10119
  }
10120
+ function asRecord(value) {
10121
+ return value && typeof value === "object" ? value : void 0;
10122
+ }
10123
+ function hasRealtimeReplyApi(reply) {
10124
+ return Boolean(
10125
+ reply?.dispatchReplyWithDispatcher || reply?.dispatchReplyWithBufferedBlockDispatcher
10126
+ );
10127
+ }
10046
10128
  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) {
10129
+ const runtimeRecord = asRecord(params.runtime);
10130
+ const runtimeChannel = asRecord(runtimeRecord?.channel);
10131
+ const channelRuntime = asRecord(params.channelRuntime);
10132
+ if (!runtimeRecord && !channelRuntime) {
10133
+ return void 0;
10134
+ }
10135
+ if (!runtimeChannel && !channelRuntime) {
10052
10136
  return runtimeRecord;
10053
10137
  }
10138
+ const resolvedChannel = {
10139
+ ...runtimeChannel ?? {},
10140
+ ...channelRuntime ?? {}
10141
+ };
10142
+ for (const key of ["routing", "reply", "session", "text"]) {
10143
+ const mergedSection = {
10144
+ ...asRecord(runtimeChannel?.[key]) ?? {},
10145
+ ...asRecord(channelRuntime?.[key]) ?? {}
10146
+ };
10147
+ if (Object.keys(mergedSection).length > 0) {
10148
+ resolvedChannel[key] = mergedSection;
10149
+ }
10150
+ }
10054
10151
  return {
10055
10152
  ...runtimeRecord ?? {},
10056
10153
  channel: resolvedChannel
@@ -10378,7 +10475,7 @@ var dingtalkPlugin = {
10378
10475
  channelRuntime: ctx.channelRuntime
10379
10476
  });
10380
10477
  const candidate = runtimeCandidate;
10381
- if (candidate?.channel?.routing?.resolveAgentRoute && candidate.channel?.reply?.dispatchReplyFromConfig) {
10478
+ if (candidate?.channel?.routing?.resolveAgentRoute && hasRealtimeReplyApi(candidate.channel?.reply)) {
10382
10479
  setDingtalkRuntime(runtimeCandidate);
10383
10480
  }
10384
10481
  return monitorDingtalkProvider({