@yanhaidao/wecom 2.3.2 → 2.3.4

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.
@@ -4,6 +4,7 @@ on:
4
4
  push:
5
5
  tags:
6
6
  - 'v*'
7
+ - 'wecom-v*'
7
8
 
8
9
  jobs:
9
10
  publish:
@@ -34,6 +35,51 @@ jobs:
34
35
  run: |
35
36
  npm i -g npm@^11.5.1
36
37
  npm -v
38
+ npm config delete always-auth || true
39
+
40
+ - name: Resolve release metadata
41
+ id: meta
42
+ run: |
43
+ set -euo pipefail
44
+ TAG="${GITHUB_REF_NAME}"
45
+ if [[ "${TAG}" == wecom-v* ]]; then
46
+ VERSION="${TAG#wecom-v}"
47
+ elif [[ "${TAG}" == v* ]]; then
48
+ VERSION="${TAG#v}"
49
+ else
50
+ echo "::error::Unsupported tag format: ${TAG}. Use v<version> or wecom-v<version>."
51
+ exit 1
52
+ fi
53
+ PKG_NAME="$(node -p "require('./package.json').name")"
54
+ PKG_VERSION="$(node -p "require('./package.json').version")"
55
+
56
+ echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
57
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
58
+ echo "pkg_name=${PKG_NAME}" >> "$GITHUB_OUTPUT"
59
+ echo "pkg_version=${PKG_VERSION}" >> "$GITHUB_OUTPUT"
60
+
61
+ echo "tag=${TAG}"
62
+ echo "version_from_tag=${VERSION}"
63
+ echo "package=${PKG_NAME}@${PKG_VERSION}"
64
+
65
+ if [ "${VERSION}" != "${PKG_VERSION}" ]; then
66
+ echo "::error::Tag version (${VERSION}) does not match package.json version (${PKG_VERSION})."
67
+ exit 1
68
+ fi
69
+
70
+ - name: Check npm version already published
71
+ id: npm_check
72
+ run: |
73
+ set -euo pipefail
74
+ PKG="${{ steps.meta.outputs.pkg_name }}"
75
+ VER="${{ steps.meta.outputs.pkg_version }}"
76
+ if npm view "${PKG}@${VER}" version >/dev/null 2>&1; then
77
+ echo "exists=true" >> "$GITHUB_OUTPUT"
78
+ echo "npm version already published: ${PKG}@${VER}"
79
+ else
80
+ echo "exists=false" >> "$GITHUB_OUTPUT"
81
+ echo "npm version not published yet: ${PKG}@${VER}"
82
+ fi
37
83
 
38
84
  - name: Install Dependencies
39
85
  run: npm install --legacy-peer-deps || npm install
@@ -43,13 +89,35 @@ jobs:
43
89
 
44
90
  # 🚀 自动发布到 npm (使用 OIDC 自动授权,无需 Token)
45
91
  - name: Publish to npm
92
+ if: steps.npm_check.outputs.exists != 'true'
46
93
  run: npm publish --access public
47
94
 
95
+ - name: Prepare release notes body
96
+ id: notes
97
+ run: |
98
+ set -euo pipefail
99
+ CHANGELOG_FILE="changelog/v${{ steps.meta.outputs.version }}.md"
100
+ if [ -f "${CHANGELOG_FILE}" ]; then
101
+ echo "Using changelog file: ${CHANGELOG_FILE}"
102
+ cp "${CHANGELOG_FILE}" /tmp/release-body.md
103
+ else
104
+ echo "Changelog file missing: ${CHANGELOG_FILE}; using fallback notes."
105
+ {
106
+ echo "# @yanhaidao/wecom v${{ steps.meta.outputs.version }}"
107
+ echo
108
+ if [ "${{ steps.npm_check.outputs.exists }}" = "true" ]; then
109
+ echo "- npm publish skipped: version already exists."
110
+ else
111
+ echo "- npm publish completed."
112
+ fi
113
+ } > /tmp/release-body.md
114
+ fi
115
+
48
116
  # 自动同步发布 GitHub Release
49
117
  - name: Create GitHub Release
50
118
  uses: softprops/action-gh-release@v2
51
119
  with:
52
- body_path: changelog/${{ github.ref_name }}.md
120
+ body_path: /tmp/release-body.md
53
121
  draft: false
54
122
  prerelease: false
55
123
  env:
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # OpenClaw 企业微信(WeCom)Channel 插件
2
2
 
3
+ > [!WARNING]
4
+ > **OpenClaw 3.1+ 升级必读**:升级到 OpenClaw `3.1` 及以上版本的用户务必同步升级本插件,并将企业微信回调 URL 更新为 OpenClaw 推荐路径:`/plugins/wecom/bot/{accountId}` 与 `/plugins/wecom/agent/{accountId}`(旧 `/wecom/*` 仍兼容但不再维护)。
5
+
3
6
  <p align="center">
4
7
  <img src="https://img.shields.io/badge/Original%20Project-YanHaidao-orange?style=for-the-badge&logo=github" alt="Original Project" />
5
8
  <img src="https://img.shields.io/badge/License-ISC-blue?style=for-the-badge" alt="License" />
@@ -80,7 +83,6 @@
80
83
  ## 一、🚀 快速开始
81
84
 
82
85
  > 默认推荐:**多账号 + 多 Agent(matrix)**。
83
- > 单账号 Bot/Agent 配置仍然支持,但建议仅用于兼容或小规模场景。
84
86
  > 建议 OpenClaw 使用 **2026.2.24+** 版本以获得完整生命周期与多账号行为修复。
85
87
 
86
88
  ### 1.1 安装插件
@@ -152,19 +154,12 @@ openclaw channels status
152
154
  ```
153
155
 
154
156
  Webhook 回调建议按账号分别配置:
155
- - Bot:`/wecom/bot/{accountId}`
156
- - Agent:`/wecom/agent/{accountId}`
157
+ - Bot(推荐):`/plugins/wecom/bot/{accountId}`
158
+ - Agent(推荐):`/plugins/wecom/agent/{accountId}`
157
159
 
158
160
  > 提示:如果你已有 `bindings`,请先备份并按需合并,避免覆盖其它通道绑定。
159
161
 
160
- ### 1.3 兼容模式(单账号)
161
-
162
- 为降低主线认知负担,README 默认仅展示多账号配置。
163
- 如果你在维护历史部署或只需单账号,请查看兼容文档:
164
-
165
- - [单账号兼容模式配置指南](./compat-single-account.md)
166
-
167
- ### 1.4 高级网络配置(公网出口代理)
162
+ ### 1.3 高级网络配置(公网出口代理)
168
163
  如果您的服务器使用 **动态 IP** (如家庭宽带、内网穿透) 或 **无公网 IP**,企业微信 API 会因 IP 变动报错 `60020 not allow to access from your ip`。
169
164
  此时需配置一个**固定 IP 的正向代理** (如 Squid),让插件通过该代理访问企微 API。
170
165
 
@@ -172,7 +167,7 @@ Webhook 回调建议按账号分别配置:
172
167
  openclaw config set channels.wecom.network.egressProxyUrl "http://proxy.company.local:3128"
173
168
  ```
174
169
 
175
- ### 1.5 验证
170
+ ### 1.4 验证
176
171
 
177
172
  ```bash
178
173
  openclaw config set gateway.bind lan
@@ -286,38 +281,32 @@ openclaw channels status
286
281
  }
287
282
  ```
288
283
 
289
- ### 2.2 兼容模式文档(单账号)
290
-
291
- - [单账号兼容模式配置指南](./compat-single-account.md)
292
-
293
- ### 2.3 路由第一性原则
284
+ ### 2.2 路由第一性原则
294
285
 
295
286
  - `accountId` 是会话隔离边界:不同账号不共享会话、不共享动态 Agent。
296
287
  - Bot 无法交付时,只回退到**同组** Agent,不跨账号兜底。
297
288
  - 只有在未显式指定 `accountId` 时,才使用 `defaultAccount`。
298
289
 
299
- ### 2.4 Webhook 路径(优先使用账号路径)
290
+ ### 2.3 Webhook 路径(必须使用账号路径)
300
291
 
301
292
  | 模式 | 路径 | 说明 |
302
293
  |:---|:---|:---|
303
- | Bot(推荐,多账号) | `/wecom/bot/{accountId}` | 指定账号回调(例如 `/wecom/bot/default`) |
304
- | Agent(推荐,多账号) | `/wecom/agent/{accountId}` | 指定账号回调(例如 `/wecom/agent/default`) |
305
- | Bot(兼容,单账号 legacy) | `/wecom/bot` 或 `/wecom` | 历史路径,仅单账号模式建议保留 |
306
- | Agent(兼容,单账号 legacy) | `/wecom/agent` | 历史路径,单账号模式可用 |
294
+ | Bot(推荐,多账号) | `/plugins/wecom/bot/{accountId}` | 指定账号回调(例如 `/plugins/wecom/bot/default`) |
295
+ | Agent(推荐,多账号) | `/plugins/wecom/agent/{accountId}` | 指定账号回调(例如 `/plugins/wecom/agent/default`) |
307
296
 
308
- ### 2.5 从单账号迁移到多账号(4 步)
297
+ ### 2.4 从单账号迁移到多账号(4 步)
309
298
 
310
299
  1. 把原来的 `channels.wecom.bot` / `channels.wecom.agent` 拆到 `channels.wecom.accounts.default.bot/agent`。
311
300
  2. 按业务继续新增 `channels.wecom.accounts.<accountId>`(例如 `ops`、`sales`)。
312
301
  3. 为每个账号增加 `bindings[].match.accountId`,映射到对应 OpenClaw agent。
313
- 4. 企业微信后台把回调 URL 改成账号路径:`/wecom/bot/{accountId}`、`/wecom/agent/{accountId}`,然后执行 `openclaw channels status` 验证。
302
+ 4. 企业微信后台把回调 URL 改成账号路径:`/plugins/wecom/bot/{accountId}`、`/plugins/wecom/agent/{accountId}`,然后执行 `openclaw channels status` 验证。
314
303
 
315
- ### 2.6 DM 策略
304
+ ### 2.5 DM 策略
316
305
 
317
306
  - **不配置 `dm.allowFrom`** → 所有人可用(默认)
318
307
  - **配置 `dm.allowFrom: ["user1", "user2"]`** → 白名单模式,仅列表内用户可私聊
319
308
 
320
- ### 2.7 常用指令
309
+ ### 2.6 常用指令
321
310
 
322
311
  | 指令 | 说明 | 示例 |
323
312
  |:---|:---|:---|
@@ -335,7 +324,7 @@ openclaw channels status
335
324
  1. 登录 [企业微信管理后台](https://work.weixin.qq.com/wework_admin/frame#/manageTools)
336
325
  2. 进入「安全与管理」→「管理工具」→「智能机器人」
337
326
  3. 创建机器人,选择 **API 模式**
338
- 4. 填写回调 URL:`https://your-domain.com/wecom/bot/{accountId}`(例如默认账号:`https://your-domain.com/wecom/bot/default`)
327
+ 4. 填写回调 URL:`https://your-domain.com/plugins/wecom/bot/{accountId}`(例如默认账号:`https://your-domain.com/plugins/wecom/bot/default`)
339
328
  5. 记录 Token 和 EncodingAESKey
340
329
 
341
330
  ### 3.2 Agent 模式(自建应用)
@@ -346,7 +335,7 @@ openclaw channels status
346
335
  4. **重要:** 进入「企业可信IP」→「配置」→ 添加你服务器的 IP 地址
347
336
  - 如果你使用内网穿透/动态 IP,建议配置 `channels.wecom.network.egressProxyUrl` 走固定出口代理,否则可能出现:`60020 not allow to access from your ip`
348
337
  5. 在应用详情中设置「接收消息 - 设置API接收」
349
- 6. 填写回调 URL:`https://your-domain.com/wecom/agent/{accountId}`(例如默认账号:`https://your-domain.com/wecom/agent/default`)
338
+ 6. 填写回调 URL:`https://your-domain.com/plugins/wecom/agent/{accountId}`(例如默认账号:`https://your-domain.com/plugins/wecom/agent/default`)
350
339
  7. 记录回调 Token 和 EncodingAESKey
351
340
 
352
341
  <div align="center">
@@ -511,6 +500,24 @@ Agent 输出 `{"template_card": ...}` 时自动渲染为交互卡片:
511
500
  <a id="sec-10"></a>
512
501
  ## 八、📝 更新日志
513
502
 
503
+ ### 2026.3.3(今日更新简报)
504
+
505
+ - 【SDK适配】♻️ **插件 HTTP 注册升级**:入口改为 `registerHttpRoute`(`/plugins/wecom` + `match=prefix` + `auth=plugin`),适配 OpenClaw 新版插件接口。
506
+ - 【兼容修复】🔁 **旧入口保持可达**:同步注册 `/wecom` 前缀路由,保障历史 Bot/Agent 回调地址继续可用。
507
+ - 【兼容性修复】🧩 **OpenClaw 3.1 路由抢占问题修复**:推荐回调地址升级为 `/plugins/wecom/bot/{accountId}`、`/plugins/wecom/agent/{accountId}`,规避根路径 Control UI fallback 抢占 webhook。
508
+ - 【引导收敛】🧭 **Onboarding 仅支持账号化配置**:配置向导统一写入 `channels.wecom.accounts.<accountId>`,不再引导单账号旧结构。
509
+ - 【兼容策略】🔁 **旧路径兼容保留**:`/wecom/*` 历史回调路径保留兼容能力,但不再作为维护主路径。
510
+ - 【分流稳定性】🧭 **路由识别增强**:monitor 按插件命名空间账号路径识别,确保 Bot/Agent 分支稳定命中。
511
+ - 【链路一致性】🔒 **Bot 回复不再误走 Agent**:修复 Bot 上下文通道标识,避免 `routeReply` 误触发到 outbound 主动发送链路。
512
+ - 【验证结果】✅ WeCom 插件测试通过:`10` files / `41` tests。
513
+
514
+ ### 2026.3.2(版本更新简报)
515
+
516
+ - 【交付收口】🔄 修复 Bot 会话“正在搜索相关内容”不结束的问题,并在可用时推送最终流帧结束状态。
517
+ - 【媒体兜底】📎 统一非图片文件、媒体失败和超时场景为“Bot 提示 + Agent 私信兜底”闭环,确保结果可达。
518
+ - 【类型兼容】🧠 扩展 `txt/docx/xlsx/pptx/csv/zip` 等常见文件类型识别,并保留 `application/octet-stream` 自动重试。
519
+ - 【工具治理】🛡 修复 Bot 会话 `message` 工具禁用策略,避免绕过 Bot 交付链路导致会话错位。
520
+
514
521
  ### 2026.2.28
515
522
 
516
523
  - 【重磅更新】🎯 **多账号/多智能体可用性增强**:支持按 `accountId` 做组内隔离(Bot + Agent + 路由绑定同组生效),动态 Agent 与会话键增加 `accountId` 维度,避免跨账号串会话。
@@ -1,70 +1,28 @@
1
- # 🚀 OpenClaw 企业微信 (WeCom) 插件 v2.3.2 - Bot/Agent 交付收口与文件发送兼容性增强
2
-
3
- 本次 v2.3.2 版本聚焦修复企业微信 Bot 模式在复杂交付场景下的稳定性问题,重点解决:
4
- - Bot 已收到文件但界面仍停留在“正在搜索相关内容”不结束。
5
- - 本地文件下发时,`txt`、`docx` 以及更多文件类型发送不稳定的问题。
6
-
7
- ---
8
-
9
- ### 🌟 版本亮点 (Release Highlights)
10
-
11
- * **修复“正在搜索相关内容”不收口**:Agent 链路执行完成后,Bot 会主动推送最终流帧,确保企微思考态正确结束。
12
- * 🔒 **修复消息工具绕过链路问题**:在 WeCom Bot 会话中正确禁用顶层 `message` 工具,避免模型绕开 Bot 流式交付导致链路错位。
13
- * 📁 **本地路径下发能力增强**:支持识别 `/root`、`/home` Linux 常见路径,减少文件请求误分流。
14
- * 📄 **文件类型兼容大幅提升**:补齐 `txt`、`docx`、`xlsx`、`pptx`、`csv`、`zip` 等常见类型 MIME,并增强入站媒体类型推断准确率。
15
- * 🛟 **未知类型自动兜底**:上传失败时自动回退到 `application/octet-stream` 重试,提高“能发出去”的成功率。
16
-
17
- ---
18
-
19
- ### 📝 详细更新日志 (Changelog)
20
-
21
- #### 【交付稳定性】🔄 Bot/Agent 链路收口修复
22
- - 增加统一“最终流帧”主动推送逻辑,确保 `response_url` 可用时会主动收口。
23
- - 当回复内容为空时增加最终可见兜底文案,避免企微前端悬空态。
24
- - 非图片文件、媒体处理失败、超时切换等场景统一走“Bot 提示 + Agent 私信兜底”闭环。
25
-
26
- #### 【链路一致性】🧭 工具策略修复
27
- - WeCom Bot 会话中将 `message` 工具写入 `tools.deny`(并同步 sandbox deny),修复错误禁用位置造成的链路绕过。
28
- - 避免“文件已发出但 Bot 思考流未结束”的链路错位。
29
-
30
- #### 【文件兼容性】📎 MIME 与上传兜底增强
31
- - 扩展本地文件 MIME 推断覆盖:`txt/csv/tsv/md/json/xml/yaml/yml/pdf/doc/docx/xls/xlsx/ppt/pptx/zip/rar/7z/tar/gz/tgz/rtf/odt`。
32
- - 上传 multipart 时增加文件名规范化,降低特殊文件名导致的兼容性问题。
33
- - 新增“首选 MIME 失败 -> octet-stream 自动重试”机制,提升未知/边缘类型上传成功率。
34
-
35
- #### 【类型识别优化】🧠 入站文件名与后缀判定更准确
36
- - 下载并解密媒体时保留源信息(`content-type`、`content-disposition`、最终 URL),用于后续精确判断。
37
- - 新增三层文件类型判定策略(按优先级):
38
- - 二进制内容特征(Magic Number / 文件头)
39
- - 非泛型响应头 `content-type`
40
- - 文件名后缀映射
41
- - 文件名确定策略(按优先级):
42
- - 回调体显式文件名字段
43
- - 下载响应 `content-disposition` 文件名
44
- - URL 路径 basename(仅在看起来是有效文件名时采用)
45
- - 兜底默认名(自动补扩展名)
46
- - 对 OOXML(`docx/xlsx/pptx`)增加 ZIP 内容探测,降低仅靠 URL 无后缀时误判为 `zip/bin` 的概率。
47
-
48
- #### 【质量保障】✅ 回归测试补充
49
- - 新增上传链路单测,覆盖:
50
- - `.txt` 使用 `text/plain`
51
- - `.docx` 使用官方 MIME
52
- - 首次 MIME 失败时自动回退重试
53
- - 同步通过 WeCom monitor/outbound 相关回归测试,确保无行为回退。
54
-
55
- ---
56
-
57
- ### 💾 安装与升级 (Install & Update)
58
-
59
- 使用 **OpenClaw** CLI 一键升级 **插件**:
60
-
61
- ```bash
62
- openclaw plugins upgrade wecom
63
- ```
64
-
65
- 或手动更新版本至 `v2.3.2`。
66
-
67
- ---
68
-
69
- ### 📮 联系我们
70
- 如果您在 **企业微信 / 微信** 接入过程中遇到任何问题,欢迎提交 Issue 反馈日志与复现场景。
1
+ # OpenClaw WeCom 插件 v2.3.2 变更简报
2
+
3
+ > [!WARNING]
4
+ > **OpenClaw 3.1+ 升级必读**:升级到 OpenClaw `3.1` 及以上版本的用户务必同步升级本插件,并将企业微信回调 URL 更新为 OpenClaw 推荐路径:`/plugins/wecom/bot/{accountId}` 与 `/plugins/wecom/agent/{accountId}`(旧 `/wecom/*` 仍兼容但不再维护)。
5
+
6
+ ## 2026-03-03(今日)
7
+ - 【路由兼容】🧩 修复 OpenClaw 3.1 下 Control UI fallback 可能抢占 `/wecom/*` webhook 的路由冲突问题。
8
+ - 【引导收敛】🧭 将 WeCom onboarding 统一为账号化配置写入 `channels.wecom.accounts.<accountId>`,不再引导单账号旧结构。
9
+ - 【回调路径】🔁 WeCom 回调路径的推荐方案统一为 `/plugins/wecom/bot/{accountId}` 与 `/plugins/wecom/agent/{accountId}`。
10
+ - 【兼容策略】🔁 保留 `/wecom/*` 历史回调路径兼容能力,但不再维护旧路径分支。
11
+ - 【分流稳定】🧭 monitor 分流升级为按插件命名空间账号路径识别,确保 Bot/Agent 稳定命中。
12
+ - 【链路一致】🔒 Bot 上下文 `Surface` 对齐为 `wecom`,避免核心误判后错误走到 Agent outbound。
13
+ - 【账号必填】🧱 matrix 模式下对无 accountId 的基础路径返回 `wecom_matrix_path_required`,强制使用账号化回调路径。
14
+ - 【文档同步】📘 将回调地址文档与 onboarding 提示统一为 `/plugins/wecom/*/{accountId}` 唯一推荐路径。
15
+
16
+ ## 2026-03-02(v2.3.2 主体)
17
+ - 【交付收口】🔄 修复 Bot 结果回写后“正在搜索相关内容”不收口的问题,并在可用时推送最终流帧结束思考态。
18
+ - 【媒体兜底】📎 统一非图片文件、媒体失败和超时场景为“Bot 提示 + Agent 私信兜底”闭环,保证结果可达。
19
+ - 【工具治理】🛡 修复 WeCom Bot 会话中 `message` 工具禁用位置,避免模型绕过 Bot 交付链路直接主动发送。
20
+ - 【类型兼容】🧠 扩展本地与远端文件 MIME 识别覆盖 `txt/docx/xlsx/pptx/csv/zip` 等常见类型,并保留 `octet-stream` 重试兜底。
21
+ - 【判定增强】🔍 将入站文件类型推断升级为“文件头特征 + 响应头 + 文件名后缀”多层判定,提升无后缀和异常 URL 的识别准确率。
22
+
23
+ ## 验证结果
24
+ - WeCom 插件测试通过 `10` 个测试文件共 `41` 条用例,覆盖 webhook 生命周期、路径分流、媒体兜底与回归场景。
25
+
26
+ ## 升级提示
27
+ - 推荐在企业微信后台使用 `https://<your-domain>/plugins/wecom/bot/{accountId}` `https://<your-domain>/plugins/wecom/agent/{accountId}` 作为回调地址。
28
+ - 旧地址 `/wecom/bot/{accountId}` 与 `/wecom/agent/{accountId}` 仍兼容但不再维护,建议尽快迁移到 `/plugins/wecom/*/{accountId}`。
@@ -0,0 +1,20 @@
1
+ # OpenClaw WeCom 插件 v2.3.4 变更简报
2
+
3
+ > [!WARNING]
4
+ > **可用性热修复版本**:`v2.3.4` 重点修复 OpenClaw 3.1+ 下 WeCom webhook 路由兼容与可达性问题,保障历史配置可继续使用。
5
+
6
+ ## 2026-03-03(v2.3.4 热修复)
7
+ - 【SDK适配】♻️ 插件入口从 `registerHttpHandler` 迁移为 `registerHttpRoute`(`/plugins/wecom` + `match=prefix` + `auth=plugin`),兼容 OpenClaw 新版插件 HTTP 注册模型。
8
+ - 【兼容修复】🔁 补回旧路径入口注册:新增 `/wecom` 前缀路由注册,确保历史 Bot/Agent webhook 继续可达。
9
+ - 【模式兼容】🧭 保持 legacy 单账号配置可运行,并明确 matrix 模式必须使用带 `accountId` 的回调路径。
10
+
11
+ ## 影响说明
12
+ - WeCom Bot/Agent 业务链路保持兼容;核心变化为插件 HTTP 注册 API 的升级适配与旧路径可达性恢复。
13
+
14
+ ## 兼容矩阵(单账号/多账号)
15
+ | 场景 | 配置形态 | 回调地址 | 兼容状态 | 说明 |
16
+ |---|---|---|---|---|
17
+ | 历史单账号(legacy) | `channels.wecom.bot/agent` | Bot: `/wecom`(默认)或 `/wecom/bot`;Agent: `/wecom/agent` | ✅ 兼容保留 | 适用于存量部署,不作为新引导方案。 |
18
+ | 多账号(matrix)错误用法 | `channels.wecom.accounts.*` | `/wecom/bot`、`/wecom/agent`(无 accountId) | ❌ 不可用 | 会返回 `wecom_matrix_path_required`。 |
19
+ | 多账号(matrix)兼容路径 | `channels.wecom.accounts.*` | `/wecom/bot/{accountId}`、`/wecom/agent/{accountId}` | ✅ 兼容保留 | 历史路径可用,但不再维护。 |
20
+ | 多账号(matrix)推荐路径 | `channels.wecom.accounts.*` | `/plugins/wecom/bot/{accountId}`、`/plugins/wecom/agent/{accountId}` | ✅ 推荐 | 当前主维护路径。 |
@@ -108,7 +108,7 @@ openclaw channels status
108
108
 
109
109
  ## A.4 Webhook 路径
110
110
 
111
- - Bot: `/wecom/bot`
111
+ - Bot: `/wecom`(默认)或 `/wecom/bot`
112
112
  - Agent: `/wecom/agent`
113
113
 
114
114
  ## A.5 迁移建议
package/index.test.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
3
+ import plugin from "./index.js";
4
+
5
+ describe("wecom plugin register", () => {
6
+ it("registers both recommended and legacy webhook route prefixes", () => {
7
+ const registerChannel = vi.fn();
8
+ const registerHttpRoute = vi.fn();
9
+ const api = {
10
+ runtime: {},
11
+ registerChannel,
12
+ registerHttpRoute,
13
+ } as unknown as OpenClawPluginApi;
14
+
15
+ plugin.register(api);
16
+
17
+ expect(registerChannel).toHaveBeenCalledTimes(1);
18
+ expect(registerHttpRoute).toHaveBeenCalledTimes(2);
19
+ expect(registerHttpRoute).toHaveBeenCalledWith(
20
+ expect.objectContaining({
21
+ path: "/plugins/wecom",
22
+ auth: "plugin",
23
+ match: "prefix",
24
+ }),
25
+ );
26
+ expect(registerHttpRoute).toHaveBeenCalledWith(
27
+ expect.objectContaining({
28
+ path: "/wecom",
29
+ auth: "plugin",
30
+ match: "prefix",
31
+ }),
32
+ );
33
+ });
34
+ });
package/index.ts CHANGED
@@ -15,16 +15,24 @@ const plugin = {
15
15
  configSchema: emptyPluginConfigSchema(),
16
16
  /**
17
17
  * **register (注册插件)**
18
- *
18
+ *
19
19
  * OpenClaw 插件入口点。
20
20
  * 1. 注入 Runtime 环境 (api.runtime)。
21
21
  * 2. 注册 WeCom 渠道插件 (ChannelPlugin)。
22
- * 3. 注册 Webhook HTTP 处理器 (handleWecomWebhookRequest)。
22
+ * 3. 注册 Webhook HTTP 路由(推荐 /plugins/wecom/*,兼容 /wecom*)。
23
23
  */
24
24
  register(api: OpenClawPluginApi) {
25
25
  setWecomRuntime(api.runtime);
26
26
  api.registerChannel({ plugin: wecomPlugin });
27
- api.registerHttpHandler(handleWecomWebhookRequest);
27
+ const routes = ["/plugins/wecom", "/wecom"];
28
+ for (const path of routes) {
29
+ api.registerHttpRoute({
30
+ path,
31
+ handler: handleWecomWebhookRequest,
32
+ auth: "plugin",
33
+ match: "prefix",
34
+ });
35
+ }
28
36
  },
29
37
  };
30
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yanhaidao/wecom",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
4
4
  "type": "module",
5
5
  "description": "OpenClaw WeCom (WeChat Work) intelligent bot channel plugin",
6
6
  "repository": {
@@ -178,26 +178,44 @@ describe("wecomPlugin gateway lifecycle", () => {
178
178
  const startPromise = wecomPlugin.gateway!.startAccount!(ctx);
179
179
  await Promise.resolve();
180
180
 
181
- const active = await sendWecomGetVerify({
181
+ const activeLegacyRoute = await sendWecomGetVerify({
182
182
  path: "/wecom/bot",
183
183
  token,
184
184
  encodingAESKey,
185
185
  receiveId,
186
186
  });
187
- expect(active.handled).toBe(true);
188
- expect(active.status).toBe(200);
189
- expect(active.body).toBe("ping");
187
+ expect(activeLegacyRoute.handled).toBe(true);
188
+ expect(activeLegacyRoute.status).toBe(200);
189
+ expect(activeLegacyRoute.body).toBe("ping");
190
+
191
+ const activePluginRoute = await sendWecomGetVerify({
192
+ path: "/plugins/wecom/bot",
193
+ token,
194
+ encodingAESKey,
195
+ receiveId,
196
+ });
197
+ expect(activePluginRoute.handled).toBe(true);
198
+ expect(activePluginRoute.status).toBe(200);
199
+ expect(activePluginRoute.body).toBe("ping");
190
200
 
191
201
  abortController.abort();
192
202
  await startPromise;
193
203
 
194
- const inactive = await sendWecomGetVerify({
204
+ const inactiveLegacyRoute = await sendWecomGetVerify({
195
205
  path: "/wecom/bot",
196
206
  token,
197
207
  encodingAESKey,
198
208
  receiveId,
199
209
  });
200
- expect(inactive.handled).toBe(false);
210
+ expect(inactiveLegacyRoute.handled).toBe(false);
211
+
212
+ const inactivePluginRoute = await sendWecomGetVerify({
213
+ path: "/plugins/wecom/bot",
214
+ token,
215
+ encodingAESKey,
216
+ receiveId,
217
+ });
218
+ expect(inactivePluginRoute.handled).toBe(false);
201
219
  });
202
220
 
203
221
  it("rejects startup when matrix account credentials conflict", async () => {
package/src/channel.ts CHANGED
@@ -19,6 +19,7 @@ import type { ResolvedWecomAccount } from "./types/index.js";
19
19
  import { monitorWecomProvider } from "./gateway-monitor.js";
20
20
  import { wecomOnboardingAdapter } from "./onboarding.js";
21
21
  import { wecomOutbound } from "./outbound.js";
22
+ import { WEBHOOK_PATHS } from "./types/constants.js";
22
23
 
23
24
  const meta = {
24
25
  id: "wecom",
@@ -108,10 +109,10 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
108
109
  enabled: account.enabled,
109
110
  configured: account.configured && !conflict,
110
111
  webhookPath: account.bot?.config
111
- ? (matrixMode ? `/wecom/bot/${account.accountId}` : "/wecom/bot")
112
+ ? (matrixMode ? `${WEBHOOK_PATHS.BOT_PLUGIN}/${account.accountId}` : WEBHOOK_PATHS.BOT_PLUGIN)
112
113
  : account.agent?.config
113
- ? (matrixMode ? `/wecom/agent/${account.accountId}` : "/wecom/agent")
114
- : "/wecom",
114
+ ? (matrixMode ? `${WEBHOOK_PATHS.AGENT_PLUGIN}/${account.accountId}` : WEBHOOK_PATHS.AGENT_PLUGIN)
115
+ : WEBHOOK_PATHS.BOT_PLUGIN,
115
116
  };
116
117
  },
117
118
  resolveAllowFrom: ({ cfg, accountId }) => {
@@ -176,10 +177,14 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
176
177
  enabled: account.enabled,
177
178
  configured: account.configured && !conflict,
178
179
  webhookPath: account.bot?.config
179
- ? (account.accountId === DEFAULT_ACCOUNT_ID ? "/wecom/bot" : `/wecom/bot/${account.accountId}`)
180
+ ? (account.accountId === DEFAULT_ACCOUNT_ID
181
+ ? WEBHOOK_PATHS.BOT_PLUGIN
182
+ : `${WEBHOOK_PATHS.BOT_PLUGIN}/${account.accountId}`)
180
183
  : account.agent?.config
181
- ? (account.accountId === DEFAULT_ACCOUNT_ID ? "/wecom/agent" : `/wecom/agent/${account.accountId}`)
182
- : "/wecom",
184
+ ? (account.accountId === DEFAULT_ACCOUNT_ID
185
+ ? WEBHOOK_PATHS.AGENT_PLUGIN
186
+ : `${WEBHOOK_PATHS.AGENT_PLUGIN}/${account.accountId}`)
187
+ : WEBHOOK_PATHS.BOT_PLUGIN,
183
188
  running: runtime?.running ?? false,
184
189
  lastStartAt: runtime?.lastStartAt ?? null,
185
190
  lastStopAt: runtime?.lastStopAt ?? null,
@@ -12,10 +12,11 @@ import {
12
12
  } from "./config/index.js";
13
13
  import { registerAgentWebhookTarget, registerWecomWebhookTarget } from "./monitor.js";
14
14
  import type { ResolvedWecomAccount, WecomConfig } from "./types/index.js";
15
+ import { WEBHOOK_PATHS } from "./types/constants.js";
15
16
 
16
17
  type AccountRouteRegistryItem = {
17
18
  botPaths: string[];
18
- agentPath?: string;
19
+ agentPaths: string[];
19
20
  };
20
21
 
21
22
  const accountRouteRegistry = new Map<string, AccountRouteRegistryItem>();
@@ -41,7 +42,7 @@ function logRegisteredRouteSummary(
41
42
  const routes = accountRouteRegistry.get(accountId);
42
43
  if (!routes) return undefined;
43
44
  const botText = routes.botPaths.length > 0 ? routes.botPaths.join(", ") : "未启用";
44
- const agentText = routes.agentPath ?? "未启用";
45
+ const agentText = routes.agentPaths.length > 0 ? routes.agentPaths.join(", ") : "未启用";
45
46
  return `accountId=${accountId}(Bot: ${botText};Agent: ${agentText})`;
46
47
  })
47
48
  .filter((entry): entry is string => Boolean(entry));
@@ -74,6 +75,30 @@ function waitForAbortSignal(abortSignal: AbortSignal): Promise<void> {
74
75
  });
75
76
  }
76
77
 
78
+ function uniquePaths(paths: string[]): string[] {
79
+ return Array.from(new Set(paths.map((path) => path.trim()).filter(Boolean)));
80
+ }
81
+
82
+ function resolveBotRegistrationPaths(params: { accountId: string; matrixMode: boolean }): string[] {
83
+ if (params.matrixMode) {
84
+ return uniquePaths([
85
+ `${WEBHOOK_PATHS.BOT_PLUGIN}/${params.accountId}`,
86
+ `${WEBHOOK_PATHS.BOT_ALT}/${params.accountId}`,
87
+ ]);
88
+ }
89
+ return uniquePaths([WEBHOOK_PATHS.BOT_PLUGIN, WEBHOOK_PATHS.BOT, WEBHOOK_PATHS.BOT_ALT]);
90
+ }
91
+
92
+ function resolveAgentRegistrationPaths(params: { accountId: string; matrixMode: boolean }): string[] {
93
+ if (params.matrixMode) {
94
+ return uniquePaths([
95
+ `${WEBHOOK_PATHS.AGENT_PLUGIN}/${params.accountId}`,
96
+ `${WEBHOOK_PATHS.AGENT}/${params.accountId}`,
97
+ ]);
98
+ }
99
+ return uniquePaths([WEBHOOK_PATHS.AGENT_PLUGIN, WEBHOOK_PATHS.AGENT]);
100
+ }
101
+
77
102
  /**
78
103
  * Keeps WeCom webhook targets registered for the account lifecycle.
79
104
  * The promise only settles after gateway abort/reload signals shutdown.
@@ -129,12 +154,13 @@ export async function monitorWecomProvider(
129
154
 
130
155
  const unregisters: Array<() => void> = [];
131
156
  const botPaths: string[] = [];
132
- let agentPath: string | undefined;
157
+ const agentPaths: string[] = [];
133
158
  try {
134
159
  if (bot && botConfigured) {
135
- const paths = matrixMode
136
- ? [`/wecom/bot/${account.accountId}`]
137
- : ["/wecom", "/wecom/bot"];
160
+ const paths = resolveBotRegistrationPaths({
161
+ accountId: account.accountId,
162
+ matrixMode,
163
+ });
138
164
  for (const path of paths) {
139
165
  unregisters.push(
140
166
  registerWecomWebhookTarget({
@@ -154,20 +180,25 @@ export async function monitorWecomProvider(
154
180
  }
155
181
 
156
182
  if (agent && agentConfigured) {
157
- const path = matrixMode ? `/wecom/agent/${account.accountId}` : "/wecom/agent";
158
- unregisters.push(
159
- registerAgentWebhookTarget({
160
- agent,
161
- config: cfg,
162
- runtime: ctx.runtime,
163
- path,
164
- }),
165
- );
166
- agentPath = path;
167
- ctx.log?.info(`[${account.accountId}] wecom agent webhook registered at ${path}`);
183
+ const paths = resolveAgentRegistrationPaths({
184
+ accountId: account.accountId,
185
+ matrixMode,
186
+ });
187
+ for (const path of paths) {
188
+ unregisters.push(
189
+ registerAgentWebhookTarget({
190
+ agent,
191
+ config: cfg,
192
+ runtime: ctx.runtime,
193
+ path,
194
+ }),
195
+ );
196
+ }
197
+ agentPaths.push(...paths);
198
+ ctx.log?.info(`[${account.accountId}] wecom agent webhook registered at ${paths.join(", ")}`);
168
199
  }
169
200
 
170
- accountRouteRegistry.set(account.accountId, { botPaths, agentPath });
201
+ accountRouteRegistry.set(account.accountId, { botPaths, agentPaths });
171
202
  const shouldLogSummary =
172
203
  expectedRouteSummaryAccountIds.length <= 1 ||
173
204
  expectedRouteSummaryAccountIds.every((accountId) => accountRouteRegistry.has(accountId));
@@ -180,8 +211,8 @@ export async function monitorWecomProvider(
180
211
  running: true,
181
212
  configured: true,
182
213
  webhookPath: botConfigured
183
- ? (matrixMode ? `/wecom/bot/${account.accountId}` : "/wecom/bot")
184
- : (matrixMode ? `/wecom/agent/${account.accountId}` : "/wecom/agent"),
214
+ ? (botPaths[0] ?? WEBHOOK_PATHS.BOT_PLUGIN)
215
+ : (agentPaths[0] ?? WEBHOOK_PATHS.AGENT_PLUGIN),
185
216
  lastStartAt: Date.now(),
186
217
  });
187
218