@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.
package/dist/bin.js CHANGED
@@ -1,118 +1,49 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * wecom-aibot-mcp - 企业微信智能机器人 MCP 服务
3
+ * wecom-aibot-mcp - 企业微信智能机器人 MCP 客户端
4
4
  *
5
- * npx 运行入口
5
+ * 连接远程 wecom-aibot-server daemon,为 Claude Code 提供微信消息通道。
6
6
  *
7
- * v2.0 架构变更:
8
- * - 使用 Session 管理
9
- * - robotName 作为连接索引
10
- * - 不再使用 projectDir
7
+ * 运行模式:
8
+ * --channel Channel MCP 代理(SSE 唤醒,推荐)
9
+ * --install 交互式安装向导(配置 daemon 地址 + Token)
10
+ * --version 版本号
11
+ * --help 帮助
11
12
  */
12
- import { spawn, execSync } from 'child_process';
13
- import * as fs from 'fs';
14
13
  import * as path from 'path';
15
14
  import * as os from 'os';
16
- import { runConfigWizard, loadConfig, saveConfig, deleteRobotConfigInteractive, uninstall, addMcpConfig, detectUserIdFromMessage, ensureHookInstalled, listAllRobots, ensureGlobalConfigs, getInstalledMode, getAuthToken, setAuthToken, getHttpsConfig, setHttpsConfig, updateMcpAuthHeaders, runRemoteInstallWizard, VERSION, } from './config-wizard.js';
17
- import { initClient } from './client.js';
18
- import { registerTools } from './tools/index.js';
19
- import { startHttpServer, stopHttpServer, HTTP_PORT } from './http-server.js';
20
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
21
- import { getAllConnectionStates } from './connection-manager.js';
22
- import { loadStats, cleanupOldLogs } from './connection-log.js';
23
- import { startKeepaliveMonitor, stopKeepaliveMonitor } from './keepalive-monitor.js';
15
+ import { VERSION, runRemoteInstallWizard, uninstall, getInstalledMode } from './config-wizard.js';
16
+ import { startChannelServer } from './channel-server.js';
24
17
  import { logger } from './logger.js';
25
- const PID_FILE = path.join(os.homedir(), '.wecom-aibot-mcp', 'server.pid');
18
+ const CONFIG_DIR = path.join(os.homedir(), '.wecom-aibot-mcp');
26
19
  function showHelp() {
27
20
  console.log(`
28
- 企业微信智能机器人 MCP 服务 v${VERSION}
21
+ 企业微信智能机器人 MCP 客户端 v${VERSION}
29
22
 
30
23
  安装:
31
- npx @vrs-soft/wecom-aibot-mcp
24
+ npx @vrs-soft/wecom-aibot-mcp --install
32
25
 
33
26
  用法:
34
27
  npx @vrs-soft/wecom-aibot-mcp [选项]
35
28
 
36
29
  选项:
37
- --help, -h 显示帮助信息
30
+ --help, -h 显示帮助
38
31
  --version, -v 显示版本号
39
- --setup 安装向导(交互式,询问本地 / 远程)
40
- --setup --server 服务器端安装(配置机器人 + Token
41
- --setup --channel Channel 客户端安装(写入 Channel MCP)
42
- --setup --server --channel 本地完整安装(HTTP + Channel)
43
- --upgrade 强制升级全局配置(覆盖 MCP 配置、权限、skill)
44
- --reinstall 重新安装全局配置(删除后重新写入,保留机器人配置)
45
- --start 启动 HTTP MCP Server(后台守护进程,日志写 server.log)
46
- --stop 停止 MCP Server
47
- --debug 前台启动 + debug 级日志(日志同时落 server.log,stdout 实时打印)
48
- --channel 启动 Channel MCP Proxy(stdio 代理 + SSE 唤醒,日志写 channel.log)
49
- --http-only 仅写 HTTP-only 配置(已废弃;--start 默认就是 daemon-only 行为)
50
- --channel-only 仅配置 Channel MCP(本地连接远程 HTTP Server)
51
- --status 显示服务状态和机器人配置
52
- --config 重新配置默认机器人(修改 Bot ID / Secret / 目标用户)
53
- --add 添加新的机器人配置(多机器人场景)
54
- --rename [名称] 重命名机器人(可选参数:旧名称,交互式输入新名称)
55
- --list 列出所有已配置的机器人及其占用状态
56
- --delete [名称] 删除指定的机器人配置(保留 MCP 配置)
57
- --uninstall 卸载并删除所有配置(包括 MCP 配置、hook、skill)
58
- --set-token [token] 设置/清除 Auth Token(远程部署用,--set-token --clear 清除)
59
- --clean-cache 清空 CC 注册表缓存(清理异常断线残留的 ccId)
32
+ --install 交互式安装向导(配置 daemon 地址 + Auth Token)
33
+ --channel 启动 Channel MCP 代理(stdio,日志写 channel.log
34
+ --uninstall 卸载并清除所有本地配置
60
35
 
61
- 使用流程:
62
- 1. 安装: npx @vrs-soft/wecom-aibot-mcp --setup
63
- (根据角色选择参数:--server / --channel / 两者都传 / 不传交互选择)
36
+ 连接模式:
37
+ Channel(推荐): SSE 长连接,消息到达即唤醒 agent
38
+ 配置写入 ~/.claude.json: wecom-aibot-channel
39
+ 环境变量: MCP_URL=<daemon 地址>, MCP_AUTH_TOKEN=<token>
64
40
 
65
- 2. 启动服务: npx @vrs-soft/wecom-aibot-mcp --start
66
- (后台启动 MCP HTTP Server)
41
+ HTTP(直连): Claude Code 直接连接 daemon /mcp 端点
42
+ 安装时选择 HTTP 模式,或手动写入 ~/.claude.json
67
43
 
68
- 3. 停止服务: npx @vrs-soft/wecom-aibot-mcp --stop
69
-
70
- 拆分部署(远程 HTTP + 本地 Channel):
71
-
72
- 远程服务器:
73
- npx @vrs-soft/wecom-aibot-mcp --start
74
- # 启动 HTTP Server(daemon 不会写本地 client MCP 配置)
75
-
76
- 本地机器:
77
- MCP_URL=http://远程IP:18963 npx @vrs-soft/wecom-aibot-mcp --channel-only
78
- # 必须通过 MCP_URL 指定远程 HTTP MCP 地址
79
- # 只配置 Channel MCP,连接远程 HTTP Server
80
-
81
- MCP 配置(默认安装同时配置两种模式):
82
-
83
- HTTP Transport(轮询模式):
84
- "wecom-aibot": {
85
- "type": "http",
86
- "url": "http://127.0.0.1:18963/mcp"
87
- }
88
-
89
- Channel Transport(SSE 推送模式):
90
- "wecom-aibot-channel": {
91
- "command": "npx",
92
- "args": ["@vrs-soft/wecom-aibot-mcp", "--channel"]
93
- }
94
-
95
- Channel 模式优势:微信消息自动唤醒 agent,无需主动轮询
96
- 启动 Channel 模式(研究预览):
97
- claude --dangerously-load-development-channels server:wecom-aibot-channel
98
-
99
- 日志:
100
-
101
- Daemon (HTTP MCP):
102
- ~/.wecom-aibot-mcp/server.log info 级(永久)+ debug 级(仅 --debug 时写)
103
- ~/.wecom-aibot-mcp/server.log.1..5 自动滚动备份(每份 ≤10MB,共保留 5 份)
104
-
105
- Channel MCP:
106
- ~/.wecom-aibot-mcp/channel.log info 级(永久)+ debug 级(仅 debug 模式时写)
107
- ~/.wecom-aibot-mcp/channel.log.1..5 自动滚动备份
108
-
109
- WebSocket 连接事件:
110
- ~/.wecom-aibot-mcp/connection.log 专用文件(connect/disconnect/auth 事件)
111
-
112
- Debug 标记:
113
- ~/.wecom-aibot-mcp/debug 存在则启用 debug 级输出(--debug 时自动创建)
114
-
115
- 日志格式: JSON Lines,每行一个 {ts, level, msg, data?},方便 jq/grep 检索
44
+ 前提条件:
45
+ 需要运行中的 wecom-aibot-server daemon(私有部署)
46
+ daemon 地址示例: https://your-server:18963
116
47
 
117
48
  更多信息: https://github.com/eric2877/wecom-aibot-mcp
118
49
  `);
@@ -120,294 +51,8 @@ MCP 配置(默认安装同时配置两种模式):
120
51
  function showVersion() {
121
52
  console.log(`wecom-aibot-mcp v${VERSION}`);
122
53
  }
123
- function showStatus() {
124
- const allRobots = listAllRobots();
125
- const connections = getAllConnectionStates();
126
- const authToken = getAuthToken();
127
- // 检查服务是否运行
128
- const serverRunning = isServerRunning();
129
- console.log(`\n服务状态: ${serverRunning ? '✅ 运行中' : '❌ 未启动'}`);
130
- // 显示 Auth Token 状态(带部分 token 显示)
131
- if (authToken) {
132
- const maskedToken = authToken.length > 12
133
- ? `${authToken.slice(0, 8)}...${authToken.slice(-4)}`
134
- : `${authToken.slice(0, 4)}...`;
135
- console.log(`Auth Token: ✅ 已配置 (${maskedToken})`);
136
- }
137
- else {
138
- console.log(`Auth Token: (未配置,本地部署无需 token)`);
139
- }
140
- console.log('');
141
- if (allRobots.length === 0) {
142
- console.log('尚未配置机器人,请运行 npx @vrs-soft/wecom-aibot-mcp 启动配置向导');
143
- return;
144
- }
145
- // 构建机器人占用信息
146
- const robotUsage = new Map();
147
- for (const conn of connections) {
148
- if (conn.agentName) {
149
- robotUsage.set(conn.robotName, { agentName: conn.agentName });
150
- }
151
- }
152
- console.log(`已配置 ${allRobots.length} 个机器人:\n`);
153
- for (const robot of allRobots) {
154
- const usage = robotUsage.get(robot.name);
155
- const statusTag = usage ? ` [使用中]` : '';
156
- const docTag = robot.doc_mcp_url ? ' [文档✅]' : '';
157
- console.log(` Bot名称: ${robot.name}${statusTag}${docTag}`);
158
- console.log(` Bot ID: ${robot.botId}`);
159
- console.log(` 目标用户:${robot.targetUserId}`);
160
- if (usage) {
161
- console.log(` 使用者: ${usage.agentName}`);
162
- }
163
- console.log('');
164
- }
165
- }
166
- // 检查服务是否运行
167
- function isServerRunning() {
168
- if (!fs.existsSync(PID_FILE)) {
169
- return false;
170
- }
171
- try {
172
- const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
173
- // 检查进程是否存在
174
- process.kill(pid, 0);
175
- return true;
176
- }
177
- catch {
178
- // 进程不存在,清理 PID 文件(可能已被进程自身删除)
179
- if (fs.existsSync(PID_FILE)) {
180
- fs.unlinkSync(PID_FILE);
181
- }
182
- return false;
183
- }
184
- }
185
- // 通过端口查找进程 PID(fallback,当 PID 文件不存在时)
186
- function findPidByPort(port) {
187
- try {
188
- // Linux: ss -tlnp | grep :18963
189
- const output = execSync(`ss -tlnp 2>/dev/null | grep ':${port}'`, { encoding: 'utf-8' });
190
- const match = output.match(/pid=(\d+)/);
191
- if (match)
192
- return parseInt(match[1]);
193
- }
194
- catch { /* ignore */ }
195
- try {
196
- // macOS: lsof -ti :18963
197
- const output = execSync(`lsof -ti :${port} 2>/dev/null`, { encoding: 'utf-8' }).trim();
198
- if (output)
199
- return parseInt(output.split('\n')[0]);
200
- }
201
- catch { /* ignore */ }
202
- return null;
203
- }
204
- // 停止服务
205
- function stopServer() {
206
- let pid = null;
207
- // 优先从 PID 文件获取
208
- if (fs.existsSync(PID_FILE)) {
209
- pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
210
- // 检查进程是否存在
211
- try {
212
- process.kill(pid, 0);
213
- }
214
- catch {
215
- // PID 文件残留但进程已死,清理 PID 文件
216
- console.log('[mcp] PID 文件残留,进程已退出,清理中...');
217
- fs.unlinkSync(PID_FILE);
218
- pid = null;
219
- }
220
- }
221
- // PID 文件不存在或残留:通过端口查找
222
- if (pid === null) {
223
- pid = findPidByPort(HTTP_PORT);
224
- if (pid === null) {
225
- console.log('[mcp] 服务未运行');
226
- return false;
227
- }
228
- console.log(`[mcp] 通过端口 ${HTTP_PORT} 找到进程 PID: ${pid}`);
229
- }
230
- // 发送 SIGTERM
231
- try {
232
- process.kill(pid, 'SIGTERM');
233
- }
234
- catch {
235
- // ESRCH: 进程不存在,清理即可
236
- if (fs.existsSync(PID_FILE))
237
- fs.unlinkSync(PID_FILE);
238
- console.log('[mcp] 服务已停止');
239
- return true;
240
- }
241
- // 等待进程退出(最多 5 秒)
242
- const deadline = Date.now() + 5000;
243
- while (Date.now() < deadline) {
244
- try {
245
- process.kill(pid, 0);
246
- // 进程还在,同步等待 100ms
247
- const waitUntil = Date.now() + 100;
248
- while (Date.now() < waitUntil) { /* busy wait */ }
249
- }
250
- catch {
251
- break;
252
- }
253
- }
254
- // 清理 PID 文件
255
- if (fs.existsSync(PID_FILE)) {
256
- fs.unlinkSync(PID_FILE);
257
- }
258
- console.log('[mcp] 服务已停止');
259
- return true;
260
- }
261
- // 等待连接验证(用于配置向导验证凭证)
262
- async function waitForConnection(client, timeoutMs = 10000) {
263
- return new Promise((resolve) => {
264
- const startTime = Date.now();
265
- const checkInterval = setInterval(() => {
266
- if (client.isConnected()) {
267
- clearInterval(checkInterval);
268
- resolve(true);
269
- }
270
- else if (Date.now() - startTime > timeoutMs) {
271
- clearInterval(checkInterval);
272
- resolve(false);
273
- }
274
- }, 500);
275
- });
276
- }
277
- // 启动 MCP Server(前台运行,供 --start 使用)
278
- async function startMcpServerForeground(isDebug = false) {
279
- const savedConfig = loadConfig();
280
- if (!savedConfig || !savedConfig.botId || !savedConfig.secret || !savedConfig.targetUserId) {
281
- logger.error('[mcp] 未找到配置,请先运行: npx @vrs-soft/wecom-aibot-mcp');
282
- process.exit(1);
283
- }
284
- // 写入 PID 文件
285
- fs.writeFileSync(PID_FILE, String(process.pid));
286
- // Debug 模式:创建 debug 标记文件
287
- if (isDebug) {
288
- const debugFile = path.join(os.homedir(), '.wecom-aibot-mcp', 'debug');
289
- fs.writeFileSync(debugFile, 'true');
290
- console.log('[mcp] Debug 标记文件已创建');
291
- }
292
- // 配置统一日志输出到文件(JSON Lines + 自动滚动)
293
- logger.setLogFile(path.join(os.homedir(), '.wecom-aibot-mcp', 'server.log'));
294
- if (isDebug)
295
- logger.setDebug(true);
296
- // 确保 hook 已安装
297
- ensureHookInstalled();
298
- // 加载统计并清理旧日志
299
- loadStats();
300
- cleanupOldLogs(1 / 24);
301
- // 创建 MCP Server
302
- const server = new McpServer({
303
- name: 'wecom-aibot-mcp',
304
- version: VERSION,
305
- });
306
- registerTools(server);
307
- // 启动 HTTP 服务
308
- logger.log('');
309
- logger.log(' ╔════════════════════════════════════════════════════════╗');
310
- logger.log(` ║ 企业微信智能机器人 MCP 服务 v${VERSION} ║`);
311
- logger.log(' ║ Claude Code 审批通道 ║');
312
- logger.log(' ╚════════════════════════════════════════════════════════╝');
313
- logger.log('');
314
- const httpsConfig = getHttpsConfig() ?? undefined;
315
- const protocol = httpsConfig ? 'HTTPS' : 'HTTP';
316
- logger.log(`[mcp] 启动 MCP ${protocol} Server (端口: ${HTTP_PORT})...`);
317
- await startHttpServer(server, HTTP_PORT, httpsConfig);
318
- startKeepaliveMonitor();
319
- logger.log(`[mcp] MCP Server 已就绪`);
320
- logger.log(`[mcp] HTTP endpoint: http://127.0.0.1:${HTTP_PORT}/mcp`);
321
- logger.log(`[mcp] 健康检查: http://127.0.0.1:${HTTP_PORT}/health`);
322
- logger.log(`[mcp] 微信模式:enter_headless_mode 时建立连接`);
323
- logger.log(`[mcp] PID: ${process.pid}`);
324
- // 写入启动事件到 server.log(永久记录)
325
- logger.info('daemon started', { version: VERSION, port: HTTP_PORT, protocol, pid: process.pid, debug: isDebug });
326
- // 退出处理
327
- const gracefulShutdown = () => {
328
- console.log('[mcp] 正在关闭...');
329
- logger.info('daemon shutdown', { pid: process.pid });
330
- stopKeepaliveMonitor();
331
- stopHttpServer();
332
- if (fs.existsSync(PID_FILE)) {
333
- fs.unlinkSync(PID_FILE);
334
- }
335
- // Debug 模式:删除 debug 标记文件
336
- if (isDebug) {
337
- const debugFile = path.join(os.homedir(), '.wecom-aibot-mcp', 'debug');
338
- if (fs.existsSync(debugFile)) {
339
- fs.unlinkSync(debugFile);
340
- console.log('[mcp] Debug 标记文件已删除');
341
- }
342
- }
343
- process.exit(0);
344
- };
345
- process.on('SIGINT', gracefulShutdown);
346
- process.on('SIGTERM', gracefulShutdown);
347
- }
348
- // 后台启动 MCP Server(使用 spawn)
349
- function startMcpServerBackground() {
350
- // 检查配置是否存在
351
- const savedConfig = loadConfig();
352
- if (!savedConfig || !savedConfig.botId || !savedConfig.secret || !savedConfig.targetUserId) {
353
- logger.error('[mcp] 未找到配置,请先运行: npx @vrs-soft/wecom-aibot-mcp');
354
- process.exit(1);
355
- }
356
- // 检查是否已运行
357
- if (isServerRunning()) {
358
- console.log('[mcp] 服务已在运行中');
359
- return;
360
- }
361
- const nodePath = process.execPath;
362
- const scriptPath = process.argv[1];
363
- const child = spawn(nodePath, [scriptPath, '--start', '--foreground'], {
364
- detached: true,
365
- stdio: 'ignore',
366
- });
367
- child.unref();
368
- console.log('[mcp] MCP Server 已在后台启动');
369
- logger.log(`[mcp] HTTP endpoint: http://127.0.0.1:18963/mcp`);
370
- console.log('[mcp] 健康检查: curl http://127.0.0.1:18963/health');
371
- console.log('[mcp] 停止服务: npx @vrs-soft/wecom-aibot-mcp --stop');
372
- console.log('[mcp] 调试模式: npx @vrs-soft/wecom-aibot-mcp --debug');
373
- }
374
54
  async function main() {
375
55
  const args = process.argv.slice(2);
376
- // 确定安装模式:优先 CLI flag,其次复用 version.json 里上次的 mode(保持 remote / channel-only 等模式不被 --upgrade 打回 full)
377
- const explicitMode = args.includes('--http-only') ? 'http-only' :
378
- args.includes('--channel-only') ? 'channel-only' : undefined;
379
- const prior = getInstalledMode();
380
- const installMode = explicitMode || prior.mode || 'full';
381
- const remoteOptions = (installMode === 'remote' || installMode === 'remote-channel') && prior.remote?.url
382
- ? { url: prior.remote.url, token: prior.remote.token || '' }
383
- : undefined;
384
- // 以下命令跳过顶部 ensureGlobalConfigs,避免覆盖配置
385
- // --setup: 向导完成后自己调用
386
- // --channel: 作为 Channel MCP 代理运行,不应改写全局配置
387
- // --reinstall / --http-only: 有自己的处理逻辑
388
- // --version / -v: 只查版本,不写配置
389
- // --stop / --status / --list / --clean-cache / --set-token / --config: 管理命令,不应改写配置
390
- const skipEnsure = args.includes('--reinstall') || args.includes('--http-only') ||
391
- args.includes('--setup') || args.includes('--channel') ||
392
- args.includes('--version') || args.includes('-v') ||
393
- args.includes('--start') || args.includes('--debug') ||
394
- args.includes('--stop') || args.includes('--status') || args.includes('--list') ||
395
- args.includes('--clean-cache') || args.includes('--set-token') || args.includes('--config');
396
- if (!skipEnsure) {
397
- // 强制覆盖所有全局配置(不依赖智能体)
398
- if (installMode === 'remote' || installMode === 'remote-channel') {
399
- if (remoteOptions) {
400
- ensureGlobalConfigs(installMode, remoteOptions);
401
- }
402
- else {
403
- console.log(`[mcp] 检测到上次安装模式 ${installMode},但缺少远程参数;跳过配置写入。如需变更请使用 --setup`);
404
- }
405
- }
406
- else {
407
- ensureGlobalConfigs(installMode);
408
- }
409
- }
410
- // 解析命令行参数
411
56
  if (args.includes('--help') || args.includes('-h')) {
412
57
  showHelp();
413
58
  process.exit(0);
@@ -416,438 +61,36 @@ async function main() {
416
61
  showVersion();
417
62
  process.exit(0);
418
63
  }
419
- // --upgrade 命令:强制升级全局配置(已在启动时执行,这里显示结果)
420
- if (args.includes('--upgrade')) {
421
- console.log('\n[mcp] ✅ 全局配置已更新完成!');
422
- console.log('[mcp] 配置位置:');
423
- console.log(' - ~/.claude.json (MCP Server 配置)');
424
- console.log(' - ~/.claude/settings.local.json (权限和 Hook)');
425
- console.log(' - ~/.wecom-aibot-mcp/version.json (版本记录)');
426
- console.log('\n[mcp] 请重启 Claude Code 以加载最新配置');
427
- process.exit(0);
428
- }
429
- // --reinstall 命令:删除所有全局配置(保留机器人配置)后重新安装
430
- if (args.includes('--reinstall')) {
431
- logger.log('\n[mcp] 重新安装全局配置...');
432
- console.log('[mcp] 保留所有机器人配置: ~/.wecom-aibot-mcp/robot-*.json');
433
- const CLAUDE_CONFIG_FILE = path.join(os.homedir(), '.claude.json');
434
- const CLAUDE_SETTINGS_FILE = path.join(os.homedir(), '.claude', 'settings.local.json');
435
- const VERSION_FILE = path.join(os.homedir(), '.wecom-aibot-mcp', 'version.json');
436
- const HOOK_SCRIPT = path.join(os.homedir(), '.wecom-aibot-mcp', 'permission-hook.sh');
437
- // 1. 删除 ~/.claude.json 中的 wecom-aibot 配置
438
- if (fs.existsSync(CLAUDE_CONFIG_FILE)) {
439
- const content = fs.readFileSync(CLAUDE_CONFIG_FILE, 'utf-8');
440
- const config = JSON.parse(content);
441
- if (config.mcpServers?.['wecom-aibot']) {
442
- delete config.mcpServers['wecom-aibot'];
443
- fs.writeFileSync(CLAUDE_CONFIG_FILE, JSON.stringify(config, null, 2));
444
- console.log('[mcp] 已删除 ~/.claude.json 中的 wecom-aibot 配置');
445
- }
446
- }
447
- // 2. 删除 ~/.claude/settings.local.json 中的权限和 Hook
448
- if (fs.existsSync(CLAUDE_SETTINGS_FILE)) {
449
- const content = fs.readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8');
450
- const config = JSON.parse(content);
451
- if (config.permissions?.allow) {
452
- config.permissions.allow = config.permissions.allow.filter((p) => !p.startsWith('mcp__wecom-aibot__'));
453
- console.log('[mcp] 已删除 wecom-aibot 工具权限');
454
- }
455
- if (config.hooks?.PermissionRequest) {
456
- config.hooks.PermissionRequest = config.hooks.PermissionRequest.filter((h) => !h.hooks?.some?.((hook) => hook.command?.includes?.('wecom-aibot-mcp')));
457
- if (config.hooks.PermissionRequest.length === 0) {
458
- delete config.hooks.PermissionRequest;
459
- }
460
- console.log('[mcp] 已删除 PermissionRequest hook');
461
- }
462
- fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(config, null, 2));
463
- }
464
- // 3. 删除版本文件
465
- if (fs.existsSync(VERSION_FILE)) {
466
- fs.unlinkSync(VERSION_FILE);
467
- console.log('[mcp] 已删除 ~/.wecom-aibot-mcp/version.json');
468
- }
469
- // 4. 删除 hook 脚本
470
- if (fs.existsSync(HOOK_SCRIPT)) {
471
- fs.unlinkSync(HOOK_SCRIPT);
472
- console.log('[mcp] 已删除 ~/.wecom-aibot-mcp/permission-hook.sh');
473
- }
474
- // 5. 重新安装全局配置
475
- logger.log('\n[mcp] 正在重新安装...');
476
- ensureGlobalConfigs();
477
- logger.log('\n[mcp] ✅ 重新安装完成!');
478
- console.log('[mcp] 请重启 Claude Code 以加载最新配置');
479
- process.exit(0);
480
- }
481
- if (args.includes('--status') || args.includes('--list')) {
482
- showStatus();
483
- process.exit(0);
484
- }
485
- // --stop 命令:停止服务
486
- if (args.includes('--stop')) {
487
- stopServer();
488
- process.exit(0);
489
- }
490
- // --clean-cache 命令:清空 CC 注册表缓存
491
- if (args.includes('--clean-cache')) {
492
- if (!isServerRunning()) {
493
- console.log('[mcp] 服务未运行,无需清理缓存');
494
- process.exit(0);
495
- }
496
- try {
497
- const res = await fetch(`http://127.0.0.1:${HTTP_PORT}/admin/clean-cache`, { method: 'POST' });
498
- const data = await res.json();
499
- if (data.ok) {
500
- console.log(`[mcp] 已清空 CC 注册表,共清理 ${data.cleared} 条`);
501
- if (data.entries.length > 0) {
502
- console.log(`[mcp] 已清理: ${data.entries.join(', ')}`);
503
- }
504
- }
505
- }
506
- catch (err) {
507
- console.error('[mcp] 清理失败:', err);
508
- }
509
- process.exit(0);
510
- }
511
- // --uninstall 命令:先停止服务再卸载
512
- if (args.includes('--uninstall')) {
513
- if (isServerRunning()) {
514
- console.log('[mcp] 正在停止服务...');
515
- stopServer();
516
- }
517
- uninstall();
518
- process.exit(0);
519
- }
520
- // --set-token 命令:设置/清除 Auth Token
521
- if (args.includes('--set-token')) {
522
- const tokenIndex = args.indexOf('--set-token');
523
- const clearToken = args.includes('--clear');
524
- if (clearToken) {
525
- setAuthToken(undefined);
526
- updateMcpAuthHeaders(undefined);
527
- console.log('[mcp] ✅ Auth Token 已清除(服务端 + 客户端 MCP 配置)');
528
- process.exit(0);
529
- }
530
- // 检查下一个参数是否是 token(不是另一个 --flag)
531
- const nextArg = args[tokenIndex + 1];
532
- const token = (nextArg && !nextArg.startsWith('--')) ? nextArg : undefined;
533
- if (!token) {
534
- // 交互式输入 token
535
- const readline = await import('readline');
536
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
537
- const input = await new Promise((resolve) => {
538
- rl.question('请输入 Auth Token(留空取消): ', (answer) => {
539
- rl.close();
540
- resolve(answer.trim());
541
- });
542
- });
543
- if (!input) {
544
- console.log('[mcp] 已取消');
545
- process.exit(0);
546
- }
547
- setAuthToken(input);
548
- updateMcpAuthHeaders(input);
549
- console.log('[mcp] ✅ Auth Token 已设置');
550
- console.log(`[mcp] 服务端: ~/.wecom-aibot-mcp/server.json`);
551
- console.log(`[mcp] 客户端: ~/.claude.json MCP headers 已同步`);
552
- console.log(`[mcp] Token: ${input.slice(0, 8)}...${input.slice(-4)}`);
553
- }
554
- else {
555
- setAuthToken(token);
556
- updateMcpAuthHeaders(token);
557
- console.log('[mcp] ✅ Auth Token 已设置');
558
- console.log(`[mcp] Token: ${token.slice(0, 8)}...${token.slice(-4)}`);
559
- }
560
- process.exit(0);
561
- }
562
- if (args.includes('--add')) {
563
- await addMcpConfig();
564
- process.exit(0);
565
- }
566
- // --delete 命令:删除单个机器人配置
567
- const deleteIndex = args.indexOf('--delete');
568
- if (deleteIndex !== -1) {
569
- const robotName = args[deleteIndex + 1]; // 可选参数:机器人名称
570
- await deleteRobotConfigInteractive(robotName);
571
- process.exit(0);
572
- }
573
- // --start --foreground:前台启动(内部调用,输出到日志文件)
574
- if (args.includes('--start') && args.includes('--foreground')) {
575
- await startMcpServerForeground();
576
- return; // 保持运行,不 exit
577
- }
578
- // --setup:统一安装向导
579
- // --setup → 交互式(询问本地 / 远程)
580
- // --setup --server → 服务器端(机器人配置 + Token)
581
- // --setup --channel → Channel 客户端(写入 Channel MCP)
582
- // --setup --server --channel → 本地完整安装(HTTP + Channel)
583
- if (args.includes('--setup')) {
584
- const wantServer = args.includes('--server');
585
- const wantChannel = args.includes('--channel');
586
- if (wantServer && wantChannel) {
587
- // 本地完整安装
588
- console.log('\n[setup] 本地完整安装模式\n');
589
- const savedConfig = loadConfig();
590
- if (!savedConfig?.botId)
591
- await runConfigWizard();
592
- ensureGlobalConfigs('full');
593
- startMcpServerBackground();
594
- console.log('[setup] 安装完成!请重启 Claude Code 以加载配置');
595
- }
596
- else if (wantServer) {
597
- // 服务器端:分两步——先完成 Server 安装,再配置机器人
598
- console.log('\n[setup] ─── 步骤 1/2:Server 安装 ───\n');
599
- console.log(' Server 负责运行 HTTP MCP 服务,Bot 配置在下一步单独完成\n');
600
- const readline = await import('readline');
601
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
602
- const token = await new Promise(resolve => rl.question('Auth Token(Client 端连接时需填写相同 Token,留空跳过): ', a => { rl.close(); resolve(a.trim()); }));
603
- if (token)
604
- setAuthToken(token);
605
- // HTTPS 证书配置
606
- const defaultCertPath = path.join(os.homedir(), '.wecom-aibot-mcp', 'cert.pem');
607
- console.log('\n HTTPS 证书配置(留空跳过,保持 HTTP 模式)');
608
- console.log(' 请输入完整路径含文件名(.pem / .crt / .key 均可),例如:');
609
- console.log(` ${defaultCertPath}`);
610
- console.log(' /etc/letsencrypt/live/example.com/fullchain.pem');
611
- console.log(' /etc/gitlab/ssl/gitlab.example.com.crt\n');
612
- const checkFile = (p, label) => {
613
- if (!fs.existsSync(p)) {
614
- console.log(`[setup] ⚠️ ${label}文件不存在: ${p}`);
615
- return false;
616
- }
617
- if (fs.statSync(p).isDirectory()) {
618
- console.log(`[setup] ⚠️ ${label}路径是目录而非文件: ${p}`);
619
- return false;
620
- }
621
- return true;
622
- };
623
- const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
624
- const certInput = await new Promise(resolve => rl2.question(`SSL 证书文件完整路径(留空跳过): `, a => { rl2.close(); resolve(a.trim()); }));
625
- if (certInput) {
626
- if (!checkFile(certInput, '证书')) {
627
- console.log('[setup] 跳过 HTTPS 配置');
628
- }
629
- else {
630
- const rl3 = readline.createInterface({ input: process.stdin, output: process.stdout });
631
- const keyInput = await new Promise(resolve => rl3.question(`SSL 私钥文件完整路径: `, a => { rl3.close(); resolve(a.trim()); }));
632
- if (keyInput && checkFile(keyInput, '私钥')) {
633
- setHttpsConfig(certInput, keyInput);
634
- console.log(`[setup] HTTPS 已配置`);
635
- console.log(` 证书: ${certInput}`);
636
- console.log(` 私钥: ${keyInput}`);
637
- }
638
- else if (!keyInput) {
639
- console.log('[setup] 私钥路径不能为空,跳过 HTTPS 配置');
640
- }
641
- }
642
- }
643
- else {
644
- console.log(`[setup] 跳过 HTTPS,使用 HTTP 模式`);
645
- console.log(`[setup] 如需启用 HTTPS,配置证书后重新运行 --setup --server`);
646
- }
647
- console.log('\n[setup] Server 配置完成!');
648
- console.log(' 启动: npx @vrs-soft/wecom-aibot-mcp --start');
649
- console.log('\n[setup] ─── 步骤 2/2:配置企业微信机器人 ───\n');
650
- await addMcpConfig();
651
- }
652
- else if (wantChannel) {
653
- // Channel 客户端
654
- console.log('\n[setup] Channel Client 安装模式\n');
655
- // 交互式安装必须每次都提示,不能直接用已有的环境变量(可能是旧值)
656
- const readline = await import('readline');
657
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
658
- const existingUrl = process.env.MCP_URL || '';
659
- const urlPrompt = existingUrl
660
- ? `远程服务器地址(当前: ${existingUrl},直接回车保持不变): `
661
- : `远程服务器地址(如 https://your-server:18963): `;
662
- const urlInput = await new Promise(resolve => rl.question(urlPrompt, a => { rl.close(); resolve(a.trim()); }));
663
- const mcpUrl = urlInput || existingUrl;
664
- if (!mcpUrl) {
665
- console.log('[setup] ❌ 地址不能为空');
666
- process.exit(1);
667
- }
668
- process.env.MCP_URL = mcpUrl;
669
- {
670
- const existingToken = getAuthToken();
671
- const tokenPrompt = existingToken
672
- ? `Auth Token(当前: ${existingToken.slice(0, 8)}...${existingToken.slice(-4)},直接回车保持不变): `
673
- : 'Auth Token(留空跳过): ';
674
- const readline2 = await import('readline');
675
- const rl2 = readline2.createInterface({ input: process.stdin, output: process.stdout });
676
- const tokenInput = await new Promise(resolve => rl2.question(tokenPrompt, a => { rl2.close(); resolve(a.trim()); }));
677
- const finalToken = tokenInput || existingToken || '';
678
- if (finalToken)
679
- setAuthToken(finalToken);
680
- }
681
- ensureGlobalConfigs('channel-only');
682
- console.log('[setup] Channel MCP 配置完成!请重启 Claude Code 以加载配置');
683
- }
684
- else {
685
- // 交互式:1/2 模式选择
686
- console.log('\n请选择安装模式:\n');
687
- console.log(' 1. 本地安装(完整功能:HTTP + Channel MCP)');
688
- console.log(' 2. 远程服务器(连接远程 HTTP MCP)\n');
689
- const readline = await import('readline');
690
- const modeChoice = await new Promise((resolve) => {
691
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
692
- rl.question('请选择 (1/2,默认 1): ', a => { rl.close(); resolve(a.trim() || '1'); });
693
- });
694
- if (modeChoice === '2') {
695
- await runRemoteInstallWizard();
696
- }
697
- else {
698
- await runConfigWizard();
699
- ensureGlobalConfigs('full');
700
- startMcpServerBackground();
701
- }
702
- }
703
- process.exit(0);
704
- }
705
- // --channel:启动 Channel MCP 代理(stdio)
706
- // 注意:必须在 --debug 之前检查,否则 --channel --debug 会先触发 HTTP Server
707
- // --setup --channel 已在上方处理,这里不拦截
708
- if (args.includes('--channel') && !args.includes('--setup')) {
709
- // 检查 HTTP MCP 的 debug 标记文件
710
- const debugFile = path.join(os.homedir(), '.wecom-aibot-mcp', 'debug');
711
- const isDebug = fs.existsSync(debugFile) || args.includes('--debug');
712
- if (isDebug) {
713
- console.log('[channel] Debug 模式:日志输出到 stderr(跟随 HTTP MCP debug)');
714
- if (!fs.existsSync(debugFile)) {
715
- fs.writeFileSync(debugFile, 'true');
716
- }
717
- }
718
- // 配置统一日志输出到文件(每个 channel-server 进程都写同一份 channel.log,多进程并发追加)
719
- logger.setLogFile(path.join(os.homedir(), '.wecom-aibot-mcp', 'channel.log'));
720
- if (isDebug)
721
- logger.setDebug(true);
722
- console.log('[channel] Starting Channel MCP Proxy...');
723
- const { startChannelServer } = await import('./channel-server.js');
64
+ if (args.includes('--channel')) {
65
+ // Channel MCP 代理模式(由 Claude Code 通过 stdio 启动)
66
+ logger.setLogFile(path.join(CONFIG_DIR, 'channel.log'));
724
67
  await startChannelServer();
725
- // Channel MCP 退出时不删除 debug 文件(由 HTTP MCP 管理)
726
- return; // 保持运行,不 exit
727
- }
728
- // --debug:前台启动,日志直接输出到终端
729
- if (args.includes('--debug')) {
730
- console.log('[mcp] Debug 模式:前台运行,Ctrl+C 退出');
731
- await startMcpServerForeground(true);
732
68
  return;
733
69
  }
734
- // --http-only:仅启动 HTTP Server(远程部署场景)
735
- if (args.includes('--http-only') && !args.includes('--start')) {
736
- console.log('[mcp] HTTP-only 模式:仅启动 HTTP Server');
737
- console.log('[mcp] 不写入 MCP 配置(远程部署场景)');
738
- console.log('[mcp] 使用 --start 启动服务');
70
+ if (args.includes('--install')) {
71
+ await runRemoteInstallWizard();
739
72
  process.exit(0);
740
73
  }
741
- // --channel-only:仅配置 Channel MCP(本地连接远程 HTTP Server)
742
- if (args.includes('--channel-only')) {
743
- const mcpUrl = process.env.MCP_URL;
744
- if (!mcpUrl) {
745
- console.log('[mcp] ❌ Channel-only 模式需要指定远程 HTTP MCP 地址');
746
- console.log('[mcp] 请设置环境变量 MCP_URL:');
747
- console.log('[mcp] MCP_URL=http://远程IP:18963 npx @vrs-soft/wecom-aibot-mcp --channel-only');
748
- process.exit(1);
749
- }
750
- console.log(`[mcp] Channel-only 模式:Channel MCP 已配置`);
751
- console.log(`[mcp] 连接地址: ${mcpUrl}`);
752
- console.log('[mcp] 请确保远程 HTTP Server 已启动');
753
- console.log('[mcp] 启动 Channel: npx @vrs-soft/wecom-aibot-mcp --channel');
74
+ if (args.includes('--uninstall')) {
75
+ uninstall();
754
76
  process.exit(0);
755
77
  }
756
- // --start:后台启动
757
- if (args.includes('--start')) {
758
- startMcpServerBackground();
759
- process.exit(0);
78
+ // 无参数:检查是否已有配置,否则引导安装
79
+ if (!process.stdin.isTTY) {
80
+ // 非交互式(stdio MCP 模式):不应出现这种情况,但安全起见给出提示
81
+ logger.error('[mcp] 请通过 --channel 参数启动 Channel MCP 代理');
82
+ process.exit(1);
760
83
  }
761
- const reconfig = args.includes('--config');
762
- const isInteractive = process.stdin.isTTY; // 是否为用户交互模式
763
- logger.log('');
764
- logger.log(' ╔════════════════════════════════════════════════════════╗');
765
- logger.log(` ║ 企业微信智能机器人 MCP 服务 v${VERSION} ║`);
766
- logger.log(' ║ Claude Code 审批通道 ║');
767
- logger.log(' ╚════════════════════════════════════════════════════════╝');
768
- logger.log('');
769
- // 加载统计并清理旧日志(保留 1 小时)
770
- loadStats();
771
- cleanupOldLogs(1 / 24);
772
- // 获取或初始化配置
773
- let config;
774
- let ranWizard = false; // 是否运行了配置向导
775
- let instanceName = 'wecom-aibot';
776
- if (reconfig) {
777
- console.log('[config] 重新配置模式\n');
778
- const result = await runConfigWizard();
779
- config = result.config;
780
- instanceName = result.instanceName;
781
- ranWizard = true;
84
+ // TTY 模式:直接进入安装向导
85
+ const { mode } = getInstalledMode();
86
+ if (mode) {
87
+ console.log(`\n已配置(模式: ${mode})。重新配置请运行 --install,卸载请运行 --uninstall\n`);
782
88
  }
783
89
  else {
784
- // 检查是否已有配置
785
- const savedConfig = loadConfig();
786
- if (savedConfig && savedConfig.botId && savedConfig.secret && savedConfig.targetUserId) {
787
- config = savedConfig;
788
- }
789
- else if (isInteractive) {
790
- // TTY 模式下没有配置:提示使用 --setup,不再隐式弹向导
791
- console.log('[config] 未找到机器人配置。');
792
- console.log('[config] 请运行: npx @vrs-soft/wecom-aibot-mcp --setup');
793
- process.exit(1);
794
- }
795
- else {
796
- // 非 TTY 模式(MCP HTTP),必须有配置
797
- logger.error('[config] 未找到配置,且当前为非交互模式。');
798
- logger.error('[config] 请在终端运行: npx @vrs-soft/wecom-aibot-mcp --config');
799
- process.exit(1);
800
- }
801
- }
802
- // 确保 hook 已安装(幂等,每次启动检查)
803
- ensureHookInstalled();
804
- // 配置向导模式:验证连接并识别用户 ID
805
- if (isInteractive && (ranWizard || reconfig)) {
806
- console.log('[mcp] 验证机器人连接...');
807
- // 临时建立连接验证凭证
808
- const tempClient = initClient(config.botId, config.secret, config.targetUserId || 'placeholder', 'temp-validation');
809
- const connected = await waitForConnection(tempClient, 10000);
810
- if (!connected) {
811
- console.log('[mcp] ❌ 连接失败,请检查:');
812
- console.log(' 1. Bot ID 和 Secret 是否正确');
813
- console.log(' 2. 新建机器人需等待约 2 分钟同步');
814
- console.log(' 3. 是否已完成授权(机器人详情 → 可使用权限 → 授权)');
815
- console.log('\n修复后重新运行: npx @vrs-soft/wecom-aibot-mcp --config');
816
- tempClient.disconnect();
817
- process.exit(1);
818
- }
819
- // 连接成功
820
- logger.log('\n[mcp] ✅ 机器人凭证验证成功!');
821
- // 保存配置(使用原用户 ID 或等待识别)
822
- if (!config.targetUserId || config.targetUserId === 'placeholder' || config.targetUserId === '') {
823
- // 新机器人,需要识别用户 ID
824
- const userId = await detectUserIdFromMessage(tempClient, 180);
825
- if (!userId) {
826
- logger.log('\n[mcp] 未能在规定时间内识别用户 ID');
827
- console.log('[mcp] 请重新运行配置:npx @vrs-soft/wecom-aibot-mcp --config');
828
- tempClient.disconnect();
829
- process.exit(1);
830
- }
831
- config.targetUserId = userId;
832
- }
833
- // 保存最终配置
834
- saveConfig(config, instanceName);
835
- logger.log('\n[mcp] ✅ 配置完成!');
836
- logger.log(`[mcp] 用户 ID: ${config.targetUserId}`);
837
- // 配置完成后断开连接
838
- tempClient.disconnect();
839
- // 首次安装后自动后台启动服务
840
- logger.log('\n[mcp] 正在后台启动 MCP Server...');
841
- startMcpServerBackground();
842
- console.log('[mcp] 请重启 Claude Code 以加载 MCP 服务\n');
843
- process.exit(0);
90
+ await runRemoteInstallWizard();
844
91
  }
845
- // 已有配置,显示状态并提示启动命令
846
- showStatus();
847
- logger.log('\n[mcp] 使用 --start 启动服务,--stop 停止服务');
848
- console.log('[mcp] 命令: npx @vrs-soft/wecom-aibot-mcp --start\n');
849
92
  }
850
93
  main().catch((err) => {
851
- logger.error('[mcp] 启动失败:', err);
94
+ logger.error('[mcp] Fatal error:', err);
852
95
  process.exit(1);
853
96
  });