coding-tool-x 3.3.5 → 3.3.6
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/dist/web/assets/{Analytics-B6CWdkhx.js → Analytics-TtaduRqL.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BW6LEgd8.js → ConfigTemplates-BP2lLBMN.js} +1 -1
- package/dist/web/assets/{Home-B2B2gS2-.js → Home-CbbyopS-.js} +1 -1
- package/dist/web/assets/{PluginManager-Bqc7ldY-.js → PluginManager-HmISlyMK.js} +1 -1
- package/dist/web/assets/{ProjectList-BFdZZm_8.js → ProjectList-DoN8Hjbu.js} +1 -1
- package/dist/web/assets/{SessionList-B_Tp37kM.js → SessionList-Da8BYzNi.js} +1 -1
- package/dist/web/assets/{SkillManager-ul2rcS3o.js → SkillManager-DqLAXh9o.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Dp5Jvdtu.js → WorkspaceManager-B_TxOgPW.js} +1 -1
- package/dist/web/assets/{index-CSBDZxYn.js → index-By3mDEvx.js} +2 -2
- package/dist/web/assets/{index-DxRneGyu.css → index-CsWInMQV.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/commands/update.js +21 -6
- package/src/server/api/config-export.js +21 -2
- package/src/server/services/config-export-service.js +89 -22
- package/src/server/services/mcp-client.js +101 -14
- package/src/server/services/mcp-service.js +91 -4
package/dist/web/index.html
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
<link rel="icon" href="/favicon.ico">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<title>CC-TOOL - ClaudeCode增强工作助手</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-By3mDEvx.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/markdown-C9MYpaSi.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vue-vendor-DET08QYg.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/vendors-DMjSfzlv.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/naive-ui-CxpuzdjU.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/icons-B29onFfZ.js">
|
|
14
14
|
<link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CsWInMQV.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="app"></div>
|
package/package.json
CHANGED
package/src/commands/update.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
const { spawn } = require('child_process');
|
|
1
|
+
const { spawn, execFile } = require('child_process');
|
|
2
2
|
const { promisify } = require('util');
|
|
3
|
-
const { exec } = require('child_process');
|
|
4
3
|
const semver = require('semver');
|
|
5
4
|
const chalk = require('chalk');
|
|
6
5
|
const packageInfo = require('../../package.json');
|
|
7
6
|
|
|
8
|
-
const
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
|
|
9
|
+
function resolveNpmCommand() {
|
|
10
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
11
|
+
}
|
|
9
12
|
|
|
10
13
|
async function getLatestVersion(packageName) {
|
|
11
|
-
const
|
|
14
|
+
const npmCommand = resolveNpmCommand();
|
|
15
|
+
const { stdout } = await execFileAsync(
|
|
16
|
+
npmCommand,
|
|
17
|
+
['view', packageName, 'version', '--json'],
|
|
18
|
+
{ timeout: 15000 }
|
|
19
|
+
);
|
|
12
20
|
const parsed = JSON.parse(stdout.trim());
|
|
13
21
|
if (typeof parsed === 'string') return parsed;
|
|
14
22
|
throw new Error('无法解析 npm 返回的版本号');
|
|
@@ -16,11 +24,18 @@ async function getLatestVersion(packageName) {
|
|
|
16
24
|
|
|
17
25
|
function runNpmInstall(packageName, version) {
|
|
18
26
|
return new Promise((resolve, reject) => {
|
|
19
|
-
const
|
|
27
|
+
const npmCommand = resolveNpmCommand();
|
|
28
|
+
const child = spawn(npmCommand, ['install', '-g', `${packageName}@${version}`], {
|
|
20
29
|
stdio: 'inherit'
|
|
21
30
|
});
|
|
22
31
|
|
|
23
|
-
child.on('error',
|
|
32
|
+
child.on('error', (err) => {
|
|
33
|
+
if (err && err.code === 'ENOENT') {
|
|
34
|
+
reject(new Error(`命令 "${npmCommand}" 未找到,请确认 Node.js/npm 已安装并在 PATH 中`));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
reject(err);
|
|
38
|
+
});
|
|
24
39
|
child.on('exit', (code) => {
|
|
25
40
|
if (code === 0) {
|
|
26
41
|
resolve();
|
|
@@ -18,13 +18,32 @@ function parseConfigZip(buffer) {
|
|
|
18
18
|
return JSON.parse(content);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function resolveChannelsByType(exportData) {
|
|
22
|
+
const raw = exportData?.data || {};
|
|
23
|
+
const typed = raw.channelsByType && typeof raw.channelsByType === 'object' ? raw.channelsByType : {};
|
|
24
|
+
return {
|
|
25
|
+
claude: Array.isArray(typed.claude) ? typed.claude : (Array.isArray(raw.channels) ? raw.channels : []),
|
|
26
|
+
codex: Array.isArray(typed.codex) ? typed.codex : [],
|
|
27
|
+
gemini: Array.isArray(typed.gemini) ? typed.gemini : [],
|
|
28
|
+
opencode: Array.isArray(typed.opencode) ? typed.opencode : []
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
function buildPreviewSummary(data) {
|
|
33
|
+
const channelsByType = resolveChannelsByType(data);
|
|
34
|
+
const allChannels = [
|
|
35
|
+
...channelsByType.claude.map(c => ({ ...c, type: c.type || 'claude' })),
|
|
36
|
+
...channelsByType.codex.map(c => ({ ...c, type: c.type || 'codex' })),
|
|
37
|
+
...channelsByType.gemini.map(c => ({ ...c, type: c.type || 'gemini' })),
|
|
38
|
+
...channelsByType.opencode.map(c => ({ ...c, type: c.type || 'opencode' }))
|
|
39
|
+
];
|
|
40
|
+
|
|
22
41
|
return {
|
|
23
42
|
version: data.version,
|
|
24
43
|
exportedAt: data.exportedAt,
|
|
25
44
|
counts: {
|
|
26
45
|
configTemplates: (data.data.configTemplates || []).length,
|
|
27
|
-
channels:
|
|
46
|
+
channels: allChannels.length,
|
|
28
47
|
plugins: (data.data.plugins || []).length
|
|
29
48
|
},
|
|
30
49
|
items: {
|
|
@@ -33,7 +52,7 @@ function buildPreviewSummary(data) {
|
|
|
33
52
|
name: t.name,
|
|
34
53
|
description: t.description
|
|
35
54
|
})),
|
|
36
|
-
channels:
|
|
55
|
+
channels: allChannels.map(c => ({
|
|
37
56
|
id: c.id,
|
|
38
57
|
name: c.name,
|
|
39
58
|
type: c.type
|
|
@@ -8,6 +8,9 @@ const path = require('path');
|
|
|
8
8
|
const AdmZip = require('adm-zip');
|
|
9
9
|
const configTemplatesService = require('./config-templates-service');
|
|
10
10
|
const channelsService = require('./channels');
|
|
11
|
+
const codexChannelsService = require('./codex-channels');
|
|
12
|
+
const geminiChannelsService = require('./gemini-channels');
|
|
13
|
+
const opencodeChannelsService = require('./opencode-channels');
|
|
11
14
|
const { AgentsService } = require('./agents-service');
|
|
12
15
|
const { CommandsService } = require('./commands-service');
|
|
13
16
|
const { SkillService } = require('./skill-service');
|
|
@@ -414,6 +417,14 @@ function writeTextFile(baseDir, relativePath, content, overwrite) {
|
|
|
414
417
|
return 'success';
|
|
415
418
|
}
|
|
416
419
|
|
|
420
|
+
function getAllChannelsByType() {
|
|
421
|
+
const claude = channelsService.getAllChannels() || [];
|
|
422
|
+
const codex = codexChannelsService.getChannels()?.channels || [];
|
|
423
|
+
const gemini = geminiChannelsService.getChannels()?.channels || [];
|
|
424
|
+
const opencode = opencodeChannelsService.getChannels()?.channels || [];
|
|
425
|
+
return { claude, codex, gemini, opencode };
|
|
426
|
+
}
|
|
427
|
+
|
|
417
428
|
/**
|
|
418
429
|
* 导出所有配置为JSON
|
|
419
430
|
* @returns {Object} 配置导出对象
|
|
@@ -424,8 +435,9 @@ function exportAllConfigs() {
|
|
|
424
435
|
const allConfigTemplates = configTemplatesService.getAllTemplates();
|
|
425
436
|
const customConfigTemplates = allConfigTemplates.filter(t => !t.isBuiltin);
|
|
426
437
|
|
|
427
|
-
//
|
|
428
|
-
const
|
|
438
|
+
// 获取所有频道配置(向后兼容:channels 仍保留 Claude 渠道)
|
|
439
|
+
const channelsByType = getAllChannelsByType();
|
|
440
|
+
const channels = channelsByType.claude || [];
|
|
429
441
|
|
|
430
442
|
// 获取工作区配置
|
|
431
443
|
const workspaceService = require('./workspace-service');
|
|
@@ -514,6 +526,7 @@ function exportAllConfigs() {
|
|
|
514
526
|
data: {
|
|
515
527
|
configTemplates: customConfigTemplates,
|
|
516
528
|
channels: channels || [],
|
|
529
|
+
channelsByType,
|
|
517
530
|
workspaces: workspaces || { workspaces: [] },
|
|
518
531
|
favorites: favorites || { favorites: [] },
|
|
519
532
|
agents: agents || [],
|
|
@@ -602,6 +615,7 @@ async function importConfigs(importData, options = {}) {
|
|
|
602
615
|
const {
|
|
603
616
|
configTemplates = [],
|
|
604
617
|
channels = [],
|
|
618
|
+
channelsByType = null,
|
|
605
619
|
workspaces = null,
|
|
606
620
|
favorites = null,
|
|
607
621
|
agents = [],
|
|
@@ -616,6 +630,16 @@ async function importConfigs(importData, options = {}) {
|
|
|
616
630
|
claudeHooks = null
|
|
617
631
|
} = importData.data;
|
|
618
632
|
|
|
633
|
+
const hasTypedChannels = channelsByType && typeof channelsByType === 'object';
|
|
634
|
+
const importChannelsByType = {
|
|
635
|
+
claude: hasTypedChannels && Array.isArray(channelsByType.claude)
|
|
636
|
+
? channelsByType.claude
|
|
637
|
+
: (Array.isArray(channels) ? channels : []),
|
|
638
|
+
codex: hasTypedChannels && Array.isArray(channelsByType.codex) ? channelsByType.codex : [],
|
|
639
|
+
gemini: hasTypedChannels && Array.isArray(channelsByType.gemini) ? channelsByType.gemini : [],
|
|
640
|
+
opencode: hasTypedChannels && Array.isArray(channelsByType.opencode) ? channelsByType.opencode : []
|
|
641
|
+
};
|
|
642
|
+
|
|
619
643
|
// 导入配置模板
|
|
620
644
|
for (const template of configTemplates) {
|
|
621
645
|
try {
|
|
@@ -643,29 +667,72 @@ async function importConfigs(importData, options = {}) {
|
|
|
643
667
|
}
|
|
644
668
|
}
|
|
645
669
|
|
|
646
|
-
//
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
670
|
+
// 导入频道配置(兼容旧结构 channels 和新结构 channelsByType)
|
|
671
|
+
const importTypedChannels = (type, service, createChannel, findExisting = null) => {
|
|
672
|
+
const sourceChannels = importChannelsByType[type];
|
|
673
|
+
for (const channel of sourceChannels) {
|
|
674
|
+
try {
|
|
675
|
+
const existingChannels = service.getChannels
|
|
676
|
+
? (service.getChannels()?.channels || [])
|
|
677
|
+
: (service.getAllChannels?.() || []);
|
|
678
|
+
const existing = typeof findExisting === 'function'
|
|
679
|
+
? findExisting(existingChannels, channel)
|
|
680
|
+
: existingChannels.find(c => c.id === channel.id);
|
|
681
|
+
|
|
682
|
+
if (existing && !overwrite) {
|
|
683
|
+
results.channels.skipped++;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
656
686
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
687
|
+
if (existing && overwrite) {
|
|
688
|
+
service.updateChannel(existing.id, { ...channel, id: existing.id });
|
|
689
|
+
} else {
|
|
690
|
+
createChannel(channel);
|
|
691
|
+
}
|
|
692
|
+
results.channels.success++;
|
|
693
|
+
} catch (err) {
|
|
694
|
+
console.error(`[ConfigImport] 导入${type}频道失败: ${channel.name}`, err);
|
|
695
|
+
results.channels.failed++;
|
|
662
696
|
}
|
|
663
|
-
results.channels.success++;
|
|
664
|
-
} catch (err) {
|
|
665
|
-
console.error(`[ConfigImport] 导入频道失败: ${channel.name}`, err);
|
|
666
|
-
results.channels.failed++;
|
|
667
697
|
}
|
|
668
|
-
}
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
importTypedChannels('claude', channelsService, channel => {
|
|
701
|
+
const { name, baseUrl, apiKey, websiteUrl, ...extraConfig } = channel;
|
|
702
|
+
channelsService.createChannel(name, baseUrl, apiKey, websiteUrl, extraConfig);
|
|
703
|
+
}, (existingChannels, channel) => existingChannels.find(c => c.id === channel.id));
|
|
704
|
+
|
|
705
|
+
importTypedChannels('codex', codexChannelsService, channel => {
|
|
706
|
+
const {
|
|
707
|
+
name,
|
|
708
|
+
providerKey,
|
|
709
|
+
baseUrl,
|
|
710
|
+
apiKey,
|
|
711
|
+
wireApi,
|
|
712
|
+
...extraConfig
|
|
713
|
+
} = channel;
|
|
714
|
+
codexChannelsService.createChannel(name, providerKey, baseUrl, apiKey, wireApi, extraConfig);
|
|
715
|
+
}, (existingChannels, channel) => existingChannels.find(c =>
|
|
716
|
+
(channel.id && c.id === channel.id) ||
|
|
717
|
+
(channel.providerKey && c.providerKey === channel.providerKey)
|
|
718
|
+
));
|
|
719
|
+
|
|
720
|
+
importTypedChannels('gemini', geminiChannelsService, channel => {
|
|
721
|
+
const { name, baseUrl, apiKey, model, ...extraConfig } = channel;
|
|
722
|
+
geminiChannelsService.createChannel(name, baseUrl, apiKey, model, extraConfig);
|
|
723
|
+
}, (existingChannels, channel) => existingChannels.find(c =>
|
|
724
|
+
(channel.id && c.id === channel.id) ||
|
|
725
|
+
(channel.name && c.name === channel.name)
|
|
726
|
+
));
|
|
727
|
+
|
|
728
|
+
importTypedChannels('opencode', opencodeChannelsService, channel => {
|
|
729
|
+
const { name, baseUrl, apiKey, ...extraConfig } = channel;
|
|
730
|
+
opencodeChannelsService.createChannel(name, baseUrl, apiKey, extraConfig);
|
|
731
|
+
}, (existingChannels, channel) => existingChannels.find(c =>
|
|
732
|
+
(channel.id && c.id === channel.id) ||
|
|
733
|
+
(channel.providerKey && c.providerKey === channel.providerKey) ||
|
|
734
|
+
(channel.name && channel.baseUrl && c.name === channel.name && c.baseUrl === channel.baseUrl)
|
|
735
|
+
));
|
|
669
736
|
|
|
670
737
|
// 导入工作区配置
|
|
671
738
|
if (workspaces && overwrite) {
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
const { spawn } = require('child_process');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
17
19
|
const http = require('http');
|
|
18
20
|
const https = require('https');
|
|
19
21
|
const { EventEmitter } = require('events');
|
|
@@ -22,6 +24,89 @@ const DEFAULT_TIMEOUT = 10000; // 10 seconds
|
|
|
22
24
|
const JSONRPC_VERSION = '2.0';
|
|
23
25
|
const MCP_PROTOCOL_VERSION = '2024-11-05';
|
|
24
26
|
|
|
27
|
+
function getPathEnvKey(envObj = {}) {
|
|
28
|
+
return Object.keys(envObj).find(key => key.toLowerCase() === 'path') || 'PATH';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function stripWrappingQuotes(value) {
|
|
32
|
+
const text = String(value || '').trim();
|
|
33
|
+
if (!text) return '';
|
|
34
|
+
if (
|
|
35
|
+
(text.startsWith('"') && text.endsWith('"')) ||
|
|
36
|
+
(text.startsWith("'") && text.endsWith("'"))
|
|
37
|
+
) {
|
|
38
|
+
return text.slice(1, -1);
|
|
39
|
+
}
|
|
40
|
+
return text;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function mergeSpawnEnv(extraEnv = {}) {
|
|
44
|
+
const mergedEnv = { ...process.env, ...extraEnv };
|
|
45
|
+
const processPathKey = getPathEnvKey(process.env);
|
|
46
|
+
const extraPathKey = getPathEnvKey(extraEnv);
|
|
47
|
+
const mergedPathKey = getPathEnvKey(mergedEnv);
|
|
48
|
+
|
|
49
|
+
const extraPath = extraEnv && typeof extraEnv[extraPathKey] === 'string'
|
|
50
|
+
? extraEnv[extraPathKey]
|
|
51
|
+
: '';
|
|
52
|
+
const processPath = process.env && typeof process.env[processPathKey] === 'string'
|
|
53
|
+
? process.env[processPathKey]
|
|
54
|
+
: '';
|
|
55
|
+
|
|
56
|
+
if (extraPath && processPath) {
|
|
57
|
+
mergedEnv[mergedPathKey] = `${extraPath}${path.delimiter}${processPath}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return mergedEnv;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveWindowsSpawnCommand(command, env, cwd) {
|
|
64
|
+
if (process.platform !== 'win32') {
|
|
65
|
+
return stripWrappingQuotes(command);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const normalizedCommand = stripWrappingQuotes(command);
|
|
69
|
+
if (!normalizedCommand) {
|
|
70
|
+
return normalizedCommand;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const hasPathSegment = /[\\/]/.test(normalizedCommand) || /^[a-zA-Z]:/.test(normalizedCommand);
|
|
74
|
+
const hasExtension = path.extname(normalizedCommand).length > 0;
|
|
75
|
+
const extensions = hasExtension ? [''] : ['.cmd', '.exe', '.bat', '.com'];
|
|
76
|
+
const resolveCandidate = (basePath) => {
|
|
77
|
+
for (const ext of extensions) {
|
|
78
|
+
const candidate = ext ? `${basePath}${ext}` : basePath;
|
|
79
|
+
if (fs.existsSync(candidate)) {
|
|
80
|
+
return candidate;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (hasPathSegment) {
|
|
87
|
+
const absoluteBasePath = path.isAbsolute(normalizedCommand)
|
|
88
|
+
? normalizedCommand
|
|
89
|
+
: path.resolve(cwd || process.cwd(), normalizedCommand);
|
|
90
|
+
return resolveCandidate(absoluteBasePath) || normalizedCommand;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const pathKey = getPathEnvKey(env || process.env);
|
|
94
|
+
const pathValue = env && typeof env[pathKey] === 'string' ? env[pathKey] : '';
|
|
95
|
+
if (!pathValue) {
|
|
96
|
+
return normalizedCommand;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const searchPaths = pathValue.split(path.delimiter).filter(Boolean);
|
|
100
|
+
for (const searchPath of searchPaths) {
|
|
101
|
+
const found = resolveCandidate(path.join(searchPath.trim(), normalizedCommand));
|
|
102
|
+
if (found) {
|
|
103
|
+
return found;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return normalizedCommand;
|
|
108
|
+
}
|
|
109
|
+
|
|
25
110
|
// ============================================================================
|
|
26
111
|
// McpClient
|
|
27
112
|
// ============================================================================
|
|
@@ -237,22 +322,19 @@ class McpClient extends EventEmitter {
|
|
|
237
322
|
reject(new McpClientError(`Connection timeout after ${this._timeout}ms`));
|
|
238
323
|
}, this._timeout);
|
|
239
324
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
// 如果用户提供的 env 中有 PATH,将其追加到系统 PATH 前面
|
|
244
|
-
if (env && env.PATH && process.env.PATH) {
|
|
245
|
-
mergedEnv.PATH = `${env.PATH}:${process.env.PATH}`;
|
|
246
|
-
}
|
|
325
|
+
const finalCwd = cwd || process.cwd();
|
|
326
|
+
const mergedEnv = mergeSpawnEnv(env || {});
|
|
327
|
+
const resolvedCommand = resolveWindowsSpawnCommand(command, mergedEnv, finalCwd);
|
|
247
328
|
|
|
248
|
-
|
|
329
|
+
try {
|
|
330
|
+
this._child = spawn(resolvedCommand, args, {
|
|
249
331
|
env: mergedEnv,
|
|
250
332
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
251
|
-
cwd:
|
|
333
|
+
cwd: finalCwd
|
|
252
334
|
});
|
|
253
335
|
} catch (err) {
|
|
254
336
|
clearTimeout(timer);
|
|
255
|
-
throw new McpClientError(`Failed to spawn "${
|
|
337
|
+
throw new McpClientError(`Failed to spawn "${resolvedCommand}": ${err.message}`);
|
|
256
338
|
}
|
|
257
339
|
|
|
258
340
|
// Once we get the spawn event (or first stdout), consider connected
|
|
@@ -274,13 +356,18 @@ class McpClient extends EventEmitter {
|
|
|
274
356
|
|
|
275
357
|
this._child.on('error', (err) => {
|
|
276
358
|
if (err.code === 'ENOENT') {
|
|
277
|
-
const
|
|
278
|
-
|
|
359
|
+
const pathKey = getPathEnvKey(mergedEnv);
|
|
360
|
+
const pathValue = typeof mergedEnv[pathKey] === 'string' ? mergedEnv[pathKey] : '';
|
|
361
|
+
const pathHint = pathValue
|
|
362
|
+
? `\n Current PATH: ${pathValue.split(path.delimiter).slice(0, 5).join(path.delimiter)}\n (showing first 5 entries)`
|
|
279
363
|
: '\n PATH is not set!';
|
|
364
|
+
const commandHint = resolvedCommand === command
|
|
365
|
+
? command
|
|
366
|
+
: `${command} (resolved: ${resolvedCommand})`;
|
|
280
367
|
settle(new McpClientError(
|
|
281
|
-
`Command "${
|
|
368
|
+
`Command "${commandHint}" not found. Please check:\n` +
|
|
282
369
|
` 1. Is "${command}" installed?\n` +
|
|
283
|
-
` 2. Try using absolute path (e.g., /usr/bin/node or $(which ${command}))\n` +
|
|
370
|
+
` 2. Try using absolute path${process.platform === 'win32' ? ' (e.g., C:\\\\Program Files\\\\nodejs\\\\npx.cmd)' : ` (e.g., /usr/bin/node or $(which ${command}))`}\n` +
|
|
284
371
|
` 3. Check your PATH environment variable${pathHint}`
|
|
285
372
|
));
|
|
286
373
|
} else {
|
|
@@ -418,6 +418,89 @@ function writeOpenCodeConfig(filePath, data) {
|
|
|
418
418
|
fs.renameSync(tempPath, filePath);
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
function getPathEnvKey(envObj = {}) {
|
|
422
|
+
return Object.keys(envObj).find(key => key.toLowerCase() === 'path') || 'PATH';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function stripWrappingQuotes(value) {
|
|
426
|
+
const text = String(value || '').trim();
|
|
427
|
+
if (!text) return '';
|
|
428
|
+
if (
|
|
429
|
+
(text.startsWith('"') && text.endsWith('"')) ||
|
|
430
|
+
(text.startsWith("'") && text.endsWith("'"))
|
|
431
|
+
) {
|
|
432
|
+
return text.slice(1, -1);
|
|
433
|
+
}
|
|
434
|
+
return text;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function mergeSpawnEnv(extraEnv = {}) {
|
|
438
|
+
const mergedEnv = { ...process.env, ...extraEnv };
|
|
439
|
+
const processPathKey = getPathEnvKey(process.env);
|
|
440
|
+
const extraPathKey = getPathEnvKey(extraEnv);
|
|
441
|
+
const mergedPathKey = getPathEnvKey(mergedEnv);
|
|
442
|
+
|
|
443
|
+
const extraPath = extraEnv && typeof extraEnv[extraPathKey] === 'string'
|
|
444
|
+
? extraEnv[extraPathKey]
|
|
445
|
+
: '';
|
|
446
|
+
const processPath = process.env && typeof process.env[processPathKey] === 'string'
|
|
447
|
+
? process.env[processPathKey]
|
|
448
|
+
: '';
|
|
449
|
+
|
|
450
|
+
if (extraPath && processPath) {
|
|
451
|
+
mergedEnv[mergedPathKey] = `${extraPath}${path.delimiter}${processPath}`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return mergedEnv;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function resolveWindowsSpawnCommand(command, env, cwd) {
|
|
458
|
+
if (process.platform !== 'win32') {
|
|
459
|
+
return stripWrappingQuotes(command);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const normalizedCommand = stripWrappingQuotes(command);
|
|
463
|
+
if (!normalizedCommand) {
|
|
464
|
+
return normalizedCommand;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const hasPathSegment = /[\\/]/.test(normalizedCommand) || /^[a-zA-Z]:/.test(normalizedCommand);
|
|
468
|
+
const hasExtension = path.extname(normalizedCommand).length > 0;
|
|
469
|
+
const extensions = hasExtension ? [''] : ['.cmd', '.exe', '.bat', '.com'];
|
|
470
|
+
const resolveCandidate = (basePath) => {
|
|
471
|
+
for (const ext of extensions) {
|
|
472
|
+
const candidate = ext ? `${basePath}${ext}` : basePath;
|
|
473
|
+
if (fs.existsSync(candidate)) {
|
|
474
|
+
return candidate;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
if (hasPathSegment) {
|
|
481
|
+
const absoluteBasePath = path.isAbsolute(normalizedCommand)
|
|
482
|
+
? normalizedCommand
|
|
483
|
+
: path.resolve(cwd || process.cwd(), normalizedCommand);
|
|
484
|
+
return resolveCandidate(absoluteBasePath) || normalizedCommand;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const pathKey = getPathEnvKey(env || process.env);
|
|
488
|
+
const pathValue = env && typeof env[pathKey] === 'string' ? env[pathKey] : '';
|
|
489
|
+
if (!pathValue) {
|
|
490
|
+
return normalizedCommand;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const searchPaths = pathValue.split(path.delimiter).filter(Boolean);
|
|
494
|
+
for (const searchPath of searchPaths) {
|
|
495
|
+
const found = resolveCandidate(path.join(searchPath.trim(), normalizedCommand));
|
|
496
|
+
if (found) {
|
|
497
|
+
return found;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return normalizedCommand;
|
|
502
|
+
}
|
|
503
|
+
|
|
421
504
|
// ============================================================================
|
|
422
505
|
// MCP 数据管理
|
|
423
506
|
// ============================================================================
|
|
@@ -1237,6 +1320,9 @@ async function testStdioServer(spec) {
|
|
|
1237
1320
|
// 检查命令是否存在
|
|
1238
1321
|
const command = spec.command;
|
|
1239
1322
|
const args = spec.args || [];
|
|
1323
|
+
const cwd = spec.cwd || process.cwd();
|
|
1324
|
+
const mergedEnv = mergeSpawnEnv(spec.env || {});
|
|
1325
|
+
const resolvedCommand = resolveWindowsSpawnCommand(command, mergedEnv, cwd);
|
|
1240
1326
|
|
|
1241
1327
|
let child;
|
|
1242
1328
|
let resolved = false;
|
|
@@ -1260,10 +1346,10 @@ async function testStdioServer(spec) {
|
|
|
1260
1346
|
};
|
|
1261
1347
|
|
|
1262
1348
|
try {
|
|
1263
|
-
child = spawn(
|
|
1264
|
-
env:
|
|
1349
|
+
child = spawn(resolvedCommand, args, {
|
|
1350
|
+
env: mergedEnv,
|
|
1265
1351
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1266
|
-
cwd
|
|
1352
|
+
cwd
|
|
1267
1353
|
});
|
|
1268
1354
|
|
|
1269
1355
|
child.stdout.on('data', (data) => {
|
|
@@ -1284,9 +1370,10 @@ async function testStdioServer(spec) {
|
|
|
1284
1370
|
|
|
1285
1371
|
child.on('error', (err) => {
|
|
1286
1372
|
if (err.code === 'ENOENT') {
|
|
1373
|
+
const commandLabel = resolvedCommand === command ? command : `${command} (resolved: ${resolvedCommand})`;
|
|
1287
1374
|
done({
|
|
1288
1375
|
success: false,
|
|
1289
|
-
message: `命令 "${
|
|
1376
|
+
message: `命令 "${commandLabel}" 未找到,请确保已安装(Windows 可尝试 npx.cmd 或绝对路径)`,
|
|
1290
1377
|
duration: Date.now() - startTime
|
|
1291
1378
|
});
|
|
1292
1379
|
} else {
|