@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 +51 -46
- package/dist/bin.js +154 -22
- package/dist/channel-server.js +17 -2
- package/dist/config-wizard.d.ts +10 -2
- package/dist/config-wizard.js +228 -112
- package/dist/connection-manager.js +2 -2
- package/dist/daemon.js +1 -17
- package/dist/http-server.js +11 -2
- package/dist/tools/index.js +2 -2
- package/package.json +1 -1
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
|
-
-
|
|
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
|
-
|
|
27
|
+
根据部署角色选择参数:
|
|
22
28
|
|
|
23
|
-
|
|
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
|
-
|
|
39
|
+
npx @vrs-soft/wecom-aibot-mcp --http-only --start
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**后台启动 / 停止(本地或 Server 端)**:
|
|
30
43
|
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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.
|
|
59
|
-
|
|
62
|
+
1. 安装: npx @vrs-soft/wecom-aibot-mcp --setup
|
|
63
|
+
(根据角色选择参数:--server / --channel / 两者都传 / 不传交互选择)
|
|
60
64
|
|
|
61
|
-
2.
|
|
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
|
-
|
|
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 ? '✅ 运行中' : '❌ 未启动'}
|
|
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
|
-
|
|
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
|
|
301
|
-
//
|
|
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/
|
|
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
|
-
|
|
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]
|
|
514
|
-
|
|
515
|
-
|
|
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),必须有配置
|
package/dist/channel-server.js
CHANGED
|
@@ -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:
|
|
594
|
+
version: VERSION,
|
|
580
595
|
}, {
|
|
581
596
|
capabilities: {
|
|
582
597
|
// 必须声明 experimental['claude/channel'],Claude Code 才会注册 notification listener
|
package/dist/config-wizard.d.ts
CHANGED
|
@@ -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/
|
|
75
|
+
* 2. 保存的配置文件(~/.wecom-aibot-mcp/robot-*.json)
|
|
68
76
|
* 3. 运行配置向导
|
|
69
77
|
*/
|
|
70
78
|
export declare function getOrInitConfig(): Promise<WecomConfig>;
|
package/dist/config-wizard.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 首次运行时引导用户配置 Bot ID、Secret 和默认目标用户
|
|
5
5
|
*
|
|
6
6
|
* 配置存储位置:
|
|
7
|
-
* - 机器人配置:~/.wecom-aibot-mcp/
|
|
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/
|
|
40
|
+
// 从 ~/.wecom-aibot-mcp/robot-*.json 读取第一个有效配置
|
|
41
41
|
export function loadConfig() {
|
|
42
42
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
// 删除整个配置目录(包括
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
-
//
|
|
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: [
|
|
1004
|
-
env:
|
|
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: [
|
|
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
|
-
//
|
|
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/
|
|
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
|
-
//
|
|
33
|
-
const allFiles =
|
|
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 {
|
package/dist/http-server.js
CHANGED
|
@@ -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 流
|
package/dist/tools/index.js
CHANGED
|
@@ -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:
|
|
253
|
+
version: VERSION,
|
|
254
254
|
requirements: {
|
|
255
255
|
// 权限配置需求
|
|
256
256
|
permissions: {
|