@wecom/wecom-openclaw-plugin 2026.3.31-beta.3 → 2026.3.31-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +171 -33
- package/dist/index.js +4 -59
- package/dist/src/agent/handler.js +3 -15
- package/dist/src/agent/webhook.d.ts +3 -2
- package/dist/src/agent/webhook.js +5 -3
- package/dist/src/channel.js +16 -1
- package/dist/src/dynamic-routing.d.ts +2 -12
- package/dist/src/dynamic-routing.js +11 -77
- package/dist/src/group-policy.js +5 -15
- package/dist/src/monitor.js +32 -90
- package/dist/src/types/config.d.ts +0 -9
- package/dist/src/utils.d.ts +3 -1
- package/dist/src/webhook/gateway.js +9 -1
- package/dist/src/webhook/monitor.js +0 -19
- package/package.json +1 -1
- package/skills/wecom-doc-manager/references/api-edit-doc-content.md +1 -1
- package/skills/wecom-meeting-create/SKILL.md +1 -1
- package/skills/wecom-meeting-manage/SKILL.md +5 -5
- package/skills/wecom-schedule/SKILL.md +3 -3
- package/skills/wecom-schedule/references/api-check-availability.md +2 -2
- package/skills/wecom-smartsheet-data/SKILL.md +5 -2
- package/skills/wecom-smartsheet-data/references/api-get-records.md +1 -0
- package/skills/wecom-smartsheet-data/references/cell-value-formats.md +35 -3
- package/dist/src/routing.d.ts +0 -24
- package/dist/src/routing.js +0 -34
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
**WeCom channel plugin for [OpenClaw](https://github.com/openclaw)** — by the Tencent WeCom team.
|
|
21
21
|
|
|
22
|
-
> A channel plugin powered by WeCom. Supports **Bot mode** (WebSocket long-polling) and **Agent mode** (HTTP webhook with XML encrypted callbacks). Direct messages, group chats, streaming replies, and proactive messaging.
|
|
22
|
+
> A channel plugin powered by WeCom. Supports **Bot mode** (WebSocket long-polling or HTTP webhook with JSON callbacks) and **Agent mode** (HTTP webhook with XML encrypted callbacks). Direct messages, group chats, streaming replies, and proactive messaging.
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
@@ -28,16 +28,27 @@
|
|
|
28
28
|
|
|
29
29
|
## ✨ Features
|
|
30
30
|
|
|
31
|
-
- 🔗 **Dual-mode**: Bot (WebSocket) and Agent (HTTP webhook) can run independently or together
|
|
31
|
+
- 🔗 **Dual-mode**: Bot (WebSocket / Webhook) and Agent (HTTP webhook) can run independently or together
|
|
32
32
|
- 💬 Supports both direct messages (DM) and group chat
|
|
33
|
-
- 📤 Proactive messaging to specific users or
|
|
34
|
-
- 🖼️ Receives and processes image
|
|
33
|
+
- 📤 Proactive messaging to specific users, groups, departments, or tags
|
|
34
|
+
- 🖼️ Receives and processes image, voice, video, file, and **mixed (图文混排)** messages with automatic downloading
|
|
35
|
+
- 🗣️ Voice-to-text: automatically extracts transcribed text from voice messages
|
|
36
|
+
- 💬 Quote message support: processes quoted text, image, voice, and file messages
|
|
35
37
|
- ⏳ Streaming replies with "thinking" placeholder messages (Bot mode)
|
|
36
38
|
- 🔐 Agent mode: AES-256-CBC encrypted XML callbacks with SHA1 signature verification
|
|
37
39
|
- 📝 Markdown formatting support for replies
|
|
40
|
+
- 🃏 Template card messages (text_notice, news_notice, button_interaction, vote_interaction, multiple_interaction) with **event callback handling**
|
|
38
41
|
- 🔒 Built-in access control: DM Policy (pairing / open / allowlist / disabled) and Group Policy (open / allowlist / disabled)
|
|
42
|
+
- 🔑 Command authorization: per-account command permission control with access group support
|
|
39
43
|
- 👥 Multi-account support: run multiple WeCom accounts with independent bot/agent configs
|
|
40
|
-
-
|
|
44
|
+
- 🧩 MCP tool integration (`wecom_mcp`) with interceptor pipeline (biz-error, media, smartpage-create, smartpage-export)
|
|
45
|
+
- 🎯 **15 built-in Skills**: contact lookup, doc management, todo, meeting, schedule, messaging, smartsheet, template cards, and more
|
|
46
|
+
- 🔀 Dynamic Agent routing: auto-create isolated agents per user/group
|
|
47
|
+
- 📁 Local file sending with configurable media path allowlist (`mediaLocalRoots`)
|
|
48
|
+
- 📊 Smart media size limits with auto-downgrade (image 10MB → file, video 10MB → file, voice 2MB/AMR-only → file, max 20MB)
|
|
49
|
+
- 🔄 **Bot-first, Agent-fallback** outbound delivery: auto fallback to Agent HTTP API when Bot WS is unavailable
|
|
50
|
+
- ⚡ Auto heartbeat keep-alive and reconnection (up to 10 reconnect attempts, 5 auth failure retries)
|
|
51
|
+
- 🛡️ Anti-kick protection: suppresses auto-restart on server-side disconnection to prevent mutual kicking loops
|
|
41
52
|
- 🧙 Interactive CLI setup wizard
|
|
42
53
|
|
|
43
54
|
---
|
|
@@ -46,7 +57,7 @@
|
|
|
46
57
|
|
|
47
58
|
### Requirements
|
|
48
59
|
|
|
49
|
-
- OpenClaw `>= 2026.
|
|
60
|
+
- OpenClaw `>= 2026.3.28`
|
|
50
61
|
|
|
51
62
|
### Quick Install
|
|
52
63
|
|
|
@@ -97,22 +108,81 @@ The plugin supports two connection modes that can be used independently or toget
|
|
|
97
108
|
|
|
98
109
|
| Mode | Connection | Message Format | Use Case |
|
|
99
110
|
|------|-----------|---------------|----------|
|
|
100
|
-
| **Bot** (智能体) | WebSocket
|
|
111
|
+
| **Bot** (智能体) | WebSocket (default) or HTTP webhook | JSON | Quick setup, streaming replies |
|
|
101
112
|
| **Agent** (自建应用) | HTTP webhook callbacks | XML | Enterprise apps, API-driven messaging |
|
|
102
113
|
|
|
114
|
+
> **Note**: Bot mode supports two connection methods via `connectionMode`:
|
|
115
|
+
> - `websocket` (default) — WebSocket long-polling, requires `botId` + `secret`
|
|
116
|
+
> - `webhook` — HTTP callback, requires `token` + `encodingAESKey`
|
|
117
|
+
|
|
103
118
|
### Bot Mode Configuration
|
|
104
119
|
|
|
120
|
+
#### Core Settings
|
|
121
|
+
|
|
122
|
+
| Config Path | Description | Options | Default |
|
|
123
|
+
|---|---|---|---|
|
|
124
|
+
| `channels.wecom.enabled` | Enable the channel | `true` / `false` | `false` |
|
|
125
|
+
| `channels.wecom.connectionMode` | Bot connection mode | `websocket` / `webhook` | `websocket` |
|
|
126
|
+
| `channels.wecom.name` | Account display name | — | `企业微信` |
|
|
127
|
+
|
|
128
|
+
#### WebSocket Mode (default)
|
|
129
|
+
|
|
105
130
|
| Config Path | Description | Options | Default |
|
|
106
131
|
|---|---|---|---|
|
|
107
132
|
| `channels.wecom.botId` | WeCom bot ID | — | — |
|
|
108
133
|
| `channels.wecom.secret` | WeCom bot secret | — | — |
|
|
109
|
-
| `channels.wecom.enabled` | Enable the channel | `true` / `false` | `false` |
|
|
110
134
|
| `channels.wecom.websocketUrl` | WebSocket endpoint | — | `wss://openws.work.weixin.qq.com` |
|
|
135
|
+
| `channels.wecom.sendThinkingMessage` | Send "thinking" placeholder | `true` / `false` | `true` |
|
|
136
|
+
|
|
137
|
+
#### Webhook Mode (`connectionMode: "webhook"`)
|
|
138
|
+
|
|
139
|
+
| Config Path | Description | Options | Default |
|
|
140
|
+
|---|---|---|---|
|
|
141
|
+
| `channels.wecom.token` | Webhook verification token | — | — |
|
|
142
|
+
| `channels.wecom.encodingAESKey` | AES encryption key (43 chars Base64) | — | — |
|
|
143
|
+
| `channels.wecom.receiveId` | Receiver ID (for decryption verification) | — | — |
|
|
144
|
+
| `channels.wecom.welcomeText` | Welcome message on enter_chat event | — | — |
|
|
145
|
+
| `channels.wecom.streamPlaceholderContent` | Stream placeholder content | — | — |
|
|
146
|
+
|
|
147
|
+
#### Access Control
|
|
148
|
+
|
|
149
|
+
| Config Path | Description | Options | Default |
|
|
150
|
+
|---|---|---|---|
|
|
111
151
|
| `channels.wecom.dmPolicy` | DM access policy | `pairing` / `open` / `allowlist` / `disabled` | `open` |
|
|
112
152
|
| `channels.wecom.allowFrom` | DM allowlist (user IDs) | — | `[]` |
|
|
113
153
|
| `channels.wecom.groupPolicy` | Group chat access policy | `open` / `allowlist` / `disabled` | `open` |
|
|
114
154
|
| `channels.wecom.groupAllowFrom` | Group allowlist (group IDs) | — | `[]` |
|
|
115
|
-
| `channels.wecom.
|
|
155
|
+
| `channels.wecom.groups` | Per-group config (e.g. sender allowlist) | — | `{}` |
|
|
156
|
+
|
|
157
|
+
#### Media Settings
|
|
158
|
+
|
|
159
|
+
| Config Path | Description | Default |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `channels.wecom.mediaLocalRoots` | Extra local paths allowed for media sending (supports `~`) | `[]` |
|
|
162
|
+
| `channels.wecom.media.maxBytes` | Max media file size in bytes | `20971520` (20MB) |
|
|
163
|
+
| `channels.wecom.media.tempDir` | Temp directory for media processing | — |
|
|
164
|
+
| `channels.wecom.media.retentionHours` | Media file retention hours | — |
|
|
165
|
+
| `channels.wecom.media.cleanupOnStart` | Clean temp media on startup | — |
|
|
166
|
+
|
|
167
|
+
**Media Size Limits & Auto-Downgrade:**
|
|
168
|
+
|
|
169
|
+
| Media Type | Max Size | Downgrade Behavior |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| Image | 10 MB | Exceeds → sent as file |
|
|
172
|
+
| Video | 10 MB | Exceeds → sent as file |
|
|
173
|
+
| Voice | 2 MB (AMR only) | Non-AMR format or exceeds → sent as file |
|
|
174
|
+
| File | 20 MB | Exceeds → rejected (cannot send) |
|
|
175
|
+
|
|
176
|
+
#### Network Settings
|
|
177
|
+
|
|
178
|
+
| Config Path | Description | Default |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| `channels.wecom.network.timeoutMs` | HTTP request timeout (ms) | — |
|
|
181
|
+
| `channels.wecom.network.retries` | Number of retries | — |
|
|
182
|
+
| `channels.wecom.network.retryDelayMs` | Delay between retries (ms) | — |
|
|
183
|
+
| `channels.wecom.network.egressProxyUrl` | Egress proxy URL for trusted IP scenarios | — |
|
|
184
|
+
|
|
185
|
+
> **Egress Proxy Priority**: `channels.wecom.network.egressProxyUrl` > `OPENCLAW_WECOM_EGRESS_PROXY_URL` > `WECOM_EGRESS_PROXY_URL` > `HTTPS_PROXY` > `ALL_PROXY` > `HTTP_PROXY`
|
|
116
186
|
|
|
117
187
|
### Agent Mode Configuration
|
|
118
188
|
|
|
@@ -121,13 +191,16 @@ Agent mode uses HTTP webhook callbacks with XML encrypted messages. You need to
|
|
|
121
191
|
#### Prerequisites
|
|
122
192
|
|
|
123
193
|
1. Create a self-built app in [WeCom Admin Console](https://work.weixin.qq.com/wework_admin/frame#apps)
|
|
124
|
-
2.
|
|
125
|
-
|
|
126
|
-
- **Token
|
|
127
|
-
- **
|
|
128
|
-
3. Note down the **CorpID**, **CorpSecret**, **Token**, and **EncodingAESKey**
|
|
194
|
+
2. Note down the **CorpID**, **CorpSecret** (from app settings), and **AgentId**
|
|
195
|
+
3. In the app settings, go to "API Receive" (API接收):
|
|
196
|
+
- Note down the **Token** and **EncodingAESKey** (auto-generated or custom)
|
|
197
|
+
- **Do NOT click save yet** — WeCom will verify the callback URL immediately when you save
|
|
129
198
|
|
|
130
|
-
####
|
|
199
|
+
#### Setup Steps
|
|
200
|
+
|
|
201
|
+
> **Important**: You must configure the Gateway **before** saving the callback URL in WeCom admin console. WeCom sends a verification request (GET with `echostr`) immediately when you save, and the Gateway needs the `token` and `encodingAESKey` to decrypt and respond correctly.
|
|
202
|
+
|
|
203
|
+
**Step 1: Configure Gateway**
|
|
131
204
|
|
|
132
205
|
```shell
|
|
133
206
|
openclaw config set channels.wecom.agent.corpId <YOUR_CORP_ID>
|
|
@@ -139,6 +212,13 @@ openclaw config set channels.wecom.enabled true
|
|
|
139
212
|
openclaw gateway restart
|
|
140
213
|
```
|
|
141
214
|
|
|
215
|
+
**Step 2: Save callback URL in WeCom admin console**
|
|
216
|
+
|
|
217
|
+
Go back to the "API Receive" settings and enter the callback URL:
|
|
218
|
+
- **URL**: `https://<your-gateway-host>/plugins/wecom/agent/<accountId>` (e.g. `/plugins/wecom/agent/default`); single-account mode can also use `/plugins/wecom/agent`
|
|
219
|
+
|
|
220
|
+
Now click save — the verification should pass.
|
|
221
|
+
|
|
142
222
|
#### JSON Configuration
|
|
143
223
|
|
|
144
224
|
```json
|
|
@@ -167,14 +247,29 @@ openclaw gateway restart
|
|
|
167
247
|
| `channels.wecom.agent.agentId` | App agent ID | No (needed for proactive messaging) |
|
|
168
248
|
| `channels.wecom.agent.token` | Callback verification token | Yes |
|
|
169
249
|
| `channels.wecom.agent.encodingAESKey` | Callback encryption key (43 chars) | Yes |
|
|
250
|
+
| `channels.wecom.agent.welcomeText` | Welcome message | No |
|
|
251
|
+
| `channels.wecom.agent.dmPolicy` | DM access policy (overrides top-level) | No |
|
|
252
|
+
| `channels.wecom.agent.allowFrom` | DM allowlist (overrides top-level) | No |
|
|
170
253
|
|
|
171
254
|
#### Webhook Paths
|
|
172
255
|
|
|
256
|
+
**Agent Mode:**
|
|
257
|
+
|
|
258
|
+
| Path | Description |
|
|
259
|
+
|---|---|
|
|
260
|
+
| `/plugins/wecom/agent/<accountId>` | 推荐路径(例如 `/plugins/wecom/agent/default`) |
|
|
261
|
+
| `/plugins/wecom/agent/default` | 多账号模式下自动路由到默认账号(即使默认账号 ID 不是 `default`) |
|
|
262
|
+
| `/plugins/wecom/agent` | 兼容路径(单账号 / 多账号签名匹配) |
|
|
263
|
+
| `/wecom/agent` | Legacy 兼容路径 |
|
|
264
|
+
|
|
265
|
+
**Bot Webhook Mode** (`connectionMode: "webhook"`):
|
|
266
|
+
|
|
173
267
|
| Path | Description |
|
|
174
268
|
|---|---|
|
|
175
|
-
| `/plugins/wecom/
|
|
176
|
-
| `/plugins/wecom/
|
|
177
|
-
| `/wecom/
|
|
269
|
+
| `/plugins/wecom/bot` | Recommended path (single account) |
|
|
270
|
+
| `/plugins/wecom/bot/<accountId>` | Multi-account path |
|
|
271
|
+
| `/wecom/bot` | Legacy compatible path |
|
|
272
|
+
| `/wecom` | Legacy compatible path |
|
|
178
273
|
|
|
179
274
|
### Outbound Delivery (Bot WS → Agent HTTP Fallback)
|
|
180
275
|
|
|
@@ -186,6 +281,7 @@ The plugin uses a **Bot-first, Agent-fallback** strategy for outbound message de
|
|
|
186
281
|
This means:
|
|
187
282
|
- **Agent-only accounts** (no Bot configured) can still send proactive messages, cron deliveries, and broadcasts
|
|
188
283
|
- **Target formats** like `party:1`, `tag:Ops`, `user:zhangsan` are fully supported in both paths
|
|
284
|
+
- **Media fallback**: when Bot WS is unavailable, media files are downloaded, uploaded to WeCom via Agent API, then sent; if upload fails, falls back to text + URL
|
|
189
285
|
- No manual switching needed — the plugin handles fallback transparently
|
|
190
286
|
|
|
191
287
|
### Using Both Modes Together
|
|
@@ -213,19 +309,19 @@ Bot and Agent can run simultaneously on the same account. Bot handles WebSocket
|
|
|
213
309
|
|
|
214
310
|
### Multi-Account Configuration
|
|
215
311
|
|
|
216
|
-
Use `accounts` to configure multiple WeCom accounts, each with optional bot and/or agent sub-configs
|
|
312
|
+
Use `accounts` to configure multiple WeCom accounts, each with optional bot and/or agent sub-configs. Account-level fields override top-level fields of the same name.
|
|
217
313
|
|
|
218
314
|
```json
|
|
219
315
|
{
|
|
220
316
|
"channels": {
|
|
221
317
|
"wecom": {
|
|
222
318
|
"enabled": true,
|
|
319
|
+
"defaultAccount": "main",
|
|
320
|
+
"dmPolicy": "open",
|
|
223
321
|
"accounts": {
|
|
224
322
|
"main": {
|
|
225
|
-
"
|
|
226
|
-
|
|
227
|
-
"secret": "secret-1"
|
|
228
|
-
},
|
|
323
|
+
"botId": "bot-id-1",
|
|
324
|
+
"secret": "secret-1",
|
|
229
325
|
"agent": {
|
|
230
326
|
"corpId": "ww1234567890abcdef",
|
|
231
327
|
"corpSecret": "secret-a",
|
|
@@ -235,6 +331,8 @@ Use `accounts` to configure multiple WeCom accounts, each with optional bot and/
|
|
|
235
331
|
}
|
|
236
332
|
},
|
|
237
333
|
"support": {
|
|
334
|
+
"dmPolicy": "allowlist",
|
|
335
|
+
"allowFrom": ["admin1"],
|
|
238
336
|
"agent": {
|
|
239
337
|
"corpId": "ww1234567890abcdef",
|
|
240
338
|
"corpSecret": "secret-b",
|
|
@@ -249,6 +347,41 @@ Use `accounts` to configure multiple WeCom accounts, each with optional bot and/
|
|
|
249
347
|
}
|
|
250
348
|
```
|
|
251
349
|
|
|
350
|
+
> **Note**: In multi-account mode, accounts without explicit `bindings` will not fall back to the default agent. Configure bindings for each account:
|
|
351
|
+
> ```json
|
|
352
|
+
> {
|
|
353
|
+
> "bindings": [
|
|
354
|
+
> { "agentId": "your-agent", "match": { "channel": "wecom", "accountId": "main" } }
|
|
355
|
+
> ]
|
|
356
|
+
> }
|
|
357
|
+
> ```
|
|
358
|
+
|
|
359
|
+
### Dynamic Agent Configuration
|
|
360
|
+
|
|
361
|
+
Dynamic Agent routing automatically creates isolated agents per user or group, enabling session isolation.
|
|
362
|
+
|
|
363
|
+
```json
|
|
364
|
+
{
|
|
365
|
+
"channels": {
|
|
366
|
+
"wecom": {
|
|
367
|
+
"dynamicAgents": {
|
|
368
|
+
"enabled": true,
|
|
369
|
+
"dmCreateAgent": true,
|
|
370
|
+
"groupEnabled": true,
|
|
371
|
+
"adminUsers": ["admin_user_id"]
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
| Config Path | Description | Default |
|
|
379
|
+
|---|---|---|
|
|
380
|
+
| `channels.wecom.dynamicAgents.enabled` | Enable dynamic agent routing | `false` |
|
|
381
|
+
| `channels.wecom.dynamicAgents.dmCreateAgent` | Create isolated agent per DM user | `true` |
|
|
382
|
+
| `channels.wecom.dynamicAgents.groupEnabled` | Enable dynamic agent for group chats | `true` |
|
|
383
|
+
| `channels.wecom.dynamicAgents.adminUsers` | Admin users (bypass dynamic routing, use main agent) | `[]` |
|
|
384
|
+
|
|
252
385
|
---
|
|
253
386
|
|
|
254
387
|
## 🔒 Access Control
|
|
@@ -353,17 +486,19 @@ The plugin supports scheduled message delivery via OpenClaw's built-in Cron serv
|
|
|
353
486
|
The `delivery.to` field supports the following target formats:
|
|
354
487
|
|
|
355
488
|
| Format | Target | Example |
|
|
356
|
-
|
|
489
|
+
|--------|--------|--------|
|
|
357
490
|
| `party:<id>` | Department (all members) | `party:1` (root dept = all employees) |
|
|
358
|
-
| `dept:<id>` | Department (alias) | `dept:5` |
|
|
491
|
+
| `dept:<id>` | Department (alias for party) | `dept:5` |
|
|
359
492
|
| `tag:<id>` | Tag group | `tag:Ops` |
|
|
360
493
|
| `user:<id>` | Specific user | `user:zhangsan` |
|
|
361
494
|
| `group:<id>` | External group chat | `group:wr123abc` |
|
|
362
|
-
| `chat:<id>` | Group chat (alias) | `chat:wc456def` |
|
|
495
|
+
| `chat:<id>` | Group chat (alias for group) | `chat:wc456def` |
|
|
363
496
|
| Pure number | Auto-detected as department | `1` → `party:1` |
|
|
364
497
|
| `wr...` / `wc...` | Auto-detected as group chat | `wr123` → `chatid` |
|
|
365
498
|
| Other string | Auto-detected as user | `zhangsan` → `touser` |
|
|
366
499
|
|
|
500
|
+
> **Namespace prefixes** (`wecom:`, `qywx:`, `wework:`, `wechatwork:`, `wecom-agent:`) are automatically stripped before parsing.
|
|
501
|
+
|
|
367
502
|
### Method 1: CLI (Recommended — takes effect immediately)
|
|
368
503
|
|
|
369
504
|
```shell
|
|
@@ -371,20 +506,26 @@ openclaw cron add \
|
|
|
371
506
|
--name "daily-report" \
|
|
372
507
|
--agent main \
|
|
373
508
|
--cron "0 9 * * 1-5" \
|
|
509
|
+
--tz "Asia/Shanghai" \
|
|
374
510
|
--message "Good morning! Here is your daily briefing." \
|
|
375
|
-
--
|
|
376
|
-
--
|
|
377
|
-
--
|
|
511
|
+
--announce \
|
|
512
|
+
--channel wecom \
|
|
513
|
+
--to "party:1"
|
|
378
514
|
```
|
|
379
515
|
|
|
516
|
+
> **Note**: `--announce` enables delivery mode (broadcasts the AI response to the target chat). Use `--no-deliver` to keep output internal. The deprecated `--deliver` flag is an alias for `--announce`.
|
|
517
|
+
|
|
380
518
|
Common CLI commands:
|
|
381
519
|
|
|
382
520
|
```shell
|
|
383
521
|
openclaw cron list # List all cron jobs
|
|
522
|
+
openclaw cron show <id> # Show job details
|
|
384
523
|
openclaw cron enable <id> # Enable a job
|
|
385
524
|
openclaw cron disable <id> # Disable a job
|
|
386
525
|
openclaw cron remove <id> # Remove a job
|
|
387
526
|
openclaw cron run <id> # Manually trigger a job
|
|
527
|
+
openclaw cron runs --id <id> # View run history
|
|
528
|
+
openclaw cron edit <id> --message "New prompt" # Edit a job
|
|
388
529
|
```
|
|
389
530
|
|
|
390
531
|
### Method 2: Edit `jobs.json` (requires gateway restart)
|
|
@@ -405,10 +546,7 @@ File path: `~/.openclaw/cron/jobs.json`
|
|
|
405
546
|
"wakeMode": "now",
|
|
406
547
|
"payload": {
|
|
407
548
|
"kind": "agentTurn",
|
|
408
|
-
"message": "Generate today's briefing and send it."
|
|
409
|
-
"deliver": true,
|
|
410
|
-
"channel": "wecom",
|
|
411
|
-
"to": "party:1"
|
|
549
|
+
"message": "Generate today's briefing and send it."
|
|
412
550
|
},
|
|
413
551
|
"delivery": {
|
|
414
552
|
"mode": "announce",
|
package/dist/index.js
CHANGED
|
@@ -3,59 +3,8 @@ import { wecomPlugin } from "./src/channel.js";
|
|
|
3
3
|
import { createWeComMcpTool } from "./src/mcp/index.js";
|
|
4
4
|
import { setWeComRuntime } from "./src/runtime.js";
|
|
5
5
|
import { CHANNEL_ID, WEBHOOK_PATHS } from "./src/const.js";
|
|
6
|
-
import {
|
|
6
|
+
import { createWecomAgentWebhookHandler } from "./src/agent/webhook.js";
|
|
7
7
|
import { handleWecomWebhookRequest } from "./src/webhook/index.js";
|
|
8
|
-
// ============================================================================
|
|
9
|
-
// 需要自动注入 tools.alsoAllow 的工具名列表
|
|
10
|
-
// ============================================================================
|
|
11
|
-
const REQUIRED_ALSO_ALLOW_TOOLS = ["wecom_mcp"];
|
|
12
|
-
/**
|
|
13
|
-
* 确保 tools.alsoAllow 中包含插件所需的工具名。
|
|
14
|
-
*
|
|
15
|
-
* 逻辑:
|
|
16
|
-
* 1. 读取当前配置
|
|
17
|
-
* 2. 检查 tools.alsoAllow 中是否已包含所需工具
|
|
18
|
-
* 3. 如有缺失,合并写入配置文件
|
|
19
|
-
*
|
|
20
|
-
* 幂等操作——重复调用不会产生副作用。
|
|
21
|
-
* 若 tools.allow 已显式设置(与 alsoAllow 互斥),则跳过注入并打印提示。
|
|
22
|
-
*/
|
|
23
|
-
async function ensureToolsAlsoAllow(api) {
|
|
24
|
-
try {
|
|
25
|
-
const cfg = api.runtime.config.loadConfig();
|
|
26
|
-
const tools = cfg.tools ?? {};
|
|
27
|
-
// 如果用户显式配置了 tools.allow(全量白名单),则 alsoAllow 与之互斥,
|
|
28
|
-
// 此时不应自动追加 alsoAllow,避免产生校验冲突
|
|
29
|
-
if (tools.allow && tools.allow.length > 0) {
|
|
30
|
-
const missing = REQUIRED_ALSO_ALLOW_TOOLS.filter((t) => !tools.allow.includes(t));
|
|
31
|
-
if (missing.length > 0) {
|
|
32
|
-
console.warn(`[wecom] tools.allow 已显式设置,无法自动注入 alsoAllow。` +
|
|
33
|
-
`请手动将 ${JSON.stringify(missing)} 加入 tools.allow。`);
|
|
34
|
-
}
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const existing = tools.alsoAllow ?? [];
|
|
38
|
-
const missing = REQUIRED_ALSO_ALLOW_TOOLS.filter((t) => !existing.includes(t));
|
|
39
|
-
if (missing.length === 0) {
|
|
40
|
-
// 所有工具已在白名单中,无需操作
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
const merged = [...existing, ...missing];
|
|
44
|
-
const nextConfig = {
|
|
45
|
-
...cfg,
|
|
46
|
-
tools: {
|
|
47
|
-
...tools,
|
|
48
|
-
alsoAllow: merged,
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
await api.runtime.config.writeConfigFile(nextConfig);
|
|
52
|
-
console.log(`[wecom] 已自动将 ${JSON.stringify(missing)} 加入 tools.alsoAllow`);
|
|
53
|
-
}
|
|
54
|
-
catch (err) {
|
|
55
|
-
// 配置写入失败不应阻断 Gateway 启动
|
|
56
|
-
console.error(`[wecom] 自动注入 tools.alsoAllow 失败:`, err instanceof Error ? err.message : String(err));
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
8
|
const plugin = {
|
|
60
9
|
id: "wecom-openclaw-plugin",
|
|
61
10
|
name: "企业微信",
|
|
@@ -66,16 +15,17 @@ const plugin = {
|
|
|
66
15
|
api.registerChannel({ plugin: wecomPlugin });
|
|
67
16
|
// 注册 wecom_mcp:通过 HTTP 直接调用企业微信 MCP Server
|
|
68
17
|
api.registerTool(createWeComMcpTool(), { name: "wecom_mcp" });
|
|
18
|
+
const agentWebhookHandler = createWecomAgentWebhookHandler(api.runtime);
|
|
69
19
|
// 注册 Agent 模式 HTTP 路由(prefix match 覆盖带 accountId 的子路径)
|
|
70
20
|
api.registerHttpRoute({
|
|
71
21
|
path: WEBHOOK_PATHS.AGENT_PLUGIN,
|
|
72
|
-
handler:
|
|
22
|
+
handler: agentWebhookHandler,
|
|
73
23
|
auth: "plugin",
|
|
74
24
|
match: "prefix",
|
|
75
25
|
});
|
|
76
26
|
api.registerHttpRoute({
|
|
77
27
|
path: WEBHOOK_PATHS.AGENT,
|
|
78
|
-
handler:
|
|
28
|
+
handler: agentWebhookHandler,
|
|
79
29
|
auth: "plugin",
|
|
80
30
|
match: "prefix",
|
|
81
31
|
});
|
|
@@ -90,11 +40,6 @@ const plugin = {
|
|
|
90
40
|
match: "prefix",
|
|
91
41
|
});
|
|
92
42
|
}
|
|
93
|
-
// ── Gateway 启动时自动确保 tools.alsoAllow 包含 wecom_mcp ──────────
|
|
94
|
-
// 在 gateway_start 阶段检测并写入,保证插件安装/更新后首次启动即生效
|
|
95
|
-
// api.on("gateway_start", async () => {
|
|
96
|
-
// await ensureToolsAlsoAllow(api);
|
|
97
|
-
// });
|
|
98
43
|
// 注入媒体发送指令和文件大小限制提示词(仅对企业微信 channel 生效)
|
|
99
44
|
api.on("before_prompt_build", (_event, ctx) => {
|
|
100
45
|
// 只在企业微信 channel 的会话中注入,避免影响其他 channel 插件
|
|
@@ -9,7 +9,7 @@ import { extractMsgType, extractFromUser, extractContent, extractChatId, extract
|
|
|
9
9
|
import { sendText, downloadMedia, uploadMedia, sendMedia as sendAgentMedia } from "./api-client.js";
|
|
10
10
|
import { buildWecomUnauthorizedCommandPrompt, resolveWecomCommandAuthorization } from "../shared/command-auth.js";
|
|
11
11
|
import { processDynamicRouting } from "../dynamic-routing.js";
|
|
12
|
-
import { DEFAULT_MEDIA_MAX_MB } from "../const.js";
|
|
12
|
+
import { CHANNEL_ID, DEFAULT_MEDIA_MAX_MB } from "../const.js";
|
|
13
13
|
function resolveWecomMediaMaxBytes(config) {
|
|
14
14
|
return config.channels?.wecom?.media?.maxBytes ?? DEFAULT_MEDIA_MAX_MB * 1024 * 1024;
|
|
15
15
|
}
|
|
@@ -345,18 +345,6 @@ async function processAgentMessage(params) {
|
|
|
345
345
|
log: (msg) => log?.(msg.replace(/^\[dynamic-routing\]/, "[wecom-agent]")),
|
|
346
346
|
error: (msg) => error?.(msg.replace(/^\[dynamic-routing\]/, "[wecom-agent]")),
|
|
347
347
|
});
|
|
348
|
-
// 处理路由拦截
|
|
349
|
-
if (routingResult.rejected) {
|
|
350
|
-
error?.(`[wecom-agent] 🚫 routing guard: blocked default fallback accountId=${agent.accountId} matchedBy=${route.matchedBy} from=${fromUser}`);
|
|
351
|
-
try {
|
|
352
|
-
await sendText({ agent, toUser: fromUser, chatId: undefined, text: routingResult.rejectReason });
|
|
353
|
-
log?.(`[wecom-agent] routing guard prompt delivered to ${fromUser}`);
|
|
354
|
-
}
|
|
355
|
-
catch (err) {
|
|
356
|
-
error?.(`[wecom-agent] routing guard prompt failed: ${String(err)}`);
|
|
357
|
-
}
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
348
|
// 应用动态路由结果
|
|
361
349
|
if (routingResult.routeModified) {
|
|
362
350
|
route.agentId = routingResult.finalAgentId;
|
|
@@ -416,9 +404,9 @@ async function processAgentMessage(params) {
|
|
|
416
404
|
ConversationLabel: fromLabel,
|
|
417
405
|
SenderName: fromUser,
|
|
418
406
|
SenderId: fromUser,
|
|
419
|
-
Provider:
|
|
407
|
+
Provider: CHANNEL_ID,
|
|
420
408
|
Surface: "webchat",
|
|
421
|
-
OriginatingChannel:
|
|
409
|
+
OriginatingChannel: CHANNEL_ID,
|
|
422
410
|
// 标记为 Agent 会话的回复路由目标,避免与 Bot 会话混淆:
|
|
423
411
|
// - 用于让 /new /reset 这类命令回执不被 Bot 侧策略拦截
|
|
424
412
|
// - 群聊场景也统一路由为私信触发者(与 deliver 策略一致)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 3. POST → XML body 解密 → 调用 handleAgentWebhook
|
|
8
8
|
*/
|
|
9
9
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
10
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
10
|
+
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
11
11
|
import type { ResolvedAgentAccount } from "../types/index.js";
|
|
12
12
|
export type AgentWebhookTarget = {
|
|
13
13
|
agent: ResolvedAgentAccount;
|
|
@@ -20,4 +20,5 @@ export type AgentWebhookTarget = {
|
|
|
20
20
|
};
|
|
21
21
|
export declare function registerAgentWebhookTarget(target: AgentWebhookTarget): void;
|
|
22
22
|
export declare function deregisterAgentWebhookTarget(accountId: string): void;
|
|
23
|
-
export declare function
|
|
23
|
+
export declare function createWecomAgentWebhookHandler(runtime: PluginRuntime): (req: IncomingMessage, res: ServerResponse) => Promise<boolean | void>;
|
|
24
|
+
export declare function handleWecomAgentWebhookRequest(req: IncomingMessage, res: ServerResponse, runtime: PluginRuntime): Promise<boolean | void>;
|
|
@@ -11,7 +11,6 @@ import { WecomCrypto } from "@wecom/aibot-node-sdk";
|
|
|
11
11
|
import { extractEncryptFromXml } from "./xml.js";
|
|
12
12
|
import { parseXml, extractAgentId } from "../shared/xml-parser.js";
|
|
13
13
|
import { handleAgentWebhook } from "./index.js";
|
|
14
|
-
import { getWeComRuntime } from "../runtime.js";
|
|
15
14
|
import { WEBHOOK_PATHS, LIMITS } from "../const.js";
|
|
16
15
|
// ============================================================================
|
|
17
16
|
// Target Registry
|
|
@@ -92,7 +91,10 @@ function normalizeAgentIdValue(value) {
|
|
|
92
91
|
// ============================================================================
|
|
93
92
|
// Main HTTP handler
|
|
94
93
|
// ============================================================================
|
|
95
|
-
export
|
|
94
|
+
export function createWecomAgentWebhookHandler(runtime) {
|
|
95
|
+
return (req, res) => handleWecomAgentWebhookRequest(req, res, runtime);
|
|
96
|
+
}
|
|
97
|
+
export async function handleWecomAgentWebhookRequest(req, res, runtime) {
|
|
96
98
|
const path = resolvePath(req);
|
|
97
99
|
const reqId = crypto.randomUUID().slice(0, 8);
|
|
98
100
|
// 检查是否为 Agent 路径
|
|
@@ -204,7 +206,7 @@ export async function handleWecomAgentWebhookRequest(req, res) {
|
|
|
204
206
|
inboundAgentId !== selected.agent.agentId) {
|
|
205
207
|
selected.runtime.error?.(`[wecom] inbound(agent): reqId=${reqId} accountId=${selected.agent.accountId} agentId_mismatch expected=${selected.agent.agentId} actual=${inboundAgentId}`);
|
|
206
208
|
}
|
|
207
|
-
const core =
|
|
209
|
+
const core = runtime;
|
|
208
210
|
selected.runtime.log?.(`[wecom] inbound(agent): reqId=${reqId} method=POST remote=${req.socket?.remoteAddress ?? "unknown"} accountId=${selected.agent.accountId}`);
|
|
209
211
|
return handleAgentWebhook({
|
|
210
212
|
req,
|
package/dist/src/channel.js
CHANGED
|
@@ -416,14 +416,29 @@ export const wecomPlugin = {
|
|
|
416
416
|
const agent = account.agent;
|
|
417
417
|
if (agent?.configured) {
|
|
418
418
|
const isMulti = hasMultiAccounts(ctx.cfg);
|
|
419
|
+
const defaultId = resolveDefaultWeComAccountId(ctx.cfg);
|
|
420
|
+
const isDefault = ctx.accountId === defaultId;
|
|
419
421
|
const paths = isMulti
|
|
420
422
|
? [
|
|
421
423
|
`${WEBHOOK_PATHS.AGENT_PLUGIN}/${ctx.accountId}`,
|
|
422
424
|
`${WEBHOOK_PATHS.AGENT}/${ctx.accountId}`,
|
|
425
|
+
// 默认账号额外注册 /default 别名路径
|
|
426
|
+
...(isDefault && ctx.accountId !== DEFAULT_ACCOUNT_ID
|
|
427
|
+
? [
|
|
428
|
+
`${WEBHOOK_PATHS.AGENT_PLUGIN}/${DEFAULT_ACCOUNT_ID}`,
|
|
429
|
+
`${WEBHOOK_PATHS.AGENT}/${DEFAULT_ACCOUNT_ID}`,
|
|
430
|
+
]
|
|
431
|
+
: []),
|
|
423
432
|
WEBHOOK_PATHS.AGENT_PLUGIN,
|
|
424
433
|
WEBHOOK_PATHS.AGENT,
|
|
425
434
|
]
|
|
426
|
-
: [
|
|
435
|
+
: [
|
|
436
|
+
// 单账号模式:同时注册 /default 路径以支持显式指定
|
|
437
|
+
WEBHOOK_PATHS.AGENT_PLUGIN,
|
|
438
|
+
WEBHOOK_PATHS.AGENT,
|
|
439
|
+
`${WEBHOOK_PATHS.AGENT_PLUGIN}/${DEFAULT_ACCOUNT_ID}`,
|
|
440
|
+
`${WEBHOOK_PATHS.AGENT}/${DEFAULT_ACCOUNT_ID}`,
|
|
441
|
+
];
|
|
427
442
|
for (const p of paths) {
|
|
428
443
|
registerAgentWebhookTarget({
|
|
429
444
|
agent,
|
|
@@ -43,10 +43,6 @@ export interface DynamicRoutingParams {
|
|
|
43
43
|
export interface DynamicRoutingResult {
|
|
44
44
|
/** 是否使用动态 Agent */
|
|
45
45
|
useDynamicAgent: boolean;
|
|
46
|
-
/** 是否被默认路由拦截 */
|
|
47
|
-
rejected: boolean;
|
|
48
|
-
/** 拒绝原因(如果 rejected=true) */
|
|
49
|
-
rejectReason?: string;
|
|
50
46
|
/** 最终的 agentId(可能被动态注入修改) */
|
|
51
47
|
finalAgentId: string;
|
|
52
48
|
/** 最终的 sessionKey(可能被动态注入修改) */
|
|
@@ -60,14 +56,8 @@ export interface DynamicRoutingResult {
|
|
|
60
56
|
* 功能:
|
|
61
57
|
* 1. 判断是否需要使用动态 Agent
|
|
62
58
|
* 2. 根据 matchedBy 判断配置类型
|
|
63
|
-
* 3.
|
|
64
|
-
* 4.
|
|
65
|
-
* 5. 输出详细的调试日志
|
|
66
|
-
*
|
|
67
|
-
* **重要规则**:
|
|
68
|
-
* - matchedBy = "binding.peer" 或 "binding.peer.parent" → 跳过动态路由
|
|
69
|
-
* - matchedBy = "binding.account" 或其他 → 继续使用动态路由
|
|
70
|
-
* - matchedBy = "default" → 检查 fail-closed 策略
|
|
59
|
+
* 3. 返回最终的路由信息(不修改传入的 route 对象)
|
|
60
|
+
* 4. 输出详细的调试日志
|
|
71
61
|
*
|
|
72
62
|
* @param params 动态路由处理参数
|
|
73
63
|
* @returns 处理结果
|