@sciagent/cli 1.1.53 → 1.1.55

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
@@ -1,922 +1,922 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * SciAgent CLI 薄壳脚本
5
- * 自动识别平台并调用对应的预编译二进制文件
6
- *
7
- * 二进制查找策略:
8
- * 1. %LOCALAPPDATA%\sciagent\bin\ (Windows) 或 ~/.sciagent/bin/ (Linux/Mac)
9
- * - 带严格版本校验(.version文件必须匹配CURRENT_VERSION)
10
- * - 版本不匹配时自动从 Release Server 下载正确版本
11
- * 2. 本地开发目录(仅开发模式)
12
- * 3. 未找到时自动从 Release Server 下载
13
- *
14
- * 注意:不再从npm optionalDependencies中查找二进制,因为:
15
- * - npm缓存中的旧包可能包含旧版二进制
16
- * - npm可能修改package.json版本号来匹配请求,但二进制文件仍是旧的
17
- * - 超过250MB的包无法发布到npm
18
- */
19
-
20
- const { spawn, execSync } = require('child_process');
21
- const path = require('path');
22
- const fs = require('fs');
23
- const os = require('os');
24
- const https = require('https');
25
- const http = require('http');
26
-
27
- // 当前版本号 - 与 postinstall.js 和 package.json 保持同步
28
- const CURRENT_VERSION = '1.1.52';
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
- ];
44
-
45
- // 平台和架构映射
46
- const PLATFORM_MAP = {
47
- linux: 'linux',
48
- darwin: 'darwin',
49
- win32: 'win32'
50
- };
51
-
52
- const ARCH_MAP = {
53
- x64: 'x64',
54
- arm64: 'arm64',
55
- amd64: 'x64'
56
- };
57
-
58
- /**
59
- * 比较语义化版本号
60
- * 返回: -1 (a<b), 0 (a==b), 1 (a>b)
61
- */
62
- function compareVersions(a, b) {
63
- const pa = a.split('.').map(Number);
64
- const pb = b.split('.').map(Number);
65
- for (let i = 0; i < 3; i++) {
66
- const na = pa[i] || 0;
67
- const nb = pb[i] || 0;
68
- if (na < nb) return -1;
69
- if (na > nb) return 1;
70
- }
71
- return 0;
72
- }
73
-
74
- /**
75
- * 检查并应用 .pending-update 标记的更新
76
- * 在 sciagent 启动时调用(此时旧进程已退出,二进制文件不再被锁定)
77
- * 返回: true 如果应用了更新,false 如果没有待更新
78
- */
79
- function applyPendingUpdate(installDir) {
80
- const pendingFile = path.join(installDir, '.pending-update');
81
-
82
- if (!fs.existsSync(pendingFile)) {
83
- return false;
84
- }
85
-
86
- try {
87
- const pendingData = JSON.parse(fs.readFileSync(pendingFile, 'utf8'));
88
- const { version, tempPath, timestamp } = pendingData;
89
-
90
- console.log(`[UPDATE] Found pending update: v${version} (queued at ${timestamp})`);
91
-
92
- // 检查临时文件是否存在
93
- if (!fs.existsSync(tempPath)) {
94
- console.log(`[UPDATE] Temp file not found: ${tempPath}`);
95
- console.log(`[UPDATE] Clearing pending update marker.`);
96
- fs.unlinkSync(pendingFile);
97
- return false;
98
- }
99
-
100
- // 验证临时文件大小
101
- const tempStats = fs.statSync(tempPath);
102
- if (tempStats.size < 10 * 1024 * 1024) {
103
- console.log(`[UPDATE] Temp file too small (${(tempStats.size / 1024).toFixed(0)} KB), discarding.`);
104
- try { fs.unlinkSync(tempPath); } catch (e) {}
105
- fs.unlinkSync(pendingFile);
106
- return false;
107
- }
108
-
109
- // 确定目标路径
110
- const platform = PLATFORM_MAP[process.platform];
111
- const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
112
- const targetPath = path.join(installDir, binName);
113
-
114
- // 尝试替换
115
- try {
116
- if (process.platform === 'win32') {
117
- // Windows: 使用 PowerShell
118
- const tmpScript = path.join(os.tmpdir(), 'sciagent-apply-update.ps1');
119
- const scriptContent = [
120
- `Copy-Item -Path "${tempPath}" -Destination "${targetPath}" -Force`,
121
- 'if (Test-Path $out) { Write-Host "OK" } else { exit 1 }'
122
- ].join('\r\n');
123
- fs.writeFileSync(tmpScript, scriptContent, 'utf8');
124
- try {
125
- execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${tmpScript}"`, {
126
- stdio: 'pipe', timeout: 30000
127
- });
128
- } finally {
129
- try { fs.unlinkSync(tmpScript); } catch (e) {}
130
- }
131
- } else {
132
- fs.copyFileSync(tempPath, targetPath);
133
- fs.chmodSync(targetPath, 0o755);
134
- }
135
-
136
- // 验证替换成功
137
- if (fs.existsSync(targetPath)) {
138
- const targetStats = fs.statSync(targetPath);
139
- if (targetStats.size > 10 * 1024 * 1024) {
140
- // 更新版本文件
141
- fs.writeFileSync(path.join(installDir, '.version'), version);
142
- // 清理临时文件和标记
143
- try { fs.unlinkSync(tempPath); } catch (e) {}
144
- fs.unlinkSync(pendingFile);
145
- console.log(`[UPDATE] ✅ Applied pending update: v${version} (${(targetStats.size / (1024 * 1024)).toFixed(1)} MB)`);
146
- return true;
147
- }
148
- }
149
-
150
- console.log(`[UPDATE] ⚠️ Replace succeeded but validation failed.`);
151
- } catch (e) {
152
- console.log(`[UPDATE] ⚠️ Failed to apply pending update: ${e.message}`);
153
- console.log(`[UPDATE] The update will be retried on next startup.`);
154
- // 不删除 pending 文件,下次启动重试
155
- return false;
156
- }
157
- } catch (e) {
158
- console.log(`[UPDATE] ⚠️ Error reading pending update: ${e.message}`);
159
- try { fs.unlinkSync(pendingFile); } catch (e2) {}
160
- }
161
-
162
- return false;
163
- }
164
-
165
- /**
166
- * 从 Release Server 查询最新版本号
167
- * 返回: 版本字符串 或 null
168
- */
169
- function fetchLatestVersion() {
170
- return new Promise((resolve) => {
171
- // 使用自建服务器 /api/releases/list 获取所有版本,取最新的
172
- const url = `${SELF_HOSTED_URL}/api/releases/list`;
173
-
174
- const protocol = url.startsWith('https') ? https : http;
175
- const request = protocol.get(url, {
176
- headers: { 'User-Agent': 'sciagent-cli/1.0' },
177
- timeout: 10000
178
- }, (response) => {
179
- let data = '';
180
- response.on('data', (chunk) => { data += chunk; });
181
- response.on('end', () => {
182
- try {
183
- const result = JSON.parse(data);
184
- const releases = result.releases || {};
185
- // 找到所有版本中的最新版本
186
- let latestVersion = null;
187
- for (const key of Object.keys(releases)) {
188
- const versions = releases[key];
189
- if (Array.isArray(versions) && versions.length > 0 && versions[0].version) {
190
- if (!latestVersion || compareVersions(versions[0].version, latestVersion) > 0) {
191
- latestVersion = versions[0].version;
192
- }
193
- }
194
- }
195
- resolve(latestVersion);
196
- } catch (e) {
197
- resolve(null);
198
- }
199
- });
200
- });
201
-
202
- request.on('error', () => resolve(null));
203
- request.on('timeout', () => { request.destroy(); resolve(null); });
204
- });
205
- }
206
-
207
- /**
208
- * 后台下载新版本到临时目录
209
- * 下载完成后写入 .pending-update 标记
210
- * 这是异步操作,不阻塞主进程启动
211
- * 支持多源下载:优先 JihuLab CDN,失败回退到自建服务器
212
- */
213
- function backgroundDownloadUpdate(platform, arch, latestVersion) {
214
- const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
215
- const ext = platform === 'win32' ? '.exe' : '';
216
- const filename = `sciagent-${platform}-${arch}${ext}`;
217
-
218
- const installDir = getInstallDir(platform);
219
- const tempDir = path.join(os.tmpdir(), 'sciagent-update');
220
- fs.mkdirSync(tempDir, { recursive: true });
221
- const tempPath = path.join(tempDir, binName);
222
-
223
- // 清理旧的临时文件
224
- try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e) {}
225
-
226
- console.log(`[AUTO-UPDATE] Downloading v${latestVersion} in background...`);
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
-
235
- if (platform === 'win32') {
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');
256
- } else {
257
- scriptLines.push('Invoke-WebRequest -Uri $uri -OutFile $out -UseBasicParsing');
258
- }
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
- };
295
-
296
- tryDownload(0);
297
- } else {
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;
303
- }
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
- };
343
-
344
- tryDownload(0);
345
- }
346
- }
347
-
348
- /**
349
- * 检查自动更新(后台异步)
350
- * 1. 查询 Release Server 最新版本
351
- * 2. 如果有新版本,后台下载到临时目录
352
- * 3. 下载完成后写入 .pending-update 标记
353
- * 4. 下次启动时自动应用
354
- */
355
- function checkAutoUpdate(platform, arch) {
356
- // 不在后台检查中阻塞主进程
357
- const installDir = getInstallDir(platform);
358
- const pendingFile = path.join(installDir, '.pending-update');
359
-
360
- // 如果已有待更新,跳过检查
361
- if (fs.existsSync(pendingFile)) {
362
- try {
363
- const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf8'));
364
- console.log(`[AUTO-UPDATE] Pending update v${pending.version} already queued. Will apply on next restart.`);
365
- return;
366
- } catch (e) {}
367
- }
368
-
369
- // 检查上次检查时间(避免频繁查询,至少间隔4小时)
370
- const lastCheckFile = path.join(installDir, '.last-update-check');
371
- if (fs.existsSync(lastCheckFile)) {
372
- try {
373
- const lastCheck = new Date(fs.readFileSync(lastCheckFile, 'utf8').trim());
374
- const hoursSinceLastCheck = (Date.now() - lastCheck.getTime()) / (1000 * 60 * 60);
375
- if (hoursSinceLastCheck < 4) {
376
- return; // 4小时内已检查过,跳过
377
- }
378
- } catch (e) {}
379
- }
380
-
381
- // 记录检查时间
382
- fs.writeFileSync(lastCheckFile, new Date().toISOString(), 'utf8');
383
-
384
- // 异步查询最新版本
385
- fetchLatestVersion().then((latestVersion) => {
386
- if (!latestVersion) {
387
- return;
388
- }
389
-
390
- const cmp = compareVersions(latestVersion, CURRENT_VERSION);
391
- if (cmp > 0) {
392
- console.log(`[AUTO-UPDATE] 🆕 New version available: v${latestVersion} (current: v${CURRENT_VERSION})`);
393
- console.log(`[AUTO-UPDATE] Downloading in background...`);
394
- backgroundDownloadUpdate(platform, arch, latestVersion);
395
- }
396
- // 版本相同或更高,无需更新
397
- }).catch(() => {
398
- // 静默失败,不影响正常使用
399
- });
400
- }
401
-
402
- /**
403
- * 获取二进制安装目录
404
- */
405
- function getInstallDir(platform) {
406
- return platform === 'win32'
407
- ? path.join(process.env.LOCALAPPDATA || os.homedir(), 'sciagent', 'bin')
408
- : path.join(os.homedir(), '.sciagent', 'bin');
409
- }
410
-
411
- /**
412
- * 获取当前平台的二进制文件路径
413
- */
414
- function getBinaryPath() {
415
- const platform = PLATFORM_MAP[process.platform];
416
- const arch = ARCH_MAP[process.arch];
417
-
418
- if (!platform) {
419
- console.error(`Error: Unsupported platform: ${process.platform}`);
420
- console.error('Supported platforms: linux, darwin, win32');
421
- process.exit(1);
422
- }
423
-
424
- if (!arch) {
425
- console.error(`Error: Unsupported architecture: ${process.arch}`);
426
- console.error('Supported architectures: x64, arm64');
427
- process.exit(1);
428
- }
429
-
430
- const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
431
- const installDir = getInstallDir(platform);
432
- const binPath = path.join(installDir, binName);
433
- const versionFile = path.join(installDir, '.version');
434
-
435
- // 1. 首先检查并应用 pending update(此时旧进程已退出,文件不再锁定)
436
- const updated = applyPendingUpdate(installDir);
437
- if (updated) {
438
- // 更新已应用,重新读取版本信息
439
- console.log(`[INFO] Update applied. Starting with new version...`);
440
- }
441
-
442
- // 2. 检查已安装的二进制
443
- if (fs.existsSync(binPath)) {
444
- const stats = fs.statSync(binPath);
445
- if (stats.size > 10 * 1024 * 1024) {
446
- // 读取版本文件
447
- if (fs.existsSync(versionFile)) {
448
- const installedVersion = fs.readFileSync(versionFile, 'utf8').trim();
449
- const cmp = compareVersions(installedVersion, CURRENT_VERSION);
450
-
451
- if (cmp === 0) {
452
- // 版本精确匹配 → 直接使用
453
- console.log(`[INFO] SciAgent v${installedVersion} (${(stats.size / (1024 * 1024)).toFixed(1)} MB, ${stats.mtime.toISOString().slice(0,10)})`);
454
- // 后台检查自动更新
455
- checkAutoUpdate(platform, arch);
456
- return binPath;
457
- } else if (cmp < 0) {
458
- // 版本过低 → 同步下载新版本并替换,然后再启动
459
- console.log(`[INFO] SciAgent version outdated: v${installedVersion} (${(stats.size / (1024 * 1024)).toFixed(1)} MB) → v${CURRENT_VERSION}`);
460
- console.log(`[INFO] Downloading new version from server...`);
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}.`);
474
- }
475
- return binPath;
476
- } else {
477
- // 版本更高 → 允许运行(用户可能手动安装了新版)
478
- console.log(`[INFO] SciAgent v${installedVersion} (newer than package v${CURRENT_VERSION}, ${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
479
- checkAutoUpdate(platform, arch);
480
- return binPath;
481
- }
482
- } else {
483
- // 没有.version文件 → 版本未知,同步下载正确版本
484
- console.log(`[INFO] SciAgent binary found but version unknown, downloading v${CURRENT_VERSION}...`);
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.`);
498
- }
499
- return binPath;
500
- }
501
- } else {
502
- // 文件太小,损坏
503
- console.log(`[INFO] SciAgent binary too small (${(stats.size / 1024).toFixed(0)} KB), removing...`);
504
- try { fs.unlinkSync(binPath); } catch (e) {}
505
- }
506
- }
507
-
508
- // 开发模式:从本地packages目录查找
509
- const localPath = path.join(__dirname, '..', 'packages', `sciagent-${platform}-${arch}`, 'bin', binName);
510
- if (fs.existsSync(localPath)) {
511
- const stats = fs.statSync(localPath);
512
- if (stats.size > 10 * 1024 * 1024) {
513
- console.log(`[INFO] SciAgent (dev mode)`);
514
- return localPath;
515
- }
516
- }
517
-
518
- // 未找到二进制文件 - 自动从 Release Server 下载
519
- console.log(`[INFO] SciAgent binary not found, downloading v${CURRENT_VERSION} from server...`);
520
-
521
- const downloaded = downloadBinary(platform, arch, CURRENT_VERSION);
522
- if (downloaded && fs.existsSync(downloaded)) {
523
- checkAutoUpdate(platform, arch);
524
- return downloaded;
525
- }
526
-
527
- console.error(`\n❌ Failed to download SciAgent binary.`);
528
- console.error(`Please reinstall: npm install -g @sciagent/cli`);
529
- process.exit(1);
530
- }
531
-
532
- /**
533
- * 从 Release Server 下载二进制文件到临时目录(不替换当前运行的二进制)
534
- * 下载完成后写入 .pending-update 标记,下次启动时自动应用
535
- * 支持多源下载:优先 JihuLab CDN,失败回退到自建服务器
536
- * 返回: tempPath 或 null
537
- */
538
- function downloadBinaryToTemp(platform, arch, version) {
539
- const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
540
- const ext = platform === 'win32' ? '.exe' : '';
541
- const filename = `sciagent-${platform}-${arch}${ext}`;
542
-
543
- const installDir = getInstallDir(platform);
544
- const tempDir = path.join(os.tmpdir(), 'sciagent-update');
545
- fs.mkdirSync(tempDir, { recursive: true });
546
- const tempPath = path.join(tempDir, binName);
547
-
548
- // 清理旧的临时文件
549
- try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e) {}
550
-
551
- console.log(` Downloading ${filename} v${version} to temp...`);
552
- console.log(` Temp: ${tempPath}`);
553
-
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}...`);
563
-
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
- }
593
- } else {
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);
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) {}
625
- }
626
- }
627
-
628
- console.error(` ❌ All download mirrors failed.`);
629
-
630
- return null;
631
- }
632
-
633
- /**
634
- * 从 Release Server 下载二进制文件(直接到目标路径,用于首次安装或版本更新)
635
- * 如果目标文件已存在,先下载到临时目录再替换
636
- * 支持多源下载:优先 JihuLab CDN,失败回退到自建服务器
637
- */
638
- function downloadBinary(platform, arch, version) {
639
- const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
640
- const ext = platform === 'win32' ? '.exe' : '';
641
- const filename = `sciagent-${platform}-${arch}${ext}`;
642
-
643
- const installDir = getInstallDir(platform);
644
- fs.mkdirSync(installDir, { recursive: true });
645
- const targetPath = path.join(installDir, binName);
646
- const targetExists = fs.existsSync(targetPath);
647
-
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
- }));
654
-
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}...`);
667
-
668
- try {
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) {}
764
- }
765
- }
766
-
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
- }
807
- } else {
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);
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) {}
833
- }
834
- }
835
-
836
- console.error(` ❌ All download mirrors failed.`);
837
- return null;
838
- }
839
-
840
- /**
841
- * 主函数
842
- */
843
- function main() {
844
- const binaryPath = getBinaryPath();
845
-
846
- // 检查二进制文件是否存在
847
- if (!fs.existsSync(binaryPath)) {
848
- console.error(`Error: Binary not found at: ${binaryPath}`);
849
- console.error('The package may be corrupted. Please reinstall:');
850
- console.error(' npm install -g @sciagent/cli');
851
- process.exit(1);
852
- }
853
-
854
- // 检查可执行权限(非Windows)
855
- if (process.platform !== 'win32') {
856
- try {
857
- fs.accessSync(binaryPath, fs.constants.X_OK);
858
- } catch (e) {
859
- // 添加可执行权限
860
- fs.chmodSync(binaryPath, 0o755);
861
- }
862
- }
863
-
864
- // 获取命令行参数(跳过node和脚本路径)
865
- const args = process.argv.slice(2);
866
-
867
- // 启动子进程
868
- const child = spawn(binaryPath, args, {
869
- stdio: 'inherit', // 继承父进程的stdio
870
- windowsHide: false // Windows下不隐藏控制台
871
- });
872
-
873
- // 处理子进程退出
874
- child.on('exit', (code, signal) => {
875
- if (signal) {
876
- // 被信号终止
877
- process.kill(process.pid, signal);
878
- } else {
879
- // 正常退出,传递退出码
880
- process.exit(code || 0);
881
- }
882
- });
883
-
884
- // 处理子进程错误
885
- child.on('error', (err) => {
886
- if (err.code === 'ENOENT') {
887
- console.error(`Error: Could not execute binary: ${binaryPath}`);
888
- console.error('The binary may be corrupted or missing.');
889
- } else if (err.code === 'EACCES') {
890
- console.error(`Error: Permission denied: ${binaryPath}`);
891
- console.error('Please check file permissions.');
892
- } else {
893
- console.error(`Error: Failed to start SciAgent: ${err.message}`);
894
- }
895
- process.exit(1);
896
- });
897
-
898
- // 转发信号到子进程
899
- process.on('SIGINT', () => {
900
- child.kill('SIGINT');
901
- });
902
-
903
- process.on('SIGTERM', () => {
904
- child.kill('SIGTERM');
905
- });
906
-
907
- // Windows下处理CTRL+C
908
- if (process.platform === 'win32') {
909
- const readline = require('readline');
910
- const rl = readline.createInterface({
911
- input: process.stdin,
912
- output: process.stdout
913
- });
914
-
915
- rl.on('SIGINT', () => {
916
- child.kill('SIGINT');
917
- });
918
- }
919
- }
920
-
921
- // 运行主函数
922
- main();
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * SciAgent CLI 薄壳脚本
5
+ * 自动识别平台并调用对应的预编译二进制文件
6
+ *
7
+ * 二进制查找策略:
8
+ * 1. %LOCALAPPDATA%\sciagent\bin\ (Windows) 或 ~/.sciagent/bin/ (Linux/Mac)
9
+ * - 带严格版本校验(.version文件必须匹配CURRENT_VERSION)
10
+ * - 版本不匹配时自动从 Release Server 下载正确版本
11
+ * 2. 本地开发目录(仅开发模式)
12
+ * 3. 未找到时自动从 Release Server 下载
13
+ *
14
+ * 注意:不再从npm optionalDependencies中查找二进制,因为:
15
+ * - npm缓存中的旧包可能包含旧版二进制
16
+ * - npm可能修改package.json版本号来匹配请求,但二进制文件仍是旧的
17
+ * - 超过250MB的包无法发布到npm
18
+ */
19
+
20
+ const { spawn, execSync } = require('child_process');
21
+ const path = require('path');
22
+ const fs = require('fs');
23
+ const os = require('os');
24
+ const https = require('https');
25
+ const http = require('http');
26
+
27
+ // 当前版本号 - 与 postinstall.js 和 package.json 保持同步
28
+ const CURRENT_VERSION = '1.1.55';
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
+ ];
44
+
45
+ // 平台和架构映射
46
+ const PLATFORM_MAP = {
47
+ linux: 'linux',
48
+ darwin: 'darwin',
49
+ win32: 'win32'
50
+ };
51
+
52
+ const ARCH_MAP = {
53
+ x64: 'x64',
54
+ arm64: 'arm64',
55
+ amd64: 'x64'
56
+ };
57
+
58
+ /**
59
+ * 比较语义化版本号
60
+ * 返回: -1 (a<b), 0 (a==b), 1 (a>b)
61
+ */
62
+ function compareVersions(a, b) {
63
+ const pa = a.split('.').map(Number);
64
+ const pb = b.split('.').map(Number);
65
+ for (let i = 0; i < 3; i++) {
66
+ const na = pa[i] || 0;
67
+ const nb = pb[i] || 0;
68
+ if (na < nb) return -1;
69
+ if (na > nb) return 1;
70
+ }
71
+ return 0;
72
+ }
73
+
74
+ /**
75
+ * 检查并应用 .pending-update 标记的更新
76
+ * 在 sciagent 启动时调用(此时旧进程已退出,二进制文件不再被锁定)
77
+ * 返回: true 如果应用了更新,false 如果没有待更新
78
+ */
79
+ function applyPendingUpdate(installDir) {
80
+ const pendingFile = path.join(installDir, '.pending-update');
81
+
82
+ if (!fs.existsSync(pendingFile)) {
83
+ return false;
84
+ }
85
+
86
+ try {
87
+ const pendingData = JSON.parse(fs.readFileSync(pendingFile, 'utf8'));
88
+ const { version, tempPath, timestamp } = pendingData;
89
+
90
+ console.log(`[UPDATE] Found pending update: v${version} (queued at ${timestamp})`);
91
+
92
+ // 检查临时文件是否存在
93
+ if (!fs.existsSync(tempPath)) {
94
+ console.log(`[UPDATE] Temp file not found: ${tempPath}`);
95
+ console.log(`[UPDATE] Clearing pending update marker.`);
96
+ fs.unlinkSync(pendingFile);
97
+ return false;
98
+ }
99
+
100
+ // 验证临时文件大小
101
+ const tempStats = fs.statSync(tempPath);
102
+ if (tempStats.size < 10 * 1024 * 1024) {
103
+ console.log(`[UPDATE] Temp file too small (${(tempStats.size / 1024).toFixed(0)} KB), discarding.`);
104
+ try { fs.unlinkSync(tempPath); } catch (e) {}
105
+ fs.unlinkSync(pendingFile);
106
+ return false;
107
+ }
108
+
109
+ // 确定目标路径
110
+ const platform = PLATFORM_MAP[process.platform];
111
+ const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
112
+ const targetPath = path.join(installDir, binName);
113
+
114
+ // 尝试替换
115
+ try {
116
+ if (process.platform === 'win32') {
117
+ // Windows: 使用 PowerShell
118
+ const tmpScript = path.join(os.tmpdir(), 'sciagent-apply-update.ps1');
119
+ const scriptContent = [
120
+ `Copy-Item -Path "${tempPath}" -Destination "${targetPath}" -Force`,
121
+ 'if (Test-Path $out) { Write-Host "OK" } else { exit 1 }'
122
+ ].join('\r\n');
123
+ fs.writeFileSync(tmpScript, scriptContent, 'utf8');
124
+ try {
125
+ execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${tmpScript}"`, {
126
+ stdio: 'pipe', timeout: 30000
127
+ });
128
+ } finally {
129
+ try { fs.unlinkSync(tmpScript); } catch (e) {}
130
+ }
131
+ } else {
132
+ fs.copyFileSync(tempPath, targetPath);
133
+ fs.chmodSync(targetPath, 0o755);
134
+ }
135
+
136
+ // 验证替换成功
137
+ if (fs.existsSync(targetPath)) {
138
+ const targetStats = fs.statSync(targetPath);
139
+ if (targetStats.size > 10 * 1024 * 1024) {
140
+ // 更新版本文件
141
+ fs.writeFileSync(path.join(installDir, '.version'), version);
142
+ // 清理临时文件和标记
143
+ try { fs.unlinkSync(tempPath); } catch (e) {}
144
+ fs.unlinkSync(pendingFile);
145
+ console.log(`[UPDATE] ✅ Applied pending update: v${version} (${(targetStats.size / (1024 * 1024)).toFixed(1)} MB)`);
146
+ return true;
147
+ }
148
+ }
149
+
150
+ console.log(`[UPDATE] ⚠️ Replace succeeded but validation failed.`);
151
+ } catch (e) {
152
+ console.log(`[UPDATE] ⚠️ Failed to apply pending update: ${e.message}`);
153
+ console.log(`[UPDATE] The update will be retried on next startup.`);
154
+ // 不删除 pending 文件,下次启动重试
155
+ return false;
156
+ }
157
+ } catch (e) {
158
+ console.log(`[UPDATE] ⚠️ Error reading pending update: ${e.message}`);
159
+ try { fs.unlinkSync(pendingFile); } catch (e2) {}
160
+ }
161
+
162
+ return false;
163
+ }
164
+
165
+ /**
166
+ * 从 Release Server 查询最新版本号
167
+ * 返回: 版本字符串 或 null
168
+ */
169
+ function fetchLatestVersion() {
170
+ return new Promise((resolve) => {
171
+ // 使用自建服务器 /api/releases/list 获取所有版本,取最新的
172
+ const url = `${SELF_HOSTED_URL}/api/releases/list`;
173
+
174
+ const protocol = url.startsWith('https') ? https : http;
175
+ const request = protocol.get(url, {
176
+ headers: { 'User-Agent': 'sciagent-cli/1.0' },
177
+ timeout: 10000
178
+ }, (response) => {
179
+ let data = '';
180
+ response.on('data', (chunk) => { data += chunk; });
181
+ response.on('end', () => {
182
+ try {
183
+ const result = JSON.parse(data);
184
+ const releases = result.releases || {};
185
+ // 找到所有版本中的最新版本
186
+ let latestVersion = null;
187
+ for (const key of Object.keys(releases)) {
188
+ const versions = releases[key];
189
+ if (Array.isArray(versions) && versions.length > 0 && versions[0].version) {
190
+ if (!latestVersion || compareVersions(versions[0].version, latestVersion) > 0) {
191
+ latestVersion = versions[0].version;
192
+ }
193
+ }
194
+ }
195
+ resolve(latestVersion);
196
+ } catch (e) {
197
+ resolve(null);
198
+ }
199
+ });
200
+ });
201
+
202
+ request.on('error', () => resolve(null));
203
+ request.on('timeout', () => { request.destroy(); resolve(null); });
204
+ });
205
+ }
206
+
207
+ /**
208
+ * 后台下载新版本到临时目录
209
+ * 下载完成后写入 .pending-update 标记
210
+ * 这是异步操作,不阻塞主进程启动
211
+ * 支持多源下载:优先 JihuLab CDN,失败回退到自建服务器
212
+ */
213
+ function backgroundDownloadUpdate(platform, arch, latestVersion) {
214
+ const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
215
+ const ext = platform === 'win32' ? '.exe' : '';
216
+ const filename = `sciagent-${platform}-${arch}${ext}`;
217
+
218
+ const installDir = getInstallDir(platform);
219
+ const tempDir = path.join(os.tmpdir(), 'sciagent-update');
220
+ fs.mkdirSync(tempDir, { recursive: true });
221
+ const tempPath = path.join(tempDir, binName);
222
+
223
+ // 清理旧的临时文件
224
+ try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e) {}
225
+
226
+ console.log(`[AUTO-UPDATE] Downloading v${latestVersion} in background...`);
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
+
235
+ if (platform === 'win32') {
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');
256
+ } else {
257
+ scriptLines.push('Invoke-WebRequest -Uri $uri -OutFile $out -UseBasicParsing');
258
+ }
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
+ };
295
+
296
+ tryDownload(0);
297
+ } else {
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;
303
+ }
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
+ };
343
+
344
+ tryDownload(0);
345
+ }
346
+ }
347
+
348
+ /**
349
+ * 检查自动更新(后台异步)
350
+ * 1. 查询 Release Server 最新版本
351
+ * 2. 如果有新版本,后台下载到临时目录
352
+ * 3. 下载完成后写入 .pending-update 标记
353
+ * 4. 下次启动时自动应用
354
+ */
355
+ function checkAutoUpdate(platform, arch) {
356
+ // 不在后台检查中阻塞主进程
357
+ const installDir = getInstallDir(platform);
358
+ const pendingFile = path.join(installDir, '.pending-update');
359
+
360
+ // 如果已有待更新,跳过检查
361
+ if (fs.existsSync(pendingFile)) {
362
+ try {
363
+ const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf8'));
364
+ console.log(`[AUTO-UPDATE] Pending update v${pending.version} already queued. Will apply on next restart.`);
365
+ return;
366
+ } catch (e) {}
367
+ }
368
+
369
+ // 检查上次检查时间(避免频繁查询,至少间隔4小时)
370
+ const lastCheckFile = path.join(installDir, '.last-update-check');
371
+ if (fs.existsSync(lastCheckFile)) {
372
+ try {
373
+ const lastCheck = new Date(fs.readFileSync(lastCheckFile, 'utf8').trim());
374
+ const hoursSinceLastCheck = (Date.now() - lastCheck.getTime()) / (1000 * 60 * 60);
375
+ if (hoursSinceLastCheck < 4) {
376
+ return; // 4小时内已检查过,跳过
377
+ }
378
+ } catch (e) {}
379
+ }
380
+
381
+ // 记录检查时间
382
+ fs.writeFileSync(lastCheckFile, new Date().toISOString(), 'utf8');
383
+
384
+ // 异步查询最新版本
385
+ fetchLatestVersion().then((latestVersion) => {
386
+ if (!latestVersion) {
387
+ return;
388
+ }
389
+
390
+ const cmp = compareVersions(latestVersion, CURRENT_VERSION);
391
+ if (cmp > 0) {
392
+ console.log(`[AUTO-UPDATE] 🆕 New version available: v${latestVersion} (current: v${CURRENT_VERSION})`);
393
+ console.log(`[AUTO-UPDATE] Downloading in background...`);
394
+ backgroundDownloadUpdate(platform, arch, latestVersion);
395
+ }
396
+ // 版本相同或更高,无需更新
397
+ }).catch(() => {
398
+ // 静默失败,不影响正常使用
399
+ });
400
+ }
401
+
402
+ /**
403
+ * 获取二进制安装目录
404
+ */
405
+ function getInstallDir(platform) {
406
+ return platform === 'win32'
407
+ ? path.join(process.env.LOCALAPPDATA || os.homedir(), 'sciagent', 'bin')
408
+ : path.join(os.homedir(), '.sciagent', 'bin');
409
+ }
410
+
411
+ /**
412
+ * 获取当前平台的二进制文件路径
413
+ */
414
+ function getBinaryPath() {
415
+ const platform = PLATFORM_MAP[process.platform];
416
+ const arch = ARCH_MAP[process.arch];
417
+
418
+ if (!platform) {
419
+ console.error(`Error: Unsupported platform: ${process.platform}`);
420
+ console.error('Supported platforms: linux, darwin, win32');
421
+ process.exit(1);
422
+ }
423
+
424
+ if (!arch) {
425
+ console.error(`Error: Unsupported architecture: ${process.arch}`);
426
+ console.error('Supported architectures: x64, arm64');
427
+ process.exit(1);
428
+ }
429
+
430
+ const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
431
+ const installDir = getInstallDir(platform);
432
+ const binPath = path.join(installDir, binName);
433
+ const versionFile = path.join(installDir, '.version');
434
+
435
+ // 1. 首先检查并应用 pending update(此时旧进程已退出,文件不再锁定)
436
+ const updated = applyPendingUpdate(installDir);
437
+ if (updated) {
438
+ // 更新已应用,重新读取版本信息
439
+ console.log(`[INFO] Update applied. Starting with new version...`);
440
+ }
441
+
442
+ // 2. 检查已安装的二进制
443
+ if (fs.existsSync(binPath)) {
444
+ const stats = fs.statSync(binPath);
445
+ if (stats.size > 10 * 1024 * 1024) {
446
+ // 读取版本文件
447
+ if (fs.existsSync(versionFile)) {
448
+ const installedVersion = fs.readFileSync(versionFile, 'utf8').trim();
449
+ const cmp = compareVersions(installedVersion, CURRENT_VERSION);
450
+
451
+ if (cmp === 0) {
452
+ // 版本精确匹配 → 直接使用
453
+ console.log(`[INFO] SciAgent v${installedVersion} (${(stats.size / (1024 * 1024)).toFixed(1)} MB, ${stats.mtime.toISOString().slice(0,10)})`);
454
+ // 后台检查自动更新
455
+ checkAutoUpdate(platform, arch);
456
+ return binPath;
457
+ } else if (cmp < 0) {
458
+ // 版本过低 → 同步下载新版本并替换,然后再启动
459
+ console.log(`[INFO] SciAgent version outdated: v${installedVersion} (${(stats.size / (1024 * 1024)).toFixed(1)} MB) → v${CURRENT_VERSION}`);
460
+ console.log(`[INFO] Downloading new version from server...`);
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}.`);
474
+ }
475
+ return binPath;
476
+ } else {
477
+ // 版本更高 → 允许运行(用户可能手动安装了新版)
478
+ console.log(`[INFO] SciAgent v${installedVersion} (newer than package v${CURRENT_VERSION}, ${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
479
+ checkAutoUpdate(platform, arch);
480
+ return binPath;
481
+ }
482
+ } else {
483
+ // 没有.version文件 → 版本未知,同步下载正确版本
484
+ console.log(`[INFO] SciAgent binary found but version unknown, downloading v${CURRENT_VERSION}...`);
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.`);
498
+ }
499
+ return binPath;
500
+ }
501
+ } else {
502
+ // 文件太小,损坏
503
+ console.log(`[INFO] SciAgent binary too small (${(stats.size / 1024).toFixed(0)} KB), removing...`);
504
+ try { fs.unlinkSync(binPath); } catch (e) {}
505
+ }
506
+ }
507
+
508
+ // 开发模式:从本地packages目录查找
509
+ const localPath = path.join(__dirname, '..', 'packages', `sciagent-${platform}-${arch}`, 'bin', binName);
510
+ if (fs.existsSync(localPath)) {
511
+ const stats = fs.statSync(localPath);
512
+ if (stats.size > 10 * 1024 * 1024) {
513
+ console.log(`[INFO] SciAgent (dev mode)`);
514
+ return localPath;
515
+ }
516
+ }
517
+
518
+ // 未找到二进制文件 - 自动从 Release Server 下载
519
+ console.log(`[INFO] SciAgent binary not found, downloading v${CURRENT_VERSION} from server...`);
520
+
521
+ const downloaded = downloadBinary(platform, arch, CURRENT_VERSION);
522
+ if (downloaded && fs.existsSync(downloaded)) {
523
+ checkAutoUpdate(platform, arch);
524
+ return downloaded;
525
+ }
526
+
527
+ console.error(`\n❌ Failed to download SciAgent binary.`);
528
+ console.error(`Please reinstall: npm install -g @sciagent/cli`);
529
+ process.exit(1);
530
+ }
531
+
532
+ /**
533
+ * 从 Release Server 下载二进制文件到临时目录(不替换当前运行的二进制)
534
+ * 下载完成后写入 .pending-update 标记,下次启动时自动应用
535
+ * 支持多源下载:优先 JihuLab CDN,失败回退到自建服务器
536
+ * 返回: tempPath 或 null
537
+ */
538
+ function downloadBinaryToTemp(platform, arch, version) {
539
+ const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
540
+ const ext = platform === 'win32' ? '.exe' : '';
541
+ const filename = `sciagent-${platform}-${arch}${ext}`;
542
+
543
+ const installDir = getInstallDir(platform);
544
+ const tempDir = path.join(os.tmpdir(), 'sciagent-update');
545
+ fs.mkdirSync(tempDir, { recursive: true });
546
+ const tempPath = path.join(tempDir, binName);
547
+
548
+ // 清理旧的临时文件
549
+ try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e) {}
550
+
551
+ console.log(` Downloading ${filename} v${version} to temp...`);
552
+ console.log(` Temp: ${tempPath}`);
553
+
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}...`);
563
+
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
+ }
593
+ } else {
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);
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) {}
625
+ }
626
+ }
627
+
628
+ console.error(` ❌ All download mirrors failed.`);
629
+
630
+ return null;
631
+ }
632
+
633
+ /**
634
+ * 从 Release Server 下载二进制文件(直接到目标路径,用于首次安装或版本更新)
635
+ * 如果目标文件已存在,先下载到临时目录再替换
636
+ * 支持多源下载:优先 JihuLab CDN,失败回退到自建服务器
637
+ */
638
+ function downloadBinary(platform, arch, version) {
639
+ const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
640
+ const ext = platform === 'win32' ? '.exe' : '';
641
+ const filename = `sciagent-${platform}-${arch}${ext}`;
642
+
643
+ const installDir = getInstallDir(platform);
644
+ fs.mkdirSync(installDir, { recursive: true });
645
+ const targetPath = path.join(installDir, binName);
646
+ const targetExists = fs.existsSync(targetPath);
647
+
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
+ }));
654
+
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}...`);
667
+
668
+ try {
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) {}
764
+ }
765
+ }
766
+
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
+ }
807
+ } else {
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);
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) {}
833
+ }
834
+ }
835
+
836
+ console.error(` ❌ All download mirrors failed.`);
837
+ return null;
838
+ }
839
+
840
+ /**
841
+ * 主函数
842
+ */
843
+ function main() {
844
+ const binaryPath = getBinaryPath();
845
+
846
+ // 检查二进制文件是否存在
847
+ if (!fs.existsSync(binaryPath)) {
848
+ console.error(`Error: Binary not found at: ${binaryPath}`);
849
+ console.error('The package may be corrupted. Please reinstall:');
850
+ console.error(' npm install -g @sciagent/cli');
851
+ process.exit(1);
852
+ }
853
+
854
+ // 检查可执行权限(非Windows)
855
+ if (process.platform !== 'win32') {
856
+ try {
857
+ fs.accessSync(binaryPath, fs.constants.X_OK);
858
+ } catch (e) {
859
+ // 添加可执行权限
860
+ fs.chmodSync(binaryPath, 0o755);
861
+ }
862
+ }
863
+
864
+ // 获取命令行参数(跳过node和脚本路径)
865
+ const args = process.argv.slice(2);
866
+
867
+ // 启动子进程
868
+ const child = spawn(binaryPath, args, {
869
+ stdio: 'inherit', // 继承父进程的stdio
870
+ windowsHide: false // Windows下不隐藏控制台
871
+ });
872
+
873
+ // 处理子进程退出
874
+ child.on('exit', (code, signal) => {
875
+ if (signal) {
876
+ // 被信号终止
877
+ process.kill(process.pid, signal);
878
+ } else {
879
+ // 正常退出,传递退出码
880
+ process.exit(code || 0);
881
+ }
882
+ });
883
+
884
+ // 处理子进程错误
885
+ child.on('error', (err) => {
886
+ if (err.code === 'ENOENT') {
887
+ console.error(`Error: Could not execute binary: ${binaryPath}`);
888
+ console.error('The binary may be corrupted or missing.');
889
+ } else if (err.code === 'EACCES') {
890
+ console.error(`Error: Permission denied: ${binaryPath}`);
891
+ console.error('Please check file permissions.');
892
+ } else {
893
+ console.error(`Error: Failed to start SciAgent: ${err.message}`);
894
+ }
895
+ process.exit(1);
896
+ });
897
+
898
+ // 转发信号到子进程
899
+ process.on('SIGINT', () => {
900
+ child.kill('SIGINT');
901
+ });
902
+
903
+ process.on('SIGTERM', () => {
904
+ child.kill('SIGTERM');
905
+ });
906
+
907
+ // Windows下处理CTRL+C
908
+ if (process.platform === 'win32') {
909
+ const readline = require('readline');
910
+ const rl = readline.createInterface({
911
+ input: process.stdin,
912
+ output: process.stdout
913
+ });
914
+
915
+ rl.on('SIGINT', () => {
916
+ child.kill('SIGINT');
917
+ });
918
+ }
919
+ }
920
+
921
+ // 运行主函数
922
+ main();