@sciagent/cli 1.0.50 → 1.0.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,686 +1,686 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * SciAgent CLI postinstall 脚本
5
- * 1. 自动检测并安装平台特定的二进制包
6
- * 2. 自动下载 CodeBuddy SDK 二进制文件(使用国内镜像源)
7
- */
8
-
9
- const { execSync } = require('child_process');
10
- const path = require('path');
11
- const fs = require('fs');
12
- const https = require('https');
13
- const http = require('http');
14
- const os = require('os');
15
-
16
- // 平台和架构映射
17
- const PLATFORM_MAP = {
18
- linux: 'linux',
19
- darwin: 'darwin',
20
- win32: 'win32'
21
- };
22
-
23
- const ARCH_MAP = {
24
- x64: 'x64',
25
- arm64: 'arm64',
26
- amd64: 'x64'
27
- };
28
-
29
- // 当前版本号 - 每次发布时同步更新
30
- const CURRENT_VERSION = '1.0.50';
31
-
32
- // Releases 服务器配置
33
- // 优先使用环境变量,否则使用默认服务器
34
- const RELEASE_SERVER_URL = process.env.RELEASE_SERVER_URL ||
35
- 'https://u250924-adc6-f977430f.westb.seetacloud.com:8443';
36
-
37
- // PyPI 镜像源列表(国内优先)
38
- const PYPI_MIRRORS = [
39
- 'https://mirrors.cloud.tencent.com/pypi/simple', // 腾讯云
40
- 'https://mirrors.aliyun.com/pypi/simple', // 阿里云
41
- 'https://pypi.tuna.tsinghua.edu.cn/simple', // 清华
42
- 'https://pypi.org/simple' // 官方(备用)
43
- ];
44
-
45
- // PyPI 下载URL的镜像(直接下载文件)
46
- const PYPI_DOWNLOAD_MIRRORS = [
47
- 'https://mirrors.cloud.tencent.com/pypi/packages', // 腾讯云
48
- 'https://mirrors.aliyun.com/pypi/packages', // 阿里云
49
- 'https://pypi.tuna.tsinghua.edu.cn/packages', // 清华
50
- 'https://files.pythonhosted.org/packages' // 官方(备用)
51
- ];
52
-
53
- // PyPI wheel 平台标识映射
54
- const PYPI_PLATFORM_MAP = {
55
- 'win32-x64': 'win_amd64',
56
- 'darwin-arm64': 'macosx_11_0_arm64',
57
- 'darwin-x64': 'macosx_10_12_x86_64',
58
- 'linux-x64': 'manylinux_2_17_x86_64',
59
- 'linux-arm64': 'manylinux_2_17_aarch64'
60
- };
61
-
62
- // 二进制文件名
63
- const BINARY_NAMES = {
64
- win32: 'codebuddy-headless.exe',
65
- darwin: 'codebuddy-headless',
66
- linux: 'codebuddy-headless'
67
- };
68
-
69
- function getCodebuddyBinDir() {
70
- const platform = process.platform;
71
- if (platform === 'win32') {
72
- const base = process.env.LOCALAPPDATA || os.homedir();
73
- return path.join(base, 'sciagent', 'bin');
74
- } else {
75
- return path.join(os.homedir(), '.sciagent', 'bin');
76
- }
77
- }
78
-
79
- function checkBinaryInstalled(platform, arch) {
80
- const packageName = `@sciagent/cli-${platform}-${arch}`;
81
- const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
82
-
83
- try {
84
- const packagePath = require.resolve(`${packageName}/bin/${binName}`);
85
- return { installed: true, path: packagePath };
86
- } catch (e) {
87
- return { installed: false };
88
- }
89
- }
90
-
91
- function checkCodebuddyBinaryInstalled() {
92
- const binDir = getCodebuddyBinDir();
93
- const binaryName = BINARY_NAMES[process.platform] || 'codebuddy-headless';
94
- const binaryPath = path.join(binDir, binaryName);
95
-
96
- if (fs.existsSync(binaryPath)) {
97
- const stats = fs.statSync(binaryPath);
98
- // 检查文件大小是否合理(至少10MB,防止损坏的文件)
99
- if (stats.size > 10 * 1024 * 1024) {
100
- return { installed: true, path: binaryPath, size: stats.size };
101
- }
102
- }
103
- return { installed: false };
104
- }
105
-
106
- function downloadFile(url, destPath, timeout = 120000) {
107
- return new Promise((resolve, reject) => {
108
- const protocol = url.startsWith('https') ? https : http;
109
- const file = fs.createWriteStream(destPath);
110
- let completed = false;
111
-
112
- const timer = setTimeout(() => {
113
- if (!completed) {
114
- file.close();
115
- try { fs.unlinkSync(destPath); } catch (e) {}
116
- reject(new Error('Download timeout'));
117
- }
118
- }, timeout);
119
-
120
- const request = protocol.get(url, {
121
- headers: { 'User-Agent': 'sciagent-cli/1.0' },
122
- timeout: 30000
123
- }, (response) => {
124
- // Handle redirects
125
- if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
126
- file.close();
127
- try { fs.unlinkSync(destPath); } catch (e) {}
128
- clearTimeout(timer);
129
- downloadFile(response.headers.location, destPath, timeout).then(resolve).catch(reject);
130
- return;
131
- }
132
-
133
- if (response.statusCode !== 200) {
134
- file.close();
135
- try { fs.unlinkSync(destPath); } catch (e) {}
136
- clearTimeout(timer);
137
- reject(new Error(`HTTP ${response.statusCode}`));
138
- return;
139
- }
140
-
141
- const totalSize = parseInt(response.headers['content-length'], 10);
142
- let downloadedSize = 0;
143
-
144
- response.on('data', (chunk) => {
145
- downloadedSize += chunk.length;
146
- if (totalSize) {
147
- const percent = Math.floor((downloadedSize / totalSize) * 100);
148
- const mb = (downloadedSize / (1024 * 1024)).toFixed(1);
149
- const totalMb = (totalSize / (1024 * 1024)).toFixed(1);
150
- process.stdout.write(`\r 下载进度: ${percent}% (${mb}/${totalMb} MB)`);
151
- }
152
- });
153
-
154
- response.pipe(file);
155
-
156
- file.on('finish', () => {
157
- completed = true;
158
- clearTimeout(timer);
159
- file.close();
160
- console.log(); // New line after progress
161
- resolve();
162
- });
163
-
164
- file.on('error', (err) => {
165
- completed = true;
166
- clearTimeout(timer);
167
- file.close();
168
- try { fs.unlinkSync(destPath); } catch (e) {}
169
- reject(err);
170
- });
171
- });
172
-
173
- request.on('error', (err) => {
174
- completed = true;
175
- clearTimeout(timer);
176
- file.close();
177
- try { fs.unlinkSync(destPath); } catch (e) {}
178
- reject(err);
179
- });
180
-
181
- request.on('timeout', () => {
182
- request.destroy();
183
- });
184
- });
185
- }
186
-
187
- async function getSdkDownloadInfo(platformTag) {
188
- // 尝试从多个镜像获取版本信息和下载URL
189
- const mirrors = [
190
- 'https://mirrors.cloud.tencent.com/pypi/pypi/codebuddy-agent-sdk/json',
191
- 'https://mirrors.aliyun.com/pypi/pypi/codebuddy-agent-sdk/json',
192
- 'https://pypi.tuna.tsinghua.edu.cn/pypi/pypi/codebuddy-agent-sdk/json',
193
- 'https://pypi.org/pypi/codebuddy-agent-sdk/json'
194
- ];
195
-
196
- for (const url of mirrors) {
197
- try {
198
- const result = await new Promise((resolve, reject) => {
199
- const protocol = url.startsWith('https') ? https : http;
200
- protocol.get(url, {
201
- headers: { 'User-Agent': 'sciagent-cli/1.0' },
202
- timeout: 15000
203
- }, (response) => {
204
- // Handle redirects
205
- if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
206
- protocol.get(response.headers.location, {
207
- headers: { 'User-Agent': 'sciagent-cli/1.0' },
208
- timeout: 15000
209
- }, (res2) => {
210
- let data = '';
211
- res2.on('data', (chunk) => { data += chunk; });
212
- res2.on('end', () => {
213
- try {
214
- const json = JSON.parse(data);
215
- resolve(json);
216
- } catch (e) {
217
- reject(new Error('Parse error'));
218
- }
219
- });
220
- }).on('error', reject);
221
- return;
222
- }
223
-
224
- let data = '';
225
- response.on('data', (chunk) => { data += chunk; });
226
- response.on('end', () => {
227
- try {
228
- const json = JSON.parse(data);
229
- resolve(json);
230
- } catch (e) {
231
- reject(new Error('Parse error'));
232
- }
233
- });
234
- }).on('error', reject);
235
- });
236
-
237
- if (result && result.info && result.urls) {
238
- const version = result.info.version;
239
- // 查找匹配平台的wheel文件
240
- const wheelUrl = result.urls.find(u =>
241
- u.filename && u.filename.includes(platformTag) && u.filename.endsWith('.whl')
242
- );
243
-
244
- if (wheelUrl) {
245
- console.log(` 从 ${url.split('/')[2]} 获取到版本信息`);
246
- return { version, url: wheelUrl.url, size: wheelUrl.size };
247
- } else {
248
- console.log(` ${url.split('/')[2]}: 未找到 ${platformTag} 平台的wheel文件`);
249
- }
250
- }
251
- } catch (e) {
252
- console.log(` ${url.split('/')[2]}: ${e.message}`);
253
- // 继续尝试下一个镜像
254
- }
255
- }
256
-
257
- throw new Error('无法获取SDK下载信息,请检查网络连接');
258
- }
259
-
260
- async function installCodebuddySdk() {
261
- const existing = checkCodebuddyBinaryInstalled();
262
- if (existing.installed) {
263
- console.log(`✅ CodeBuddy SDK 已安装: ${existing.path}`);
264
- return true;
265
- }
266
-
267
- const platform = process.platform;
268
- const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
269
- const platformKey = `${platform}-${arch}`;
270
- const platformTag = PYPI_PLATFORM_MAP[platformKey];
271
-
272
- if (!platformTag) {
273
- console.log(`⚠️ 不支持的平台: ${platformKey},跳过 CodeBuddy SDK 安装`);
274
- return false;
275
- }
276
-
277
- console.log('');
278
- console.log('📦 正在安装 CodeBuddy SDK...');
279
-
280
- try {
281
- // 获取版本信息和下载URL
282
- console.log(' 正在获取版本信息...');
283
- const sdkInfo = await getSdkDownloadInfo(platformTag);
284
- console.log(` 版本: ${sdkInfo.version}`);
285
- console.log(` 平台: ${platformTag}`);
286
- console.log(` 文件大小: ${(sdkInfo.size / (1024 * 1024)).toFixed(1)} MB`);
287
-
288
- // 创建临时目录
289
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sciagent-sdk-'));
290
- const wheelFilename = `codebuddy_agent_sdk-${sdkInfo.version}-py3-none-${platformTag}.whl`;
291
- const wheelPath = path.join(tmpDir, wheelFilename);
292
-
293
- // 使用从JSON API获取的正确URL下载
294
- const binaryName = BINARY_NAMES[platform] || 'codebuddy-headless';
295
- const binDir = getCodebuddyBinDir();
296
- const targetPath = path.join(binDir, binaryName);
297
-
298
- console.log(' 正在下载...');
299
- await downloadFile(sdkInfo.url, wheelPath);
300
- console.log(' ✅ 下载成功');
301
-
302
- // 提取二进制文件
303
- console.log(' 正在提取二进制文件...');
304
- fs.mkdirSync(binDir, { recursive: true });
305
-
306
- // 使用 Python 解压 wheel 文件
307
- const extractScript = `
308
- import zipfile, sys, os
309
- wheel_path = sys.argv[1]
310
- target_dir = sys.argv[2]
311
- binary_name = sys.argv[3]
312
-
313
- with zipfile.ZipFile(wheel_path, 'r') as zf:
314
- for name in zf.namelist():
315
- if binary_name in name and '/bin/' in name:
316
- with zf.open(name) as src:
317
- target_path = os.path.join(target_dir, binary_name)
318
- with open(target_path, 'wb') as dst:
319
- dst.write(src.read())
320
- if sys.platform != 'win32':
321
- os.chmod(target_path, 0o755)
322
- print(f'Extracted: {target_path}')
323
- sys.exit(0)
324
- print(f'Error: {binary_name} not found in wheel')
325
- sys.exit(1)
326
- `;
327
-
328
- const scriptPath = path.join(tmpDir, 'extract.py');
329
- fs.writeFileSync(scriptPath, extractScript);
330
-
331
- try {
332
- execSync(`python3 "${scriptPath}" "${wheelPath}" "${binDir}" "${binaryName}"`, {
333
- stdio: 'inherit',
334
- timeout: 120000
335
- });
336
- } catch (e) {
337
- // 如果python3失败,尝试python
338
- execSync(`python "${scriptPath}" "${wheelPath}" "${binDir}" "${binaryName}"`, {
339
- stdio: 'inherit',
340
- timeout: 120000
341
- });
342
- }
343
-
344
- // 验证安装
345
- if (fs.existsSync(targetPath)) {
346
- const stats = fs.statSync(targetPath);
347
- const sizeMb = (stats.size / (1024 * 1024)).toFixed(1);
348
- console.log(`✅ CodeBuddy SDK 安装成功!`);
349
- console.log(` 路径: ${targetPath}`);
350
- console.log(` 大小: ${sizeMb} MB`);
351
-
352
- // 清理临时文件
353
- try {
354
- fs.unlinkSync(wheelPath);
355
- fs.unlinkSync(scriptPath);
356
- fs.rmdirSync(tmpDir);
357
- } catch (e) {}
358
-
359
- return true;
360
- } else {
361
- console.error('❌ 安装验证失败');
362
- return false;
363
- }
364
-
365
- } catch (e) {
366
- console.error(`❌ CodeBuddy SDK 安装失败: ${e.message}`);
367
- console.error(' 你可以稍后运行 "sciagent install-sdk" 手动安装');
368
- return false;
369
- }
370
- }
371
-
372
- // GitHub Releases 配置
373
- const GITHUB_REPO = 'poisondrinker/research-agent';
374
- const GITHUB_API = `https://api.github.com/repos/${GITHUB_REPO}/releases`;
375
-
376
- async function downloadFromGitHub(platform, arch, version) {
377
- const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
378
- const assetName = `sciagent-${platform}-${arch}-${version}${platform === 'win32' ? '.exe' : ''}`;
379
-
380
- console.log(`\n 📥 尝试从 GitHub Releases 下载...`);
381
-
382
- // 确定安装目录
383
- const installDir = path.join(os.homedir(), '.sciagent', 'bin');
384
- fs.mkdirSync(installDir, { recursive: true });
385
- const targetPath = path.join(installDir, binName);
386
-
387
- try {
388
- // 获取 release 信息
389
- const releaseUrl = `${GITHUB_API}/tags/v${version}`;
390
- console.log(` 查找 release: v${version}`);
391
-
392
- const releaseInfo = await new Promise((resolve, reject) => {
393
- https.get(releaseUrl, {
394
- headers: {
395
- 'User-Agent': 'sciagent-cli/1.0',
396
- 'Accept': 'application/vnd.github.v3+json'
397
- },
398
- timeout: 15000
399
- }, (response) => {
400
- let data = '';
401
- if (response.statusCode === 301 || response.statusCode === 302) {
402
- // Follow redirect
403
- https.get(response.headers.location, {
404
- headers: { 'User-Agent': 'sciagent-cli/1.0' },
405
- timeout: 15000
406
- }, (res2) => {
407
- let data2 = '';
408
- res2.on('data', (chunk) => { data2 += chunk; });
409
- res2.on('end', () => {
410
- try { resolve(JSON.parse(data2)); } catch (e) { reject(e); }
411
- });
412
- }).on('error', reject);
413
- return;
414
- }
415
- response.on('data', (chunk) => { data += chunk; });
416
- response.on('end', () => {
417
- if (response.statusCode === 404) {
418
- reject(new Error(`Release v${version} not found`));
419
- return;
420
- }
421
- try { resolve(JSON.parse(data)); } catch (e) { reject(e); }
422
- });
423
- }).on('error', reject);
424
- });
425
-
426
- // 查找匹配的 asset
427
- const assets = releaseInfo.assets || [];
428
- const asset = assets.find(a => a.name === assetName || a.name.includes(`${platform}-${arch}`));
429
-
430
- if (!asset) {
431
- console.log(` ⚠️ 未找到匹配的 asset: ${assetName}`);
432
- console.log(` 可用 assets: ${assets.map(a => a.name).join(', ') || 'none'}`);
433
- return false;
434
- }
435
-
436
- console.log(` 找到: ${asset.name} (${(asset.size / (1024 * 1024)).toFixed(1)} MB)`);
437
- console.log(` 下载中...`);
438
-
439
- // 下载 asset(带 redirect 支持)
440
- await new Promise((resolve, reject) => {
441
- const downloadUrl = asset.browser_download_url;
442
- const protocol = downloadUrl.startsWith('https') ? https : http;
443
-
444
- const doDownload = (url) => {
445
- protocol.get(url, {
446
- headers: { 'User-Agent': 'sciagent-cli/1.0' },
447
- timeout: 30000
448
- }, (response) => {
449
- if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
450
- doDownload(response.headers.location);
451
- return;
452
- }
453
- if (response.statusCode !== 200) {
454
- reject(new Error(`HTTP ${response.statusCode}`));
455
- return;
456
- }
457
-
458
- const totalSize = parseInt(response.headers['content-length'], 10);
459
- let downloadedSize = 0;
460
- const file = fs.createWriteStream(targetPath);
461
-
462
- response.on('data', (chunk) => {
463
- downloadedSize += chunk.length;
464
- if (totalSize) {
465
- const percent = Math.floor((downloadedSize / totalSize) * 100);
466
- process.stdout.write(`\r 下载进度: ${percent}% (${(downloadedSize / (1024*1024)).toFixed(1)}/${(totalSize / (1024*1024)).toFixed(1)} MB)`);
467
- }
468
- });
469
-
470
- response.pipe(file);
471
- file.on('finish', () => { file.close(); console.log(); resolve(); });
472
- file.on('error', (err) => { file.close(); try { fs.unlinkSync(targetPath); } catch(e){} reject(err); });
473
- }).on('error', reject);
474
- };
475
-
476
- doDownload(downloadUrl);
477
- });
478
-
479
- // 验证下载
480
- if (fs.existsSync(targetPath)) {
481
- const stats = fs.statSync(targetPath);
482
- if (stats.size > 10 * 1024 * 1024) {
483
- console.log(` ✅ GitHub 下载成功: ${targetPath} (${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
484
- if (platform !== 'win32') {
485
- fs.chmodSync(targetPath, 0o755);
486
- }
487
- return true;
488
- }
489
- }
490
-
491
- console.log(` ⚠️ 下载文件验证失败`);
492
- return false;
493
- } catch (e) {
494
- console.log(` ⚠️ GitHub 下载失败: ${e.message}`);
495
- return false;
496
- }
497
- }
498
-
499
- async function downloadFromServer(platform, arch, version) {
500
- const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
501
- const downloadUrl = `${RELEASE_SERVER_URL}/api/releases/download/${platform}/${arch}/${version}`;
502
-
503
- console.log(`\n 📥 从 Releases 服务器下载: ${downloadUrl}`);
504
-
505
- // 确定安装目录
506
- const installDir = path.join(os.homedir(), '.sciagent', 'bin');
507
- fs.mkdirSync(installDir, { recursive: true });
508
-
509
- const targetPath = path.join(installDir, binName);
510
-
511
- try {
512
- await downloadFile(downloadUrl, targetPath, 600000); // 10 分钟超时
513
-
514
- // 验证下载
515
- if (fs.existsSync(targetPath)) {
516
- const stats = fs.statSync(targetPath);
517
- if (stats.size > 10 * 1024 * 1024) { // 至少 10MB
518
- console.log(` ✅ 下载成功: ${targetPath} (${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
519
-
520
- // Linux/macOS 添加执行权限
521
- if (platform !== 'win32') {
522
- fs.chmodSync(targetPath, 0o755);
523
- }
524
-
525
- return true;
526
- }
527
- }
528
-
529
- console.log(` ⚠️ 下载文件验证失败`);
530
- return false;
531
- } catch (e) {
532
- console.log(` ⚠️ 服务器下载失败: ${e.message}`);
533
- return false;
534
- }
535
- }
536
-
537
- function installBinaryPackage(platform, arch) {
538
- // 版本回退列表:优先使用已知包含实际二进制文件的版本
539
- // 1.0.44+ 的 npm 包因超过 250MB 限制只有空壳,需要从服务器下载
540
- // 1.0.40/1.0.38/1.0.36 包含完整的二进制文件
541
- const FALLBACK_VERSIONS = [CURRENT_VERSION, '1.0.48', '1.0.46', '1.0.40', '1.0.38', '1.0.36'];
542
-
543
- let npmCmd = 'npm';
544
- const isGlobal = process.env.npm_config_global === 'true' ||
545
- process.env.npm_lifecycle_event === 'postinstall';
546
-
547
- for (const version of FALLBACK_VERSIONS) {
548
- const packageName = `@sciagent/cli-${platform}-${arch}@${version}`;
549
- console.log(`\n Installing platform binary: ${packageName}`);
550
- console.log(' This may take a moment...\n');
551
-
552
- try {
553
- let installArgs = ['install', '-g', packageName];
554
-
555
- if (!isGlobal) {
556
- installArgs = ['install', packageName];
557
- }
558
-
559
- execSync(`${npmCmd} ${installArgs.join(' ')}`, {
560
- stdio: 'inherit',
561
- timeout: 300000
562
- });
563
-
564
- // 关键修复:npm install 成功不等于二进制文件存在
565
- // 空壳包(423 bytes)也会返回成功,但没有实际二进制
566
- const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
567
- try {
568
- const resolvedPath = require.resolve(`@sciagent/cli-${platform}-${arch}/bin/${binName}`);
569
- if (fs.existsSync(resolvedPath)) {
570
- const stats = fs.statSync(resolvedPath);
571
- // 二进制文件应该至少 10MB
572
- if (stats.size > 10 * 1024 * 1024) {
573
- console.log(` ✅ Verified binary: ${resolvedPath} (${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
574
- return true;
575
- } else {
576
- console.log(` ⚠️ Binary too small (${stats.size} bytes), likely a stub package`);
577
- }
578
- }
579
- } catch (e) {
580
- // require.resolve 失败说明 bin 目录不存在
581
- }
582
- console.log(` ⚠️ ${packageName} installed but has no binary, trying next version...`);
583
- } catch (e) {
584
- console.log(` ⚠️ ${packageName} not available, trying next version...`);
585
- }
586
- }
587
-
588
- console.error(`\n ❌ Failed to install platform binary for ${platform}-${arch}`);
589
- return false;
590
- }
591
-
592
- async function main() {
593
- const platform = PLATFORM_MAP[process.platform];
594
- const arch = ARCH_MAP[process.arch];
595
-
596
- console.log('');
597
- console.log('╔══════════════════════════════════════════════════════════╗');
598
- console.log('║ SciAgent CLI - Post Install Setup ║');
599
- console.log('╚══════════════════════════════════════════════════════════╝');
600
- console.log('');
601
- console.log(` Platform: ${platform || process.platform}`);
602
- console.log(` Architecture: ${arch || process.arch}`);
603
- console.log(` Node.js: ${process.version}`);
604
- console.log('');
605
-
606
- if (!platform || !arch) {
607
- console.error('❌ Unsupported platform or architecture');
608
- console.error(` Platform: ${process.platform}`);
609
- console.error(` Architecture: ${process.arch}`);
610
- console.error('');
611
- console.error(' Supported platforms: linux, darwin, win32');
612
- console.error(' Supported architectures: x64, arm64');
613
- process.exit(1);
614
- }
615
-
616
- // 检查 SciAgent CLI 二进制
617
- const packageName = `@sciagent/cli-${platform}-${arch}`;
618
- const result = checkBinaryInstalled(platform, arch);
619
-
620
- if (result.installed) {
621
- console.log(`✅ Platform binary already installed: ${packageName}`);
622
- console.log(` Path: ${result.path}`);
623
- } else {
624
- console.log(`⚠️ Platform binary not found: ${packageName}`);
625
-
626
- // 下载策略: 1) Releases服务器 2) GitHub Releases 3) npm包
627
- console.log('');
628
- console.log(' 尝试从 Releases 服务器下载...');
629
- const serverSuccess = await downloadFromServer(platform, arch, CURRENT_VERSION);
630
-
631
- if (!serverSuccess) {
632
- // Releases 服务器失败,尝试 GitHub Releases
633
- console.log('');
634
- console.log(' 尝试从 GitHub Releases 下载...');
635
- const githubSuccess = await downloadFromGitHub(platform, arch, CURRENT_VERSION);
636
-
637
- if (!githubSuccess) {
638
- // GitHub 也失败,回退到 npm 包安装
639
- console.log('');
640
- console.log(' 回退到 npm 包安装...');
641
- const npmSuccess = installBinaryPackage(platform, arch);
642
-
643
- if (!npmSuccess) {
644
- console.error('');
645
- console.error('╔══════════════════════════════════════════════════════════╗');
646
- console.error('║ Manual Installation Required ║');
647
- console.error('╚══════════════════════════════════════════════════════════╝');
648
- console.error('');
649
- console.error(' Please run this command manually:');
650
- console.error(` npm install -g ${packageName}@${CURRENT_VERSION}`);
651
- console.error('');
652
- process.exit(1);
653
- }
654
- }
655
- }
656
-
657
- const verifyResult = checkBinaryInstalled(platform, arch);
658
- if (verifyResult.installed) {
659
- console.log(`\n✅ Platform binary installed successfully`);
660
- } else {
661
- console.error(`\n❌ Installation verification failed. Please install manually:`);
662
- console.error(` npm install -g ${packageName}@${CURRENT_VERSION}`);
663
- process.exit(1);
664
- }
665
- }
666
-
667
- // 安装 CodeBuddy SDK(使用国内镜像)
668
- await installCodebuddySdk();
669
-
670
- console.log('');
671
- console.log('Usage:');
672
- console.log(' sciagent # Start with default ports');
673
- console.log(' sciagent --port 8080 # Custom proxy port');
674
- console.log(' sciagent --no-browser # Don\'t open browser');
675
- console.log(' sciagent install-sdk # Install CodeBuddy SDK manually');
676
- console.log(' sciagent --help # Show help');
677
- console.log('');
678
- console.log('Documentation: https://gitee.com/garva/research-agent');
679
- console.log('');
680
- }
681
-
682
- // 运行主函数
683
- main().catch(err => {
684
- console.error('Post install error:', err.message);
685
- process.exit(1);
686
- });
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * SciAgent CLI postinstall 脚本
5
+ * 1. 自动检测并安装平台特定的二进制包
6
+ * 2. 自动下载 CodeBuddy SDK 二进制文件(使用国内镜像源)
7
+ */
8
+
9
+ const { execSync } = require('child_process');
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+ const https = require('https');
13
+ const http = require('http');
14
+ const os = require('os');
15
+
16
+ // 平台和架构映射
17
+ const PLATFORM_MAP = {
18
+ linux: 'linux',
19
+ darwin: 'darwin',
20
+ win32: 'win32'
21
+ };
22
+
23
+ const ARCH_MAP = {
24
+ x64: 'x64',
25
+ arm64: 'arm64',
26
+ amd64: 'x64'
27
+ };
28
+
29
+ // 当前版本号 - 每次发布时同步更新
30
+ const CURRENT_VERSION = '1.0.51';
31
+
32
+ // Releases 服务器配置
33
+ // 优先使用环境变量,否则使用默认服务器
34
+ const RELEASE_SERVER_URL = process.env.RELEASE_SERVER_URL ||
35
+ 'https://u250924-adc6-f977430f.westb.seetacloud.com:8443';
36
+
37
+ // PyPI 镜像源列表(国内优先)
38
+ const PYPI_MIRRORS = [
39
+ 'https://mirrors.cloud.tencent.com/pypi/simple', // 腾讯云
40
+ 'https://mirrors.aliyun.com/pypi/simple', // 阿里云
41
+ 'https://pypi.tuna.tsinghua.edu.cn/simple', // 清华
42
+ 'https://pypi.org/simple' // 官方(备用)
43
+ ];
44
+
45
+ // PyPI 下载URL的镜像(直接下载文件)
46
+ const PYPI_DOWNLOAD_MIRRORS = [
47
+ 'https://mirrors.cloud.tencent.com/pypi/packages', // 腾讯云
48
+ 'https://mirrors.aliyun.com/pypi/packages', // 阿里云
49
+ 'https://pypi.tuna.tsinghua.edu.cn/packages', // 清华
50
+ 'https://files.pythonhosted.org/packages' // 官方(备用)
51
+ ];
52
+
53
+ // PyPI wheel 平台标识映射
54
+ const PYPI_PLATFORM_MAP = {
55
+ 'win32-x64': 'win_amd64',
56
+ 'darwin-arm64': 'macosx_11_0_arm64',
57
+ 'darwin-x64': 'macosx_10_12_x86_64',
58
+ 'linux-x64': 'manylinux_2_17_x86_64',
59
+ 'linux-arm64': 'manylinux_2_17_aarch64'
60
+ };
61
+
62
+ // 二进制文件名
63
+ const BINARY_NAMES = {
64
+ win32: 'codebuddy-headless.exe',
65
+ darwin: 'codebuddy-headless',
66
+ linux: 'codebuddy-headless'
67
+ };
68
+
69
+ function getCodebuddyBinDir() {
70
+ const platform = process.platform;
71
+ if (platform === 'win32') {
72
+ const base = process.env.LOCALAPPDATA || os.homedir();
73
+ return path.join(base, 'sciagent', 'bin');
74
+ } else {
75
+ return path.join(os.homedir(), '.sciagent', 'bin');
76
+ }
77
+ }
78
+
79
+ function checkBinaryInstalled(platform, arch) {
80
+ const packageName = `@sciagent/cli-${platform}-${arch}`;
81
+ const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
82
+
83
+ try {
84
+ const packagePath = require.resolve(`${packageName}/bin/${binName}`);
85
+ return { installed: true, path: packagePath };
86
+ } catch (e) {
87
+ return { installed: false };
88
+ }
89
+ }
90
+
91
+ function checkCodebuddyBinaryInstalled() {
92
+ const binDir = getCodebuddyBinDir();
93
+ const binaryName = BINARY_NAMES[process.platform] || 'codebuddy-headless';
94
+ const binaryPath = path.join(binDir, binaryName);
95
+
96
+ if (fs.existsSync(binaryPath)) {
97
+ const stats = fs.statSync(binaryPath);
98
+ // 检查文件大小是否合理(至少10MB,防止损坏的文件)
99
+ if (stats.size > 10 * 1024 * 1024) {
100
+ return { installed: true, path: binaryPath, size: stats.size };
101
+ }
102
+ }
103
+ return { installed: false };
104
+ }
105
+
106
+ function downloadFile(url, destPath, timeout = 120000) {
107
+ return new Promise((resolve, reject) => {
108
+ const protocol = url.startsWith('https') ? https : http;
109
+ const file = fs.createWriteStream(destPath);
110
+ let completed = false;
111
+
112
+ const timer = setTimeout(() => {
113
+ if (!completed) {
114
+ file.close();
115
+ try { fs.unlinkSync(destPath); } catch (e) {}
116
+ reject(new Error('Download timeout'));
117
+ }
118
+ }, timeout);
119
+
120
+ const request = protocol.get(url, {
121
+ headers: { 'User-Agent': 'sciagent-cli/1.0' },
122
+ timeout: 30000
123
+ }, (response) => {
124
+ // Handle redirects
125
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
126
+ file.close();
127
+ try { fs.unlinkSync(destPath); } catch (e) {}
128
+ clearTimeout(timer);
129
+ downloadFile(response.headers.location, destPath, timeout).then(resolve).catch(reject);
130
+ return;
131
+ }
132
+
133
+ if (response.statusCode !== 200) {
134
+ file.close();
135
+ try { fs.unlinkSync(destPath); } catch (e) {}
136
+ clearTimeout(timer);
137
+ reject(new Error(`HTTP ${response.statusCode}`));
138
+ return;
139
+ }
140
+
141
+ const totalSize = parseInt(response.headers['content-length'], 10);
142
+ let downloadedSize = 0;
143
+
144
+ response.on('data', (chunk) => {
145
+ downloadedSize += chunk.length;
146
+ if (totalSize) {
147
+ const percent = Math.floor((downloadedSize / totalSize) * 100);
148
+ const mb = (downloadedSize / (1024 * 1024)).toFixed(1);
149
+ const totalMb = (totalSize / (1024 * 1024)).toFixed(1);
150
+ process.stdout.write(`\r 下载进度: ${percent}% (${mb}/${totalMb} MB)`);
151
+ }
152
+ });
153
+
154
+ response.pipe(file);
155
+
156
+ file.on('finish', () => {
157
+ completed = true;
158
+ clearTimeout(timer);
159
+ file.close();
160
+ console.log(); // New line after progress
161
+ resolve();
162
+ });
163
+
164
+ file.on('error', (err) => {
165
+ completed = true;
166
+ clearTimeout(timer);
167
+ file.close();
168
+ try { fs.unlinkSync(destPath); } catch (e) {}
169
+ reject(err);
170
+ });
171
+ });
172
+
173
+ request.on('error', (err) => {
174
+ completed = true;
175
+ clearTimeout(timer);
176
+ file.close();
177
+ try { fs.unlinkSync(destPath); } catch (e) {}
178
+ reject(err);
179
+ });
180
+
181
+ request.on('timeout', () => {
182
+ request.destroy();
183
+ });
184
+ });
185
+ }
186
+
187
+ async function getSdkDownloadInfo(platformTag) {
188
+ // 尝试从多个镜像获取版本信息和下载URL
189
+ const mirrors = [
190
+ 'https://mirrors.cloud.tencent.com/pypi/pypi/codebuddy-agent-sdk/json',
191
+ 'https://mirrors.aliyun.com/pypi/pypi/codebuddy-agent-sdk/json',
192
+ 'https://pypi.tuna.tsinghua.edu.cn/pypi/pypi/codebuddy-agent-sdk/json',
193
+ 'https://pypi.org/pypi/codebuddy-agent-sdk/json'
194
+ ];
195
+
196
+ for (const url of mirrors) {
197
+ try {
198
+ const result = await new Promise((resolve, reject) => {
199
+ const protocol = url.startsWith('https') ? https : http;
200
+ protocol.get(url, {
201
+ headers: { 'User-Agent': 'sciagent-cli/1.0' },
202
+ timeout: 15000
203
+ }, (response) => {
204
+ // Handle redirects
205
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
206
+ protocol.get(response.headers.location, {
207
+ headers: { 'User-Agent': 'sciagent-cli/1.0' },
208
+ timeout: 15000
209
+ }, (res2) => {
210
+ let data = '';
211
+ res2.on('data', (chunk) => { data += chunk; });
212
+ res2.on('end', () => {
213
+ try {
214
+ const json = JSON.parse(data);
215
+ resolve(json);
216
+ } catch (e) {
217
+ reject(new Error('Parse error'));
218
+ }
219
+ });
220
+ }).on('error', reject);
221
+ return;
222
+ }
223
+
224
+ let data = '';
225
+ response.on('data', (chunk) => { data += chunk; });
226
+ response.on('end', () => {
227
+ try {
228
+ const json = JSON.parse(data);
229
+ resolve(json);
230
+ } catch (e) {
231
+ reject(new Error('Parse error'));
232
+ }
233
+ });
234
+ }).on('error', reject);
235
+ });
236
+
237
+ if (result && result.info && result.urls) {
238
+ const version = result.info.version;
239
+ // 查找匹配平台的wheel文件
240
+ const wheelUrl = result.urls.find(u =>
241
+ u.filename && u.filename.includes(platformTag) && u.filename.endsWith('.whl')
242
+ );
243
+
244
+ if (wheelUrl) {
245
+ console.log(` 从 ${url.split('/')[2]} 获取到版本信息`);
246
+ return { version, url: wheelUrl.url, size: wheelUrl.size };
247
+ } else {
248
+ console.log(` ${url.split('/')[2]}: 未找到 ${platformTag} 平台的wheel文件`);
249
+ }
250
+ }
251
+ } catch (e) {
252
+ console.log(` ${url.split('/')[2]}: ${e.message}`);
253
+ // 继续尝试下一个镜像
254
+ }
255
+ }
256
+
257
+ throw new Error('无法获取SDK下载信息,请检查网络连接');
258
+ }
259
+
260
+ async function installCodebuddySdk() {
261
+ const existing = checkCodebuddyBinaryInstalled();
262
+ if (existing.installed) {
263
+ console.log(`✅ CodeBuddy SDK 已安装: ${existing.path}`);
264
+ return true;
265
+ }
266
+
267
+ const platform = process.platform;
268
+ const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
269
+ const platformKey = `${platform}-${arch}`;
270
+ const platformTag = PYPI_PLATFORM_MAP[platformKey];
271
+
272
+ if (!platformTag) {
273
+ console.log(`⚠️ 不支持的平台: ${platformKey},跳过 CodeBuddy SDK 安装`);
274
+ return false;
275
+ }
276
+
277
+ console.log('');
278
+ console.log('📦 正在安装 CodeBuddy SDK...');
279
+
280
+ try {
281
+ // 获取版本信息和下载URL
282
+ console.log(' 正在获取版本信息...');
283
+ const sdkInfo = await getSdkDownloadInfo(platformTag);
284
+ console.log(` 版本: ${sdkInfo.version}`);
285
+ console.log(` 平台: ${platformTag}`);
286
+ console.log(` 文件大小: ${(sdkInfo.size / (1024 * 1024)).toFixed(1)} MB`);
287
+
288
+ // 创建临时目录
289
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sciagent-sdk-'));
290
+ const wheelFilename = `codebuddy_agent_sdk-${sdkInfo.version}-py3-none-${platformTag}.whl`;
291
+ const wheelPath = path.join(tmpDir, wheelFilename);
292
+
293
+ // 使用从JSON API获取的正确URL下载
294
+ const binaryName = BINARY_NAMES[platform] || 'codebuddy-headless';
295
+ const binDir = getCodebuddyBinDir();
296
+ const targetPath = path.join(binDir, binaryName);
297
+
298
+ console.log(' 正在下载...');
299
+ await downloadFile(sdkInfo.url, wheelPath);
300
+ console.log(' ✅ 下载成功');
301
+
302
+ // 提取二进制文件
303
+ console.log(' 正在提取二进制文件...');
304
+ fs.mkdirSync(binDir, { recursive: true });
305
+
306
+ // 使用 Python 解压 wheel 文件
307
+ const extractScript = `
308
+ import zipfile, sys, os
309
+ wheel_path = sys.argv[1]
310
+ target_dir = sys.argv[2]
311
+ binary_name = sys.argv[3]
312
+
313
+ with zipfile.ZipFile(wheel_path, 'r') as zf:
314
+ for name in zf.namelist():
315
+ if binary_name in name and '/bin/' in name:
316
+ with zf.open(name) as src:
317
+ target_path = os.path.join(target_dir, binary_name)
318
+ with open(target_path, 'wb') as dst:
319
+ dst.write(src.read())
320
+ if sys.platform != 'win32':
321
+ os.chmod(target_path, 0o755)
322
+ print(f'Extracted: {target_path}')
323
+ sys.exit(0)
324
+ print(f'Error: {binary_name} not found in wheel')
325
+ sys.exit(1)
326
+ `;
327
+
328
+ const scriptPath = path.join(tmpDir, 'extract.py');
329
+ fs.writeFileSync(scriptPath, extractScript);
330
+
331
+ try {
332
+ execSync(`python3 "${scriptPath}" "${wheelPath}" "${binDir}" "${binaryName}"`, {
333
+ stdio: 'inherit',
334
+ timeout: 120000
335
+ });
336
+ } catch (e) {
337
+ // 如果python3失败,尝试python
338
+ execSync(`python "${scriptPath}" "${wheelPath}" "${binDir}" "${binaryName}"`, {
339
+ stdio: 'inherit',
340
+ timeout: 120000
341
+ });
342
+ }
343
+
344
+ // 验证安装
345
+ if (fs.existsSync(targetPath)) {
346
+ const stats = fs.statSync(targetPath);
347
+ const sizeMb = (stats.size / (1024 * 1024)).toFixed(1);
348
+ console.log(`✅ CodeBuddy SDK 安装成功!`);
349
+ console.log(` 路径: ${targetPath}`);
350
+ console.log(` 大小: ${sizeMb} MB`);
351
+
352
+ // 清理临时文件
353
+ try {
354
+ fs.unlinkSync(wheelPath);
355
+ fs.unlinkSync(scriptPath);
356
+ fs.rmdirSync(tmpDir);
357
+ } catch (e) {}
358
+
359
+ return true;
360
+ } else {
361
+ console.error('❌ 安装验证失败');
362
+ return false;
363
+ }
364
+
365
+ } catch (e) {
366
+ console.error(`❌ CodeBuddy SDK 安装失败: ${e.message}`);
367
+ console.error(' 你可以稍后运行 "sciagent install-sdk" 手动安装');
368
+ return false;
369
+ }
370
+ }
371
+
372
+ // GitHub Releases 配置
373
+ const GITHUB_REPO = 'poisondrinker/research-agent';
374
+ const GITHUB_API = `https://api.github.com/repos/${GITHUB_REPO}/releases`;
375
+
376
+ async function downloadFromGitHub(platform, arch, version) {
377
+ const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
378
+ const assetName = `sciagent-${platform}-${arch}${platform === 'win32' ? '.exe' : ''}`;
379
+
380
+ console.log(`\n 📥 尝试从 GitHub Releases 下载...`);
381
+
382
+ // 确定安装目录
383
+ const installDir = path.join(os.homedir(), '.sciagent', 'bin');
384
+ fs.mkdirSync(installDir, { recursive: true });
385
+ const targetPath = path.join(installDir, binName);
386
+
387
+ try {
388
+ // 获取 release 信息
389
+ const releaseUrl = `${GITHUB_API}/tags/v${version}`;
390
+ console.log(` 查找 release: v${version}`);
391
+
392
+ const releaseInfo = await new Promise((resolve, reject) => {
393
+ https.get(releaseUrl, {
394
+ headers: {
395
+ 'User-Agent': 'sciagent-cli/1.0',
396
+ 'Accept': 'application/vnd.github.v3+json'
397
+ },
398
+ timeout: 15000
399
+ }, (response) => {
400
+ let data = '';
401
+ if (response.statusCode === 301 || response.statusCode === 302) {
402
+ // Follow redirect
403
+ https.get(response.headers.location, {
404
+ headers: { 'User-Agent': 'sciagent-cli/1.0' },
405
+ timeout: 15000
406
+ }, (res2) => {
407
+ let data2 = '';
408
+ res2.on('data', (chunk) => { data2 += chunk; });
409
+ res2.on('end', () => {
410
+ try { resolve(JSON.parse(data2)); } catch (e) { reject(e); }
411
+ });
412
+ }).on('error', reject);
413
+ return;
414
+ }
415
+ response.on('data', (chunk) => { data += chunk; });
416
+ response.on('end', () => {
417
+ if (response.statusCode === 404) {
418
+ reject(new Error(`Release v${version} not found`));
419
+ return;
420
+ }
421
+ try { resolve(JSON.parse(data)); } catch (e) { reject(e); }
422
+ });
423
+ }).on('error', reject);
424
+ });
425
+
426
+ // 查找匹配的 asset
427
+ const assets = releaseInfo.assets || [];
428
+ const asset = assets.find(a => a.name === assetName || a.name.includes(`${platform}-${arch}`));
429
+
430
+ if (!asset) {
431
+ console.log(` ⚠️ 未找到匹配的 asset: ${assetName}`);
432
+ console.log(` 可用 assets: ${assets.map(a => a.name).join(', ') || 'none'}`);
433
+ return false;
434
+ }
435
+
436
+ console.log(` 找到: ${asset.name} (${(asset.size / (1024 * 1024)).toFixed(1)} MB)`);
437
+ console.log(` 下载中...`);
438
+
439
+ // 下载 asset(带 redirect 支持)
440
+ await new Promise((resolve, reject) => {
441
+ const downloadUrl = asset.browser_download_url;
442
+ const protocol = downloadUrl.startsWith('https') ? https : http;
443
+
444
+ const doDownload = (url) => {
445
+ protocol.get(url, {
446
+ headers: { 'User-Agent': 'sciagent-cli/1.0' },
447
+ timeout: 30000
448
+ }, (response) => {
449
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
450
+ doDownload(response.headers.location);
451
+ return;
452
+ }
453
+ if (response.statusCode !== 200) {
454
+ reject(new Error(`HTTP ${response.statusCode}`));
455
+ return;
456
+ }
457
+
458
+ const totalSize = parseInt(response.headers['content-length'], 10);
459
+ let downloadedSize = 0;
460
+ const file = fs.createWriteStream(targetPath);
461
+
462
+ response.on('data', (chunk) => {
463
+ downloadedSize += chunk.length;
464
+ if (totalSize) {
465
+ const percent = Math.floor((downloadedSize / totalSize) * 100);
466
+ process.stdout.write(`\r 下载进度: ${percent}% (${(downloadedSize / (1024*1024)).toFixed(1)}/${(totalSize / (1024*1024)).toFixed(1)} MB)`);
467
+ }
468
+ });
469
+
470
+ response.pipe(file);
471
+ file.on('finish', () => { file.close(); console.log(); resolve(); });
472
+ file.on('error', (err) => { file.close(); try { fs.unlinkSync(targetPath); } catch(e){} reject(err); });
473
+ }).on('error', reject);
474
+ };
475
+
476
+ doDownload(downloadUrl);
477
+ });
478
+
479
+ // 验证下载
480
+ if (fs.existsSync(targetPath)) {
481
+ const stats = fs.statSync(targetPath);
482
+ if (stats.size > 10 * 1024 * 1024) {
483
+ console.log(` ✅ GitHub 下载成功: ${targetPath} (${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
484
+ if (platform !== 'win32') {
485
+ fs.chmodSync(targetPath, 0o755);
486
+ }
487
+ return true;
488
+ }
489
+ }
490
+
491
+ console.log(` ⚠️ 下载文件验证失败`);
492
+ return false;
493
+ } catch (e) {
494
+ console.log(` ⚠️ GitHub 下载失败: ${e.message}`);
495
+ return false;
496
+ }
497
+ }
498
+
499
+ async function downloadFromServer(platform, arch, version) {
500
+ const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
501
+ const downloadUrl = `${RELEASE_SERVER_URL}/api/releases/download/${platform}/${arch}/${version}`;
502
+
503
+ console.log(`\n 📥 从 Releases 服务器下载: ${downloadUrl}`);
504
+
505
+ // 确定安装目录
506
+ const installDir = path.join(os.homedir(), '.sciagent', 'bin');
507
+ fs.mkdirSync(installDir, { recursive: true });
508
+
509
+ const targetPath = path.join(installDir, binName);
510
+
511
+ try {
512
+ await downloadFile(downloadUrl, targetPath, 600000); // 10 分钟超时
513
+
514
+ // 验证下载
515
+ if (fs.existsSync(targetPath)) {
516
+ const stats = fs.statSync(targetPath);
517
+ if (stats.size > 10 * 1024 * 1024) { // 至少 10MB
518
+ console.log(` ✅ 下载成功: ${targetPath} (${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
519
+
520
+ // Linux/macOS 添加执行权限
521
+ if (platform !== 'win32') {
522
+ fs.chmodSync(targetPath, 0o755);
523
+ }
524
+
525
+ return true;
526
+ }
527
+ }
528
+
529
+ console.log(` ⚠️ 下载文件验证失败`);
530
+ return false;
531
+ } catch (e) {
532
+ console.log(` ⚠️ 服务器下载失败: ${e.message}`);
533
+ return false;
534
+ }
535
+ }
536
+
537
+ function installBinaryPackage(platform, arch) {
538
+ // 版本回退列表:优先使用已知包含实际二进制文件的版本
539
+ // 1.0.44+ 的 npm 包因超过 250MB 限制只有空壳,需要从服务器下载
540
+ // 1.0.40/1.0.38/1.0.36 包含完整的二进制文件
541
+ const FALLBACK_VERSIONS = [CURRENT_VERSION, '1.0.48', '1.0.46', '1.0.40', '1.0.38', '1.0.36'];
542
+
543
+ let npmCmd = 'npm';
544
+ const isGlobal = process.env.npm_config_global === 'true' ||
545
+ process.env.npm_lifecycle_event === 'postinstall';
546
+
547
+ for (const version of FALLBACK_VERSIONS) {
548
+ const packageName = `@sciagent/cli-${platform}-${arch}@${version}`;
549
+ console.log(`\n Installing platform binary: ${packageName}`);
550
+ console.log(' This may take a moment...\n');
551
+
552
+ try {
553
+ let installArgs = ['install', '-g', packageName];
554
+
555
+ if (!isGlobal) {
556
+ installArgs = ['install', packageName];
557
+ }
558
+
559
+ execSync(`${npmCmd} ${installArgs.join(' ')}`, {
560
+ stdio: 'inherit',
561
+ timeout: 300000
562
+ });
563
+
564
+ // 关键修复:npm install 成功不等于二进制文件存在
565
+ // 空壳包(423 bytes)也会返回成功,但没有实际二进制
566
+ const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
567
+ try {
568
+ const resolvedPath = require.resolve(`@sciagent/cli-${platform}-${arch}/bin/${binName}`);
569
+ if (fs.existsSync(resolvedPath)) {
570
+ const stats = fs.statSync(resolvedPath);
571
+ // 二进制文件应该至少 10MB
572
+ if (stats.size > 10 * 1024 * 1024) {
573
+ console.log(` ✅ Verified binary: ${resolvedPath} (${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
574
+ return true;
575
+ } else {
576
+ console.log(` ⚠️ Binary too small (${stats.size} bytes), likely a stub package`);
577
+ }
578
+ }
579
+ } catch (e) {
580
+ // require.resolve 失败说明 bin 目录不存在
581
+ }
582
+ console.log(` ⚠️ ${packageName} installed but has no binary, trying next version...`);
583
+ } catch (e) {
584
+ console.log(` ⚠️ ${packageName} not available, trying next version...`);
585
+ }
586
+ }
587
+
588
+ console.error(`\n ❌ Failed to install platform binary for ${platform}-${arch}`);
589
+ return false;
590
+ }
591
+
592
+ async function main() {
593
+ const platform = PLATFORM_MAP[process.platform];
594
+ const arch = ARCH_MAP[process.arch];
595
+
596
+ console.log('');
597
+ console.log('╔══════════════════════════════════════════════════════════╗');
598
+ console.log('║ SciAgent CLI - Post Install Setup ║');
599
+ console.log('╚══════════════════════════════════════════════════════════╝');
600
+ console.log('');
601
+ console.log(` Platform: ${platform || process.platform}`);
602
+ console.log(` Architecture: ${arch || process.arch}`);
603
+ console.log(` Node.js: ${process.version}`);
604
+ console.log('');
605
+
606
+ if (!platform || !arch) {
607
+ console.error('❌ Unsupported platform or architecture');
608
+ console.error(` Platform: ${process.platform}`);
609
+ console.error(` Architecture: ${process.arch}`);
610
+ console.error('');
611
+ console.error(' Supported platforms: linux, darwin, win32');
612
+ console.error(' Supported architectures: x64, arm64');
613
+ process.exit(1);
614
+ }
615
+
616
+ // 检查 SciAgent CLI 二进制
617
+ const packageName = `@sciagent/cli-${platform}-${arch}`;
618
+ const result = checkBinaryInstalled(platform, arch);
619
+
620
+ if (result.installed) {
621
+ console.log(`✅ Platform binary already installed: ${packageName}`);
622
+ console.log(` Path: ${result.path}`);
623
+ } else {
624
+ console.log(`⚠️ Platform binary not found: ${packageName}`);
625
+
626
+ // 下载策略: 1) Releases服务器 2) GitHub Releases 3) npm包
627
+ console.log('');
628
+ console.log(' 尝试从 Releases 服务器下载...');
629
+ const serverSuccess = await downloadFromServer(platform, arch, CURRENT_VERSION);
630
+
631
+ if (!serverSuccess) {
632
+ // Releases 服务器失败,尝试 GitHub Releases
633
+ console.log('');
634
+ console.log(' 尝试从 GitHub Releases 下载...');
635
+ const githubSuccess = await downloadFromGitHub(platform, arch, CURRENT_VERSION);
636
+
637
+ if (!githubSuccess) {
638
+ // GitHub 也失败,回退到 npm 包安装
639
+ console.log('');
640
+ console.log(' 回退到 npm 包安装...');
641
+ const npmSuccess = installBinaryPackage(platform, arch);
642
+
643
+ if (!npmSuccess) {
644
+ console.error('');
645
+ console.error('╔══════════════════════════════════════════════════════════╗');
646
+ console.error('║ Manual Installation Required ║');
647
+ console.error('╚══════════════════════════════════════════════════════════╝');
648
+ console.error('');
649
+ console.error(' Please run this command manually:');
650
+ console.error(` npm install -g ${packageName}@${CURRENT_VERSION}`);
651
+ console.error('');
652
+ process.exit(1);
653
+ }
654
+ }
655
+ }
656
+
657
+ const verifyResult = checkBinaryInstalled(platform, arch);
658
+ if (verifyResult.installed) {
659
+ console.log(`\n✅ Platform binary installed successfully`);
660
+ } else {
661
+ console.error(`\n❌ Installation verification failed. Please install manually:`);
662
+ console.error(` npm install -g ${packageName}@${CURRENT_VERSION}`);
663
+ process.exit(1);
664
+ }
665
+ }
666
+
667
+ // 安装 CodeBuddy SDK(使用国内镜像)
668
+ await installCodebuddySdk();
669
+
670
+ console.log('');
671
+ console.log('Usage:');
672
+ console.log(' sciagent # Start with default ports');
673
+ console.log(' sciagent --port 8080 # Custom proxy port');
674
+ console.log(' sciagent --no-browser # Don\'t open browser');
675
+ console.log(' sciagent install-sdk # Install CodeBuddy SDK manually');
676
+ console.log(' sciagent --help # Show help');
677
+ console.log('');
678
+ console.log('Documentation: https://gitee.com/garva/research-agent');
679
+ console.log('');
680
+ }
681
+
682
+ // 运行主函数
683
+ main().catch(err => {
684
+ console.error('Post install error:', err.message);
685
+ process.exit(1);
686
+ });