@icyfenix-dmla/cli 2026.4.19-856 → 2026.4.19-935
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/README.md +46 -0
- package/package.json +9 -1
- package/scripts/build.js +50 -0
- package/src/server/index.js +48 -0
- package/src/server/routes/sandbox.js +115 -0
- package/src/server/sandbox.js +351 -0
- package/tests/cli.test.js +0 -34
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @icyfenix-dmla/cli
|
|
2
|
+
|
|
3
|
+
DMLA 沙箱服务命令行工具。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @icyfenix-dmla/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 使用
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# 启动服务
|
|
15
|
+
dmla start # 默认端口 3001
|
|
16
|
+
dmla start --port 8080 # 自定义端口
|
|
17
|
+
dmla start --gpu # GPU 模式
|
|
18
|
+
|
|
19
|
+
# 停止服务
|
|
20
|
+
dmla stop
|
|
21
|
+
|
|
22
|
+
# 查看状态
|
|
23
|
+
dmla status
|
|
24
|
+
|
|
25
|
+
# 安装镜像
|
|
26
|
+
dmla install # 安装所有镜像(默认从 Docker Hub)
|
|
27
|
+
dmla install --cpu # 仅 CPU 版本
|
|
28
|
+
dmla install --gpu # 仅 GPU 版本
|
|
29
|
+
dmla install --registry acr # 从阿里云 ACR 安装(国内加速)
|
|
30
|
+
|
|
31
|
+
# 更新
|
|
32
|
+
dmla update # 更新 npm 包和镜像
|
|
33
|
+
dmla update --registry acr
|
|
34
|
+
|
|
35
|
+
# 环境诊断
|
|
36
|
+
dmla doctor
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 要求
|
|
40
|
+
|
|
41
|
+
- Node.js >= 18.0.0
|
|
42
|
+
- Docker
|
|
43
|
+
|
|
44
|
+
## 许可证
|
|
45
|
+
|
|
46
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@icyfenix-dmla/cli",
|
|
3
|
-
"version": "2026.4.19-
|
|
3
|
+
"version": "2026.4.19-935",
|
|
4
4
|
"description": "DMLA 沙箱服务命令行工具",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -9,8 +9,16 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node src/index.js",
|
|
12
|
+
"build": "node scripts/build.js",
|
|
13
|
+
"prepublishOnly": "npm run build",
|
|
12
14
|
"test": "node --experimental-vm-modules $(npm root)/jest/bin/jest.js"
|
|
13
15
|
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin/",
|
|
18
|
+
"src/",
|
|
19
|
+
"scripts/",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
14
22
|
"dependencies": {
|
|
15
23
|
"commander": "^12.1.0",
|
|
16
24
|
"chalk": "^5.3.0",
|
package/scripts/build.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 构建脚本:将 local-server 代码复制到 CLI 包中
|
|
3
|
+
* 用于 npm 发布时包含完整的服务器代码
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs'
|
|
6
|
+
import path from 'path'
|
|
7
|
+
import { fileURLToPath } from 'url'
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
10
|
+
const __dirname = path.dirname(__filename)
|
|
11
|
+
|
|
12
|
+
const rootDir = path.resolve(__dirname, '../../..')
|
|
13
|
+
const localServerSrc = path.resolve(rootDir, 'local-server/src')
|
|
14
|
+
const cliServerDest = path.resolve(__dirname, '../src/server')
|
|
15
|
+
|
|
16
|
+
console.log('📦 构建 CLI 包...')
|
|
17
|
+
console.log(` 源目录: ${localServerSrc}`)
|
|
18
|
+
console.log(` 目标目录: ${cliServerDest}`)
|
|
19
|
+
|
|
20
|
+
// 递归复制目录
|
|
21
|
+
function copyDir(src, dest) {
|
|
22
|
+
if (!fs.existsSync(src)) {
|
|
23
|
+
console.error(`❌ 源目录不存在: ${src}`)
|
|
24
|
+
process.exit(1)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 创建目标目录
|
|
28
|
+
if (!fs.existsSync(dest)) {
|
|
29
|
+
fs.mkdirSync(dest, { recursive: true })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const entries = fs.readdirSync(src, { withFileTypes: true })
|
|
33
|
+
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const srcPath = path.join(src, entry.name)
|
|
36
|
+
const destPath = path.join(dest, entry.name)
|
|
37
|
+
|
|
38
|
+
if (entry.isDirectory()) {
|
|
39
|
+
copyDir(srcPath, destPath)
|
|
40
|
+
} else if (entry.isFile() && entry.name.endsWith('.js')) {
|
|
41
|
+
fs.copyFileSync(srcPath, destPath)
|
|
42
|
+
console.log(` ✓ 复制: ${entry.name}`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 执行复制
|
|
48
|
+
copyDir(localServerSrc, cliServerDest)
|
|
49
|
+
|
|
50
|
+
console.log('✅ 服务器代码已复制到 CLI 包')
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design Machine Learning Applications 本地服务
|
|
3
|
+
* 提供 Python 代码沙箱执行 API
|
|
4
|
+
*/
|
|
5
|
+
import express from 'express'
|
|
6
|
+
import cors from 'cors'
|
|
7
|
+
import { fileURLToPath } from 'url'
|
|
8
|
+
import { resolve } from 'path'
|
|
9
|
+
import sandboxRouter from './routes/sandbox.js'
|
|
10
|
+
|
|
11
|
+
export const app = express()
|
|
12
|
+
const PORT = process.env.PORT || 3001
|
|
13
|
+
|
|
14
|
+
// 中间件
|
|
15
|
+
app.use(cors())
|
|
16
|
+
app.use(express.json())
|
|
17
|
+
|
|
18
|
+
// 健康检查
|
|
19
|
+
app.get('/api/health', (req, res) => {
|
|
20
|
+
res.json({ status: 'ok', timestamp: new Date().toISOString() })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// 沙箱 API
|
|
24
|
+
app.use('/api/sandbox', sandboxRouter)
|
|
25
|
+
|
|
26
|
+
// 错误处理
|
|
27
|
+
app.use((err, req, res, next) => {
|
|
28
|
+
console.error('Error:', err)
|
|
29
|
+
res.status(500).json({
|
|
30
|
+
success: false,
|
|
31
|
+
error: err.message || 'Internal Server Error'
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// 仅在直接运行时启动服务器(被 import 时不启动)
|
|
36
|
+
// 使用路径比较,兼容 Windows 和 Unix 系统
|
|
37
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
38
|
+
const entryPoint = resolve(process.argv[1] || '')
|
|
39
|
+
|
|
40
|
+
if (__filename === entryPoint) {
|
|
41
|
+
app.listen(PORT, () => {
|
|
42
|
+
console.log(`🚀 DMLA 本地服务已启动`)
|
|
43
|
+
console.log(` API: http://localhost:${PORT}`)
|
|
44
|
+
console.log(` 健康检查: http://localhost:${PORT}/api/health`)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default app
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 沙箱 API 路由
|
|
3
|
+
*/
|
|
4
|
+
import { Router } from 'express'
|
|
5
|
+
import { runPythonCode, checkImageExists, checkGPUAvailable } from '../sandbox.js'
|
|
6
|
+
|
|
7
|
+
const router = Router()
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 健康检查
|
|
11
|
+
*/
|
|
12
|
+
router.get('/health', async (req, res) => {
|
|
13
|
+
try {
|
|
14
|
+
const imageCpuExists = await checkImageExists(false)
|
|
15
|
+
const imageGpuExists = await checkImageExists(true)
|
|
16
|
+
const gpuAvailable = await checkGPUAvailable()
|
|
17
|
+
|
|
18
|
+
res.json({
|
|
19
|
+
status: 'ok',
|
|
20
|
+
images: {
|
|
21
|
+
cpu: imageCpuExists,
|
|
22
|
+
gpu: imageGpuExists
|
|
23
|
+
},
|
|
24
|
+
gpu: gpuAvailable
|
|
25
|
+
})
|
|
26
|
+
} catch (error) {
|
|
27
|
+
res.status(500).json({
|
|
28
|
+
status: 'error',
|
|
29
|
+
error: error.message
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 执行代码
|
|
36
|
+
* POST /api/sandbox/run
|
|
37
|
+
* Body: { code: string, useGpu?: boolean }
|
|
38
|
+
*/
|
|
39
|
+
router.post('/run', async (req, res) => {
|
|
40
|
+
const { code, useGpu = false } = req.body
|
|
41
|
+
|
|
42
|
+
// 验证请求
|
|
43
|
+
if (!code || typeof code !== 'string') {
|
|
44
|
+
return res.status(400).json({
|
|
45
|
+
success: false,
|
|
46
|
+
error: 'Missing or invalid code parameter'
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 代码长度限制 (约 100KB)
|
|
51
|
+
if (code.length > 100000) {
|
|
52
|
+
return res.status(400).json({
|
|
53
|
+
success: false,
|
|
54
|
+
error: 'Code too long (max 100KB)'
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// 检查镜像是否存在
|
|
60
|
+
const imageExists = await checkImageExists(useGpu)
|
|
61
|
+
|
|
62
|
+
if (!imageExists) {
|
|
63
|
+
return res.status(503).json({
|
|
64
|
+
success: false,
|
|
65
|
+
error: useGpu
|
|
66
|
+
? 'GPU 镜像未安装。请运行以下命令安装:\n\nnpm run build:sandbox:gpu\n\n或使用 dmla CLI:\n\ndmla install --gpu'
|
|
67
|
+
: '沙箱镜像未安装。请运行以下命令安装:\n\nnpm run build:sandbox:cpu\n\n或使用 dmla CLI:\n\ndmla install --cpu'
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 如果请求 GPU,检查 GPU 是否可用
|
|
72
|
+
if (useGpu) {
|
|
73
|
+
const gpuAvailable = await checkGPUAvailable()
|
|
74
|
+
if (!gpuAvailable) {
|
|
75
|
+
return res.status(503).json({
|
|
76
|
+
success: false,
|
|
77
|
+
error: 'GPU 硬件不可用。请确保系统安装了 NVIDIA GPU 驱动和 nvidia-container-toolkit。\n\n诊断步骤:\n1. 运行 nvidia-smi 检查 GPU 状态\n2. 运行 docker run --rm --gpus all nvidia/cuda:11.8-base nvidia-smi 测试 Docker GPU 支持\n\n或使用 dmla doctor 进行环境诊断'
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 执行代码
|
|
83
|
+
const result = await runPythonCode(code, useGpu)
|
|
84
|
+
|
|
85
|
+
res.json(result)
|
|
86
|
+
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Sandbox error:', error)
|
|
89
|
+
res.status(500).json({
|
|
90
|
+
success: false,
|
|
91
|
+
error: error.message || 'Internal sandbox error'
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* GPU 状态检查
|
|
98
|
+
*/
|
|
99
|
+
router.get('/gpu', async (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
const gpuAvailable = await checkGPUAvailable()
|
|
102
|
+
|
|
103
|
+
res.json({
|
|
104
|
+
available: gpuAvailable,
|
|
105
|
+
message: gpuAvailable ? 'GPU is available' : 'No GPU detected'
|
|
106
|
+
})
|
|
107
|
+
} catch (error) {
|
|
108
|
+
res.status(500).json({
|
|
109
|
+
available: false,
|
|
110
|
+
error: error.message
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
export default router
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 沙箱管理模块
|
|
3
|
+
* 负责创建和管理 Docker 容器执行 Python 代码
|
|
4
|
+
*/
|
|
5
|
+
import Docker from 'dockerode'
|
|
6
|
+
import path from 'path'
|
|
7
|
+
import { fileURLToPath } from 'url'
|
|
8
|
+
import fs from 'fs'
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
11
|
+
const __dirname = path.dirname(__filename)
|
|
12
|
+
|
|
13
|
+
// 检测运行模式并计算正确的路径
|
|
14
|
+
// 开发模式: 从 local-server/src 运行,项目根目录在上两级
|
|
15
|
+
// 独立模式: 从 packages/cli/src/server 运行,无 shared_modules 目录
|
|
16
|
+
function detectProjectRoot() {
|
|
17
|
+
// 尝试向上两级查找 local-server 目录(开发模式)
|
|
18
|
+
const candidateRoot = path.resolve(__dirname, '..', '..')
|
|
19
|
+
const localServerPath = path.join(candidateRoot, 'local-server')
|
|
20
|
+
if (fs.existsSync(localServerPath)) {
|
|
21
|
+
return candidateRoot
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 尝试向上三级查找(独立模式下的项目根目录)
|
|
25
|
+
const standaloneRoot = path.resolve(__dirname, '..', '..', '..')
|
|
26
|
+
const standaloneLocalServer = path.join(standaloneRoot, 'local-server')
|
|
27
|
+
if (fs.existsSync(standaloneLocalServer)) {
|
|
28
|
+
return standaloneRoot
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 独立安装模式,无项目根目录
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const PROJECT_ROOT = detectProjectRoot()
|
|
36
|
+
|
|
37
|
+
// 共享模块目录(仅开发模式可用)
|
|
38
|
+
const DEFAULT_SHARED_MODULES_PATH = PROJECT_ROOT
|
|
39
|
+
? path.join(PROJECT_ROOT, 'local-server', 'shared_modules')
|
|
40
|
+
: null
|
|
41
|
+
|
|
42
|
+
const docker = new Docker()
|
|
43
|
+
|
|
44
|
+
// 沙箱配置
|
|
45
|
+
const SANDBOX_CONFIG = {
|
|
46
|
+
imageCpu: 'dmla-sandbox:cpu',
|
|
47
|
+
imageGpu: 'dmla-sandbox:gpu',
|
|
48
|
+
timeout: 60000, // 60 秒超时
|
|
49
|
+
memory: 4 * 1024 * 1024 * 1024 // 4GB 内存
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 获取共享模块路径
|
|
54
|
+
*/
|
|
55
|
+
function getSharedModulesPath() {
|
|
56
|
+
// 优先使用环境变量指定的路径
|
|
57
|
+
if (process.env.SHARED_MODULES_PATH) {
|
|
58
|
+
return process.env.SHARED_MODULES_PATH
|
|
59
|
+
}
|
|
60
|
+
// 开发模式下的默认路径
|
|
61
|
+
return DEFAULT_SHARED_MODULES_PATH
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 检查是否启用 Volume Mount
|
|
66
|
+
*/
|
|
67
|
+
function shouldMountSharedModules() {
|
|
68
|
+
return process.env.MOUNT_SHARED_MODULES !== 'false'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 检查 GPU 是否可用
|
|
73
|
+
* 通过运行 nvidia-smi 命令检测 GPU 状态
|
|
74
|
+
*/
|
|
75
|
+
export async function checkGPUAvailable() {
|
|
76
|
+
let container = null
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// 创建容器运行 nvidia-smi 命令
|
|
80
|
+
container = await docker.createContainer({
|
|
81
|
+
Image: 'nvidia/cuda:11.8-base',
|
|
82
|
+
Cmd: ['nvidia-smi', '-L'],
|
|
83
|
+
HostConfig: {
|
|
84
|
+
DeviceRequests: [{
|
|
85
|
+
Driver: 'nvidia',
|
|
86
|
+
Count: -1, // 使用所有 GPU
|
|
87
|
+
Capabilities: [['gpu']]
|
|
88
|
+
}]
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// 启动容器
|
|
93
|
+
await container.start()
|
|
94
|
+
|
|
95
|
+
// 等待执行完成
|
|
96
|
+
await container.wait()
|
|
97
|
+
|
|
98
|
+
// 获取输出日志
|
|
99
|
+
const logs = await container.logs({
|
|
100
|
+
stdout: true,
|
|
101
|
+
stderr: true
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// 解析输出
|
|
105
|
+
const output = parseDockerLogs(logs)
|
|
106
|
+
|
|
107
|
+
// 检查输出是否包含 GPU 信息
|
|
108
|
+
return output.includes('GPU')
|
|
109
|
+
} catch {
|
|
110
|
+
// GPU 不可用或 Docker/nvidia-smi 执行失败
|
|
111
|
+
return false
|
|
112
|
+
} finally {
|
|
113
|
+
// 清理容器
|
|
114
|
+
if (container) {
|
|
115
|
+
try {
|
|
116
|
+
await container.remove({ force: true })
|
|
117
|
+
} catch {
|
|
118
|
+
// 忽略清理错误
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 执行 Python 代码
|
|
126
|
+
* 使用 IPython Kernel 执行代码,支持富输出(图片、文本、错误等)
|
|
127
|
+
* @param {string} code - Python 代码
|
|
128
|
+
* @param {boolean} useGpu - 是否使用 GPU
|
|
129
|
+
* @returns {Promise<{success: boolean, outputs: Array, executionTime: number, gpuUsed: boolean}>}
|
|
130
|
+
*/
|
|
131
|
+
export async function runPythonCode(code, useGpu = false) {
|
|
132
|
+
const startTime = Date.now()
|
|
133
|
+
|
|
134
|
+
// 选择镜像
|
|
135
|
+
const image = useGpu ? SANDBOX_CONFIG.imageGpu : SANDBOX_CONFIG.imageCpu
|
|
136
|
+
|
|
137
|
+
// 创建容器配置 - 使用 kernel_runner.py 执行代码
|
|
138
|
+
const containerConfig = {
|
|
139
|
+
Image: image,
|
|
140
|
+
Cmd: ['python3', '/workspace/kernel_runner.py', '--code', code, '--timeout', String(Math.floor(SANDBOX_CONFIG.timeout / 1000))],
|
|
141
|
+
HostConfig: {
|
|
142
|
+
Memory: SANDBOX_CONFIG.memory,
|
|
143
|
+
AutoRemove: false // 手动移除以获取日志
|
|
144
|
+
},
|
|
145
|
+
Env: [
|
|
146
|
+
'PYTHONUNBUFFERED=1'
|
|
147
|
+
// matplotlib 使用 IPython Kernel 的 inline 后端,自动发送 display_data
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Volume Mount 配置 - 挂载共享模块
|
|
152
|
+
const useMount = shouldMountSharedModules()
|
|
153
|
+
const sharedModulesPath = getSharedModulesPath()
|
|
154
|
+
|
|
155
|
+
if (useMount && sharedModulesPath) {
|
|
156
|
+
// 检查共享模块目录是否存在
|
|
157
|
+
if (fs.existsSync(sharedModulesPath)) {
|
|
158
|
+
containerConfig.HostConfig.Binds = [
|
|
159
|
+
`${sharedModulesPath}:/usr/local/lib/python3.11/site-packages/shared:ro`
|
|
160
|
+
]
|
|
161
|
+
console.log(`[Sandbox] Volume Mount 已启用: ${sharedModulesPath}`)
|
|
162
|
+
} else {
|
|
163
|
+
console.warn(`[Sandbox] 警告: 共享模块目录不存在: ${sharedModulesPath}`)
|
|
164
|
+
console.warn('[Sandbox] 提示: 运行 npm run extract:shared 生成共享模块')
|
|
165
|
+
}
|
|
166
|
+
} else if (!sharedModulesPath) {
|
|
167
|
+
console.log('[Sandbox] 独立安装模式,无共享模块目录')
|
|
168
|
+
} else {
|
|
169
|
+
console.log('[Sandbox] Volume Mount 已禁用 (MOUNT_SHARED_MODULES=false)')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// GPU 配置
|
|
173
|
+
if (useGpu) {
|
|
174
|
+
containerConfig.HostConfig.DeviceRequests = [{
|
|
175
|
+
Driver: 'nvidia',
|
|
176
|
+
Count: -1, // 使用所有 GPU
|
|
177
|
+
Capabilities: [['gpu']]
|
|
178
|
+
}]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let container = null
|
|
182
|
+
let timeoutId = null
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
// 创建容器
|
|
186
|
+
container = await docker.createContainer(containerConfig)
|
|
187
|
+
|
|
188
|
+
// 设置超时
|
|
189
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
190
|
+
timeoutId = setTimeout(() => {
|
|
191
|
+
reject(new Error('Execution timeout'))
|
|
192
|
+
}, SANDBOX_CONFIG.timeout + 10000) // 额外 10 秒用于清理
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// 启动容器
|
|
196
|
+
await container.start()
|
|
197
|
+
|
|
198
|
+
// 等待执行完成
|
|
199
|
+
const waitPromise = container.wait()
|
|
200
|
+
|
|
201
|
+
// 竞速: 超时 vs 正常完成
|
|
202
|
+
const result = await Promise.race([waitPromise, timeoutPromise])
|
|
203
|
+
|
|
204
|
+
// 清除超时
|
|
205
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
206
|
+
|
|
207
|
+
// 获取输出
|
|
208
|
+
const logs = await container.logs({
|
|
209
|
+
stdout: true,
|
|
210
|
+
stderr: true
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// 解析输出
|
|
214
|
+
const rawOutput = parseDockerLogs(logs)
|
|
215
|
+
|
|
216
|
+
// 解析 JSON 输出
|
|
217
|
+
let parsedResult
|
|
218
|
+
try {
|
|
219
|
+
parsedResult = JSON.parse(rawOutput)
|
|
220
|
+
} catch (parseError) {
|
|
221
|
+
// 如果 JSON 解析失败,返回原始输出作为错误
|
|
222
|
+
const executionTime = (Date.now() - startTime) / 1000
|
|
223
|
+
return {
|
|
224
|
+
success: false,
|
|
225
|
+
outputs: [{
|
|
226
|
+
type: 'error',
|
|
227
|
+
ename: 'OutputParseError',
|
|
228
|
+
evalue: 'Failed to parse kernel output',
|
|
229
|
+
traceback: [rawOutput]
|
|
230
|
+
}],
|
|
231
|
+
executionTime,
|
|
232
|
+
gpuUsed: useGpu
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
success: parsedResult.success,
|
|
238
|
+
outputs: parsedResult.outputs || [],
|
|
239
|
+
executionTime: parsedResult.executionTime || (Date.now() - startTime) / 1000,
|
|
240
|
+
gpuUsed: useGpu
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
} catch (error) {
|
|
244
|
+
// 清除超时
|
|
245
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
246
|
+
|
|
247
|
+
const executionTime = (Date.now() - startTime) / 1000
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
success: false,
|
|
251
|
+
outputs: [{
|
|
252
|
+
type: 'error',
|
|
253
|
+
ename: error.name || 'ExecutionError',
|
|
254
|
+
evalue: error.message || 'Unknown error',
|
|
255
|
+
traceback: [error.message || 'Unknown error']
|
|
256
|
+
}],
|
|
257
|
+
executionTime,
|
|
258
|
+
gpuUsed: useGpu
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
} finally {
|
|
262
|
+
// 清理容器
|
|
263
|
+
if (container) {
|
|
264
|
+
try {
|
|
265
|
+
await container.remove({ force: true })
|
|
266
|
+
} catch (e) {
|
|
267
|
+
console.warn('Failed to remove container:', e.message)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 解析 Docker 日志输出
|
|
275
|
+
* Docker 日志格式: [8字节头][数据]
|
|
276
|
+
*/
|
|
277
|
+
function parseDockerLogs(logs) {
|
|
278
|
+
if (!logs || logs.length === 0) return ''
|
|
279
|
+
|
|
280
|
+
// 如果是 Buffer
|
|
281
|
+
if (Buffer.isBuffer(logs)) {
|
|
282
|
+
let output = ''
|
|
283
|
+
let offset = 0
|
|
284
|
+
|
|
285
|
+
while (offset < logs.length) {
|
|
286
|
+
// 跳过 8 字节头
|
|
287
|
+
if (offset + 8 > logs.length) break
|
|
288
|
+
|
|
289
|
+
const streamType = logs[offset]
|
|
290
|
+
const length = logs.readUInt32BE(offset + 4)
|
|
291
|
+
|
|
292
|
+
offset += 8
|
|
293
|
+
|
|
294
|
+
if (offset + length > logs.length) break
|
|
295
|
+
|
|
296
|
+
const chunk = logs.slice(offset, offset + length).toString('utf8')
|
|
297
|
+
output += chunk
|
|
298
|
+
offset += length
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return output
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 如果是字符串
|
|
305
|
+
return logs.toString()
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* 检查沙箱镜像是否存在
|
|
310
|
+
*/
|
|
311
|
+
export async function checkImageExists(useGpu = false) {
|
|
312
|
+
const image = useGpu ? SANDBOX_CONFIG.imageGpu : SANDBOX_CONFIG.imageCpu
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
await docker.getImage(image).inspect()
|
|
316
|
+
return true
|
|
317
|
+
} catch {
|
|
318
|
+
return false
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 拉取沙箱镜像
|
|
324
|
+
*/
|
|
325
|
+
export async function pullImage(useGpu = false) {
|
|
326
|
+
const image = useGpu ? SANDBOX_CONFIG.imageGpu : SANDBOX_CONFIG.imageCpu
|
|
327
|
+
|
|
328
|
+
return new Promise((resolve, reject) => {
|
|
329
|
+
docker.pull(image, (err, stream) => {
|
|
330
|
+
if (err) {
|
|
331
|
+
reject(err)
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
docker.modem.followProgress(stream, (err, output) => {
|
|
336
|
+
if (err) {
|
|
337
|
+
reject(err)
|
|
338
|
+
} else {
|
|
339
|
+
resolve(output)
|
|
340
|
+
}
|
|
341
|
+
})
|
|
342
|
+
})
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export default {
|
|
347
|
+
runPythonCode,
|
|
348
|
+
checkGPUAvailable,
|
|
349
|
+
checkImageExists,
|
|
350
|
+
pullImage
|
|
351
|
+
}
|
package/tests/cli.test.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI 命令单元测试
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect, beforeAll } from '@jest/globals'
|
|
5
|
-
import { fileURLToPath } from 'url'
|
|
6
|
-
import path from 'path'
|
|
7
|
-
import fs from 'fs'
|
|
8
|
-
|
|
9
|
-
// ESM 中获取 __dirname
|
|
10
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
11
|
-
const __dirname = path.dirname(__filename)
|
|
12
|
-
|
|
13
|
-
// 基础测试:确保模块可导入
|
|
14
|
-
describe('CLI Module', () => {
|
|
15
|
-
beforeAll(() => {
|
|
16
|
-
// 设置测试环境
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('should have correct package.json', async () => {
|
|
20
|
-
const pkgPath = path.resolve(__dirname, '../package.json')
|
|
21
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
22
|
-
|
|
23
|
-
expect(pkg.name).toBe('@icyfenix-dmla/cli')
|
|
24
|
-
expect(pkg.bin).toBeDefined()
|
|
25
|
-
expect(pkg.bin.dmla).toBe('./bin/dmla.js')
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('should have commander dependency', async () => {
|
|
29
|
-
const pkgPath = path.resolve(__dirname, '../package.json')
|
|
30
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
31
|
-
|
|
32
|
-
expect(pkg.dependencies.commander).toBeDefined()
|
|
33
|
-
})
|
|
34
|
-
})
|