@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 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 groups
34
- - 🖼️ Receives and processes image and file messages with automatic downloading
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
- - ⚡ Auto heartbeat keep-alive and reconnection (up to 100 reconnect attempts)
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.2.13`
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 long-polling | JSON | Quick setup, streaming replies |
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.sendThinkingMessage` | Send "thinking" placeholder | `true` / `false` | `true` |
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. In the app settings, go to "API Receive" (API接收) and configure:
125
- - **URL**: `https://<your-gateway-host>/plugins/wecom/agent` (or `/plugins/wecom/agent/<accountId>` for multi-account)
126
- - **Token**: auto-generated or custom
127
- - **EncodingAESKey**: auto-generated or custom
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
- #### Quick Setup
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/agent` | Recommended path (single account) |
176
- | `/plugins/wecom/agent/<accountId>` | Multi-account path |
177
- | `/wecom/agent` | Legacy compatible path |
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
- "bot": {
226
- "botId": "bot-id-1",
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
- --deliver \
376
- --delivery-channel wecom \
377
- --delivery-to "party:1"
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 { handleWecomAgentWebhookRequest } from "./src/agent/webhook.js";
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: handleWecomAgentWebhookRequest,
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: handleWecomAgentWebhookRequest,
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: "wecom",
407
+ Provider: CHANNEL_ID,
420
408
  Surface: "webchat",
421
- OriginatingChannel: "wecom",
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 handleWecomAgentWebhookRequest(req: IncomingMessage, res: ServerResponse): Promise<boolean | void>;
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 async function handleWecomAgentWebhookRequest(req, res) {
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 = getWeComRuntime();
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,
@@ -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
- : [WEBHOOK_PATHS.AGENT_PLUGIN, WEBHOOK_PATHS.AGENT];
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. 检查是否被默认路由拦截(fail-closed 策略)
64
- * 4. 返回最终的路由信息(不修改传入的 route 对象)
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 处理结果