@wecom/wecom-openclaw-plugin 2026.4.2 → 2026.4.3-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 +395 -10
- package/dist/index.d.ts +2 -4
- package/dist/index.js +58 -0
- package/dist/src/accounts.d.ts +57 -0
- package/dist/src/accounts.js +222 -0
- package/dist/src/agent/api-client.d.ts +94 -0
- package/dist/src/agent/api-client.js +292 -0
- package/dist/src/agent/handler.d.ts +63 -0
- package/dist/src/agent/handler.js +551 -0
- package/dist/src/agent/index.d.ts +5 -0
- package/dist/src/agent/index.js +5 -0
- package/dist/src/agent/webhook.d.ts +24 -0
- package/dist/src/agent/webhook.js +228 -0
- package/dist/src/agent/xml.d.ts +21 -0
- package/dist/src/agent/xml.js +41 -0
- package/dist/src/channel.js +579 -0
- package/dist/src/chat-queue.d.ts +31 -0
- package/dist/src/chat-queue.js +45 -0
- package/dist/{esm/types/src → src}/const.d.ts +46 -0
- package/dist/{esm/src → src}/const.js +77 -30
- package/dist/{esm/src → src}/dm-policy.js +6 -9
- package/dist/src/dynamic-agent.d.ts +37 -0
- package/dist/src/dynamic-agent.js +60 -0
- package/dist/src/dynamic-routing.d.ts +65 -0
- package/dist/src/dynamic-routing.js +58 -0
- package/dist/{esm/src → src}/group-policy.js +8 -22
- package/dist/src/http.d.ts +26 -0
- package/dist/src/http.js +93 -0
- package/dist/{esm/types/src → src}/interface.d.ts +0 -2
- package/dist/src/interface.js +4 -0
- package/dist/src/mcp/index.js +6 -0
- package/dist/{esm/src → src}/mcp/interceptors/biz-error.js +2 -6
- package/dist/{esm/src → src}/mcp/interceptors/index.js +6 -10
- package/dist/{esm/src → src}/mcp/interceptors/msg-media.js +6 -10
- package/dist/{esm/src → src}/mcp/interceptors/smartpage-create.js +2 -6
- package/dist/{esm/src → src}/mcp/interceptors/smartpage-export.js +2 -6
- package/dist/src/mcp/interceptors/types.js +7 -0
- package/dist/{esm/src → src}/mcp/schema.js +1 -4
- package/dist/{esm/src → src}/mcp/tool.js +4 -8
- package/dist/{esm/src → src}/mcp/transport.js +15 -16
- package/dist/{esm/src → src}/media-handler.js +6 -10
- package/dist/{esm/src → src}/media-uploader.js +64 -10
- package/dist/{esm/src → src}/message-parser.js +1 -4
- package/dist/{esm/types/src → src}/message-sender.d.ts +18 -0
- package/dist/{esm/src → src}/message-sender.js +45 -11
- package/dist/{esm/src → src}/monitor.js +145 -292
- package/dist/{esm/src → src}/onboarding.js +22 -26
- package/dist/{esm/types/src → src}/openclaw-compat.d.ts +49 -0
- package/dist/{esm/src → src}/openclaw-compat.js +133 -27
- package/dist/{esm/src → src}/reqid-store.js +4 -7
- package/dist/{esm/src → src}/runtime.js +2 -5
- package/dist/src/shared/command-auth.d.ts +23 -0
- package/dist/src/shared/command-auth.js +73 -0
- package/dist/src/shared/xml-parser.d.ts +46 -0
- package/dist/src/shared/xml-parser.js +210 -0
- package/dist/{esm/src → src}/state-dir-resolve.js +3 -7
- package/dist/{esm/src → src}/state-manager.js +84 -16
- package/dist/src/target.d.ts +35 -0
- package/dist/src/target.js +67 -0
- package/dist/src/template-card-manager.d.ts +55 -0
- package/dist/src/template-card-manager.js +205 -0
- package/dist/{esm/src → src}/template-card-parser.js +17 -8
- package/dist/{esm/src → src}/timeout.js +2 -5
- package/dist/src/types/account.d.ts +29 -0
- package/dist/src/types/account.js +4 -0
- package/dist/src/types/config.d.ts +91 -0
- package/dist/src/types/config.js +7 -0
- package/dist/src/types/constants.d.ts +42 -0
- package/dist/src/types/constants.js +42 -0
- package/dist/src/types/index.d.ts +7 -0
- package/dist/src/types/index.js +5 -0
- package/dist/src/types/message.d.ts +238 -0
- package/dist/src/types/message.js +5 -0
- package/dist/{esm/types/src → src}/utils.d.ts +41 -0
- package/dist/{esm/src → src}/utils.js +24 -10
- package/dist/src/version.d.ts +2 -0
- package/dist/src/version.js +24 -0
- package/dist/src/webhook/command-auth.d.ts +47 -0
- package/dist/src/webhook/command-auth.js +96 -0
- package/dist/src/webhook/gateway.d.ts +36 -0
- package/dist/src/webhook/gateway.js +235 -0
- package/dist/src/webhook/handler.d.ts +18 -0
- package/dist/src/webhook/handler.js +420 -0
- package/dist/src/webhook/helpers.d.ts +156 -0
- package/dist/src/webhook/helpers.js +833 -0
- package/dist/src/webhook/http.d.ts +26 -0
- package/dist/src/webhook/http.js +94 -0
- package/dist/src/webhook/index.d.ts +11 -0
- package/dist/src/webhook/index.js +14 -0
- package/dist/src/webhook/media.d.ts +29 -0
- package/dist/src/webhook/media.js +88 -0
- package/dist/src/webhook/monitor.d.ts +59 -0
- package/dist/src/webhook/monitor.js +1161 -0
- package/dist/src/webhook/state.d.ts +220 -0
- package/dist/src/webhook/state.js +491 -0
- package/dist/src/webhook/target.d.ts +41 -0
- package/dist/src/webhook/target.js +132 -0
- package/dist/src/webhook/types.d.ts +347 -0
- package/dist/src/webhook/types.js +33 -0
- package/dist/src/webhook/video-frame.d.ts +13 -0
- package/dist/src/webhook/video-frame.js +30 -0
- package/package.json +15 -7
- package/dist/cjs/index.js +0 -45
- package/dist/cjs/index.js.map +0 -1
- package/dist/cjs/src/channel.js +0 -339
- package/dist/cjs/src/channel.js.map +0 -1
- package/dist/cjs/src/const.js +0 -137
- package/dist/cjs/src/const.js.map +0 -1
- package/dist/cjs/src/dm-policy.js +0 -89
- package/dist/cjs/src/dm-policy.js.map +0 -1
- package/dist/cjs/src/group-policy.js +0 -133
- package/dist/cjs/src/group-policy.js.map +0 -1
- package/dist/cjs/src/mcp/interceptors/biz-error.js +0 -70
- package/dist/cjs/src/mcp/interceptors/biz-error.js.map +0 -1
- package/dist/cjs/src/mcp/interceptors/index.js +0 -73
- package/dist/cjs/src/mcp/interceptors/index.js.map +0 -1
- package/dist/cjs/src/mcp/interceptors/msg-media.js +0 -159
- package/dist/cjs/src/mcp/interceptors/msg-media.js.map +0 -1
- package/dist/cjs/src/mcp/interceptors/smartpage-create.js +0 -157
- package/dist/cjs/src/mcp/interceptors/smartpage-create.js.map +0 -1
- package/dist/cjs/src/mcp/interceptors/smartpage-export.js +0 -93
- package/dist/cjs/src/mcp/interceptors/smartpage-export.js.map +0 -1
- package/dist/cjs/src/mcp/schema.js +0 -104
- package/dist/cjs/src/mcp/schema.js.map +0 -1
- package/dist/cjs/src/mcp/tool.js +0 -208
- package/dist/cjs/src/mcp/tool.js.map +0 -1
- package/dist/cjs/src/mcp/transport.js +0 -417
- package/dist/cjs/src/mcp/transport.js.map +0 -1
- package/dist/cjs/src/media-handler.js +0 -129
- package/dist/cjs/src/media-handler.js.map +0 -1
- package/dist/cjs/src/media-uploader.js +0 -284
- package/dist/cjs/src/media-uploader.js.map +0 -1
- package/dist/cjs/src/message-parser.js +0 -134
- package/dist/cjs/src/message-parser.js.map +0 -1
- package/dist/cjs/src/message-sender.js +0 -89
- package/dist/cjs/src/message-sender.js.map +0 -1
- package/dist/cjs/src/monitor.js +0 -991
- package/dist/cjs/src/monitor.js.map +0 -1
- package/dist/cjs/src/onboarding.js +0 -182
- package/dist/cjs/src/onboarding.js.map +0 -1
- package/dist/cjs/src/openclaw-compat.js +0 -400
- package/dist/cjs/src/openclaw-compat.js.map +0 -1
- package/dist/cjs/src/reqid-store.js +0 -88
- package/dist/cjs/src/reqid-store.js.map +0 -1
- package/dist/cjs/src/runtime.js +0 -16
- package/dist/cjs/src/runtime.js.map +0 -1
- package/dist/cjs/src/state-dir-resolve.js +0 -35
- package/dist/cjs/src/state-dir-resolve.js.map +0 -1
- package/dist/cjs/src/state-manager.js +0 -158
- package/dist/cjs/src/state-manager.js.map +0 -1
- package/dist/cjs/src/template-card-parser.js +0 -641
- package/dist/cjs/src/template-card-parser.js.map +0 -1
- package/dist/cjs/src/timeout.js +0 -42
- package/dist/cjs/src/timeout.js.map +0 -1
- package/dist/cjs/src/utils.js +0 -60
- package/dist/cjs/src/utils.js.map +0 -1
- package/dist/cjs/src/version.js +0 -26
- package/dist/cjs/src/version.js.map +0 -1
- package/dist/esm/index.js +0 -41
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/src/channel.js +0 -337
- package/dist/esm/src/channel.js.map +0 -1
- package/dist/esm/src/const.js.map +0 -1
- package/dist/esm/src/dm-policy.js.map +0 -1
- package/dist/esm/src/group-policy.js.map +0 -1
- package/dist/esm/src/mcp/interceptors/biz-error.js.map +0 -1
- package/dist/esm/src/mcp/interceptors/index.js.map +0 -1
- package/dist/esm/src/mcp/interceptors/msg-media.js.map +0 -1
- package/dist/esm/src/mcp/interceptors/smartpage-create.js.map +0 -1
- package/dist/esm/src/mcp/interceptors/smartpage-export.js.map +0 -1
- package/dist/esm/src/mcp/schema.js.map +0 -1
- package/dist/esm/src/mcp/tool.js.map +0 -1
- package/dist/esm/src/mcp/transport.js.map +0 -1
- package/dist/esm/src/media-handler.js.map +0 -1
- package/dist/esm/src/media-uploader.js.map +0 -1
- package/dist/esm/src/message-parser.js.map +0 -1
- package/dist/esm/src/message-sender.js.map +0 -1
- package/dist/esm/src/monitor.js.map +0 -1
- package/dist/esm/src/onboarding.js.map +0 -1
- package/dist/esm/src/openclaw-compat.js.map +0 -1
- package/dist/esm/src/reqid-store.js.map +0 -1
- package/dist/esm/src/runtime.js.map +0 -1
- package/dist/esm/src/state-dir-resolve.js.map +0 -1
- package/dist/esm/src/state-manager.js.map +0 -1
- package/dist/esm/src/template-card-parser.js.map +0 -1
- package/dist/esm/src/timeout.js.map +0 -1
- package/dist/esm/src/utils.js.map +0 -1
- package/dist/esm/src/version.js +0 -23
- package/dist/esm/src/version.js.map +0 -1
- package/dist/esm/types/index.d.ts +0 -9
- package/dist/esm/types/src/version.d.ts +0 -2
- /package/dist/{esm/types/src → src}/channel.d.ts +0 -0
- /package/dist/{esm/types/src → src}/dm-policy.d.ts +0 -0
- /package/dist/{esm/types/src → src}/group-policy.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/index.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/interceptors/biz-error.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/interceptors/index.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/interceptors/msg-media.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/interceptors/smartpage-create.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/interceptors/smartpage-export.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/interceptors/types.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/schema.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/tool.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/transport.d.ts +0 -0
- /package/dist/{esm/types/src → src}/media-handler.d.ts +0 -0
- /package/dist/{esm/types/src → src}/media-uploader.d.ts +0 -0
- /package/dist/{esm/types/src → src}/message-parser.d.ts +0 -0
- /package/dist/{esm/types/src → src}/monitor.d.ts +0 -0
- /package/dist/{esm/types/src → src}/onboarding.d.ts +0 -0
- /package/dist/{esm/types/src → src}/reqid-store.d.ts +0 -0
- /package/dist/{esm/types/src → src}/runtime.d.ts +0 -0
- /package/dist/{esm/types/src → src}/state-dir-resolve.d.ts +0 -0
- /package/dist/{esm/types/src → src}/state-manager.d.ts +0 -0
- /package/dist/{esm/types/src → src}/template-card-parser.d.ts +0 -0
- /package/dist/{esm/types/src → src}/timeout.d.ts +0 -0
|
@@ -1,417 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var aibotNodeSdk = require('@wecom/aibot-node-sdk');
|
|
4
|
-
var openclawCompat = require('../openclaw-compat.js');
|
|
5
|
-
var stateManager = require('../state-manager.js');
|
|
6
|
-
var _const = require('../const.js');
|
|
7
|
-
var timeout = require('../timeout.js');
|
|
8
|
-
var version = require('../version.js');
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* MCP Streamable HTTP 传输层模块
|
|
12
|
-
*
|
|
13
|
-
* 负责:
|
|
14
|
-
* - MCP JSON-RPC over HTTP 通信(发送请求、解析响应)
|
|
15
|
-
* - Streamable HTTP session 生命周期管理(initialize 握手 → Mcp-Session-Id 维护 → 失效重建)
|
|
16
|
-
* - 自动检测无状态 Server:如果 initialize 响应未返回 Mcp-Session-Id,
|
|
17
|
-
* 则标记为无状态模式,后续请求跳过握手和 session 管理
|
|
18
|
-
* - SSE 流式响应解析
|
|
19
|
-
* - MCP 配置运行时缓存(通过 WSClient 拉取 URL 并缓存在内存中)
|
|
20
|
-
*/
|
|
21
|
-
// ============================================================================
|
|
22
|
-
// 内部状态
|
|
23
|
-
// ============================================================================
|
|
24
|
-
/** HTTP 请求超时时间(毫秒) */
|
|
25
|
-
const HTTP_REQUEST_TIMEOUT_MS = 30000;
|
|
26
|
-
/** 媒体下载请求超时时间(毫秒),base64 编码的媒体文件最大可达 ~27MB */
|
|
27
|
-
const MEDIA_DOWNLOAD_TIMEOUT_MS = 120000;
|
|
28
|
-
/** 日志前缀 */
|
|
29
|
-
const LOG_TAG = "[mcp]";
|
|
30
|
-
/**
|
|
31
|
-
* MCP JSON-RPC 错误
|
|
32
|
-
*
|
|
33
|
-
* 携带服务端返回的 JSON-RPC error.code,
|
|
34
|
-
* 用于上层按错误码进行差异化处理(如特定错误码触发缓存清理)。
|
|
35
|
-
*/
|
|
36
|
-
class McpRpcError extends Error {
|
|
37
|
-
constructor(code, message, data) {
|
|
38
|
-
super(message);
|
|
39
|
-
this.code = code;
|
|
40
|
-
this.data = data;
|
|
41
|
-
this.name = "McpRpcError";
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* MCP HTTP 错误
|
|
46
|
-
*
|
|
47
|
-
* 携带 HTTP 状态码,用于精确判断 session 失效(404)等场景,
|
|
48
|
-
* 避免通过字符串匹配 "404" 导致的误判。
|
|
49
|
-
*/
|
|
50
|
-
class McpHttpError extends Error {
|
|
51
|
-
constructor(statusCode, message) {
|
|
52
|
-
super(message);
|
|
53
|
-
this.statusCode = statusCode;
|
|
54
|
-
this.name = "McpHttpError";
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* 需要清理缓存的 JSON-RPC 错误码集合
|
|
59
|
-
*
|
|
60
|
-
* 当 MCP Server 返回以下错误码时,说明服务端状态已发生变化(如配置变更、
|
|
61
|
-
* 服务重启等),需要清理对应 category 的全部缓存,确保下次请求重新
|
|
62
|
-
* 拉取配置并重建会话。
|
|
63
|
-
*
|
|
64
|
-
* - -32001: 服务不可用(Server Unavailable)
|
|
65
|
-
* - -32002: 配置已变更(Config Changed)
|
|
66
|
-
* - -32003: 认证失败(Auth Failed)
|
|
67
|
-
*/
|
|
68
|
-
const CACHE_CLEAR_ERROR_CODES = new Set([-32001, -32002, -32003]);
|
|
69
|
-
/** MCP 配置缓存:category → response.body(完整配置) */
|
|
70
|
-
const mcpConfigCache = new Map();
|
|
71
|
-
/** Streamable HTTP 会话缓存:category → session */
|
|
72
|
-
const mcpSessionCache = new Map();
|
|
73
|
-
/** 已确认为无状态的 MCP Server 品类集合(跳过后续握手) */
|
|
74
|
-
const statelessCategories = new Set();
|
|
75
|
-
/** 正在进行中的 initialize 请求(防止并发重复初始化),key 为 category */
|
|
76
|
-
const inflightInitRequests = new Map();
|
|
77
|
-
// ============================================================================
|
|
78
|
-
// MCP 配置拉取与缓存
|
|
79
|
-
// ============================================================================
|
|
80
|
-
/**
|
|
81
|
-
* 通过 WSClient 拉取指定 category 的 MCP 完整配置
|
|
82
|
-
*
|
|
83
|
-
* @param category - MCP 品类名称,如 doc、contact
|
|
84
|
-
* @returns 完整的 response.body 配置对象(至少包含 url 字段)
|
|
85
|
-
*/
|
|
86
|
-
async function fetchMcpConfig(category) {
|
|
87
|
-
const wsClient = stateManager.getWeComWebSocket(openclawCompat.DEFAULT_ACCOUNT_ID);
|
|
88
|
-
if (!wsClient) {
|
|
89
|
-
throw new Error("WSClient 未连接,无法拉取 MCP 配置");
|
|
90
|
-
}
|
|
91
|
-
const reqId = aibotNodeSdk.generateReqId("mcp_config");
|
|
92
|
-
const response = await timeout.withTimeout(wsClient.reply({ headers: { req_id: reqId } }, { biz_type: category, plugin_version: version.PLUGIN_VERSION }, _const.MCP_GET_CONFIG_CMD), _const.MCP_CONFIG_FETCH_TIMEOUT_MS, `MCP config fetch for "${category}" timed out after ${_const.MCP_CONFIG_FETCH_TIMEOUT_MS}ms`);
|
|
93
|
-
if (response.errcode !== undefined && response.errcode !== 0) {
|
|
94
|
-
const errMsg = `MCP 配置请求失败: errcode=${response.errcode}, errmsg=${response.errmsg ?? "unknown"}`;
|
|
95
|
-
console.error(`${LOG_TAG} ${errMsg}`);
|
|
96
|
-
throw new Error(errMsg);
|
|
97
|
-
}
|
|
98
|
-
const body = response.body;
|
|
99
|
-
if (!body?.url) {
|
|
100
|
-
throw new Error(`MCP 配置响应缺少 url 字段 (category="${category}")`);
|
|
101
|
-
}
|
|
102
|
-
console.log(`${LOG_TAG} 配置拉取成功 (category="${category}")`);
|
|
103
|
-
return body;
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* 获取指定品类的 MCP Server URL
|
|
107
|
-
*
|
|
108
|
-
* 优先从内存缓存中读取,未命中时通过 WSClient 拉取并缓存。
|
|
109
|
-
*
|
|
110
|
-
* @param category - MCP 品类名称
|
|
111
|
-
* @returns MCP Server URL
|
|
112
|
-
*/
|
|
113
|
-
async function getMcpUrl(category) {
|
|
114
|
-
// 查内存缓存
|
|
115
|
-
const cached = mcpConfigCache.get(category);
|
|
116
|
-
if (cached)
|
|
117
|
-
return cached.url;
|
|
118
|
-
// 缓存未命中,通过 WSClient 拉取
|
|
119
|
-
const body = await fetchMcpConfig(category);
|
|
120
|
-
// 写入缓存
|
|
121
|
-
mcpConfigCache.set(category, body);
|
|
122
|
-
console.log(`${LOG_TAG} getMcpUrl ${category}: ${body.url}`);
|
|
123
|
-
return body.url;
|
|
124
|
-
}
|
|
125
|
-
// ============================================================================
|
|
126
|
-
// HTTP 底层通信
|
|
127
|
-
// ============================================================================
|
|
128
|
-
/**
|
|
129
|
-
* 发送原始 HTTP 请求到 MCP Server(底层方法)
|
|
130
|
-
*
|
|
131
|
-
* 自动携带 Mcp-Session-Id 请求头(如果有),
|
|
132
|
-
* 并从响应头中更新 sessionId。
|
|
133
|
-
*/
|
|
134
|
-
async function sendRawJsonRpc(url, session, body, timeoutMs = HTTP_REQUEST_TIMEOUT_MS) {
|
|
135
|
-
const controller = new AbortController();
|
|
136
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
137
|
-
const headers = {
|
|
138
|
-
"Content-Type": "application/json",
|
|
139
|
-
Accept: "application/json, text/event-stream",
|
|
140
|
-
};
|
|
141
|
-
// Streamable HTTP:携带会话 ID
|
|
142
|
-
if (session.sessionId) {
|
|
143
|
-
headers["Mcp-Session-Id"] = session.sessionId;
|
|
144
|
-
}
|
|
145
|
-
let response;
|
|
146
|
-
try {
|
|
147
|
-
response = await fetch(url, {
|
|
148
|
-
method: "POST",
|
|
149
|
-
headers,
|
|
150
|
-
body: JSON.stringify(body),
|
|
151
|
-
signal: controller.signal,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
catch (err) {
|
|
155
|
-
if (err instanceof DOMException && err.name === "AbortError") {
|
|
156
|
-
throw new Error(`MCP 请求超时 (${timeoutMs}ms)`);
|
|
157
|
-
}
|
|
158
|
-
throw new Error(`MCP 网络请求失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
159
|
-
}
|
|
160
|
-
finally {
|
|
161
|
-
clearTimeout(timeoutId);
|
|
162
|
-
}
|
|
163
|
-
// 从响应头提取新的 sessionId(不直接修改入参,由调用方决定如何更新)
|
|
164
|
-
const newSessionId = response.headers.get("mcp-session-id");
|
|
165
|
-
if (!response.ok) {
|
|
166
|
-
throw new McpHttpError(response.status, `MCP HTTP 请求失败: ${response.status} ${response.statusText}`);
|
|
167
|
-
}
|
|
168
|
-
// Streamable HTTP:notification 响应可能无响应体(204 或 content-length: 0)
|
|
169
|
-
const contentLength = response.headers.get("content-length");
|
|
170
|
-
if (response.status === 204 || contentLength === "0") {
|
|
171
|
-
return { response, rpcResult: undefined, newSessionId };
|
|
172
|
-
}
|
|
173
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
174
|
-
// 处理 SSE 流式响应
|
|
175
|
-
if (contentType.includes("text/event-stream")) {
|
|
176
|
-
return { response, rpcResult: await parseSseResponse(response), newSessionId };
|
|
177
|
-
}
|
|
178
|
-
// 普通 JSON 响应 — 先读取文本,防止空内容导致 JSON.parse 报错
|
|
179
|
-
const text = await response.text();
|
|
180
|
-
if (!text.trim()) {
|
|
181
|
-
return { response, rpcResult: undefined, newSessionId };
|
|
182
|
-
}
|
|
183
|
-
const rpc = JSON.parse(text);
|
|
184
|
-
if (rpc.error) {
|
|
185
|
-
throw new McpRpcError(rpc.error.code, `MCP 调用错误 [${rpc.error.code}]: ${rpc.error.message}`, rpc.error.data);
|
|
186
|
-
}
|
|
187
|
-
return { response, rpcResult: rpc.result, newSessionId };
|
|
188
|
-
}
|
|
189
|
-
// ============================================================================
|
|
190
|
-
// Session 管理
|
|
191
|
-
// ============================================================================
|
|
192
|
-
/**
|
|
193
|
-
* 对指定 URL 执行 Streamable HTTP 的 initialize 握手
|
|
194
|
-
*
|
|
195
|
-
* 发送 initialize → 接收 serverInfo → 发送 initialized 通知。
|
|
196
|
-
* 如果服务端未返回 Mcp-Session-Id,则标记为无状态模式,后续请求跳过 session 管理。
|
|
197
|
-
*/
|
|
198
|
-
async function initializeSession(url, category) {
|
|
199
|
-
const session = { sessionId: null, initialized: false, stateless: false };
|
|
200
|
-
console.log(`${LOG_TAG} 开始 initialize 握手 (category="${category}")`);
|
|
201
|
-
// 1. 发送 initialize 请求
|
|
202
|
-
const initBody = {
|
|
203
|
-
jsonrpc: "2.0",
|
|
204
|
-
id: aibotNodeSdk.generateReqId("mcp_init"),
|
|
205
|
-
method: "initialize",
|
|
206
|
-
params: {
|
|
207
|
-
protocolVersion: "2025-03-26",
|
|
208
|
-
capabilities: {},
|
|
209
|
-
clientInfo: { name: "wecom_mcp", version: "1.0.0" },
|
|
210
|
-
},
|
|
211
|
-
};
|
|
212
|
-
const { newSessionId: initSessionId } = await sendRawJsonRpc(url, session, initBody);
|
|
213
|
-
// 用返回的 newSessionId 更新 session(不再依赖副作用修改)
|
|
214
|
-
if (initSessionId) {
|
|
215
|
-
session.sessionId = initSessionId;
|
|
216
|
-
}
|
|
217
|
-
// 检查服务端是否返回了 Mcp-Session-Id
|
|
218
|
-
// 如果没有返回,说明该 Server 是无状态实现,无需维护 session
|
|
219
|
-
if (!session.sessionId) {
|
|
220
|
-
session.stateless = true;
|
|
221
|
-
session.initialized = true;
|
|
222
|
-
statelessCategories.add(category);
|
|
223
|
-
mcpSessionCache.set(category, session);
|
|
224
|
-
console.log(`${LOG_TAG} 无状态 Server 确认 (category="${category}")`);
|
|
225
|
-
return session;
|
|
226
|
-
}
|
|
227
|
-
// 2. 发送 initialized 通知(JSON-RPC notification 不带 id 字段)
|
|
228
|
-
const notifyBody = {
|
|
229
|
-
jsonrpc: "2.0",
|
|
230
|
-
method: "notifications/initialized",
|
|
231
|
-
};
|
|
232
|
-
// initialized 通知不需要等待响应,但 Streamable HTTP 要求通过 POST 发送
|
|
233
|
-
const { newSessionId: notifySessionId } = await sendRawJsonRpc(url, session, notifyBody);
|
|
234
|
-
// 如果 initialized 通知的响应也携带了 sessionId,以最新的为准
|
|
235
|
-
if (notifySessionId) {
|
|
236
|
-
session.sessionId = notifySessionId;
|
|
237
|
-
}
|
|
238
|
-
session.initialized = true;
|
|
239
|
-
mcpSessionCache.set(category, session);
|
|
240
|
-
console.log(`${LOG_TAG} 有状态 Session 建立成功 (category="${category}", sessionId="${session.sessionId}")`);
|
|
241
|
-
return session;
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* 获取或创建指定 URL 的 MCP 会话
|
|
245
|
-
*
|
|
246
|
-
* - 已确认无状态的 category:直接返回空 session,跳过握手
|
|
247
|
-
* - 已有可用有状态会话:直接返回缓存
|
|
248
|
-
* - 其他情况:执行 initialize 握手,并发请求会被合并
|
|
249
|
-
*/
|
|
250
|
-
async function getOrCreateSession(url, category) {
|
|
251
|
-
// 已确认为无状态的 Server,直接返回空 session 跳过握手
|
|
252
|
-
if (statelessCategories.has(category)) {
|
|
253
|
-
const cached = mcpSessionCache.get(category);
|
|
254
|
-
if (cached)
|
|
255
|
-
return cached;
|
|
256
|
-
// 首次发现被清除(理论上不会走到这里),重新走握手探测
|
|
257
|
-
}
|
|
258
|
-
const cached = mcpSessionCache.get(category);
|
|
259
|
-
if (cached?.initialized)
|
|
260
|
-
return cached;
|
|
261
|
-
// 防止并发重复初始化
|
|
262
|
-
const inflight = inflightInitRequests.get(category);
|
|
263
|
-
if (inflight)
|
|
264
|
-
return inflight;
|
|
265
|
-
const promise = initializeSession(url, category).finally(() => {
|
|
266
|
-
inflightInitRequests.delete(category);
|
|
267
|
-
});
|
|
268
|
-
inflightInitRequests.set(category, promise);
|
|
269
|
-
return promise;
|
|
270
|
-
}
|
|
271
|
-
// ============================================================================
|
|
272
|
-
// SSE 解析
|
|
273
|
-
// ============================================================================
|
|
274
|
-
/**
|
|
275
|
-
* 解析 SSE 流式响应,提取最终的 JSON-RPC result
|
|
276
|
-
*
|
|
277
|
-
* 按照 SSE 规范,同一事件中的多个 `data:` 行会用换行符拼接。
|
|
278
|
-
* 空行分隔不同事件,取最后一个完整事件的数据。
|
|
279
|
-
*/
|
|
280
|
-
async function parseSseResponse(response) {
|
|
281
|
-
const text = await response.text();
|
|
282
|
-
const lines = text.split("\n");
|
|
283
|
-
// 按 SSE 规范解析:空行分隔事件,同一事件内的 data 行用换行拼接
|
|
284
|
-
let currentDataParts = [];
|
|
285
|
-
let lastEventData = "";
|
|
286
|
-
for (const line of lines) {
|
|
287
|
-
if (line.startsWith("data: ")) {
|
|
288
|
-
currentDataParts.push(line.slice(6));
|
|
289
|
-
}
|
|
290
|
-
else if (line.startsWith("data:")) {
|
|
291
|
-
// data: 后无空格时,值为空字符串
|
|
292
|
-
currentDataParts.push(line.slice(5));
|
|
293
|
-
}
|
|
294
|
-
else if (line.trim() === "" && currentDataParts.length > 0) {
|
|
295
|
-
// 空行表示事件结束,拼接所有 data 行
|
|
296
|
-
lastEventData = currentDataParts.join("\n").trim();
|
|
297
|
-
currentDataParts = [];
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
// 处理最后一个未以空行结尾的事件
|
|
301
|
-
if (currentDataParts.length > 0) {
|
|
302
|
-
lastEventData = currentDataParts.join("\n").trim();
|
|
303
|
-
}
|
|
304
|
-
if (!lastEventData) {
|
|
305
|
-
throw new Error("SSE 响应中未包含有效数据");
|
|
306
|
-
}
|
|
307
|
-
try {
|
|
308
|
-
const rpc = JSON.parse(lastEventData);
|
|
309
|
-
if (rpc.error) {
|
|
310
|
-
throw new McpRpcError(rpc.error.code, `MCP 调用错误 [${rpc.error.code}]: ${rpc.error.message}`, rpc.error.data);
|
|
311
|
-
}
|
|
312
|
-
return rpc.result;
|
|
313
|
-
}
|
|
314
|
-
catch (err) {
|
|
315
|
-
if (err instanceof SyntaxError) {
|
|
316
|
-
throw new Error(`SSE 响应解析失败: ${lastEventData.slice(0, 200)}`);
|
|
317
|
-
}
|
|
318
|
-
throw err;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
// ============================================================================
|
|
322
|
-
// 公共 API
|
|
323
|
-
// ============================================================================
|
|
324
|
-
/**
|
|
325
|
-
* 清理指定品类的所有 MCP 缓存(配置、会话、无状态标记)
|
|
326
|
-
*
|
|
327
|
-
* 当 MCP Server 返回特定错误码时调用,确保下次请求重新拉取配置并重建会话。
|
|
328
|
-
*
|
|
329
|
-
* @param category - MCP 品类名称
|
|
330
|
-
*/
|
|
331
|
-
function clearCategoryCache(category) {
|
|
332
|
-
console.log(`${LOG_TAG} 清理缓存 (category="${category}")`);
|
|
333
|
-
mcpConfigCache.delete(category);
|
|
334
|
-
mcpSessionCache.delete(category);
|
|
335
|
-
statelessCategories.delete(category);
|
|
336
|
-
inflightInitRequests.delete(category);
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* 发送 JSON-RPC 请求到 MCP Server(Streamable HTTP 协议)
|
|
340
|
-
*
|
|
341
|
-
* 自动管理 session 生命周期:
|
|
342
|
-
* - 无状态 Server:跳过 session 管理,直接发送请求
|
|
343
|
-
* - 有状态 Server:首次调用先执行 initialize 握手,session 失效(404)时自动重建并重试
|
|
344
|
-
*
|
|
345
|
-
* @param category - MCP 品类名称
|
|
346
|
-
* @param method - JSON-RPC 方法名
|
|
347
|
-
* @param params - JSON-RPC 参数
|
|
348
|
-
* @param options - 可选配置(如自定义超时)
|
|
349
|
-
* @returns JSON-RPC result
|
|
350
|
-
*/
|
|
351
|
-
async function sendJsonRpc(category, method, params, options) {
|
|
352
|
-
const url = await getMcpUrl(category);
|
|
353
|
-
const timeoutMs = options?.timeoutMs;
|
|
354
|
-
const body = {
|
|
355
|
-
jsonrpc: "2.0",
|
|
356
|
-
id: aibotNodeSdk.generateReqId("mcp_rpc"),
|
|
357
|
-
method,
|
|
358
|
-
...(params !== undefined ? { params } : {}),
|
|
359
|
-
};
|
|
360
|
-
let session = await getOrCreateSession(url, category);
|
|
361
|
-
try {
|
|
362
|
-
const { rpcResult, newSessionId } = await sendRawJsonRpc(url, session, body, timeoutMs);
|
|
363
|
-
// 用最新的 sessionId 更新 session
|
|
364
|
-
if (newSessionId) {
|
|
365
|
-
session.sessionId = newSessionId;
|
|
366
|
-
}
|
|
367
|
-
return rpcResult;
|
|
368
|
-
}
|
|
369
|
-
catch (err) {
|
|
370
|
-
// 特定 JSON-RPC 错误码触发缓存清理(统一在传输层处理,上层无需关心)
|
|
371
|
-
if (err instanceof McpRpcError && CACHE_CLEAR_ERROR_CODES.has(err.code)) {
|
|
372
|
-
clearCategoryCache(category);
|
|
373
|
-
}
|
|
374
|
-
// 无状态 Server 不存在 session 失效问题,直接抛出错误
|
|
375
|
-
if (session.stateless)
|
|
376
|
-
throw err;
|
|
377
|
-
// 有状态 Server:session 失效时服务端返回 404,需要重新初始化并重试一次
|
|
378
|
-
// 使用 McpHttpError.statusCode 精确匹配,避免字符串匹配 "404" 导致误判
|
|
379
|
-
if (err instanceof McpHttpError && err.statusCode === 404) {
|
|
380
|
-
console.log(`${LOG_TAG} Session 失效 (category="${category}"),开始重建...`);
|
|
381
|
-
mcpSessionCache.delete(category);
|
|
382
|
-
// 使用 rebuildSession 合并并发的 session 重建请求,避免竞态条件
|
|
383
|
-
session = await rebuildSession(url, category);
|
|
384
|
-
const { rpcResult, newSessionId } = await sendRawJsonRpc(url, session, body, timeoutMs);
|
|
385
|
-
if (newSessionId) {
|
|
386
|
-
session.sessionId = newSessionId;
|
|
387
|
-
}
|
|
388
|
-
return rpcResult;
|
|
389
|
-
}
|
|
390
|
-
// 其他错误记录日志后抛出
|
|
391
|
-
console.error(`${LOG_TAG} RPC 请求失败 (category="${category}", method="${method}"): ${err instanceof Error ? err.message : String(err)}`);
|
|
392
|
-
throw err;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* 合并并发的 session 重建请求
|
|
397
|
-
*
|
|
398
|
-
* 与 getOrCreateSession 类似,使用 inflightInitRequests 防止
|
|
399
|
-
* 多个并发请求同时遇到 404 时重复执行 initialize 握手。
|
|
400
|
-
*/
|
|
401
|
-
async function rebuildSession(url, category) {
|
|
402
|
-
const inflight = inflightInitRequests.get(category);
|
|
403
|
-
if (inflight)
|
|
404
|
-
return inflight;
|
|
405
|
-
const promise = initializeSession(url, category).finally(() => {
|
|
406
|
-
inflightInitRequests.delete(category);
|
|
407
|
-
});
|
|
408
|
-
inflightInitRequests.set(category, promise);
|
|
409
|
-
return promise;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
exports.MEDIA_DOWNLOAD_TIMEOUT_MS = MEDIA_DOWNLOAD_TIMEOUT_MS;
|
|
413
|
-
exports.McpHttpError = McpHttpError;
|
|
414
|
-
exports.McpRpcError = McpRpcError;
|
|
415
|
-
exports.clearCategoryCache = clearCategoryCache;
|
|
416
|
-
exports.sendJsonRpc = sendJsonRpc;
|
|
417
|
-
//# sourceMappingURL=transport.js.map
|
|
@@ -1 +0,0 @@
|
|
|
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":["getWeComWebSocket","DEFAULT_ACCOUNT_ID","generateReqId","withTimeout","PLUGIN_VERSION","MCP_GET_CONFIG_CMD","MCP_CONFIG_FETCH_TIMEOUT_MS"],"mappings":"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"}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var fileType = require('file-type');
|
|
4
|
-
var runtime = require('./runtime.js');
|
|
5
|
-
var _const = require('./const.js');
|
|
6
|
-
var timeout = require('./timeout.js');
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 企业微信媒体(图片)下载和保存模块
|
|
10
|
-
*
|
|
11
|
-
* 负责下载、检测格式、保存图片到本地,包含超时保护
|
|
12
|
-
*/
|
|
13
|
-
// ============================================================================
|
|
14
|
-
// 图片格式检测辅助函数(基于 file-type 包)
|
|
15
|
-
// ============================================================================
|
|
16
|
-
/**
|
|
17
|
-
* 检查 Buffer 是否为有效的图片格式
|
|
18
|
-
*/
|
|
19
|
-
async function isImageBuffer(data) {
|
|
20
|
-
const type = await fileType.fileTypeFromBuffer(data);
|
|
21
|
-
return type?.mime.startsWith("image/") ?? false;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* 检测 Buffer 的图片内容类型
|
|
25
|
-
*/
|
|
26
|
-
async function detectImageContentType(data) {
|
|
27
|
-
const type = await fileType.fileTypeFromBuffer(data);
|
|
28
|
-
if (type?.mime.startsWith("image/")) {
|
|
29
|
-
return type.mime;
|
|
30
|
-
}
|
|
31
|
-
return "application/octet-stream";
|
|
32
|
-
}
|
|
33
|
-
// ============================================================================
|
|
34
|
-
// 图片下载和保存
|
|
35
|
-
// ============================================================================
|
|
36
|
-
/**
|
|
37
|
-
* 下载并保存所有图片到本地,每张图片的下载带超时保护
|
|
38
|
-
*/
|
|
39
|
-
async function downloadAndSaveImages(params) {
|
|
40
|
-
const { imageUrls, config, runtime: runtime$1, wsClient } = params;
|
|
41
|
-
const core = runtime.getWeComRuntime();
|
|
42
|
-
const mediaList = [];
|
|
43
|
-
for (const imageUrl of imageUrls) {
|
|
44
|
-
try {
|
|
45
|
-
runtime$1.log?.(`[wecom] Downloading image: url=${imageUrl}`);
|
|
46
|
-
const mediaMaxMb = config.agents?.defaults?.mediaMaxMb ?? _const.DEFAULT_MEDIA_MAX_MB;
|
|
47
|
-
const maxBytes = mediaMaxMb * 1024 * 1024;
|
|
48
|
-
let imageBuffer;
|
|
49
|
-
let imageContentType;
|
|
50
|
-
let originalFilename;
|
|
51
|
-
const imageAesKey = params.imageAesKeys?.get(imageUrl);
|
|
52
|
-
try {
|
|
53
|
-
// 优先使用 SDK 的 downloadFile 方法下载(带超时保护)
|
|
54
|
-
const result = await timeout.withTimeout(wsClient.downloadFile(imageUrl, imageAesKey), _const.IMAGE_DOWNLOAD_TIMEOUT_MS, `Image download timed out: ${imageUrl}`);
|
|
55
|
-
imageBuffer = result.buffer;
|
|
56
|
-
originalFilename = result.filename;
|
|
57
|
-
imageContentType = await detectImageContentType(imageBuffer);
|
|
58
|
-
runtime$1.log?.(`[wecom] Image downloaded: size=${imageBuffer.length}, contentType=${imageContentType}, filename=${originalFilename ?? '(none)'}`);
|
|
59
|
-
}
|
|
60
|
-
catch (sdkError) {
|
|
61
|
-
// 如果 SDK 方法失败,回退到原有方式(带超时保护)
|
|
62
|
-
runtime$1.log?.(`[wecom] SDK download failed, fallback: ${String(sdkError)}`);
|
|
63
|
-
const fetched = await timeout.withTimeout(core.channel.media.fetchRemoteMedia({ url: imageUrl }), _const.IMAGE_DOWNLOAD_TIMEOUT_MS, `Manual image download timed out: ${imageUrl}`);
|
|
64
|
-
runtime$1.log?.(`[wecom] Image fetched: contentType=${fetched.contentType}, size=${fetched.buffer.length}`);
|
|
65
|
-
imageBuffer = fetched.buffer;
|
|
66
|
-
imageContentType = fetched.contentType ?? "application/octet-stream";
|
|
67
|
-
const isValidImage = await isImageBuffer(fetched.buffer);
|
|
68
|
-
if (!isValidImage) {
|
|
69
|
-
runtime$1.log?.(`[wecom] WARN: Downloaded data is not a valid image format`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
const saved = await core.channel.media.saveMediaBuffer(imageBuffer, imageContentType, "inbound", maxBytes, originalFilename);
|
|
73
|
-
mediaList.push({ path: saved.path, contentType: saved.contentType });
|
|
74
|
-
runtime$1.log?.(`[wecom][plugin] Image saved: path=${saved.path}, contentType=${saved.contentType}`);
|
|
75
|
-
}
|
|
76
|
-
catch (err) {
|
|
77
|
-
runtime$1.error?.(`[wecom] Failed to download image: ${String(err)}`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return mediaList;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* 下载并保存所有文件到本地,每个文件的下载带超时保护
|
|
84
|
-
*/
|
|
85
|
-
async function downloadAndSaveFiles(params) {
|
|
86
|
-
const { fileUrls, config, runtime: runtime$1, wsClient } = params;
|
|
87
|
-
const core = runtime.getWeComRuntime();
|
|
88
|
-
const mediaList = [];
|
|
89
|
-
for (const fileUrl of fileUrls) {
|
|
90
|
-
try {
|
|
91
|
-
runtime$1.log?.(`[wecom] Downloading file: url=${fileUrl}`);
|
|
92
|
-
const mediaMaxMb = config.agents?.defaults?.mediaMaxMb ?? _const.DEFAULT_MEDIA_MAX_MB;
|
|
93
|
-
const maxBytes = mediaMaxMb * 1024 * 1024;
|
|
94
|
-
let fileBuffer;
|
|
95
|
-
let fileContentType;
|
|
96
|
-
let originalFilename;
|
|
97
|
-
const fileAesKey = params.fileAesKeys?.get(fileUrl);
|
|
98
|
-
try {
|
|
99
|
-
// 使用 SDK 的 downloadFile 方法下载(带超时保护)
|
|
100
|
-
const result = await timeout.withTimeout(wsClient.downloadFile(fileUrl, fileAesKey), _const.FILE_DOWNLOAD_TIMEOUT_MS, `File download timed out: ${fileUrl}`);
|
|
101
|
-
fileBuffer = result.buffer;
|
|
102
|
-
originalFilename = result.filename;
|
|
103
|
-
// 检测文件类型
|
|
104
|
-
const type = await fileType.fileTypeFromBuffer(fileBuffer);
|
|
105
|
-
fileContentType = type?.mime ?? "application/octet-stream";
|
|
106
|
-
runtime$1.log?.(`[wecom] File downloaded: size=${fileBuffer.length}, contentType=${fileContentType}, filename=${originalFilename ?? '(none)'}`);
|
|
107
|
-
}
|
|
108
|
-
catch (sdkError) {
|
|
109
|
-
// 如果 SDK 方法失败,回退到 fetchRemoteMedia(带超时保护)
|
|
110
|
-
runtime$1.log?.(`[wecom] SDK file download failed, fallback: ${String(sdkError)}`);
|
|
111
|
-
const fetched = await timeout.withTimeout(core.channel.media.fetchRemoteMedia({ url: fileUrl }), _const.FILE_DOWNLOAD_TIMEOUT_MS, `Manual file download timed out: ${fileUrl}`);
|
|
112
|
-
runtime$1.log?.(`[wecom] File fetched: contentType=${fetched.contentType}, size=${fetched.buffer.length}`);
|
|
113
|
-
fileBuffer = fetched.buffer;
|
|
114
|
-
fileContentType = fetched.contentType ?? "application/octet-stream";
|
|
115
|
-
}
|
|
116
|
-
const saved = await core.channel.media.saveMediaBuffer(fileBuffer, fileContentType, "inbound", maxBytes, originalFilename);
|
|
117
|
-
mediaList.push({ path: saved.path, contentType: saved.contentType });
|
|
118
|
-
runtime$1.log?.(`[wecom][plugin] File saved: path=${saved.path}, contentType=${saved.contentType}`);
|
|
119
|
-
}
|
|
120
|
-
catch (err) {
|
|
121
|
-
runtime$1.error?.(`[wecom] Failed to download file: ${String(err)}`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return mediaList;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
exports.downloadAndSaveFiles = downloadAndSaveFiles;
|
|
128
|
-
exports.downloadAndSaveImages = downloadAndSaveImages;
|
|
129
|
-
//# sourceMappingURL=media-handler.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"media-handler.js","sources":["../../../../../src/media-handler.ts"],"sourcesContent":["/**\n * 企业微信媒体(图片)下载和保存模块\n *\n * 负责下载、检测格式、保存图片到本地,包含超时保护\n */\n\nimport type { OpenClawConfig } from \"openclaw/plugin-sdk/core\";\nimport type { RuntimeEnv } from \"openclaw/plugin-sdk/runtime-env\";\nimport type { WSClient } from \"@wecom/aibot-node-sdk\";\nimport { fileTypeFromBuffer } from \"file-type\";\nimport { getWeComRuntime } from \"./runtime.js\";\nimport { IMAGE_DOWNLOAD_TIMEOUT_MS, FILE_DOWNLOAD_TIMEOUT_MS, DEFAULT_MEDIA_MAX_MB } from \"./const.js\";\nimport { withTimeout } from \"./timeout.js\";\nimport type { ResolvedWeComAccount } from \"./utils.js\";\n\n// ============================================================================\n// 图片格式检测辅助函数(基于 file-type 包)\n// ============================================================================\n\n/**\n * 检查 Buffer 是否为有效的图片格式\n */\nasync function isImageBuffer(data: Buffer): Promise<boolean> {\n const type = await fileTypeFromBuffer(data);\n return type?.mime.startsWith(\"image/\") ?? false;\n}\n\n/**\n * 检测 Buffer 的图片内容类型\n */\nasync function detectImageContentType(data: Buffer): Promise<string> {\n const type = await fileTypeFromBuffer(data);\n if (type?.mime.startsWith(\"image/\")) {\n return type.mime;\n }\n return \"application/octet-stream\";\n}\n\n// ============================================================================\n// 图片下载和保存\n// ============================================================================\n\n/**\n * 下载并保存所有图片到本地,每张图片的下载带超时保护\n */\nexport async function downloadAndSaveImages(params: {\n imageUrls: string[];\n imageAesKeys?: Map<string, string>;\n account: ResolvedWeComAccount;\n config: OpenClawConfig;\n runtime: RuntimeEnv;\n wsClient: WSClient;\n}): Promise<Array<{ path: string; contentType?: string }>> {\n const { imageUrls, config, runtime, wsClient } = params;\n const core = getWeComRuntime();\n const mediaList: Array<{ path: string; contentType?: string }> = [];\n\n for (const imageUrl of imageUrls) {\n try {\n runtime.log?.(`[wecom] Downloading image: url=${imageUrl}`);\n const mediaMaxMb = config.agents?.defaults?.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB;\n const maxBytes = mediaMaxMb * 1024 * 1024;\n\n let imageBuffer: Buffer;\n let imageContentType: string;\n let originalFilename: string | undefined;\n const imageAesKey = params.imageAesKeys?.get(imageUrl);\n\n try {\n // 优先使用 SDK 的 downloadFile 方法下载(带超时保护)\n const result = await withTimeout(\n wsClient.downloadFile(imageUrl, imageAesKey),\n IMAGE_DOWNLOAD_TIMEOUT_MS,\n `Image download timed out: ${imageUrl}`,\n );\n imageBuffer = result.buffer;\n originalFilename = result.filename;\n imageContentType = await detectImageContentType(imageBuffer);\n runtime.log?.(`[wecom] Image downloaded: size=${imageBuffer.length}, contentType=${imageContentType}, filename=${originalFilename ?? '(none)'}`);\n } catch (sdkError) {\n // 如果 SDK 方法失败,回退到原有方式(带超时保护)\n runtime.log?.(`[wecom] SDK download failed, fallback: ${String(sdkError)}`);\n const fetched = await withTimeout(\n core.channel.media.fetchRemoteMedia({ url: imageUrl }),\n IMAGE_DOWNLOAD_TIMEOUT_MS,\n `Manual image download timed out: ${imageUrl}`,\n ) as { buffer: Buffer; contentType?: string };\n runtime.log?.(`[wecom] Image fetched: contentType=${fetched.contentType}, size=${fetched.buffer.length}`);\n\n imageBuffer = fetched.buffer;\n imageContentType = fetched.contentType ?? \"application/octet-stream\";\n const isValidImage = await isImageBuffer(fetched.buffer);\n\n if (!isValidImage) {\n runtime.log?.(`[wecom] WARN: Downloaded data is not a valid image format`);\n }\n }\n\n const saved = await core.channel.media.saveMediaBuffer(\n imageBuffer,\n imageContentType,\n \"inbound\",\n maxBytes,\n originalFilename,\n );\n mediaList.push({ path: saved.path, contentType: saved.contentType });\n runtime.log?.(`[wecom][plugin] Image saved: path=${saved.path}, contentType=${saved.contentType}`);\n } catch (err) {\n runtime.error?.(`[wecom] Failed to download image: ${String(err)}`);\n }\n }\n\n return mediaList;\n}\n\n/**\n * 下载并保存所有文件到本地,每个文件的下载带超时保护\n */\nexport async function downloadAndSaveFiles(params: {\n fileUrls: string[];\n fileAesKeys?: Map<string, string>;\n account: ResolvedWeComAccount;\n config: OpenClawConfig;\n runtime: RuntimeEnv;\n wsClient: WSClient;\n}): Promise<Array<{ path: string; contentType?: string }>> {\n const { fileUrls, config, runtime, wsClient } = params;\n const core = getWeComRuntime();\n const mediaList: Array<{ path: string; contentType?: string }> = [];\n\n for (const fileUrl of fileUrls) {\n try {\n runtime.log?.(`[wecom] Downloading file: url=${fileUrl}`);\n const mediaMaxMb = config.agents?.defaults?.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB;\n const maxBytes = mediaMaxMb * 1024 * 1024;\n\n let fileBuffer: Buffer;\n let fileContentType: string;\n let originalFilename: string | undefined;\n const fileAesKey = params.fileAesKeys?.get(fileUrl);\n\n try {\n // 使用 SDK 的 downloadFile 方法下载(带超时保护)\n const result = await withTimeout(\n wsClient.downloadFile(fileUrl, fileAesKey),\n FILE_DOWNLOAD_TIMEOUT_MS,\n `File download timed out: ${fileUrl}`,\n );\n fileBuffer = result.buffer;\n originalFilename = result.filename;\n\n // 检测文件类型\n const type = await fileTypeFromBuffer(fileBuffer);\n fileContentType = type?.mime ?? \"application/octet-stream\";\n runtime.log?.(`[wecom] File downloaded: size=${fileBuffer.length}, contentType=${fileContentType}, filename=${originalFilename ?? '(none)'}`);\n } catch (sdkError) {\n // 如果 SDK 方法失败,回退到 fetchRemoteMedia(带超时保护)\n runtime.log?.(`[wecom] SDK file download failed, fallback: ${String(sdkError)}`);\n const fetched = await withTimeout(\n core.channel.media.fetchRemoteMedia({ url: fileUrl }),\n FILE_DOWNLOAD_TIMEOUT_MS,\n `Manual file download timed out: ${fileUrl}`,\n ) as { buffer: Buffer; contentType?: string };\n runtime.log?.(`[wecom] File fetched: contentType=${fetched.contentType}, size=${fetched.buffer.length}`);\n\n fileBuffer = fetched.buffer;\n fileContentType = fetched.contentType ?? \"application/octet-stream\";\n }\n\n const saved = await core.channel.media.saveMediaBuffer(\n fileBuffer,\n fileContentType,\n \"inbound\",\n maxBytes,\n originalFilename,\n );\n mediaList.push({ path: saved.path, contentType: saved.contentType });\n runtime.log?.(`[wecom][plugin] File saved: path=${saved.path}, contentType=${saved.contentType}`);\n } catch (err) {\n runtime.error?.(`[wecom] Failed to download file: ${String(err)}`);\n }\n }\n\n return mediaList;\n}\n"],"names":["fileTypeFromBuffer","runtime","getWeComRuntime","DEFAULT_MEDIA_MAX_MB","withTimeout","IMAGE_DOWNLOAD_TIMEOUT_MS","FILE_DOWNLOAD_TIMEOUT_MS"],"mappings":"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"}
|