@vrs-soft/wecom-aibot-mcp 2.3.3 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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** 以及 **DocURL**(文档url)。
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
package/dist/client.d.ts CHANGED
@@ -25,6 +25,12 @@ interface MessageRecord {
25
25
  chattype: 'single' | 'group';
26
26
  quoteContent?: string;
27
27
  }
28
+ interface PendingMessage {
29
+ type: 'text' | 'approval';
30
+ content: any;
31
+ targetUser?: string;
32
+ timestamp: number;
33
+ }
28
34
  declare class WecomClient extends EventEmitter {
29
35
  private wsClient;
30
36
  private approvals;
@@ -77,6 +83,10 @@ declare class WecomClient extends EventEmitter {
77
83
  * allow-once 消费后标记为已消费
78
84
  */
79
85
  consumeApproval(taskId: string): 'allow-once' | 'allow-always' | 'deny' | null;
86
+ /**
87
+ * 获取所有未解决的审批记录 Map(用于重连时迁移到新 client)
88
+ */
89
+ getUnresolvedApprovalMap(): Map<string, ApprovalRecord>;
80
90
  /**
81
91
  * 注入审批记录(MCP 重启恢复用)
82
92
  * 如果 taskId 已存在则跳过,避免覆盖用户已点击的结果
@@ -88,6 +98,15 @@ declare class WecomClient extends EventEmitter {
88
98
  cleanupMessages(maxAgeMs?: number): void;
89
99
  private flushPendingMessages;
90
100
  getPendingMessageCount(): number;
101
+ /**
102
+ * 取出所有待发送的审批消息并清空队列(用于 client 迁移)
103
+ * 仅迁移 approval 类型,text 通知类消息不需要迁移
104
+ */
105
+ takePendingApprovalMessages(): PendingMessage[];
106
+ /**
107
+ * 注入待发送消息(用于 client 迁移,新 client 连接后会自动重发)
108
+ */
109
+ injectPendingMessages(messages: PendingMessage[]): void;
91
110
  getReconnectStatus(): {
92
111
  wasReconnecting: boolean;
93
112
  attempt: number;
@@ -96,4 +115,4 @@ declare class WecomClient extends EventEmitter {
96
115
  }
97
116
  export declare function initClient(botId: string, secret: string, targetUserId: string, robotName: string): WecomClient;
98
117
  export declare function getClient(): WecomClient;
99
- export { WecomClient, ApprovalRecord, MessageRecord, MAX_PENDING_MESSAGES };
118
+ export { WecomClient, ApprovalRecord, MessageRecord, PendingMessage, MAX_PENDING_MESSAGES };
package/dist/client.js CHANGED
@@ -528,6 +528,18 @@ class WecomClient extends EventEmitter {
528
528
  }
529
529
  return approval.result;
530
530
  }
531
+ /**
532
+ * 获取所有未解决的审批记录 Map(用于重连时迁移到新 client)
533
+ */
534
+ getUnresolvedApprovalMap() {
535
+ const pending = new Map();
536
+ this.approvals.forEach((approval, taskId) => {
537
+ if (!approval.resolved) {
538
+ pending.set(taskId, approval);
539
+ }
540
+ });
541
+ return pending;
542
+ }
531
543
  /**
532
544
  * 注入审批记录(MCP 重启恢复用)
533
545
  * 如果 taskId 已存在则跳过,避免覆盖用户已点击的结果
@@ -607,6 +619,24 @@ class WecomClient extends EventEmitter {
607
619
  getPendingMessageCount() {
608
620
  return this.pendingMessages.length;
609
621
  }
622
+ /**
623
+ * 取出所有待发送的审批消息并清空队列(用于 client 迁移)
624
+ * 仅迁移 approval 类型,text 通知类消息不需要迁移
625
+ */
626
+ takePendingApprovalMessages() {
627
+ const approvalMessages = this.pendingMessages.filter(m => m.type === 'approval');
628
+ this.pendingMessages = this.pendingMessages.filter(m => m.type !== 'approval');
629
+ return approvalMessages;
630
+ }
631
+ /**
632
+ * 注入待发送消息(用于 client 迁移,新 client 连接后会自动重发)
633
+ */
634
+ injectPendingMessages(messages) {
635
+ if (messages.length === 0)
636
+ return;
637
+ this.pendingMessages.unshift(...messages);
638
+ logger.log(`[wecom] 注入待发送消息: ${messages.length} 条`);
639
+ }
610
640
  // 获取重连状态
611
641
  getReconnectStatus() {
612
642
  return {
@@ -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 {
@@ -409,13 +444,14 @@ if ! echo "$HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
409
444
  exit 0
410
445
  fi
411
446
 
412
- # 读取当前项目使用的机器人名称
447
+ # 读取当前项目使用的机器人名称和 ccId
413
448
  ROBOT_NAME=$(jq -r '.robotName // empty' "$CONFIG_FILE" 2>/dev/null)
449
+ CC_ID=$(jq -r '.ccId // empty' "$CONFIG_FILE" 2>/dev/null)
414
450
 
415
451
  # 发送审批请求(使用 pwd 作为 projectDir)
416
452
  TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
417
- BODY=$(jq -n --arg tool_name "$TOOL_NAME" --argjson tool_input "$TOOL_INPUT" --arg project_dir "$PROJECT_DIR" --arg robot_name "$ROBOT_NAME" \\
418
- '{"tool_name":$tool_name,"tool_input":$tool_input,"projectDir":$project_dir,"robotName":$robot_name}')
453
+ BODY=$(jq -n --arg tool_name "$TOOL_NAME" --argjson tool_input "$TOOL_INPUT" --arg project_dir "$PROJECT_DIR" --arg robot_name "$ROBOT_NAME" --arg cc_id "$CC_ID" \\
454
+ '{"tool_name":$tool_name,"tool_input":$tool_input,"projectDir":$project_dir,"robotName":$robot_name,"ccId":$cc_id}')
419
455
 
420
456
  log_debug "[$(date)] Sending approval request..."
421
457
  RESPONSE=$(curl -s -m 10 -X POST "http://127.0.0.1:$MCP_PORT/approve" \\
@@ -672,18 +708,10 @@ function writeMcpServerConfig(config, instanceName) {
672
708
  console.log(`[config] 已更新机器人配置: ${existingConfigFile}`);
673
709
  }
674
710
  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
- }
711
+ // 新机器人:统一使用 robot-*.json
712
+ const newConfigPath = path.join(CONFIG_DIR, `robot-${Date.now()}.json`);
713
+ fs.writeFileSync(newConfigPath, JSON.stringify(botConfig, null, 2));
714
+ console.log(`[config] 已添加新机器人配置: ${newConfigPath}`);
687
715
  }
688
716
  // 2. 写入 MCP 配置到 ~/.claude.json(仅 URL)
689
717
  let claudeConfig = {};
@@ -707,7 +735,7 @@ function writeMcpServerConfig(config, instanceName) {
707
735
  logger.error('[config] 写入配置失败:', err);
708
736
  console.log('[config] ⚠️ 请手动配置:');
709
737
  console.log('');
710
- console.log('~/.wecom-aibot-mcp/config.json:');
738
+ console.log('~/.wecom-aibot-mcp/robot-*.json:');
711
739
  console.log(JSON.stringify({
712
740
  botId: config.botId,
713
741
  secret: config.secret,
@@ -751,6 +779,9 @@ export async function addMcpConfig() {
751
779
  console.log('Secret 不能为空');
752
780
  secret = await question(rl, 'Secret: ');
753
781
  }
782
+ // 获取文档 MCP URL(可选)
783
+ console.log('');
784
+ const docMcpUrl = await question(rl, '文档 MCP URL(可选,企业微信管理后台获取,留空跳过): ');
754
785
  rl.close();
755
786
  // 检查是否已存在相同 botId 的配置
756
787
  const existingRobots = listAllRobots();
@@ -809,22 +840,14 @@ export async function addMcpConfig() {
809
840
  secret,
810
841
  targetUserId,
811
842
  nameTag: robotName,
843
+ ...(docMcpUrl ? { doc_mcp_url: docMcpUrl } : {}),
812
844
  };
813
845
  // 确保配置目录存在
814
846
  ensureConfigDir();
815
- // 如果是第一个机器人,保存为默认配置
816
- const defaultConfigPath = BOT_CONFIG_FILE;
847
+ // 统一使用 robot-*.json 格式
817
848
  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
- }
849
+ fs.writeFileSync(robotConfigPath, JSON.stringify(robotConfig, null, 2));
850
+ console.log(`\n[config] ✅ 已添加机器人: ${robotName}`);
828
851
  console.log(`[config] 用户 ID: ${targetUserId}`);
829
852
  // 列出所有机器人
830
853
  const robots = listAllRobots();
@@ -842,23 +865,7 @@ export async function addMcpConfig() {
842
865
  // 列出所有机器人配置
843
866
  export function listAllRobots() {
844
867
  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
- // 其他机器人配置
868
+ // 所有机器人配置(统一 robot-*.json 格式)
862
869
  if (fs.existsSync(CONFIG_DIR)) {
863
870
  const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
864
871
  for (const file of files) {
@@ -958,7 +965,7 @@ export function ensureHookInstalled() {
958
965
  writeTaskCompletedHookScript();
959
966
  }
960
967
  // 确保所有全局配置已写入(强制覆盖,不依赖智能体)
961
- export function ensureGlobalConfigs(mode = 'full') {
968
+ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
962
969
  ensureConfigDir();
963
970
  // 读取已安装版本
964
971
  let previousVersion;
@@ -981,6 +988,64 @@ export function ensureGlobalConfigs(mode = 'full') {
981
988
  fs.writeFileSync(VERSION_FILE, JSON.stringify({ version: VERSION, installedAt: Date.now() }, null, 2));
982
989
  return { upgraded, previousVersion };
983
990
  }
991
+ // remote 模式:仅写入远程 HTTP MCP 配置(带 token headers),不装 Channel/Hook
992
+ if (mode === 'remote') {
993
+ if (!remoteOptions?.url || !remoteOptions?.token) {
994
+ console.log('[config] ❌ 远程模式需要提供 URL 和 Token');
995
+ return { upgraded: false, previousVersion };
996
+ }
997
+ let claudeConfig = {};
998
+ if (fs.existsSync(CLAUDE_CONFIG_FILE)) {
999
+ claudeConfig = JSON.parse(fs.readFileSync(CLAUDE_CONFIG_FILE, 'utf-8'));
1000
+ }
1001
+ if (!claudeConfig.mcpServers)
1002
+ claudeConfig.mcpServers = {};
1003
+ claudeConfig.mcpServers['wecom-aibot'] = {
1004
+ type: 'http',
1005
+ url: remoteOptions.url,
1006
+ headers: { Authorization: `Bearer ${remoteOptions.token}` },
1007
+ };
1008
+ fs.writeFileSync(CLAUDE_CONFIG_FILE, JSON.stringify(claudeConfig, null, 2));
1009
+ console.log('[config] remote 模式:已写入远程 HTTP MCP 配置(带 Token)');
1010
+ fs.writeFileSync(VERSION_FILE, JSON.stringify({ version: VERSION, installedAt: Date.now() }, null, 2));
1011
+ return { upgraded, previousVersion };
1012
+ }
1013
+ // remote-channel 模式:写入远程 HTTP MCP(带 token)+ Channel MCP
1014
+ if (mode === 'remote-channel') {
1015
+ if (!remoteOptions?.url || !remoteOptions?.token) {
1016
+ console.log('[config] ❌ 远程模式需要提供 URL 和 Token');
1017
+ return { upgraded: false, previousVersion };
1018
+ }
1019
+ let claudeConfig = {};
1020
+ if (fs.existsSync(CLAUDE_CONFIG_FILE)) {
1021
+ claudeConfig = JSON.parse(fs.readFileSync(CLAUDE_CONFIG_FILE, 'utf-8'));
1022
+ }
1023
+ if (!claudeConfig.mcpServers)
1024
+ 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
+ };
1031
+ // Channel MCP 配置(带 MCP_URL + MCP_AUTH_TOKEN)
1032
+ const binPath = path.join(__dirname, 'bin.js');
1033
+ 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
+ },
1040
+ };
1041
+ fs.writeFileSync(CLAUDE_CONFIG_FILE, JSON.stringify(claudeConfig, null, 2));
1042
+ console.log('[config] remote-channel 模式:已写入 HTTP MCP + Channel MCP 配置(带 Token)');
1043
+ // Channel 模式需要权限配置
1044
+ writeMcpPermissions();
1045
+ console.log('[config] 已写入权限配置到 ~/.claude/settings.local.json');
1046
+ fs.writeFileSync(VERSION_FILE, JSON.stringify({ version: VERSION, installedAt: Date.now() }, null, 2));
1047
+ return { upgraded, previousVersion };
1048
+ }
984
1049
  // 1. 强制写入 MCP 配置到 ~/.claude.json
985
1050
  let claudeConfig = {};
986
1051
  if (fs.existsSync(CLAUDE_CONFIG_FILE)) {
@@ -997,11 +1062,17 @@ export function ensureGlobalConfigs(mode = 'full') {
997
1062
  console.log('[config] 请设置环境变量: MCP_URL=http://远程IP:18963');
998
1063
  return { upgraded: false, previousVersion };
999
1064
  }
1000
- // Channel MCP 配置:硬编码本地路径
1065
+ // Channel MCP 配置:使用当前模块路径
1066
+ const binPath = path.join(__dirname, 'bin.js');
1067
+ const channelEnv = { MCP_URL: mcpUrl };
1068
+ const authToken = getAuthToken();
1069
+ if (authToken) {
1070
+ channelEnv.MCP_AUTH_TOKEN = authToken;
1071
+ }
1001
1072
  claudeConfig.mcpServers['wecom-aibot-channel'] = {
1002
1073
  command: 'node',
1003
- args: ['/Volumes/Mac_Data/VScode/wecom-aibot-mcp/dist/bin.js', '--channel'],
1004
- env: { MCP_URL: mcpUrl },
1074
+ args: [binPath, '--channel'],
1075
+ env: channelEnv,
1005
1076
  };
1006
1077
  console.log(`[config] Channel-only 模式:Channel MCP 使用本地路径`);
1007
1078
  }
@@ -1011,10 +1082,11 @@ export function ensureGlobalConfigs(mode = 'full') {
1011
1082
  type: 'http',
1012
1083
  url: 'http://127.0.0.1:18963/mcp',
1013
1084
  };
1014
- // Channel MCP 配置:硬编码本地路径
1085
+ // Channel MCP 配置:使用当前模块路径
1086
+ const binPath = path.join(__dirname, 'bin.js');
1015
1087
  claudeConfig.mcpServers['wecom-aibot-channel'] = {
1016
1088
  command: 'node',
1017
- args: ['/Volumes/Mac_Data/VScode/wecom-aibot-mcp/dist/bin.js', '--channel'],
1089
+ args: [binPath, '--channel'],
1018
1090
  env: { MCP_URL: 'http://127.0.0.1:18963' },
1019
1091
  };
1020
1092
  console.log(`[config] full 模式:Channel MCP 使用本地路径`);
@@ -1029,7 +1101,69 @@ export function ensureGlobalConfigs(mode = 'full') {
1029
1101
  console.log(`[config] 已记录版本号: ${VERSION}`);
1030
1102
  return { upgraded, previousVersion };
1031
1103
  }
1032
- // 保存配置(直接写入 ~/.claude.json
1104
+ // 远程安装向导(交互式输入 URL + Token
1105
+ export async function runRemoteInstallWizard() {
1106
+ const rl = createRL();
1107
+ const CLAUDE_CONFIG_FILE = path.join(os.homedir(), '.claude.json');
1108
+ try {
1109
+ // 检测本机是否有 ~/.claude.json(判断是 Client 还是 Server)
1110
+ const hasClaudeConfig = fs.existsSync(CLAUDE_CONFIG_FILE);
1111
+ if (!hasClaudeConfig) {
1112
+ // Server 安装模式:本机无 ~/.claude.json,作为远程服务器
1113
+ console.log('\n检测到本机无 ~/.claude.json → Server 安装模式\n');
1114
+ console.log(' Server 端只需启动 HTTP MCP Server,不写入 MCP 配置');
1115
+ console.log(' Client 端在其他机器上安装\n');
1116
+ const confirm = await question(rl, '确认作为远程 Server 安装?(y/N): ');
1117
+ if (confirm.toLowerCase() !== 'y') {
1118
+ console.log('[config] 已取消');
1119
+ return null;
1120
+ }
1121
+ // Server 不写入 ~/.claude.json,只提示启动命令
1122
+ console.log('\n─────────────────────────────────────');
1123
+ console.log('Server 安装完成!');
1124
+ console.log(' 启动命令: npx @anthropic/wecom-aibot-mcp --http-only --start');
1125
+ console.log(' 或者: npm run start:http');
1126
+ console.log('─────────────────────────────────────\n');
1127
+ console.log('[config] Client 端请在其他机器运行安装程序连接本服务器\n');
1128
+ return 'server';
1129
+ }
1130
+ // Client 安装模式:本机有 ~/.claude.json,作为客户端
1131
+ console.log('\n检测到本机有 ~/.claude.json → Client 安装模式\n');
1132
+ console.log(' 请选择连接远程服务器的方式:\n');
1133
+ console.log(' 1. 仅 HTTP MCP(轮询模式)');
1134
+ console.log(' 2. HTTP MCP + Channel MCP(推荐,消息自动唤醒)\n');
1135
+ const choice = await question(rl, '请选择 (1/2): ');
1136
+ const mode = choice === '2' ? 'remote-channel' : 'remote';
1137
+ let serverUrl = await question(rl, '远程服务器地址(如 https://your-server:18963): ');
1138
+ while (!serverUrl) {
1139
+ console.log('服务器地址不能为空');
1140
+ serverUrl = await question(rl, '远程服务器地址: ');
1141
+ }
1142
+ // 标准化 URL(去掉尾部斜杠)
1143
+ serverUrl = serverUrl.replace(/\/+$/, '');
1144
+ let token = await question(rl, 'Auth Token(必填,远程服务器需配置相同 Token): ');
1145
+ while (!token) {
1146
+ console.log('Auth Token 不能为空');
1147
+ token = await question(rl, 'Auth Token: ');
1148
+ }
1149
+ // 写入配置
1150
+ ensureGlobalConfigs(mode, { url: serverUrl, token });
1151
+ console.log('\n─────────────────────────────────────');
1152
+ console.log('Client 配置完成!');
1153
+ console.log(` 模式: ${mode === 'remote-channel' ? 'HTTP + Channel' : '仅 HTTP'}`);
1154
+ console.log(` 服务器: ${serverUrl}`);
1155
+ console.log(` Auth Token: ${token.slice(0, 8)}...${token.slice(-4)}`);
1156
+ console.log('─────────────────────────────────────\n');
1157
+ if (mode === 'remote-channel') {
1158
+ console.log('Channel 模式优势:微信消息自动唤醒 agent,无需主动轮询');
1159
+ }
1160
+ console.log('[config] 请重启 Claude Code 以加载最新配置\n');
1161
+ return mode;
1162
+ }
1163
+ finally {
1164
+ rl.close();
1165
+ }
1166
+ }
1033
1167
  export function saveConfig(config, instanceName) {
1034
1168
  ensureConfigDir(); // 确保运行时文件目录存在
1035
1169
  // 写入 MCP Server 配置到 ~/.claude.json
@@ -1232,15 +1366,6 @@ export async function runConfigWizard() {
1232
1366
  }
1233
1367
  // 查找机器人配置文件路径(按名称)
1234
1368
  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
1369
  if (fs.existsSync(CONFIG_DIR)) {
1245
1370
  const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
1246
1371
  for (const file of files) {
@@ -1256,14 +1381,6 @@ export function findRobotConfigFile(robotName) {
1256
1381
  }
1257
1382
  // 查找机器人配置文件路径(按 botId)
1258
1383
  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
1384
  if (fs.existsSync(CONFIG_DIR)) {
1268
1385
  const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
1269
1386
  for (const file of files) {
@@ -1335,7 +1452,7 @@ export async function detectUserIdFromMessage(client, timeoutSeconds = 60) {
1335
1452
  *
1336
1453
  * 优先级:
1337
1454
  * 1. 环境变量(WECOM_BOT_ID, WECOM_SECRET, WECOM_TARGET_USER)
1338
- * 2. 保存的配置文件(~/.wecom-aibot-mcp/config.json)
1455
+ * 2. 保存的配置文件(~/.wecom-aibot-mcp/robot-*.json)
1339
1456
  * 3. 运行配置向导
1340
1457
  */
1341
1458
  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) {
@@ -146,7 +146,22 @@ export async function getClient(robotName) {
146
146
  const robot = await findRobotConfig(state.robotName);
147
147
  if (robot) {
148
148
  logger.log(`[connection] 重连机器人: ${robot.name}`);
149
+ const oldClient = state.client;
150
+ // 迁移旧 client 的未解决审批记录和待发送审批消息
151
+ const pendingApprovals = oldClient.getUnresolvedApprovalMap();
152
+ const pendingApprovalMessages = oldClient.takePendingApprovalMessages();
153
+ // 先断开旧 client,防止旧 client 重连后与新 client 并存导致事件分流
154
+ oldClient.disconnect();
149
155
  state.client = new WecomClient(robot.botId, robot.secret, robot.targetUserId, robot.name);
156
+ // 注入审批记录,确保 Hook 轮询和用户点击仍能正常处理
157
+ pendingApprovals.forEach((approval, taskId) => {
158
+ state.client.injectApprovalRecord(taskId, {
159
+ toolName: approval.toolName,
160
+ toolInput: approval.toolInput,
161
+ });
162
+ });
163
+ // 注入待发送的审批卡片消息,新 client 连接后会自动重发
164
+ state.client.injectPendingMessages(pendingApprovalMessages);
150
165
  state.client.connect();
151
166
  const connected = await waitForConnection(state.client, 5000);
152
167
  if (connected) {
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 流
@@ -824,7 +833,7 @@ async function handleApprovalRequest(req, res) {
824
833
  }
825
834
  const title = `【待审批】${tool_name}`;
826
835
  const requestId = `hook_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
827
- const taskId = await client.sendApprovalRequest(title, description, requestId);
836
+ const taskId = await client.sendApprovalRequest(title, description, requestId, undefined, tool_input, ccId);
828
837
  logger.log(`[http] 审批请求已发送: ${taskId} (机器人: ${robotName})`);
829
838
  // 存储审批并启动超时计时器
830
839
  const entry = {
@@ -864,6 +873,9 @@ function handleApprovalStatus(_req, res, url) {
864
873
  res.writeHead(200, { 'Content-Type': 'application/json' });
865
874
  res.end(JSON.stringify({ status: 'pending', result: 'pending' }));
866
875
  }
876
+ }).catch(() => {
877
+ res.writeHead(200, { 'Content-Type': 'application/json' });
878
+ res.end(JSON.stringify({ status: 'pending', result: 'pending' }));
867
879
  });
868
880
  return;
869
881
  }
@@ -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.1",
4
4
  "description": "企业微信智能机器人 MCP 服务 - Claude Code 审批通道",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",