@sciagent/cli 1.0.67 → 1.0.69

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