coding-tool-x 3.3.5 → 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-B6CWdkhx.js → Analytics-IW6eAy9u.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BW6LEgd8.js → ConfigTemplates-BPtkTMSc.js} +1 -1
- package/dist/web/assets/{Home-B2B2gS2-.js → Home-obifg_9E.js} +1 -1
- package/dist/web/assets/{PluginManager-Bqc7ldY-.js → PluginManager-BGx9MSDV.js} +1 -1
- package/dist/web/assets/{ProjectList-BFdZZm_8.js → ProjectList-BCn-mrCx.js} +1 -1
- package/dist/web/assets/{SessionList-B_Tp37kM.js → SessionList-CzLfebJQ.js} +1 -1
- package/dist/web/assets/{SkillManager-ul2rcS3o.js → SkillManager-CXz2vBQx.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Dp5Jvdtu.js → WorkspaceManager-CHtgMfKc.js} +1 -1
- package/dist/web/assets/index-C7LPdVsN.js +2 -0
- package/dist/web/assets/{index-DxRneGyu.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/commands/update.js +21 -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/config-export.js +21 -2
- package/src/server/api/mcp.js +26 -4
- package/src/server/index.js +25 -2
- package/src/server/services/config-export-service.js +639 -138
- package/src/server/services/mcp-client.js +162 -18
- package/src/server/services/mcp-service.js +130 -15
- package/src/server/services/model-detector.js +1 -0
- package/src/utils/port-helper.js +87 -2
- package/dist/web/assets/index-CSBDZxYn.js +0 -2
|
@@ -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,148 @@ 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
|
+
|
|
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
|
+
|
|
25
169
|
// ============================================================================
|
|
26
170
|
// McpClient
|
|
27
171
|
// ============================================================================
|
|
@@ -237,22 +381,19 @@ class McpClient extends EventEmitter {
|
|
|
237
381
|
reject(new McpClientError(`Connection timeout after ${this._timeout}ms`));
|
|
238
382
|
}, this._timeout);
|
|
239
383
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
// 如果用户提供的 env 中有 PATH,将其追加到系统 PATH 前面
|
|
244
|
-
if (env && env.PATH && process.env.PATH) {
|
|
245
|
-
mergedEnv.PATH = `${env.PATH}:${process.env.PATH}`;
|
|
246
|
-
}
|
|
384
|
+
const finalCwd = cwd || process.cwd();
|
|
385
|
+
const mergedEnv = mergeSpawnEnv(env || {});
|
|
386
|
+
const resolvedCommand = resolveWindowsSpawnCommand(command, mergedEnv, finalCwd);
|
|
247
387
|
|
|
248
|
-
|
|
388
|
+
try {
|
|
389
|
+
this._child = spawn(resolvedCommand, args, {
|
|
249
390
|
env: mergedEnv,
|
|
250
391
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
251
|
-
cwd:
|
|
392
|
+
cwd: finalCwd
|
|
252
393
|
});
|
|
253
394
|
} catch (err) {
|
|
254
395
|
clearTimeout(timer);
|
|
255
|
-
throw new McpClientError(`Failed to spawn "${
|
|
396
|
+
throw new McpClientError(`Failed to spawn "${resolvedCommand}": ${err.message}`);
|
|
256
397
|
}
|
|
257
398
|
|
|
258
399
|
// Once we get the spawn event (or first stdout), consider connected
|
|
@@ -274,14 +415,11 @@ class McpClient extends EventEmitter {
|
|
|
274
415
|
|
|
275
416
|
this._child.on('error', (err) => {
|
|
276
417
|
if (err.code === 'ENOENT') {
|
|
277
|
-
const
|
|
278
|
-
? `\n Current PATH: ${mergedEnv.PATH.split(':').slice(0, 5).join(':')}\n (showing first 5 entries)`
|
|
279
|
-
: '\n PATH is not set!';
|
|
418
|
+
const hint = createMissingCommandHint(command, resolvedCommand, mergedEnv)
|
|
280
419
|
settle(new McpClientError(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
` 3. Check your PATH environment variable${pathHint}`
|
|
420
|
+
buildMissingCommandMessage(command, resolvedCommand, mergedEnv),
|
|
421
|
+
undefined,
|
|
422
|
+
{ hint }
|
|
285
423
|
));
|
|
286
424
|
} else {
|
|
287
425
|
settle(new McpClientError(`Failed to start process: ${err.message}`));
|
|
@@ -786,5 +924,11 @@ async function createClient(serverSpec, options = {}) {
|
|
|
786
924
|
module.exports = {
|
|
787
925
|
McpClient,
|
|
788
926
|
McpClientError,
|
|
789
|
-
createClient
|
|
927
|
+
createClient,
|
|
928
|
+
buildMissingCommandMessage,
|
|
929
|
+
createMissingCommandHint,
|
|
930
|
+
_test: {
|
|
931
|
+
createMissingCommandHint,
|
|
932
|
+
buildMissingCommandMessage
|
|
933
|
+
}
|
|
790
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
|
|
|
@@ -418,6 +418,102 @@ 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
|
+
|
|
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
|
+
|
|
421
517
|
// ============================================================================
|
|
422
518
|
// MCP 数据管理
|
|
423
519
|
// ============================================================================
|
|
@@ -1218,10 +1314,12 @@ async function testServer(serverId) {
|
|
|
1218
1314
|
return { success: false, message: `不支持的服务器类型: ${type}` };
|
|
1219
1315
|
}
|
|
1220
1316
|
} catch (err) {
|
|
1317
|
+
const failure = buildMcpFailureResult(err, err.message, Date.now() - startTime);
|
|
1221
1318
|
return {
|
|
1222
1319
|
success: false,
|
|
1223
|
-
message:
|
|
1224
|
-
|
|
1320
|
+
message: failure.message,
|
|
1321
|
+
hint: failure.hint,
|
|
1322
|
+
duration: failure.duration
|
|
1225
1323
|
};
|
|
1226
1324
|
}
|
|
1227
1325
|
}
|
|
@@ -1237,6 +1335,9 @@ async function testStdioServer(spec) {
|
|
|
1237
1335
|
// 检查命令是否存在
|
|
1238
1336
|
const command = spec.command;
|
|
1239
1337
|
const args = spec.args || [];
|
|
1338
|
+
const cwd = spec.cwd || process.cwd();
|
|
1339
|
+
const mergedEnv = mergeSpawnEnv(spec.env || {});
|
|
1340
|
+
const resolvedCommand = resolveWindowsSpawnCommand(command, mergedEnv, cwd);
|
|
1240
1341
|
|
|
1241
1342
|
let child;
|
|
1242
1343
|
let resolved = false;
|
|
@@ -1260,10 +1361,10 @@ async function testStdioServer(spec) {
|
|
|
1260
1361
|
};
|
|
1261
1362
|
|
|
1262
1363
|
try {
|
|
1263
|
-
child = spawn(
|
|
1264
|
-
env:
|
|
1364
|
+
child = spawn(resolvedCommand, args, {
|
|
1365
|
+
env: mergedEnv,
|
|
1265
1366
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1266
|
-
cwd
|
|
1367
|
+
cwd
|
|
1267
1368
|
});
|
|
1268
1369
|
|
|
1269
1370
|
child.stdout.on('data', (data) => {
|
|
@@ -1284,9 +1385,11 @@ async function testStdioServer(spec) {
|
|
|
1284
1385
|
|
|
1285
1386
|
child.on('error', (err) => {
|
|
1286
1387
|
if (err.code === 'ENOENT') {
|
|
1388
|
+
const hint = createMissingCommandHint(command, resolvedCommand, mergedEnv);
|
|
1287
1389
|
done({
|
|
1288
1390
|
success: false,
|
|
1289
|
-
message:
|
|
1391
|
+
message: buildMissingCommandMessage(command, resolvedCommand, mergedEnv),
|
|
1392
|
+
hint,
|
|
1290
1393
|
duration: Date.now() - startTime
|
|
1291
1394
|
});
|
|
1292
1395
|
} else {
|
|
@@ -1336,10 +1439,12 @@ async function testStdioServer(spec) {
|
|
|
1336
1439
|
}, timeout);
|
|
1337
1440
|
|
|
1338
1441
|
} catch (err) {
|
|
1442
|
+
const failure = buildMcpFailureResult(err, `测试失败: ${err.message}`, Date.now() - startTime);
|
|
1339
1443
|
done({
|
|
1340
1444
|
success: false,
|
|
1341
|
-
message:
|
|
1342
|
-
|
|
1445
|
+
message: failure.message,
|
|
1446
|
+
hint: failure.hint,
|
|
1447
|
+
duration: failure.duration
|
|
1343
1448
|
});
|
|
1344
1449
|
}
|
|
1345
1450
|
});
|
|
@@ -1484,11 +1589,14 @@ async function getServerTools(serverId) {
|
|
|
1484
1589
|
mcpClientPool.delete(serverId);
|
|
1485
1590
|
}
|
|
1486
1591
|
|
|
1592
|
+
const failure = buildMcpFailureResult(err, err.message, Date.now() - startTime);
|
|
1487
1593
|
return {
|
|
1488
1594
|
tools: [],
|
|
1489
|
-
duration:
|
|
1595
|
+
duration: failure.duration,
|
|
1490
1596
|
status: 'error',
|
|
1491
|
-
error:
|
|
1597
|
+
error: failure.message,
|
|
1598
|
+
message: failure.message,
|
|
1599
|
+
hint: failure.hint
|
|
1492
1600
|
};
|
|
1493
1601
|
}
|
|
1494
1602
|
}
|
|
@@ -1590,14 +1698,17 @@ async function callServerTool(serverId, toolName, arguments = {}) {
|
|
|
1590
1698
|
mcpClientPool.delete(serverId);
|
|
1591
1699
|
}
|
|
1592
1700
|
|
|
1701
|
+
const failure = buildMcpFailureResult(err, err.message, Date.now() - startTime);
|
|
1593
1702
|
return {
|
|
1594
1703
|
result: {
|
|
1595
|
-
error:
|
|
1704
|
+
error: failure.message,
|
|
1596
1705
|
code: err.code,
|
|
1597
1706
|
data: err.data
|
|
1598
1707
|
},
|
|
1599
|
-
duration:
|
|
1600
|
-
isError: true
|
|
1708
|
+
duration: failure.duration,
|
|
1709
|
+
isError: true,
|
|
1710
|
+
message: failure.message,
|
|
1711
|
+
hint: failure.hint
|
|
1601
1712
|
};
|
|
1602
1713
|
}
|
|
1603
1714
|
}
|
|
@@ -1777,5 +1888,9 @@ module.exports = {
|
|
|
1777
1888
|
callServerTool,
|
|
1778
1889
|
updateServerStatus,
|
|
1779
1890
|
updateServerOrder,
|
|
1780
|
-
exportServers
|
|
1891
|
+
exportServers,
|
|
1892
|
+
_test: {
|
|
1893
|
+
extractMcpHint,
|
|
1894
|
+
buildMcpFailureResult
|
|
1895
|
+
}
|
|
1781
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
|
};
|