@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.
@@ -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/config.json)
75
+ * 2. 保存的配置文件(~/.wecom-aibot-mcp/robot-*.json)
62
76
  * 3. 运行配置向导
63
77
  */
64
78
  export declare function getOrInitConfig(): Promise<WecomConfig>;
@@ -4,7 +4,7 @@
4
4
  * 首次运行时引导用户配置 Bot ID、Secret 和默认目标用户
5
5
  *
6
6
  * 配置存储位置:
7
- * - 机器人配置:~/.wecom-aibot-mcp/config.json
7
+ * - 机器人配置:~/.wecom-aibot-mcp/robot-*.json
8
8
  * - MCP 配置:~/.claude.json (仅 URL)
9
9
  */
10
10
  import * as readline from 'readline';
@@ -14,8 +14,8 @@ import * as os from 'os';
14
14
  import { fileURLToPath } from 'url';
15
15
  import { logger } from './logger.js';
16
16
  const CONFIG_DIR = path.join(os.homedir(), '.wecom-aibot-mcp');
17
- const BOT_CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
18
17
  const VERSION_FILE = path.join(CONFIG_DIR, 'version.json');
18
+ const SERVER_CONFIG_FILE = path.join(CONFIG_DIR, 'server.json'); // HTTP Server 配置(auth token 等)
19
19
  const CLAUDE_CONFIG_FILE = path.join(os.homedir(), '.claude.json');
20
20
  const CLAUDE_SETTINGS_FILE = path.join(os.homedir(), '.claude', 'settings.local.json');
21
21
  const HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'permission-hook.sh');
@@ -23,8 +23,8 @@ const TASK_COMPLETED_HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'task-completed-ho
23
23
  // Skill 模板路径(包内)- 使用 fileURLToPath 确保跨平台兼容
24
24
  const __filename = fileURLToPath(import.meta.url);
25
25
  const __dirname = path.dirname(__filename);
26
- // 版本号(从 package.json 读取)
27
- const VERSION = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8')).version;
26
+ // 版本号(从 package.json 读取,全局共享)
27
+ export const VERSION = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8')).version;
28
28
  const SKILL_TEMPLATE_DIR = path.join(__dirname, '..', 'skills', 'headless-mode');
29
29
  const SKILL_TEMPLATE_FILE = path.join(SKILL_TEMPLATE_DIR, 'SKILL.md');
30
30
  // MCP 工具权限列表(需要预授权以避免 headless 模式阻断)
@@ -37,19 +37,26 @@ function ensureConfigDir() {
37
37
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
38
38
  }
39
39
  }
40
- // 从 ~/.wecom-aibot-mcp/config.json 读取已保存的配置
40
+ // 从 ~/.wecom-aibot-mcp/robot-*.json 读取第一个有效配置
41
41
  export function loadConfig() {
42
42
  try {
43
- // 从机器人配置文件读取
44
- if (fs.existsSync(BOT_CONFIG_FILE)) {
45
- const content = fs.readFileSync(BOT_CONFIG_FILE, 'utf-8');
43
+ if (!fs.existsSync(CONFIG_DIR))
44
+ return null;
45
+ const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
46
+ for (const file of files) {
47
+ const content = fs.readFileSync(path.join(CONFIG_DIR, file), 'utf-8');
46
48
  const config = JSON.parse(content);
47
49
  if (config.botId && config.secret && config.targetUserId) {
48
- return {
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
- let isDefault = false;
138
- // 检查是否是默认机器人(config.json)
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
- if (isDefault) {
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
- // 删除整个配置目录(包括 config.json、robot-*.json、hook 脚本、日志等)
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
- if (fs.existsSync(BOT_CONFIG_FILE)) {
669
- // 有默认配置,创建新的 robot-*.json 文件
670
- const newConfigPath = path.join(CONFIG_DIR, `robot-${Date.now()}.json`);
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/config.json:');
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
- if (!fs.existsSync(defaultConfigPath)) {
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
- // 主配置文件(config.json
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: ['/Volumes/Mac_Data/VScode/wecom-aibot-mcp/dist/bin.js', '--channel'],
965
- env: { MCP_URL: mcpUrl },
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: ['/Volumes/Mac_Data/VScode/wecom-aibot-mcp/dist/bin.js', '--channel'],
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
- // 保存配置(直接写入 ~/.claude.json
1103
+ // 远程安装向导(交互式输入 URL + Token
1104
+ export async function runRemoteInstallWizard() {
1105
+ const rl = createRL();
1106
+ const CLAUDE_CONFIG_FILE = path.join(os.homedir(), '.claude.json');
1107
+ try {
1108
+ // 检测本机是否有 ~/.claude.json(判断是 Client 还是 Server)
1109
+ const hasClaudeConfig = fs.existsSync(CLAUDE_CONFIG_FILE);
1110
+ if (!hasClaudeConfig) {
1111
+ // Server 安装模式:本机无 ~/.claude.json,作为远程服务器
1112
+ console.log('\n检测到本机无 ~/.claude.json → Server 安装模式\n');
1113
+ console.log(' Server 端只需启动 HTTP MCP Server,不写入 MCP 配置');
1114
+ console.log(' Client 端在其他机器上安装\n');
1115
+ const confirm = await question(rl, '确认作为远程 Server 安装?(y/N): ');
1116
+ if (confirm.toLowerCase() !== 'y') {
1117
+ console.log('[config] 已取消');
1118
+ return null;
1119
+ }
1120
+ // Server 不写入 ~/.claude.json,只提示启动命令
1121
+ console.log('\n─────────────────────────────────────');
1122
+ console.log('Server 安装完成!');
1123
+ console.log(' 启动命令: npx @anthropic/wecom-aibot-mcp --http-only --start');
1124
+ console.log(' 或者: npm run start:http');
1125
+ console.log('─────────────────────────────────────\n');
1126
+ console.log('[config] Client 端请在其他机器运行安装程序连接本服务器\n');
1127
+ return 'server';
1128
+ }
1129
+ // Client 安装模式:本机有 ~/.claude.json,作为客户端
1130
+ console.log('\n检测到本机有 ~/.claude.json → Client 安装模式\n');
1131
+ console.log(' 请选择连接远程服务器的方式:\n');
1132
+ console.log(' 1. 仅 HTTP MCP(轮询模式)');
1133
+ console.log(' 2. HTTP MCP + Channel MCP(推荐,消息自动唤醒)\n');
1134
+ const choice = await question(rl, '请选择 (1/2): ');
1135
+ const mode = choice === '2' ? 'remote-channel' : 'remote';
1136
+ let serverUrl = await question(rl, '远程服务器地址(如 https://your-server:18963): ');
1137
+ while (!serverUrl) {
1138
+ console.log('服务器地址不能为空');
1139
+ serverUrl = await question(rl, '远程服务器地址: ');
1140
+ }
1141
+ // 标准化 URL(去掉尾部斜杠)
1142
+ serverUrl = serverUrl.replace(/\/+$/, '');
1143
+ let token = await question(rl, 'Auth Token(必填,远程服务器需配置相同 Token): ');
1144
+ while (!token) {
1145
+ console.log('Auth Token 不能为空');
1146
+ token = await question(rl, 'Auth Token: ');
1147
+ }
1148
+ // 写入配置
1149
+ ensureGlobalConfigs(mode, { url: serverUrl, token });
1150
+ console.log('\n─────────────────────────────────────');
1151
+ console.log('Client 配置完成!');
1152
+ console.log(` 模式: ${mode === 'remote-channel' ? 'HTTP + Channel' : '仅 HTTP'}`);
1153
+ console.log(` 服务器: ${serverUrl}`);
1154
+ console.log(` Auth Token: ${token.slice(0, 8)}...${token.slice(-4)}`);
1155
+ console.log('─────────────────────────────────────\n');
1156
+ if (mode === 'remote-channel') {
1157
+ console.log('Channel 模式优势:微信消息自动唤醒 agent,无需主动轮询');
1158
+ }
1159
+ console.log('[config] 请重启 Claude Code 以加载最新配置\n');
1160
+ return mode;
1161
+ }
1162
+ finally {
1163
+ rl.close();
1164
+ }
1165
+ }
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
- console.log(` ${idx + 1}. ${robot.name} (Bot ID: ${robot.botId.slice(0, 12)}...)`);
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/config.json)
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
- // 搜索所有配置文件(config.json + robot-*.json)
33
- const allFiles = ['config.json', ...fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'))];
32
+ // 搜索所有机器人配置文件(robot-*.json)
33
+ const allFiles = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith('robot-') && f.endsWith('.json'));
34
34
  const files = allFiles.filter(f => fs.existsSync(path.join(CONFIG_DIR, f)));
35
35
  // 先按 botId 精确匹配找 secret
36
36
  for (const file of files) {