@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 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 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, 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
- - ⚡ Auto heartbeat keep-alive and reconnection (up to 100 reconnect attempts)
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.2.13`
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 long-polling | JSON | Quick setup, streaming replies |
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.sendThinkingMessage` | Send "thinking" placeholder | `true` / `false` | `true` |
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` (or `/plugins/wecom/agent/<accountId>` for multi-account)
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/agent` | Recommended path (single account) |
176
- | `/plugins/wecom/agent/<accountId>` | Multi-account path |
177
- | `/wecom/agent` | Legacy compatible path |
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
- "bot": {
226
- "botId": "bot-id-1",
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 { handleWecomAgentWebhookRequest } from "./src/agent/webhook.js";
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: handleWecomAgentWebhookRequest,
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: handleWecomAgentWebhookRequest,
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 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,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. 检查是否被默认路由拦截(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 处理结果
@@ -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. 检查是否被默认路由拦截(fail-closed 策略)
47
- * 4. 返回最终的路由信息(不修改传入的 route 对象)
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
- // 根据 matchedBy 和配置内容判断是否跳过动态路由
63
- if (shouldSkipDynamicRouting({ matchedBy: route.matchedBy, config, route, chatType, log })) {
64
- // 精确配置(binding.peer)且类型匹配,跳过动态路由
65
- log?.(`[dynamic-routing] ℹ️ 检测到精确配置的 bindings (matchedBy=${route.matchedBy}),且聊天类型匹配,跳过动态路由`);
66
- return {
67
- useDynamicAgent: false,
68
- rejected: false,
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,
@@ -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 { rejected: false, ctxPayload, route, storePath, chatId, chatType };
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 contextResult = buildMessageContext(frame, account, config, text, mediaList, quoteContent, runtime);
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
- // @ts-ignore
15
- import { resolveStateDir } from "./state-dir-resolve";
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 */
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wecom/wecom-openclaw-plugin",
3
- "version": "2026.3.31-beta.2",
3
+ "version": "2026.3.31-beta.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -65,4 +65,4 @@
65
65
 
66
66
  - `content_type` 当前仅支持 `1`(Markdown 格式)
67
67
  - 此操作为**覆写**,会替换文档全部内容
68
- - 建议先调用 `get_document` 了解当前内容再编辑
68
+ - 建议先调用 `get_doc_content` 了解当前内容再编辑
@@ -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 | 是 | 被邀请人, 格式:`{"userid": ["lisi", "wangwu"]}` |
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 | 否 | 受邀成员列表, 每项包含 `userid` 字段 |
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
- 4. **展示结果**:
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. **获取当前受邀成员**: `set_invite_members` 为全量覆盖, 必须先通过 `wecom-meeting-query` 技能的 `get_meeting_info` 获取会议详情, 获取现有成员后再合并.
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
- 7. **展示结果**:
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
- - **成员更新为全量覆盖**: `set_invite_members` 传入的列表将替换现有成员列表, 需先获取当前成员再合并
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
- ### check_availablity — 查询闲忙
67
+ ### check_availability — 查询闲忙
68
68
 
69
- 使用 `wecom_mcp` tool 调用 `wecom_mcp call schedule check_availablity '{"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"}'`
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. 调用 `check_availablity` 查询指定时间范围内各用户的忙碌时段
159
+ 2. 调用 `check_availability` 查询指定时间范围内各用户的忙碌时段
160
160
  3. 分析所有用户的忙碌时段,计算出共同空闲时段并推荐给用户
161
161
  4. 用户确认时段后,调用 `create_schedule` 创建会议并自动添加参与人
@@ -1,4 +1,4 @@
1
- # check_availablity API
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 check_availablity '{"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"}'`
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
- - 使用 `wecom_mcp` tool 调用 `wecom_mcp call doc smartsheet_get_records '{"docid": "DOCID", "sheet_id": "SHEETID"}'`
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
- 更新一行或多行记录,单次必须在 500 行内。需提供 record_id(通过 `smartsheet_get_records` 获取)。
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
 
@@ -59,3 +59,4 @@
59
59
  "sheet_id": "123Abc"
60
60
  }
61
61
  ```
62
+
@@ -1,6 +1,7 @@
1
1
  # 单元格值格式参考
2
2
 
3
- 在 `smartsheet_add_records` 和 `smartsheet_update_records` 中,`values` 的 key **必须是字段标题(field_title)**,不能使用字段 ID。
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
- 选项可附带 `id`(已存在选项)和 `style`(颜色 1-27)。
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
+
@@ -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;
@@ -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
- }