@sciagent/cli 1.1.49 → 1.1.51

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/bin/sciagent.js CHANGED
@@ -25,10 +25,22 @@ const https = require('https');
25
25
  const http = require('http');
26
26
 
27
27
  // 当前版本号 - 与 postinstall.js 和 package.json 保持同步
28
- const CURRENT_VERSION = '1.1.48';
29
-
30
- // Releases 服务器下载配置
31
- const RELEASE_SERVER_URL = process.env.RELEASE_SERVER_URL || 'https://sciagent.tech';
28
+ const CURRENT_VERSION = '1.1.51';
29
+
30
+ // Releases 下载源配置(优先级从高到低)
31
+ // JihuLab 通用包仓库(国内 CDN,速度快)作为主源
32
+ // sciagent.tech 自建服务器作为备用源
33
+ const JIHULAB_PROJECT_ID = '351778';
34
+ const JIHULAB_DEPLOY_TOKEN_USER = 'gitlab+deploy-token-15400';
35
+ const JIHULAB_DEPLOY_TOKEN = 'gldt-5kp7mgt4ztyyBMx1Uvn6';
36
+ const JIHULAB_BASE_URL = `https://jihulab.com/api/v4/projects/${JIHULAB_PROJECT_ID}/packages/generic/sciagent`;
37
+ const SELF_HOSTED_URL = process.env.RELEASE_SERVER_URL || 'https://sciagent.tech';
38
+
39
+ // 下载源列表(按优先级排序)
40
+ const DOWNLOAD_MIRRORS = [
41
+ { name: 'JihuLab CDN', getUrl: (platform, arch, version, filename) => `${JIHULAB_BASE_URL}/${version}/${filename}`, auth: 'jihulab' },
42
+ { name: 'Self-hosted', getUrl: (platform, arch, version, filename) => `${SELF_HOSTED_URL}/api/releases/download/${platform}/${arch}/${version}`, auth: 'none' },
43
+ ];
32
44
 
33
45
  // 平台和架构映射
34
46
  const PLATFORM_MAP = {
@@ -156,8 +168,8 @@ function applyPendingUpdate(installDir) {
156
168
  */
157
169
  function fetchLatestVersion() {
158
170
  return new Promise((resolve) => {
159
- // 使用 /api/releases/list 获取所有版本,取最新的
160
- const url = `${RELEASE_SERVER_URL}/api/releases/list`;
171
+ // 使用自建服务器 /api/releases/list 获取所有版本,取最新的
172
+ const url = `${SELF_HOSTED_URL}/api/releases/list`;
161
173
 
162
174
  const protocol = url.startsWith('https') ? https : http;
163
175
  const request = protocol.get(url, {
@@ -196,16 +208,13 @@ function fetchLatestVersion() {
196
208
  * 后台下载新版本到临时目录
197
209
  * 下载完成后写入 .pending-update 标记
198
210
  * 这是异步操作,不阻塞主进程启动
211
+ * 支持多源下载:优先 JihuLab CDN,失败回退到自建服务器
199
212
  */
200
213
  function backgroundDownloadUpdate(platform, arch, latestVersion) {
201
214
  const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
202
215
  const ext = platform === 'win32' ? '.exe' : '';
203
216
  const filename = `sciagent-${platform}-${arch}${ext}`;
204
217
 
205
- const downloadUrl = (
206
- `${RELEASE_SERVER_URL}/api/releases/download/${platform}/${arch}/${latestVersion}`
207
- );
208
-
209
218
  const installDir = getInstallDir(platform);
210
219
  const tempDir = path.join(os.tmpdir(), 'sciagent-update');
211
220
  fs.mkdirSync(tempDir, { recursive: true });
@@ -216,73 +225,123 @@ function backgroundDownloadUpdate(platform, arch, latestVersion) {
216
225
 
217
226
  console.log(`[AUTO-UPDATE] Downloading v${latestVersion} in background...`);
218
227
 
228
+ // 构建下载 URL 列表(按优先级)
229
+ const downloadUrls = DOWNLOAD_MIRRORS.map(mirror => ({
230
+ url: mirror.getUrl(platform, arch, latestVersion, filename),
231
+ name: mirror.name,
232
+ auth: mirror.auth
233
+ }));
234
+
219
235
  if (platform === 'win32') {
220
- // Windows: 使用 PowerShell 后台下载
221
- const tmpScript = path.join(os.tmpdir(), 'sciagent-bg-download.ps1');
222
- const scriptContent = [
223
- '$ProgressPreference = "SilentlyContinue"',
224
- `$uri = "${downloadUrl}"`,
225
- `$out = "${tempPath}"`,
226
- 'Invoke-WebRequest -Uri $uri -OutFile $out -UseBasicParsing',
227
- `if (Test-Path $out) { $s = (Get-Item $out).Length; Write-Host "Downloaded: $s bytes" } else { exit 1 }`
228
- ].join('\r\n');
229
- fs.writeFileSync(tmpScript, scriptContent, 'utf8');
230
-
231
- const bgProcess = spawn('powershell', [
232
- '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', tmpScript
233
- ], { stdio: 'pipe', detached: true, windowsHide: true });
234
-
235
- bgProcess.on('exit', (code) => {
236
- try { fs.unlinkSync(tmpScript); } catch (e) {}
237
- if (code === 0 && fs.existsSync(tempPath)) {
238
- const stats = fs.statSync(tempPath);
239
- if (stats.size > 10 * 1024 * 1024) {
240
- console.log(`[AUTO-UPDATE] ✅ Downloaded v${latestVersion} (${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
241
- // 写入 pending update 标记
242
- const pendingData = {
243
- version: latestVersion,
244
- tempPath: tempPath,
245
- timestamp: new Date().toISOString(),
246
- platform: process.platform
247
- };
248
- fs.writeFileSync(path.join(installDir, '.pending-update'), JSON.stringify(pendingData, null, 2), 'utf8');
249
- console.log(`[AUTO-UPDATE] 📋 Update will be applied on next restart.`);
250
- }
236
+ // Windows: 使用 PowerShell 后台下载,尝试多个源
237
+ const tryDownload = (urlIndex) => {
238
+ if (urlIndex >= downloadUrls.length) {
239
+ console.log(`[AUTO-UPDATE] ⚠️ Background download failed from all mirrors.`);
240
+ return;
241
+ }
242
+
243
+ const source = downloadUrls[urlIndex];
244
+ const tmpScript = path.join(os.tmpdir(), 'sciagent-bg-download.ps1');
245
+ let scriptLines = [
246
+ '$ProgressPreference = "SilentlyContinue"',
247
+ `$uri = "${source.url}"`,
248
+ `$out = "${tempPath}"`,
249
+ ];
250
+
251
+ if (source.auth === 'jihulab') {
252
+ // JihuLab 需要 Basic Auth
253
+ const b64 = Buffer.from(`${JIHULAB_DEPLOY_TOKEN_USER}:${JIHULAB_DEPLOY_TOKEN}`).toString('base64');
254
+ scriptLines.push(`$headers = @{ "Authorization" = "Basic ${b64}" }`);
255
+ scriptLines.push('Invoke-WebRequest -Uri $uri -OutFile $out -UseBasicParsing -Headers $headers');
251
256
  } else {
252
- console.log(`[AUTO-UPDATE] ⚠️ Background download failed.`);
253
- try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e) {}
257
+ scriptLines.push('Invoke-WebRequest -Uri $uri -OutFile $out -UseBasicParsing');
254
258
  }
255
- });
259
+ scriptLines.push(`if (Test-Path $out) { $s = (Get-Item $out).Length; Write-Host "Downloaded: $s bytes" } else { exit 1 }`);
260
+
261
+ fs.writeFileSync(tmpScript, scriptLines.join('\r\n'), 'utf8');
262
+
263
+ const bgProcess = spawn('powershell', [
264
+ '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', tmpScript
265
+ ], { stdio: 'pipe', detached: true, windowsHide: true });
266
+
267
+ bgProcess.on('exit', (code) => {
268
+ try { fs.unlinkSync(tmpScript); } catch (e) {}
269
+ if (code === 0 && fs.existsSync(tempPath)) {
270
+ const stats = fs.statSync(tempPath);
271
+ if (stats.size > 10 * 1024 * 1024) {
272
+ console.log(`[AUTO-UPDATE] ✅ Downloaded v${latestVersion} from ${source.name} (${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
273
+ const pendingData = {
274
+ version: latestVersion,
275
+ tempPath: tempPath,
276
+ timestamp: new Date().toISOString(),
277
+ platform: process.platform
278
+ };
279
+ fs.writeFileSync(path.join(installDir, '.pending-update'), JSON.stringify(pendingData, null, 2), 'utf8');
280
+ console.log(`[AUTO-UPDATE] 📋 Update will be applied on next restart.`);
281
+ } else {
282
+ console.log(`[AUTO-UPDATE] ⚠️ Download from ${source.name} too small, trying next...`);
283
+ try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e) {}
284
+ tryDownload(urlIndex + 1);
285
+ }
286
+ } else {
287
+ console.log(`[AUTO-UPDATE] ⚠️ Download from ${source.name} failed, trying next...`);
288
+ try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e) {}
289
+ tryDownload(urlIndex + 1);
290
+ }
291
+ });
292
+
293
+ bgProcess.unref();
294
+ };
256
295
 
257
- bgProcess.unref();
296
+ tryDownload(0);
258
297
  } else {
259
- // Linux/macOS: 使用 curl 后台下载
260
- const bgProcess = spawn('curl', [
261
- '-fsSL', '-o', tempPath, downloadUrl
262
- ], { stdio: 'pipe', detached: true });
263
-
264
- bgProcess.on('exit', (code) => {
265
- if (code === 0 && fs.existsSync(tempPath)) {
266
- const stats = fs.statSync(tempPath);
267
- if (stats.size > 10 * 1024 * 1024) {
268
- fs.chmodSync(tempPath, 0o755);
269
- console.log(`[AUTO-UPDATE] ✅ Downloaded v${latestVersion} (${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
270
- const pendingData = {
271
- version: latestVersion,
272
- tempPath: tempPath,
273
- timestamp: new Date().toISOString(),
274
- platform: process.platform
275
- };
276
- fs.writeFileSync(path.join(installDir, '.pending-update'), JSON.stringify(pendingData, null, 2), 'utf8');
277
- console.log(`[AUTO-UPDATE] 📋 Update will be applied on next restart.`);
278
- }
279
- } else {
280
- console.log(`[AUTO-UPDATE] ⚠️ Background download failed.`);
281
- try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e) {}
298
+ // Linux/macOS: 使用 curl 后台下载,尝试多个源
299
+ const tryDownload = (urlIndex) => {
300
+ if (urlIndex >= downloadUrls.length) {
301
+ console.log(`[AUTO-UPDATE] ⚠️ Background download failed from all mirrors.`);
302
+ return;
282
303
  }
283
- });
304
+
305
+ const source = downloadUrls[urlIndex];
306
+ let curlArgs = ['-fsSL', '-o', tempPath];
307
+
308
+ if (source.auth === 'jihulab') {
309
+ curlArgs.push('-u', `${JIHULAB_DEPLOY_TOKEN_USER}:${JIHULAB_DEPLOY_TOKEN}`);
310
+ }
311
+ curlArgs.push(source.url);
312
+
313
+ const bgProcess = spawn('curl', curlArgs, { stdio: 'pipe', detached: true });
314
+
315
+ bgProcess.on('exit', (code) => {
316
+ if (code === 0 && fs.existsSync(tempPath)) {
317
+ const stats = fs.statSync(tempPath);
318
+ if (stats.size > 10 * 1024 * 1024) {
319
+ fs.chmodSync(tempPath, 0o755);
320
+ console.log(`[AUTO-UPDATE] ✅ Downloaded v${latestVersion} from ${source.name} (${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
321
+ const pendingData = {
322
+ version: latestVersion,
323
+ tempPath: tempPath,
324
+ timestamp: new Date().toISOString(),
325
+ platform: process.platform
326
+ };
327
+ fs.writeFileSync(path.join(installDir, '.pending-update'), JSON.stringify(pendingData, null, 2), 'utf8');
328
+ console.log(`[AUTO-UPDATE] 📋 Update will be applied on next restart.`);
329
+ } else {
330
+ console.log(`[AUTO-UPDATE] ⚠️ Download from ${source.name} too small, trying next...`);
331
+ try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e) {}
332
+ tryDownload(urlIndex + 1);
333
+ }
334
+ } else {
335
+ console.log(`[AUTO-UPDATE] ⚠️ Download from ${source.name} failed, trying next...`);
336
+ try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e) {}
337
+ tryDownload(urlIndex + 1);
338
+ }
339
+ });
340
+
341
+ bgProcess.unref();
342
+ };
284
343
 
285
- bgProcess.unref();
344
+ tryDownload(0);
286
345
  }
287
346
  }
288
347
 
@@ -396,19 +455,23 @@ function getBinaryPath() {
396
455
  checkAutoUpdate(platform, arch);
397
456
  return binPath;
398
457
  } else if (cmp < 0) {
399
- // 版本过低 → 自动下载新版本
458
+ // 版本过低 → 同步下载新版本并替换,然后再启动
400
459
  console.log(`[INFO] SciAgent version outdated: v${installedVersion} (${(stats.size / (1024 * 1024)).toFixed(1)} MB) → v${CURRENT_VERSION}`);
401
460
  console.log(`[INFO] Downloading new version from server...`);
402
- // 注意:此时当前进程正在运行旧二进制,无法直接替换
403
- // 下载到临时目录,写入 .pending-update,下次启动时应用
404
- const downloaded = downloadBinaryToTemp(platform, arch, CURRENT_VERSION);
405
- if (downloaded) {
406
- console.log(`[INFO] ✅ New version downloaded. Please restart sciagent to apply update.`);
407
- // 继续使用当前版本
408
- return binPath;
461
+ const downloaded = downloadBinary(platform, arch, CURRENT_VERSION);
462
+ if (downloaded && fs.existsSync(downloaded)) {
463
+ console.log(`[INFO] Updated to v${CURRENT_VERSION}. Starting...`);
464
+ checkAutoUpdate(platform, arch);
465
+ return downloaded;
466
+ }
467
+ // 下载失败,尝试临时目录下载
468
+ const tempDownloaded = downloadBinaryToTemp(platform, arch, CURRENT_VERSION);
469
+ if (tempDownloaded) {
470
+ console.log(`[INFO] ✅ New version downloaded to temp. Will apply on next restart.`);
471
+ console.log(`[INFO] ⚠️ Continuing with current version v${installedVersion} this time.`);
472
+ } else {
473
+ console.log(`[INFO] ⚠️ Download failed. Continuing with current version v${installedVersion}.`);
409
474
  }
410
- // 下载失败,继续使用当前版本
411
- console.log(`[INFO] ⚠️ Download failed. Continuing with current version.`);
412
475
  return binPath;
413
476
  } else {
414
477
  // 版本更高 → 允许运行(用户可能手动安装了新版)
@@ -417,11 +480,21 @@ function getBinaryPath() {
417
480
  return binPath;
418
481
  }
419
482
  } else {
420
- // 没有.version文件 → 可能是旧安装,下载到临时目录
483
+ // 没有.version文件 → 版本未知,同步下载正确版本
421
484
  console.log(`[INFO] SciAgent binary found but version unknown, downloading v${CURRENT_VERSION}...`);
422
- const downloaded = downloadBinaryToTemp(platform, arch, CURRENT_VERSION);
423
- if (downloaded) {
424
- console.log(`[INFO] ✅ New version downloaded. Please restart sciagent to apply update.`);
485
+ const downloaded = downloadBinary(platform, arch, CURRENT_VERSION);
486
+ if (downloaded && fs.existsSync(downloaded)) {
487
+ console.log(`[INFO] ✅ Installed v${CURRENT_VERSION}. Starting...`);
488
+ checkAutoUpdate(platform, arch);
489
+ return downloaded;
490
+ }
491
+ // 下载失败,尝试临时目录
492
+ const tempDownloaded = downloadBinaryToTemp(platform, arch, CURRENT_VERSION);
493
+ if (tempDownloaded) {
494
+ console.log(`[INFO] ✅ New version downloaded to temp. Will apply on next restart.`);
495
+ console.log(`[INFO] ⚠️ Continuing with unknown version this time.`);
496
+ } else {
497
+ console.log(`[INFO] ⚠️ Download failed. Continuing with unknown version.`);
425
498
  }
426
499
  return binPath;
427
500
  }
@@ -459,6 +532,7 @@ function getBinaryPath() {
459
532
  /**
460
533
  * 从 Release Server 下载二进制文件到临时目录(不替换当前运行的二进制)
461
534
  * 下载完成后写入 .pending-update 标记,下次启动时自动应用
535
+ * 支持多源下载:优先 JihuLab CDN,失败回退到自建服务器
462
536
  * 返回: tempPath 或 null
463
537
  */
464
538
  function downloadBinaryToTemp(platform, arch, version) {
@@ -466,10 +540,6 @@ function downloadBinaryToTemp(platform, arch, version) {
466
540
  const ext = platform === 'win32' ? '.exe' : '';
467
541
  const filename = `sciagent-${platform}-${arch}${ext}`;
468
542
 
469
- const downloadUrl = (
470
- `${RELEASE_SERVER_URL}/api/releases/download/${platform}/${arch}/${version}`
471
- );
472
-
473
543
  const installDir = getInstallDir(platform);
474
544
  const tempDir = path.join(os.tmpdir(), 'sciagent-update');
475
545
  fs.mkdirSync(tempDir, { recursive: true });
@@ -481,132 +551,289 @@ function downloadBinaryToTemp(platform, arch, version) {
481
551
  console.log(` Downloading ${filename} v${version} to temp...`);
482
552
  console.log(` Temp: ${tempPath}`);
483
553
 
484
- try {
485
- if (platform === 'win32') {
486
- const tmpScript = path.join(os.tmpdir(), 'sciagent-download.ps1');
487
- const scriptContent = [
488
- '$ProgressPreference = "SilentlyContinue"',
489
- `$uri = "${downloadUrl}"`,
490
- `$out = "${tempPath}"`,
491
- 'Write-Host " Downloading from server..."',
492
- 'Invoke-WebRequest -Uri $uri -OutFile $out -UseBasicParsing',
493
- 'if (Test-Path $out) { $s = (Get-Item $out).Length; Write-Host " Downloaded: $s bytes" } else { Write-Error "File not created"; exit 1 }'
494
- ].join('\r\n');
495
- fs.writeFileSync(tmpScript, scriptContent, 'utf8');
496
-
497
- try {
498
- execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${tmpScript}"`, {
499
- stdio: 'inherit',
500
- timeout: 600000
501
- });
502
- } finally {
503
- try { fs.unlinkSync(tmpScript); } catch (e) {}
504
- }
505
- } else {
506
- execSync(`curl -fsSL -o '${tempPath}' '${downloadUrl}'`, {
507
- stdio: 'pipe',
508
- timeout: 600000
509
- });
510
- fs.chmodSync(tempPath, 0o755);
511
- }
554
+ // 构建下载 URL 列表(按优先级)
555
+ const downloadUrls = DOWNLOAD_MIRRORS.map(mirror => ({
556
+ url: mirror.getUrl(platform, arch, version, filename),
557
+ name: mirror.name,
558
+ auth: mirror.auth
559
+ }));
560
+
561
+ for (const source of downloadUrls) {
562
+ console.log(` Trying ${source.name}...`);
512
563
 
513
- // 验证下载
514
- if (fs.existsSync(tempPath)) {
515
- const stats = fs.statSync(tempPath);
516
- if (stats.size > 10 * 1024 * 1024) {
517
- console.log(` ✅ Downloaded: ${(stats.size / (1024 * 1024)).toFixed(1)} MB`);
518
- // 写入 .pending-update 标记
519
- const pendingData = {
520
- version: version,
521
- tempPath: tempPath,
522
- timestamp: new Date().toISOString(),
523
- platform: process.platform
524
- };
525
- fs.writeFileSync(path.join(installDir, '.pending-update'), JSON.stringify(pendingData, null, 2), 'utf8');
526
- return tempPath;
564
+ try {
565
+ if (platform === 'win32') {
566
+ const tmpScript = path.join(os.tmpdir(), 'sciagent-download.ps1');
567
+ let scriptLines = [
568
+ '$ProgressPreference = "SilentlyContinue"',
569
+ `$uri = "${source.url}"`,
570
+ `$out = "${tempPath}"`,
571
+ 'Write-Host " Downloading..."',
572
+ ];
573
+
574
+ if (source.auth === 'jihulab') {
575
+ const b64 = Buffer.from(`${JIHULAB_DEPLOY_TOKEN_USER}:${JIHULAB_DEPLOY_TOKEN}`).toString('base64');
576
+ scriptLines.push(`$headers = @{ "Authorization" = "Basic ${b64}" }`);
577
+ scriptLines.push('Invoke-WebRequest -Uri $uri -OutFile $out -UseBasicParsing -Headers $headers');
578
+ } else {
579
+ scriptLines.push('Invoke-WebRequest -Uri $uri -OutFile $out -UseBasicParsing');
580
+ }
581
+ scriptLines.push('if (Test-Path $out) { $s = (Get-Item $out).Length; Write-Host " Downloaded: $s bytes" } else { Write-Error "File not created"; exit 1 }');
582
+
583
+ fs.writeFileSync(tmpScript, scriptLines.join('\r\n'), 'utf8');
584
+
585
+ try {
586
+ execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${tmpScript}"`, {
587
+ stdio: 'inherit',
588
+ timeout: 600000
589
+ });
590
+ } finally {
591
+ try { fs.unlinkSync(tmpScript); } catch (e) {}
592
+ }
527
593
  } else {
528
- console.error(` ❌ Downloaded file too small: ${(stats.size / 1024).toFixed(0)} KB`);
529
- try { fs.unlinkSync(tempPath); } catch (e) {}
594
+ let curlCmd = `curl -fsSL -o '${tempPath}'`;
595
+ if (source.auth === 'jihulab') {
596
+ curlCmd += ` -u '${JIHULAB_DEPLOY_TOKEN_USER}:${JIHULAB_DEPLOY_TOKEN}'`;
597
+ }
598
+ curlCmd += ` '${source.url}'`;
599
+ execSync(curlCmd, { stdio: 'pipe', timeout: 600000 });
600
+ fs.chmodSync(tempPath, 0o755);
530
601
  }
602
+
603
+ // 验证下载
604
+ if (fs.existsSync(tempPath)) {
605
+ const stats = fs.statSync(tempPath);
606
+ if (stats.size > 10 * 1024 * 1024) {
607
+ console.log(` ✅ Downloaded from ${source.name}: ${(stats.size / (1024 * 1024)).toFixed(1)} MB`);
608
+ // 写入 .pending-update 标记
609
+ const pendingData = {
610
+ version: version,
611
+ tempPath: tempPath,
612
+ timestamp: new Date().toISOString(),
613
+ platform: process.platform
614
+ };
615
+ fs.writeFileSync(path.join(installDir, '.pending-update'), JSON.stringify(pendingData, null, 2), 'utf8');
616
+ return tempPath;
617
+ } else {
618
+ console.error(` ❌ Downloaded from ${source.name} too small: ${(stats.size / 1024).toFixed(0)} KB`);
619
+ try { fs.unlinkSync(tempPath); } catch (e) {}
620
+ }
621
+ }
622
+ } catch (e) {
623
+ console.error(` ❌ Download from ${source.name} failed: ${e.message}`);
624
+ try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e2) {}
531
625
  }
532
- } catch (e) {
533
- console.error(` ❌ Download failed: ${e.message}`);
534
- try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e2) {}
535
626
  }
536
627
 
628
+ console.error(` ❌ All download mirrors failed.`);
629
+
537
630
  return null;
538
631
  }
539
632
 
540
633
  /**
541
- * 从 Release Server 下载二进制文件(直接到目标路径,用于首次安装)
634
+ * 从 Release Server 下载二进制文件(直接到目标路径,用于首次安装或版本更新)
635
+ * 如果目标文件已存在,先下载到临时目录再替换
636
+ * 支持多源下载:优先 JihuLab CDN,失败回退到自建服务器
542
637
  */
543
638
  function downloadBinary(platform, arch, version) {
544
639
  const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
545
640
  const ext = platform === 'win32' ? '.exe' : '';
546
641
  const filename = `sciagent-${platform}-${arch}${ext}`;
547
642
 
548
- const downloadUrl = (
549
- `${RELEASE_SERVER_URL}/api/releases/download/${platform}/${arch}/${version}`
550
- );
551
-
552
643
  const installDir = getInstallDir(platform);
553
644
  fs.mkdirSync(installDir, { recursive: true });
554
645
  const targetPath = path.join(installDir, binName);
646
+ const targetExists = fs.existsSync(targetPath);
555
647
 
556
- console.log(` Downloading ${filename} v${version}...`);
557
- console.log(` Target: ${targetPath}`);
648
+ // 构建下载 URL 列表(按优先级)
649
+ const downloadUrls = DOWNLOAD_MIRRORS.map(mirror => ({
650
+ url: mirror.getUrl(platform, arch, version, filename),
651
+ name: mirror.name,
652
+ auth: mirror.auth
653
+ }));
558
654
 
559
- try {
560
- if (platform === 'win32') {
561
- // Windows: 使用 PowerShell 下载
562
- // 将URL和目标路径写入临时PS1脚本文件,避免命令行引号/特殊字符问题
563
- const tmpScript = path.join(os.tmpdir(), 'sciagent-download.ps1');
564
- const scriptContent = [
565
- '$ProgressPreference = "SilentlyContinue"',
566
- `$uri = "${downloadUrl}"`,
567
- `$out = "${targetPath}"`,
568
- 'Write-Host " Downloading from server..."',
569
- 'Invoke-WebRequest -Uri $uri -OutFile $out -UseBasicParsing',
570
- 'if (Test-Path $out) { $s = (Get-Item $out).Length; Write-Host " Downloaded: $s bytes" } else { Write-Error "File not created"; exit 1 }'
571
- ].join('\r\n');
572
- fs.writeFileSync(tmpScript, scriptContent, 'utf8');
655
+ // 如果目标文件已存在,先下载到临时目录再替换
656
+ if (targetExists) {
657
+ const tempDir = path.join(os.tmpdir(), 'sciagent-update');
658
+ fs.mkdirSync(tempDir, { recursive: true });
659
+ const tempPath = path.join(tempDir, binName);
660
+ // 清理旧的临时文件
661
+ try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e) {}
662
+
663
+ console.log(` Downloading ${filename} v${version} to temp...`);
664
+
665
+ for (const source of downloadUrls) {
666
+ console.log(` Trying ${source.name}...`);
573
667
 
574
668
  try {
575
- execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${tmpScript}"`, {
576
- stdio: 'inherit',
577
- timeout: 600000
578
- });
579
- } finally {
580
- try { fs.unlinkSync(tmpScript); } catch (e) {}
669
+ if (platform === 'win32') {
670
+ const tmpScript = path.join(os.tmpdir(), 'sciagent-download.ps1');
671
+ let scriptLines = [
672
+ '$ProgressPreference = "SilentlyContinue"',
673
+ `$uri = "${source.url}"`,
674
+ `$out = "${tempPath}"`,
675
+ 'Write-Host " Downloading..."',
676
+ ];
677
+
678
+ if (source.auth === 'jihulab') {
679
+ const b64 = Buffer.from(`${JIHULAB_DEPLOY_TOKEN_USER}:${JIHULAB_DEPLOY_TOKEN}`).toString('base64');
680
+ scriptLines.push(`$headers = @{ "Authorization" = "Basic ${b64}" }`);
681
+ scriptLines.push('Invoke-WebRequest -Uri $uri -OutFile $out -UseBasicParsing -Headers $headers');
682
+ } else {
683
+ scriptLines.push('Invoke-WebRequest -Uri $uri -OutFile $out -UseBasicParsing');
684
+ }
685
+ scriptLines.push('if (Test-Path $out) { $s = (Get-Item $out).Length; Write-Host " Downloaded: $s bytes" } else { Write-Error "File not created"; exit 1 }');
686
+
687
+ fs.writeFileSync(tmpScript, scriptLines.join('\r\n'), 'utf8');
688
+
689
+ try {
690
+ execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${tmpScript}"`, {
691
+ stdio: 'inherit',
692
+ timeout: 600000
693
+ });
694
+ } finally {
695
+ try { fs.unlinkSync(tmpScript); } catch (e) {}
696
+ }
697
+ } else {
698
+ let curlCmd = `curl -fsSL -o '${tempPath}'`;
699
+ if (source.auth === 'jihulab') {
700
+ curlCmd += ` -u '${JIHULAB_DEPLOY_TOKEN_USER}:${JIHULAB_DEPLOY_TOKEN}'`;
701
+ }
702
+ curlCmd += ` '${source.url}'`;
703
+ execSync(curlCmd, { stdio: 'pipe', timeout: 600000 });
704
+ fs.chmodSync(tempPath, 0o755);
705
+ }
706
+
707
+ // 验证临时文件
708
+ if (fs.existsSync(tempPath)) {
709
+ const tempStats = fs.statSync(tempPath);
710
+ if (tempStats.size > 10 * 1024 * 1024) {
711
+ console.log(` ✅ Downloaded from ${source.name}: ${(tempStats.size / (1024 * 1024)).toFixed(1)} MB`);
712
+
713
+ // 尝试替换目标文件
714
+ try {
715
+ if (platform === 'win32') {
716
+ const tmpScript = path.join(os.tmpdir(), 'sciagent-replace.ps1');
717
+ const scriptContent = [
718
+ `Copy-Item -Path "${tempPath}" -Destination "${targetPath}" -Force`,
719
+ 'if (Test-Path $out) { Write-Host "OK" } else { Write-Error "Copy failed"; exit 1 }'
720
+ ].join('\r\n');
721
+ fs.writeFileSync(tmpScript, scriptContent, 'utf8');
722
+ try {
723
+ execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${tmpScript}"`, {
724
+ stdio: 'pipe', timeout: 30000
725
+ });
726
+ } finally {
727
+ try { fs.unlinkSync(tmpScript); } catch (e) {}
728
+ }
729
+ } else {
730
+ fs.copyFileSync(tempPath, targetPath);
731
+ fs.chmodSync(targetPath, 0o755);
732
+ }
733
+
734
+ // 验证替换成功
735
+ if (fs.existsSync(targetPath)) {
736
+ const targetStats = fs.statSync(targetPath);
737
+ if (targetStats.size > 10 * 1024 * 1024) {
738
+ fs.writeFileSync(path.join(installDir, '.version'), version);
739
+ try { fs.unlinkSync(tempPath); } catch (e) {}
740
+ return targetPath;
741
+ }
742
+ }
743
+ } catch (replaceErr) {
744
+ // 替换失败(可能被锁定),写入 pending update
745
+ console.log(` ⚠️ Cannot replace running binary. Writing pending update...`);
746
+ const pendingData = {
747
+ version: version,
748
+ tempPath: tempPath,
749
+ timestamp: new Date().toISOString(),
750
+ platform: process.platform
751
+ };
752
+ fs.writeFileSync(path.join(installDir, '.pending-update'), JSON.stringify(pendingData, null, 2), 'utf8');
753
+ console.log(` 📋 Update will be applied on next restart.`);
754
+ return null;
755
+ }
756
+ } else {
757
+ console.error(` ❌ Download from ${source.name} too small: ${(tempStats.size / 1024).toFixed(0)} KB`);
758
+ try { fs.unlinkSync(tempPath); } catch (e) {}
759
+ }
760
+ }
761
+ } catch (e) {
762
+ console.error(` ❌ Download from ${source.name} failed: ${e.message}`);
763
+ try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e2) {}
581
764
  }
582
- } else {
583
- // Linux/macOS: 使用 curl
584
- execSync(`curl -fsSL -o '${targetPath}' '${downloadUrl}'`, {
585
- stdio: 'pipe',
586
- timeout: 600000
587
- });
588
- fs.chmodSync(targetPath, 0o755);
589
765
  }
590
766
 
591
- // 验证下载
592
- if (fs.existsSync(targetPath)) {
593
- const stats = fs.statSync(targetPath);
594
- if (stats.size > 10 * 1024 * 1024) {
595
- console.log(` ✅ Downloaded: ${(stats.size / (1024 * 1024)).toFixed(1)} MB`);
596
- // 写入版本文件
597
- fs.writeFileSync(path.join(installDir, '.version'), version);
598
- return targetPath;
767
+ console.error(` ❌ All download mirrors failed.`);
768
+ return null;
769
+ }
770
+
771
+ // 目标文件不存在(首次安装),直接下载到目标路径,尝试多个源
772
+ console.log(` Downloading ${filename} v${version}...`);
773
+ console.log(` Target: ${targetPath}`);
774
+
775
+ for (const source of downloadUrls) {
776
+ console.log(` Trying ${source.name}...`);
777
+
778
+ try {
779
+ if (platform === 'win32') {
780
+ const tmpScript = path.join(os.tmpdir(), 'sciagent-download.ps1');
781
+ let scriptLines = [
782
+ '$ProgressPreference = "SilentlyContinue"',
783
+ `$uri = "${source.url}"`,
784
+ `$out = "${targetPath}"`,
785
+ 'Write-Host " Downloading..."',
786
+ ];
787
+
788
+ if (source.auth === 'jihulab') {
789
+ const b64 = Buffer.from(`${JIHULAB_DEPLOY_TOKEN_USER}:${JIHULAB_DEPLOY_TOKEN}`).toString('base64');
790
+ scriptLines.push(`$headers = @{ "Authorization" = "Basic ${b64}" }`);
791
+ scriptLines.push('Invoke-WebRequest -Uri $uri -OutFile $out -UseBasicParsing -Headers $headers');
792
+ } else {
793
+ scriptLines.push('Invoke-WebRequest -Uri $uri -OutFile $out -UseBasicParsing');
794
+ }
795
+ scriptLines.push('if (Test-Path $out) { $s = (Get-Item $out).Length; Write-Host " Downloaded: $s bytes" } else { Write-Error "File not created"; exit 1 }');
796
+
797
+ fs.writeFileSync(tmpScript, scriptLines.join('\r\n'), 'utf8');
798
+
799
+ try {
800
+ execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${tmpScript}"`, {
801
+ stdio: 'inherit',
802
+ timeout: 600000
803
+ });
804
+ } finally {
805
+ try { fs.unlinkSync(tmpScript); } catch (e) {}
806
+ }
599
807
  } else {
600
- console.error(` ❌ Downloaded file too small: ${(stats.size / 1024).toFixed(0)} KB`);
601
- try { fs.unlinkSync(targetPath); } catch (e) {}
808
+ let curlCmd = `curl -fsSL -o '${targetPath}'`;
809
+ if (source.auth === 'jihulab') {
810
+ curlCmd += ` -u '${JIHULAB_DEPLOY_TOKEN_USER}:${JIHULAB_DEPLOY_TOKEN}'`;
811
+ }
812
+ curlCmd += ` '${source.url}'`;
813
+ execSync(curlCmd, { stdio: 'pipe', timeout: 600000 });
814
+ fs.chmodSync(targetPath, 0o755);
602
815
  }
816
+
817
+ // 验证下载
818
+ if (fs.existsSync(targetPath)) {
819
+ const stats = fs.statSync(targetPath);
820
+ if (stats.size > 10 * 1024 * 1024) {
821
+ console.log(` ✅ Downloaded from ${source.name}: ${(stats.size / (1024 * 1024)).toFixed(1)} MB`);
822
+ // 写入版本文件
823
+ fs.writeFileSync(path.join(installDir, '.version'), version);
824
+ return targetPath;
825
+ } else {
826
+ console.error(` ❌ Download from ${source.name} too small: ${(stats.size / 1024).toFixed(0)} KB`);
827
+ try { fs.unlinkSync(targetPath); } catch (e) {}
828
+ }
829
+ }
830
+ } catch (e) {
831
+ console.error(` ❌ Download from ${source.name} failed: ${e.message}`);
832
+ try { if (fs.existsSync(targetPath)) fs.unlinkSync(targetPath); } catch (e2) {}
603
833
  }
604
- } catch (e) {
605
- console.error(` ❌ Download failed: ${e.message}`);
606
- // 清理可能的部分下载
607
- try { if (fs.existsSync(targetPath)) fs.unlinkSync(targetPath); } catch (e2) {}
608
834
  }
609
835
 
836
+ console.error(` ❌ All download mirrors failed.`);
610
837
  return null;
611
838
  }
612
839
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sciagent/cli",
3
- "version": "1.1.49",
3
+ "version": "1.1.51",
4
4
  "description": "SciAgent CLI - AI Research Assistant",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -27,15 +27,25 @@ const ARCH_MAP = {
27
27
  };
28
28
 
29
29
  // 当前版本号 - 每次发布时同步更新
30
- const CURRENT_VERSION = '1.1.48';
30
+ const CURRENT_VERSION = '1.1.51';
31
31
 
32
32
  // GitHub Releases 回退版本列表(当当前版本的 release 不存在时尝试)
33
33
  const GITHUB_FALLBACK_VERSIONS = ['1.1.3', '1.0.50', '1.0.48', '1.0.46', '1.0.40', '1.0.38', '1.0.36'];
34
34
 
35
- // Releases 服务器配置
36
- // 优先使用环境变量,否则使用默认服务器
37
- const RELEASE_SERVER_URL = process.env.RELEASE_SERVER_URL ||
38
- 'https://sciagent.tech';
35
+ // JihuLab 通用包仓库配置(国内 CDN,速度快)
36
+ const JIHULAB_PROJECT_ID = '351778';
37
+ const JIHULAB_DEPLOY_TOKEN_USER = 'gitlab+deploy-token-15400';
38
+ const JIHULAB_DEPLOY_TOKEN = 'gldt-5kp7mgt4ztyyBMx1Uvn6';
39
+ const JIHULAB_BASE_URL = `https://jihulab.com/api/v4/projects/${JIHULAB_PROJECT_ID}/packages/generic/sciagent`;
40
+
41
+ // 自建服务器配置(备用源)
42
+ const SELF_HOSTED_URL = process.env.RELEASE_SERVER_URL || 'https://sciagent.tech';
43
+
44
+ // 下载源列表(按优先级排序)
45
+ const DOWNLOAD_MIRRORS = [
46
+ { name: 'JihuLab CDN', getUrl: (platform, arch, version, filename) => `${JIHULAB_BASE_URL}/${version}/${filename}`, auth: 'jihulab' },
47
+ { name: 'Self-hosted', getUrl: (platform, arch, version, filename) => `${SELF_HOSTED_URL}/api/releases/download/${platform}/${arch}/${version}`, auth: 'none' },
48
+ ];
39
49
 
40
50
  // PyPI 镜像源列表(国内优先)
41
51
  const PYPI_MIRRORS = [
@@ -110,13 +120,21 @@ function checkBinaryInstalled(platform, arch) {
110
120
  } else {
111
121
  console.log(` ℹ️ 已安装版本 ${installedVersion} != 目标版本 ${CURRENT_VERSION},需要更新`);
112
122
  // 版本不匹配,删除旧文件强制重新下载
113
- _removeBinaryFiles(homeBinDir, homeBinPath);
123
+ const allRemoved = _removeBinaryFiles(homeBinDir, homeBinPath);
124
+ if (!allRemoved) {
125
+ // 删除失败(可能被锁定),标记需要更新但不阻塞
126
+ // downloadFromServer 会下载到临时目录并处理锁定情况
127
+ console.log(` ℹ️ 旧文件删除失败(可能被占用),将下载新版本到临时目录`);
128
+ }
114
129
  return { installed: false };
115
130
  }
116
131
  } else {
117
132
  console.log(` ℹ️ 二进制版本未知(无.version文件),需要重新下载`);
118
133
  // 版本未知,删除文件强制重新下载
119
- _removeBinaryFiles(homeBinDir, homeBinPath);
134
+ const allRemoved = _removeBinaryFiles(homeBinDir, homeBinPath);
135
+ if (!allRemoved) {
136
+ console.log(` ℹ️ 旧文件删除失败(可能被占用),将下载新版本到临时目录`);
137
+ }
120
138
  return { installed: false };
121
139
  }
122
140
  } else {
@@ -144,14 +162,22 @@ function _removeBinaryFiles(binDir, binPath) {
144
162
  if (fs.existsSync(versionFile)) {
145
163
  filesToRemove.push(versionFile);
146
164
  }
165
+ // 也删除 .pending-update 文件
166
+ const pendingFile = path.join(binDir, '.pending-update');
167
+ if (fs.existsSync(pendingFile)) {
168
+ filesToRemove.push(pendingFile);
169
+ }
170
+ let allRemoved = true;
147
171
  for (const f of filesToRemove) {
148
172
  try {
149
173
  fs.unlinkSync(f);
150
174
  console.log(` ℹ️ 已删除: ${f}`);
151
175
  } catch (e) {
152
176
  console.log(` ⚠️ 无法删除 ${f}: ${e.message}`);
177
+ allRemoved = false;
153
178
  }
154
179
  }
180
+ return allRemoved;
155
181
  }
156
182
 
157
183
  function checkCodebuddyBinaryInstalled() {
@@ -169,7 +195,7 @@ function checkCodebuddyBinaryInstalled() {
169
195
  return { installed: false };
170
196
  }
171
197
 
172
- function downloadFile(url, destPath, timeout = 120000) {
198
+ function downloadFile(url, destPath, timeout = 120000, extraHeaders = {}) {
173
199
  return new Promise((resolve, reject) => {
174
200
  const protocol = url.startsWith('https') ? https : http;
175
201
 
@@ -205,7 +231,7 @@ function downloadFile(url, destPath, timeout = 120000) {
205
231
  }
206
232
 
207
233
  const request = protocol.get(requestUrl, {
208
- headers: { 'User-Agent': 'sciagent-cli/1.0' },
234
+ headers: { 'User-Agent': 'sciagent-cli/1.0', ...extraHeaders },
209
235
  timeout: 60000
210
236
  }, (response) => {
211
237
  // Handle redirects
@@ -497,7 +523,7 @@ sys.exit(1)
497
523
  fs.mkdirSync(binDir, { recursive: true });
498
524
  const binaryName = BINARY_NAMES[platform] || 'codebuddy-headless';
499
525
  const targetPath = path.join(binDir, binaryName);
500
- const downloadUrl = `${RELEASE_SERVER_URL}/api/releases/download/codebuddy/${platform}/${arch}/latest`;
526
+ const downloadUrl = `${SELF_HOSTED_URL}/api/releases/download/codebuddy/${platform}/${arch}/latest`;
501
527
 
502
528
  await downloadFile(downloadUrl, targetPath, 600000);
503
529
 
@@ -679,10 +705,6 @@ async function downloadFromServer(platform, arch, version) {
679
705
  const ext = platform === 'win32' ? '.exe' : '';
680
706
  const filename = `sciagent-${platform}-${arch}${ext}`;
681
707
 
682
- const downloadUrl = `${RELEASE_SERVER_URL}/api/releases/download/${platform}/${arch}/${version}`;
683
-
684
- console.log(`\n [Server] Downloading ${filename} v${version}...`);
685
-
686
708
  // 确定安装目录
687
709
  const installDir = getHomeBinDir();
688
710
  fs.mkdirSync(installDir, { recursive: true });
@@ -696,68 +718,92 @@ async function downloadFromServer(platform, arch, version) {
696
718
  // 清理旧的临时文件
697
719
  try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e) {}
698
720
 
699
- // Windows: 优先使用 PowerShell(更可靠的大文件下载)
700
- if (platform === 'win32') {
701
- try {
702
- const tmpScript = path.join(os.tmpdir(), 'sciagent-download.ps1');
703
- const scriptContent = [
704
- '$ProgressPreference = "SilentlyContinue"',
705
- `$uri = "${downloadUrl}"`,
706
- `$out = "${tempPath}"`,
707
- 'Write-Host " [Server] Downloading..."',
708
- 'Invoke-WebRequest -Uri $uri -OutFile $out -UseBasicParsing',
709
- 'if (Test-Path $out) { $s = (Get-Item $out).Length; Write-Host " [Server] Downloaded: $s bytes" } else { Write-Error "File not created"; exit 1 }'
710
- ].join('\r\n');
711
- fs.writeFileSync(tmpScript, scriptContent, 'utf8');
712
-
721
+ // 构建下载 URL 列表(按优先级)
722
+ const downloadUrls = DOWNLOAD_MIRRORS.map(mirror => ({
723
+ url: mirror.getUrl(platform, arch, version, filename),
724
+ name: mirror.name,
725
+ auth: mirror.auth
726
+ }));
727
+
728
+ for (const source of downloadUrls) {
729
+ console.log(`\n [${source.name}] Downloading ${filename} v${version}...`);
730
+
731
+ // Windows: 优先使用 PowerShell(更可靠的大文件下载)
732
+ if (platform === 'win32') {
713
733
  try {
714
- execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${tmpScript}"`, {
715
- stdio: 'inherit',
716
- timeout: 600000
717
- });
718
- } finally {
719
- try { fs.unlinkSync(tmpScript); } catch (e) {}
734
+ const tmpScript = path.join(os.tmpdir(), 'sciagent-download.ps1');
735
+ let scriptLines = [
736
+ '$ProgressPreference = "SilentlyContinue"',
737
+ `$uri = "${source.url}"`,
738
+ `$out = "${tempPath}"`,
739
+ 'Write-Host " Downloading..."',
740
+ ];
741
+
742
+ if (source.auth === 'jihulab') {
743
+ const b64 = Buffer.from(`${JIHULAB_DEPLOY_TOKEN_USER}:${JIHULAB_DEPLOY_TOKEN}`).toString('base64');
744
+ scriptLines.push(`$headers = @{ "Authorization" = "Basic ${b64}" }`);
745
+ scriptLines.push('Invoke-WebRequest -Uri $uri -OutFile $out -UseBasicParsing -Headers $headers');
746
+ } else {
747
+ scriptLines.push('Invoke-WebRequest -Uri $uri -OutFile $out -UseBasicParsing');
748
+ }
749
+ scriptLines.push('if (Test-Path $out) { $s = (Get-Item $out).Length; Write-Host " Downloaded: $s bytes" } else { Write-Error "File not created"; exit 1 }');
750
+
751
+ fs.writeFileSync(tmpScript, scriptLines.join('\r\n'), 'utf8');
752
+
753
+ try {
754
+ execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${tmpScript}"`, {
755
+ stdio: 'inherit',
756
+ timeout: 600000
757
+ });
758
+ } finally {
759
+ try { fs.unlinkSync(tmpScript); } catch (e) {}
760
+ }
761
+
762
+ // 验证下载到临时文件
763
+ if (fs.existsSync(tempPath)) {
764
+ const stats = fs.statSync(tempPath);
765
+ if (stats.size > 10 * 1024 * 1024) {
766
+ console.log(` [OK] Downloaded from ${source.name}: ${tempPath} (${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
767
+ // 尝试替换
768
+ return await applyDownloadedBinary(tempPath, targetPath, installDir, version, platform);
769
+ }
770
+ }
771
+
772
+ console.log(` [WARN] ${source.name} download validation failed, trying next...`);
773
+ } catch (e) {
774
+ console.log(` [WARN] ${source.name} download failed: ${e.message}, trying next...`);
720
775
  }
776
+ }
777
+
778
+ // 通用方式: Node.js https 下载到临时文件
779
+ try {
780
+ // 如果是 JihuLab,需要添加认证头
781
+ const headers = source.auth === 'jihulab'
782
+ ? { 'Authorization': `Basic ${Buffer.from(`${JIHULAB_DEPLOY_TOKEN_USER}:${JIHULAB_DEPLOY_TOKEN}`).toString('base64')}` }
783
+ : {};
784
+ await downloadFile(source.url, tempPath, 600000, headers);
721
785
 
722
- // 验证下载到临时文件
786
+ // 验证下载
723
787
  if (fs.existsSync(tempPath)) {
724
788
  const stats = fs.statSync(tempPath);
725
789
  if (stats.size > 10 * 1024 * 1024) {
726
- console.log(` [OK] Downloaded to temp: ${tempPath} (${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
790
+ console.log(` [OK] Downloaded from ${source.name}: ${tempPath} (${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
791
+ if (platform !== 'win32') {
792
+ fs.chmodSync(tempPath, 0o755);
793
+ }
727
794
  // 尝试替换
728
795
  return await applyDownloadedBinary(tempPath, targetPath, installDir, version, platform);
729
796
  }
730
797
  }
731
798
 
732
- console.log(` [WARN] PowerShell download validation failed, trying Node.js...`);
799
+ console.log(` [WARN] ${source.name} download validation failed, trying next...`);
733
800
  } catch (e) {
734
- console.log(` [WARN] PowerShell download failed: ${e.message}, trying Node.js...`);
801
+ console.log(` [WARN] ${source.name} download failed: ${e.message}, trying next...`);
735
802
  }
736
803
  }
737
804
 
738
- // 通用方式: Node.js https 下载到临时文件
739
- try {
740
- await downloadFile(downloadUrl, tempPath, 600000); // 10 分钟超时
741
-
742
- // 验证下载
743
- if (fs.existsSync(tempPath)) {
744
- const stats = fs.statSync(tempPath);
745
- if (stats.size > 10 * 1024 * 1024) {
746
- console.log(` [OK] Downloaded to temp: ${tempPath} (${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
747
- if (platform !== 'win32') {
748
- fs.chmodSync(tempPath, 0o755);
749
- }
750
- // 尝试替换
751
- return await applyDownloadedBinary(tempPath, targetPath, installDir, version, platform);
752
- }
753
- }
754
-
755
- console.log(` [WARN] Downloaded file validation failed`);
756
- return false;
757
- } catch (e) {
758
- console.log(` [WARN] Server download failed: ${e.message}`);
759
- return false;
760
- }
805
+ console.log(` [WARN] All download mirrors failed.`);
806
+ return false;
761
807
  }
762
808
 
763
809
  /**