@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/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 || (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
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
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
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
//
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
})
|
|
1995
|
+
});
|
|
1925
1996
|
}
|
|
1926
1997
|
catch (err) {
|
|
1927
|
-
runtime.error?.(`[wecom][plugin] Message processing failed
|
|
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
|
}
|