@yanhaidao/wecom 2.3.270 → 2.4.160

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.
Files changed (44) hide show
  1. package/README.md +79 -3
  2. package/UPSTREAM_CONFIG.md +170 -0
  3. package/UPSTREAM_PLAN.md +175 -0
  4. package/changelog/v2.4.12.md +37 -0
  5. package/changelog/v2.4.16.md +19 -0
  6. package/package.json +1 -1
  7. package/src/agent/handler.event-filter.test.ts +30 -1
  8. package/src/agent/handler.ts +226 -17
  9. package/src/app/account-runtime.ts +1 -1
  10. package/src/capability/agent/upstream-delivery-service.ts +96 -0
  11. package/src/capability/bot/sandbox-media.test.ts +221 -0
  12. package/src/capability/bot/sandbox-media.ts +176 -0
  13. package/src/capability/bot/stream-orchestrator.ts +19 -0
  14. package/src/channel.meta.test.ts +10 -0
  15. package/src/channel.ts +4 -1
  16. package/src/config/index.ts +5 -1
  17. package/src/config/network.ts +33 -0
  18. package/src/config/schema.ts +4 -0
  19. package/src/context-store.ts +41 -8
  20. package/src/http.ts +9 -1
  21. package/src/outbound.test.ts +211 -2
  22. package/src/outbound.ts +323 -70
  23. package/src/runtime/session-manager.test.ts +39 -0
  24. package/src/runtime/session-manager.ts +17 -0
  25. package/src/runtime/source-registry.ts +5 -0
  26. package/src/shared/media-asset.ts +78 -0
  27. package/src/shared/media-service.test.ts +111 -0
  28. package/src/shared/media-service.ts +42 -14
  29. package/src/target.ts +40 -0
  30. package/src/transport/agent-api/client.ts +233 -0
  31. package/src/transport/agent-api/core.ts +101 -5
  32. package/src/transport/agent-api/upstream-delivery.ts +45 -0
  33. package/src/transport/agent-api/upstream-media-upload.ts +70 -0
  34. package/src/transport/agent-api/upstream-reply.ts +43 -0
  35. package/src/transport/bot-webhook/inbound-normalizer.test.ts +433 -0
  36. package/src/transport/bot-webhook/inbound-normalizer.ts +240 -53
  37. package/src/transport/bot-webhook/message-shape.ts +3 -0
  38. package/src/transport/bot-ws/inbound.test.ts +195 -1
  39. package/src/transport/bot-ws/inbound.ts +57 -10
  40. package/src/types/config.ts +22 -0
  41. package/src/types/message.ts +11 -7
  42. package/src/upstream/index.ts +150 -0
  43. package/src/upstream.test.ts +84 -0
  44. package/vitest.config.ts +15 -4
package/README.md CHANGED
@@ -163,6 +163,11 @@
163
163
  > 项目保持高频迭代,全面对齐甚至超越企业真实业务诉求。
164
164
  > **为保持精简,以下仅展示近期 5 次重要更新,完整历史版本(含全部 `v2.2.x`)请前往 [changelog/ 目录](./changelog/) 查阅。**
165
165
 
166
+ #### 📌 v2.4.16(2026-04-16)
167
+ - **[版本对齐] 今日 changelog 已补齐到当前包版本** 🧾 `README` 摘要和 `changelog/` 目录现在都直接对应 `package.json` 里的 `v2.4.16`,不会再停留在旧版本号。
168
+ - **[展示修正] 最新版本入口已切换到 `v2.4.16`** 🔖 现在从项目首页查看最近更新时,看到的版本号就是当前实际包版本,减少发布说明和安装版本不一致的困惑。
169
+ - **[维护优化] 变更追踪更直接** 📚 后续如果继续补充 `v2.4.16` 的详细更新内容,可以直接落在 `changelog/v2.4.16.md`,不需要复用旧版本文档。
170
+
166
171
  #### 📌 v2.3.27(2026-03-27)
167
172
  - **[重要修复] `channel add` 重新支持 WeCom guided setup** 🧭 之前有些环境下,`wecom` 虽然已经安装,却仍会在 OpenClaw 里显示成 “does not support guided setup yet”,导致无法直接通过交互式向导添加。现在插件已经对齐 OpenClaw 当前的 `setupWizard` 接口,`openclaw channels add` 会重新正常识别和进入配置流程。
168
173
  - **[重要修复] 修复 `installedCatalogById is not defined`** 🔧 部分用户在渠道添加或选择阶段会直接遇到 `ReferenceError: installedCatalogById is not defined`,表现上像是“选了渠道就报错”或“添加流程突然失效”。这一版已经修复对应的目录访问逻辑,添加流程恢复稳定。
@@ -184,9 +189,6 @@
184
189
  - **[多账号硬隔离]** 彻底重构 MCP 缓存池实现 `accountId + category` 的二次硬维隔离,无论您的矩阵挂载了多少家企业的助手,上下文及鉴权缓存绝不会交叉重叠。
185
190
  - **[媒体通道重构]** 补齐 Bot WS 本地的媒体上传链,同时设立了严格的 `5秒熔断机制`,若 WebSocket 长通道大文件卡死将无感静默降级到 Agent 私信发送。
186
191
 
187
- #### 📌 v2.3.16(2026-03-16)
188
- - **[解析增强] 混合消息媒体正确接管** 🛠 重点修复在 `Bot WS` 通道下,用户如果发了“一张截图 + 一段文字指示”,以前容易丢掉截图或者 AI 只能看到无法查看的腾讯云临时链接。新版底层引擎将自动扫过所有的媒体节点摘取 URL 与解密 AES Key,还大模型一双慧眼。
189
-
190
192
  *(查看更早期关于“超时熔断代投、动态扩容矩阵”等功能的更新日志,请移步 [changelog/ 目录](./changelog/))*
191
193
 
192
194
  ---
@@ -262,6 +264,12 @@ openclaw plugins enable wecom
262
264
  "dm": {
263
265
  "policy": "open",
264
266
  "allowFrom": []
267
+ },
268
+ "upstreamCorps": { // 可选:给上下游企业用户发消息时使用
269
+ "ww_partner_corp": {
270
+ "corpId": "ww_partner_corp",
271
+ "agentId": 1000002
272
+ }
265
273
  }
266
274
  }
267
275
  }
@@ -296,6 +304,7 @@ openclaw plugins enable wecom
296
304
  - 旧的 `channels.wecom.media.maxBytes` 仍然兼容,但仅作为向后兼容兜底;新配置建议统一改成 `mediaMaxMb`。
297
305
  - 这些目录会和 OpenClaw 默认允许的媒体目录一起生效,不会覆盖默认白名单。
298
306
  - 也就是说,像 `~/Downloads/01.png` 这类本机文件现在默认就可以直接发到企微,不需要再单独配置。
307
+ - 如果你需要给上下游企业用户回消息,可以在 `agent` 下追加 `upstreamCorps`;下面的 `1.6` 会单独展开说明。
299
308
 
300
309
  > **注意:** 历史配置里的 `agent.corpSecret` 引擎依然能够向后兼容拾起,但后续的新项目推荐采用标准的 `agentSecret` 作为对齐键。
301
310
 
@@ -385,6 +394,73 @@ openclaw plugins enable wecom
385
394
 
386
395
  一句话:`localRoots` 管“能不能读这个本地路径”,`mediaMaxMb` 管“最多读多大”。
387
396
 
397
+ ### 1.6 上下游企业配置:如何给上下游企业用户发消息
398
+
399
+ 如果你的企业微信应用已经共享给上下游企业,插件现在可以根据下游企业的 `CorpID` 和 `AgentID`,把回复准确发回对应的上下游用户。
400
+
401
+ 这件事适合的场景很明确:
402
+
403
+ - 你的主企业已经把自建应用共享给经销商、供应商或合作方
404
+ - 这些上下游企业用户会从不同 `CorpID` 进入同一个 Agent 通道
405
+ - 你希望插件能自动识别“这是下游企业用户”,并走对应企业的应用身份发消息
406
+
407
+ 最小配置示例如下:
408
+
409
+ ```jsonc
410
+ {
411
+ "channels": {
412
+ "wecom": {
413
+ "accounts": {
414
+ "default": {
415
+ "agent": {
416
+ "corpId": "ww_primary_corp",
417
+ "agentId": 1000001,
418
+ "agentSecret": "PRIMARY_AGENT_SECRET",
419
+ "token": "PRIMARY_CALLBACK_TOKEN",
420
+ "encodingAESKey": "PRIMARY_ENCODING_AES_KEY",
421
+ "upstreamCorps": {
422
+ "ww_partner_corp": {
423
+ "corpId": "ww_partner_corp",
424
+ "agentId": 1000002
425
+ }
426
+ }
427
+ }
428
+ }
429
+ }
430
+ }
431
+ }
432
+ }
433
+ ```
434
+
435
+ 可以这样理解这组配置:
436
+
437
+ - `agent.corpId` / `agent.agentId` 是上游主企业自己的应用配置
438
+ - `upstreamCorps.<key>.corpId` 是某个下游企业的 `CorpID`
439
+ - `upstreamCorps.<key>.agentId` 是这个下游企业里共享应用对应的 `AgentID`
440
+ - 下游企业不需要单独配置 `agentSecret`;仍然使用主企业应用的鉴权链路
441
+
442
+ 这些参数通常可以从两条路拿到:
443
+
444
+ - 直接从企业微信管理后台查看下游企业的 `CorpID` 和共享应用的 `AgentID`
445
+ - 通过企业微信“获取应用共享信息”接口批量拉取
446
+
447
+ 如果你打算走自动拉取,最关键的信息只有两个:
448
+
449
+ - 官方文档:`https://developer.work.weixin.qq.com/document/path/95813`
450
+ - 你需要把返回里的 `corp_list[].corpid` 映射到 `upstreamCorps.<key>.corpId`,把 `corp_list[].agentid` 映射到 `upstreamCorps.<key>.agentId`
451
+
452
+ 插件内部的工作逻辑是:
453
+
454
+ - 收到消息时,会先看回调里的 `ToUserName`
455
+ - 如果这个 `CorpID` 和主企业 `corpId` 不一致,就把它识别成上下游企业用户
456
+ - 回复时会自动走对应的上下游 target 和下游企业配置,而不是误发回主企业通道
457
+
458
+ 需要特别注意三点:
459
+
460
+ - `upstreamCorps` 只解决“发给哪个下游企业”的问题,不替代主企业应用本身的授权配置
461
+ - 上下游企业需要先在企业微信后台完成应用共享,并确保应用已加入“可调用接口的应用”
462
+ - 如果你只是想快速看完整字段说明、接口映射和日志样例,可以直接看 [UPSTREAM_CONFIG.md](./UPSTREAM_CONFIG.md)
463
+
388
464
  ---
389
465
 
390
466
  ## 二、🏢 企业微信后台回调挂载指南 (针对使用了 Webhook 或 Agent Callback 的重度用户)
@@ -0,0 +1,170 @@
1
+ # WeCom 插件上下游企业配置指南
2
+
3
+ ## 背景
4
+
5
+ 企业微信的「上下游」功能允许企业与其经销商、供应商、合作伙伴便捷沟通、共享应用。
6
+
7
+ ## 问题
8
+
9
+ - 上下游企业的 CorpID 与主企业不同
10
+ - 上下游企业只能使用 Agent 渠道(没有 Bot 渠道)
11
+ - 需要使用下游企业的 access_token 来发送消息
12
+
13
+ ## 解决方案
14
+
15
+ 修改后的 WeCom 插件支持通过配置 `upstreamCorps` 来发送消息给上下游用户。
16
+
17
+ ## 配置方法
18
+
19
+ 在 `openclaw.json` 中,为需要支持上下游的账号添加 `upstreamCorps` 配置:
20
+
21
+ ```json
22
+ {
23
+ "channels": {
24
+ "wecom": {
25
+ "accounts": {
26
+ "<ACCOUNT_ID>": {
27
+ "enabled": true,
28
+ "name": "<ACCOUNT_NAME>",
29
+ "agent": {
30
+ "corpId": "<PRIMARY_CORP_ID>",
31
+ "agentId": <PRIMARY_AGENT_ID>,
32
+ "agentSecret": "<PRIMARY_AGENT_SECRET>",
33
+ "token": "<PRIMARY_CALLBACK_TOKEN>",
34
+ "encodingAESKey": "<PRIMARY_ENCODING_AES_KEY>",
35
+ "welcomeText": "<WELCOME_TEXT>",
36
+ "dm": {
37
+ "policy": "open",
38
+ "allowFrom": []
39
+ },
40
+ "upstreamCorps": {
41
+ "<UPSTREAM_CORP_KEY>": {
42
+ "corpId": "<UPSTREAM_CORP_ID>",
43
+ "agentId": <UPSTREAM_AGENT_ID>
44
+ }
45
+ }
46
+ },
47
+ "bot": {
48
+ "primaryTransport": "webhook",
49
+ "streamPlaceholderContent": "正在思考...",
50
+ "welcomeText": "<BOT_WELCOME_TEXT>",
51
+ "dm": {
52
+ "policy": "open",
53
+ "allowFrom": []
54
+ },
55
+ "webhook": {
56
+ "token": "<BOT_WEBHOOK_TOKEN>",
57
+ "encodingAESKey": "<BOT_WEBHOOK_ENCODING_AES_KEY>"
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ 占位符说明:
68
+
69
+ 1. `<ACCOUNT_ID>`: OpenClaw 中的 WeCom 账号 ID(如 `default`、`lab`)。
70
+ 2. `<PRIMARY_CORP_ID>` / `<PRIMARY_AGENT_ID>`: 上游(主)企业应用信息。
71
+ 3. `<UPSTREAM_CORP_ID>` / `<UPSTREAM_AGENT_ID>`: 下游企业应用信息(可由 95813 接口返回)。
72
+ 4. `<UPSTREAM_CORP_KEY>`: `upstreamCorps` 的配置键,建议与 `<UPSTREAM_CORP_ID>` 保持一致。
73
+
74
+ ## 配置说明
75
+
76
+ ### upstreamCorps 字段
77
+
78
+ - **key**: 下游企业标识(推荐直接使用下游 CorpID,例如 `<UPSTREAM_CORP_ID>`)
79
+ - **value**: 该下游企业的 Agent 配置
80
+ - `corpId`: 下游企业的 CorpID
81
+ - `agentId`: 下游企业的 AgentID
82
+
83
+ ## 获取下游企业配置信息
84
+
85
+ 1. **CorpID**: 从企业微信管理后台获取,或从消息回调中的 `ToUserName` 字段获取
86
+ 2. **AgentID**: 从企业微信管理后台 - 应用管理 中获取
87
+ 3. **AgentSecret**: 仅主企业应用需要配置(用于获取主企业 access_token)
88
+
89
+ ### 通过接口自动获取(推荐)
90
+
91
+ 你也可以通过企业微信官方接口「获取应用共享信息」批量拉取上下游企业的 `corpid` 与 `agentid`:
92
+
93
+ - 文档: https://developer.work.weixin.qq.com/document/path/95813
94
+ - 接口: `POST https://qyapi.weixin.qq.com/cgi-bin/corpgroup/corp/list_app_share_info?access_token=ACCESS_TOKEN`
95
+
96
+ 请求体示例(上下游场景):
97
+
98
+ ```json
99
+ {
100
+ "agentid": <PRIMARY_AGENT_ID>,
101
+ "business_type": 1,
102
+ "limit": 100
103
+ }
104
+ ```
105
+
106
+ 参数要点:
107
+
108
+ 1. `access_token` 使用上游企业应用的 access_token。
109
+ 2. `business_type` 传 `1` 表示上下游企业。
110
+ 3. `agentid` 传上游企业当前应用的 AgentID。
111
+ 4. 当企业较多时,用 `cursor` + `next_cursor` 分页拉取,直到 `ending=1`。
112
+
113
+ 返回字段映射到配置:
114
+
115
+ 1. `corp_list[].corpid` -> `upstreamCorps.<key>.corpId`
116
+ 2. `corp_list[].agentid` -> `upstreamCorps.<key>.agentId`
117
+
118
+ 示例返回(节选):
119
+
120
+ ```json
121
+ {
122
+ "errcode": 0,
123
+ "errmsg": "ok",
124
+ "ending": 0,
125
+ "next_cursor": "<NEXT_CURSOR>",
126
+ "corp_list": [
127
+ {
128
+ "corpid": "<UPSTREAM_CORP_ID>",
129
+ "corp_name": "<UPSTREAM_CORP_NAME>",
130
+ "agentid": <UPSTREAM_AGENT_ID>
131
+ }
132
+ ]
133
+ }
134
+ ```
135
+
136
+ 可直接转换成:
137
+
138
+ ```json
139
+ {
140
+ "upstreamCorps": {
141
+ "<UPSTREAM_CORP_KEY>": {
142
+ "corpId": "<UPSTREAM_CORP_ID>",
143
+ "agentId": <UPSTREAM_AGENT_ID>
144
+ }
145
+ }
146
+ }
147
+ ```
148
+
149
+ 提示:如果某个下游企业未在 `corp_list` 中出现,通常是该企业还未确认应用共享或共享未生效。
150
+
151
+ ## 工作原理
152
+
153
+ 1. 当收到消息时,插件检测消息中的 `ToUserName`(CorpID)
154
+ 2. 如果 `ToUserName` 与主 CorpID 不同,则识别为上下游用户
155
+ 3. 回复时使用 `wecom-agent-upstream:{accountId}:{corpId}:{userId}` 格式的 target
156
+ 4. Outbound 模块解析该 target,使用对应的上下游 Agent 配置发送消息
157
+
158
+ ## 日志示例
159
+
160
+ ```
161
+ [wecom-agent] detected upstream user: from=<UPSTREAM_USER_ID> toCorpId=<UPSTREAM_CORP_ID>
162
+ [wecom-outbound] Sending text to upstream target=wecom-agent-upstream:<ACCOUNT_ID>:<UPSTREAM_CORP_ID>:<UPSTREAM_USER_ID> corpId=<UPSTREAM_CORP_ID>
163
+ [wecom-outbound] Successfully sent upstream Agent text to wecom-agent-upstream:<ACCOUNT_ID>:<UPSTREAM_CORP_ID>:<UPSTREAM_USER_ID>
164
+ ```
165
+
166
+ ## 注意事项
167
+
168
+ 1. `upstreamCorps` 仅需配置下游 `corpId` 与 `agentId`,不需要下游 `agentSecret`
169
+ 2. 上下游企业需要在企业微信管理后台配置「可调用接口的应用」
170
+ 3. 上游企业需要将应用共享给下游企业
@@ -0,0 +1,175 @@
1
+ # WeCom 上下游企业支持修改计划
2
+
3
+ ## 问题分析
4
+
5
+ 根据企业微信文档和日志分析,问题出在**企业微信上下游的 Agent 消息发送机制**:
6
+
7
+ 1. **上下游用户的 CorpID 不同**:
8
+ - 主企业:`<PRIMARY_CORP_ID>`
9
+ - 上下游企业:`<UPSTREAM_CORP_ID>`
10
+
11
+ 2. **错误码 81013 的含义**:
12
+ `user & party & tag all invalid` - 用户、部门、标签全部无效
13
+
14
+ 3. **根本原因**:
15
+ OpenClaw 使用主企业的 Agent (corpId=<PRIMARY_CORP_ID>, agentId=<PRIMARY_AGENT_ID>) 尝试给上下游用户发送消息,但企业微信的 Agent **只能向本企业可见成员发送消息**,上下游用户不在主企业 Agent 的可见范围内。
16
+
17
+ ## 解决方案
18
+
19
+ 根据企业微信文档 https://developer.work.weixin.qq.com/document/path/95816,正确的解决方案是:
20
+
21
+ ### 核心逻辑
22
+
23
+ 1. **检测上下游用户**:通过消息中的 `ToUserName`(CorpID)来判断
24
+ 2. **获取下游企业的 access_token**:
25
+ - 使用上游企业的 access_token 作为调用凭证
26
+ - 调用 `corpgroup/corp/gettoken` 接口获取下游企业的 access_token
27
+ 3. **使用下游企业的 access_token 发送消息**:
28
+ - 使用下游企业的 `agentId`
29
+ - 使用获取到的下游企业 access_token
30
+
31
+ ### 获取下游企业 access_token 的接口
32
+
33
+ ```
34
+ POST https://qyapi.weixin.qq.com/cgi-bin/corpgroup/corp/gettoken?access_token=ACCESS_TOKEN
35
+ {
36
+ "corpid": "下游企业corpid",
37
+ "business_type": 1, // 1 表示上下游企业
38
+ "agentid": 下游企业应用ID
39
+ }
40
+ ```
41
+
42
+ **注意**:
43
+ - 需要使用上游企业的 access_token 作为调用凭证
44
+ - `business_type` 必须设置为 `1` 表示上下游企业
45
+ - 返回的 access_token 可用于调用下游企业通讯录的只读接口
46
+
47
+ ### 修改模块
48
+
49
+ #### 模块 1:配置扩展(types/config.ts)
50
+
51
+ ```typescript
52
+ export type WecomUpstreamCorpConfig = {
53
+ corpId: string;
54
+ agentId: number;
55
+ };
56
+
57
+ export type WecomAgentConfig = {
58
+ // ... 其他配置
59
+ /**
60
+ * 上下游企业配置映射
61
+ * key: 配置名称(可自定义)
62
+ * value: 下游企业的 CorpID 和 AgentID
63
+ */
64
+ upstreamCorps?: Record<string, WecomUpstreamCorpConfig>;
65
+ };
66
+ ```
67
+
68
+ #### 模块 2:上下游支持模块(upstream/index.ts)
69
+
70
+ - `detectUpstreamUser()`: 检测是否是上下游用户
71
+ - `createUpstreamAgentConfig()`: 创建上下游 Agent 配置
72
+ - `resolveUpstreamCorpConfig()`: 从配置中解析上下游企业配置
73
+ - `buildUpstreamAgentSessionTarget()`: 构建上下游用户的回复目标
74
+ - `parseUpstreamAgentSessionTarget()`: 解析上下游用户的回复目标
75
+
76
+ #### 模块 3:access_token 获取(transport/agent-api/core.ts)
77
+
78
+ 添加 `getUpstreamAccessToken()` 函数:
79
+ - 先获取上游企业的 access_token
80
+ - 调用 `corpgroup/corp/gettoken` 接口获取下游企业的 access_token
81
+
82
+ #### 模块 4:上下游消息发送(transport/agent-api/client.ts)
83
+
84
+ 添加 `sendUpstreamAgentApiText()` 和 `sendUpstreamAgentApiMedia()` 函数:
85
+ - 使用 `getUpstreamAgentApiAccessToken()` 获取下游企业的 access_token
86
+ - 使用下游企业的 `agentId` 发送消息
87
+
88
+ #### 模块 5:上下游 DeliveryService(capability/agent/upstream-delivery-service.ts)
89
+
90
+ 新建 `WecomUpstreamAgentDeliveryService` 类:
91
+ - 专门用于发送消息给上下游用户
92
+ - 使用下游企业的 access_token 和 agentId
93
+
94
+ #### 模块 6:消息发送路由(outbound.ts)
95
+
96
+ - 检测 `wecom-agent-upstream:` 格式的目标
97
+ - 使用 `WecomUpstreamAgentDeliveryService` 发送消息
98
+
99
+ ## 配置示例
100
+
101
+ ```yaml
102
+ channels:
103
+ wecom:
104
+ accounts:
105
+ <ACCOUNT_KEY>:
106
+ agent:
107
+ corpId: "<PRIMARY_CORP_ID>" # 主企业 CorpID
108
+ agentId: <PRIMARY_AGENT_ID>
109
+ agentSecret: "<PRIMARY_AGENT_SECRET>"
110
+ token: "<CALLBACK_TOKEN>"
111
+ encodingAESKey: "<CALLBACK_ENCODING_AES_KEY>"
112
+ # 上下游企业配置
113
+ upstreamCorps:
114
+ <UPSTREAM_KEY>: # 自定义名称
115
+ corpId: "<UPSTREAM_CORP_ID>" # 下游企业 CorpID
116
+ agentId: <UPSTREAM_AGENT_ID> # 下游企业的 AgentID
117
+ ```
118
+
119
+ ## 关键实现细节
120
+
121
+ ### 1. 获取下游企业 access_token
122
+
123
+ ```typescript
124
+ const primaryToken = await getAccessToken(primaryAgent);
125
+ const url = `https://qyapi.weixin.qq.com/cgi-bin/corpgroup/corp/gettoken?access_token=${primaryToken}`;
126
+ const res = await wecomFetch(url, {
127
+ method: "POST",
128
+ headers: { "Content-Type": "application/json" },
129
+ body: JSON.stringify({
130
+ corpid: "<UPSTREAM_CORP_ID>",
131
+ business_type: 1, // 1 表示上下游企业
132
+ agentid: <UPSTREAM_AGENT_ID>,
133
+ }),
134
+ });
135
+ ```
136
+
137
+ ### 2. 发送消息
138
+
139
+ 使用获取到的下游企业 access_token 和下游企业的 agentId 发送消息:
140
+
141
+ ```typescript
142
+ const token = await getUpstreamAgentApiAccessToken({
143
+ primaryAgent,
144
+ upstreamCorpId,
145
+ upstreamAgentId,
146
+ });
147
+ const url = `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${token}`;
148
+ const body = {
149
+ touser: "<UPSTREAM_USER_ID>",
150
+ msgtype: "text",
151
+ agentid: <UPSTREAM_AGENT_ID>,
152
+ text: { content: text },
153
+ };
154
+ ```
155
+
156
+ ### 3. 上下游用户检测
157
+
158
+ 在消息接收时,通过比较 `ToUserName`(消息中的 CorpID)和配置的 `corpId` 来检测:
159
+
160
+ ```typescript
161
+ const isUpstreamUser = messageToUserName !== primaryCorpId;
162
+ ```
163
+
164
+ ## 测试步骤
165
+
166
+ 1. 配置上下游企业的 `corpId` 和 `agentId`
167
+ 2. 让上下游用户发送消息到应用
168
+ 3. 验证是否能正确接收消息
169
+ 4. 验证是否能正确回复消息
170
+ 5. 检查日志中的 `corpId` 和 `agentId` 是否正确
171
+
172
+ ## 参考资料
173
+
174
+ - 企业微信上下游概述:https://developer.work.weixin.qq.com/document/path/97213
175
+ - 获取下游企业 access_token:https://developer.work.weixin.qq.com/document/path/95816
@@ -0,0 +1,37 @@
1
+ # OpenClaw WeCom 插件 v2.4.12 变更简报
2
+
3
+ > [!TIP]
4
+ > `v2.4.12` 是一个“新增能力 + 接入修稳”并行推进的版本。最直接的三个变化是:第一,企业微信自建应用现在可以把菜单点击这类 `event` 事件按规则路由到本地脚本处理;第二,Agent 通道现在补齐了上下游企业用户的识别与回传链路;第三,Webhook 入站附件在进入会话工作区时,不会再被固定 `5MB` 的旧限制误伤,媒体落盘和后续会话使用更符合当前配置。
5
+
6
+ ## 2026-04-12(v2.4.12)
7
+ - 【新增优化】**自建应用菜单事件支持路由到本地脚本**。现在企业微信自建应用收到 `click` 等 `event` 事件后,可以根据 `eventType`、`eventKey`、`changeType` 等条件命中规则,再交给本地 `Node.js` 或 `Python` 脚本处理。也就是说,菜单项终于不只是“点了有事件”,而是可以直接接你自己的自动化逻辑。
8
+ - 【新增优化】**脚本可直接回复,也可继续链到默认 Agent**。本地脚本既可以直接返回一段文本给用户,也可以通过 `chainToAgent` 控制是否继续进入默认 AI 流程。适合做“菜单先走本地轻逻辑,复杂问题再交给 Agent”的组合式处理。
9
+ - 【接入增强】**事件路由规则已进入正式配置面**。这一版把事件路由、脚本运行时白名单、超时和输出大小等配置都纳入了插件配置结构里,便于在多账号场景下做显式治理,而不是靠零散脚本硬接。
10
+ - 【新增能力】**支持上下游企业通过 Agent 渠道互通**。如果你的自建应用已经共享给上下游企业,现在插件可以根据回调里的下游 `CorpID` 识别上下游用户,并结合 `agent.upstreamCorps` 把回复准确发回对应企业,而不是错误地回到主企业通道。
11
+ - 【体验补齐】**上下游企业链路下的图片和语音发送一起补稳了**。这意味着上下游企业用户不再只是“文本能通、媒体掉链”,图片和语音等常见媒体回复也能跟着同一条 Agent 链路走通。
12
+ - 【体验修复】**Webhook 入站文件进入工作区时,不再被固定 5MB 限制误拦**。之前某些从企业微信发进来的附件,明明账号配置已经放宽了大小限制,但在进入会话工作区前还是会被旧的固定阈值挡住。现在这条链路已经改成按当前 WeCom 配置解析后的大小限制执行。
13
+ - 【稳定性增强】**工作区媒体暂存更适合后续 Agent 使用**。入站媒体进入工作区后的路径、命名冲突处理和沙箱会话下的落点都补得更完整,后续让 Agent 读取附件、基于文件继续处理时更稳。
14
+
15
+ ## 升级后你会直接感受到
16
+
17
+ - 企业微信自建应用菜单现在可以真正接本地自动化脚本,不需要所有按钮都先兜一圈 AI。
18
+ - 你可以把“帮助”“状态查询”“同步”“触发内部流程”这类菜单动作做成确定性的本地处理,再按需决定是否继续交给 Agent。
19
+ - 如果你的应用已经共享给上下游企业,下游企业用户现在可以沿着 Agent 通道正常收发消息,不再更容易出现“识别到了消息,但回复回不去”。
20
+ - 上下游企业场景下的图片、语音等媒体回复会比之前稳得多,不再只剩文本链路勉强可用。
21
+ - 从企业微信发进来的文件,尤其是大于 5MB 但仍在你实际配置范围内的附件,不会再在进入工作区前被莫名其妙拦下。
22
+ - 后续基于这些入站文件继续做 Agent 分析、摘要或处理时,整体链路会更顺。
23
+
24
+ ## 这次版本背后的最小理解模型
25
+
26
+ 把这次更新理解成三句话就够了:
27
+
28
+ 1. `wecom` 现在不只是“收消息 -> 丢给 AI”,而是多了一层可编排的事件路由,你可以先让菜单事件命中本地脚本。
29
+ 2. 企业微信上下游企业用户,已经可以通过 `agent.upstreamCorps` 接到正确的 Agent 回传链路,不再默认只按主企业处理。
30
+ 3. 企业微信入站附件进入会话工作区的链路,已经从“固定阈值拦截”改成“跟随当前配置”,真实可用性明显更高。
31
+
32
+ ## 升级提示
33
+
34
+ - 执行 `openclaw plugins update wecom` 即可升级到 `v2.4.12`。
35
+ - 如果你准备启用菜单事件脚本,请同时检查 `scriptRuntime.allowPaths`,确保脚本目录已加入允许列表。
36
+ - 如果你准备启用上下游企业回复,请补齐 `channels.wecom.accounts.<accountId>.agent.upstreamCorps`,并确认企业微信后台已完成应用共享。
37
+ - 如果你之前遇到“Webhook 收到了文件,但进不了工作区”或“大文件总在落盘前被拦”的情况,这一版就是对应修复。
@@ -0,0 +1,19 @@
1
+ # OpenClaw WeCom 插件 v2.4.16 变更简报
2
+
3
+ > [!TIP]
4
+ > `v2.4.16` 是一次版本对齐型更新。本次主要补齐今天版本的 changelog 入口,让文档展示的版本号与当前 [package.json](/Users/YanHaidao/openclaw/extensions/wecom/package.json) 保持一致,避免 README 摘要、变更日志文件名和实际发布版本出现错位。
5
+
6
+ ## 2026-04-16(v2.4.16)
7
+ - 【版本对齐】补充今天版本的独立 changelog 文件,文档侧现在可以直接对应到 `v2.4.16`。
8
+ - 【展示修正】README 的“最近更新”摘要已同步切换到 `v2.4.16`,版本号与当前包版本保持一致。
9
+ - 【维护优化】后续查阅变更时,不再需要从旧的 `v2.4.12` 文案反推当前实际发布版本。
10
+
11
+ ## 升级后你会直接感受到
12
+
13
+ - 在 README 和 `changelog/` 目录里看到的最新版本号,会和当前 `package.json` 一致。
14
+ - 对外查看版本更新时,不会再出现“包版本已经变了,但 changelog 还停留在旧版本”的混淆。
15
+
16
+ ## 升级提示
17
+
18
+ - 当前插件版本为 `v2.4.16`。
19
+ - 如需继续补充该版本的详细功能点,可以直接在本文件追加内容。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yanhaidao/wecom",
3
- "version": "2.3.270",
3
+ "version": "2.4.160",
4
4
  "description": "OpenClaw 企业微信(WeCom)插件,默认 Bot WebSocket,支持加密媒体解密、Agent 主动发消息与多账号接入",
5
5
  "license": "ISC",
6
6
  "author": "YanHaidao (VX: YanHaidao)",
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
- import { shouldProcessAgentInboundMessage } from "./handler.js";
3
+ import { shouldProcessAgentInboundMessage, shouldSuppressAgentReplyText } from "./handler.js";
4
4
 
5
5
  describe("shouldProcessAgentInboundMessage", () => {
6
6
  it("allows enter_agent/subscribe through the filter (handled earlier by static welcome)", () => {
@@ -69,3 +69,32 @@ describe("shouldProcessAgentInboundMessage", () => {
69
69
  expect(normalMessage.reason).toBe("user_message");
70
70
  });
71
71
  });
72
+
73
+ describe("shouldSuppressAgentReplyText", () => {
74
+ it("keeps plain text replies when no media reply has been seen", () => {
75
+ expect(
76
+ shouldSuppressAgentReplyText({
77
+ text: "这里是正常文本",
78
+ mediaReplySeen: false,
79
+ }),
80
+ ).toBe(false);
81
+ });
82
+
83
+ it("suppresses companion text once the reply flow includes media", () => {
84
+ expect(
85
+ shouldSuppressAgentReplyText({
86
+ text: "文件已发送,请查收",
87
+ mediaReplySeen: true,
88
+ }),
89
+ ).toBe(true);
90
+ });
91
+
92
+ it("does not suppress empty text even after media replies", () => {
93
+ expect(
94
+ shouldSuppressAgentReplyText({
95
+ text: " ",
96
+ mediaReplySeen: true,
97
+ }),
98
+ ).toBe(false);
99
+ });
100
+ });