@wecom/wecom-openclaw-plugin 2026.3.20-beta.4 → 2026.3.20-beta.7
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 +8 -0
- package/dist/index.cjs.js +93 -11
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +94 -12
- package/dist/index.esm.js.map +1 -1
- package/dist/src/const.d.ts +4 -2
- package/dist/src/interface.d.ts +2 -0
- package/package.json +2 -2
- package/skills/wecom-msg/SKILL.md +15 -10
- package/skills/wecom-msg/references/api-get-msg-media.md +1 -1
- package/skills/wecom-send-media/SKILL.md +9 -8
package/README.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
> 💡 **快速上手指引 & 交流群**
|
|
2
|
+
>
|
|
3
|
+
> 📖 [点击查看完整接入指引文档](https://doc.weixin.qq.com/doc/w3_AFYA1wY6ACoCNRxfnyGRJQaSa6jjJ?scode=AJEAIQdfAAo0RJmzxLAFYA1wY6ACo) — 包含配置步骤、产品介绍、常见问题解答等。
|
|
4
|
+
>
|
|
5
|
+
> 💬 扫码加入企业微信交流群:
|
|
6
|
+
>
|
|
7
|
+
> <img src="https://wwcdn.weixin.qq.com/node/wework/images/202603181801.a77d987fb8.png" alt="扫码入群交流" width="200" />
|
|
8
|
+
|
|
1
9
|
# 🤖 WeCom OpenClaw Plugin
|
|
2
10
|
|
|
3
11
|
**WeCom channel plugin for [OpenClaw](https://github.com/openclaw)** — by the Tencent WeCom team.
|
package/dist/index.cjs.js
CHANGED
|
@@ -386,8 +386,10 @@ const REPLY_SEND_TIMEOUT_MS = 15000;
|
|
|
386
386
|
const MESSAGE_PROCESS_TIMEOUT_MS = 5 * 60 * 1000;
|
|
387
387
|
/** WebSocket 心跳间隔(毫秒) */
|
|
388
388
|
const WS_HEARTBEAT_INTERVAL_MS = 30000;
|
|
389
|
-
/** WebSocket
|
|
390
|
-
const WS_MAX_RECONNECT_ATTEMPTS =
|
|
389
|
+
/** WebSocket 连接断开时的最大重连次数 */
|
|
390
|
+
const WS_MAX_RECONNECT_ATTEMPTS = 10;
|
|
391
|
+
/** WebSocket 认证失败时的最大重试次数 */
|
|
392
|
+
const WS_MAX_AUTH_FAILURE_ATTEMPTS = 5;
|
|
391
393
|
// ============================================================================
|
|
392
394
|
// 消息状态管理配置
|
|
393
395
|
// ============================================================================
|
|
@@ -2095,8 +2097,8 @@ function createSdkLogger(runtime, accountId) {
|
|
|
2095
2097
|
* 使用 aibot-node-sdk 简化连接管理
|
|
2096
2098
|
*/
|
|
2097
2099
|
async function monitorWeComProvider(options) {
|
|
2098
|
-
const { account, config, runtime, abortSignal } = options;
|
|
2099
|
-
runtime.log?.(`[${account.accountId}] Initializing WSClient with SDK...`);
|
|
2100
|
+
const { account, config, runtime, abortSignal, setStatus } = options;
|
|
2101
|
+
runtime.log?.(`[${account.accountId}] [${PLUGIN_VERSION}] Initializing WSClient with SDK...`);
|
|
2100
2102
|
// 启动消息状态定期清理
|
|
2101
2103
|
startMessageStateCleanup();
|
|
2102
2104
|
return new Promise((resolve, reject) => {
|
|
@@ -2108,18 +2110,26 @@ async function monitorWeComProvider(options) {
|
|
|
2108
2110
|
logger,
|
|
2109
2111
|
heartbeatInterval: WS_HEARTBEAT_INTERVAL_MS,
|
|
2110
2112
|
maxReconnectAttempts: WS_MAX_RECONNECT_ATTEMPTS,
|
|
2113
|
+
maxAuthFailureAttempts: WS_MAX_AUTH_FAILURE_ATTEMPTS,
|
|
2111
2114
|
scene: SCENE_WECOM_OPENCLAW,
|
|
2112
2115
|
plug_version: PLUGIN_VERSION,
|
|
2113
2116
|
});
|
|
2114
|
-
//
|
|
2117
|
+
// 防止 cleanup 被多次调用(abort handler、error handler、disconnected_event 可能竞态触发)
|
|
2118
|
+
let cleanedUp = false;
|
|
2119
|
+
// 清理函数:确保所有资源被释放(幂等)
|
|
2115
2120
|
const cleanup = async () => {
|
|
2121
|
+
if (cleanedUp)
|
|
2122
|
+
return;
|
|
2123
|
+
cleanedUp = true;
|
|
2116
2124
|
stopMessageStateCleanup();
|
|
2117
2125
|
await cleanupAccount(account.accountId);
|
|
2118
2126
|
};
|
|
2119
|
-
//
|
|
2127
|
+
// 处理中止信号(框架 stopChannel 会触发 abort)
|
|
2128
|
+
// resolve() 让 Promise settle → 框架清理 store.tasks/store.aborts
|
|
2120
2129
|
if (abortSignal) {
|
|
2121
2130
|
abortSignal.addEventListener("abort", async () => {
|
|
2122
2131
|
runtime.log?.(`[${account.accountId}] Connection aborted`);
|
|
2132
|
+
wsClient.disconnect();
|
|
2123
2133
|
await cleanup();
|
|
2124
2134
|
resolve();
|
|
2125
2135
|
});
|
|
@@ -2137,16 +2147,84 @@ async function monitorWeComProvider(options) {
|
|
|
2137
2147
|
wsClient.on("disconnected", (reason) => {
|
|
2138
2148
|
runtime.log?.(`[${account.accountId}] WebSocket disconnected: ${reason}`);
|
|
2139
2149
|
});
|
|
2150
|
+
// 监听被踢下线事件(服务端因新连接建立而主动断开旧连接)
|
|
2151
|
+
//
|
|
2152
|
+
// SDK 内部已设置 isManualClose=true 阻止 SDK 层自动重连,连接不会自行恢复。
|
|
2153
|
+
// **不 reject/resolve Promise**——保持 pending 以阻止框架层 auto-restart。
|
|
2154
|
+
//
|
|
2155
|
+
// 为什么不能 reject/resolve:
|
|
2156
|
+
// - reject → 框架 auto-restart 介入 → 新连接建立 → 又被踢 → 两个实例互踢无限循环
|
|
2157
|
+
// - resolve → 同上,框架 .then() 中的 auto-restart 也会触发
|
|
2158
|
+
//
|
|
2159
|
+
// Promise pending 的安全性:
|
|
2160
|
+
// - store.tasks.has(id) = true → 阻止 Health Monitor 直接 startChannel(startChannel 检查 tasks.has)
|
|
2161
|
+
// - 框架 stopChannel → abort() → abort handler 中 resolve() → tasks 正常清理
|
|
2162
|
+
// - 用户修改配置 → config reload → stopChannel + startChannel → 正常恢复
|
|
2163
|
+
//
|
|
2164
|
+
// 显式调用 wsClient.disconnect() 确保 SDK 内部资源(定时器、队列等)完全释放。
|
|
2165
|
+
wsClient.on("event.disconnected_event", async () => {
|
|
2166
|
+
const errorMsg = `Kicked by server: a new connection was established elsewhere. Auto-restart is suppressed to avoid mutual kicking. Please check for duplicate instances.`;
|
|
2167
|
+
runtime.error?.(`[${account.accountId}] ${errorMsg}`);
|
|
2168
|
+
wsClient.disconnect();
|
|
2169
|
+
await cleanup();
|
|
2170
|
+
setStatus?.({
|
|
2171
|
+
accountId: account.accountId,
|
|
2172
|
+
running: false,
|
|
2173
|
+
lastError: errorMsg,
|
|
2174
|
+
lastStopAt: Date.now(),
|
|
2175
|
+
});
|
|
2176
|
+
// Promise 保持 pending,不触发 auto-restart
|
|
2177
|
+
});
|
|
2140
2178
|
// 监听重连事件
|
|
2141
2179
|
wsClient.on("reconnecting", (attempt) => {
|
|
2142
2180
|
runtime.log?.(`[${account.accountId}] Reconnecting attempt ${attempt}...`);
|
|
2143
2181
|
});
|
|
2144
2182
|
// 监听错误事件
|
|
2145
|
-
wsClient.on("error", (error) => {
|
|
2183
|
+
wsClient.on("error", async (error) => {
|
|
2146
2184
|
runtime.error?.(`[${account.accountId}] WebSocket error: ${error.message}`);
|
|
2147
|
-
|
|
2148
|
-
|
|
2185
|
+
if (error instanceof aibotNodeSdk.WSAuthFailureError) {
|
|
2186
|
+
// 认证失败重试次数用尽(SDK 层已重试 WS_MAX_AUTH_FAILURE_ATTEMPTS 次)。
|
|
2187
|
+
// 配置错误(如 botId/secret 无效),框架 auto-restart 也无法恢复。
|
|
2188
|
+
//
|
|
2189
|
+
// **不 reject/resolve Promise**——保持 pending 以阻止框架层 auto-restart。
|
|
2190
|
+
//
|
|
2191
|
+
// 为什么不能 reject/resolve:
|
|
2192
|
+
// - reject/resolve → 框架 auto-restart(最多 10 次)× SDK 重试(5 次)= 60 次无意义尝试
|
|
2193
|
+
// - 且 Health Monitor 每小时还会 resetRestartAttempts 再来一轮
|
|
2194
|
+
//
|
|
2195
|
+
// Promise pending 的安全性:同被踢下线场景
|
|
2196
|
+
// - store.tasks.has(id) = true → 阻止 Health Monitor 直接 startChannel
|
|
2197
|
+
// - 框架 stopChannel / config reload → abort handler 中 resolve() → 正常清理
|
|
2198
|
+
// - 用户修改配置后框架通过 reload 机制重新启动
|
|
2199
|
+
const errorMsg = `Auth failure attempts exhausted (${WS_MAX_AUTH_FAILURE_ATTEMPTS} attempts). Please check botId/secret configuration.`;
|
|
2200
|
+
runtime.error?.(`[${account.accountId}] ${errorMsg}`);
|
|
2201
|
+
wsClient.disconnect();
|
|
2202
|
+
await cleanup();
|
|
2203
|
+
setStatus?.({
|
|
2204
|
+
accountId: account.accountId,
|
|
2205
|
+
running: false,
|
|
2206
|
+
lastError: errorMsg,
|
|
2207
|
+
lastStopAt: Date.now(),
|
|
2208
|
+
});
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
if (error instanceof aibotNodeSdk.WSReconnectExhaustedError) {
|
|
2212
|
+
// 网络断线重连次数用尽(SDK 层已重试 WS_MAX_RECONNECT_ATTEMPTS 次)。
|
|
2213
|
+
// 通常是网络/服务端问题,框架 auto-restart 可能恢复。
|
|
2214
|
+
//
|
|
2215
|
+
// reject Promise → 框架 auto-restart 介入(最多 MAX_RESTART_ATTEMPTS=10 次)
|
|
2216
|
+
// 总连接尝试次数 = (1 首次 + WS_MAX_RECONNECT_ATTEMPTS 重连) × (1 首轮 + 10 auto-restart)
|
|
2217
|
+
// = 11 × 11 = 121 次
|
|
2218
|
+
//
|
|
2219
|
+
// 如果 Health Monitor 介入(每 5 分钟检查),会 resetRestartAttempts 重新计数,
|
|
2220
|
+
// 受限于 DEFAULT_MAX_RESTARTS_PER_HOUR=10,每小时最多额外 10 × 121 = 1210 次。
|
|
2221
|
+
// 但因网络断线通常是暂时性的,auto-restart + Health Monitor 的兜底机制是合理的。
|
|
2222
|
+
//
|
|
2223
|
+
// 显式调用 wsClient.disconnect() 确保 SDK 内部资源完全释放,
|
|
2224
|
+
// 避免旧实例的定时器/队列残留。
|
|
2225
|
+
wsClient.disconnect();
|
|
2149
2226
|
cleanup().finally(() => reject(error));
|
|
2227
|
+
return;
|
|
2150
2228
|
}
|
|
2151
2229
|
});
|
|
2152
2230
|
// 监听版本检查事件:收到 enter_check_update 时回复当前插件版本
|
|
@@ -2639,6 +2717,7 @@ const wecomPlugin = {
|
|
|
2639
2717
|
config: ctx.cfg,
|
|
2640
2718
|
runtime: ctx.runtime,
|
|
2641
2719
|
abortSignal: ctx.abortSignal,
|
|
2720
|
+
setStatus: ctx.setStatus,
|
|
2642
2721
|
});
|
|
2643
2722
|
},
|
|
2644
2723
|
logoutAccount: async ({ cfg }) => {
|
|
@@ -3319,8 +3398,12 @@ async function interceptMediaResponse(result) {
|
|
|
3319
3398
|
// 5. 检测 contentType,并通过 saveMediaBuffer 保存到本地媒体目录
|
|
3320
3399
|
const contentType = await detectMime({ buffer, filePath: mediaName }) ?? "application/octet-stream";
|
|
3321
3400
|
const tMimeDetected = performance.now();
|
|
3401
|
+
// 企业微信聊天记录附件可达 20MB(文件消息上限),
|
|
3402
|
+
// 而 saveMediaBuffer 默认 maxBytes 为 5MB(针对 outbound 场景),
|
|
3403
|
+
// 此处显式放宽到 20MB 以支持大文件下载。
|
|
3404
|
+
const INBOUND_MAX_BYTES = 20 * 1024 * 1024; // 20MB
|
|
3322
3405
|
const core = getWeComRuntime();
|
|
3323
|
-
const saved = await core.channel.media.saveMediaBuffer(buffer, contentType, "inbound",
|
|
3406
|
+
const saved = await core.channel.media.saveMediaBuffer(buffer, contentType, "inbound", INBOUND_MAX_BYTES, // maxBytes: 放宽到 20MB,匹配企业微信文件消息上限
|
|
3324
3407
|
mediaName);
|
|
3325
3408
|
// 5.1 补偿:核心库 EXT_BY_MIME 可能缺少某些格式映射(如 audio/amr),
|
|
3326
3409
|
// 导致保存的文件没有后缀。此处检测并修复。
|
|
@@ -3486,7 +3569,6 @@ function createWeComMcpTool() {
|
|
|
3486
3569
|
};
|
|
3487
3570
|
}
|
|
3488
3571
|
|
|
3489
|
-
console.log(`[wecom] v${PLUGIN_VERSION} loaded`);
|
|
3490
3572
|
const plugin = {
|
|
3491
3573
|
id: "wecom-openclaw-plugin",
|
|
3492
3574
|
name: "企业微信",
|