@vrs-soft/wecom-aibot-mcp 2.6.0 → 3.1.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.
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * 配置向导模块
3
3
  *
4
- * 首次运行时引导用户配置 Bot ID、Secret 和默认目标用户
4
+ * 首次运行时引导用户配置 daemon 地址和 Auth Token
5
5
  *
6
6
  * 配置存储位置:
7
- * - 机器人配置:~/.wecom-aibot-mcp/robot-*.json
7
+ * - 安装记录:~/.wecom-aibot-mcp/version.json
8
8
  * - MCP 配置:~/.claude.json (仅 URL)
9
9
  */
10
10
  import * as readline from 'readline';
@@ -15,7 +15,7 @@ import { fileURLToPath } from 'url';
15
15
  import { logger } from './logger.js';
16
16
  const CONFIG_DIR = path.join(os.homedir(), '.wecom-aibot-mcp');
17
17
  const VERSION_FILE = path.join(CONFIG_DIR, 'version.json');
18
- const SERVER_CONFIG_FILE = path.join(CONFIG_DIR, 'server.json'); // HTTP Server 配置(auth token 等)
18
+ const SERVER_CONFIG_FILE = path.join(CONFIG_DIR, 'server.json');
19
19
  const CLAUDE_CONFIG_FILE = path.join(os.homedir(), '.claude.json');
20
20
  const CLAUDE_SETTINGS_FILE = path.join(os.homedir(), '.claude', 'settings.local.json');
21
21
  const HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'permission-hook.sh');
@@ -37,34 +37,6 @@ function ensureConfigDir() {
37
37
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
38
38
  }
39
39
  }
40
- // 从 ~/.wecom-aibot-mcp/robot-*.json 读取第一个有效配置
41
- export function loadConfig() {
42
- try {
43
- if (!fs.existsSync(CONFIG_DIR))
44
- return null;
45
- const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
46
- for (const file of files) {
47
- const content = fs.readFileSync(path.join(CONFIG_DIR, file), 'utf-8');
48
- const config = JSON.parse(content);
49
- if (config.botId && config.secret && config.targetUserId) {
50
- const result = {
51
- botId: config.botId,
52
- secret: config.secret,
53
- targetUserId: config.targetUserId,
54
- };
55
- if (config.nameTag)
56
- result.nameTag = config.nameTag;
57
- if (config.doc_mcp_url)
58
- result.doc_mcp_url = config.doc_mcp_url;
59
- return result;
60
- }
61
- }
62
- }
63
- catch (err) {
64
- logger.error('[config] 读取配置失败:', err);
65
- }
66
- return null;
67
- }
68
40
  // 获取 HTTP Server 的 auth token(从 server.json 读取)
69
41
  export function getAuthToken() {
70
42
  if (!fs.existsSync(SERVER_CONFIG_FILE))
@@ -77,100 +49,6 @@ export function getAuthToken() {
77
49
  return undefined;
78
50
  }
79
51
  }
80
- // 设置/清除 HTTP Server 的 auth token(写入 server.json)
81
- export function setAuthToken(token) {
82
- ensureConfigDir();
83
- let config = {};
84
- if (fs.existsSync(SERVER_CONFIG_FILE)) {
85
- try {
86
- config = JSON.parse(fs.readFileSync(SERVER_CONFIG_FILE, 'utf-8'));
87
- }
88
- catch {
89
- // ignore
90
- }
91
- }
92
- if (token) {
93
- config.authToken = token;
94
- }
95
- else {
96
- delete config.authToken;
97
- // 如果 config 为空,删除文件
98
- if (Object.keys(config).length === 0) {
99
- if (fs.existsSync(SERVER_CONFIG_FILE))
100
- fs.unlinkSync(SERVER_CONFIG_FILE);
101
- return true;
102
- }
103
- }
104
- fs.writeFileSync(SERVER_CONFIG_FILE, JSON.stringify(config, null, 2));
105
- return true;
106
- }
107
- // 获取 HTTPS 证书配置(从 server.json 读取)
108
- export function getHttpsConfig() {
109
- if (!fs.existsSync(SERVER_CONFIG_FILE))
110
- return null;
111
- try {
112
- const config = JSON.parse(fs.readFileSync(SERVER_CONFIG_FILE, 'utf-8'));
113
- if (config.certPath && config.keyPath) {
114
- return { certPath: config.certPath, keyPath: config.keyPath };
115
- }
116
- return null;
117
- }
118
- catch {
119
- return null;
120
- }
121
- }
122
- // 设置 HTTPS 证书配置(写入 server.json)
123
- export function setHttpsConfig(certPath, keyPath) {
124
- ensureConfigDir();
125
- let config = {};
126
- if (fs.existsSync(SERVER_CONFIG_FILE)) {
127
- try {
128
- config = JSON.parse(fs.readFileSync(SERVER_CONFIG_FILE, 'utf-8'));
129
- }
130
- catch {
131
- // ignore
132
- }
133
- }
134
- config.certPath = certPath;
135
- config.keyPath = keyPath;
136
- fs.writeFileSync(SERVER_CONFIG_FILE, JSON.stringify(config, null, 2));
137
- return true;
138
- }
139
- // 更新 ~/.claude.json 中 wecom-aibot MCP 配置的 auth headers
140
- export function updateMcpAuthHeaders(token) {
141
- if (!fs.existsSync(CLAUDE_CONFIG_FILE))
142
- return;
143
- try {
144
- const content = fs.readFileSync(CLAUDE_CONFIG_FILE, 'utf-8');
145
- const claudeConfig = JSON.parse(content);
146
- if (!claudeConfig.mcpServers)
147
- return;
148
- // 更新所有 wecom-aibot 相关的 HTTP MCP 配置
149
- for (const name of Object.keys(claudeConfig.mcpServers)) {
150
- if (name.startsWith('wecom-aibot') && claudeConfig.mcpServers[name].type === 'http') {
151
- if (token) {
152
- claudeConfig.mcpServers[name].headers = { Authorization: `Bearer ${token}` };
153
- }
154
- else {
155
- delete claudeConfig.mcpServers[name].headers;
156
- }
157
- }
158
- }
159
- fs.writeFileSync(CLAUDE_CONFIG_FILE, JSON.stringify(claudeConfig, null, 2));
160
- }
161
- catch {
162
- // ignore
163
- }
164
- }
165
- // 获取所有 wecom-aibot 相关的 MCP 实例
166
- export function listAllMcpInstances() {
167
- // 现在只有一个主配置文件
168
- const config = loadConfig();
169
- if (config) {
170
- return [{ name: 'wecom-aibot', config }];
171
- }
172
- return [];
173
- }
174
52
  // 删除配置(从 ~/.claude.json)
175
53
  export function deleteConfig() {
176
54
  try {
@@ -249,131 +127,6 @@ export function deleteSkills() {
249
127
  logger.error('[config] 删除 skill 失败:', err);
250
128
  }
251
129
  }
252
- // 删除单个机器人配置(按名称)
253
- export function deleteRobotConfig(robotName) {
254
- try {
255
- const robots = listAllRobots();
256
- const robot = robots.find(r => r.name === robotName);
257
- if (!robot) {
258
- console.log(`[config] 机器人 "${robotName}" 不存在`);
259
- return false;
260
- }
261
- // 查找机器人对应的配置文件
262
- let configFile = null;
263
- // 从 robot-*.json 中查找
264
- if (fs.existsSync(CONFIG_DIR)) {
265
- const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
266
- for (const file of files) {
267
- const filePath = path.join(CONFIG_DIR, file);
268
- const config = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
269
- const name = config.nameTag || file.replace('.json', '');
270
- if (name === robotName) {
271
- configFile = filePath;
272
- break;
273
- }
274
- }
275
- }
276
- if (!configFile) {
277
- console.log(`[config] 未找到机器人 "${robotName}" 的配置文件`);
278
- return false;
279
- }
280
- // 直接删除
281
- fs.unlinkSync(configFile);
282
- console.log(`[config] 已删除机器人: ${robotName}`);
283
- return true;
284
- }
285
- catch (err) {
286
- logger.error('[config] 删除机器人配置失败:', err);
287
- return false;
288
- }
289
- }
290
- // 删除单个 MCP 配置(按实例名)- 已弃用,保留用于 --uninstall
291
- export function deleteMcpConfig(instanceName) {
292
- try {
293
- if (!fs.existsSync(CLAUDE_CONFIG_FILE)) {
294
- console.log('[config] ~/.claude.json 不存在');
295
- return false;
296
- }
297
- const content = fs.readFileSync(CLAUDE_CONFIG_FILE, 'utf-8');
298
- const claudeConfig = JSON.parse(content);
299
- if (!claudeConfig.mcpServers?.[instanceName]) {
300
- console.log(`[config] 实例 "${instanceName}" 不存在`);
301
- return false;
302
- }
303
- // 检查是否是 wecom-aibot 相关配置
304
- const serverConfig = claudeConfig.mcpServers[instanceName];
305
- if (!serverConfig?.env?.WECOM_BOT_ID) {
306
- console.log(`[config] "${instanceName}" 不是企业微信机器人配置`);
307
- return false;
308
- }
309
- delete claudeConfig.mcpServers[instanceName];
310
- fs.writeFileSync(CLAUDE_CONFIG_FILE, JSON.stringify(claudeConfig, null, 2));
311
- console.log(`[config] 已删除实例: ${instanceName}`);
312
- return true;
313
- }
314
- catch (err) {
315
- logger.error('[config] 删除配置失败:', err);
316
- return false;
317
- }
318
- }
319
- // 交互式删除机器人配置
320
- export async function deleteRobotConfigInteractive(robotName) {
321
- const robots = listAllRobots();
322
- if (robots.length === 0) {
323
- console.log('[config] 没有找到任何企业微信机器人配置');
324
- return;
325
- }
326
- // 始终显示列表
327
- console.log('\n企业微信机器人配置列表:\n');
328
- robots.forEach((robot, idx) => {
329
- const isDefault = idx === 0;
330
- const defaultTag = isDefault ? ' [默认]' : '';
331
- console.log(` ${idx + 1}. ${robot.name}${defaultTag} (Bot ID: ${robot.botId.slice(0, 12)}..., 用户: ${robot.targetUserId})`);
332
- });
333
- // 如果提供了机器人名称,验证并删除
334
- if (robotName) {
335
- const found = robots.find(r => r.name === robotName);
336
- if (!found) {
337
- console.log(`\n[config] 未找到名为 "${robotName}" 的机器人`);
338
- return;
339
- }
340
- console.log(`\n[config] 将删除机器人: ${robotName}`);
341
- deleteRobotConfig(robotName);
342
- return;
343
- }
344
- // 没有提供名称,让用户选择
345
- console.log(` 0. 取消\n`);
346
- const rl = createRL();
347
- try {
348
- const choice = await question(rl, '请选择要删除的机器人序号: ');
349
- const choiceNum = parseInt(choice);
350
- if (choiceNum === 0) {
351
- console.log('[config] 已取消');
352
- return;
353
- }
354
- if (choiceNum < 1 || choiceNum > robots.length) {
355
- console.log('[config] 无效选择');
356
- return;
357
- }
358
- const selected = robots[choiceNum - 1];
359
- const confirm = await question(rl, `确认删除机器人 "${selected.name}"?(y/N): `);
360
- if (confirm.toLowerCase() === 'y') {
361
- deleteRobotConfig(selected.name);
362
- console.log(`[config] MCP 配置保留,其他机器人仍可正常使用\n`);
363
- }
364
- else {
365
- console.log('[config] 已取消');
366
- }
367
- }
368
- finally {
369
- rl.close();
370
- }
371
- }
372
- // 交互式删除 MCP 配置(已弃用,保留用于兼容)
373
- export async function deleteMcpConfigInteractive(instanceName) {
374
- // 转换为删除机器人配置
375
- await deleteRobotConfigInteractive(instanceName);
376
- }
377
130
  // 完全卸载(删除所有相关配置)
378
131
  export function uninstall() {
379
132
  console.log('\n[config] 开始卸载 wecom-aibot-mcp...\n');
@@ -392,10 +145,8 @@ export function uninstall() {
392
145
  }
393
146
  }
394
147
  // 删除整个配置目录(包括 robot-*.json、hook 脚本、日志等)
395
- // 使用 recursive: true 和 force: true 确保完全删除
396
148
  if (fs.existsSync(CONFIG_DIR)) {
397
149
  try {
398
- // 先删除所有文件,再删除目录(防止文件被重建)
399
150
  const files = fs.readdirSync(CONFIG_DIR);
400
151
  for (const file of files) {
401
152
  const filePath = path.join(CONFIG_DIR, file);
@@ -411,13 +162,11 @@ export function uninstall() {
411
162
  // 忽略单个文件删除失败
412
163
  }
413
164
  }
414
- // 最后尝试删除目录本身
415
165
  try {
416
166
  fs.rmSync(CONFIG_DIR, { recursive: true, force: true });
417
167
  console.log('[config] 已删除配置目录');
418
168
  }
419
169
  catch {
420
- // 目录可能被其他进程占用,下次启动时会清理
421
170
  console.log('[config] 配置目录已清空(部分文件可能被占用)');
422
171
  }
423
172
  }
@@ -808,243 +557,6 @@ exit 2
808
557
  fs.writeFileSync(STOP_HOOK_SCRIPT_PATH, script, { mode: 0o755 });
809
558
  console.log(`[config] Stop Hook 脚本已写入: ${STOP_HOOK_SCRIPT_PATH}`);
810
559
  }
811
- // 写入 MCP Server 配置到 ~/.claude.json
812
- function writeMcpServerConfig(config, instanceName) {
813
- try {
814
- ensureConfigDir();
815
- // 构建机器人配置对象
816
- const botConfig = {
817
- botId: config.botId,
818
- secret: config.secret,
819
- targetUserId: config.targetUserId,
820
- };
821
- if (config.nameTag) {
822
- botConfig.nameTag = config.nameTag;
823
- }
824
- if (config.doc_mcp_url) {
825
- botConfig.doc_mcp_url = config.doc_mcp_url;
826
- }
827
- // 检查名称唯一性(如果设置了新名称)
828
- if (config.nameTag && isRobotNameExists(config.nameTag, config.botId)) {
829
- console.log(`[config] ❌ 机器人名称 "${config.nameTag}" 已被其他机器人使用`);
830
- console.log('[config] 请使用不同的名称');
831
- return false;
832
- }
833
- // 按 botId 查找现有配置文件
834
- const existingConfigFile = findRobotConfigFileByBotId(config.botId);
835
- if (existingConfigFile) {
836
- // 更新现有配置文件
837
- fs.writeFileSync(existingConfigFile, JSON.stringify(botConfig, null, 2));
838
- console.log(`[config] 已更新机器人配置: ${existingConfigFile}`);
839
- }
840
- else {
841
- // 新机器人:统一使用 robot-*.json
842
- const newConfigPath = path.join(CONFIG_DIR, `robot-${Date.now()}.json`);
843
- fs.writeFileSync(newConfigPath, JSON.stringify(botConfig, null, 2));
844
- console.log(`[config] 已添加新机器人配置: ${newConfigPath}`);
845
- }
846
- // 2. 写入 MCP 配置到 ~/.claude.json(仅 URL)
847
- let claudeConfig = {};
848
- if (fs.existsSync(CLAUDE_CONFIG_FILE)) {
849
- const content = fs.readFileSync(CLAUDE_CONFIG_FILE, 'utf-8');
850
- claudeConfig = JSON.parse(content);
851
- }
852
- if (!claudeConfig.mcpServers)
853
- claudeConfig.mcpServers = {};
854
- const name = instanceName || 'wecom-aibot';
855
- // HTTP Transport 配置格式
856
- claudeConfig.mcpServers[name] = {
857
- type: 'http',
858
- url: 'http://127.0.0.1:18963/mcp',
859
- };
860
- fs.writeFileSync(CLAUDE_CONFIG_FILE, JSON.stringify(claudeConfig, null, 2));
861
- console.log(`[config] MCP 配置已写入 ~/.claude.json (实例名: ${name})`);
862
- return true;
863
- }
864
- catch (err) {
865
- logger.error('[config] 写入配置失败:', err);
866
- console.log('[config] ⚠️ 请手动配置:');
867
- console.log('');
868
- console.log('~/.wecom-aibot-mcp/robot-*.json:');
869
- console.log(JSON.stringify({
870
- botId: config.botId,
871
- secret: config.secret,
872
- targetUserId: config.targetUserId,
873
- }, null, 2));
874
- console.log('');
875
- console.log('~/.claude.json:');
876
- console.log(JSON.stringify({
877
- mcpServers: {
878
- 'wecom-aibot': {
879
- type: 'http',
880
- url: 'http://127.0.0.1:18963/mcp',
881
- },
882
- },
883
- }, null, 2));
884
- return false;
885
- }
886
- }
887
- // 添加新的机器人配置(多机器人场景)
888
- export async function addMcpConfig() {
889
- const rl = createRL();
890
- try {
891
- console.log('\n添加新的企业微信机器人配置\n');
892
- console.log('提示:多个机器人共享同一个 MCP 配置,只需添加机器人凭证即可\n');
893
- // 获取机器人名称(用于识别)
894
- const robotName = await question(rl, '机器人名称(如"张三的机器人"): ');
895
- if (!robotName) {
896
- console.log('[config] 机器人名称不能为空');
897
- rl.close();
898
- return;
899
- }
900
- // 获取 Bot ID
901
- let botId = await question(rl, 'Bot ID: ');
902
- while (!botId) {
903
- console.log('Bot ID 不能为空');
904
- botId = await question(rl, 'Bot ID: ');
905
- }
906
- // 获取 Secret
907
- let secret = await question(rl, 'Secret: ');
908
- while (!secret) {
909
- console.log('Secret 不能为空');
910
- secret = await question(rl, 'Secret: ');
911
- }
912
- // 获取文档 MCP URL(可选)
913
- console.log('');
914
- const docMcpUrl = await question(rl, '文档 MCP URL(可选,企业微信管理后台获取,留空跳过): ');
915
- rl.close();
916
- // 检查是否已存在相同 botId 的配置
917
- const existingRobots = listAllRobots();
918
- const duplicate = existingRobots.find(r => r.botId === botId);
919
- if (duplicate) {
920
- console.log(`\n[config] ⚠️ 机器人已存在!`);
921
- console.log(`[config] 已配置的机器人: ${duplicate.name} (Bot ID: ${duplicate.botId.slice(0, 12)}...)`);
922
- console.log(`[config] 如需更新配置,请使用 --config 命令`);
923
- return;
924
- }
925
- // 检查名称是否重复(阻止)
926
- const duplicateName = existingRobots.find(r => r.name === robotName);
927
- if (duplicateName) {
928
- console.log(`\n[config] ❌ 名称 "${robotName}" 已被使用`);
929
- console.log(`[config] 请使用不同的名称以方便识别`);
930
- console.log(`[config] 当前已配置的机器人:`);
931
- existingRobots.forEach((r, i) => {
932
- console.log(` ${i + 1}. ${r.name}`);
933
- });
934
- return;
935
- }
936
- // 先连接验证凭证
937
- console.log('\n[config] 正在连接企业微信...');
938
- const { initClient } = await import('./client.js');
939
- const client = initClient(botId, secret, 'placeholder', 'config-validation');
940
- // 等待连接(最多10秒)
941
- const connected = await new Promise((resolve) => {
942
- const startTime = Date.now();
943
- const checkInterval = setInterval(() => {
944
- if (client.isConnected()) {
945
- clearInterval(checkInterval);
946
- resolve(true);
947
- }
948
- else if (Date.now() - startTime > 10000) {
949
- clearInterval(checkInterval);
950
- resolve(false);
951
- }
952
- }, 500);
953
- });
954
- if (!connected) {
955
- console.log('\n[config] ❌ 连接失败,请检查 Bot ID 和 Secret 是否正确');
956
- console.log('[config] 新建机器人需要等待约 2 分钟同步时间');
957
- console.log('[config] 如需授权,请访问企业微信管理后台完成授权');
958
- return;
959
- }
960
- // 通过消息自动识别用户 ID
961
- const targetUserId = await detectUserIdFromMessage(client, 180);
962
- if (!targetUserId) {
963
- console.log('\n[config] 未能在规定时间内识别用户 ID');
964
- console.log('[config] 请重新运行: npx @vrs-soft/wecom-aibot-mcp --add');
965
- return;
966
- }
967
- // 保存机器人配置
968
- const robotConfig = {
969
- botId,
970
- secret,
971
- targetUserId,
972
- nameTag: robotName,
973
- ...(docMcpUrl ? { doc_mcp_url: docMcpUrl } : {}),
974
- };
975
- // 确保配置目录存在
976
- ensureConfigDir();
977
- // 统一使用 robot-*.json 格式
978
- const robotConfigPath = path.join(CONFIG_DIR, `robot-${Date.now()}.json`);
979
- fs.writeFileSync(robotConfigPath, JSON.stringify(robotConfig, null, 2));
980
- console.log(`\n[config] ✅ 已添加机器人: ${robotName}`);
981
- console.log(`[config] 用户 ID: ${targetUserId}`);
982
- // 列出所有机器人
983
- const robots = listAllRobots();
984
- console.log(`\n[config] 当前共 ${robots.length} 个机器人配置`);
985
- robots.forEach((r, i) => {
986
- console.log(` ${i + 1}. ${r.name} (${r.targetUserId})`);
987
- });
988
- console.log('\n[config] MCP 配置无需修改,多个机器人共享同一个 HTTP 服务');
989
- }
990
- catch (err) {
991
- logger.error('[config] 添加配置失败:', err);
992
- rl.close();
993
- }
994
- }
995
- // 列出所有机器人配置
996
- export function listAllRobots() {
997
- const robots = [];
998
- // 所有机器人配置(统一 robot-*.json 格式)
999
- if (fs.existsSync(CONFIG_DIR)) {
1000
- const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
1001
- for (const file of files) {
1002
- try {
1003
- const config = JSON.parse(fs.readFileSync(path.join(CONFIG_DIR, file), 'utf-8'));
1004
- const name = config.nameTag || file.replace('.json', '');
1005
- robots.push({
1006
- name,
1007
- botId: config.botId,
1008
- targetUserId: config.targetUserId,
1009
- ...(config.doc_mcp_url ? { doc_mcp_url: config.doc_mcp_url } : {}),
1010
- });
1011
- }
1012
- catch {
1013
- // ignore
1014
- }
1015
- }
1016
- }
1017
- return robots;
1018
- }
1019
- // 获取指定机器人(或唯一机器人)的文档 MCP URL
1020
- export function getDocMcpUrl(robotName) {
1021
- const robots = listAllRobots();
1022
- const robotsWithDoc = robots.filter(r => r.doc_mcp_url);
1023
- if (robotsWithDoc.length === 0) {
1024
- return {
1025
- url: null,
1026
- error: '未配置文档 MCP URL。请运行 `npx @vrs-soft/wecom-aibot-mcp --add` 添加机器人时填写文档 MCP URL,或通过 `add_robot_config` 工具设置。',
1027
- };
1028
- }
1029
- if (robotName) {
1030
- const robot = robotsWithDoc.find(r => r.name === robotName);
1031
- if (!robot) {
1032
- return {
1033
- url: null,
1034
- error: `未找到名为 "${robotName}" 的机器人,或该机器人未配置文档 MCP URL。已配置文档能力的机器人: ${robotsWithDoc.map(r => r.name).join(', ')}`,
1035
- };
1036
- }
1037
- return { url: robot.doc_mcp_url };
1038
- }
1039
- if (robotsWithDoc.length === 1) {
1040
- return { url: robotsWithDoc[0].doc_mcp_url };
1041
- }
1042
- // 多个机器人有 doc_mcp_url,需要用户指定
1043
- return {
1044
- url: null,
1045
- error: `有多个机器人配置了文档 MCP URL,请通过 robot_name 参数指定使用哪个机器人。已配置文档能力的机器人: ${robotsWithDoc.map(r => r.name).join(', ')}`,
1046
- };
1047
- }
1048
560
  // 写入 MCP 工具权限到 Claude settings
1049
561
  function writeMcpPermissions() {
1050
562
  try {
@@ -1069,9 +581,6 @@ function writeMcpPermissions() {
1069
581
  if (!existingPerms.has(perm))
1070
582
  settings.permissions.allow.push(perm);
1071
583
  }
1072
- // 注意:PermissionRequest hook 通过项目级 settings.json 配置,不注册全局 hook
1073
- // HTTP 模式:enter_headless_mode 在项目目录写入 hook
1074
- // Channel 模式:由 Claude Code 自动审批,不需要 hook
1075
584
  fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2));
1076
585
  // 确保 hook 脚本文件存在(供项目级 hook 引用)
1077
586
  writeHookScript();
@@ -1111,7 +620,7 @@ function writeVersionFile(mode, remoteOptions) {
1111
620
  fs.writeFileSync(VERSION_FILE, JSON.stringify(payload, null, 2));
1112
621
  }
1113
622
  // 确保所有全局配置已写入(强制覆盖,不依赖智能体)
1114
- export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
623
+ export function ensureGlobalConfigs(mode = 'channel-only', remoteOptions) {
1115
624
  ensureConfigDir();
1116
625
  // 读取已安装版本
1117
626
  let previousVersion;
@@ -1125,15 +634,6 @@ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
1125
634
  upgraded = true;
1126
635
  console.log(`[config] 版本升级: ${previousVersion || '未安装'} -> ${VERSION}`);
1127
636
  }
1128
- // http-only 模式:不写入 MCP 配置(远程部署场景)
1129
- if (mode === 'http-only') {
1130
- console.log('[config] HTTP-only 模式:跳过 MCP 配置写入');
1131
- // 只写权限配置和 Hook(可选,用于本地调试)
1132
- writeMcpPermissions();
1133
- console.log('[config] 已写入权限配置到 ~/.claude/settings.local.json');
1134
- writeVersionFile(mode);
1135
- return { upgraded, previousVersion };
1136
- }
1137
637
  // remote 模式:仅写入远程 HTTP MCP 配置(带 token headers),不装 Channel/Hook
1138
638
  if (mode === 'remote') {
1139
639
  if (!remoteOptions?.url || !remoteOptions?.token) {
@@ -1158,7 +658,6 @@ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
1158
658
  return { upgraded, previousVersion };
1159
659
  }
1160
660
  // remote-channel 模式:远程部署的 Channel 客户端——只写 Channel MCP,不写 HTTP MCP
1161
- // (HTTP MCP daemon 在远端,本地不需要 HTTP transport client config)
1162
661
  if (mode === 'remote-channel') {
1163
662
  if (!remoteOptions?.url) {
1164
663
  console.log('[config] ❌ 远程模式需要提供 URL');
@@ -1170,7 +669,6 @@ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
1170
669
  }
1171
670
  if (!claudeConfig.mcpServers)
1172
671
  claudeConfig.mcpServers = {};
1173
- // 只写 Channel MCP 配置(带 MCP_URL + MCP_AUTH_TOKEN),HTTP MCP 由远端 daemon 提供,本地无需 client 配置
1174
672
  const channelEnvRemote = { MCP_URL: remoteOptions.url.replace(/\/+$/, '') };
1175
673
  if (remoteOptions.token)
1176
674
  channelEnvRemote.MCP_AUTH_TOKEN = remoteOptions.token;
@@ -1186,13 +684,12 @@ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
1186
684
  }
1187
685
  fs.writeFileSync(CLAUDE_CONFIG_FILE, JSON.stringify(claudeConfig, null, 2));
1188
686
  console.log('[config] remote-channel 模式:仅写入 Channel MCP 配置');
1189
- // Channel 模式需要权限配置
1190
687
  writeMcpPermissions();
1191
688
  console.log('[config] 已写入权限配置到 ~/.claude/settings.local.json');
1192
689
  writeVersionFile(mode, remoteOptions);
1193
690
  return { upgraded, previousVersion };
1194
691
  }
1195
- // 1. 强制写入 MCP 配置到 ~/.claude.json
692
+ // channel-only 模式:必须通过 MCP_URL 指定远程地址
1196
693
  let claudeConfig = {};
1197
694
  if (fs.existsSync(CLAUDE_CONFIG_FILE)) {
1198
695
  const content = fs.readFileSync(CLAUDE_CONFIG_FILE, 'utf-8');
@@ -1200,63 +697,30 @@ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
1200
697
  }
1201
698
  if (!claudeConfig.mcpServers)
1202
699
  claudeConfig.mcpServers = {};
1203
- // 从 node_modules 运行(npm/npx 安装)时用 npx,本地开发时用绝对路径
1204
700
  const isPackageInstall = __dirname.includes('node_modules');
1205
701
  const channelCmd = isPackageInstall
1206
702
  ? { command: 'npx', args: ['-y', '@vrs-soft/wecom-aibot-mcp', '--channel'] }
1207
703
  : { command: 'node', args: [path.join(__dirname, 'bin.js'), '--channel'] };
1208
- if (mode === 'channel-only') {
1209
- // Channel-only 模式:必须通过 MCP_URL 指定远程地址
1210
- const mcpUrl = process.env.MCP_URL;
1211
- if (!mcpUrl) {
1212
- console.log('[config] Channel-only 模式需要指定 MCP_URL');
1213
- console.log('[config] 请设置环境变量: MCP_URL=http://远程IP:18963');
1214
- return { upgraded: false, previousVersion };
1215
- }
1216
- const channelEnv = { MCP_URL: mcpUrl.replace(/\/+$/, '') };
1217
- const authToken = getAuthToken();
1218
- if (authToken) {
1219
- channelEnv.MCP_AUTH_TOKEN = authToken;
1220
- }
1221
- claudeConfig.mcpServers['wecom-aibot-channel'] = {
1222
- command: channelCmd.command,
1223
- args: channelCmd.args,
1224
- env: channelEnv,
1225
- };
1226
- console.log(`[config] Channel-only 模式:Channel MCP 已配置`);
1227
- }
1228
- else {
1229
- // full 模式:同时写入 HTTP MCP 和 Channel MCP 配置
1230
- claudeConfig.mcpServers['wecom-aibot'] = {
1231
- type: 'http',
1232
- url: 'http://127.0.0.1:18963/mcp',
1233
- };
1234
- // Channel MCP 配置:保留已有的自定义 MCP_URL(如 channel-only 安装时写入的远程地址)
1235
- const existingChannel = claudeConfig.mcpServers['wecom-aibot-channel'];
1236
- const existingMcpUrl = existingChannel?.env?.MCP_URL;
1237
- const isRemote = existingMcpUrl && !existingMcpUrl.startsWith('http://127.0.0.1');
1238
- const channelMcpUrl = isRemote ? existingMcpUrl : 'http://127.0.0.1:18963';
1239
- const channelEnvFull = { MCP_URL: channelMcpUrl };
1240
- // 保留已有的 MCP_AUTH_TOKEN(远程安装时写入),或从 server.json 读取
1241
- const existingToken = existingChannel?.env?.MCP_AUTH_TOKEN;
1242
- if (isRemote) {
1243
- const token = existingToken || getAuthToken();
1244
- if (token)
1245
- channelEnvFull.MCP_AUTH_TOKEN = token;
1246
- }
1247
- claudeConfig.mcpServers['wecom-aibot-channel'] = {
1248
- command: channelCmd.command,
1249
- args: channelCmd.args,
1250
- env: channelEnvFull,
1251
- };
1252
- console.log(`[config] full 模式:Channel MCP 使用本地路径`);
1253
- }
704
+ const mcpUrl = process.env.MCP_URL;
705
+ if (!mcpUrl) {
706
+ console.log('[config] Channel-only 模式需要指定 MCP_URL');
707
+ console.log('[config] 请设置环境变量: MCP_URL=http://远程IP:18963');
708
+ return { upgraded: false, previousVersion };
709
+ }
710
+ const channelEnv = { MCP_URL: mcpUrl.replace(/\/+$/, '') };
711
+ const authToken = getAuthToken();
712
+ if (authToken)
713
+ channelEnv.MCP_AUTH_TOKEN = authToken;
714
+ claudeConfig.mcpServers['wecom-aibot-channel'] = {
715
+ command: channelCmd.command,
716
+ args: channelCmd.args,
717
+ env: channelEnv,
718
+ };
719
+ console.log('[config] Channel-only 模式:Channel MCP 已配置');
1254
720
  fs.writeFileSync(CLAUDE_CONFIG_FILE, JSON.stringify(claudeConfig, null, 2));
1255
721
  console.log('[config] 已写入 MCP 配置到 ~/.claude.json');
1256
- // 2. 强制写入权限配置和 Hook
1257
722
  writeMcpPermissions();
1258
723
  console.log('[config] 已写入权限配置到 ~/.claude/settings.local.json');
1259
- // 3. 写入版本号
1260
724
  writeVersionFile(mode);
1261
725
  console.log(`[config] 已记录版本号: ${VERSION}`);
1262
726
  return { upgraded, previousVersion };
@@ -1264,33 +728,8 @@ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
1264
728
  // 远程安装向导(交互式输入 URL + Token)
1265
729
  export async function runRemoteInstallWizard() {
1266
730
  const rl = createRL();
1267
- const CLAUDE_CONFIG_FILE = path.join(os.homedir(), '.claude.json');
1268
731
  try {
1269
- // 检测本机是否有 ~/.claude.json(判断是 Client 还是 Server)
1270
- const hasClaudeConfig = fs.existsSync(CLAUDE_CONFIG_FILE);
1271
- if (!hasClaudeConfig) {
1272
- // Server 安装模式:本机无 ~/.claude.json,作为远程服务器
1273
- console.log('\n检测到本机无 ~/.claude.json → Server 安装模式\n');
1274
- console.log(' Server 端只需启动 HTTP MCP Server,不写入 MCP 配置');
1275
- console.log(' Client 端在其他机器上安装\n');
1276
- const confirm = await question(rl, '确认作为远程 Server 安装?(y/N): ');
1277
- if (confirm.toLowerCase() !== 'y') {
1278
- console.log('[config] 已取消');
1279
- return null;
1280
- }
1281
- // Server 不写入 ~/.claude.json,只提示启动命令
1282
- console.log('\n─────────────────────────────────────');
1283
- console.log('Server 安装完成!');
1284
- console.log(' 启动命令: npx @vrs-soft/wecom-aibot-mcp --start');
1285
- console.log(' 或者: npm run start:http');
1286
- console.log('─────────────────────────────────────\n');
1287
- console.log('[config] Client 端请在其他机器运行安装程序连接本服务器\n');
1288
- return 'server';
1289
- }
1290
- // Client 安装模式:本机有 ~/.claude.json,作为客户端
1291
- // 远程模式 = HTTP/Channel 完全分离:本地只装 Client 配置,daemon 在远端
1292
- console.log('\n检测到本机有 ~/.claude.json → Client 安装模式\n');
1293
- console.log(' 请选择连接远程服务器的方式:\n');
732
+ console.log('\n请选择连接远程服务器的方式:\n');
1294
733
  console.log(' 1. Channel MCP(推荐:SSE 自动推送,消息到达立即唤醒 agent)');
1295
734
  console.log(' 2. HTTP MCP(轮询模式,兼容不支持 Channel 的 Claude Code)\n');
1296
735
  const choice = await question(rl, '请选择 (1/2): ');
@@ -1326,19 +765,6 @@ export async function runRemoteInstallWizard() {
1326
765
  rl.close();
1327
766
  }
1328
767
  }
1329
- export function saveConfig(config, instanceName) {
1330
- ensureConfigDir(); // 确保运行时文件目录存在
1331
- // 写入 MCP Server 配置到 ~/.claude.json
1332
- const success = writeMcpServerConfig(config, instanceName);
1333
- if (!success) {
1334
- return false;
1335
- }
1336
- // 写入 MCP 工具权限和 Hook 到 ~/.claude/settings.local.json
1337
- writeMcpPermissions();
1338
- // 安装 skill 到项目目录(项目级别,支持远程部署)
1339
- installSkill(process.cwd());
1340
- return true;
1341
- }
1342
768
  /**
1343
769
  * 安装 headless-mode skill 到项目目录
1344
770
  * 返回:{ success, skillUrl? } - 如果模板不存在,返回 HTTP endpoint URL
@@ -1353,7 +779,6 @@ export function installSkill(projectDir) {
1353
779
  // 检查模板文件是否存在
1354
780
  if (!fs.existsSync(SKILL_TEMPLATE_FILE)) {
1355
781
  console.log('[config] Skill 模板文件不存在,返回 HTTP endpoint URL');
1356
- // 返回 HTTP endpoint URL,让 agent 通过 WebFetch 下载
1357
782
  return {
1358
783
  success: false,
1359
784
  skillUrl: `${process.env.MCP_URL || 'http://127.0.0.1:18963'}/skill`,
@@ -1380,321 +805,3 @@ function question(rl, prompt) {
1380
805
  });
1381
806
  });
1382
807
  }
1383
- // 获取用户列表(通过企业微信 API)
1384
- async function fetchUserList(botId, secret) {
1385
- console.log('[config] 正在获取用户列表...');
1386
- // 使用 WebSocket 获取用户列表
1387
- // 注意:智能机器人 API 可能没有直接获取用户列表的接口
1388
- // 我们需要通过其他方式获取,比如让用户输入或从消息记录中推断
1389
- // 暂时返回空列表,让用户手动输入
1390
- return [];
1391
- }
1392
- /**
1393
- * 运行配置向导
1394
- */
1395
- export async function runConfigWizard() {
1396
- console.log('\n');
1397
- console.log('╔════════════════════════════════════════════════════════════╗');
1398
- console.log('║ 企业微信智能机器人 MCP 服务 - 配置向导 ║');
1399
- console.log('╚════════════════════════════════════════════════════════════╝');
1400
- const rl = createRL();
1401
- try {
1402
- const robots = listAllRobots();
1403
- let targetRobot = null;
1404
- let isNewRobot = false;
1405
- // 第一步:选择要修改的机器人
1406
- if (robots.length === 0) {
1407
- console.log('\n首次配置,将创建新机器人\n');
1408
- isNewRobot = true;
1409
- }
1410
- else {
1411
- console.log('\n请选择要操作的机器人:\n');
1412
- robots.forEach((robot, idx) => {
1413
- const docTag = robot.doc_mcp_url ? ' [文档✅]' : '';
1414
- console.log(` ${idx + 1}. ${robot.name} (Bot ID: ${robot.botId.slice(0, 12)}...)${docTag}`);
1415
- });
1416
- console.log(` ${robots.length + 1}. 添加新机器人\n`);
1417
- const choice = await question(rl, '请输入序号: ');
1418
- const choiceNum = parseInt(choice);
1419
- if (choiceNum >= 1 && choiceNum <= robots.length) {
1420
- targetRobot = robots[choiceNum - 1];
1421
- console.log(`\n已选择修改: ${targetRobot.name}\n`);
1422
- }
1423
- else if (choiceNum === robots.length + 1) {
1424
- isNewRobot = true;
1425
- console.log('\n将创建新机器人\n');
1426
- }
1427
- else {
1428
- console.log('[config] 无效选择');
1429
- process.exit(1);
1430
- }
1431
- }
1432
- // 第二步:输入机器人名称
1433
- let robotName = await question(rl, `机器人名称(${targetRobot ? `当前: ${targetRobot.name}` : '用于识别'}): `);
1434
- if (!robotName) {
1435
- if (targetRobot) {
1436
- robotName = targetRobot.name; // 保持原名称
1437
- console.log(`[config] 保持原名称: ${robotName}`);
1438
- }
1439
- else {
1440
- console.log('[config] 机器人名称不能为空');
1441
- process.exit(1);
1442
- }
1443
- }
1444
- // 检查名称是否与其他机器人重复
1445
- if (isNewRobot || (targetRobot && robotName !== targetRobot.name)) {
1446
- const duplicateName = robots.find(r => r.name === robotName && r !== targetRobot);
1447
- if (duplicateName) {
1448
- console.log(`[config] ❌ 名称 "${robotName}" 已被使用`);
1449
- process.exit(1);
1450
- }
1451
- }
1452
- // 第三步:输入 Bot ID
1453
- let botId = await question(rl, `Bot ID(${targetRobot ? `当前: ${targetRobot.botId.slice(0, 12)}...` : '必填'}): `);
1454
- if (!botId) {
1455
- if (targetRobot) {
1456
- botId = targetRobot.botId; // 保持原 Bot ID
1457
- console.log(`[config] 保持原 Bot ID`);
1458
- }
1459
- else {
1460
- console.log('Bot ID 不能为空');
1461
- botId = await question(rl, 'Bot ID: ');
1462
- if (!botId) {
1463
- console.log('[config] Bot ID 不能为空');
1464
- process.exit(1);
1465
- }
1466
- }
1467
- }
1468
- // 第四步:输入 Secret
1469
- let secret = await question(rl, `Secret(${targetRobot ? `当前: ${targetRobot.botId.slice(0, 8)}...` : '必填'}): `);
1470
- if (!secret) {
1471
- if (targetRobot) {
1472
- // 读取原 Secret
1473
- const configFile = findRobotConfigFile(targetRobot.name);
1474
- if (configFile) {
1475
- const config = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
1476
- secret = config.secret;
1477
- console.log(`[config] 保持原 Secret`);
1478
- }
1479
- }
1480
- else {
1481
- console.log('Secret 不能为空');
1482
- secret = await question(rl, 'Secret: ');
1483
- if (!secret) {
1484
- console.log('[config] Secret 不能为空');
1485
- process.exit(1);
1486
- }
1487
- }
1488
- }
1489
- // 第五步:文档 MCP URL(可选)
1490
- const currentDocUrl = targetRobot?.doc_mcp_url ?? '';
1491
- const docUrlPrompt = currentDocUrl
1492
- ? `文档 MCP URL(当前: ${currentDocUrl.slice(0, 40)}...,留空保持不变): `
1493
- : '文档 MCP URL(可选,企业微信管理后台获取,留空跳过): ';
1494
- let docMcpUrl = await question(rl, docUrlPrompt);
1495
- if (!docMcpUrl && currentDocUrl) {
1496
- docMcpUrl = currentDocUrl;
1497
- console.log('[config] 保持原文档 MCP URL');
1498
- }
1499
- // 第六步:目标用户(默认联系人)
1500
- // 修改场景:询问是否要重新识别;选 Y 则连接 bot 等待用户消息(与 --add 一致);选 N 保持原值
1501
- let targetUserId = targetRobot?.targetUserId || '';
1502
- if (targetRobot) {
1503
- console.log(`\n当前默认联系人(targetUserId): ${targetUserId || '(未设置)'}`);
1504
- const changeContact = await question(rl, '是否重新识别?(y/N): ');
1505
- if (changeContact.toLowerCase() === 'y') {
1506
- // 临时连接 bot 等待用户消息以识别 userid
1507
- console.log('\n[config] 正在连接企业微信验证凭证...');
1508
- const { initClient } = await import('./client.js');
1509
- const tmpClient = initClient(botId, secret, 'placeholder', 'config-detect');
1510
- // 等待连接(最多10秒)
1511
- const connected = await new Promise((resolve) => {
1512
- const start = Date.now();
1513
- const iv = setInterval(() => {
1514
- if (tmpClient.isConnected()) {
1515
- clearInterval(iv);
1516
- resolve(true);
1517
- }
1518
- else if (Date.now() - start > 10000) {
1519
- clearInterval(iv);
1520
- resolve(false);
1521
- }
1522
- }, 500);
1523
- });
1524
- if (!connected) {
1525
- console.log('[config] ❌ 连接失败(Bot ID/Secret 可能有误),保持原默认联系人');
1526
- tmpClient.disconnect();
1527
- }
1528
- else {
1529
- const detectedUserId = await detectUserIdFromMessage(tmpClient, 180);
1530
- tmpClient.disconnect();
1531
- if (detectedUserId) {
1532
- targetUserId = detectedUserId;
1533
- console.log(`[config] ✅ 默认联系人已更新: ${targetUserId}`);
1534
- }
1535
- else {
1536
- console.log('[config] 未识别到用户消息,保持原默认联系人');
1537
- }
1538
- }
1539
- }
1540
- else {
1541
- console.log('[config] 保持原默认联系人');
1542
- }
1543
- }
1544
- // 第七步:确认
1545
- console.log('\n─────────────────────────────────────');
1546
- console.log('配置确认:');
1547
- console.log(` 机器人名称: ${robotName}`);
1548
- console.log(` Bot ID: ${botId}`);
1549
- console.log(` Secret: ${secret.slice(0, 8)}...${secret.slice(-4)}`);
1550
- console.log(` 文档 MCP: ${docMcpUrl ? '✅ 已配置' : '(未配置)'}`);
1551
- console.log(` 默认联系人: ${targetUserId || '(将通过消息自动识别)'}`);
1552
- console.log('─────────────────────────────────────\n');
1553
- const confirm = await question(rl, '确认配置?(Y/n): ');
1554
- if (confirm.toLowerCase() === 'n') {
1555
- console.log('[config] 配置已取消');
1556
- process.exit(0);
1557
- }
1558
- // 返回最终配置
1559
- const config = {
1560
- botId,
1561
- secret,
1562
- targetUserId, // 修改时保留 / 已变更,新建时为空(稍后识别)
1563
- nameTag: robotName,
1564
- ...(docMcpUrl ? { doc_mcp_url: docMcpUrl } : {}),
1565
- };
1566
- // 如果是修改现有机器人,返回其 instanceName(用于删除旧配置)
1567
- const instanceName = targetRobot ? targetRobot.name : 'wecom-aibot';
1568
- return { config, instanceName };
1569
- }
1570
- finally {
1571
- rl.close();
1572
- }
1573
- }
1574
- // 查找机器人配置文件路径(按名称)
1575
- export function findRobotConfigFile(robotName) {
1576
- if (fs.existsSync(CONFIG_DIR)) {
1577
- const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
1578
- for (const file of files) {
1579
- const filePath = path.join(CONFIG_DIR, file);
1580
- const config = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
1581
- const name = config.nameTag || file.replace('.json', '');
1582
- if (name === robotName) {
1583
- return filePath;
1584
- }
1585
- }
1586
- }
1587
- return null;
1588
- }
1589
- // 查找机器人配置文件路径(按 botId)
1590
- export function findRobotConfigFileByBotId(botId) {
1591
- if (fs.existsSync(CONFIG_DIR)) {
1592
- const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
1593
- for (const file of files) {
1594
- const filePath = path.join(CONFIG_DIR, file);
1595
- const config = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
1596
- if (config.botId === botId) {
1597
- return filePath;
1598
- }
1599
- }
1600
- }
1601
- return null;
1602
- }
1603
- // 检查机器人名称是否已存在(排除指定 botId)
1604
- export function isRobotNameExists(name, excludeBotId) {
1605
- const robots = listAllRobots();
1606
- for (const robot of robots) {
1607
- if (robot.name === name && robot.botId !== excludeBotId) {
1608
- return true;
1609
- }
1610
- }
1611
- return false;
1612
- }
1613
- /**
1614
- * 通过等待用户消息来识别用户 ID(使用已有的 client)
1615
- */
1616
- export async function detectUserIdFromMessage(client, timeoutSeconds = 60) {
1617
- return new Promise((resolve) => {
1618
- if (!client.isConnected()) {
1619
- console.log('\n[config] 客户端未连接');
1620
- resolve(null);
1621
- return;
1622
- }
1623
- console.log('\n[config] ✅ 连接成功!');
1624
- console.log('\n╔════════════════════════════════════════════════════════╗');
1625
- console.log('║ 请让需要接收审批消息的人,在企业微信中给机器人发送 ║');
1626
- console.log('║ 一条消息(任意内容),系统将自动识别其用户 ID ║');
1627
- console.log('╚════════════════════════════════════════════════════════╝');
1628
- console.log(`\n[config] 等待消息中...(${timeoutSeconds}秒内)`);
1629
- // 设置超时
1630
- const timeout = setTimeout(() => {
1631
- console.log(`\n[config] 等待超时(${timeoutSeconds}秒),未收到用户消息`);
1632
- resolve(null);
1633
- }, timeoutSeconds * 1000);
1634
- // 轮询等待消息
1635
- const pollInterval = setInterval(async () => {
1636
- const messages = client.getPendingMessages(false);
1637
- if (messages.length > 0) {
1638
- clearTimeout(timeout);
1639
- clearInterval(pollInterval);
1640
- const msg = messages[0];
1641
- const userId = msg.from_userid;
1642
- console.log(`\n[config] ✅ 收到消息!`);
1643
- console.log(`[config] 识别到用户 ID: ${userId}`);
1644
- // 发送确认消息
1645
- try {
1646
- await client.sendText(`**机器人配置成功!**\n\n默认向用户 ID: \`${userId}\` 发送消息互动。\n\n您现在可以使用 Claude Code 审批功能了。`, userId);
1647
- console.log(`[config] 已发送确认消息到 ${userId}`);
1648
- }
1649
- catch (err) {
1650
- console.log(`[config] 发送确认消息失败: ${err}`);
1651
- }
1652
- resolve(userId);
1653
- }
1654
- }, 1000);
1655
- });
1656
- }
1657
- /**
1658
- * 检查并获取配置
1659
- *
1660
- * 优先级:
1661
- * 1. 环境变量(WECOM_BOT_ID, WECOM_SECRET, WECOM_TARGET_USER)
1662
- * 2. 保存的配置文件(~/.wecom-aibot-mcp/robot-*.json)
1663
- * 3. 运行配置向导
1664
- */
1665
- export async function getOrInitConfig() {
1666
- // 1. 检查环境变量(最高优先级,支持多实例场景)
1667
- const envBotId = process.env.WECOM_BOT_ID;
1668
- const envSecret = process.env.WECOM_SECRET;
1669
- const envTargetUser = process.env.WECOM_TARGET_USER;
1670
- if (envBotId && envSecret && envTargetUser) {
1671
- console.log(`[config] 使用环境变量配置: Bot ID=${envBotId}, 目标用户=${envTargetUser}`);
1672
- return {
1673
- botId: envBotId,
1674
- secret: envSecret,
1675
- targetUserId: envTargetUser,
1676
- };
1677
- }
1678
- // 部分环境变量存在时给出提示
1679
- if (envBotId || envSecret || envTargetUser) {
1680
- console.log('[config] 检测到部分环境变量,但配置不完整');
1681
- console.log('[config] 需要同时设置: WECOM_BOT_ID, WECOM_SECRET, WECOM_TARGET_USER');
1682
- }
1683
- // 2. 检查保存的配置文件
1684
- const savedConfig = loadConfig();
1685
- if (savedConfig && savedConfig.botId && savedConfig.secret && savedConfig.targetUserId) {
1686
- console.log(`[config] 已加载配置: Bot ID=${savedConfig.botId}, 目标用户=${savedConfig.targetUserId}`);
1687
- return savedConfig;
1688
- }
1689
- // 3. 非 TTY(MCP stdio 模式)不能启动交互向导
1690
- if (!process.stdin.isTTY) {
1691
- logger.error('[config] 未找到配置,且当前为非交互模式。');
1692
- logger.error('[config] 请在 ~/.claude.json 的 mcpServers 中设置环境变量:');
1693
- logger.error('[config] WECOM_BOT_ID, WECOM_SECRET, WECOM_TARGET_USER');
1694
- process.exit(1);
1695
- }
1696
- // 4. TTY 模式下运行配置向导
1697
- console.log('[config] 未找到有效配置,启动配置向导...\n');
1698
- const result = await runConfigWizard();
1699
- return result.config;
1700
- }