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

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 CHANGED
@@ -62,7 +62,7 @@ openclaw gateway restart
62
62
  | `channels.wecom.secret` | WeCom bot secret | — | — |
63
63
  | `channels.wecom.enabled` | Enable the channel | `true` / `false` | `false` |
64
64
  | `channels.wecom.websocketUrl` | WebSocket endpoint | — | `wss://openws.work.weixin.qq.com` |
65
- | `channels.wecom.dmPolicy` | DM access policy | `pairing` / `open` / `allowlist` / `disabled` | `pairing` |
65
+ | `channels.wecom.dmPolicy` | DM access policy | `pairing` / `open` / `allowlist` / `disabled` | `open` |
66
66
  | `channels.wecom.allowFrom` | DM allowlist (user IDs) | — | `[]` |
67
67
  | `channels.wecom.groupPolicy` | Group chat access policy | `open` / `allowlist` / `disabled` | `open` |
68
68
  | `channels.wecom.groupAllowFrom` | Group allowlist (group IDs) | — | `[]` |
@@ -74,7 +74,7 @@ openclaw gateway restart
74
74
 
75
75
  ### DM (Direct Message) Access
76
76
 
77
- **Default**: `dmPolicy: "pairing"` — unrecognized users will receive a pairing code.
77
+ **Default**: `dmPolicy: "open"` — all users can send direct messages without approval.
78
78
 
79
79
  #### Approve Pairing
80
80
 
package/dist/index.cjs.js CHANGED
@@ -4,10 +4,14 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var pluginSdk = require('openclaw/plugin-sdk');
6
6
  var aibotNodeSdk = require('@wecom/aibot-node-sdk');
7
+ var path = require('path');
8
+ var url = require('url');
7
9
  var fileType = require('file-type');
8
10
  var os = require('node:os');
9
- var path = require('node:path');
11
+ var path$1 = require('node:path');
12
+ var os$1 = require('os');
10
13
 
14
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
11
15
  let runtime = null;
12
16
  function setWeComRuntime(r) {
13
17
  runtime = r;
@@ -76,6 +80,20 @@ const MEDIA_DOCUMENT_PLACEHOLDER = "<media:document>";
76
80
  // ============================================================================
77
81
  // 默认值
78
82
  // ============================================================================
83
+ // ============================================================================
84
+ // MCP 配置
85
+ // ============================================================================
86
+ /** 获取 MCP 配置的 WebSocket 命令 */
87
+ const MCP_GET_CONFIG_CMD = "aibot_get_mcp_config";
88
+ /** MCP 配置拉取超时时间(毫秒) */
89
+ const MCP_CONFIG_FETCH_TIMEOUT_MS = 15000;
90
+ const __filename$1 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs.js', document.baseURI).href)));
91
+ const __dirname$1 = path.dirname(__filename$1);
92
+ const PLUGIN_JSON_FILENAME = "openclaw.plugin.json";
93
+ path.resolve(path.join(__dirname$1, "../", PLUGIN_JSON_FILENAME));
94
+ // ============================================================================
95
+ // 默认值
96
+ // ============================================================================
79
97
  /** 默认媒体大小上限(MB) */
80
98
  const DEFAULT_MEDIA_MAX_MB = 5;
81
99
  /** 文本分块大小上限 */
@@ -494,7 +512,7 @@ async function checkDmPolicy(params) {
494
512
  if (isGroup) {
495
513
  return { allowed: true };
496
514
  }
497
- const dmPolicy = account.config.dmPolicy ?? "pairing";
515
+ const dmPolicy = account.config.dmPolicy ?? "open";
498
516
  const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
499
517
  // 如果 dmPolicy 是 disabled,直接拒绝
500
518
  if (dmPolicy === "disabled") {
@@ -581,13 +599,13 @@ function resolveStateDirFromEnv(env = process.env) {
581
599
  return stateOverride;
582
600
  }
583
601
  if (env.VITEST || env.NODE_ENV === "test") {
584
- return path.join(os.tmpdir(), ["openclaw-vitest", String(process.pid)].join("-"));
602
+ return path$1.join(os.tmpdir(), ["openclaw-vitest", String(process.pid)].join("-"));
585
603
  }
586
- return path.join(os.homedir(), ".openclaw");
604
+ return path$1.join(os.homedir(), ".openclaw");
587
605
  }
588
606
  function resolveReqIdFilePath(accountId) {
589
607
  const safe = accountId.replace(/[^a-zA-Z0-9_-]/g, "_");
590
- return path.join(resolveStateDirFromEnv(), "wecom", `reqid-map-${safe}.json`);
608
+ return path$1.join(resolveStateDirFromEnv(), "wecom", `reqid-map-${safe}.json`);
591
609
  }
592
610
  // ============================================================================
593
611
  // 核心实现
@@ -943,6 +961,138 @@ async function cleanupAccount(accountId) {
943
961
  }
944
962
  }
945
963
 
964
+ /**
965
+ * MCP 配置拉取与持久化模块
966
+ *
967
+ * 负责:
968
+ * - 通过 WSClient 发送 aibot_get_mcp_config 请求
969
+ * - 解析服务端响应,提取 MCP 配置(url、type、is_authed)
970
+ * - 将配置写入 openclaw.plugin.json 的 wecomMcp 字段
971
+ */
972
+ // ============================================================================
973
+ // MCP 配置拉取
974
+ // ============================================================================
975
+ /**
976
+ * 通过 WSClient 发送 aibot_get_mcp_config 命令,获取 MCP 配置
977
+ *
978
+ * @param wsClient - 已认证的 WSClient 实例
979
+ * @returns MCP 配置(url、type、is_authed)
980
+ * @throws 响应错误码非 0 或缺少 url 字段时抛出错误
981
+ */
982
+ async function fetchMcpConfig(wsClient) {
983
+ const reqId = aibotNodeSdk.generateReqId("mcp_config");
984
+ // 通过 reply 方法发送自定义命令
985
+ 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`);
986
+ // 校验响应错误码
987
+ if (response.errcode && response.errcode !== 0) {
988
+ throw new Error(`MCP config request failed: errcode=${response.errcode}, errmsg=${response.errmsg ?? "unknown"}`);
989
+ }
990
+ // 提取并校验 body
991
+ const body = response.body;
992
+ if (!body?.url) {
993
+ throw new Error("MCP config response missing required 'url' field");
994
+ }
995
+ return {
996
+ url: body.url,
997
+ type: 'doc',
998
+ is_authed: body.is_authed,
999
+ };
1000
+ }
1001
+ // ============================================================================
1002
+ // 配置持久化
1003
+ // ============================================================================
1004
+ /**
1005
+ * 将 MCP 配置写入 openclaw.plugin.json 的 mcpConfig 字段
1006
+ *
1007
+ * 使用 OpenClaw SDK 提供的文件锁和原子写入,保证并发安全。
1008
+ * 配置格式: { mcpConfig: { [type]: { type, url } } }
1009
+ * 示例: { "mcpConfig": { "doc": { "type": "streamable-http", "url": "..." } } }
1010
+ *
1011
+ * @param config - MCP 配置
1012
+ * @param runtime - 运行时环境
1013
+ */
1014
+ async function saveMcpConfigToPluginJson(config, runtime) {
1015
+ // const pluginJsonPath = PLUGIN_JSON_PATH;
1016
+ // 获取绝对路径并写入到配置文件
1017
+ // const absolutePath = path.resolve(pluginJsonPath);
1018
+ const wecomConfigDir = path.join(os$1.homedir(), ".openclaw", "wecomConfig");
1019
+ const wecomConfigPath = path.join(wecomConfigDir, "config.json");
1020
+ // // 写入 wecom 配置文件
1021
+ // try {
1022
+ // const lockOptions = {
1023
+ // stale: 60_000,
1024
+ // retries: {
1025
+ // retries: 3,
1026
+ // factor: 1.35,
1027
+ // minTimeout: 8,
1028
+ // maxTimeout: 200,
1029
+ // randomize: true,
1030
+ // },
1031
+ // };
1032
+ // await withFileLock(wecomConfigPath, lockOptions, async () => {
1033
+ // const { value: wecomConfig } = await readJsonFileWithFallback<Record<string, unknown>>(
1034
+ // wecomConfigPath,
1035
+ // {},
1036
+ // );
1037
+ // wecomConfig.pluginConfigPath = absolutePath;
1038
+ // await writeJsonFileAtomically(wecomConfigPath, wecomConfig);
1039
+ // runtime.log?.(`[WeCom] Plugin config path saved to ${wecomConfigPath}`);
1040
+ // });
1041
+ // } catch (err) {
1042
+ // runtime.error?.(`[WeCom] Failed to save plugin config path: ${String(err)}`);
1043
+ // }
1044
+ const lockOptions = {
1045
+ stale: 60000, // 60秒锁过期
1046
+ retries: {
1047
+ retries: 6,
1048
+ factor: 1.35,
1049
+ minTimeout: 8,
1050
+ maxTimeout: 1200,
1051
+ randomize: true,
1052
+ },
1053
+ };
1054
+ await pluginSdk.withFileLock(wecomConfigPath, lockOptions, async () => {
1055
+ // 读取现有配置(不存在时使用空对象)
1056
+ const { value: pluginJson } = await pluginSdk.readJsonFileWithFallback(wecomConfigPath, {});
1057
+ // 确保 mcpConfig 字段存在且为对象
1058
+ if (!pluginJson.mcpConfig || typeof pluginJson.mcpConfig !== 'object') {
1059
+ pluginJson.mcpConfig = {};
1060
+ }
1061
+ // 使用 type 作为键存储配置
1062
+ const typeKey = config.type || 'default';
1063
+ pluginJson.mcpConfig[typeKey] = {
1064
+ type: config.type,
1065
+ url: config.url,
1066
+ };
1067
+ // 原子写入
1068
+ await pluginSdk.writeJsonFileAtomically(wecomConfigPath, pluginJson);
1069
+ runtime.log?.(`[WeCom] MCP config saved to ${wecomConfigPath}`);
1070
+ });
1071
+ }
1072
+ // ============================================================================
1073
+ // 组合入口
1074
+ // ============================================================================
1075
+ /**
1076
+ * 拉取 MCP 配置并持久化到 openclaw.plugin.json
1077
+ *
1078
+ * 认证成功后调用。失败仅记录日志,不影响 WebSocket 消息正常收发。
1079
+ *
1080
+ * @param wsClient - 已认证的 WSClient 实例
1081
+ * @param accountId - 账户 ID(用于日志)
1082
+ * @param runtime - 运行时环境(用于日志)
1083
+ */
1084
+ async function fetchAndSaveMcpConfig(wsClient, accountId, runtime) {
1085
+ try {
1086
+ runtime.log?.(`[${accountId}] Fetching MCP config...`);
1087
+ const config = await fetchMcpConfig(wsClient);
1088
+ runtime.log?.(`[${accountId}] MCP config fetched: url=${config.url}, type=${config.type ?? "N/A"}, is_authed=${config.is_authed ?? "N/A"}`);
1089
+ await saveMcpConfigToPluginJson(config, runtime);
1090
+ }
1091
+ catch (err) {
1092
+ runtime.error?.(`[${accountId}] Failed to fetch/save MCP config: ${String(err)}`);
1093
+ }
1094
+ }
1095
+
946
1096
  /**
947
1097
  * 企业微信 WebSocket 监控器主模块
948
1098
  *
@@ -1278,6 +1428,8 @@ async function monitorWeComProvider(options) {
1278
1428
  wsClient.on("authenticated", () => {
1279
1429
  runtime.log?.(`[${account.accountId}] Authentication successful`);
1280
1430
  setWeComWebSocket(account.accountId, wsClient);
1431
+ // 认证成功后自动拉取 MCP 配置(异步,失败不影响主流程)
1432
+ fetchAndSaveMcpConfig(wsClient, account.accountId, runtime);
1281
1433
  });
1282
1434
  // 监听断开事件
1283
1435
  wsClient.on("disconnected", (reason) => {
@@ -1431,7 +1583,7 @@ const dmPolicy = {
1431
1583
  allowFromKey: `channels.${CHANNEL_ID}.allowFrom`,
1432
1584
  getCurrent: (cfg) => {
1433
1585
  const account = resolveWeComAccount(cfg);
1434
- return account.config.dmPolicy ?? "pairing";
1586
+ return account.config.dmPolicy ?? "open";
1435
1587
  },
1436
1588
  setPolicy: (cfg, policy) => {
1437
1589
  return setWeComDmPolicy(cfg, policy);
@@ -1477,7 +1629,7 @@ const wecomOnboardingAdapter = {
1477
1629
  botId,
1478
1630
  secret,
1479
1631
  enabled: true,
1480
- dmPolicy: account.config.dmPolicy ?? "pairing",
1632
+ dmPolicy: account.config.dmPolicy ?? "open",
1481
1633
  allowFrom: account.config.allowFrom ?? [],
1482
1634
  });
1483
1635
  return { cfg: cfgWithAccount };
@@ -1613,7 +1765,7 @@ const wecomPlugin = {
1613
1765
  resolveDmPolicy: ({ account }) => {
1614
1766
  const basePath = `channels.${CHANNEL_ID}.`;
1615
1767
  return {
1616
- policy: account.config.dmPolicy ?? "pairing",
1768
+ policy: account.config.dmPolicy ?? "open",
1617
1769
  allowFrom: account.config.allowFrom ?? [],
1618
1770
  policyPath: `${basePath}dmPolicy`,
1619
1771
  allowFromPath: basePath,
@@ -1624,7 +1776,7 @@ const wecomPlugin = {
1624
1776
  collectWarnings: ({ account, cfg }) => {
1625
1777
  const warnings = [];
1626
1778
  // DM 策略警告
1627
- const dmPolicy = account.config.dmPolicy ?? "pairing";
1779
+ const dmPolicy = account.config.dmPolicy ?? "open";
1628
1780
  if (dmPolicy === "open") {
1629
1781
  const hasWildcard = (account.config.allowFrom ?? []).some((entry) => String(entry).trim() === "*");
1630
1782
  if (!hasWildcard) {
@@ -1666,7 +1818,7 @@ const wecomPlugin = {
1666
1818
  listGroups: async () => [],
1667
1819
  },
1668
1820
  outbound: {
1669
- deliveryMode: "direct",
1821
+ deliveryMode: "gateway",
1670
1822
  chunker: (text, limit) => getWeComRuntime().channel.text.chunkMarkdownText(text, limit),
1671
1823
  textChunkLimit: TEXT_CHUNK_LIMIT,
1672
1824
  sendText: async ({ to, text, accountId, ...rest }) => {