@wecom/wecom-openclaw-plugin 2026.3.25-beta.0 → 2026.3.25

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.cjs.js CHANGED
@@ -447,8 +447,6 @@ const IMAGE_DOWNLOAD_TIMEOUT_MS = 30000;
447
447
  const FILE_DOWNLOAD_TIMEOUT_MS = 60000;
448
448
  /** 消息发送超时时间(毫秒) */
449
449
  const REPLY_SEND_TIMEOUT_MS = 15000;
450
- /** 消息处理总超时时间(毫秒) */
451
- const MESSAGE_PROCESS_TIMEOUT_MS = 5 * 60 * 1000;
452
450
  /** WebSocket 心跳间隔(毫秒) */
453
451
  const WS_HEARTBEAT_INTERVAL_MS = 30000;
454
452
  /** WebSocket 连接断开时的最大重连次数 */
@@ -641,6 +639,23 @@ class TimeoutError extends Error {
641
639
  * 负责通过 WSClient 发送回复消息,包含超时保护
642
640
  */
643
641
  // ============================================================================
642
+ // 流式过期错误(errcode 846608)
643
+ // ============================================================================
644
+ /** 流式回复超时错误码(>6分钟未更新,服务端拒绝继续流式更新) */
645
+ const STREAM_EXPIRED_ERRCODE = 846608;
646
+ /**
647
+ * 流式回复过期错误
648
+ * 当服务端返回 errcode=846608 时抛出,表示流式消息已超过6分钟无法更新,
649
+ * 调用方需降级为主动发送(sendMessage)方式回复。
650
+ */
651
+ class StreamExpiredError extends Error {
652
+ constructor(message) {
653
+ super(message ?? `Stream message update expired (errcode=${STREAM_EXPIRED_ERRCODE})`);
654
+ this.errcode = STREAM_EXPIRED_ERRCODE;
655
+ this.name = "StreamExpiredError";
656
+ }
657
+ }
658
+ // ============================================================================
644
659
  // 消息发送
645
660
  // ============================================================================
646
661
  /**
@@ -660,7 +675,18 @@ async function sendWeComReply(params) {
660
675
  throw new Error("WSClient not connected");
661
676
  }
662
677
  // 使用 SDK 的 replyStream 方法发送消息,带超时保护
663
- await withTimeout(wsClient.replyStream(frame, streamId, text, finish), REPLY_SEND_TIMEOUT_MS, `Reply send timed out (streamId=${streamId})`);
678
+ try {
679
+ await withTimeout(wsClient.replyStream(frame, streamId, text, finish), REPLY_SEND_TIMEOUT_MS, `Reply send timed out (streamId=${streamId})`);
680
+ }
681
+ catch (err) {
682
+ // 服务端返回 846608:流式消息超过6分钟无法更新,需降级为主动发送
683
+ const errMsg = err?.errmsg || err?.message || String(err);
684
+ if (err?.errcode === STREAM_EXPIRED_ERRCODE ||
685
+ errMsg.includes(String(STREAM_EXPIRED_ERRCODE))) {
686
+ throw new StreamExpiredError(errMsg);
687
+ }
688
+ throw err;
689
+ }
664
690
  runtime.log?.(`[plugin -> server] streamId=${streamId}, finish=${finish}`);
665
691
  return streamId;
666
692
  }
@@ -1655,7 +1681,7 @@ function buildMessageContext(frame, account, config, text, mediaList, quoteConte
1655
1681
  * 发送"思考中"消息
1656
1682
  */
1657
1683
  async function sendThinkingReply(params) {
1658
- const { wsClient, frame, streamId, runtime } = params;
1684
+ const { wsClient, frame, streamId, runtime, state } = params;
1659
1685
  try {
1660
1686
  await sendWeComReply({
1661
1687
  wsClient,
@@ -1667,7 +1693,13 @@ async function sendThinkingReply(params) {
1667
1693
  });
1668
1694
  }
1669
1695
  catch (err) {
1670
- runtime.error?.(`[wecom] Failed to send thinking message: ${String(err)}`);
1696
+ if (err instanceof StreamExpiredError && state) {
1697
+ state.streamExpired = true;
1698
+ runtime.log?.(`[wecom] Stream expired during thinking reply, will fallback to proactive send`);
1699
+ }
1700
+ else {
1701
+ runtime.error?.(`[wecom] Failed to send thinking message: ${String(err)}`);
1702
+ }
1671
1703
  }
1672
1704
  }
1673
1705
  /**
@@ -1681,7 +1713,7 @@ async function sendMediaBatch(ctx, mediaUrls) {
1681
1713
  const body = frame.body;
1682
1714
  const chatId = body.chatid || body.from.userid;
1683
1715
  const mediaLocalRoots = await getExtendedMediaLocalRoots(account.config);
1684
- runtime.log?.(`[wecom][debug] mediaLocalRoots=${JSON.stringify(mediaLocalRoots)}, mediaUrls=${JSON.stringify(mediaUrls)}, hasText=${!!state.hasText}`);
1716
+ runtime.log?.(`[wecom][debug] mediaLocalRoots=${JSON.stringify(mediaLocalRoots)}, mediaUrls=${JSON.stringify(mediaUrls)}`);
1685
1717
  for (const mediaUrl of mediaUrls) {
1686
1718
  const result = await uploadAndSendMedia({
1687
1719
  wsClient,
@@ -1722,9 +1754,15 @@ async function sendMediaBatch(ctx, mediaUrls) {
1722
1754
  * (agent 可能已通过内置 message 工具直接发送了文件,
1723
1755
  * 该路径走 outbound.sendMedia 完全绕过 deliver 回调,
1724
1756
  * 所以 state 中无记录,但文件已实际送达)
1757
+ *
1758
+ * 降级策略:
1759
+ * - 当 streamExpired=true(errcode 846608)时,流式通道已不可用(>6分钟),
1760
+ * 改用 wsClient.sendMessage 主动发送完整文本。
1725
1761
  */
1726
1762
  async function finishThinkingStream(ctx) {
1727
1763
  const { wsClient, frame, state, runtime } = ctx;
1764
+ const body = frame.body;
1765
+ const chatId = body.chatid || body.from.userid;
1728
1766
  let finishText = state.accumulatedText;
1729
1767
  if (state.hasMedia) {
1730
1768
  if (state.hasMediaFailed && state.mediaErrorSummary) {
@@ -1739,7 +1777,28 @@ async function finishThinkingStream(ctx) {
1739
1777
  // finishText = "✅ 处理完成。";
1740
1778
  // }
1741
1779
  if (finishText) {
1742
- await sendWeComReply({ wsClient, frame, text: finishText, runtime, finish: true, streamId: state.streamId });
1780
+ // 尝试流式发送;若已知过期或发送时发现过期,统一降级为主动发送
1781
+ let expired = state.streamExpired;
1782
+ if (!expired) {
1783
+ try {
1784
+ await sendWeComReply({ wsClient, frame, text: finishText, runtime, finish: true, streamId: state.streamId });
1785
+ }
1786
+ catch (err) {
1787
+ if (err instanceof StreamExpiredError) {
1788
+ expired = true;
1789
+ }
1790
+ else {
1791
+ throw err;
1792
+ }
1793
+ }
1794
+ }
1795
+ if (expired) {
1796
+ runtime.log?.(`[wecom] Stream expired, sending final text via sendMessage (proactive)`);
1797
+ await wsClient.sendMessage(chatId, {
1798
+ msgtype: "markdown",
1799
+ markdown: { content: finishText },
1800
+ });
1801
+ }
1743
1802
  }
1744
1803
  }
1745
1804
  /**
@@ -1762,11 +1821,25 @@ async function routeAndDispatchMessage(params) {
1762
1821
  await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
1763
1822
  ctx: ctxPayload,
1764
1823
  cfg: config,
1824
+ // replyResolver: async (ctx, opts) => {
1825
+ // const startTime = Date.now();
1826
+ // const TEN_MINUTES = 7 * 60 * 1000;
1827
+ //
1828
+ // console.log('开始输出内容');
1829
+ //
1830
+ // while (Date.now() - startTime < TEN_MINUTES) {
1831
+ // // 每隔一段时间发送一个 block reply
1832
+ // await opts?.onBlockReply?.({ text: `输出内容 ${new Date().toISOString()}` });
1833
+ // await new Promise(resolve => setTimeout(resolve, 30 * 1000)); // 每5秒输出一次
1834
+ // }
1835
+ //
1836
+ // return { text: "10分钟输出完成" }; // 最终回复
1837
+ // },
1765
1838
  dispatcherOptions: {
1766
1839
  onReplyStart: async () => {
1767
1840
  if (!isShowThink && state.streamId && !state.accumulatedText) {
1768
1841
  try {
1769
- await sendThinkingReply({ wsClient, frame, streamId: state.streamId, runtime });
1842
+ await sendThinkingReply({ wsClient, frame, streamId: state.streamId, runtime, state });
1770
1843
  }
1771
1844
  catch (e) {
1772
1845
  runtime.error?.(`[wecom] sendThinkingReply threw err: ${String(e)}`);
@@ -1799,9 +1872,20 @@ async function routeAndDispatchMessage(params) {
1799
1872
  runtime.error?.(`[wecom] sendMediaBatch threw: ${errMsg}`);
1800
1873
  }
1801
1874
  }
1802
- // 中间帧:有可见文本时流式更新
1803
- if (info.kind !== "final" && state.hasText && state.accumulatedText) {
1804
- await sendWeComReply({ wsClient, frame, text: state.accumulatedText, runtime, finish: false, streamId: state.streamId });
1875
+ // 中间帧:有可见文本时流式更新(流式过期后跳过,等 deliver 完成后主动发送)
1876
+ if (info.kind !== "final" && state.accumulatedText && !state.streamExpired) {
1877
+ try {
1878
+ await sendWeComReply({ wsClient, frame, text: state.accumulatedText, runtime, finish: false, streamId: state.streamId });
1879
+ }
1880
+ catch (err) {
1881
+ if (err instanceof StreamExpiredError) {
1882
+ state.streamExpired = true;
1883
+ runtime.log?.(`[wecom] Stream expired during intermediate reply, will fallback to proactive send`);
1884
+ }
1885
+ else {
1886
+ throw err;
1887
+ }
1888
+ }
1805
1889
  }
1806
1890
  },
1807
1891
  onError: (err, info) => {
@@ -1926,7 +2010,7 @@ async function processWeComMessage(params) {
1926
2010
  const ctxPayload = buildMessageContext(frame, account, config, text, mediaList, quoteContent);
1927
2011
  // runtime.log?.(`[plugin -> openclaw] body=${text}, mediaPaths=${JSON.stringify(mediaList.map(m => m.path))}${quoteContent ? `, quote=${quoteContent}` : ''}`);
1928
2012
  try {
1929
- await withTimeout(routeAndDispatchMessage({
2013
+ await routeAndDispatchMessage({
1930
2014
  ctxPayload,
1931
2015
  config,
1932
2016
  account,
@@ -1935,10 +2019,10 @@ async function processWeComMessage(params) {
1935
2019
  state,
1936
2020
  runtime,
1937
2021
  onCleanup: cleanupState,
1938
- }), MESSAGE_PROCESS_TIMEOUT_MS, `Message processing timed out (msgId=${messageId})`);
2022
+ });
1939
2023
  }
1940
2024
  catch (err) {
1941
- runtime.error?.(`[wecom][plugin] Message processing failed or timed out: ${String(err)}`);
2025
+ runtime.error?.(`[wecom][plugin] Message processing failed: ${String(err)}`);
1942
2026
  cleanupState();
1943
2027
  }
1944
2028
  }