@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 +1 -1
- package/src/commands/data.js +204 -57
- package/src/commands/server.js +197 -4
- package/src/index.js +29 -5
- package/src/server/dmla_progress.py +35 -2
- package/src/server/index.js +65 -11
- package/src/server/native_env_check.js +377 -0
- package/src/server/native_executor.js +469 -0
- package/src/server/routes/native.js +263 -0
- package/version.json +2 -2
package/package.json
CHANGED
package/src/commands/data.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
393
|
-
|
|
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.
|
|
527
|
-
console.log(chalk.yellow('
|
|
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
|
-
//
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
// 清理临时目录
|
|
599
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
720
|
+
// 清理临时目录
|
|
721
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
600
722
|
|
|
601
|
-
|
|
602
|
-
|
|
723
|
+
// 删除 zip 文件
|
|
724
|
+
fs.rmSync(zipPath, { force: true })
|
|
603
725
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
677
|
-
|
|
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
|
}
|
package/src/commands/server.js
CHANGED
|
@@ -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
|
-
|
|
795
|
-
console.log(chalk.
|
|
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('--
|
|
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
|
-
.
|
|
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
|
-
|
|
158
|
+
console.log(chalk.gray(` 端口: ${port}`))
|
|
159
|
+
await stopServer(port)
|
|
136
160
|
})
|
|
137
161
|
|
|
138
162
|
// ─────────────────────────────────────────────────────────────
|