@vrs-soft/wecom-aibot-mcp 2.3.2 → 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
  /**
@@ -38,6 +48,7 @@ function logChannel(message, data) {
38
48
  let sseConnected = false;
39
49
  let sseAbortController = null;
40
50
  let mcpServer = null;
51
+ let sseCurrentCcId = undefined;
41
52
  // HTTP MCP session ID(需要在转发请求前初始化)
42
53
  let httpSessionId = null;
43
54
  /**
@@ -52,6 +63,7 @@ async function initHttpSession() {
52
63
  headers: {
53
64
  'Content-Type': 'application/json',
54
65
  'Accept': 'application/json, text/event-stream',
66
+ ...getAuthHeaders(),
55
67
  },
56
68
  body: JSON.stringify({
57
69
  jsonrpc: '2.0',
@@ -109,6 +121,7 @@ async function forwardToHttpMcp(toolName, params) {
109
121
  'Content-Type': 'application/json',
110
122
  'Accept': 'application/json, text/event-stream',
111
123
  'mcp-session-id': sessionId,
124
+ ...getAuthHeaders(),
112
125
  },
113
126
  body: JSON.stringify({
114
127
  jsonrpc: '2.0',
@@ -172,6 +185,7 @@ function connectSSE(ccId) {
172
185
  return;
173
186
  }
174
187
  sseConnected = true;
188
+ sseCurrentCcId = ccId;
175
189
  const sseUrl = ccId ? `${MCP_URL}/sse/${ccId}` : `${MCP_URL}/sse`;
176
190
  logChannel('Connecting to SSE', { url: sseUrl, ccId, mcpServerReady: mcpServer ? 'yes' : 'no' });
177
191
  sseAbortController = new AbortController();
@@ -183,6 +197,7 @@ function connectSSE(ccId) {
183
197
  'Accept': 'text/event-stream',
184
198
  'Cache-Control': 'no-cache',
185
199
  'Connection': 'keep-alive',
200
+ ...getAuthHeaders(),
186
201
  },
187
202
  }).then(async (res) => {
188
203
  if (!res.ok) {
@@ -210,6 +225,11 @@ function connectSSE(ccId) {
210
225
  logChannel('SSE stream ended');
211
226
  clearInterval(heartbeatInterval);
212
227
  sseConnected = false;
228
+ // 非主动断开时自动重连
229
+ if (!sseAbortController?.signal.aborted) {
230
+ logChannel('SSE 断线,3 秒后重连', { ccId });
231
+ setTimeout(() => connectSSE(ccId), 3000);
232
+ }
213
233
  break;
214
234
  }
215
235
  const chunk = decoder.decode(value, { stream: true });
@@ -280,6 +300,11 @@ function connectSSE(ccId) {
280
300
  }).catch((err) => {
281
301
  logChannel('SSE error', { error: String(err) });
282
302
  sseConnected = false;
303
+ // 非主动断开时自动重连
304
+ if (!sseAbortController?.signal.aborted) {
305
+ logChannel('SSE 出错,3 秒后重连', { ccId });
306
+ setTimeout(() => connectSSE(ccId), 3000);
307
+ }
283
308
  });
284
309
  }
285
310
  /**
@@ -352,8 +377,9 @@ function registerChannelTools(server) {
352
377
  bot_id: z.string().describe('企业微信 Bot ID'),
353
378
  secret: z.string().describe('机器人密钥'),
354
379
  default_user: z.string().optional().describe('默认目标用户'),
355
- }, async ({ name, bot_id, secret, default_user }) => {
356
- return forwardToHttpMcp('add_robot_config', { name, bot_id, secret, default_user });
380
+ doc_mcp_url: z.string().optional().describe('机器人文档 MCP URL(企业微信文档能力)'),
381
+ }, async ({ name, bot_id, secret, default_user, doc_mcp_url }) => {
382
+ return forwardToHttpMcp('add_robot_config', { name, bot_id, secret, default_user, doc_mcp_url });
357
383
  });
358
384
  // ============================================
359
385
  // 工具 8: 列出所有机器人
@@ -416,11 +442,12 @@ function registerChannelTools(server) {
416
442
  cc_id: z.string().describe('CC 唯一标识(enter_headless_mode 返回的 ccId)'),
417
443
  project_dir: z.string().optional().describe('项目目录路径(用于更新配置文件)'),
418
444
  }, async ({ cc_id, project_dir }) => {
419
- // 断开 SSE 连接
445
+ // 断开 SSE 连接(abort 后重连逻辑不会触发)
420
446
  if (sseAbortController) {
421
447
  sseAbortController.abort();
422
448
  sseAbortController = null;
423
449
  sseConnected = false;
450
+ sseCurrentCcId = undefined;
424
451
  logChannel('SSE disconnected', { cc_id });
425
452
  }
426
453
  return forwardToHttpMcp('exit_headless_mode', { cc_id, project_dir: project_dir || process.cwd() });
@@ -445,7 +472,9 @@ function registerChannelTools(server) {
445
472
  // ============================================
446
473
  server.tool('get_skill', '获取 headless-mode skill 文件内容,用于写入本地项目目录。远程部署时 HTTP MCP 可能不在本地,skill 文件需要从此接口获取。', {}, async () => {
447
474
  // 直接请求 HTTP MCP 的 /skill 端点
448
- const res = await fetch(`${MCP_URL}/skill`);
475
+ const res = await fetch(`${MCP_URL}/skill`, {
476
+ headers: getAuthHeaders(),
477
+ });
449
478
  if (!res.ok) {
450
479
  return {
451
480
  content: [{
@@ -462,7 +491,97 @@ function registerChannelTools(server) {
462
491
  }],
463
492
  };
464
493
  });
465
- logChannel('Registered 13 tools');
494
+ // ============================================
495
+ // 文档代理工具(转发到 HTTP MCP)
496
+ // ============================================
497
+ const docTools = [
498
+ ['create_doc', '新建文档或智能表格', {
499
+ doc_type: z.number().int().describe('文档类型:3=文档,10=智能表格'),
500
+ doc_name: z.string().describe('文档名称'),
501
+ robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
502
+ }],
503
+ ['get_doc_content', '获取文档内容(Markdown 格式)', {
504
+ type: z.number().int().describe('内容格式:2=Markdown'),
505
+ url: z.string().optional().describe('文档链接'),
506
+ docid: z.string().optional().describe('文档 docid'),
507
+ task_id: z.string().optional().describe('任务 ID(轮询时填写)'),
508
+ robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
509
+ }],
510
+ ['edit_doc_content', '编辑文档内容(Markdown 格式覆写)', {
511
+ content: z.string().describe('覆写的文档内容'),
512
+ content_type: z.number().int().describe('内容类型:1=Markdown'),
513
+ url: z.string().optional().describe('文档链接'),
514
+ docid: z.string().optional().describe('文档 docid'),
515
+ robot_name: z.string().optional().describe('指定机器人名称(多机器人时必填)'),
516
+ }],
517
+ ['smartsheet_get_sheet', '查询智能表格子表信息', {
518
+ url: z.string().optional(), docid: z.string().optional(),
519
+ robot_name: z.string().optional(),
520
+ }],
521
+ ['smartsheet_add_sheet', '添加智能表格子表', {
522
+ url: z.string().optional(), docid: z.string().optional(),
523
+ properties: z.object({ title: z.string().optional() }).optional(),
524
+ robot_name: z.string().optional(),
525
+ }],
526
+ ['smartsheet_update_sheet', '更新智能表格子表标题', {
527
+ properties: z.object({ sheet_id: z.string(), title: z.string() }),
528
+ url: z.string().optional(), docid: z.string().optional(),
529
+ robot_name: z.string().optional(),
530
+ }],
531
+ ['smartsheet_delete_sheet', '删除智能表格子表', {
532
+ sheet_id: z.string(), url: z.string().optional(), docid: z.string().optional(),
533
+ robot_name: z.string().optional(),
534
+ }],
535
+ ['smartsheet_get_fields', '查询智能表格字段', {
536
+ sheet_id: z.string(), url: z.string().optional(), docid: z.string().optional(),
537
+ robot_name: z.string().optional(),
538
+ }],
539
+ ['smartsheet_add_fields', '添加智能表格字段', {
540
+ sheet_id: z.string(),
541
+ fields: z.array(z.object({ field_title: z.string(), field_type: z.string() })),
542
+ url: z.string().optional(), docid: z.string().optional(),
543
+ robot_name: z.string().optional(),
544
+ }],
545
+ ['smartsheet_update_fields', '更新智能表格字段标题', {
546
+ sheet_id: z.string(),
547
+ fields: z.array(z.object({ field_id: z.string(), field_title: z.string(), field_type: z.string() })),
548
+ url: z.string().optional(), docid: z.string().optional(),
549
+ robot_name: z.string().optional(),
550
+ }],
551
+ ['smartsheet_delete_fields', '删除智能表格字段', {
552
+ sheet_id: z.string(), field_ids: z.array(z.string()),
553
+ url: z.string().optional(), docid: z.string().optional(),
554
+ robot_name: z.string().optional(),
555
+ }],
556
+ ['smartsheet_get_records', '查询智能表格记录', {
557
+ sheet_id: z.string(), url: z.string().optional(), docid: z.string().optional(),
558
+ robot_name: z.string().optional(),
559
+ }],
560
+ ['smartsheet_add_records', '添加智能表格记录', {
561
+ sheet_id: z.string(),
562
+ records: z.array(z.object({ values: z.record(z.string(), z.unknown()) })),
563
+ url: z.string().optional(), docid: z.string().optional(),
564
+ robot_name: z.string().optional(),
565
+ }],
566
+ ['smartsheet_update_records', '更新智能表格记录', {
567
+ sheet_id: z.string(),
568
+ records: z.array(z.object({ record_id: z.string(), values: z.record(z.string(), z.unknown()) })),
569
+ key_type: z.enum(['CELL_VALUE_KEY_TYPE_FIELD_TITLE', 'CELL_VALUE_KEY_TYPE_FIELD_ID']),
570
+ url: z.string().optional(), docid: z.string().optional(),
571
+ robot_name: z.string().optional(),
572
+ }],
573
+ ['smartsheet_delete_records', '删除智能表格记录', {
574
+ sheet_id: z.string(), record_ids: z.array(z.string()),
575
+ url: z.string().optional(), docid: z.string().optional(),
576
+ robot_name: z.string().optional(),
577
+ }],
578
+ ];
579
+ for (const [toolName, description, schema] of docTools) {
580
+ server.tool(toolName, description, schema, async (args) => {
581
+ return forwardToHttpMcp(toolName, args);
582
+ });
583
+ }
584
+ logChannel('Registered 28 tools (13 core + 15 doc proxy)');
466
585
  }
467
586
  /**
468
587
  * 启动 Channel MCP Server
@@ -472,7 +591,7 @@ export async function startChannelServer() {
472
591
  // 创建 MCP Server
473
592
  mcpServer = new McpServer({
474
593
  name: 'wecom-aibot-channel',
475
- version: '2.0.0',
594
+ version: VERSION,
476
595
  }, {
477
596
  capabilities: {
478
597
  // 必须声明 experimental['claude/channel'],Claude Code 才会注册 notification listener
@@ -14,6 +14,7 @@ export interface WecomConfig {
14
14
  secret: string;
15
15
  defaultUser: string;
16
16
  nameTag?: string;
17
+ doc_mcp_url?: string;
17
18
  }
18
19
  /**
19
20
  * 获取或创建 WecomClient 实例