@yanhaidao/wecom 2.3.190 → 2.3.270
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/.github/workflows/release.yml +23 -4
- package/README.md +17 -6
- package/changelog/v2.3.26.md +21 -0
- package/changelog/v2.3.27.md +33 -0
- package/index.test.ts +5 -1
- package/package.json +17 -17
- package/src/agent/handler.ts +2 -0
- package/src/app/account-runtime.ts +5 -1
- package/src/app/index.ts +120 -1
- package/src/capability/mcp/tool.ts +7 -3
- package/src/channel.meta.test.ts +4 -0
- package/src/channel.ts +30 -60
- package/src/config/media.test.ts +1 -1
- package/src/config/media.ts +3 -5
- package/src/context-store.ts +264 -0
- package/src/onboarding.test.ts +42 -24
- package/src/onboarding.ts +598 -553
- package/src/outbound.test.ts +404 -2
- package/src/outbound.ts +96 -15
- package/src/runtime/dispatcher.ts +24 -5
- package/src/runtime/session-manager.test.ts +135 -0
- package/src/runtime/session-manager.ts +40 -8
- package/src/runtime/source-registry.ts +79 -0
- package/src/runtime.ts +3 -0
- package/src/target.ts +20 -8
- package/src/transport/bot-ws/media.test.ts +9 -9
- package/src/transport/bot-ws/media.ts +51 -2
- package/src/transport/bot-ws/reply.test.ts +1 -1
- package/src/transport/bot-ws/reply.ts +8 -3
- package/src/transport/bot-ws/sdk-adapter.ts +6 -6
- package/src/transport/http/registry.ts +1 -1
- package/src/types/runtime.ts +1 -0
- package/src/wecom_msg_adapter/markdown_adapter.ts +331 -0
|
@@ -96,14 +96,33 @@ jobs:
|
|
|
96
96
|
id: notes
|
|
97
97
|
run: |
|
|
98
98
|
set -euo pipefail
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
VERSION="${{ steps.meta.outputs.version }}"
|
|
100
|
+
CHANGELOG_DIR="changelog"
|
|
101
|
+
CHANGELOG_FILE=""
|
|
102
|
+
CANDIDATES=("v${VERSION}.md")
|
|
103
|
+
|
|
104
|
+
IFS='.' read -r major minor patch <<< "${VERSION}"
|
|
105
|
+
if [[ -n "${major:-}" && -n "${minor:-}" && -n "${patch:-}" && "${patch}" =~ ^[0-9]+$ && ${#patch} -gt 2 ]]; then
|
|
106
|
+
trimmed_patch="${patch%?}"
|
|
107
|
+
CANDIDATES+=("v${major}.${minor}.${trimmed_patch}.md")
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
echo "Release notes candidates:"
|
|
111
|
+
for candidate in "${CANDIDATES[@]}"; do
|
|
112
|
+
path="${CHANGELOG_DIR}/${candidate}"
|
|
113
|
+
echo " - ${path}"
|
|
114
|
+
if [[ -z "${CHANGELOG_FILE}" && -f "${path}" ]]; then
|
|
115
|
+
CHANGELOG_FILE="${path}"
|
|
116
|
+
fi
|
|
117
|
+
done
|
|
118
|
+
|
|
119
|
+
if [[ -n "${CHANGELOG_FILE}" ]]; then
|
|
101
120
|
echo "Using changelog file: ${CHANGELOG_FILE}"
|
|
102
121
|
cp "${CHANGELOG_FILE}" /tmp/release-body.md
|
|
103
122
|
else
|
|
104
|
-
echo "
|
|
123
|
+
echo "No matching changelog file found; using fallback notes."
|
|
105
124
|
{
|
|
106
|
-
echo "# @yanhaidao/wecom v${
|
|
125
|
+
echo "# @yanhaidao/wecom v${VERSION}"
|
|
107
126
|
echo
|
|
108
127
|
if [ "${{ steps.npm_check.outputs.exists }}" = "true" ]; then
|
|
109
128
|
echo "- npm publish skipped: version already exists."
|
package/README.md
CHANGED
|
@@ -161,7 +161,18 @@
|
|
|
161
161
|
## 📋 最近更新 (Changelog摘要)
|
|
162
162
|
|
|
163
163
|
> 项目保持高频迭代,全面对齐甚至超越企业真实业务诉求。
|
|
164
|
-
> **为保持精简,以下仅展示近期
|
|
164
|
+
> **为保持精简,以下仅展示近期 5 次重要更新,完整历史版本(含全部 `v2.2.x`)请前往 [changelog/ 目录](./changelog/) 查阅。**
|
|
165
|
+
|
|
166
|
+
#### 📌 v2.3.27(2026-03-27)
|
|
167
|
+
- **[重要修复] `channel add` 重新支持 WeCom guided setup** 🧭 之前有些环境下,`wecom` 虽然已经安装,却仍会在 OpenClaw 里显示成 “does not support guided setup yet”,导致无法直接通过交互式向导添加。现在插件已经对齐 OpenClaw 当前的 `setupWizard` 接口,`openclaw channels add` 会重新正常识别和进入配置流程。
|
|
168
|
+
- **[重要修复] 修复 `installedCatalogById is not defined`** 🔧 部分用户在渠道添加或选择阶段会直接遇到 `ReferenceError: installedCatalogById is not defined`,表现上像是“选了渠道就报错”或“添加流程突然失效”。这一版已经修复对应的目录访问逻辑,添加流程恢复稳定。
|
|
169
|
+
- **[升级兼容] 清理 OpenClaw 新版下失效的 SDK 旧入口** 📦 这次同步迁移了 `wecom` 插件里几处已经不再建议继续从 `openclaw/plugin-sdk` 根入口直接拿的旧接口,重点覆盖工具上下文、outbound 适配器和 Bot WS 媒体发送链路,升级 OpenClaw 后更不容易再出现“有的地方能跑、有的地方直接炸”的兼容问题。
|
|
170
|
+
|
|
171
|
+
#### 📌 v2.3.26(2026-03-26)
|
|
172
|
+
- **[重要修复] 升级 OpenClaw 后不再乱报错** 🔧 修复了新版 OpenClaw 下 `wecom` 插件容易出现的 `is not a function` 一类启动/运行错误。
|
|
173
|
+
- **[回复更稳] Agent 和 Bot WS 不再乱串** ↔️ 现在是谁收到消息,就尽量由谁来回复,不再容易出现“在 Agent 里说话,结果 Bot WS 回你”的情况。
|
|
174
|
+
- **[体验修复] Bot WS 发图后不再多冒一条 `Done...`** 🖼 之前常见表现是:`正在思考` -> 图片 -> 又多一条完成提示。现在最终收尾会尽量接回原来的回复链路。
|
|
175
|
+
- **[占位符修复] 不会一直卡在“正在思考...”** ⏳ 如果图片或文本已经发出去了,占位符会更自然地结束,不会继续无意义地刷屏。
|
|
165
176
|
|
|
166
177
|
#### 📌 v2.3.19(2026-03-19)
|
|
167
178
|
- **[重要修复] Bot WS 现在也真正走 `dynamicAgents`** 🧭 之前同样开启动态路由时,不同消息链路的行为并不完全一致:Webhook / Agent 能按用户、群聊隔离,Bot WebSocket 却可能重新落回主 Agent。现在 WS 运行时也执行同样的动态路由逻辑,会话隔离终于统一了。
|
|
@@ -176,10 +187,6 @@
|
|
|
176
187
|
#### 📌 v2.3.16(2026-03-16)
|
|
177
188
|
- **[解析增强] 混合消息媒体正确接管** 🛠 重点修复在 `Bot WS` 通道下,用户如果发了“一张截图 + 一段文字指示”,以前容易丢掉截图或者 AI 只能看到无法查看的腾讯云临时链接。新版底层引擎将自动扫过所有的媒体节点摘取 URL 与解密 AES Key,还大模型一双慧眼。
|
|
178
189
|
|
|
179
|
-
#### 📌 v2.3.15(2026-03-14)
|
|
180
|
-
- **[原生资产稳定写入]** 📄 深度强化大模型创建企微相关原生存根文档/表格时的写入稳定性。执行 `init_content` 有了更强的前置图片上传清洗能力;极大程度杜绝了混发段落/文本/图片触发的企微官方 `Validator` 异常。
|
|
181
|
-
- **[复杂分发目标解包]** 💬 补齐所有关于企业微信“群聊、部门、标签组”作为 `To` 目标的精确指令解析算法,保障向全公司的组织架构层级呼气 AI 报告不再报出 `81013 target invalid`。
|
|
182
|
-
|
|
183
190
|
*(查看更早期关于“超时熔断代投、动态扩容矩阵”等功能的更新日志,请移步 [changelog/ 目录](./changelog/))*
|
|
184
191
|
|
|
185
192
|
---
|
|
@@ -198,7 +205,7 @@ openclaw plugins enable wecom
|
|
|
198
205
|
|
|
199
206
|
### 1.2 互动向导式初配 (适合个人开发者与极客)
|
|
200
207
|
|
|
201
|
-
如果您不想手写繁杂的 JSON 配置文件,可以通过交互式向导快速完成最轻量的 WebSocket
|
|
208
|
+
如果您不想手写繁杂的 JSON 配置文件,可以通过交互式向导快速完成最轻量的 WebSocket 长连接部署。`v2.3.27` 起,`wecom` 已重新对齐 OpenClaw 当前的 guided setup 流程,`openclaw channels add` 可以直接识别并进入配置:
|
|
202
209
|
|
|
203
210
|
1. 确保已启用本插件。
|
|
204
211
|
2. 在终端运行添加渠道指令:
|
|
@@ -208,6 +215,10 @@ openclaw plugins enable wecom
|
|
|
208
215
|
3. 选择下拉列表中第一顺位的:**企业微信 (WeCom)**
|
|
209
216
|
4. 根据终端亮色指引,填入企微机器人对应的 `Bot ID` 及 `Secret`,机器人即可完成握手并进入可用状态。
|
|
210
217
|
|
|
218
|
+
> **如果您最近刚升级 OpenClaw:**
|
|
219
|
+
> - 若之前在添加渠道时看到 `wecom does not support guided setup yet`,请更新到当前版本后重试。
|
|
220
|
+
> - 若之前在渠道添加阶段见过 `ReferenceError: installedCatalogById is not defined`,这一版也已一并修复。
|
|
221
|
+
|
|
211
222
|
### 1.3 生产环境顶配架构示范(Bot WS 流式交互 + Agent 私有通道兜底发送)
|
|
212
223
|
|
|
213
224
|
如果您的目标不是“接进来能聊两句”,而是让团队在企业微信里长期稳定使用 AI,这套组合更接近生产环境的推荐形态:
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# OpenClaw WeCom 插件 v2.3.26 变更简报
|
|
2
|
+
|
|
3
|
+
> [!TIP]
|
|
4
|
+
> 这一版主要就是把最近最烦人的几类问题修顺了:升级后不再乱报错,Agent 和 Bot WS 不再乱串回复,Bot WS 发图片后不再多冒一条 `Done...`,占位符也不会一直卡着不收。
|
|
5
|
+
|
|
6
|
+
## 2026-03-26(v2.3.26)
|
|
7
|
+
- 修复了 OpenClaw 升级后 `wecom` 插件报 `is not a function` 的问题。之前是 SDK 导入路径没跟着新版调整,这一版已经统一修好。
|
|
8
|
+
- 修复了 Agent 和 Bot WS 串回复的问题。现在是谁收到消息,就尽量由谁来回复,不再出现“在 Agent 里说话,结果 Bot WS 回你”这种情况。
|
|
9
|
+
- 修复了 Bot WS 会话里发图片后的收尾问题。之前常见表现是:`正在思考` -> 图片 -> 又多一条 `Done!...`。现在最终文字会尽量接回原来的回复链路,不再额外多发一条完成提示。
|
|
10
|
+
- 修复了 `message` 工具和 Bot WS 占位符不同步的问题。现在如果图片或文本已经发出去了,占位符不会还在那里一直刷“正在思考...”。
|
|
11
|
+
- 顺手加强了“全局安装的 OpenClaw + 本地 wecom 插件源码”这种混合运行方式下的稳定性,开发时不容易再出现一边生效、一边不生效的割裂情况。
|
|
12
|
+
|
|
13
|
+
## 升级后你会感受到
|
|
14
|
+
- 升级 OpenClaw 后,`wecom` 插件不再因为导入问题直接报错。
|
|
15
|
+
- Agent 和 Bot WS 同时开着时,回复会更稳地留在正确的对话里。
|
|
16
|
+
- Bot WS 发图、发文件后,回复看起来更像一条完整对话,不会再碎成好几段。
|
|
17
|
+
- 占位符会更自然地结束,不会一直停在“正在思考...”。
|
|
18
|
+
|
|
19
|
+
## 升级提示
|
|
20
|
+
- 执行 `openclaw plugins update wecom` 即可升级到 `v2.3.26`。
|
|
21
|
+
- 如果你是“全局 `openclaw` + 本地 `extensions/wecom` 源码”在跑,升级后记得完整重启 gateway。
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# OpenClaw WeCom 插件 v2.3.27 变更简报
|
|
2
|
+
|
|
3
|
+
> [!TIP]
|
|
4
|
+
> `v2.3.27` 是一个以“升级兼容”和“接入恢复”为主的修复版本。重点解决三类真实会挡住使用的问题:第一,`channel add` 里 `wecom` 又能正常被识别和添加了;第二,修复了部分用户在配置或选择渠道时遇到的 `ReferenceError: installedCatalogById is not defined`;第三,把这轮 OpenClaw 升级后 `wecom` 插件里几处已经失效的 SDK 旧入口统一迁到了当前可用的公共接口,避免出现“有的地方能跑、有的地方一升级就炸”的割裂状态。
|
|
5
|
+
|
|
6
|
+
## 2026-03-27(v2.3.27)
|
|
7
|
+
- 【恢复 `channel add` 接入】**[重要修复]** 之前在 OpenClaw 的渠道添加流程里,`wecom` 虽然已经安装,但仍可能被显示成 “does not support guided setup yet”,导致用户无法直接从 `channel add` 走完接入。现在插件已经切到 OpenClaw 当前识别的 `setupWizard` 接口,`wecom` 会重新出现在正常的 guided setup 流程中。
|
|
8
|
+
- 【修复 `installedCatalogById is not defined`】**[重要修复]** 某些用户在渠道选择或安装判断阶段会直接撞到 `ReferenceError: installedCatalogById is not defined`,表现上像是“选了渠道就报错”或“添加流程突然失效”。这次已经把对应的渠道目录访问逻辑改稳,不再依赖容易失效的解构引用。
|
|
9
|
+
- 【迁移失效的插件 SDK 入口】OpenClaw 升级后,`wecom` 插件里原来直接从 `openclaw/plugin-sdk` 根入口拿的一些类型和能力,已经不再按旧方式导出。`v2.3.27` 已经把这几处高风险点迁到当前公开可用的子路径,重点覆盖了工具上下文、outbound 适配器和媒体发送相关接口。
|
|
10
|
+
- 【修复 `wecom_mcp` 工具返回协议】随着 OpenClaw 新版工具结果协议收紧,`wecom_mcp` 这类插件工具如果还按旧格式返回,就会在注册或执行阶段出现类型或运行时不匹配。现在该工具返回结果已经补齐新版要求的 `details` 字段,和当前工具调用协议保持一致。
|
|
11
|
+
- 【修复 Bot WS 媒体发送对旧导出的依赖】之前 Bot WebSocket 媒体发送链路依赖了一个在当前发布版 OpenClaw 中已经不可直接使用的媒体加载导出,升级后容易出现“代码看起来没问题,但媒体链路单独报错”的情况。现在媒体读取改为基于当前仍然公开可用的远程抓取、本地路径校验和 MIME 识别能力自行组装,兼容当前版本的 OpenClaw 发布面。
|
|
12
|
+
- 【补齐严格类型下的 WS 回复头处理】随着依赖和 SDK 类型变严格,Bot WS 主动回复和命令回复链路里对 `req_id` 头部的处理更容易暴露问题。这一版顺手把回复头的兜底生成与返回值收窄一起补齐,减少升级后只在某些路径上报类型错误或运行时异常的概率。
|
|
13
|
+
|
|
14
|
+
## 升级后你会直接感受到
|
|
15
|
+
|
|
16
|
+
- 在 OpenClaw 里重新执行 `channel add` 时,`wecom` 不会再被误判成“不支持 guided setup”。
|
|
17
|
+
- 遇到 `ReferenceError: installedCatalogById is not defined` 的用户,渠道添加和选择流程会恢复正常。
|
|
18
|
+
- OpenClaw 升级后,`wecom` 插件不再那么依赖旧版 SDK 的根入口导出,单独安装或混合运行时更稳。
|
|
19
|
+
- Bot WS 的媒体发送链路不再因为旧导出失效而单独掉链子。
|
|
20
|
+
|
|
21
|
+
## 这次版本背后的最小理解模型
|
|
22
|
+
|
|
23
|
+
把这次更新理解成三句话就够了:
|
|
24
|
+
|
|
25
|
+
1. OpenClaw 的渠道接入面已经变了,`wecom` 不能再继续挂在旧的 onboarding 接口上。
|
|
26
|
+
2. 插件如果直接依赖 `openclaw/plugin-sdk` 根入口里那些历史上“顺手可用”的符号,升级后迟早会踩到导出面收缩的问题。
|
|
27
|
+
3. 真正稳定的兼容修复,不只是把一个 import 改过去,而是把接入流程、工具返回协议、outbound 类型和媒体链路一起对齐到当前公开接口。
|
|
28
|
+
|
|
29
|
+
## 升级提示
|
|
30
|
+
|
|
31
|
+
- 执行 `openclaw plugins update wecom` 即可升级到 `v2.3.27`。
|
|
32
|
+
- 如果你刚升级 OpenClaw 后发现 `wecom` 在 `channel add` 里突然不能配置,或者用户侧开始报 `installedCatalogById is not defined`,这一版就是对应修复。
|
|
33
|
+
- 如果你是“全局安装的 OpenClaw + 本地 `wecom` 源码插件”混合运行,升级后建议完整重启一次 gateway,确保新的 setup 和 outbound 逻辑都已生效。
|
package/index.test.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
import plugin from "./index.js";
|
|
4
4
|
|
|
5
5
|
describe("wecom plugin register", () => {
|
|
6
6
|
it("registers both recommended and legacy webhook route prefixes", () => {
|
|
7
7
|
const registerChannel = vi.fn();
|
|
8
8
|
const registerHttpRoute = vi.fn();
|
|
9
|
+
const registerTool = vi.fn();
|
|
10
|
+
const on = vi.fn();
|
|
9
11
|
const api = {
|
|
10
12
|
runtime: {},
|
|
11
13
|
registerChannel,
|
|
12
14
|
registerHttpRoute,
|
|
15
|
+
registerTool,
|
|
16
|
+
on,
|
|
13
17
|
} as unknown as OpenClawPluginApi;
|
|
14
18
|
|
|
15
19
|
plugin.register(api);
|
package/package.json
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yanhaidao/wecom",
|
|
3
|
-
"version": "2.3.
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "2.3.270",
|
|
5
4
|
"description": "OpenClaw 企业微信(WeCom)插件,默认 Bot WebSocket,支持加密媒体解密、Agent 主动发消息与多账号接入",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"author": "YanHaidao (VX: YanHaidao)",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
8
9
|
"url": "git+https://github.com/YanHaidao/wecom.git"
|
|
9
10
|
},
|
|
11
|
+
"type": "module",
|
|
10
12
|
"publishConfig": {
|
|
11
13
|
"access": "public"
|
|
12
14
|
},
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@wecom/aibot-node-sdk": "^1.0.0",
|
|
17
|
+
"fast-xml-parser": "5.3.4",
|
|
18
|
+
"undici": "^7.20.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^25.2.0",
|
|
22
|
+
"typescript": "^5.9.3",
|
|
23
|
+
"vitest": "^2.1.8"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"openclaw": "^2026.3.23-2"
|
|
27
|
+
},
|
|
15
28
|
"openclaw": {
|
|
16
29
|
"extensions": [
|
|
17
30
|
"./index.ts"
|
|
@@ -38,18 +51,5 @@
|
|
|
38
51
|
"localPath": "extensions/wecom",
|
|
39
52
|
"defaultChoice": "npm"
|
|
40
53
|
}
|
|
41
|
-
},
|
|
42
|
-
"dependencies": {
|
|
43
|
-
"@wecom/aibot-node-sdk": "^1.0.0",
|
|
44
|
-
"fast-xml-parser": "5.3.4",
|
|
45
|
-
"undici": "^7.20.0"
|
|
46
|
-
},
|
|
47
|
-
"peerDependencies": {
|
|
48
|
-
"openclaw": ">=2026.2.24"
|
|
49
|
-
},
|
|
50
|
-
"devDependencies": {
|
|
51
|
-
"@types/node": "^25.2.0",
|
|
52
|
-
"typescript": "^5.9.3",
|
|
53
|
-
"vitest": "^2.1.8"
|
|
54
54
|
}
|
|
55
55
|
}
|
package/src/agent/handler.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/infra-runtime";
|
|
2
3
|
import type { ResolvedRuntimeAccount } from "../config/runtime-config.js";
|
|
3
4
|
import { WecomAuditLog } from "../observability/audit-log.js";
|
|
4
5
|
import { WecomStatusRegistry } from "../observability/status-registry.js";
|
|
@@ -107,6 +108,9 @@ export class WecomAccountRuntime {
|
|
|
107
108
|
);
|
|
108
109
|
await replyHandle.fail?.(error);
|
|
109
110
|
},
|
|
111
|
+
markExternalActivity: () => {
|
|
112
|
+
replyHandle.markExternalActivity?.();
|
|
113
|
+
},
|
|
110
114
|
};
|
|
111
115
|
|
|
112
116
|
try {
|
package/src/app/index.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
2
|
import { clearWecomSourceAccount } from "../runtime/source-registry.js";
|
|
3
|
+
import type { ReplyHandle } from "../types/index.js";
|
|
3
4
|
import { WecomAccountRuntime } from "./account-runtime.js";
|
|
4
5
|
|
|
5
6
|
let runtime: PluginRuntime | null = null;
|
|
6
7
|
const runtimes = new Map<string, WecomAccountRuntime>();
|
|
7
8
|
const botWsPushHandles = new Map<string, BotWsPushHandle>();
|
|
9
|
+
const activeBotWsReplyHandlesBySession = new Map<string, ReplyHandle>();
|
|
10
|
+
const activeBotWsReplyHandlesByPeer = new Map<string, ReplyHandle>();
|
|
8
11
|
|
|
9
12
|
export type BotWsPushHandle = {
|
|
10
13
|
isConnected: () => boolean;
|
|
@@ -12,7 +15,7 @@ export type BotWsPushHandle = {
|
|
|
12
15
|
replyCommand: (params: {
|
|
13
16
|
cmd: string;
|
|
14
17
|
body?: Record<string, unknown>;
|
|
15
|
-
headers?: Record<string, string
|
|
18
|
+
headers?: ({ req_id?: string } & Record<string, string>) | undefined;
|
|
16
19
|
}) => Promise<Record<string, unknown>>;
|
|
17
20
|
sendMedia: (params: {
|
|
18
21
|
chatId: string;
|
|
@@ -29,6 +32,28 @@ export type BotWsPushHandle = {
|
|
|
29
32
|
}>;
|
|
30
33
|
};
|
|
31
34
|
|
|
35
|
+
function normalizeOptional(value: string | null | undefined): string | undefined {
|
|
36
|
+
const trimmed = String(value ?? "").trim();
|
|
37
|
+
return trimmed || undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizePeerId(value: string | null | undefined): string | undefined {
|
|
41
|
+
const trimmed = normalizeOptional(value);
|
|
42
|
+
return trimmed ? trimmed.toLowerCase() : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildSessionHandleKey(accountId: string, sessionKey: string): string {
|
|
46
|
+
return `${accountId}::session::${sessionKey}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildPeerHandleKey(
|
|
50
|
+
accountId: string,
|
|
51
|
+
peerKind: "direct" | "group",
|
|
52
|
+
peerId: string,
|
|
53
|
+
): string {
|
|
54
|
+
return `${accountId}::peer::${peerKind}::${peerId}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
32
57
|
export function setWecomRuntime(next: PluginRuntime): void {
|
|
33
58
|
runtime = next;
|
|
34
59
|
}
|
|
@@ -61,6 +86,90 @@ export function getBotWsPushHandle(accountId: string): BotWsPushHandle | undefin
|
|
|
61
86
|
return botWsPushHandles.get(accountId);
|
|
62
87
|
}
|
|
63
88
|
|
|
89
|
+
export function registerActiveBotWsReplyHandle(params: {
|
|
90
|
+
accountId: string;
|
|
91
|
+
sessionKey?: string | null;
|
|
92
|
+
peerKind?: "direct" | "group" | null;
|
|
93
|
+
peerId?: string | null;
|
|
94
|
+
handle: ReplyHandle;
|
|
95
|
+
}): void {
|
|
96
|
+
const accountId = normalizeOptional(params.accountId);
|
|
97
|
+
const sessionKey = normalizeOptional(params.sessionKey);
|
|
98
|
+
const peerId = normalizePeerId(params.peerId);
|
|
99
|
+
if (!accountId) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (sessionKey) {
|
|
103
|
+
activeBotWsReplyHandlesBySession.set(
|
|
104
|
+
buildSessionHandleKey(accountId, sessionKey),
|
|
105
|
+
params.handle,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if ((params.peerKind === "direct" || params.peerKind === "group") && peerId) {
|
|
109
|
+
activeBotWsReplyHandlesByPeer.set(
|
|
110
|
+
buildPeerHandleKey(accountId, params.peerKind, peerId),
|
|
111
|
+
params.handle,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getActiveBotWsReplyHandle(params: {
|
|
117
|
+
accountId: string;
|
|
118
|
+
sessionKey?: string | null;
|
|
119
|
+
peerKind?: "direct" | "group" | null;
|
|
120
|
+
peerId?: string | null;
|
|
121
|
+
}): ReplyHandle | undefined {
|
|
122
|
+
const accountId = normalizeOptional(params.accountId);
|
|
123
|
+
const sessionKey = normalizeOptional(params.sessionKey);
|
|
124
|
+
const peerId = normalizePeerId(params.peerId);
|
|
125
|
+
if (!accountId) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
if (sessionKey) {
|
|
129
|
+
const handle = activeBotWsReplyHandlesBySession.get(
|
|
130
|
+
buildSessionHandleKey(accountId, sessionKey),
|
|
131
|
+
);
|
|
132
|
+
if (handle) {
|
|
133
|
+
return handle;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if ((params.peerKind === "direct" || params.peerKind === "group") && peerId) {
|
|
137
|
+
return activeBotWsReplyHandlesByPeer.get(
|
|
138
|
+
buildPeerHandleKey(accountId, params.peerKind, peerId),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function unregisterActiveBotWsReplyHandle(params: {
|
|
145
|
+
accountId: string;
|
|
146
|
+
sessionKey?: string | null;
|
|
147
|
+
peerKind?: "direct" | "group" | null;
|
|
148
|
+
peerId?: string | null;
|
|
149
|
+
handle?: ReplyHandle;
|
|
150
|
+
}): void {
|
|
151
|
+
const accountId = normalizeOptional(params.accountId);
|
|
152
|
+
const sessionKey = normalizeOptional(params.sessionKey);
|
|
153
|
+
const peerId = normalizePeerId(params.peerId);
|
|
154
|
+
if (!accountId) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (sessionKey) {
|
|
158
|
+
const key = buildSessionHandleKey(accountId, sessionKey);
|
|
159
|
+
const current = activeBotWsReplyHandlesBySession.get(key);
|
|
160
|
+
if (!params.handle || current === params.handle) {
|
|
161
|
+
activeBotWsReplyHandlesBySession.delete(key);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if ((params.peerKind === "direct" || params.peerKind === "group") && peerId) {
|
|
165
|
+
const key = buildPeerHandleKey(accountId, params.peerKind, peerId);
|
|
166
|
+
const current = activeBotWsReplyHandlesByPeer.get(key);
|
|
167
|
+
if (!params.handle || current === params.handle) {
|
|
168
|
+
activeBotWsReplyHandlesByPeer.delete(key);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
64
173
|
export function unregisterBotWsPushHandle(accountId: string): void {
|
|
65
174
|
botWsPushHandles.delete(accountId);
|
|
66
175
|
}
|
|
@@ -68,6 +177,16 @@ export function unregisterBotWsPushHandle(accountId: string): void {
|
|
|
68
177
|
export function unregisterAccountRuntime(accountId: string): void {
|
|
69
178
|
runtimes.delete(accountId);
|
|
70
179
|
botWsPushHandles.delete(accountId);
|
|
180
|
+
for (const key of activeBotWsReplyHandlesBySession.keys()) {
|
|
181
|
+
if (key.startsWith(`${accountId}::`)) {
|
|
182
|
+
activeBotWsReplyHandlesBySession.delete(key);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const key of activeBotWsReplyHandlesByPeer.keys()) {
|
|
186
|
+
if (key.startsWith(`${accountId}::`)) {
|
|
187
|
+
activeBotWsReplyHandlesByPeer.delete(key);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
71
190
|
clearWecomSourceAccount(accountId);
|
|
72
191
|
console.log(`[wecom-runtime] unregister account=${accountId}`);
|
|
73
192
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
OpenClawPluginToolContext,
|
|
3
|
+
OpenClawPluginToolFactory,
|
|
4
|
+
} from "openclaw/plugin-sdk/core";
|
|
2
5
|
import { resolveWecomSourceSnapshot } from "../../runtime/source-registry.js";
|
|
3
6
|
import { cleanSchemaForGemini } from "./schema.js";
|
|
4
7
|
import { clearWecomMcpCategoryCache, sendJsonRpc, type McpToolInfo } from "./transport.js";
|
|
@@ -12,9 +15,10 @@ type WecomMcpParams = {
|
|
|
12
15
|
|
|
13
16
|
const BIZ_CACHE_CLEAR_ERROR_CODES = new Set([850002]);
|
|
14
17
|
|
|
15
|
-
function textResult(data:
|
|
18
|
+
function textResult<TDetails>(data: TDetails) {
|
|
16
19
|
return {
|
|
17
20
|
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
21
|
+
details: data,
|
|
18
22
|
};
|
|
19
23
|
}
|
|
20
24
|
|
|
@@ -96,7 +100,7 @@ async function handleCall(
|
|
|
96
100
|
return result;
|
|
97
101
|
}
|
|
98
102
|
|
|
99
|
-
export function createWeComMcpToolFactory() {
|
|
103
|
+
export function createWeComMcpToolFactory(): OpenClawPluginToolFactory {
|
|
100
104
|
return (toolContext: OpenClawPluginToolContext) => {
|
|
101
105
|
if (toolContext.messageChannel !== "wecom") {
|
|
102
106
|
return null;
|
package/src/channel.meta.test.ts
CHANGED
|
@@ -9,4 +9,8 @@ describe("wecomPlugin meta", () => {
|
|
|
9
9
|
expect(wecomPlugin.meta.docsLabel).toBe("企业微信");
|
|
10
10
|
expect(wecomPlugin.meta.selectionDocsPrefix).toBe("文档:");
|
|
11
11
|
});
|
|
12
|
+
|
|
13
|
+
it("exposes a setupWizard for guided setup discovery", () => {
|
|
14
|
+
expect(wecomPlugin.setupWizard?.channel).toBe("wecom");
|
|
15
|
+
});
|
|
12
16
|
});
|
package/src/channel.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ChannelAccountSnapshot,
|
|
3
|
-
ChannelPlugin,
|
|
4
|
-
OpenClawConfig,
|
|
5
|
-
} from "openclaw/plugin-sdk";
|
|
1
|
+
import type { ChannelAccountSnapshot, ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk";
|
|
6
2
|
import {
|
|
7
3
|
deleteAccountFromConfigSection,
|
|
8
4
|
setAccountEnabledInConfigSection,
|
|
9
|
-
} from "openclaw/plugin-sdk";
|
|
10
|
-
|
|
5
|
+
} from "openclaw/plugin-sdk/core";
|
|
11
6
|
import {
|
|
12
7
|
DEFAULT_ACCOUNT_ID,
|
|
13
8
|
listWecomAccountIds,
|
|
@@ -16,10 +11,10 @@ import {
|
|
|
16
11
|
resolveWecomAccount,
|
|
17
12
|
resolveWecomAccountConflict,
|
|
18
13
|
} from "./config/index.js";
|
|
19
|
-
import type { ResolvedWecomAccount } from "./types/index.js";
|
|
20
14
|
import { monitorWecomProvider } from "./gateway-monitor.js";
|
|
21
|
-
import {
|
|
15
|
+
import { wecomSetupWizard } from "./onboarding.js";
|
|
22
16
|
import { wecomOutbound } from "./outbound.js";
|
|
17
|
+
import type { ResolvedWecomAccount } from "./types/index.js";
|
|
23
18
|
|
|
24
19
|
const meta = {
|
|
25
20
|
id: "wecom",
|
|
@@ -27,22 +22,16 @@ const meta = {
|
|
|
27
22
|
selectionLabel: "WeCom (企业微信)",
|
|
28
23
|
docsPath: "/channels/wecom",
|
|
29
24
|
docsLabel: "企业微信",
|
|
30
|
-
blurb:
|
|
31
|
-
"企业微信官方推荐三方插件,默认 Bot WS 配置简单,支持主动发消息与 Agent 全能力。",
|
|
25
|
+
blurb: "企业微信官方推荐三方插件,默认 Bot WS 配置简单,支持主动发消息与 Agent 全能力。",
|
|
32
26
|
selectionDocsPrefix: "文档:",
|
|
33
27
|
aliases: ["wechatwork", "wework", "qywx", "企微", "企业微信"],
|
|
34
28
|
order: 85,
|
|
35
29
|
quickstartAllowFrom: true,
|
|
36
30
|
};
|
|
37
31
|
|
|
38
|
-
function resolveAccountInboundPath(
|
|
39
|
-
account: ResolvedWecomAccount,
|
|
40
|
-
): string | undefined {
|
|
32
|
+
function resolveAccountInboundPath(account: ResolvedWecomAccount): string | undefined {
|
|
41
33
|
const derivedPaths = resolveDerivedPathSummary(account.accountId);
|
|
42
|
-
if (
|
|
43
|
-
account.bot?.primaryTransport === "webhook" &&
|
|
44
|
-
account.bot.webhookConfigured
|
|
45
|
-
) {
|
|
34
|
+
if (account.bot?.primaryTransport === "webhook" && account.bot.webhookConfigured) {
|
|
46
35
|
return derivedPaths.botWebhook[0];
|
|
47
36
|
}
|
|
48
37
|
if (account.agent?.callbackConfigured) {
|
|
@@ -57,15 +46,13 @@ function normalizeWecomMessagingTarget(raw: string): string | undefined {
|
|
|
57
46
|
if (/^wecom-agent:/i.test(trimmed)) {
|
|
58
47
|
return trimmed;
|
|
59
48
|
}
|
|
60
|
-
return (
|
|
61
|
-
trimmed.replace(/^(wecom|wechatwork|wework|qywx):/i, "").trim() || undefined
|
|
62
|
-
);
|
|
49
|
+
return trimmed.replace(/^(wecom|wechatwork|wework|qywx):/i, "").trim() || undefined;
|
|
63
50
|
}
|
|
64
51
|
|
|
65
52
|
export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
|
|
66
53
|
id: "wecom",
|
|
67
54
|
meta,
|
|
68
|
-
|
|
55
|
+
setupWizard: wecomSetupWizard,
|
|
69
56
|
capabilities: {
|
|
70
57
|
chatTypes: ["direct", "group"],
|
|
71
58
|
media: true,
|
|
@@ -88,8 +75,7 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
|
|
|
88
75
|
listAccountIds: (cfg) => listWecomAccountIds(cfg as OpenClawConfig),
|
|
89
76
|
resolveAccount: (cfg, accountId) =>
|
|
90
77
|
resolveWecomAccount({ cfg: cfg as OpenClawConfig, accountId }),
|
|
91
|
-
defaultAccountId: (cfg) =>
|
|
92
|
-
resolveDefaultWecomAccountId(cfg as OpenClawConfig),
|
|
78
|
+
defaultAccountId: (cfg) => resolveDefaultWecomAccountId(cfg as OpenClawConfig),
|
|
93
79
|
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
|
94
80
|
setAccountEnabledInConfigSection({
|
|
95
81
|
cfg: cfg as OpenClawConfig,
|
|
@@ -139,9 +125,7 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
|
|
|
139
125
|
});
|
|
140
126
|
// 与其他渠道保持一致:直接返回 allowFrom,空则允许所有人
|
|
141
127
|
const allowFrom =
|
|
142
|
-
account.agent?.config.dm?.allowFrom ??
|
|
143
|
-
account.bot?.config.dm?.allowFrom ??
|
|
144
|
-
[];
|
|
128
|
+
account.agent?.config.dm?.allowFrom ?? account.bot?.config.dm?.allowFrom ?? [];
|
|
145
129
|
return allowFrom.map((entry) => String(entry));
|
|
146
130
|
},
|
|
147
131
|
formatAllowFrom: ({ allowFrom }) =>
|
|
@@ -183,31 +167,24 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
|
|
|
183
167
|
transport: (snapshot as { transport?: string }).transport ?? null,
|
|
184
168
|
ownerId: (snapshot as { ownerId?: string }).ownerId ?? null,
|
|
185
169
|
health: (snapshot as { health?: string }).health ?? "idle",
|
|
186
|
-
ownerDriftAt:
|
|
187
|
-
(snapshot as { ownerDriftAt?: number | null }).ownerDriftAt ?? null,
|
|
170
|
+
ownerDriftAt: (snapshot as { ownerDriftAt?: number | null }).ownerDriftAt ?? null,
|
|
188
171
|
connected: (snapshot as { connected?: boolean }).connected,
|
|
189
172
|
authenticated: (snapshot as { authenticated?: boolean }).authenticated,
|
|
190
173
|
lastStartAt: snapshot.lastStartAt ?? null,
|
|
191
174
|
lastStopAt: snapshot.lastStopAt ?? null,
|
|
192
175
|
lastError: snapshot.lastError ?? null,
|
|
193
|
-
lastErrorAt:
|
|
194
|
-
(snapshot as { lastErrorAt?: number | null }).lastErrorAt ?? null,
|
|
176
|
+
lastErrorAt: (snapshot as { lastErrorAt?: number | null }).lastErrorAt ?? null,
|
|
195
177
|
lastInboundAt: snapshot.lastInboundAt ?? null,
|
|
196
178
|
lastOutboundAt: snapshot.lastOutboundAt ?? null,
|
|
197
179
|
recentInboundSummary:
|
|
198
|
-
(snapshot as { recentInboundSummary?: string | null })
|
|
199
|
-
.recentInboundSummary ?? null,
|
|
180
|
+
(snapshot as { recentInboundSummary?: string | null }).recentInboundSummary ?? null,
|
|
200
181
|
recentOutboundSummary:
|
|
201
|
-
(snapshot as { recentOutboundSummary?: string | null })
|
|
202
|
-
.recentOutboundSummary ?? null,
|
|
182
|
+
(snapshot as { recentOutboundSummary?: string | null }).recentOutboundSummary ?? null,
|
|
203
183
|
recentIssueCategory:
|
|
204
|
-
(snapshot as { recentIssueCategory?: string | null })
|
|
205
|
-
.recentIssueCategory ?? null,
|
|
184
|
+
(snapshot as { recentIssueCategory?: string | null }).recentIssueCategory ?? null,
|
|
206
185
|
recentIssueSummary:
|
|
207
|
-
(snapshot as { recentIssueSummary?: string | null })
|
|
208
|
-
|
|
209
|
-
transportSessions:
|
|
210
|
-
(snapshot as { transportSessions?: string[] }).transportSessions ?? [],
|
|
186
|
+
(snapshot as { recentIssueSummary?: string | null }).recentIssueSummary ?? null,
|
|
187
|
+
transportSessions: (snapshot as { transportSessions?: string[] }).transportSessions ?? [],
|
|
211
188
|
probe: snapshot.probe,
|
|
212
189
|
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
213
190
|
}),
|
|
@@ -224,42 +201,35 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
|
|
|
224
201
|
configured: account.configured && !conflict,
|
|
225
202
|
webhookPath: resolveAccountInboundPath(account),
|
|
226
203
|
primaryTransport:
|
|
227
|
-
account.bot?.primaryTransport ??
|
|
228
|
-
|
|
229
|
-
transport:
|
|
230
|
-
(runtime as { transport?: string } | undefined)?.transport ?? null,
|
|
204
|
+
account.bot?.primaryTransport ?? (account.agent ? "agent-callback" : null),
|
|
205
|
+
transport: (runtime as { transport?: string } | undefined)?.transport ?? null,
|
|
231
206
|
ownerId: (runtime as { ownerId?: string } | undefined)?.ownerId ?? null,
|
|
232
207
|
health: (runtime as { health?: string } | undefined)?.health ?? "idle",
|
|
233
208
|
ownerDriftAt:
|
|
234
|
-
(runtime as { ownerDriftAt?: number | null } | undefined)
|
|
235
|
-
?.ownerDriftAt ?? null,
|
|
209
|
+
(runtime as { ownerDriftAt?: number | null } | undefined)?.ownerDriftAt ?? null,
|
|
236
210
|
connected: (runtime as { connected?: boolean } | undefined)?.connected,
|
|
237
|
-
authenticated: (runtime as { authenticated?: boolean } | undefined)
|
|
238
|
-
?.authenticated,
|
|
211
|
+
authenticated: (runtime as { authenticated?: boolean } | undefined)?.authenticated,
|
|
239
212
|
running: runtime?.running ?? false,
|
|
240
213
|
lastStartAt: runtime?.lastStartAt ?? null,
|
|
241
214
|
lastStopAt: runtime?.lastStopAt ?? null,
|
|
242
215
|
lastError: runtime?.lastError ?? conflict?.message ?? null,
|
|
243
|
-
lastErrorAt:
|
|
244
|
-
(runtime as { lastErrorAt?: number | null } | undefined)
|
|
245
|
-
?.lastErrorAt ?? null,
|
|
216
|
+
lastErrorAt: (runtime as { lastErrorAt?: number | null } | undefined)?.lastErrorAt ?? null,
|
|
246
217
|
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
247
218
|
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
248
219
|
recentInboundSummary:
|
|
249
|
-
(runtime as { recentInboundSummary?: string | null } | undefined)
|
|
250
|
-
|
|
220
|
+
(runtime as { recentInboundSummary?: string | null } | undefined)?.recentInboundSummary ??
|
|
221
|
+
null,
|
|
251
222
|
recentOutboundSummary:
|
|
252
223
|
(runtime as { recentOutboundSummary?: string | null } | undefined)
|
|
253
224
|
?.recentOutboundSummary ?? null,
|
|
254
225
|
recentIssueCategory:
|
|
255
|
-
(runtime as { recentIssueCategory?: string | null } | undefined)
|
|
256
|
-
|
|
226
|
+
(runtime as { recentIssueCategory?: string | null } | undefined)?.recentIssueCategory ??
|
|
227
|
+
null,
|
|
257
228
|
recentIssueSummary:
|
|
258
|
-
(runtime as { recentIssueSummary?: string | null } | undefined)
|
|
259
|
-
|
|
229
|
+
(runtime as { recentIssueSummary?: string | null } | undefined)?.recentIssueSummary ??
|
|
230
|
+
null,
|
|
260
231
|
transportSessions:
|
|
261
|
-
(runtime as { transportSessions?: string[] } | undefined)
|
|
262
|
-
?.transportSessions ?? [],
|
|
232
|
+
(runtime as { transportSessions?: string[] } | undefined)?.transportSessions ?? [],
|
|
263
233
|
dmPolicy: account.bot?.config.dm?.policy ?? "pairing",
|
|
264
234
|
};
|
|
265
235
|
},
|
package/src/config/media.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk";
|
|
4
|
+
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/infra-runtime";
|
|
5
5
|
import { resolveWecomMediaMaxBytes, resolveWecomMergedMediaLocalRoots } from "./media.js";
|
|
6
6
|
|
|
7
7
|
describe("resolveWecomMergedMediaLocalRoots", () => {
|