@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/README.md +10 -1
- package/dist/index.cjs.js +127 -72
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +127 -72
- 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/README.md
CHANGED
|
@@ -4,7 +4,16 @@
|
|
|
4
4
|
>
|
|
5
5
|
> 💬 扫码加入企业微信交流群:
|
|
6
6
|
>
|
|
7
|
-
> <img src="https://wwcdn.weixin.qq.com/node/wework/images/
|
|
7
|
+
> <img src="https://wwcdn.weixin.qq.com/node/wework/images/202603241759.3fb01c32cc.png" alt="扫码入群交流" width="200" />
|
|
8
|
+
|
|
9
|
+
# 特别说明
|
|
10
|
+
> ****2026.3.22 版本 OpenClaw 兼容说明****
|
|
11
|
+
>
|
|
12
|
+
> 如果你的 OpenClaw 是 2026.3.22 及以上的版本,请升级插件到 2026.3.24 及以上版本。
|
|
13
|
+
>
|
|
14
|
+
> 如果你的 OpenClaw 是 2026.3.22 以下的版本,请保持插件版本在 2026.3.20 版本。
|
|
15
|
+
>
|
|
16
|
+
> 你可以使用以下命令快速安装: `npx -y @wecom/wecom-openclaw-cli install --force`
|
|
8
17
|
|
|
9
18
|
# 🤖 WeCom OpenClaw Plugin
|
|
10
19
|
|
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
|
}
|
|
@@ -1525,14 +1551,6 @@ const PLUGIN_VERSION = getVersion();
|
|
|
1525
1551
|
* - state-manager.ts : 全局状态管理(带 TTL 清理)
|
|
1526
1552
|
* - timeout.ts : 超时工具
|
|
1527
1553
|
*/
|
|
1528
|
-
/**
|
|
1529
|
-
* 去除文本中的 `<think>...</think>` 标签(支持跨行),返回剩余可见文本。
|
|
1530
|
-
* 用于判断大模型回复中是否包含实际用户可见内容(而非仅有 thinking 推理过程)。
|
|
1531
|
-
*/
|
|
1532
|
-
function stripThinkTags(text) {
|
|
1533
|
-
return text;
|
|
1534
|
-
// return text.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
|
|
1535
|
-
}
|
|
1536
1554
|
// ============================================================================
|
|
1537
1555
|
// 媒体本地路径白名单扩展
|
|
1538
1556
|
// ============================================================================
|
|
@@ -1663,7 +1681,7 @@ function buildMessageContext(frame, account, config, text, mediaList, quoteConte
|
|
|
1663
1681
|
* 发送"思考中"消息
|
|
1664
1682
|
*/
|
|
1665
1683
|
async function sendThinkingReply(params) {
|
|
1666
|
-
const { wsClient, frame, streamId, runtime } = params;
|
|
1684
|
+
const { wsClient, frame, streamId, runtime, state } = params;
|
|
1667
1685
|
try {
|
|
1668
1686
|
await sendWeComReply({
|
|
1669
1687
|
wsClient,
|
|
@@ -1675,16 +1693,13 @@ async function sendThinkingReply(params) {
|
|
|
1675
1693
|
});
|
|
1676
1694
|
}
|
|
1677
1695
|
catch (err) {
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
state.accumulatedText += text;
|
|
1686
|
-
if (!state.hasText && stripThinkTags(state.accumulatedText)) {
|
|
1687
|
-
state.hasText = true;
|
|
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
|
+
}
|
|
1688
1703
|
}
|
|
1689
1704
|
}
|
|
1690
1705
|
/**
|
|
@@ -1739,33 +1754,52 @@ async function sendMediaBatch(ctx, mediaUrls) {
|
|
|
1739
1754
|
* (agent 可能已通过内置 message 工具直接发送了文件,
|
|
1740
1755
|
* 该路径走 outbound.sendMedia 完全绕过 deliver 回调,
|
|
1741
1756
|
* 所以 state 中无记录,但文件已实际送达)
|
|
1757
|
+
*
|
|
1758
|
+
* 降级策略:
|
|
1759
|
+
* - 当 streamExpired=true(errcode 846608)时,流式通道已不可用(>6分钟),
|
|
1760
|
+
* 改用 wsClient.sendMessage 主动发送完整文本。
|
|
1742
1761
|
*/
|
|
1743
1762
|
async function finishThinkingStream(ctx) {
|
|
1744
1763
|
const { wsClient, frame, state, runtime } = ctx;
|
|
1745
|
-
const
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
// 媒体发送失败:直接用错误摘要替换 thinking 流(不再额外发 sendMessage)
|
|
1757
|
-
finishText = state.mediaErrorSummary;
|
|
1764
|
+
const body = frame.body;
|
|
1765
|
+
const chatId = body.chatid || body.from.userid;
|
|
1766
|
+
let finishText = state.accumulatedText;
|
|
1767
|
+
if (state.hasMedia) {
|
|
1768
|
+
if (state.hasMediaFailed && state.mediaErrorSummary) {
|
|
1769
|
+
// 媒体成功发送:用友好提示告知用户
|
|
1770
|
+
finishText = finishText ? `${finishText}\n\n${state.mediaErrorSummary}` : state.mediaErrorSummary;
|
|
1771
|
+
}
|
|
1772
|
+
else if (!finishText) {
|
|
1773
|
+
finishText = "📎 文件已发送,请查收。";
|
|
1774
|
+
}
|
|
1758
1775
|
}
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
//
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1776
|
+
// if (!finishText) {
|
|
1777
|
+
// finishText = "✅ 处理完成。";
|
|
1778
|
+
// }
|
|
1779
|
+
if (finishText) {
|
|
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
|
+
}
|
|
1767
1802
|
}
|
|
1768
|
-
await sendWeComReply({ wsClient, frame, text: finishText, runtime, finish: true, streamId: state.streamId });
|
|
1769
1803
|
}
|
|
1770
1804
|
/**
|
|
1771
1805
|
* 路由消息到核心处理流程并处理回复
|
|
@@ -1782,17 +1816,43 @@ async function routeAndDispatchMessage(params) {
|
|
|
1782
1816
|
onCleanup();
|
|
1783
1817
|
}
|
|
1784
1818
|
};
|
|
1819
|
+
let isShowThink = !(account.sendThinkingMessage ?? true);
|
|
1785
1820
|
try {
|
|
1786
1821
|
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
1787
1822
|
ctx: ctxPayload,
|
|
1788
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
|
+
// },
|
|
1789
1838
|
dispatcherOptions: {
|
|
1839
|
+
onReplyStart: async () => {
|
|
1840
|
+
if (!isShowThink && state.streamId && !state.accumulatedText) {
|
|
1841
|
+
try {
|
|
1842
|
+
await sendThinkingReply({ wsClient, frame, streamId: state.streamId, runtime, state });
|
|
1843
|
+
}
|
|
1844
|
+
catch (e) {
|
|
1845
|
+
runtime.error?.(`[wecom] sendThinkingReply threw err: ${String(e)}`);
|
|
1846
|
+
}
|
|
1847
|
+
isShowThink = true;
|
|
1848
|
+
}
|
|
1849
|
+
},
|
|
1790
1850
|
deliver: async (payload, info) => {
|
|
1791
1851
|
state.deliverCalled = true;
|
|
1792
1852
|
// runtime.log?.(`[openclaw -> plugin] kind=${info.kind}, text=${payload.text ?? ''}, mediaUrl=${payload.mediaUrl ?? ''}, mediaUrls=${JSON.stringify(payload.mediaUrls ?? [])}`);
|
|
1793
1853
|
// 累积文本
|
|
1794
1854
|
if (payload.text) {
|
|
1795
|
-
|
|
1855
|
+
state.accumulatedText += (payload.text || '');
|
|
1796
1856
|
}
|
|
1797
1857
|
// 发送媒体(统一走主动发送)
|
|
1798
1858
|
const mediaUrls = payload.mediaUrls?.length ? payload.mediaUrls : payload.mediaUrl ? [payload.mediaUrl] : [];
|
|
@@ -1812,9 +1872,20 @@ async function routeAndDispatchMessage(params) {
|
|
|
1812
1872
|
runtime.error?.(`[wecom] sendMediaBatch threw: ${errMsg}`);
|
|
1813
1873
|
}
|
|
1814
1874
|
}
|
|
1815
|
-
//
|
|
1816
|
-
if (info.kind !== "final" && state.hasText && state.accumulatedText) {
|
|
1817
|
-
|
|
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
|
+
}
|
|
1818
1889
|
}
|
|
1819
1890
|
},
|
|
1820
1891
|
onError: (err, info) => {
|
|
@@ -1930,16 +2001,16 @@ async function processWeComMessage(params) {
|
|
|
1930
2001
|
const cleanupState = () => {
|
|
1931
2002
|
deleteMessageState(messageId);
|
|
1932
2003
|
};
|
|
1933
|
-
// Step 6: 发送"思考中"消息
|
|
1934
|
-
const shouldSendThinking = account.sendThinkingMessage ?? true;
|
|
1935
|
-
if (shouldSendThinking) {
|
|
1936
|
-
|
|
1937
|
-
}
|
|
2004
|
+
// // Step 6: 发送"思考中"消息
|
|
2005
|
+
// const shouldSendThinking = account.sendThinkingMessage ?? true;
|
|
2006
|
+
// if (shouldSendThinking) {
|
|
2007
|
+
// await sendThinkingReply({ wsClient, frame, streamId, runtime });
|
|
2008
|
+
// }
|
|
1938
2009
|
// Step 7: 构建上下文并路由到核心处理流程(带整体超时保护)
|
|
1939
2010
|
const ctxPayload = buildMessageContext(frame, account, config, text, mediaList, quoteContent);
|
|
1940
2011
|
// runtime.log?.(`[plugin -> openclaw] body=${text}, mediaPaths=${JSON.stringify(mediaList.map(m => m.path))}${quoteContent ? `, quote=${quoteContent}` : ''}`);
|
|
1941
2012
|
try {
|
|
1942
|
-
await
|
|
2013
|
+
await routeAndDispatchMessage({
|
|
1943
2014
|
ctxPayload,
|
|
1944
2015
|
config,
|
|
1945
2016
|
account,
|
|
@@ -1948,26 +2019,10 @@ async function processWeComMessage(params) {
|
|
|
1948
2019
|
state,
|
|
1949
2020
|
runtime,
|
|
1950
2021
|
onCleanup: cleanupState,
|
|
1951
|
-
})
|
|
2022
|
+
});
|
|
1952
2023
|
}
|
|
1953
2024
|
catch (err) {
|
|
1954
|
-
runtime.error?.(`[wecom][plugin] Message processing failed
|
|
1955
|
-
// 确保 thinking 流被关闭,防止异常/超时时 thinking 消息一直残留
|
|
1956
|
-
try {
|
|
1957
|
-
if (shouldSendThinking) {
|
|
1958
|
-
await sendWeComReply({
|
|
1959
|
-
wsClient,
|
|
1960
|
-
frame,
|
|
1961
|
-
text: "处理消息时出现异常,请稍后重试。",
|
|
1962
|
-
runtime,
|
|
1963
|
-
finish: true,
|
|
1964
|
-
streamId: state.streamId,
|
|
1965
|
-
});
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
catch (finishErr) {
|
|
1969
|
-
runtime.error?.(`[wecom] Failed to finish thinking stream on error: ${String(finishErr)}`);
|
|
1970
|
-
}
|
|
2025
|
+
runtime.error?.(`[wecom][plugin] Message processing failed: ${String(err)}`);
|
|
1971
2026
|
cleanupState();
|
|
1972
2027
|
}
|
|
1973
2028
|
}
|