@wecom/wecom-openclaw-plugin 1.0.8 → 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/README.md +2 -2
- package/dist/index.cjs.js +166 -14
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +165 -14
- package/dist/index.esm.js.map +1 -1
- package/dist/src/const.d.ts +6 -0
- package/dist/src/interface.d.ts +11 -0
- package/dist/src/mcp-config.d.ts +29 -0
- package/package.json +1 -1
- package/skills/wecom-doc/SKILL.md +73 -37
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 ?? "
|
|
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
|
*
|
|
@@ -1118,10 +1267,10 @@ async function processWeComMessage(params) {
|
|
|
1118
1267
|
// Step 1: 解析消息内容
|
|
1119
1268
|
const { textParts, imageUrls, imageAesKeys, fileUrls, fileAesKeys, quoteContent } = parseMessageContent(body);
|
|
1120
1269
|
let text = textParts.join("\n").trim();
|
|
1121
|
-
// 群聊中移除 @机器人 的提及标记
|
|
1122
|
-
if (body.chattype === "group") {
|
|
1123
|
-
|
|
1124
|
-
}
|
|
1270
|
+
// // 群聊中移除 @机器人 的提及标记
|
|
1271
|
+
// if (body.chattype === "group") {
|
|
1272
|
+
// text = text.replace(/@\S+/g, "").trim();
|
|
1273
|
+
// }
|
|
1125
1274
|
// 如果文本为空但存在引用消息,使用引用消息内容
|
|
1126
1275
|
if (!text && quoteContent) {
|
|
1127
1276
|
text = quoteContent;
|
|
@@ -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 ?? "
|
|
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 ?? "
|
|
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 ?? "
|
|
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 ?? "
|
|
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: "
|
|
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 }) => {
|