@vrs-soft/wecom-aibot-mcp 2.4.2 → 2.4.4

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/bin.js CHANGED
@@ -13,7 +13,7 @@ import { spawn } from 'child_process';
13
13
  import * as fs from 'fs';
14
14
  import * as path from 'path';
15
15
  import * as os from 'os';
16
- import { runConfigWizard, loadConfig, saveConfig, deleteRobotConfigInteractive, uninstall, addMcpConfig, detectUserIdFromMessage, ensureHookInstalled, listAllRobots, ensureGlobalConfigs, getAuthToken, setAuthToken, updateMcpAuthHeaders, runRemoteInstallWizard, VERSION, } from './config-wizard.js';
16
+ import { runConfigWizard, loadConfig, saveConfig, deleteRobotConfigInteractive, uninstall, addMcpConfig, detectUserIdFromMessage, ensureHookInstalled, listAllRobots, ensureGlobalConfigs, getAuthToken, setAuthToken, getHttpsConfig, setHttpsConfig, updateMcpAuthHeaders, runRemoteInstallWizard, VERSION, } from './config-wizard.js';
17
17
  import { initClient } from './client.js';
18
18
  import { registerTools } from './tools/index.js';
19
19
  import { startHttpServer, stopHttpServer, HTTP_PORT } from './http-server.js';
@@ -251,8 +251,10 @@ async function startMcpServerForeground(isDebug = false) {
251
251
  logger.log(' ║ Claude Code 审批通道 ║');
252
252
  logger.log(' ╚════════════════════════════════════════════════════════╝');
253
253
  logger.log('');
254
- logger.log(`[mcp] 启动 MCP HTTP Server (端口: ${HTTP_PORT})...`);
255
- await startHttpServer(server);
254
+ const httpsConfig = getHttpsConfig() ?? undefined;
255
+ const protocol = httpsConfig ? 'HTTPS' : 'HTTP';
256
+ logger.log(`[mcp] 启动 MCP ${protocol} Server (端口: ${HTTP_PORT})...`);
257
+ await startHttpServer(server, HTTP_PORT, httpsConfig);
256
258
  startKeepaliveMonitor();
257
259
  logger.log(`[mcp] MCP Server 已就绪`);
258
260
  logger.log(`[mcp] HTTP endpoint: http://127.0.0.1:${HTTP_PORT}/mcp`);
@@ -311,9 +313,15 @@ async function main() {
311
313
  // 确定安装模式
312
314
  const installMode = args.includes('--http-only') ? 'http-only' :
313
315
  args.includes('--channel-only') ? 'channel-only' : 'full';
314
- // --reinstall / --http-only / --setup 命令跳过顶部 ensureGlobalConfigs
315
- // (--setup 自己在向导完成后调用)
316
- if (!args.includes('--reinstall') && !args.includes('--http-only') && !args.includes('--setup')) {
316
+ // 以下命令跳过顶部 ensureGlobalConfigs,避免覆盖配置
317
+ // --setup: 向导完成后自己调用
318
+ // --channel: 作为 Channel MCP 代理运行,不应改写全局配置
319
+ // --reinstall / --http-only: 有自己的处理逻辑
320
+ // --version / -v: 只查版本,不写配置
321
+ const skipEnsure = args.includes('--reinstall') || args.includes('--http-only') ||
322
+ args.includes('--setup') || args.includes('--channel') ||
323
+ args.includes('--version') || args.includes('-v');
324
+ if (!skipEnsure) {
317
325
  // 强制覆盖所有全局配置(不依赖智能体)
318
326
  ensureGlobalConfigs(installMode);
319
327
  }
@@ -504,39 +512,89 @@ async function main() {
504
512
  console.log('[setup] 安装完成!请重启 Claude Code 以加载配置');
505
513
  }
506
514
  else if (wantServer) {
507
- // 服务器端
508
- console.log('\n[setup] Server 安装模式\n');
509
- const savedConfig = loadConfig();
510
- if (!savedConfig?.botId)
511
- await runConfigWizard();
515
+ // 服务器端:分两步——先完成 Server 安装,再配置机器人
516
+ console.log('\n[setup] ─── 步骤 1/2:Server 安装 ───\n');
517
+ console.log(' Server 负责运行 HTTP MCP 服务,Bot 配置在下一步单独完成\n');
512
518
  const readline = await import('readline');
513
519
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
514
- const token = await new Promise(resolve => rl.question('Auth Token(Client 端需填写相同 Token,留空跳过): ', a => { rl.close(); resolve(a.trim()); }));
520
+ const token = await new Promise(resolve => rl.question('Auth Token(Client 端连接时需填写相同 Token,留空跳过): ', a => { rl.close(); resolve(a.trim()); }));
515
521
  if (token)
516
522
  setAuthToken(token);
523
+ // HTTPS 证书配置
524
+ const defaultCertPath = path.join(os.homedir(), '.wecom-aibot-mcp', 'cert.pem');
525
+ console.log('\n HTTPS 证书配置(留空跳过,保持 HTTP 模式)');
526
+ console.log(' 请输入完整路径含文件名(.pem / .crt / .key 均可),例如:');
527
+ console.log(` ${defaultCertPath}`);
528
+ console.log(' /etc/letsencrypt/live/example.com/fullchain.pem');
529
+ console.log(' /etc/gitlab/ssl/gitlab.example.com.crt\n');
530
+ const checkFile = (p, label) => {
531
+ if (!fs.existsSync(p)) {
532
+ console.log(`[setup] ⚠️ ${label}文件不存在: ${p}`);
533
+ return false;
534
+ }
535
+ if (fs.statSync(p).isDirectory()) {
536
+ console.log(`[setup] ⚠️ ${label}路径是目录而非文件: ${p}`);
537
+ return false;
538
+ }
539
+ return true;
540
+ };
541
+ const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
542
+ const certInput = await new Promise(resolve => rl2.question(`SSL 证书文件完整路径(留空跳过): `, a => { rl2.close(); resolve(a.trim()); }));
543
+ if (certInput) {
544
+ if (!checkFile(certInput, '证书')) {
545
+ console.log('[setup] 跳过 HTTPS 配置');
546
+ }
547
+ else {
548
+ const rl3 = readline.createInterface({ input: process.stdin, output: process.stdout });
549
+ const keyInput = await new Promise(resolve => rl3.question(`SSL 私钥文件完整路径: `, a => { rl3.close(); resolve(a.trim()); }));
550
+ if (keyInput && checkFile(keyInput, '私钥')) {
551
+ setHttpsConfig(certInput, keyInput);
552
+ console.log(`[setup] HTTPS 已配置`);
553
+ console.log(` 证书: ${certInput}`);
554
+ console.log(` 私钥: ${keyInput}`);
555
+ }
556
+ else if (!keyInput) {
557
+ console.log('[setup] 私钥路径不能为空,跳过 HTTPS 配置');
558
+ }
559
+ }
560
+ }
561
+ else {
562
+ console.log(`[setup] 跳过 HTTPS,使用 HTTP 模式`);
563
+ console.log(`[setup] 如需启用 HTTPS,配置证书后重新运行 --setup --server`);
564
+ }
517
565
  console.log('\n[setup] Server 配置完成!');
518
566
  console.log(' 启动: npx @vrs-soft/wecom-aibot-mcp --http-only --start');
567
+ console.log('\n[setup] ─── 步骤 2/2:配置企业微信机器人 ───\n');
568
+ await addMcpConfig();
519
569
  }
520
570
  else if (wantChannel) {
521
571
  // Channel 客户端
522
572
  console.log('\n[setup] Channel Client 安装模式\n');
523
- let mcpUrl = process.env.MCP_URL;
573
+ // 交互式安装必须每次都提示,不能直接用已有的环境变量(可能是旧值)
574
+ const readline = await import('readline');
575
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
576
+ const existingUrl = process.env.MCP_URL || '';
577
+ const urlPrompt = existingUrl
578
+ ? `远程服务器地址(当前: ${existingUrl},直接回车保持不变): `
579
+ : `远程服务器地址(如 https://your-server:18963): `;
580
+ const urlInput = await new Promise(resolve => rl.question(urlPrompt, a => { rl.close(); resolve(a.trim()); }));
581
+ const mcpUrl = urlInput || existingUrl;
524
582
  if (!mcpUrl) {
525
- const readline = await import('readline');
526
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
527
- mcpUrl = await new Promise(resolve => rl.question('远程服务器地址(如 https://your-server:18963): ', a => { rl.close(); resolve(a.trim()); }));
528
- if (!mcpUrl) {
529
- console.log('[setup] ❌ 地址不能为空');
530
- process.exit(1);
531
- }
532
- process.env.MCP_URL = mcpUrl;
583
+ console.log('[setup] 地址不能为空');
584
+ process.exit(1);
533
585
  }
534
- if (!getAuthToken()) {
535
- const readline = await import('readline');
536
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
537
- const token = await new Promise(resolve => rl.question('Auth Token: ', a => { rl.close(); resolve(a.trim()); }));
538
- if (token)
539
- setAuthToken(token);
586
+ process.env.MCP_URL = mcpUrl;
587
+ {
588
+ const existingToken = getAuthToken();
589
+ const tokenPrompt = existingToken
590
+ ? `Auth Token(当前: ${existingToken.slice(0, 8)}...${existingToken.slice(-4)},直接回车保持不变): `
591
+ : 'Auth Token(留空跳过): ';
592
+ const readline2 = await import('readline');
593
+ const rl2 = readline2.createInterface({ input: process.stdin, output: process.stdout });
594
+ const tokenInput = await new Promise(resolve => rl2.question(tokenPrompt, a => { rl2.close(); resolve(a.trim()); }));
595
+ const finalToken = tokenInput || existingToken || '';
596
+ if (finalToken)
597
+ setAuthToken(finalToken);
540
598
  }
541
599
  ensureGlobalConfigs('channel-only');
542
600
  console.log('[setup] Channel MCP 配置完成!请重启 Claude Code 以加载配置');
@@ -10,6 +10,11 @@ export declare const VERSION: string;
10
10
  export declare function loadConfig(): WecomConfig | null;
11
11
  export declare function getAuthToken(): string | undefined;
12
12
  export declare function setAuthToken(token: string | undefined): boolean;
13
+ export declare function getHttpsConfig(): {
14
+ certPath: string;
15
+ keyPath: string;
16
+ } | null;
17
+ export declare function setHttpsConfig(certPath: string, keyPath: string): boolean;
13
18
  export declare function updateMcpAuthHeaders(token?: string): void;
14
19
  export declare function listAllMcpInstances(): Array<{
15
20
  name: string;
@@ -104,6 +104,38 @@ export function setAuthToken(token) {
104
104
  fs.writeFileSync(SERVER_CONFIG_FILE, JSON.stringify(config, null, 2));
105
105
  return true;
106
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
+ }
107
139
  // 更新 ~/.claude.json 中 wecom-aibot MCP 配置的 auth headers
108
140
  export function updateMcpAuthHeaders(token) {
109
141
  if (!fs.existsSync(CLAUDE_CONFIG_FILE))
@@ -1000,9 +1032,10 @@ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
1000
1032
  }
1001
1033
  if (!claudeConfig.mcpServers)
1002
1034
  claudeConfig.mcpServers = {};
1035
+ const mcpEndpointUrl = remoteOptions.url.replace(/\/+$/, '') + '/mcp';
1003
1036
  claudeConfig.mcpServers['wecom-aibot'] = {
1004
1037
  type: 'http',
1005
- url: remoteOptions.url,
1038
+ url: mcpEndpointUrl,
1006
1039
  headers: { Authorization: `Bearer ${remoteOptions.token}` },
1007
1040
  };
1008
1041
  fs.writeFileSync(CLAUDE_CONFIG_FILE, JSON.stringify(claudeConfig, null, 2));
@@ -1012,8 +1045,8 @@ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
1012
1045
  }
1013
1046
  // remote-channel 模式:写入远程 HTTP MCP(带 token)+ Channel MCP
1014
1047
  if (mode === 'remote-channel') {
1015
- if (!remoteOptions?.url || !remoteOptions?.token) {
1016
- console.log('[config] ❌ 远程模式需要提供 URL 和 Token');
1048
+ if (!remoteOptions?.url) {
1049
+ console.log('[config] ❌ 远程模式需要提供 URL');
1017
1050
  return { upgraded: false, previousVersion };
1018
1051
  }
1019
1052
  let claudeConfig = {};
@@ -1022,21 +1055,20 @@ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
1022
1055
  }
1023
1056
  if (!claudeConfig.mcpServers)
1024
1057
  claudeConfig.mcpServers = {};
1025
- // HTTP MCP 配置(带 token
1026
- claudeConfig.mcpServers['wecom-aibot'] = {
1027
- type: 'http',
1028
- url: remoteOptions.url,
1029
- headers: { Authorization: `Bearer ${remoteOptions.token}` },
1030
- };
1058
+ // HTTP MCP 配置(带 token,可选)
1059
+ const mcpEndpointUrl = remoteOptions.url.replace(/\/+$/, '') + '/mcp';
1060
+ const httpMcpConfig = { type: 'http', url: mcpEndpointUrl };
1061
+ if (remoteOptions.token)
1062
+ httpMcpConfig.headers = { Authorization: `Bearer ${remoteOptions.token}` };
1063
+ claudeConfig.mcpServers['wecom-aibot'] = httpMcpConfig;
1031
1064
  // Channel MCP 配置(带 MCP_URL + MCP_AUTH_TOKEN)
1032
- const binPath = path.join(__dirname, 'bin.js');
1065
+ const channelEnvRemote = { MCP_URL: remoteOptions.url.replace(/\/+$/, '') };
1066
+ if (remoteOptions.token)
1067
+ channelEnvRemote.MCP_AUTH_TOKEN = remoteOptions.token;
1033
1068
  claudeConfig.mcpServers['wecom-aibot-channel'] = {
1034
- command: 'node',
1035
- args: [binPath, '--channel'],
1036
- env: {
1037
- MCP_URL: remoteOptions.url,
1038
- MCP_AUTH_TOKEN: remoteOptions.token,
1039
- },
1069
+ command: 'npx',
1070
+ args: ['-y', '@vrs-soft/wecom-aibot-mcp', '--channel'],
1071
+ env: channelEnvRemote,
1040
1072
  };
1041
1073
  fs.writeFileSync(CLAUDE_CONFIG_FILE, JSON.stringify(claudeConfig, null, 2));
1042
1074
  console.log('[config] remote-channel 模式:已写入 HTTP MCP + Channel MCP 配置(带 Token)');
@@ -1054,6 +1086,11 @@ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
1054
1086
  }
1055
1087
  if (!claudeConfig.mcpServers)
1056
1088
  claudeConfig.mcpServers = {};
1089
+ // 从 node_modules 运行(npm/npx 安装)时用 npx,本地开发时用绝对路径
1090
+ const isPackageInstall = __dirname.includes('node_modules');
1091
+ const channelCmd = isPackageInstall
1092
+ ? { command: 'npx', args: ['-y', '@vrs-soft/wecom-aibot-mcp', '--channel'] }
1093
+ : { command: 'node', args: [path.join(__dirname, 'bin.js'), '--channel'] };
1057
1094
  if (mode === 'channel-only') {
1058
1095
  // Channel-only 模式:必须通过 MCP_URL 指定远程地址
1059
1096
  const mcpUrl = process.env.MCP_URL;
@@ -1062,19 +1099,17 @@ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
1062
1099
  console.log('[config] 请设置环境变量: MCP_URL=http://远程IP:18963');
1063
1100
  return { upgraded: false, previousVersion };
1064
1101
  }
1065
- // Channel MCP 配置:使用当前模块路径
1066
- const binPath = path.join(__dirname, 'bin.js');
1067
- const channelEnv = { MCP_URL: mcpUrl };
1102
+ const channelEnv = { MCP_URL: mcpUrl.replace(/\/+$/, '') };
1068
1103
  const authToken = getAuthToken();
1069
1104
  if (authToken) {
1070
1105
  channelEnv.MCP_AUTH_TOKEN = authToken;
1071
1106
  }
1072
1107
  claudeConfig.mcpServers['wecom-aibot-channel'] = {
1073
- command: 'node',
1074
- args: [binPath, '--channel'],
1108
+ command: channelCmd.command,
1109
+ args: channelCmd.args,
1075
1110
  env: channelEnv,
1076
1111
  };
1077
- console.log(`[config] Channel-only 模式:Channel MCP 使用本地路径`);
1112
+ console.log(`[config] Channel-only 模式:Channel MCP 已配置`);
1078
1113
  }
1079
1114
  else {
1080
1115
  // full 模式:同时写入 HTTP MCP 和 Channel MCP 配置
@@ -1082,12 +1117,23 @@ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
1082
1117
  type: 'http',
1083
1118
  url: 'http://127.0.0.1:18963/mcp',
1084
1119
  };
1085
- // Channel MCP 配置:使用当前模块路径
1086
- const binPath = path.join(__dirname, 'bin.js');
1120
+ // Channel MCP 配置:保留已有的自定义 MCP_URL(如 channel-only 安装时写入的远程地址)
1121
+ const existingChannel = claudeConfig.mcpServers['wecom-aibot-channel'];
1122
+ const existingMcpUrl = existingChannel?.env?.MCP_URL;
1123
+ const isRemote = existingMcpUrl && !existingMcpUrl.startsWith('http://127.0.0.1');
1124
+ const channelMcpUrl = isRemote ? existingMcpUrl : 'http://127.0.0.1:18963';
1125
+ const channelEnvFull = { MCP_URL: channelMcpUrl };
1126
+ // 保留已有的 MCP_AUTH_TOKEN(远程安装时写入),或从 server.json 读取
1127
+ const existingToken = existingChannel?.env?.MCP_AUTH_TOKEN;
1128
+ if (isRemote) {
1129
+ const token = existingToken || getAuthToken();
1130
+ if (token)
1131
+ channelEnvFull.MCP_AUTH_TOKEN = token;
1132
+ }
1087
1133
  claudeConfig.mcpServers['wecom-aibot-channel'] = {
1088
- command: 'node',
1089
- args: [binPath, '--channel'],
1090
- env: { MCP_URL: 'http://127.0.0.1:18963' },
1134
+ command: channelCmd.command,
1135
+ args: channelCmd.args,
1136
+ env: channelEnvFull,
1091
1137
  };
1092
1138
  console.log(`[config] full 模式:Channel MCP 使用本地路径`);
1093
1139
  }
@@ -62,7 +62,10 @@ export declare function getCCRegistryEntry(ccId: string): CCRegistryEntry | null
62
62
  export declare function getCCCount(): number;
63
63
  export declare function getCCCountByRobot(robotName: string): number;
64
64
  export declare function getOnlineCcIds(): string[];
65
- export declare function startHttpServer(_server: McpServer, port?: number): Promise<void>;
65
+ export declare function startHttpServer(_server: McpServer, port?: number, httpsConfig?: {
66
+ certPath: string;
67
+ keyPath: string;
68
+ }): Promise<void>;
66
69
  export declare function stopHttpServer(): void;
67
70
  export declare function cleanupPortFile(): void;
68
71
  export {};
@@ -13,6 +13,7 @@
13
13
  * - Session → robotName → WebSocket Connection
14
14
  */
15
15
  import * as http from 'http';
16
+ import * as https from 'https';
16
17
  import * as path from 'path';
17
18
  import * as os from 'os';
18
19
  import * as fs from 'fs';
@@ -461,12 +462,12 @@ ${onlineList.map(id => `• 【${id}】`).join('\n')}
461
462
  示例:引用【${onlineList[0]}】的消息后回复`;
462
463
  await client.sendText(reply);
463
464
  }
464
- export async function startHttpServer(_server, port = HTTP_PORT) {
465
+ export async function startHttpServer(_server, port = HTTP_PORT, httpsConfig) {
465
466
  startTime = Date.now();
466
467
  // 初始化 MCP Server
467
468
  initMcpServer();
468
469
  return new Promise((resolve, reject) => {
469
- httpServer = http.createServer(async (req, res) => {
470
+ const requestHandler = async (req, res) => {
470
471
  res.setHeader('Access-Control-Allow-Origin', '*');
471
472
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
472
473
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Mcp-Session-Id');
@@ -758,7 +759,16 @@ export async function startHttpServer(_server, port = HTTP_PORT) {
758
759
  }
759
760
  res.writeHead(404, { 'Content-Type': 'application/json' });
760
761
  res.end(JSON.stringify({ error: 'Not Found' }));
761
- });
762
+ };
763
+ // 根据是否有 HTTPS 配置创建对应的 server
764
+ if (httpsConfig) {
765
+ const cert = fs.readFileSync(httpsConfig.certPath, 'utf-8');
766
+ const key = fs.readFileSync(httpsConfig.keyPath, 'utf-8');
767
+ httpServer = https.createServer({ cert, key }, requestHandler);
768
+ }
769
+ else {
770
+ httpServer = http.createServer(requestHandler);
771
+ }
762
772
  httpServer.on('error', (err) => {
763
773
  if (err.code === 'EADDRINUSE') {
764
774
  reject(new Error(`端口 ${port} 已被占用`));
@@ -767,9 +777,12 @@ export async function startHttpServer(_server, port = HTTP_PORT) {
767
777
  reject(err);
768
778
  }
769
779
  });
770
- httpServer.listen(port, '127.0.0.1', async () => {
771
- logger.log(`[http] MCP Server 已启动: http://127.0.0.1:${port}`);
772
- logger.log(`[http] MCP endpoint: http://127.0.0.1:${port}/mcp (stateless mode)`);
780
+ // HTTPS 模式绑定所有网卡(供远程客户端访问),HTTP 模式只绑本地
781
+ const host = httpsConfig ? '0.0.0.0' : '127.0.0.1';
782
+ const protocol = httpsConfig ? 'https' : 'http';
783
+ httpServer.listen(port, host, async () => {
784
+ logger.log(`[http] MCP Server 已启动: ${protocol}://${host}:${port}`);
785
+ logger.log(`[http] MCP endpoint: ${protocol}://${host}:${port}/mcp (stateless mode)`);
773
786
  // 自动连接所有配置的机器人
774
787
  await connectAllRobots();
775
788
  resolve();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vrs-soft/wecom-aibot-mcp",
3
- "version": "2.4.2",
3
+ "version": "2.4.4",
4
4
  "description": "企业微信智能机器人 MCP 服务 - Claude Code 审批通道",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",