@wecom/wecom-openclaw-plugin 2026.3.31-beta.3 → 2026.3.31-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +171 -33
- package/dist/index.js +4 -59
- package/dist/src/agent/handler.js +3 -15
- package/dist/src/agent/webhook.d.ts +3 -2
- package/dist/src/agent/webhook.js +5 -3
- package/dist/src/channel.js +16 -1
- package/dist/src/dynamic-routing.d.ts +2 -12
- package/dist/src/dynamic-routing.js +11 -77
- package/dist/src/group-policy.js +5 -15
- package/dist/src/monitor.js +32 -90
- package/dist/src/types/config.d.ts +0 -9
- package/dist/src/utils.d.ts +3 -1
- package/dist/src/webhook/gateway.js +9 -1
- package/dist/src/webhook/monitor.js +0 -19
- package/package.json +1 -1
- package/skills/wecom-doc-manager/references/api-edit-doc-content.md +1 -1
- package/skills/wecom-meeting-create/SKILL.md +1 -1
- package/skills/wecom-meeting-manage/SKILL.md +5 -5
- package/skills/wecom-schedule/SKILL.md +3 -3
- package/skills/wecom-schedule/references/api-check-availability.md +2 -2
- package/skills/wecom-smartsheet-data/SKILL.md +5 -2
- package/skills/wecom-smartsheet-data/references/api-get-records.md +1 -0
- package/skills/wecom-smartsheet-data/references/cell-value-formats.md +35 -3
- package/dist/src/routing.d.ts +0 -24
- package/dist/src/routing.js +0 -34
|
@@ -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/group-policy.js
CHANGED
|
@@ -76,21 +76,11 @@ function isGroupSenderAllowed(params) {
|
|
|
76
76
|
* @returns 检查结果,包含是否允许继续处理
|
|
77
77
|
*/
|
|
78
78
|
export function checkGroupPolicy(params) {
|
|
79
|
-
const { chatId, senderId, account,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
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,
|
package/dist/src/monitor.js
CHANGED
|
@@ -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,
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
384
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
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
|
|
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 */
|
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 */
|
|
@@ -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
|
-
|
|
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
|
@@ -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
|
+
|