@vrs-soft/wecom-aibot-mcp 2.3.3 → 2.4.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/README.md CHANGED
@@ -4,34 +4,62 @@
4
4
 
5
5
  企业微信智能机器人 MCP 服务 - 让 Claude Code 通过微信远程审批和交互。
6
6
 
7
- ## 功能
8
-
7
+ **核心功能**:
9
8
  - 远程审批敏感操作(Bash/Write/Edit),微信卡片一键通过/拒绝
10
9
  - 离开电脑后通过微信下达任务,实时接收进度通知
11
- - 支持 Channel 模式(SSE 推送唤醒)和 HTTP 模式(心跳轮询)
12
- - 支持群聊 @机器人,自动回复到对应会话
13
- - 支持多机器人、多用户
10
+ - 支持群聊 @机器人,多机器人、多用户并发
11
+ - 代理企业微信文档 MCP,支持文档和智能表格操作
12
+
13
+ ---
14
+
15
+ ## 前置条件
16
+
17
+ 企业微信管理后台创建智能机器人,连接方式选「使用长连接」,记录 **Bot ID** 和 **Secret**。
18
+
19
+ ---
14
20
 
15
21
  ## 安装
16
22
 
17
23
  ```bash
18
- npx @vrs-soft/wecom-aibot-mcp
24
+ npx @vrs-soft/wecom-aibot-mcp --setup
19
25
  ```
20
26
 
21
- 首次运行进入配置向导,完成后自动启动服务并写入 Claude Code MCP 配置。
27
+ 根据部署角色选择参数:
22
28
 
23
- **前置条件**:企业微信管理后台创建智能机器人,连接方式选「使用长连接」,记录 Bot ID Secret。
29
+ | 命令 | 角色 | 说明 |
30
+ |------|------|------|
31
+ | `--setup` | 交互式 | 询问本地 / 远程,自动引导 |
32
+ | `--setup --server` | 服务器端 | 配置机器人 + Token,不写本机 MCP 配置 |
33
+ | `--setup --channel` | Channel 客户端 | 连接远程 Server,写入 Channel MCP |
34
+ | `--setup --server --channel` | 本地完整 | HTTP + Channel 全安装 |
24
35
 
25
- ## 启动服务
36
+ **Server 端安装后启动**:
26
37
 
27
38
  ```bash
28
- # 后台启动(常用)
29
- npx @vrs-soft/wecom-aibot-mcp --start
39
+ npx @vrs-soft/wecom-aibot-mcp --http-only --start
40
+ ```
41
+
42
+ **后台启动 / 停止(本地或 Server 端)**:
30
43
 
31
- # Channel 模式(需 claude.ai 直连账号)
32
- claude --dangerously-load-development-channels server:wecom-aibot-channel
44
+ ```bash
45
+ npx @vrs-soft/wecom-aibot-mcp --start # 后台启动
46
+ npx @vrs-soft/wecom-aibot-mcp --stop # 停止
33
47
  ```
34
48
 
49
+ ---
50
+
51
+ ## 运行模式对比
52
+
53
+ | | Channel 模式 | HTTP 模式 |
54
+ |-|-------------|----------|
55
+ | 消息接收 | SSE 自动推送唤醒 | `/loop` 心跳轮询 |
56
+ | 响应延迟 | 即时 | ≤1 分钟 |
57
+ | 账号要求 | claude.ai 直连 | 任意(含 API 中转)|
58
+
59
+ 使用微信模式时告诉 Claude「**现在开始通过微信联系**」,会自动触发 `headless-mode` skill。
60
+
61
+ ---
62
+
35
63
  ## 常用命令
36
64
 
37
65
  | 命令 | 说明 |
@@ -40,60 +68,37 @@ claude --dangerously-load-development-channels server:wecom-aibot-channel
40
68
  | `--status` | 查看服务状态和机器人列表 |
41
69
  | `--config` | 修改默认机器人配置 |
42
70
  | `--add / --delete` | 添加/删除机器人 |
71
+ | `--set-token [token]` | 设置 Auth Token(远程部署用) |
72
+ | `--set-token --clear` | 清除 Auth Token |
43
73
  | `--debug` | 前台启动,输出调试日志 |
74
+ | `--http-only` | 仅启动 HTTP MCP Server(服务器端用) |
75
+ | `--channel-only` | 仅配置 Channel MCP(需 `MCP_URL` 环境变量) |
44
76
  | `--clean-cache` | 清空 CC 注册表缓存 |
45
77
  | `--upgrade` | 强制升级全局配置 |
46
78
  | `--uninstall` | 完全卸载 |
47
79
 
48
- ## 运行模式
49
-
50
- | | Channel 模式 | HTTP 模式 |
51
- |-|-------------|----------|
52
- | 消息接收 | SSE 自动推送唤醒 | `/loop` 心跳轮询 |
53
- | 响应延迟 | 即时 | ≤1 分钟 |
54
- | 账号要求 | claude.ai 直连 | 任意(含 API 中转)|
55
-
56
- 使用微信模式时告诉 Claude「现在开始通过微信联系」,会自动触发 `headless-mode` skill。
57
-
58
- ## 配置说明
59
-
60
- 机器人配置保存在 `~/.wecom-aibot-mcp/`,支持多个机器人并发:
61
-
62
- ```bash
63
- npx @vrs-soft/wecom-aibot-mcp --add # 添加机器人
64
- npx @vrs-soft/wecom-aibot-mcp --status # 查看占用情况
65
- ```
66
-
67
80
  超时自动审批(默认 10 分钟):在机器人配置中设置 `"autoApproveTimeout": 600`。
68
81
 
82
+ ---
83
+
69
84
  ## 故障排查
70
85
 
71
86
  ```bash
72
- # 检查服务
87
+ # 检查服务是否运行
73
88
  curl http://127.0.0.1:18963/health
74
89
 
75
90
  # Channel 不可用("Channels are not currently available")
76
91
  # → 使用 API Key 或中转服务,改用 HTTP 模式
77
92
 
78
93
  # 端口占用
79
- lsof -i :18963 | grep LISTEN # 找到 PID
94
+ lsof -i :18963 | grep LISTEN
80
95
  kill <PID>
81
96
 
82
- # 清理断线残留
97
+ # 清理断线残留的 ccId 注册
83
98
  npx @vrs-soft/wecom-aibot-mcp --clean-cache
84
99
  ```
85
100
 
86
- ## 拆分部署
87
-
88
- HTTP MCP 跑在远程服务器,Channel 代理跑在本地:
89
-
90
- ```bash
91
- # 远程服务器
92
- npx @vrs-soft/wecom-aibot-mcp --http-only --start
93
-
94
- # 本地
95
- MCP_URL=http://远程IP:18963 npx @vrs-soft/wecom-aibot-mcp --channel-only
96
- ```
101
+ ---
97
102
 
98
103
  ## License
99
104
 
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, } from './config-wizard.js';
16
+ import { runConfigWizard, loadConfig, saveConfig, deleteRobotConfigInteractive, uninstall, addMcpConfig, detectUserIdFromMessage, ensureHookInstalled, listAllRobots, ensureGlobalConfigs, getAuthToken, setAuthToken, 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';
@@ -22,7 +22,6 @@ import { getAllConnectionStates } from './connection-manager.js';
22
22
  import { loadStats, cleanupOldLogs } from './connection-log.js';
23
23
  import { startKeepaliveMonitor, stopKeepaliveMonitor } from './keepalive-monitor.js';
24
24
  import { logger } from './logger.js';
25
- const VERSION = '2.0.0';
26
25
  const PID_FILE = path.join(os.homedir(), '.wecom-aibot-mcp', 'server.pid');
27
26
  function showHelp() {
28
27
  console.log(`
@@ -37,6 +36,10 @@ function showHelp() {
37
36
  选项:
38
37
  --help, -h 显示帮助信息
39
38
  --version, -v 显示版本号
39
+ --setup 安装向导(交互式,询问本地 / 远程)
40
+ --setup --server 服务器端安装(配置机器人 + Token)
41
+ --setup --channel Channel 客户端安装(写入 Channel MCP)
42
+ --setup --server --channel 本地完整安装(HTTP + Channel)
40
43
  --upgrade 强制升级全局配置(覆盖 MCP 配置、权限、skill)
41
44
  --reinstall 重新安装全局配置(删除后重新写入,保留机器人配置)
42
45
  --start 启动 MCP Server(后台服务模式)
@@ -52,19 +55,17 @@ function showHelp() {
52
55
  --list 列出所有已配置的机器人及其占用状态
53
56
  --delete [名称] 删除指定的机器人配置(保留 MCP 配置)
54
57
  --uninstall 卸载并删除所有配置(包括 MCP 配置、hook、skill)
58
+ --set-token [token] 设置/清除 Auth Token(远程部署用,--set-token --clear 清除)
55
59
  --clean-cache 清空 CC 注册表缓存(清理异常断线残留的 ccId)
56
60
 
57
61
  使用流程:
58
- 1. 首次安装: npx @vrs-soft/wecom-aibot-mcp
59
- (进入配置向导,完成后自动后台启动服务)
62
+ 1. 安装: npx @vrs-soft/wecom-aibot-mcp --setup
63
+ (根据角色选择参数:--server / --channel / 两者都传 / 不传交互选择)
60
64
 
61
- 2. 已有配置: npx @vrs-soft/wecom-aibot-mcp
62
- (显示状态,提示使用 --start 启动)
63
-
64
- 3. 启动服务: npx @vrs-soft/wecom-aibot-mcp --start
65
+ 2. 启动服务: npx @vrs-soft/wecom-aibot-mcp --start
65
66
  (后台启动 MCP HTTP Server)
66
67
 
67
- 4. 停止服务: npx @vrs-soft/wecom-aibot-mcp --stop
68
+ 3. 停止服务: npx @vrs-soft/wecom-aibot-mcp --stop
68
69
 
69
70
  拆分部署(远程 HTTP + 本地 Channel):
70
71
 
@@ -104,9 +105,21 @@ function showVersion() {
104
105
  function showStatus() {
105
106
  const allRobots = listAllRobots();
106
107
  const connections = getAllConnectionStates();
108
+ const authToken = getAuthToken();
107
109
  // 检查服务是否运行
108
110
  const serverRunning = isServerRunning();
109
- console.log(`\n服务状态: ${serverRunning ? '✅ 运行中' : '❌ 未启动'}\n`);
111
+ console.log(`\n服务状态: ${serverRunning ? '✅ 运行中' : '❌ 未启动'}`);
112
+ // 显示 Auth Token 状态(带部分 token 显示)
113
+ if (authToken) {
114
+ const maskedToken = authToken.length > 12
115
+ ? `${authToken.slice(0, 8)}...${authToken.slice(-4)}`
116
+ : `${authToken.slice(0, 4)}...`;
117
+ console.log(`Auth Token: ✅ 已配置 (${maskedToken})`);
118
+ }
119
+ else {
120
+ console.log(`Auth Token: (未配置,本地部署无需 token)`);
121
+ }
122
+ console.log('');
110
123
  if (allRobots.length === 0) {
111
124
  console.log('尚未配置机器人,请运行 npx @vrs-soft/wecom-aibot-mcp 启动配置向导');
112
125
  return;
@@ -122,7 +135,8 @@ function showStatus() {
122
135
  for (const robot of allRobots) {
123
136
  const usage = robotUsage.get(robot.name);
124
137
  const statusTag = usage ? ` [使用中]` : '';
125
- console.log(` Bot名称: ${robot.name}${statusTag}`);
138
+ const docTag = robot.doc_mcp_url ? ' [文档✅]' : '';
139
+ console.log(` Bot名称: ${robot.name}${statusTag}${docTag}`);
126
140
  console.log(` Bot ID: ${robot.botId}`);
127
141
  console.log(` 目标用户:${robot.targetUserId}`);
128
142
  if (usage) {
@@ -297,9 +311,9 @@ async function main() {
297
311
  // 确定安装模式
298
312
  const installMode = args.includes('--http-only') ? 'http-only' :
299
313
  args.includes('--channel-only') ? 'channel-only' : 'full';
300
- // --reinstall 命令需要先删除再安装,跳过开头的 ensureGlobalConfigs
301
- // --http-only 模式不需要写 MCP 配置
302
- if (!args.includes('--reinstall') && !args.includes('--http-only')) {
314
+ // --reinstall / --http-only / --setup 命令跳过顶部 ensureGlobalConfigs
315
+ // (--setup 自己在向导完成后调用)
316
+ if (!args.includes('--reinstall') && !args.includes('--http-only') && !args.includes('--setup')) {
303
317
  // 强制覆盖所有全局配置(不依赖智能体)
304
318
  ensureGlobalConfigs(installMode);
305
319
  }
@@ -325,7 +339,7 @@ async function main() {
325
339
  // --reinstall 命令:删除所有全局配置(保留机器人配置)后重新安装
326
340
  if (args.includes('--reinstall')) {
327
341
  logger.log('\n[mcp] 重新安装全局配置...');
328
- console.log('[mcp] 保留所有机器人配置: ~/.wecom-aibot-mcp/config.json 和 robot-*.json');
342
+ console.log('[mcp] 保留所有机器人配置: ~/.wecom-aibot-mcp/robot-*.json');
329
343
  const CLAUDE_CONFIG_FILE = path.join(os.homedir(), '.claude.json');
330
344
  const CLAUDE_SETTINGS_FILE = path.join(os.homedir(), '.claude', 'settings.local.json');
331
345
  const VERSION_FILE = path.join(os.homedir(), '.wecom-aibot-mcp', 'version.json');
@@ -413,6 +427,48 @@ async function main() {
413
427
  uninstall();
414
428
  process.exit(0);
415
429
  }
430
+ // --set-token 命令:设置/清除 Auth Token
431
+ if (args.includes('--set-token')) {
432
+ const tokenIndex = args.indexOf('--set-token');
433
+ const clearToken = args.includes('--clear');
434
+ if (clearToken) {
435
+ setAuthToken(undefined);
436
+ updateMcpAuthHeaders(undefined);
437
+ console.log('[mcp] ✅ Auth Token 已清除(服务端 + 客户端 MCP 配置)');
438
+ process.exit(0);
439
+ }
440
+ // 检查下一个参数是否是 token(不是另一个 --flag)
441
+ const nextArg = args[tokenIndex + 1];
442
+ const token = (nextArg && !nextArg.startsWith('--')) ? nextArg : undefined;
443
+ if (!token) {
444
+ // 交互式输入 token
445
+ const readline = await import('readline');
446
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
447
+ const input = await new Promise((resolve) => {
448
+ rl.question('请输入 Auth Token(留空取消): ', (answer) => {
449
+ rl.close();
450
+ resolve(answer.trim());
451
+ });
452
+ });
453
+ if (!input) {
454
+ console.log('[mcp] 已取消');
455
+ process.exit(0);
456
+ }
457
+ setAuthToken(input);
458
+ updateMcpAuthHeaders(input);
459
+ console.log('[mcp] ✅ Auth Token 已设置');
460
+ console.log(`[mcp] 服务端: ~/.wecom-aibot-mcp/server.json`);
461
+ console.log(`[mcp] 客户端: ~/.claude.json MCP headers 已同步`);
462
+ console.log(`[mcp] Token: ${input.slice(0, 8)}...${input.slice(-4)}`);
463
+ }
464
+ else {
465
+ setAuthToken(token);
466
+ updateMcpAuthHeaders(token);
467
+ console.log('[mcp] ✅ Auth Token 已设置');
468
+ console.log(`[mcp] Token: ${token.slice(0, 8)}...${token.slice(-4)}`);
469
+ }
470
+ process.exit(0);
471
+ }
416
472
  if (args.includes('--add')) {
417
473
  await addMcpConfig();
418
474
  process.exit(0);
@@ -429,9 +485,87 @@ async function main() {
429
485
  await startMcpServerForeground();
430
486
  return; // 保持运行,不 exit
431
487
  }
488
+ // --setup:统一安装向导
489
+ // --setup → 交互式(询问本地 / 远程)
490
+ // --setup --server → 服务器端(机器人配置 + Token)
491
+ // --setup --channel → Channel 客户端(写入 Channel MCP)
492
+ // --setup --server --channel → 本地完整安装(HTTP + Channel)
493
+ if (args.includes('--setup')) {
494
+ const wantServer = args.includes('--server');
495
+ const wantChannel = args.includes('--channel');
496
+ if (wantServer && wantChannel) {
497
+ // 本地完整安装
498
+ console.log('\n[setup] 本地完整安装模式\n');
499
+ const savedConfig = loadConfig();
500
+ if (!savedConfig?.botId)
501
+ await runConfigWizard();
502
+ ensureGlobalConfigs('full');
503
+ startMcpServerBackground();
504
+ console.log('[setup] 安装完成!请重启 Claude Code 以加载配置');
505
+ }
506
+ else if (wantServer) {
507
+ // 服务器端
508
+ console.log('\n[setup] Server 安装模式\n');
509
+ const savedConfig = loadConfig();
510
+ if (!savedConfig?.botId)
511
+ await runConfigWizard();
512
+ const readline = await import('readline');
513
+ 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()); }));
515
+ if (token)
516
+ setAuthToken(token);
517
+ console.log('\n[setup] Server 配置完成!');
518
+ console.log(' 启动: npx @vrs-soft/wecom-aibot-mcp --http-only --start');
519
+ }
520
+ else if (wantChannel) {
521
+ // Channel 客户端
522
+ console.log('\n[setup] Channel Client 安装模式\n');
523
+ let mcpUrl = process.env.MCP_URL;
524
+ 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;
533
+ }
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);
540
+ }
541
+ ensureGlobalConfigs('channel-only');
542
+ console.log('[setup] Channel MCP 配置完成!请重启 Claude Code 以加载配置');
543
+ }
544
+ else {
545
+ // 交互式:1/2 模式选择
546
+ console.log('\n请选择安装模式:\n');
547
+ console.log(' 1. 本地安装(完整功能:HTTP + Channel MCP)');
548
+ console.log(' 2. 远程服务器(连接远程 HTTP MCP)\n');
549
+ const readline = await import('readline');
550
+ const modeChoice = await new Promise((resolve) => {
551
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
552
+ rl.question('请选择 (1/2,默认 1): ', a => { rl.close(); resolve(a.trim() || '1'); });
553
+ });
554
+ if (modeChoice === '2') {
555
+ await runRemoteInstallWizard();
556
+ }
557
+ else {
558
+ await runConfigWizard();
559
+ ensureGlobalConfigs('full');
560
+ startMcpServerBackground();
561
+ }
562
+ }
563
+ process.exit(0);
564
+ }
432
565
  // --channel:启动 Channel MCP 代理(stdio)
433
566
  // 注意:必须在 --debug 之前检查,否则 --channel --debug 会先触发 HTTP Server
434
- if (args.includes('--channel')) {
567
+ // --setup --channel 已在上方处理,这里不拦截
568
+ if (args.includes('--channel') && !args.includes('--setup')) {
435
569
  // 检查 HTTP MCP 的 debug 标记文件
436
570
  const debugFile = path.join(os.homedir(), '.wecom-aibot-mcp', 'debug');
437
571
  const isDebug = fs.existsSync(debugFile) || args.includes('--debug');
@@ -509,12 +643,10 @@ async function main() {
509
643
  config = savedConfig;
510
644
  }
511
645
  else if (isInteractive) {
512
- // TTY 模式下没有配置,启动配置向导
513
- console.log('[config] 未找到配置,启动配置向导...\n');
514
- const result = await runConfigWizard();
515
- config = result.config;
516
- instanceName = result.instanceName;
517
- ranWizard = true;
646
+ // TTY 模式下没有配置:提示使用 --setup,不再隐式弹向导
647
+ console.log('[config] 未找到机器人配置。');
648
+ console.log('[config] 请运行: npx @vrs-soft/wecom-aibot-mcp --setup');
649
+ process.exit(1);
518
650
  }
519
651
  else {
520
652
  // 非 TTY 模式(MCP HTTP),必须有配置
@@ -15,7 +15,17 @@ import { z } from 'zod';
15
15
  import * as fs from 'fs';
16
16
  import * as path from 'path';
17
17
  import * as os from 'os';
18
+ import { VERSION } from './config-wizard.js';
18
19
  const MCP_URL = process.env.MCP_URL || 'http://127.0.0.1:18963';
20
+ const MCP_AUTH_TOKEN = process.env.MCP_AUTH_TOKEN;
21
+ // 构建带 auth 的 fetch headers
22
+ function getAuthHeaders() {
23
+ const headers = {};
24
+ if (MCP_AUTH_TOKEN) {
25
+ headers['Authorization'] = `Bearer ${MCP_AUTH_TOKEN}`;
26
+ }
27
+ return headers;
28
+ }
19
29
  // Channel 日志文件
20
30
  const CHANNEL_LOG_FILE = path.join(os.homedir(), '.wecom-aibot-mcp', 'channel.log');
21
31
  /**
@@ -53,6 +63,7 @@ async function initHttpSession() {
53
63
  headers: {
54
64
  'Content-Type': 'application/json',
55
65
  'Accept': 'application/json, text/event-stream',
66
+ ...getAuthHeaders(),
56
67
  },
57
68
  body: JSON.stringify({
58
69
  jsonrpc: '2.0',
@@ -110,6 +121,7 @@ async function forwardToHttpMcp(toolName, params) {
110
121
  'Content-Type': 'application/json',
111
122
  'Accept': 'application/json, text/event-stream',
112
123
  'mcp-session-id': sessionId,
124
+ ...getAuthHeaders(),
113
125
  },
114
126
  body: JSON.stringify({
115
127
  jsonrpc: '2.0',
@@ -185,6 +197,7 @@ function connectSSE(ccId) {
185
197
  'Accept': 'text/event-stream',
186
198
  'Cache-Control': 'no-cache',
187
199
  'Connection': 'keep-alive',
200
+ ...getAuthHeaders(),
188
201
  },
189
202
  }).then(async (res) => {
190
203
  if (!res.ok) {
@@ -459,7 +472,9 @@ function registerChannelTools(server) {
459
472
  // ============================================
460
473
  server.tool('get_skill', '获取 headless-mode skill 文件内容,用于写入本地项目目录。远程部署时 HTTP MCP 可能不在本地,skill 文件需要从此接口获取。', {}, async () => {
461
474
  // 直接请求 HTTP MCP 的 /skill 端点
462
- const res = await fetch(`${MCP_URL}/skill`);
475
+ const res = await fetch(`${MCP_URL}/skill`, {
476
+ headers: getAuthHeaders(),
477
+ });
463
478
  if (!res.ok) {
464
479
  return {
465
480
  content: [{
@@ -576,7 +591,7 @@ export async function startChannelServer() {
576
591
  // 创建 MCP Server
577
592
  mcpServer = new McpServer({
578
593
  name: 'wecom-aibot-channel',
579
- version: '2.0.0',
594
+ version: VERSION,
580
595
  }, {
581
596
  capabilities: {
582
597
  // 必须声明 experimental['claude/channel'],Claude Code 才会注册 notification listener
@@ -6,7 +6,11 @@ export interface WecomConfig {
6
6
  nameTag?: string;
7
7
  doc_mcp_url?: string;
8
8
  }
9
+ export declare const VERSION: string;
9
10
  export declare function loadConfig(): WecomConfig | null;
11
+ export declare function getAuthToken(): string | undefined;
12
+ export declare function setAuthToken(token: string | undefined): boolean;
13
+ export declare function updateMcpAuthHeaders(token?: string): void;
10
14
  export declare function listAllMcpInstances(): Array<{
11
15
  name: string;
12
16
  config: WecomConfig;
@@ -31,10 +35,14 @@ export declare function getDocMcpUrl(robotName?: string): {
31
35
  error?: string;
32
36
  };
33
37
  export declare function ensureHookInstalled(): void;
34
- export declare function ensureGlobalConfigs(mode?: 'full' | 'http-only' | 'channel-only'): {
38
+ export declare function ensureGlobalConfigs(mode?: 'full' | 'http-only' | 'channel-only' | 'remote' | 'remote-channel', remoteOptions?: {
39
+ url: string;
40
+ token: string;
41
+ }): {
35
42
  upgraded: boolean;
36
43
  previousVersion?: string;
37
44
  };
45
+ export declare function runRemoteInstallWizard(): Promise<'remote' | 'remote-channel' | 'server' | null>;
38
46
  export declare function saveConfig(config: WecomConfig, instanceName?: string): boolean;
39
47
  /**
40
48
  * 安装 headless-mode skill 到项目目录
@@ -64,7 +72,7 @@ export declare function detectUserIdFromMessage(client: any, timeoutSeconds?: nu
64
72
  *
65
73
  * 优先级:
66
74
  * 1. 环境变量(WECOM_BOT_ID, WECOM_SECRET, WECOM_TARGET_USER)
67
- * 2. 保存的配置文件(~/.wecom-aibot-mcp/config.json)
75
+ * 2. 保存的配置文件(~/.wecom-aibot-mcp/robot-*.json)
68
76
  * 3. 运行配置向导
69
77
  */
70
78
  export declare function getOrInitConfig(): Promise<WecomConfig>;
@@ -4,7 +4,7 @@
4
4
  * 首次运行时引导用户配置 Bot ID、Secret 和默认目标用户
5
5
  *
6
6
  * 配置存储位置:
7
- * - 机器人配置:~/.wecom-aibot-mcp/config.json
7
+ * - 机器人配置:~/.wecom-aibot-mcp/robot-*.json
8
8
  * - MCP 配置:~/.claude.json (仅 URL)
9
9
  */
10
10
  import * as readline from 'readline';
@@ -14,8 +14,8 @@ import * as os from 'os';
14
14
  import { fileURLToPath } from 'url';
15
15
  import { logger } from './logger.js';
16
16
  const CONFIG_DIR = path.join(os.homedir(), '.wecom-aibot-mcp');
17
- const BOT_CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
18
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 等)
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');
@@ -23,8 +23,8 @@ const TASK_COMPLETED_HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'task-completed-ho
23
23
  // Skill 模板路径(包内)- 使用 fileURLToPath 确保跨平台兼容
24
24
  const __filename = fileURLToPath(import.meta.url);
25
25
  const __dirname = path.dirname(__filename);
26
- // 版本号(从 package.json 读取)
27
- const VERSION = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8')).version;
26
+ // 版本号(从 package.json 读取,全局共享)
27
+ export const VERSION = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8')).version;
28
28
  const SKILL_TEMPLATE_DIR = path.join(__dirname, '..', 'skills', 'headless-mode');
29
29
  const SKILL_TEMPLATE_FILE = path.join(SKILL_TEMPLATE_DIR, 'SKILL.md');
30
30
  // MCP 工具权限列表(需要预授权以避免 headless 模式阻断)
@@ -37,12 +37,14 @@ function ensureConfigDir() {
37
37
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
38
38
  }
39
39
  }
40
- // 从 ~/.wecom-aibot-mcp/config.json 读取已保存的配置
40
+ // 从 ~/.wecom-aibot-mcp/robot-*.json 读取第一个有效配置
41
41
  export function loadConfig() {
42
42
  try {
43
- // 从机器人配置文件读取
44
- if (fs.existsSync(BOT_CONFIG_FILE)) {
45
- const content = fs.readFileSync(BOT_CONFIG_FILE, 'utf-8');
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');
46
48
  const config = JSON.parse(content);
47
49
  if (config.botId && config.secret && config.targetUserId) {
48
50
  const result = {
@@ -63,6 +65,71 @@ export function loadConfig() {
63
65
  }
64
66
  return null;
65
67
  }
68
+ // 获取 HTTP Server 的 auth token(从 server.json 读取)
69
+ export function getAuthToken() {
70
+ if (!fs.existsSync(SERVER_CONFIG_FILE))
71
+ return undefined;
72
+ try {
73
+ const config = JSON.parse(fs.readFileSync(SERVER_CONFIG_FILE, 'utf-8'));
74
+ return config.authToken || undefined;
75
+ }
76
+ catch {
77
+ return undefined;
78
+ }
79
+ }
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
+ // 更新 ~/.claude.json 中 wecom-aibot MCP 配置的 auth headers
108
+ export function updateMcpAuthHeaders(token) {
109
+ if (!fs.existsSync(CLAUDE_CONFIG_FILE))
110
+ return;
111
+ try {
112
+ const content = fs.readFileSync(CLAUDE_CONFIG_FILE, 'utf-8');
113
+ const claudeConfig = JSON.parse(content);
114
+ if (!claudeConfig.mcpServers)
115
+ return;
116
+ // 更新所有 wecom-aibot 相关的 HTTP MCP 配置
117
+ for (const name of Object.keys(claudeConfig.mcpServers)) {
118
+ if (name.startsWith('wecom-aibot') && claudeConfig.mcpServers[name].type === 'http') {
119
+ if (token) {
120
+ claudeConfig.mcpServers[name].headers = { Authorization: `Bearer ${token}` };
121
+ }
122
+ else {
123
+ delete claudeConfig.mcpServers[name].headers;
124
+ }
125
+ }
126
+ }
127
+ fs.writeFileSync(CLAUDE_CONFIG_FILE, JSON.stringify(claudeConfig, null, 2));
128
+ }
129
+ catch {
130
+ // ignore
131
+ }
132
+ }
66
133
  // 获取所有 wecom-aibot 相关的 MCP 实例
67
134
  export function listAllMcpInstances() {
68
135
  // 现在只有一个主配置文件
@@ -139,18 +206,8 @@ export function deleteRobotConfig(robotName) {
139
206
  }
140
207
  // 查找机器人对应的配置文件
141
208
  let configFile = null;
142
- let isDefault = false;
143
- // 检查是否是默认机器人(config.json)
144
- if (fs.existsSync(BOT_CONFIG_FILE)) {
145
- const config = JSON.parse(fs.readFileSync(BOT_CONFIG_FILE, 'utf-8'));
146
- const name = config.nameTag || `机器人-${config.botId?.slice(0, 8) || 'unknown'}`;
147
- if (name === robotName) {
148
- configFile = BOT_CONFIG_FILE;
149
- isDefault = true;
150
- }
151
- }
152
- // 检查其他机器人配置文件
153
- if (!configFile && fs.existsSync(CONFIG_DIR)) {
209
+ // robot-*.json 中查找
210
+ if (fs.existsSync(CONFIG_DIR)) {
154
211
  const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
155
212
  for (const file of files) {
156
213
  const filePath = path.join(CONFIG_DIR, file);
@@ -166,30 +223,8 @@ export function deleteRobotConfig(robotName) {
166
223
  console.log(`[config] 未找到机器人 "${robotName}" 的配置文件`);
167
224
  return false;
168
225
  }
169
- // 如果是默认机器人,需要处理迁移
170
- if (isDefault) {
171
- // 查找其他机器人配置文件
172
- const otherRobotFiles = fs.existsSync(CONFIG_DIR)
173
- ? fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'))
174
- : [];
175
- if (otherRobotFiles.length > 0) {
176
- // 将第一个其他机器人提升为默认
177
- const newDefaultFile = path.join(CONFIG_DIR, otherRobotFiles[0]);
178
- const newDefaultConfig = JSON.parse(fs.readFileSync(newDefaultFile, 'utf-8'));
179
- fs.writeFileSync(BOT_CONFIG_FILE, JSON.stringify(newDefaultConfig, null, 2));
180
- fs.unlinkSync(newDefaultFile);
181
- console.log(`[config] 已将 "${newDefaultConfig.nameTag || otherRobotFiles[0]}" 提升为默认机器人`);
182
- }
183
- else {
184
- // 没有其他机器人,直接删除默认配置
185
- fs.unlinkSync(BOT_CONFIG_FILE);
186
- console.log('[config] 已删除最后一个机器人配置');
187
- }
188
- }
189
- else {
190
- // 不是默认机器人,直接删除
191
- fs.unlinkSync(configFile);
192
- }
226
+ // 直接删除
227
+ fs.unlinkSync(configFile);
193
228
  console.log(`[config] 已删除机器人: ${robotName}`);
194
229
  return true;
195
230
  }
@@ -302,7 +337,7 @@ export function uninstall() {
302
337
  logger.error('[config] 删除 headless 状态索引失败:', err);
303
338
  }
304
339
  }
305
- // 删除整个配置目录(包括 config.json、robot-*.json、hook 脚本、日志等)
340
+ // 删除整个配置目录(包括 robot-*.json、hook 脚本、日志等)
306
341
  // 使用 recursive: true 和 force: true 确保完全删除
307
342
  if (fs.existsSync(CONFIG_DIR)) {
308
343
  try {
@@ -672,18 +707,10 @@ function writeMcpServerConfig(config, instanceName) {
672
707
  console.log(`[config] 已更新机器人配置: ${existingConfigFile}`);
673
708
  }
674
709
  else {
675
- // 新配置:检查是否有默认配置文件
676
- if (fs.existsSync(BOT_CONFIG_FILE)) {
677
- // 有默认配置,创建新的 robot-*.json 文件
678
- const newConfigPath = path.join(CONFIG_DIR, `robot-${Date.now()}.json`);
679
- fs.writeFileSync(newConfigPath, JSON.stringify(botConfig, null, 2));
680
- console.log(`[config] 已添加新机器人配置: ${newConfigPath}`);
681
- }
682
- else {
683
- // 没有默认配置,写入 config.json
684
- fs.writeFileSync(BOT_CONFIG_FILE, JSON.stringify(botConfig, null, 2));
685
- console.log('[config] 已写入机器人配置 ~/.wecom-aibot-mcp/config.json');
686
- }
710
+ // 新机器人:统一使用 robot-*.json
711
+ const newConfigPath = path.join(CONFIG_DIR, `robot-${Date.now()}.json`);
712
+ fs.writeFileSync(newConfigPath, JSON.stringify(botConfig, null, 2));
713
+ console.log(`[config] 已添加新机器人配置: ${newConfigPath}`);
687
714
  }
688
715
  // 2. 写入 MCP 配置到 ~/.claude.json(仅 URL)
689
716
  let claudeConfig = {};
@@ -707,7 +734,7 @@ function writeMcpServerConfig(config, instanceName) {
707
734
  logger.error('[config] 写入配置失败:', err);
708
735
  console.log('[config] ⚠️ 请手动配置:');
709
736
  console.log('');
710
- console.log('~/.wecom-aibot-mcp/config.json:');
737
+ console.log('~/.wecom-aibot-mcp/robot-*.json:');
711
738
  console.log(JSON.stringify({
712
739
  botId: config.botId,
713
740
  secret: config.secret,
@@ -751,6 +778,9 @@ export async function addMcpConfig() {
751
778
  console.log('Secret 不能为空');
752
779
  secret = await question(rl, 'Secret: ');
753
780
  }
781
+ // 获取文档 MCP URL(可选)
782
+ console.log('');
783
+ const docMcpUrl = await question(rl, '文档 MCP URL(可选,企业微信管理后台获取,留空跳过): ');
754
784
  rl.close();
755
785
  // 检查是否已存在相同 botId 的配置
756
786
  const existingRobots = listAllRobots();
@@ -809,22 +839,14 @@ export async function addMcpConfig() {
809
839
  secret,
810
840
  targetUserId,
811
841
  nameTag: robotName,
842
+ ...(docMcpUrl ? { doc_mcp_url: docMcpUrl } : {}),
812
843
  };
813
844
  // 确保配置目录存在
814
845
  ensureConfigDir();
815
- // 如果是第一个机器人,保存为默认配置
816
- const defaultConfigPath = BOT_CONFIG_FILE;
846
+ // 统一使用 robot-*.json 格式
817
847
  const robotConfigPath = path.join(CONFIG_DIR, `robot-${Date.now()}.json`);
818
- if (!fs.existsSync(defaultConfigPath)) {
819
- // 第一个机器人作为默认
820
- fs.writeFileSync(defaultConfigPath, JSON.stringify(robotConfig, null, 2));
821
- console.log(`\n[config] ✅ 已设为默认机器人: ${robotName}`);
822
- }
823
- else {
824
- // 后续机器人保存为独立文件
825
- fs.writeFileSync(robotConfigPath, JSON.stringify(robotConfig, null, 2));
826
- console.log(`\n[config] ✅ 已添加新机器人: ${robotName}`);
827
- }
848
+ fs.writeFileSync(robotConfigPath, JSON.stringify(robotConfig, null, 2));
849
+ console.log(`\n[config] ✅ 已添加机器人: ${robotName}`);
828
850
  console.log(`[config] 用户 ID: ${targetUserId}`);
829
851
  // 列出所有机器人
830
852
  const robots = listAllRobots();
@@ -842,23 +864,7 @@ export async function addMcpConfig() {
842
864
  // 列出所有机器人配置
843
865
  export function listAllRobots() {
844
866
  const robots = [];
845
- // 主配置文件(config.json
846
- if (fs.existsSync(BOT_CONFIG_FILE)) {
847
- try {
848
- const config = JSON.parse(fs.readFileSync(BOT_CONFIG_FILE, 'utf-8'));
849
- const name = config.nameTag || `机器人-${config.botId?.slice(0, 8) || 'unknown'}`;
850
- robots.push({
851
- name,
852
- botId: config.botId,
853
- targetUserId: config.targetUserId,
854
- ...(config.doc_mcp_url ? { doc_mcp_url: config.doc_mcp_url } : {}),
855
- });
856
- }
857
- catch {
858
- // ignore
859
- }
860
- }
861
- // 其他机器人配置
867
+ // 所有机器人配置(统一 robot-*.json 格式)
862
868
  if (fs.existsSync(CONFIG_DIR)) {
863
869
  const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
864
870
  for (const file of files) {
@@ -958,7 +964,7 @@ export function ensureHookInstalled() {
958
964
  writeTaskCompletedHookScript();
959
965
  }
960
966
  // 确保所有全局配置已写入(强制覆盖,不依赖智能体)
961
- export function ensureGlobalConfigs(mode = 'full') {
967
+ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
962
968
  ensureConfigDir();
963
969
  // 读取已安装版本
964
970
  let previousVersion;
@@ -981,6 +987,64 @@ export function ensureGlobalConfigs(mode = 'full') {
981
987
  fs.writeFileSync(VERSION_FILE, JSON.stringify({ version: VERSION, installedAt: Date.now() }, null, 2));
982
988
  return { upgraded, previousVersion };
983
989
  }
990
+ // remote 模式:仅写入远程 HTTP MCP 配置(带 token headers),不装 Channel/Hook
991
+ if (mode === 'remote') {
992
+ if (!remoteOptions?.url || !remoteOptions?.token) {
993
+ console.log('[config] ❌ 远程模式需要提供 URL 和 Token');
994
+ return { upgraded: false, previousVersion };
995
+ }
996
+ let claudeConfig = {};
997
+ if (fs.existsSync(CLAUDE_CONFIG_FILE)) {
998
+ claudeConfig = JSON.parse(fs.readFileSync(CLAUDE_CONFIG_FILE, 'utf-8'));
999
+ }
1000
+ if (!claudeConfig.mcpServers)
1001
+ claudeConfig.mcpServers = {};
1002
+ claudeConfig.mcpServers['wecom-aibot'] = {
1003
+ type: 'http',
1004
+ url: remoteOptions.url,
1005
+ headers: { Authorization: `Bearer ${remoteOptions.token}` },
1006
+ };
1007
+ fs.writeFileSync(CLAUDE_CONFIG_FILE, JSON.stringify(claudeConfig, null, 2));
1008
+ console.log('[config] remote 模式:已写入远程 HTTP MCP 配置(带 Token)');
1009
+ fs.writeFileSync(VERSION_FILE, JSON.stringify({ version: VERSION, installedAt: Date.now() }, null, 2));
1010
+ return { upgraded, previousVersion };
1011
+ }
1012
+ // remote-channel 模式:写入远程 HTTP MCP(带 token)+ Channel MCP
1013
+ if (mode === 'remote-channel') {
1014
+ if (!remoteOptions?.url || !remoteOptions?.token) {
1015
+ console.log('[config] ❌ 远程模式需要提供 URL 和 Token');
1016
+ return { upgraded: false, previousVersion };
1017
+ }
1018
+ let claudeConfig = {};
1019
+ if (fs.existsSync(CLAUDE_CONFIG_FILE)) {
1020
+ claudeConfig = JSON.parse(fs.readFileSync(CLAUDE_CONFIG_FILE, 'utf-8'));
1021
+ }
1022
+ if (!claudeConfig.mcpServers)
1023
+ claudeConfig.mcpServers = {};
1024
+ // HTTP MCP 配置(带 token)
1025
+ claudeConfig.mcpServers['wecom-aibot'] = {
1026
+ type: 'http',
1027
+ url: remoteOptions.url,
1028
+ headers: { Authorization: `Bearer ${remoteOptions.token}` },
1029
+ };
1030
+ // Channel MCP 配置(带 MCP_URL + MCP_AUTH_TOKEN)
1031
+ const binPath = path.join(__dirname, 'bin.js');
1032
+ claudeConfig.mcpServers['wecom-aibot-channel'] = {
1033
+ command: 'node',
1034
+ args: [binPath, '--channel'],
1035
+ env: {
1036
+ MCP_URL: remoteOptions.url,
1037
+ MCP_AUTH_TOKEN: remoteOptions.token,
1038
+ },
1039
+ };
1040
+ fs.writeFileSync(CLAUDE_CONFIG_FILE, JSON.stringify(claudeConfig, null, 2));
1041
+ console.log('[config] remote-channel 模式:已写入 HTTP MCP + Channel MCP 配置(带 Token)');
1042
+ // Channel 模式需要权限配置
1043
+ writeMcpPermissions();
1044
+ console.log('[config] 已写入权限配置到 ~/.claude/settings.local.json');
1045
+ fs.writeFileSync(VERSION_FILE, JSON.stringify({ version: VERSION, installedAt: Date.now() }, null, 2));
1046
+ return { upgraded, previousVersion };
1047
+ }
984
1048
  // 1. 强制写入 MCP 配置到 ~/.claude.json
985
1049
  let claudeConfig = {};
986
1050
  if (fs.existsSync(CLAUDE_CONFIG_FILE)) {
@@ -997,11 +1061,17 @@ export function ensureGlobalConfigs(mode = 'full') {
997
1061
  console.log('[config] 请设置环境变量: MCP_URL=http://远程IP:18963');
998
1062
  return { upgraded: false, previousVersion };
999
1063
  }
1000
- // Channel MCP 配置:硬编码本地路径
1064
+ // Channel MCP 配置:使用当前模块路径
1065
+ const binPath = path.join(__dirname, 'bin.js');
1066
+ const channelEnv = { MCP_URL: mcpUrl };
1067
+ const authToken = getAuthToken();
1068
+ if (authToken) {
1069
+ channelEnv.MCP_AUTH_TOKEN = authToken;
1070
+ }
1001
1071
  claudeConfig.mcpServers['wecom-aibot-channel'] = {
1002
1072
  command: 'node',
1003
- args: ['/Volumes/Mac_Data/VScode/wecom-aibot-mcp/dist/bin.js', '--channel'],
1004
- env: { MCP_URL: mcpUrl },
1073
+ args: [binPath, '--channel'],
1074
+ env: channelEnv,
1005
1075
  };
1006
1076
  console.log(`[config] Channel-only 模式:Channel MCP 使用本地路径`);
1007
1077
  }
@@ -1011,10 +1081,11 @@ export function ensureGlobalConfigs(mode = 'full') {
1011
1081
  type: 'http',
1012
1082
  url: 'http://127.0.0.1:18963/mcp',
1013
1083
  };
1014
- // Channel MCP 配置:硬编码本地路径
1084
+ // Channel MCP 配置:使用当前模块路径
1085
+ const binPath = path.join(__dirname, 'bin.js');
1015
1086
  claudeConfig.mcpServers['wecom-aibot-channel'] = {
1016
1087
  command: 'node',
1017
- args: ['/Volumes/Mac_Data/VScode/wecom-aibot-mcp/dist/bin.js', '--channel'],
1088
+ args: [binPath, '--channel'],
1018
1089
  env: { MCP_URL: 'http://127.0.0.1:18963' },
1019
1090
  };
1020
1091
  console.log(`[config] full 模式:Channel MCP 使用本地路径`);
@@ -1029,7 +1100,69 @@ export function ensureGlobalConfigs(mode = 'full') {
1029
1100
  console.log(`[config] 已记录版本号: ${VERSION}`);
1030
1101
  return { upgraded, previousVersion };
1031
1102
  }
1032
- // 保存配置(直接写入 ~/.claude.json
1103
+ // 远程安装向导(交互式输入 URL + Token
1104
+ export async function runRemoteInstallWizard() {
1105
+ const rl = createRL();
1106
+ const CLAUDE_CONFIG_FILE = path.join(os.homedir(), '.claude.json');
1107
+ try {
1108
+ // 检测本机是否有 ~/.claude.json(判断是 Client 还是 Server)
1109
+ const hasClaudeConfig = fs.existsSync(CLAUDE_CONFIG_FILE);
1110
+ if (!hasClaudeConfig) {
1111
+ // Server 安装模式:本机无 ~/.claude.json,作为远程服务器
1112
+ console.log('\n检测到本机无 ~/.claude.json → Server 安装模式\n');
1113
+ console.log(' Server 端只需启动 HTTP MCP Server,不写入 MCP 配置');
1114
+ console.log(' Client 端在其他机器上安装\n');
1115
+ const confirm = await question(rl, '确认作为远程 Server 安装?(y/N): ');
1116
+ if (confirm.toLowerCase() !== 'y') {
1117
+ console.log('[config] 已取消');
1118
+ return null;
1119
+ }
1120
+ // Server 不写入 ~/.claude.json,只提示启动命令
1121
+ console.log('\n─────────────────────────────────────');
1122
+ console.log('Server 安装完成!');
1123
+ console.log(' 启动命令: npx @anthropic/wecom-aibot-mcp --http-only --start');
1124
+ console.log(' 或者: npm run start:http');
1125
+ console.log('─────────────────────────────────────\n');
1126
+ console.log('[config] Client 端请在其他机器运行安装程序连接本服务器\n');
1127
+ return 'server';
1128
+ }
1129
+ // Client 安装模式:本机有 ~/.claude.json,作为客户端
1130
+ console.log('\n检测到本机有 ~/.claude.json → Client 安装模式\n');
1131
+ console.log(' 请选择连接远程服务器的方式:\n');
1132
+ console.log(' 1. 仅 HTTP MCP(轮询模式)');
1133
+ console.log(' 2. HTTP MCP + Channel MCP(推荐,消息自动唤醒)\n');
1134
+ const choice = await question(rl, '请选择 (1/2): ');
1135
+ const mode = choice === '2' ? 'remote-channel' : 'remote';
1136
+ let serverUrl = await question(rl, '远程服务器地址(如 https://your-server:18963): ');
1137
+ while (!serverUrl) {
1138
+ console.log('服务器地址不能为空');
1139
+ serverUrl = await question(rl, '远程服务器地址: ');
1140
+ }
1141
+ // 标准化 URL(去掉尾部斜杠)
1142
+ serverUrl = serverUrl.replace(/\/+$/, '');
1143
+ let token = await question(rl, 'Auth Token(必填,远程服务器需配置相同 Token): ');
1144
+ while (!token) {
1145
+ console.log('Auth Token 不能为空');
1146
+ token = await question(rl, 'Auth Token: ');
1147
+ }
1148
+ // 写入配置
1149
+ ensureGlobalConfigs(mode, { url: serverUrl, token });
1150
+ console.log('\n─────────────────────────────────────');
1151
+ console.log('Client 配置完成!');
1152
+ console.log(` 模式: ${mode === 'remote-channel' ? 'HTTP + Channel' : '仅 HTTP'}`);
1153
+ console.log(` 服务器: ${serverUrl}`);
1154
+ console.log(` Auth Token: ${token.slice(0, 8)}...${token.slice(-4)}`);
1155
+ console.log('─────────────────────────────────────\n');
1156
+ if (mode === 'remote-channel') {
1157
+ console.log('Channel 模式优势:微信消息自动唤醒 agent,无需主动轮询');
1158
+ }
1159
+ console.log('[config] 请重启 Claude Code 以加载最新配置\n');
1160
+ return mode;
1161
+ }
1162
+ finally {
1163
+ rl.close();
1164
+ }
1165
+ }
1033
1166
  export function saveConfig(config, instanceName) {
1034
1167
  ensureConfigDir(); // 确保运行时文件目录存在
1035
1168
  // 写入 MCP Server 配置到 ~/.claude.json
@@ -1232,15 +1365,6 @@ export async function runConfigWizard() {
1232
1365
  }
1233
1366
  // 查找机器人配置文件路径(按名称)
1234
1367
  export function findRobotConfigFile(robotName) {
1235
- // 检查默认配置文件
1236
- if (fs.existsSync(BOT_CONFIG_FILE)) {
1237
- const config = JSON.parse(fs.readFileSync(BOT_CONFIG_FILE, 'utf-8'));
1238
- const name = config.nameTag || `机器人-${config.botId?.slice(0, 8) || 'unknown'}`;
1239
- if (name === robotName) {
1240
- return BOT_CONFIG_FILE;
1241
- }
1242
- }
1243
- // 检查其他机器人配置文件
1244
1368
  if (fs.existsSync(CONFIG_DIR)) {
1245
1369
  const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
1246
1370
  for (const file of files) {
@@ -1256,14 +1380,6 @@ export function findRobotConfigFile(robotName) {
1256
1380
  }
1257
1381
  // 查找机器人配置文件路径(按 botId)
1258
1382
  export function findRobotConfigFileByBotId(botId) {
1259
- // 检查默认配置文件
1260
- if (fs.existsSync(BOT_CONFIG_FILE)) {
1261
- const config = JSON.parse(fs.readFileSync(BOT_CONFIG_FILE, 'utf-8'));
1262
- if (config.botId === botId) {
1263
- return BOT_CONFIG_FILE;
1264
- }
1265
- }
1266
- // 检查其他机器人配置文件
1267
1383
  if (fs.existsSync(CONFIG_DIR)) {
1268
1384
  const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
1269
1385
  for (const file of files) {
@@ -1335,7 +1451,7 @@ export async function detectUserIdFromMessage(client, timeoutSeconds = 60) {
1335
1451
  *
1336
1452
  * 优先级:
1337
1453
  * 1. 环境变量(WECOM_BOT_ID, WECOM_SECRET, WECOM_TARGET_USER)
1338
- * 2. 保存的配置文件(~/.wecom-aibot-mcp/config.json)
1454
+ * 2. 保存的配置文件(~/.wecom-aibot-mcp/robot-*.json)
1339
1455
  * 3. 运行配置向导
1340
1456
  */
1341
1457
  export async function getOrInitConfig() {
@@ -29,8 +29,8 @@ function findRobotConfig(robotName) {
29
29
  const robot = robots.find(r => r.name === robotName || r.botId === robotName || r.name.includes(robotName));
30
30
  if (!robot)
31
31
  return null;
32
- // 搜索所有配置文件(config.json + robot-*.json)
33
- const allFiles = ['config.json', ...fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'))];
32
+ // 搜索所有机器人配置文件(robot-*.json)
33
+ const allFiles = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
34
34
  const files = allFiles.filter(f => fs.existsSync(path.join(CONFIG_DIR, f)));
35
35
  // 先按 botId 精确匹配找 secret
36
36
  for (const file of files) {
package/dist/daemon.js CHANGED
@@ -47,23 +47,7 @@ class ConnectionDaemon {
47
47
  }
48
48
  loadAllRobots() {
49
49
  const robots = [];
50
- // 主配置文件
51
- const mainConfigPath = path.join(CONFIG_DIR, 'config.json');
52
- if (fs.existsSync(mainConfigPath)) {
53
- try {
54
- const config = JSON.parse(fs.readFileSync(mainConfigPath, 'utf-8'));
55
- robots.push({
56
- name: config.nameTag || 'default',
57
- botId: config.botId,
58
- secret: config.secret,
59
- targetUserId: config.targetUserId,
60
- });
61
- }
62
- catch (err) {
63
- this.log(`加载主配置失败: ${err}`);
64
- }
65
- }
66
- // 机器人配置文件 (robot-*.json)
50
+ // 所有机器人配置(统一 robot-*.json 格式)
67
51
  const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
68
52
  for (const file of files) {
69
53
  try {
@@ -23,7 +23,7 @@ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
23
23
  import { registerTools } from './tools/index.js';
24
24
  import { getClient, getConnectionState, getAllConnectionStates, connectAllRobots } from './connection-manager.js';
25
25
  import { subscribeWecomMessage, getSubscriberCount } from './message-bus.js';
26
- import { listAllRobots } from './config-wizard.js';
26
+ import { listAllRobots, VERSION, getAuthToken } from './config-wizard.js';
27
27
  import { logger } from './logger.js';
28
28
  // ESM 兼容的 __dirname
29
29
  const __filename = fileURLToPath(import.meta.url);
@@ -185,7 +185,6 @@ export function getOnlineCcIds() {
185
185
  }
186
186
  // 使用 Map 存储多个待处理审批(按 taskId 索引)
187
187
  const pendingApprovals = new Map();
188
- const VERSION = '2.0.0';
189
188
  const transports = new Map();
190
189
  const sseClients = new Map(); // clientId -> SSEClient
191
190
  // 初始化 MCP Server(不再全局连接)
@@ -479,6 +478,16 @@ export async function startHttpServer(_server, port = HTTP_PORT) {
479
478
  return;
480
479
  }
481
480
  const url = req.url || '/';
481
+ // Auth token 校验(排除 /health)
482
+ const authToken = getAuthToken();
483
+ if (authToken && url !== '/health') {
484
+ const authHeader = req.headers['authorization'];
485
+ if (authHeader !== `Bearer ${authToken}`) {
486
+ res.writeHead(401, { 'Content-Type': 'application/json' });
487
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
488
+ return;
489
+ }
490
+ }
482
491
  // MCP endpoint - 每个客户端一个独立的 server 和 transport
483
492
  // POST /mcp: 初始化或调用工具
484
493
  // GET /mcp: 建立 SSE 流
@@ -20,7 +20,7 @@
20
20
  * - 从 Session 自动获取 robotName
21
21
  */
22
22
  import { z } from 'zod';
23
- import { listAllRobots, getDocMcpUrl, installSkill } from '../config-wizard.js';
23
+ import { listAllRobots, getDocMcpUrl, installSkill, VERSION } from '../config-wizard.js';
24
24
  import { callDocTool } from '../doc-proxy.js';
25
25
  import { connectRobot, disconnectRobot, getClient, getConnectionState, } from '../connection-manager.js';
26
26
  import { registerCcId, unregisterCcId, getRobotByCcId, getProjectDirByCcId, generateCcId, } from '../http-server.js';
@@ -250,7 +250,7 @@ npx @vrs-soft/wecom-aibot-mcp
250
250
  content: [{
251
251
  type: 'text',
252
252
  text: JSON.stringify({
253
- version: '2.0.0',
253
+ version: VERSION,
254
254
  requirements: {
255
255
  // 权限配置需求
256
256
  permissions: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vrs-soft/wecom-aibot-mcp",
3
- "version": "2.3.3",
3
+ "version": "2.4.0",
4
4
  "description": "企业微信智能机器人 MCP 服务 - Claude Code 审批通道",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",