@icyfenix-dmla/cli 2026.5.2-7 → 2026.5.3-821
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 +9 -6
- package/scripts/build.js +44 -11
- package/shared_modules/__init__.py +10 -0
- package/shared_modules/bayesian/__init__.py +6 -0
- package/shared_modules/bayesian/bayesian_network.py +105 -0
- package/shared_modules/bayesian/gaussian_mixture_model.py +141 -0
- package/shared_modules/bayesian/gaussian_mixturemodel.py +141 -0
- package/shared_modules/bayesian/multinomial_naive_bayes.py +74 -0
- package/shared_modules/bayesian/simple_bayesian_network.py +99 -0
- package/shared_modules/bayesian/simple_bayesiannetwork.py +99 -0
- package/shared_modules/cnn/__init__.py +5 -0
- package/shared_modules/cnn/alex_net.py +65 -0
- package/shared_modules/cnn/alexnet.py +65 -0
- package/shared_modules/cnn/t_e_r_m1.py +65 -0
- package/shared_modules/cnn/tiny_image_net_dataset.py +67 -0
- package/shared_modules/cnn/tiny_imagenet_dataset.py +67 -0
- package/shared_modules/cnn/tiny_imagenetdataset.py +67 -0
- package/shared_modules/cnn/tinyimagenetdataset.py +67 -0
- package/shared_modules/linear/__init__.py +6 -0
- package/shared_modules/linear/lasso_regression.py +93 -0
- package/shared_modules/linear/logistic_regression.py +78 -0
- package/shared_modules/linear/naive_bayes.py +141 -0
- package/shared_modules/linear/ridge_regression.py +58 -0
- package/shared_modules/neural/__init__.py +4 -0
- package/shared_modules/neural/perceptron.py +80 -0
- package/shared_modules/svm/__init__.py +5 -0
- package/shared_modules/svm/kernel_s_v_m.py +98 -0
- package/shared_modules/svm/kernel_svm.py +98 -0
- package/shared_modules/svm/simple_s_v_m.py +111 -0
- package/shared_modules/svm/simple_svm.py +111 -0
- package/shared_modules/tree/__init__.py +6 -0
- package/shared_modules/tree/ada_boost.py +77 -0
- package/shared_modules/tree/decision_tree_classifier.py +235 -0
- package/shared_modules/tree/decision_treeclassifier.py +235 -0
- package/shared_modules/tree/random_forest_classifier.py +88 -0
- package/shared_modules/tree/random_forestclassifier.py +88 -0
- package/shared_modules/unsupervised/__init__.py +5 -0
- package/shared_modules/unsupervised/k_means.py +127 -0
- package/shared_modules/unsupervised/kmeans.py +127 -0
- package/shared_modules/unsupervised/p_c_a.py +111 -0
- package/shared_modules/unsupervised/pca.py +111 -0
- package/src/commands/data.js +823 -0
- package/src/commands/server.js +209 -4
- package/src/index.js +23 -2
- package/src/server/routes/sandbox.js +70 -3
- package/src/server/sandbox.js +87 -11
- package/version.json +4 -0
package/src/commands/server.js
CHANGED
|
@@ -21,6 +21,106 @@ const CONFIG = {
|
|
|
21
21
|
defaultPort: 3001
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* 从 Docker 镜像标签解析日期
|
|
26
|
+
* 标签格式: YYYY.M.D-HHMM (如 2026.4.21-2025)
|
|
27
|
+
* @returns {Date|null}
|
|
28
|
+
*/
|
|
29
|
+
function parseImageTagDate(tag) {
|
|
30
|
+
const match = tag.match(/^(\d{4})\.(\d{1,2})\.(\d{1,2})-(\d{4})$/)
|
|
31
|
+
if (!match) return null
|
|
32
|
+
|
|
33
|
+
const [, year, month, day, time] = match
|
|
34
|
+
const hour = time.substring(0, 2)
|
|
35
|
+
const minute = time.substring(2, 4)
|
|
36
|
+
|
|
37
|
+
return new Date(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}T${hour}:${minute}:00+08:00`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 获取 Docker 镜像的最新标签日期
|
|
42
|
+
* @param {string} imageType - 'cpu' 或 'gpu'
|
|
43
|
+
* @returns {Promise<Date|null>}
|
|
44
|
+
*/
|
|
45
|
+
async function getImageLatestDate(imageType) {
|
|
46
|
+
const imageName = imageType === 'gpu' ? 'dmla-sandbox' : 'dmla-sandbox'
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const images = await docker.listImages()
|
|
50
|
+
const targetTag = imageType === 'gpu' ? ':gpu' : ':cpu'
|
|
51
|
+
|
|
52
|
+
for (const image of images) {
|
|
53
|
+
if (image.RepoTags) {
|
|
54
|
+
for (const tag of image.RepoTags) {
|
|
55
|
+
if (tag.includes(imageName) && tag.includes(targetTag)) {
|
|
56
|
+
// 尝试从标签解析日期
|
|
57
|
+
const tagPart = tag.split(':')[1]
|
|
58
|
+
const date = parseImageTagDate(tagPart)
|
|
59
|
+
if (date) return date
|
|
60
|
+
|
|
61
|
+
// 如果标签不是日期格式,使用镜像创建时间
|
|
62
|
+
return new Date(image.Created * 1000)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null
|
|
68
|
+
} catch {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 获取 CLI 包的构建日期
|
|
75
|
+
* @returns {Date|null}
|
|
76
|
+
*/
|
|
77
|
+
function getCliBuildDate() {
|
|
78
|
+
try {
|
|
79
|
+
// 尝试读取 version.json(build 时生成)
|
|
80
|
+
const versionPath = path.resolve(__dirname, '../../version.json')
|
|
81
|
+
if (fs.existsSync(versionPath)) {
|
|
82
|
+
const versionInfo = JSON.parse(fs.readFileSync(versionPath, 'utf8'))
|
|
83
|
+
return new Date(versionInfo.buildTime)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 如果没有 version.json,使用 package.json 的修改时间作为参考
|
|
87
|
+
const pkgPath = path.resolve(__dirname, '../../package.json')
|
|
88
|
+
if (fs.existsSync(pkgPath)) {
|
|
89
|
+
const stats = fs.statSync(pkgPath)
|
|
90
|
+
return stats.mtime
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null
|
|
94
|
+
} catch {
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 检查 --dev 模式的版本兼容性
|
|
101
|
+
* 比较镜像标签日期和 CLI 包构建日期
|
|
102
|
+
*/
|
|
103
|
+
async function checkDevModeCompatibility(imageType) {
|
|
104
|
+
const imageDate = await getImageLatestDate(imageType)
|
|
105
|
+
const cliDate = getCliBuildDate()
|
|
106
|
+
|
|
107
|
+
if (!imageDate || !cliDate) {
|
|
108
|
+
// 无法获取日期,不警告
|
|
109
|
+
return { compatible: true, imageDate: null, cliDate: null }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 镜像日期比 CLI 包日期新,说明用户可能更新了镜像但 CLI 包是旧版本
|
|
113
|
+
// 在 --dev 模式下,本地代码会覆盖镜像中的代码,可能导致代码版本不匹配
|
|
114
|
+
const imageNewer = imageDate > cliDate
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
compatible: !imageNewer,
|
|
118
|
+
imageDate,
|
|
119
|
+
cliDate,
|
|
120
|
+
warning: imageNewer ? '镜像版本比 CLI 包更新,本地代码可能覆盖了新版本镜像中的代码' : null
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
24
124
|
/**
|
|
25
125
|
* 检查端口是否可用
|
|
26
126
|
*/
|
|
@@ -226,12 +326,43 @@ function findServerPath() {
|
|
|
226
326
|
return null
|
|
227
327
|
}
|
|
228
328
|
|
|
329
|
+
/**
|
|
330
|
+
* 查找共享模块目录
|
|
331
|
+
* --dev 模式下需要挂载此目录
|
|
332
|
+
*/
|
|
333
|
+
function findSharedModulesPath() {
|
|
334
|
+
// 开发环境路径:packages/cli/src/commands -> ../../../local-server/shared_modules
|
|
335
|
+
const devPath = path.resolve(__dirname, '../../../local-server/shared_modules')
|
|
336
|
+
// npm 包路径:packages/cli/src/commands -> ../../shared_modules(构建后)
|
|
337
|
+
const npmPath = path.resolve(__dirname, '../../shared_modules')
|
|
338
|
+
// CLI 包根目录下的 shared_modules(构建后)
|
|
339
|
+
const cliRootPath = path.resolve(__dirname, '../../shared_modules')
|
|
340
|
+
|
|
341
|
+
// 优先使用开发环境路径(如果 local-server 存在)
|
|
342
|
+
if (fs.existsSync(devPath) && fs.readdirSync(devPath).length > 0) {
|
|
343
|
+
return devPath
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 其次使用 npm 包路径
|
|
347
|
+
if (fs.existsSync(npmPath) && fs.readdirSync(npmPath).length > 0) {
|
|
348
|
+
return npmPath
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 最后检查 CLI 包根目录
|
|
352
|
+
if (fs.existsSync(cliRootPath) && fs.readdirSync(cliRootPath).length > 0) {
|
|
353
|
+
return cliRootPath
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return null
|
|
357
|
+
}
|
|
358
|
+
|
|
229
359
|
/**
|
|
230
360
|
* 同步启动服务(在当前进程运行,用于调试)
|
|
231
361
|
* @param {number} port - 服务端口
|
|
232
362
|
* @param {boolean} useGpu - 是否使用 GPU(可选,自动检测)
|
|
363
|
+
* @param {boolean} dev - 开发模式(挂载本地代码)
|
|
233
364
|
*/
|
|
234
|
-
export async function startServerSync(port, useGpu = false) {
|
|
365
|
+
export async function startServerSync(port, useGpu = false, dev = false) {
|
|
235
366
|
// 检查端口
|
|
236
367
|
const portAvailable = await checkPortAvailable(port)
|
|
237
368
|
if (!portAvailable) {
|
|
@@ -249,6 +380,20 @@ export async function startServerSync(port, useGpu = false) {
|
|
|
249
380
|
}
|
|
250
381
|
const resolvedUseGpu = imageResolution.imageType === 'gpu'
|
|
251
382
|
|
|
383
|
+
// --dev 模式版本检查
|
|
384
|
+
if (dev) {
|
|
385
|
+
const compat = await checkDevModeCompatibility(imageResolution.imageType)
|
|
386
|
+
if (!compat.compatible) {
|
|
387
|
+
console.log(chalk.yellow('⚠️ 开发模式版本兼容性警告'))
|
|
388
|
+
console.log(chalk.gray(` 镜像构建时间: ${compat.imageDate?.toLocaleString('zh-CN') || '未知'}`))
|
|
389
|
+
console.log(chalk.gray(` CLI 包构建时间: ${compat.cliDate?.toLocaleString('zh-CN') || '未知'}`))
|
|
390
|
+
console.log(chalk.yellow(` 风险: ${compat.warning}`))
|
|
391
|
+
console.log(chalk.gray(' 说明: --dev 模式会挂载本地代码到容器,可能覆盖镜像中的新版本代码'))
|
|
392
|
+
console.log(chalk.gray(' 建议: 如需使用镜像最新功能,请退出 --dev 模式,或更新 CLI 包'))
|
|
393
|
+
console.log()
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
252
397
|
// 检查服务是否已运行
|
|
253
398
|
const alreadyRunning = await checkServiceRunning(port)
|
|
254
399
|
if (alreadyRunning) {
|
|
@@ -276,13 +421,23 @@ export async function startServerSync(port, useGpu = false) {
|
|
|
276
421
|
const actualServerPath = findServerPath()
|
|
277
422
|
if (!actualServerPath) {
|
|
278
423
|
console.log(chalk.red('❌ 找不到服务入口文件'))
|
|
279
|
-
console.log(chalk.yellow('提示:
|
|
424
|
+
console.log(chalk.yellow('提示: 保正确安装了 @icyfenix-dmla/cli'))
|
|
280
425
|
return
|
|
281
426
|
}
|
|
282
427
|
|
|
428
|
+
// 查找共享模块路径(--dev 模式需要)
|
|
429
|
+
const sharedModulesPath = dev ? findSharedModulesPath() : null
|
|
430
|
+
if (dev && !sharedModulesPath) {
|
|
431
|
+
console.log(chalk.yellow('⚠️ --dev 模式需要共享模块目录'))
|
|
432
|
+
console.log(chalk.gray(' 未找到 shared_modules,将仅使用镜像内置模块'))
|
|
433
|
+
}
|
|
434
|
+
|
|
283
435
|
console.log(chalk.gray(` 镜像类型: ${imageResolution.message}`))
|
|
284
436
|
console.log(chalk.gray(' 同步模式启动...'))
|
|
285
437
|
console.log(chalk.gray(` 服务入口: ${actualServerPath}`))
|
|
438
|
+
if (dev && sharedModulesPath) {
|
|
439
|
+
console.log(chalk.gray(` 共享模块: ${sharedModulesPath}`))
|
|
440
|
+
}
|
|
286
441
|
console.log()
|
|
287
442
|
|
|
288
443
|
// 设置环境变量
|
|
@@ -290,6 +445,15 @@ export async function startServerSync(port, useGpu = false) {
|
|
|
290
445
|
process.env.USE_GPU = resolvedUseGpu ? 'true' : 'false'
|
|
291
446
|
process.env.DMLA_SYNC_MODE = 'true' // 标记同步模式,让服务器在 import 时启动
|
|
292
447
|
|
|
448
|
+
// --dev 模式:启用 Volume Mount
|
|
449
|
+
if (dev) {
|
|
450
|
+
process.env.MOUNT_SHARED_MODULES = 'true'
|
|
451
|
+
process.env.MOUNT_KERNEL_RUNNER = 'true'
|
|
452
|
+
if (sharedModulesPath) {
|
|
453
|
+
process.env.SHARED_MODULES_PATH = sharedModulesPath
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
293
457
|
// 动态 import 服务器模块并直接运行
|
|
294
458
|
// 服务器模块会在 import 时自动启动(因为入口点检测逻辑)
|
|
295
459
|
// Windows 需要将路径转换为 file:// URL 格式
|
|
@@ -304,8 +468,11 @@ export async function startServerSync(port, useGpu = false) {
|
|
|
304
468
|
|
|
305
469
|
/**
|
|
306
470
|
* 启动服务(异步模式,spawn 子进程)
|
|
471
|
+
* @param {number} port - 服务端口
|
|
472
|
+
* @param {boolean} useGpu - 是否使用 GPU(可选,自动检测)
|
|
473
|
+
* @param {boolean} dev - 开发模式(挂载本地代码)
|
|
307
474
|
*/
|
|
308
|
-
export async function startServer(port, useGpu = false) {
|
|
475
|
+
export async function startServer(port, useGpu = false, dev = false) {
|
|
309
476
|
// 检查端口
|
|
310
477
|
const portAvailable = await checkPortAvailable(port)
|
|
311
478
|
if (!portAvailable) {
|
|
@@ -323,6 +490,20 @@ export async function startServer(port, useGpu = false) {
|
|
|
323
490
|
}
|
|
324
491
|
const resolvedUseGpu = imageResolution.imageType === 'gpu'
|
|
325
492
|
|
|
493
|
+
// --dev 模式版本检查
|
|
494
|
+
if (dev) {
|
|
495
|
+
const compat = await checkDevModeCompatibility(imageResolution.imageType)
|
|
496
|
+
if (!compat.compatible) {
|
|
497
|
+
console.log(chalk.yellow('⚠️ 开发模式版本兼容性警告'))
|
|
498
|
+
console.log(chalk.gray(` 镜像构建时间: ${compat.imageDate?.toLocaleString('zh-CN') || '未知'}`))
|
|
499
|
+
console.log(chalk.gray(` CLI 包构建时间: ${compat.cliDate?.toLocaleString('zh-CN') || '未知'}`))
|
|
500
|
+
console.log(chalk.yellow(` 风险: ${compat.warning}`))
|
|
501
|
+
console.log(chalk.gray(' 说明: --dev 模式会挂载本地代码到容器,可能覆盖镜像中的新版本代码'))
|
|
502
|
+
console.log(chalk.gray(' 建议: 如需使用镜像最新功能,请退出 --dev 模式,或更新 CLI 包'))
|
|
503
|
+
console.log()
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
326
507
|
// 检查服务是否已运行
|
|
327
508
|
const alreadyRunning = await checkServiceRunning(port)
|
|
328
509
|
if (alreadyRunning) {
|
|
@@ -338,7 +519,7 @@ export async function startServer(port, useGpu = false) {
|
|
|
338
519
|
console.log(chalk.gray(` 当前驱动: ${driverCheck.driverVersion}`))
|
|
339
520
|
console.log(chalk.gray(` CUDA 12.8 需要: 驱动 >= 570`))
|
|
340
521
|
console.log(chalk.yellow(' 解决方案:'))
|
|
341
|
-
console.log(chalk.gray(' 1. 升级 NVIDIA
|
|
522
|
+
console.log(chalk.gray(' 1. 升级 NVIDIA 驾动到 570+ 版本'))
|
|
342
523
|
console.log(chalk.gray(' 2. 使用 CPU 模式: dmla start'))
|
|
343
524
|
console.log()
|
|
344
525
|
console.log(chalk.gray(' 继续启动 GPU 模式(可能会失败)...'))
|
|
@@ -359,6 +540,12 @@ export async function startServer(port, useGpu = false) {
|
|
|
359
540
|
return
|
|
360
541
|
}
|
|
361
542
|
|
|
543
|
+
// 查找共享模块路径(--dev 模式需要)
|
|
544
|
+
const sharedModulesPath = dev ? findSharedModulesPath() : null
|
|
545
|
+
if (dev && sharedModulesPath) {
|
|
546
|
+
console.log(chalk.gray(` 共享模块: ${sharedModulesPath}`))
|
|
547
|
+
}
|
|
548
|
+
|
|
362
549
|
// 日志文件路径
|
|
363
550
|
const logDir = path.resolve(__dirname, '../../logs')
|
|
364
551
|
if (!fs.existsSync(logDir)) {
|
|
@@ -380,12 +567,27 @@ export async function startServer(port, useGpu = false) {
|
|
|
380
567
|
DMLA_LOG_FILE: logFile // 传递日志文件路径给服务端
|
|
381
568
|
}
|
|
382
569
|
|
|
570
|
+
// --dev 模式:启用 Volume Mount
|
|
571
|
+
if (dev) {
|
|
572
|
+
env.MOUNT_SHARED_MODULES = 'true'
|
|
573
|
+
env.MOUNT_KERNEL_RUNNER = 'true'
|
|
574
|
+
if (sharedModulesPath) {
|
|
575
|
+
env.SHARED_MODULES_PATH = sharedModulesPath
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
383
579
|
// 写入启动日志
|
|
384
580
|
const timestamp = new Date().toISOString()
|
|
385
581
|
fs.writeSync(logStream, `[${timestamp}] Server starting...\n`)
|
|
386
582
|
fs.writeSync(logStream, `[${timestamp}] Server path: ${actualServerPath}\n`)
|
|
387
583
|
fs.writeSync(logStream, `[${timestamp}] Port: ${port}\n`)
|
|
388
584
|
fs.writeSync(logStream, `[${timestamp}] GPU: ${resolvedUseGpu} (${imageResolution.message})\n`)
|
|
585
|
+
if (dev) {
|
|
586
|
+
fs.writeSync(logStream, `[${timestamp}] Dev mode: enabled (volume mount)\n`)
|
|
587
|
+
if (sharedModulesPath) {
|
|
588
|
+
fs.writeSync(logStream, `[${timestamp}] Shared modules: ${sharedModulesPath}\n`)
|
|
589
|
+
}
|
|
590
|
+
}
|
|
389
591
|
|
|
390
592
|
// 使用 spawn 启动 server 进程
|
|
391
593
|
// 重要:stdio 必须是 'ignore' 或管道,不能是 'inherit'
|
|
@@ -425,6 +627,9 @@ export async function startServer(port, useGpu = false) {
|
|
|
425
627
|
console.log(chalk.green(`✅ 服务已启动: http://localhost:${port}`))
|
|
426
628
|
console.log(chalk.gray(` 健康检查: http://localhost:${port}/api/health`))
|
|
427
629
|
console.log(chalk.gray(` 日志查看: ${logFile}`))
|
|
630
|
+
if (dev) {
|
|
631
|
+
console.log(chalk.cyan(' 开发模式: 已启用 Volume Mount'))
|
|
632
|
+
}
|
|
428
633
|
return
|
|
429
634
|
}
|
|
430
635
|
await new Promise(resolve => setTimeout(resolve, 500))
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'url'
|
|
|
9
9
|
import fs from 'fs'
|
|
10
10
|
import { startServer, startServerSync, stopServer, getStatus } from './commands/server.js'
|
|
11
11
|
import { runDoctor } from './commands/manage.js'
|
|
12
|
+
import { runDataTUI, runDataCommand } from './commands/data.js'
|
|
12
13
|
import { runInstallTUI } from '@icyfenix-dmla/install'
|
|
13
14
|
|
|
14
15
|
// 从 package.json 读取版本号
|
|
@@ -93,10 +94,12 @@ program
|
|
|
93
94
|
.option('-p, --port <number>', '服务端口', '3001')
|
|
94
95
|
.option('--gpu', '使用 GPU 镜像')
|
|
95
96
|
.option('--sync', '同步模式:在当前进程运行,日志直接输出(用于调试)')
|
|
97
|
+
.option('--dev', '开发模式:挂载本地代码到容器,无需重建镜像')
|
|
96
98
|
.action(async (options) => {
|
|
97
99
|
const port = parseInt(options.port, 10)
|
|
98
100
|
const useGpu = options.gpu
|
|
99
101
|
const sync = options.sync
|
|
102
|
+
const dev = options.dev
|
|
100
103
|
|
|
101
104
|
console.log(chalk.blue('启动 DMLA 沙箱服务...'))
|
|
102
105
|
console.log(chalk.gray(` 端口: ${port}`))
|
|
@@ -104,11 +107,14 @@ program
|
|
|
104
107
|
if (sync) {
|
|
105
108
|
console.log(chalk.yellow(` 模式: 同步(调试模式)`))
|
|
106
109
|
}
|
|
110
|
+
if (dev) {
|
|
111
|
+
console.log(chalk.cyan(` 模式: 开发(挂载本地代码)`))
|
|
112
|
+
}
|
|
107
113
|
|
|
108
114
|
if (sync) {
|
|
109
|
-
await startServerSync(port, useGpu)
|
|
115
|
+
await startServerSync(port, useGpu, dev)
|
|
110
116
|
} else {
|
|
111
|
-
await startServer(port, useGpu)
|
|
117
|
+
await startServer(port, useGpu, dev)
|
|
112
118
|
}
|
|
113
119
|
})
|
|
114
120
|
|
|
@@ -155,4 +161,19 @@ program
|
|
|
155
161
|
await runDoctor()
|
|
156
162
|
})
|
|
157
163
|
|
|
164
|
+
// ─────────────────────────────────────────────────────────────
|
|
165
|
+
// data 命令
|
|
166
|
+
// ─────────────────────────────────────────────────────────────
|
|
167
|
+
program
|
|
168
|
+
.command('data [subcommand]')
|
|
169
|
+
.description('数据管理(挂载、下载、清理等)')
|
|
170
|
+
.option('-p, --path <path>', '设置挂载路径')
|
|
171
|
+
.action(async (subcommand, options) => {
|
|
172
|
+
if (subcommand) {
|
|
173
|
+
await runDataCommand(subcommand, options)
|
|
174
|
+
} else {
|
|
175
|
+
await runDataTUI()
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
|
|
158
179
|
program.parse()
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* 沙箱 API 路由
|
|
3
3
|
*/
|
|
4
4
|
import { Router } from 'express'
|
|
5
|
+
import Docker from 'dockerode'
|
|
5
6
|
import sandbox, { runPythonCode, checkImageExists, checkGPUAvailable, checkCUDACompatibility } from '../sandbox.js'
|
|
6
7
|
|
|
7
8
|
const { SANDBOX_CONFIG } = sandbox
|
|
9
|
+
const docker = new Docker()
|
|
8
10
|
|
|
9
11
|
const router = Router()
|
|
10
12
|
|
|
@@ -36,10 +38,10 @@ router.get('/health', async (req, res) => {
|
|
|
36
38
|
/**
|
|
37
39
|
* 执行代码
|
|
38
40
|
* POST /api/sandbox/run
|
|
39
|
-
* Body: { code: string, useGpu?: boolean }
|
|
41
|
+
* Body: { code: string, useGpu?: boolean, timeout?: number|null }
|
|
40
42
|
*/
|
|
41
43
|
router.post('/run', async (req, res) => {
|
|
42
|
-
const { code, useGpu = false } = req.body
|
|
44
|
+
const { code, useGpu = false, timeout = null } = req.body
|
|
43
45
|
|
|
44
46
|
// 验证请求
|
|
45
47
|
if (!code || typeof code !== 'string') {
|
|
@@ -57,6 +59,16 @@ router.post('/run', async (req, res) => {
|
|
|
57
59
|
})
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
// 验证 timeout 参数
|
|
63
|
+
if (timeout !== null && timeout !== undefined) {
|
|
64
|
+
if (typeof timeout !== 'number' || timeout < 0 || timeout > 86400) {
|
|
65
|
+
return res.status(400).json({
|
|
66
|
+
success: false,
|
|
67
|
+
error: 'Invalid timeout parameter (must be null or number 0-86400)'
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
60
72
|
try {
|
|
61
73
|
// 检查镜像是否存在,智能降级
|
|
62
74
|
// GPU镜像包含CPU的全部功能,如果CPU镜像不存在但GPU镜像存在,可以使用GPU镜像执行CPU代码
|
|
@@ -96,7 +108,7 @@ router.post('/run', async (req, res) => {
|
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
// 执行代码(使用确定后的镜像)
|
|
99
|
-
const result = await runPythonCode(code, actualUseGpu, actualImage)
|
|
111
|
+
const result = await runPythonCode(code, actualUseGpu, actualImage, timeout)
|
|
100
112
|
|
|
101
113
|
res.json(result)
|
|
102
114
|
|
|
@@ -128,6 +140,61 @@ router.get('/gpu', async (req, res) => {
|
|
|
128
140
|
}
|
|
129
141
|
})
|
|
130
142
|
|
|
143
|
+
/**
|
|
144
|
+
* 进度查询
|
|
145
|
+
* GET /api/sandbox/progress
|
|
146
|
+
* 用于长时间任务的进度轮询
|
|
147
|
+
*/
|
|
148
|
+
router.get('/progress', async (req, res) => {
|
|
149
|
+
try {
|
|
150
|
+
// 进度文件在容器内的 /workspace/progress.json
|
|
151
|
+
// 需要通过 docker exec 读取
|
|
152
|
+
|
|
153
|
+
// 查找运行中的沙箱容器
|
|
154
|
+
const containers = await docker.listContainers({ filters: { status: ['running'] } })
|
|
155
|
+
const sandboxContainer = containers.find(c =>
|
|
156
|
+
c.Names.some(name => name.includes('dmla')) ||
|
|
157
|
+
c.Image.includes('dmla-sandbox')
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if (!sandboxContainer) {
|
|
161
|
+
return res.json({ status: 'no_task', message: 'No running task' })
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 在容器内读取进度文件
|
|
165
|
+
const container = docker.getContainer(sandboxContainer.Id)
|
|
166
|
+
const exec = await container.exec({
|
|
167
|
+
Cmd: ['cat', '/workspace/progress.json'],
|
|
168
|
+
AttachStdout: true
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
const stream = await exec.start()
|
|
172
|
+
const chunks = []
|
|
173
|
+
stream.on('data', chunk => chunks.push(chunk))
|
|
174
|
+
|
|
175
|
+
await new Promise(resolve => stream.on('end', resolve))
|
|
176
|
+
|
|
177
|
+
const output = Buffer.concat(chunks).toString()
|
|
178
|
+
|
|
179
|
+
// 解析进度 JSON
|
|
180
|
+
try {
|
|
181
|
+
// 去除 Docker exec 的头部信息
|
|
182
|
+
const jsonStart = output.indexOf('{')
|
|
183
|
+
if (jsonStart !== -1) {
|
|
184
|
+
const progressData = JSON.parse(output.substring(jsonStart))
|
|
185
|
+
return res.json(progressData)
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
// JSON 解析失败
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return res.json({ status: 'no_progress', message: 'Progress file not found' })
|
|
192
|
+
|
|
193
|
+
} catch (error) {
|
|
194
|
+
res.json({ status: 'error', message: error.message })
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
|
|
131
198
|
/**
|
|
132
199
|
* CUDA 兼容性检查
|
|
133
200
|
* 返回详细的 CUDA 环境诊断信息
|
package/src/server/sandbox.js
CHANGED
|
@@ -6,6 +6,7 @@ import Docker from 'dockerode'
|
|
|
6
6
|
import path from 'path'
|
|
7
7
|
import { fileURLToPath } from 'url'
|
|
8
8
|
import fs from 'fs'
|
|
9
|
+
import os from 'os'
|
|
9
10
|
|
|
10
11
|
const __filename = fileURLToPath(import.meta.url)
|
|
11
12
|
const __dirname = path.dirname(__filename)
|
|
@@ -63,6 +64,34 @@ const SANDBOX_CONFIG = {
|
|
|
63
64
|
memory: 4 * 1024 * 1024 * 1024 // 4GB 内存
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
// DMLA 配置文件路径
|
|
68
|
+
const DMLA_CONFIG_DIR = path.join(os.homedir(), '.dmla')
|
|
69
|
+
const DMLA_CONFIG_FILE = path.join(DMLA_CONFIG_DIR, 'config.json')
|
|
70
|
+
const DEFAULT_DATA_DIR = path.join(os.homedir(), 'dmla-data')
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 读取 DMLA 配置文件
|
|
74
|
+
*/
|
|
75
|
+
function readDmlaConfig() {
|
|
76
|
+
try {
|
|
77
|
+
if (fs.existsSync(DMLA_CONFIG_FILE)) {
|
|
78
|
+
const content = fs.readFileSync(DMLA_CONFIG_FILE, 'utf8')
|
|
79
|
+
return JSON.parse(content)
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
log(`配置文件读取失败: ${error.message}`)
|
|
83
|
+
}
|
|
84
|
+
return { dataVolumePath: DEFAULT_DATA_DIR }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 获取数据卷路径
|
|
89
|
+
*/
|
|
90
|
+
function getDataVolumePath() {
|
|
91
|
+
const config = readDmlaConfig()
|
|
92
|
+
return config.dataVolumePath || DEFAULT_DATA_DIR
|
|
93
|
+
}
|
|
94
|
+
|
|
66
95
|
/**
|
|
67
96
|
* 获取共享模块路径
|
|
68
97
|
*/
|
|
@@ -87,6 +116,21 @@ function getKernelRunnerPath() {
|
|
|
87
116
|
return DEFAULT_KERNEL_RUNNER_PATH
|
|
88
117
|
}
|
|
89
118
|
|
|
119
|
+
/**
|
|
120
|
+
* 获取 dmla_progress.py 路径(开发模式挂载)
|
|
121
|
+
*/
|
|
122
|
+
function getProgressReporterPath() {
|
|
123
|
+
if (process.env.PROGRESS_REPORTER_PATH) {
|
|
124
|
+
return process.env.PROGRESS_REPORTER_PATH
|
|
125
|
+
}
|
|
126
|
+
return DEFAULT_PROGRESS_REPORTER_PATH
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// dmla_progress.py 默认路径
|
|
130
|
+
const DEFAULT_PROGRESS_REPORTER_PATH = PROJECT_ROOT
|
|
131
|
+
? path.join(PROJECT_ROOT, 'local-server', 'src', 'dmla_progress.py')
|
|
132
|
+
: null
|
|
133
|
+
|
|
90
134
|
/**
|
|
91
135
|
* 检查是否启用 Volume Mount
|
|
92
136
|
*/
|
|
@@ -270,12 +314,18 @@ print(json.dumps(result))
|
|
|
270
314
|
* @param {string} code - Python 代码
|
|
271
315
|
* @param {boolean} useGpu - 是否启用 GPU 设备
|
|
272
316
|
* @param {string|null} imageOverride - 可选,指定使用的镜像名称(覆盖默认选择)
|
|
317
|
+
* @param {number|null} timeoutOverride - 可选,超时时间(秒),null 表示 unlimited
|
|
273
318
|
* @returns {Promise<{success: boolean, outputs: Array, executionTime: number, gpuUsed: boolean}>}
|
|
274
319
|
*/
|
|
275
|
-
export async function runPythonCode(code, useGpu = false, imageOverride = null) {
|
|
320
|
+
export async function runPythonCode(code, useGpu = false, imageOverride = null, timeoutOverride = null) {
|
|
276
321
|
const startTime = Date.now()
|
|
277
322
|
|
|
278
|
-
|
|
323
|
+
// 计算实际超时时间
|
|
324
|
+
const actualTimeout = timeoutOverride === null
|
|
325
|
+
? null // unlimited
|
|
326
|
+
: (timeoutOverride || Math.floor(SANDBOX_CONFIG.timeout / 1000))
|
|
327
|
+
|
|
328
|
+
log(`runPythonCode called, useGpu=${useGpu}, code length=${code.length}, imageOverride=${imageOverride}, timeout=${actualTimeout === null ? 'unlimited' : actualTimeout}`)
|
|
279
329
|
|
|
280
330
|
// 选择镜像:优先使用指定的镜像,否则根据 useGpu 选择
|
|
281
331
|
const image = imageOverride || (useGpu ? SANDBOX_CONFIG.imageGpu : SANDBOX_CONFIG.imageCpu)
|
|
@@ -329,17 +379,21 @@ export async function runPythonCode(code, useGpu = false, imageOverride = null)
|
|
|
329
379
|
}
|
|
330
380
|
|
|
331
381
|
// 创建容器配置 - 使用 kernel_runner.py 执行代码
|
|
382
|
+
const timeoutSeconds = actualTimeout === null ? 86400 : actualTimeout // unlimited 使用 24 小时
|
|
332
383
|
const containerConfig = {
|
|
333
384
|
Image: image,
|
|
334
|
-
Cmd: ['python3', '/workspace/kernel_runner.py', '--code', code, '--timeout', String(
|
|
385
|
+
Cmd: ['python3', '/workspace/kernel_runner.py', '--code', code, '--timeout', String(timeoutSeconds)],
|
|
335
386
|
HostConfig: {
|
|
336
387
|
Memory: SANDBOX_CONFIG.memory,
|
|
337
388
|
AutoRemove: false // 手动移除以获取日志
|
|
338
389
|
},
|
|
390
|
+
// matplotlib 使用 IPython Kernel 的 inline 后端,自动发送 display_data
|
|
391
|
+
// PYTHONPATH 添加 /workspace 以支持导入 volume-mounted 的模块
|
|
339
392
|
Env: [
|
|
340
|
-
'PYTHONUNBUFFERED=1'
|
|
341
|
-
|
|
342
|
-
|
|
393
|
+
'PYTHONUNBUFFERED=1',
|
|
394
|
+
'PYTHONPATH=/workspace',
|
|
395
|
+
actualTimeout === null ? 'DMLA_NO_TIMEOUT=1' : ''
|
|
396
|
+
].filter(e => e) // 过滤空字符串
|
|
343
397
|
}
|
|
344
398
|
|
|
345
399
|
log('Container config created')
|
|
@@ -353,10 +407,22 @@ export async function runPythonCode(code, useGpu = false, imageOverride = null)
|
|
|
353
407
|
// 收集所有需要挂载的路径
|
|
354
408
|
const binds = []
|
|
355
409
|
|
|
356
|
-
//
|
|
410
|
+
// 挂载数据目录
|
|
411
|
+
const dataVolumePath = getDataVolumePath()
|
|
412
|
+
if (dataVolumePath && fs.existsSync(dataVolumePath)) {
|
|
413
|
+
binds.push(`${dataVolumePath}:/data`)
|
|
414
|
+
console.log(`[Sandbox] 数据目录 Volume Mount: ${dataVolumePath}`)
|
|
415
|
+
} else if (dataVolumePath) {
|
|
416
|
+
console.warn(`[Sandbox] 警告: 数据目录不存在: ${dataVolumePath}`)
|
|
417
|
+
console.log(`[Sandbox] 提示: 运行 'dmla data' 创建数据目录`)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 挂载共享模块到 /workspace/shared(而非 site-packages)
|
|
421
|
+
// 原因:PYTHONPATH=/workspace,这样 Python 可以直接导入 shared.xxx
|
|
422
|
+
// 避免 Windows Docker 的 site-packages 路径兼容性问题
|
|
357
423
|
if (useMount && sharedModulesPath && fs.existsSync(sharedModulesPath)) {
|
|
358
|
-
binds.push(`${sharedModulesPath}:/
|
|
359
|
-
console.log(`[Sandbox] 共享模块 Volume Mount: ${sharedModulesPath}`)
|
|
424
|
+
binds.push(`${sharedModulesPath}:/workspace/shared:ro`)
|
|
425
|
+
console.log(`[Sandbox] 共享模块 Volume Mount: ${sharedModulesPath} -> /workspace/shared`)
|
|
360
426
|
} else if (useMount && sharedModulesPath) {
|
|
361
427
|
console.warn(`[Sandbox] 警告: 共享模块目录不存在: ${sharedModulesPath}`)
|
|
362
428
|
}
|
|
@@ -369,6 +435,15 @@ export async function runPythonCode(code, useGpu = false, imageOverride = null)
|
|
|
369
435
|
console.warn(`[Sandbox] 警告: kernel_runner.py 不存在: ${kernelRunnerPath}`)
|
|
370
436
|
}
|
|
371
437
|
|
|
438
|
+
// 挂载 dmla_progress.py(开发模式新增文件,无需重建镜像)
|
|
439
|
+
const progressReporterPath = getProgressReporterPath()
|
|
440
|
+
if (mountKernelRunner && progressReporterPath && fs.existsSync(progressReporterPath)) {
|
|
441
|
+
binds.push(`${progressReporterPath}:/workspace/dmla_progress.py:ro`)
|
|
442
|
+
console.log(`[Sandbox] dmla_progress.py Volume Mount: ${progressReporterPath}`)
|
|
443
|
+
} else if (mountKernelRunner && progressReporterPath) {
|
|
444
|
+
console.warn(`[Sandbox] 警告: dmla_progress.py 不存在: ${progressReporterPath}`)
|
|
445
|
+
}
|
|
446
|
+
|
|
372
447
|
// 设置 Binds
|
|
373
448
|
if (binds.length > 0) {
|
|
374
449
|
containerConfig.HostConfig.Binds = binds
|
|
@@ -396,12 +471,13 @@ export async function runPythonCode(code, useGpu = false, imageOverride = null)
|
|
|
396
471
|
container = await docker.createContainer(containerConfig)
|
|
397
472
|
log(`Container created: ${container.id}`)
|
|
398
473
|
|
|
399
|
-
//
|
|
474
|
+
// 设置超时(使用动态计算的超时时间,unlimited 时为 24 小时)
|
|
475
|
+
const containerTimeoutMs = timeoutSeconds * 1000 + 10000 // 转换为毫秒,额外 10 秒用于清理
|
|
400
476
|
const timeoutPromise = new Promise((_, reject) => {
|
|
401
477
|
timeoutId = setTimeout(() => {
|
|
402
478
|
log('Execution timeout triggered')
|
|
403
479
|
reject(new Error('Execution timeout'))
|
|
404
|
-
},
|
|
480
|
+
}, containerTimeoutMs)
|
|
405
481
|
})
|
|
406
482
|
|
|
407
483
|
// 启动容器
|
package/version.json
ADDED