@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.
@@ -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,
@@ -76,21 +76,11 @@ function isGroupSenderAllowed(params) {
76
76
  * @returns 检查结果,包含是否允许继续处理
77
77
  */
78
78
  export function checkGroupPolicy(params) {
79
- const { chatId, senderId, account, config, runtime } = params;
80
- const wecomConfig = (config.channels?.[CHANNEL_ID] ?? {});
81
- const defaultGroupPolicy = config.channels?.[CHANNEL_ID]?.groupPolicy;
82
- const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "open";
83
- // const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
84
- // providerConfigPresent: config.channels?.[CHANNEL_ID] !== undefined,
85
- // groupPolicy: wecomConfig.groupPolicy,
86
- // defaultGroupPolicy,
87
- // });
88
- // warnMissingProviderGroupPolicyFallbackOnce({
89
- // providerMissingFallbackApplied,
90
- // providerKey: CHANNEL_ID,
91
- // accountId: account.accountId,
92
- // log: (msg) => runtime.log?.(msg),
93
- // });
79
+ const { chatId, senderId, account, runtime } = params;
80
+ // 使用 account.config(已经过多账号合并),而非顶层 config.channels.wecom
81
+ // 避免多账户模式下 groupAllowFrom / groups 等字段取不到账号级配置
82
+ const wecomConfig = account.config;
83
+ const groupPolicy = wecomConfig.groupPolicy ?? "open";
94
84
  const groupAllowFrom = wecomConfig.groupAllowFrom ?? [];
95
85
  const groupAllowed = isWeComGroupAllowed({
96
86
  groupPolicy,
@@ -22,7 +22,7 @@ import { getWeComRuntime } from "./runtime.js";
22
22
  import { getDefaultMediaLocalRoots, resolveStateDir } from "./openclaw-compat.js";
23
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";
24
24
  import { parseMessageContent } from "./message-parser.js";
25
- import { sendWeComReply, sendWeComReplyNonBlocking, StreamExpiredError } from "./message-sender.js";
25
+ import { sendWeComReply, StreamExpiredError } from "./message-sender.js";
26
26
  import { downloadAndSaveImages, downloadAndSaveFiles } from "./media-handler.js";
27
27
  import { uploadAndSendMedia } from "./media-uploader.js";
28
28
  import { maskTemplateCardBlocks } from "./template-card-parser.js";
@@ -32,14 +32,6 @@ import { checkDmPolicy } from "./dm-policy.js";
32
32
  import { setWeComWebSocket, setMessageState, deleteMessageState, setReqIdForChat, warmupReqIdStore, startMessageStateCleanup, stopMessageStateCleanup, cleanupAccount, } from "./state-manager.js";
33
33
  import { PLUGIN_VERSION } from "./version.js";
34
34
  import { processDynamicRouting } from "./dynamic-routing.js";
35
- /**
36
- * 去除文本中的 `<think>...</think>` 标签(支持跨行),返回剩余可见文本。
37
- * 用于判断大模型回复中是否包含实际用户可见内容(而非仅有 thinking 推理过程)。
38
- */
39
- function stripThinkTags(text) {
40
- return text;
41
- // return text.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
42
- }
43
35
  // ============================================================================
44
36
  // 媒体本地路径白名单扩展
45
37
  // ============================================================================
@@ -99,9 +91,12 @@ function buildMediaErrorSummary(mediaUrl, result) {
99
91
  export { WeComCommand } from "./const.js";
100
92
  export { getWeComWebSocket, setReqIdForChat, getReqIdForChatAsync, getReqIdForChat, deleteReqIdForChat, warmupReqIdStore, flushReqIdStore, } from "./state-manager.js";
101
93
  export { sendWeComReply } from "./message-sender.js";
94
+ // ============================================================================
95
+ // 消息上下文构建
96
+ // ============================================================================
102
97
  /**
103
98
  * 构建消息上下文
104
- * @returns 消息上下文对象,或路由拒绝信息(当路由被拦截时)
99
+ * @returns 消息上下文对象
105
100
  */
106
101
  function buildMessageContext(frame, account, config, text, mediaList, quoteContent, runtime) {
107
102
  const core = getWeComRuntime();
@@ -130,14 +125,6 @@ function buildMessageContext(frame, account, config, text, mediaList, quoteConte
130
125
  log: runtime?.log ? (...args) => runtime.log?.(...args) : undefined,
131
126
  error: runtime?.error ? (...args) => runtime.error?.(...args) : undefined,
132
127
  });
133
- // 处理路由拦截
134
- if (routingResult.rejected) {
135
- runtime?.error?.(`[wecom] 🚫 routing guard: blocked default fallback accountId=${account.accountId} matchedBy=${route.matchedBy}`);
136
- return {
137
- rejected: true,
138
- rejectReason: routingResult.rejectReason,
139
- };
140
- }
141
128
  // 应用动态路由结果
142
129
  if (routingResult.routeModified) {
143
130
  route.agentId = routingResult.finalAgentId;
@@ -187,7 +174,7 @@ function buildMessageContext(frame, account, config, text, mediaList, quoteConte
187
174
  MediaUrls: mediaPaths,
188
175
  ReplyToBody: quoteContent,
189
176
  });
190
- return { rejected: false, ctxPayload, route, storePath, chatId, chatType };
177
+ return { ctxPayload, route, storePath, chatId, chatType };
191
178
  }
192
179
  /**
193
180
  * 发送"思考中"消息
@@ -275,7 +262,7 @@ async function finishThinkingStream(ctx) {
275
262
  const { wsClient, frame, state, runtime } = ctx;
276
263
  const body = frame.body;
277
264
  const chatId = body.chatid || body.from.userid;
278
- const visibleText = stripThinkTags(state.accumulatedText);
265
+ const visibleText = state.accumulatedText;
279
266
  let finishText = state.accumulatedText;
280
267
  if (visibleText) {
281
268
  finishText = state.accumulatedText;
@@ -353,20 +340,6 @@ async function routeAndDispatchMessage(params) {
353
340
  await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
354
341
  ctx: ctxPayload,
355
342
  cfg: config,
356
- // replyResolver: async (ctx, opts) => {
357
- // const startTime = Date.now();
358
- // const TEN_MINUTES = 7 * 60 * 1000;
359
- //
360
- // console.log('开始输出内容');
361
- //
362
- // while (Date.now() - startTime < TEN_MINUTES) {
363
- // // 每隔一段时间发送一个 block reply
364
- // await opts?.onBlockReply?.({ text: `输出内容 ${new Date().toISOString()}` });
365
- // await new Promise(resolve => setTimeout(resolve, 30 * 1000)); // 每5秒输出一次
366
- // }
367
- //
368
- // return { text: "10分钟输出完成" }; // 最终回复
369
- // },
370
343
  dispatcherOptions: {
371
344
  onReplyStart: async () => {
372
345
  if (!isShowThink && state.streamId && !state.accumulatedText) {
@@ -380,10 +353,10 @@ async function routeAndDispatchMessage(params) {
380
353
  }
381
354
  },
382
355
  deliver: async (payload, info) => {
383
- // runtime.log?.(`[openclaw -> plugin] kind=${info.kind}, text=${payload.text ?? ''}, mediaUrl=${payload.mediaUrl ?? ''}, mediaUrls=${JSON.stringify(payload.mediaUrls ?? [])}`);
384
- // 累积文本(快速指令等非流式回复只走 deliver,不走 onPartialReply)
356
+ runtime.log?.(`[openclaw -> plugin] kind=${info.kind}, text=${payload.text ?? ''}, mediaUrl=${payload.mediaUrl ?? ''}, mediaUrls=${JSON.stringify(payload.mediaUrls ?? [])}`);
357
+ // 累积文本
385
358
  if (payload.text) {
386
- state.accumulatedText = payload.text;
359
+ state.accumulatedText += `${payload.text || ''}`;
387
360
  }
388
361
  // 发送媒体(统一走主动发送)
389
362
  const mediaUrls = payload.mediaUrls?.length ? payload.mediaUrls : payload.mediaUrl ? [payload.mediaUrl] : [];
@@ -403,41 +376,30 @@ async function routeAndDispatchMessage(params) {
403
376
  runtime.error?.(`[wecom] sendMediaBatch threw: ${errMsg}`);
404
377
  }
405
378
  }
406
- // 中间帧流式更新已迁移到 onPartialReply(token 级别,更细粒度)
407
- // deliver 只负责文本累积和媒体发送
408
- },
409
- onError: (err, info) => {
410
- runtime.error?.(`[wecom] ${info.kind} reply failed: ${String(err)}`);
411
- },
412
- },
413
- // onPartialReply:token 级别的流式 UI 刷新
414
- // 使用 SDK 的 replyStreamNonBlocking:上一条同 reqId 的消息未收到 ack 时跳过本次发送,
415
- // 避免流式中间帧排队积压导致延迟。丢帧在流式场景下可接受(text 是累积文本,最终由 finishThinkingStream 兜底)。
416
- replyOptions: {
417
- onPartialReply: (data) => {
418
- // 累积文本
419
- if (data.text) {
420
- state.accumulatedText = (data.text || '');
421
- }
422
- if (!state.streamId || !data.text || state.streamExpired)
423
- return;
379
+ // 中间帧:有可见文本时流式更新(流式过期后跳过,等 deliver 完成后主动发送)
424
380
  // 使用 maskTemplateCardBlocks 遮罩正在构建中的模板卡片代码块,
425
381
  // 避免 JSON 源码在流式输出过程中暴露给终端用户
426
- const displayText = maskTemplateCardBlocks(data.text, (...args) => runtime.log?.(...args));
427
- if (displayText !== data.text) {
428
- // runtime.log?.(`[wecom][template-card] Partial-reply masked: original=${data.text.length}chars, masked=${displayText.length}chars`);
429
- }
430
- // fire-and-forget:不阻塞 onPartialReply 回调
431
- sendWeComReplyNonBlocking({
432
- wsClient, frame, text: displayText, runtime,
433
- streamId: state.streamId,
434
- }).catch((err) => {
435
- if (err instanceof StreamExpiredError) {
436
- state.streamExpired = true;
437
- runtime.log?.(`[wecom] Stream expired during partial reply, will fallback to proactive send`);
382
+ if (state.accumulatedText && !state.streamExpired) {
383
+ try {
384
+ const displayText = maskTemplateCardBlocks(state.accumulatedText, (...args) => runtime.log?.(...args));
385
+ // if (displayText !== state.accumulatedText) {
386
+ // runtime.log?.(`[wecom][template-card] Mid-frame masked: original=${state.accumulatedText.length}chars, masked=${displayText.length}chars`);
387
+ // }
388
+ await sendWeComReply({ wsClient, frame, text: displayText, runtime, finish: false, streamId: state.streamId });
438
389
  }
439
- // 其他错误静默忽略,不影响主流程(deliver + finishThinkingStream 兜底)
440
- });
390
+ catch (err) {
391
+ if (err instanceof StreamExpiredError) {
392
+ state.streamExpired = true;
393
+ runtime.log?.(`[wecom] Stream expired during intermediate reply, will fallback to proactive send`);
394
+ }
395
+ else {
396
+ throw err;
397
+ }
398
+ }
399
+ }
400
+ },
401
+ onError: (err, info) => {
402
+ runtime.error?.(`[wecom] ${info.kind} reply failed: ${String(err)}`);
441
403
  },
442
404
  },
443
405
  });
@@ -578,24 +540,7 @@ async function processWeComMessageNow(entry) {
578
540
  // await sendThinkingReply({ wsClient, frame, streamId, runtime });
579
541
  // }
580
542
  // Step 7: 构建上下文并路由到核心处理流程(带整体超时保护)
581
- const contextResult = buildMessageContext(frame, account, config, text, mediaList, quoteContent, runtime);
582
- // 处理路由拦截
583
- if (contextResult.rejected) {
584
- runtime.log?.(`[wecom] Routing guard: sending rejection prompt to user`);
585
- try {
586
- await wsClient.sendMessage(chatId, {
587
- msgtype: "markdown",
588
- markdown: { content: contextResult.rejectReason },
589
- });
590
- runtime.log?.(`[wecom] Routing guard prompt delivered`);
591
- }
592
- catch (err) {
593
- runtime.error?.(`[wecom] Routing guard prompt failed: ${String(err)}`);
594
- }
595
- cleanupState();
596
- return;
597
- }
598
- const { ctxPayload, route, storePath, chatId: resolvedChatId, chatType } = contextResult;
543
+ const { ctxPayload, route, storePath, chatId: resolvedChatId, chatType } = buildMessageContext(frame, account, config, text, mediaList, quoteContent, runtime);
599
544
  // runtime.log?.(`[plugin -> openclaw] body=${text}, mediaPaths=${JSON.stringify(mediaList.map(m => m.path))}${quoteContent ? `, quote=${quoteContent}` : ''}`);
600
545
  try {
601
546
  await routeAndDispatchMessage({
@@ -650,7 +595,6 @@ function createSdkLogger(runtime, accountId) {
650
595
  export async function monitorWeComProvider(options) {
651
596
  const { account, config, runtime, abortSignal, setStatus } = options;
652
597
  runtime.log?.(`[${account.accountId}] [${PLUGIN_VERSION}] Initializing WSClient with SDK...`);
653
- runtime.error?.(`[${account.accountId}] [diag] monitor boot marker: build=20260325-event-debug-1`);
654
598
  // 启动消息状态定期清理
655
599
  startMessageStateCleanup();
656
600
  return new Promise((resolve, reject) => {
@@ -824,7 +768,6 @@ export async function monitorWeComProvider(options) {
824
768
  const eventBody = frame.body;
825
769
  const eventType = eventBody.event?.eventtype;
826
770
  runtime.log?.(`[${account.accountId}] Received event callback: eventtype=${eventType ?? ""}, msgid=${eventBody.msgid ?? ""}`);
827
- runtime.error?.(`[${account.accountId}] [diag] event-listener fired: eventtype=${eventType ?? ""}, msgid=${eventBody.msgid ?? ""}`);
828
771
  if (eventType !== "template_card_event") {
829
772
  return;
830
773
  }
@@ -857,7 +800,6 @@ export async function monitorWeComProvider(options) {
857
800
  }
858
801
  });
859
802
  runtime.log?.(`[${account.accountId}] Event listeners attached: message + event(template_card_event)`);
860
- runtime.error?.(`[${account.accountId}] [diag] listeners-ready marker`);
861
803
  // 启动前预热 reqId 缓存,确保完成后再建立连接,避免 getSync 在预热完成前返回 undefined
862
804
  warmupReqIdStore(account.accountId, (...args) => runtime.log?.(...args))
863
805
  .then((count) => {
@@ -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 */
@@ -15,6 +15,7 @@ import { monitorState } from "./state.js";
15
15
  import { registerWecomWebhookTarget, hasActiveTargets } from "./target.js";
16
16
  import { startAgentForStream } from "./monitor.js";
17
17
  import { hasMultiAccounts } from "../accounts.js";
18
+ import { DEFAULT_ACCOUNT_ID } from "../openclaw-compat.js";
18
19
  import { getWeComRuntime } from "../runtime.js";
19
20
  // ============================================================================
20
21
  // 全局状态
@@ -51,7 +52,14 @@ function resolveBotRegistrationPaths(params) {
51
52
  WEBHOOK_PATHS.BOT_ALT,
52
53
  ]);
53
54
  }
54
- return uniquePaths([WEBHOOK_PATHS.BOT_PLUGIN, WEBHOOK_PATHS.BOT, WEBHOOK_PATHS.BOT_ALT]);
55
+ // 单账号模式:同时注册 /default 路径以支持显式指定
56
+ return uniquePaths([
57
+ WEBHOOK_PATHS.BOT_PLUGIN,
58
+ WEBHOOK_PATHS.BOT,
59
+ WEBHOOK_PATHS.BOT_ALT,
60
+ `${WEBHOOK_PATHS.BOT_PLUGIN}/${DEFAULT_ACCOUNT_ID}`,
61
+ `${WEBHOOK_PATHS.BOT_ALT}/${DEFAULT_ACCOUNT_ID}`,
62
+ ]);
55
63
  }
56
64
  // ============================================================================
57
65
  // 公共 API
@@ -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.3",
3
+ "version": "2026.3.31-beta.5",
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
+