@icyfenix-dmla/cli 2026.4.19-954 → 2026.5.2-7
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 +2 -2
- package/src/commands/manage.js +65 -74
- package/src/commands/server.js +266 -37
- package/src/index.js +78 -21
- package/src/server/index.js +72 -5
- package/src/server/routes/sandbox.js +61 -8
- package/src/server/sandbox.js +324 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@icyfenix-dmla/cli",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.5.2-7",
|
|
4
4
|
"description": "DMLA 沙箱服务命令行工具",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"commander": "^12.1.0",
|
|
24
24
|
"chalk": "^5.3.0",
|
|
25
25
|
"enquirer": "^2.4.1",
|
|
26
|
-
"dockerode": "^
|
|
26
|
+
"dockerode": "^5.0.0",
|
|
27
27
|
"express": "^4.21.2",
|
|
28
28
|
"cors": "^2.8.5",
|
|
29
29
|
"@icyfenix-dmla/install": "*"
|
package/src/commands/manage.js
CHANGED
|
@@ -66,7 +66,7 @@ export async function installImages(types, registry = 'dockerhub') {
|
|
|
66
66
|
|
|
67
67
|
console.log()
|
|
68
68
|
console.log(chalk.green('🎉 镜像安装完成'))
|
|
69
|
-
console.log(chalk.yellow('
|
|
69
|
+
console.log(chalk.yellow('提示: 运行 dmla start 启动服务'))
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
@@ -105,70 +105,6 @@ async function pullImageWithProgress(imageName) {
|
|
|
105
105
|
})
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
/**
|
|
109
|
-
* 更新所有组件
|
|
110
|
-
*/
|
|
111
|
-
export async function updateAll(registry = 'dockerhub') {
|
|
112
|
-
console.log()
|
|
113
|
-
|
|
114
|
-
// 更新 npm 包
|
|
115
|
-
console.log(chalk.bold('📦 更新 npm 包'))
|
|
116
|
-
try {
|
|
117
|
-
console.log(chalk.gray(' 执行 npm update -g @icyfenix-dmla/cli...'))
|
|
118
|
-
execSync('npm update -g @icyfenix-dmla/cli', { stdio: 'inherit' })
|
|
119
|
-
console.log(chalk.green('✅ npm 包已更新'))
|
|
120
|
-
} catch (error) {
|
|
121
|
-
console.log(chalk.yellow('⚠️ npm 包更新失败或已是最新版本'))
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
console.log()
|
|
125
|
-
|
|
126
|
-
// 检查并更新镜像
|
|
127
|
-
console.log(chalk.bold('🖼️ 检查 Docker 镜像更新'))
|
|
128
|
-
const registryUrl = getRegistryUrl(registry)
|
|
129
|
-
|
|
130
|
-
for (const type of ['cpu', 'gpu']) {
|
|
131
|
-
const remoteImage = `${registryUrl}:${type}`
|
|
132
|
-
const localImage = type === 'gpu' ? CONFIG.imageGpu : CONFIG.imageCpu
|
|
133
|
-
|
|
134
|
-
console.log(chalk.gray(` 检查 ${type.toUpperCase()} 版本...`))
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
// 检查本地镜像是否存在
|
|
138
|
-
let localImageInfo = null
|
|
139
|
-
try {
|
|
140
|
-
localImageInfo = await docker.getImage(localImage).inspect()
|
|
141
|
-
} catch {
|
|
142
|
-
// 本地镜像不存在,需要拉取
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// 拉取最新镜像
|
|
146
|
-
console.log(chalk.gray(` 拉取最新 ${type.toUpperCase()} 镜像...`))
|
|
147
|
-
await pullImageWithProgress(remoteImage)
|
|
148
|
-
|
|
149
|
-
// 获取拉取的镜像信息
|
|
150
|
-
const remoteImageInfo = await docker.getImage(remoteImage).inspect()
|
|
151
|
-
|
|
152
|
-
// 比较镜像 ID
|
|
153
|
-
if (localImageInfo && localImageInfo.Id === remoteImageInfo.Id) {
|
|
154
|
-
console.log(chalk.green(`✅ ${type.toUpperCase()} 镜像已是最新版本`))
|
|
155
|
-
} else {
|
|
156
|
-
// Tag 为本地名称
|
|
157
|
-
console.log(chalk.gray(` 重命名为 ${localImage}...`))
|
|
158
|
-
const image = docker.getImage(remoteImage)
|
|
159
|
-
await image.tag({ repo: CONFIG.imageName, tag: type })
|
|
160
|
-
|
|
161
|
-
console.log(chalk.green(`✅ ${type.toUpperCase()} 镜像已更新`))
|
|
162
|
-
}
|
|
163
|
-
} catch (error) {
|
|
164
|
-
console.log(chalk.yellow(`⚠️ ${type.toUpperCase()} 镜像更新失败: ${error.message}`))
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
console.log()
|
|
169
|
-
console.log(chalk.green('🎉 更新完成'))
|
|
170
|
-
}
|
|
171
|
-
|
|
172
108
|
/**
|
|
173
109
|
* 环境诊断
|
|
174
110
|
*/
|
|
@@ -201,7 +137,7 @@ export async function runDoctor() {
|
|
|
201
137
|
// ───────────────────────────────────────────────────────────
|
|
202
138
|
// 镜像检查
|
|
203
139
|
// ───────────────────────────────────────────────────────────
|
|
204
|
-
console.log(chalk.bold('
|
|
140
|
+
console.log(chalk.bold('Docker 镜像'))
|
|
205
141
|
|
|
206
142
|
const cpuImage = CONFIG.imageCpu
|
|
207
143
|
const gpuImage = CONFIG.imageGpu
|
|
@@ -234,18 +170,73 @@ export async function runDoctor() {
|
|
|
234
170
|
// ───────────────────────────────────────────────────────────
|
|
235
171
|
// GPU 检查
|
|
236
172
|
// ───────────────────────────────────────────────────────────
|
|
237
|
-
console.log(chalk.bold('
|
|
173
|
+
console.log(chalk.bold('GPU 驱动'))
|
|
238
174
|
|
|
239
175
|
try {
|
|
240
|
-
|
|
176
|
+
// 获取完整的 nvidia-smi 输出以解析驱动和 CUDA 版本
|
|
177
|
+
const output = execSync('nvidia-smi', { timeout: 5000, encoding: 'utf8' })
|
|
178
|
+
|
|
179
|
+
// 解析驱动版本
|
|
180
|
+
const driverMatch = output.match(/Driver Version:\s*(\d+\.\d+)/)
|
|
181
|
+
const driverVersion = driverMatch ? driverMatch[1] : null
|
|
182
|
+
|
|
183
|
+
// 解析 CUDA 兼容上限
|
|
184
|
+
const cudaMatch = output.match(/CUDA Version:\s*(\d+\.\d+)/)
|
|
185
|
+
const cudaVersion = cudaMatch ? cudaMatch[1] : null
|
|
186
|
+
|
|
241
187
|
if (output.includes('GPU')) {
|
|
242
188
|
console.log(chalk.green(' ✅ NVIDIA GPU 可用'))
|
|
189
|
+
|
|
190
|
+
// 显示驱动版本和 CUDA 兼容上限
|
|
191
|
+
if (driverVersion) {
|
|
192
|
+
console.log(chalk.gray(` 驱动版本: ${driverVersion}`))
|
|
193
|
+
}
|
|
194
|
+
if (cudaVersion) {
|
|
195
|
+
console.log(chalk.gray(` CUDA 兼容上限: ${cudaVersion}`))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// GPU 镜像兼容性检查(CUDA 12.8 需要驱动 >= 570)
|
|
199
|
+
const minDriverForGpuImage = 570
|
|
200
|
+
const driverNum = parseFloat(driverVersion || '0')
|
|
201
|
+
|
|
202
|
+
// 显示 GPU 设备信息
|
|
243
203
|
const lines = output.split('\n').filter(l => l.trim())
|
|
244
|
-
lines.forEach(line =>
|
|
204
|
+
lines.slice(0, 20).forEach(line => {
|
|
205
|
+
if (line.includes('GPU') && !line.includes('Driver Version') && !line.includes('CUDA Version')) {
|
|
206
|
+
console.log(chalk.gray(` ${line.trim()}`))
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
console.log()
|
|
211
|
+
|
|
212
|
+
// GPU 镜像兼容性诊断
|
|
213
|
+
console.log(chalk.bold('GPU 镜像兼容性'))
|
|
214
|
+
|
|
215
|
+
if (gpuExists) {
|
|
216
|
+
if (driverNum >= minDriverForGpuImage) {
|
|
217
|
+
console.log(chalk.green(` ✅ GPU 镜像可用 (驱动 ${driverVersion} >= ${minDriverForGpuImage})`))
|
|
218
|
+
} else {
|
|
219
|
+
console.log(chalk.red(` ❌ GPU 镜像不兼容 (驱动 ${driverVersion} < ${minDriverForGpuImage})`))
|
|
220
|
+
console.log(chalk.yellow(` 💡 CUDA 12.8 需要驱动 >= ${minDriverForGpuImage}`))
|
|
221
|
+
console.log(chalk.yellow(' 解决方案:'))
|
|
222
|
+
console.log(chalk.gray(' 1. 升级 NVIDIA 驱动到 570+ 版本'))
|
|
223
|
+
console.log(chalk.gray(' 2. 或在前端选择 "Run on CPU" 模式'))
|
|
224
|
+
issues.push('GPU 镜像不兼容,请升级驱动或使用 CPU 模式')
|
|
225
|
+
}
|
|
226
|
+
} else if (!gpuExists) {
|
|
227
|
+
console.log(chalk.yellow(' ⚠️ GPU 镜像未安装'))
|
|
228
|
+
if (driverNum >= minDriverForGpuImage) {
|
|
229
|
+
console.log(chalk.green(` ✅ 驱动兼容,可以安装 GPU 镜像`))
|
|
230
|
+
console.log(chalk.gray(' 运行 dmla install --gpu 安装'))
|
|
231
|
+
} else {
|
|
232
|
+
console.log(chalk.yellow(` ⚠️ 驱动 ${driverVersion} 不兼容 CUDA 12.8`))
|
|
233
|
+
console.log(chalk.yellow(` 💡 需要驱动 >= ${minDriverForGpuImage} 才能使用 GPU 镜像`))
|
|
234
|
+
}
|
|
235
|
+
}
|
|
245
236
|
|
|
246
237
|
// 检查 GPU 镜像
|
|
247
|
-
if (!gpuExists) {
|
|
248
|
-
console.log(chalk.yellow(' 💡
|
|
238
|
+
if (!gpuExists && driverNum >= minDriverForGpuImage) {
|
|
239
|
+
console.log(chalk.yellow(' 💡 检测到兼容 GPU,建议安装 GPU 镜像'))
|
|
249
240
|
issues.push('运行 dmla install --gpu 安装 GPU 镜像')
|
|
250
241
|
}
|
|
251
242
|
} else {
|
|
@@ -261,7 +252,7 @@ export async function runDoctor() {
|
|
|
261
252
|
// ───────────────────────────────────────────────────────────
|
|
262
253
|
// 端口检查
|
|
263
254
|
// ───────────────────────────────────────────────────────────
|
|
264
|
-
console.log(chalk.bold('
|
|
255
|
+
console.log(chalk.bold('端口可用性'))
|
|
265
256
|
|
|
266
257
|
const port = CONFIG.defaultPort
|
|
267
258
|
const portAvailable = await checkPortAvailable(port)
|
|
@@ -310,11 +301,11 @@ export async function runDoctor() {
|
|
|
310
301
|
console.log(chalk.red(` ${i + 1}. ${issue}`))
|
|
311
302
|
})
|
|
312
303
|
console.log()
|
|
313
|
-
console.log(chalk.yellow('
|
|
304
|
+
console.log(chalk.yellow('请根据上述提示解决问题后再次运行 dmla doctor'))
|
|
314
305
|
} else {
|
|
315
306
|
console.log(chalk.bold.green('✅ 所有检查通过,环境正常'))
|
|
316
307
|
console.log()
|
|
317
|
-
console.log(chalk.gray('
|
|
308
|
+
console.log(chalk.gray('运行 dmla start 启动服务'))
|
|
318
309
|
}
|
|
319
310
|
}
|
|
320
311
|
|
package/src/commands/server.js
CHANGED
|
@@ -6,7 +6,7 @@ import Docker from 'dockerode'
|
|
|
6
6
|
import { spawn } from 'child_process'
|
|
7
7
|
import http from 'http'
|
|
8
8
|
import path from 'path'
|
|
9
|
-
import { fileURLToPath } from 'url'
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'url'
|
|
10
10
|
import fs from 'fs'
|
|
11
11
|
|
|
12
12
|
const __filename = fileURLToPath(import.meta.url)
|
|
@@ -49,6 +49,41 @@ async function checkImageExists(type) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* 检查可用镜像并自动选择
|
|
54
|
+
* @returns {Object} { imageType: 'cpu'|'gpu', message: string }
|
|
55
|
+
*/
|
|
56
|
+
async function resolveImageType(useGpu) {
|
|
57
|
+
const cpuExists = await checkImageExists('cpu')
|
|
58
|
+
const gpuExists = await checkImageExists('gpu')
|
|
59
|
+
|
|
60
|
+
// 用户明确指定了 GPU
|
|
61
|
+
if (useGpu) {
|
|
62
|
+
if (gpuExists) {
|
|
63
|
+
return { imageType: 'gpu', message: 'GPU' }
|
|
64
|
+
}
|
|
65
|
+
// 用户想要 GPU 但 GPU 镜像不存在
|
|
66
|
+
if (cpuExists) {
|
|
67
|
+
console.log(chalk.yellow('⚠️ GPU 镜像不存在,将使用 CPU 镜像'))
|
|
68
|
+
return { imageType: 'cpu', message: 'CPU (降级)' }
|
|
69
|
+
}
|
|
70
|
+
return { imageType: null, message: '无可用镜像' }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 用户未指定,自动选择
|
|
74
|
+
if (cpuExists) {
|
|
75
|
+
return { imageType: 'cpu', message: 'CPU' }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// CPU 镜像不存在
|
|
79
|
+
if (gpuExists) {
|
|
80
|
+
console.log(chalk.yellow('⚠️ CPU 镜像不存在,自动使用 GPU 镜像'))
|
|
81
|
+
return { imageType: 'gpu', message: 'GPU (自动)' }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { imageType: null, message: '无可用镜像' }
|
|
85
|
+
}
|
|
86
|
+
|
|
52
87
|
/**
|
|
53
88
|
* 检查 GPU 是否可用
|
|
54
89
|
*/
|
|
@@ -72,6 +107,44 @@ async function checkGPUAvailable() {
|
|
|
72
107
|
}
|
|
73
108
|
}
|
|
74
109
|
|
|
110
|
+
/**
|
|
111
|
+
* 检查 GPU 驱动兼容性
|
|
112
|
+
* @returns {Promise<{compatible: boolean, driverVersion: string|null, cudaVersion: string|null}>}
|
|
113
|
+
*/
|
|
114
|
+
async function checkGPUDriverCompatibility() {
|
|
115
|
+
const minDriverForCuda128 = 570
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const result = await new Promise((resolve, reject) => {
|
|
119
|
+
const proc = spawn('nvidia-smi', [], { timeout: 5000 })
|
|
120
|
+
let output = ''
|
|
121
|
+
proc.stdout.on('data', (data) => output += data.toString())
|
|
122
|
+
proc.stderr.on('data', (data) => output += data.toString())
|
|
123
|
+
proc.on('close', (code) => {
|
|
124
|
+
if (code === 0) resolve(output)
|
|
125
|
+
else reject(new Error('nvidia-smi failed'))
|
|
126
|
+
})
|
|
127
|
+
proc.on('error', reject)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// 解析驱动版本
|
|
131
|
+
const driverMatch = result.match(/Driver Version:\s*(\d+\.\d+)/)
|
|
132
|
+
const driverVersion = driverMatch ? driverMatch[1] : null
|
|
133
|
+
|
|
134
|
+
// 解析 CUDA 兼容上限
|
|
135
|
+
const cudaMatch = result.match(/CUDA Version:\s*(\d+\.\d+)/)
|
|
136
|
+
const cudaVersion = cudaMatch ? cudaMatch[1] : null
|
|
137
|
+
|
|
138
|
+
// 判断兼容性
|
|
139
|
+
const driverNum = parseFloat(driverVersion || '0')
|
|
140
|
+
const compatible = driverNum >= minDriverForCuda128
|
|
141
|
+
|
|
142
|
+
return { compatible, driverVersion, cudaVersion }
|
|
143
|
+
} catch {
|
|
144
|
+
return { compatible: false, driverVersion: null, cudaVersion: null }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
75
148
|
/**
|
|
76
149
|
* 检查服务是否运行
|
|
77
150
|
*/
|
|
@@ -117,35 +190,64 @@ async function findServiceContainer() {
|
|
|
117
190
|
* 查找服务器入口文件
|
|
118
191
|
*/
|
|
119
192
|
function findServerPath() {
|
|
193
|
+
// 开发环境路径:packages/cli/src/commands -> ../../../local-server/src/index.js
|
|
120
194
|
const serverPath = path.resolve(__dirname, '../../../local-server/src/index.js')
|
|
195
|
+
// npm 包路径:packages/cli/src/commands -> ../server/index.js
|
|
121
196
|
const standaloneServerPath = path.resolve(__dirname, '../server/index.js')
|
|
122
197
|
|
|
123
|
-
|
|
124
|
-
|
|
198
|
+
// 检查 __dirname 是否正确(调试用)
|
|
199
|
+
const cliPackageRoot = path.resolve(__dirname, '../..')
|
|
200
|
+
const expectedServerDir = path.resolve(cliPackageRoot, 'src/server')
|
|
201
|
+
|
|
202
|
+
if (fs.existsSync(serverPath)) {
|
|
203
|
+
return serverPath
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (fs.existsSync(standaloneServerPath)) {
|
|
207
|
+
return standaloneServerPath
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 调试输出:显示路径信息帮助诊断
|
|
211
|
+
console.log(chalk.yellow('⚠️ 服务入口文件查找失败'))
|
|
212
|
+
console.log(chalk.gray(` __dirname: ${__dirname}`))
|
|
213
|
+
console.log(chalk.gray(` 开发路径: ${serverPath} (${fs.existsSync(serverPath) ? '存在' : '不存在'})`))
|
|
214
|
+
console.log(chalk.gray(` npm路径: ${standaloneServerPath} (${fs.existsSync(standaloneServerPath) ? '存在' : '不存在'})`))
|
|
215
|
+
console.log(chalk.gray(` CLI包根目录: ${cliPackageRoot}`))
|
|
216
|
+
|
|
217
|
+
// 检查 CLI 包根目录下的文件结构
|
|
218
|
+
const srcDir = path.resolve(cliPackageRoot, 'src')
|
|
219
|
+
if (fs.existsSync(srcDir)) {
|
|
220
|
+
console.log(chalk.gray(` src目录内容: ${fs.readdirSync(srcDir).join(', ')}`))
|
|
221
|
+
if (fs.existsSync(expectedServerDir)) {
|
|
222
|
+
console.log(chalk.gray(` server目录内容: ${fs.readdirSync(expectedServerDir).join(', ')}`))
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return null
|
|
125
227
|
}
|
|
126
228
|
|
|
127
229
|
/**
|
|
128
230
|
* 同步启动服务(在当前进程运行,用于调试)
|
|
129
231
|
* @param {number} port - 服务端口
|
|
130
|
-
* @param {boolean} useGpu - 是否使用 GPU
|
|
232
|
+
* @param {boolean} useGpu - 是否使用 GPU(可选,自动检测)
|
|
131
233
|
*/
|
|
132
234
|
export async function startServerSync(port, useGpu = false) {
|
|
133
235
|
// 检查端口
|
|
134
236
|
const portAvailable = await checkPortAvailable(port)
|
|
135
237
|
if (!portAvailable) {
|
|
136
238
|
console.log(chalk.red(`❌ 端口 ${port} 已被占用`))
|
|
137
|
-
console.log(chalk.yellow('
|
|
239
|
+
console.log(chalk.yellow('提示: 使用 --port 选项指定其他端口'))
|
|
138
240
|
return
|
|
139
241
|
}
|
|
140
242
|
|
|
141
|
-
//
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
console.log(chalk.
|
|
146
|
-
console.log(chalk.yellow('💡 提示: 运行 dmla install 安装镜像'))
|
|
243
|
+
// 智能选择镜像
|
|
244
|
+
const imageResolution = await resolveImageType(useGpu)
|
|
245
|
+
if (!imageResolution.imageType) {
|
|
246
|
+
console.log(chalk.red('❌ 无可用镜像'))
|
|
247
|
+
console.log(chalk.yellow('提示: 运行 dmla install 安装镜像'))
|
|
147
248
|
return
|
|
148
249
|
}
|
|
250
|
+
const resolvedUseGpu = imageResolution.imageType === 'gpu'
|
|
149
251
|
|
|
150
252
|
// 检查服务是否已运行
|
|
151
253
|
const alreadyRunning = await checkServiceRunning(port)
|
|
@@ -154,27 +256,46 @@ export async function startServerSync(port, useGpu = false) {
|
|
|
154
256
|
return
|
|
155
257
|
}
|
|
156
258
|
|
|
259
|
+
// GPU 驱动兼容性预检
|
|
260
|
+
if (resolvedUseGpu) {
|
|
261
|
+
const driverCheck = await checkGPUDriverCompatibility()
|
|
262
|
+
if (!driverCheck.compatible && driverCheck.driverVersion) {
|
|
263
|
+
console.log(chalk.yellow(`⚠️ GPU 驱动兼容性警告`))
|
|
264
|
+
console.log(chalk.gray(` 当前驱动: ${driverCheck.driverVersion}`))
|
|
265
|
+
console.log(chalk.gray(` CUDA 12.8 需要: 驱动 >= 570`))
|
|
266
|
+
console.log(chalk.yellow(' 解决方案:'))
|
|
267
|
+
console.log(chalk.gray(' 1. 升级 NVIDIA 驾动到 570+ 版本'))
|
|
268
|
+
console.log(chalk.gray(' 2. 使用 CPU 模式: dmla start'))
|
|
269
|
+
console.log()
|
|
270
|
+
console.log(chalk.gray(' 继续启动 GPU 模式(可能会失败)...'))
|
|
271
|
+
console.log()
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
157
275
|
// 查找服务器入口
|
|
158
276
|
const actualServerPath = findServerPath()
|
|
159
277
|
if (!actualServerPath) {
|
|
160
278
|
console.log(chalk.red('❌ 找不到服务入口文件'))
|
|
161
|
-
console.log(chalk.yellow('
|
|
279
|
+
console.log(chalk.yellow('提示: 确保正确安装了 @icyfenix-dmla/cli'))
|
|
162
280
|
return
|
|
163
281
|
}
|
|
164
282
|
|
|
283
|
+
console.log(chalk.gray(` 镜像类型: ${imageResolution.message}`))
|
|
165
284
|
console.log(chalk.gray(' 同步模式启动...'))
|
|
166
285
|
console.log(chalk.gray(` 服务入口: ${actualServerPath}`))
|
|
167
286
|
console.log()
|
|
168
287
|
|
|
169
288
|
// 设置环境变量
|
|
170
289
|
process.env.PORT = port.toString()
|
|
171
|
-
process.env.USE_GPU =
|
|
290
|
+
process.env.USE_GPU = resolvedUseGpu ? 'true' : 'false'
|
|
172
291
|
process.env.DMLA_SYNC_MODE = 'true' // 标记同步模式,让服务器在 import 时启动
|
|
173
292
|
|
|
174
293
|
// 动态 import 服务器模块并直接运行
|
|
175
294
|
// 服务器模块会在 import 时自动启动(因为入口点检测逻辑)
|
|
295
|
+
// Windows 需要将路径转换为 file:// URL 格式
|
|
176
296
|
try {
|
|
177
|
-
|
|
297
|
+
const serverURL = pathToFileURL(actualServerPath).href
|
|
298
|
+
await import(serverURL)
|
|
178
299
|
} catch (error) {
|
|
179
300
|
console.log(chalk.red(`❌ 服务启动失败: ${error.message}`))
|
|
180
301
|
console.log(chalk.gray(error.stack))
|
|
@@ -189,18 +310,18 @@ export async function startServer(port, useGpu = false) {
|
|
|
189
310
|
const portAvailable = await checkPortAvailable(port)
|
|
190
311
|
if (!portAvailable) {
|
|
191
312
|
console.log(chalk.red(`❌ 端口 ${port} 已被占用`))
|
|
192
|
-
console.log(chalk.yellow('
|
|
313
|
+
console.log(chalk.yellow('提示: 使用 --port 选项指定其他端口'))
|
|
193
314
|
return
|
|
194
315
|
}
|
|
195
316
|
|
|
196
|
-
//
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
console.log(chalk.
|
|
201
|
-
console.log(chalk.yellow('💡 提示: 运行 dmla install 安装镜像'))
|
|
317
|
+
// 智能选择镜像
|
|
318
|
+
const imageResolution = await resolveImageType(useGpu)
|
|
319
|
+
if (!imageResolution.imageType) {
|
|
320
|
+
console.log(chalk.red('❌ 无可用镜像'))
|
|
321
|
+
console.log(chalk.yellow('提示: 运行 dmla install 安装镜像'))
|
|
202
322
|
return
|
|
203
323
|
}
|
|
324
|
+
const resolvedUseGpu = imageResolution.imageType === 'gpu'
|
|
204
325
|
|
|
205
326
|
// 检查服务是否已运行
|
|
206
327
|
const alreadyRunning = await checkServiceRunning(port)
|
|
@@ -209,7 +330,24 @@ export async function startServer(port, useGpu = false) {
|
|
|
209
330
|
return
|
|
210
331
|
}
|
|
211
332
|
|
|
333
|
+
// GPU 驱动兼容性预检
|
|
334
|
+
if (resolvedUseGpu) {
|
|
335
|
+
const driverCheck = await checkGPUDriverCompatibility()
|
|
336
|
+
if (!driverCheck.compatible && driverCheck.driverVersion) {
|
|
337
|
+
console.log(chalk.yellow(`⚠️ GPU 驱动兼容性警告`))
|
|
338
|
+
console.log(chalk.gray(` 当前驱动: ${driverCheck.driverVersion}`))
|
|
339
|
+
console.log(chalk.gray(` CUDA 12.8 需要: 驱动 >= 570`))
|
|
340
|
+
console.log(chalk.yellow(' 解决方案:'))
|
|
341
|
+
console.log(chalk.gray(' 1. 升级 NVIDIA 驱动到 570+ 版本'))
|
|
342
|
+
console.log(chalk.gray(' 2. 使用 CPU 模式: dmla start'))
|
|
343
|
+
console.log()
|
|
344
|
+
console.log(chalk.gray(' 继续启动 GPU 模式(可能会失败)...'))
|
|
345
|
+
console.log()
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
212
349
|
// 启动服务
|
|
350
|
+
console.log(chalk.gray(` 镜像类型: ${imageResolution.message}`))
|
|
213
351
|
console.log(chalk.gray(' 正在启动...'))
|
|
214
352
|
|
|
215
353
|
try {
|
|
@@ -217,24 +355,65 @@ export async function startServer(port, useGpu = false) {
|
|
|
217
355
|
|
|
218
356
|
if (!actualServerPath) {
|
|
219
357
|
console.log(chalk.red('❌ 找不到服务入口文件'))
|
|
220
|
-
console.log(chalk.yellow('
|
|
358
|
+
console.log(chalk.yellow('提示: 确保正确安装了 @icyfenix-dmla/cli'))
|
|
221
359
|
return
|
|
222
360
|
}
|
|
223
361
|
|
|
362
|
+
// 日志文件路径
|
|
363
|
+
const logDir = path.resolve(__dirname, '../../logs')
|
|
364
|
+
if (!fs.existsSync(logDir)) {
|
|
365
|
+
fs.mkdirSync(logDir, { recursive: true })
|
|
366
|
+
}
|
|
367
|
+
const logFile = path.join(logDir, 'server.log')
|
|
368
|
+
const errorLogFile = path.join(logDir, 'server-error.log')
|
|
369
|
+
|
|
370
|
+
console.log(chalk.gray(` 日志文件: ${logFile}`))
|
|
371
|
+
|
|
372
|
+
// 创建日志文件流
|
|
373
|
+
const logStream = fs.openSync(logFile, 'a')
|
|
374
|
+
const errorLogStream = fs.openSync(errorLogFile, 'a')
|
|
375
|
+
|
|
224
376
|
const env = {
|
|
225
377
|
...process.env,
|
|
226
378
|
PORT: port.toString(),
|
|
227
|
-
USE_GPU:
|
|
379
|
+
USE_GPU: resolvedUseGpu ? 'true' : 'false',
|
|
380
|
+
DMLA_LOG_FILE: logFile // 传递日志文件路径给服务端
|
|
228
381
|
}
|
|
229
382
|
|
|
383
|
+
// 写入启动日志
|
|
384
|
+
const timestamp = new Date().toISOString()
|
|
385
|
+
fs.writeSync(logStream, `[${timestamp}] Server starting...\n`)
|
|
386
|
+
fs.writeSync(logStream, `[${timestamp}] Server path: ${actualServerPath}\n`)
|
|
387
|
+
fs.writeSync(logStream, `[${timestamp}] Port: ${port}\n`)
|
|
388
|
+
fs.writeSync(logStream, `[${timestamp}] GPU: ${resolvedUseGpu} (${imageResolution.message})\n`)
|
|
389
|
+
|
|
390
|
+
// 使用 spawn 启动 server 进程
|
|
391
|
+
// 重要:stdio 必须是 'ignore' 或管道,不能是 'inherit'
|
|
392
|
+
// 因为 'inherit' 会让子进程依赖父进程的 stdout,父进程退出后子进程也会退出
|
|
230
393
|
const serverProcess = spawn('node', [actualServerPath], {
|
|
231
394
|
env,
|
|
232
|
-
stdio: '
|
|
233
|
-
detached: true
|
|
395
|
+
stdio: ['ignore', logStream, errorLogStream], // stdin: ignore, stdout: log file, stderr: error log
|
|
396
|
+
detached: true,
|
|
397
|
+
windowsHide: true // Windows 下隐藏窗口
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
// 监听子进程事件(调试用)
|
|
401
|
+
serverProcess.on('error', (err) => {
|
|
402
|
+
fs.writeSync(errorLogStream, `[${new Date().toISOString()}] Spawn error: ${err.message}\n`)
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
serverProcess.on('exit', (code, signal) => {
|
|
406
|
+
const msg = `[${new Date().toISOString()}] Process exited: code=${code}, signal=${signal}\n`
|
|
407
|
+
fs.writeSync(logStream, msg)
|
|
408
|
+
fs.writeSync(errorLogStream, msg)
|
|
234
409
|
})
|
|
235
410
|
|
|
236
411
|
serverProcess.unref()
|
|
237
412
|
|
|
413
|
+
// 关闭父进程中的文件描述符(子进程会保留自己的副本)
|
|
414
|
+
fs.closeSync(logStream)
|
|
415
|
+
fs.closeSync(errorLogStream)
|
|
416
|
+
|
|
238
417
|
// 等待服务启动
|
|
239
418
|
console.log(chalk.gray(' 等待服务就绪...'))
|
|
240
419
|
let attempts = 0
|
|
@@ -245,15 +424,19 @@ export async function startServer(port, useGpu = false) {
|
|
|
245
424
|
if (running) {
|
|
246
425
|
console.log(chalk.green(`✅ 服务已启动: http://localhost:${port}`))
|
|
247
426
|
console.log(chalk.gray(` 健康检查: http://localhost:${port}/api/health`))
|
|
427
|
+
console.log(chalk.gray(` 日志查看: ${logFile}`))
|
|
248
428
|
return
|
|
249
429
|
}
|
|
250
430
|
await new Promise(resolve => setTimeout(resolve, 500))
|
|
251
431
|
attempts++
|
|
252
432
|
}
|
|
253
433
|
|
|
254
|
-
console.log(chalk.yellow('⚠️
|
|
434
|
+
console.log(chalk.yellow('⚠️ 服务启动超时'))
|
|
435
|
+
console.log(chalk.gray(` 请查看日志: ${logFile}`))
|
|
436
|
+
console.log(chalk.gray(` 或使用 --sync 模式调试`))
|
|
255
437
|
} catch (error) {
|
|
256
438
|
console.log(chalk.red(`❌ 启动失败: ${error.message}`))
|
|
439
|
+
console.log(chalk.gray(error.stack))
|
|
257
440
|
}
|
|
258
441
|
}
|
|
259
442
|
|
|
@@ -261,7 +444,51 @@ export async function startServer(port, useGpu = false) {
|
|
|
261
444
|
* 停止服务
|
|
262
445
|
*/
|
|
263
446
|
export async function stopServer() {
|
|
264
|
-
//
|
|
447
|
+
// 首先尝试通过 API 停止服务
|
|
448
|
+
const port = CONFIG.defaultPort
|
|
449
|
+
const running = await checkServiceRunning(port)
|
|
450
|
+
|
|
451
|
+
if (running) {
|
|
452
|
+
try {
|
|
453
|
+
// 调用 shutdown API
|
|
454
|
+
await new Promise((resolve, reject) => {
|
|
455
|
+
const req = http.request({
|
|
456
|
+
hostname: 'localhost',
|
|
457
|
+
port: port,
|
|
458
|
+
path: '/api/shutdown',
|
|
459
|
+
method: 'POST',
|
|
460
|
+
timeout: 5000
|
|
461
|
+
}, (res) => {
|
|
462
|
+
if (res.statusCode === 200) {
|
|
463
|
+
console.log(chalk.green('✅ 服务已停止'))
|
|
464
|
+
resolve()
|
|
465
|
+
} else {
|
|
466
|
+
reject(new Error(`HTTP ${res.statusCode}`))
|
|
467
|
+
}
|
|
468
|
+
})
|
|
469
|
+
req.on('error', (e) => reject(e))
|
|
470
|
+
req.on('timeout', () => {
|
|
471
|
+
req.destroy()
|
|
472
|
+
reject(new Error('Timeout'))
|
|
473
|
+
})
|
|
474
|
+
req.end()
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
// 等待服务完全关闭
|
|
478
|
+
let attempts = 0
|
|
479
|
+
while (attempts < 10) {
|
|
480
|
+
const stillRunning = await checkServiceRunning(port)
|
|
481
|
+
if (!stillRunning) break
|
|
482
|
+
await new Promise(r => setTimeout(r, 200))
|
|
483
|
+
attempts++
|
|
484
|
+
}
|
|
485
|
+
return
|
|
486
|
+
} catch (error) {
|
|
487
|
+
console.log(chalk.yellow(`⚠️ 通过 API 停止失败: ${error.message}`))
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// 尝试查找并停止 Docker 容器
|
|
265
492
|
const container = await findServiceContainer()
|
|
266
493
|
|
|
267
494
|
if (container) {
|
|
@@ -269,14 +496,15 @@ export async function stopServer() {
|
|
|
269
496
|
const containerObj = docker.getContainer(container.Id)
|
|
270
497
|
await containerObj.stop()
|
|
271
498
|
await containerObj.remove()
|
|
272
|
-
console.log(chalk.green('✅
|
|
499
|
+
console.log(chalk.green('✅ 服务容器已停止'))
|
|
273
500
|
} catch (error) {
|
|
274
|
-
console.log(chalk.red(`❌
|
|
501
|
+
console.log(chalk.red(`❌ 停止容器失败: ${error.message}`))
|
|
275
502
|
}
|
|
503
|
+
} else if (!running) {
|
|
504
|
+
console.log(chalk.gray(' 服务未运行'))
|
|
276
505
|
} else {
|
|
277
|
-
|
|
278
|
-
console.log(chalk.
|
|
279
|
-
console.log(chalk.gray(' 提示: 服务可能以非容器模式运行'))
|
|
506
|
+
console.log(chalk.yellow('⚠️ 无法停止服务'))
|
|
507
|
+
console.log(chalk.gray(' 提示: 手动终止端口 3001 上的进程'))
|
|
280
508
|
}
|
|
281
509
|
}
|
|
282
510
|
|
|
@@ -287,9 +515,10 @@ export async function getStatus() {
|
|
|
287
515
|
console.log()
|
|
288
516
|
|
|
289
517
|
// 检查 npm 包版本
|
|
290
|
-
console.log(chalk.bold('
|
|
518
|
+
console.log(chalk.bold('npm 包版本'))
|
|
291
519
|
try {
|
|
292
|
-
|
|
520
|
+
// __dirname 是 src/commands,需要向上两级到包根目录
|
|
521
|
+
const pkgPath = path.resolve(__dirname, '../../package.json')
|
|
293
522
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
294
523
|
console.log(chalk.gray(` @icyfenix-dmla/cli: ${pkg.version}`))
|
|
295
524
|
} catch {
|
|
@@ -299,7 +528,7 @@ export async function getStatus() {
|
|
|
299
528
|
console.log()
|
|
300
529
|
|
|
301
530
|
// 检查镜像
|
|
302
|
-
console.log(chalk.bold('
|
|
531
|
+
console.log(chalk.bold('Docker 镜像'))
|
|
303
532
|
const cpuExists = await checkImageExists('cpu')
|
|
304
533
|
const gpuExists = await checkImageExists('gpu')
|
|
305
534
|
console.log(chalk.gray(` CPU: ${cpuExists ? chalk.green('已安装') : chalk.red('未安装')}`))
|
|
@@ -308,7 +537,7 @@ export async function getStatus() {
|
|
|
308
537
|
console.log()
|
|
309
538
|
|
|
310
539
|
// 检查 GPU
|
|
311
|
-
console.log(chalk.bold('
|
|
540
|
+
console.log(chalk.bold('GPU 状态'))
|
|
312
541
|
const gpuAvailable = await checkGPUAvailable()
|
|
313
542
|
if (gpuAvailable) {
|
|
314
543
|
console.log(chalk.green(' GPU 可用'))
|
|
@@ -326,7 +555,7 @@ export async function getStatus() {
|
|
|
326
555
|
console.log()
|
|
327
556
|
|
|
328
557
|
// 检查服务
|
|
329
|
-
console.log(chalk.bold('
|
|
558
|
+
console.log(chalk.bold('服务状态'))
|
|
330
559
|
const running = await checkServiceRunning(CONFIG.defaultPort)
|
|
331
560
|
if (running) {
|
|
332
561
|
console.log(chalk.green(` 服务运行中 (端口 ${CONFIG.defaultPort})`))
|