@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 +51 -46
- package/dist/bin.js +154 -22
- package/dist/channel-server.js +125 -6
- package/dist/client-pool.d.ts +1 -0
- package/dist/config-wizard.d.ts +16 -2
- package/dist/config-wizard.js +283 -115
- package/dist/connection-manager.js +2 -2
- package/dist/daemon.js +1 -17
- package/dist/doc-proxy.d.ts +21 -0
- package/dist/doc-proxy.js +46 -0
- package/dist/http-server.js +11 -2
- package/dist/tools/index.js +212 -8
- package/package.json +1 -1
package/dist/config-wizard.d.ts
CHANGED
|
@@ -4,8 +4,13 @@ export interface WecomConfig {
|
|
|
4
4
|
targetUserId: string;
|
|
5
5
|
targetUserName?: string;
|
|
6
6
|
nameTag?: string;
|
|
7
|
+
doc_mcp_url?: string;
|
|
7
8
|
}
|
|
9
|
+
export declare const VERSION: string;
|
|
8
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;
|
|
9
14
|
export declare function listAllMcpInstances(): Array<{
|
|
10
15
|
name: string;
|
|
11
16
|
config: WecomConfig;
|
|
@@ -23,12 +28,21 @@ export declare function listAllRobots(): Array<{
|
|
|
23
28
|
name: string;
|
|
24
29
|
botId: string;
|
|
25
30
|
targetUserId: string;
|
|
31
|
+
doc_mcp_url?: string;
|
|
26
32
|
}>;
|
|
33
|
+
export declare function getDocMcpUrl(robotName?: string): {
|
|
34
|
+
url: string | null;
|
|
35
|
+
error?: string;
|
|
36
|
+
};
|
|
27
37
|
export declare function ensureHookInstalled(): void;
|
|
28
|
-
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
|
+
}): {
|
|
29
42
|
upgraded: boolean;
|
|
30
43
|
previousVersion?: string;
|
|
31
44
|
};
|
|
45
|
+
export declare function runRemoteInstallWizard(): Promise<'remote' | 'remote-channel' | 'server' | null>;
|
|
32
46
|
export declare function saveConfig(config: WecomConfig, instanceName?: string): boolean;
|
|
33
47
|
/**
|
|
34
48
|
* 安装 headless-mode skill 到项目目录
|
|
@@ -58,7 +72,7 @@ export declare function detectUserIdFromMessage(client: any, timeoutSeconds?: nu
|
|
|
58
72
|
*
|
|
59
73
|
* 优先级:
|
|
60
74
|
* 1. 环境变量(WECOM_BOT_ID, WECOM_SECRET, WECOM_TARGET_USER)
|
|
61
|
-
* 2. 保存的配置文件(~/.wecom-aibot-mcp/
|
|
75
|
+
* 2. 保存的配置文件(~/.wecom-aibot-mcp/robot-*.json)
|
|
62
76
|
* 3. 运行配置向导
|
|
63
77
|
*/
|
|
64
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,19 +37,26 @@ 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 = {
|
|
49
51
|
botId: config.botId,
|
|
50
52
|
secret: config.secret,
|
|
51
53
|
targetUserId: config.targetUserId,
|
|
52
54
|
};
|
|
55
|
+
if (config.nameTag)
|
|
56
|
+
result.nameTag = config.nameTag;
|
|
57
|
+
if (config.doc_mcp_url)
|
|
58
|
+
result.doc_mcp_url = config.doc_mcp_url;
|
|
59
|
+
return result;
|
|
53
60
|
}
|
|
54
61
|
}
|
|
55
62
|
}
|
|
@@ -58,6 +65,71 @@ export function loadConfig() {
|
|
|
58
65
|
}
|
|
59
66
|
return null;
|
|
60
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
|
+
}
|
|
61
133
|
// 获取所有 wecom-aibot 相关的 MCP 实例
|
|
62
134
|
export function listAllMcpInstances() {
|
|
63
135
|
// 现在只有一个主配置文件
|
|
@@ -134,18 +206,8 @@ export function deleteRobotConfig(robotName) {
|
|
|
134
206
|
}
|
|
135
207
|
// 查找机器人对应的配置文件
|
|
136
208
|
let configFile = null;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (fs.existsSync(BOT_CONFIG_FILE)) {
|
|
140
|
-
const config = JSON.parse(fs.readFileSync(BOT_CONFIG_FILE, 'utf-8'));
|
|
141
|
-
const name = config.nameTag || `机器人-${config.botId?.slice(0, 8) || 'unknown'}`;
|
|
142
|
-
if (name === robotName) {
|
|
143
|
-
configFile = BOT_CONFIG_FILE;
|
|
144
|
-
isDefault = true;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
// 检查其他机器人配置文件
|
|
148
|
-
if (!configFile && fs.existsSync(CONFIG_DIR)) {
|
|
209
|
+
// 从 robot-*.json 中查找
|
|
210
|
+
if (fs.existsSync(CONFIG_DIR)) {
|
|
149
211
|
const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
|
|
150
212
|
for (const file of files) {
|
|
151
213
|
const filePath = path.join(CONFIG_DIR, file);
|
|
@@ -161,30 +223,8 @@ export function deleteRobotConfig(robotName) {
|
|
|
161
223
|
console.log(`[config] 未找到机器人 "${robotName}" 的配置文件`);
|
|
162
224
|
return false;
|
|
163
225
|
}
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
// 查找其他机器人配置文件
|
|
167
|
-
const otherRobotFiles = fs.existsSync(CONFIG_DIR)
|
|
168
|
-
? fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'))
|
|
169
|
-
: [];
|
|
170
|
-
if (otherRobotFiles.length > 0) {
|
|
171
|
-
// 将第一个其他机器人提升为默认
|
|
172
|
-
const newDefaultFile = path.join(CONFIG_DIR, otherRobotFiles[0]);
|
|
173
|
-
const newDefaultConfig = JSON.parse(fs.readFileSync(newDefaultFile, 'utf-8'));
|
|
174
|
-
fs.writeFileSync(BOT_CONFIG_FILE, JSON.stringify(newDefaultConfig, null, 2));
|
|
175
|
-
fs.unlinkSync(newDefaultFile);
|
|
176
|
-
console.log(`[config] 已将 "${newDefaultConfig.nameTag || otherRobotFiles[0]}" 提升为默认机器人`);
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
// 没有其他机器人,直接删除默认配置
|
|
180
|
-
fs.unlinkSync(BOT_CONFIG_FILE);
|
|
181
|
-
console.log('[config] 已删除最后一个机器人配置');
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
else {
|
|
185
|
-
// 不是默认机器人,直接删除
|
|
186
|
-
fs.unlinkSync(configFile);
|
|
187
|
-
}
|
|
226
|
+
// 直接删除
|
|
227
|
+
fs.unlinkSync(configFile);
|
|
188
228
|
console.log(`[config] 已删除机器人: ${robotName}`);
|
|
189
229
|
return true;
|
|
190
230
|
}
|
|
@@ -297,7 +337,7 @@ export function uninstall() {
|
|
|
297
337
|
logger.error('[config] 删除 headless 状态索引失败:', err);
|
|
298
338
|
}
|
|
299
339
|
}
|
|
300
|
-
// 删除整个配置目录(包括
|
|
340
|
+
// 删除整个配置目录(包括 robot-*.json、hook 脚本、日志等)
|
|
301
341
|
// 使用 recursive: true 和 force: true 确保完全删除
|
|
302
342
|
if (fs.existsSync(CONFIG_DIR)) {
|
|
303
343
|
try {
|
|
@@ -650,6 +690,9 @@ function writeMcpServerConfig(config, instanceName) {
|
|
|
650
690
|
if (config.nameTag) {
|
|
651
691
|
botConfig.nameTag = config.nameTag;
|
|
652
692
|
}
|
|
693
|
+
if (config.doc_mcp_url) {
|
|
694
|
+
botConfig.doc_mcp_url = config.doc_mcp_url;
|
|
695
|
+
}
|
|
653
696
|
// 检查名称唯一性(如果设置了新名称)
|
|
654
697
|
if (config.nameTag && isRobotNameExists(config.nameTag, config.botId)) {
|
|
655
698
|
console.log(`[config] ❌ 机器人名称 "${config.nameTag}" 已被其他机器人使用`);
|
|
@@ -664,18 +707,10 @@ function writeMcpServerConfig(config, instanceName) {
|
|
|
664
707
|
console.log(`[config] 已更新机器人配置: ${existingConfigFile}`);
|
|
665
708
|
}
|
|
666
709
|
else {
|
|
667
|
-
//
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
fs.writeFileSync(newConfigPath, JSON.stringify(botConfig, null, 2));
|
|
672
|
-
console.log(`[config] 已添加新机器人配置: ${newConfigPath}`);
|
|
673
|
-
}
|
|
674
|
-
else {
|
|
675
|
-
// 没有默认配置,写入 config.json
|
|
676
|
-
fs.writeFileSync(BOT_CONFIG_FILE, JSON.stringify(botConfig, null, 2));
|
|
677
|
-
console.log('[config] 已写入机器人配置 ~/.wecom-aibot-mcp/config.json');
|
|
678
|
-
}
|
|
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}`);
|
|
679
714
|
}
|
|
680
715
|
// 2. 写入 MCP 配置到 ~/.claude.json(仅 URL)
|
|
681
716
|
let claudeConfig = {};
|
|
@@ -699,7 +734,7 @@ function writeMcpServerConfig(config, instanceName) {
|
|
|
699
734
|
logger.error('[config] 写入配置失败:', err);
|
|
700
735
|
console.log('[config] ⚠️ 请手动配置:');
|
|
701
736
|
console.log('');
|
|
702
|
-
console.log('~/.wecom-aibot-mcp/
|
|
737
|
+
console.log('~/.wecom-aibot-mcp/robot-*.json:');
|
|
703
738
|
console.log(JSON.stringify({
|
|
704
739
|
botId: config.botId,
|
|
705
740
|
secret: config.secret,
|
|
@@ -743,6 +778,9 @@ export async function addMcpConfig() {
|
|
|
743
778
|
console.log('Secret 不能为空');
|
|
744
779
|
secret = await question(rl, 'Secret: ');
|
|
745
780
|
}
|
|
781
|
+
// 获取文档 MCP URL(可选)
|
|
782
|
+
console.log('');
|
|
783
|
+
const docMcpUrl = await question(rl, '文档 MCP URL(可选,企业微信管理后台获取,留空跳过): ');
|
|
746
784
|
rl.close();
|
|
747
785
|
// 检查是否已存在相同 botId 的配置
|
|
748
786
|
const existingRobots = listAllRobots();
|
|
@@ -801,22 +839,14 @@ export async function addMcpConfig() {
|
|
|
801
839
|
secret,
|
|
802
840
|
targetUserId,
|
|
803
841
|
nameTag: robotName,
|
|
842
|
+
...(docMcpUrl ? { doc_mcp_url: docMcpUrl } : {}),
|
|
804
843
|
};
|
|
805
844
|
// 确保配置目录存在
|
|
806
845
|
ensureConfigDir();
|
|
807
|
-
//
|
|
808
|
-
const defaultConfigPath = BOT_CONFIG_FILE;
|
|
846
|
+
// 统一使用 robot-*.json 格式
|
|
809
847
|
const robotConfigPath = path.join(CONFIG_DIR, `robot-${Date.now()}.json`);
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
fs.writeFileSync(defaultConfigPath, JSON.stringify(robotConfig, null, 2));
|
|
813
|
-
console.log(`\n[config] ✅ 已设为默认机器人: ${robotName}`);
|
|
814
|
-
}
|
|
815
|
-
else {
|
|
816
|
-
// 后续机器人保存为独立文件
|
|
817
|
-
fs.writeFileSync(robotConfigPath, JSON.stringify(robotConfig, null, 2));
|
|
818
|
-
console.log(`\n[config] ✅ 已添加新机器人: ${robotName}`);
|
|
819
|
-
}
|
|
848
|
+
fs.writeFileSync(robotConfigPath, JSON.stringify(robotConfig, null, 2));
|
|
849
|
+
console.log(`\n[config] ✅ 已添加机器人: ${robotName}`);
|
|
820
850
|
console.log(`[config] 用户 ID: ${targetUserId}`);
|
|
821
851
|
// 列出所有机器人
|
|
822
852
|
const robots = listAllRobots();
|
|
@@ -834,22 +864,7 @@ export async function addMcpConfig() {
|
|
|
834
864
|
// 列出所有机器人配置
|
|
835
865
|
export function listAllRobots() {
|
|
836
866
|
const robots = [];
|
|
837
|
-
//
|
|
838
|
-
if (fs.existsSync(BOT_CONFIG_FILE)) {
|
|
839
|
-
try {
|
|
840
|
-
const config = JSON.parse(fs.readFileSync(BOT_CONFIG_FILE, 'utf-8'));
|
|
841
|
-
const name = config.nameTag || `机器人-${config.botId?.slice(0, 8) || 'unknown'}`;
|
|
842
|
-
robots.push({
|
|
843
|
-
name,
|
|
844
|
-
botId: config.botId,
|
|
845
|
-
targetUserId: config.targetUserId,
|
|
846
|
-
});
|
|
847
|
-
}
|
|
848
|
-
catch {
|
|
849
|
-
// ignore
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
// 其他机器人配置
|
|
867
|
+
// 所有机器人配置(统一 robot-*.json 格式)
|
|
853
868
|
if (fs.existsSync(CONFIG_DIR)) {
|
|
854
869
|
const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
|
|
855
870
|
for (const file of files) {
|
|
@@ -860,6 +875,7 @@ export function listAllRobots() {
|
|
|
860
875
|
name,
|
|
861
876
|
botId: config.botId,
|
|
862
877
|
targetUserId: config.targetUserId,
|
|
878
|
+
...(config.doc_mcp_url ? { doc_mcp_url: config.doc_mcp_url } : {}),
|
|
863
879
|
});
|
|
864
880
|
}
|
|
865
881
|
catch {
|
|
@@ -869,6 +885,35 @@ export function listAllRobots() {
|
|
|
869
885
|
}
|
|
870
886
|
return robots;
|
|
871
887
|
}
|
|
888
|
+
// 获取指定机器人(或唯一机器人)的文档 MCP URL
|
|
889
|
+
export function getDocMcpUrl(robotName) {
|
|
890
|
+
const robots = listAllRobots();
|
|
891
|
+
const robotsWithDoc = robots.filter(r => r.doc_mcp_url);
|
|
892
|
+
if (robotsWithDoc.length === 0) {
|
|
893
|
+
return {
|
|
894
|
+
url: null,
|
|
895
|
+
error: '未配置文档 MCP URL。请运行 `npx @vrs-soft/wecom-aibot-mcp --add` 添加机器人时填写文档 MCP URL,或通过 `add_robot_config` 工具设置。',
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
if (robotName) {
|
|
899
|
+
const robot = robotsWithDoc.find(r => r.name === robotName);
|
|
900
|
+
if (!robot) {
|
|
901
|
+
return {
|
|
902
|
+
url: null,
|
|
903
|
+
error: `未找到名为 "${robotName}" 的机器人,或该机器人未配置文档 MCP URL。已配置文档能力的机器人: ${robotsWithDoc.map(r => r.name).join(', ')}`,
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
return { url: robot.doc_mcp_url };
|
|
907
|
+
}
|
|
908
|
+
if (robotsWithDoc.length === 1) {
|
|
909
|
+
return { url: robotsWithDoc[0].doc_mcp_url };
|
|
910
|
+
}
|
|
911
|
+
// 多个机器人有 doc_mcp_url,需要用户指定
|
|
912
|
+
return {
|
|
913
|
+
url: null,
|
|
914
|
+
error: `有多个机器人配置了文档 MCP URL,请通过 robot_name 参数指定使用哪个机器人。已配置文档能力的机器人: ${robotsWithDoc.map(r => r.name).join(', ')}`,
|
|
915
|
+
};
|
|
916
|
+
}
|
|
872
917
|
// 写入 MCP 工具权限 + 注册 PermissionRequest hook 到 Claude settings
|
|
873
918
|
function writeMcpPermissions() {
|
|
874
919
|
try {
|
|
@@ -919,7 +964,7 @@ export function ensureHookInstalled() {
|
|
|
919
964
|
writeTaskCompletedHookScript();
|
|
920
965
|
}
|
|
921
966
|
// 确保所有全局配置已写入(强制覆盖,不依赖智能体)
|
|
922
|
-
export function ensureGlobalConfigs(mode = 'full') {
|
|
967
|
+
export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
|
|
923
968
|
ensureConfigDir();
|
|
924
969
|
// 读取已安装版本
|
|
925
970
|
let previousVersion;
|
|
@@ -942,6 +987,64 @@ export function ensureGlobalConfigs(mode = 'full') {
|
|
|
942
987
|
fs.writeFileSync(VERSION_FILE, JSON.stringify({ version: VERSION, installedAt: Date.now() }, null, 2));
|
|
943
988
|
return { upgraded, previousVersion };
|
|
944
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
|
+
}
|
|
945
1048
|
// 1. 强制写入 MCP 配置到 ~/.claude.json
|
|
946
1049
|
let claudeConfig = {};
|
|
947
1050
|
if (fs.existsSync(CLAUDE_CONFIG_FILE)) {
|
|
@@ -958,11 +1061,17 @@ export function ensureGlobalConfigs(mode = 'full') {
|
|
|
958
1061
|
console.log('[config] 请设置环境变量: MCP_URL=http://远程IP:18963');
|
|
959
1062
|
return { upgraded: false, previousVersion };
|
|
960
1063
|
}
|
|
961
|
-
// 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
|
+
}
|
|
962
1071
|
claudeConfig.mcpServers['wecom-aibot-channel'] = {
|
|
963
1072
|
command: 'node',
|
|
964
|
-
args: [
|
|
965
|
-
env:
|
|
1073
|
+
args: [binPath, '--channel'],
|
|
1074
|
+
env: channelEnv,
|
|
966
1075
|
};
|
|
967
1076
|
console.log(`[config] Channel-only 模式:Channel MCP 使用本地路径`);
|
|
968
1077
|
}
|
|
@@ -972,10 +1081,11 @@ export function ensureGlobalConfigs(mode = 'full') {
|
|
|
972
1081
|
type: 'http',
|
|
973
1082
|
url: 'http://127.0.0.1:18963/mcp',
|
|
974
1083
|
};
|
|
975
|
-
// Channel MCP
|
|
1084
|
+
// Channel MCP 配置:使用当前模块路径
|
|
1085
|
+
const binPath = path.join(__dirname, 'bin.js');
|
|
976
1086
|
claudeConfig.mcpServers['wecom-aibot-channel'] = {
|
|
977
1087
|
command: 'node',
|
|
978
|
-
args: [
|
|
1088
|
+
args: [binPath, '--channel'],
|
|
979
1089
|
env: { MCP_URL: 'http://127.0.0.1:18963' },
|
|
980
1090
|
};
|
|
981
1091
|
console.log(`[config] full 模式:Channel MCP 使用本地路径`);
|
|
@@ -990,7 +1100,69 @@ export function ensureGlobalConfigs(mode = 'full') {
|
|
|
990
1100
|
console.log(`[config] 已记录版本号: ${VERSION}`);
|
|
991
1101
|
return { upgraded, previousVersion };
|
|
992
1102
|
}
|
|
993
|
-
//
|
|
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
|
+
}
|
|
994
1166
|
export function saveConfig(config, instanceName) {
|
|
995
1167
|
ensureConfigDir(); // 确保运行时文件目录存在
|
|
996
1168
|
// 写入 MCP Server 配置到 ~/.claude.json
|
|
@@ -1075,7 +1247,8 @@ export async function runConfigWizard() {
|
|
|
1075
1247
|
else {
|
|
1076
1248
|
console.log('\n请选择要操作的机器人:\n');
|
|
1077
1249
|
robots.forEach((robot, idx) => {
|
|
1078
|
-
|
|
1250
|
+
const docTag = robot.doc_mcp_url ? ' [文档✅]' : '';
|
|
1251
|
+
console.log(` ${idx + 1}. ${robot.name} (Bot ID: ${robot.botId.slice(0, 12)}...)${docTag}`);
|
|
1079
1252
|
});
|
|
1080
1253
|
console.log(` ${robots.length + 1}. 添加新机器人\n`);
|
|
1081
1254
|
const choice = await question(rl, '请输入序号: ');
|
|
@@ -1150,12 +1323,23 @@ export async function runConfigWizard() {
|
|
|
1150
1323
|
}
|
|
1151
1324
|
}
|
|
1152
1325
|
}
|
|
1153
|
-
//
|
|
1326
|
+
// 第五步:文档 MCP URL(可选)
|
|
1327
|
+
const currentDocUrl = targetRobot?.doc_mcp_url ?? '';
|
|
1328
|
+
const docUrlPrompt = currentDocUrl
|
|
1329
|
+
? `文档 MCP URL(当前: ${currentDocUrl.slice(0, 40)}...,留空保持不变): `
|
|
1330
|
+
: '文档 MCP URL(可选,企业微信管理后台获取,留空跳过): ';
|
|
1331
|
+
let docMcpUrl = await question(rl, docUrlPrompt);
|
|
1332
|
+
if (!docMcpUrl && currentDocUrl) {
|
|
1333
|
+
docMcpUrl = currentDocUrl;
|
|
1334
|
+
console.log('[config] 保持原文档 MCP URL');
|
|
1335
|
+
}
|
|
1336
|
+
// 第六步:目标用户(稍后通过消息自动识别)
|
|
1154
1337
|
console.log('\n─────────────────────────────────────');
|
|
1155
1338
|
console.log('配置确认:');
|
|
1156
1339
|
console.log(` 机器人名称: ${robotName}`);
|
|
1157
1340
|
console.log(` Bot ID: ${botId}`);
|
|
1158
1341
|
console.log(` Secret: ${secret.slice(0, 8)}...${secret.slice(-4)}`);
|
|
1342
|
+
console.log(` 文档 MCP: ${docMcpUrl ? '✅ 已配置' : '(未配置)'}`);
|
|
1159
1343
|
console.log(` 目标用户: (将通过消息自动识别)`);
|
|
1160
1344
|
console.log('─────────────────────────────────────\n');
|
|
1161
1345
|
const confirm = await question(rl, '确认配置?(Y/n): ');
|
|
@@ -1167,8 +1351,9 @@ export async function runConfigWizard() {
|
|
|
1167
1351
|
const config = {
|
|
1168
1352
|
botId,
|
|
1169
1353
|
secret,
|
|
1170
|
-
targetUserId: '', //
|
|
1354
|
+
targetUserId: targetRobot?.targetUserId || '', // 修改时保留原值,新建时稍后识别
|
|
1171
1355
|
nameTag: robotName,
|
|
1356
|
+
...(docMcpUrl ? { doc_mcp_url: docMcpUrl } : {}),
|
|
1172
1357
|
};
|
|
1173
1358
|
// 如果是修改现有机器人,返回其 instanceName(用于删除旧配置)
|
|
1174
1359
|
const instanceName = targetRobot ? targetRobot.name : 'wecom-aibot';
|
|
@@ -1180,15 +1365,6 @@ export async function runConfigWizard() {
|
|
|
1180
1365
|
}
|
|
1181
1366
|
// 查找机器人配置文件路径(按名称)
|
|
1182
1367
|
export function findRobotConfigFile(robotName) {
|
|
1183
|
-
// 检查默认配置文件
|
|
1184
|
-
if (fs.existsSync(BOT_CONFIG_FILE)) {
|
|
1185
|
-
const config = JSON.parse(fs.readFileSync(BOT_CONFIG_FILE, 'utf-8'));
|
|
1186
|
-
const name = config.nameTag || `机器人-${config.botId?.slice(0, 8) || 'unknown'}`;
|
|
1187
|
-
if (name === robotName) {
|
|
1188
|
-
return BOT_CONFIG_FILE;
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
// 检查其他机器人配置文件
|
|
1192
1368
|
if (fs.existsSync(CONFIG_DIR)) {
|
|
1193
1369
|
const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
|
|
1194
1370
|
for (const file of files) {
|
|
@@ -1204,14 +1380,6 @@ export function findRobotConfigFile(robotName) {
|
|
|
1204
1380
|
}
|
|
1205
1381
|
// 查找机器人配置文件路径(按 botId)
|
|
1206
1382
|
export function findRobotConfigFileByBotId(botId) {
|
|
1207
|
-
// 检查默认配置文件
|
|
1208
|
-
if (fs.existsSync(BOT_CONFIG_FILE)) {
|
|
1209
|
-
const config = JSON.parse(fs.readFileSync(BOT_CONFIG_FILE, 'utf-8'));
|
|
1210
|
-
if (config.botId === botId) {
|
|
1211
|
-
return BOT_CONFIG_FILE;
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
// 检查其他机器人配置文件
|
|
1215
1383
|
if (fs.existsSync(CONFIG_DIR)) {
|
|
1216
1384
|
const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
|
|
1217
1385
|
for (const file of files) {
|
|
@@ -1283,7 +1451,7 @@ export async function detectUserIdFromMessage(client, timeoutSeconds = 60) {
|
|
|
1283
1451
|
*
|
|
1284
1452
|
* 优先级:
|
|
1285
1453
|
* 1. 环境变量(WECOM_BOT_ID, WECOM_SECRET, WECOM_TARGET_USER)
|
|
1286
|
-
* 2. 保存的配置文件(~/.wecom-aibot-mcp/
|
|
1454
|
+
* 2. 保存的配置文件(~/.wecom-aibot-mcp/robot-*.json)
|
|
1287
1455
|
* 3. 运行配置向导
|
|
1288
1456
|
*/
|
|
1289
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) {
|