@wecom/wecom-openclaw-plugin 1.0.11 → 1.0.12-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.
Files changed (40) hide show
  1. package/dist/index.cjs.js +713 -144
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.esm.js +717 -150
  4. package/dist/index.esm.js.map +1 -1
  5. package/dist/src/const.d.ts +0 -2
  6. package/dist/src/interface.d.ts +0 -13
  7. package/dist/src/mcp/index.d.ts +6 -0
  8. package/dist/src/mcp/schema.d.ts +11 -0
  9. package/dist/src/mcp/tool.d.ts +51 -0
  10. package/dist/src/mcp/transport.d.ts +59 -0
  11. package/dist/src/version.d.ts +2 -0
  12. package/package.json +3 -3
  13. package/skills/wecom-contact-lookup/SKILL.md +160 -0
  14. package/skills/wecom-doc-manager/SKILL.md +53 -0
  15. package/skills/wecom-doc-manager/references/api-create-doc.md +56 -0
  16. package/skills/wecom-doc-manager/references/api-edit-doc-content.md +68 -0
  17. package/skills/wecom-doc-manager/references/api-export-document.md +88 -0
  18. package/skills/wecom-edit-todo/SKILL.md +247 -0
  19. package/skills/wecom-get-todo-detail/SKILL.md +141 -0
  20. package/skills/wecom-get-todo-list/SKILL.md +125 -0
  21. package/skills/wecom-meeting-create/SKILL.md +152 -0
  22. package/skills/wecom-meeting-create/references/example-full.md +30 -0
  23. package/skills/wecom-meeting-create/references/example-reminder.md +46 -0
  24. package/skills/wecom-meeting-create/references/example-security.md +22 -0
  25. package/skills/wecom-meeting-manage/SKILL.md +151 -0
  26. package/skills/wecom-meeting-query/SKILL.md +328 -0
  27. package/skills/wecom-schedule/SKILL.md +157 -0
  28. package/skills/wecom-schedule/references/api-check-availability.md +56 -0
  29. package/skills/wecom-schedule/references/api-create-schedule.md +38 -0
  30. package/skills/wecom-schedule/references/api-get-schedule-detail.md +81 -0
  31. package/skills/wecom-schedule/references/api-update-schedule.md +30 -0
  32. package/skills/wecom-schedule/references/ref-reminders.md +24 -0
  33. package/skills/wecom-smartsheet-data/SKILL.md +60 -0
  34. package/skills/wecom-smartsheet-data/references/api-get-records.md +61 -0
  35. package/skills/wecom-smartsheet-data/references/cell-value-formats.md +120 -0
  36. package/skills/wecom-smartsheet-schema/SKILL.md +79 -0
  37. package/skills/wecom-smartsheet-schema/references/field-types.md +43 -0
  38. package/dist/src/mcp-config.d.ts +0 -29
  39. package/skills/wecom-doc/SKILL.md +0 -363
  40. package/skills/wecom-doc/references/doc-api.md +0 -224
package/dist/index.esm.js CHANGED
@@ -1,17 +1,15 @@
1
1
  import { readJsonFileWithFallback, withFileLock, writeJsonFileAtomically, DEFAULT_ACCOUNT_ID, addWildcardAllowFrom, formatPairingApproveHint, emptyPluginConfigSchema } from 'openclaw/plugin-sdk';
2
2
  import * as os$1 from 'os';
3
- import os__default$1 from 'os';
4
3
  import * as path$1 from 'path';
5
- import path__default from 'path';
6
4
  import { generateReqId, WSClient } from '@wecom/aibot-node-sdk';
7
5
  import * as fs from 'node:fs/promises';
8
6
  import * as path from 'node:path';
9
- import path__default$1 from 'node:path';
7
+ import path__default, { dirname, resolve } from 'node:path';
10
8
  import * as os from 'node:os';
11
9
  import os__default from 'node:os';
12
10
  import { fileURLToPath } from 'node:url';
13
- import { fileURLToPath as fileURLToPath$1 } from 'url';
14
11
  import { fileTypeFromBuffer } from 'file-type';
12
+ import { readFileSync } from 'node:fs';
15
13
 
16
14
  let runtime = null;
17
15
  function setWeComRuntime(r) {
@@ -392,10 +390,6 @@ const MEDIA_DOCUMENT_PLACEHOLDER = "<media:document>";
392
390
  const MCP_GET_CONFIG_CMD = "aibot_get_mcp_config";
393
391
  /** MCP 配置拉取超时时间(毫秒) */
394
392
  const MCP_CONFIG_FETCH_TIMEOUT_MS = 15000;
395
- const __filename$1 = fileURLToPath$1(import.meta.url);
396
- const __dirname$1 = path__default.dirname(__filename$1);
397
- const PLUGIN_JSON_FILENAME = "openclaw.plugin.json";
398
- path__default.resolve(path__default.join(__dirname$1, "../", PLUGIN_JSON_FILENAME));
399
393
  // ============================================================================
400
394
  // 默认值
401
395
  // ============================================================================
@@ -562,7 +556,7 @@ async function sendWeComReply(params) {
562
556
  }
563
557
  // 使用 SDK 的 replyStream 方法发送消息,带超时保护
564
558
  await withTimeout(wsClient.replyStream(frame, streamId, text, finish), REPLY_SEND_TIMEOUT_MS, `Reply send timed out (streamId=${streamId})`);
565
- runtime.log?.(`[plugin -> server] streamId=${streamId}, finish=${finish}, text=${text}`);
559
+ runtime.log?.(`[plugin -> server] streamId=${streamId}, finish=${finish}`);
566
560
  return streamId;
567
561
  }
568
562
 
@@ -1191,13 +1185,13 @@ function resolveStateDirFromEnv(env = process.env) {
1191
1185
  return stateOverride;
1192
1186
  }
1193
1187
  if (env.VITEST || env.NODE_ENV === "test") {
1194
- return path__default$1.join(os__default.tmpdir(), ["openclaw-vitest", String(process.pid)].join("-"));
1188
+ return path__default.join(os__default.tmpdir(), ["openclaw-vitest", String(process.pid)].join("-"));
1195
1189
  }
1196
- return path__default$1.join(os__default.homedir(), ".openclaw");
1190
+ return path__default.join(os__default.homedir(), ".openclaw");
1197
1191
  }
1198
1192
  function resolveReqIdFilePath(accountId) {
1199
1193
  const safe = accountId.replace(/[^a-zA-Z0-9_-]/g, "_");
1200
- return path__default$1.join(resolveStateDirFromEnv(), "wecom", `reqid-map-${safe}.json`);
1194
+ return path__default.join(resolveStateDirFromEnv(), "wecom", `reqid-map-${safe}.json`);
1201
1195
  }
1202
1196
  // ============================================================================
1203
1197
  // 核心实现
@@ -1553,140 +1547,6 @@ async function cleanupAccount(accountId) {
1553
1547
  }
1554
1548
  }
1555
1549
 
1556
- /**
1557
- * MCP 配置拉取与持久化模块
1558
- *
1559
- * 负责:
1560
- * - 通过 WSClient 发送 aibot_get_mcp_config 请求
1561
- * - 解析服务端响应,提取 MCP 配置(url、type、is_authed)
1562
- * - 将配置写入 openclaw.plugin.json 的 wecomMcp 字段
1563
- */
1564
- // ============================================================================
1565
- // MCP 配置拉取
1566
- // ============================================================================
1567
- /**
1568
- * 通过 WSClient 发送 aibot_get_mcp_config 命令,获取 MCP 配置
1569
- *
1570
- * @param wsClient - 已认证的 WSClient 实例
1571
- * @returns MCP 配置(url、type、is_authed)
1572
- * @throws 响应错误码非 0 或缺少 url 字段时抛出错误
1573
- */
1574
- async function fetchMcpConfig(wsClient) {
1575
- const biz_type = "doc";
1576
- const reqId = generateReqId("mcp_config");
1577
- // 通过 reply 方法发送自定义命令
1578
- const response = await withTimeout(wsClient.reply({ headers: { req_id: reqId } }, { biz_type }, MCP_GET_CONFIG_CMD), MCP_CONFIG_FETCH_TIMEOUT_MS, `MCP config fetch timed out after ${MCP_CONFIG_FETCH_TIMEOUT_MS}ms`);
1579
- // 校验响应错误码
1580
- if (response.errcode && response.errcode !== 0) {
1581
- throw new Error(`MCP config request failed: errcode=${response.errcode}, errmsg=${response.errmsg ?? "unknown"}`);
1582
- }
1583
- // 提取并校验 body
1584
- const body = response.body;
1585
- if (!body?.url) {
1586
- throw new Error("MCP config response missing required 'url' field");
1587
- }
1588
- return {
1589
- biz_type,
1590
- url: body.url,
1591
- type: body.type,
1592
- is_authed: body.is_authed,
1593
- };
1594
- }
1595
- // ============================================================================
1596
- // 配置持久化
1597
- // ============================================================================
1598
- /**
1599
- * 将 MCP 配置写入 openclaw.plugin.json 的 mcpConfig 字段
1600
- *
1601
- * 使用 OpenClaw SDK 提供的文件锁和原子写入,保证并发安全。
1602
- * 配置格式: { mcpConfig: { [type]: { type, url } } }
1603
- * 示例: { "mcpConfig": { "doc": { "type": "streamable-http", "url": "..." } } }
1604
- *
1605
- * @param config - MCP 配置
1606
- * @param runtime - 运行时环境
1607
- */
1608
- async function saveMcpConfigToPluginJson(config, runtime) {
1609
- // const pluginJsonPath = PLUGIN_JSON_PATH;
1610
- // 获取绝对路径并写入到配置文件
1611
- // const absolutePath = path.resolve(pluginJsonPath);
1612
- const wecomConfigDir = path__default.join(os__default$1.homedir(), ".openclaw", "wecomConfig");
1613
- const wecomConfigPath = path__default.join(wecomConfigDir, "config.json");
1614
- // // 写入 wecom 配置文件
1615
- // try {
1616
- // const lockOptions = {
1617
- // stale: 60_000,
1618
- // retries: {
1619
- // retries: 3,
1620
- // factor: 1.35,
1621
- // minTimeout: 8,
1622
- // maxTimeout: 200,
1623
- // randomize: true,
1624
- // },
1625
- // };
1626
- // await withFileLock(wecomConfigPath, lockOptions, async () => {
1627
- // const { value: wecomConfig } = await readJsonFileWithFallback<Record<string, unknown>>(
1628
- // wecomConfigPath,
1629
- // {},
1630
- // );
1631
- // wecomConfig.pluginConfigPath = absolutePath;
1632
- // await writeJsonFileAtomically(wecomConfigPath, wecomConfig);
1633
- // runtime.log?.(`[WeCom] Plugin config path saved to ${wecomConfigPath}`);
1634
- // });
1635
- // } catch (err) {
1636
- // runtime.error?.(`[WeCom] Failed to save plugin config path: ${String(err)}`);
1637
- // }
1638
- const lockOptions = {
1639
- stale: 60000, // 60秒锁过期
1640
- retries: {
1641
- retries: 6,
1642
- factor: 1.35,
1643
- minTimeout: 8,
1644
- maxTimeout: 1200,
1645
- randomize: true,
1646
- },
1647
- };
1648
- await withFileLock(wecomConfigPath, lockOptions, async () => {
1649
- // 读取现有配置(不存在时使用空对象)
1650
- const { value: pluginJson } = await readJsonFileWithFallback(wecomConfigPath, {});
1651
- // 确保 mcpConfig 字段存在且为对象
1652
- if (!pluginJson.mcpConfig || typeof pluginJson.mcpConfig !== 'object') {
1653
- pluginJson.mcpConfig = {};
1654
- }
1655
- // 使用 type 作为键存储配置
1656
- const typeKey = config.biz_type || 'default';
1657
- pluginJson.mcpConfig[typeKey] = {
1658
- type: config.type,
1659
- url: config.url,
1660
- };
1661
- // 原子写入
1662
- await writeJsonFileAtomically(wecomConfigPath, pluginJson);
1663
- runtime.log?.(`[WeCom] MCP config saved to ${wecomConfigPath}`);
1664
- });
1665
- }
1666
- // ============================================================================
1667
- // 组合入口
1668
- // ============================================================================
1669
- /**
1670
- * 拉取 MCP 配置并持久化到 openclaw.plugin.json
1671
- *
1672
- * 认证成功后调用。失败仅记录日志,不影响 WebSocket 消息正常收发。
1673
- *
1674
- * @param wsClient - 已认证的 WSClient 实例
1675
- * @param accountId - 账户 ID(用于日志)
1676
- * @param runtime - 运行时环境(用于日志)
1677
- */
1678
- async function fetchAndSaveMcpConfig(wsClient, accountId, runtime) {
1679
- try {
1680
- runtime.log?.(`[${accountId}] Fetching MCP config...`);
1681
- const config = await fetchMcpConfig(wsClient);
1682
- runtime.log?.(`[${accountId}] MCP config fetched: url=${config.url}, type=${config.type ?? "N/A"}, is_authed=${config.is_authed ?? "N/A"}`);
1683
- await saveMcpConfigToPluginJson(config, runtime);
1684
- }
1685
- catch (err) {
1686
- runtime.error?.(`[${accountId}] Failed to fetch/save MCP config: ${String(err)}`);
1687
- }
1688
- }
1689
-
1690
1550
  /**
1691
1551
  * 企业微信 WebSocket 监控器主模块
1692
1552
  *
@@ -1968,7 +1828,7 @@ async function routeAndDispatchMessage(params) {
1968
1828
  dispatcherOptions: {
1969
1829
  deliver: async (payload, info) => {
1970
1830
  state.deliverCalled = true;
1971
- runtime.log?.(`[openclaw -> plugin] kind=${info.kind}, text=${payload.text ?? ''}, mediaUrl=${payload.mediaUrl ?? ''}, mediaUrls=${JSON.stringify(payload.mediaUrls ?? [])}`);
1831
+ // runtime.log?.(`[openclaw -> plugin] kind=${info.kind}, text=${payload.text ?? ''}, mediaUrl=${payload.mediaUrl ?? ''}, mediaUrls=${JSON.stringify(payload.mediaUrls ?? [])}`);
1972
1832
  // 累积文本
1973
1833
  if (payload.text) {
1974
1834
  accumulateText(state, payload.text);
@@ -2116,7 +1976,7 @@ async function processWeComMessage(params) {
2116
1976
  }
2117
1977
  // Step 7: 构建上下文并路由到核心处理流程(带整体超时保护)
2118
1978
  const ctxPayload = buildMessageContext(frame, account, config, text, mediaList, quoteContent);
2119
- runtime.log?.(`[plugin -> openclaw] body=${text}, mediaPaths=${JSON.stringify(mediaList.map(m => m.path))}${quoteContent ? `, quote=${quoteContent}` : ''}`);
1979
+ // runtime.log?.(`[plugin -> openclaw] body=${text}, mediaPaths=${JSON.stringify(mediaList.map(m => m.path))}${quoteContent ? `, quote=${quoteContent}` : ''}`);
2120
1980
  try {
2121
1981
  await withTimeout(routeAndDispatchMessage({
2122
1982
  ctxPayload,
@@ -2215,8 +2075,6 @@ async function monitorWeComProvider(options) {
2215
2075
  wsClient.on("authenticated", () => {
2216
2076
  runtime.log?.(`[${account.accountId}] Authentication successful`);
2217
2077
  setWeComWebSocket(account.accountId, wsClient);
2218
- // 认证成功后自动拉取 MCP 配置(异步,失败不影响主流程)
2219
- fetchAndSaveMcpConfig(wsClient, account.accountId, runtime);
2220
2078
  });
2221
2079
  // 监听断开事件
2222
2080
  wsClient.on("disconnected", (reason) => {
@@ -2751,6 +2609,713 @@ const wecomPlugin = {
2751
2609
  },
2752
2610
  };
2753
2611
 
2612
+ /** 从 package.json 中读取版本号,兼容打包产物和直接运行 .ts 两种场景 */
2613
+ const getVersion = () => {
2614
+ try {
2615
+ // ESM 环境使用 import.meta.url,CJS 环境使用全局 __dirname
2616
+ const currentDir = dirname(fileURLToPath(import.meta.url));
2617
+ // 直接运行 .ts 时在 src/ 下,打包后在 dist/ 下,都向上一级找 package.json
2618
+ const pkgPath = resolve(currentDir, "..", "package.json");
2619
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
2620
+ return pkg.version ?? "";
2621
+ }
2622
+ catch {
2623
+ return "";
2624
+ }
2625
+ };
2626
+ /** 插件版本号,来源于 package.json */
2627
+ const PLUGIN_VERSION = getVersion();
2628
+
2629
+ /**
2630
+ * MCP Streamable HTTP 传输层模块
2631
+ *
2632
+ * 负责:
2633
+ * - MCP JSON-RPC over HTTP 通信(发送请求、解析响应)
2634
+ * - Streamable HTTP session 生命周期管理(initialize 握手 → Mcp-Session-Id 维护 → 失效重建)
2635
+ * - 自动检测无状态 Server:如果 initialize 响应未返回 Mcp-Session-Id,
2636
+ * 则标记为无状态模式,后续请求跳过握手和 session 管理
2637
+ * - SSE 流式响应解析
2638
+ * - MCP 配置运行时缓存(通过 WSClient 拉取 URL 并缓存在内存中)
2639
+ */
2640
+ // ============================================================================
2641
+ // 内部状态
2642
+ // ============================================================================
2643
+ /** HTTP 请求超时时间(毫秒) */
2644
+ const HTTP_REQUEST_TIMEOUT_MS = 30000;
2645
+ /** 日志前缀 */
2646
+ const LOG_TAG = "[mcp]";
2647
+ /**
2648
+ * MCP JSON-RPC 错误
2649
+ *
2650
+ * 携带服务端返回的 JSON-RPC error.code,
2651
+ * 用于上层按错误码进行差异化处理(如特定错误码触发缓存清理)。
2652
+ */
2653
+ class McpRpcError extends Error {
2654
+ constructor(code, message, data) {
2655
+ super(message);
2656
+ this.code = code;
2657
+ this.data = data;
2658
+ this.name = "McpRpcError";
2659
+ }
2660
+ }
2661
+ /**
2662
+ * MCP HTTP 错误
2663
+ *
2664
+ * 携带 HTTP 状态码,用于精确判断 session 失效(404)等场景,
2665
+ * 避免通过字符串匹配 "404" 导致的误判。
2666
+ */
2667
+ class McpHttpError extends Error {
2668
+ constructor(statusCode, message) {
2669
+ super(message);
2670
+ this.statusCode = statusCode;
2671
+ this.name = "McpHttpError";
2672
+ }
2673
+ }
2674
+ /**
2675
+ * 需要清理缓存的 JSON-RPC 错误码集合
2676
+ *
2677
+ * 当 MCP Server 返回以下错误码时,说明服务端状态已发生变化(如配置变更、
2678
+ * 服务重启等),需要清理对应 category 的全部缓存,确保下次请求重新
2679
+ * 拉取配置并重建会话。
2680
+ *
2681
+ * - -32001: 服务不可用(Server Unavailable)
2682
+ * - -32002: 配置已变更(Config Changed)
2683
+ * - -32003: 认证失败(Auth Failed)
2684
+ */
2685
+ const CACHE_CLEAR_ERROR_CODES = new Set([-32001, -32002, -32003]);
2686
+ /** MCP 配置缓存:category → response.body(完整配置) */
2687
+ const mcpConfigCache = new Map();
2688
+ /** Streamable HTTP 会话缓存:category → session */
2689
+ const mcpSessionCache = new Map();
2690
+ /** 已确认为无状态的 MCP Server 品类集合(跳过后续握手) */
2691
+ const statelessCategories = new Set();
2692
+ /** 正在进行中的 initialize 请求(防止并发重复初始化),key 为 category */
2693
+ const inflightInitRequests = new Map();
2694
+ // ============================================================================
2695
+ // MCP 配置拉取与缓存
2696
+ // ============================================================================
2697
+ /**
2698
+ * 通过 WSClient 拉取指定 category 的 MCP 完整配置
2699
+ *
2700
+ * @param category - MCP 品类名称,如 doc、contact
2701
+ * @returns 完整的 response.body 配置对象(至少包含 url 字段)
2702
+ */
2703
+ async function fetchMcpConfig(category) {
2704
+ const wsClient = getWeComWebSocket(DEFAULT_ACCOUNT_ID);
2705
+ if (!wsClient) {
2706
+ throw new Error("WSClient 未连接,无法拉取 MCP 配置");
2707
+ }
2708
+ const reqId = generateReqId("mcp_config");
2709
+ const response = await withTimeout(wsClient.reply({ headers: { req_id: reqId } }, { biz_type: category, plugin_version: PLUGIN_VERSION }, MCP_GET_CONFIG_CMD), MCP_CONFIG_FETCH_TIMEOUT_MS, `MCP config fetch for "${category}" timed out after ${MCP_CONFIG_FETCH_TIMEOUT_MS}ms`);
2710
+ if (response.errcode !== undefined && response.errcode !== 0) {
2711
+ const errMsg = `MCP 配置请求失败: errcode=${response.errcode}, errmsg=${response.errmsg ?? "unknown"}`;
2712
+ console.error(`${LOG_TAG} ${errMsg}`);
2713
+ throw new Error(errMsg);
2714
+ }
2715
+ const body = response.body;
2716
+ if (!body?.url) {
2717
+ throw new Error(`MCP 配置响应缺少 url 字段 (category="${category}")`);
2718
+ }
2719
+ console.log(`${LOG_TAG} 配置拉取成功 (category="${category}")`);
2720
+ return body;
2721
+ }
2722
+ /**
2723
+ * 获取指定品类的 MCP Server URL
2724
+ *
2725
+ * 优先从内存缓存中读取,未命中时通过 WSClient 拉取并缓存。
2726
+ *
2727
+ * @param category - MCP 品类名称
2728
+ * @returns MCP Server URL
2729
+ */
2730
+ async function getMcpUrl(category) {
2731
+ // 查内存缓存
2732
+ const cached = mcpConfigCache.get(category);
2733
+ if (cached)
2734
+ return cached.url;
2735
+ // 缓存未命中,通过 WSClient 拉取
2736
+ const body = await fetchMcpConfig(category);
2737
+ // 写入缓存
2738
+ mcpConfigCache.set(category, body);
2739
+ console.log(`${LOG_TAG} getMcpUrl ${category}: ${body.url}`);
2740
+ return body.url;
2741
+ }
2742
+ // ============================================================================
2743
+ // HTTP 底层通信
2744
+ // ============================================================================
2745
+ /**
2746
+ * 发送原始 HTTP 请求到 MCP Server(底层方法)
2747
+ *
2748
+ * 自动携带 Mcp-Session-Id 请求头(如果有),
2749
+ * 并从响应头中更新 sessionId。
2750
+ */
2751
+ async function sendRawJsonRpc(url, session, body) {
2752
+ const controller = new AbortController();
2753
+ const timeoutId = setTimeout(() => controller.abort(), HTTP_REQUEST_TIMEOUT_MS);
2754
+ const headers = {
2755
+ "Content-Type": "application/json",
2756
+ Accept: "application/json, text/event-stream",
2757
+ };
2758
+ // Streamable HTTP:携带会话 ID
2759
+ if (session.sessionId) {
2760
+ headers["Mcp-Session-Id"] = session.sessionId;
2761
+ }
2762
+ let response;
2763
+ try {
2764
+ response = await fetch(url, {
2765
+ method: "POST",
2766
+ headers,
2767
+ body: JSON.stringify(body),
2768
+ signal: controller.signal,
2769
+ });
2770
+ }
2771
+ catch (err) {
2772
+ if (err instanceof DOMException && err.name === "AbortError") {
2773
+ throw new Error(`MCP 请求超时 (${HTTP_REQUEST_TIMEOUT_MS}ms)`);
2774
+ }
2775
+ throw new Error(`MCP 网络请求失败: ${err instanceof Error ? err.message : String(err)}`);
2776
+ }
2777
+ finally {
2778
+ clearTimeout(timeoutId);
2779
+ }
2780
+ // 从响应头提取新的 sessionId(不直接修改入参,由调用方决定如何更新)
2781
+ const newSessionId = response.headers.get("mcp-session-id");
2782
+ if (!response.ok) {
2783
+ throw new McpHttpError(response.status, `MCP HTTP 请求失败: ${response.status} ${response.statusText}`);
2784
+ }
2785
+ // Streamable HTTP:notification 响应可能无响应体(204 或 content-length: 0)
2786
+ const contentLength = response.headers.get("content-length");
2787
+ if (response.status === 204 || contentLength === "0") {
2788
+ return { response, rpcResult: undefined, newSessionId };
2789
+ }
2790
+ const contentType = response.headers.get("content-type") ?? "";
2791
+ // 处理 SSE 流式响应
2792
+ if (contentType.includes("text/event-stream")) {
2793
+ return { response, rpcResult: await parseSseResponse(response), newSessionId };
2794
+ }
2795
+ // 普通 JSON 响应 — 先读取文本,防止空内容导致 JSON.parse 报错
2796
+ const text = await response.text();
2797
+ if (!text.trim()) {
2798
+ return { response, rpcResult: undefined, newSessionId };
2799
+ }
2800
+ const rpc = JSON.parse(text);
2801
+ if (rpc.error) {
2802
+ throw new McpRpcError(rpc.error.code, `MCP 调用错误 [${rpc.error.code}]: ${rpc.error.message}`, rpc.error.data);
2803
+ }
2804
+ return { response, rpcResult: rpc.result, newSessionId };
2805
+ }
2806
+ // ============================================================================
2807
+ // Session 管理
2808
+ // ============================================================================
2809
+ /**
2810
+ * 对指定 URL 执行 Streamable HTTP 的 initialize 握手
2811
+ *
2812
+ * 发送 initialize → 接收 serverInfo → 发送 initialized 通知。
2813
+ * 如果服务端未返回 Mcp-Session-Id,则标记为无状态模式,后续请求跳过 session 管理。
2814
+ */
2815
+ async function initializeSession(url, category) {
2816
+ const session = { sessionId: null, initialized: false, stateless: false };
2817
+ console.log(`${LOG_TAG} 开始 initialize 握手 (category="${category}")`);
2818
+ // 1. 发送 initialize 请求
2819
+ const initBody = {
2820
+ jsonrpc: "2.0",
2821
+ id: generateReqId("mcp_init"),
2822
+ method: "initialize",
2823
+ params: {
2824
+ protocolVersion: "2025-03-26",
2825
+ capabilities: {},
2826
+ clientInfo: { name: "wecom_mcp", version: "1.0.0" },
2827
+ },
2828
+ };
2829
+ const { newSessionId: initSessionId } = await sendRawJsonRpc(url, session, initBody);
2830
+ // 用返回的 newSessionId 更新 session(不再依赖副作用修改)
2831
+ if (initSessionId) {
2832
+ session.sessionId = initSessionId;
2833
+ }
2834
+ // 检查服务端是否返回了 Mcp-Session-Id
2835
+ // 如果没有返回,说明该 Server 是无状态实现,无需维护 session
2836
+ if (!session.sessionId) {
2837
+ session.stateless = true;
2838
+ session.initialized = true;
2839
+ statelessCategories.add(category);
2840
+ mcpSessionCache.set(category, session);
2841
+ console.log(`${LOG_TAG} 无状态 Server 确认 (category="${category}")`);
2842
+ return session;
2843
+ }
2844
+ // 2. 发送 initialized 通知(JSON-RPC notification 不带 id 字段)
2845
+ const notifyBody = {
2846
+ jsonrpc: "2.0",
2847
+ method: "notifications/initialized",
2848
+ };
2849
+ // initialized 通知不需要等待响应,但 Streamable HTTP 要求通过 POST 发送
2850
+ const { newSessionId: notifySessionId } = await sendRawJsonRpc(url, session, notifyBody);
2851
+ // 如果 initialized 通知的响应也携带了 sessionId,以最新的为准
2852
+ if (notifySessionId) {
2853
+ session.sessionId = notifySessionId;
2854
+ }
2855
+ session.initialized = true;
2856
+ mcpSessionCache.set(category, session);
2857
+ console.log(`${LOG_TAG} 有状态 Session 建立成功 (category="${category}", sessionId="${session.sessionId}")`);
2858
+ return session;
2859
+ }
2860
+ /**
2861
+ * 获取或创建指定 URL 的 MCP 会话
2862
+ *
2863
+ * - 已确认无状态的 category:直接返回空 session,跳过握手
2864
+ * - 已有可用有状态会话:直接返回缓存
2865
+ * - 其他情况:执行 initialize 握手,并发请求会被合并
2866
+ */
2867
+ async function getOrCreateSession(url, category) {
2868
+ // 已确认为无状态的 Server,直接返回空 session 跳过握手
2869
+ if (statelessCategories.has(category)) {
2870
+ const cached = mcpSessionCache.get(category);
2871
+ if (cached)
2872
+ return cached;
2873
+ // 首次发现被清除(理论上不会走到这里),重新走握手探测
2874
+ }
2875
+ const cached = mcpSessionCache.get(category);
2876
+ if (cached?.initialized)
2877
+ return cached;
2878
+ // 防止并发重复初始化
2879
+ const inflight = inflightInitRequests.get(category);
2880
+ if (inflight)
2881
+ return inflight;
2882
+ const promise = initializeSession(url, category).finally(() => {
2883
+ inflightInitRequests.delete(category);
2884
+ });
2885
+ inflightInitRequests.set(category, promise);
2886
+ return promise;
2887
+ }
2888
+ // ============================================================================
2889
+ // SSE 解析
2890
+ // ============================================================================
2891
+ /**
2892
+ * 解析 SSE 流式响应,提取最终的 JSON-RPC result
2893
+ *
2894
+ * 按照 SSE 规范,同一事件中的多个 `data:` 行会用换行符拼接。
2895
+ * 空行分隔不同事件,取最后一个完整事件的数据。
2896
+ */
2897
+ async function parseSseResponse(response) {
2898
+ const text = await response.text();
2899
+ const lines = text.split("\n");
2900
+ // 按 SSE 规范解析:空行分隔事件,同一事件内的 data 行用换行拼接
2901
+ let currentDataParts = [];
2902
+ let lastEventData = "";
2903
+ for (const line of lines) {
2904
+ if (line.startsWith("data: ")) {
2905
+ currentDataParts.push(line.slice(6));
2906
+ }
2907
+ else if (line.startsWith("data:")) {
2908
+ // data: 后无空格时,值为空字符串
2909
+ currentDataParts.push(line.slice(5));
2910
+ }
2911
+ else if (line.trim() === "" && currentDataParts.length > 0) {
2912
+ // 空行表示事件结束,拼接所有 data 行
2913
+ lastEventData = currentDataParts.join("\n").trim();
2914
+ currentDataParts = [];
2915
+ }
2916
+ }
2917
+ // 处理最后一个未以空行结尾的事件
2918
+ if (currentDataParts.length > 0) {
2919
+ lastEventData = currentDataParts.join("\n").trim();
2920
+ }
2921
+ if (!lastEventData) {
2922
+ throw new Error("SSE 响应中未包含有效数据");
2923
+ }
2924
+ try {
2925
+ const rpc = JSON.parse(lastEventData);
2926
+ if (rpc.error) {
2927
+ throw new McpRpcError(rpc.error.code, `MCP 调用错误 [${rpc.error.code}]: ${rpc.error.message}`, rpc.error.data);
2928
+ }
2929
+ return rpc.result;
2930
+ }
2931
+ catch (err) {
2932
+ if (err instanceof SyntaxError) {
2933
+ throw new Error(`SSE 响应解析失败: ${lastEventData.slice(0, 200)}`);
2934
+ }
2935
+ throw err;
2936
+ }
2937
+ }
2938
+ // ============================================================================
2939
+ // 公共 API
2940
+ // ============================================================================
2941
+ /**
2942
+ * 清理指定品类的所有 MCP 缓存(配置、会话、无状态标记)
2943
+ *
2944
+ * 当 MCP Server 返回特定错误码时调用,确保下次请求重新拉取配置并重建会话。
2945
+ *
2946
+ * @param category - MCP 品类名称
2947
+ */
2948
+ function clearCategoryCache(category) {
2949
+ console.log(`${LOG_TAG} 清理缓存 (category="${category}")`);
2950
+ mcpConfigCache.delete(category);
2951
+ mcpSessionCache.delete(category);
2952
+ statelessCategories.delete(category);
2953
+ inflightInitRequests.delete(category);
2954
+ }
2955
+ /**
2956
+ * 发送 JSON-RPC 请求到 MCP Server(Streamable HTTP 协议)
2957
+ *
2958
+ * 自动管理 session 生命周期:
2959
+ * - 无状态 Server:跳过 session 管理,直接发送请求
2960
+ * - 有状态 Server:首次调用先执行 initialize 握手,session 失效(404)时自动重建并重试
2961
+ *
2962
+ * @param category - MCP 品类名称
2963
+ * @param method - JSON-RPC 方法名
2964
+ * @param params - JSON-RPC 参数
2965
+ * @returns JSON-RPC result
2966
+ */
2967
+ async function sendJsonRpc(category, method, params) {
2968
+ const url = await getMcpUrl(category);
2969
+ const body = {
2970
+ jsonrpc: "2.0",
2971
+ id: generateReqId("mcp_rpc"),
2972
+ method,
2973
+ ...(params !== undefined ? { params } : {}),
2974
+ };
2975
+ let session = await getOrCreateSession(url, category);
2976
+ try {
2977
+ const { rpcResult, newSessionId } = await sendRawJsonRpc(url, session, body);
2978
+ // 用最新的 sessionId 更新 session
2979
+ if (newSessionId) {
2980
+ session.sessionId = newSessionId;
2981
+ }
2982
+ return rpcResult;
2983
+ }
2984
+ catch (err) {
2985
+ // 特定 JSON-RPC 错误码触发缓存清理(统一在传输层处理,上层无需关心)
2986
+ if (err instanceof McpRpcError && CACHE_CLEAR_ERROR_CODES.has(err.code)) {
2987
+ clearCategoryCache(category);
2988
+ }
2989
+ // 无状态 Server 不存在 session 失效问题,直接抛出错误
2990
+ if (session.stateless)
2991
+ throw err;
2992
+ // 有状态 Server:session 失效时服务端返回 404,需要重新初始化并重试一次
2993
+ // 使用 McpHttpError.statusCode 精确匹配,避免字符串匹配 "404" 导致误判
2994
+ if (err instanceof McpHttpError && err.statusCode === 404) {
2995
+ console.log(`${LOG_TAG} Session 失效 (category="${category}"),开始重建...`);
2996
+ mcpSessionCache.delete(category);
2997
+ // 使用 rebuildSession 合并并发的 session 重建请求,避免竞态条件
2998
+ session = await rebuildSession(url, category);
2999
+ const { rpcResult, newSessionId } = await sendRawJsonRpc(url, session, body);
3000
+ if (newSessionId) {
3001
+ session.sessionId = newSessionId;
3002
+ }
3003
+ return rpcResult;
3004
+ }
3005
+ // 其他错误记录日志后抛出
3006
+ console.error(`${LOG_TAG} RPC 请求失败 (category="${category}", method="${method}"): ${err instanceof Error ? err.message : String(err)}`);
3007
+ throw err;
3008
+ }
3009
+ }
3010
+ /**
3011
+ * 合并并发的 session 重建请求
3012
+ *
3013
+ * 与 getOrCreateSession 类似,使用 inflightInitRequests 防止
3014
+ * 多个并发请求同时遇到 404 时重复执行 initialize 握手。
3015
+ */
3016
+ async function rebuildSession(url, category) {
3017
+ const inflight = inflightInitRequests.get(category);
3018
+ if (inflight)
3019
+ return inflight;
3020
+ const promise = initializeSession(url, category).finally(() => {
3021
+ inflightInitRequests.delete(category);
3022
+ });
3023
+ inflightInitRequests.set(category, promise);
3024
+ return promise;
3025
+ }
3026
+
3027
+ /**
3028
+ * MCP Schema 清洗模块
3029
+ *
3030
+ * 负责内联 $ref/$defs 引用并移除 Gemini 不支持的 JSON Schema 关键词,
3031
+ * 防止 Gemini 模型解析 function response 时报 400 错误。
3032
+ */
3033
+ /** Gemini 不支持的 JSON Schema 关键词 */
3034
+ const GEMINI_UNSUPPORTED_KEYWORDS = new Set([
3035
+ "patternProperties", "additionalProperties", "$schema", "$id", "$ref", "$defs",
3036
+ "definitions", "examples", "minLength", "maxLength", "minimum", "maximum",
3037
+ "multipleOf", "pattern", "format", "minItems", "maxItems", "uniqueItems",
3038
+ "minProperties", "maxProperties",
3039
+ ]);
3040
+ /**
3041
+ * 清洗 JSON Schema,内联 $ref 引用并移除 Gemini 不支持的关键词,
3042
+ * 防止 Gemini 模型解析 function response 时报 400 错误。
3043
+ */
3044
+ function cleanSchemaForGemini(schema) {
3045
+ if (!schema || typeof schema !== "object")
3046
+ return schema;
3047
+ if (Array.isArray(schema))
3048
+ return schema.map(cleanSchemaForGemini);
3049
+ const obj = schema;
3050
+ // 收集 $defs/definitions 用于后续 $ref 内联解析
3051
+ const defs = {
3052
+ ...(obj.$defs && typeof obj.$defs === "object" ? obj.$defs : {}),
3053
+ ...(obj.definitions && typeof obj.definitions === "object" ? obj.definitions : {}),
3054
+ };
3055
+ return cleanWithDefs(obj, defs, new Set());
3056
+ }
3057
+ function cleanWithDefs(schema, defs, refStack) {
3058
+ if (!schema || typeof schema !== "object")
3059
+ return schema;
3060
+ if (Array.isArray(schema))
3061
+ return schema.map((item) => cleanWithDefs(item, defs, refStack));
3062
+ const obj = schema;
3063
+ // 合并当前层级的 $defs/definitions 到 defs 中
3064
+ if (obj.$defs && typeof obj.$defs === "object") {
3065
+ Object.assign(defs, obj.$defs);
3066
+ }
3067
+ if (obj.definitions && typeof obj.definitions === "object") {
3068
+ Object.assign(defs, obj.definitions);
3069
+ }
3070
+ // 处理 $ref 引用:尝试内联解析
3071
+ if (typeof obj.$ref === "string") {
3072
+ const ref = obj.$ref;
3073
+ if (refStack.has(ref))
3074
+ return {}; // 防止循环引用
3075
+ const match = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/);
3076
+ if (match && match[1] && defs[match[1]]) {
3077
+ const nextStack = new Set(refStack);
3078
+ nextStack.add(ref);
3079
+ return cleanWithDefs(defs[match[1]], defs, nextStack);
3080
+ }
3081
+ return {}; // 无法解析的 $ref,返回空对象
3082
+ }
3083
+ const cleaned = {};
3084
+ for (const [key, value] of Object.entries(obj)) {
3085
+ if (GEMINI_UNSUPPORTED_KEYWORDS.has(key))
3086
+ continue;
3087
+ if (key === "const") {
3088
+ cleaned.enum = [value];
3089
+ continue;
3090
+ }
3091
+ if (key === "properties" && value && typeof value === "object" && !Array.isArray(value)) {
3092
+ cleaned[key] = Object.fromEntries(Object.entries(value).map(([k, v]) => [
3093
+ k, cleanWithDefs(v, defs, refStack),
3094
+ ]));
3095
+ }
3096
+ else if (key === "items" && value) {
3097
+ cleaned[key] = Array.isArray(value)
3098
+ ? value.map((item) => cleanWithDefs(item, defs, refStack))
3099
+ : cleanWithDefs(value, defs, refStack);
3100
+ }
3101
+ else if ((key === "anyOf" || key === "oneOf" || key === "allOf") && Array.isArray(value)) {
3102
+ // 过滤掉 null 类型的变体
3103
+ const nonNull = value.filter((v) => {
3104
+ if (!v || typeof v !== "object")
3105
+ return true;
3106
+ const r = v;
3107
+ return r.type !== "null";
3108
+ });
3109
+ if (nonNull.length === 1) {
3110
+ // 只剩一个变体时直接内联
3111
+ const single = cleanWithDefs(nonNull[0], defs, refStack);
3112
+ if (single && typeof single === "object" && !Array.isArray(single)) {
3113
+ Object.assign(cleaned, single);
3114
+ }
3115
+ }
3116
+ else {
3117
+ cleaned[key] = nonNull.map((v) => cleanWithDefs(v, defs, refStack));
3118
+ }
3119
+ }
3120
+ else {
3121
+ cleaned[key] = value;
3122
+ }
3123
+ }
3124
+ return cleaned;
3125
+ }
3126
+
3127
+ /**
3128
+ * wecom_mcp — 模拟 MCP 调用的 Agent Tool
3129
+ *
3130
+ * 通过 MCP Streamable HTTP 传输协议调用企业微信 MCP Server,
3131
+ * 提供 list(列出所有工具)和 call(调用工具)两个操作。
3132
+ *
3133
+ * 在 skills 中的使用方式:
3134
+ * wecom_mcp list <category>
3135
+ * wecom_mcp call <category> <method> '<jsonArgs>'
3136
+ *
3137
+ * 示例:
3138
+ * wecom_mcp list contact
3139
+ * wecom_mcp call contact getContact '{}'
3140
+ */
3141
+ // ============================================================================
3142
+ // 响应构造辅助
3143
+ // ============================================================================
3144
+ /** 构造统一的文本响应结构 */
3145
+ const textResult = (data) => ({
3146
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
3147
+ });
3148
+ /** 构造错误响应 */
3149
+ const errorResult = (err) => {
3150
+ // 适配企业微信 API 返回的 { errcode, errmsg } 结构
3151
+ if (err && typeof err === "object" && "errcode" in err) {
3152
+ const { errcode, errmsg } = err;
3153
+ return textResult({ error: errmsg ?? `错误码: ${errcode}`, errcode });
3154
+ }
3155
+ const message = err instanceof Error ? err.message : String(err);
3156
+ return textResult({ error: message });
3157
+ };
3158
+ // ============================================================================
3159
+ // list 操作:列出某品类的所有 MCP 工具
3160
+ // ============================================================================
3161
+ const handleList = async (category) => {
3162
+ const result = await sendJsonRpc(category, "tools/list");
3163
+ const tools = result?.tools ?? [];
3164
+ if (tools.length === 0) {
3165
+ return { message: `品类 "${category}" 下暂无可用工具`, tools: [] };
3166
+ }
3167
+ return {
3168
+ category,
3169
+ count: tools.length,
3170
+ tools: tools.map((t) => ({
3171
+ name: t.name,
3172
+ description: t.description ?? "",
3173
+ // 清洗 inputSchema,内联 $ref/$defs 引用并移除 Gemini 不支持的关键词,
3174
+ // 避免 Gemini 模型解析 function response 时报 400 错误
3175
+ inputSchema: t.inputSchema ? cleanSchemaForGemini(t.inputSchema) : undefined,
3176
+ })),
3177
+ };
3178
+ };
3179
+ // ============================================================================
3180
+ // call 操作:调用某品类的某个 MCP 工具
3181
+ // ============================================================================
3182
+ /**
3183
+ * 需要触发缓存清理的业务错误码集合
3184
+ *
3185
+ * 这些错误码出现在 MCP 工具调用返回的 content 文本中(业务层面),
3186
+ * 与 JSON-RPC 层面的错误码不同,需要在此处额外检测。
3187
+ *
3188
+ * - 850002: 机器人未被授权使用对应能力,需清理缓存以便下次重新拉取配置
3189
+ */
3190
+ const BIZ_CACHE_CLEAR_ERROR_CODES = new Set([850002]);
3191
+ /**
3192
+ * 检查 tools/call 的返回结果中是否包含需要清理缓存的业务错误码
3193
+ *
3194
+ * MCP Server 可能在正常的 JSON-RPC 响应中返回业务层错误,
3195
+ * 这些错误被包裹在 result.content[].text 中,需要解析后判断。
3196
+ */
3197
+ const checkBizErrorAndClearCache = (result, category) => {
3198
+ if (!result || typeof result !== "object")
3199
+ return;
3200
+ const { content } = result;
3201
+ if (!Array.isArray(content))
3202
+ return;
3203
+ for (const item of content) {
3204
+ if (item.type !== "text" || !item.text)
3205
+ continue;
3206
+ try {
3207
+ const parsed = JSON.parse(item.text);
3208
+ if (typeof parsed.errcode === "number" && BIZ_CACHE_CLEAR_ERROR_CODES.has(parsed.errcode)) {
3209
+ console.log(`[mcp] 检测到业务错误码 ${parsed.errcode} (category="${category}"),清理缓存`);
3210
+ clearCategoryCache(category);
3211
+ return;
3212
+ }
3213
+ }
3214
+ catch {
3215
+ // text 不是 JSON 格式,跳过
3216
+ }
3217
+ }
3218
+ };
3219
+ const handleCall = async (category, method, args) => {
3220
+ const result = await sendJsonRpc(category, "tools/call", {
3221
+ name: method,
3222
+ arguments: args,
3223
+ });
3224
+ // 检查业务层错误码,必要时清理缓存
3225
+ checkBizErrorAndClearCache(result, category);
3226
+ return result;
3227
+ };
3228
+ // ============================================================================
3229
+ // 参数解析
3230
+ // ============================================================================
3231
+ /**
3232
+ * 解析 args 参数:支持 JSON 字符串或直接的对象
3233
+ */
3234
+ const parseArgs = (args) => {
3235
+ if (!args)
3236
+ return {};
3237
+ if (typeof args === "object")
3238
+ return args;
3239
+ try {
3240
+ return JSON.parse(args);
3241
+ }
3242
+ catch (err) {
3243
+ const detail = err instanceof SyntaxError ? err.message : String(err);
3244
+ throw new Error(`args 参数不是合法的 JSON: ${args} (${detail})`);
3245
+ }
3246
+ };
3247
+ // ============================================================================
3248
+ // 工具定义 & 导出
3249
+ // ============================================================================
3250
+ /**
3251
+ * 创建 wecom_mcp Agent Tool 定义
3252
+ */
3253
+ function createWeComMcpTool() {
3254
+ return {
3255
+ name: "wecom_mcp",
3256
+ label: "企业微信 MCP 工具",
3257
+ description: [
3258
+ "通过 HTTP 直接调用企业微信 MCP Server。",
3259
+ "支持两种操作:",
3260
+ " - list: 列出指定品类的所有 MCP 工具",
3261
+ " - call: 调用指定品类的某个 MCP 工具",
3262
+ "",
3263
+ "使用方式:",
3264
+ " wecom_mcp list <category>",
3265
+ " wecom_mcp call <category> <method> '<jsonArgs>'",
3266
+ "",
3267
+ "示例:",
3268
+ " 列出 contact 品类所有工具:wecom_mcp list contact",
3269
+ " 调用 contact 的 getContact:wecom_mcp call contact getContact '{}'",
3270
+ ].join("\n"),
3271
+ parameters: {
3272
+ type: "object",
3273
+ properties: {
3274
+ action: {
3275
+ type: "string",
3276
+ enum: ["list", "call"],
3277
+ description: "操作类型:list(列出工具)或 call(调用工具)",
3278
+ },
3279
+ category: {
3280
+ type: "string",
3281
+ description: "MCP 品类名称,如 doc、contact 等,对应 mcpConfig 中的 key",
3282
+ },
3283
+ method: {
3284
+ type: "string",
3285
+ description: "要调用的 MCP 方法名(action=call 时必填)",
3286
+ },
3287
+ args: {
3288
+ type: ["string", "object"],
3289
+ description: "调用 MCP 方法的参数,可以是 JSON 字符串或对象(action=call 时使用,默认 {})",
3290
+ },
3291
+ },
3292
+ required: ["action", "category"],
3293
+ },
3294
+ async execute(_toolCallId, params) {
3295
+ const p = params;
3296
+ try {
3297
+ switch (p.action) {
3298
+ case "list":
3299
+ return textResult(await handleList(p.category));
3300
+ case "call": {
3301
+ if (!p.method) {
3302
+ return textResult({ error: "action 为 call 时必须提供 method 参数" });
3303
+ }
3304
+ const args = parseArgs(p.args);
3305
+ return textResult(await handleCall(p.category, p.method, args));
3306
+ }
3307
+ default:
3308
+ return textResult({ error: `未知操作类型: ${String(p.action)},支持 list 和 call` });
3309
+ }
3310
+ }
3311
+ catch (err) {
3312
+ return errorResult(err);
3313
+ }
3314
+ },
3315
+ };
3316
+ }
3317
+
3318
+ console.log(`[wecom] v${PLUGIN_VERSION} loaded`);
2754
3319
  const plugin = {
2755
3320
  id: "wecom-openclaw-plugin",
2756
3321
  name: "企业微信",
@@ -2759,6 +3324,8 @@ const plugin = {
2759
3324
  register(api) {
2760
3325
  setWeComRuntime(api.runtime);
2761
3326
  api.registerChannel({ plugin: wecomPlugin });
3327
+ // 注册 wecom_mcp:通过 HTTP 直接调用企业微信 MCP Server
3328
+ api.registerTool(createWeComMcpTool(), { name: "wecom_mcp" });
2762
3329
  // 注入媒体发送指令和文件大小限制提示词
2763
3330
  api.on("before_prompt_build", () => {
2764
3331
  return {