@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.esm.js CHANGED
@@ -420,8 +420,6 @@ const IMAGE_DOWNLOAD_TIMEOUT_MS = 30000;
420
420
  const FILE_DOWNLOAD_TIMEOUT_MS = 60000;
421
421
  /** 消息发送超时时间(毫秒) */
422
422
  const REPLY_SEND_TIMEOUT_MS = 15000;
423
- /** 消息处理总超时时间(毫秒) */
424
- const MESSAGE_PROCESS_TIMEOUT_MS = 5 * 60 * 1000;
425
423
  /** WebSocket 心跳间隔(毫秒) */
426
424
  const WS_HEARTBEAT_INTERVAL_MS = 30000;
427
425
  /** WebSocket 连接断开时的最大重连次数 */
@@ -614,6 +612,23 @@ class TimeoutError extends Error {
614
612
  * 负责通过 WSClient 发送回复消息,包含超时保护
615
613
  */
616
614
  // ============================================================================
615
+ // 流式过期错误(errcode 846608)
616
+ // ============================================================================
617
+ /** 流式回复超时错误码(>6分钟未更新,服务端拒绝继续流式更新) */
618
+ const STREAM_EXPIRED_ERRCODE = 846608;
619
+ /**
620
+ * 流式回复过期错误
621
+ * 当服务端返回 errcode=846608 时抛出,表示流式消息已超过6分钟无法更新,
622
+ * 调用方需降级为主动发送(sendMessage)方式回复。
623
+ */
624
+ class StreamExpiredError extends Error {
625
+ constructor(message) {
626
+ super(message ?? `Stream message update expired (errcode=${STREAM_EXPIRED_ERRCODE})`);
627
+ this.errcode = STREAM_EXPIRED_ERRCODE;
628
+ this.name = "StreamExpiredError";
629
+ }
630
+ }
631
+ // ============================================================================
617
632
  // 消息发送
618
633
  // ============================================================================
619
634
  /**
@@ -633,7 +648,18 @@ async function sendWeComReply(params) {
633
648
  throw new Error("WSClient not connected");
634
649
  }
635
650
  // 使用 SDK 的 replyStream 方法发送消息,带超时保护
636
- await withTimeout(wsClient.replyStream(frame, streamId, text, finish), REPLY_SEND_TIMEOUT_MS, `Reply send timed out (streamId=${streamId})`);
651
+ try {
652
+ await withTimeout(wsClient.replyStream(frame, streamId, text, finish), REPLY_SEND_TIMEOUT_MS, `Reply send timed out (streamId=${streamId})`);
653
+ }
654
+ catch (err) {
655
+ // 服务端返回 846608:流式消息超过6分钟无法更新,需降级为主动发送
656
+ const errMsg = err?.errmsg || err?.message || String(err);
657
+ if (err?.errcode === STREAM_EXPIRED_ERRCODE ||
658
+ errMsg.includes(String(STREAM_EXPIRED_ERRCODE))) {
659
+ throw new StreamExpiredError(errMsg);
660
+ }
661
+ throw err;
662
+ }
637
663
  runtime.log?.(`[plugin -> server] streamId=${streamId}, finish=${finish}`);
638
664
  return streamId;
639
665
  }
@@ -1628,7 +1654,7 @@ function buildMessageContext(frame, account, config, text, mediaList, quoteConte
1628
1654
  * 发送"思考中"消息
1629
1655
  */
1630
1656
  async function sendThinkingReply(params) {
1631
- const { wsClient, frame, streamId, runtime } = params;
1657
+ const { wsClient, frame, streamId, runtime, state } = params;
1632
1658
  try {
1633
1659
  await sendWeComReply({
1634
1660
  wsClient,
@@ -1640,7 +1666,13 @@ async function sendThinkingReply(params) {
1640
1666
  });
1641
1667
  }
1642
1668
  catch (err) {
1643
- runtime.error?.(`[wecom] Failed to send thinking message: ${String(err)}`);
1669
+ if (err instanceof StreamExpiredError && state) {
1670
+ state.streamExpired = true;
1671
+ runtime.log?.(`[wecom] Stream expired during thinking reply, will fallback to proactive send`);
1672
+ }
1673
+ else {
1674
+ runtime.error?.(`[wecom] Failed to send thinking message: ${String(err)}`);
1675
+ }
1644
1676
  }
1645
1677
  }
1646
1678
  /**
@@ -1654,7 +1686,7 @@ async function sendMediaBatch(ctx, mediaUrls) {
1654
1686
  const body = frame.body;
1655
1687
  const chatId = body.chatid || body.from.userid;
1656
1688
  const mediaLocalRoots = await getExtendedMediaLocalRoots(account.config);
1657
- runtime.log?.(`[wecom][debug] mediaLocalRoots=${JSON.stringify(mediaLocalRoots)}, mediaUrls=${JSON.stringify(mediaUrls)}, hasText=${!!state.hasText}`);
1689
+ runtime.log?.(`[wecom][debug] mediaLocalRoots=${JSON.stringify(mediaLocalRoots)}, mediaUrls=${JSON.stringify(mediaUrls)}`);
1658
1690
  for (const mediaUrl of mediaUrls) {
1659
1691
  const result = await uploadAndSendMedia({
1660
1692
  wsClient,
@@ -1695,9 +1727,15 @@ async function sendMediaBatch(ctx, mediaUrls) {
1695
1727
  * (agent 可能已通过内置 message 工具直接发送了文件,
1696
1728
  * 该路径走 outbound.sendMedia 完全绕过 deliver 回调,
1697
1729
  * 所以 state 中无记录,但文件已实际送达)
1730
+ *
1731
+ * 降级策略:
1732
+ * - 当 streamExpired=true(errcode 846608)时,流式通道已不可用(>6分钟),
1733
+ * 改用 wsClient.sendMessage 主动发送完整文本。
1698
1734
  */
1699
1735
  async function finishThinkingStream(ctx) {
1700
1736
  const { wsClient, frame, state, runtime } = ctx;
1737
+ const body = frame.body;
1738
+ const chatId = body.chatid || body.from.userid;
1701
1739
  let finishText = state.accumulatedText;
1702
1740
  if (state.hasMedia) {
1703
1741
  if (state.hasMediaFailed && state.mediaErrorSummary) {
@@ -1712,7 +1750,28 @@ async function finishThinkingStream(ctx) {
1712
1750
  // finishText = "✅ 处理完成。";
1713
1751
  // }
1714
1752
  if (finishText) {
1715
- await sendWeComReply({ wsClient, frame, text: finishText, runtime, finish: true, streamId: state.streamId });
1753
+ // 尝试流式发送;若已知过期或发送时发现过期,统一降级为主动发送
1754
+ let expired = state.streamExpired;
1755
+ if (!expired) {
1756
+ try {
1757
+ await sendWeComReply({ wsClient, frame, text: finishText, runtime, finish: true, streamId: state.streamId });
1758
+ }
1759
+ catch (err) {
1760
+ if (err instanceof StreamExpiredError) {
1761
+ expired = true;
1762
+ }
1763
+ else {
1764
+ throw err;
1765
+ }
1766
+ }
1767
+ }
1768
+ if (expired) {
1769
+ runtime.log?.(`[wecom] Stream expired, sending final text via sendMessage (proactive)`);
1770
+ await wsClient.sendMessage(chatId, {
1771
+ msgtype: "markdown",
1772
+ markdown: { content: finishText },
1773
+ });
1774
+ }
1716
1775
  }
1717
1776
  }
1718
1777
  /**
@@ -1735,11 +1794,25 @@ async function routeAndDispatchMessage(params) {
1735
1794
  await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
1736
1795
  ctx: ctxPayload,
1737
1796
  cfg: config,
1797
+ // replyResolver: async (ctx, opts) => {
1798
+ // const startTime = Date.now();
1799
+ // const TEN_MINUTES = 7 * 60 * 1000;
1800
+ //
1801
+ // console.log('开始输出内容');
1802
+ //
1803
+ // while (Date.now() - startTime < TEN_MINUTES) {
1804
+ // // 每隔一段时间发送一个 block reply
1805
+ // await opts?.onBlockReply?.({ text: `输出内容 ${new Date().toISOString()}` });
1806
+ // await new Promise(resolve => setTimeout(resolve, 30 * 1000)); // 每5秒输出一次
1807
+ // }
1808
+ //
1809
+ // return { text: "10分钟输出完成" }; // 最终回复
1810
+ // },
1738
1811
  dispatcherOptions: {
1739
1812
  onReplyStart: async () => {
1740
1813
  if (!isShowThink && state.streamId && !state.accumulatedText) {
1741
1814
  try {
1742
- await sendThinkingReply({ wsClient, frame, streamId: state.streamId, runtime });
1815
+ await sendThinkingReply({ wsClient, frame, streamId: state.streamId, runtime, state });
1743
1816
  }
1744
1817
  catch (e) {
1745
1818
  runtime.error?.(`[wecom] sendThinkingReply threw err: ${String(e)}`);
@@ -1772,9 +1845,20 @@ async function routeAndDispatchMessage(params) {
1772
1845
  runtime.error?.(`[wecom] sendMediaBatch threw: ${errMsg}`);
1773
1846
  }
1774
1847
  }
1775
- // 中间帧:有可见文本时流式更新
1776
- if (info.kind !== "final" && state.hasText && state.accumulatedText) {
1777
- await sendWeComReply({ wsClient, frame, text: state.accumulatedText, runtime, finish: false, streamId: state.streamId });
1848
+ // 中间帧:有可见文本时流式更新(流式过期后跳过,等 deliver 完成后主动发送)
1849
+ if (info.kind !== "final" && state.accumulatedText && !state.streamExpired) {
1850
+ try {
1851
+ await sendWeComReply({ wsClient, frame, text: state.accumulatedText, runtime, finish: false, streamId: state.streamId });
1852
+ }
1853
+ catch (err) {
1854
+ if (err instanceof StreamExpiredError) {
1855
+ state.streamExpired = true;
1856
+ runtime.log?.(`[wecom] Stream expired during intermediate reply, will fallback to proactive send`);
1857
+ }
1858
+ else {
1859
+ throw err;
1860
+ }
1861
+ }
1778
1862
  }
1779
1863
  },
1780
1864
  onError: (err, info) => {
@@ -1899,7 +1983,7 @@ async function processWeComMessage(params) {
1899
1983
  const ctxPayload = buildMessageContext(frame, account, config, text, mediaList, quoteContent);
1900
1984
  // runtime.log?.(`[plugin -> openclaw] body=${text}, mediaPaths=${JSON.stringify(mediaList.map(m => m.path))}${quoteContent ? `, quote=${quoteContent}` : ''}`);
1901
1985
  try {
1902
- await withTimeout(routeAndDispatchMessage({
1986
+ await routeAndDispatchMessage({
1903
1987
  ctxPayload,
1904
1988
  config,
1905
1989
  account,
@@ -1908,10 +1992,10 @@ async function processWeComMessage(params) {
1908
1992
  state,
1909
1993
  runtime,
1910
1994
  onCleanup: cleanupState,
1911
- }), MESSAGE_PROCESS_TIMEOUT_MS, `Message processing timed out (msgId=${messageId})`);
1995
+ });
1912
1996
  }
1913
1997
  catch (err) {
1914
- runtime.error?.(`[wecom][plugin] Message processing failed or timed out: ${String(err)}`);
1998
+ runtime.error?.(`[wecom][plugin] Message processing failed: ${String(err)}`);
1915
1999
  cleanupState();
1916
2000
  }
1917
2001
  }