@dingxiang-me/openclaw-wechat 2.0.1 → 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 +13 -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/bot-inbound-executor.js +1 -1
- package/src/wecom/bot-inbound-guards.js +47 -22
- package/src/wecom/bot-long-connection-manager.js +4 -4
- 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/pairing.js +188 -0
- package/src/wecom/plugin-constants.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ 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
|
+
|
|
7
20
|
## [2.0.1] - 2026-03-14
|
|
8
21
|
|
|
9
22
|
### Fixed
|
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.0
|
|
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.0
|
|
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}`,
|
|
@@ -125,7 +125,7 @@ export async function executeWecomBotInboundFlow(payload = {}) {
|
|
|
125
125
|
}
|
|
126
126
|
state.commandBody = groupGuardResult.commandBody;
|
|
127
127
|
|
|
128
|
-
const commandGuardResult = applyWecomBotCommandAndSenderGuard({
|
|
128
|
+
const commandGuardResult = await applyWecomBotCommandAndSenderGuard({
|
|
129
129
|
api,
|
|
130
130
|
accountId: state.accountId,
|
|
131
131
|
fromUser,
|
|
@@ -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(`bot-inbound-guards: ${name} is required`);
|
|
@@ -50,7 +55,7 @@ export function applyWecomBotGroupChatGuard({
|
|
|
50
55
|
};
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
export function applyWecomBotCommandAndSenderGuard({
|
|
58
|
+
export async function applyWecomBotCommandAndSenderGuard({
|
|
54
59
|
api,
|
|
55
60
|
accountId = "default",
|
|
56
61
|
fromUser,
|
|
@@ -77,37 +82,57 @@ export function applyWecomBotCommandAndSenderGuard({
|
|
|
77
82
|
const commandPolicy = resolveWecomCommandPolicy(api);
|
|
78
83
|
const isAdminUser = commandPolicy.adminUsers.includes(String(normalizedFromUser ?? "").trim().toLowerCase());
|
|
79
84
|
const dmPolicy = resolveWecomDmPolicy(api, accountId, {});
|
|
85
|
+
const allowFromPolicy = resolveWecomAllowFromPolicy(api, accountId, {});
|
|
86
|
+
|
|
80
87
|
if (!isGroupChat) {
|
|
81
|
-
|
|
88
|
+
const dmAccess = await resolveWecomDirectMessageAccess({
|
|
89
|
+
api,
|
|
90
|
+
accountId,
|
|
91
|
+
dmPolicy,
|
|
92
|
+
allowFromPolicy,
|
|
93
|
+
normalizedFromUser,
|
|
94
|
+
isAdminUser,
|
|
95
|
+
isWecomSenderAllowed,
|
|
96
|
+
});
|
|
97
|
+
if (dmAccess.decision === "pairing") {
|
|
98
|
+
const pairing = await issueWecomPairingChallenge({
|
|
99
|
+
api,
|
|
100
|
+
accountId,
|
|
101
|
+
fromUser,
|
|
102
|
+
normalizedFromUser,
|
|
103
|
+
sendPairingReply: async (text) => text,
|
|
104
|
+
});
|
|
82
105
|
return {
|
|
83
106
|
ok: false,
|
|
84
|
-
finishText:
|
|
107
|
+
finishText:
|
|
108
|
+
pairing.created
|
|
109
|
+
? pairing.replyText || ""
|
|
110
|
+
: !pairing.unsupported
|
|
111
|
+
? ""
|
|
112
|
+
: dmPolicy.rejectMessage || "当前私聊需先完成配对审批。",
|
|
85
113
|
commandBody: String(commandBody ?? ""),
|
|
86
114
|
isAdminUser,
|
|
87
115
|
commandPolicy,
|
|
88
116
|
};
|
|
89
117
|
}
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
finishText: dmPolicy.rejectMessage || "当前私聊账号未授权,请联系管理员。",
|
|
99
|
-
commandBody: String(commandBody ?? ""),
|
|
100
|
-
isAdminUser,
|
|
101
|
-
commandPolicy,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
118
|
+
if (dmAccess.decision !== "allow") {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
finishText: dmAccess.rejectText || dmPolicy.rejectMessage || "当前私聊账号未授权,请联系管理员。",
|
|
122
|
+
commandBody: String(commandBody ?? ""),
|
|
123
|
+
isAdminUser,
|
|
124
|
+
commandPolicy,
|
|
125
|
+
};
|
|
104
126
|
}
|
|
105
127
|
}
|
|
106
|
-
|
|
107
|
-
const senderAllowed =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
128
|
+
|
|
129
|
+
const senderAllowed =
|
|
130
|
+
isAdminUser ||
|
|
131
|
+
allowFromPolicy.allowFrom.includes("*") ||
|
|
132
|
+
isWecomSenderAllowed({
|
|
133
|
+
senderId: normalizedFromUser,
|
|
134
|
+
allowFrom: allowFromPolicy.allowFrom,
|
|
135
|
+
});
|
|
111
136
|
if (!senderAllowed) {
|
|
112
137
|
return {
|
|
113
138
|
ok: false,
|
|
@@ -260,7 +260,7 @@ export function createWecomBotLongConnectionManager({
|
|
|
260
260
|
if (!agent) {
|
|
261
261
|
agent = new wsProxyAgentCtor(proxyUrl);
|
|
262
262
|
wsProxyAgentCache.set(proxyUrl, agent);
|
|
263
|
-
api?.logger?.
|
|
263
|
+
api?.logger?.debug?.(
|
|
264
264
|
`wecom(bot-longconn): websocket proxy enabled account=${client.accountId} proxy=${printableProxy}`,
|
|
265
265
|
);
|
|
266
266
|
}
|
|
@@ -729,7 +729,7 @@ export function createWecomBotLongConnectionManager({
|
|
|
729
729
|
transport: "bot.longConnection",
|
|
730
730
|
});
|
|
731
731
|
startPingLoop(client, api);
|
|
732
|
-
api?.logger?.
|
|
732
|
+
api?.logger?.debug?.(`wecom(bot-longconn): subscribed account=${client.accountId}`);
|
|
733
733
|
} else {
|
|
734
734
|
api?.logger?.warn?.(
|
|
735
735
|
`wecom(bot-longconn): subscribe failed account=${client.accountId} errcode=${errcode} errmsg=${errmsg}`,
|
|
@@ -799,7 +799,7 @@ export function createWecomBotLongConnectionManager({
|
|
|
799
799
|
`wecom(bot-longconn): websocket proxy init failed account=${client.accountId}: ${String(err?.message || err)}`,
|
|
800
800
|
);
|
|
801
801
|
}
|
|
802
|
-
api?.logger?.
|
|
802
|
+
api?.logger?.debug?.(
|
|
803
803
|
`wecom(bot-longconn): connect attempt account=${client.accountId} marker=${LONG_CONNECTION_RUNTIME_MARKER} url=${wsUrl} proxy=${proxyUrl || "direct"} wsCtor=${String(webSocketCtor?.name || "unknown")}`,
|
|
804
804
|
);
|
|
805
805
|
client.ws = new webSocketCtor(wsUrl, [], wsOptions);
|
|
@@ -822,7 +822,7 @@ export function createWecomBotLongConnectionManager({
|
|
|
822
822
|
secret: client.config.longConnection.secret,
|
|
823
823
|
},
|
|
824
824
|
}) || "";
|
|
825
|
-
api?.logger?.
|
|
825
|
+
api?.logger?.debug?.(`wecom(bot-longconn): socket opened account=${client.accountId} url=${wsUrl}`);
|
|
826
826
|
});
|
|
827
827
|
bindSocketListener(client.ws, "message", (event) => {
|
|
828
828
|
void handleSocketMessage(client, api, event);
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
getWecomChannelInboundActivity,
|
|
4
4
|
getWecomInboundActivity,
|
|
5
5
|
} from "./channel-status-state.js";
|
|
6
|
+
import { normalizeWecomAllowFromEntry } from "../core.js";
|
|
6
7
|
|
|
7
8
|
function assertFunction(name, fn) {
|
|
8
9
|
if (typeof fn !== "function") {
|
|
@@ -160,9 +161,30 @@ export function createWecomChannelPlugin({
|
|
|
160
161
|
id: "wecom",
|
|
161
162
|
label: "企业微信 WeCom",
|
|
162
163
|
selectionLabel: "企业微信 WeCom(自建应用/Bot)",
|
|
164
|
+
detailLabel: "企业微信自建应用 / Bot",
|
|
163
165
|
docsPath: "/channels/wecom",
|
|
164
166
|
blurb: "企业微信消息通道(自建应用回调 + Bot 回调 + 发送 API)。",
|
|
165
167
|
aliases: ["wework", "qiwei", "wxwork"],
|
|
168
|
+
systemImage: "building.2.crop.circle",
|
|
169
|
+
quickstartAllowFrom: true,
|
|
170
|
+
},
|
|
171
|
+
pairing: {
|
|
172
|
+
idLabel: "wecomUserId",
|
|
173
|
+
normalizeAllowEntry: (entry) => normalizeWecomAllowFromEntry(entry),
|
|
174
|
+
notifyApproval: async ({ cfg, id }) => {
|
|
175
|
+
const normalizedUserId = normalizeWecomAllowFromEntry(id);
|
|
176
|
+
if (!normalizedUserId) return;
|
|
177
|
+
const config = getWecomConfig({ config: cfg }, "default");
|
|
178
|
+
if (!config?.corpId || !config?.corpSecret || !config?.agentId) return;
|
|
179
|
+
await sendWecomText({
|
|
180
|
+
corpId: config.corpId,
|
|
181
|
+
corpSecret: config.corpSecret,
|
|
182
|
+
agentId: config.agentId,
|
|
183
|
+
toUser: normalizedUserId,
|
|
184
|
+
text: "OpenClaw: your access has been approved.",
|
|
185
|
+
proxyUrl: config.outboundProxy,
|
|
186
|
+
});
|
|
187
|
+
},
|
|
166
188
|
},
|
|
167
189
|
configSchema: {
|
|
168
190
|
schema: wecomChannelConfigSchema,
|
|
@@ -98,6 +98,7 @@ export function createWecomCommandHandlers({
|
|
|
98
98
|
async function handleStatusCommand({ api, fromUser, corpId, corpSecret, agentId, accountId, proxyUrl }) {
|
|
99
99
|
const config = getWecomConfig(api, accountId);
|
|
100
100
|
const accountIds = listWecomAccountIds(api);
|
|
101
|
+
const bindingsCount = Array.isArray(api?.config?.bindings) ? api.config.bindings.length : 0;
|
|
101
102
|
const webhookTargetAliases = listWebhookTargetAliases(config);
|
|
102
103
|
const voiceConfig = resolveWecomVoiceTranscriptionConfig(api);
|
|
103
104
|
const voiceRuntimeInfo = await inspectWecomVoiceTranscriptionRuntime({ api, voiceConfig });
|
|
@@ -134,6 +135,7 @@ export function createWecomCommandHandlers({
|
|
|
134
135
|
webhookBotPolicy,
|
|
135
136
|
dynamicAgentPolicy,
|
|
136
137
|
observabilityMetrics,
|
|
138
|
+
bindingsCount,
|
|
137
139
|
});
|
|
138
140
|
|
|
139
141
|
await sendWecomText({
|
|
@@ -150,6 +152,8 @@ export function createWecomCommandHandlers({
|
|
|
150
152
|
|
|
151
153
|
function buildBotStatus(api, fromUser) {
|
|
152
154
|
const allWebhookTargetAliases = listAllWebhookTargetAliases(api);
|
|
155
|
+
const config = getWecomConfig(api, "default");
|
|
156
|
+
const bindingsCount = Array.isArray(api?.config?.bindings) ? api.config.bindings.length : 0;
|
|
153
157
|
const commandPolicy = resolveWecomCommandPolicy(api);
|
|
154
158
|
const allowFromPolicy = resolveWecomAllowFromPolicy(api, "default", {});
|
|
155
159
|
const dmPolicy = resolveWecomDmPolicy(api, "default", {});
|
|
@@ -176,6 +180,8 @@ export function createWecomCommandHandlers({
|
|
|
176
180
|
webhookBotPolicy,
|
|
177
181
|
dynamicAgentPolicy,
|
|
178
182
|
observabilityMetrics,
|
|
183
|
+
config,
|
|
184
|
+
bindingsCount,
|
|
179
185
|
});
|
|
180
186
|
}
|
|
181
187
|
|
|
@@ -17,9 +17,29 @@ function buildDmPolicyStatusLine(dmPolicy = {}) {
|
|
|
17
17
|
const count = Array.isArray(dmPolicy?.allowFrom) ? dmPolicy.allowFrom.length : 0;
|
|
18
18
|
return `✅ 私聊策略:白名单(${count} 个用户)`;
|
|
19
19
|
}
|
|
20
|
+
if (mode === "pairing") {
|
|
21
|
+
const count = Array.isArray(dmPolicy?.allowFrom) ? dmPolicy.allowFrom.length : 0;
|
|
22
|
+
return count > 0
|
|
23
|
+
? `✅ 私聊策略:配对审批(pairing,显式放行 ${count} 个用户)`
|
|
24
|
+
: "✅ 私聊策略:配对审批(pairing,首次私聊需审批)";
|
|
25
|
+
}
|
|
20
26
|
return "ℹ️ 私聊策略:开放(open)";
|
|
21
27
|
}
|
|
22
28
|
|
|
29
|
+
function buildRoutePolicyStatusLine({ bindingsCount = 0, dynamicAgentPolicy = {} } = {}) {
|
|
30
|
+
const count = Math.max(0, Number(bindingsCount) || 0);
|
|
31
|
+
if (count > 0 && dynamicAgentPolicy?.enabled) {
|
|
32
|
+
return `✅ 路由策略:OpenClaw bindings 优先(${count} 条),动态 Agent 补充(mode=${dynamicAgentPolicy.mode})`;
|
|
33
|
+
}
|
|
34
|
+
if (count > 0) {
|
|
35
|
+
return `✅ 路由策略:OpenClaw bindings(${count} 条)`;
|
|
36
|
+
}
|
|
37
|
+
if (dynamicAgentPolicy?.enabled) {
|
|
38
|
+
return `✅ 路由策略:动态 Agent(mode=${dynamicAgentPolicy.mode})`;
|
|
39
|
+
}
|
|
40
|
+
return "ℹ️ 路由策略:使用 OpenClaw 默认 Agent 路由";
|
|
41
|
+
}
|
|
42
|
+
|
|
23
43
|
function buildEventPolicyStatusLine(eventPolicy = {}) {
|
|
24
44
|
if (eventPolicy?.enabled === false) {
|
|
25
45
|
return "⚠️ 事件策略:已关闭";
|
|
@@ -80,6 +100,43 @@ function buildVoiceStatusLine(voiceConfig = {}, voiceRuntimeInfo = null) {
|
|
|
80
100
|
return `${baseLine}(${commandState},${ffmpegState})${issueSuffix}`;
|
|
81
101
|
}
|
|
82
102
|
|
|
103
|
+
function buildAgentReadinessLines({ config = {}, voiceConfig = {} } = {}) {
|
|
104
|
+
const canReceive = Boolean(config?.callbackToken && config?.callbackAesKey && config?.webhookPath);
|
|
105
|
+
const canReply = Boolean(config?.corpId && config?.corpSecret && config?.agentId);
|
|
106
|
+
const docEnabled = config?.tools?.doc !== false;
|
|
107
|
+
const voiceEnabled = voiceConfig?.enabled === true;
|
|
108
|
+
return [
|
|
109
|
+
`${canReceive ? "✅" : "⚠️"} 收消息:${canReceive ? "Agent 回调已配置" : "缺少 callbackToken / callbackAesKey / webhookPath"}`,
|
|
110
|
+
`${canReply ? "✅" : "⚠️"} 回消息:${canReply ? "Agent API 可用" : "缺少 corpId / corpSecret / agentId"}`,
|
|
111
|
+
`${canReply ? "✅" : "⚠️"} 主动发送:${canReply ? "文本/图片/文件可主动发送" : "主动发送依赖 Agent API 配置"}`,
|
|
112
|
+
`${canReply ? "✅" : "⚠️"} 媒体链路:${canReply ? "图片/文件/语音回退链路可用" : "需先完成 Agent API 配置"}`,
|
|
113
|
+
`${voiceEnabled ? "✅" : "ℹ️"} 语音能力:${voiceEnabled ? "已启用本地转写回退" : "仅使用企业微信 Recognition"}`,
|
|
114
|
+
`${docEnabled ? "✅" : "ℹ️"} 文档工具:${docEnabled ? "wecom_doc 已启用" : "未启用"}`,
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildBotReadinessLines({ botConfig = {}, config = {} } = {}) {
|
|
119
|
+
const longConnectionEnabled =
|
|
120
|
+
botConfig?.longConnection?.enabled === true &&
|
|
121
|
+
Boolean(String(botConfig?.longConnection?.botId ?? "").trim()) &&
|
|
122
|
+
Boolean(String(botConfig?.longConnection?.secret ?? "").trim());
|
|
123
|
+
const webhookEnabled =
|
|
124
|
+
botConfig?.enabled === true &&
|
|
125
|
+
Boolean(String(botConfig?.token ?? "").trim()) &&
|
|
126
|
+
Boolean(String(botConfig?.encodingAesKey ?? "").trim()) &&
|
|
127
|
+
Boolean(String(botConfig?.webhookPath ?? "").trim());
|
|
128
|
+
const canReceive = longConnectionEnabled || webhookEnabled;
|
|
129
|
+
const canReply = canReceive;
|
|
130
|
+
const docEnabled = config?.tools?.doc !== false;
|
|
131
|
+
return [
|
|
132
|
+
`${canReceive ? "✅" : "⚠️"} 收消息:${canReceive ? (longConnectionEnabled ? "Bot 长连接/回调已配置" : "Bot webhook 已配置") : "缺少 Bot webhook 或长连接凭证"}`,
|
|
133
|
+
`${canReply ? "✅" : "⚠️"} 回消息:${canReply ? "原生 stream + fallback 可用" : "需先完成 Bot 配置"}`,
|
|
134
|
+
`${canReceive ? "ℹ️" : "⚠️"} 主动发送:${canReceive ? "Bot 以会话回包为主;跨会话主动发送建议配合 Agent/Webhook 目标" : "需先完成 Bot 配置"}`,
|
|
135
|
+
`${canReceive ? "✅" : "⚠️"} 媒体链路:${canReceive ? "Bot 图片/文件/PDF 入站理解已启用" : "需先完成 Bot 配置"}`,
|
|
136
|
+
`${docEnabled ? "✅" : "ℹ️"} 文档工具:${docEnabled ? "wecom_doc 已启用" : "未启用"}`,
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
|
|
83
140
|
export function buildAgentStatusText({
|
|
84
141
|
fromUser,
|
|
85
142
|
config,
|
|
@@ -100,9 +157,11 @@ export function buildAgentStatusText({
|
|
|
100
157
|
webhookBotPolicy,
|
|
101
158
|
dynamicAgentPolicy,
|
|
102
159
|
observabilityMetrics,
|
|
160
|
+
bindingsCount = 0,
|
|
103
161
|
} = {}) {
|
|
104
162
|
const proxyEnabled = Boolean(config?.outboundProxy);
|
|
105
163
|
const voiceStatusLine = buildVoiceStatusLine(voiceConfig, voiceRuntimeInfo);
|
|
164
|
+
const readinessLines = buildAgentReadinessLines({ config, voiceConfig });
|
|
106
165
|
const commandPolicyLine = commandPolicy.enabled
|
|
107
166
|
? `✅ 指令白名单已启用(${commandPolicy.allowlist.length} 条,管理员 ${commandPolicy.adminUsers.length} 人)`
|
|
108
167
|
: "ℹ️ 指令白名单未启用";
|
|
@@ -141,6 +200,7 @@ export function buildAgentStatusText({
|
|
|
141
200
|
const dynamicAgentPolicyLine = dynamicAgentPolicy.enabled
|
|
142
201
|
? `✅ 动态 Agent 路由已启用(mode=${dynamicAgentPolicy.mode},用户映射 ${Object.keys(dynamicAgentPolicy.userMap || {}).length},群映射 ${Object.keys(dynamicAgentPolicy.groupMap || {}).length})`
|
|
143
202
|
: "ℹ️ 动态 Agent 路由未启用";
|
|
203
|
+
const routePolicyLine = buildRoutePolicyStatusLine({ bindingsCount, dynamicAgentPolicy });
|
|
144
204
|
const entryVisibilityLine = "✅ 微信插件入口联系人:Agent 模式可见(自建应用)";
|
|
145
205
|
const observabilityLines = buildObservabilityStatusLines(observabilityMetrics);
|
|
146
206
|
|
|
@@ -153,8 +213,8 @@ export function buildAgentStatusText({
|
|
|
153
213
|
插件版本:${pluginVersion}
|
|
154
214
|
|
|
155
215
|
功能状态:
|
|
216
|
+
${readinessLines.join("\n")}
|
|
156
217
|
✅ 文本消息
|
|
157
|
-
✅ 图片发送/接收
|
|
158
218
|
✅ 消息分段 (2048字符)
|
|
159
219
|
✅ 命令系统
|
|
160
220
|
✅ Markdown 转换
|
|
@@ -172,6 +232,7 @@ ${streamManagerPolicyLine}
|
|
|
172
232
|
${webhookBotPolicyLine}
|
|
173
233
|
${webhookTargetsLine}
|
|
174
234
|
${dynamicAgentPolicyLine}
|
|
235
|
+
${routePolicyLine}
|
|
175
236
|
${entryVisibilityLine}
|
|
176
237
|
${proxyEnabled ? "✅ WeCom 出站代理已启用" : "ℹ️ WeCom 出站代理未启用"}
|
|
177
238
|
${voiceStatusLine}
|
|
@@ -194,7 +255,10 @@ export function buildBotStatusText({
|
|
|
194
255
|
webhookBotPolicy,
|
|
195
256
|
dynamicAgentPolicy,
|
|
196
257
|
observabilityMetrics,
|
|
258
|
+
config = {},
|
|
259
|
+
bindingsCount = 0,
|
|
197
260
|
} = {}) {
|
|
261
|
+
const readinessLines = buildBotReadinessLines({ botConfig, config });
|
|
198
262
|
const commandPolicyLine = commandPolicy.enabled
|
|
199
263
|
? `✅ 指令白名单已启用(${commandPolicy.allowlist.length} 条,管理员 ${commandPolicy.adminUsers.length} 人)`
|
|
200
264
|
: "ℹ️ 指令白名单未启用";
|
|
@@ -227,6 +291,7 @@ export function buildBotStatusText({
|
|
|
227
291
|
const dynamicAgentPolicyLine = dynamicAgentPolicy.enabled
|
|
228
292
|
? `✅ 动态 Agent 路由已启用(mode=${dynamicAgentPolicy.mode},用户映射 ${Object.keys(dynamicAgentPolicy.userMap || {}).length},群映射 ${Object.keys(dynamicAgentPolicy.groupMap || {}).length})`
|
|
229
293
|
: "ℹ️ 动态 Agent 路由未启用";
|
|
294
|
+
const routePolicyLine = buildRoutePolicyStatusLine({ bindingsCount, dynamicAgentPolicy });
|
|
230
295
|
const entryVisibilityLine = "ℹ️ 微信插件入口联系人:Bot 模式通常不显示(请通过机器人会话/群聊入口触发)";
|
|
231
296
|
const observabilityLines = buildObservabilityStatusLines(observabilityMetrics);
|
|
232
297
|
return `📊 系统状态
|
|
@@ -237,6 +302,7 @@ export function buildBotStatusText({
|
|
|
237
302
|
Bot Webhook:${botConfig.webhookPath}
|
|
238
303
|
|
|
239
304
|
功能状态:
|
|
305
|
+
${readinessLines.join("\n")}
|
|
240
306
|
✅ 原生流式回复(stream)
|
|
241
307
|
${commandPolicyLine}
|
|
242
308
|
${allowFromPolicyLine}
|
|
@@ -246,8 +312,10 @@ ${groupPolicyLine}
|
|
|
246
312
|
${fallbackPolicyLine}
|
|
247
313
|
${streamManagerPolicyLine}
|
|
248
314
|
${webhookBotPolicyLine}
|
|
315
|
+
${longConnectionLine}
|
|
249
316
|
${webhookTargetsLine}
|
|
250
317
|
${dynamicAgentPolicyLine}
|
|
318
|
+
${routePolicyLine}
|
|
251
319
|
${entryVisibilityLine}
|
|
252
320
|
${observabilityLines.status}
|
|
253
321
|
${observabilityLines.recent}`;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { normalizeWecomAllowFromEntry } from "../core.js";
|
|
2
|
+
|
|
3
|
+
function normalizeAccountId(accountId) {
|
|
4
|
+
const normalized = String(accountId ?? "default").trim().toLowerCase();
|
|
5
|
+
return normalized || "default";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function uniqueAllowFrom(values = []) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
for (const value of Array.isArray(values) ? values : []) {
|
|
12
|
+
const normalized = normalizeWecomAllowFromEntry(value);
|
|
13
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
14
|
+
seen.add(normalized);
|
|
15
|
+
out.push(normalized);
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getPairingRuntime(api) {
|
|
21
|
+
const pairing = api?.runtime?.channel?.pairing;
|
|
22
|
+
if (!pairing || typeof pairing !== "object") return null;
|
|
23
|
+
if (typeof pairing.readAllowFromStore !== "function") return null;
|
|
24
|
+
if (typeof pairing.upsertPairingRequest !== "function") return null;
|
|
25
|
+
if (typeof pairing.buildPairingReply !== "function") return null;
|
|
26
|
+
return pairing;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function matchesAllowFrom({ allowFrom = [], senderId = "", isWecomSenderAllowed } = {}) {
|
|
30
|
+
const normalizedAllowFrom = Array.isArray(allowFrom) ? allowFrom : [];
|
|
31
|
+
if (normalizedAllowFrom.includes("*")) return true;
|
|
32
|
+
if (typeof isWecomSenderAllowed !== "function") return false;
|
|
33
|
+
return isWecomSenderAllowed({
|
|
34
|
+
senderId,
|
|
35
|
+
allowFrom: normalizedAllowFrom,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function readWecomPairingAllowFromStore({ api, accountId = "default" } = {}) {
|
|
40
|
+
const pairing = getPairingRuntime(api);
|
|
41
|
+
if (!pairing) return [];
|
|
42
|
+
try {
|
|
43
|
+
const storeEntries = await pairing.readAllowFromStore({
|
|
44
|
+
channel: "wecom",
|
|
45
|
+
accountId: normalizeAccountId(accountId),
|
|
46
|
+
});
|
|
47
|
+
return uniqueAllowFrom(storeEntries);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
api?.logger?.warn?.(`wecom: failed to read pairing store: ${String(err?.message || err)}`);
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function resolveWecomDirectMessageAccess({
|
|
55
|
+
api,
|
|
56
|
+
accountId = "default",
|
|
57
|
+
dmPolicy = {},
|
|
58
|
+
allowFromPolicy = {},
|
|
59
|
+
normalizedFromUser = "",
|
|
60
|
+
isAdminUser = false,
|
|
61
|
+
isWecomSenderAllowed,
|
|
62
|
+
} = {}) {
|
|
63
|
+
const mode = String(dmPolicy?.mode ?? "open").trim().toLowerCase() || "open";
|
|
64
|
+
const normalizedSender = normalizeWecomAllowFromEntry(normalizedFromUser);
|
|
65
|
+
const configuredAllowFrom = uniqueAllowFrom(dmPolicy?.allowFrom);
|
|
66
|
+
const baseAllowFrom = Array.isArray(allowFromPolicy?.allowFrom) ? allowFromPolicy.allowFrom : [];
|
|
67
|
+
const senderAllowedByBasePolicy =
|
|
68
|
+
isAdminUser ||
|
|
69
|
+
matchesAllowFrom({
|
|
70
|
+
senderId: normalizedSender,
|
|
71
|
+
allowFrom: baseAllowFrom,
|
|
72
|
+
isWecomSenderAllowed,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (mode === "deny") {
|
|
76
|
+
return {
|
|
77
|
+
decision: "block",
|
|
78
|
+
reason: "dm-deny",
|
|
79
|
+
rejectText: dmPolicy?.rejectMessage || "当前渠道私聊已关闭,请联系管理员。",
|
|
80
|
+
configuredAllowFrom,
|
|
81
|
+
effectiveAllowFrom: configuredAllowFrom,
|
|
82
|
+
storeAllowFrom: [],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (mode === "open") {
|
|
87
|
+
return {
|
|
88
|
+
decision: senderAllowedByBasePolicy ? "allow" : "block",
|
|
89
|
+
reason: senderAllowedByBasePolicy ? "dm-open" : "allowFrom",
|
|
90
|
+
rejectText: senderAllowedByBasePolicy
|
|
91
|
+
? ""
|
|
92
|
+
: allowFromPolicy?.rejectMessage || "当前账号未授权,请联系管理员。",
|
|
93
|
+
configuredAllowFrom,
|
|
94
|
+
effectiveAllowFrom: configuredAllowFrom,
|
|
95
|
+
storeAllowFrom: [],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (mode === "allowlist") {
|
|
100
|
+
const allowed =
|
|
101
|
+
isAdminUser ||
|
|
102
|
+
matchesAllowFrom({
|
|
103
|
+
senderId: normalizedSender,
|
|
104
|
+
allowFrom: configuredAllowFrom,
|
|
105
|
+
isWecomSenderAllowed,
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
decision: senderAllowedByBasePolicy && allowed ? "allow" : "block",
|
|
109
|
+
reason: !senderAllowedByBasePolicy ? "allowFrom" : allowed ? "dm-allowlist" : "dm-allowlist-blocked",
|
|
110
|
+
rejectText: !senderAllowedByBasePolicy
|
|
111
|
+
? allowFromPolicy?.rejectMessage || "当前账号未授权,请联系管理员。"
|
|
112
|
+
: dmPolicy?.rejectMessage || "当前私聊账号未授权,请联系管理员。",
|
|
113
|
+
configuredAllowFrom,
|
|
114
|
+
effectiveAllowFrom: configuredAllowFrom,
|
|
115
|
+
storeAllowFrom: [],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!senderAllowedByBasePolicy) {
|
|
120
|
+
return {
|
|
121
|
+
decision: "block",
|
|
122
|
+
reason: "allowFrom",
|
|
123
|
+
rejectText: allowFromPolicy?.rejectMessage || "当前账号未授权,请联系管理员。",
|
|
124
|
+
configuredAllowFrom,
|
|
125
|
+
effectiveAllowFrom: configuredAllowFrom,
|
|
126
|
+
storeAllowFrom: [],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const storeAllowFrom = await readWecomPairingAllowFromStore({
|
|
131
|
+
api,
|
|
132
|
+
accountId,
|
|
133
|
+
});
|
|
134
|
+
const effectiveAllowFrom = uniqueAllowFrom([...configuredAllowFrom, ...storeAllowFrom]);
|
|
135
|
+
const allowed =
|
|
136
|
+
isAdminUser ||
|
|
137
|
+
matchesAllowFrom({
|
|
138
|
+
senderId: normalizedSender,
|
|
139
|
+
allowFrom: effectiveAllowFrom,
|
|
140
|
+
isWecomSenderAllowed,
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
decision: allowed ? "allow" : "pairing",
|
|
144
|
+
reason: allowed ? "dm-pairing-approved" : "dm-pairing",
|
|
145
|
+
rejectText: allowed ? "" : dmPolicy?.rejectMessage || "当前私聊需先完成配对审批。",
|
|
146
|
+
configuredAllowFrom,
|
|
147
|
+
effectiveAllowFrom,
|
|
148
|
+
storeAllowFrom,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function issueWecomPairingChallenge({
|
|
153
|
+
api,
|
|
154
|
+
accountId = "default",
|
|
155
|
+
fromUser = "",
|
|
156
|
+
normalizedFromUser = "",
|
|
157
|
+
sendPairingReply,
|
|
158
|
+
} = {}) {
|
|
159
|
+
const pairing = getPairingRuntime(api);
|
|
160
|
+
const senderId = normalizeWecomAllowFromEntry(normalizedFromUser || fromUser);
|
|
161
|
+
if (!pairing || !senderId || typeof sendPairingReply !== "function") {
|
|
162
|
+
return { created: false, unsupported: true };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { code, created } = await pairing.upsertPairingRequest({
|
|
166
|
+
channel: "wecom",
|
|
167
|
+
accountId: normalizeAccountId(accountId),
|
|
168
|
+
id: senderId,
|
|
169
|
+
meta: {
|
|
170
|
+
name: String(fromUser ?? "").trim() || undefined,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
if (!created) {
|
|
174
|
+
return { created: false, code };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const replyText = pairing.buildPairingReply({
|
|
178
|
+
channel: "wecom",
|
|
179
|
+
idLine: `Your WeCom user id: ${senderId}`,
|
|
180
|
+
code,
|
|
181
|
+
});
|
|
182
|
+
try {
|
|
183
|
+
await sendPairingReply(replyText);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
api?.logger?.warn?.(`wecom: pairing reply failed: ${String(err?.message || err)}`);
|
|
186
|
+
}
|
|
187
|
+
return { created: true, code, replyText };
|
|
188
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const MAX_REQUEST_BODY_SIZE = 1024 * 1024;
|
|
2
|
-
export const PLUGIN_VERSION = "2.0
|
|
2
|
+
export const PLUGIN_VERSION = "2.1.0";
|
|
3
3
|
export const WECOM_TEMP_DIR_NAME = "openclaw-wechat";
|
|
4
4
|
export const WECOM_TEMP_FILE_RETENTION_MS = 30 * 60 * 1000;
|
|
5
5
|
export const WECOM_MIN_FILE_SIZE = 5;
|