@wecom/wecom-openclaw-plugin 2026.3.25-beta.0 → 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.cjs.js +97 -13
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +97 -13
- package/dist/index.esm.js.map +1 -1
- package/dist/src/interface.d.ts +2 -0
- package/dist/src/message-sender.d.ts +11 -0
- package/package.json +1 -1
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
|
-
|
|
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 || (typeof err === "object" ? JSON.stringify(err) : 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
|
-
|
|
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
|
/**
|
|
@@ -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
|
-
|
|
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
|
-
|
|
1875
|
+
// 中间帧:有可见文本时流式更新(流式过期后跳过,等 deliver 完成后主动发送)
|
|
1876
|
+
if (info.kind !== "final" && state.hasText && 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
|
|
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
|
-
})
|
|
2022
|
+
});
|
|
1939
2023
|
}
|
|
1940
2024
|
catch (err) {
|
|
1941
|
-
runtime.error?.(`[wecom][plugin] Message processing failed
|
|
2025
|
+
runtime.error?.(`[wecom][plugin] Message processing failed: ${String(err)}`);
|
|
1942
2026
|
cleanupState();
|
|
1943
2027
|
}
|
|
1944
2028
|
}
|