@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icyfenix-dmla/cli",
3
- "version": "2026.5.6-1126",
3
+ "version": "2026.5.13-1007",
4
4
  "description": "DMLA 沙箱服务命令行工具",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -3,8 +3,10 @@
3
3
 
4
4
  from .bayesian import *
5
5
  from .cnn import *
6
+ from .gan import *
6
7
  from .linear import *
7
8
  from .neural import *
9
+ from .sequence_models import *
8
10
  from .svm import *
9
11
  from .tree import *
10
12
  from .unsupervised import *
@@ -0,0 +1,6 @@
1
+ # GAN 模块
2
+ from .dcgan_discriminator import DCGANDiscriminator
3
+ from .dcgan_generator import DCGANGenerator
4
+ from .image_vae import ImageVAE
5
+
6
+ __all__ = ['DCGANDiscriminator', 'DCGANGenerator', 'ImageVAE']
@@ -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,4 @@
1
+ # SEQUENCE_MODELS 模块
2
+ from .poetry_lstm import PoetryLSTM
3
+
4
+ __all__ = ['PoetryLSTM']
@@ -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)
@@ -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 的输出)
@@ -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
- // 解析 Docker 日志格式
897
- const lines = parseDockerLogLines(chunk)
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
- log(`Stream output (${streamType}): ${text.substring(0, 100)}...`)
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
- res.write(text + '\n')
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
- logStream.on('end', resolve)
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
- // 按行分割(一个 Docker 消息可能包含多行)
1027
- const chunkLines = chunk.split('\n').filter(l => l.trim())
1028
- for (const line of chunkLines) {
1029
- // 返回包含 streamType 的对象
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: line
1239
+ text: text
1033
1240
  })
1034
1241
  }
1035
1242
  }
package/version.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "buildTime": "2026-05-06T03:26:55.383Z",
3
- "cliVersion": "2026.5.6-1126"
2
+ "buildTime": "2026-05-13T02:08:13.257Z",
3
+ "cliVersion": "2026.5.13-1007"
4
4
  }