@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.
Files changed (47) hide show
  1. package/package.json +9 -6
  2. package/scripts/build.js +44 -11
  3. package/shared_modules/__init__.py +10 -0
  4. package/shared_modules/bayesian/__init__.py +6 -0
  5. package/shared_modules/bayesian/bayesian_network.py +105 -0
  6. package/shared_modules/bayesian/gaussian_mixture_model.py +141 -0
  7. package/shared_modules/bayesian/gaussian_mixturemodel.py +141 -0
  8. package/shared_modules/bayesian/multinomial_naive_bayes.py +74 -0
  9. package/shared_modules/bayesian/simple_bayesian_network.py +99 -0
  10. package/shared_modules/bayesian/simple_bayesiannetwork.py +99 -0
  11. package/shared_modules/cnn/__init__.py +5 -0
  12. package/shared_modules/cnn/alex_net.py +65 -0
  13. package/shared_modules/cnn/alexnet.py +65 -0
  14. package/shared_modules/cnn/t_e_r_m1.py +65 -0
  15. package/shared_modules/cnn/tiny_image_net_dataset.py +67 -0
  16. package/shared_modules/cnn/tiny_imagenet_dataset.py +67 -0
  17. package/shared_modules/cnn/tiny_imagenetdataset.py +67 -0
  18. package/shared_modules/cnn/tinyimagenetdataset.py +67 -0
  19. package/shared_modules/linear/__init__.py +6 -0
  20. package/shared_modules/linear/lasso_regression.py +93 -0
  21. package/shared_modules/linear/logistic_regression.py +78 -0
  22. package/shared_modules/linear/naive_bayes.py +141 -0
  23. package/shared_modules/linear/ridge_regression.py +58 -0
  24. package/shared_modules/neural/__init__.py +4 -0
  25. package/shared_modules/neural/perceptron.py +80 -0
  26. package/shared_modules/svm/__init__.py +5 -0
  27. package/shared_modules/svm/kernel_s_v_m.py +98 -0
  28. package/shared_modules/svm/kernel_svm.py +98 -0
  29. package/shared_modules/svm/simple_s_v_m.py +111 -0
  30. package/shared_modules/svm/simple_svm.py +111 -0
  31. package/shared_modules/tree/__init__.py +6 -0
  32. package/shared_modules/tree/ada_boost.py +77 -0
  33. package/shared_modules/tree/decision_tree_classifier.py +235 -0
  34. package/shared_modules/tree/decision_treeclassifier.py +235 -0
  35. package/shared_modules/tree/random_forest_classifier.py +88 -0
  36. package/shared_modules/tree/random_forestclassifier.py +88 -0
  37. package/shared_modules/unsupervised/__init__.py +5 -0
  38. package/shared_modules/unsupervised/k_means.py +127 -0
  39. package/shared_modules/unsupervised/kmeans.py +127 -0
  40. package/shared_modules/unsupervised/p_c_a.py +111 -0
  41. package/shared_modules/unsupervised/pca.py +111 -0
  42. package/src/commands/data.js +823 -0
  43. package/src/commands/server.js +209 -4
  44. package/src/index.js +23 -2
  45. package/src/server/routes/sandbox.js +70 -3
  46. package/src/server/sandbox.js +87 -11
  47. package/version.json +4 -0
@@ -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('提示: 确保正确安装了 @icyfenix-dmla/cli'))
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 驱动到 570+ 版本'))
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 环境诊断信息
@@ -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
- log(`runPythonCode called, useGpu=${useGpu}, code length=${code.length}, imageOverride=${imageOverride}`)
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(Math.floor(SANDBOX_CONFIG.timeout / 1000))],
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
- // matplotlib 使用 IPython Kernel 的 inline 后端,自动发送 display_data
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}:/usr/local/lib/python3.11/site-packages/shared:ro`)
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
- }, SANDBOX_CONFIG.timeout + 10000) // 额外 10 秒用于清理
480
+ }, containerTimeoutMs)
405
481
  })
406
482
 
407
483
  // 启动容器
package/version.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "buildTime": "2026-05-03T00:21:45.756Z",
3
+ "cliVersion": "2026.5.3-821"
4
+ }