@yanhaidao/wecom 2.3.2 → 2.3.3
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 +33 -28
- package/changelog/v2.3.2.md +28 -70
- package/package.json +1 -1
- package/src/channel.lifecycle.test.ts +24 -6
- package/src/channel.ts +11 -6
- package/src/gateway-monitor.ts +51 -20
- package/src/monitor.active.test.ts +2 -2
- package/src/monitor.integration.test.ts +4 -2
- package/src/monitor.ts +27 -10
- package/src/monitor.webhook.test.ts +104 -11
- package/src/onboarding.ts +219 -43
- package/src/types/constants.ts +7 -3
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
|
|
156
|
-
- Agent
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
304
|
+
### 2.5 DM 策略
|
|
316
305
|
|
|
317
306
|
- **不配置 `dm.allowFrom`** → 所有人可用(默认)
|
|
318
307
|
- **配置 `dm.allowFrom: ["user1", "user2"]`** → 白名单模式,仅列表内用户可私聊
|
|
319
308
|
|
|
320
|
-
### 2.
|
|
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,22 @@ Agent 输出 `{"template_card": ...}` 时自动渲染为交互卡片:
|
|
|
511
500
|
<a id="sec-10"></a>
|
|
512
501
|
## 八、📝 更新日志
|
|
513
502
|
|
|
503
|
+
### 2026.3.3(今日更新简报)
|
|
504
|
+
|
|
505
|
+
- 【兼容性修复】🧩 **OpenClaw 3.1 路由抢占问题修复**:推荐回调地址升级为 `/plugins/wecom/bot/{accountId}`、`/plugins/wecom/agent/{accountId}`,规避根路径 Control UI fallback 抢占 webhook。
|
|
506
|
+
- 【引导收敛】🧭 **Onboarding 仅支持账号化配置**:配置向导统一写入 `channels.wecom.accounts.<accountId>`,不再引导单账号旧结构。
|
|
507
|
+
- 【兼容策略】🔁 **旧路径兼容保留**:`/wecom/*` 历史回调路径保留兼容能力,但不再作为维护主路径。
|
|
508
|
+
- 【分流稳定性】🧭 **路由识别增强**:monitor 按插件命名空间账号路径识别,确保 Bot/Agent 分支稳定命中。
|
|
509
|
+
- 【链路一致性】🔒 **Bot 回复不再误走 Agent**:修复 Bot 上下文通道标识,避免 `routeReply` 误触发到 outbound 主动发送链路。
|
|
510
|
+
- 【验证结果】✅ WeCom 插件测试通过:`10` files / `41` tests。
|
|
511
|
+
|
|
512
|
+
### 2026.3.2(版本更新简报)
|
|
513
|
+
|
|
514
|
+
- 【交付收口】🔄 修复 Bot 会话“正在搜索相关内容”不结束的问题,并在可用时推送最终流帧结束状态。
|
|
515
|
+
- 【媒体兜底】📎 统一非图片文件、媒体失败和超时场景为“Bot 提示 + Agent 私信兜底”闭环,确保结果可达。
|
|
516
|
+
- 【类型兼容】🧠 扩展 `txt/docx/xlsx/pptx/csv/zip` 等常见文件类型识别,并保留 `application/octet-stream` 自动重试。
|
|
517
|
+
- 【工具治理】🛡 修复 Bot 会话 `message` 工具禁用策略,避免绕过 Bot 交付链路导致会话错位。
|
|
518
|
+
|
|
514
519
|
### 2026.2.28
|
|
515
520
|
|
|
516
521
|
- 【重磅更新】🎯 **多账号/多智能体可用性增强**:支持按 `accountId` 做组内隔离(Bot + Agent + 路由绑定同组生效),动态 Agent 与会话键增加 `accountId` 维度,避免跨账号串会话。
|
package/changelog/v2.3.2.md
CHANGED
|
@@ -1,70 +1,28 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
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}`。
|
package/package.json
CHANGED
|
@@ -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
|
|
181
|
+
const activeLegacyRoute = await sendWecomGetVerify({
|
|
182
182
|
path: "/wecom/bot",
|
|
183
183
|
token,
|
|
184
184
|
encodingAESKey,
|
|
185
185
|
receiveId,
|
|
186
186
|
});
|
|
187
|
-
expect(
|
|
188
|
-
expect(
|
|
189
|
-
expect(
|
|
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
|
|
204
|
+
const inactiveLegacyRoute = await sendWecomGetVerify({
|
|
195
205
|
path: "/wecom/bot",
|
|
196
206
|
token,
|
|
197
207
|
encodingAESKey,
|
|
198
208
|
receiveId,
|
|
199
209
|
});
|
|
200
|
-
expect(
|
|
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 ?
|
|
112
|
+
? (matrixMode ? `${WEBHOOK_PATHS.BOT_PLUGIN}/${account.accountId}` : WEBHOOK_PATHS.BOT_PLUGIN)
|
|
112
113
|
: account.agent?.config
|
|
113
|
-
? (matrixMode ?
|
|
114
|
-
:
|
|
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
|
|
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
|
|
182
|
-
|
|
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,
|
package/src/gateway-monitor.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
157
|
+
const agentPaths: string[] = [];
|
|
133
158
|
try {
|
|
134
159
|
if (bot && botConfigured) {
|
|
135
|
-
const paths =
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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,
|
|
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
|
-
? (
|
|
184
|
-
: (
|
|
214
|
+
? (botPaths[0] ?? WEBHOOK_PATHS.BOT_PLUGIN)
|
|
215
|
+
: (agentPaths[0] ?? WEBHOOK_PATHS.AGENT_PLUGIN),
|
|
185
216
|
lastStartAt: Date.now(),
|
|
186
217
|
});
|
|
187
218
|
|
|
@@ -28,7 +28,7 @@ function createMockRequest(bodyObj: any): IncomingMessage {
|
|
|
28
28
|
const socket = new Socket();
|
|
29
29
|
const req = new IncomingMessage(socket);
|
|
30
30
|
req.method = "POST";
|
|
31
|
-
req.url = "/wecom?timestamp=123&nonce=456&signature=789";
|
|
31
|
+
req.url = "/plugins/wecom/bot/default?timestamp=123&nonce=456&signature=789";
|
|
32
32
|
req.push(JSON.stringify(bodyObj));
|
|
33
33
|
req.push(null);
|
|
34
34
|
return req;
|
|
@@ -140,7 +140,7 @@ describe("Monitor Active Features", () => {
|
|
|
140
140
|
} as any,
|
|
141
141
|
runtime: { log: () => { } },
|
|
142
142
|
core: mockCore,
|
|
143
|
-
path: "/wecom"
|
|
143
|
+
path: "/plugins/wecom/bot/default"
|
|
144
144
|
});
|
|
145
145
|
});
|
|
146
146
|
|
|
@@ -21,7 +21,7 @@ function createMockRequest(bodyObj: any, query: URLSearchParams): IncomingMessag
|
|
|
21
21
|
const socket = new Socket();
|
|
22
22
|
const req = new IncomingMessage(socket);
|
|
23
23
|
req.method = "POST";
|
|
24
|
-
req.url = `/wecom?${query.toString()}`;
|
|
24
|
+
req.url = `/plugins/wecom/bot/default?${query.toString()}`;
|
|
25
25
|
req.push(JSON.stringify(bodyObj));
|
|
26
26
|
req.push(null);
|
|
27
27
|
return req;
|
|
@@ -108,7 +108,7 @@ describe("Monitor Integration: Inbound Image", () => {
|
|
|
108
108
|
config: {} as any,
|
|
109
109
|
runtime: { log: console.log, error: console.error },
|
|
110
110
|
core: mockCore as any,
|
|
111
|
-
path: "/wecom"
|
|
111
|
+
path: "/plugins/wecom/bot/default"
|
|
112
112
|
});
|
|
113
113
|
});
|
|
114
114
|
|
|
@@ -198,6 +198,8 @@ describe("Monitor Integration: Inbound Image", () => {
|
|
|
198
198
|
// Expect Context Injection
|
|
199
199
|
expect(ctx.MediaPath).toBe("/tmp/saved-image.jpg");
|
|
200
200
|
expect(ctx.MediaType).toBe("image/jpeg");
|
|
201
|
+
expect(ctx.Surface).toBe("wecom");
|
|
202
|
+
expect(ctx.OriginatingChannel).toBe("wecom");
|
|
201
203
|
|
|
202
204
|
expect(undiciFetch).toHaveBeenCalledWith(
|
|
203
205
|
imageUrl,
|
package/src/monitor.ts
CHANGED
|
@@ -220,16 +220,24 @@ type RouteFailureReason =
|
|
|
220
220
|
| "wecom_identity_mismatch"
|
|
221
221
|
| "wecom_matrix_path_required";
|
|
222
222
|
|
|
223
|
-
function
|
|
224
|
-
return
|
|
223
|
+
function isNonMatrixWecomBasePath(path: string): boolean {
|
|
224
|
+
return (
|
|
225
|
+
path === WEBHOOK_PATHS.BOT ||
|
|
226
|
+
path === WEBHOOK_PATHS.BOT_ALT ||
|
|
227
|
+
path === WEBHOOK_PATHS.AGENT ||
|
|
228
|
+
path === WEBHOOK_PATHS.BOT_PLUGIN ||
|
|
229
|
+
path === WEBHOOK_PATHS.AGENT_PLUGIN
|
|
230
|
+
);
|
|
225
231
|
}
|
|
226
232
|
|
|
227
233
|
function hasMatrixExplicitRoutesRegistered(): boolean {
|
|
228
234
|
for (const key of webhookTargets.keys()) {
|
|
229
235
|
if (key.startsWith(`${WEBHOOK_PATHS.BOT_ALT}/`)) return true;
|
|
236
|
+
if (key.startsWith(`${WEBHOOK_PATHS.BOT_PLUGIN}/`)) return true;
|
|
230
237
|
}
|
|
231
238
|
for (const key of agentTargets.keys()) {
|
|
232
239
|
if (key.startsWith(`${WEBHOOK_PATHS.AGENT}/`)) return true;
|
|
240
|
+
if (key.startsWith(`${WEBHOOK_PATHS.AGENT_PLUGIN}/`)) return true;
|
|
233
241
|
}
|
|
234
242
|
return false;
|
|
235
243
|
}
|
|
@@ -1549,7 +1557,11 @@ async function startAgentForStream(params: {
|
|
|
1549
1557
|
SenderName: userid,
|
|
1550
1558
|
SenderId: userid,
|
|
1551
1559
|
Provider: "wecom",
|
|
1552
|
-
Surface
|
|
1560
|
+
// Keep Surface aligned with OriginatingChannel for Bot-mode delivery.
|
|
1561
|
+
// If Surface is "webchat", core dispatch treats this as cross-channel
|
|
1562
|
+
// and routes replies via routeReply -> wecom outbound (Agent API),
|
|
1563
|
+
// bypassing the Bot stream deliver path.
|
|
1564
|
+
Surface: "wecom",
|
|
1553
1565
|
MessageSid: msg.msgid,
|
|
1554
1566
|
CommandAuthorized: commandAuthorized,
|
|
1555
1567
|
OriginatingChannel: "wecom",
|
|
@@ -2110,7 +2122,7 @@ export function registerAgentWebhookTarget(target: AgentWebhookTarget): () => vo
|
|
|
2110
2122
|
*
|
|
2111
2123
|
* 处理来自企业微信的所有 Webhook 请求。
|
|
2112
2124
|
* 职责:
|
|
2113
|
-
* 1.
|
|
2125
|
+
* 1. 路由分发:优先按 `/plugins/wecom/{bot|agent}/{accountId}` 分流,并兼容历史 `/wecom/*` 路径。
|
|
2114
2126
|
* 2. 安全校验:验证企业微信签名 (Signature)。
|
|
2115
2127
|
* 3. 消息解密:处理企业微信的加密包。
|
|
2116
2128
|
* 4. 响应处理:
|
|
@@ -2134,7 +2146,7 @@ export async function handleWecomWebhookRequest(req: IncomingMessage, res: Serve
|
|
|
2134
2146
|
`[wecom] inbound(http): reqId=${reqId} path=${path} method=${req.method ?? "UNKNOWN"} remote=${remote} ua=${ua ? `"${ua}"` : "N/A"} contentLength=${cl || "N/A"} query={timestamp:${hasTimestamp},nonce:${hasNonce},echostr:${hasEchostr},msg_signature:${hasMsgSig},signature:${hasSignature}}`,
|
|
2135
2147
|
);
|
|
2136
2148
|
|
|
2137
|
-
if (hasMatrixExplicitRoutesRegistered() &&
|
|
2149
|
+
if (hasMatrixExplicitRoutesRegistered() && isNonMatrixWecomBasePath(path)) {
|
|
2138
2150
|
logRouteFailure({
|
|
2139
2151
|
reqId,
|
|
2140
2152
|
path,
|
|
@@ -2145,14 +2157,19 @@ export async function handleWecomWebhookRequest(req: IncomingMessage, res: Serve
|
|
|
2145
2157
|
writeRouteFailure(
|
|
2146
2158
|
res,
|
|
2147
2159
|
"wecom_matrix_path_required",
|
|
2148
|
-
"Matrix mode requires explicit account path. Use /wecom/bot/{accountId} or /wecom/agent/{accountId}.",
|
|
2160
|
+
"Matrix mode requires explicit account path. Use /plugins/wecom/bot/{accountId} or /plugins/wecom/agent/{accountId}.",
|
|
2149
2161
|
);
|
|
2150
2162
|
return true;
|
|
2151
2163
|
}
|
|
2152
2164
|
|
|
2153
|
-
const
|
|
2154
|
-
|
|
2155
|
-
|
|
2165
|
+
const isAgentPathCandidate =
|
|
2166
|
+
path === WEBHOOK_PATHS.AGENT ||
|
|
2167
|
+
path === WEBHOOK_PATHS.AGENT_PLUGIN ||
|
|
2168
|
+
path.startsWith(`${WEBHOOK_PATHS.AGENT}/`) ||
|
|
2169
|
+
path.startsWith(`${WEBHOOK_PATHS.AGENT_PLUGIN}/`);
|
|
2170
|
+
const matchedAgentTargets = agentTargets.get(path) ?? [];
|
|
2171
|
+
if (matchedAgentTargets.length > 0 || isAgentPathCandidate) {
|
|
2172
|
+
const targets = matchedAgentTargets;
|
|
2156
2173
|
if (targets.length > 0) {
|
|
2157
2174
|
const query = resolveQueryParams(req);
|
|
2158
2175
|
const timestamp = query.get("timestamp") ?? "";
|
|
@@ -2328,7 +2345,7 @@ export async function handleWecomWebhookRequest(req: IncomingMessage, res: Serve
|
|
|
2328
2345
|
return true;
|
|
2329
2346
|
}
|
|
2330
2347
|
|
|
2331
|
-
// Bot 模式路由: /wecom
|
|
2348
|
+
// Bot 模式路由: /plugins/wecom/bot(推荐)以及 /wecom、/wecom/bot(兼容)
|
|
2332
2349
|
const targets = webhookTargets.get(path);
|
|
2333
2350
|
if (!targets || targets.length === 0) return false;
|
|
2334
2351
|
|
|
@@ -369,7 +369,7 @@ describe("handleWecomWebhookRequest", () => {
|
|
|
369
369
|
}
|
|
370
370
|
});
|
|
371
371
|
|
|
372
|
-
it("routes
|
|
372
|
+
it("routes bot callback by explicit plugin account path", async () => {
|
|
373
373
|
const token = "MATRIX-TOKEN";
|
|
374
374
|
const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
|
|
375
375
|
|
|
@@ -390,7 +390,7 @@ describe("handleWecomWebhookRequest", () => {
|
|
|
390
390
|
config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
|
|
391
391
|
runtime: {},
|
|
392
392
|
core: {} as any,
|
|
393
|
-
path: "/wecom/bot/acct-a",
|
|
393
|
+
path: "/plugins/wecom/bot/acct-a",
|
|
394
394
|
});
|
|
395
395
|
const unregisterB = registerWecomWebhookTarget({
|
|
396
396
|
account: {
|
|
@@ -409,12 +409,12 @@ describe("handleWecomWebhookRequest", () => {
|
|
|
409
409
|
config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
|
|
410
410
|
runtime: {},
|
|
411
411
|
core: {} as any,
|
|
412
|
-
path: "/wecom/bot/acct-b",
|
|
412
|
+
path: "/plugins/wecom/bot/acct-b",
|
|
413
413
|
});
|
|
414
414
|
|
|
415
415
|
try {
|
|
416
416
|
const timestamp = "1700000999";
|
|
417
|
-
const nonce = "nonce-
|
|
417
|
+
const nonce = "nonce-plugin-account";
|
|
418
418
|
const plain = JSON.stringify({
|
|
419
419
|
msgid: "MATRIX-MSG-1",
|
|
420
420
|
aibotid: "BOT_B",
|
|
@@ -422,13 +422,94 @@ describe("handleWecomWebhookRequest", () => {
|
|
|
422
422
|
from: { userid: "USERID_B" },
|
|
423
423
|
response_url: "RESPONSEURL",
|
|
424
424
|
msgtype: "text",
|
|
425
|
-
text: { content: "hello
|
|
425
|
+
text: { content: "hello plugin account path" },
|
|
426
426
|
});
|
|
427
427
|
const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
|
|
428
428
|
const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });
|
|
429
429
|
const req = createMockRequest({
|
|
430
430
|
method: "POST",
|
|
431
|
-
url: `/wecom/bot/acct-b?msg_signature=${encodeURIComponent(msg_signature)}×tamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
|
|
431
|
+
url: `/plugins/wecom/bot/acct-b?msg_signature=${encodeURIComponent(msg_signature)}×tamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
|
|
432
|
+
body: { encrypt },
|
|
433
|
+
});
|
|
434
|
+
const res = createMockResponse();
|
|
435
|
+
const handled = await handleWecomWebhookRequest(req, res);
|
|
436
|
+
expect(handled).toBe(true);
|
|
437
|
+
expect(res._getStatusCode()).toBe(200);
|
|
438
|
+
|
|
439
|
+
const json = JSON.parse(res._getData()) as any;
|
|
440
|
+
const replyPlain = decryptWecomEncrypted({
|
|
441
|
+
encodingAESKey,
|
|
442
|
+
receiveId: "",
|
|
443
|
+
encrypt: json.encrypt,
|
|
444
|
+
});
|
|
445
|
+
const reply = JSON.parse(replyPlain) as any;
|
|
446
|
+
expect(reply.stream?.content).toBe("B处理中");
|
|
447
|
+
} finally {
|
|
448
|
+
unregisterA();
|
|
449
|
+
unregisterB();
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("routes bot callback by explicit plugin namespace path", async () => {
|
|
454
|
+
const token = "MATRIX-TOKEN-PLUGIN";
|
|
455
|
+
const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
|
|
456
|
+
|
|
457
|
+
const unregisterA = registerWecomWebhookTarget({
|
|
458
|
+
account: {
|
|
459
|
+
accountId: "acct-a",
|
|
460
|
+
enabled: true,
|
|
461
|
+
configured: true,
|
|
462
|
+
token,
|
|
463
|
+
encodingAESKey,
|
|
464
|
+
receiveId: "",
|
|
465
|
+
config: {
|
|
466
|
+
token,
|
|
467
|
+
encodingAESKey,
|
|
468
|
+
streamPlaceholderContent: "A处理中",
|
|
469
|
+
} as any,
|
|
470
|
+
} as any,
|
|
471
|
+
config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
|
|
472
|
+
runtime: {},
|
|
473
|
+
core: {} as any,
|
|
474
|
+
path: "/plugins/wecom/bot/acct-a",
|
|
475
|
+
});
|
|
476
|
+
const unregisterB = registerWecomWebhookTarget({
|
|
477
|
+
account: {
|
|
478
|
+
accountId: "acct-b",
|
|
479
|
+
enabled: true,
|
|
480
|
+
configured: true,
|
|
481
|
+
token,
|
|
482
|
+
encodingAESKey,
|
|
483
|
+
receiveId: "",
|
|
484
|
+
config: {
|
|
485
|
+
token,
|
|
486
|
+
encodingAESKey,
|
|
487
|
+
streamPlaceholderContent: "B处理中",
|
|
488
|
+
} as any,
|
|
489
|
+
} as any,
|
|
490
|
+
config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
|
|
491
|
+
runtime: {},
|
|
492
|
+
core: {} as any,
|
|
493
|
+
path: "/plugins/wecom/bot/acct-b",
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
const timestamp = "1700001000";
|
|
498
|
+
const nonce = "nonce-matrix-plugin";
|
|
499
|
+
const plain = JSON.stringify({
|
|
500
|
+
msgid: "MATRIX-MSG-PLUGIN-1",
|
|
501
|
+
aibotid: "BOT_B",
|
|
502
|
+
chattype: "single",
|
|
503
|
+
from: { userid: "USERID_B_PLUGIN" },
|
|
504
|
+
response_url: "RESPONSEURL",
|
|
505
|
+
msgtype: "text",
|
|
506
|
+
text: { content: "hello matrix plugin path" },
|
|
507
|
+
});
|
|
508
|
+
const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
|
|
509
|
+
const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });
|
|
510
|
+
const req = createMockRequest({
|
|
511
|
+
method: "POST",
|
|
512
|
+
url: `/plugins/wecom/bot/acct-b?msg_signature=${encodeURIComponent(msg_signature)}×tamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
|
|
432
513
|
body: { encrypt },
|
|
433
514
|
});
|
|
434
515
|
const res = createMockResponse();
|
|
@@ -501,7 +582,7 @@ describe("handleWecomWebhookRequest", () => {
|
|
|
501
582
|
}
|
|
502
583
|
});
|
|
503
584
|
|
|
504
|
-
it("rejects legacy
|
|
585
|
+
it("rejects legacy paths and accountless plugin paths", async () => {
|
|
505
586
|
const token = "MATRIX-TOKEN-3";
|
|
506
587
|
const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
|
|
507
588
|
const unregister = registerWecomWebhookTarget({
|
|
@@ -517,7 +598,7 @@ describe("handleWecomWebhookRequest", () => {
|
|
|
517
598
|
config: { channels: { wecom: { accounts: { "acct-a": { bot: {} } } } } } as OpenClawConfig,
|
|
518
599
|
runtime: {},
|
|
519
600
|
core: {} as any,
|
|
520
|
-
path: "/wecom/bot/acct-a",
|
|
601
|
+
path: "/plugins/wecom/bot/acct-a",
|
|
521
602
|
});
|
|
522
603
|
try {
|
|
523
604
|
const req = createMockRequest({
|
|
@@ -531,6 +612,18 @@ describe("handleWecomWebhookRequest", () => {
|
|
|
531
612
|
expect(JSON.parse(res._getData())).toMatchObject({
|
|
532
613
|
error: "wecom_matrix_path_required",
|
|
533
614
|
});
|
|
615
|
+
|
|
616
|
+
const pluginReq = createMockRequest({
|
|
617
|
+
method: "GET",
|
|
618
|
+
url: "/plugins/wecom/bot?timestamp=t&nonce=n&msg_signature=s&echostr=e",
|
|
619
|
+
});
|
|
620
|
+
const pluginRes = createMockResponse();
|
|
621
|
+
const pluginHandled = await handleWecomWebhookRequest(pluginReq, pluginRes);
|
|
622
|
+
expect(pluginHandled).toBe(true);
|
|
623
|
+
expect(pluginRes._getStatusCode()).toBe(401);
|
|
624
|
+
expect(JSON.parse(pluginRes._getData())).toMatchObject({
|
|
625
|
+
error: "wecom_matrix_path_required",
|
|
626
|
+
});
|
|
534
627
|
} finally {
|
|
535
628
|
unregister();
|
|
536
629
|
}
|
|
@@ -557,7 +650,7 @@ describe("handleWecomWebhookRequest", () => {
|
|
|
557
650
|
},
|
|
558
651
|
config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
|
|
559
652
|
runtime: {},
|
|
560
|
-
path: "/wecom/agent",
|
|
653
|
+
path: "/plugins/wecom/agent/default",
|
|
561
654
|
} as any);
|
|
562
655
|
const unregisterB = registerAgentWebhookTarget({
|
|
563
656
|
agent: {
|
|
@@ -573,13 +666,13 @@ describe("handleWecomWebhookRequest", () => {
|
|
|
573
666
|
},
|
|
574
667
|
config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
|
|
575
668
|
runtime: {},
|
|
576
|
-
path: "/wecom/agent",
|
|
669
|
+
path: "/plugins/wecom/agent/default",
|
|
577
670
|
} as any);
|
|
578
671
|
|
|
579
672
|
try {
|
|
580
673
|
const req = createMockRequest({
|
|
581
674
|
method: "GET",
|
|
582
|
-
url: `/wecom/agent?msg_signature=${encodeURIComponent(signature)}×tamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}&echostr=${encodeURIComponent(echostr)}`,
|
|
675
|
+
url: `/plugins/wecom/agent/default?msg_signature=${encodeURIComponent(signature)}×tamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}&echostr=${encodeURIComponent(echostr)}`,
|
|
583
676
|
});
|
|
584
677
|
const res = createMockResponse();
|
|
585
678
|
const handled = await handleWecomWebhookRequest(req, res);
|
package/src/onboarding.ts
CHANGED
|
@@ -9,8 +9,9 @@ import type {
|
|
|
9
9
|
OpenClawConfig,
|
|
10
10
|
WizardPrompter,
|
|
11
11
|
} from "openclaw/plugin-sdk";
|
|
12
|
-
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
|
|
13
|
-
import
|
|
12
|
+
import { DEFAULT_ACCOUNT_ID, promptAccountId } from "openclaw/plugin-sdk";
|
|
13
|
+
import { listWecomAccountIds, resolveDefaultWecomAccountId, resolveWecomAccount, resolveWecomAccounts } from "./config/index.js";
|
|
14
|
+
import type { WecomConfig, WecomBotConfig, WecomAgentConfig, WecomDmConfig, WecomAccountConfig } from "./types/index.js";
|
|
14
15
|
|
|
15
16
|
const channel = "wecom" as const;
|
|
16
17
|
|
|
@@ -37,29 +38,118 @@ function setWecomEnabled(cfg: OpenClawConfig, enabled: boolean): OpenClawConfig
|
|
|
37
38
|
} as OpenClawConfig;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function
|
|
41
|
+
function shouldUseAccountScopedConfig(wecom: WecomConfig | undefined, accountId: string): boolean {
|
|
42
|
+
void wecom;
|
|
43
|
+
void accountId;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ensureMatrixAccounts(wecom: WecomConfig): WecomConfig {
|
|
48
|
+
const accounts = wecom.accounts ?? {};
|
|
49
|
+
if (Object.keys(accounts).length > 0) {
|
|
50
|
+
return wecom;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!wecom.bot && !wecom.agent) {
|
|
54
|
+
return wecom;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { bot: legacyBot, agent: legacyAgent, ...rest } = wecom;
|
|
58
|
+
const defaultAccount: WecomAccountConfig = {
|
|
59
|
+
enabled: true,
|
|
60
|
+
...(legacyBot ? { bot: legacyBot } : {}),
|
|
61
|
+
...(legacyAgent ? { agent: legacyAgent } : {}),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
...rest,
|
|
66
|
+
defaultAccount: rest.defaultAccount?.trim() || DEFAULT_ACCOUNT_ID,
|
|
67
|
+
accounts: {
|
|
68
|
+
[DEFAULT_ACCOUNT_ID]: defaultAccount,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function accountWebhookPath(kind: "bot" | "agent", accountId: string): string {
|
|
74
|
+
const recommendedBase = kind === "bot" ? "/plugins/wecom/bot" : "/plugins/wecom/agent";
|
|
75
|
+
return `${recommendedBase}/${accountId}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function setWecomBotConfig(cfg: OpenClawConfig, bot: WecomBotConfig, accountId: string): OpenClawConfig {
|
|
79
|
+
const wecom = getWecomConfig(cfg) ?? {};
|
|
80
|
+
if (!shouldUseAccountScopedConfig(wecom, accountId)) {
|
|
81
|
+
return {
|
|
82
|
+
...cfg,
|
|
83
|
+
channels: {
|
|
84
|
+
...cfg.channels,
|
|
85
|
+
wecom: {
|
|
86
|
+
...wecom,
|
|
87
|
+
enabled: true,
|
|
88
|
+
bot,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
} as OpenClawConfig;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const matrixWecom = ensureMatrixAccounts(wecom);
|
|
95
|
+
const accounts = matrixWecom.accounts ?? {};
|
|
96
|
+
const existingAccount = accounts[accountId] ?? {};
|
|
41
97
|
return {
|
|
42
98
|
...cfg,
|
|
43
99
|
channels: {
|
|
44
100
|
...cfg.channels,
|
|
45
101
|
wecom: {
|
|
46
|
-
...
|
|
102
|
+
...matrixWecom,
|
|
47
103
|
enabled: true,
|
|
48
|
-
|
|
104
|
+
defaultAccount: matrixWecom.defaultAccount?.trim() || DEFAULT_ACCOUNT_ID,
|
|
105
|
+
accounts: {
|
|
106
|
+
...accounts,
|
|
107
|
+
[accountId]: {
|
|
108
|
+
...existingAccount,
|
|
109
|
+
enabled: existingAccount.enabled ?? true,
|
|
110
|
+
bot,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
49
113
|
},
|
|
50
114
|
},
|
|
51
115
|
} as OpenClawConfig;
|
|
52
116
|
}
|
|
53
117
|
|
|
54
|
-
function setWecomAgentConfig(cfg: OpenClawConfig, agent: WecomAgentConfig): OpenClawConfig {
|
|
118
|
+
function setWecomAgentConfig(cfg: OpenClawConfig, agent: WecomAgentConfig, accountId: string): OpenClawConfig {
|
|
119
|
+
const wecom = getWecomConfig(cfg) ?? {};
|
|
120
|
+
if (!shouldUseAccountScopedConfig(wecom, accountId)) {
|
|
121
|
+
return {
|
|
122
|
+
...cfg,
|
|
123
|
+
channels: {
|
|
124
|
+
...cfg.channels,
|
|
125
|
+
wecom: {
|
|
126
|
+
...wecom,
|
|
127
|
+
enabled: true,
|
|
128
|
+
agent,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
} as OpenClawConfig;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const matrixWecom = ensureMatrixAccounts(wecom);
|
|
135
|
+
const accounts = matrixWecom.accounts ?? {};
|
|
136
|
+
const existingAccount = accounts[accountId] ?? {};
|
|
55
137
|
return {
|
|
56
138
|
...cfg,
|
|
57
139
|
channels: {
|
|
58
140
|
...cfg.channels,
|
|
59
141
|
wecom: {
|
|
60
|
-
...
|
|
142
|
+
...matrixWecom,
|
|
61
143
|
enabled: true,
|
|
62
|
-
|
|
144
|
+
defaultAccount: matrixWecom.defaultAccount?.trim() || DEFAULT_ACCOUNT_ID,
|
|
145
|
+
accounts: {
|
|
146
|
+
...accounts,
|
|
147
|
+
[accountId]: {
|
|
148
|
+
...existingAccount,
|
|
149
|
+
enabled: existingAccount.enabled ?? true,
|
|
150
|
+
agent,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
63
153
|
},
|
|
64
154
|
},
|
|
65
155
|
} as OpenClawConfig;
|
|
@@ -69,8 +159,49 @@ function setWecomDmPolicy(
|
|
|
69
159
|
cfg: OpenClawConfig,
|
|
70
160
|
mode: "bot" | "agent",
|
|
71
161
|
dm: WecomDmConfig,
|
|
162
|
+
accountId: string,
|
|
72
163
|
): OpenClawConfig {
|
|
73
164
|
const wecom = getWecomConfig(cfg) ?? {};
|
|
165
|
+
if (shouldUseAccountScopedConfig(wecom, accountId)) {
|
|
166
|
+
const matrixWecom = ensureMatrixAccounts(wecom);
|
|
167
|
+
const accounts = matrixWecom.accounts ?? {};
|
|
168
|
+
const existingAccount = accounts[accountId] ?? {};
|
|
169
|
+
const nextAccount: WecomAccountConfig =
|
|
170
|
+
mode === "bot"
|
|
171
|
+
? {
|
|
172
|
+
...existingAccount,
|
|
173
|
+
bot: {
|
|
174
|
+
...existingAccount.bot,
|
|
175
|
+
dm,
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
: {
|
|
179
|
+
...existingAccount,
|
|
180
|
+
agent: {
|
|
181
|
+
...existingAccount.agent,
|
|
182
|
+
dm,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
...cfg,
|
|
187
|
+
channels: {
|
|
188
|
+
...cfg.channels,
|
|
189
|
+
wecom: {
|
|
190
|
+
...matrixWecom,
|
|
191
|
+
enabled: true,
|
|
192
|
+
defaultAccount: matrixWecom.defaultAccount?.trim() || DEFAULT_ACCOUNT_ID,
|
|
193
|
+
accounts: {
|
|
194
|
+
...accounts,
|
|
195
|
+
[accountId]: {
|
|
196
|
+
...nextAccount,
|
|
197
|
+
enabled: nextAccount.enabled ?? true,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
} as OpenClawConfig;
|
|
203
|
+
}
|
|
204
|
+
|
|
74
205
|
if (mode === "bot") {
|
|
75
206
|
return {
|
|
76
207
|
...cfg,
|
|
@@ -101,6 +232,28 @@ function setWecomDmPolicy(
|
|
|
101
232
|
} as OpenClawConfig;
|
|
102
233
|
}
|
|
103
234
|
|
|
235
|
+
async function resolveOnboardingAccountId(params: {
|
|
236
|
+
cfg: OpenClawConfig;
|
|
237
|
+
prompter: WizardPrompter;
|
|
238
|
+
accountOverride?: string;
|
|
239
|
+
shouldPromptAccountIds: boolean;
|
|
240
|
+
}): Promise<string> {
|
|
241
|
+
const defaultAccountId = resolveDefaultWecomAccountId(params.cfg);
|
|
242
|
+
const override = params.accountOverride?.trim();
|
|
243
|
+
let accountId = override || defaultAccountId;
|
|
244
|
+
if (!override && params.shouldPromptAccountIds) {
|
|
245
|
+
accountId = await promptAccountId({
|
|
246
|
+
cfg: params.cfg,
|
|
247
|
+
prompter: params.prompter,
|
|
248
|
+
label: "WeCom",
|
|
249
|
+
currentId: accountId,
|
|
250
|
+
listAccountIds: (cfg) => listWecomAccountIds(cfg),
|
|
251
|
+
defaultAccountId,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return accountId.trim() || DEFAULT_ACCOUNT_ID;
|
|
255
|
+
}
|
|
256
|
+
|
|
104
257
|
// ============================================================
|
|
105
258
|
// 欢迎与引导
|
|
106
259
|
// ============================================================
|
|
@@ -155,13 +308,15 @@ async function promptMode(prompter: WizardPrompter): Promise<WecomMode> {
|
|
|
155
308
|
async function configureBotMode(
|
|
156
309
|
cfg: OpenClawConfig,
|
|
157
310
|
prompter: WizardPrompter,
|
|
311
|
+
accountId: string,
|
|
158
312
|
): Promise<OpenClawConfig> {
|
|
313
|
+
const recommendedPath = accountWebhookPath("bot", accountId);
|
|
159
314
|
await prompter.note(
|
|
160
315
|
[
|
|
161
316
|
"正在配置 Bot 模式...",
|
|
162
317
|
"",
|
|
163
318
|
"💡 操作指南: 请在企微后台【管理工具 -> 智能机器人】开启 API 模式。",
|
|
164
|
-
|
|
319
|
+
`🔗 回调 URL (推荐): https://您的域名${recommendedPath}`,
|
|
165
320
|
"",
|
|
166
321
|
"请先在后台填入回调 URL,然后获取以下信息。",
|
|
167
322
|
].join("\n"),
|
|
@@ -206,7 +361,7 @@ async function configureBotMode(
|
|
|
206
361
|
welcomeText: welcomeText?.trim() || undefined,
|
|
207
362
|
};
|
|
208
363
|
|
|
209
|
-
return setWecomBotConfig(cfg, botConfig);
|
|
364
|
+
return setWecomBotConfig(cfg, botConfig, accountId);
|
|
210
365
|
}
|
|
211
366
|
|
|
212
367
|
// ============================================================
|
|
@@ -216,7 +371,9 @@ async function configureBotMode(
|
|
|
216
371
|
async function configureAgentMode(
|
|
217
372
|
cfg: OpenClawConfig,
|
|
218
373
|
prompter: WizardPrompter,
|
|
374
|
+
accountId: string,
|
|
219
375
|
): Promise<OpenClawConfig> {
|
|
376
|
+
const recommendedPath = accountWebhookPath("agent", accountId);
|
|
220
377
|
await prompter.note(
|
|
221
378
|
[
|
|
222
379
|
"正在配置 Agent 模式...",
|
|
@@ -256,7 +413,7 @@ async function configureAgentMode(
|
|
|
256
413
|
await prompter.note(
|
|
257
414
|
[
|
|
258
415
|
"💡 操作指南: 请在自建应用详情页进入【接收消息 -> 设置API接收】。",
|
|
259
|
-
|
|
416
|
+
`🔗 回调 URL (推荐): https://您的域名${recommendedPath}`,
|
|
260
417
|
"",
|
|
261
418
|
"请先在后台填入回调 URL,然后获取以下信息。",
|
|
262
419
|
].join("\n"),
|
|
@@ -297,7 +454,7 @@ async function configureAgentMode(
|
|
|
297
454
|
welcomeText: welcomeText?.trim() || undefined,
|
|
298
455
|
};
|
|
299
456
|
|
|
300
|
-
return setWecomAgentConfig(cfg, agentConfig);
|
|
457
|
+
return setWecomAgentConfig(cfg, agentConfig, accountId);
|
|
301
458
|
}
|
|
302
459
|
|
|
303
460
|
// ============================================================
|
|
@@ -308,6 +465,7 @@ async function promptDmPolicy(
|
|
|
308
465
|
cfg: OpenClawConfig,
|
|
309
466
|
prompter: WizardPrompter,
|
|
310
467
|
modes: ("bot" | "agent")[],
|
|
468
|
+
accountId: string,
|
|
311
469
|
): Promise<OpenClawConfig> {
|
|
312
470
|
const policyChoice = await prompter.select({
|
|
313
471
|
message: "请选择私聊 (DM) 访问策略:",
|
|
@@ -338,7 +496,7 @@ async function promptDmPolicy(
|
|
|
338
496
|
|
|
339
497
|
let result = cfg;
|
|
340
498
|
for (const mode of modes) {
|
|
341
|
-
result = setWecomDmPolicy(result, mode, dm);
|
|
499
|
+
result = setWecomDmPolicy(result, mode, dm, accountId);
|
|
342
500
|
}
|
|
343
501
|
return result;
|
|
344
502
|
}
|
|
@@ -347,20 +505,22 @@ async function promptDmPolicy(
|
|
|
347
505
|
// 配置汇总
|
|
348
506
|
// ============================================================
|
|
349
507
|
|
|
350
|
-
async function showSummary(cfg: OpenClawConfig, prompter: WizardPrompter): Promise<void> {
|
|
351
|
-
const
|
|
508
|
+
async function showSummary(cfg: OpenClawConfig, prompter: WizardPrompter, accountId: string): Promise<void> {
|
|
509
|
+
const account = resolveWecomAccount({ cfg, accountId });
|
|
352
510
|
const lines: string[] = ["✅ 配置已保存!", ""];
|
|
353
511
|
|
|
354
|
-
if (
|
|
512
|
+
if (account.bot?.configured) {
|
|
355
513
|
lines.push("📱 Bot 模式: 已配置");
|
|
356
|
-
lines.push(` 回调 URL: https
|
|
514
|
+
lines.push(` 回调 URL: https://您的域名${accountWebhookPath("bot", accountId)}`);
|
|
357
515
|
}
|
|
358
516
|
|
|
359
|
-
if (
|
|
517
|
+
if (account.agent?.configured) {
|
|
360
518
|
lines.push("🏢 Agent 模式: 已配置");
|
|
361
|
-
lines.push(` 回调 URL: https
|
|
519
|
+
lines.push(` 回调 URL: https://您的域名${accountWebhookPath("agent", accountId)}`);
|
|
362
520
|
}
|
|
363
521
|
|
|
522
|
+
lines.push(` 账号 ID: ${accountId}`);
|
|
523
|
+
|
|
364
524
|
lines.push("");
|
|
365
525
|
lines.push("⚠️ 请确保您已在企微后台填写了正确的回调 URL,");
|
|
366
526
|
lines.push(" 并点击了后台的『保存』按钮完成验证。");
|
|
@@ -378,11 +538,12 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
|
378
538
|
policyKey: "channels.wecom.bot.dm.policy",
|
|
379
539
|
allowFromKey: "channels.wecom.bot.dm.allowFrom",
|
|
380
540
|
getCurrent: (cfg: OpenClawConfig) => {
|
|
381
|
-
const
|
|
382
|
-
return (
|
|
541
|
+
const account = resolveWecomAccount({ cfg });
|
|
542
|
+
return (account.bot?.config.dm?.policy ?? "pairing") as "pairing";
|
|
383
543
|
},
|
|
384
544
|
setPolicy: (cfg: OpenClawConfig, policy: "pairing" | "allowlist" | "open" | "disabled") => {
|
|
385
|
-
|
|
545
|
+
const accountId = resolveDefaultWecomAccountId(cfg);
|
|
546
|
+
return setWecomDmPolicy(cfg, "bot", { policy }, accountId);
|
|
386
547
|
},
|
|
387
548
|
promptAllowFrom: async ({ cfg, prompter }: { cfg: OpenClawConfig; prompter: WizardPrompter }) => {
|
|
388
549
|
const allowFromStr = String(
|
|
@@ -392,7 +553,8 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
|
392
553
|
}),
|
|
393
554
|
).trim();
|
|
394
555
|
const allowFrom = allowFromStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
395
|
-
|
|
556
|
+
const accountId = resolveDefaultWecomAccountId(cfg);
|
|
557
|
+
return setWecomDmPolicy(cfg, "bot", { policy: "allowlist", allowFrom }, accountId);
|
|
396
558
|
},
|
|
397
559
|
};
|
|
398
560
|
|
|
@@ -404,60 +566,74 @@ export const wecomOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
404
566
|
channel,
|
|
405
567
|
dmPolicy,
|
|
406
568
|
getStatus: async ({ cfg }: { cfg: OpenClawConfig }) => {
|
|
407
|
-
const
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
);
|
|
412
|
-
const configured = botConfigured || agentConfigured;
|
|
569
|
+
const resolved = resolveWecomAccounts(cfg);
|
|
570
|
+
const accounts = Object.values(resolved.accounts).filter((account) => account.enabled !== false);
|
|
571
|
+
const botConfigured = accounts.some((account) => Boolean(account.bot?.configured));
|
|
572
|
+
const agentConfigured = accounts.some((account) => Boolean(account.agent?.configured));
|
|
573
|
+
const configured = accounts.some((account) => account.configured);
|
|
413
574
|
|
|
414
575
|
const statusParts: string[] = [];
|
|
415
576
|
if (botConfigured) statusParts.push("Bot ✓");
|
|
416
577
|
if (agentConfigured) statusParts.push("Agent ✓");
|
|
578
|
+
const accountSuffix = accounts.length > 1 ? ` · ${accounts.length} accounts` : "";
|
|
579
|
+
const statusSummary = statusParts.length > 0 ? statusParts.join(" + ") : "已配置";
|
|
417
580
|
|
|
418
581
|
return {
|
|
419
582
|
channel,
|
|
420
583
|
configured,
|
|
421
584
|
statusLines: [
|
|
422
|
-
`WeCom: ${configured ?
|
|
585
|
+
`WeCom: ${configured ? `${statusSummary}${accountSuffix}` : "需要配置"}`,
|
|
423
586
|
],
|
|
424
587
|
selectionHint: configured
|
|
425
|
-
? `configured · ${
|
|
588
|
+
? `configured · ${statusSummary}${accountSuffix}`
|
|
426
589
|
: "enterprise-ready · dual-mode",
|
|
427
590
|
quickstartScore: configured ? 1 : 8,
|
|
428
591
|
};
|
|
429
592
|
},
|
|
430
|
-
configure: async ({
|
|
593
|
+
configure: async ({
|
|
594
|
+
cfg,
|
|
595
|
+
prompter,
|
|
596
|
+
accountOverrides,
|
|
597
|
+
shouldPromptAccountIds,
|
|
598
|
+
}) => {
|
|
431
599
|
// 1. 欢迎
|
|
432
600
|
await showWelcome(prompter);
|
|
433
601
|
|
|
434
|
-
// 2.
|
|
602
|
+
// 2. 账号选择
|
|
603
|
+
const accountId = await resolveOnboardingAccountId({
|
|
604
|
+
cfg,
|
|
605
|
+
prompter,
|
|
606
|
+
accountOverride: accountOverrides.wecom,
|
|
607
|
+
shouldPromptAccountIds,
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// 3. 模式选择
|
|
435
611
|
const mode = await promptMode(prompter);
|
|
436
612
|
|
|
437
613
|
let next = cfg;
|
|
438
614
|
const configuredModes: ("bot" | "agent")[] = [];
|
|
439
615
|
|
|
440
|
-
//
|
|
616
|
+
// 4. 配置 Bot
|
|
441
617
|
if (mode === "bot" || mode === "both") {
|
|
442
|
-
next = await configureBotMode(next, prompter);
|
|
618
|
+
next = await configureBotMode(next, prompter, accountId);
|
|
443
619
|
configuredModes.push("bot");
|
|
444
620
|
}
|
|
445
621
|
|
|
446
|
-
//
|
|
622
|
+
// 5. 配置 Agent
|
|
447
623
|
if (mode === "agent" || mode === "both") {
|
|
448
|
-
next = await configureAgentMode(next, prompter);
|
|
624
|
+
next = await configureAgentMode(next, prompter, accountId);
|
|
449
625
|
configuredModes.push("agent");
|
|
450
626
|
}
|
|
451
627
|
|
|
452
|
-
//
|
|
453
|
-
next = await promptDmPolicy(next, prompter, configuredModes);
|
|
628
|
+
// 6. DM 策略
|
|
629
|
+
next = await promptDmPolicy(next, prompter, configuredModes, accountId);
|
|
454
630
|
|
|
455
|
-
//
|
|
631
|
+
// 7. 启用通道
|
|
456
632
|
next = setWecomEnabled(next, true);
|
|
457
633
|
|
|
458
|
-
//
|
|
459
|
-
await showSummary(next, prompter);
|
|
634
|
+
// 8. 汇总
|
|
635
|
+
await showSummary(next, prompter, accountId);
|
|
460
636
|
|
|
461
|
-
return { cfg: next, accountId
|
|
637
|
+
return { cfg: next, accountId };
|
|
462
638
|
},
|
|
463
639
|
};
|
package/src/types/constants.ts
CHANGED
|
@@ -4,12 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
/** 固定 Webhook 路径 */
|
|
6
6
|
export const WEBHOOK_PATHS = {
|
|
7
|
-
/** Bot
|
|
7
|
+
/** Bot 模式历史兼容路径(不再维护) */
|
|
8
8
|
BOT: "/wecom",
|
|
9
|
-
/** Bot
|
|
9
|
+
/** Bot 模式历史备用兼容路径(不再维护) */
|
|
10
10
|
BOT_ALT: "/wecom/bot",
|
|
11
|
-
/** Agent
|
|
11
|
+
/** Agent 模式历史兼容路径(不再维护) */
|
|
12
12
|
AGENT: "/wecom/agent",
|
|
13
|
+
/** Bot 模式(唯一支持路径前缀) */
|
|
14
|
+
BOT_PLUGIN: "/plugins/wecom/bot",
|
|
15
|
+
/** Agent 模式(唯一支持路径前缀) */
|
|
16
|
+
AGENT_PLUGIN: "/plugins/wecom/agent",
|
|
13
17
|
} as const;
|
|
14
18
|
|
|
15
19
|
/** 企业微信 API 端点 */
|