@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 +98 -14
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +98 -14
- package/dist/index.esm.js.map +1 -1
- package/dist/src/interface.d.ts +2 -2
- package/dist/src/message-sender.d.ts +11 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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)}
|
|
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
|
-
|
|
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.
|
|
1777
|
-
|
|
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
|
|
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
|
-
})
|
|
1995
|
+
});
|
|
1912
1996
|
}
|
|
1913
1997
|
catch (err) {
|
|
1914
|
-
runtime.error?.(`[wecom][plugin] Message processing failed
|
|
1998
|
+
runtime.error?.(`[wecom][plugin] Message processing failed: ${String(err)}`);
|
|
1915
1999
|
cleanupState();
|
|
1916
2000
|
}
|
|
1917
2001
|
}
|