@icyfenix-dmla/cli 2026.5.6-1126 → 2026.5.13-1007
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/shared/__init__.py +2 -0
- package/shared/gan/__init__.py +6 -0
- package/shared/gan/dcgan_discriminator.py +44 -0
- package/shared/gan/dcgan_generator.py +49 -0
- package/shared/gan/image_vae.py +67 -0
- package/shared/sequence_models/__init__.py +4 -0
- package/shared/sequence_models/poetry_lstm.py +63 -0
- package/src/commands/data.js +21 -1
- package/src/server/kernel_runner.py +6 -2
- package/src/server/sandbox.js +218 -11
- package/version.json +2 -2
package/package.json
CHANGED
package/shared/__init__.py
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DCGANDiscriminator(nn.Module):
|
|
6
|
+
"""
|
|
7
|
+
DCGAN 判别器
|
|
8
|
+
|
|
9
|
+
输入: 64×64×3 RGB 图像 (值域 [-1, 1])
|
|
10
|
+
输出: 真假概率 [0, 1]
|
|
11
|
+
|
|
12
|
+
架构: 卷积逐步下采样
|
|
13
|
+
64×64 → 32×32 → 16×16 → 8×8 → 4×4 → 1×1
|
|
14
|
+
"""
|
|
15
|
+
def __init__(self, img_channels=3):
|
|
16
|
+
super(DCGANDiscriminator, self).__init__()
|
|
17
|
+
|
|
18
|
+
self.main = nn.Sequential(
|
|
19
|
+
# 3 × 64 × 64 → 64 × 32 × 32 (无 BatchNorm)
|
|
20
|
+
nn.Conv2d(img_channels, 64, kernel_size=4, stride=2, padding=1, bias=False),
|
|
21
|
+
nn.LeakyReLU(0.2, inplace=True),
|
|
22
|
+
|
|
23
|
+
# 64 × 32 × 32 → 128 × 16 × 16
|
|
24
|
+
nn.Conv2d(64, 128, kernel_size=4, stride=2, padding=1, bias=False),
|
|
25
|
+
nn.BatchNorm2d(128),
|
|
26
|
+
nn.LeakyReLU(0.2, inplace=True),
|
|
27
|
+
|
|
28
|
+
# 128 × 16 × 16 → 256 × 8 × 8
|
|
29
|
+
nn.Conv2d(128, 256, kernel_size=4, stride=2, padding=1, bias=False),
|
|
30
|
+
nn.BatchNorm2d(256),
|
|
31
|
+
nn.LeakyReLU(0.2, inplace=True),
|
|
32
|
+
|
|
33
|
+
# 256 × 8 × 8 → 512 × 4 × 4
|
|
34
|
+
nn.Conv2d(256, 512, kernel_size=4, stride=2, padding=1, bias=False),
|
|
35
|
+
nn.BatchNorm2d(512),
|
|
36
|
+
nn.LeakyReLU(0.2, inplace=True),
|
|
37
|
+
|
|
38
|
+
# 512 × 4 × 4 → 1 × 1 × 1
|
|
39
|
+
nn.Conv2d(512, 1, kernel_size=4, stride=1, padding=0, bias=False),
|
|
40
|
+
nn.Sigmoid()
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def forward(self, img):
|
|
44
|
+
return self.main(img).view(-1)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# DCGANGenerator 类定义
|
|
2
|
+
# 从文档自动提取生成
|
|
3
|
+
|
|
4
|
+
import torch
|
|
5
|
+
import torch.nn as nn
|
|
6
|
+
|
|
7
|
+
class DCGANGenerator(nn.Module):
|
|
8
|
+
"""
|
|
9
|
+
DCGAN 生成器
|
|
10
|
+
|
|
11
|
+
输入: 噪声向量 z (latent_dim 维)
|
|
12
|
+
输出: 64×64×3 RGB 图像 (值域 [-1, 1])
|
|
13
|
+
|
|
14
|
+
架构: 转置卷积逐步上采样
|
|
15
|
+
1×1 → 4×4 → 8×8 → 16×16 → 32×32 → 64×64
|
|
16
|
+
"""
|
|
17
|
+
def __init__(self, latent_dim=100, img_channels=3):
|
|
18
|
+
super(DCGANGenerator, self).__init__()
|
|
19
|
+
self.latent_dim = latent_dim
|
|
20
|
+
|
|
21
|
+
self.main = nn.Sequential(
|
|
22
|
+
# 输入: latent_dim × 1 × 1 → 512 × 4 × 4
|
|
23
|
+
nn.ConvTranspose2d(latent_dim, 512, kernel_size=4, stride=1, padding=0, bias=False),
|
|
24
|
+
nn.BatchNorm2d(512),
|
|
25
|
+
nn.ReLU(True),
|
|
26
|
+
|
|
27
|
+
# 512 × 4 × 4 → 256 × 8 × 8
|
|
28
|
+
nn.ConvTranspose2d(512, 256, kernel_size=4, stride=2, padding=1, bias=False),
|
|
29
|
+
nn.BatchNorm2d(256),
|
|
30
|
+
nn.ReLU(True),
|
|
31
|
+
|
|
32
|
+
# 256 × 8 × 8 → 128 × 16 × 16
|
|
33
|
+
nn.ConvTranspose2d(256, 128, kernel_size=4, stride=2, padding=1, bias=False),
|
|
34
|
+
nn.BatchNorm2d(128),
|
|
35
|
+
nn.ReLU(True),
|
|
36
|
+
|
|
37
|
+
# 128 × 16 × 16 → 64 × 32 × 32
|
|
38
|
+
nn.ConvTranspose2d(128, 64, kernel_size=4, stride=2, padding=1, bias=False),
|
|
39
|
+
nn.BatchNorm2d(64),
|
|
40
|
+
nn.ReLU(True),
|
|
41
|
+
|
|
42
|
+
# 64 × 32 × 32 → 3 × 64 × 64
|
|
43
|
+
nn.ConvTranspose2d(64, img_channels, kernel_size=4, stride=2, padding=1, bias=False),
|
|
44
|
+
nn.Tanh()
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def forward(self, z):
|
|
48
|
+
# 将噪声向量 reshape 为 4D 张量: (batch, latent_dim, 1, 1)
|
|
49
|
+
return self.main(z.view(z.size(0), z.size(1), 1, 1))
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# ImageVAE 类定义
|
|
2
|
+
# 从文档自动提取生成
|
|
3
|
+
|
|
4
|
+
import torch
|
|
5
|
+
import torch.nn as nn
|
|
6
|
+
from PIL import Image
|
|
7
|
+
|
|
8
|
+
class ImageVAE(nn.Module):
|
|
9
|
+
"""
|
|
10
|
+
用于 MNIST 图像生成的 VAE
|
|
11
|
+
|
|
12
|
+
网络结构:
|
|
13
|
+
- 编码器: 784 → 512 → 256 → (μ, σ)
|
|
14
|
+
- 解码器: z → 256 → 512 → 784
|
|
15
|
+
|
|
16
|
+
潜在空间维度: 20
|
|
17
|
+
"""
|
|
18
|
+
def __init__(self, latent_dim=20):
|
|
19
|
+
super().__init__()
|
|
20
|
+
|
|
21
|
+
# 编码器(更深的网络,提取更丰富的特征)
|
|
22
|
+
self.encoder = nn.Sequential(
|
|
23
|
+
nn.Linear(784, 512),
|
|
24
|
+
nn.ReLU(),
|
|
25
|
+
nn.Linear(512, 256),
|
|
26
|
+
nn.ReLU()
|
|
27
|
+
)
|
|
28
|
+
self.fc_mu = nn.Linear(256, latent_dim)
|
|
29
|
+
self.fc_logvar = nn.Linear(256, latent_dim)
|
|
30
|
+
|
|
31
|
+
# 解码器(对称结构)
|
|
32
|
+
self.decoder = nn.Sequential(
|
|
33
|
+
nn.Linear(latent_dim, 256),
|
|
34
|
+
nn.ReLU(),
|
|
35
|
+
nn.Linear(256, 512),
|
|
36
|
+
nn.ReLU(),
|
|
37
|
+
nn.Linear(512, 784),
|
|
38
|
+
nn.Sigmoid() # 输出像素概率
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
self.latent_dim = latent_dim
|
|
42
|
+
|
|
43
|
+
def encode(self, x):
|
|
44
|
+
"""编码过程"""
|
|
45
|
+
h = self.encoder(x)
|
|
46
|
+
return self.fc_mu(h), self.fc_logvar(h)
|
|
47
|
+
|
|
48
|
+
def reparameterize(self, mu, logvar):
|
|
49
|
+
"""重参数化"""
|
|
50
|
+
std = torch.exp(logvar / 2)
|
|
51
|
+
eps = torch.randn_like(std)
|
|
52
|
+
return mu + std * eps
|
|
53
|
+
|
|
54
|
+
def decode(self, z):
|
|
55
|
+
"""解码过程"""
|
|
56
|
+
return self.decoder(z)
|
|
57
|
+
|
|
58
|
+
def forward(self, x):
|
|
59
|
+
"""完整流程"""
|
|
60
|
+
mu, logvar = self.encode(x)
|
|
61
|
+
z = self.reparameterize(mu, logvar)
|
|
62
|
+
return self.decode(z), mu, logvar
|
|
63
|
+
|
|
64
|
+
def generate(self, num_samples):
|
|
65
|
+
"""生成新样本"""
|
|
66
|
+
z = torch.randn(num_samples, self.latent_dim)
|
|
67
|
+
return self.decode(z)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# PoetryLSTM 类定义
|
|
2
|
+
# 从文档自动提取生成
|
|
3
|
+
|
|
4
|
+
import torch
|
|
5
|
+
import torch.nn as nn
|
|
6
|
+
|
|
7
|
+
class PoetryLSTM(nn.Module):
|
|
8
|
+
"""LSTM 语言模型(用于古诗词生成)
|
|
9
|
+
|
|
10
|
+
架构: Embedding -> LSTM -> Linear -> Softmax
|
|
11
|
+
"""
|
|
12
|
+
def __init__(self, vocab_size, embedding_dim=256, hidden_dim=256, num_layers=2, dropout=0.3):
|
|
13
|
+
super(PoetryLSTM, self).__init__()
|
|
14
|
+
|
|
15
|
+
self.vocab_size = vocab_size
|
|
16
|
+
self.hidden_dim = hidden_dim
|
|
17
|
+
self.num_layers = num_layers
|
|
18
|
+
|
|
19
|
+
# 嵌入层:字符索引 -> 稠密向量
|
|
20
|
+
self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=0)
|
|
21
|
+
|
|
22
|
+
# LSTM 层
|
|
23
|
+
self.lstm = nn.LSTM(
|
|
24
|
+
input_size=embedding_dim,
|
|
25
|
+
hidden_size=hidden_dim,
|
|
26
|
+
num_layers=num_layers,
|
|
27
|
+
batch_first=True,
|
|
28
|
+
dropout=dropout if num_layers > 1 else 0
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# 输出层:隐藏状态 -> 词汇表概率分布
|
|
32
|
+
self.fc = nn.Linear(hidden_dim, vocab_size)
|
|
33
|
+
|
|
34
|
+
# Dropout 层
|
|
35
|
+
self.dropout = nn.Dropout(dropout)
|
|
36
|
+
|
|
37
|
+
def forward(self, x, hidden=None):
|
|
38
|
+
"""
|
|
39
|
+
参数:
|
|
40
|
+
x: 输入序列 (batch_size, seq_len)
|
|
41
|
+
hidden: 初始隐藏状态 (可选)
|
|
42
|
+
|
|
43
|
+
返回:
|
|
44
|
+
output: 输出 logits (batch_size, seq_len, vocab_size)
|
|
45
|
+
hidden: 最终隐藏状态
|
|
46
|
+
"""
|
|
47
|
+
# 嵌入: (batch_size, seq_len) -> (batch_size, seq_len, embedding_dim)
|
|
48
|
+
embedded = self.embedding(x)
|
|
49
|
+
embedded = self.dropout(embedded)
|
|
50
|
+
|
|
51
|
+
# LSTM: (batch_size, seq_len, embedding_dim) -> (batch_size, seq_len, hidden_dim)
|
|
52
|
+
lstm_out, hidden = self.lstm(embedded, hidden)
|
|
53
|
+
|
|
54
|
+
# 输出: (batch_size, seq_len, hidden_dim) -> (batch_size, seq_len, vocab_size)
|
|
55
|
+
output = self.fc(lstm_out)
|
|
56
|
+
|
|
57
|
+
return output, hidden
|
|
58
|
+
|
|
59
|
+
def init_hidden(self, batch_size, device):
|
|
60
|
+
"""初始化隐藏状态"""
|
|
61
|
+
h0 = torch.zeros(self.num_layers, batch_size, self.hidden_dim, device=device)
|
|
62
|
+
c0 = torch.zeros(self.num_layers, batch_size, self.hidden_dim, device=device)
|
|
63
|
+
return (h0, c0)
|
package/src/commands/data.js
CHANGED
|
@@ -51,6 +51,25 @@ const DATASETS = [
|
|
|
51
51
|
format: 'git',
|
|
52
52
|
targetDir: 'datasets/mnist',
|
|
53
53
|
source: 'ModelScope (icyfenix)'
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'cartoon-face',
|
|
57
|
+
name: 'Cartoon Face',
|
|
58
|
+
url: 'https://www.modelscope.cn/datasets/icyfenix/Cartoon_Face.git',
|
|
59
|
+
size: '288MB',
|
|
60
|
+
format: 'git',
|
|
61
|
+
targetDir: 'datasets/cartoon-face',
|
|
62
|
+
source: 'ModelScope (icyfenix)',
|
|
63
|
+
zipFile: 'faces.zip'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'chinese-poetry',
|
|
67
|
+
name: 'Chinese Poetry (古诗词)',
|
|
68
|
+
url: 'https://www.modelscope.cn/datasets/icyfenix/Chinese-Poetry.git',
|
|
69
|
+
size: '~50MB',
|
|
70
|
+
format: 'git',
|
|
71
|
+
targetDir: 'datasets/chinese-poetry',
|
|
72
|
+
source: 'ModelScope (icyfenix)'
|
|
54
73
|
}
|
|
55
74
|
]
|
|
56
75
|
|
|
@@ -578,6 +597,8 @@ async function downloadDataset(dataPath, dataset) {
|
|
|
578
597
|
fs.mkdirSync(parentDir, { recursive: true })
|
|
579
598
|
}
|
|
580
599
|
|
|
600
|
+
let hasGitLfs = false // 在 try 块开头定义,确保整个块内可见
|
|
601
|
+
|
|
581
602
|
try {
|
|
582
603
|
if (dataset.format === 'git') {
|
|
583
604
|
// 使用 git clone 下载 ModelScope 数据集
|
|
@@ -585,7 +606,6 @@ async function downloadDataset(dataPath, dataset) {
|
|
|
585
606
|
console.log()
|
|
586
607
|
|
|
587
608
|
// 检查并安装 Git LFS
|
|
588
|
-
let hasGitLfs = false
|
|
589
609
|
try {
|
|
590
610
|
execSync('git lfs install', { stdio: 'pipe' })
|
|
591
611
|
hasGitLfs = true
|
|
@@ -225,11 +225,15 @@ def run_code(code: str, timeout: int = DEFAULT_TIMEOUT, stream: bool = False) ->
|
|
|
225
225
|
restore_stdout()
|
|
226
226
|
log_debug('stdout restored for code execution')
|
|
227
227
|
|
|
228
|
-
# 3.
|
|
229
|
-
log_debug('Injecting global variables')
|
|
228
|
+
# 3. 注入全局变量和数据路径兼容
|
|
229
|
+
log_debug('Injecting global variables and matplotlib config')
|
|
230
230
|
setup_code = '''
|
|
231
231
|
import os
|
|
232
232
|
DATA_DIR = os.environ.get('DMLA_DATA_PATH', '/data')
|
|
233
|
+
|
|
234
|
+
# 配置 matplotlib inline 后端(在用户 import matplotlib 之前设置)
|
|
235
|
+
import matplotlib
|
|
236
|
+
matplotlib.use('module://matplotlib_inline.backend_inline')
|
|
233
237
|
'''
|
|
234
238
|
kc.execute(setup_code, allow_stdin=False)
|
|
235
239
|
# 等待 setup 执行完成(读取并丢弃 setup 的输出)
|
package/src/server/sandbox.js
CHANGED
|
@@ -891,19 +891,63 @@ export async function runPythonCodeStreaming(code, useGpu = false, res, imageOve
|
|
|
891
891
|
})
|
|
892
892
|
|
|
893
893
|
// 处理日志流数据
|
|
894
|
+
let totalChunks = 0
|
|
895
|
+
let totalBytes = 0
|
|
896
|
+
let jsonBuffer = '' // 用于累积跨多个帧的 JSON 消息
|
|
897
|
+
let frameBuffer = Buffer.alloc(0) // 用于累积不完整的 Docker 日志帧
|
|
898
|
+
|
|
894
899
|
logStream.on('data', (chunk) => {
|
|
900
|
+
totalChunks++
|
|
901
|
+
totalBytes += chunk.length
|
|
902
|
+
log(`Chunk ${totalChunks}: ${chunk.length} bytes, total: ${totalBytes} bytes`)
|
|
903
|
+
|
|
895
904
|
if (Buffer.isBuffer(chunk)) {
|
|
896
|
-
//
|
|
897
|
-
const
|
|
905
|
+
// 合并帧缓冲和新数据
|
|
906
|
+
const combinedBuffer = Buffer.concat([frameBuffer, chunk])
|
|
907
|
+
log(`Combined buffer: ${combinedBuffer.length} bytes (frameBuffer: ${frameBuffer.length}, chunk: ${chunk.length})`)
|
|
908
|
+
|
|
909
|
+
// 解析 Docker 日志格式,返回未处理的剩余缓冲
|
|
910
|
+
const { lines, remainingBuffer } = parseDockerLogLinesWithBuffer(combinedBuffer)
|
|
911
|
+
frameBuffer = remainingBuffer
|
|
912
|
+
log(`Parsed ${lines.length} messages, remaining buffer: ${frameBuffer.length} bytes`)
|
|
913
|
+
|
|
898
914
|
for (const { streamType, text } of lines) {
|
|
899
915
|
if (text && text.trim()) {
|
|
900
|
-
|
|
916
|
+
const preview = text.substring(0, 200)
|
|
917
|
+
log(`Message (${streamType}): length=${text.length}, preview: ${preview.endsWith('...') ? preview : preview + '...'}`)
|
|
918
|
+
|
|
919
|
+
// 检查是否有未完成的 JSON 缓冲
|
|
920
|
+
if (jsonBuffer) {
|
|
921
|
+
// 将当前文本追加到缓冲
|
|
922
|
+
jsonBuffer += text
|
|
923
|
+
log(`Appending to JSON buffer, total length: ${jsonBuffer.length}`)
|
|
924
|
+
|
|
925
|
+
// 检查是否完成(找到闭合括号)
|
|
926
|
+
if (isJsonComplete(jsonBuffer)) {
|
|
927
|
+
log(`JSON buffer complete, forwarding: ${jsonBuffer.length} bytes`)
|
|
928
|
+
res.write(jsonBuffer + '\n')
|
|
929
|
+
jsonBuffer = ''
|
|
930
|
+
} else {
|
|
931
|
+
log(`JSON buffer incomplete, waiting for more data`)
|
|
932
|
+
}
|
|
933
|
+
continue
|
|
934
|
+
}
|
|
935
|
+
|
|
901
936
|
// kernel_runner.py 已经输出 JSON 格式消息,直接转发
|
|
902
937
|
// 检查是否已经是 JSON 格式(stream, result, progress 等消息)
|
|
903
938
|
if (text.trim().startsWith('{') && text.includes('"type":')) {
|
|
904
|
-
|
|
939
|
+
// 检查 JSON 是否完整
|
|
940
|
+
if (isJsonComplete(text)) {
|
|
941
|
+
log(`Forwarding complete JSON message: ${text.length} bytes`)
|
|
942
|
+
res.write(text + '\n')
|
|
943
|
+
} else {
|
|
944
|
+
// JSON 不完整,存入缓冲等待后续帧
|
|
945
|
+
log(`JSON message incomplete, buffering: ${text.length} bytes`)
|
|
946
|
+
jsonBuffer = text
|
|
947
|
+
}
|
|
905
948
|
} else {
|
|
906
949
|
// 非 JSON 内容(如容器启动日志),包装为 stream 消息
|
|
950
|
+
log(`Wrapping non-JSON content as stream message`)
|
|
907
951
|
res.write(JSON.stringify({
|
|
908
952
|
type: 'stream',
|
|
909
953
|
name: streamType,
|
|
@@ -914,6 +958,7 @@ export async function runPythonCodeStreaming(code, useGpu = false, res, imageOve
|
|
|
914
958
|
}
|
|
915
959
|
} else {
|
|
916
960
|
// 字符串格式(fallback)
|
|
961
|
+
log(`Received string chunk: ${chunk.length} chars`)
|
|
917
962
|
const textLines = chunk.toString().split('\n').filter(l => l.trim())
|
|
918
963
|
for (const line of textLines) {
|
|
919
964
|
if (line.trim().startsWith('{') && line.includes('"type":')) {
|
|
@@ -929,6 +974,50 @@ export async function runPythonCodeStreaming(code, useGpu = false, res, imageOve
|
|
|
929
974
|
}
|
|
930
975
|
})
|
|
931
976
|
|
|
977
|
+
logStream.on('end', () => {
|
|
978
|
+
// 流结束时,处理剩余的帧缓冲
|
|
979
|
+
if (frameBuffer.length > 0) {
|
|
980
|
+
log(`Stream ended with frame buffer remaining: ${frameBuffer.length} bytes`)
|
|
981
|
+
// 尝试解析剩余的帧缓冲(可能不完整)
|
|
982
|
+
const { lines, remainingBuffer } = parseDockerLogLinesWithBuffer(frameBuffer)
|
|
983
|
+
frameBuffer = remainingBuffer
|
|
984
|
+
|
|
985
|
+
for (const { streamType, text } of lines) {
|
|
986
|
+
if (text && text.trim()) {
|
|
987
|
+
log(`Final frame message (${streamType}): length=${text.length}`)
|
|
988
|
+
// 处理剩余消息(与主循环相同的逻辑)
|
|
989
|
+
if (jsonBuffer) {
|
|
990
|
+
jsonBuffer += text
|
|
991
|
+
if (isJsonComplete(jsonBuffer)) {
|
|
992
|
+
res.write(jsonBuffer + '\n')
|
|
993
|
+
jsonBuffer = ''
|
|
994
|
+
}
|
|
995
|
+
} else if (text.trim().startsWith('{') && text.includes('"type":')) {
|
|
996
|
+
if (isJsonComplete(text)) {
|
|
997
|
+
res.write(text + '\n')
|
|
998
|
+
} else {
|
|
999
|
+
jsonBuffer = text
|
|
1000
|
+
}
|
|
1001
|
+
} else {
|
|
1002
|
+
res.write(JSON.stringify({
|
|
1003
|
+
type: 'stream',
|
|
1004
|
+
name: streamType,
|
|
1005
|
+
text: text
|
|
1006
|
+
}) + '\n')
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// 处理剩余的 JSON 缓冲
|
|
1013
|
+
if (jsonBuffer) {
|
|
1014
|
+
log(`Stream ended with JSON buffer remaining: ${jsonBuffer.length} bytes`)
|
|
1015
|
+
// 尝试转发剩余缓冲(可能不完整但应该发送)
|
|
1016
|
+
res.write(jsonBuffer + '\n')
|
|
1017
|
+
jsonBuffer = ''
|
|
1018
|
+
}
|
|
1019
|
+
})
|
|
1020
|
+
|
|
932
1021
|
logStream.on('error', (err) => {
|
|
933
1022
|
log(`Log stream error: ${err.message}`)
|
|
934
1023
|
const errorMsg = {
|
|
@@ -945,11 +1034,29 @@ export async function runPythonCodeStreaming(code, useGpu = false, res, imageOve
|
|
|
945
1034
|
await container.wait()
|
|
946
1035
|
log('Container finished')
|
|
947
1036
|
|
|
948
|
-
//
|
|
1037
|
+
// 等待日志流结束(带超时保护)
|
|
949
1038
|
await new Promise((resolve) => {
|
|
950
|
-
|
|
1039
|
+
const timeout = setTimeout(() => {
|
|
1040
|
+
log('Log stream timeout, forcing resolve')
|
|
1041
|
+
resolve()
|
|
1042
|
+
}, 5000) // 最多等待 5 秒
|
|
1043
|
+
|
|
1044
|
+
logStream.on('end', () => {
|
|
1045
|
+
clearTimeout(timeout)
|
|
1046
|
+
log('Log stream end event triggered')
|
|
1047
|
+
resolve()
|
|
1048
|
+
})
|
|
1049
|
+
|
|
1050
|
+
logStream.on('close', () => {
|
|
1051
|
+
clearTimeout(timeout)
|
|
1052
|
+
log('Log stream close event triggered')
|
|
1053
|
+
resolve()
|
|
1054
|
+
})
|
|
1055
|
+
|
|
951
1056
|
// 确保流已结束(可能已经结束)
|
|
952
1057
|
if (logStream.destroyed || logStream.readableEnded) {
|
|
1058
|
+
clearTimeout(timeout)
|
|
1059
|
+
log('Log stream already ended/destroyed')
|
|
953
1060
|
resolve()
|
|
954
1061
|
}
|
|
955
1062
|
})
|
|
@@ -996,6 +1103,106 @@ export async function runPythonCodeStreaming(code, useGpu = false, res, imageOve
|
|
|
996
1103
|
}
|
|
997
1104
|
}
|
|
998
1105
|
|
|
1106
|
+
/**
|
|
1107
|
+
* 检查 JSON 字符串是否完整(括号是否匹配)
|
|
1108
|
+
* @param {string} jsonStr - JSON 字符串
|
|
1109
|
+
* @returns {boolean} - 是否完整
|
|
1110
|
+
*/
|
|
1111
|
+
function isJsonComplete(jsonStr) {
|
|
1112
|
+
if (!jsonStr || !jsonStr.trim().startsWith('{')) return false
|
|
1113
|
+
|
|
1114
|
+
let depth = 0
|
|
1115
|
+
let inString = false
|
|
1116
|
+
let escapeNext = false
|
|
1117
|
+
|
|
1118
|
+
for (let i = 0; i < jsonStr.length; i++) {
|
|
1119
|
+
const char = jsonStr[i]
|
|
1120
|
+
|
|
1121
|
+
if (escapeNext) {
|
|
1122
|
+
escapeNext = false
|
|
1123
|
+
continue
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
if (char === '\\' && inString) {
|
|
1127
|
+
escapeNext = true
|
|
1128
|
+
continue
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
if (char === '"' && !escapeNext) {
|
|
1132
|
+
inString = !inString
|
|
1133
|
+
continue
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (!inString) {
|
|
1137
|
+
if (char === '{') depth++
|
|
1138
|
+
else if (char === '}') {
|
|
1139
|
+
depth--
|
|
1140
|
+
if (depth === 0) {
|
|
1141
|
+
// 找到闭合括号,JSON 完整
|
|
1142
|
+
return true
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// 未找到闭合括号
|
|
1149
|
+
return false
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* 解析 Docker 日志流中的多行数据(带帧缓冲)
|
|
1154
|
+
* Docker 日志格式: [8字节头][数据]
|
|
1155
|
+
* 返回解析的消息和剩余的不完整帧缓冲
|
|
1156
|
+
* @param {Buffer} buffer - Docker 日志 buffer(可能包含之前的帧缓冲)
|
|
1157
|
+
* @returns {{ lines: Array, remainingBuffer: Buffer }} - 解析后的消息和剩余缓冲
|
|
1158
|
+
*/
|
|
1159
|
+
function parseDockerLogLinesWithBuffer(buffer) {
|
|
1160
|
+
if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
|
|
1161
|
+
return { lines: [], remainingBuffer: Buffer.alloc(0) }
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const lines = []
|
|
1165
|
+
let offset = 0
|
|
1166
|
+
|
|
1167
|
+
while (offset < buffer.length) {
|
|
1168
|
+
// 检查是否有完整的头部(8字节)
|
|
1169
|
+
if (offset + 8 > buffer.length) {
|
|
1170
|
+
// 头部不完整,返回剩余部分作为缓冲
|
|
1171
|
+
const remainingBuffer = buffer.slice(offset)
|
|
1172
|
+
return { lines, remainingBuffer }
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const streamType = buffer[offset] // 1=stdout, 2=stderr
|
|
1176
|
+
const length = buffer.readUInt32BE(offset + 4)
|
|
1177
|
+
|
|
1178
|
+
offset += 8
|
|
1179
|
+
|
|
1180
|
+
// 检查是否有完整的数据
|
|
1181
|
+
if (offset + length > buffer.length) {
|
|
1182
|
+
// 数据不完整,返回从头部开始的部分作为缓冲
|
|
1183
|
+
// 注意:需要包含头部,所以 offset 要减去 8
|
|
1184
|
+
const remainingBuffer = buffer.slice(offset - 8)
|
|
1185
|
+
return { lines, remainingBuffer }
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const chunk = buffer.slice(offset, offset + length).toString('utf8')
|
|
1189
|
+
offset += length
|
|
1190
|
+
|
|
1191
|
+
// 不按行分割,保留完整的 chunk(大 JSON 消息可能包含换行符)
|
|
1192
|
+
// 仅处理末尾的换行符(kernel_runner.py 输出时添加的)
|
|
1193
|
+
const text = chunk.endsWith('\n') ? chunk.slice(0, -1) : chunk
|
|
1194
|
+
if (text.trim()) {
|
|
1195
|
+
lines.push({
|
|
1196
|
+
streamType: streamType === 1 ? 'stdout' : 'stderr',
|
|
1197
|
+
text: text
|
|
1198
|
+
})
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// 所有数据已解析完成,返回空缓冲
|
|
1203
|
+
return { lines, remainingBuffer: Buffer.alloc(0) }
|
|
1204
|
+
}
|
|
1205
|
+
|
|
999
1206
|
/**
|
|
1000
1207
|
* 解析 Docker 日志流中的多行数据
|
|
1001
1208
|
* Docker 日志格式: [8字节头][数据]
|
|
@@ -1023,13 +1230,13 @@ function parseDockerLogLines(buffer) {
|
|
|
1023
1230
|
const chunk = buffer.slice(offset, offset + length).toString('utf8')
|
|
1024
1231
|
offset += length
|
|
1025
1232
|
|
|
1026
|
-
//
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1233
|
+
// 不按行分割,保留完整的 chunk(大 JSON 消息可能包含换行符)
|
|
1234
|
+
// 仅处理末尾的换行符(kernel_runner.py 输出时添加的)
|
|
1235
|
+
const text = chunk.endsWith('\n') ? chunk.slice(0, -1) : chunk
|
|
1236
|
+
if (text.trim()) {
|
|
1030
1237
|
lines.push({
|
|
1031
1238
|
streamType: streamType === 1 ? 'stdout' : 'stderr',
|
|
1032
|
-
text:
|
|
1239
|
+
text: text
|
|
1033
1240
|
})
|
|
1034
1241
|
}
|
|
1035
1242
|
}
|
package/version.json
CHANGED