@sciagent/cli 1.1.53 → 1.1.54
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 +922 -922
- package/package.json +43 -43
- package/scripts/chmod.js +29 -29
- package/scripts/postinstall.js +1016 -1016
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.
|
|
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.54';
|
|
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();
|