@wecom/wecom-openclaw-plugin 1.0.9-beta.0 → 1.0.9-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -1,8 +1,11 @@
1
1
  import { readJsonFileWithFallback, withFileLock, writeJsonFileAtomically, DEFAULT_ACCOUNT_ID, addWildcardAllowFrom, formatPairingApproveHint, emptyPluginConfigSchema } from 'openclaw/plugin-sdk';
2
2
  import { generateReqId, WSClient } from '@wecom/aibot-node-sdk';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
3
5
  import { fileTypeFromBuffer } from 'file-type';
4
6
  import os from 'node:os';
5
- import path from 'node:path';
7
+ import path$1 from 'node:path';
8
+ import os$1 from 'os';
6
9
 
7
10
  let runtime = null;
8
11
  function setWeComRuntime(r) {
@@ -72,6 +75,20 @@ const MEDIA_DOCUMENT_PLACEHOLDER = "<media:document>";
72
75
  // ============================================================================
73
76
  // 默认值
74
77
  // ============================================================================
78
+ // ============================================================================
79
+ // MCP 配置
80
+ // ============================================================================
81
+ /** 获取 MCP 配置的 WebSocket 命令 */
82
+ const MCP_GET_CONFIG_CMD = "aibot_get_mcp_config";
83
+ /** MCP 配置拉取超时时间(毫秒) */
84
+ const MCP_CONFIG_FETCH_TIMEOUT_MS = 15000;
85
+ const __filename$1 = fileURLToPath(import.meta.url);
86
+ const __dirname$1 = path.dirname(__filename$1);
87
+ const PLUGIN_JSON_FILENAME = "openclaw.plugin.json";
88
+ path.resolve(path.join(__dirname$1, "../", PLUGIN_JSON_FILENAME));
89
+ // ============================================================================
90
+ // 默认值
91
+ // ============================================================================
75
92
  /** 默认媒体大小上限(MB) */
76
93
  const DEFAULT_MEDIA_MAX_MB = 5;
77
94
  /** 文本分块大小上限 */
@@ -490,7 +507,7 @@ async function checkDmPolicy(params) {
490
507
  if (isGroup) {
491
508
  return { allowed: true };
492
509
  }
493
- const dmPolicy = account.config.dmPolicy ?? "pairing";
510
+ const dmPolicy = account.config.dmPolicy ?? "open";
494
511
  const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
495
512
  // 如果 dmPolicy 是 disabled,直接拒绝
496
513
  if (dmPolicy === "disabled") {
@@ -577,13 +594,13 @@ function resolveStateDirFromEnv(env = process.env) {
577
594
  return stateOverride;
578
595
  }
579
596
  if (env.VITEST || env.NODE_ENV === "test") {
580
- return path.join(os.tmpdir(), ["openclaw-vitest", String(process.pid)].join("-"));
597
+ return path$1.join(os.tmpdir(), ["openclaw-vitest", String(process.pid)].join("-"));
581
598
  }
582
- return path.join(os.homedir(), ".openclaw");
599
+ return path$1.join(os.homedir(), ".openclaw");
583
600
  }
584
601
  function resolveReqIdFilePath(accountId) {
585
602
  const safe = accountId.replace(/[^a-zA-Z0-9_-]/g, "_");
586
- return path.join(resolveStateDirFromEnv(), "wecom", `reqid-map-${safe}.json`);
603
+ return path$1.join(resolveStateDirFromEnv(), "wecom", `reqid-map-${safe}.json`);
587
604
  }
588
605
  // ============================================================================
589
606
  // 核心实现
@@ -939,6 +956,138 @@ async function cleanupAccount(accountId) {
939
956
  }
940
957
  }
941
958
 
959
+ /**
960
+ * MCP 配置拉取与持久化模块
961
+ *
962
+ * 负责:
963
+ * - 通过 WSClient 发送 aibot_get_mcp_config 请求
964
+ * - 解析服务端响应,提取 MCP 配置(url、type、is_authed)
965
+ * - 将配置写入 openclaw.plugin.json 的 wecomMcp 字段
966
+ */
967
+ // ============================================================================
968
+ // MCP 配置拉取
969
+ // ============================================================================
970
+ /**
971
+ * 通过 WSClient 发送 aibot_get_mcp_config 命令,获取 MCP 配置
972
+ *
973
+ * @param wsClient - 已认证的 WSClient 实例
974
+ * @returns MCP 配置(url、type、is_authed)
975
+ * @throws 响应错误码非 0 或缺少 url 字段时抛出错误
976
+ */
977
+ async function fetchMcpConfig(wsClient) {
978
+ const reqId = generateReqId("mcp_config");
979
+ // 通过 reply 方法发送自定义命令
980
+ const response = await withTimeout(wsClient.reply({ headers: { req_id: reqId } }, { biz_type: "doc" }, MCP_GET_CONFIG_CMD), MCP_CONFIG_FETCH_TIMEOUT_MS, `MCP config fetch timed out after ${MCP_CONFIG_FETCH_TIMEOUT_MS}ms`);
981
+ // 校验响应错误码
982
+ if (response.errcode && response.errcode !== 0) {
983
+ throw new Error(`MCP config request failed: errcode=${response.errcode}, errmsg=${response.errmsg ?? "unknown"}`);
984
+ }
985
+ // 提取并校验 body
986
+ const body = response.body;
987
+ if (!body?.url) {
988
+ throw new Error("MCP config response missing required 'url' field");
989
+ }
990
+ return {
991
+ url: body.url,
992
+ type: 'doc',
993
+ is_authed: body.is_authed,
994
+ };
995
+ }
996
+ // ============================================================================
997
+ // 配置持久化
998
+ // ============================================================================
999
+ /**
1000
+ * 将 MCP 配置写入 openclaw.plugin.json 的 mcpConfig 字段
1001
+ *
1002
+ * 使用 OpenClaw SDK 提供的文件锁和原子写入,保证并发安全。
1003
+ * 配置格式: { mcpConfig: { [type]: { type, url } } }
1004
+ * 示例: { "mcpConfig": { "doc": { "type": "streamable-http", "url": "..." } } }
1005
+ *
1006
+ * @param config - MCP 配置
1007
+ * @param runtime - 运行时环境
1008
+ */
1009
+ async function saveMcpConfigToPluginJson(config, runtime) {
1010
+ // const pluginJsonPath = PLUGIN_JSON_PATH;
1011
+ // 获取绝对路径并写入到配置文件
1012
+ // const absolutePath = path.resolve(pluginJsonPath);
1013
+ const wecomConfigDir = path.join(os$1.homedir(), ".openclaw", "wecomConfig");
1014
+ const wecomConfigPath = path.join(wecomConfigDir, "config.json");
1015
+ // // 写入 wecom 配置文件
1016
+ // try {
1017
+ // const lockOptions = {
1018
+ // stale: 60_000,
1019
+ // retries: {
1020
+ // retries: 3,
1021
+ // factor: 1.35,
1022
+ // minTimeout: 8,
1023
+ // maxTimeout: 200,
1024
+ // randomize: true,
1025
+ // },
1026
+ // };
1027
+ // await withFileLock(wecomConfigPath, lockOptions, async () => {
1028
+ // const { value: wecomConfig } = await readJsonFileWithFallback<Record<string, unknown>>(
1029
+ // wecomConfigPath,
1030
+ // {},
1031
+ // );
1032
+ // wecomConfig.pluginConfigPath = absolutePath;
1033
+ // await writeJsonFileAtomically(wecomConfigPath, wecomConfig);
1034
+ // runtime.log?.(`[WeCom] Plugin config path saved to ${wecomConfigPath}`);
1035
+ // });
1036
+ // } catch (err) {
1037
+ // runtime.error?.(`[WeCom] Failed to save plugin config path: ${String(err)}`);
1038
+ // }
1039
+ const lockOptions = {
1040
+ stale: 60000, // 60秒锁过期
1041
+ retries: {
1042
+ retries: 6,
1043
+ factor: 1.35,
1044
+ minTimeout: 8,
1045
+ maxTimeout: 1200,
1046
+ randomize: true,
1047
+ },
1048
+ };
1049
+ await withFileLock(wecomConfigPath, lockOptions, async () => {
1050
+ // 读取现有配置(不存在时使用空对象)
1051
+ const { value: pluginJson } = await readJsonFileWithFallback(wecomConfigPath, {});
1052
+ // 确保 mcpConfig 字段存在且为对象
1053
+ if (!pluginJson.mcpConfig || typeof pluginJson.mcpConfig !== 'object') {
1054
+ pluginJson.mcpConfig = {};
1055
+ }
1056
+ // 使用 type 作为键存储配置
1057
+ const typeKey = config.type || 'default';
1058
+ pluginJson.mcpConfig[typeKey] = {
1059
+ type: config.type,
1060
+ url: config.url,
1061
+ };
1062
+ // 原子写入
1063
+ await writeJsonFileAtomically(wecomConfigPath, pluginJson);
1064
+ runtime.log?.(`[WeCom] MCP config saved to ${wecomConfigPath}`);
1065
+ });
1066
+ }
1067
+ // ============================================================================
1068
+ // 组合入口
1069
+ // ============================================================================
1070
+ /**
1071
+ * 拉取 MCP 配置并持久化到 openclaw.plugin.json
1072
+ *
1073
+ * 认证成功后调用。失败仅记录日志,不影响 WebSocket 消息正常收发。
1074
+ *
1075
+ * @param wsClient - 已认证的 WSClient 实例
1076
+ * @param accountId - 账户 ID(用于日志)
1077
+ * @param runtime - 运行时环境(用于日志)
1078
+ */
1079
+ async function fetchAndSaveMcpConfig(wsClient, accountId, runtime) {
1080
+ try {
1081
+ runtime.log?.(`[${accountId}] Fetching MCP config...`);
1082
+ const config = await fetchMcpConfig(wsClient);
1083
+ runtime.log?.(`[${accountId}] MCP config fetched: url=${config.url}, type=${config.type ?? "N/A"}, is_authed=${config.is_authed ?? "N/A"}`);
1084
+ await saveMcpConfigToPluginJson(config, runtime);
1085
+ }
1086
+ catch (err) {
1087
+ runtime.error?.(`[${accountId}] Failed to fetch/save MCP config: ${String(err)}`);
1088
+ }
1089
+ }
1090
+
942
1091
  /**
943
1092
  * 企业微信 WebSocket 监控器主模块
944
1093
  *
@@ -1274,6 +1423,8 @@ async function monitorWeComProvider(options) {
1274
1423
  wsClient.on("authenticated", () => {
1275
1424
  runtime.log?.(`[${account.accountId}] Authentication successful`);
1276
1425
  setWeComWebSocket(account.accountId, wsClient);
1426
+ // 认证成功后自动拉取 MCP 配置(异步,失败不影响主流程)
1427
+ fetchAndSaveMcpConfig(wsClient, account.accountId, runtime);
1277
1428
  });
1278
1429
  // 监听断开事件
1279
1430
  wsClient.on("disconnected", (reason) => {
@@ -1427,7 +1578,7 @@ const dmPolicy = {
1427
1578
  allowFromKey: `channels.${CHANNEL_ID}.allowFrom`,
1428
1579
  getCurrent: (cfg) => {
1429
1580
  const account = resolveWeComAccount(cfg);
1430
- return account.config.dmPolicy ?? "pairing";
1581
+ return account.config.dmPolicy ?? "open";
1431
1582
  },
1432
1583
  setPolicy: (cfg, policy) => {
1433
1584
  return setWeComDmPolicy(cfg, policy);
@@ -1473,7 +1624,7 @@ const wecomOnboardingAdapter = {
1473
1624
  botId,
1474
1625
  secret,
1475
1626
  enabled: true,
1476
- dmPolicy: account.config.dmPolicy ?? "pairing",
1627
+ dmPolicy: account.config.dmPolicy ?? "open",
1477
1628
  allowFrom: account.config.allowFrom ?? [],
1478
1629
  });
1479
1630
  return { cfg: cfgWithAccount };
@@ -1609,7 +1760,7 @@ const wecomPlugin = {
1609
1760
  resolveDmPolicy: ({ account }) => {
1610
1761
  const basePath = `channels.${CHANNEL_ID}.`;
1611
1762
  return {
1612
- policy: account.config.dmPolicy ?? "pairing",
1763
+ policy: account.config.dmPolicy ?? "open",
1613
1764
  allowFrom: account.config.allowFrom ?? [],
1614
1765
  policyPath: `${basePath}dmPolicy`,
1615
1766
  allowFromPath: basePath,
@@ -1620,7 +1771,7 @@ const wecomPlugin = {
1620
1771
  collectWarnings: ({ account, cfg }) => {
1621
1772
  const warnings = [];
1622
1773
  // DM 策略警告
1623
- const dmPolicy = account.config.dmPolicy ?? "pairing";
1774
+ const dmPolicy = account.config.dmPolicy ?? "open";
1624
1775
  if (dmPolicy === "open") {
1625
1776
  const hasWildcard = (account.config.allowFrom ?? []).some((entry) => String(entry).trim() === "*");
1626
1777
  if (!hasWildcard) {
@@ -1662,7 +1813,7 @@ const wecomPlugin = {
1662
1813
  listGroups: async () => [],
1663
1814
  },
1664
1815
  outbound: {
1665
- deliveryMode: "direct",
1816
+ deliveryMode: "gateway",
1666
1817
  chunker: (text, limit) => getWeComRuntime().channel.text.chunkMarkdownText(text, limit),
1667
1818
  textChunkLimit: TEXT_CHUNK_LIMIT,
1668
1819
  sendText: async ({ to, text, accountId, ...rest }) => {