@wecom/wecom-openclaw-plugin 2026.3.31-beta.2 → 2026.3.31-beta.4
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 +130 -18
- package/dist/index.js +4 -3
- package/dist/src/agent/handler.js +0 -12
- package/dist/src/agent/webhook.d.ts +3 -2
- package/dist/src/agent/webhook.js +5 -3
- package/dist/src/channel.js +9 -0
- package/dist/src/dynamic-routing.d.ts +2 -12
- package/dist/src/dynamic-routing.js +11 -77
- package/dist/src/monitor.js +6 -29
- package/dist/src/openclaw-compat.d.ts +2 -0
- package/dist/src/openclaw-compat.js +2 -2
- package/dist/src/types/config.d.ts +0 -9
- package/dist/src/utils.d.ts +3 -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,20 @@
|
|
|
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 and file messages with automatic downloading
|
|
33
|
+
- 📤 Proactive messaging to specific users, groups, departments, or tags
|
|
34
|
+
- 🖼️ Receives and processes image, voice, video, and file messages with automatic downloading
|
|
35
35
|
- ⏳ Streaming replies with "thinking" placeholder messages (Bot mode)
|
|
36
36
|
- 🔐 Agent mode: AES-256-CBC encrypted XML callbacks with SHA1 signature verification
|
|
37
37
|
- 📝 Markdown formatting support for replies
|
|
38
|
+
- 🃏 Template card messages (text_notice, news_notice, button_interaction, vote_interaction, multiple_interaction)
|
|
38
39
|
- 🔒 Built-in access control: DM Policy (pairing / open / allowlist / disabled) and Group Policy (open / allowlist / disabled)
|
|
39
40
|
- 👥 Multi-account support: run multiple WeCom accounts with independent bot/agent configs
|
|
40
|
-
-
|
|
41
|
+
- 🧩 MCP tool integration (`wecom_mcp`) for calling WeCom MCP Server
|
|
42
|
+
- 🔀 Dynamic Agent routing: auto-create isolated agents per user/group
|
|
43
|
+
- 📁 Local file sending with configurable media path allowlist (`mediaLocalRoots`)
|
|
44
|
+
- ⚡ Auto heartbeat keep-alive and reconnection (up to 10 reconnect attempts)
|
|
41
45
|
- 🧙 Interactive CLI setup wizard
|
|
42
46
|
|
|
43
47
|
---
|
|
@@ -46,7 +50,7 @@
|
|
|
46
50
|
|
|
47
51
|
### Requirements
|
|
48
52
|
|
|
49
|
-
- OpenClaw `>= 2026.
|
|
53
|
+
- OpenClaw `>= 2026.3.28`
|
|
50
54
|
|
|
51
55
|
### Quick Install
|
|
52
56
|
|
|
@@ -97,22 +101,72 @@ The plugin supports two connection modes that can be used independently or toget
|
|
|
97
101
|
|
|
98
102
|
| Mode | Connection | Message Format | Use Case |
|
|
99
103
|
|------|-----------|---------------|----------|
|
|
100
|
-
| **Bot** (智能体) | WebSocket
|
|
104
|
+
| **Bot** (智能体) | WebSocket (default) or HTTP webhook | JSON | Quick setup, streaming replies |
|
|
101
105
|
| **Agent** (自建应用) | HTTP webhook callbacks | XML | Enterprise apps, API-driven messaging |
|
|
102
106
|
|
|
107
|
+
> **Note**: Bot mode supports two connection methods via `connectionMode`:
|
|
108
|
+
> - `websocket` (default) — WebSocket long-polling, requires `botId` + `secret`
|
|
109
|
+
> - `webhook` — HTTP callback, requires `token` + `encodingAESKey`
|
|
110
|
+
|
|
103
111
|
### Bot Mode Configuration
|
|
104
112
|
|
|
113
|
+
#### Core Settings
|
|
114
|
+
|
|
115
|
+
| Config Path | Description | Options | Default |
|
|
116
|
+
|---|---|---|---|
|
|
117
|
+
| `channels.wecom.enabled` | Enable the channel | `true` / `false` | `false` |
|
|
118
|
+
| `channels.wecom.connectionMode` | Bot connection mode | `websocket` / `webhook` | `websocket` |
|
|
119
|
+
| `channels.wecom.name` | Account display name | — | `企业微信` |
|
|
120
|
+
|
|
121
|
+
#### WebSocket Mode (default)
|
|
122
|
+
|
|
105
123
|
| Config Path | Description | Options | Default |
|
|
106
124
|
|---|---|---|---|
|
|
107
125
|
| `channels.wecom.botId` | WeCom bot ID | — | — |
|
|
108
126
|
| `channels.wecom.secret` | WeCom bot secret | — | — |
|
|
109
|
-
| `channels.wecom.enabled` | Enable the channel | `true` / `false` | `false` |
|
|
110
127
|
| `channels.wecom.websocketUrl` | WebSocket endpoint | — | `wss://openws.work.weixin.qq.com` |
|
|
128
|
+
| `channels.wecom.sendThinkingMessage` | Send "thinking" placeholder | `true` / `false` | `true` |
|
|
129
|
+
|
|
130
|
+
#### Webhook Mode (`connectionMode: "webhook"`)
|
|
131
|
+
|
|
132
|
+
| Config Path | Description | Options | Default |
|
|
133
|
+
|---|---|---|---|
|
|
134
|
+
| `channels.wecom.token` | Webhook verification token | — | — |
|
|
135
|
+
| `channels.wecom.encodingAESKey` | AES encryption key (43 chars Base64) | — | — |
|
|
136
|
+
| `channels.wecom.receiveId` | Receiver ID (for decryption verification) | — | — |
|
|
137
|
+
| `channels.wecom.welcomeText` | Welcome message on enter_chat event | — | — |
|
|
138
|
+
| `channels.wecom.streamPlaceholderContent` | Stream placeholder content | — | — |
|
|
139
|
+
|
|
140
|
+
#### Access Control
|
|
141
|
+
|
|
142
|
+
| Config Path | Description | Options | Default |
|
|
143
|
+
|---|---|---|---|
|
|
111
144
|
| `channels.wecom.dmPolicy` | DM access policy | `pairing` / `open` / `allowlist` / `disabled` | `open` |
|
|
112
145
|
| `channels.wecom.allowFrom` | DM allowlist (user IDs) | — | `[]` |
|
|
113
146
|
| `channels.wecom.groupPolicy` | Group chat access policy | `open` / `allowlist` / `disabled` | `open` |
|
|
114
147
|
| `channels.wecom.groupAllowFrom` | Group allowlist (group IDs) | — | `[]` |
|
|
115
|
-
| `channels.wecom.
|
|
148
|
+
| `channels.wecom.groups` | Per-group config (e.g. sender allowlist) | — | `{}` |
|
|
149
|
+
|
|
150
|
+
#### Media Settings
|
|
151
|
+
|
|
152
|
+
| Config Path | Description | Default |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| `channels.wecom.mediaLocalRoots` | Extra local paths allowed for media sending (supports `~`) | `[]` |
|
|
155
|
+
| `channels.wecom.media.maxBytes` | Max media file size in bytes | `5242880` (5MB) |
|
|
156
|
+
| `channels.wecom.media.tempDir` | Temp directory for media processing | — |
|
|
157
|
+
| `channels.wecom.media.retentionHours` | Media file retention hours | — |
|
|
158
|
+
| `channels.wecom.media.cleanupOnStart` | Clean temp media on startup | — |
|
|
159
|
+
|
|
160
|
+
#### Network Settings
|
|
161
|
+
|
|
162
|
+
| Config Path | Description | Default |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| `channels.wecom.network.timeoutMs` | HTTP request timeout (ms) | — |
|
|
165
|
+
| `channels.wecom.network.retries` | Number of retries | — |
|
|
166
|
+
| `channels.wecom.network.retryDelayMs` | Delay between retries (ms) | — |
|
|
167
|
+
| `channels.wecom.network.egressProxyUrl` | Egress proxy URL for trusted IP scenarios | — |
|
|
168
|
+
|
|
169
|
+
> **Egress Proxy Priority**: `channels.wecom.network.egressProxyUrl` > `OPENCLAW_WECOM_EGRESS_PROXY_URL` > `WECOM_EGRESS_PROXY_URL` > `HTTPS_PROXY` > `ALL_PROXY` > `HTTP_PROXY`
|
|
116
170
|
|
|
117
171
|
### Agent Mode Configuration
|
|
118
172
|
|
|
@@ -122,7 +176,7 @@ Agent mode uses HTTP webhook callbacks with XML encrypted messages. You need to
|
|
|
122
176
|
|
|
123
177
|
1. Create a self-built app in [WeCom Admin Console](https://work.weixin.qq.com/wework_admin/frame#apps)
|
|
124
178
|
2. In the app settings, go to "API Receive" (API接收) and configure:
|
|
125
|
-
- **URL**: `https://<your-gateway-host>/plugins/wecom/agent
|
|
179
|
+
- **URL**: `https://<your-gateway-host>/plugins/wecom/agent/<accountId>`(例如 `/plugins/wecom/agent/default`);单账号模式也可使用 `/plugins/wecom/agent`
|
|
126
180
|
- **Token**: auto-generated or custom
|
|
127
181
|
- **EncodingAESKey**: auto-generated or custom
|
|
128
182
|
3. Note down the **CorpID**, **CorpSecret**, **Token**, and **EncodingAESKey**
|
|
@@ -167,14 +221,29 @@ openclaw gateway restart
|
|
|
167
221
|
| `channels.wecom.agent.agentId` | App agent ID | No (needed for proactive messaging) |
|
|
168
222
|
| `channels.wecom.agent.token` | Callback verification token | Yes |
|
|
169
223
|
| `channels.wecom.agent.encodingAESKey` | Callback encryption key (43 chars) | Yes |
|
|
224
|
+
| `channels.wecom.agent.welcomeText` | Welcome message | No |
|
|
225
|
+
| `channels.wecom.agent.dmPolicy` | DM access policy (overrides top-level) | No |
|
|
226
|
+
| `channels.wecom.agent.allowFrom` | DM allowlist (overrides top-level) | No |
|
|
170
227
|
|
|
171
228
|
#### Webhook Paths
|
|
172
229
|
|
|
230
|
+
**Agent Mode:**
|
|
231
|
+
|
|
232
|
+
| Path | Description |
|
|
233
|
+
|---|---|
|
|
234
|
+
| `/plugins/wecom/agent/<accountId>` | 推荐路径(例如 `/plugins/wecom/agent/default`) |
|
|
235
|
+
| `/plugins/wecom/agent/default` | 多账号模式下自动路由到默认账号(即使默认账号 ID 不是 `default`) |
|
|
236
|
+
| `/plugins/wecom/agent` | 兼容路径(单账号 / 多账号签名匹配) |
|
|
237
|
+
| `/wecom/agent` | Legacy 兼容路径 |
|
|
238
|
+
|
|
239
|
+
**Bot Webhook Mode** (`connectionMode: "webhook"`):
|
|
240
|
+
|
|
173
241
|
| Path | Description |
|
|
174
242
|
|---|---|
|
|
175
|
-
| `/plugins/wecom/
|
|
176
|
-
| `/plugins/wecom/
|
|
177
|
-
| `/wecom/
|
|
243
|
+
| `/plugins/wecom/bot` | Recommended path (single account) |
|
|
244
|
+
| `/plugins/wecom/bot/<accountId>` | Multi-account path |
|
|
245
|
+
| `/wecom/bot` | Legacy compatible path |
|
|
246
|
+
| `/wecom` | Legacy compatible path |
|
|
178
247
|
|
|
179
248
|
### Outbound Delivery (Bot WS → Agent HTTP Fallback)
|
|
180
249
|
|
|
@@ -213,19 +282,19 @@ Bot and Agent can run simultaneously on the same account. Bot handles WebSocket
|
|
|
213
282
|
|
|
214
283
|
### Multi-Account Configuration
|
|
215
284
|
|
|
216
|
-
Use `accounts` to configure multiple WeCom accounts, each with optional bot and/or agent sub-configs
|
|
285
|
+
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
286
|
|
|
218
287
|
```json
|
|
219
288
|
{
|
|
220
289
|
"channels": {
|
|
221
290
|
"wecom": {
|
|
222
291
|
"enabled": true,
|
|
292
|
+
"defaultAccount": "main",
|
|
293
|
+
"dmPolicy": "open",
|
|
223
294
|
"accounts": {
|
|
224
295
|
"main": {
|
|
225
|
-
"
|
|
226
|
-
|
|
227
|
-
"secret": "secret-1"
|
|
228
|
-
},
|
|
296
|
+
"botId": "bot-id-1",
|
|
297
|
+
"secret": "secret-1",
|
|
229
298
|
"agent": {
|
|
230
299
|
"corpId": "ww1234567890abcdef",
|
|
231
300
|
"corpSecret": "secret-a",
|
|
@@ -235,6 +304,8 @@ Use `accounts` to configure multiple WeCom accounts, each with optional bot and/
|
|
|
235
304
|
}
|
|
236
305
|
},
|
|
237
306
|
"support": {
|
|
307
|
+
"dmPolicy": "allowlist",
|
|
308
|
+
"allowFrom": ["admin1"],
|
|
238
309
|
"agent": {
|
|
239
310
|
"corpId": "ww1234567890abcdef",
|
|
240
311
|
"corpSecret": "secret-b",
|
|
@@ -249,6 +320,47 @@ Use `accounts` to configure multiple WeCom accounts, each with optional bot and/
|
|
|
249
320
|
}
|
|
250
321
|
```
|
|
251
322
|
|
|
323
|
+
> **Note**: In multi-account mode, `routing.failClosedOnDefaultRoute` defaults to `true` — accounts without explicit `bindings` will not fall back to the default agent. Configure bindings for each account:
|
|
324
|
+
> ```json
|
|
325
|
+
> {
|
|
326
|
+
> "bindings": [
|
|
327
|
+
> { "agentId": "your-agent", "match": { "channel": "wecom", "accountId": "main" } }
|
|
328
|
+
> ]
|
|
329
|
+
> }
|
|
330
|
+
> ```
|
|
331
|
+
|
|
332
|
+
### Dynamic Agent Configuration
|
|
333
|
+
|
|
334
|
+
Dynamic Agent routing automatically creates isolated agents per user or group, enabling session isolation.
|
|
335
|
+
|
|
336
|
+
```json
|
|
337
|
+
{
|
|
338
|
+
"channels": {
|
|
339
|
+
"wecom": {
|
|
340
|
+
"dynamicAgents": {
|
|
341
|
+
"enabled": true,
|
|
342
|
+
"dmCreateAgent": true,
|
|
343
|
+
"groupEnabled": true,
|
|
344
|
+
"adminUsers": ["admin_user_id"]
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
| Config Path | Description | Default |
|
|
352
|
+
|---|---|---|
|
|
353
|
+
| `channels.wecom.dynamicAgents.enabled` | Enable dynamic agent routing | `false` |
|
|
354
|
+
| `channels.wecom.dynamicAgents.dmCreateAgent` | Create isolated agent per DM user | `true` |
|
|
355
|
+
| `channels.wecom.dynamicAgents.groupEnabled` | Enable dynamic agent for group chats | `true` |
|
|
356
|
+
| `channels.wecom.dynamicAgents.adminUsers` | Admin users (bypass dynamic routing, use main agent) | `[]` |
|
|
357
|
+
|
|
358
|
+
### Routing Configuration
|
|
359
|
+
|
|
360
|
+
| Config Path | Description | Default |
|
|
361
|
+
|---|---|---|
|
|
362
|
+
| `channels.wecom.routing.failClosedOnDefaultRoute` | Reject unbound accounts from falling back to default agent | `true` (multi-account) / `false` (single account) |
|
|
363
|
+
|
|
252
364
|
---
|
|
253
365
|
|
|
254
366
|
## 🔒 Access Control
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ 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
8
|
// ============================================================================
|
|
9
9
|
// 需要自动注入 tools.alsoAllow 的工具名列表
|
|
@@ -66,16 +66,17 @@ const plugin = {
|
|
|
66
66
|
api.registerChannel({ plugin: wecomPlugin });
|
|
67
67
|
// 注册 wecom_mcp:通过 HTTP 直接调用企业微信 MCP Server
|
|
68
68
|
api.registerTool(createWeComMcpTool(), { name: "wecom_mcp" });
|
|
69
|
+
const agentWebhookHandler = createWecomAgentWebhookHandler(api.runtime);
|
|
69
70
|
// 注册 Agent 模式 HTTP 路由(prefix match 覆盖带 accountId 的子路径)
|
|
70
71
|
api.registerHttpRoute({
|
|
71
72
|
path: WEBHOOK_PATHS.AGENT_PLUGIN,
|
|
72
|
-
handler:
|
|
73
|
+
handler: agentWebhookHandler,
|
|
73
74
|
auth: "plugin",
|
|
74
75
|
match: "prefix",
|
|
75
76
|
});
|
|
76
77
|
api.registerHttpRoute({
|
|
77
78
|
path: WEBHOOK_PATHS.AGENT,
|
|
78
|
-
handler:
|
|
79
|
+
handler: agentWebhookHandler,
|
|
79
80
|
auth: "plugin",
|
|
80
81
|
match: "prefix",
|
|
81
82
|
});
|
|
@@ -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;
|
|
@@ -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,10 +416,19 @@ 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
|
]
|
|
@@ -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 处理结果
|
|
@@ -3,54 +3,14 @@
|
|
|
3
3
|
* 提供统一的路由注入接口。
|
|
4
4
|
*/
|
|
5
5
|
import { shouldUseDynamicAgent, generateAgentId } from "./dynamic-agent.js";
|
|
6
|
-
import { shouldRejectWecomDefaultRoute } from "./routing.js";
|
|
7
|
-
/**
|
|
8
|
-
* 根据 matchedBy 和配置内容判断是否应该跳过动态路由
|
|
9
|
-
* **重要**:即使 matchedBy=binding.peer,也需要检查配置的 peer.kind 是否与实际聊天类型匹配
|
|
10
|
-
* @param params 判断参数
|
|
11
|
-
* @returns 是否跳过动态路由
|
|
12
|
-
*/
|
|
13
|
-
function shouldSkipDynamicRouting(params) {
|
|
14
|
-
const { matchedBy, config, route, chatType, log } = params;
|
|
15
|
-
// 只有精确匹配到 peer 时才可能跳过动态路由
|
|
16
|
-
if (matchedBy !== "binding.peer" && matchedBy !== "binding.peer.parent") {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
const binding = config.bindings?.find(b => b.agentId === route.agentId);
|
|
20
|
-
if (!binding) {
|
|
21
|
-
log?.(`[dynamic-routing] ⚠️ 警告:matchedBy=binding.peer,但在配置中找不到 agentId=${route.agentId} 的 binding`);
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
// 检查配置的 peer.kind 是否与实际聊天类型匹配
|
|
25
|
-
const configuredKind = binding.match?.peer?.kind;
|
|
26
|
-
if (!configuredKind) {
|
|
27
|
-
// 配置中没有指定 kind,使用动态路由
|
|
28
|
-
log?.(`[dynamic-routing] ℹ️ binding 未指定`);
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
const actualKind = chatType === "group" ? "group" : "direct";
|
|
32
|
-
if (configuredKind !== actualKind) {
|
|
33
|
-
// 类型不匹配,继续使用动态路由
|
|
34
|
-
log?.(`[dynamic-routing] ⚠️ binding 配置的聊天类型 (${configuredKind}) 与实际类型 (${actualKind}) 不匹配,继续使用动态路由`);
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
// 类型匹配,跳过动态路由
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
6
|
/**
|
|
41
7
|
* 统一处理动态路由注入逻辑
|
|
42
8
|
*
|
|
43
9
|
* 功能:
|
|
44
10
|
* 1. 判断是否需要使用动态 Agent
|
|
45
11
|
* 2. 根据 matchedBy 判断配置类型
|
|
46
|
-
* 3.
|
|
47
|
-
* 4.
|
|
48
|
-
* 5. 输出详细的调试日志
|
|
49
|
-
*
|
|
50
|
-
* **重要规则**:
|
|
51
|
-
* - matchedBy = "binding.peer" 或 "binding.peer.parent" → 跳过动态路由
|
|
52
|
-
* - matchedBy = "binding.account" 或其他 → 继续使用动态路由
|
|
53
|
-
* - matchedBy = "default" → 检查 fail-closed 策略
|
|
12
|
+
* 3. 返回最终的路由信息(不修改传入的 route 对象)
|
|
13
|
+
* 4. 输出详细的调试日志
|
|
54
14
|
*
|
|
55
15
|
* @param params 动态路由处理参数
|
|
56
16
|
* @returns 处理结果
|
|
@@ -59,22 +19,13 @@ export function processDynamicRouting(params) {
|
|
|
59
19
|
const { route, config, accountId, chatType, chatId, senderId, log } = params;
|
|
60
20
|
log?.(`[dynamic-routing] 🔍 调试 - matchedBy=${route.matchedBy}, agentId=${route.agentId}`);
|
|
61
21
|
if (route.matchedBy !== "default") {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
finalAgentId: route.agentId,
|
|
70
|
-
finalSessionKey: route.sessionKey,
|
|
71
|
-
routeModified: false,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
// 其他类型的配置(binding.account、binding.channel 等),继续使用动态路由
|
|
76
|
-
log?.(`[dynamic-routing] ℹ️ 检测到非精确配置的 bindings (matchedBy=${route.matchedBy}),继续使用动态路由`);
|
|
77
|
-
}
|
|
22
|
+
log?.(`[dynamic-routing] ℹ️ 检测到匹配的 bindings (matchedBy=${route.matchedBy}),跳过动态路由`);
|
|
23
|
+
return {
|
|
24
|
+
useDynamicAgent: false,
|
|
25
|
+
finalAgentId: route.agentId,
|
|
26
|
+
finalSessionKey: route.sessionKey,
|
|
27
|
+
routeModified: false,
|
|
28
|
+
};
|
|
78
29
|
}
|
|
79
30
|
// 判断是否使用动态 Agent
|
|
80
31
|
const useDynamicAgent = shouldUseDynamicAgent({
|
|
@@ -82,23 +33,7 @@ export function processDynamicRouting(params) {
|
|
|
82
33
|
senderId,
|
|
83
34
|
config,
|
|
84
35
|
});
|
|
85
|
-
|
|
86
|
-
const rejected = shouldRejectWecomDefaultRoute({
|
|
87
|
-
cfg: config,
|
|
88
|
-
matchedBy: route.matchedBy,
|
|
89
|
-
useDynamicAgent,
|
|
90
|
-
});
|
|
91
|
-
if (rejected) {
|
|
92
|
-
const rejectReason = `当前账号(${accountId})未绑定 OpenClaw Agent,已拒绝回退到默认主智能体。请在 bindings 中添加:{"agentId":"你的Agent","match":{"channel":"wecom","accountId":"${accountId}"}}`;
|
|
93
|
-
return {
|
|
94
|
-
useDynamicAgent,
|
|
95
|
-
rejected: true,
|
|
96
|
-
rejectReason,
|
|
97
|
-
finalAgentId: route.agentId,
|
|
98
|
-
finalSessionKey: route.sessionKey,
|
|
99
|
-
routeModified: false,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
36
|
+
log?.(`[dynamic-routing] 是否使用动态路由: useDynamicAgent=${useDynamicAgent}`);
|
|
102
37
|
// 使用动态 Agent
|
|
103
38
|
if (useDynamicAgent) {
|
|
104
39
|
log?.(`[dynamic-routing] 原始路由信息: agentId=${route.agentId}, matchedBy=${route.matchedBy}, sessionKey=${route.sessionKey}`);
|
|
@@ -107,16 +42,15 @@ export function processDynamicRouting(params) {
|
|
|
107
42
|
log?.(`[dynamic-routing] 🔄 路由注入: agentId=${targetAgentId}, sessionKey=${targetSessionKey}`);
|
|
108
43
|
return {
|
|
109
44
|
useDynamicAgent: true,
|
|
110
|
-
rejected: false,
|
|
111
45
|
finalAgentId: targetAgentId,
|
|
112
46
|
finalSessionKey: targetSessionKey,
|
|
113
47
|
routeModified: true,
|
|
114
48
|
};
|
|
115
49
|
}
|
|
50
|
+
log?.('[dynamic-routing] 🔄不使用动态路由');
|
|
116
51
|
// 不使用动态 Agent,返回原始路由
|
|
117
52
|
return {
|
|
118
53
|
useDynamicAgent: false,
|
|
119
|
-
rejected: false,
|
|
120
54
|
finalAgentId: route.agentId,
|
|
121
55
|
finalSessionKey: route.sessionKey,
|
|
122
56
|
routeModified: false,
|
package/dist/src/monitor.js
CHANGED
|
@@ -19,7 +19,6 @@ import * as os from "os";
|
|
|
19
19
|
import * as path from "path";
|
|
20
20
|
import { WSClient, generateReqId, WSAuthFailureError, WSReconnectExhaustedError } from "@wecom/aibot-node-sdk";
|
|
21
21
|
import { getWeComRuntime } from "./runtime.js";
|
|
22
|
-
// @ts-ignore
|
|
23
22
|
import { getDefaultMediaLocalRoots, resolveStateDir } from "./openclaw-compat.js";
|
|
24
23
|
import { CHANNEL_ID, THINKING_MESSAGE, MEDIA_IMAGE_PLACEHOLDER, MEDIA_DOCUMENT_PLACEHOLDER, WS_HEARTBEAT_INTERVAL_MS, WS_MAX_RECONNECT_ATTEMPTS, WS_MAX_AUTH_FAILURE_ATTEMPTS, EVENT_ENTER_CHECK_UPDATE, CMD_ENTER_EVENT_REPLY, SCENE_WECOM_OPENCLAW, } from "./const.js";
|
|
25
24
|
import { parseMessageContent } from "./message-parser.js";
|
|
@@ -100,9 +99,12 @@ function buildMediaErrorSummary(mediaUrl, result) {
|
|
|
100
99
|
export { WeComCommand } from "./const.js";
|
|
101
100
|
export { getWeComWebSocket, setReqIdForChat, getReqIdForChatAsync, getReqIdForChat, deleteReqIdForChat, warmupReqIdStore, flushReqIdStore, } from "./state-manager.js";
|
|
102
101
|
export { sendWeComReply } from "./message-sender.js";
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// 消息上下文构建
|
|
104
|
+
// ============================================================================
|
|
103
105
|
/**
|
|
104
106
|
* 构建消息上下文
|
|
105
|
-
* @returns
|
|
107
|
+
* @returns 消息上下文对象
|
|
106
108
|
*/
|
|
107
109
|
function buildMessageContext(frame, account, config, text, mediaList, quoteContent, runtime) {
|
|
108
110
|
const core = getWeComRuntime();
|
|
@@ -131,14 +133,6 @@ function buildMessageContext(frame, account, config, text, mediaList, quoteConte
|
|
|
131
133
|
log: runtime?.log ? (...args) => runtime.log?.(...args) : undefined,
|
|
132
134
|
error: runtime?.error ? (...args) => runtime.error?.(...args) : undefined,
|
|
133
135
|
});
|
|
134
|
-
// 处理路由拦截
|
|
135
|
-
if (routingResult.rejected) {
|
|
136
|
-
runtime?.error?.(`[wecom] 🚫 routing guard: blocked default fallback accountId=${account.accountId} matchedBy=${route.matchedBy}`);
|
|
137
|
-
return {
|
|
138
|
-
rejected: true,
|
|
139
|
-
rejectReason: routingResult.rejectReason,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
136
|
// 应用动态路由结果
|
|
143
137
|
if (routingResult.routeModified) {
|
|
144
138
|
route.agentId = routingResult.finalAgentId;
|
|
@@ -188,7 +182,7 @@ function buildMessageContext(frame, account, config, text, mediaList, quoteConte
|
|
|
188
182
|
MediaUrls: mediaPaths,
|
|
189
183
|
ReplyToBody: quoteContent,
|
|
190
184
|
});
|
|
191
|
-
return {
|
|
185
|
+
return { ctxPayload, route, storePath, chatId, chatType };
|
|
192
186
|
}
|
|
193
187
|
/**
|
|
194
188
|
* 发送"思考中"消息
|
|
@@ -579,24 +573,7 @@ async function processWeComMessageNow(entry) {
|
|
|
579
573
|
// await sendThinkingReply({ wsClient, frame, streamId, runtime });
|
|
580
574
|
// }
|
|
581
575
|
// Step 7: 构建上下文并路由到核心处理流程(带整体超时保护)
|
|
582
|
-
const
|
|
583
|
-
// 处理路由拦截
|
|
584
|
-
if (contextResult.rejected) {
|
|
585
|
-
runtime.log?.(`[wecom] Routing guard: sending rejection prompt to user`);
|
|
586
|
-
try {
|
|
587
|
-
await wsClient.sendMessage(chatId, {
|
|
588
|
-
msgtype: "markdown",
|
|
589
|
-
markdown: { content: contextResult.rejectReason },
|
|
590
|
-
});
|
|
591
|
-
runtime.log?.(`[wecom] Routing guard prompt delivered`);
|
|
592
|
-
}
|
|
593
|
-
catch (err) {
|
|
594
|
-
runtime.error?.(`[wecom] Routing guard prompt failed: ${String(err)}`);
|
|
595
|
-
}
|
|
596
|
-
cleanupState();
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
const { ctxPayload, route, storePath, chatId: resolvedChatId, chatType } = contextResult;
|
|
576
|
+
const { ctxPayload, route, storePath, chatId: resolvedChatId, chatType } = buildMessageContext(frame, account, config, text, mediaList, quoteContent, runtime);
|
|
600
577
|
// runtime.log?.(`[plugin -> openclaw] body=${text}, mediaPaths=${JSON.stringify(mediaList.map(m => m.path))}${quoteContent ? `, quote=${quoteContent}` : ''}`);
|
|
601
578
|
try {
|
|
602
579
|
await routeAndDispatchMessage({
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* 本模块在加载时一次性探测 SDK 导出,存在则直接 re-export SDK 版本,
|
|
8
8
|
* 不存在则导出 fallback 实现。其他模块统一从本文件导入,无需关心底层兼容细节。
|
|
9
9
|
*/
|
|
10
|
+
import { resolveStateDir } from "./state-dir-resolve.js";
|
|
11
|
+
export { resolveStateDir };
|
|
10
12
|
export declare const DEFAULT_ACCOUNT_ID = "default";
|
|
11
13
|
/** 与 openclaw plugin-sdk 中 WebMediaResult 兼容的类型 */
|
|
12
14
|
export type WebMediaResult = {
|
|
@@ -11,8 +11,8 @@ import * as fs from "node:fs/promises";
|
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import * as os from "node:os";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
import { resolveStateDir } from "./state-dir-resolve.js";
|
|
15
|
+
export { resolveStateDir };
|
|
16
16
|
export const DEFAULT_ACCOUNT_ID = "default";
|
|
17
17
|
const _sdkReady = import("openclaw/plugin-sdk/core")
|
|
18
18
|
.then((sdk) => {
|
|
@@ -78,15 +78,6 @@ export type WecomAgentConfig = {
|
|
|
78
78
|
/** 允许的用户列表,为空表示允许所有人 */
|
|
79
79
|
allowFrom?: Array<string | number>;
|
|
80
80
|
};
|
|
81
|
-
/** 路由行为配置 */
|
|
82
|
-
export type WecomRoutingConfig = {
|
|
83
|
-
/**
|
|
84
|
-
* 当路由未命中 bindings(matchedBy=default)时是否拒绝继续处理。
|
|
85
|
-
* - true: fail-closed(推荐于多账号)
|
|
86
|
-
* - false: 允许回退默认 agent(历史兼容)
|
|
87
|
-
*/
|
|
88
|
-
failClosedOnDefaultRoute?: boolean;
|
|
89
|
-
};
|
|
90
81
|
/** 动态 Agent 配置 */
|
|
91
82
|
export type WecomDynamicAgentsConfig = {
|
|
92
83
|
/** 是否启用动态 Agent */
|
package/dist/src/utils.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
5
5
|
import type { ResolvedAgentAccount } from "./types/account.js";
|
|
6
|
-
import type { WecomAgentConfig, WecomNetworkConfig, WecomMediaConfig } from "./types/config.js";
|
|
6
|
+
import type { WecomAgentConfig, WecomNetworkConfig, WecomMediaConfig, WecomDynamicAgentsConfig } from "./types/config.js";
|
|
7
7
|
/**
|
|
8
8
|
* 企业微信群组配置
|
|
9
9
|
*/
|
|
@@ -38,6 +38,8 @@ export interface WeComConfig {
|
|
|
38
38
|
network?: WecomNetworkConfig;
|
|
39
39
|
/** 媒体处理配置 */
|
|
40
40
|
media?: WecomMediaConfig;
|
|
41
|
+
/** 动态 Agent 配置 */
|
|
42
|
+
dynamicAgents?: WecomDynamicAgentsConfig;
|
|
41
43
|
/** 连接模式:webhook | websocket(默认 websocket) */
|
|
42
44
|
connectionMode?: "webhook" | "websocket";
|
|
43
45
|
/** Webhook 验证 token */
|
|
@@ -503,25 +503,6 @@ export async function startAgentForStream(params) {
|
|
|
503
503
|
log: (msg) => target.runtime.log?.(msg.replace(/^\[dynamic-routing\]/, "[webhook]")),
|
|
504
504
|
error: (msg) => target.runtime.error?.(msg.replace(/^\[dynamic-routing\]/, "[webhook]")),
|
|
505
505
|
});
|
|
506
|
-
// 处理路由拦截
|
|
507
|
-
if (routingResult.rejected) {
|
|
508
|
-
target.runtime.error?.(`[wecom] routing guard: blocked default fallback accountId=${account.accountId} matchedBy=${route.matchedBy} streamId=${streamId}`);
|
|
509
|
-
if (account.agent) {
|
|
510
|
-
try {
|
|
511
|
-
await sendAgentText({
|
|
512
|
-
agent: account.agent,
|
|
513
|
-
toUser: chatType === "direct" ? userid : undefined,
|
|
514
|
-
chatId: chatType === "group" ? chatId : undefined,
|
|
515
|
-
text: routingResult.rejectReason,
|
|
516
|
-
});
|
|
517
|
-
target.runtime.log?.(`[webhook] routing guard prompt delivered to ${chatType === "direct" ? userid : chatId}`);
|
|
518
|
-
}
|
|
519
|
-
catch (err) {
|
|
520
|
-
target.runtime.error?.(`[webhook] routing guard prompt failed: ${String(err)}`);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
506
|
// 应用动态路由结果
|
|
526
507
|
if (routingResult.routeModified) {
|
|
527
508
|
route.agentId = routingResult.finalAgentId;
|
package/package.json
CHANGED
|
@@ -39,7 +39,7 @@ wecom-meeting-create 提供企业微信预约会议的创建能力, 支持设置
|
|
|
39
39
|
| `meeting_duration` | integer | 是 | 会议持续时长 (秒), 例如 3600 = 1 小时 |
|
|
40
40
|
| `description` | string | 否 | 会议描述 |
|
|
41
41
|
| `location` | string | 否 | 会议地点 |
|
|
42
|
-
| `invitees` | object |
|
|
42
|
+
| `invitees` | object | 否 | 被邀请人, 格式:`{"userid": ["lisi", "wangwu"]}` |
|
|
43
43
|
| `settings` | object | 否 | 会议设置 (详见下方) |
|
|
44
44
|
|
|
45
45
|
> 被邀请人 userid 通过 `wecom-contact-lookup` 技能获取
|
|
@@ -63,7 +63,7 @@ wecom-meeting-manage 提供企业微信会议管理能力, 包含以下功能:
|
|
|
63
63
|
| 参数 | 类型 | 必填 | 说明 |
|
|
64
64
|
| ------------- | ------ | ---- | -------------------------------------- |
|
|
65
65
|
| `meetingid` | string | 是 | 会议 ID, 通过 `wecom-meeting-query` 技能获取 |
|
|
66
|
-
| `invitees` | array |
|
|
66
|
+
| `invitees` | array | 是 | 受邀成员列表, 每项包含 `userid` 字段 |
|
|
67
67
|
|
|
68
68
|
> **注意**: invitees 为全量覆盖, 传入的列表将替换现有成员列表.
|
|
69
69
|
> invitees 的 userid 通过 `wecom-contact-lookup` 技能获取
|
|
@@ -92,7 +92,7 @@ wecom-meeting-manage 提供企业微信会议管理能力, 包含以下功能:
|
|
|
92
92
|
|
|
93
93
|
使用 `wecom_mcp` tool 调用 `wecom_mcp call meeting cancel_meeting '{"meetingid": "<target_meetingid>"}'`
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
3. **展示结果**:
|
|
96
96
|
|
|
97
97
|
```
|
|
98
98
|
✅ 会议已取消: 技术方案评审
|
|
@@ -107,7 +107,7 @@ wecom-meeting-manage 提供企业微信会议管理能力, 包含以下功能:
|
|
|
107
107
|
**步骤:**
|
|
108
108
|
|
|
109
109
|
1. **定位会议**: 通过 `wecom-meeting-query` 技能查询会议列表 + 匹配找到目标会议.
|
|
110
|
-
2. **获取当前受邀成员**: `
|
|
110
|
+
2. **获取当前受邀成员**: `set_invite_meeting_members` 为全量覆盖, 必须先通过 `wecom-meeting-query` 技能的 `get_meeting_info` 获取会议详情, 获取现有成员后再合并.
|
|
111
111
|
3. **通讯录查询**: 调用 `wecom-contact-lookup` 技能获取通讯录成员, 按姓名筛选出王五的 userid.
|
|
112
112
|
|
|
113
113
|
使用 `wecom_mcp` tool 调用 `wecom_mcp call contact get_userlist '{}'`
|
|
@@ -119,7 +119,7 @@ wecom-meeting-manage 提供企业微信会议管理能力, 包含以下功能:
|
|
|
119
119
|
|
|
120
120
|
使用 `wecom_mcp` tool 调用 `wecom_mcp call meeting set_invite_meeting_members '{"meetingid": "<target_meetingid>", "invitees": [{"userid": "zhangsan"}, {"userid": "lisi"}, {"userid": "wangwu"}]}'`
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
6. **展示结果**:
|
|
123
123
|
|
|
124
124
|
```
|
|
125
125
|
✅ 会议成员已更新: 技术方案评审
|
|
@@ -133,4 +133,4 @@ wecom-meeting-manage 提供企业微信会议管理能力, 包含以下功能:
|
|
|
133
133
|
- **参与人仅支持企业内成员**, 不支持外部人员
|
|
134
134
|
- **通讯录查询**: 涉及参与人时, 需先通过 `wecom-contact-lookup` 技能的 `get_userlist` 接口获取全量通讯录成员, 再按姓名/别名本地筛选匹配出对应的 `userid`. 该接口无入参, 返回当前用户可见范围内的成员列表 (含 `userid`, `name`, `alias`)
|
|
135
135
|
- **定位会议**: 管理操作需先通过 `wecom-meeting-query` 技能查询到目标会议的 meetingid
|
|
136
|
-
- **成员更新为全量覆盖**: `
|
|
136
|
+
- **成员更新为全量覆盖**: `set_invite_meeting_members` 传入的列表将替换现有成员列表, 需先获取当前成员再合并
|
|
@@ -64,9 +64,9 @@ metadata:
|
|
|
64
64
|
- 添加参与人:使用 `wecom_mcp` tool 调用 `wecom_mcp call schedule add_schedule_attendees '{"schedule_id": "SCHEDULE_ID", "attendees": [{"userid": "USER_ID"}]}'`
|
|
65
65
|
- 移除参与人:使用 `wecom_mcp` tool 调用 `wecom_mcp call schedule del_schedule_attendees '{"schedule_id": "SCHEDULE_ID", "attendees": [{"userid": "USER_ID"}]}'`
|
|
66
66
|
|
|
67
|
-
###
|
|
67
|
+
### check_availability — 查询闲忙
|
|
68
68
|
|
|
69
|
-
使用 `wecom_mcp` tool 调用 `wecom_mcp call schedule
|
|
69
|
+
使用 `wecom_mcp` tool 调用 `wecom_mcp call schedule check_availability '{"check_user_list": ["USER_ID_1", "USER_ID_2"], "start_time": "YYYY-MM-DD HH:MM:SS", "end_time": "YYYY-MM-DD HH:MM:SS"}'`
|
|
70
70
|
|
|
71
71
|
支持 1~10 个用户,返回各用户的忙碌时段列表。参见 [API 详情](references/api-check-availability.md)。
|
|
72
72
|
|
|
@@ -156,6 +156,6 @@ metadata:
|
|
|
156
156
|
|
|
157
157
|
**流程:**
|
|
158
158
|
1. 通过 **wecom-contact-lookup** 获取相关人员 userid
|
|
159
|
-
2. 调用 `
|
|
159
|
+
2. 调用 `check_availability` 查询指定时间范围内各用户的忙碌时段
|
|
160
160
|
3. 分析所有用户的忙碌时段,计算出共同空闲时段并推荐给用户
|
|
161
161
|
4. 用户确认时段后,调用 `create_schedule` 创建会议并自动添加参与人
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# check_availability API
|
|
2
2
|
|
|
3
3
|
查询指定用户在某时间范围内的忙碌时段。
|
|
4
4
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
## 请求示例
|
|
14
14
|
|
|
15
|
-
使用 `wecom_mcp` tool 调用 `wecom_mcp call schedule
|
|
15
|
+
使用 `wecom_mcp` tool 调用 `wecom_mcp call schedule check_availability '{"check_user_list": ["USER_ID_1", "USER_ID_2"], "start_time": "YYYY-MM-DD HH:MM:SS", "end_time": "YYYY-MM-DD HH:MM:SS"}'`
|
|
16
16
|
|
|
17
17
|
## 返回字段
|
|
18
18
|
|
|
@@ -32,7 +32,7 @@ description: 企业微信智能表格数据(记录)管理技能。提供智
|
|
|
32
32
|
|
|
33
33
|
查询子表全部记录。
|
|
34
34
|
|
|
35
|
-
-
|
|
35
|
+
- 通过 sheetid:使用 `wecom_mcp` tool 调用 `wecom_mcp call doc smartsheet_get_records '{"docid": "DOCID", "sheet_id": "SHEETID"}'`
|
|
36
36
|
- 或通过 URL:使用 `wecom_mcp` tool 调用 `wecom_mcp call doc smartsheet_get_records '{"url": "https://doc.weixin.qq.com/smartsheet/xxx", "sheet_id": "SHEETID"}'`
|
|
37
37
|
|
|
38
38
|
参见 [API 详情](references/api-get-records.md)。
|
|
@@ -49,7 +49,10 @@ description: 企业微信智能表格数据(记录)管理技能。提供智
|
|
|
49
49
|
|
|
50
50
|
### smartsheet_update_records
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
更新一行或多行记录,单次建议在 500 行内。需提供 record_id(通过 `smartsheet_get_records` 获取)。支持通过 `key_type` 指定 values 的 key 使用字段标题或字段 ID:
|
|
53
|
+
|
|
54
|
+
- `CELL_VALUE_KEY_TYPE_FIELD_TITLE`:key 为字段标题
|
|
55
|
+
- `CELL_VALUE_KEY_TYPE_FIELD_ID`:key 为字段 ID
|
|
53
56
|
|
|
54
57
|
使用 `wecom_mcp` tool 调用 `wecom_mcp call doc smartsheet_update_records '{"docid": "DOCID", "sheet_id": "SHEETID", "key_type": "CELL_VALUE_KEY_TYPE_FIELD_ID", "records": [{"record_id": "RECORDID", "values": {"FIELDID": [{"type": "text", "text": "更新后的内容"}]}}]}'`
|
|
55
58
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# 单元格值格式参考
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`smartsheet_add_records` 仅支持**字段标题**作为key。
|
|
4
|
+
`smartsheet_update_records` 支持通过 `key_type` 参数指定使用字段标题(`CELL_VALUE_KEY_TYPE_FIELD_TITLE`)或字段 ID(`CELL_VALUE_KEY_TYPE_FIELD_ID`)。
|
|
4
5
|
|
|
5
6
|
## 各字段类型的值格式
|
|
6
7
|
|
|
@@ -36,10 +37,40 @@
|
|
|
36
37
|
|
|
37
38
|
```json
|
|
38
39
|
"优先级": [{"text": "高"}],
|
|
39
|
-
"标签": [{"text": "紧急"}, {"text": "重要"}]
|
|
40
|
+
"标签": [{"text": "紧急", "style": 17}, {"text": "重要", "style": 12}]
|
|
40
41
|
```
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
已存在的选项应通过 `id` 匹配(`id` 可从 `smartsheet_get_fields` 返回中获取),新增选项时不填 `id`。可附带 `style`(颜色 1-27),对照表如下:
|
|
44
|
+
|
|
45
|
+
| style | 颜色 |
|
|
46
|
+
|-------|------|
|
|
47
|
+
| 1 | 浅红1 |
|
|
48
|
+
| 2 | 浅橙1 |
|
|
49
|
+
| 3 | 浅天蓝1 |
|
|
50
|
+
| 4 | 浅绿1 |
|
|
51
|
+
| 5 | 浅紫1 |
|
|
52
|
+
| 6 | 浅粉红1 |
|
|
53
|
+
| 7 | 浅灰1 |
|
|
54
|
+
| 8 | 白 |
|
|
55
|
+
| 9 | 灰 |
|
|
56
|
+
| 10 | 浅蓝1 |
|
|
57
|
+
| 11 | 浅蓝2 |
|
|
58
|
+
| 12 | 蓝 |
|
|
59
|
+
| 13 | 浅天蓝2 |
|
|
60
|
+
| 14 | 天蓝 |
|
|
61
|
+
| 15 | 浅绿2 |
|
|
62
|
+
| 16 | 绿 |
|
|
63
|
+
| 17 | 浅红2 |
|
|
64
|
+
| 18 | 红 |
|
|
65
|
+
| 19 | 浅橙2 |
|
|
66
|
+
| 20 | 橙 |
|
|
67
|
+
| 21 | 浅黄1 |
|
|
68
|
+
| 22 | 浅黄2 |
|
|
69
|
+
| 23 | 黄 |
|
|
70
|
+
| 24 | 浅紫2 |
|
|
71
|
+
| 25 | 紫 |
|
|
72
|
+
| 26 | 浅粉红2 |
|
|
73
|
+
| 27 | 粉红 |
|
|
43
74
|
|
|
44
75
|
### 5. 日期时间 (DATE_TIME)
|
|
45
76
|
|
|
@@ -118,3 +149,4 @@
|
|
|
118
149
|
}]
|
|
119
150
|
}
|
|
120
151
|
```
|
|
152
|
+
|
package/dist/src/routing.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
-
import type { WeComMultiAccountConfig } from "./accounts.ts";
|
|
3
|
-
/**
|
|
4
|
-
* 默认策略:
|
|
5
|
-
* - matrix(多账号): 开启 fail-closed,防止未绑定账号回退到 main
|
|
6
|
-
* - legacy(单账号兼容): 维持历史行为,不强制拦截
|
|
7
|
-
*/
|
|
8
|
-
/** 解析模式 */
|
|
9
|
-
export type ResolvedMode = "disabled" | "legacy" | "matrix";
|
|
10
|
-
/**
|
|
11
|
-
* 检测配置中启用的模式
|
|
12
|
-
*/
|
|
13
|
-
export declare function detectMode(config: WeComMultiAccountConfig | undefined): ResolvedMode;
|
|
14
|
-
/**
|
|
15
|
-
* 默认策略:
|
|
16
|
-
* - matrix(多账号): 开启 fail-closed,防止未绑定账号回退到 main
|
|
17
|
-
* - legacy(单账号兼容): 维持历史行为,不强制拦截
|
|
18
|
-
*/
|
|
19
|
-
export declare function resolveWecomFailClosedOnDefaultRoute(cfg: OpenClawConfig): boolean;
|
|
20
|
-
export declare function shouldRejectWecomDefaultRoute(params: {
|
|
21
|
-
cfg: OpenClawConfig;
|
|
22
|
-
matchedBy: string;
|
|
23
|
-
useDynamicAgent: boolean;
|
|
24
|
-
}): boolean;
|
package/dist/src/routing.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 检测配置中启用的模式
|
|
3
|
-
*/
|
|
4
|
-
export function detectMode(config) {
|
|
5
|
-
if (!config || config.enabled === false)
|
|
6
|
-
return "disabled";
|
|
7
|
-
const accounts = config.accounts;
|
|
8
|
-
if (accounts && typeof accounts === "object") {
|
|
9
|
-
const enabledEntries = Object.values(accounts).filter((entry) => entry && entry.enabled !== false);
|
|
10
|
-
if (enabledEntries.length > 0)
|
|
11
|
-
return "matrix";
|
|
12
|
-
}
|
|
13
|
-
return "legacy";
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* 默认策略:
|
|
17
|
-
* - matrix(多账号): 开启 fail-closed,防止未绑定账号回退到 main
|
|
18
|
-
* - legacy(单账号兼容): 维持历史行为,不强制拦截
|
|
19
|
-
*/
|
|
20
|
-
export function resolveWecomFailClosedOnDefaultRoute(cfg) {
|
|
21
|
-
const wecom = cfg.channels?.wecom;
|
|
22
|
-
// @ts-ignore
|
|
23
|
-
const explicit = wecom?.routing?.failClosedOnDefaultRoute;
|
|
24
|
-
if (typeof explicit === "boolean")
|
|
25
|
-
return explicit;
|
|
26
|
-
return detectMode(wecom) === "matrix";
|
|
27
|
-
}
|
|
28
|
-
export function shouldRejectWecomDefaultRoute(params) {
|
|
29
|
-
if (params.matchedBy !== "default")
|
|
30
|
-
return false;
|
|
31
|
-
if (params.useDynamicAgent)
|
|
32
|
-
return false;
|
|
33
|
-
return resolveWecomFailClosedOnDefaultRoute(params.cfg);
|
|
34
|
-
}
|