@wecom/wecom-openclaw-plugin 2026.3.31-beta.0 → 2026.4.1-beta.1
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 +4 -251
- package/dist/cjs/index.js +45 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/src/channel.js +339 -0
- package/dist/cjs/src/channel.js.map +1 -0
- package/dist/cjs/src/const.js +137 -0
- package/dist/cjs/src/const.js.map +1 -0
- package/dist/cjs/src/dm-policy.js +89 -0
- package/dist/cjs/src/dm-policy.js.map +1 -0
- package/dist/cjs/src/group-policy.js +133 -0
- package/dist/cjs/src/group-policy.js.map +1 -0
- package/dist/cjs/src/mcp/interceptors/biz-error.js +70 -0
- package/dist/cjs/src/mcp/interceptors/biz-error.js.map +1 -0
- package/dist/cjs/src/mcp/interceptors/index.js +73 -0
- package/dist/cjs/src/mcp/interceptors/index.js.map +1 -0
- package/dist/cjs/src/mcp/interceptors/msg-media.js +159 -0
- package/dist/cjs/src/mcp/interceptors/msg-media.js.map +1 -0
- package/dist/cjs/src/mcp/interceptors/smartpage-create.js +157 -0
- package/dist/cjs/src/mcp/interceptors/smartpage-create.js.map +1 -0
- package/dist/cjs/src/mcp/interceptors/smartpage-export.js +93 -0
- package/dist/cjs/src/mcp/interceptors/smartpage-export.js.map +1 -0
- package/dist/cjs/src/mcp/schema.js +104 -0
- package/dist/cjs/src/mcp/schema.js.map +1 -0
- package/dist/cjs/src/mcp/tool.js +208 -0
- package/dist/cjs/src/mcp/tool.js.map +1 -0
- package/dist/cjs/src/mcp/transport.js +417 -0
- package/dist/cjs/src/mcp/transport.js.map +1 -0
- package/dist/cjs/src/media-handler.js +129 -0
- package/dist/cjs/src/media-handler.js.map +1 -0
- package/dist/cjs/src/media-uploader.js +284 -0
- package/dist/cjs/src/media-uploader.js.map +1 -0
- package/dist/cjs/src/message-parser.js +134 -0
- package/dist/cjs/src/message-parser.js.map +1 -0
- package/dist/cjs/src/message-sender.js +89 -0
- package/dist/cjs/src/message-sender.js.map +1 -0
- package/dist/cjs/src/monitor.js +991 -0
- package/dist/cjs/src/monitor.js.map +1 -0
- package/dist/cjs/src/onboarding.js +182 -0
- package/dist/cjs/src/onboarding.js.map +1 -0
- package/dist/cjs/src/openclaw-compat.js +400 -0
- package/dist/cjs/src/openclaw-compat.js.map +1 -0
- package/dist/cjs/src/reqid-store.js +88 -0
- package/dist/cjs/src/reqid-store.js.map +1 -0
- package/dist/cjs/src/runtime.js +16 -0
- package/dist/cjs/src/runtime.js.map +1 -0
- package/dist/cjs/src/state-dir-resolve.js +35 -0
- package/dist/cjs/src/state-dir-resolve.js.map +1 -0
- package/dist/cjs/src/state-manager.js +158 -0
- package/dist/cjs/src/state-manager.js.map +1 -0
- package/dist/cjs/src/template-card-parser.js +641 -0
- package/dist/cjs/src/template-card-parser.js.map +1 -0
- package/dist/cjs/src/timeout.js +42 -0
- package/dist/cjs/src/timeout.js.map +1 -0
- package/dist/cjs/src/utils.js +60 -0
- package/dist/cjs/src/utils.js.map +1 -0
- package/dist/cjs/src/version.js +26 -0
- package/dist/cjs/src/version.js.map +1 -0
- package/dist/esm/index.js +41 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/src/channel.js +337 -0
- package/dist/esm/src/channel.js.map +1 -0
- package/dist/{src → esm/src}/const.js +30 -77
- package/dist/esm/src/const.js.map +1 -0
- package/dist/{src → esm/src}/dm-policy.js +9 -6
- package/dist/esm/src/dm-policy.js.map +1 -0
- package/dist/{src → esm/src}/group-policy.js +7 -3
- package/dist/esm/src/group-policy.js.map +1 -0
- package/dist/{src → esm/src}/mcp/interceptors/biz-error.js +6 -2
- package/dist/esm/src/mcp/interceptors/biz-error.js.map +1 -0
- package/dist/{src → esm/src}/mcp/interceptors/index.js +10 -6
- package/dist/esm/src/mcp/interceptors/index.js.map +1 -0
- package/dist/{src → esm/src}/mcp/interceptors/msg-media.js +10 -6
- package/dist/esm/src/mcp/interceptors/msg-media.js.map +1 -0
- package/dist/{src → esm/src}/mcp/interceptors/smartpage-create.js +6 -2
- package/dist/esm/src/mcp/interceptors/smartpage-create.js.map +1 -0
- package/dist/{src → esm/src}/mcp/interceptors/smartpage-export.js +6 -2
- package/dist/esm/src/mcp/interceptors/smartpage-export.js.map +1 -0
- package/dist/{src → esm/src}/mcp/schema.js +4 -1
- package/dist/esm/src/mcp/schema.js.map +1 -0
- package/dist/{src → esm/src}/mcp/tool.js +8 -4
- package/dist/esm/src/mcp/tool.js.map +1 -0
- package/dist/{src → esm/src}/mcp/transport.js +16 -15
- package/dist/esm/src/mcp/transport.js.map +1 -0
- package/dist/{src → esm/src}/media-handler.js +10 -6
- package/dist/esm/src/media-handler.js.map +1 -0
- package/dist/{src → esm/src}/media-uploader.js +10 -64
- package/dist/esm/src/media-uploader.js.map +1 -0
- package/dist/{src → esm/src}/message-parser.js +4 -1
- package/dist/esm/src/message-parser.js.map +1 -0
- package/dist/{src → esm/src}/message-sender.js +11 -46
- package/dist/esm/src/message-sender.js.map +1 -0
- package/dist/{src → esm/src}/monitor.js +282 -151
- package/dist/esm/src/monitor.js.map +1 -0
- package/dist/{src → esm/src}/onboarding.js +26 -22
- package/dist/esm/src/onboarding.js.map +1 -0
- package/dist/{src → esm/src}/openclaw-compat.js +27 -143
- package/dist/esm/src/openclaw-compat.js.map +1 -0
- package/dist/{src → esm/src}/reqid-store.js +7 -4
- package/dist/esm/src/reqid-store.js.map +1 -0
- package/dist/{src → esm/src}/runtime.js +5 -2
- package/dist/esm/src/runtime.js.map +1 -0
- package/dist/esm/src/state-dir-resolve.js +13 -0
- package/dist/esm/src/state-dir-resolve.js.map +1 -0
- package/dist/{src → esm/src}/state-manager.js +16 -84
- package/dist/esm/src/state-manager.js.map +1 -0
- package/dist/{src → esm/src}/template-card-parser.js +8 -17
- package/dist/esm/src/template-card-parser.js.map +1 -0
- package/dist/{src → esm/src}/timeout.js +5 -2
- package/dist/esm/src/timeout.js.map +1 -0
- package/dist/{src → esm/src}/utils.js +10 -24
- package/dist/esm/src/utils.js.map +1 -0
- package/dist/esm/src/version.js +23 -0
- package/dist/esm/src/version.js.map +1 -0
- package/dist/esm/types/index.d.ts +9 -0
- package/dist/{src → esm/types/src}/const.d.ts +0 -46
- package/dist/{src → esm/types/src}/interface.d.ts +2 -0
- package/dist/{src → esm/types/src}/message-sender.d.ts +0 -18
- package/dist/{src → esm/types/src}/openclaw-compat.d.ts +0 -49
- package/dist/esm/types/src/state-dir-resolve.d.ts +2 -0
- package/dist/{src → esm/types/src}/utils.d.ts +0 -39
- package/dist/esm/types/src/version.d.ts +2 -0
- package/dist/index.d.ts +4 -2
- package/package.json +7 -15
- package/dist/index.js +0 -113
- package/dist/src/accounts.d.ts +0 -57
- package/dist/src/accounts.js +0 -222
- package/dist/src/agent/api-client.d.ts +0 -94
- package/dist/src/agent/api-client.js +0 -292
- package/dist/src/agent/handler.d.ts +0 -63
- package/dist/src/agent/handler.js +0 -532
- package/dist/src/agent/index.d.ts +0 -5
- package/dist/src/agent/index.js +0 -5
- package/dist/src/agent/webhook.d.ts +0 -23
- package/dist/src/agent/webhook.js +0 -226
- package/dist/src/agent/xml.d.ts +0 -21
- package/dist/src/agent/xml.js +0 -41
- package/dist/src/channel.js +0 -564
- package/dist/src/chat-queue.d.ts +0 -31
- package/dist/src/chat-queue.js +0 -45
- package/dist/src/http.d.ts +0 -26
- package/dist/src/http.js +0 -93
- package/dist/src/interface.js +0 -4
- package/dist/src/mcp/index.js +0 -6
- package/dist/src/mcp/interceptors/types.js +0 -7
- package/dist/src/shared/command-auth.d.ts +0 -23
- package/dist/src/shared/command-auth.js +0 -73
- package/dist/src/shared/xml-parser.d.ts +0 -46
- package/dist/src/shared/xml-parser.js +0 -210
- package/dist/src/target.d.ts +0 -35
- package/dist/src/target.js +0 -67
- package/dist/src/template-card-manager.d.ts +0 -55
- package/dist/src/template-card-manager.js +0 -205
- package/dist/src/types/account.d.ts +0 -29
- package/dist/src/types/account.js +0 -4
- package/dist/src/types/config.d.ts +0 -80
- package/dist/src/types/config.js +0 -7
- package/dist/src/types/constants.d.ts +0 -42
- package/dist/src/types/constants.js +0 -42
- package/dist/src/types/index.d.ts +0 -7
- package/dist/src/types/index.js +0 -5
- package/dist/src/types/message.d.ts +0 -238
- package/dist/src/types/message.js +0 -5
- package/dist/src/version.d.ts +0 -2
- package/dist/src/version.js +0 -24
- package/dist/src/webhook/command-auth.d.ts +0 -47
- package/dist/src/webhook/command-auth.js +0 -96
- package/dist/src/webhook/gateway.d.ts +0 -36
- package/dist/src/webhook/gateway.js +0 -227
- package/dist/src/webhook/handler.d.ts +0 -18
- package/dist/src/webhook/handler.js +0 -420
- package/dist/src/webhook/helpers.d.ts +0 -156
- package/dist/src/webhook/helpers.js +0 -833
- package/dist/src/webhook/http.d.ts +0 -26
- package/dist/src/webhook/http.js +0 -94
- package/dist/src/webhook/index.d.ts +0 -11
- package/dist/src/webhook/index.js +0 -14
- package/dist/src/webhook/media.d.ts +0 -29
- package/dist/src/webhook/media.js +0 -88
- package/dist/src/webhook/monitor.d.ts +0 -59
- package/dist/src/webhook/monitor.js +0 -1147
- package/dist/src/webhook/state.d.ts +0 -220
- package/dist/src/webhook/state.js +0 -491
- package/dist/src/webhook/target.d.ts +0 -41
- package/dist/src/webhook/target.js +0 -132
- package/dist/src/webhook/types.d.ts +0 -347
- package/dist/src/webhook/types.js +0 -33
- /package/dist/{src → esm/types/src}/channel.d.ts +0 -0
- /package/dist/{src → esm/types/src}/dm-policy.d.ts +0 -0
- /package/dist/{src → esm/types/src}/group-policy.d.ts +0 -0
- /package/dist/{src → esm/types/src}/mcp/index.d.ts +0 -0
- /package/dist/{src → esm/types/src}/mcp/interceptors/biz-error.d.ts +0 -0
- /package/dist/{src → esm/types/src}/mcp/interceptors/index.d.ts +0 -0
- /package/dist/{src → esm/types/src}/mcp/interceptors/msg-media.d.ts +0 -0
- /package/dist/{src → esm/types/src}/mcp/interceptors/smartpage-create.d.ts +0 -0
- /package/dist/{src → esm/types/src}/mcp/interceptors/smartpage-export.d.ts +0 -0
- /package/dist/{src → esm/types/src}/mcp/interceptors/types.d.ts +0 -0
- /package/dist/{src → esm/types/src}/mcp/schema.d.ts +0 -0
- /package/dist/{src → esm/types/src}/mcp/tool.d.ts +0 -0
- /package/dist/{src → esm/types/src}/mcp/transport.d.ts +0 -0
- /package/dist/{src → esm/types/src}/media-handler.d.ts +0 -0
- /package/dist/{src → esm/types/src}/media-uploader.d.ts +0 -0
- /package/dist/{src → esm/types/src}/message-parser.d.ts +0 -0
- /package/dist/{src → esm/types/src}/monitor.d.ts +0 -0
- /package/dist/{src → esm/types/src}/onboarding.d.ts +0 -0
- /package/dist/{src → esm/types/src}/reqid-store.d.ts +0 -0
- /package/dist/{src → esm/types/src}/runtime.d.ts +0 -0
- /package/dist/{src → esm/types/src}/state-manager.d.ts +0 -0
- /package/dist/{src → esm/types/src}/template-card-parser.d.ts +0 -0
- /package/dist/{src → esm/types/src}/timeout.d.ts +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { CHANNEL_ID } from './const.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* 企业微信群组访问控制模块
|
|
3
5
|
*
|
|
4
6
|
* 负责群组策略检查(groupPolicy、群组白名单、群内发送者白名单)
|
|
5
7
|
*/
|
|
6
|
-
import { CHANNEL_ID } from "./const.js";
|
|
7
8
|
// ============================================================================
|
|
8
9
|
// 内部辅助函数
|
|
9
10
|
// ============================================================================
|
|
@@ -75,7 +76,7 @@ function isGroupSenderAllowed(params) {
|
|
|
75
76
|
* 检查群组策略访问控制
|
|
76
77
|
* @returns 检查结果,包含是否允许继续处理
|
|
77
78
|
*/
|
|
78
|
-
|
|
79
|
+
function checkGroupPolicy(params) {
|
|
79
80
|
const { chatId, senderId, account, config, runtime } = params;
|
|
80
81
|
const wecomConfig = (config.channels?.[CHANNEL_ID] ?? {});
|
|
81
82
|
const defaultGroupPolicy = config.channels?.[CHANNEL_ID]?.groupPolicy;
|
|
@@ -115,7 +116,7 @@ export function checkGroupPolicy(params) {
|
|
|
115
116
|
/**
|
|
116
117
|
* 检查发送者是否在允许列表中(通用)
|
|
117
118
|
*/
|
|
118
|
-
|
|
119
|
+
function isSenderAllowed(senderId, allowFrom) {
|
|
119
120
|
if (allowFrom.includes("*")) {
|
|
120
121
|
return true;
|
|
121
122
|
}
|
|
@@ -124,3 +125,6 @@ export function isSenderAllowed(senderId, allowFrom) {
|
|
|
124
125
|
return normalized === senderId || normalized === `user:${senderId}`;
|
|
125
126
|
});
|
|
126
127
|
}
|
|
128
|
+
|
|
129
|
+
export { checkGroupPolicy, isSenderAllowed };
|
|
130
|
+
//# sourceMappingURL=group-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"group-policy.js","sources":["../../../../../src/group-policy.ts"],"sourcesContent":["/**\n * 企业微信群组访问控制模块\n *\n * 负责群组策略检查(groupPolicy、群组白名单、群内发送者白名单)\n */\n\nimport type { OpenClawConfig } from \"openclaw/plugin-sdk/core\";\nimport type { RuntimeEnv } from \"openclaw/plugin-sdk/runtime-env\";\nimport { CHANNEL_ID } from \"./const.js\";\nimport type { ResolvedWeComAccount, WeComConfig, WeComGroupConfig } from \"./utils.js\";\n\n// ============================================================================\n// 检查结果类型\n// ============================================================================\n\n/**\n * 群组策略检查结果\n */\nexport interface GroupPolicyCheckResult {\n /** 是否允许继续处理消息 */\n allowed: boolean;\n}\n\n// ============================================================================\n// 内部辅助函数\n// ============================================================================\n\n/**\n * 解析企业微信群组配置\n */\nfunction resolveWeComGroupConfig(params: {\n cfg?: WeComConfig;\n groupId?: string | null;\n}): WeComGroupConfig | undefined {\n const groups = params.cfg?.groups ?? {};\n const wildcard = groups[\"*\"];\n const groupId = params.groupId?.trim();\n if (!groupId) {\n return undefined;\n }\n\n const direct = groups[groupId];\n if (direct) {\n return direct;\n }\n\n const lowered = groupId.toLowerCase();\n const matchKey = Object.keys(groups).find((key) => key.toLowerCase() === lowered);\n if (matchKey) {\n return groups[matchKey];\n }\n return wildcard;\n}\n\n/**\n * 检查群组是否在允许列表中\n */\nfunction isWeComGroupAllowed(params: {\n groupPolicy: \"open\" | \"allowlist\" | \"disabled\";\n allowFrom: Array<string | number>;\n groupId: string;\n}): boolean {\n const { groupPolicy } = params;\n if (groupPolicy === \"disabled\") {\n return false;\n }\n if (groupPolicy === \"open\") {\n return true;\n }\n // allowlist 模式:检查群组是否在允许列表中\n const normalizedAllowFrom = params.allowFrom.map((entry) =>\n String(entry).replace(new RegExp(`^${CHANNEL_ID}:`, \"i\"), \"\").trim()\n );\n if (normalizedAllowFrom.includes(\"*\")) {\n return true;\n }\n const normalizedGroupId = params.groupId.trim();\n return normalizedAllowFrom.some(\n (entry) => entry === normalizedGroupId || entry.toLowerCase() === normalizedGroupId.toLowerCase()\n );\n}\n\n/**\n * 检查群组内发送者是否在允许列表中\n */\nfunction isGroupSenderAllowed(params: {\n senderId: string;\n groupId: string;\n wecomConfig: WeComConfig;\n}): boolean {\n const { senderId, groupId, wecomConfig } = params;\n\n const groupConfig = resolveWeComGroupConfig({\n cfg: wecomConfig,\n groupId,\n });\n\n const perGroupSenderAllowFrom = (groupConfig?.allowFrom ?? []).map((v) => String(v));\n\n if (perGroupSenderAllowFrom.length === 0) {\n return true;\n }\n\n if (perGroupSenderAllowFrom.includes(\"*\")) {\n return true;\n }\n\n return perGroupSenderAllowFrom.some((entry) => {\n const normalized = entry.replace(new RegExp(`^${CHANNEL_ID}:`, \"i\"), \"\").trim();\n return normalized === senderId || normalized === `user:${senderId}`;\n });\n}\n\n// ============================================================================\n// 公开 API\n// ============================================================================\n\n/**\n * 检查群组策略访问控制\n * @returns 检查结果,包含是否允许继续处理\n */\nexport function checkGroupPolicy(params: {\n chatId: string;\n senderId: string;\n account: ResolvedWeComAccount;\n config: OpenClawConfig;\n runtime: RuntimeEnv;\n}): GroupPolicyCheckResult {\n const { chatId, senderId, account, config, runtime } = params;\n const wecomConfig = (config.channels?.[CHANNEL_ID] ?? {}) as WeComConfig;\n\n const defaultGroupPolicy = config.channels?.[CHANNEL_ID]?.groupPolicy;\n const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? \"open\"\n // const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({\n // providerConfigPresent: config.channels?.[CHANNEL_ID] !== undefined,\n // groupPolicy: wecomConfig.groupPolicy,\n // defaultGroupPolicy,\n // });\n\n // warnMissingProviderGroupPolicyFallbackOnce({\n // providerMissingFallbackApplied,\n // providerKey: CHANNEL_ID,\n // accountId: account.accountId,\n // log: (msg) => runtime.log?.(msg),\n // });\n\n const groupAllowFrom = wecomConfig.groupAllowFrom ?? [];\n const groupAllowed = isWeComGroupAllowed({\n groupPolicy,\n allowFrom: groupAllowFrom,\n groupId: chatId,\n });\n\n if (!groupAllowed) {\n runtime.log?.(\n `[WeCom] Group ${chatId} not allowed (groupPolicy=${groupPolicy})`,\n );\n return { allowed: false };\n }\n\n const senderAllowed = isGroupSenderAllowed({\n senderId,\n groupId: chatId,\n wecomConfig,\n });\n\n if (!senderAllowed) {\n runtime.log?.(\n `[WeCom] Sender ${senderId} not in group ${chatId} sender allowlist`,\n );\n return { allowed: false };\n }\n\n return { allowed: true };\n}\n\n/**\n * 检查发送者是否在允许列表中(通用)\n */\nexport function isSenderAllowed(senderId: string, allowFrom: string[]): boolean {\n if (allowFrom.includes(\"*\")) {\n return true;\n }\n return allowFrom.some((entry) => {\n const normalized = entry.replace(new RegExp(`^${CHANNEL_ID}:`, \"i\"), \"\").trim();\n return normalized === senderId || normalized === `user:${senderId}`;\n });\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { clearCategoryCache } from '../transport.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* 业务错误码检查拦截器
|
|
3
5
|
*
|
|
@@ -7,7 +9,6 @@
|
|
|
7
9
|
*
|
|
8
10
|
* 此拦截器对所有 call 调用生效。
|
|
9
11
|
*/
|
|
10
|
-
import { clearCategoryCache } from "../transport.js";
|
|
11
12
|
// ============================================================================
|
|
12
13
|
// 常量
|
|
13
14
|
// ============================================================================
|
|
@@ -23,7 +24,7 @@ const BIZ_CACHE_CLEAR_ERROR_CODES = new Set([850002]);
|
|
|
23
24
|
// ============================================================================
|
|
24
25
|
// 拦截器实现
|
|
25
26
|
// ============================================================================
|
|
26
|
-
|
|
27
|
+
const bizErrorInterceptor = {
|
|
27
28
|
name: "biz-error",
|
|
28
29
|
/** 对所有 call 调用生效 */
|
|
29
30
|
match: () => true,
|
|
@@ -62,3 +63,6 @@ function checkBizErrorAndClearCache(result, category) {
|
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
}
|
|
66
|
+
|
|
67
|
+
export { bizErrorInterceptor };
|
|
68
|
+
//# sourceMappingURL=biz-error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"biz-error.js","sources":["../../../../../../../src/mcp/interceptors/biz-error.ts"],"sourcesContent":["/**\n * 业务错误码检查拦截器\n *\n * 检查 tools/call 返回结果中是否包含需要清理缓存的业务错误码。\n * MCP Server 可能在正常的 JSON-RPC 响应中返回业务层错误,\n * 这些错误被包裹在 result.content[].text 中,需要解析后判断。\n *\n * 此拦截器对所有 call 调用生效。\n */\n\nimport { clearCategoryCache } from \"../transport.js\";\nimport type { CallInterceptor, CallContext } from \"./types.js\";\n\n// ============================================================================\n// 常量\n// ============================================================================\n\n/**\n * 需要触发缓存清理的业务错误码集合\n *\n * 这些错误码出现在 MCP 工具调用返回的 content 文本中(业务层面),\n * 与 JSON-RPC 层面的错误码不同,需要在此处额外检测。\n *\n * - 850002: 机器人未被授权使用对应能力,需清理缓存以便下次重新拉取配置\n */\nconst BIZ_CACHE_CLEAR_ERROR_CODES = new Set([850002]);\n\n// ============================================================================\n// 拦截器实现\n// ============================================================================\n\nexport const bizErrorInterceptor: CallInterceptor = {\n name: \"biz-error\",\n\n /** 对所有 call 调用生效 */\n match: () => true,\n\n /** 检查返回结果中的业务错误码,必要时清理缓存 */\n afterCall(ctx: CallContext, result: unknown): unknown {\n checkBizErrorAndClearCache(result, ctx.category);\n // 不修改 result,透传给下一个拦截器\n return result;\n },\n};\n\n// ============================================================================\n// 内部实现\n// ============================================================================\n\n/**\n * 检查 tools/call 的返回结果中是否包含需要清理缓存的业务错误码\n */\nfunction checkBizErrorAndClearCache(result: unknown, category: string): void {\n if (!result || typeof result !== \"object\") return;\n\n const { content } = result as { content?: Array<{ type: string; text?: string }> };\n if (!Array.isArray(content)) return;\n\n for (const item of content) {\n if (item.type !== \"text\" || !item.text) continue;\n try {\n const parsed = JSON.parse(item.text) as Record<string, unknown>;\n if (typeof parsed.errcode === \"number\" && BIZ_CACHE_CLEAR_ERROR_CODES.has(parsed.errcode)) {\n console.log(`[mcp] 检测到业务错误码 ${parsed.errcode} (category=\"${category}\"),清理缓存`);\n clearCategoryCache(category);\n return;\n }\n } catch {\n // text 不是 JSON 格式,跳过\n }\n }\n}\n"],"names":[],"mappings":";;AAAA;;;;;;;;AAQG;AAKH;AACA;AACA;AAEA;;;;;;;AAOG;AACH,MAAM,2BAA2B,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD;AACA;AACA;AAEO,MAAM,mBAAmB,GAAoB;AAClD,IAAA,IAAI,EAAE,WAAW;;AAGjB,IAAA,KAAK,EAAE,MAAM,IAAI;;IAGjB,SAAS,CAAC,GAAgB,EAAE,MAAe,EAAA;AACzC,QAAA,0BAA0B,CAAC,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC;;AAEhD,QAAA,OAAO,MAAM;IACf,CAAC;;AAGH;AACA;AACA;AAEA;;AAEG;AACH,SAAS,0BAA0B,CAAC,MAAe,EAAE,QAAgB,EAAA;AACnE,IAAA,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE;AAE3C,IAAA,MAAM,EAAE,OAAO,EAAE,GAAG,MAA8D;AAClF,IAAA,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE;AAE7B,IAAA,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE;QAC1B,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI;YAAE;AACxC,QAAA,IAAI;YACF,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAA4B;AAC/D,YAAA,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,IAAI,2BAA2B,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE;gBACzF,OAAO,CAAC,GAAG,CAAC,CAAA,eAAA,EAAkB,MAAM,CAAC,OAAO,CAAA,YAAA,EAAe,QAAQ,CAAA,OAAA,CAAS,CAAC;gBAC7E,kBAAkB,CAAC,QAAQ,CAAC;gBAC5B;YACF;QACF;AAAE,QAAA,MAAM;;QAER;IACF;AACF;;;;"}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { bizErrorInterceptor } from './biz-error.js';
|
|
2
|
+
import { mediaInterceptor } from './msg-media.js';
|
|
3
|
+
import { smartpageCreateInterceptor } from './smartpage-create.js';
|
|
4
|
+
import { smartpageExportInterceptor } from './smartpage-export.js';
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* MCP call 拦截器注册表与调度入口
|
|
3
8
|
*
|
|
@@ -8,10 +13,6 @@
|
|
|
8
13
|
*
|
|
9
14
|
* tool.ts 的 handleCall 无需任何改动。
|
|
10
15
|
*/
|
|
11
|
-
import { bizErrorInterceptor } from "./biz-error.js";
|
|
12
|
-
import { mediaInterceptor } from "./msg-media.js";
|
|
13
|
-
import { smartpageCreateInterceptor } from "./smartpage-create.js";
|
|
14
|
-
import { smartpageExportInterceptor } from "./smartpage-export.js";
|
|
15
16
|
// ============================================================================
|
|
16
17
|
// 拦截器注册表(按注册顺序执行)
|
|
17
18
|
// ============================================================================
|
|
@@ -28,7 +29,7 @@ const interceptors = [
|
|
|
28
29
|
* - timeoutMs: 取所有拦截器返回值中的最大值
|
|
29
30
|
* - args: 后注册的拦截器覆盖前者(一般同一调用只有一个拦截器会返回 args)
|
|
30
31
|
*/
|
|
31
|
-
|
|
32
|
+
async function resolveBeforeCall(ctx) {
|
|
32
33
|
let mergedTimeoutMs;
|
|
33
34
|
let mergedArgs;
|
|
34
35
|
for (const interceptor of interceptors) {
|
|
@@ -55,7 +56,7 @@ export async function resolveBeforeCall(ctx) {
|
|
|
55
56
|
* 前一个拦截器的返回值作为下一个拦截器的输入。
|
|
56
57
|
* 拦截器若不需要修改 result,应原样返回。
|
|
57
58
|
*/
|
|
58
|
-
|
|
59
|
+
async function runAfterCall(ctx, result) {
|
|
59
60
|
let current = result;
|
|
60
61
|
for (const interceptor of interceptors) {
|
|
61
62
|
if (!interceptor.match(ctx) || !interceptor.afterCall)
|
|
@@ -64,3 +65,6 @@ export async function runAfterCall(ctx, result) {
|
|
|
64
65
|
}
|
|
65
66
|
return current;
|
|
66
67
|
}
|
|
68
|
+
|
|
69
|
+
export { resolveBeforeCall, runAfterCall };
|
|
70
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../../../../../src/mcp/interceptors/index.ts"],"sourcesContent":["/**\n * MCP call 拦截器注册表与调度入口\n *\n * 所有 call 拦截器在此注册,按注册顺序执行。\n * 新增拦截器只需:\n * 1. 在 interceptors/ 目录下新建文件,实现 CallInterceptor 接口\n * 2. 在下方 interceptors 数组中注册\n *\n * tool.ts 的 handleCall 无需任何改动。\n */\n\nimport { bizErrorInterceptor } from \"./biz-error.js\";\nimport { mediaInterceptor } from \"./msg-media.js\";\nimport { smartpageCreateInterceptor } from \"./smartpage-create.js\";\nimport { smartpageExportInterceptor } from \"./smartpage-export.js\";\nimport type { CallContext, CallInterceptor, BeforeCallOptions } from \"./types.js\";\nimport type { SendJsonRpcOptions } from \"../transport.js\";\n\nexport type { CallContext, CallInterceptor, BeforeCallOptions } from \"./types.js\";\n\n// ============================================================================\n// 拦截器注册表(按注册顺序执行)\n// ============================================================================\n\nconst interceptors: CallInterceptor[] = [\n bizErrorInterceptor, // 业务错误码检查(所有 call 生效)\n mediaInterceptor, // get_msg_media base64 拦截\n smartpageCreateInterceptor, // smartpage_create 本地文件读取\n smartpageExportInterceptor, // smartpage_get_export_result content → 本地文件\n];\n\n// ============================================================================\n// 调度 API\n// ============================================================================\n\n/** resolveBeforeCall 的返回值 */\nexport interface ResolvedBeforeCall {\n /** 合并后的 sendJsonRpc 选项(如超时时间) */\n options?: SendJsonRpcOptions;\n /** 替换后的 args(如从本地文件读取的请求体) */\n args?: Record<string, unknown>;\n}\n\n/**\n * 收集匹配的 beforeCall 配置,合并后返回\n *\n * 合并策略:\n * - timeoutMs: 取所有拦截器返回值中的最大值\n * - args: 后注册的拦截器覆盖前者(一般同一调用只有一个拦截器会返回 args)\n */\nexport async function resolveBeforeCall(ctx: CallContext): Promise<ResolvedBeforeCall> {\n let mergedTimeoutMs: number | undefined;\n let mergedArgs: Record<string, unknown> | undefined;\n\n for (const interceptor of interceptors) {\n if (!interceptor.match(ctx) || !interceptor.beforeCall) continue;\n\n const opts = await interceptor.beforeCall(ctx);\n if (opts?.timeoutMs !== undefined) {\n mergedTimeoutMs = mergedTimeoutMs === undefined\n ? opts.timeoutMs\n : Math.max(mergedTimeoutMs, opts.timeoutMs);\n }\n if (opts?.args !== undefined) {\n mergedArgs = opts.args;\n }\n }\n\n return {\n options: mergedTimeoutMs !== undefined ? { timeoutMs: mergedTimeoutMs } : undefined,\n args: mergedArgs,\n };\n}\n\n/**\n * 依次执行匹配的 afterCall 拦截器,管道式传递 result\n *\n * 前一个拦截器的返回值作为下一个拦截器的输入。\n * 拦截器若不需要修改 result,应原样返回。\n */\nexport async function runAfterCall(ctx: CallContext, result: unknown): Promise<unknown> {\n let current = result;\n\n for (const interceptor of interceptors) {\n if (!interceptor.match(ctx) || !interceptor.afterCall) continue;\n current = await interceptor.afterCall(ctx, current);\n }\n\n return current;\n}\n"],"names":[],"mappings":";;;;;AAAA;;;;;;;;;AASG;AAWH;AACA;AACA;AAEA,MAAM,YAAY,GAAsB;AACtC,IAAA,mBAAmB;AACnB,IAAA,gBAAgB;AAChB,IAAA,0BAA0B;AAC1B,IAAA,0BAA0B;CAC3B;AAcD;;;;;;AAMG;AACI,eAAe,iBAAiB,CAAC,GAAgB,EAAA;AACtD,IAAA,IAAI,eAAmC;AACvC,IAAA,IAAI,UAA+C;AAEnD,IAAA,KAAK,MAAM,WAAW,IAAI,YAAY,EAAE;QACtC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,UAAU;YAAE;QAExD,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,UAAU,CAAC,GAAG,CAAC;AAC9C,QAAA,IAAI,IAAI,EAAE,SAAS,KAAK,SAAS,EAAE;YACjC,eAAe,GAAG,eAAe,KAAK;kBAClC,IAAI,CAAC;kBACL,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,SAAS,CAAC;QAC/C;AACA,QAAA,IAAI,IAAI,EAAE,IAAI,KAAK,SAAS,EAAE;AAC5B,YAAA,UAAU,GAAG,IAAI,CAAC,IAAI;QACxB;IACF;IAEA,OAAO;AACL,QAAA,OAAO,EAAE,eAAe,KAAK,SAAS,GAAG,EAAE,SAAS,EAAE,eAAe,EAAE,GAAG,SAAS;AACnF,QAAA,IAAI,EAAE,UAAU;KACjB;AACH;AAEA;;;;;AAKG;AACI,eAAe,YAAY,CAAC,GAAgB,EAAE,MAAe,EAAA;IAClE,IAAI,OAAO,GAAG,MAAM;AAEpB,IAAA,KAAK,MAAM,WAAW,IAAI,YAAY,EAAE;QACtC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS;YAAE;QACvD,OAAO,GAAG,MAAM,WAAW,CAAC,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC;IACrD;AAEA,IAAA,OAAO,OAAO;AAChB;;;;"}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { MEDIA_DOWNLOAD_TIMEOUT_MS } from '../transport.js';
|
|
4
|
+
import { getWeComRuntime } from '../../runtime.js';
|
|
5
|
+
import { detectMime } from '../../openclaw-compat.js';
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* get_msg_media 响应拦截器
|
|
3
9
|
*
|
|
@@ -7,15 +13,10 @@
|
|
|
7
13
|
* 解码为 Buffer 并通过 saveMediaBuffer 保存到本地媒体目录,
|
|
8
14
|
* 替换响应中的 base64_data 为 local_path,避免大模型被 base64 数据消耗 token
|
|
9
15
|
*/
|
|
10
|
-
import * as fs from "node:fs/promises";
|
|
11
|
-
import * as path from "node:path";
|
|
12
|
-
import { MEDIA_DOWNLOAD_TIMEOUT_MS } from "../transport.js";
|
|
13
|
-
import { getWeComRuntime } from "../../runtime.js";
|
|
14
|
-
import { detectMime } from "../../openclaw-compat.js";
|
|
15
16
|
// ============================================================================
|
|
16
17
|
// 拦截器实现
|
|
17
18
|
// ============================================================================
|
|
18
|
-
|
|
19
|
+
const mediaInterceptor = {
|
|
19
20
|
name: "media",
|
|
20
21
|
/** 仅对 get_msg_media 方法生效 */
|
|
21
22
|
match: (ctx) => ctx.method === "get_msg_media",
|
|
@@ -131,3 +132,6 @@ async function interceptMediaResponse(result) {
|
|
|
131
132
|
}],
|
|
132
133
|
};
|
|
133
134
|
}
|
|
135
|
+
|
|
136
|
+
export { mediaInterceptor };
|
|
137
|
+
//# sourceMappingURL=msg-media.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"msg-media.js","sources":["../../../../../../../src/mcp/interceptors/msg-media.ts"],"sourcesContent":["/**\n * get_msg_media 响应拦截器\n *\n * 核心逻辑:\n * 1. beforeCall: 设置延长的超时时间(120s),因为 base64 数据可达 ~27MB\n * 2. afterCall: 从 MCP result 的 content[].text 中提取 base64_data,\n * 解码为 Buffer 并通过 saveMediaBuffer 保存到本地媒体目录,\n * 替换响应中的 base64_data 为 local_path,避免大模型被 base64 数据消耗 token\n */\n\nimport * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { MEDIA_DOWNLOAD_TIMEOUT_MS } from \"../transport.js\";\nimport { getWeComRuntime } from \"../../runtime.js\";\nimport { detectMime } from \"../../openclaw-compat.js\";\nimport type { CallInterceptor, CallContext } from \"./types.js\";\n\n// ============================================================================\n// 拦截器实现\n// ============================================================================\n\nexport const mediaInterceptor: CallInterceptor = {\n name: \"media\",\n\n /** 仅对 get_msg_media 方法生效 */\n match: (ctx: CallContext) => ctx.method === \"get_msg_media\",\n\n /** 设置延长的超时时间 */\n beforeCall() {\n return { timeoutMs: MEDIA_DOWNLOAD_TIMEOUT_MS };\n },\n\n /** 拦截响应:base64 → 本地文件 */\n async afterCall(ctx: CallContext, result: unknown): Promise<unknown> {\n return interceptMediaResponse(result);\n },\n};\n\n// ============================================================================\n// 内部实现\n// ============================================================================\n\n/**\n * 拦截 get_msg_media 的 MCP 响应\n *\n * 1. 从 MCP result 的 content[].text 中提取业务 JSON\n * 2. 提取 media_item.base64_data,解码为 Buffer\n * 3. 通过 openclaw SDK 的 saveMediaBuffer 保存到本地媒体目录\n * 4. 替换响应:移除 base64_data,加入 local_path\n *\n * 这样大模型只看到轻量的文件路径信息,不会被 base64 数据消耗 token。\n */\nasync function interceptMediaResponse(result: unknown): Promise<unknown> {\n const t0 = performance.now();\n\n // 1. 提取 MCP result 中的 content 数组\n const content = (result as Record<string, unknown>)?.content;\n if (!Array.isArray(content)) return result;\n\n const textItem = content.find(\n (c: Record<string, unknown>) => c.type === \"text\" && typeof c.text === \"string\",\n ) as { type: string; text: string } | undefined;\n if (!textItem) return result;\n\n // 2. 解析业务 JSON\n let bizData: Record<string, unknown>;\n try {\n bizData = JSON.parse(textItem.text) as Record<string, unknown>;\n } catch {\n // 非 JSON 格式,原样返回\n return result;\n }\n\n // 3. 校验业务响应:errcode !== 0 或无 media_item 时原样返回\n if (bizData.errcode !== 0) return result;\n\n const mediaItem = bizData.media_item as Record<string, unknown> | undefined;\n if (!mediaItem || typeof mediaItem.base64_data !== \"string\") return result;\n\n const base64Data = mediaItem.base64_data as string;\n const mediaName = mediaItem.name as string | undefined;\n const mediaType = mediaItem.type as string | undefined;\n const mediaId = mediaItem.media_id as string | undefined;\n\n const tParsed = performance.now();\n\n // 4. 解码 base64 → Buffer\n const buffer = Buffer.from(base64Data, \"base64\");\n const tDecoded = performance.now();\n\n // 5. 检测 contentType,并通过 saveMediaBuffer 保存到本地媒体目录\n const contentType = await detectMime({ buffer, filePath: mediaName }) ?? \"application/octet-stream\";\n const tMimeDetected = performance.now();\n\n // 企业微信聊天记录附件可达 20MB(文件消息上限),\n // 而 saveMediaBuffer 默认 maxBytes 为 5MB(针对 outbound 场景),\n // 此处显式放宽到 20MB 以支持大文件下载。\n const INBOUND_MAX_BYTES = 20 * 1024 * 1024; // 20MB\n\n const core = getWeComRuntime();\n const saved = await core.channel.media.saveMediaBuffer(\n buffer,\n contentType,\n \"inbound\",\n INBOUND_MAX_BYTES, // maxBytes: 放宽到 20MB,匹配企业微信文件消息上限\n mediaName, // originalFilename: 保留原始文件名\n );\n\n // 5.1 补偿:核心库 EXT_BY_MIME 可能缺少某些格式映射(如 audio/amr),\n // 导致保存的文件没有后缀。此处检测并修复。\n const MIME_EXT_PATCH: Record<string, string> = {\n \"audio/amr\": \".amr\",\n };\n const patchExt = MIME_EXT_PATCH[contentType];\n if (patchExt && !path.extname(saved.path)) {\n const newPath = saved.path + patchExt;\n try {\n await fs.rename(saved.path, newPath);\n saved.path = newPath;\n } catch {\n // rename 失败不影响主流程,文件仍可用\n }\n }\n\n const tSaved = performance.now();\n\n // 6. 构造精简响应,移除 base64_data,加入本地路径\n const newBizData = {\n errcode: 0,\n errmsg: \"ok\",\n media_item: {\n media_id: mediaId,\n name: mediaName ?? saved.path.split(\"/\").pop(),\n type: mediaType,\n local_path: saved.path,\n size: buffer.length,\n content_type: saved.contentType,\n },\n };\n\n const tEnd = performance.now();\n\n // 耗时日志:各环节耗时(ms)\n console.log(\n `[mcp] get_msg_media 拦截成功: media_id=${mediaId ?? \"unknown\"}, ` +\n `type=${mediaType ?? \"unknown\"}, size=${buffer.length}, saved=${saved.path}\\n` +\n ` ⏱ 耗时明细 (总 ${(tEnd - t0).toFixed(1)}ms):\\n` +\n ` 解析响应 JSON: ${(tParsed - t0).toFixed(1)}ms\\n` +\n ` base64 解码: ${(tDecoded - tParsed).toFixed(1)}ms (${(base64Data.length / 1024).toFixed(0)}KB base64 → ${(buffer.length / 1024).toFixed(0)}KB buffer)\\n` +\n ` MIME 检测: ${(tMimeDetected - tDecoded).toFixed(1)}ms (${contentType})\\n` +\n ` saveMediaBuffer: ${(tSaved - tMimeDetected).toFixed(1)}ms\\n` +\n ` 构造响应: ${(tEnd - tSaved).toFixed(1)}ms`,\n );\n\n // 7. 返回修改后的 MCP result 结构\n return {\n content: [{\n type: \"text\" as const,\n text: JSON.stringify(newBizData),\n }],\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* smartpage_create 请求拦截器
|
|
3
5
|
*
|
|
@@ -26,7 +28,6 @@
|
|
|
26
28
|
* 传给 MCP Server 的格式:
|
|
27
29
|
* { "title": "...", "pages": [{"page_title": "...", "page_content": "...", "content_type": "..."}] }
|
|
28
30
|
*/
|
|
29
|
-
import * as fs from "node:fs/promises";
|
|
30
31
|
// ============================================================================
|
|
31
32
|
// 常量
|
|
32
33
|
// ============================================================================
|
|
@@ -110,7 +111,7 @@ async function resolvePages(ctx, pages) {
|
|
|
110
111
|
// ============================================================================
|
|
111
112
|
// 拦截器实现
|
|
112
113
|
// ============================================================================
|
|
113
|
-
|
|
114
|
+
const smartpageCreateInterceptor = {
|
|
114
115
|
name: "smartpage-create",
|
|
115
116
|
/** 仅对 doc 品类的 smartpage_create 方法生效 */
|
|
116
117
|
match: (ctx) => ctx.category === "doc" && ctx.method === "smartpage_create",
|
|
@@ -130,3 +131,6 @@ export const smartpageCreateInterceptor = {
|
|
|
130
131
|
return resolvePages(ctx, pages);
|
|
131
132
|
},
|
|
132
133
|
};
|
|
134
|
+
|
|
135
|
+
export { smartpageCreateInterceptor };
|
|
136
|
+
//# sourceMappingURL=smartpage-create.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smartpage-create.js","sources":["../../../../../../../src/mcp/interceptors/smartpage-create.ts"],"sourcesContent":["/**\n * smartpage_create 请求拦截器\n *\n * 核心逻辑:\n * smartpage_create 的 pages 数组中,每个 page 可能包含 page_filepath 字段\n * (指向本地 markdown 文件),用于避免在命令行传递大段文本内容。\n * 本拦截器在 beforeCall 阶段遍历 pages 数组,逐个读取 page_filepath\n * 指向的本地文件内容,填入 page_content 字段,并移除 page_filepath。\n *\n * 入参约定:\n * wecom_mcp call doc smartpage_create '{\n * \"title\": \"主页标题\",\n * \"pages\": [\n * {\"page_title\": \"页面1\", \"page_filepath\": \"/tmp/page1.md\", \"content_type\": \"markdown\"},\n * {\"page_title\": \"页面2\", \"page_filepath\": \"/tmp/page2.md\", \"content_type\": \"markdown\"}\n * ]\n * }'\n *\n * 拦截器行为:\n * 1. 检测 args.pages 数组\n * 2. 校验文件大小:单文件不超过 10MB,所有文件总计不超过 20MB\n * 3. 遍历每个 page,若存在 page_filepath 则读取本地文件内容\n * 4. 将文件内容填入 page_content 字段,移除 page_filepath\n * 5. 返回修改后的完整 args\n *\n * 传给 MCP Server 的格式:\n * { \"title\": \"...\", \"pages\": [{\"page_title\": \"...\", \"page_content\": \"...\", \"content_type\": \"...\"}] }\n */\n\nimport * as fs from \"node:fs/promises\";\nimport type { CallInterceptor, CallContext, BeforeCallOptions } from \"./types.js\";\n\n// ============================================================================\n// 常量\n// ============================================================================\n\n/** 单个 page_filepath 文件大小上限:10MB */\nconst MAX_SINGLE_FILE_SIZE = 10 * 1024 * 1024;\n/** 所有 page_filepath 文件总大小上限:20MB */\nconst MAX_TOTAL_FILE_SIZE = 20 * 1024 * 1024;\n\n// ============================================================================\n// 内部辅助函数\n// ============================================================================\n\n/**\n * 校验所有 page_filepath 的文件大小\n *\n * 使用 fs.stat 在读取文件内容之前检查大小,避免超大文件被加载到内存。\n * - 单文件 > 10MB → 报错\n * - 所有文件累计 > 20MB → 报错\n */\nasync function validateFileSize(\n pages: Record<string, unknown>[],\n): Promise<void> {\n let totalSize = 0;\n\n for (let i = 0; i < pages.length; i++) {\n const filePath = pages[i].page_filepath;\n if (typeof filePath !== \"string\" || !filePath) continue;\n\n let stat: Awaited<ReturnType<typeof fs.stat>>;\n try {\n stat = await fs.stat(filePath);\n } catch (err) {\n // stat 失败不在这里处理,留给后续 readFile 阶段抛出更详细的错误\n continue;\n }\n\n if (stat.size > MAX_SINGLE_FILE_SIZE) {\n console.error(\n `[mcp] smartpage_create: pages[${i}] 文件 \"${filePath}\" ` +\n `大小 ${(stat.size / 1024 / 1024).toFixed(1)}MB 超过单文件上限 10MB`,\n );\n throw new Error(\"内容大小超出限制,无法创建\");\n }\n\n totalSize += stat.size;\n if (totalSize > MAX_TOTAL_FILE_SIZE) {\n console.error(\n `[mcp] smartpage_create: 累计文件大小 ${(totalSize / 1024 / 1024).toFixed(1)}MB ` +\n `超过总上限 20MB(在 pages[${i}] \"${filePath}\" 处超出)`,\n );\n throw new Error(\"内容大小超出限制,无法创建\");\n }\n }\n\n if (totalSize > 0) {\n console.log(\n `[mcp] smartpage_create: 文件大小校验通过,总计 ${(totalSize / 1024 / 1024).toFixed(2)}MB`,\n );\n }\n}\n\n/** 异步解析 pages 中的 page_filepath,返回 BeforeCallOptions */\nasync function resolvePages(\n ctx: CallContext,\n pages: Record<string, unknown>[],\n): Promise<BeforeCallOptions> {\n console.log(`[mcp] smartpage_create: 开始解析 ${pages.length} 个 page 的 page_filepath`);\n\n // 阶段 1:文件大小校验(stat 阶段,不读内容)\n await validateFileSize(pages);\n\n // 阶段 2:读取文件内容\n const resolvedPages = await Promise.all(\n pages.map(async (page: Record<string, unknown>, index: number) => {\n const filePath = page.page_filepath;\n if (typeof filePath !== \"string\" || !filePath) {\n // 该 page 没有 page_filepath,保留原样(可能已有 page_content)\n return page;\n }\n\n let fileContent: string;\n try {\n fileContent = await fs.readFile(filePath, \"utf-8\");\n } catch (err) {\n throw new Error(\n `smartpage_create: pages[${index}] 无法读取文件 \"${filePath}\": ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n console.log(\n `[mcp] smartpage_create: pages[${index}] 读取成功 \"${filePath}\" (${fileContent.length} chars)`,\n );\n\n // 构造新的 page 对象:填入 page_content,移除 page_filepath\n const { page_filepath: _, ...rest } = page;\n return { ...rest, page_content: fileContent };\n }),\n );\n\n console.log(`[mcp] smartpage_create: 所有 page_filepath 解析完成`);\n\n // 返回修改后的完整 args\n return {\n args: {\n ...ctx.args,\n pages: resolvedPages,\n },\n };\n}\n\n// ============================================================================\n// 拦截器实现\n// ============================================================================\n\nexport const smartpageCreateInterceptor: CallInterceptor = {\n name: \"smartpage-create\",\n\n /** 仅对 doc 品类的 smartpage_create 方法生效 */\n match: (ctx: CallContext) => ctx.category === \"doc\" && ctx.method === \"smartpage_create\",\n\n /** 遍历 pages 数组,逐个读取 page_filepath 填入 page_content */\n beforeCall(ctx: CallContext) {\n const pages = ctx.args.pages;\n if (!Array.isArray(pages) || pages.length === 0) {\n // 没有 pages 数组,不做拦截\n return undefined;\n }\n\n // 检查是否有任何 page 包含 page_filepath\n const hasFilePath = pages.some(\n (p: Record<string, unknown>) => typeof p.page_filepath === \"string\" && p.page_filepath,\n );\n if (!hasFilePath) {\n // 所有 page 都没有 page_filepath(可能直接传了 page_content),不做拦截\n return undefined;\n }\n\n return resolvePages(ctx, pages);\n },\n};\n"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getWeComRuntime } from '../../runtime.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* smartpage_get_export_result 响应拦截器
|
|
3
5
|
*
|
|
@@ -13,11 +15,10 @@
|
|
|
13
15
|
*
|
|
14
16
|
* 这样 LLM 只看到轻量的文件路径信息,Skill 可通过文件路径读取完整内容。
|
|
15
17
|
*/
|
|
16
|
-
import { getWeComRuntime } from "../../runtime.js";
|
|
17
18
|
// ============================================================================
|
|
18
19
|
// 拦截器实现
|
|
19
20
|
// ============================================================================
|
|
20
|
-
|
|
21
|
+
const smartpageExportInterceptor = {
|
|
21
22
|
name: "smartpage-export",
|
|
22
23
|
/** 仅对 doc 品类的 smartpage_get_export_result 方法生效 */
|
|
23
24
|
match: (ctx) => ctx.category === "doc" && ctx.method === "smartpage_get_export_result",
|
|
@@ -85,3 +86,6 @@ async function interceptExportResponse(result) {
|
|
|
85
86
|
}],
|
|
86
87
|
};
|
|
87
88
|
}
|
|
89
|
+
|
|
90
|
+
export { smartpageExportInterceptor };
|
|
91
|
+
//# sourceMappingURL=smartpage-export.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smartpage-export.js","sources":["../../../../../../../src/mcp/interceptors/smartpage-export.ts"],"sourcesContent":["/**\n * smartpage_get_export_result 响应拦截器\n *\n * 核心逻辑:\n * MCP Server 返回的 smartpage_get_export_result 响应中,当 task_done=true 时\n * 会包含 content 字段(markdown 文本内容)。该内容可能很大,直接返回给 LLM\n * 会消耗大量 token。\n *\n * 本拦截器在 afterCall 阶段:\n * 1. 检测 task_done=true 且存在 content 字段\n * 2. 将 content 保存到本地文件(使用与 msg-media 一致的媒体目录)\n * 3. 用 content_path(文件路径)替换 content 字段\n *\n * 这样 LLM 只看到轻量的文件路径信息,Skill 可通过文件路径读取完整内容。\n */\n\nimport { getWeComRuntime } from \"../../runtime.js\";\nimport type { CallInterceptor, CallContext } from \"./types.js\";\n\n// ============================================================================\n// 拦截器实现\n// ============================================================================\n\nexport const smartpageExportInterceptor: CallInterceptor = {\n name: \"smartpage-export\",\n\n /** 仅对 doc 品类的 smartpage_get_export_result 方法生效 */\n match: (ctx: CallContext) => ctx.category === \"doc\" && ctx.method === \"smartpage_get_export_result\",\n\n /** 拦截响应:将 markdown content 保存为本地文件 */\n async afterCall(_ctx: CallContext, result: unknown): Promise<unknown> {\n return interceptExportResponse(result);\n },\n};\n\n// ============================================================================\n// 内部实现\n// ============================================================================\n\n/**\n * 拦截 smartpage_get_export_result 的 MCP 响应\n *\n * 1. 从 MCP result 的 content[].text 中提取业务 JSON\n * 2. 检测 task_done=true 且存在 content 字段\n * 3. 将 content(markdown 文本)通过 saveMediaBuffer 保存到本地媒体目录\n * 4. 构造新响应:移除 content,添加 content_path\n */\nasync function interceptExportResponse(result: unknown): Promise<unknown> {\n // 1. 提取 MCP result 中的 content 数组\n const content = (result as Record<string, unknown>)?.content;\n if (!Array.isArray(content)) return result;\n\n const textItem = content.find(\n (c: Record<string, unknown>) => c.type === \"text\" && typeof c.text === \"string\",\n ) as { type: string; text: string } | undefined;\n if (!textItem) return result;\n\n // 2. 解析业务 JSON\n let bizData: Record<string, unknown>;\n try {\n bizData = JSON.parse(textItem.text) as Record<string, unknown>;\n } catch {\n // 非 JSON 格式,原样返回\n return result;\n }\n\n // 3. 校验:errcode !== 0 或 task_done 不为 true 或无 content 时原样返回\n if (bizData.errcode !== 0) return result;\n if (bizData.task_done !== true) return result;\n if (typeof bizData.content !== \"string\") return result;\n\n const markdownContent = bizData.content as string;\n\n console.log(\n `[mcp] smartpage_get_export_result: 拦截 content (${markdownContent.length} chars),保存到本地文件`,\n );\n\n // 4. 将 markdown 内容通过 saveMediaBuffer 保存到本地媒体目录\n // 使用 text/markdown 类型,与 msg-media 拦截器保持一致的路径管理\n const buffer = Buffer.from(markdownContent, \"utf-8\");\n const core = getWeComRuntime();\n const saved = await core.channel.media.saveMediaBuffer(\n buffer,\n \"text/markdown\",\n \"inbound\",\n undefined, // maxBytes: markdown 文本通常不大,使用默认限制\n \"smartpage_export.md\", // originalFilename\n );\n\n console.log(`[mcp] smartpage_get_export_result: 已保存到 ${saved.path}`);\n\n // 5. 构造新响应:移除 content,添加 content_path\n const newBizData = {\n errcode: bizData.errcode,\n errmsg: bizData.errmsg ?? \"ok\",\n task_done: true,\n content_path: saved.path,\n };\n\n // 6. 返回修改后的 MCP result 结构\n return {\n content: [{\n type: \"text\" as const,\n text: JSON.stringify(newBizData),\n }],\n };\n}\n"],"names":[],"mappings":";;AAAA;;;;;;;;;;;;;;AAcG;AAKH;AACA;AACA;AAEO,MAAM,0BAA0B,GAAoB;AACzD,IAAA,IAAI,EAAE,kBAAkB;;AAGxB,IAAA,KAAK,EAAE,CAAC,GAAgB,KAAK,GAAG,CAAC,QAAQ,KAAK,KAAK,IAAI,GAAG,CAAC,MAAM,KAAK,6BAA6B;;AAGnG,IAAA,MAAM,SAAS,CAAC,IAAiB,EAAE,MAAe,EAAA;AAChD,QAAA,OAAO,uBAAuB,CAAC,MAAM,CAAC;IACxC,CAAC;;AAGH;AACA;AACA;AAEA;;;;;;;AAOG;AACH,eAAe,uBAAuB,CAAC,MAAe,EAAA;;AAEpD,IAAA,MAAM,OAAO,GAAI,MAAkC,EAAE,OAAO;AAC5D,IAAA,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;AAAE,QAAA,OAAO,MAAM;IAE1C,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAC3B,CAAC,CAA0B,KAAK,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAClC;AAC/C,IAAA,IAAI,CAAC,QAAQ;AAAE,QAAA,OAAO,MAAM;;AAG5B,IAAA,IAAI,OAAgC;AACpC,IAAA,IAAI;QACF,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAA4B;IAChE;AAAE,IAAA,MAAM;;AAEN,QAAA,OAAO,MAAM;IACf;;AAGA,IAAA,IAAI,OAAO,CAAC,OAAO,KAAK,CAAC;AAAE,QAAA,OAAO,MAAM;AACxC,IAAA,IAAI,OAAO,CAAC,SAAS,KAAK,IAAI;AAAE,QAAA,OAAO,MAAM;AAC7C,IAAA,IAAI,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ;AAAE,QAAA,OAAO,MAAM;AAEtD,IAAA,MAAM,eAAe,GAAG,OAAO,CAAC,OAAiB;IAEjD,OAAO,CAAC,GAAG,CACT,CAAA,+CAAA,EAAkD,eAAe,CAAC,MAAM,CAAA,eAAA,CAAiB,CAC1F;;;IAID,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC;AACpD,IAAA,MAAM,IAAI,GAAG,eAAe,EAAE;AAC9B,IAAA,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,eAAe,CACpD,MAAM,EACN,eAAe,EACf,SAAS,EACT,SAAS;AACT,IAAA,qBAAqB,CACtB;IAED,OAAO,CAAC,GAAG,CAAC,CAAA,wCAAA,EAA2C,KAAK,CAAC,IAAI,CAAA,CAAE,CAAC;;AAGpE,IAAA,MAAM,UAAU,GAAG;QACjB,OAAO,EAAE,OAAO,CAAC,OAAO;AACxB,QAAA,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,IAAI;AAC9B,QAAA,SAAS,EAAE,IAAI;QACf,YAAY,EAAE,KAAK,CAAC,IAAI;KACzB;;IAGD,OAAO;AACL,QAAA,OAAO,EAAE,CAAC;AACR,gBAAA,IAAI,EAAE,MAAe;AACrB,gBAAA,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC;aACjC,CAAC;KACH;AACH;;;;"}
|
|
@@ -15,7 +15,7 @@ const GEMINI_UNSUPPORTED_KEYWORDS = new Set([
|
|
|
15
15
|
* 清洗 JSON Schema,内联 $ref 引用并移除 Gemini 不支持的关键词,
|
|
16
16
|
* 防止 Gemini 模型解析 function response 时报 400 错误。
|
|
17
17
|
*/
|
|
18
|
-
|
|
18
|
+
function cleanSchemaForGemini(schema) {
|
|
19
19
|
if (!schema || typeof schema !== "object")
|
|
20
20
|
return schema;
|
|
21
21
|
if (Array.isArray(schema))
|
|
@@ -97,3 +97,6 @@ function cleanWithDefs(schema, defs, refStack) {
|
|
|
97
97
|
}
|
|
98
98
|
return cleaned;
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
export { cleanSchemaForGemini };
|
|
102
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sources":["../../../../../../src/mcp/schema.ts"],"sourcesContent":["/**\n * MCP Schema 清洗模块\n *\n * 负责内联 $ref/$defs 引用并移除 Gemini 不支持的 JSON Schema 关键词,\n * 防止 Gemini 模型解析 function response 时报 400 错误。\n */\n\n/** Gemini 不支持的 JSON Schema 关键词 */\nconst GEMINI_UNSUPPORTED_KEYWORDS = new Set([\n \"patternProperties\", \"additionalProperties\", \"$schema\", \"$id\", \"$ref\", \"$defs\",\n \"definitions\", \"examples\", \"minLength\", \"maxLength\", \"minimum\", \"maximum\",\n \"multipleOf\", \"pattern\", \"format\", \"minItems\", \"maxItems\", \"uniqueItems\",\n \"minProperties\", \"maxProperties\",\n]);\n\n/**\n * 清洗 JSON Schema,内联 $ref 引用并移除 Gemini 不支持的关键词,\n * 防止 Gemini 模型解析 function response 时报 400 错误。\n */\nexport function cleanSchemaForGemini(schema: unknown): unknown {\n if (!schema || typeof schema !== \"object\") return schema;\n if (Array.isArray(schema)) return schema.map(cleanSchemaForGemini);\n\n const obj = schema as Record<string, unknown>;\n\n // 收集 $defs/definitions 用于后续 $ref 内联解析\n const defs: Record<string, unknown> = {\n ...(obj.$defs && typeof obj.$defs === \"object\" ? obj.$defs as Record<string, unknown> : {}),\n ...(obj.definitions && typeof obj.definitions === \"object\" ? obj.definitions as Record<string, unknown> : {}),\n };\n\n return cleanWithDefs(obj, defs, new Set());\n}\n\nfunction cleanWithDefs(\n schema: unknown,\n defs: Record<string, unknown>,\n refStack: Set<string>,\n): unknown {\n if (!schema || typeof schema !== \"object\") return schema;\n if (Array.isArray(schema)) return schema.map((item) => cleanWithDefs(item, defs, refStack));\n\n const obj = schema as Record<string, unknown>;\n\n // 合并当前层级的 $defs/definitions 到 defs 中\n if (obj.$defs && typeof obj.$defs === \"object\") {\n Object.assign(defs, obj.$defs as Record<string, unknown>);\n }\n if (obj.definitions && typeof obj.definitions === \"object\") {\n Object.assign(defs, obj.definitions as Record<string, unknown>);\n }\n\n // 处理 $ref 引用:尝试内联解析\n if (typeof obj.$ref === \"string\") {\n const ref = obj.$ref;\n if (refStack.has(ref)) return {}; // 防止循环引用\n\n const match = ref.match(/^#\\/(?:\\$defs|definitions)\\/(.+)$/);\n if (match && match[1] && defs[match[1]]) {\n const nextStack = new Set(refStack);\n nextStack.add(ref);\n return cleanWithDefs(defs[match[1]], defs, nextStack);\n }\n return {}; // 无法解析的 $ref,返回空对象\n }\n\n const cleaned: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (GEMINI_UNSUPPORTED_KEYWORDS.has(key)) continue;\n\n if (key === \"const\") {\n cleaned.enum = [value];\n continue;\n }\n\n if (key === \"properties\" && value && typeof value === \"object\" && !Array.isArray(value)) {\n cleaned[key] = Object.fromEntries(\n Object.entries(value as Record<string, unknown>).map(([k, v]) => [\n k, cleanWithDefs(v, defs, refStack),\n ]),\n );\n } else if (key === \"items\" && value) {\n cleaned[key] = Array.isArray(value)\n ? value.map((item) => cleanWithDefs(item, defs, refStack))\n : cleanWithDefs(value, defs, refStack);\n } else if ((key === \"anyOf\" || key === \"oneOf\" || key === \"allOf\") && Array.isArray(value)) {\n // 过滤掉 null 类型的变体\n const nonNull = value.filter((v) => {\n if (!v || typeof v !== \"object\") return true;\n const r = v as Record<string, unknown>;\n return r.type !== \"null\";\n });\n if (nonNull.length === 1) {\n // 只剩一个变体时直接内联\n const single = cleanWithDefs(nonNull[0], defs, refStack);\n if (single && typeof single === \"object\" && !Array.isArray(single)) {\n Object.assign(cleaned, single as Record<string, unknown>);\n }\n } else {\n cleaned[key] = nonNull.map((v) => cleanWithDefs(v, defs, refStack));\n }\n } else {\n cleaned[key] = value;\n }\n }\n\n return cleaned;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { sendJsonRpc } from './transport.js';
|
|
2
|
+
import { cleanSchemaForGemini } from './schema.js';
|
|
3
|
+
import { resolveBeforeCall, runAfterCall } from './interceptors/index.js';
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* wecom_mcp — 模拟 MCP 调用的 Agent Tool
|
|
3
7
|
*
|
|
@@ -12,9 +16,6 @@
|
|
|
12
16
|
* wecom_mcp list contact
|
|
13
17
|
* wecom_mcp call contact getContact '{}'
|
|
14
18
|
*/
|
|
15
|
-
import { sendJsonRpc } from "./transport.js";
|
|
16
|
-
import { cleanSchemaForGemini } from "./schema.js";
|
|
17
|
-
import { resolveBeforeCall, runAfterCall } from "./interceptors/index.js";
|
|
18
19
|
// ============================================================================
|
|
19
20
|
// 响应构造辅助
|
|
20
21
|
// ============================================================================
|
|
@@ -122,7 +123,7 @@ const parseArgs = (args) => {
|
|
|
122
123
|
/**
|
|
123
124
|
* 创建 wecom_mcp Agent Tool 定义
|
|
124
125
|
*/
|
|
125
|
-
|
|
126
|
+
function createWeComMcpTool() {
|
|
126
127
|
return {
|
|
127
128
|
name: "wecom_mcp",
|
|
128
129
|
label: "企业微信 MCP 工具",
|
|
@@ -200,3 +201,6 @@ export function createWeComMcpTool() {
|
|
|
200
201
|
},
|
|
201
202
|
};
|
|
202
203
|
}
|
|
204
|
+
|
|
205
|
+
export { createWeComMcpTool };
|
|
206
|
+
//# sourceMappingURL=tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool.js","sources":["../../../../../../src/mcp/tool.ts"],"sourcesContent":["/**\n * wecom_mcp — 模拟 MCP 调用的 Agent Tool\n *\n * 通过 MCP Streamable HTTP 传输协议调用企业微信 MCP Server,\n * 提供 list(列出所有工具)和 call(调用工具)两个操作。\n *\n * 在 skills 中的使用方式:\n * wecom_mcp list <category>\n * wecom_mcp call <category> <method> '<jsonArgs>'\n *\n * 示例:\n * wecom_mcp list contact\n * wecom_mcp call contact getContact '{}'\n */\n\nimport { sendJsonRpc, type McpToolInfo } from \"./transport.js\";\nimport { cleanSchemaForGemini } from \"./schema.js\";\nimport { resolveBeforeCall, runAfterCall } from \"./interceptors/index.js\";\n\n// ============================================================================\n// 类型定义\n// ============================================================================\n\n/** wecom_mcp 的入参 */\ninterface WeComToolsParams {\n /** 操作类型:list | call */\n action: \"list\" | \"call\";\n /** MCP 品类,对应 mcpConfig 中的 key,如 doc、contact */\n category: string;\n /** 调用的 MCP 方法名(action=call 时必填) */\n method?: string;\n /** 调用 MCP 方法的 JSON 参数(action=call 时使用) */\n args?: string | Record<string, unknown>;\n}\n\n// ============================================================================\n// 响应构造辅助\n// ============================================================================\n\n/** 构造统一的文本响应结构 */\nconst textResult = (data: unknown) => ({\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n});\n\n/** 构造错误响应 */\nconst errorResult = (err: unknown) => {\n // 适配企业微信 API 返回的 { errcode, errmsg } 结构\n if (err && typeof err === \"object\" && \"errcode\" in err) {\n const { errcode, errmsg } = err as { errcode: number; errmsg?: string };\n return textResult({ error: errmsg ?? `错误码: ${errcode}`, errcode });\n }\n\n const message = err instanceof Error ? err.message : String(err);\n return textResult({ error: message });\n};\n\n// ============================================================================\n// list 操作:列出某品类的所有 MCP 工具\n// ============================================================================\n\nconst handleList = async (category: string): Promise<unknown> => {\n const result = await sendJsonRpc(category, \"tools/list\") as { tools?: McpToolInfo[] } | undefined;\n\n const tools = result?.tools ?? [];\n if (tools.length === 0) {\n return { message: `品类 \"${category}\" 下暂无可用工具`, tools: [] };\n }\n\n return {\n category,\n count: tools.length,\n tools: tools.map((t) => ({\n name: t.name,\n description: t.description ?? \"\",\n // 清洗 inputSchema,内联 $ref/$defs 引用并移除 Gemini 不支持的关键词,\n // 避免 Gemini 模型解析 function response 时报 400 错误\n inputSchema: t.inputSchema ? cleanSchemaForGemini(t.inputSchema) : undefined,\n })),\n };\n};\n\n// ============================================================================\n// call 操作:调用某品类的某个 MCP 工具\n// ============================================================================\n\nconst handleCall = async (\n category: string,\n method: string,\n args: Record<string, unknown>,\n): Promise<unknown> => {\n const ctx = { category, method, args };\n const callStart = performance.now();\n\n console.log(`[mcp] handleCall ${category}/${method} 入参: ${JSON.stringify(args)}`);\n\n // 1. 收集拦截器的 beforeCall 配置(如超时时间、替换 args)\n const { options, args: resolvedArgs } = await resolveBeforeCall(ctx);\n const finalArgs = resolvedArgs ?? args;\n\n if (resolvedArgs) {\n console.log(\n `[mcp] handleCall ${category}/${method} 拦截器替换 args: ${JSON.stringify(resolvedArgs).slice(0, 500)}` +\n (JSON.stringify(resolvedArgs).length > 500 ? \"...(truncated)\" : \"\"),\n );\n }\n if (options) {\n console.log(`[mcp] handleCall ${category}/${method} 拦截器选项: ${JSON.stringify(options)}`);\n }\n\n // 2. 执行 MCP 调用\n const result = await sendJsonRpc(category, \"tools/call\", {\n name: method,\n arguments: finalArgs,\n }, options);\n\n const rpcDone = performance.now();\n const rpcMs = (rpcDone - callStart).toFixed(1);\n\n const resultStr = JSON.stringify(result);\n console.log(\n `[mcp] handleCall ${category}/${method} MCP 响应 (${rpcMs}ms): ${resultStr.slice(0, 800)}` +\n (resultStr.length > 800 ? \"...(truncated)\" : \"\"),\n );\n\n // 3. 管道式执行 afterCall 拦截器(业务错误码检查、响应变换等)\n const finalResult = await runAfterCall(ctx, result);\n\n const totalMs = (performance.now() - callStart).toFixed(1);\n const interceptMs = (performance.now() - rpcDone).toFixed(1);\n\n // 有拦截器处理时打印详细耗时,否则只打印 RPC 耗时\n if (finalResult !== result) {\n const finalStr = JSON.stringify(finalResult);\n console.log(\n `[mcp] handleCall ${category}/${method} afterCall 变换后 (${interceptMs}ms): ${finalStr.slice(0, 500)}` +\n (finalStr.length > 500 ? \"...(truncated)\" : \"\"),\n );\n console.log(\n `[mcp] handleCall ${category}/${method} 总耗时: ${totalMs}ms` +\n ` (MCP请求: ${rpcMs}ms, 拦截处理: ${interceptMs}ms)`,\n );\n } else {\n console.log(`[mcp] handleCall ${category}/${method} 耗时: ${rpcMs}ms`);\n }\n\n return finalResult;\n};\n\n// ============================================================================\n// 参数解析\n// ============================================================================\n\n/**\n * 解析 args 参数:支持 JSON 字符串或直接的对象\n */\nconst parseArgs = (args: string | Record<string, unknown> | undefined): Record<string, unknown> => {\n if (!args) return {};\n if (typeof args === \"object\") return args;\n try {\n return JSON.parse(args) as Record<string, unknown>;\n } catch (err) {\n const detail = err instanceof SyntaxError ? err.message : String(err);\n throw new Error(`args 参数不是合法的 JSON: ${args} (${detail})`);\n }\n};\n\n// ============================================================================\n// 工具定义 & 导出\n// ============================================================================\n\n/**\n * 创建 wecom_mcp Agent Tool 定义\n */\nexport function createWeComMcpTool() {\n return {\n name: \"wecom_mcp\",\n label: \"企业微信 MCP 工具\",\n description: [\n \"通过 HTTP 直接调用企业微信 MCP Server。\",\n \"支持两种操作:\",\n \" - list: 列出指定品类的所有 MCP 工具\",\n \" - call: 调用指定品类的某个 MCP 工具\",\n \"\",\n \"使用方式:\",\n \" wecom_mcp list <category>\",\n \" wecom_mcp call <category> <method> '<jsonArgs>'\",\n \"\",\n \"示例:\",\n \" 列出 contact 品类所有工具:wecom_mcp list contact\",\n \" 调用 contact 的 getContact:wecom_mcp call contact getContact '{}'\",\n ].join(\"\\n\"),\n parameters: {\n type: \"object\" as const,\n properties: {\n action: {\n type: \"string\",\n enum: [\"list\", \"call\"],\n description: \"操作类型:list(列出工具)或 call(调用工具)\",\n },\n category: {\n type: \"string\",\n description: \"MCP 品类名称,如 doc、contact 等,对应 mcpConfig 中的 key\",\n },\n method: {\n type: \"string\",\n description: \"要调用的 MCP 方法名(action=call 时必填)\",\n },\n args: {\n type: [\"string\", \"object\"],\n description: \"调用 MCP 方法的参数,可以是 JSON 字符串或对象(action=call 时使用,默认 {})\",\n },\n },\n required: [\"action\", \"category\"],\n },\n async execute(_toolCallId: string, params: unknown) {\n const p = params as WeComToolsParams;\n console.log(\n `[mcp] execute: action=${p.action}, category=${p.category}` +\n (p.method ? `, method=${p.method}` : \"\") +\n (p.args ? `, args=${typeof p.args === \"string\" ? p.args : JSON.stringify(p.args)}` : \"\"),\n );\n try {\n let result: ReturnType<typeof textResult>;\n switch (p.action) {\n case \"list\":\n result = textResult(await handleList(p.category));\n break;\n case \"call\": {\n if (!p.method) {\n result = textResult({ error: \"action 为 call 时必须提供 method 参数\" });\n break;\n }\n const args = parseArgs(p.args);\n result = textResult(await handleCall(p.category, p.method, args));\n break;\n }\n default:\n result = textResult({ error: `未知操作类型: ${String(p.action)},支持 list 和 call` });\n }\n console.log(\n `[mcp] execute: action=${p.action}, category=${p.category}` +\n (p.method ? `, method=${p.method}` : \"\") +\n ` → 响应长度=${result.content[0].text.length} chars`,\n );\n return result;\n } catch (err) {\n console.error(\n `[mcp] execute: action=${p.action}, category=${p.category}` +\n (p.method ? `, method=${p.method}` : \"\") +\n ` → 异常: ${err instanceof Error ? err.message : String(err)}`,\n );\n return errorResult(err);\n }\n },\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { generateReqId } from '@wecom/aibot-node-sdk';
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID } from '../openclaw-compat.js';
|
|
3
|
+
import { getWeComWebSocket } from '../state-manager.js';
|
|
4
|
+
import { MCP_CONFIG_FETCH_TIMEOUT_MS, MCP_GET_CONFIG_CMD } from '../const.js';
|
|
5
|
+
import { withTimeout } from '../timeout.js';
|
|
6
|
+
import { PLUGIN_VERSION } from '../version.js';
|
|
7
|
+
|
|
1
8
|
/**
|
|
2
9
|
* MCP Streamable HTTP 传输层模块
|
|
3
10
|
*
|
|
@@ -9,19 +16,13 @@
|
|
|
9
16
|
* - SSE 流式响应解析
|
|
10
17
|
* - MCP 配置运行时缓存(通过 WSClient 拉取 URL 并缓存在内存中)
|
|
11
18
|
*/
|
|
12
|
-
import { generateReqId } from "@wecom/aibot-node-sdk";
|
|
13
|
-
import { DEFAULT_ACCOUNT_ID } from "../openclaw-compat.js";
|
|
14
|
-
import { getWeComWebSocket } from "../state-manager.js";
|
|
15
|
-
import { MCP_GET_CONFIG_CMD, MCP_CONFIG_FETCH_TIMEOUT_MS } from "../const.js";
|
|
16
|
-
import { withTimeout } from "../timeout.js";
|
|
17
|
-
import { PLUGIN_VERSION } from "../version.js";
|
|
18
19
|
// ============================================================================
|
|
19
20
|
// 内部状态
|
|
20
21
|
// ============================================================================
|
|
21
22
|
/** HTTP 请求超时时间(毫秒) */
|
|
22
|
-
const HTTP_REQUEST_TIMEOUT_MS =
|
|
23
|
+
const HTTP_REQUEST_TIMEOUT_MS = 30000;
|
|
23
24
|
/** 媒体下载请求超时时间(毫秒),base64 编码的媒体文件最大可达 ~27MB */
|
|
24
|
-
|
|
25
|
+
const MEDIA_DOWNLOAD_TIMEOUT_MS = 120000;
|
|
25
26
|
/** 日志前缀 */
|
|
26
27
|
const LOG_TAG = "[mcp]";
|
|
27
28
|
/**
|
|
@@ -30,9 +31,7 @@ const LOG_TAG = "[mcp]";
|
|
|
30
31
|
* 携带服务端返回的 JSON-RPC error.code,
|
|
31
32
|
* 用于上层按错误码进行差异化处理(如特定错误码触发缓存清理)。
|
|
32
33
|
*/
|
|
33
|
-
|
|
34
|
-
code;
|
|
35
|
-
data;
|
|
34
|
+
class McpRpcError extends Error {
|
|
36
35
|
constructor(code, message, data) {
|
|
37
36
|
super(message);
|
|
38
37
|
this.code = code;
|
|
@@ -46,8 +45,7 @@ export class McpRpcError extends Error {
|
|
|
46
45
|
* 携带 HTTP 状态码,用于精确判断 session 失效(404)等场景,
|
|
47
46
|
* 避免通过字符串匹配 "404" 导致的误判。
|
|
48
47
|
*/
|
|
49
|
-
|
|
50
|
-
statusCode;
|
|
48
|
+
class McpHttpError extends Error {
|
|
51
49
|
constructor(statusCode, message) {
|
|
52
50
|
super(message);
|
|
53
51
|
this.statusCode = statusCode;
|
|
@@ -328,7 +326,7 @@ async function parseSseResponse(response) {
|
|
|
328
326
|
*
|
|
329
327
|
* @param category - MCP 品类名称
|
|
330
328
|
*/
|
|
331
|
-
|
|
329
|
+
function clearCategoryCache(category) {
|
|
332
330
|
console.log(`${LOG_TAG} 清理缓存 (category="${category}")`);
|
|
333
331
|
mcpConfigCache.delete(category);
|
|
334
332
|
mcpSessionCache.delete(category);
|
|
@@ -348,7 +346,7 @@ export function clearCategoryCache(category) {
|
|
|
348
346
|
* @param options - 可选配置(如自定义超时)
|
|
349
347
|
* @returns JSON-RPC result
|
|
350
348
|
*/
|
|
351
|
-
|
|
349
|
+
async function sendJsonRpc(category, method, params, options) {
|
|
352
350
|
const url = await getMcpUrl(category);
|
|
353
351
|
const timeoutMs = options?.timeoutMs;
|
|
354
352
|
const body = {
|
|
@@ -408,3 +406,6 @@ async function rebuildSession(url, category) {
|
|
|
408
406
|
inflightInitRequests.set(category, promise);
|
|
409
407
|
return promise;
|
|
410
408
|
}
|
|
409
|
+
|
|
410
|
+
export { MEDIA_DOWNLOAD_TIMEOUT_MS, McpHttpError, McpRpcError, clearCategoryCache, sendJsonRpc };
|
|
411
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sources":["../../../../../../src/mcp/transport.ts"],"sourcesContent":["/**\n * MCP Streamable HTTP 传输层模块\n *\n * 负责:\n * - MCP JSON-RPC over HTTP 通信(发送请求、解析响应)\n * - Streamable HTTP session 生命周期管理(initialize 握手 → Mcp-Session-Id 维护 → 失效重建)\n * - 自动检测无状态 Server:如果 initialize 响应未返回 Mcp-Session-Id,\n * 则标记为无状态模式,后续请求跳过握手和 session 管理\n * - SSE 流式响应解析\n * - MCP 配置运行时缓存(通过 WSClient 拉取 URL 并缓存在内存中)\n */\n\nimport { generateReqId } from \"@wecom/aibot-node-sdk\";\nimport { DEFAULT_ACCOUNT_ID } from \"../openclaw-compat.js\";\nimport { getWeComWebSocket } from \"../state-manager.js\";\nimport { MCP_GET_CONFIG_CMD, MCP_CONFIG_FETCH_TIMEOUT_MS } from \"../const.js\";\nimport { withTimeout } from \"../timeout.js\";\nimport { PLUGIN_VERSION } from \"../version.js\";\n\n// ============================================================================\n// 类型定义\n// ============================================================================\n\n/** MCP JSON-RPC 请求体 */\ninterface JsonRpcRequest {\n jsonrpc: \"2.0\";\n id?: string;\n method: string;\n params?: Record<string, unknown>;\n}\n\n/** MCP JSON-RPC 响应体 */\ninterface JsonRpcResponse {\n jsonrpc: \"2.0\";\n id: number | string;\n result?: unknown;\n error?: {\n code: number;\n message: string;\n data?: unknown;\n };\n}\n\n/**\n * Streamable HTTP 会话信息\n *\n * 每个 MCP Server category 维护一个独立的会话,包含:\n * - sessionId: 服务端通过 Mcp-Session-Id 响应头返回的会话标识\n * - initialized: 是否已完成 initialize 握手\n * - stateless: 服务端未返回 Mcp-Session-Id 时标记为无状态模式,后续请求跳过 session 管理\n */\ninterface McpSession {\n sessionId: string | null;\n initialized: boolean;\n stateless: boolean;\n}\n\n// ============================================================================\n// 内部状态\n// ============================================================================\n\n/** HTTP 请求超时时间(毫秒) */\nconst HTTP_REQUEST_TIMEOUT_MS = 30_000;\n\n/** 媒体下载请求超时时间(毫秒),base64 编码的媒体文件最大可达 ~27MB */\nexport const MEDIA_DOWNLOAD_TIMEOUT_MS = 120_000;\n\n/** 日志前缀 */\nconst LOG_TAG = \"[mcp]\";\n\n/**\n * MCP JSON-RPC 错误\n *\n * 携带服务端返回的 JSON-RPC error.code,\n * 用于上层按错误码进行差异化处理(如特定错误码触发缓存清理)。\n */\nexport class McpRpcError extends Error {\n constructor(\n public readonly code: number,\n message: string,\n public readonly data?: unknown,\n ) {\n super(message);\n this.name = \"McpRpcError\";\n }\n}\n\n/**\n * MCP HTTP 错误\n *\n * 携带 HTTP 状态码,用于精确判断 session 失效(404)等场景,\n * 避免通过字符串匹配 \"404\" 导致的误判。\n */\nexport class McpHttpError extends Error {\n constructor(\n public readonly statusCode: number,\n message: string,\n ) {\n super(message);\n this.name = \"McpHttpError\";\n }\n}\n\n/**\n * 需要清理缓存的 JSON-RPC 错误码集合\n *\n * 当 MCP Server 返回以下错误码时,说明服务端状态已发生变化(如配置变更、\n * 服务重启等),需要清理对应 category 的全部缓存,确保下次请求重新\n * 拉取配置并重建会话。\n *\n * - -32001: 服务不可用(Server Unavailable)\n * - -32002: 配置已变更(Config Changed)\n * - -32003: 认证失败(Auth Failed)\n */\nconst CACHE_CLEAR_ERROR_CODES = new Set([-32001, -32002, -32003]);\n\n/** MCP 配置缓存:category → response.body(完整配置) */\nconst mcpConfigCache = new Map<string, Record<string, unknown>>();\n\n/** Streamable HTTP 会话缓存:category → session */\nconst mcpSessionCache = new Map<string, McpSession>();\n\n/** 已确认为无状态的 MCP Server 品类集合(跳过后续握手) */\nconst statelessCategories = new Set<string>();\n\n/** 正在进行中的 initialize 请求(防止并发重复初始化),key 为 category */\nconst inflightInitRequests = new Map<string, Promise<McpSession>>();\n\n// ============================================================================\n// MCP 配置拉取与缓存\n// ============================================================================\n\n/**\n * 通过 WSClient 拉取指定 category 的 MCP 完整配置\n *\n * @param category - MCP 品类名称,如 doc、contact\n * @returns 完整的 response.body 配置对象(至少包含 url 字段)\n */\nasync function fetchMcpConfig(category: string): Promise<Record<string, unknown>> {\n const wsClient = getWeComWebSocket(DEFAULT_ACCOUNT_ID);\n if (!wsClient) {\n throw new Error(\"WSClient 未连接,无法拉取 MCP 配置\");\n }\n\n const reqId = generateReqId(\"mcp_config\");\n\n const response = await withTimeout(\n wsClient.reply(\n { headers: { req_id: reqId } },\n { biz_type: category, plugin_version: PLUGIN_VERSION },\n MCP_GET_CONFIG_CMD,\n ),\n MCP_CONFIG_FETCH_TIMEOUT_MS,\n `MCP config fetch for \"${category}\" timed out after ${MCP_CONFIG_FETCH_TIMEOUT_MS}ms`,\n );\n\n if (response.errcode !== undefined && response.errcode !== 0) {\n const errMsg = `MCP 配置请求失败: errcode=${response.errcode}, errmsg=${response.errmsg ?? \"unknown\"}`;\n console.error(`${LOG_TAG} ${errMsg}`);\n throw new Error(errMsg);\n }\n\n const body = response.body as { url?: string } | undefined;\n if (!body?.url) {\n throw new Error(\n `MCP 配置响应缺少 url 字段 (category=\"${category}\")`,\n );\n }\n\n console.log(`${LOG_TAG} 配置拉取成功 (category=\"${category}\")`);\n return body as Record<string, unknown>;\n}\n\n/**\n * 获取指定品类的 MCP Server URL\n *\n * 优先从内存缓存中读取,未命中时通过 WSClient 拉取并缓存。\n *\n * @param category - MCP 品类名称\n * @returns MCP Server URL\n */\nasync function getMcpUrl(category: string): Promise<string> {\n // 查内存缓存\n const cached = mcpConfigCache.get(category);\n if (cached) return cached.url as string;\n\n // 缓存未命中,通过 WSClient 拉取\n const body = await fetchMcpConfig(category);\n\n // 写入缓存\n mcpConfigCache.set(category, body);\n\n console.log(`${LOG_TAG} getMcpUrl ${category}: ${body.url}`);\n\n return body.url as string;\n}\n\n// ============================================================================\n// HTTP 底层通信\n// ============================================================================\n\n/**\n * 发送原始 HTTP 请求到 MCP Server(底层方法)\n *\n * 自动携带 Mcp-Session-Id 请求头(如果有),\n * 并从响应头中更新 sessionId。\n */\nasync function sendRawJsonRpc(\n url: string,\n session: McpSession,\n body: JsonRpcRequest,\n timeoutMs: number = HTTP_REQUEST_TIMEOUT_MS,\n): Promise<{ response: Response; rpcResult: unknown; newSessionId: string | null }> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json, text/event-stream\",\n };\n // Streamable HTTP:携带会话 ID\n if (session.sessionId) {\n headers[\"Mcp-Session-Id\"] = session.sessionId;\n }\n\n let response: Response;\n try {\n response = await fetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n } catch (err) {\n if (err instanceof DOMException && err.name === \"AbortError\") {\n throw new Error(`MCP 请求超时 (${timeoutMs}ms)`);\n }\n throw new Error(`MCP 网络请求失败: ${err instanceof Error ? err.message : String(err)}`);\n } finally {\n clearTimeout(timeoutId);\n }\n\n // 从响应头提取新的 sessionId(不直接修改入参,由调用方决定如何更新)\n const newSessionId = response.headers.get(\"mcp-session-id\");\n\n if (!response.ok) {\n throw new McpHttpError(\n response.status,\n `MCP HTTP 请求失败: ${response.status} ${response.statusText}`,\n );\n }\n\n // Streamable HTTP:notification 响应可能无响应体(204 或 content-length: 0)\n const contentLength = response.headers.get(\"content-length\");\n if (response.status === 204 || contentLength === \"0\") {\n return { response, rpcResult: undefined, newSessionId };\n }\n\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n\n // 处理 SSE 流式响应\n if (contentType.includes(\"text/event-stream\")) {\n return { response, rpcResult: await parseSseResponse(response), newSessionId };\n }\n\n // 普通 JSON 响应 — 先读取文本,防止空内容导致 JSON.parse 报错\n const text = await response.text();\n if (!text.trim()) {\n return { response, rpcResult: undefined, newSessionId };\n }\n\n const rpc = JSON.parse(text) as JsonRpcResponse;\n if (rpc.error) {\n throw new McpRpcError(\n rpc.error.code,\n `MCP 调用错误 [${rpc.error.code}]: ${rpc.error.message}`,\n rpc.error.data,\n );\n }\n return { response, rpcResult: rpc.result, newSessionId };\n}\n\n// ============================================================================\n// Session 管理\n// ============================================================================\n\n/**\n * 对指定 URL 执行 Streamable HTTP 的 initialize 握手\n *\n * 发送 initialize → 接收 serverInfo → 发送 initialized 通知。\n * 如果服务端未返回 Mcp-Session-Id,则标记为无状态模式,后续请求跳过 session 管理。\n */\nasync function initializeSession(url: string, category: string): Promise<McpSession> {\n const session: McpSession = { sessionId: null, initialized: false, stateless: false };\n\n console.log(`${LOG_TAG} 开始 initialize 握手 (category=\"${category}\")`);\n\n // 1. 发送 initialize 请求\n const initBody: JsonRpcRequest = {\n jsonrpc: \"2.0\",\n id: generateReqId(\"mcp_init\"),\n method: \"initialize\",\n params: {\n protocolVersion: \"2025-03-26\",\n capabilities: {},\n clientInfo: { name: \"wecom_mcp\", version: \"1.0.0\" },\n },\n };\n\n const { newSessionId: initSessionId } = await sendRawJsonRpc(url, session, initBody);\n\n // 用返回的 newSessionId 更新 session(不再依赖副作用修改)\n if (initSessionId) {\n session.sessionId = initSessionId;\n }\n\n // 检查服务端是否返回了 Mcp-Session-Id\n // 如果没有返回,说明该 Server 是无状态实现,无需维护 session\n if (!session.sessionId) {\n session.stateless = true;\n session.initialized = true;\n statelessCategories.add(category);\n mcpSessionCache.set(category, session);\n console.log(`${LOG_TAG} 无状态 Server 确认 (category=\"${category}\")`);\n return session;\n }\n\n // 2. 发送 initialized 通知(JSON-RPC notification 不带 id 字段)\n const notifyBody: JsonRpcRequest = {\n jsonrpc: \"2.0\",\n method: \"notifications/initialized\",\n };\n // initialized 通知不需要等待响应,但 Streamable HTTP 要求通过 POST 发送\n const { newSessionId: notifySessionId } = await sendRawJsonRpc(url, session, notifyBody);\n\n // 如果 initialized 通知的响应也携带了 sessionId,以最新的为准\n if (notifySessionId) {\n session.sessionId = notifySessionId;\n }\n\n session.initialized = true;\n mcpSessionCache.set(category, session);\n console.log(`${LOG_TAG} 有状态 Session 建立成功 (category=\"${category}\", sessionId=\"${session.sessionId}\")`);\n return session;\n}\n\n/**\n * 获取或创建指定 URL 的 MCP 会话\n *\n * - 已确认无状态的 category:直接返回空 session,跳过握手\n * - 已有可用有状态会话:直接返回缓存\n * - 其他情况:执行 initialize 握手,并发请求会被合并\n */\nasync function getOrCreateSession(url: string, category: string): Promise<McpSession> {\n // 已确认为无状态的 Server,直接返回空 session 跳过握手\n if (statelessCategories.has(category)) {\n const cached = mcpSessionCache.get(category);\n if (cached) return cached;\n // 首次发现被清除(理论上不会走到这里),重新走握手探测\n }\n\n const cached = mcpSessionCache.get(category);\n if (cached?.initialized) return cached;\n\n // 防止并发重复初始化\n const inflight = inflightInitRequests.get(category);\n if (inflight) return inflight;\n\n const promise = initializeSession(url, category).finally(() => {\n inflightInitRequests.delete(category);\n });\n inflightInitRequests.set(category, promise);\n return promise;\n}\n\n// ============================================================================\n// SSE 解析\n// ============================================================================\n\n/**\n * 解析 SSE 流式响应,提取最终的 JSON-RPC result\n *\n * 按照 SSE 规范,同一事件中的多个 `data:` 行会用换行符拼接。\n * 空行分隔不同事件,取最后一个完整事件的数据。\n */\nasync function parseSseResponse(response: Response): Promise<unknown> {\n const text = await response.text();\n const lines = text.split(\"\\n\");\n\n // 按 SSE 规范解析:空行分隔事件,同一事件内的 data 行用换行拼接\n let currentDataParts: string[] = [];\n let lastEventData = \"\";\n\n for (const line of lines) {\n if (line.startsWith(\"data: \")) {\n currentDataParts.push(line.slice(6));\n } else if (line.startsWith(\"data:\")) {\n // data: 后无空格时,值为空字符串\n currentDataParts.push(line.slice(5));\n } else if (line.trim() === \"\" && currentDataParts.length > 0) {\n // 空行表示事件结束,拼接所有 data 行\n lastEventData = currentDataParts.join(\"\\n\").trim();\n currentDataParts = [];\n }\n }\n\n // 处理最后一个未以空行结尾的事件\n if (currentDataParts.length > 0) {\n lastEventData = currentDataParts.join(\"\\n\").trim();\n }\n\n if (!lastEventData) {\n throw new Error(\"SSE 响应中未包含有效数据\");\n }\n\n try {\n const rpc = JSON.parse(lastEventData) as JsonRpcResponse;\n if (rpc.error) {\n throw new McpRpcError(\n rpc.error.code,\n `MCP 调用错误 [${rpc.error.code}]: ${rpc.error.message}`,\n rpc.error.data,\n );\n }\n return rpc.result;\n } catch (err) {\n if (err instanceof SyntaxError) {\n throw new Error(`SSE 响应解析失败: ${lastEventData.slice(0, 200)}`);\n }\n throw err;\n }\n}\n\n// ============================================================================\n// 公共 API\n// ============================================================================\n\n/**\n * 清理指定品类的所有 MCP 缓存(配置、会话、无状态标记)\n *\n * 当 MCP Server 返回特定错误码时调用,确保下次请求重新拉取配置并重建会话。\n *\n * @param category - MCP 品类名称\n */\nexport function clearCategoryCache(category: string): void {\n console.log(`${LOG_TAG} 清理缓存 (category=\"${category}\")`);\n mcpConfigCache.delete(category);\n mcpSessionCache.delete(category);\n statelessCategories.delete(category);\n inflightInitRequests.delete(category);\n}\n\n/** tools/list 返回的工具描述 */\nexport interface McpToolInfo {\n name: string;\n description?: string;\n inputSchema?: Record<string, unknown>;\n}\n\n/** sendJsonRpc 的可选配置 */\nexport interface SendJsonRpcOptions {\n /** 自定义 HTTP 请求超时时间(毫秒),默认使用 HTTP_REQUEST_TIMEOUT_MS */\n timeoutMs?: number;\n}\n\n/**\n * 发送 JSON-RPC 请求到 MCP Server(Streamable HTTP 协议)\n *\n * 自动管理 session 生命周期:\n * - 无状态 Server:跳过 session 管理,直接发送请求\n * - 有状态 Server:首次调用先执行 initialize 握手,session 失效(404)时自动重建并重试\n *\n * @param category - MCP 品类名称\n * @param method - JSON-RPC 方法名\n * @param params - JSON-RPC 参数\n * @param options - 可选配置(如自定义超时)\n * @returns JSON-RPC result\n */\nexport async function sendJsonRpc(\n category: string,\n method: string,\n params?: Record<string, unknown>,\n options?: SendJsonRpcOptions,\n): Promise<unknown> {\n const url = await getMcpUrl(category);\n const timeoutMs = options?.timeoutMs;\n\n const body: JsonRpcRequest = {\n jsonrpc: \"2.0\",\n id: generateReqId(\"mcp_rpc\"),\n method,\n ...(params !== undefined ? { params } : {}),\n };\n\n let session = await getOrCreateSession(url, category);\n\n try {\n const { rpcResult, newSessionId } = await sendRawJsonRpc(url, session, body, timeoutMs);\n // 用最新的 sessionId 更新 session\n if (newSessionId) {\n session.sessionId = newSessionId;\n }\n return rpcResult;\n } catch (err) {\n // 特定 JSON-RPC 错误码触发缓存清理(统一在传输层处理,上层无需关心)\n if (err instanceof McpRpcError && CACHE_CLEAR_ERROR_CODES.has(err.code)) {\n clearCategoryCache(category);\n }\n\n // 无状态 Server 不存在 session 失效问题,直接抛出错误\n if (session.stateless) throw err;\n\n // 有状态 Server:session 失效时服务端返回 404,需要重新初始化并重试一次\n // 使用 McpHttpError.statusCode 精确匹配,避免字符串匹配 \"404\" 导致误判\n if (err instanceof McpHttpError && err.statusCode === 404) {\n console.log(`${LOG_TAG} Session 失效 (category=\"${category}\"),开始重建...`);\n mcpSessionCache.delete(category);\n\n // 使用 rebuildSession 合并并发的 session 重建请求,避免竞态条件\n session = await rebuildSession(url, category);\n const { rpcResult, newSessionId } = await sendRawJsonRpc(url, session, body, timeoutMs);\n if (newSessionId) {\n session.sessionId = newSessionId;\n }\n return rpcResult;\n }\n\n // 其他错误记录日志后抛出\n console.error(`${LOG_TAG} RPC 请求失败 (category=\"${category}\", method=\"${method}\"): ${err instanceof Error ? err.message : String(err)}`);\n throw err;\n }\n}\n\n/**\n * 合并并发的 session 重建请求\n *\n * 与 getOrCreateSession 类似,使用 inflightInitRequests 防止\n * 多个并发请求同时遇到 404 时重复执行 initialize 握手。\n */\nasync function rebuildSession(url: string, category: string): Promise<McpSession> {\n const inflight = inflightInitRequests.get(category);\n if (inflight) return inflight;\n\n const promise = initializeSession(url, category).finally(() => {\n inflightInitRequests.delete(category);\n });\n inflightInitRequests.set(category, promise);\n return promise;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { fileTypeFromBuffer } from 'file-type';
|
|
2
|
+
import { getWeComRuntime } from './runtime.js';
|
|
3
|
+
import { DEFAULT_MEDIA_MAX_MB, IMAGE_DOWNLOAD_TIMEOUT_MS, FILE_DOWNLOAD_TIMEOUT_MS } from './const.js';
|
|
4
|
+
import { withTimeout } from './timeout.js';
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* 企业微信媒体(图片)下载和保存模块
|
|
3
8
|
*
|
|
4
9
|
* 负责下载、检测格式、保存图片到本地,包含超时保护
|
|
5
10
|
*/
|
|
6
|
-
import { fileTypeFromBuffer } from "file-type";
|
|
7
|
-
import { getWeComRuntime } from "./runtime.js";
|
|
8
|
-
import { IMAGE_DOWNLOAD_TIMEOUT_MS, FILE_DOWNLOAD_TIMEOUT_MS, DEFAULT_MEDIA_MAX_MB } from "./const.js";
|
|
9
|
-
import { withTimeout } from "./timeout.js";
|
|
10
11
|
// ============================================================================
|
|
11
12
|
// 图片格式检测辅助函数(基于 file-type 包)
|
|
12
13
|
// ============================================================================
|
|
@@ -33,7 +34,7 @@ async function detectImageContentType(data) {
|
|
|
33
34
|
/**
|
|
34
35
|
* 下载并保存所有图片到本地,每张图片的下载带超时保护
|
|
35
36
|
*/
|
|
36
|
-
|
|
37
|
+
async function downloadAndSaveImages(params) {
|
|
37
38
|
const { imageUrls, config, runtime, wsClient } = params;
|
|
38
39
|
const core = getWeComRuntime();
|
|
39
40
|
const mediaList = [];
|
|
@@ -79,7 +80,7 @@ export async function downloadAndSaveImages(params) {
|
|
|
79
80
|
/**
|
|
80
81
|
* 下载并保存所有文件到本地,每个文件的下载带超时保护
|
|
81
82
|
*/
|
|
82
|
-
|
|
83
|
+
async function downloadAndSaveFiles(params) {
|
|
83
84
|
const { fileUrls, config, runtime, wsClient } = params;
|
|
84
85
|
const core = getWeComRuntime();
|
|
85
86
|
const mediaList = [];
|
|
@@ -120,3 +121,6 @@ export async function downloadAndSaveFiles(params) {
|
|
|
120
121
|
}
|
|
121
122
|
return mediaList;
|
|
122
123
|
}
|
|
124
|
+
|
|
125
|
+
export { downloadAndSaveFiles, downloadAndSaveImages };
|
|
126
|
+
//# sourceMappingURL=media-handler.js.map
|