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.
@@ -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-CSBDZxYn.js"></script>
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-DxRneGyu.css">
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-tool-x",
3
- "version": "3.3.5",
3
+ "version": "3.3.6",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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 execAsync = promisify(exec);
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 { stdout } = await execAsync(`npm view ${packageName} version --json`, { timeout: 15000 });
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 child = spawn('npm', ['install', '-g', `${packageName}@${version}`], {
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', reject);
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: (data.data.channels || []).length,
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: (data.data.channels || []).map(c => ({
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 channels = channelsService.getAllChannels() || [];
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
- for (const channel of channels) {
648
- try {
649
- const existingChannels = channelsService.getAllChannels() || [];
650
- const existing = existingChannels.find(c => c.id === channel.id);
651
-
652
- if (existing && !overwrite) {
653
- results.channels.skipped++;
654
- continue;
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
- if (existing && overwrite) {
658
- channelsService.updateChannel(channel.id, channel);
659
- } else {
660
- const { name, baseUrl, apiKey, websiteUrl, ...extraConfig } = channel;
661
- channelsService.createChannel(name, baseUrl, apiKey, websiteUrl, extraConfig);
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
- try {
241
- // 确保 PATH 不被覆盖,优先使用用户提供的 env,但保留 PATH
242
- const mergedEnv = { ...process.env, ...env };
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
- this._child = spawn(command, args, {
329
+ try {
330
+ this._child = spawn(resolvedCommand, args, {
249
331
  env: mergedEnv,
250
332
  stdio: ['pipe', 'pipe', 'pipe'],
251
- cwd: cwd || process.cwd()
333
+ cwd: finalCwd
252
334
  });
253
335
  } catch (err) {
254
336
  clearTimeout(timer);
255
- throw new McpClientError(`Failed to spawn "${command}": ${err.message}`);
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 pathHint = mergedEnv.PATH
278
- ? `\n Current PATH: ${mergedEnv.PATH.split(':').slice(0, 5).join(':')}\n (showing first 5 entries)`
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 "${command}" not found. Please check:\n` +
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(command, args, {
1264
- env: { ...process.env, ...spec.env },
1349
+ child = spawn(resolvedCommand, args, {
1350
+ env: mergedEnv,
1265
1351
  stdio: ['pipe', 'pipe', 'pipe'],
1266
- cwd: spec.cwd || process.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: `命令 "${command}" 未找到,请确保已安装`,
1376
+ message: `命令 "${commandLabel}" 未找到,请确保已安装(Windows 可尝试 npx.cmd 或绝对路径)`,
1290
1377
  duration: Date.now() - startTime
1291
1378
  });
1292
1379
  } else {