@wecom/wecom-openclaw-plugin 2026.3.24 → 2026.3.25-beta.1

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 || (typeof err === "object" ? JSON.stringify(err) : 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
  }
@@ -1498,14 +1524,6 @@ const PLUGIN_VERSION = getVersion();
1498
1524
  * - state-manager.ts : 全局状态管理(带 TTL 清理)
1499
1525
  * - timeout.ts : 超时工具
1500
1526
  */
1501
- /**
1502
- * 去除文本中的 `<think>...</think>` 标签(支持跨行),返回剩余可见文本。
1503
- * 用于判断大模型回复中是否包含实际用户可见内容(而非仅有 thinking 推理过程)。
1504
- */
1505
- function stripThinkTags(text) {
1506
- return text;
1507
- // return text.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
1508
- }
1509
1527
  // ============================================================================
1510
1528
  // 媒体本地路径白名单扩展
1511
1529
  // ============================================================================
@@ -1636,7 +1654,7 @@ function buildMessageContext(frame, account, config, text, mediaList, quoteConte
1636
1654
  * 发送"思考中"消息
1637
1655
  */
1638
1656
  async function sendThinkingReply(params) {
1639
- const { wsClient, frame, streamId, runtime } = params;
1657
+ const { wsClient, frame, streamId, runtime, state } = params;
1640
1658
  try {
1641
1659
  await sendWeComReply({
1642
1660
  wsClient,
@@ -1648,16 +1666,13 @@ async function sendThinkingReply(params) {
1648
1666
  });
1649
1667
  }
1650
1668
  catch (err) {
1651
- runtime.error?.(`[wecom] Failed to send thinking message: ${String(err)}`);
1652
- }
1653
- }
1654
- /**
1655
- * 累积文本并判断是否有可见内容(去除 <think> 标签后)
1656
- */
1657
- function accumulateText(state, text) {
1658
- state.accumulatedText += text;
1659
- if (!state.hasText && stripThinkTags(state.accumulatedText)) {
1660
- state.hasText = true;
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
+ }
1661
1676
  }
1662
1677
  }
1663
1678
  /**
@@ -1712,33 +1727,52 @@ async function sendMediaBatch(ctx, mediaUrls) {
1712
1727
  * (agent 可能已通过内置 message 工具直接发送了文件,
1713
1728
  * 该路径走 outbound.sendMedia 完全绕过 deliver 回调,
1714
1729
  * 所以 state 中无记录,但文件已实际送达)
1730
+ *
1731
+ * 降级策略:
1732
+ * - 当 streamExpired=true(errcode 846608)时,流式通道已不可用(>6分钟),
1733
+ * 改用 wsClient.sendMessage 主动发送完整文本。
1715
1734
  */
1716
1735
  async function finishThinkingStream(ctx) {
1717
1736
  const { wsClient, frame, state, runtime } = ctx;
1718
- const visibleText = stripThinkTags(state.accumulatedText);
1719
- let finishText;
1720
- if (visibleText) {
1721
- // 有可见文本:用完整文本关闭流(覆盖 thinking 为真实内容)
1722
- finishText = state.accumulatedText;
1723
- }
1724
- else if (state.hasMedia) {
1725
- // 媒体成功发送:用友好提示告知用户
1726
- finishText = "📎 文件已发送,请查收。";
1727
- }
1728
- else if (state.hasMediaFailed && state.mediaErrorSummary) {
1729
- // 媒体发送失败:直接用错误摘要替换 thinking 流(不再额外发 sendMessage)
1730
- finishText = state.mediaErrorSummary;
1737
+ const body = frame.body;
1738
+ const chatId = body.chatid || body.from.userid;
1739
+ let finishText = state.accumulatedText;
1740
+ if (state.hasMedia) {
1741
+ if (state.hasMediaFailed && state.mediaErrorSummary) {
1742
+ // 媒体成功发送:用友好提示告知用户
1743
+ finishText = finishText ? `${finishText}\n\n${state.mediaErrorSummary}` : state.mediaErrorSummary;
1744
+ }
1745
+ else if (!finishText) {
1746
+ finishText = "📎 文件已发送,请查收。";
1747
+ }
1731
1748
  }
1732
- else {
1733
- // 核心无可见文本且 deliver 中未处理过媒体。
1734
- //
1735
- // 不使用错误提示,因为 agent 可能已通过内置 message 工具直接调用
1736
- // outbound.sendMedia 成功发送了文件——该路径完全绕过 monitor 的 deliver
1737
- // 回调,所以 state.deliverCalled / state.hasMedia 均为 false,但文件
1738
- // 实际已送达用户。此时显示 "未生成回复" 会误导用户。
1739
- finishText = "✅ 处理完成。";
1749
+ // if (!finishText) {
1750
+ // finishText = "✅ 处理完成。";
1751
+ // }
1752
+ if (finishText) {
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
+ }
1740
1775
  }
1741
- await sendWeComReply({ wsClient, frame, text: finishText, runtime, finish: true, streamId: state.streamId });
1742
1776
  }
1743
1777
  /**
1744
1778
  * 路由消息到核心处理流程并处理回复
@@ -1755,17 +1789,43 @@ async function routeAndDispatchMessage(params) {
1755
1789
  onCleanup();
1756
1790
  }
1757
1791
  };
1792
+ let isShowThink = !(account.sendThinkingMessage ?? true);
1758
1793
  try {
1759
1794
  await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
1760
1795
  ctx: ctxPayload,
1761
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
+ // },
1762
1811
  dispatcherOptions: {
1812
+ onReplyStart: async () => {
1813
+ if (!isShowThink && state.streamId && !state.accumulatedText) {
1814
+ try {
1815
+ await sendThinkingReply({ wsClient, frame, streamId: state.streamId, runtime, state });
1816
+ }
1817
+ catch (e) {
1818
+ runtime.error?.(`[wecom] sendThinkingReply threw err: ${String(e)}`);
1819
+ }
1820
+ isShowThink = true;
1821
+ }
1822
+ },
1763
1823
  deliver: async (payload, info) => {
1764
1824
  state.deliverCalled = true;
1765
1825
  // runtime.log?.(`[openclaw -> plugin] kind=${info.kind}, text=${payload.text ?? ''}, mediaUrl=${payload.mediaUrl ?? ''}, mediaUrls=${JSON.stringify(payload.mediaUrls ?? [])}`);
1766
1826
  // 累积文本
1767
1827
  if (payload.text) {
1768
- accumulateText(state, payload.text);
1828
+ state.accumulatedText += (payload.text || '');
1769
1829
  }
1770
1830
  // 发送媒体(统一走主动发送)
1771
1831
  const mediaUrls = payload.mediaUrls?.length ? payload.mediaUrls : payload.mediaUrl ? [payload.mediaUrl] : [];
@@ -1785,9 +1845,20 @@ async function routeAndDispatchMessage(params) {
1785
1845
  runtime.error?.(`[wecom] sendMediaBatch threw: ${errMsg}`);
1786
1846
  }
1787
1847
  }
1788
- // 中间帧:有可见文本时流式更新
1789
- if (info.kind !== "final" && state.hasText && state.accumulatedText) {
1790
- await sendWeComReply({ wsClient, frame, text: state.accumulatedText, runtime, finish: false, streamId: state.streamId });
1848
+ // 中间帧:有可见文本时流式更新(流式过期后跳过,等 deliver 完成后主动发送)
1849
+ if (info.kind !== "final" && state.hasText && 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
+ }
1791
1862
  }
1792
1863
  },
1793
1864
  onError: (err, info) => {
@@ -1903,16 +1974,16 @@ async function processWeComMessage(params) {
1903
1974
  const cleanupState = () => {
1904
1975
  deleteMessageState(messageId);
1905
1976
  };
1906
- // Step 6: 发送"思考中"消息
1907
- const shouldSendThinking = account.sendThinkingMessage ?? true;
1908
- if (shouldSendThinking) {
1909
- await sendThinkingReply({ wsClient, frame, streamId, runtime });
1910
- }
1977
+ // // Step 6: 发送"思考中"消息
1978
+ // const shouldSendThinking = account.sendThinkingMessage ?? true;
1979
+ // if (shouldSendThinking) {
1980
+ // await sendThinkingReply({ wsClient, frame, streamId, runtime });
1981
+ // }
1911
1982
  // Step 7: 构建上下文并路由到核心处理流程(带整体超时保护)
1912
1983
  const ctxPayload = buildMessageContext(frame, account, config, text, mediaList, quoteContent);
1913
1984
  // runtime.log?.(`[plugin -> openclaw] body=${text}, mediaPaths=${JSON.stringify(mediaList.map(m => m.path))}${quoteContent ? `, quote=${quoteContent}` : ''}`);
1914
1985
  try {
1915
- await withTimeout(routeAndDispatchMessage({
1986
+ await routeAndDispatchMessage({
1916
1987
  ctxPayload,
1917
1988
  config,
1918
1989
  account,
@@ -1921,26 +1992,10 @@ async function processWeComMessage(params) {
1921
1992
  state,
1922
1993
  runtime,
1923
1994
  onCleanup: cleanupState,
1924
- }), MESSAGE_PROCESS_TIMEOUT_MS, `Message processing timed out (msgId=${messageId})`);
1995
+ });
1925
1996
  }
1926
1997
  catch (err) {
1927
- runtime.error?.(`[wecom][plugin] Message processing failed or timed out: ${String(err)}`);
1928
- // 确保 thinking 流被关闭,防止异常/超时时 thinking 消息一直残留
1929
- try {
1930
- if (shouldSendThinking) {
1931
- await sendWeComReply({
1932
- wsClient,
1933
- frame,
1934
- text: "处理消息时出现异常,请稍后重试。",
1935
- runtime,
1936
- finish: true,
1937
- streamId: state.streamId,
1938
- });
1939
- }
1940
- }
1941
- catch (finishErr) {
1942
- runtime.error?.(`[wecom] Failed to finish thinking stream on error: ${String(finishErr)}`);
1943
- }
1998
+ runtime.error?.(`[wecom][plugin] Message processing failed: ${String(err)}`);
1944
1999
  cleanupState();
1945
2000
  }
1946
2001
  }