@dingxiang-me/openclaw-wechat 2.0.0 → 2.1.0
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/CHANGELOG.md +22 -0
- package/README.en.md +37 -32
- package/README.md +54 -68
- package/openclaw.plugin.json +7 -5
- package/package.json +4 -1
- package/src/core.js +12 -4
- package/src/wecom/agent-inbound-guards.js +40 -17
- package/src/wecom/api-client-send-text.js +43 -20
- package/src/wecom/bot-inbound-content.js +14 -6
- package/src/wecom/bot-inbound-executor-helpers.js +33 -7
- package/src/wecom/bot-inbound-executor.js +7 -1
- package/src/wecom/bot-inbound-guards.js +47 -22
- package/src/wecom/bot-long-connection-manager.js +16 -4
- package/src/wecom/bot-webhook-dispatch.js +2 -0
- package/src/wecom/channel-plugin.js +22 -0
- package/src/wecom/command-handlers.js +6 -0
- package/src/wecom/command-status-text.js +69 -1
- package/src/wecom/outbound-delivery.js +0 -3
- package/src/wecom/outbound-webhook-sender.js +39 -16
- package/src/wecom/pairing.js +188 -0
- package/src/wecom/plugin-constants.js +1 -1
- package/src/wecom/target-utils.js +41 -5
- package/src/wecom/voice-transcription-process.js +65 -3
- package/src/wecom/voice-transcription.js +3 -2
- package/src/wecom/webhook-adapter-normalize.js +29 -0
- package/src/wecom/webhook-adapter.js +294 -59
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.1.0] - 2026-03-14
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- 新增 `dm.mode=pairing`,首次私聊可接入 OpenClaw 原生配对审批流,不再只能依赖静态 `allowFrom`
|
|
11
|
+
- 新增 WeCom 渠道 pairing metadata,支持 `quickstartAllowFrom`、`wecomUserId` 归一化以及审批通过后的默认通知回执
|
|
12
|
+
- `wecom:selfcheck`、`wecom:agent:selfcheck`、`wecom:bot:selfcheck` 新增接入摘要,直接输出 `readiness` 与 `routing`
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- WeCom 渠道 metadata 补齐 `detailLabel`、`systemImage` 与 quickstart 提示,接入路径更贴近 OpenClaw 原生渠道
|
|
16
|
+
- `/status` 输出优先展示“收消息 / 回消息 / 主动发送 / 媒体 / 语音 / 文档”准备情况,并显式展示 `bindings` / `dynamicAgent` 路由来源
|
|
17
|
+
- WeCom Bot 长连接正常连接日志默认降到 `debug`,减少 `connect/opened/subscribed` 噪音
|
|
18
|
+
- README(中英文)改成 `2.1.0` 接入口径,补充 `pairing`、selfcheck 摘要和推荐接入顺序
|
|
19
|
+
|
|
20
|
+
## [2.0.1] - 2026-03-14
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- 修复 WeCom Agent 回复 `/workspace/...` 文件路径时因动态会话工作区缺少目标文件,最终只回显路径文本而不实际发送图片/文件的问题
|
|
24
|
+
- 修复 WeCom Bot 长连接对附件回调 payload 形态兼容不足,导致 PDF/文件消息被当成普通文本继续处理的问题;补充 `msg_type`、嵌套 `message`、`attachments/items`、`file_url/download_url/file_name/aes_key` 等兼容解析
|
|
25
|
+
- 修复 WeCom Bot 入站文件/图片处理未完整透传媒体级 AES Key 的问题,降低 PDF/图片解密失败和内容损坏风险
|
|
26
|
+
- 修复本地 `whisper-cli` 转写时临时音频文件在子进程真正读取前被提前清理,导致语音识别报错 `input file not found`
|
|
27
|
+
- 补充 WeCom Bot 长连接未解析 callback 的运行日志,便于继续定位企业微信新回调形态
|
|
28
|
+
|
|
7
29
|
## [2.0.0] - 2026-03-13
|
|
8
30
|
|
|
9
31
|
### Changed
|
package/README.en.md
CHANGED
|
@@ -10,7 +10,7 @@ OpenClaw-Wechat is an OpenClaw channel plugin for Enterprise WeChat (WeCom), wit
|
|
|
10
10
|
|
|
11
11
|
## Table of Contents
|
|
12
12
|
|
|
13
|
-
- [
|
|
13
|
+
- [Onboarding Update (v2.1.0)](#onboarding-update-v210)
|
|
14
14
|
- [Highlights](#highlights)
|
|
15
15
|
- [Mode Comparison](#mode-comparison)
|
|
16
16
|
- [5-Minute Quick Start](#5-minute-quick-start)
|
|
@@ -28,41 +28,27 @@ OpenClaw-Wechat is an OpenClaw channel plugin for Enterprise WeChat (WeCom), wit
|
|
|
28
28
|
- [Development](#development)
|
|
29
29
|
- [FAQ](#faq)
|
|
30
30
|
|
|
31
|
-
##
|
|
31
|
+
## Onboarding Update (v2.1.0)
|
|
32
32
|
|
|
33
|
-
This
|
|
34
|
-
`OpenClaw-Wechat` now has **working WeCom Bot long-connection support** in real gateway runtime.
|
|
33
|
+
This release focuses on setup quality, not feature sprawl. The goal is to make `OpenClaw-Wechat` behave more like a native OpenClaw channel during install and first-run.
|
|
35
34
|
|
|
36
|
-
###
|
|
35
|
+
### What changed
|
|
37
36
|
|
|
38
37
|
| Item | Result |
|
|
39
38
|
|---|---|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
47
|
-
### Control UI and operations
|
|
48
|
-
|
|
49
|
-
This release also keeps the earlier operations improvements: **WeCom supports visual configuration in Control UI**, so you no longer need to rely on manual JSON edits only.
|
|
50
|
-
|
|
51
|
-
### Visual config in Control UI
|
|
52
|
-
|
|
53
|
-
| Item | Status | Notes |
|
|
54
|
-
|---|---|---|
|
|
55
|
-
| WeCom config form | ✅ | `channels.wecom` can be edited/saved directly from UI |
|
|
56
|
-
| Localized labels | ✅ | Common fields are now clearly labeled in Chinese for ops teams |
|
|
57
|
-
| Sensitive-field hints | ✅ | secret/token/aesKey fields are marked as sensitive |
|
|
58
|
-
| Runtime status clarity | ✅ | `Connected` is no longer stuck at `n/a`; default account display name is localized |
|
|
59
|
-
| Inbound activity tracking | ✅ | `Last inbound` is updated after webhook callbacks (`n/a` before first inbound after restart is expected) |
|
|
39
|
+
| DM pairing approval | Added `dm.mode=pairing`, backed by OpenClaw native pairing flow |
|
|
40
|
+
| Quickstart metadata | Added `detailLabel`, `systemImage`, and `quickstartAllowFrom` |
|
|
41
|
+
| `/status` readability | Status now leads with `receive / reply / send / media / voice / doc` readiness |
|
|
42
|
+
| Selfcheck summaries | `wecom:selfcheck`, `wecom:agent:selfcheck`, and `wecom:bot:selfcheck` now print `readiness` and `routing` summaries |
|
|
43
|
+
| Route visibility | Status and selfcheck now show whether routing comes from `bindings` or `dynamicAgent` |
|
|
44
|
+
| Long-connection log noise | Normal connect / opened / subscribed logs moved to `debug` |
|
|
60
45
|
|
|
61
46
|
### Practical impact
|
|
62
47
|
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
48
|
+
- New users can start from the smallest working config, then add multi-account or dynamic routing later.
|
|
49
|
+
- Teams that want controlled DM access can now use `pairing` instead of maintaining `allowFrom` only.
|
|
50
|
+
- `/status` and selfcheck answer “can it receive, reply, and send?” before lower-level transport details.
|
|
51
|
+
- Bot long-connection is quieter by default while still keeping useful warnings and errors.
|
|
66
52
|
|
|
67
53
|
## Highlights
|
|
68
54
|
|
|
@@ -75,7 +61,7 @@ This release also keeps the earlier operations improvements: **WeCom supports vi
|
|
|
75
61
|
| Bot card replies | ✅ | `markdown/template_card` with automatic text fallback |
|
|
76
62
|
| Multi-account support | ✅ | `channels.wecom.accounts.<id>` |
|
|
77
63
|
| Sender allowlist and admin bypass | ✅ | `allowFrom` + `adminUsers` |
|
|
78
|
-
| Direct-message policy | ✅ | `dm.mode=open/allowlist/deny` + account overrides |
|
|
64
|
+
| Direct-message policy | ✅ | `dm.mode=open/allowlist/pairing/deny` + account overrides |
|
|
79
65
|
| Event welcome reply (`enter_agent`) | ✅ | configurable via `events.enterAgentWelcome*` |
|
|
80
66
|
| Command allowlist | ✅ | `/help`, `/status`, `/clear`, `/new`, etc. |
|
|
81
67
|
| Group trigger policy | ✅ | mention-required or direct-trigger |
|
|
@@ -103,7 +89,7 @@ This release also keeps the earlier operations improvements: **WeCom supports vi
|
|
|
103
89
|
openclaw plugins install @dingxiang-me/openclaw-wechat
|
|
104
90
|
```
|
|
105
91
|
|
|
106
|
-
Recommended minimum package version: `2.
|
|
92
|
+
Recommended minimum package version: `2.1.0`. If `plugins.installs.openclaw-wechat` in `openclaw.json` still reports `1.7.x`, upgrade or reinstall first; those older npm packages do not expose the current WeCom channel metadata.
|
|
107
93
|
|
|
108
94
|
For local development or direct source-path loading, use:
|
|
109
95
|
|
|
@@ -159,6 +145,20 @@ If you are loading from source path instead, use the version below with `load.pa
|
|
|
159
145
|
|
|
160
146
|
> Agent-mode note (important): configure **Trusted IP** in the WeCom self-built app settings and include the real egress IP of your OpenClaw gateway. Otherwise you may see "messages received but no reply".
|
|
161
147
|
|
|
148
|
+
If you want first-contact DM approval, add:
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"channels": {
|
|
153
|
+
"wecom": {
|
|
154
|
+
"dm": {
|
|
155
|
+
"mode": "pairing"
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
162
|
### 4) Restart and verify
|
|
163
163
|
|
|
164
164
|
```bash
|
|
@@ -169,6 +169,11 @@ npm run wecom:agent:selfcheck -- --all-accounts
|
|
|
169
169
|
npm run wecom:bot:selfcheck -- --all-accounts
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
+
Selfcheck now starts with two summary lines:
|
|
173
|
+
|
|
174
|
+
- `readiness`: whether receive / reply / send are currently usable
|
|
175
|
+
- `routing`: whether `bindings` and `dynamicAgent` are active
|
|
176
|
+
|
|
172
177
|
## Requirements
|
|
173
178
|
|
|
174
179
|
| Item | Description |
|
|
@@ -310,7 +315,7 @@ When multi-account is enabled, each account can override Bot callback credential
|
|
|
310
315
|
| Sender ACL | `allowFrom`, `allowFromRejectMessage` |
|
|
311
316
|
| Command ACL | `commands.enabled`, `commands.allowlist`, `commands.rejectMessage` |
|
|
312
317
|
| Admin bypass | `adminUsers` |
|
|
313
|
-
| Direct-message policy | `dm.mode`, `dm.allowFrom`, `dm.rejectMessage` |
|
|
318
|
+
| Direct-message policy | `dm.mode`, `dm.allowFrom`, `dm.rejectMessage` (`open / allowlist / pairing / deny`) |
|
|
314
319
|
| Event policy | `events.enabled`, `events.enterAgentWelcomeEnabled`, `events.enterAgentWelcomeText` |
|
|
315
320
|
| Group trigger | `groupChat.enabled`, `groupChat.triggerMode`, `groupChat.mentionPatterns`, `groupChat.triggerKeywords` |
|
|
316
321
|
| Dynamic route | `dynamicAgent.*` (compatible with `dynamicAgents.*`, `dm.createAgentOnFirstMessage`) |
|
|
@@ -433,7 +438,7 @@ Outbound target formats:
|
|
|
433
438
|
|---|---|
|
|
434
439
|
| `WECOM_ALLOW_FROM*` | sender authorization |
|
|
435
440
|
| `WECOM_COMMANDS_*` | command ACL |
|
|
436
|
-
| `WECOM_DM_*`, `WECOM_<ACCOUNT>_DM_*` | DM policy + allowlist |
|
|
441
|
+
| `WECOM_DM_*`, `WECOM_<ACCOUNT>_DM_*` | DM policy + allowlist / pairing controls |
|
|
437
442
|
| `WECOM_EVENTS_*`, `WECOM_<ACCOUNT>_EVENTS_*` | event handling + enter_agent welcome text |
|
|
438
443
|
| `WECOM_GROUP_CHAT_*` | group trigger policy |
|
|
439
444
|
| `WECOM_DEBOUNCE_*` | text debounce |
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ OpenClaw-Wechat 是一个面向 OpenClaw 的企业微信渠道插件,支持两
|
|
|
12
12
|
|
|
13
13
|
## 目录
|
|
14
14
|
|
|
15
|
-
- [
|
|
15
|
+
- [接入更新(v2.1.0)](#接入更新v210)
|
|
16
16
|
- [功能概览](#功能概览)
|
|
17
17
|
- [模式对比](#模式对比)
|
|
18
18
|
- [5 分钟极速上手](#5-分钟极速上手)
|
|
@@ -33,75 +33,42 @@ OpenClaw-Wechat 是一个面向 OpenClaw 的企业微信渠道插件,支持两
|
|
|
33
33
|
- [FAQ](#faq)
|
|
34
34
|
- [版本与贡献](#版本与贡献)
|
|
35
35
|
|
|
36
|
-
##
|
|
36
|
+
## 接入更新(v2.1.0)
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
`OpenClaw-Wechat` 现在已经把 **企业微信智能机器人长连接** 正式做成可用能力,并且已经在真实网关环境下完成:
|
|
38
|
+
这版不是继续堆新平台能力,而是把接入体验收口到“更像 OpenClaw 原生渠道”的状态。
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
- `aibot_subscribe` 鉴权成功
|
|
43
|
-
- 心跳 `ping` / 回执成功
|
|
44
|
-
- 网关日志出现 `socket opened` 和 `subscribed`
|
|
45
|
-
|
|
46
|
-
### 企业微信 Bot 长连接已正式可用
|
|
40
|
+
### 这版新增了什么
|
|
47
41
|
|
|
48
42
|
| 项目 | 结果 |
|
|
49
43
|
|---|---|
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
### 可视化配置能力(Control UI)
|
|
80
|
-
|
|
81
|
-
| 项目 | 现状 | 说明 |
|
|
82
|
-
|---|---|---|
|
|
83
|
-
| WeCom 配置表单 | ✅ | `channels.wecom` 支持在后台页面直接编辑与保存 |
|
|
84
|
-
| 中文字段标签 | ✅ | 常用字段(如 `corpId`、`callbackToken`、`accounts.*`)均已中文化 |
|
|
85
|
-
| 敏感项标记 | ✅ | `secret/token/aesKey` 字段按敏感项展示 |
|
|
86
|
-
| 状态展示 | ✅ | `Connected` 不再长期 `n/a`,默认账号显示名中文化为“默认账号” |
|
|
87
|
-
| 入站状态追踪 | ✅ | 收到回调后自动更新 `Last inbound`(重启后首次入站前为 `n/a` 属正常) |
|
|
88
|
-
| 文档工具开关 | ✅ | `tools.doc` 等文档相关配置可被 schema 正确识别 |
|
|
89
|
-
|
|
90
|
-
### 文档工具升级摘要
|
|
91
|
-
|
|
92
|
-
| 能力层 | 新增内容 |
|
|
93
|
-
|---|---|
|
|
94
|
-
| 文档基础 | `create`、`rename`、`get_info`、`share`、`delete` |
|
|
95
|
-
| 权限管理 | `grant_access`、`add_collaborators`、`set_join_rule`、`set_member_auth`、`set_safety_setting` |
|
|
96
|
-
| 运维诊断 | `get_auth`、`diagnose_auth`、`validate_share_link` |
|
|
97
|
-
| 表格/收集表 | `get_sheet_properties`、`create_collect`、`modify_collect`、`get_form_info`、`get_form_answer`、`get_form_statistic` |
|
|
98
|
-
|
|
99
|
-
### 你升级后能直接获得的变化
|
|
100
|
-
|
|
101
|
-
- 在 OpenClaw 后台的 `Channels -> WeCom` 页面可直接配置并保存核心参数。
|
|
102
|
-
- 在同一个插件里直接调用 `wecom_doc`,不需要额外安装第二个 WeCom Doc 插件。
|
|
103
|
-
- 现在排查“为什么这个链接打不开”不需要再手翻日志和原始 JSON,工具会直接给出结论。
|
|
104
|
-
- 文档创建、协作者授权、分享链接诊断已经形成闭环,适合直接上线给真实企微用户使用。
|
|
44
|
+
| 私聊配对审批 | 新增 `dm.mode=pairing`,首次私聊会生成 OpenClaw 原生配对审批 |
|
|
45
|
+
| Quickstart 元信息 | 渠道 metadata 补齐 `detailLabel`、`systemImage`、`quickstartAllowFrom` |
|
|
46
|
+
| `/status` 可读性 | 优先展示 `收消息 / 回消息 / 主动发送 / 媒体 / 语音 / 文档` 准备情况 |
|
|
47
|
+
| Selfcheck 摘要 | `wecom:selfcheck` / `wecom:agent:selfcheck` / `wecom:bot:selfcheck` 新增 `readiness` 与 `routing` 摘要 |
|
|
48
|
+
| 路由可见性 | 明确显示当前是否命中 `bindings` 或 `dynamicAgent` |
|
|
49
|
+
| 长连接日志降噪 | 正常的 connect / opened / subscribed 改为 `debug`,减少默认噪音 |
|
|
50
|
+
|
|
51
|
+
### 这版对接入有什么实际影响
|
|
52
|
+
|
|
53
|
+
- 新用户现在可以按最小配置先跑通,再决定是否启用多账号、动态路由和文档工具。
|
|
54
|
+
- 需要私聊审批时,不再只能靠手写 `allowFrom`,可以直接用 `pairing` 模式接到 OpenClaw 审批流。
|
|
55
|
+
- `/status` 和 selfcheck 先回答“能不能收、能不能回、能不能发”,再展开工程细节。
|
|
56
|
+
- Bot 长连接默认日志更安静,排障时再开 `debug` 看正常心跳和订阅细节。
|
|
57
|
+
|
|
58
|
+
### 当前推荐的接入顺序
|
|
59
|
+
|
|
60
|
+
1. 先选一个主入口:
|
|
61
|
+
- Bot 长连接:适合最快跑通对话
|
|
62
|
+
- Agent:适合需要主动发送、菜单、自建应用能力
|
|
63
|
+
2. 再决定私聊准入:
|
|
64
|
+
- `open`:直接可聊
|
|
65
|
+
- `allowlist`:仅显式白名单
|
|
66
|
+
- `pairing`:首次私聊先审批
|
|
67
|
+
3. 最后再加增强项:
|
|
68
|
+
- `bindings`
|
|
69
|
+
- `dynamicAgent`
|
|
70
|
+
- `wecom_doc`
|
|
71
|
+
- 本地语音转写
|
|
105
72
|
|
|
106
73
|
## 5 分钟极速上手
|
|
107
74
|
|
|
@@ -113,7 +80,7 @@ OpenClaw-Wechat 是一个面向 OpenClaw 的企业微信渠道插件,支持两
|
|
|
113
80
|
openclaw plugins install @dingxiang-me/openclaw-wechat
|
|
114
81
|
```
|
|
115
82
|
|
|
116
|
-
> 最低建议版本:`2.
|
|
83
|
+
> 最低建议版本:`2.1.0`。如果 `openclaw.json` 里的 `plugins.installs.openclaw-wechat` 仍显示 `1.7.x`,请先升级或重装;旧包不会正确注册 `wecom` channel 元数据。
|
|
117
84
|
|
|
118
85
|
如果你是在本地开发或要直接跑仓库源码,再用下面这套:
|
|
119
86
|
|
|
@@ -170,6 +137,20 @@ npm install
|
|
|
170
137
|
|
|
171
138
|
> Agent 模式补充(重要):在企业微信自建应用后台请配置**可信 IP**,把 OpenClaw 网关实际出网 IP 加入白名单。否则可能出现“能收到消息但不回复”。
|
|
172
139
|
|
|
140
|
+
如果你希望私聊先审批,再开放会话,可以直接加:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"channels": {
|
|
145
|
+
"wecom": {
|
|
146
|
+
"dm": {
|
|
147
|
+
"mode": "pairing"
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
173
154
|
### Step 4. 重启并自检
|
|
174
155
|
|
|
175
156
|
```bash
|
|
@@ -180,6 +161,11 @@ npm run wecom:agent:selfcheck -- --all-accounts
|
|
|
180
161
|
npm run wecom:bot:selfcheck -- --all-accounts
|
|
181
162
|
```
|
|
182
163
|
|
|
164
|
+
现在自检输出会先给出两行摘要:
|
|
165
|
+
|
|
166
|
+
- `readiness`: 当前是否能收、能回、能发
|
|
167
|
+
- `routing`: 当前是否启用 `bindings` 和 `dynamicAgent`
|
|
168
|
+
|
|
183
169
|
### Step 5. 发一条消息验证
|
|
184
170
|
|
|
185
171
|
| 验证项 | 预期结果 |
|
|
@@ -201,7 +187,7 @@ npm run wecom:bot:selfcheck -- --all-accounts
|
|
|
201
187
|
| Bot 卡片回包 | ✅ | 支持 `markdown/template_card`,失败自动降级文本 |
|
|
202
188
|
| 多账户 | ✅ | `channels.wecom.accounts.<id>` |
|
|
203
189
|
| 发送者授权控制 | ✅ | `allowFrom` + 账户级覆盖 |
|
|
204
|
-
| 私聊策略(DM) | ✅ | `dm.mode=open/allowlist/deny` + 账户级覆盖 |
|
|
190
|
+
| 私聊策略(DM) | ✅ | `dm.mode=open/allowlist/pairing/deny` + 账户级覆盖 |
|
|
205
191
|
| 事件欢迎语(enter_agent) | ✅ | `events.enterAgentWelcome*` 可配置 |
|
|
206
192
|
| 命令白名单 | ✅ | `/help` `/status` `/clear` `/new` 等 |
|
|
207
193
|
| 群聊触发策略 | ✅ | 支持 `direct/mention/keyword` 三种模式 |
|
|
@@ -662,7 +648,7 @@ node ./scripts/wecom-bot-selfcheck.mjs --help
|
|
|
662
648
|
| 拒绝文案 | `allowFromRejectMessage` | 未授权提示 |
|
|
663
649
|
| 管理员 | `adminUsers` | 绕过命令白名单 |
|
|
664
650
|
| 命令白名单 | `commands.enabled` + `commands.allowlist` | 限制 `/` 指令 |
|
|
665
|
-
| 私聊策略 | `dm.mode` + `dm.allowFrom` + `dm.rejectMessage` |
|
|
651
|
+
| 私聊策略 | `dm.mode` + `dm.allowFrom` + `dm.rejectMessage` | 控制私聊开放/白名单/配对审批/拒绝 |
|
|
666
652
|
| 事件策略 | `events.enabled` + `events.enterAgentWelcomeEnabled` + `events.enterAgentWelcomeText` | 控制事件处理与 enter_agent 欢迎语 |
|
|
667
653
|
| 群聊触发 | `groupChat.enabled` + `triggerMode` + `mentionPatterns` + `triggerKeywords` | 控制群消息触发条件(自建应用支持 `direct/mention/keyword`;Bot 模式按平台限制固定 `mention`) |
|
|
668
654
|
| 动态路由 | `dynamicAgent.*`(兼容 `dynamicAgents.*`、`dm.createAgentOnFirstMessage`) | 动态 Agent + workspace bootstrap 播种 |
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-wechat",
|
|
3
3
|
"name": "OpenClaw-Wechat",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.1.0",
|
|
5
5
|
"description": "WeCom (企业微信) channel plugin for OpenClaw (自建应用回调 + 发送 API).",
|
|
6
6
|
"channels": [
|
|
7
7
|
"wecom"
|
|
@@ -672,10 +672,11 @@
|
|
|
672
672
|
"enum": [
|
|
673
673
|
"open",
|
|
674
674
|
"allowlist",
|
|
675
|
+
"pairing",
|
|
675
676
|
"deny"
|
|
676
677
|
],
|
|
677
678
|
"default": "open",
|
|
678
|
-
"description": "私聊策略:open=开放,allowlist=白名单,deny=关闭"
|
|
679
|
+
"description": "私聊策略:open=开放,allowlist=白名单,pairing=首次私聊需审批,deny=关闭"
|
|
679
680
|
},
|
|
680
681
|
"rejectMessage": {
|
|
681
682
|
"type": "string",
|
|
@@ -688,7 +689,7 @@
|
|
|
688
689
|
},
|
|
689
690
|
"allowFrom": {
|
|
690
691
|
"type": "array",
|
|
691
|
-
"description": "私聊白名单(mode=allowlist
|
|
692
|
+
"description": "私聊白名单(mode=allowlist/pairing 时作为显式放行名单)",
|
|
692
693
|
"items": {
|
|
693
694
|
"type": "string",
|
|
694
695
|
"minLength": 1
|
|
@@ -1194,14 +1195,15 @@
|
|
|
1194
1195
|
"enum": [
|
|
1195
1196
|
"open",
|
|
1196
1197
|
"allowlist",
|
|
1198
|
+
"pairing",
|
|
1197
1199
|
"deny"
|
|
1198
1200
|
],
|
|
1199
1201
|
"default": "open",
|
|
1200
|
-
"description": "私聊策略:open=开放,allowlist=白名单,deny=关闭"
|
|
1202
|
+
"description": "私聊策略:open=开放,allowlist=白名单,pairing=首次私聊需审批,deny=关闭"
|
|
1201
1203
|
},
|
|
1202
1204
|
"allowFrom": {
|
|
1203
1205
|
"type": "array",
|
|
1204
|
-
"description": "私聊白名单(mode=allowlist
|
|
1206
|
+
"description": "私聊白名单(mode=allowlist/pairing 时作为显式放行名单)",
|
|
1205
1207
|
"items": {
|
|
1206
1208
|
"type": "string",
|
|
1207
1209
|
"minLength": 1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dingxiang-me/openclaw-wechat",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "WeCom (企业微信) channel plugin for OpenClaw (自建应用回调 + 发送 API).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,10 +24,13 @@
|
|
|
24
24
|
"id": "wecom",
|
|
25
25
|
"label": "企业微信 WeCom",
|
|
26
26
|
"selectionLabel": "企业微信 WeCom(自建应用/Bot)",
|
|
27
|
+
"detailLabel": "企业微信自建应用 / Bot",
|
|
27
28
|
"docsPath": "/channels/wecom",
|
|
28
29
|
"docsLabel": "wecom",
|
|
29
30
|
"blurb": "企业微信消息通道(自建应用回调 + Bot 回调 + 发送 API)。",
|
|
30
31
|
"order": 80,
|
|
32
|
+
"systemImage": "building.2.crop.circle",
|
|
33
|
+
"quickstartAllowFrom": true,
|
|
31
34
|
"aliases": [
|
|
32
35
|
"wework",
|
|
33
36
|
"qiwei",
|
package/src/core.js
CHANGED
|
@@ -55,7 +55,7 @@ const BOT_CARD_MODE_SET = new Set(["markdown", "template_card"]);
|
|
|
55
55
|
const DELIVERY_FALLBACK_LAYER_SET = new Set(DEFAULT_DELIVERY_FALLBACK_ORDER);
|
|
56
56
|
const DYNAMIC_AGENT_MAP_SPLITTER = /[,\n]/;
|
|
57
57
|
const GROUP_CHAT_TRIGGER_MODE_SET = new Set(["direct", "mention", "keyword"]);
|
|
58
|
-
const DM_POLICY_MODE_SET = new Set(["open", "allowlist", "deny"]);
|
|
58
|
+
const DM_POLICY_MODE_SET = new Set(["open", "allowlist", "pairing", "deny"]);
|
|
59
59
|
const DYNAMIC_AGENT_MODE_SET = new Set(["mapping", "deterministic", "hybrid"]);
|
|
60
60
|
const DYNAMIC_AGENT_ID_STRATEGY_SET = new Set(["readable-hash"]);
|
|
61
61
|
const LEGACY_INLINE_ACCOUNT_RESERVED_KEYS = new Set([
|
|
@@ -582,6 +582,7 @@ function normalizeWecomDmPolicyMode(value, fallback = "open") {
|
|
|
582
582
|
.toLowerCase();
|
|
583
583
|
if (!normalized) return fallback;
|
|
584
584
|
if (normalized === "closed" || normalized === "close") return "deny";
|
|
585
|
+
if (normalized === "disabled") return "deny";
|
|
585
586
|
if (normalized === "whitelist") return "allowlist";
|
|
586
587
|
if (DM_POLICY_MODE_SET.has(normalized)) return normalized;
|
|
587
588
|
return fallback;
|
|
@@ -836,9 +837,16 @@ export function resolveWecomDmPolicyConfig({
|
|
|
836
837
|
channelDmConfig.rejectMessage,
|
|
837
838
|
channelDmConfig.blockMessage,
|
|
838
839
|
readDmRejectMessageEnv(envVars, processEnv, accountId),
|
|
839
|
-
mode === "deny"
|
|
840
|
-
|
|
841
|
-
|
|
840
|
+
mode === "deny"
|
|
841
|
+
? "当前渠道私聊已关闭,请联系管理员。"
|
|
842
|
+
: mode === "pairing"
|
|
843
|
+
? "当前私聊需先完成配对审批。"
|
|
844
|
+
: "当前私聊账号未授权,请联系管理员。",
|
|
845
|
+
);
|
|
846
|
+
const effectiveMode =
|
|
847
|
+
mode === "allowlist" && allowFrom.length === 0
|
|
848
|
+
? "deny"
|
|
849
|
+
: mode;
|
|
842
850
|
return {
|
|
843
851
|
mode: effectiveMode,
|
|
844
852
|
allowFrom,
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
issueWecomPairingChallenge,
|
|
3
|
+
resolveWecomDirectMessageAccess,
|
|
4
|
+
} from "./pairing.js";
|
|
5
|
+
|
|
1
6
|
function assertFunction(name, value) {
|
|
2
7
|
if (typeof value !== "function") {
|
|
3
8
|
throw new Error(`agent-inbound-guards: ${name} is required`);
|
|
@@ -61,28 +66,46 @@ export async function applyWecomAgentInboundGuards({
|
|
|
61
66
|
|
|
62
67
|
const commandPolicy = resolveWecomCommandPolicy(api);
|
|
63
68
|
const isAdminUser = commandPolicy.adminUsers.includes(normalizedFromUser);
|
|
64
|
-
const
|
|
69
|
+
const resolvedAccountId = config?.accountId || accountId || "default";
|
|
70
|
+
const dmPolicy = resolveWecomDmPolicy(api, resolvedAccountId, config);
|
|
71
|
+
const allowFromPolicy = resolveWecomAllowFromPolicy(api, resolvedAccountId, config);
|
|
72
|
+
|
|
65
73
|
if (!isGroupChat) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
const dmAccess = await resolveWecomDirectMessageAccess({
|
|
75
|
+
api,
|
|
76
|
+
accountId: resolvedAccountId,
|
|
77
|
+
dmPolicy,
|
|
78
|
+
allowFromPolicy,
|
|
79
|
+
normalizedFromUser,
|
|
80
|
+
isAdminUser,
|
|
81
|
+
isWecomSenderAllowed,
|
|
82
|
+
});
|
|
83
|
+
if (dmAccess.decision === "pairing") {
|
|
84
|
+
const pairing = await issueWecomPairingChallenge({
|
|
85
|
+
api,
|
|
86
|
+
accountId: resolvedAccountId,
|
|
87
|
+
fromUser,
|
|
88
|
+
normalizedFromUser,
|
|
89
|
+
sendPairingReply: sendTextToUser,
|
|
74
90
|
});
|
|
75
|
-
if (!
|
|
76
|
-
await sendTextToUser(dmPolicy.rejectMessage || "
|
|
77
|
-
return { ok: false, commandBody: nextCommandBody, isAdminUser };
|
|
91
|
+
if (!pairing.created && pairing.unsupported) {
|
|
92
|
+
await sendTextToUser(dmPolicy.rejectMessage || "当前私聊需先完成配对审批。");
|
|
78
93
|
}
|
|
94
|
+
return { ok: false, commandBody: nextCommandBody, isAdminUser };
|
|
95
|
+
}
|
|
96
|
+
if (dmAccess.decision !== "allow") {
|
|
97
|
+
await sendTextToUser(dmAccess.rejectText || dmPolicy.rejectMessage || "当前私聊账号未授权,请联系管理员。");
|
|
98
|
+
return { ok: false, commandBody: nextCommandBody, isAdminUser };
|
|
79
99
|
}
|
|
80
100
|
}
|
|
81
|
-
|
|
82
|
-
const senderAllowed =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
101
|
+
|
|
102
|
+
const senderAllowed =
|
|
103
|
+
isAdminUser ||
|
|
104
|
+
allowFromPolicy.allowFrom.includes("*") ||
|
|
105
|
+
isWecomSenderAllowed({
|
|
106
|
+
senderId: normalizedFromUser,
|
|
107
|
+
allowFrom: allowFromPolicy.allowFrom,
|
|
108
|
+
});
|
|
86
109
|
if (!senderAllowed) {
|
|
87
110
|
api?.logger?.warn?.(
|
|
88
111
|
`wecom: sender blocked by allowFrom account=${config?.accountId || "default"} user=${normalizedFromUser}`,
|
|
@@ -11,6 +11,26 @@ export function createWecomTextSender({
|
|
|
11
11
|
throw new Error("createWecomTextSender: sendWecomTypedMessage is required");
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
const targetSendChains = new Map();
|
|
15
|
+
|
|
16
|
+
function buildTargetKey({ corpId, agentId, toUser, toParty, toTag, chatId } = {}) {
|
|
17
|
+
const accountKey = `${corpId || "corp:unknown"}:${agentId || "agent:unknown"}`;
|
|
18
|
+
if (chatId) return `${accountKey}:chat:${chatId}`;
|
|
19
|
+
return `${accountKey}:direct:${[toUser, toParty, toTag].filter(Boolean).join("|") || "unknown"}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function enqueueTargetSend(targetKey, task) {
|
|
23
|
+
const previous = targetSendChains.get(targetKey) || Promise.resolve();
|
|
24
|
+
const run = previous.catch(() => {}).then(task);
|
|
25
|
+
const tracked = run.finally(() => {
|
|
26
|
+
if (targetSendChains.get(targetKey) === tracked) {
|
|
27
|
+
targetSendChains.delete(targetKey);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
targetSendChains.set(targetKey, tracked);
|
|
31
|
+
return run;
|
|
32
|
+
}
|
|
33
|
+
|
|
14
34
|
async function sendWecomTextSingle({
|
|
15
35
|
corpId,
|
|
16
36
|
corpSecret,
|
|
@@ -57,29 +77,32 @@ export function createWecomTextSender({
|
|
|
57
77
|
logger,
|
|
58
78
|
proxyUrl,
|
|
59
79
|
}) {
|
|
60
|
-
const
|
|
61
|
-
|
|
80
|
+
const targetKey = buildTargetKey({ corpId, agentId, toUser, toParty, toTag, chatId });
|
|
81
|
+
return enqueueTargetSend(targetKey, async () => {
|
|
82
|
+
const chunks = splitWecomText(text);
|
|
83
|
+
logger?.info?.(`wecom: splitting message into ${chunks.length} chunks, total bytes=${getByteLength(text)}`);
|
|
62
84
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// eslint-disable-next-line no-await-in-loop
|
|
66
|
-
await sendWecomTextSingle({
|
|
67
|
-
corpId,
|
|
68
|
-
corpSecret,
|
|
69
|
-
agentId,
|
|
70
|
-
toUser,
|
|
71
|
-
toParty,
|
|
72
|
-
toTag,
|
|
73
|
-
chatId,
|
|
74
|
-
text: chunks[i],
|
|
75
|
-
logger,
|
|
76
|
-
proxyUrl,
|
|
77
|
-
});
|
|
78
|
-
if (i < chunks.length - 1) {
|
|
85
|
+
for (let i = 0; i < chunks.length; i += 1) {
|
|
86
|
+
logger?.info?.(`wecom: sending chunk ${i + 1}/${chunks.length}, bytes=${getByteLength(chunks[i])}`);
|
|
79
87
|
// eslint-disable-next-line no-await-in-loop
|
|
80
|
-
await
|
|
88
|
+
await sendWecomTextSingle({
|
|
89
|
+
corpId,
|
|
90
|
+
corpSecret,
|
|
91
|
+
agentId,
|
|
92
|
+
toUser,
|
|
93
|
+
toParty,
|
|
94
|
+
toTag,
|
|
95
|
+
chatId,
|
|
96
|
+
text: chunks[i],
|
|
97
|
+
logger,
|
|
98
|
+
proxyUrl,
|
|
99
|
+
});
|
|
100
|
+
if (i < chunks.length - 1) {
|
|
101
|
+
// eslint-disable-next-line no-await-in-loop
|
|
102
|
+
await sleep(300);
|
|
103
|
+
}
|
|
81
104
|
}
|
|
82
|
-
}
|
|
105
|
+
});
|
|
83
106
|
}
|
|
84
107
|
|
|
85
108
|
return {
|
|
@@ -40,9 +40,11 @@ export function createWecomBotInboundContentBuilder({
|
|
|
40
40
|
botProxyUrl,
|
|
41
41
|
msgType = "text",
|
|
42
42
|
commandBody = "",
|
|
43
|
+
normalizedImageEntries = [],
|
|
43
44
|
normalizedImageUrls = [],
|
|
44
45
|
normalizedFileUrl = "",
|
|
45
46
|
normalizedFileName = "",
|
|
47
|
+
normalizedFileAesKey = "",
|
|
46
48
|
normalizedVoiceUrl = "",
|
|
47
49
|
normalizedVoiceMediaId = "",
|
|
48
50
|
normalizedVoiceContentType = "",
|
|
@@ -52,12 +54,17 @@ export function createWecomBotInboundContentBuilder({
|
|
|
52
54
|
const tempPathsToCleanup = [];
|
|
53
55
|
let messageText = String(commandBody ?? "").trim();
|
|
54
56
|
|
|
55
|
-
if (normalizedImageUrls.length > 0) {
|
|
57
|
+
if (normalizedImageUrls.length > 0 || normalizedImageEntries.length > 0) {
|
|
56
58
|
const fetchedImagePaths = [];
|
|
57
|
-
const
|
|
59
|
+
const imageEntriesToFetch =
|
|
60
|
+
Array.isArray(normalizedImageEntries) && normalizedImageEntries.length > 0
|
|
61
|
+
? normalizedImageEntries.slice(0, 3)
|
|
62
|
+
: normalizedImageUrls.slice(0, 3).map((url) => ({ url, aesKey: "" }));
|
|
58
63
|
const tempDir = join(tmpdir(), WECOM_TEMP_DIR_NAME);
|
|
59
64
|
await mkdir(tempDir, { recursive: true });
|
|
60
|
-
for (const
|
|
65
|
+
for (const imageEntry of imageEntriesToFetch) {
|
|
66
|
+
const imageUrl = String(imageEntry?.url ?? "").trim();
|
|
67
|
+
const imageAesKey = String(imageEntry?.aesKey ?? "").trim();
|
|
61
68
|
try {
|
|
62
69
|
const { buffer, contentType } = await fetchMediaFromUrl(imageUrl, {
|
|
63
70
|
proxyUrl: botProxyUrl,
|
|
@@ -73,10 +80,11 @@ export function createWecomBotInboundContentBuilder({
|
|
|
73
80
|
let effectiveBuffer = buffer;
|
|
74
81
|
let effectiveImageType =
|
|
75
82
|
normalizedType.startsWith("image/") ? normalizedType : detectImageContentTypeFromBuffer(buffer);
|
|
76
|
-
|
|
83
|
+
const decryptAesKey = imageAesKey || String(botModeConfig?.encodingAesKey ?? "").trim();
|
|
84
|
+
if (!effectiveImageType && decryptAesKey) {
|
|
77
85
|
try {
|
|
78
86
|
const decryptedBuffer = decryptWecomMediaBuffer({
|
|
79
|
-
aesKey:
|
|
87
|
+
aesKey: decryptAesKey,
|
|
80
88
|
encryptedBuffer: buffer,
|
|
81
89
|
});
|
|
82
90
|
const decryptedImageType = detectImageContentTypeFromBuffer(decryptedBuffer);
|
|
@@ -158,7 +166,7 @@ export function createWecomBotInboundContentBuilder({
|
|
|
158
166
|
});
|
|
159
167
|
const decrypted = smartDecryptWecomFileBuffer({
|
|
160
168
|
buffer: downloaded.buffer,
|
|
161
|
-
aesKey: botModeConfig?.encodingAesKey,
|
|
169
|
+
aesKey: normalizedFileAesKey || botModeConfig?.encodingAesKey,
|
|
162
170
|
contentType: downloaded.contentType,
|
|
163
171
|
sourceUrl: downloaded.finalUrl || normalizedFileUrl,
|
|
164
172
|
decryptFn: decryptWecomMediaBuffer,
|