@icyfenix-dmla/cli 2026.5.5-1733 → 2026.5.5-2149

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icyfenix-dmla/cli",
3
- "version": "2026.5.5-1733",
3
+ "version": "2026.5.5-2149",
4
4
  "description": "DMLA 沙箱服务命令行工具",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -158,6 +158,7 @@ function ensureDataDirStructure(dataPath) {
158
158
  function getDirectoryStats(dataPath) {
159
159
  const stats = {
160
160
  datasets: 0,
161
+ incompleteDatasets: 0,
161
162
  models: 0,
162
163
  totalSize: 0
163
164
  }
@@ -170,7 +171,16 @@ function getDirectoryStats(dataPath) {
170
171
  const fullPath = path.join(datasetsPath, d)
171
172
  return fs.statSync(fullPath).isDirectory() && d !== 'custom'
172
173
  })
173
- stats.datasets = dirs.length
174
+
175
+ for (const dir of dirs) {
176
+ // 检查是否有 LFS 不完整标记
177
+ const incompleteMarker = path.join(datasetsPath, dir, '.lfs-incomplete')
178
+ if (fs.existsSync(incompleteMarker)) {
179
+ stats.incompleteDatasets++
180
+ } else {
181
+ stats.datasets++
182
+ }
183
+ }
174
184
  }
175
185
 
176
186
  // 统计模型文件数量
@@ -200,16 +210,49 @@ function getDirectoryStats(dataPath) {
200
210
  }
201
211
 
202
212
  /**
203
- * 检查数据集是否已下载
213
+ * 检查数据集是否已下载(且完整可用)
204
214
  */
205
215
  function isDatasetDownloaded(dataPath, datasetId) {
206
216
  const dataset = DATASETS.find(d => d.id === datasetId)
207
217
  if (!dataset) return false
208
218
 
219
+ const targetPath = path.join(dataPath, dataset.targetDir)
220
+ if (!fs.existsSync(targetPath)) return false
221
+
222
+ // 检查是否有 LFS 不完整标记文件
223
+ const incompleteMarker = path.join(targetPath, '.lfs-incomplete')
224
+ if (fs.existsSync(incompleteMarker)) {
225
+ return false
226
+ }
227
+
228
+ return true
229
+ }
230
+
231
+ /**
232
+ * 检查数据集是否已存在(不管是否完整)
233
+ */
234
+ function isDatasetExists(dataPath, datasetId) {
235
+ const dataset = DATASETS.find(d => d.id === datasetId)
236
+ if (!dataset) return false
237
+
209
238
  const targetPath = path.join(dataPath, dataset.targetDir)
210
239
  return fs.existsSync(targetPath)
211
240
  }
212
241
 
242
+ /**
243
+ * 检查数据集是否不完整(LFS 未拉取)
244
+ */
245
+ function isDatasetIncomplete(dataPath, datasetId) {
246
+ const dataset = DATASETS.find(d => d.id === datasetId)
247
+ if (!dataset) return false
248
+
249
+ const targetPath = path.join(dataPath, dataset.targetDir)
250
+ if (!fs.existsSync(targetPath)) return false
251
+
252
+ const incompleteMarker = path.join(targetPath, '.lfs-incomplete')
253
+ return fs.existsSync(incompleteMarker)
254
+ }
255
+
213
256
  /**
214
257
  * 显示主菜单
215
258
  */
@@ -217,7 +260,11 @@ async function showMainMenu(dataPath) {
217
260
  const stats = getDirectoryStats(dataPath)
218
261
 
219
262
  console.log(chalk.gray(`当前挂载路径: ${dataPath}`))
220
- console.log(chalk.gray(`数据集: ${stats.datasets} 个已下载`))
263
+ if (stats.incompleteDatasets > 0) {
264
+ console.log(chalk.gray(`数据集: ${stats.datasets} 个可用, ${chalk.red(`${stats.incompleteDatasets} 个不完整`)}`))
265
+ } else {
266
+ console.log(chalk.gray(`数据集: ${stats.datasets} 个已下载`))
267
+ }
221
268
  console.log(chalk.gray(`模型: ${stats.models} 个已保存`))
222
269
  console.log()
223
270
  console.log(chalk.gray('------------------------------------'))
@@ -384,13 +431,23 @@ function listDatasets() {
384
431
  const dataPath = getDataVolumePath()
385
432
 
386
433
  console.log()
387
- console.log(chalk.bold('已下载的数据集'))
434
+ console.log(chalk.bold('数据集列表'))
388
435
  console.log()
389
436
 
390
437
  for (const dataset of DATASETS) {
391
438
  const downloaded = isDatasetDownloaded(dataPath, dataset.id)
392
- const status = downloaded ? chalk.green('[已下载]') : chalk.gray('[未下载]')
393
- console.log(`${status} ${dataset.name} (${dataset.size})`)
439
+ const exists = isDatasetExists(dataPath, dataset.id)
440
+ const incomplete = isDatasetIncomplete(dataPath, dataset.id)
441
+
442
+ if (downloaded) {
443
+ console.log(`${chalk.green('[可用]')} ${dataset.name} (${dataset.size})`)
444
+ } else if (incomplete) {
445
+ console.log(`${chalk.red('[不完整]')} ${dataset.name} (${dataset.size}) - 请安装 Git LFS 后执行 git lfs pull`)
446
+ } else if (exists) {
447
+ console.log(`${chalk.yellow('[存在]')} ${dataset.name} (${dataset.size}) - 状态未知`)
448
+ } else {
449
+ console.log(`${chalk.gray('[未下载]')} ${dataset.name} (${dataset.size})`)
450
+ }
394
451
  }
395
452
 
396
453
  console.log()
@@ -425,16 +482,19 @@ async function downloadDatasets() {
425
482
  // 构建选项列表
426
483
  const choices = DATASETS.map((dataset, index) => {
427
484
  const downloaded = isDatasetDownloaded(dataPath, dataset.id)
485
+ const incomplete = isDatasetIncomplete(dataPath, dataset.id)
428
486
 
429
487
  let message = `${dataset.name} (${dataset.size})`
430
488
  if (downloaded) {
431
489
  message += ' [已下载]'
490
+ } else if (incomplete) {
491
+ message += ' [不完整-可重新下载]'
432
492
  }
433
493
 
434
494
  return {
435
495
  name: index.toString(),
436
496
  message,
437
- disabled: downloaded
497
+ disabled: downloaded // 完整下载的才禁用,不完整的可以重新下载
438
498
  }
439
499
  })
440
500
 
@@ -469,12 +529,19 @@ async function downloadDatasets() {
469
529
  console.log()
470
530
  console.log(chalk.cyan(`────────────────────────────────────`))
471
531
 
472
- // 检查是否已下载
532
+ // 检查是否已完整下载
473
533
  if (isDatasetDownloaded(dataPath, dataset.id)) {
474
- console.log(chalk.yellow(`${dataset.name} 已下载,跳过`))
534
+ console.log(chalk.yellow(`${dataset.name} 已完整下载,跳过`))
475
535
  continue
476
536
  }
477
537
 
538
+ // 检查是否有不完整的数据,需要先删除
539
+ if (isDatasetIncomplete(dataPath, dataset.id)) {
540
+ console.log(chalk.yellow(`${dataset.name} 存在不完整数据,将删除后重新下载...`))
541
+ const targetDir = path.join(dataPath, dataset.targetDir)
542
+ fs.rmSync(targetDir, { recursive: true, force: true })
543
+ }
544
+
478
545
  await downloadDataset(dataPath, dataset)
479
546
  }
480
547
 
@@ -523,8 +590,44 @@ async function downloadDataset(dataPath, dataset) {
523
590
  execSync('git lfs install', { stdio: 'pipe' })
524
591
  hasGitLfs = true
525
592
  } catch {
526
- console.log(chalk.yellow(' Git LFS 未安装,可能无法下载大文件'))
527
- console.log(chalk.yellow('建议安装: https://git-lfs.github.com/'))
593
+ console.log(chalk.red(' Git LFS 未安装'))
594
+ console.log(chalk.yellow('数据集使用 Git LFS 存储大文件,未安装 LFS 时只能下载指针文件'))
595
+ console.log(chalk.yellow('数据集将不可用!'))
596
+ console.log()
597
+ console.log(chalk.yellow('建议安装 Git LFS 后重新下载:'))
598
+ console.log(chalk.gray(' Ubuntu/Debian: sudo apt install git-lfs'))
599
+ console.log(chalk.gray(' macOS: brew install git-lfs'))
600
+ console.log(chalk.gray(' Windows: https://git-lfs.github.com/'))
601
+ console.log()
602
+
603
+ // 提供选项:继续下载(留待后续手动拉取)或中止
604
+ const { choice } = await prompt({
605
+ type: 'select',
606
+ name: 'choice',
607
+ message: '如何处理?',
608
+ choices: [
609
+ '中止下载(删除不完整数据)',
610
+ '继续下载(安装 LFS 后手动拉取: git lfs pull)'
611
+ ],
612
+ styles: {
613
+ primary: chalk.cyan.bold
614
+ }
615
+ })
616
+
617
+ if (choice === '中止下载(删除不完整数据)') {
618
+ console.log(chalk.yellow('下载已中止'))
619
+ // 创建目标目录以便后续重试(标记为不完整)
620
+ if (!fs.existsSync(targetDir)) {
621
+ fs.mkdirSync(targetDir, { recursive: true })
622
+ }
623
+ // 写入标记文件,表明数据不完整
624
+ fs.writeFileSync(path.join(targetDir, '.lfs-incomplete'), 'Git LFS 未安装,数据不完整')
625
+ return
626
+ }
627
+
628
+ console.log(chalk.gray('继续下载指针文件...'))
629
+ console.log(chalk.yellow('⚠ 提醒: 下载完成后需安装 Git LFS 并执行 git lfs pull 才能使用数据集'))
630
+ console.log()
528
631
  }
529
632
 
530
633
  // 执行 git clone
@@ -545,10 +648,12 @@ async function downloadDataset(dataPath, dataset) {
545
648
  })
546
649
 
547
650
  console.log()
548
- console.log(chalk.green('下载完成'))
549
651
 
550
- // 拉取 Git LFS 文件(如果已安装)
652
+ // 根据 LFS 状态显示不同提示
551
653
  if (hasGitLfs) {
654
+ console.log(chalk.green('下载完成'))
655
+
656
+ // 拉取 Git LFS 文件
552
657
  console.log()
553
658
  console.log(chalk.gray('拉取 LFS 大文件...'))
554
659
  try {
@@ -556,60 +661,84 @@ async function downloadDataset(dataPath, dataset) {
556
661
  console.log(chalk.green('LFS 文件拉取完成'))
557
662
  } catch (lfsError) {
558
663
  console.log(chalk.yellow(`⚠ LFS 拉取失败: ${lfsError.message}`))
559
- console.log(chalk.yellow('数据集可能包含未下载的大文件'))
664
+ console.log(chalk.yellow('数据集可能包含未下载的大文件,请手动执行: git lfs pull'))
560
665
  }
666
+ } else {
667
+ console.log(chalk.yellow('指针文件下载完成(数据不完整)'))
668
+ console.log()
669
+ console.log(chalk.red('⚠ 数据集当前不可用!'))
670
+ console.log(chalk.yellow('请按以下步骤完成下载:'))
671
+ console.log(chalk.gray(` 1. 安装 Git LFS`))
672
+ console.log(chalk.gray(` 2. 进入目录: cd ${targetDir}`))
673
+ console.log(chalk.gray(` 3. 拉取数据: git lfs pull`))
674
+ console.log()
561
675
  }
562
676
 
563
- // 解压数据集内的 zip 文件(如果有)
564
- if (dataset.zipFile) {
677
+ // 解压数据集内的 zip 文件(仅在 LFS 正常时执行)
678
+ if (dataset.zipFile && hasGitLfs) {
565
679
  const zipPath = path.join(targetDir, dataset.zipFile)
566
680
 
567
681
  if (fs.existsSync(zipPath)) {
568
- console.log()
569
- console.log(chalk.gray(`解压 ${dataset.zipFile}...`))
570
-
571
- try {
572
- const zip = new AdmZip(zipPath)
573
-
574
- // 解压到临时目录
575
- const tempDir = path.join(targetDir, '_extract_temp')
576
- zip.extractAllTo(tempDir, true)
577
-
578
- // 将 zip 内部目录内容移到目标目录
579
- const innerDir = dataset.zipInnerDir
580
- ? path.join(tempDir, dataset.zipInnerDir)
581
- : tempDir
582
-
583
- if (fs.existsSync(innerDir)) {
584
- // 移动内部目录的所有内容到目标目录
585
- const items = fs.readdirSync(innerDir)
586
- for (const item of items) {
587
- const srcPath = path.join(innerDir, item)
588
- const destPath = path.join(targetDir, item)
589
-
590
- // 如果目标已存在且不是 zip 文件,跳过
591
- if (fs.existsSync(destPath) && item !== dataset.zipFile) {
592
- continue
682
+ // 检查 zip 文件是否是真正的 zip(而非 LFS 指针文件)
683
+ const zipStat = fs.statSync(zipPath)
684
+ if (zipStat.size < 1000) {
685
+ // 小于 1KB 的文件很可能是 LFS 指针文件
686
+ console.log()
687
+ console.log(chalk.yellow(`⚠ ${dataset.zipFile} 可能是 LFS 指针文件,跳过解压`))
688
+ console.log(chalk.yellow('请确保 git lfs pull 成功后再手动解压'))
689
+ } else {
690
+ console.log()
691
+ console.log(chalk.gray(`解压 ${dataset.zipFile}...`))
692
+
693
+ try {
694
+ const zip = new AdmZip(zipPath)
695
+
696
+ // 解压到临时目录
697
+ const tempDir = path.join(targetDir, '_extract_temp')
698
+ zip.extractAllTo(tempDir, true)
699
+
700
+ // zip 内部目录内容移到目标目录
701
+ const innerDir = dataset.zipInnerDir
702
+ ? path.join(tempDir, dataset.zipInnerDir)
703
+ : tempDir
704
+
705
+ if (fs.existsSync(innerDir)) {
706
+ // 移动内部目录的所有内容到目标目录
707
+ const items = fs.readdirSync(innerDir)
708
+ for (const item of items) {
709
+ const srcPath = path.join(innerDir, item)
710
+ const destPath = path.join(targetDir, item)
711
+
712
+ // 如果目标已存在且不是 zip 文件,跳过
713
+ if (fs.existsSync(destPath) && item !== dataset.zipFile) {
714
+ continue
715
+ }
716
+
717
+ fs.cpSync(srcPath, destPath, { recursive: true, force: true })
593
718
  }
594
719
 
595
- fs.cpSync(srcPath, destPath, { recursive: true, force: true })
596
- }
597
-
598
- // 清理临时目录
599
- fs.rmSync(tempDir, { recursive: true, force: true })
720
+ // 清理临时目录
721
+ fs.rmSync(tempDir, { recursive: true, force: true })
600
722
 
601
- // 删除 zip 文件
602
- fs.rmSync(zipPath, { force: true })
723
+ // 删除 zip 文件
724
+ fs.rmSync(zipPath, { force: true })
603
725
 
604
- console.log(chalk.green('解压完成'))
605
- } else {
606
- console.log(chalk.yellow(` ⚠ zip 内部目录 ${dataset.zipInnerDir} 不存在`))
726
+ console.log(chalk.green('解压完成'))
727
+ } else {
728
+ console.log(chalk.yellow(` ⚠ zip 内部目录 ${dataset.zipInnerDir} 不存在`))
729
+ }
730
+ } catch (err) {
731
+ console.log(chalk.red(`解压失败: ${err.message}`))
732
+ console.log(chalk.yellow(`请手动解压: ${zipPath}`))
607
733
  }
608
- } catch (err) {
609
- console.log(chalk.red(`解压失败: ${err.message}`))
610
- console.log(chalk.yellow(`请手动解压: ${zipPath}`))
611
734
  }
612
735
  }
736
+ } else if (dataset.zipFile && !hasGitLfs) {
737
+ // LFS 未安装时,检查是否有 zip 文件需要提醒用户
738
+ const zipPath = path.join(targetDir, dataset.zipFile)
739
+ if (fs.existsSync(zipPath)) {
740
+ console.log(chalk.gray(`注意: ${dataset.zipFile} 需在 git lfs pull 后手动解压`))
741
+ }
613
742
  }
614
743
 
615
744
  } else {
@@ -673,8 +802,14 @@ async function downloadDataset(dataPath, dataset) {
673
802
  fs.rmSync(downloadFile, { force: true })
674
803
  }
675
804
 
676
- console.log()
677
- console.log(chalk.green(`数据集已保存到 ${targetDir}`))
805
+ // 根据 LFS 状态显示不同的完成提示
806
+ if (hasGitLfs) {
807
+ console.log()
808
+ console.log(chalk.green(`数据集已保存到 ${targetDir}`))
809
+ } else {
810
+ console.log()
811
+ console.log(chalk.yellow(`数据集目录: ${targetDir} (数据不完整,暂不可用)`))
812
+ }
678
813
 
679
814
  // 更新配置
680
815
  const config = readConfig()
@@ -684,6 +819,13 @@ async function downloadDataset(dataPath, dataset) {
684
819
  if (!config.installedDatasets.includes(dataset.id)) {
685
820
  config.installedDatasets.push(dataset.id)
686
821
  }
822
+ // 如果 LFS 未安装,标记数据集状态为不完整
823
+ if (!hasGitLfs) {
824
+ config.incompleteDatasets = config.incompleteDatasets || []
825
+ if (!config.incompleteDatasets.includes(dataset.id)) {
826
+ config.incompleteDatasets.push(dataset.id)
827
+ }
828
+ }
687
829
  writeConfig(config)
688
830
 
689
831
  } catch (error) {
@@ -848,5 +990,10 @@ export default {
848
990
  runDataTUI,
849
991
  runDataCommand,
850
992
  getDataVolumePath,
851
- DATASETS
993
+ DATASETS,
994
+ // 导出辅助函数供测试使用
995
+ isDatasetDownloaded,
996
+ isDatasetExists,
997
+ isDatasetIncomplete,
998
+ getDirectoryStats
852
999
  }
@@ -730,10 +730,10 @@ export async function startServer(port, useGpu = false, dev = false, shmSize = 6
730
730
 
731
731
  /**
732
732
  * 停止服务
733
+ * @param {number} port - 服务端口
733
734
  */
734
- export async function stopServer() {
735
+ export async function stopServer(port = CONFIG.defaultPort) {
735
736
  // 首先尝试通过 API 停止服务
736
- const port = CONFIG.defaultPort
737
737
  const running = await checkServiceRunning(port)
738
738
 
739
739
  if (running) {
@@ -791,8 +791,26 @@ export async function stopServer() {
791
791
  } else if (!running) {
792
792
  console.log(chalk.gray(' 服务未运行'))
793
793
  } else {
794
- console.log(chalk.yellow('⚠️ 无法停止服务'))
795
- console.log(chalk.gray(' 提示: 手动终止端口 3001 上的进程'))
794
+ // API 停止失败,尝试直接 kill 进程
795
+ console.log(chalk.yellow('⚠️ API 停止失败,尝试强制终止进程...'))
796
+ try {
797
+ // 查找监听该端口的进程
798
+ const result = execSync(`lsof -ti:${port} -sTCP:LISTEN 2>/dev/null || echo ""`, { encoding: 'utf8' })
799
+ const pids = result.trim().split('\n').filter(p => p)
800
+ if (pids.length > 0) {
801
+ for (const pid of pids) {
802
+ execSync(`kill -9 ${pid} 2>/dev/null || true`)
803
+ console.log(chalk.gray(` 已终止进程 PID: ${pid}`))
804
+ }
805
+ console.log(chalk.green('✅ 进程已强制终止'))
806
+ } else {
807
+ console.log(chalk.yellow('⚠️ 无法找到监听端口的进程'))
808
+ console.log(chalk.gray(` 提示: 手动终止端口 ${port} 上的进程`))
809
+ }
810
+ } catch (e) {
811
+ console.log(chalk.yellow('⚠️ 无法停止服务'))
812
+ console.log(chalk.gray(` 提示: 手动终止端口 ${port} 上的进程`))
813
+ }
796
814
  }
797
815
  }
798
816
 
@@ -868,4 +886,179 @@ export async function getStatus() {
868
886
  console.log(chalk.gray(' 服务未运行'))
869
887
  console.log(chalk.yellow(' 提示: 运行 dmla start 启动服务'))
870
888
  }
889
+ }
890
+
891
+ // ==================== Native 模式 ====================
892
+
893
+ /**
894
+ * 同步启动 Native 服务(在当前进程运行,用于调试)
895
+ * @param {number} port - 服务端口
896
+ */
897
+ export async function startNativeServerSync(port) {
898
+ // 检查端口
899
+ const portAvailable = await checkPortAvailable(port)
900
+ if (!portAvailable) {
901
+ console.log(chalk.red(`❌ 端口 ${port} 已被占用`))
902
+ console.log(chalk.yellow('提示: 使用 --port 选项指定其他端口'))
903
+ return
904
+ }
905
+
906
+ // 检查服务是否已运行
907
+ const alreadyRunning = await checkServiceRunning(port)
908
+ if (alreadyRunning) {
909
+ console.log(chalk.green(`✅ 服务已在端口 ${port} 运行`))
910
+ return
911
+ }
912
+
913
+ // 查找服务器入口文件(Native 模式使用相同的服务入口)
914
+ const serverPath = findNativeServerPath()
915
+ if (!serverPath) {
916
+ console.log(chalk.red('❌ 找不到服务入口文件'))
917
+ console.log(chalk.yellow('提示: 确保正确安装了 @icyfenix-dmla/cli'))
918
+ return
919
+ }
920
+
921
+ console.log(chalk.gray(' 同步模式启动...'))
922
+ console.log(chalk.gray(` 服务入口: ${serverPath}`))
923
+ console.log()
924
+
925
+ // 设置环境变量(标记 Native 模式)
926
+ process.env.PORT = port.toString()
927
+ process.env.DMLA_MODE = 'native'
928
+ process.env.DMLA_SYNC_MODE = 'true'
929
+
930
+ // 动态 import 服务器模块
931
+ try {
932
+ const serverURL = pathToFileURL(serverPath).href
933
+ await import(serverURL)
934
+ } catch (error) {
935
+ console.log(chalk.red(`❌ 服务启动失败: ${error.message}`))
936
+ console.log(chalk.gray(error.stack))
937
+ }
938
+ }
939
+
940
+ /**
941
+ * 启动 Native 服务(异步模式,spawn 子进程)
942
+ * @param {number} port - 服务端口
943
+ */
944
+ export async function startNativeServer(port) {
945
+ // 检查端口
946
+ const portAvailable = await checkPortAvailable(port)
947
+ if (!portAvailable) {
948
+ console.log(chalk.red(`❌ 端口 ${port} 已被占用`))
949
+ console.log(chalk.yellow('提示: 使用 --port 选项指定其他端口'))
950
+ return
951
+ }
952
+
953
+ // 检查服务是否已运行
954
+ const alreadyRunning = await checkServiceRunning(port)
955
+ if (alreadyRunning) {
956
+ console.log(chalk.green(`✅ 服务已在端口 ${port} 运行`))
957
+ return
958
+ }
959
+
960
+ // 查找服务器入口文件
961
+ const serverPath = findNativeServerPath()
962
+ if (!serverPath) {
963
+ console.log(chalk.red('❌ 找不到服务入口文件'))
964
+ console.log(chalk.yellow('提示: 确保正确安装了 @icyfenix-dmla/cli'))
965
+ return
966
+ }
967
+
968
+ console.log(chalk.gray(' 正在启动...'))
969
+
970
+ try {
971
+ // 日志文件路径
972
+ const logDir = path.resolve(__dirname, '../../logs')
973
+ if (!fs.existsSync(logDir)) {
974
+ fs.mkdirSync(logDir, { recursive: true })
975
+ }
976
+ const logFile = path.join(logDir, 'native-server.log')
977
+ const errorLogFile = path.join(logDir, 'native-server-error.log')
978
+
979
+ console.log(chalk.gray(` 日志文件: ${logFile}`))
980
+
981
+ // 创建日志文件流
982
+ const logStream = fs.openSync(logFile, 'a')
983
+ const errorLogStream = fs.openSync(errorLogFile, 'a')
984
+
985
+ const env = {
986
+ ...process.env,
987
+ PORT: port.toString(),
988
+ DMLA_MODE: 'native',
989
+ DMLA_LOG_FILE: logFile
990
+ }
991
+
992
+ // 写入启动日志
993
+ const timestamp = new Date().toISOString()
994
+ fs.writeSync(logStream, `[${timestamp}] Native Server starting...\n`)
995
+ fs.writeSync(logStream, `[${timestamp}] Server path: ${serverPath}\n`)
996
+ fs.writeSync(logStream, `[${timestamp}] Port: ${port}\n`)
997
+ fs.writeSync(logStream, `[${timestamp}] Mode: native\n`)
998
+
999
+ // 使用 spawn 启动 server 进程
1000
+ const serverProcess = spawn('node', [serverPath], {
1001
+ env,
1002
+ stdio: ['ignore', logStream, errorLogStream],
1003
+ detached: true,
1004
+ windowsHide: true
1005
+ })
1006
+
1007
+ // 监听子进程事件
1008
+ serverProcess.on('error', (err) => {
1009
+ fs.writeSync(errorLogStream, `[${new Date().toISOString()}] Spawn error: ${err.message}\n`)
1010
+ })
1011
+
1012
+ serverProcess.on('exit', (code, signal) => {
1013
+ const msg = `[${new Date().toISOString()}] Process exited: code=${code}, signal=${signal}\n`
1014
+ fs.writeSync(logStream, msg)
1015
+ fs.writeSync(errorLogStream, msg)
1016
+ })
1017
+
1018
+ serverProcess.unref()
1019
+
1020
+ // 关闭父进程中的文件描述符
1021
+ fs.closeSync(logStream)
1022
+ fs.closeSync(errorLogStream)
1023
+
1024
+ // 等待服务启动
1025
+ console.log(chalk.gray(' 等待服务就绪...'))
1026
+ let attempts = 0
1027
+ const maxAttempts = 30
1028
+
1029
+ while (attempts < maxAttempts) {
1030
+ const running = await checkServiceRunning(port)
1031
+ if (running) {
1032
+ console.log(chalk.green(`✅ 服务已启动: http://localhost:${port}`))
1033
+ console.log(chalk.gray(` 健康检查: http://localhost:${port}/api/sandbox/health`))
1034
+ console.log(chalk.gray(` 日志查看: ${logFile}`))
1035
+ console.log(chalk.cyan(' 模式: Native(本机执行)'))
1036
+ return
1037
+ }
1038
+ await new Promise(resolve => setTimeout(resolve, 500))
1039
+ attempts++
1040
+ }
1041
+
1042
+ console.log(chalk.yellow('⚠️ 服务启动超时'))
1043
+ console.log(chalk.gray(` 请查看日志: ${logFile}`))
1044
+ console.log(chalk.gray(` 或使用 --sync 模式调试`))
1045
+ } catch (error) {
1046
+ console.log(chalk.red(`❌ 启动失败: ${error.message}`))
1047
+ console.log(chalk.gray(error.stack))
1048
+ }
1049
+ }
1050
+
1051
+ /**
1052
+ * 查找 Native 服务入口文件
1053
+ */
1054
+ function findNativeServerPath() {
1055
+ // Native 模式使用相同的服务入口,只是路由不同
1056
+ // packages/cli/src/commands -> ../server/index.js
1057
+ const serverPath = path.resolve(__dirname, '../server/index.js')
1058
+
1059
+ if (fs.existsSync(serverPath)) {
1060
+ return serverPath
1061
+ }
1062
+
1063
+ return null
871
1064
  }
package/src/index.js CHANGED
@@ -7,7 +7,7 @@ import chalk from 'chalk'
7
7
  import path from 'path'
8
8
  import { fileURLToPath } from 'url'
9
9
  import fs from 'fs'
10
- import { startServer, startServerSync, stopServer, getStatus } from './commands/server.js'
10
+ import { startServer, startServerSync, stopServer, getStatus, startNativeServer, startNativeServerSync } from './commands/server.js'
11
11
  import { runDoctor } from './commands/manage.js'
12
12
  import { runDataTUI, runDataCommand } from './commands/data.js'
13
13
  import { runImagesTUI } from './commands/images.js'
@@ -93,12 +93,14 @@ program
93
93
  .command('start')
94
94
  .description('启动沙箱服务')
95
95
  .option('-p, --port <number>', '服务端口', '3001')
96
- .option('--gpu', '使用 GPU 镜像')
96
+ .option('--native', 'Native 模式:直接在本机执行 Python 代码,无需 Docker')
97
+ .option('--gpu', '使用 GPU 镜像(仅 Docker 模式)')
97
98
  .option('--sync', '同步模式:在当前进程运行,日志直接输出(用于调试)')
98
- .option('--dev', '开发模式:挂载本地代码到容器,无需重建镜像')
99
+ .option('--dev', '开发模式:挂载本地代码到容器,无需重建镜像(仅 Docker 模式)')
99
100
  .option('--shm-size <size>', 'Docker 共享内存大小(MB),用于 DataLoader 多线程。GPU 模式默认 1024MB,CPU 模式默认 64MB')
100
101
  .action(async (options) => {
101
102
  const port = parseInt(options.port, 10)
103
+ const native = options.native
102
104
  const useGpu = options.gpu
103
105
  const sync = options.sync
104
106
  const dev = options.dev
@@ -108,6 +110,25 @@ program
108
110
 
109
111
  console.log(chalk.blue('启动 DMLA 沙箱服务...'))
110
112
  console.log(chalk.gray(` 端口: ${port}`))
113
+
114
+ // Native 模式
115
+ if (native) {
116
+ console.log(chalk.cyan(` 模式: Native(本机执行)`))
117
+ // Native 模式忽略 --gpu/--dev/--shm-size
118
+ if (useGpu || dev || options.shmSize) {
119
+ console.log(chalk.yellow(` 提示: --native 模式下 --gpu/--dev/--shm-size 参数无效`))
120
+ }
121
+
122
+ if (sync) {
123
+ console.log(chalk.yellow(` 子模式: 同步(调试模式)`))
124
+ await startNativeServerSync(port)
125
+ } else {
126
+ await startNativeServer(port)
127
+ }
128
+ return
129
+ }
130
+
131
+ // Docker 模式(现有逻辑)
111
132
  console.log(chalk.gray(` 请求类型: ${useGpu ? 'GPU' : '自动选择'}`))
112
133
  console.log(chalk.gray(` 共享内存: ${shmSize}MB(DataLoader 多线程需要足够 shm)`))
113
134
  if (sync) {
@@ -130,9 +151,12 @@ program
130
151
  program
131
152
  .command('stop')
132
153
  .description('停止运行中的沙箱服务')
133
- .action(async () => {
154
+ .option('-p, --port <number>', '指定停止的端口', '3001')
155
+ .action(async (options) => {
156
+ const port = parseInt(options.port, 10)
134
157
  console.log(chalk.blue('停止 DMLA 沙箱服务...'))
135
- await stopServer()
158
+ console.log(chalk.gray(` 端口: ${port}`))
159
+ await stopServer(port)
136
160
  })
137
161
 
138
162
  // ─────────────────────────────────────────────────────────────