@wecom/wecom-openclaw-plugin 2026.4.10-beta.1 → 2026.4.21-beta.0
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 +2 -8
- package/dist/index.js +18 -1
- package/dist/src/accounts.d.ts +1 -1
- package/dist/src/agent/handler.d.ts +1 -1
- package/dist/src/agent/webhook.d.ts +1 -1
- package/dist/src/channel.js +3 -3
- package/dist/src/const.d.ts +4 -0
- package/dist/src/const.js +4 -0
- package/dist/src/dynamic-agent.d.ts +1 -1
- package/dist/src/dynamic-routing.d.ts +1 -1
- package/dist/src/mcp/index.d.ts +1 -1
- package/dist/src/mcp/index.js +5 -1
- package/dist/src/mcp/interceptors/biz-error.js +11 -6
- package/dist/src/mcp/interceptors/doc-auth-error.d.ts +10 -0
- package/dist/src/mcp/interceptors/doc-auth-error.js +171 -0
- package/dist/src/mcp/interceptors/index.js +2 -0
- package/dist/src/mcp/interceptors/types.d.ts +8 -0
- package/dist/src/mcp/tool.d.ts +13 -1
- package/dist/src/mcp/tool.js +34 -12
- package/dist/src/mcp/transport.d.ts +30 -3
- package/dist/src/mcp/transport.js +108 -54
- package/dist/src/message-parser.d.ts +7 -0
- package/dist/src/message-parser.js +75 -1
- package/dist/src/monitor.js +42 -17
- package/dist/src/openclaw-compat.js +1 -1
- package/dist/src/shared/command-auth.d.ts +1 -1
- package/dist/src/state-manager.d.ts +19 -0
- package/dist/src/state-manager.js +34 -1
- package/dist/src/webhook/command-auth.d.ts +1 -1
- package/dist/src/webhook/helpers.d.ts +1 -1
- package/dist/src/webhook/types.d.ts +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -479,13 +479,7 @@ In addition to the group allowlist, you can restrict which members within a grou
|
|
|
479
479
|
|
|
480
480
|
## ⏰ Cronjob (Scheduled Tasks)
|
|
481
481
|
|
|
482
|
-
The plugin supports scheduled message delivery via OpenClaw's built-in Cron service. Cron jobs run through the **Agent outbound** channel, so
|
|
483
|
-
|
|
484
|
-
> **Important**: The `--agent` (CLI) or `agentId` (jobs.json) refers to the **OpenClaw agent** that processes the cron job. This OpenClaw agent must be bound (via `bindings`) to a WeCom account that has Agent mode configured. If the bound WeCom account is Bot-only (no `agent` sub-config), delivery will fail with:
|
|
485
|
-
> ```
|
|
486
|
-
> WSClient not connected for account <name> and Agent mode is not configured.
|
|
487
|
-
> ```
|
|
488
|
-
> If `--agent` is omitted, the cron job uses the default OpenClaw agent. Make sure its bound WeCom account (or the `defaultAccount`) has Agent mode enabled.
|
|
482
|
+
The plugin supports scheduled message delivery via OpenClaw's built-in Cron service. Cron jobs run through the **Agent outbound** channel, so Agent mode must be configured.
|
|
489
483
|
|
|
490
484
|
### Target Formats
|
|
491
485
|
|
|
@@ -510,6 +504,7 @@ The `delivery.to` field supports the following target formats:
|
|
|
510
504
|
```shell
|
|
511
505
|
openclaw cron add \
|
|
512
506
|
--name "daily-report" \
|
|
507
|
+
--agent main \
|
|
513
508
|
--cron "0 9 * * 1-5" \
|
|
514
509
|
--tz "Asia/Shanghai" \
|
|
515
510
|
--message "Good morning! Here is your daily briefing." \
|
|
@@ -519,7 +514,6 @@ openclaw cron add \
|
|
|
519
514
|
```
|
|
520
515
|
|
|
521
516
|
> **Note**: `--announce` enables delivery mode (broadcasts the AI response to the target chat). Use `--no-deliver` to keep output internal. The deprecated `--deliver` flag is an alias for `--announce`.
|
|
522
|
-
> You can specify `--agent <agentId>` to select a specific OpenClaw agent (e.g. `--agent main`). The agent must be bound to a WeCom account with Agent mode configured.
|
|
523
517
|
|
|
524
518
|
Common CLI commands:
|
|
525
519
|
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { emptyPluginConfigSchema } from "./src/openclaw-compat.js";
|
|
2
2
|
import { wecomPlugin } from "./src/channel.js";
|
|
3
3
|
import { createWeComMcpTool } from "./src/mcp/index.js";
|
|
4
|
+
import { getSessionChatInfo } from "./src/state-manager.js";
|
|
4
5
|
import { setWeComRuntime } from "./src/runtime.js";
|
|
5
6
|
import { CHANNEL_ID, WEBHOOK_PATHS } from "./src/const.js";
|
|
6
7
|
import { createWecomAgentWebhookHandler } from "./src/agent/webhook.js";
|
|
@@ -14,7 +15,23 @@ const plugin = {
|
|
|
14
15
|
setWeComRuntime(api.runtime);
|
|
15
16
|
api.registerChannel({ plugin: wecomPlugin });
|
|
16
17
|
// 注册 wecom_mcp:通过 HTTP 直接调用企业微信 MCP Server
|
|
17
|
-
api.registerTool(
|
|
18
|
+
api.registerTool((ctx) => {
|
|
19
|
+
const trustedRequesterUserId = ctx.messageChannel === CHANNEL_ID ? ctx.requesterSenderId?.trim() ?? undefined : undefined;
|
|
20
|
+
// 按 sessionKey 精确取回「原始大小写」的 chatId / chatType。
|
|
21
|
+
// 数据由 monitor.ts 在消息入站时通过 setSessionChatInfo 写入,
|
|
22
|
+
// 避免 parseSessionKeyChat 反解导致 chatId 被 OpenClaw core 小写化
|
|
23
|
+
// (小写后的 chatId 调用企业微信 aibot_send_biz_msg 会报 invalid chatid)。
|
|
24
|
+
const sessionChat = getSessionChatInfo(ctx.sessionKey);
|
|
25
|
+
console.log(`[mcp] registerTool ctx: sessionKey="${ctx.sessionKey}", messageChannel="${ctx.messageChannel}", ` +
|
|
26
|
+
`requesterSenderId="${ctx.requesterSenderId}", agentAccountId="${ctx.agentAccountId}", ` +
|
|
27
|
+
`sessionChat=${JSON.stringify(sessionChat)}`);
|
|
28
|
+
return createWeComMcpTool({
|
|
29
|
+
requesterUserId: trustedRequesterUserId,
|
|
30
|
+
accountId: ctx.agentAccountId,
|
|
31
|
+
chatId: sessionChat?.chatId,
|
|
32
|
+
chatType: sessionChat?.chatType,
|
|
33
|
+
});
|
|
34
|
+
}, { name: "wecom_mcp" });
|
|
18
35
|
const agentWebhookHandler = createWecomAgentWebhookHandler(api.runtime);
|
|
19
36
|
// 注册 Agent 模式 HTTP 路由(prefix match 覆盖带 accountId 的子路径)
|
|
20
37
|
api.registerHttpRoute({
|
package/dist/src/accounts.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 处理 XML 格式回调
|
|
4
4
|
*/
|
|
5
5
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
6
|
-
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
6
|
+
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
7
7
|
import type { ResolvedAgentAccount } from "../types/index.js";
|
|
8
8
|
import type { WecomAgentInboundMessage } from "../types/index.js";
|
|
9
9
|
/**
|
|
@@ -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, PluginRuntime } from "openclaw/plugin-sdk";
|
|
10
|
+
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
11
11
|
import type { ResolvedAgentAccount } from "../types/index.js";
|
|
12
12
|
export type AgentWebhookTarget = {
|
|
13
13
|
agent: ResolvedAgentAccount;
|
package/dist/src/channel.js
CHANGED
|
@@ -16,7 +16,7 @@ import { startWebhookGateway, stopWebhookGateway } from "./webhook/index.js";
|
|
|
16
16
|
* 优先 Bot WebSocket,不可用时自动回退到 Agent HTTP API
|
|
17
17
|
*/
|
|
18
18
|
async function sendWeComMessage({ to, content, accountId, cfg, }) {
|
|
19
|
-
const resolvedAccountId = accountId ??
|
|
19
|
+
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
20
20
|
// 从 to 中提取目标(格式是 "${CHANNEL_ID}:xxx" 或直接是目标字符串)
|
|
21
21
|
const channelPrefix = new RegExp(`^${CHANNEL_ID}:`, "i");
|
|
22
22
|
const chatId = to.replace(channelPrefix, "");
|
|
@@ -264,7 +264,7 @@ export const wecomPlugin = {
|
|
|
264
264
|
return sendWeComMessage({ to, content: text, accountId: accountId ?? undefined, cfg });
|
|
265
265
|
},
|
|
266
266
|
sendMedia: async ({ to, text, mediaUrl, mediaLocalRoots, accountId, cfg }) => {
|
|
267
|
-
const resolvedAccountId = accountId ??
|
|
267
|
+
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
268
268
|
const channelPrefix = new RegExp(`^${CHANNEL_ID}:`, "i");
|
|
269
269
|
const chatId = to.replace(channelPrefix, "");
|
|
270
270
|
// 如果没有 mediaUrl,fallback 为纯文本
|
|
@@ -508,7 +508,7 @@ export const wecomPlugin = {
|
|
|
508
508
|
});
|
|
509
509
|
},
|
|
510
510
|
logoutAccount: async ({ cfg, accountId }) => {
|
|
511
|
-
const resolvedAccountId = accountId ??
|
|
511
|
+
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
512
512
|
const isMulti = hasMultiAccounts(cfg);
|
|
513
513
|
let nextCfg = { ...cfg };
|
|
514
514
|
let cleared = false;
|
package/dist/src/const.d.ts
CHANGED
|
@@ -48,6 +48,10 @@ export declare const MEDIA_IMAGE_PLACEHOLDER = "<media:image>";
|
|
|
48
48
|
export declare const MEDIA_DOCUMENT_PLACEHOLDER = "<media:document>";
|
|
49
49
|
/** 获取 MCP 配置的 WebSocket 命令 */
|
|
50
50
|
export declare const MCP_GET_CONFIG_CMD = "aibot_get_mcp_config";
|
|
51
|
+
/** 发送业务消息的 WebSocket 命令(如文档授权卡片) */
|
|
52
|
+
export declare const AIBOT_SEND_BIZ_MSG_CMD = "aibot_send_biz_msg";
|
|
53
|
+
/** 业务消息超时时间(毫秒) */
|
|
54
|
+
export declare const BIZ_MSG_SEND_TIMEOUT_MS = 10000;
|
|
51
55
|
/** MCP 配置拉取超时时间(毫秒) */
|
|
52
56
|
export declare const MCP_CONFIG_FETCH_TIMEOUT_MS = 15000;
|
|
53
57
|
/** 默认媒体大小上限(MB) */
|
package/dist/src/const.js
CHANGED
|
@@ -64,6 +64,10 @@ export const MEDIA_DOCUMENT_PLACEHOLDER = "<media:document>";
|
|
|
64
64
|
// ============================================================================
|
|
65
65
|
/** 获取 MCP 配置的 WebSocket 命令 */
|
|
66
66
|
export const MCP_GET_CONFIG_CMD = "aibot_get_mcp_config";
|
|
67
|
+
/** 发送业务消息的 WebSocket 命令(如文档授权卡片) */
|
|
68
|
+
export const AIBOT_SEND_BIZ_MSG_CMD = "aibot_send_biz_msg";
|
|
69
|
+
/** 业务消息超时时间(毫秒) */
|
|
70
|
+
export const BIZ_MSG_SEND_TIMEOUT_MS = 10_000;
|
|
67
71
|
/** MCP 配置拉取超时时间(毫秒) */
|
|
68
72
|
export const MCP_CONFIG_FETCH_TIMEOUT_MS = 15_000;
|
|
69
73
|
// ============================================================================
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 为每个用户/群组自动生成独立的 Agent ID,实现会话隔离。
|
|
5
5
|
* 参考: openclaw-plugin-wecom/dynamic-agent.js
|
|
6
6
|
*/
|
|
7
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
7
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
8
8
|
export interface DynamicAgentConfig {
|
|
9
9
|
enabled: boolean;
|
|
10
10
|
dmCreateAgent: boolean;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 提供统一的路由注入接口。
|
|
4
4
|
*/
|
|
5
5
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
6
|
-
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
6
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
7
7
|
/**
|
|
8
8
|
* 路由对象(从 core.channel.routing.resolveAgentRoute 返回)
|
|
9
9
|
*/
|
package/dist/src/mcp/index.d.ts
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* MCP 模块统一导出
|
|
3
3
|
*/
|
|
4
4
|
export { createWeComMcpTool } from "./tool.js";
|
|
5
|
-
export { sendJsonRpc, clearCategoryCache, McpRpcError, McpHttpError, type McpToolInfo } from "./transport.js";
|
|
5
|
+
export { sendJsonRpc, clearCategoryCache, clearAccountCache, resolveCurrentAccountId, McpRpcError, McpHttpError, type McpToolInfo } from "./transport.js";
|
|
6
6
|
export { cleanSchemaForGemini } from "./schema.js";
|
package/dist/src/mcp/index.js
CHANGED
|
@@ -2,5 +2,9 @@
|
|
|
2
2
|
* MCP 模块统一导出
|
|
3
3
|
*/
|
|
4
4
|
export { createWeComMcpTool } from "./tool.js";
|
|
5
|
-
export { sendJsonRpc, clearCategoryCache, McpRpcError, McpHttpError } from "./transport.js";
|
|
5
|
+
export { sendJsonRpc, clearCategoryCache, clearAccountCache, resolveCurrentAccountId, McpRpcError, McpHttpError } from "./transport.js";
|
|
6
6
|
export { cleanSchemaForGemini } from "./schema.js";
|
|
7
|
+
// 注意:parseSessionKeyChat 已废弃不再导出。
|
|
8
|
+
// OpenClaw core 构建 sessionKey 时会把 chatId 小写化,
|
|
9
|
+
// 企业微信接口(如 aibot_send_biz_msg)是大小写敏感的,反解结果不可用。
|
|
10
|
+
// chatId 请通过 state-manager 的 getSessionChatInfo(sessionKey) 获取。
|
|
@@ -17,9 +17,9 @@ import { clearCategoryCache } from "../transport.js";
|
|
|
17
17
|
* 这些错误码出现在 MCP 工具调用返回的 content 文本中(业务层面),
|
|
18
18
|
* 与 JSON-RPC 层面的错误码不同,需要在此处额外检测。
|
|
19
19
|
*
|
|
20
|
-
* -
|
|
20
|
+
* - 850001/ 851014: 机器人授权过期/ 重置,需清理缓存以便下次重新拉取配置
|
|
21
21
|
*/
|
|
22
|
-
const BIZ_CACHE_CLEAR_ERROR_CODES = new Set([
|
|
22
|
+
const BIZ_CACHE_CLEAR_ERROR_CODES = new Set([850001, 851014]);
|
|
23
23
|
// ============================================================================
|
|
24
24
|
// 拦截器实现
|
|
25
25
|
// ============================================================================
|
|
@@ -29,7 +29,7 @@ export const bizErrorInterceptor = {
|
|
|
29
29
|
match: () => true,
|
|
30
30
|
/** 检查返回结果中的业务错误码,必要时清理缓存 */
|
|
31
31
|
afterCall(ctx, result) {
|
|
32
|
-
checkBizErrorAndClearCache(result, ctx
|
|
32
|
+
checkBizErrorAndClearCache(result, ctx);
|
|
33
33
|
// 不修改 result,透传给下一个拦截器
|
|
34
34
|
return result;
|
|
35
35
|
},
|
|
@@ -40,7 +40,7 @@ export const bizErrorInterceptor = {
|
|
|
40
40
|
/**
|
|
41
41
|
* 检查 tools/call 的返回结果中是否包含需要清理缓存的业务错误码
|
|
42
42
|
*/
|
|
43
|
-
function checkBizErrorAndClearCache(result,
|
|
43
|
+
function checkBizErrorAndClearCache(result, ctx) {
|
|
44
44
|
if (!result || typeof result !== "object")
|
|
45
45
|
return;
|
|
46
46
|
const { content } = result;
|
|
@@ -52,8 +52,13 @@ function checkBizErrorAndClearCache(result, category) {
|
|
|
52
52
|
try {
|
|
53
53
|
const parsed = JSON.parse(item.text);
|
|
54
54
|
if (typeof parsed.errcode === "number" && BIZ_CACHE_CLEAR_ERROR_CODES.has(parsed.errcode)) {
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
const accountId = ctx.accountId;
|
|
56
|
+
if (!accountId) {
|
|
57
|
+
console.warn(`[mcp] 检测到业务错误码 ${parsed.errcode} (category="${ctx.category}"),但 ctx.accountId 为空,跳过缓存清理`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
console.log(`[mcp] 检测到业务错误码 ${parsed.errcode} (accountId="${accountId}", category="${ctx.category}"),清理缓存`);
|
|
61
|
+
clearCategoryCache(accountId, ctx.category);
|
|
57
62
|
return;
|
|
58
63
|
}
|
|
59
64
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文档授权错误拦截器
|
|
3
|
+
*
|
|
4
|
+
* 当 category=doc 的 MCP 调用返回 errcode=851013/851014/851008(文档授权错误)时:
|
|
5
|
+
* 1. 通过 aibot_send_biz_msg 命令向用户发送授权引导卡片
|
|
6
|
+
* 2. 拦截 help_message 内容,不将其传递给 LLM
|
|
7
|
+
* 3. 返回简化响应,告知 LLM 授权卡片已发送、无需再做处理
|
|
8
|
+
*/
|
|
9
|
+
import type { CallInterceptor } from "./types.js";
|
|
10
|
+
export declare const docAuthErrorInterceptor: CallInterceptor;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文档授权错误拦截器
|
|
3
|
+
*
|
|
4
|
+
* 当 category=doc 的 MCP 调用返回 errcode=851013/851014/851008(文档授权错误)时:
|
|
5
|
+
* 1. 通过 aibot_send_biz_msg 命令向用户发送授权引导卡片
|
|
6
|
+
* 2. 拦截 help_message 内容,不将其传递给 LLM
|
|
7
|
+
* 3. 返回简化响应,告知 LLM 授权卡片已发送、无需再做处理
|
|
8
|
+
*/
|
|
9
|
+
import { generateReqId } from "@wecom/aibot-node-sdk";
|
|
10
|
+
import { getWeComWebSocket } from "../../state-manager.js";
|
|
11
|
+
import { resolveCurrentAccountId } from "../transport.js";
|
|
12
|
+
import { withTimeout } from "../../timeout.js";
|
|
13
|
+
import { AIBOT_SEND_BIZ_MSG_CMD, BIZ_MSG_SEND_TIMEOUT_MS } from "../../const.js";
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// 常量
|
|
16
|
+
// ============================================================================
|
|
17
|
+
/** 需要拦截的文档授权错误码集合 */
|
|
18
|
+
/* https://doc.weixin.qq.com/sheet/e3_AFcARgbdAFwCNU0pwubawRtGzcd6z?scode=AJEAIQdfAAoEpqtRfRAMEA4QYkACk&tab=BB08J2 */
|
|
19
|
+
const DOC_AUTH_ERROR_CODES = new Set([851013, 851014, 851008]);
|
|
20
|
+
/**
|
|
21
|
+
* 业务消息类型枚举(对应 AiBotBizMsgType)
|
|
22
|
+
* - 1: 文档权限
|
|
23
|
+
*/
|
|
24
|
+
const AIBOT_BIZ_MSG_TYPE_DOC_READ_AUTH = 1;
|
|
25
|
+
/**
|
|
26
|
+
* 聊天类型枚举(对应 AiBotBizMsgChatType)
|
|
27
|
+
* - 1: 单聊
|
|
28
|
+
* - 2: 群聊
|
|
29
|
+
*/
|
|
30
|
+
const AIBOT_BIZ_MSG_CHAT_TYPE_SINGLE = 1;
|
|
31
|
+
const AIBOT_BIZ_MSG_CHAT_TYPE_GROUP = 2;
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// 拦截器实现
|
|
34
|
+
// ============================================================================
|
|
35
|
+
export const docAuthErrorInterceptor = {
|
|
36
|
+
name: "doc-auth-error",
|
|
37
|
+
/** 仅对 doc 品类生效 */
|
|
38
|
+
match: (ctx) => ctx.category === "doc",
|
|
39
|
+
/** 检测文档授权错误,发送授权引导卡片并拦截原始响应 */
|
|
40
|
+
async afterCall(ctx, result) {
|
|
41
|
+
return interceptDocAuthError(ctx, result);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// 内部实现
|
|
46
|
+
// ============================================================================
|
|
47
|
+
/**
|
|
48
|
+
* 从 MCP 返回结果中解析业务 JSON
|
|
49
|
+
*/
|
|
50
|
+
function parseBizData(result) {
|
|
51
|
+
if (!result || typeof result !== "object")
|
|
52
|
+
return null;
|
|
53
|
+
const { content } = result;
|
|
54
|
+
if (!Array.isArray(content))
|
|
55
|
+
return null;
|
|
56
|
+
const textItem = content.find((c) => c.type === "text" && typeof c.text === "string");
|
|
57
|
+
if (!textItem)
|
|
58
|
+
return null;
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(textItem.text);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 通过 WSClient 发送 aibot_send_biz_msg 命令
|
|
68
|
+
*
|
|
69
|
+
* 按照 WwOpenWsSendAiBotBizMsgReq proto 定义构造请求体:
|
|
70
|
+
* - chat_id: 会话标识
|
|
71
|
+
* - userid: 发送者 userid
|
|
72
|
+
* - biz_type: 业务消息类型(AiBotBizMsgType)
|
|
73
|
+
* - chat_type: 聊天类型(AiBotBizMsgChatType,1=单聊,2=群聊)
|
|
74
|
+
*/
|
|
75
|
+
async function sendBizMsg(params) {
|
|
76
|
+
const accountId = params.accountId || resolveCurrentAccountId();
|
|
77
|
+
const wsClient = getWeComWebSocket(accountId);
|
|
78
|
+
if (!wsClient) {
|
|
79
|
+
console.warn(`[mcp] doc-auth-error: WSClient 未连接 (accountId="${accountId}"),无法发送授权卡片`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const reqId = generateReqId("biz_msg");
|
|
83
|
+
// 构造 WwOpenWsSendAiBotBizMsgReqBody
|
|
84
|
+
const body = {
|
|
85
|
+
biz_type: AIBOT_BIZ_MSG_TYPE_DOC_READ_AUTH,
|
|
86
|
+
};
|
|
87
|
+
if (params.chatId) {
|
|
88
|
+
body.chat_id = params.chatId;
|
|
89
|
+
}
|
|
90
|
+
if (params.userid) {
|
|
91
|
+
body.userid = params.userid;
|
|
92
|
+
}
|
|
93
|
+
if (params.chatType) {
|
|
94
|
+
body.chat_type = params.chatType === "group"
|
|
95
|
+
? AIBOT_BIZ_MSG_CHAT_TYPE_GROUP
|
|
96
|
+
: AIBOT_BIZ_MSG_CHAT_TYPE_SINGLE;
|
|
97
|
+
}
|
|
98
|
+
console.log(`[mcp] doc-auth-error: 发送 aibot_send_biz_msg, body=${JSON.stringify(body)}`);
|
|
99
|
+
try {
|
|
100
|
+
await withTimeout(wsClient.reply({ headers: { req_id: reqId } }, body, AIBOT_SEND_BIZ_MSG_CMD), BIZ_MSG_SEND_TIMEOUT_MS, `aibot_send_biz_msg timed out after ${BIZ_MSG_SEND_TIMEOUT_MS}ms`);
|
|
101
|
+
console.log(`[mcp] doc-auth-error: 授权卡片发送成功 (chatId="${params.chatId}", userid="${params.userid}", chatType="${params.chatType}")`);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
console.error(`[mcp] doc-auth-error: 授权卡片发送失败: ${err instanceof Error ? err.message : JSON.stringify(err)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 拦截文档授权错误
|
|
109
|
+
*
|
|
110
|
+
* 检测 errcode=851013/851014/851008 时:
|
|
111
|
+
* 1. 通过 aibot_send_biz_msg 发送授权引导卡片(携带 chat_id、userid、chat_type)
|
|
112
|
+
* 2. 返回简化响应,告知 LLM 授权卡片已直接发送给用户
|
|
113
|
+
*
|
|
114
|
+
* 数据来源链路(chatId 全程保留原始大小写,单一事实来源):
|
|
115
|
+
*
|
|
116
|
+
* aibot_msg_callback.body.chatid (原始大小写)
|
|
117
|
+
* ↓
|
|
118
|
+
* monitor.ts:processWeComMessageNow
|
|
119
|
+
* → setSessionChatInfo(route.sessionKey, { chatId, chatType }) ← 以 sessionKey 为键
|
|
120
|
+
* ↓
|
|
121
|
+
* OpenClaw core 调用工具工厂,传入 ctx.sessionKey
|
|
122
|
+
* ↓
|
|
123
|
+
* index.ts:registerTool
|
|
124
|
+
* → getSessionChatInfo(ctx.sessionKey) 精确取回 { chatId, chatType }
|
|
125
|
+
* → createWeComMcpTool({ chatId, chatType })
|
|
126
|
+
* ↓
|
|
127
|
+
* tool.ts:handleCall 将其注入 CallContext
|
|
128
|
+
* ↓
|
|
129
|
+
* 本拦截器直接使用 ctx.chatId / ctx.chatType (无需反解 sessionKey、无需兜底)
|
|
130
|
+
*
|
|
131
|
+
* ⚠️ 切勿使用 parseSessionKeyChat(ctx.sessionKey) 反向解析 chatId:
|
|
132
|
+
* OpenClaw core 构建 sessionKey 时会将 peer.id 强制小写化,
|
|
133
|
+
* 企业微信 aibot_send_biz_msg 的 chat_id 是大小写敏感的,
|
|
134
|
+
* 小写后会收到 errcode=93006 (invalid chatid)。
|
|
135
|
+
*/
|
|
136
|
+
async function interceptDocAuthError(ctx, result) {
|
|
137
|
+
const bizData = parseBizData(result);
|
|
138
|
+
if (!bizData)
|
|
139
|
+
return result;
|
|
140
|
+
const errcode = bizData.errcode;
|
|
141
|
+
if (typeof errcode !== "number" || !DOC_AUTH_ERROR_CODES.has(errcode)) {
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
console.log(`[mcp] doc-auth-error: 检测到文档授权错误 errcode=${errcode}, ` +
|
|
145
|
+
`method=${ctx.method}, category=${ctx.category}`);
|
|
146
|
+
// chatId / chatType 已由 monitor.ts → state-manager → index.ts 透传到 ctx,
|
|
147
|
+
// 这里直接使用即可,原始大小写保留不变。
|
|
148
|
+
const { requesterUserId: userid, chatId, chatType, accountId } = ctx;
|
|
149
|
+
if (!chatId || !chatType) {
|
|
150
|
+
console.warn(`[mcp] doc-auth-error: ctx 缺少 chatId/chatType(可能会话信息未记录),` +
|
|
151
|
+
`userid="${userid}", chatId="${chatId}", chatType="${chatType}",跳过发送授权卡片`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
console.log(`[mcp] doc-auth-error: 发送授权卡片参数: userid="${userid}", chatId="${chatId}", chatType="${chatType}"`);
|
|
155
|
+
await sendBizMsg({ chatId, userid, chatType, accountId });
|
|
156
|
+
}
|
|
157
|
+
// 构造简化响应:不将 help_message 传递给 LLM,
|
|
158
|
+
// 告知 LLM 授权提示已直接发送给用户,无需再做任何操作
|
|
159
|
+
const simplifiedResult = {
|
|
160
|
+
errcode,
|
|
161
|
+
errmsg: bizData.errmsg ?? "authorization error",
|
|
162
|
+
_biz_msg_sent: true,
|
|
163
|
+
_user_hint: "文档授权提示卡片已直接发送给用户,无需再向用户转述任何授权相关的信息。请告知用户:已发送授权引导,请按照提示完成授权后重试。",
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
content: [{
|
|
167
|
+
type: "text",
|
|
168
|
+
text: JSON.stringify(simplifiedResult),
|
|
169
|
+
}],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* tool.ts 的 handleCall 无需任何改动。
|
|
10
10
|
*/
|
|
11
11
|
import { bizErrorInterceptor } from "./biz-error.js";
|
|
12
|
+
import { docAuthErrorInterceptor } from "./doc-auth-error.js";
|
|
12
13
|
import { mediaInterceptor } from "./msg-media.js";
|
|
13
14
|
import { smartpageCreateInterceptor } from "./smartpage-create.js";
|
|
14
15
|
import { smartpageExportInterceptor } from "./smartpage-export.js";
|
|
@@ -17,6 +18,7 @@ import { smartpageExportInterceptor } from "./smartpage-export.js";
|
|
|
17
18
|
// ============================================================================
|
|
18
19
|
const interceptors = [
|
|
19
20
|
bizErrorInterceptor, // 业务错误码检查(所有 call 生效)
|
|
21
|
+
docAuthErrorInterceptor, // 文档授权错误拦截(category=doc, errcode=851013/851014/851008)
|
|
20
22
|
mediaInterceptor, // get_msg_media base64 拦截
|
|
21
23
|
smartpageCreateInterceptor, // smartpage_create 本地文件读取
|
|
22
24
|
smartpageExportInterceptor, // smartpage_get_export_result content → 本地文件
|
|
@@ -13,6 +13,14 @@ export interface CallContext {
|
|
|
13
13
|
method: string;
|
|
14
14
|
/** 调用 MCP 方法的参数 */
|
|
15
15
|
args: Record<string, unknown>;
|
|
16
|
+
/** 当前会话对应的账户 ID(由工具工厂透传,用于精确清理缓存等场景) */
|
|
17
|
+
accountId?: string;
|
|
18
|
+
/** 当前会话的 chatId(群组 ID 或用户 ID),用于 aibot_send_biz_msg 等命令 */
|
|
19
|
+
chatId?: string;
|
|
20
|
+
/** 当前会话的聊天类型:single(单聊)或 group(群聊) */
|
|
21
|
+
chatType?: "single" | "group";
|
|
22
|
+
/** 当前会话可信的企业微信 userid */
|
|
23
|
+
requesterUserId?: string;
|
|
16
24
|
}
|
|
17
25
|
/**
|
|
18
26
|
* beforeCall 返回的选项
|
package/dist/src/mcp/tool.d.ts
CHANGED
|
@@ -12,10 +12,20 @@
|
|
|
12
12
|
* wecom_mcp list contact
|
|
13
13
|
* wecom_mcp call contact getContact '{}'
|
|
14
14
|
*/
|
|
15
|
+
interface CreateWeComMcpToolOptions {
|
|
16
|
+
/** 当前会话可信的企业微信 userid */
|
|
17
|
+
requesterUserId?: string;
|
|
18
|
+
/** 当前会话对应的账户 ID(来自 ctx.agentAccountId) */
|
|
19
|
+
accountId?: string;
|
|
20
|
+
/** 当前会话的 chatId(群组 ID 或用户 ID),用于 aibot_send_biz_msg 等命令 */
|
|
21
|
+
chatId?: string;
|
|
22
|
+
/** 当前会话的聊天类型:single(单聊)或 group(群聊) */
|
|
23
|
+
chatType?: "single" | "group";
|
|
24
|
+
}
|
|
15
25
|
/**
|
|
16
26
|
* 创建 wecom_mcp Agent Tool 定义
|
|
17
27
|
*/
|
|
18
|
-
export declare function createWeComMcpTool(): {
|
|
28
|
+
export declare function createWeComMcpTool(options?: CreateWeComMcpToolOptions): {
|
|
19
29
|
name: string;
|
|
20
30
|
label: string;
|
|
21
31
|
description: string;
|
|
@@ -47,5 +57,7 @@ export declare function createWeComMcpTool(): {
|
|
|
47
57
|
type: "text";
|
|
48
58
|
text: string;
|
|
49
59
|
}[];
|
|
60
|
+
details: unknown;
|
|
50
61
|
}>;
|
|
51
62
|
};
|
|
63
|
+
export {};
|
package/dist/src/mcp/tool.js
CHANGED
|
@@ -21,7 +21,12 @@ import { resolveBeforeCall, runAfterCall } from "./interceptors/index.js";
|
|
|
21
21
|
/** 构造统一的文本响应结构 */
|
|
22
22
|
const textResult = (data) => ({
|
|
23
23
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
24
|
+
details: data,
|
|
24
25
|
});
|
|
26
|
+
const normalizeOptionalString = (value) => {
|
|
27
|
+
const trimmed = value?.trim();
|
|
28
|
+
return trimmed ? trimmed : undefined;
|
|
29
|
+
};
|
|
25
30
|
/** 构造错误响应 */
|
|
26
31
|
const errorResult = (err) => {
|
|
27
32
|
// 适配企业微信 API 返回的 { errcode, errmsg } 结构
|
|
@@ -35,14 +40,17 @@ const errorResult = (err) => {
|
|
|
35
40
|
// ============================================================================
|
|
36
41
|
// list 操作:列出某品类的所有 MCP 工具
|
|
37
42
|
// ============================================================================
|
|
38
|
-
const handleList = async (
|
|
39
|
-
const result = await sendJsonRpc(category, "tools/list"
|
|
43
|
+
const handleList = async (ctx) => {
|
|
44
|
+
const result = await sendJsonRpc(ctx.category, "tools/list", undefined, {
|
|
45
|
+
requesterUserId: ctx.requesterUserId,
|
|
46
|
+
accountId: ctx.accountId,
|
|
47
|
+
});
|
|
40
48
|
const tools = result?.tools ?? [];
|
|
41
49
|
if (tools.length === 0) {
|
|
42
|
-
return { message: `品类 "${category}" 下暂无可用工具`, tools: [] };
|
|
50
|
+
return { message: `品类 "${ctx.category}" 下暂无可用工具`, tools: [] };
|
|
43
51
|
}
|
|
44
52
|
return {
|
|
45
|
-
category,
|
|
53
|
+
category: ctx.category,
|
|
46
54
|
count: tools.length,
|
|
47
55
|
tools: tools.map((t) => ({
|
|
48
56
|
name: t.name,
|
|
@@ -56,13 +64,14 @@ const handleList = async (category) => {
|
|
|
56
64
|
// ============================================================================
|
|
57
65
|
// call 操作:调用某品类的某个 MCP 工具
|
|
58
66
|
// ============================================================================
|
|
59
|
-
const handleCall = async (
|
|
60
|
-
const
|
|
67
|
+
const handleCall = async (ctx) => {
|
|
68
|
+
const { category, method, args, requesterUserId, accountId } = ctx;
|
|
61
69
|
const callStart = performance.now();
|
|
62
70
|
console.log(`[mcp] handleCall ${category}/${method} 入参: ${JSON.stringify(args)}`);
|
|
63
71
|
// 1. 收集拦截器的 beforeCall 配置(如超时时间、替换 args)
|
|
64
72
|
const { options, args: resolvedArgs } = await resolveBeforeCall(ctx);
|
|
65
73
|
const finalArgs = resolvedArgs ?? args;
|
|
74
|
+
const requestOptions = { ...options, ...(requesterUserId ? { requesterUserId } : {}), ...(accountId ? { accountId } : {}) };
|
|
66
75
|
if (resolvedArgs) {
|
|
67
76
|
console.log(`[mcp] handleCall ${category}/${method} 拦截器替换 args: ${JSON.stringify(resolvedArgs).slice(0, 500)}` +
|
|
68
77
|
(JSON.stringify(resolvedArgs).length > 500 ? "...(truncated)" : ""));
|
|
@@ -74,7 +83,7 @@ const handleCall = async (category, method, args) => {
|
|
|
74
83
|
const result = await sendJsonRpc(category, "tools/call", {
|
|
75
84
|
name: method,
|
|
76
85
|
arguments: finalArgs,
|
|
77
|
-
},
|
|
86
|
+
}, requestOptions);
|
|
78
87
|
const rpcDone = performance.now();
|
|
79
88
|
const rpcMs = (rpcDone - callStart).toFixed(1);
|
|
80
89
|
const resultStr = JSON.stringify(result);
|
|
@@ -122,7 +131,11 @@ const parseArgs = (args) => {
|
|
|
122
131
|
/**
|
|
123
132
|
* 创建 wecom_mcp Agent Tool 定义
|
|
124
133
|
*/
|
|
125
|
-
export function createWeComMcpTool() {
|
|
134
|
+
export function createWeComMcpTool(options = {}) {
|
|
135
|
+
const requesterUserId = normalizeOptionalString(options.requesterUserId);
|
|
136
|
+
const accountId = normalizeOptionalString(options.accountId);
|
|
137
|
+
const chatId = normalizeOptionalString(options.chatId);
|
|
138
|
+
const chatType = options.chatType;
|
|
126
139
|
return {
|
|
127
140
|
name: "wecom_mcp",
|
|
128
141
|
label: "企业微信 MCP 工具",
|
|
@@ -170,17 +183,26 @@ export function createWeComMcpTool() {
|
|
|
170
183
|
(p.args ? `, args=${typeof p.args === "string" ? p.args : JSON.stringify(p.args)}` : ""));
|
|
171
184
|
try {
|
|
172
185
|
let result;
|
|
186
|
+
const ctx = {
|
|
187
|
+
category: p.category,
|
|
188
|
+
method: p.method ?? "",
|
|
189
|
+
args: {},
|
|
190
|
+
requesterUserId,
|
|
191
|
+
accountId,
|
|
192
|
+
chatId,
|
|
193
|
+
chatType,
|
|
194
|
+
};
|
|
173
195
|
switch (p.action) {
|
|
174
196
|
case "list":
|
|
175
|
-
result = textResult(await handleList(
|
|
197
|
+
result = textResult(await handleList(ctx));
|
|
176
198
|
break;
|
|
177
199
|
case "call": {
|
|
178
200
|
if (!p.method) {
|
|
179
201
|
result = textResult({ error: "action 为 call 时必须提供 method 参数" });
|
|
180
202
|
break;
|
|
181
203
|
}
|
|
182
|
-
|
|
183
|
-
result = textResult(await handleCall(
|
|
204
|
+
ctx.args = parseArgs(p.args);
|
|
205
|
+
result = textResult(await handleCall(ctx));
|
|
184
206
|
break;
|
|
185
207
|
}
|
|
186
208
|
default:
|
|
@@ -194,7 +216,7 @@ export function createWeComMcpTool() {
|
|
|
194
216
|
catch (err) {
|
|
195
217
|
console.error(`[mcp] execute: action=${p.action}, category=${p.category}` +
|
|
196
218
|
(p.method ? `, method=${p.method}` : "") +
|
|
197
|
-
` → 异常: ${err instanceof Error ? err.message :
|
|
219
|
+
` → 异常: ${err instanceof Error ? err.message : JSON.stringify(err)}`);
|
|
198
220
|
return errorResult(err);
|
|
199
221
|
}
|
|
200
222
|
},
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
/** 媒体下载请求超时时间(毫秒),base64 编码的媒体文件最大可达 ~27MB */
|
|
13
13
|
export declare const MEDIA_DOWNLOAD_TIMEOUT_MS = 120000;
|
|
14
|
+
/** 请求 MCP Server 时透传可信企业微信 userid 的 header 名 */
|
|
15
|
+
export declare const WECOM_USERID_HEADER = "x-openclaw-wecom-userid";
|
|
14
16
|
/**
|
|
15
17
|
* MCP JSON-RPC 错误
|
|
16
18
|
*
|
|
@@ -33,13 +35,31 @@ export declare class McpHttpError extends Error {
|
|
|
33
35
|
constructor(statusCode: number, message: string);
|
|
34
36
|
}
|
|
35
37
|
/**
|
|
36
|
-
*
|
|
38
|
+
* 根据配置解析当前用于 MCP 的账户 ID
|
|
39
|
+
*
|
|
40
|
+
* 通过 PluginRuntime 读取全局配置,按以下优先级选择账户:
|
|
41
|
+
* 1. 默认账户(如果配置了 botId + secret)
|
|
42
|
+
* 2. 第一个配置了 botId + secret 的长连接账户
|
|
43
|
+
* 3. 无可用长连接账户时回退到默认账户(会导致后续 WSClient 获取失败)
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveCurrentAccountId(): string;
|
|
46
|
+
/**
|
|
47
|
+
* 清理指定账户+品类的所有 MCP 缓存(配置、会话、无状态标记)
|
|
37
48
|
*
|
|
38
49
|
* 当 MCP Server 返回特定错误码时调用,确保下次请求重新拉取配置并重建会话。
|
|
39
50
|
*
|
|
51
|
+
* @param accountId - 账户 ID
|
|
40
52
|
* @param category - MCP 品类名称
|
|
41
53
|
*/
|
|
42
|
-
export declare function clearCategoryCache(category: string): void;
|
|
54
|
+
export declare function clearCategoryCache(accountId: string, category: string): void;
|
|
55
|
+
/**
|
|
56
|
+
* 清理指定账户下所有品类的 MCP 缓存
|
|
57
|
+
*
|
|
58
|
+
* 适用于账户掉线、凭据变更等需要整体清理的场景。
|
|
59
|
+
*
|
|
60
|
+
* @param accountId - 账户 ID
|
|
61
|
+
*/
|
|
62
|
+
export declare function clearAccountCache(accountId: string): void;
|
|
43
63
|
/** tools/list 返回的工具描述 */
|
|
44
64
|
export interface McpToolInfo {
|
|
45
65
|
name: string;
|
|
@@ -50,6 +70,10 @@ export interface McpToolInfo {
|
|
|
50
70
|
export interface SendJsonRpcOptions {
|
|
51
71
|
/** 自定义 HTTP 请求超时时间(毫秒),默认使用 HTTP_REQUEST_TIMEOUT_MS */
|
|
52
72
|
timeoutMs?: number;
|
|
73
|
+
/** 当前会话可信的企业微信 userid,有值时会注入自定义 header */
|
|
74
|
+
requesterUserId?: string;
|
|
75
|
+
/** 指定使用的账户 ID,有值时优先使用,否则 fallback 到 resolveCurrentAccountId() */
|
|
76
|
+
accountId?: string;
|
|
53
77
|
}
|
|
54
78
|
/**
|
|
55
79
|
* 发送 JSON-RPC 请求到 MCP Server(Streamable HTTP 协议)
|
|
@@ -58,10 +82,13 @@ export interface SendJsonRpcOptions {
|
|
|
58
82
|
* - 无状态 Server:跳过 session 管理,直接发送请求
|
|
59
83
|
* - 有状态 Server:首次调用先执行 initialize 握手,session 失效(404)时自动重建并重试
|
|
60
84
|
*
|
|
85
|
+
* accountId 优先使用 options.accountId(由工具工厂从 ctx.agentAccountId 传入),
|
|
86
|
+
* 未提供时 fallback 到 resolveCurrentAccountId()。解析结果贯穿整条调用链,避免跨账号污染。
|
|
87
|
+
*
|
|
61
88
|
* @param category - MCP 品类名称
|
|
62
89
|
* @param method - JSON-RPC 方法名
|
|
63
90
|
* @param params - JSON-RPC 参数
|
|
64
|
-
* @param options -
|
|
91
|
+
* @param options - 可选配置(如自定义超时、指定账户)
|
|
65
92
|
* @returns JSON-RPC result
|
|
66
93
|
*/
|
|
67
94
|
export declare function sendJsonRpc(category: string, method: string, params?: Record<string, unknown>, options?: SendJsonRpcOptions): Promise<unknown>;
|