coding-tool-x 3.3.6 → 3.3.7
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/CHANGELOG.md +9 -0
- package/dist/web/assets/{Analytics-TtaduRqL.js → Analytics-IW6eAy9u.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BP2lLBMN.js → ConfigTemplates-BPtkTMSc.js} +1 -1
- package/dist/web/assets/{Home-CbbyopS-.js → Home-obifg_9E.js} +1 -1
- package/dist/web/assets/{PluginManager-HmISlyMK.js → PluginManager-BGx9MSDV.js} +1 -1
- package/dist/web/assets/{ProjectList-DoN8Hjbu.js → ProjectList-BCn-mrCx.js} +1 -1
- package/dist/web/assets/{SessionList-Da8BYzNi.js → SessionList-CzLfebJQ.js} +1 -1
- package/dist/web/assets/{SkillManager-DqLAXh9o.js → SkillManager-CXz2vBQx.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-B_TxOgPW.js → WorkspaceManager-CHtgMfKc.js} +1 -1
- package/dist/web/assets/index-C7LPdVsN.js +2 -0
- package/dist/web/assets/{index-CsWInMQV.css → index-eEmjZKWP.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/commands/daemon.js +44 -6
- package/src/config/default.js +1 -1
- package/src/config/model-metadata.js +2 -2
- package/src/config/model-metadata.json +7 -2
- package/src/server/api/mcp.js +26 -4
- package/src/server/index.js +25 -2
- package/src/server/services/config-export-service.js +550 -116
- package/src/server/services/mcp-client.js +70 -13
- package/src/server/services/mcp-service.js +41 -13
- package/src/server/services/model-detector.js +1 -0
- package/src/utils/port-helper.js +87 -2
- package/dist/web/assets/index-By3mDEvx.js +0 -2
|
@@ -107,6 +107,65 @@ function resolveWindowsSpawnCommand(command, env, cwd) {
|
|
|
107
107
|
return normalizedCommand;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
function getCommandInstallHint(command) {
|
|
111
|
+
const normalized = path.basename(stripWrappingQuotes(command)).toLowerCase()
|
|
112
|
+
|
|
113
|
+
if (['node', 'node.exe', 'npm', 'npm.cmd', 'npx', 'npx.cmd'].includes(normalized)) {
|
|
114
|
+
return '请先安装 Node.js,并确认 `node` / `npm` / `npx` 已加入 PATH。'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (['uv', 'uv.exe', 'uvx', 'uvx.exe'].includes(normalized)) {
|
|
118
|
+
return '请先安装 `uv`,并确认 `uv` / `uvx` 可以在终端直接执行。'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (['python', 'python.exe', 'python3', 'py'].includes(normalized)) {
|
|
122
|
+
return '请先安装 Python,并确认对应命令已加入 PATH。'
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (process.platform === 'win32' && ['netstat', 'taskkill'].includes(normalized)) {
|
|
126
|
+
return '请确认 Windows 自带命令可用,并检查 `C:\\Windows\\System32` 是否在 PATH 中。'
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return `请确认已安装 "${command}",或改用可执行文件的绝对路径。`
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function createMissingCommandHint(command, resolvedCommand, env = {}) {
|
|
133
|
+
const pathKey = getPathEnvKey(env)
|
|
134
|
+
const pathValue = typeof env[pathKey] === 'string' ? env[pathKey] : ''
|
|
135
|
+
const pathPreview = pathValue
|
|
136
|
+
? pathValue.split(path.delimiter).slice(0, 5).join(path.delimiter)
|
|
137
|
+
: ''
|
|
138
|
+
const commandHint = resolvedCommand === command
|
|
139
|
+
? command
|
|
140
|
+
: `${command} (resolved: ${resolvedCommand})`
|
|
141
|
+
|
|
142
|
+
const details = [
|
|
143
|
+
getCommandInstallHint(command),
|
|
144
|
+
process.platform === 'win32'
|
|
145
|
+
? 'Windows 可优先尝试 `.cmd` / `.exe` 文件,必要时直接填写绝对路径。'
|
|
146
|
+
: `可尝试填写绝对路径(例如 \`/usr/bin/node\` 或 \`$(which ${command})\`)。`
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
if (pathPreview) {
|
|
150
|
+
details.push(`当前 PATH 前 5 项: ${pathPreview}`)
|
|
151
|
+
} else {
|
|
152
|
+
details.push('当前 PATH 为空,请检查环境变量配置。')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
type: 'missing-command',
|
|
157
|
+
command,
|
|
158
|
+
resolvedCommand,
|
|
159
|
+
title: `命令 "${commandHint}" 未找到`,
|
|
160
|
+
details
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function buildMissingCommandMessage(command, resolvedCommand, env = {}) {
|
|
165
|
+
const hint = createMissingCommandHint(command, resolvedCommand, env)
|
|
166
|
+
return [hint.title, ...hint.details].join('\n')
|
|
167
|
+
}
|
|
168
|
+
|
|
110
169
|
// ============================================================================
|
|
111
170
|
// McpClient
|
|
112
171
|
// ============================================================================
|
|
@@ -356,19 +415,11 @@ class McpClient extends EventEmitter {
|
|
|
356
415
|
|
|
357
416
|
this._child.on('error', (err) => {
|
|
358
417
|
if (err.code === 'ENOENT') {
|
|
359
|
-
const
|
|
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)`
|
|
363
|
-
: '\n PATH is not set!';
|
|
364
|
-
const commandHint = resolvedCommand === command
|
|
365
|
-
? command
|
|
366
|
-
: `${command} (resolved: ${resolvedCommand})`;
|
|
418
|
+
const hint = createMissingCommandHint(command, resolvedCommand, mergedEnv)
|
|
367
419
|
settle(new McpClientError(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
` 3. Check your PATH environment variable${pathHint}`
|
|
420
|
+
buildMissingCommandMessage(command, resolvedCommand, mergedEnv),
|
|
421
|
+
undefined,
|
|
422
|
+
{ hint }
|
|
372
423
|
));
|
|
373
424
|
} else {
|
|
374
425
|
settle(new McpClientError(`Failed to start process: ${err.message}`));
|
|
@@ -873,5 +924,11 @@ async function createClient(serverSpec, options = {}) {
|
|
|
873
924
|
module.exports = {
|
|
874
925
|
McpClient,
|
|
875
926
|
McpClientError,
|
|
876
|
-
createClient
|
|
927
|
+
createClient,
|
|
928
|
+
buildMissingCommandMessage,
|
|
929
|
+
createMissingCommandHint,
|
|
930
|
+
_test: {
|
|
931
|
+
createMissingCommandHint,
|
|
932
|
+
buildMissingCommandMessage
|
|
933
|
+
}
|
|
877
934
|
};
|
|
@@ -11,7 +11,7 @@ const toml = require('@iarna/toml');
|
|
|
11
11
|
const { spawn } = require('child_process');
|
|
12
12
|
const http = require('http');
|
|
13
13
|
const https = require('https');
|
|
14
|
-
const { McpClient } = require('./mcp-client');
|
|
14
|
+
const { McpClient, buildMissingCommandMessage, createMissingCommandHint } = require('./mcp-client');
|
|
15
15
|
const { NATIVE_PATHS } = require('../../config/paths');
|
|
16
16
|
const { resolvePreferredHomeDir } = require('../../utils/home-dir');
|
|
17
17
|
|
|
@@ -501,6 +501,19 @@ function resolveWindowsSpawnCommand(command, env, cwd) {
|
|
|
501
501
|
return normalizedCommand;
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
+
function extractMcpHint(error) {
|
|
505
|
+
return error?.data?.hint || error?.hint || null;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function buildMcpFailureResult(error, fallbackMessage, duration) {
|
|
509
|
+
const hint = extractMcpHint(error);
|
|
510
|
+
return {
|
|
511
|
+
message: hint?.title || fallbackMessage || error?.message || '操作失败',
|
|
512
|
+
hint,
|
|
513
|
+
duration
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
504
517
|
// ============================================================================
|
|
505
518
|
// MCP 数据管理
|
|
506
519
|
// ============================================================================
|
|
@@ -1301,10 +1314,12 @@ async function testServer(serverId) {
|
|
|
1301
1314
|
return { success: false, message: `不支持的服务器类型: ${type}` };
|
|
1302
1315
|
}
|
|
1303
1316
|
} catch (err) {
|
|
1317
|
+
const failure = buildMcpFailureResult(err, err.message, Date.now() - startTime);
|
|
1304
1318
|
return {
|
|
1305
1319
|
success: false,
|
|
1306
|
-
message:
|
|
1307
|
-
|
|
1320
|
+
message: failure.message,
|
|
1321
|
+
hint: failure.hint,
|
|
1322
|
+
duration: failure.duration
|
|
1308
1323
|
};
|
|
1309
1324
|
}
|
|
1310
1325
|
}
|
|
@@ -1370,10 +1385,11 @@ async function testStdioServer(spec) {
|
|
|
1370
1385
|
|
|
1371
1386
|
child.on('error', (err) => {
|
|
1372
1387
|
if (err.code === 'ENOENT') {
|
|
1373
|
-
const
|
|
1388
|
+
const hint = createMissingCommandHint(command, resolvedCommand, mergedEnv);
|
|
1374
1389
|
done({
|
|
1375
1390
|
success: false,
|
|
1376
|
-
message:
|
|
1391
|
+
message: buildMissingCommandMessage(command, resolvedCommand, mergedEnv),
|
|
1392
|
+
hint,
|
|
1377
1393
|
duration: Date.now() - startTime
|
|
1378
1394
|
});
|
|
1379
1395
|
} else {
|
|
@@ -1423,10 +1439,12 @@ async function testStdioServer(spec) {
|
|
|
1423
1439
|
}, timeout);
|
|
1424
1440
|
|
|
1425
1441
|
} catch (err) {
|
|
1442
|
+
const failure = buildMcpFailureResult(err, `测试失败: ${err.message}`, Date.now() - startTime);
|
|
1426
1443
|
done({
|
|
1427
1444
|
success: false,
|
|
1428
|
-
message:
|
|
1429
|
-
|
|
1445
|
+
message: failure.message,
|
|
1446
|
+
hint: failure.hint,
|
|
1447
|
+
duration: failure.duration
|
|
1430
1448
|
});
|
|
1431
1449
|
}
|
|
1432
1450
|
});
|
|
@@ -1571,11 +1589,14 @@ async function getServerTools(serverId) {
|
|
|
1571
1589
|
mcpClientPool.delete(serverId);
|
|
1572
1590
|
}
|
|
1573
1591
|
|
|
1592
|
+
const failure = buildMcpFailureResult(err, err.message, Date.now() - startTime);
|
|
1574
1593
|
return {
|
|
1575
1594
|
tools: [],
|
|
1576
|
-
duration:
|
|
1595
|
+
duration: failure.duration,
|
|
1577
1596
|
status: 'error',
|
|
1578
|
-
error:
|
|
1597
|
+
error: failure.message,
|
|
1598
|
+
message: failure.message,
|
|
1599
|
+
hint: failure.hint
|
|
1579
1600
|
};
|
|
1580
1601
|
}
|
|
1581
1602
|
}
|
|
@@ -1677,14 +1698,17 @@ async function callServerTool(serverId, toolName, arguments = {}) {
|
|
|
1677
1698
|
mcpClientPool.delete(serverId);
|
|
1678
1699
|
}
|
|
1679
1700
|
|
|
1701
|
+
const failure = buildMcpFailureResult(err, err.message, Date.now() - startTime);
|
|
1680
1702
|
return {
|
|
1681
1703
|
result: {
|
|
1682
|
-
error:
|
|
1704
|
+
error: failure.message,
|
|
1683
1705
|
code: err.code,
|
|
1684
1706
|
data: err.data
|
|
1685
1707
|
},
|
|
1686
|
-
duration:
|
|
1687
|
-
isError: true
|
|
1708
|
+
duration: failure.duration,
|
|
1709
|
+
isError: true,
|
|
1710
|
+
message: failure.message,
|
|
1711
|
+
hint: failure.hint
|
|
1688
1712
|
};
|
|
1689
1713
|
}
|
|
1690
1714
|
}
|
|
@@ -1864,5 +1888,9 @@ module.exports = {
|
|
|
1864
1888
|
callServerTool,
|
|
1865
1889
|
updateServerStatus,
|
|
1866
1890
|
updateServerOrder,
|
|
1867
|
-
exportServers
|
|
1891
|
+
exportServers,
|
|
1892
|
+
_test: {
|
|
1893
|
+
extractMcpHint,
|
|
1894
|
+
buildMcpFailureResult
|
|
1895
|
+
}
|
|
1868
1896
|
};
|
package/src/utils/port-helper.js
CHANGED
|
@@ -2,10 +2,74 @@ const { execSync } = require('child_process');
|
|
|
2
2
|
const net = require('net');
|
|
3
3
|
const { isWindowsLikePlatform } = require('./home-dir');
|
|
4
4
|
|
|
5
|
+
let lastPortToolIssue = null;
|
|
6
|
+
|
|
5
7
|
function isWindowsLikeRuntime(platform = process.platform, env = process.env) {
|
|
6
8
|
return isWindowsLikePlatform(platform, env);
|
|
7
9
|
}
|
|
8
10
|
|
|
11
|
+
function clearPortToolIssue() {
|
|
12
|
+
lastPortToolIssue = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function rememberPortToolIssue(issue) {
|
|
16
|
+
if (!lastPortToolIssue) {
|
|
17
|
+
lastPortToolIssue = issue;
|
|
18
|
+
}
|
|
19
|
+
return lastPortToolIssue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getPortToolIssue() {
|
|
23
|
+
return lastPortToolIssue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isMissingCommandError(error) {
|
|
27
|
+
const message = [
|
|
28
|
+
error && typeof error.message === 'string' ? error.message : '',
|
|
29
|
+
error && typeof error.stderr === 'string'
|
|
30
|
+
? error.stderr
|
|
31
|
+
: Buffer.isBuffer(error?.stderr)
|
|
32
|
+
? error.stderr.toString('utf-8')
|
|
33
|
+
: ''
|
|
34
|
+
].join('\n');
|
|
35
|
+
|
|
36
|
+
return error?.code === 'ENOENT' || /not found|not recognized as an internal or external command/i.test(message);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createPortToolIssue(command, phase, isWindows) {
|
|
40
|
+
if (isWindows) {
|
|
41
|
+
return {
|
|
42
|
+
command,
|
|
43
|
+
phase,
|
|
44
|
+
platform: 'windows',
|
|
45
|
+
summary: `未找到系统命令 ${command},无法${phase === 'kill' ? '关闭' : '检测'}占用端口的进程。`,
|
|
46
|
+
hints: [
|
|
47
|
+
'请确认已安装或启用 Windows 自带网络工具。',
|
|
48
|
+
'请将 `C:\\Windows\\System32` 加入 PATH 后重试。'
|
|
49
|
+
]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
command,
|
|
55
|
+
phase,
|
|
56
|
+
platform: 'unix',
|
|
57
|
+
summary: `未找到 ${command},无法${phase === 'kill' ? '关闭' : '检测'}占用端口的进程。`,
|
|
58
|
+
hints: [
|
|
59
|
+
'请安装 `lsof`,或安装提供 `fuser` 的系统工具后重试。',
|
|
60
|
+
'如果暂时不想安装,也可以改用其他端口。'
|
|
61
|
+
]
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatPortToolIssue(issue = lastPortToolIssue) {
|
|
66
|
+
if (!issue) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return [issue.summary, ...issue.hints];
|
|
71
|
+
}
|
|
72
|
+
|
|
9
73
|
function parsePidsFromNetstatOutput(output, port) {
|
|
10
74
|
const target = `:${port}`;
|
|
11
75
|
const pids = new Set();
|
|
@@ -69,6 +133,7 @@ function isPortInUse(port, host = '127.0.0.1') {
|
|
|
69
133
|
* 查找占用端口的进程PID(跨平台)
|
|
70
134
|
*/
|
|
71
135
|
function findProcessByPort(port) {
|
|
136
|
+
clearPortToolIssue();
|
|
72
137
|
const isWindows = isWindowsLikeRuntime();
|
|
73
138
|
if (isWindows) {
|
|
74
139
|
try {
|
|
@@ -76,20 +141,28 @@ function findProcessByPort(port) {
|
|
|
76
141
|
const result = execSync('netstat -ano', { encoding: 'utf-8' });
|
|
77
142
|
return parsePidsFromNetstatOutput(result, port);
|
|
78
143
|
} catch (e) {
|
|
144
|
+
if (isMissingCommandError(e)) {
|
|
145
|
+
rememberPortToolIssue(createPortToolIssue('netstat', 'lookup', true));
|
|
146
|
+
}
|
|
79
147
|
return [];
|
|
80
148
|
}
|
|
81
149
|
}
|
|
82
150
|
|
|
151
|
+
let lsofMissing = false;
|
|
83
152
|
try {
|
|
84
153
|
// macOS/Linux 使用 lsof
|
|
85
154
|
const result = execSync(`lsof -ti :${port}`, { encoding: 'utf-8' }).trim();
|
|
86
155
|
return result.split('\n').filter(pid => pid);
|
|
87
156
|
} catch (err) {
|
|
157
|
+
lsofMissing = isMissingCommandError(err);
|
|
88
158
|
// 如果 lsof 失败,尝试使用 fuser(某些 Linux 系统)
|
|
89
159
|
try {
|
|
90
160
|
const result = execSync(`fuser ${port}/tcp 2>/dev/null`, { encoding: 'utf-8' }).trim();
|
|
91
161
|
return result.split(/\s+/).filter(pid => pid);
|
|
92
162
|
} catch (e) {
|
|
163
|
+
if (lsofMissing && isMissingCommandError(e)) {
|
|
164
|
+
rememberPortToolIssue(createPortToolIssue('lsof / fuser', 'lookup', false));
|
|
165
|
+
}
|
|
93
166
|
return [];
|
|
94
167
|
}
|
|
95
168
|
}
|
|
@@ -106,6 +179,7 @@ function killProcessByPort(port) {
|
|
|
106
179
|
}
|
|
107
180
|
|
|
108
181
|
const isWindows = isWindowsLikeRuntime();
|
|
182
|
+
let killedAny = false;
|
|
109
183
|
pids.forEach(pid => {
|
|
110
184
|
try {
|
|
111
185
|
if (isWindows) {
|
|
@@ -113,12 +187,16 @@ function killProcessByPort(port) {
|
|
|
113
187
|
} else {
|
|
114
188
|
execSync(`kill -9 ${pid}`, { stdio: 'ignore' });
|
|
115
189
|
}
|
|
190
|
+
killedAny = true;
|
|
116
191
|
} catch (err) {
|
|
192
|
+
if (isWindows && isMissingCommandError(err)) {
|
|
193
|
+
rememberPortToolIssue(createPortToolIssue('taskkill', 'kill', true));
|
|
194
|
+
}
|
|
117
195
|
// 忽略单个进程杀掉失败的错误
|
|
118
196
|
}
|
|
119
197
|
});
|
|
120
198
|
|
|
121
|
-
return
|
|
199
|
+
return killedAny;
|
|
122
200
|
} catch (err) {
|
|
123
201
|
return false;
|
|
124
202
|
}
|
|
@@ -146,6 +224,13 @@ module.exports = {
|
|
|
146
224
|
findProcessByPort,
|
|
147
225
|
killProcessByPort,
|
|
148
226
|
waitForPortRelease,
|
|
227
|
+
getPortToolIssue,
|
|
228
|
+
formatPortToolIssue,
|
|
149
229
|
isWindowsLikeRuntime,
|
|
150
|
-
parsePidsFromNetstatOutput
|
|
230
|
+
parsePidsFromNetstatOutput,
|
|
231
|
+
_test: {
|
|
232
|
+
isMissingCommandError,
|
|
233
|
+
createPortToolIssue,
|
|
234
|
+
formatPortToolIssue
|
|
235
|
+
}
|
|
151
236
|
};
|