@sciagent/cli 1.1.53 → 1.1.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/sciagent.js +922 -922
- package/package.json +43 -43
- package/scripts/chmod.js +29 -29
- package/scripts/postinstall.js +1016 -1016
package/scripts/postinstall.js
CHANGED
|
@@ -1,1016 +1,1016 @@
|
|
|
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.1.
|
|
31
|
-
|
|
32
|
-
// GitHub Releases 回退版本列表(当当前版本的 release 不存在时尝试)
|
|
33
|
-
const GITHUB_FALLBACK_VERSIONS = ['1.1.3', '1.0.50', '1.0.48', '1.0.46', '1.0.40', '1.0.38', '1.0.36'];
|
|
34
|
-
|
|
35
|
-
// JihuLab 通用包仓库配置(国内 CDN,速度快)
|
|
36
|
-
const JIHULAB_PROJECT_ID = '351778';
|
|
37
|
-
const JIHULAB_DEPLOY_TOKEN_USER = 'gitlab+deploy-token-15400';
|
|
38
|
-
const JIHULAB_DEPLOY_TOKEN = 'gldt-5kp7mgt4ztyyBMx1Uvn6';
|
|
39
|
-
const JIHULAB_BASE_URL = `https://jihulab.com/api/v4/projects/${JIHULAB_PROJECT_ID}/packages/generic/sciagent`;
|
|
40
|
-
|
|
41
|
-
// 自建服务器配置(备用源)
|
|
42
|
-
const SELF_HOSTED_URL = process.env.RELEASE_SERVER_URL || 'https://sciagent.tech';
|
|
43
|
-
|
|
44
|
-
// 下载源列表(按优先级排序)
|
|
45
|
-
const DOWNLOAD_MIRRORS = [
|
|
46
|
-
{ name: 'JihuLab CDN', getUrl: (platform, arch, version, filename) => `${JIHULAB_BASE_URL}/${version}/${filename}`, auth: 'jihulab' },
|
|
47
|
-
{ name: 'Self-hosted', getUrl: (platform, arch, version, filename) => `${SELF_HOSTED_URL}/api/releases/download/${platform}/${arch}/${version}`, auth: 'none' },
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
// PyPI 镜像源列表(国内优先)
|
|
51
|
-
const PYPI_MIRRORS = [
|
|
52
|
-
'https://mirrors.cloud.tencent.com/pypi/simple', // 腾讯云
|
|
53
|
-
'https://mirrors.aliyun.com/pypi/simple', // 阿里云
|
|
54
|
-
'https://pypi.tuna.tsinghua.edu.cn/simple', // 清华
|
|
55
|
-
'https://pypi.org/simple' // 官方(备用)
|
|
56
|
-
];
|
|
57
|
-
|
|
58
|
-
// PyPI 下载URL的镜像(直接下载文件)
|
|
59
|
-
const PYPI_DOWNLOAD_MIRRORS = [
|
|
60
|
-
'https://mirrors.cloud.tencent.com/pypi/packages', // 腾讯云
|
|
61
|
-
'https://mirrors.aliyun.com/pypi/packages', // 阿里云
|
|
62
|
-
'https://pypi.tuna.tsinghua.edu.cn/packages', // 清华
|
|
63
|
-
'https://files.pythonhosted.org/packages' // 官方(备用)
|
|
64
|
-
];
|
|
65
|
-
|
|
66
|
-
// PyPI wheel 平台标识映射
|
|
67
|
-
const PYPI_PLATFORM_MAP = {
|
|
68
|
-
'win32-x64': 'win_amd64',
|
|
69
|
-
'darwin-arm64': 'macosx_11_0_arm64',
|
|
70
|
-
'darwin-x64': 'macosx_10_12_x86_64',
|
|
71
|
-
'linux-x64': 'manylinux_2_17_x86_64',
|
|
72
|
-
'linux-arm64': 'manylinux_2_17_aarch64'
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// 二进制文件名
|
|
76
|
-
const BINARY_NAMES = {
|
|
77
|
-
win32: 'codebuddy-headless.exe',
|
|
78
|
-
darwin: 'codebuddy-headless',
|
|
79
|
-
linux: 'codebuddy-headless'
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
function getCodebuddyBinDir() {
|
|
83
|
-
const platform = process.platform;
|
|
84
|
-
if (platform === 'win32') {
|
|
85
|
-
const base = process.env.LOCALAPPDATA || os.homedir();
|
|
86
|
-
return path.join(base, 'sciagent', 'bin');
|
|
87
|
-
} else {
|
|
88
|
-
return path.join(os.homedir(), '.sciagent', 'bin');
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function getHomeBinDir() {
|
|
93
|
-
return process.platform === 'win32'
|
|
94
|
-
? path.join(process.env.LOCALAPPDATA || os.homedir(), 'sciagent', 'bin')
|
|
95
|
-
: path.join(os.homedir(), '.sciagent', 'bin');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function checkBinaryInstalled(platform, arch) {
|
|
99
|
-
const packageName = `@sciagent/cli-${platform}-${arch}`;
|
|
100
|
-
const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
|
|
101
|
-
const homeBinDir = getHomeBinDir();
|
|
102
|
-
const homeBinPath = path.join(homeBinDir, binName);
|
|
103
|
-
|
|
104
|
-
// 检查 ~/.sciagent/bin/ 或 %LOCALAPPDATA%\sciagent\bin
|
|
105
|
-
if (fs.existsSync(homeBinPath)) {
|
|
106
|
-
const stats = fs.statSync(homeBinPath);
|
|
107
|
-
if (stats.size > 10 * 1024 * 1024) {
|
|
108
|
-
// 检查版本文件,版本不匹配则视为未安装(需要重新下载)
|
|
109
|
-
const versionFile = path.join(homeBinDir, '.version');
|
|
110
|
-
if (fs.existsSync(versionFile)) {
|
|
111
|
-
const installedVersion = fs.readFileSync(versionFile, 'utf8').trim();
|
|
112
|
-
if (installedVersion === CURRENT_VERSION) {
|
|
113
|
-
// 额外安全检查:验证二进制文件修改时间是否在版本发布日期之后
|
|
114
|
-
// 防止 .version 文件被错误写入(如手动编辑)导致使用旧二进制
|
|
115
|
-
const mtime = stats.mtime;
|
|
116
|
-
const now = new Date();
|
|
117
|
-
const ageDays = (now - mtime) / (1000 * 60 * 60 * 24);
|
|
118
|
-
console.log(` ℹ️ 已安装版本 ${installedVersion} (文件大小: ${(stats.size / (1024*1024)).toFixed(1)} MB, 修改时间: ${mtime.toISOString().slice(0,10)}, ${ageDays.toFixed(0)}天前)`);
|
|
119
|
-
return { installed: true, path: homeBinPath };
|
|
120
|
-
} else {
|
|
121
|
-
console.log(` ℹ️ 已安装版本 ${installedVersion} != 目标版本 ${CURRENT_VERSION},需要更新`);
|
|
122
|
-
// 版本不匹配,删除旧文件强制重新下载
|
|
123
|
-
const allRemoved = _removeBinaryFiles(homeBinDir, homeBinPath);
|
|
124
|
-
if (!allRemoved) {
|
|
125
|
-
// 删除失败(可能被锁定),标记需要更新但不阻塞
|
|
126
|
-
// downloadFromServer 会下载到临时目录并处理锁定情况
|
|
127
|
-
console.log(` ℹ️ 旧文件删除失败(可能被占用),将下载新版本到临时目录`);
|
|
128
|
-
}
|
|
129
|
-
return { installed: false };
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
console.log(` ℹ️ 二进制版本未知(无.version文件),需要重新下载`);
|
|
133
|
-
// 版本未知,删除文件强制重新下载
|
|
134
|
-
const allRemoved = _removeBinaryFiles(homeBinDir, homeBinPath);
|
|
135
|
-
if (!allRemoved) {
|
|
136
|
-
console.log(` ℹ️ 旧文件删除失败(可能被占用),将下载新版本到临时目录`);
|
|
137
|
-
}
|
|
138
|
-
return { installed: false };
|
|
139
|
-
}
|
|
140
|
-
} else {
|
|
141
|
-
// 文件太小,可能是损坏的下载
|
|
142
|
-
console.log(` ℹ️ 二进制文件太小 (${(stats.size / 1024).toFixed(0)} KB < 10 MB),可能是损坏文件`);
|
|
143
|
-
_removeBinaryFiles(homeBinDir, homeBinPath);
|
|
144
|
-
return { installed: false };
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// 不再从 npm optionalDependencies 查找二进制,原因:
|
|
149
|
-
// 1. npm 缓存中的旧包可能包含旧版二进制(如 1.0.40 的 194MB 旧 exe)
|
|
150
|
-
// 2. npm 可能修改 package.json 版本号来匹配请求,但二进制文件仍是旧的
|
|
151
|
-
// 3. 超过 250MB 的包无法发布到 npm,只有空壳包
|
|
152
|
-
// 所有二进制统一从 Release Server 下载,确保版本正确
|
|
153
|
-
|
|
154
|
-
return { installed: false };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function _removeBinaryFiles(binDir, binPath) {
|
|
158
|
-
// 删除二进制文件及相关缓存文件(.version等)
|
|
159
|
-
const filesToRemove = [binPath];
|
|
160
|
-
// 也删除 .version 文件,避免版本标记残留
|
|
161
|
-
const versionFile = path.join(binDir, '.version');
|
|
162
|
-
if (fs.existsSync(versionFile)) {
|
|
163
|
-
filesToRemove.push(versionFile);
|
|
164
|
-
}
|
|
165
|
-
// 也删除 .pending-update 文件
|
|
166
|
-
const pendingFile = path.join(binDir, '.pending-update');
|
|
167
|
-
if (fs.existsSync(pendingFile)) {
|
|
168
|
-
filesToRemove.push(pendingFile);
|
|
169
|
-
}
|
|
170
|
-
let allRemoved = true;
|
|
171
|
-
for (const f of filesToRemove) {
|
|
172
|
-
try {
|
|
173
|
-
fs.unlinkSync(f);
|
|
174
|
-
console.log(` ℹ️ 已删除: ${f}`);
|
|
175
|
-
} catch (e) {
|
|
176
|
-
console.log(` ⚠️ 无法删除 ${f}: ${e.message}`);
|
|
177
|
-
allRemoved = false;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return allRemoved;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function checkCodebuddyBinaryInstalled() {
|
|
184
|
-
const binDir = getCodebuddyBinDir();
|
|
185
|
-
const binaryName = BINARY_NAMES[process.platform] || 'codebuddy-headless';
|
|
186
|
-
const binaryPath = path.join(binDir, binaryName);
|
|
187
|
-
|
|
188
|
-
if (fs.existsSync(binaryPath)) {
|
|
189
|
-
const stats = fs.statSync(binaryPath);
|
|
190
|
-
// 检查文件大小是否合理(至少10MB,防止损坏的文件)
|
|
191
|
-
if (stats.size > 10 * 1024 * 1024) {
|
|
192
|
-
return { installed: true, path: binaryPath, size: stats.size };
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return { installed: false };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function downloadFile(url, destPath, timeout = 120000, extraHeaders = {}) {
|
|
199
|
-
return new Promise((resolve, reject) => {
|
|
200
|
-
const protocol = url.startsWith('https') ? https : http;
|
|
201
|
-
|
|
202
|
-
// 确保目标目录存在
|
|
203
|
-
const destDir = path.dirname(destPath);
|
|
204
|
-
if (!fs.existsSync(destDir)) {
|
|
205
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const file = fs.createWriteStream(destPath);
|
|
209
|
-
let completed = false;
|
|
210
|
-
let downloadedSize = 0;
|
|
211
|
-
|
|
212
|
-
const timer = setTimeout(() => {
|
|
213
|
-
if (!completed) {
|
|
214
|
-
completed = true;
|
|
215
|
-
file.close();
|
|
216
|
-
try { fs.unlinkSync(destPath); } catch (e) {}
|
|
217
|
-
reject(new Error(`Download timeout after ${timeout / 1000}s (${(downloadedSize / (1024 * 1024)).toFixed(1)} MB downloaded)`));
|
|
218
|
-
}
|
|
219
|
-
}, timeout);
|
|
220
|
-
|
|
221
|
-
function doRequest(requestUrl, redirectCount = 0) {
|
|
222
|
-
if (redirectCount > 5) {
|
|
223
|
-
if (!completed) {
|
|
224
|
-
completed = true;
|
|
225
|
-
clearTimeout(timer);
|
|
226
|
-
file.close();
|
|
227
|
-
try { fs.unlinkSync(destPath); } catch (e) {}
|
|
228
|
-
reject(new Error('Too many redirects'));
|
|
229
|
-
}
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const request = protocol.get(requestUrl, {
|
|
234
|
-
headers: { 'User-Agent': 'sciagent-cli/1.0', ...extraHeaders },
|
|
235
|
-
timeout: 60000
|
|
236
|
-
}, (response) => {
|
|
237
|
-
// Handle redirects
|
|
238
|
-
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
239
|
-
response.resume(); // drain the response
|
|
240
|
-
let location = response.headers.location;
|
|
241
|
-
// Handle relative redirects
|
|
242
|
-
if (location.startsWith('/')) {
|
|
243
|
-
const parsed = new URL(requestUrl);
|
|
244
|
-
location = `${parsed.protocol}//${parsed.host}${location}`;
|
|
245
|
-
}
|
|
246
|
-
doRequest(location, redirectCount + 1);
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (response.statusCode !== 200) {
|
|
251
|
-
response.resume(); // drain the response
|
|
252
|
-
if (!completed) {
|
|
253
|
-
completed = true;
|
|
254
|
-
clearTimeout(timer);
|
|
255
|
-
file.close();
|
|
256
|
-
try { fs.unlinkSync(destPath); } catch (e) {}
|
|
257
|
-
reject(new Error(`HTTP ${response.statusCode} for ${requestUrl}`));
|
|
258
|
-
}
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const totalSize = parseInt(response.headers['content-length'], 10);
|
|
263
|
-
|
|
264
|
-
response.on('data', (chunk) => {
|
|
265
|
-
downloadedSize += chunk.length;
|
|
266
|
-
if (totalSize) {
|
|
267
|
-
const percent = Math.floor((downloadedSize / totalSize) * 100);
|
|
268
|
-
const mb = (downloadedSize / (1024 * 1024)).toFixed(1);
|
|
269
|
-
const totalMb = (totalSize / (1024 * 1024)).toFixed(1);
|
|
270
|
-
process.stdout.write(`\r 下载进度: ${percent}% (${mb}/${totalMb} MB)`);
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
response.pipe(file);
|
|
275
|
-
|
|
276
|
-
file.on('finish', () => {
|
|
277
|
-
if (!completed) {
|
|
278
|
-
completed = true;
|
|
279
|
-
clearTimeout(timer);
|
|
280
|
-
file.close();
|
|
281
|
-
console.log(); // New line after progress
|
|
282
|
-
resolve();
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
file.on('error', (err) => {
|
|
287
|
-
if (!completed) {
|
|
288
|
-
completed = true;
|
|
289
|
-
clearTimeout(timer);
|
|
290
|
-
file.close();
|
|
291
|
-
try { fs.unlinkSync(destPath); } catch (e) {}
|
|
292
|
-
reject(err);
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
request.on('error', (err) => {
|
|
298
|
-
if (!completed) {
|
|
299
|
-
completed = true;
|
|
300
|
-
clearTimeout(timer);
|
|
301
|
-
file.close();
|
|
302
|
-
try { fs.unlinkSync(destPath); } catch (e) {}
|
|
303
|
-
reject(new Error(`Network error: ${err.message} (${(downloadedSize / (1024 * 1024)).toFixed(1)} MB downloaded)`));
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
request.on('timeout', () => {
|
|
308
|
-
request.destroy();
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
doRequest(url);
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
async function getSdkDownloadInfo(platformTag) {
|
|
317
|
-
// 尝试从多个镜像获取版本信息和下载URL
|
|
318
|
-
const mirrors = [
|
|
319
|
-
'https://mirrors.cloud.tencent.com/pypi/pypi/codebuddy-agent-sdk/json',
|
|
320
|
-
'https://mirrors.aliyun.com/pypi/pypi/codebuddy-agent-sdk/json',
|
|
321
|
-
'https://pypi.tuna.tsinghua.edu.cn/pypi/pypi/codebuddy-agent-sdk/json',
|
|
322
|
-
'https://pypi.org/pypi/codebuddy-agent-sdk/json'
|
|
323
|
-
];
|
|
324
|
-
|
|
325
|
-
for (const url of mirrors) {
|
|
326
|
-
try {
|
|
327
|
-
const result = await new Promise((resolve, reject) => {
|
|
328
|
-
const protocol = url.startsWith('https') ? https : http;
|
|
329
|
-
protocol.get(url, {
|
|
330
|
-
headers: { 'User-Agent': 'sciagent-cli/1.0' },
|
|
331
|
-
timeout: 15000
|
|
332
|
-
}, (response) => {
|
|
333
|
-
// Handle redirects
|
|
334
|
-
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
335
|
-
protocol.get(response.headers.location, {
|
|
336
|
-
headers: { 'User-Agent': 'sciagent-cli/1.0' },
|
|
337
|
-
timeout: 15000
|
|
338
|
-
}, (res2) => {
|
|
339
|
-
let data = '';
|
|
340
|
-
res2.on('data', (chunk) => { data += chunk; });
|
|
341
|
-
res2.on('end', () => {
|
|
342
|
-
try {
|
|
343
|
-
const json = JSON.parse(data);
|
|
344
|
-
resolve(json);
|
|
345
|
-
} catch (e) {
|
|
346
|
-
reject(new Error('Parse error'));
|
|
347
|
-
}
|
|
348
|
-
});
|
|
349
|
-
}).on('error', reject);
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
let data = '';
|
|
354
|
-
response.on('data', (chunk) => { data += chunk; });
|
|
355
|
-
response.on('end', () => {
|
|
356
|
-
try {
|
|
357
|
-
const json = JSON.parse(data);
|
|
358
|
-
resolve(json);
|
|
359
|
-
} catch (e) {
|
|
360
|
-
reject(new Error('Parse error'));
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
}).on('error', reject);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
if (result && result.info && result.urls) {
|
|
367
|
-
const version = result.info.version;
|
|
368
|
-
// 查找匹配平台的wheel文件
|
|
369
|
-
const wheelUrl = result.urls.find(u =>
|
|
370
|
-
u.filename && u.filename.includes(platformTag) && u.filename.endsWith('.whl')
|
|
371
|
-
);
|
|
372
|
-
|
|
373
|
-
if (wheelUrl) {
|
|
374
|
-
console.log(` 从 ${url.split('/')[2]} 获取到版本信息`);
|
|
375
|
-
return { version, url: wheelUrl.url, size: wheelUrl.size };
|
|
376
|
-
} else {
|
|
377
|
-
console.log(` ${url.split('/')[2]}: 未找到 ${platformTag} 平台的wheel文件`);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
} catch (e) {
|
|
381
|
-
console.log(` ${url.split('/')[2]}: ${e.message}`);
|
|
382
|
-
// 继续尝试下一个镜像
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
throw new Error('无法获取SDK下载信息,请检查网络连接');
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
async function installCodebuddySdk() {
|
|
390
|
-
const existing = checkCodebuddyBinaryInstalled();
|
|
391
|
-
if (existing.installed) {
|
|
392
|
-
console.log(`✅ CodeBuddy SDK 已安装: ${existing.path}`);
|
|
393
|
-
return true;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const platform = process.platform;
|
|
397
|
-
const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
|
|
398
|
-
const platformKey = `${platform}-${arch}`;
|
|
399
|
-
const platformTag = PYPI_PLATFORM_MAP[platformKey];
|
|
400
|
-
|
|
401
|
-
console.log('');
|
|
402
|
-
console.log('📦 正在安装 CodeBuddy SDK...');
|
|
403
|
-
|
|
404
|
-
// 策略1: 从 PyPI 下载 wheel(Linux/macOS 有wheel,Windows 没有)
|
|
405
|
-
if (platformTag) {
|
|
406
|
-
try {
|
|
407
|
-
console.log(' 正在获取版本信息...');
|
|
408
|
-
const sdkInfo = await getSdkDownloadInfo(platformTag);
|
|
409
|
-
console.log(` 版本: ${sdkInfo.version}`);
|
|
410
|
-
console.log(` 平台: ${platformTag}`);
|
|
411
|
-
console.log(` 文件大小: ${(sdkInfo.size / (1024 * 1024)).toFixed(1)} MB`);
|
|
412
|
-
|
|
413
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sciagent-sdk-'));
|
|
414
|
-
const wheelFilename = `codebuddy_agent_sdk-${sdkInfo.version}-py3-none-${platformTag}.whl`;
|
|
415
|
-
const wheelPath = path.join(tmpDir, wheelFilename);
|
|
416
|
-
|
|
417
|
-
const binaryName = BINARY_NAMES[platform] || 'codebuddy-headless';
|
|
418
|
-
const binDir = getCodebuddyBinDir();
|
|
419
|
-
const targetPath = path.join(binDir, binaryName);
|
|
420
|
-
|
|
421
|
-
console.log(' 正在下载...');
|
|
422
|
-
await downloadFile(sdkInfo.url, wheelPath);
|
|
423
|
-
console.log(' ✅ 下载成功');
|
|
424
|
-
|
|
425
|
-
console.log(' 正在提取二进制文件...');
|
|
426
|
-
fs.mkdirSync(binDir, { recursive: true });
|
|
427
|
-
|
|
428
|
-
const extractScript = `
|
|
429
|
-
import zipfile, sys, os
|
|
430
|
-
wheel_path = sys.argv[1]
|
|
431
|
-
target_dir = sys.argv[2]
|
|
432
|
-
binary_name = sys.argv[3]
|
|
433
|
-
|
|
434
|
-
with zipfile.ZipFile(wheel_path, 'r') as zf:
|
|
435
|
-
for name in zf.namelist():
|
|
436
|
-
if binary_name in name and '/bin/' in name:
|
|
437
|
-
with zf.open(name) as src:
|
|
438
|
-
target_path = os.path.join(target_dir, binary_name)
|
|
439
|
-
with open(target_path, 'wb') as dst:
|
|
440
|
-
dst.write(src.read())
|
|
441
|
-
if sys.platform != 'win32':
|
|
442
|
-
os.chmod(target_path, 0o755)
|
|
443
|
-
print(f'Extracted: {target_path}')
|
|
444
|
-
sys.exit(0)
|
|
445
|
-
print(f'Error: {binary_name} not found in wheel')
|
|
446
|
-
sys.exit(1)
|
|
447
|
-
`;
|
|
448
|
-
|
|
449
|
-
const scriptPath = path.join(tmpDir, 'extract.py');
|
|
450
|
-
fs.writeFileSync(scriptPath, extractScript);
|
|
451
|
-
|
|
452
|
-
try {
|
|
453
|
-
execSync(`python3 "${scriptPath}" "${wheelPath}" "${binDir}" "${binaryName}"`, {
|
|
454
|
-
stdio: 'inherit',
|
|
455
|
-
timeout: 120000
|
|
456
|
-
});
|
|
457
|
-
} catch (e) {
|
|
458
|
-
execSync(`python "${scriptPath}" "${wheelPath}" "${binDir}" "${binaryName}"`, {
|
|
459
|
-
stdio: 'inherit',
|
|
460
|
-
timeout: 120000
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (fs.existsSync(targetPath)) {
|
|
465
|
-
const stats = fs.statSync(targetPath);
|
|
466
|
-
console.log(`✅ CodeBuddy SDK 安装成功!(PyPI wheel)`);
|
|
467
|
-
console.log(` 路径: ${targetPath}`);
|
|
468
|
-
console.log(` 大小: ${(stats.size / (1024 * 1024)).toFixed(1)} MB`);
|
|
469
|
-
|
|
470
|
-
try { fs.unlinkSync(wheelPath); fs.unlinkSync(scriptPath); fs.rmdirSync(tmpDir); } catch (e) {}
|
|
471
|
-
return true;
|
|
472
|
-
}
|
|
473
|
-
} catch (e) {
|
|
474
|
-
console.log(` ⚠️ PyPI 下载失败: ${e.message}`);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// 策略2: 从 npm 安装 @tencent-ai/codebuddy-code(全平台支持,国内可访问)
|
|
479
|
-
console.log(' 尝试从 npm 安装 @tencent-ai/codebuddy-code...');
|
|
480
|
-
try {
|
|
481
|
-
execSync('npm install -g @tencent-ai/codebuddy-code', {
|
|
482
|
-
stdio: 'inherit',
|
|
483
|
-
timeout: 300000
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
// 查找 npm 安装的 codebuddy-headless.js
|
|
487
|
-
const npmRootResult = execSync('npm root -g', { encoding: 'utf8', timeout: 10000 }).trim();
|
|
488
|
-
const headlessJsPath = path.join(npmRootResult, '@tencent-ai', 'codebuddy-code', 'dist', 'codebuddy-headless.js');
|
|
489
|
-
|
|
490
|
-
if (fs.existsSync(headlessJsPath)) {
|
|
491
|
-
// 创建 wrapper 脚本,让 codebuddy-headless 可以被直接调用
|
|
492
|
-
const binDir = getCodebuddyBinDir();
|
|
493
|
-
fs.mkdirSync(binDir, { recursive: true });
|
|
494
|
-
const binaryName = BINARY_NAMES[platform] || 'codebuddy-headless';
|
|
495
|
-
const wrapperPath = path.join(binDir, binaryName);
|
|
496
|
-
|
|
497
|
-
if (platform === 'win32') {
|
|
498
|
-
// Windows: 创建 .cmd wrapper
|
|
499
|
-
const cmdPath = wrapperPath.replace(/\.(exe)?$/, '.cmd');
|
|
500
|
-
fs.writeFileSync(cmdPath, `@echo off\r\nnode "${headlessJsPath}" %*\r\n`);
|
|
501
|
-
// 也创建 .exe placeholder(实际用 .cmd)
|
|
502
|
-
fs.writeFileSync(wrapperPath, `#!/usr/bin/env node\r\nrequire("${headlessJsPath}");`);
|
|
503
|
-
} else {
|
|
504
|
-
fs.writeFileSync(wrapperPath, `#!/usr/bin/env node\nrequire("${headlessJsPath}");`);
|
|
505
|
-
fs.chmodSync(wrapperPath, 0o755);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
console.log(`✅ CodeBuddy SDK 安装成功!(npm @tencent-ai/codebuddy-code)`);
|
|
509
|
-
console.log(` 路径: ${wrapperPath}`);
|
|
510
|
-
console.log(` headless.js: ${headlessJsPath}`);
|
|
511
|
-
return true;
|
|
512
|
-
} else {
|
|
513
|
-
console.log(` ⚠️ npm 安装成功但未找到 codebuddy-headless.js`);
|
|
514
|
-
}
|
|
515
|
-
} catch (e) {
|
|
516
|
-
console.log(` ⚠️ npm 安装失败: ${e.message}`);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// 策略3: 从 releases 服务器下载
|
|
520
|
-
console.log(' 尝试从 Releases 服务器下载 CodeBuddy SDK...');
|
|
521
|
-
try {
|
|
522
|
-
const binDir = getCodebuddyBinDir();
|
|
523
|
-
fs.mkdirSync(binDir, { recursive: true });
|
|
524
|
-
const binaryName = BINARY_NAMES[platform] || 'codebuddy-headless';
|
|
525
|
-
const targetPath = path.join(binDir, binaryName);
|
|
526
|
-
const downloadUrl = `${SELF_HOSTED_URL}/api/releases/download/codebuddy/${platform}/${arch}/latest`;
|
|
527
|
-
|
|
528
|
-
await downloadFile(downloadUrl, targetPath, 600000);
|
|
529
|
-
|
|
530
|
-
if (fs.existsSync(targetPath)) {
|
|
531
|
-
const stats = fs.statSync(targetPath);
|
|
532
|
-
if (stats.size > 10 * 1024 * 1024) {
|
|
533
|
-
if (platform !== 'win32') {
|
|
534
|
-
fs.chmodSync(targetPath, 0o755);
|
|
535
|
-
}
|
|
536
|
-
console.log(`✅ CodeBuddy SDK 安装成功!(Releases 服务器)`);
|
|
537
|
-
console.log(` 路径: ${targetPath}`);
|
|
538
|
-
return true;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
} catch (e) {
|
|
542
|
-
console.log(` ⚠️ Releases 服务器下载失败: ${e.message}`);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
console.log('⚠️ CodeBuddy SDK 安装失败,CLI 仍可启动(AI 对话功能将在 SDK 安装后可用)');
|
|
546
|
-
console.log(' 手动安装: npm install -g @tencent-ai/codebuddy-code');
|
|
547
|
-
return false;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
/**
|
|
551
|
-
* 检查 sciagent 进程是否正在运行
|
|
552
|
-
*/
|
|
553
|
-
function isSciAgentRunning() {
|
|
554
|
-
try {
|
|
555
|
-
if (process.platform === 'win32') {
|
|
556
|
-
const result = execSync('tasklist /FI "IMAGENAME eq sciagent.exe" /NH', {
|
|
557
|
-
encoding: 'utf8',
|
|
558
|
-
timeout: 5000
|
|
559
|
-
});
|
|
560
|
-
return result.includes('sciagent.exe');
|
|
561
|
-
} else {
|
|
562
|
-
const result = execSync('pgrep -x sciagent || true', {
|
|
563
|
-
encoding: 'utf8',
|
|
564
|
-
timeout: 5000
|
|
565
|
-
});
|
|
566
|
-
return result.trim().length > 0;
|
|
567
|
-
}
|
|
568
|
-
} catch (e) {
|
|
569
|
-
return false;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* 终止 sciagent 进程
|
|
575
|
-
*/
|
|
576
|
-
function killSciAgent() {
|
|
577
|
-
try {
|
|
578
|
-
if (process.platform === 'win32') {
|
|
579
|
-
execSync('taskkill /F /IM sciagent.exe', { timeout: 10000 });
|
|
580
|
-
} else {
|
|
581
|
-
execSync('pkill -x sciagent', { timeout: 10000 });
|
|
582
|
-
}
|
|
583
|
-
// 等待进程完全退出
|
|
584
|
-
let retries = 10;
|
|
585
|
-
while (retries-- > 0 && isSciAgentRunning()) {
|
|
586
|
-
const sleep = require('util').promisify(setTimeout);
|
|
587
|
-
sleep(500);
|
|
588
|
-
}
|
|
589
|
-
return !isSciAgentRunning();
|
|
590
|
-
} catch (e) {
|
|
591
|
-
return false;
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
/**
|
|
596
|
-
* 尝试将临时文件替换为目标二进制文件
|
|
597
|
-
* 返回: 'replaced' | 'locked' | 'error'
|
|
598
|
-
*/
|
|
599
|
-
function tryReplaceBinary(tempPath, targetPath, installDir, version, platform) {
|
|
600
|
-
try {
|
|
601
|
-
// 先尝试直接复制
|
|
602
|
-
if (process.platform === 'win32') {
|
|
603
|
-
// Windows: 使用 PowerShell Copy-Item
|
|
604
|
-
const tmpScript = path.join(os.tmpdir(), 'sciagent-replace.ps1');
|
|
605
|
-
const scriptContent = [
|
|
606
|
-
`Copy-Item -Path "${tempPath}" -Destination "${targetPath}" -Force`,
|
|
607
|
-
'if (Test-Path $out) { Write-Host "OK" } else { Write-Error "Copy failed"; exit 1 }'
|
|
608
|
-
].join('\r\n');
|
|
609
|
-
fs.writeFileSync(tmpScript, scriptContent, 'utf8');
|
|
610
|
-
try {
|
|
611
|
-
execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${tmpScript}"`, {
|
|
612
|
-
stdio: 'pipe',
|
|
613
|
-
timeout: 30000
|
|
614
|
-
});
|
|
615
|
-
} finally {
|
|
616
|
-
try { fs.unlinkSync(tmpScript); } catch (e) {}
|
|
617
|
-
}
|
|
618
|
-
} else {
|
|
619
|
-
fs.copyFileSync(tempPath, targetPath);
|
|
620
|
-
fs.chmodSync(targetPath, 0o755);
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// 验证替换成功
|
|
624
|
-
if (fs.existsSync(targetPath)) {
|
|
625
|
-
const stats = fs.statSync(targetPath);
|
|
626
|
-
if (stats.size > 10 * 1024 * 1024) {
|
|
627
|
-
fs.writeFileSync(path.join(installDir, '.version'), version);
|
|
628
|
-
// 清理临时文件
|
|
629
|
-
try { fs.unlinkSync(tempPath); } catch (e) {}
|
|
630
|
-
return 'replaced';
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
return 'error';
|
|
634
|
-
} catch (e) {
|
|
635
|
-
// 检查是否是文件锁定错误
|
|
636
|
-
const msg = (e.message || '').toLowerCase();
|
|
637
|
-
if (msg.includes('used by another process') || msg.includes('being used') ||
|
|
638
|
-
msg.includes('eperm') || msg.includes('eacces') || msg.includes('access denied') ||
|
|
639
|
-
msg.includes('锁定') || msg.includes('denied')) {
|
|
640
|
-
return 'locked';
|
|
641
|
-
}
|
|
642
|
-
return 'error';
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
/**
|
|
647
|
-
* 写入 .pending-update 标记文件
|
|
648
|
-
* 下次 sciagent 启动时会自动应用更新
|
|
649
|
-
*/
|
|
650
|
-
function writePendingUpdate(installDir, tempPath, version) {
|
|
651
|
-
const pendingFile = path.join(installDir, '.pending-update');
|
|
652
|
-
const pendingData = {
|
|
653
|
-
version: version,
|
|
654
|
-
tempPath: tempPath,
|
|
655
|
-
timestamp: new Date().toISOString(),
|
|
656
|
-
platform: process.platform
|
|
657
|
-
};
|
|
658
|
-
fs.writeFileSync(pendingFile, JSON.stringify(pendingData, null, 2), 'utf8');
|
|
659
|
-
console.log(` [PENDING] Update deferred. Written .pending-update marker.`);
|
|
660
|
-
console.log(` [PENDING] New version will be applied on next sciagent startup.`);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
/**
|
|
664
|
-
* 交互式提示用户选择更新方式
|
|
665
|
-
* 返回: 'kill' | 'defer'
|
|
666
|
-
*/
|
|
667
|
-
function promptUpdateChoice() {
|
|
668
|
-
// 检查是否在交互式终端中
|
|
669
|
-
if (!process.stdin.isTTY) {
|
|
670
|
-
console.log(` [INFO] Non-interactive mode. Deferring update to next startup.`);
|
|
671
|
-
return 'defer';
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const readline = require('readline');
|
|
675
|
-
const rl = readline.createInterface({
|
|
676
|
-
input: process.stdin,
|
|
677
|
-
output: process.stdout
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
return new Promise((resolve) => {
|
|
681
|
-
console.log('');
|
|
682
|
-
console.log(' ╔══════════════════════════════════════════════════════════╗');
|
|
683
|
-
console.log(' ║ SciAgent is currently running. Update requires ║');
|
|
684
|
-
console.log(' ║ replacing the binary file which is locked. ║');
|
|
685
|
-
console.log(' ╠══════════════════════════════════════════════════════════╣');
|
|
686
|
-
console.log(' ║ [K] Kill SciAgent & update now ║');
|
|
687
|
-
console.log(' ║ [D] Defer - update on next startup ║');
|
|
688
|
-
console.log(' ╚══════════════════════════════════════════════════════════╝');
|
|
689
|
-
console.log('');
|
|
690
|
-
|
|
691
|
-
rl.question(' Choose [K/D] (default: D): ', (answer) => {
|
|
692
|
-
rl.close();
|
|
693
|
-
const choice = (answer || 'D').trim().toUpperCase();
|
|
694
|
-
if (choice === 'K' || choice === 'KILL') {
|
|
695
|
-
resolve('kill');
|
|
696
|
-
} else {
|
|
697
|
-
resolve('defer');
|
|
698
|
-
}
|
|
699
|
-
});
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
async function downloadFromServer(platform, arch, version) {
|
|
704
|
-
const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
|
|
705
|
-
const ext = platform === 'win32' ? '.exe' : '';
|
|
706
|
-
const filename = `sciagent-${platform}-${arch}${ext}`;
|
|
707
|
-
|
|
708
|
-
// 确定安装目录
|
|
709
|
-
const installDir = getHomeBinDir();
|
|
710
|
-
fs.mkdirSync(installDir, { recursive: true });
|
|
711
|
-
const targetPath = path.join(installDir, binName);
|
|
712
|
-
|
|
713
|
-
// 下载到临时目录(避免直接覆盖正在运行的二进制)
|
|
714
|
-
const tempDir = path.join(os.tmpdir(), 'sciagent-update');
|
|
715
|
-
fs.mkdirSync(tempDir, { recursive: true });
|
|
716
|
-
const tempPath = path.join(tempDir, binName);
|
|
717
|
-
|
|
718
|
-
// 清理旧的临时文件
|
|
719
|
-
try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e) {}
|
|
720
|
-
|
|
721
|
-
// 构建下载 URL 列表(按优先级)
|
|
722
|
-
const downloadUrls = DOWNLOAD_MIRRORS.map(mirror => ({
|
|
723
|
-
url: mirror.getUrl(platform, arch, version, filename),
|
|
724
|
-
name: mirror.name,
|
|
725
|
-
auth: mirror.auth
|
|
726
|
-
}));
|
|
727
|
-
|
|
728
|
-
for (const source of downloadUrls) {
|
|
729
|
-
console.log(`\n [${source.name}] Downloading ${filename} v${version}...`);
|
|
730
|
-
|
|
731
|
-
// Windows: 优先使用 PowerShell(更可靠的大文件下载)
|
|
732
|
-
if (platform === 'win32') {
|
|
733
|
-
try {
|
|
734
|
-
const tmpScript = path.join(os.tmpdir(), 'sciagent-download.ps1');
|
|
735
|
-
let scriptLines = [
|
|
736
|
-
'$ProgressPreference = "SilentlyContinue"',
|
|
737
|
-
`$uri = "${source.url}"`,
|
|
738
|
-
`$out = "${tempPath}"`,
|
|
739
|
-
'Write-Host " Downloading from ' + source.name + '..."',
|
|
740
|
-
];
|
|
741
|
-
|
|
742
|
-
if (source.auth === 'jihulab') {
|
|
743
|
-
const b64 = Buffer.from(`${JIHULAB_DEPLOY_TOKEN_USER}:${JIHULAB_DEPLOY_TOKEN}`).toString('base64');
|
|
744
|
-
scriptLines.push(`$headers = @{ "Authorization" = "Basic ${b64}" }`);
|
|
745
|
-
// Use WebClient for large files - much faster than Invoke-WebRequest
|
|
746
|
-
scriptLines.push('$wc = New-Object System.Net.WebClient');
|
|
747
|
-
scriptLines.push('$wc.Headers.Add("Authorization", $headers["Authorization"])');
|
|
748
|
-
scriptLines.push('Write-Host " Using JihuLab CDN with auth..."');
|
|
749
|
-
scriptLines.push('$wc.DownloadFile($uri, $out)');
|
|
750
|
-
} else {
|
|
751
|
-
scriptLines.push('$wc = New-Object System.Net.WebClient');
|
|
752
|
-
scriptLines.push('Write-Host " Using ' + source.name + '..."');
|
|
753
|
-
scriptLines.push('$wc.DownloadFile($uri, $out)');
|
|
754
|
-
}
|
|
755
|
-
scriptLines.push('if (Test-Path $out) { $s = (Get-Item $out).Length; Write-Host " Downloaded: $s bytes ($([math]::Round($s/1MB,1)) MB)" } else { Write-Error "File not created"; exit 1 }');
|
|
756
|
-
|
|
757
|
-
fs.writeFileSync(tmpScript, scriptLines.join('\r\n'), 'utf8');
|
|
758
|
-
|
|
759
|
-
try {
|
|
760
|
-
execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${tmpScript}"`, {
|
|
761
|
-
stdio: 'inherit',
|
|
762
|
-
timeout: 600000
|
|
763
|
-
});
|
|
764
|
-
} finally {
|
|
765
|
-
try { fs.unlinkSync(tmpScript); } catch (e) {}
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
// 验证下载到临时文件
|
|
769
|
-
if (fs.existsSync(tempPath)) {
|
|
770
|
-
const stats = fs.statSync(tempPath);
|
|
771
|
-
if (stats.size > 10 * 1024 * 1024) {
|
|
772
|
-
console.log(` [OK] Downloaded from ${source.name}: ${tempPath} (${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
|
|
773
|
-
// 尝试替换
|
|
774
|
-
return await applyDownloadedBinary(tempPath, targetPath, installDir, version, platform);
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
console.log(` [WARN] ${source.name} download validation failed, trying next...`);
|
|
779
|
-
} catch (e) {
|
|
780
|
-
console.log(` [WARN] ${source.name} download failed: ${e.message}, trying next...`);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// 通用方式: Node.js https 下载到临时文件
|
|
785
|
-
try {
|
|
786
|
-
// 如果是 JihuLab,需要添加认证头
|
|
787
|
-
const headers = source.auth === 'jihulab'
|
|
788
|
-
? { 'Authorization': `Basic ${Buffer.from(`${JIHULAB_DEPLOY_TOKEN_USER}:${JIHULAB_DEPLOY_TOKEN}`).toString('base64')}` }
|
|
789
|
-
: {};
|
|
790
|
-
await downloadFile(source.url, tempPath, 600000, headers);
|
|
791
|
-
|
|
792
|
-
// 验证下载
|
|
793
|
-
if (fs.existsSync(tempPath)) {
|
|
794
|
-
const stats = fs.statSync(tempPath);
|
|
795
|
-
if (stats.size > 10 * 1024 * 1024) {
|
|
796
|
-
console.log(` [OK] Downloaded from ${source.name}: ${tempPath} (${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
|
|
797
|
-
if (platform !== 'win32') {
|
|
798
|
-
fs.chmodSync(tempPath, 0o755);
|
|
799
|
-
}
|
|
800
|
-
// 尝试替换
|
|
801
|
-
return await applyDownloadedBinary(tempPath, targetPath, installDir, version, platform);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
console.log(` [WARN] ${source.name} download validation failed, trying next...`);
|
|
806
|
-
} catch (e) {
|
|
807
|
-
console.log(` [WARN] ${source.name} download failed: ${e.message}, trying next...`);
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
console.log(` [WARN] All download mirrors failed.`);
|
|
812
|
-
return false;
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
/**
|
|
816
|
-
* 将已下载到临时路径的二进制文件应用到目标位置
|
|
817
|
-
* 处理文件锁定情况:交互式选择杀死进程或延迟更新
|
|
818
|
-
*/
|
|
819
|
-
async function applyDownloadedBinary(tempPath, targetPath, installDir, version, platform) {
|
|
820
|
-
// 如果目标文件不存在(首次安装),直接移动
|
|
821
|
-
if (!fs.existsSync(targetPath)) {
|
|
822
|
-
try {
|
|
823
|
-
if (process.platform === 'win32') {
|
|
824
|
-
// Windows: 使用 PowerShell 移动
|
|
825
|
-
const tmpScript = path.join(os.tmpdir(), 'sciagent-move.ps1');
|
|
826
|
-
const scriptContent = `Move-Item -Path "${tempPath}" -Destination "${targetPath}" -Force`;
|
|
827
|
-
fs.writeFileSync(tmpScript, scriptContent, 'utf8');
|
|
828
|
-
try {
|
|
829
|
-
execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${tmpScript}"`, {
|
|
830
|
-
stdio: 'pipe', timeout: 30000
|
|
831
|
-
});
|
|
832
|
-
} finally {
|
|
833
|
-
try { fs.unlinkSync(tmpScript); } catch (e) {}
|
|
834
|
-
}
|
|
835
|
-
} else {
|
|
836
|
-
fs.renameSync(tempPath, targetPath);
|
|
837
|
-
fs.chmodSync(targetPath, 0o755);
|
|
838
|
-
}
|
|
839
|
-
fs.writeFileSync(path.join(installDir, '.version'), version);
|
|
840
|
-
console.log(` [OK] Installed: ${targetPath}`);
|
|
841
|
-
return true;
|
|
842
|
-
} catch (e) {
|
|
843
|
-
console.log(` [WARN] Move failed: ${e.message}, trying copy...`);
|
|
844
|
-
// fallback to copy
|
|
845
|
-
const result = tryReplaceBinary(tempPath, targetPath, installDir, version, platform);
|
|
846
|
-
return result === 'replaced';
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
// 目标文件已存在,尝试替换
|
|
851
|
-
const replaceResult = tryReplaceBinary(tempPath, targetPath, installDir, version, platform);
|
|
852
|
-
|
|
853
|
-
if (replaceResult === 'replaced') {
|
|
854
|
-
console.log(` [OK] Updated: ${targetPath} (v${version})`);
|
|
855
|
-
return true;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
if (replaceResult === 'locked') {
|
|
859
|
-
console.log(` [WARN] Binary file is locked (SciAgent is running).`);
|
|
860
|
-
|
|
861
|
-
// 交互式选择
|
|
862
|
-
const choice = await promptUpdateChoice();
|
|
863
|
-
|
|
864
|
-
if (choice === 'kill') {
|
|
865
|
-
console.log(` [INFO] Killing SciAgent process...`);
|
|
866
|
-
const killed = killSciAgent();
|
|
867
|
-
if (killed) {
|
|
868
|
-
console.log(` [OK] SciAgent process terminated.`);
|
|
869
|
-
// 重试替换
|
|
870
|
-
const retryResult = tryReplaceBinary(tempPath, targetPath, installDir, version, platform);
|
|
871
|
-
if (retryResult === 'replaced') {
|
|
872
|
-
console.log(` [OK] Updated: ${targetPath} (v${version})`);
|
|
873
|
-
return true;
|
|
874
|
-
} else {
|
|
875
|
-
console.log(` [WARN] Replace still failed after killing process. Deferring to next startup.`);
|
|
876
|
-
writePendingUpdate(installDir, tempPath, version);
|
|
877
|
-
return true; // 下载成功,只是替换延迟
|
|
878
|
-
}
|
|
879
|
-
} else {
|
|
880
|
-
console.log(` [WARN] Failed to kill SciAgent. Deferring update to next startup.`);
|
|
881
|
-
writePendingUpdate(installDir, tempPath, version);
|
|
882
|
-
return true; // 下载成功,只是替换延迟
|
|
883
|
-
}
|
|
884
|
-
} else {
|
|
885
|
-
// 用户选择延迟
|
|
886
|
-
writePendingUpdate(installDir, tempPath, version);
|
|
887
|
-
console.log(` [INFO] Update will be applied automatically on next startup.`);
|
|
888
|
-
return true; // 下载成功,只是替换延迟
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
// 其他错误
|
|
893
|
-
console.log(` [WARN] Replace failed with unexpected error. Deferring to next startup.`);
|
|
894
|
-
writePendingUpdate(installDir, tempPath, version);
|
|
895
|
-
return true; // 下载成功,只是替换延迟
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
// installBinaryPackage 已移除
|
|
899
|
-
// 原因:npm optionalDependencies 缓存旧包导致版本错乱
|
|
900
|
-
// 所有二进制统一从 sciagent.tech 服务器下载
|
|
901
|
-
|
|
902
|
-
async function main() {
|
|
903
|
-
console.log('[postinstall] Starting postinstall script...');
|
|
904
|
-
const platform = PLATFORM_MAP[process.platform];
|
|
905
|
-
const arch = ARCH_MAP[process.arch];
|
|
906
|
-
|
|
907
|
-
console.log('');
|
|
908
|
-
console.log('╔══════════════════════════════════════════════════════════╗');
|
|
909
|
-
console.log('║ SciAgent CLI - Post Install Setup ║');
|
|
910
|
-
console.log('╚══════════════════════════════════════════════════════════╝');
|
|
911
|
-
console.log('');
|
|
912
|
-
console.log(` Platform: ${platform || process.platform}`);
|
|
913
|
-
console.log(` Architecture: ${arch || process.arch}`);
|
|
914
|
-
console.log(` Node.js: ${process.version}`);
|
|
915
|
-
console.log('');
|
|
916
|
-
|
|
917
|
-
if (!platform || !arch) {
|
|
918
|
-
console.error('❌ Unsupported platform or architecture');
|
|
919
|
-
console.error(` Platform: ${process.platform}`);
|
|
920
|
-
console.error(` Architecture: ${process.arch}`);
|
|
921
|
-
console.error('');
|
|
922
|
-
console.error(' Supported platforms: linux, darwin, win32');
|
|
923
|
-
console.error(' Supported architectures: x64, arm64');
|
|
924
|
-
process.exit(1);
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
// 检查 SciAgent CLI 二进制
|
|
928
|
-
const packageName = `@sciagent/cli-${platform}-${arch}`;
|
|
929
|
-
const result = checkBinaryInstalled(platform, arch);
|
|
930
|
-
|
|
931
|
-
if (result.installed) {
|
|
932
|
-
console.log(`✅ Platform binary already installed: ${packageName}`);
|
|
933
|
-
console.log(` Path: ${result.path}`);
|
|
934
|
-
} else {
|
|
935
|
-
console.log(`⚠️ Platform binary not found: ${packageName}`);
|
|
936
|
-
|
|
937
|
-
// 下载策略: 从 sciagent.tech 服务器下载
|
|
938
|
-
console.log('');
|
|
939
|
-
console.log(' 尝试从服务器下载...');
|
|
940
|
-
let success = await downloadFromServer(platform, arch, CURRENT_VERSION);
|
|
941
|
-
|
|
942
|
-
// 如果当前版本失败,尝试回退版本
|
|
943
|
-
if (!success) {
|
|
944
|
-
for (const fallbackVersion of GITHUB_FALLBACK_VERSIONS) {
|
|
945
|
-
if (fallbackVersion === CURRENT_VERSION) continue;
|
|
946
|
-
console.log(` 尝试回退版本 v${fallbackVersion}...`);
|
|
947
|
-
success = await downloadFromServer(platform, arch, fallbackVersion);
|
|
948
|
-
if (success) break;
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
if (!success) {
|
|
953
|
-
console.error('');
|
|
954
|
-
console.error('╔══════════════════════════════════════════════════════════╗');
|
|
955
|
-
console.error('║ Manual Installation Required ║');
|
|
956
|
-
console.error('╚══════════════════════════════════════════════════════════╝');
|
|
957
|
-
console.error('');
|
|
958
|
-
console.error(' All download sources failed. Please try again later or:');
|
|
959
|
-
console.error(` 1. Check your network connection`);
|
|
960
|
-
console.error(` 2. Visit: https://sciagent.tech`);
|
|
961
|
-
console.error('');
|
|
962
|
-
process.exit(1);
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
const verifyResult = checkBinaryInstalled(platform, arch);
|
|
966
|
-
if (verifyResult.installed) {
|
|
967
|
-
console.log(`\n✅ Platform binary installed successfully`);
|
|
968
|
-
} else {
|
|
969
|
-
console.error(`\n❌ Installation verification failed. Please install manually:`);
|
|
970
|
-
console.error(` npm install -g ${packageName}@${CURRENT_VERSION}`);
|
|
971
|
-
process.exit(1);
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// Windows: 确保 npm 全局 bin 目录在 PATH 中
|
|
976
|
-
if (process.platform === 'win32') {
|
|
977
|
-
const npmBinDir = path.join(process.env.APPDATA || '', 'npm');
|
|
978
|
-
const userPath = (process.env.PATH || '').split(path.delimiter);
|
|
979
|
-
if (!userPath.some(p => p.toLowerCase() === npmBinDir.toLowerCase())) {
|
|
980
|
-
try {
|
|
981
|
-
const { execSync } = require('child_process');
|
|
982
|
-
// 使用 PowerShell 永久添加到用户 PATH(无 1024 字符限制)
|
|
983
|
-
execSync(
|
|
984
|
-
`powershell -Command "[Environment]::SetEnvironmentVariable('Path', [Environment]::GetEnvironmentVariable('Path','User') + ';${npmBinDir}', 'User')"`,
|
|
985
|
-
{ stdio: 'pipe', timeout: 15000 }
|
|
986
|
-
);
|
|
987
|
-
console.log(`✅ Added "${npmBinDir}" to user PATH (restart terminal to take effect)`);
|
|
988
|
-
} catch (e) {
|
|
989
|
-
console.log(`⚠️ Could not add "${npmBinDir}" to PATH automatically.`);
|
|
990
|
-
console.log(` Please run this command manually:`);
|
|
991
|
-
console.log(` powershell -Command "[Environment]::SetEnvironmentVariable('Path', [Environment]::GetEnvironmentVariable('Path','User') + ';${npmBinDir}', 'User')"`);
|
|
992
|
-
console.log(` Then restart your terminal.`);
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
// 安装 CodeBuddy SDK(使用国内镜像)
|
|
998
|
-
await installCodebuddySdk();
|
|
999
|
-
|
|
1000
|
-
console.log('');
|
|
1001
|
-
console.log('Usage:');
|
|
1002
|
-
console.log(' sciagent # Start with default ports');
|
|
1003
|
-
console.log(' sciagent --port 8080 # Custom proxy port');
|
|
1004
|
-
console.log(' sciagent --no-browser # Don\'t open browser');
|
|
1005
|
-
console.log(' sciagent install-sdk # Install CodeBuddy SDK manually');
|
|
1006
|
-
console.log(' sciagent --help # Show help');
|
|
1007
|
-
console.log('');
|
|
1008
|
-
console.log('Documentation: https://gitee.com/garva/research-agent');
|
|
1009
|
-
console.log('');
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
// 运行主函数
|
|
1013
|
-
main().catch(err => {
|
|
1014
|
-
console.error('Post install error:', err.message);
|
|
1015
|
-
process.exit(1);
|
|
1016
|
-
});
|
|
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.1.54';
|
|
31
|
+
|
|
32
|
+
// GitHub Releases 回退版本列表(当当前版本的 release 不存在时尝试)
|
|
33
|
+
const GITHUB_FALLBACK_VERSIONS = ['1.1.3', '1.0.50', '1.0.48', '1.0.46', '1.0.40', '1.0.38', '1.0.36'];
|
|
34
|
+
|
|
35
|
+
// JihuLab 通用包仓库配置(国内 CDN,速度快)
|
|
36
|
+
const JIHULAB_PROJECT_ID = '351778';
|
|
37
|
+
const JIHULAB_DEPLOY_TOKEN_USER = 'gitlab+deploy-token-15400';
|
|
38
|
+
const JIHULAB_DEPLOY_TOKEN = 'gldt-5kp7mgt4ztyyBMx1Uvn6';
|
|
39
|
+
const JIHULAB_BASE_URL = `https://jihulab.com/api/v4/projects/${JIHULAB_PROJECT_ID}/packages/generic/sciagent`;
|
|
40
|
+
|
|
41
|
+
// 自建服务器配置(备用源)
|
|
42
|
+
const SELF_HOSTED_URL = process.env.RELEASE_SERVER_URL || 'https://sciagent.tech';
|
|
43
|
+
|
|
44
|
+
// 下载源列表(按优先级排序)
|
|
45
|
+
const DOWNLOAD_MIRRORS = [
|
|
46
|
+
{ name: 'JihuLab CDN', getUrl: (platform, arch, version, filename) => `${JIHULAB_BASE_URL}/${version}/${filename}`, auth: 'jihulab' },
|
|
47
|
+
{ name: 'Self-hosted', getUrl: (platform, arch, version, filename) => `${SELF_HOSTED_URL}/api/releases/download/${platform}/${arch}/${version}`, auth: 'none' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// PyPI 镜像源列表(国内优先)
|
|
51
|
+
const PYPI_MIRRORS = [
|
|
52
|
+
'https://mirrors.cloud.tencent.com/pypi/simple', // 腾讯云
|
|
53
|
+
'https://mirrors.aliyun.com/pypi/simple', // 阿里云
|
|
54
|
+
'https://pypi.tuna.tsinghua.edu.cn/simple', // 清华
|
|
55
|
+
'https://pypi.org/simple' // 官方(备用)
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
// PyPI 下载URL的镜像(直接下载文件)
|
|
59
|
+
const PYPI_DOWNLOAD_MIRRORS = [
|
|
60
|
+
'https://mirrors.cloud.tencent.com/pypi/packages', // 腾讯云
|
|
61
|
+
'https://mirrors.aliyun.com/pypi/packages', // 阿里云
|
|
62
|
+
'https://pypi.tuna.tsinghua.edu.cn/packages', // 清华
|
|
63
|
+
'https://files.pythonhosted.org/packages' // 官方(备用)
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// PyPI wheel 平台标识映射
|
|
67
|
+
const PYPI_PLATFORM_MAP = {
|
|
68
|
+
'win32-x64': 'win_amd64',
|
|
69
|
+
'darwin-arm64': 'macosx_11_0_arm64',
|
|
70
|
+
'darwin-x64': 'macosx_10_12_x86_64',
|
|
71
|
+
'linux-x64': 'manylinux_2_17_x86_64',
|
|
72
|
+
'linux-arm64': 'manylinux_2_17_aarch64'
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// 二进制文件名
|
|
76
|
+
const BINARY_NAMES = {
|
|
77
|
+
win32: 'codebuddy-headless.exe',
|
|
78
|
+
darwin: 'codebuddy-headless',
|
|
79
|
+
linux: 'codebuddy-headless'
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function getCodebuddyBinDir() {
|
|
83
|
+
const platform = process.platform;
|
|
84
|
+
if (platform === 'win32') {
|
|
85
|
+
const base = process.env.LOCALAPPDATA || os.homedir();
|
|
86
|
+
return path.join(base, 'sciagent', 'bin');
|
|
87
|
+
} else {
|
|
88
|
+
return path.join(os.homedir(), '.sciagent', 'bin');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getHomeBinDir() {
|
|
93
|
+
return process.platform === 'win32'
|
|
94
|
+
? path.join(process.env.LOCALAPPDATA || os.homedir(), 'sciagent', 'bin')
|
|
95
|
+
: path.join(os.homedir(), '.sciagent', 'bin');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function checkBinaryInstalled(platform, arch) {
|
|
99
|
+
const packageName = `@sciagent/cli-${platform}-${arch}`;
|
|
100
|
+
const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
|
|
101
|
+
const homeBinDir = getHomeBinDir();
|
|
102
|
+
const homeBinPath = path.join(homeBinDir, binName);
|
|
103
|
+
|
|
104
|
+
// 检查 ~/.sciagent/bin/ 或 %LOCALAPPDATA%\sciagent\bin
|
|
105
|
+
if (fs.existsSync(homeBinPath)) {
|
|
106
|
+
const stats = fs.statSync(homeBinPath);
|
|
107
|
+
if (stats.size > 10 * 1024 * 1024) {
|
|
108
|
+
// 检查版本文件,版本不匹配则视为未安装(需要重新下载)
|
|
109
|
+
const versionFile = path.join(homeBinDir, '.version');
|
|
110
|
+
if (fs.existsSync(versionFile)) {
|
|
111
|
+
const installedVersion = fs.readFileSync(versionFile, 'utf8').trim();
|
|
112
|
+
if (installedVersion === CURRENT_VERSION) {
|
|
113
|
+
// 额外安全检查:验证二进制文件修改时间是否在版本发布日期之后
|
|
114
|
+
// 防止 .version 文件被错误写入(如手动编辑)导致使用旧二进制
|
|
115
|
+
const mtime = stats.mtime;
|
|
116
|
+
const now = new Date();
|
|
117
|
+
const ageDays = (now - mtime) / (1000 * 60 * 60 * 24);
|
|
118
|
+
console.log(` ℹ️ 已安装版本 ${installedVersion} (文件大小: ${(stats.size / (1024*1024)).toFixed(1)} MB, 修改时间: ${mtime.toISOString().slice(0,10)}, ${ageDays.toFixed(0)}天前)`);
|
|
119
|
+
return { installed: true, path: homeBinPath };
|
|
120
|
+
} else {
|
|
121
|
+
console.log(` ℹ️ 已安装版本 ${installedVersion} != 目标版本 ${CURRENT_VERSION},需要更新`);
|
|
122
|
+
// 版本不匹配,删除旧文件强制重新下载
|
|
123
|
+
const allRemoved = _removeBinaryFiles(homeBinDir, homeBinPath);
|
|
124
|
+
if (!allRemoved) {
|
|
125
|
+
// 删除失败(可能被锁定),标记需要更新但不阻塞
|
|
126
|
+
// downloadFromServer 会下载到临时目录并处理锁定情况
|
|
127
|
+
console.log(` ℹ️ 旧文件删除失败(可能被占用),将下载新版本到临时目录`);
|
|
128
|
+
}
|
|
129
|
+
return { installed: false };
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
console.log(` ℹ️ 二进制版本未知(无.version文件),需要重新下载`);
|
|
133
|
+
// 版本未知,删除文件强制重新下载
|
|
134
|
+
const allRemoved = _removeBinaryFiles(homeBinDir, homeBinPath);
|
|
135
|
+
if (!allRemoved) {
|
|
136
|
+
console.log(` ℹ️ 旧文件删除失败(可能被占用),将下载新版本到临时目录`);
|
|
137
|
+
}
|
|
138
|
+
return { installed: false };
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
// 文件太小,可能是损坏的下载
|
|
142
|
+
console.log(` ℹ️ 二进制文件太小 (${(stats.size / 1024).toFixed(0)} KB < 10 MB),可能是损坏文件`);
|
|
143
|
+
_removeBinaryFiles(homeBinDir, homeBinPath);
|
|
144
|
+
return { installed: false };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 不再从 npm optionalDependencies 查找二进制,原因:
|
|
149
|
+
// 1. npm 缓存中的旧包可能包含旧版二进制(如 1.0.40 的 194MB 旧 exe)
|
|
150
|
+
// 2. npm 可能修改 package.json 版本号来匹配请求,但二进制文件仍是旧的
|
|
151
|
+
// 3. 超过 250MB 的包无法发布到 npm,只有空壳包
|
|
152
|
+
// 所有二进制统一从 Release Server 下载,确保版本正确
|
|
153
|
+
|
|
154
|
+
return { installed: false };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function _removeBinaryFiles(binDir, binPath) {
|
|
158
|
+
// 删除二进制文件及相关缓存文件(.version等)
|
|
159
|
+
const filesToRemove = [binPath];
|
|
160
|
+
// 也删除 .version 文件,避免版本标记残留
|
|
161
|
+
const versionFile = path.join(binDir, '.version');
|
|
162
|
+
if (fs.existsSync(versionFile)) {
|
|
163
|
+
filesToRemove.push(versionFile);
|
|
164
|
+
}
|
|
165
|
+
// 也删除 .pending-update 文件
|
|
166
|
+
const pendingFile = path.join(binDir, '.pending-update');
|
|
167
|
+
if (fs.existsSync(pendingFile)) {
|
|
168
|
+
filesToRemove.push(pendingFile);
|
|
169
|
+
}
|
|
170
|
+
let allRemoved = true;
|
|
171
|
+
for (const f of filesToRemove) {
|
|
172
|
+
try {
|
|
173
|
+
fs.unlinkSync(f);
|
|
174
|
+
console.log(` ℹ️ 已删除: ${f}`);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
console.log(` ⚠️ 无法删除 ${f}: ${e.message}`);
|
|
177
|
+
allRemoved = false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return allRemoved;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function checkCodebuddyBinaryInstalled() {
|
|
184
|
+
const binDir = getCodebuddyBinDir();
|
|
185
|
+
const binaryName = BINARY_NAMES[process.platform] || 'codebuddy-headless';
|
|
186
|
+
const binaryPath = path.join(binDir, binaryName);
|
|
187
|
+
|
|
188
|
+
if (fs.existsSync(binaryPath)) {
|
|
189
|
+
const stats = fs.statSync(binaryPath);
|
|
190
|
+
// 检查文件大小是否合理(至少10MB,防止损坏的文件)
|
|
191
|
+
if (stats.size > 10 * 1024 * 1024) {
|
|
192
|
+
return { installed: true, path: binaryPath, size: stats.size };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { installed: false };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function downloadFile(url, destPath, timeout = 120000, extraHeaders = {}) {
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
const protocol = url.startsWith('https') ? https : http;
|
|
201
|
+
|
|
202
|
+
// 确保目标目录存在
|
|
203
|
+
const destDir = path.dirname(destPath);
|
|
204
|
+
if (!fs.existsSync(destDir)) {
|
|
205
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const file = fs.createWriteStream(destPath);
|
|
209
|
+
let completed = false;
|
|
210
|
+
let downloadedSize = 0;
|
|
211
|
+
|
|
212
|
+
const timer = setTimeout(() => {
|
|
213
|
+
if (!completed) {
|
|
214
|
+
completed = true;
|
|
215
|
+
file.close();
|
|
216
|
+
try { fs.unlinkSync(destPath); } catch (e) {}
|
|
217
|
+
reject(new Error(`Download timeout after ${timeout / 1000}s (${(downloadedSize / (1024 * 1024)).toFixed(1)} MB downloaded)`));
|
|
218
|
+
}
|
|
219
|
+
}, timeout);
|
|
220
|
+
|
|
221
|
+
function doRequest(requestUrl, redirectCount = 0) {
|
|
222
|
+
if (redirectCount > 5) {
|
|
223
|
+
if (!completed) {
|
|
224
|
+
completed = true;
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
file.close();
|
|
227
|
+
try { fs.unlinkSync(destPath); } catch (e) {}
|
|
228
|
+
reject(new Error('Too many redirects'));
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const request = protocol.get(requestUrl, {
|
|
234
|
+
headers: { 'User-Agent': 'sciagent-cli/1.0', ...extraHeaders },
|
|
235
|
+
timeout: 60000
|
|
236
|
+
}, (response) => {
|
|
237
|
+
// Handle redirects
|
|
238
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
239
|
+
response.resume(); // drain the response
|
|
240
|
+
let location = response.headers.location;
|
|
241
|
+
// Handle relative redirects
|
|
242
|
+
if (location.startsWith('/')) {
|
|
243
|
+
const parsed = new URL(requestUrl);
|
|
244
|
+
location = `${parsed.protocol}//${parsed.host}${location}`;
|
|
245
|
+
}
|
|
246
|
+
doRequest(location, redirectCount + 1);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (response.statusCode !== 200) {
|
|
251
|
+
response.resume(); // drain the response
|
|
252
|
+
if (!completed) {
|
|
253
|
+
completed = true;
|
|
254
|
+
clearTimeout(timer);
|
|
255
|
+
file.close();
|
|
256
|
+
try { fs.unlinkSync(destPath); } catch (e) {}
|
|
257
|
+
reject(new Error(`HTTP ${response.statusCode} for ${requestUrl}`));
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const totalSize = parseInt(response.headers['content-length'], 10);
|
|
263
|
+
|
|
264
|
+
response.on('data', (chunk) => {
|
|
265
|
+
downloadedSize += chunk.length;
|
|
266
|
+
if (totalSize) {
|
|
267
|
+
const percent = Math.floor((downloadedSize / totalSize) * 100);
|
|
268
|
+
const mb = (downloadedSize / (1024 * 1024)).toFixed(1);
|
|
269
|
+
const totalMb = (totalSize / (1024 * 1024)).toFixed(1);
|
|
270
|
+
process.stdout.write(`\r 下载进度: ${percent}% (${mb}/${totalMb} MB)`);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
response.pipe(file);
|
|
275
|
+
|
|
276
|
+
file.on('finish', () => {
|
|
277
|
+
if (!completed) {
|
|
278
|
+
completed = true;
|
|
279
|
+
clearTimeout(timer);
|
|
280
|
+
file.close();
|
|
281
|
+
console.log(); // New line after progress
|
|
282
|
+
resolve();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
file.on('error', (err) => {
|
|
287
|
+
if (!completed) {
|
|
288
|
+
completed = true;
|
|
289
|
+
clearTimeout(timer);
|
|
290
|
+
file.close();
|
|
291
|
+
try { fs.unlinkSync(destPath); } catch (e) {}
|
|
292
|
+
reject(err);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
request.on('error', (err) => {
|
|
298
|
+
if (!completed) {
|
|
299
|
+
completed = true;
|
|
300
|
+
clearTimeout(timer);
|
|
301
|
+
file.close();
|
|
302
|
+
try { fs.unlinkSync(destPath); } catch (e) {}
|
|
303
|
+
reject(new Error(`Network error: ${err.message} (${(downloadedSize / (1024 * 1024)).toFixed(1)} MB downloaded)`));
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
request.on('timeout', () => {
|
|
308
|
+
request.destroy();
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
doRequest(url);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function getSdkDownloadInfo(platformTag) {
|
|
317
|
+
// 尝试从多个镜像获取版本信息和下载URL
|
|
318
|
+
const mirrors = [
|
|
319
|
+
'https://mirrors.cloud.tencent.com/pypi/pypi/codebuddy-agent-sdk/json',
|
|
320
|
+
'https://mirrors.aliyun.com/pypi/pypi/codebuddy-agent-sdk/json',
|
|
321
|
+
'https://pypi.tuna.tsinghua.edu.cn/pypi/pypi/codebuddy-agent-sdk/json',
|
|
322
|
+
'https://pypi.org/pypi/codebuddy-agent-sdk/json'
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
for (const url of mirrors) {
|
|
326
|
+
try {
|
|
327
|
+
const result = await new Promise((resolve, reject) => {
|
|
328
|
+
const protocol = url.startsWith('https') ? https : http;
|
|
329
|
+
protocol.get(url, {
|
|
330
|
+
headers: { 'User-Agent': 'sciagent-cli/1.0' },
|
|
331
|
+
timeout: 15000
|
|
332
|
+
}, (response) => {
|
|
333
|
+
// Handle redirects
|
|
334
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
335
|
+
protocol.get(response.headers.location, {
|
|
336
|
+
headers: { 'User-Agent': 'sciagent-cli/1.0' },
|
|
337
|
+
timeout: 15000
|
|
338
|
+
}, (res2) => {
|
|
339
|
+
let data = '';
|
|
340
|
+
res2.on('data', (chunk) => { data += chunk; });
|
|
341
|
+
res2.on('end', () => {
|
|
342
|
+
try {
|
|
343
|
+
const json = JSON.parse(data);
|
|
344
|
+
resolve(json);
|
|
345
|
+
} catch (e) {
|
|
346
|
+
reject(new Error('Parse error'));
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}).on('error', reject);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let data = '';
|
|
354
|
+
response.on('data', (chunk) => { data += chunk; });
|
|
355
|
+
response.on('end', () => {
|
|
356
|
+
try {
|
|
357
|
+
const json = JSON.parse(data);
|
|
358
|
+
resolve(json);
|
|
359
|
+
} catch (e) {
|
|
360
|
+
reject(new Error('Parse error'));
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}).on('error', reject);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (result && result.info && result.urls) {
|
|
367
|
+
const version = result.info.version;
|
|
368
|
+
// 查找匹配平台的wheel文件
|
|
369
|
+
const wheelUrl = result.urls.find(u =>
|
|
370
|
+
u.filename && u.filename.includes(platformTag) && u.filename.endsWith('.whl')
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
if (wheelUrl) {
|
|
374
|
+
console.log(` 从 ${url.split('/')[2]} 获取到版本信息`);
|
|
375
|
+
return { version, url: wheelUrl.url, size: wheelUrl.size };
|
|
376
|
+
} else {
|
|
377
|
+
console.log(` ${url.split('/')[2]}: 未找到 ${platformTag} 平台的wheel文件`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
} catch (e) {
|
|
381
|
+
console.log(` ${url.split('/')[2]}: ${e.message}`);
|
|
382
|
+
// 继续尝试下一个镜像
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
throw new Error('无法获取SDK下载信息,请检查网络连接');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function installCodebuddySdk() {
|
|
390
|
+
const existing = checkCodebuddyBinaryInstalled();
|
|
391
|
+
if (existing.installed) {
|
|
392
|
+
console.log(`✅ CodeBuddy SDK 已安装: ${existing.path}`);
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const platform = process.platform;
|
|
397
|
+
const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
|
|
398
|
+
const platformKey = `${platform}-${arch}`;
|
|
399
|
+
const platformTag = PYPI_PLATFORM_MAP[platformKey];
|
|
400
|
+
|
|
401
|
+
console.log('');
|
|
402
|
+
console.log('📦 正在安装 CodeBuddy SDK...');
|
|
403
|
+
|
|
404
|
+
// 策略1: 从 PyPI 下载 wheel(Linux/macOS 有wheel,Windows 没有)
|
|
405
|
+
if (platformTag) {
|
|
406
|
+
try {
|
|
407
|
+
console.log(' 正在获取版本信息...');
|
|
408
|
+
const sdkInfo = await getSdkDownloadInfo(platformTag);
|
|
409
|
+
console.log(` 版本: ${sdkInfo.version}`);
|
|
410
|
+
console.log(` 平台: ${platformTag}`);
|
|
411
|
+
console.log(` 文件大小: ${(sdkInfo.size / (1024 * 1024)).toFixed(1)} MB`);
|
|
412
|
+
|
|
413
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sciagent-sdk-'));
|
|
414
|
+
const wheelFilename = `codebuddy_agent_sdk-${sdkInfo.version}-py3-none-${platformTag}.whl`;
|
|
415
|
+
const wheelPath = path.join(tmpDir, wheelFilename);
|
|
416
|
+
|
|
417
|
+
const binaryName = BINARY_NAMES[platform] || 'codebuddy-headless';
|
|
418
|
+
const binDir = getCodebuddyBinDir();
|
|
419
|
+
const targetPath = path.join(binDir, binaryName);
|
|
420
|
+
|
|
421
|
+
console.log(' 正在下载...');
|
|
422
|
+
await downloadFile(sdkInfo.url, wheelPath);
|
|
423
|
+
console.log(' ✅ 下载成功');
|
|
424
|
+
|
|
425
|
+
console.log(' 正在提取二进制文件...');
|
|
426
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
427
|
+
|
|
428
|
+
const extractScript = `
|
|
429
|
+
import zipfile, sys, os
|
|
430
|
+
wheel_path = sys.argv[1]
|
|
431
|
+
target_dir = sys.argv[2]
|
|
432
|
+
binary_name = sys.argv[3]
|
|
433
|
+
|
|
434
|
+
with zipfile.ZipFile(wheel_path, 'r') as zf:
|
|
435
|
+
for name in zf.namelist():
|
|
436
|
+
if binary_name in name and '/bin/' in name:
|
|
437
|
+
with zf.open(name) as src:
|
|
438
|
+
target_path = os.path.join(target_dir, binary_name)
|
|
439
|
+
with open(target_path, 'wb') as dst:
|
|
440
|
+
dst.write(src.read())
|
|
441
|
+
if sys.platform != 'win32':
|
|
442
|
+
os.chmod(target_path, 0o755)
|
|
443
|
+
print(f'Extracted: {target_path}')
|
|
444
|
+
sys.exit(0)
|
|
445
|
+
print(f'Error: {binary_name} not found in wheel')
|
|
446
|
+
sys.exit(1)
|
|
447
|
+
`;
|
|
448
|
+
|
|
449
|
+
const scriptPath = path.join(tmpDir, 'extract.py');
|
|
450
|
+
fs.writeFileSync(scriptPath, extractScript);
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
execSync(`python3 "${scriptPath}" "${wheelPath}" "${binDir}" "${binaryName}"`, {
|
|
454
|
+
stdio: 'inherit',
|
|
455
|
+
timeout: 120000
|
|
456
|
+
});
|
|
457
|
+
} catch (e) {
|
|
458
|
+
execSync(`python "${scriptPath}" "${wheelPath}" "${binDir}" "${binaryName}"`, {
|
|
459
|
+
stdio: 'inherit',
|
|
460
|
+
timeout: 120000
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (fs.existsSync(targetPath)) {
|
|
465
|
+
const stats = fs.statSync(targetPath);
|
|
466
|
+
console.log(`✅ CodeBuddy SDK 安装成功!(PyPI wheel)`);
|
|
467
|
+
console.log(` 路径: ${targetPath}`);
|
|
468
|
+
console.log(` 大小: ${(stats.size / (1024 * 1024)).toFixed(1)} MB`);
|
|
469
|
+
|
|
470
|
+
try { fs.unlinkSync(wheelPath); fs.unlinkSync(scriptPath); fs.rmdirSync(tmpDir); } catch (e) {}
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
} catch (e) {
|
|
474
|
+
console.log(` ⚠️ PyPI 下载失败: ${e.message}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// 策略2: 从 npm 安装 @tencent-ai/codebuddy-code(全平台支持,国内可访问)
|
|
479
|
+
console.log(' 尝试从 npm 安装 @tencent-ai/codebuddy-code...');
|
|
480
|
+
try {
|
|
481
|
+
execSync('npm install -g @tencent-ai/codebuddy-code', {
|
|
482
|
+
stdio: 'inherit',
|
|
483
|
+
timeout: 300000
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// 查找 npm 安装的 codebuddy-headless.js
|
|
487
|
+
const npmRootResult = execSync('npm root -g', { encoding: 'utf8', timeout: 10000 }).trim();
|
|
488
|
+
const headlessJsPath = path.join(npmRootResult, '@tencent-ai', 'codebuddy-code', 'dist', 'codebuddy-headless.js');
|
|
489
|
+
|
|
490
|
+
if (fs.existsSync(headlessJsPath)) {
|
|
491
|
+
// 创建 wrapper 脚本,让 codebuddy-headless 可以被直接调用
|
|
492
|
+
const binDir = getCodebuddyBinDir();
|
|
493
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
494
|
+
const binaryName = BINARY_NAMES[platform] || 'codebuddy-headless';
|
|
495
|
+
const wrapperPath = path.join(binDir, binaryName);
|
|
496
|
+
|
|
497
|
+
if (platform === 'win32') {
|
|
498
|
+
// Windows: 创建 .cmd wrapper
|
|
499
|
+
const cmdPath = wrapperPath.replace(/\.(exe)?$/, '.cmd');
|
|
500
|
+
fs.writeFileSync(cmdPath, `@echo off\r\nnode "${headlessJsPath}" %*\r\n`);
|
|
501
|
+
// 也创建 .exe placeholder(实际用 .cmd)
|
|
502
|
+
fs.writeFileSync(wrapperPath, `#!/usr/bin/env node\r\nrequire("${headlessJsPath}");`);
|
|
503
|
+
} else {
|
|
504
|
+
fs.writeFileSync(wrapperPath, `#!/usr/bin/env node\nrequire("${headlessJsPath}");`);
|
|
505
|
+
fs.chmodSync(wrapperPath, 0o755);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
console.log(`✅ CodeBuddy SDK 安装成功!(npm @tencent-ai/codebuddy-code)`);
|
|
509
|
+
console.log(` 路径: ${wrapperPath}`);
|
|
510
|
+
console.log(` headless.js: ${headlessJsPath}`);
|
|
511
|
+
return true;
|
|
512
|
+
} else {
|
|
513
|
+
console.log(` ⚠️ npm 安装成功但未找到 codebuddy-headless.js`);
|
|
514
|
+
}
|
|
515
|
+
} catch (e) {
|
|
516
|
+
console.log(` ⚠️ npm 安装失败: ${e.message}`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// 策略3: 从 releases 服务器下载
|
|
520
|
+
console.log(' 尝试从 Releases 服务器下载 CodeBuddy SDK...');
|
|
521
|
+
try {
|
|
522
|
+
const binDir = getCodebuddyBinDir();
|
|
523
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
524
|
+
const binaryName = BINARY_NAMES[platform] || 'codebuddy-headless';
|
|
525
|
+
const targetPath = path.join(binDir, binaryName);
|
|
526
|
+
const downloadUrl = `${SELF_HOSTED_URL}/api/releases/download/codebuddy/${platform}/${arch}/latest`;
|
|
527
|
+
|
|
528
|
+
await downloadFile(downloadUrl, targetPath, 600000);
|
|
529
|
+
|
|
530
|
+
if (fs.existsSync(targetPath)) {
|
|
531
|
+
const stats = fs.statSync(targetPath);
|
|
532
|
+
if (stats.size > 10 * 1024 * 1024) {
|
|
533
|
+
if (platform !== 'win32') {
|
|
534
|
+
fs.chmodSync(targetPath, 0o755);
|
|
535
|
+
}
|
|
536
|
+
console.log(`✅ CodeBuddy SDK 安装成功!(Releases 服务器)`);
|
|
537
|
+
console.log(` 路径: ${targetPath}`);
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
} catch (e) {
|
|
542
|
+
console.log(` ⚠️ Releases 服务器下载失败: ${e.message}`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
console.log('⚠️ CodeBuddy SDK 安装失败,CLI 仍可启动(AI 对话功能将在 SDK 安装后可用)');
|
|
546
|
+
console.log(' 手动安装: npm install -g @tencent-ai/codebuddy-code');
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* 检查 sciagent 进程是否正在运行
|
|
552
|
+
*/
|
|
553
|
+
function isSciAgentRunning() {
|
|
554
|
+
try {
|
|
555
|
+
if (process.platform === 'win32') {
|
|
556
|
+
const result = execSync('tasklist /FI "IMAGENAME eq sciagent.exe" /NH', {
|
|
557
|
+
encoding: 'utf8',
|
|
558
|
+
timeout: 5000
|
|
559
|
+
});
|
|
560
|
+
return result.includes('sciagent.exe');
|
|
561
|
+
} else {
|
|
562
|
+
const result = execSync('pgrep -x sciagent || true', {
|
|
563
|
+
encoding: 'utf8',
|
|
564
|
+
timeout: 5000
|
|
565
|
+
});
|
|
566
|
+
return result.trim().length > 0;
|
|
567
|
+
}
|
|
568
|
+
} catch (e) {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* 终止 sciagent 进程
|
|
575
|
+
*/
|
|
576
|
+
function killSciAgent() {
|
|
577
|
+
try {
|
|
578
|
+
if (process.platform === 'win32') {
|
|
579
|
+
execSync('taskkill /F /IM sciagent.exe', { timeout: 10000 });
|
|
580
|
+
} else {
|
|
581
|
+
execSync('pkill -x sciagent', { timeout: 10000 });
|
|
582
|
+
}
|
|
583
|
+
// 等待进程完全退出
|
|
584
|
+
let retries = 10;
|
|
585
|
+
while (retries-- > 0 && isSciAgentRunning()) {
|
|
586
|
+
const sleep = require('util').promisify(setTimeout);
|
|
587
|
+
sleep(500);
|
|
588
|
+
}
|
|
589
|
+
return !isSciAgentRunning();
|
|
590
|
+
} catch (e) {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* 尝试将临时文件替换为目标二进制文件
|
|
597
|
+
* 返回: 'replaced' | 'locked' | 'error'
|
|
598
|
+
*/
|
|
599
|
+
function tryReplaceBinary(tempPath, targetPath, installDir, version, platform) {
|
|
600
|
+
try {
|
|
601
|
+
// 先尝试直接复制
|
|
602
|
+
if (process.platform === 'win32') {
|
|
603
|
+
// Windows: 使用 PowerShell Copy-Item
|
|
604
|
+
const tmpScript = path.join(os.tmpdir(), 'sciagent-replace.ps1');
|
|
605
|
+
const scriptContent = [
|
|
606
|
+
`Copy-Item -Path "${tempPath}" -Destination "${targetPath}" -Force`,
|
|
607
|
+
'if (Test-Path $out) { Write-Host "OK" } else { Write-Error "Copy failed"; exit 1 }'
|
|
608
|
+
].join('\r\n');
|
|
609
|
+
fs.writeFileSync(tmpScript, scriptContent, 'utf8');
|
|
610
|
+
try {
|
|
611
|
+
execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${tmpScript}"`, {
|
|
612
|
+
stdio: 'pipe',
|
|
613
|
+
timeout: 30000
|
|
614
|
+
});
|
|
615
|
+
} finally {
|
|
616
|
+
try { fs.unlinkSync(tmpScript); } catch (e) {}
|
|
617
|
+
}
|
|
618
|
+
} else {
|
|
619
|
+
fs.copyFileSync(tempPath, targetPath);
|
|
620
|
+
fs.chmodSync(targetPath, 0o755);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// 验证替换成功
|
|
624
|
+
if (fs.existsSync(targetPath)) {
|
|
625
|
+
const stats = fs.statSync(targetPath);
|
|
626
|
+
if (stats.size > 10 * 1024 * 1024) {
|
|
627
|
+
fs.writeFileSync(path.join(installDir, '.version'), version);
|
|
628
|
+
// 清理临时文件
|
|
629
|
+
try { fs.unlinkSync(tempPath); } catch (e) {}
|
|
630
|
+
return 'replaced';
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return 'error';
|
|
634
|
+
} catch (e) {
|
|
635
|
+
// 检查是否是文件锁定错误
|
|
636
|
+
const msg = (e.message || '').toLowerCase();
|
|
637
|
+
if (msg.includes('used by another process') || msg.includes('being used') ||
|
|
638
|
+
msg.includes('eperm') || msg.includes('eacces') || msg.includes('access denied') ||
|
|
639
|
+
msg.includes('锁定') || msg.includes('denied')) {
|
|
640
|
+
return 'locked';
|
|
641
|
+
}
|
|
642
|
+
return 'error';
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* 写入 .pending-update 标记文件
|
|
648
|
+
* 下次 sciagent 启动时会自动应用更新
|
|
649
|
+
*/
|
|
650
|
+
function writePendingUpdate(installDir, tempPath, version) {
|
|
651
|
+
const pendingFile = path.join(installDir, '.pending-update');
|
|
652
|
+
const pendingData = {
|
|
653
|
+
version: version,
|
|
654
|
+
tempPath: tempPath,
|
|
655
|
+
timestamp: new Date().toISOString(),
|
|
656
|
+
platform: process.platform
|
|
657
|
+
};
|
|
658
|
+
fs.writeFileSync(pendingFile, JSON.stringify(pendingData, null, 2), 'utf8');
|
|
659
|
+
console.log(` [PENDING] Update deferred. Written .pending-update marker.`);
|
|
660
|
+
console.log(` [PENDING] New version will be applied on next sciagent startup.`);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* 交互式提示用户选择更新方式
|
|
665
|
+
* 返回: 'kill' | 'defer'
|
|
666
|
+
*/
|
|
667
|
+
function promptUpdateChoice() {
|
|
668
|
+
// 检查是否在交互式终端中
|
|
669
|
+
if (!process.stdin.isTTY) {
|
|
670
|
+
console.log(` [INFO] Non-interactive mode. Deferring update to next startup.`);
|
|
671
|
+
return 'defer';
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const readline = require('readline');
|
|
675
|
+
const rl = readline.createInterface({
|
|
676
|
+
input: process.stdin,
|
|
677
|
+
output: process.stdout
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
return new Promise((resolve) => {
|
|
681
|
+
console.log('');
|
|
682
|
+
console.log(' ╔══════════════════════════════════════════════════════════╗');
|
|
683
|
+
console.log(' ║ SciAgent is currently running. Update requires ║');
|
|
684
|
+
console.log(' ║ replacing the binary file which is locked. ║');
|
|
685
|
+
console.log(' ╠══════════════════════════════════════════════════════════╣');
|
|
686
|
+
console.log(' ║ [K] Kill SciAgent & update now ║');
|
|
687
|
+
console.log(' ║ [D] Defer - update on next startup ║');
|
|
688
|
+
console.log(' ╚══════════════════════════════════════════════════════════╝');
|
|
689
|
+
console.log('');
|
|
690
|
+
|
|
691
|
+
rl.question(' Choose [K/D] (default: D): ', (answer) => {
|
|
692
|
+
rl.close();
|
|
693
|
+
const choice = (answer || 'D').trim().toUpperCase();
|
|
694
|
+
if (choice === 'K' || choice === 'KILL') {
|
|
695
|
+
resolve('kill');
|
|
696
|
+
} else {
|
|
697
|
+
resolve('defer');
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function downloadFromServer(platform, arch, version) {
|
|
704
|
+
const binName = platform === 'win32' ? 'sciagent.exe' : 'sciagent';
|
|
705
|
+
const ext = platform === 'win32' ? '.exe' : '';
|
|
706
|
+
const filename = `sciagent-${platform}-${arch}${ext}`;
|
|
707
|
+
|
|
708
|
+
// 确定安装目录
|
|
709
|
+
const installDir = getHomeBinDir();
|
|
710
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
711
|
+
const targetPath = path.join(installDir, binName);
|
|
712
|
+
|
|
713
|
+
// 下载到临时目录(避免直接覆盖正在运行的二进制)
|
|
714
|
+
const tempDir = path.join(os.tmpdir(), 'sciagent-update');
|
|
715
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
716
|
+
const tempPath = path.join(tempDir, binName);
|
|
717
|
+
|
|
718
|
+
// 清理旧的临时文件
|
|
719
|
+
try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e) {}
|
|
720
|
+
|
|
721
|
+
// 构建下载 URL 列表(按优先级)
|
|
722
|
+
const downloadUrls = DOWNLOAD_MIRRORS.map(mirror => ({
|
|
723
|
+
url: mirror.getUrl(platform, arch, version, filename),
|
|
724
|
+
name: mirror.name,
|
|
725
|
+
auth: mirror.auth
|
|
726
|
+
}));
|
|
727
|
+
|
|
728
|
+
for (const source of downloadUrls) {
|
|
729
|
+
console.log(`\n [${source.name}] Downloading ${filename} v${version}...`);
|
|
730
|
+
|
|
731
|
+
// Windows: 优先使用 PowerShell(更可靠的大文件下载)
|
|
732
|
+
if (platform === 'win32') {
|
|
733
|
+
try {
|
|
734
|
+
const tmpScript = path.join(os.tmpdir(), 'sciagent-download.ps1');
|
|
735
|
+
let scriptLines = [
|
|
736
|
+
'$ProgressPreference = "SilentlyContinue"',
|
|
737
|
+
`$uri = "${source.url}"`,
|
|
738
|
+
`$out = "${tempPath}"`,
|
|
739
|
+
'Write-Host " Downloading from ' + source.name + '..."',
|
|
740
|
+
];
|
|
741
|
+
|
|
742
|
+
if (source.auth === 'jihulab') {
|
|
743
|
+
const b64 = Buffer.from(`${JIHULAB_DEPLOY_TOKEN_USER}:${JIHULAB_DEPLOY_TOKEN}`).toString('base64');
|
|
744
|
+
scriptLines.push(`$headers = @{ "Authorization" = "Basic ${b64}" }`);
|
|
745
|
+
// Use WebClient for large files - much faster than Invoke-WebRequest
|
|
746
|
+
scriptLines.push('$wc = New-Object System.Net.WebClient');
|
|
747
|
+
scriptLines.push('$wc.Headers.Add("Authorization", $headers["Authorization"])');
|
|
748
|
+
scriptLines.push('Write-Host " Using JihuLab CDN with auth..."');
|
|
749
|
+
scriptLines.push('$wc.DownloadFile($uri, $out)');
|
|
750
|
+
} else {
|
|
751
|
+
scriptLines.push('$wc = New-Object System.Net.WebClient');
|
|
752
|
+
scriptLines.push('Write-Host " Using ' + source.name + '..."');
|
|
753
|
+
scriptLines.push('$wc.DownloadFile($uri, $out)');
|
|
754
|
+
}
|
|
755
|
+
scriptLines.push('if (Test-Path $out) { $s = (Get-Item $out).Length; Write-Host " Downloaded: $s bytes ($([math]::Round($s/1MB,1)) MB)" } else { Write-Error "File not created"; exit 1 }');
|
|
756
|
+
|
|
757
|
+
fs.writeFileSync(tmpScript, scriptLines.join('\r\n'), 'utf8');
|
|
758
|
+
|
|
759
|
+
try {
|
|
760
|
+
execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${tmpScript}"`, {
|
|
761
|
+
stdio: 'inherit',
|
|
762
|
+
timeout: 600000
|
|
763
|
+
});
|
|
764
|
+
} finally {
|
|
765
|
+
try { fs.unlinkSync(tmpScript); } catch (e) {}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// 验证下载到临时文件
|
|
769
|
+
if (fs.existsSync(tempPath)) {
|
|
770
|
+
const stats = fs.statSync(tempPath);
|
|
771
|
+
if (stats.size > 10 * 1024 * 1024) {
|
|
772
|
+
console.log(` [OK] Downloaded from ${source.name}: ${tempPath} (${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
|
|
773
|
+
// 尝试替换
|
|
774
|
+
return await applyDownloadedBinary(tempPath, targetPath, installDir, version, platform);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
console.log(` [WARN] ${source.name} download validation failed, trying next...`);
|
|
779
|
+
} catch (e) {
|
|
780
|
+
console.log(` [WARN] ${source.name} download failed: ${e.message}, trying next...`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// 通用方式: Node.js https 下载到临时文件
|
|
785
|
+
try {
|
|
786
|
+
// 如果是 JihuLab,需要添加认证头
|
|
787
|
+
const headers = source.auth === 'jihulab'
|
|
788
|
+
? { 'Authorization': `Basic ${Buffer.from(`${JIHULAB_DEPLOY_TOKEN_USER}:${JIHULAB_DEPLOY_TOKEN}`).toString('base64')}` }
|
|
789
|
+
: {};
|
|
790
|
+
await downloadFile(source.url, tempPath, 600000, headers);
|
|
791
|
+
|
|
792
|
+
// 验证下载
|
|
793
|
+
if (fs.existsSync(tempPath)) {
|
|
794
|
+
const stats = fs.statSync(tempPath);
|
|
795
|
+
if (stats.size > 10 * 1024 * 1024) {
|
|
796
|
+
console.log(` [OK] Downloaded from ${source.name}: ${tempPath} (${(stats.size / (1024 * 1024)).toFixed(1)} MB)`);
|
|
797
|
+
if (platform !== 'win32') {
|
|
798
|
+
fs.chmodSync(tempPath, 0o755);
|
|
799
|
+
}
|
|
800
|
+
// 尝试替换
|
|
801
|
+
return await applyDownloadedBinary(tempPath, targetPath, installDir, version, platform);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
console.log(` [WARN] ${source.name} download validation failed, trying next...`);
|
|
806
|
+
} catch (e) {
|
|
807
|
+
console.log(` [WARN] ${source.name} download failed: ${e.message}, trying next...`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
console.log(` [WARN] All download mirrors failed.`);
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* 将已下载到临时路径的二进制文件应用到目标位置
|
|
817
|
+
* 处理文件锁定情况:交互式选择杀死进程或延迟更新
|
|
818
|
+
*/
|
|
819
|
+
async function applyDownloadedBinary(tempPath, targetPath, installDir, version, platform) {
|
|
820
|
+
// 如果目标文件不存在(首次安装),直接移动
|
|
821
|
+
if (!fs.existsSync(targetPath)) {
|
|
822
|
+
try {
|
|
823
|
+
if (process.platform === 'win32') {
|
|
824
|
+
// Windows: 使用 PowerShell 移动
|
|
825
|
+
const tmpScript = path.join(os.tmpdir(), 'sciagent-move.ps1');
|
|
826
|
+
const scriptContent = `Move-Item -Path "${tempPath}" -Destination "${targetPath}" -Force`;
|
|
827
|
+
fs.writeFileSync(tmpScript, scriptContent, 'utf8');
|
|
828
|
+
try {
|
|
829
|
+
execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${tmpScript}"`, {
|
|
830
|
+
stdio: 'pipe', timeout: 30000
|
|
831
|
+
});
|
|
832
|
+
} finally {
|
|
833
|
+
try { fs.unlinkSync(tmpScript); } catch (e) {}
|
|
834
|
+
}
|
|
835
|
+
} else {
|
|
836
|
+
fs.renameSync(tempPath, targetPath);
|
|
837
|
+
fs.chmodSync(targetPath, 0o755);
|
|
838
|
+
}
|
|
839
|
+
fs.writeFileSync(path.join(installDir, '.version'), version);
|
|
840
|
+
console.log(` [OK] Installed: ${targetPath}`);
|
|
841
|
+
return true;
|
|
842
|
+
} catch (e) {
|
|
843
|
+
console.log(` [WARN] Move failed: ${e.message}, trying copy...`);
|
|
844
|
+
// fallback to copy
|
|
845
|
+
const result = tryReplaceBinary(tempPath, targetPath, installDir, version, platform);
|
|
846
|
+
return result === 'replaced';
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// 目标文件已存在,尝试替换
|
|
851
|
+
const replaceResult = tryReplaceBinary(tempPath, targetPath, installDir, version, platform);
|
|
852
|
+
|
|
853
|
+
if (replaceResult === 'replaced') {
|
|
854
|
+
console.log(` [OK] Updated: ${targetPath} (v${version})`);
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (replaceResult === 'locked') {
|
|
859
|
+
console.log(` [WARN] Binary file is locked (SciAgent is running).`);
|
|
860
|
+
|
|
861
|
+
// 交互式选择
|
|
862
|
+
const choice = await promptUpdateChoice();
|
|
863
|
+
|
|
864
|
+
if (choice === 'kill') {
|
|
865
|
+
console.log(` [INFO] Killing SciAgent process...`);
|
|
866
|
+
const killed = killSciAgent();
|
|
867
|
+
if (killed) {
|
|
868
|
+
console.log(` [OK] SciAgent process terminated.`);
|
|
869
|
+
// 重试替换
|
|
870
|
+
const retryResult = tryReplaceBinary(tempPath, targetPath, installDir, version, platform);
|
|
871
|
+
if (retryResult === 'replaced') {
|
|
872
|
+
console.log(` [OK] Updated: ${targetPath} (v${version})`);
|
|
873
|
+
return true;
|
|
874
|
+
} else {
|
|
875
|
+
console.log(` [WARN] Replace still failed after killing process. Deferring to next startup.`);
|
|
876
|
+
writePendingUpdate(installDir, tempPath, version);
|
|
877
|
+
return true; // 下载成功,只是替换延迟
|
|
878
|
+
}
|
|
879
|
+
} else {
|
|
880
|
+
console.log(` [WARN] Failed to kill SciAgent. Deferring update to next startup.`);
|
|
881
|
+
writePendingUpdate(installDir, tempPath, version);
|
|
882
|
+
return true; // 下载成功,只是替换延迟
|
|
883
|
+
}
|
|
884
|
+
} else {
|
|
885
|
+
// 用户选择延迟
|
|
886
|
+
writePendingUpdate(installDir, tempPath, version);
|
|
887
|
+
console.log(` [INFO] Update will be applied automatically on next startup.`);
|
|
888
|
+
return true; // 下载成功,只是替换延迟
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// 其他错误
|
|
893
|
+
console.log(` [WARN] Replace failed with unexpected error. Deferring to next startup.`);
|
|
894
|
+
writePendingUpdate(installDir, tempPath, version);
|
|
895
|
+
return true; // 下载成功,只是替换延迟
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// installBinaryPackage 已移除
|
|
899
|
+
// 原因:npm optionalDependencies 缓存旧包导致版本错乱
|
|
900
|
+
// 所有二进制统一从 sciagent.tech 服务器下载
|
|
901
|
+
|
|
902
|
+
async function main() {
|
|
903
|
+
console.log('[postinstall] Starting postinstall script...');
|
|
904
|
+
const platform = PLATFORM_MAP[process.platform];
|
|
905
|
+
const arch = ARCH_MAP[process.arch];
|
|
906
|
+
|
|
907
|
+
console.log('');
|
|
908
|
+
console.log('╔══════════════════════════════════════════════════════════╗');
|
|
909
|
+
console.log('║ SciAgent CLI - Post Install Setup ║');
|
|
910
|
+
console.log('╚══════════════════════════════════════════════════════════╝');
|
|
911
|
+
console.log('');
|
|
912
|
+
console.log(` Platform: ${platform || process.platform}`);
|
|
913
|
+
console.log(` Architecture: ${arch || process.arch}`);
|
|
914
|
+
console.log(` Node.js: ${process.version}`);
|
|
915
|
+
console.log('');
|
|
916
|
+
|
|
917
|
+
if (!platform || !arch) {
|
|
918
|
+
console.error('❌ Unsupported platform or architecture');
|
|
919
|
+
console.error(` Platform: ${process.platform}`);
|
|
920
|
+
console.error(` Architecture: ${process.arch}`);
|
|
921
|
+
console.error('');
|
|
922
|
+
console.error(' Supported platforms: linux, darwin, win32');
|
|
923
|
+
console.error(' Supported architectures: x64, arm64');
|
|
924
|
+
process.exit(1);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// 检查 SciAgent CLI 二进制
|
|
928
|
+
const packageName = `@sciagent/cli-${platform}-${arch}`;
|
|
929
|
+
const result = checkBinaryInstalled(platform, arch);
|
|
930
|
+
|
|
931
|
+
if (result.installed) {
|
|
932
|
+
console.log(`✅ Platform binary already installed: ${packageName}`);
|
|
933
|
+
console.log(` Path: ${result.path}`);
|
|
934
|
+
} else {
|
|
935
|
+
console.log(`⚠️ Platform binary not found: ${packageName}`);
|
|
936
|
+
|
|
937
|
+
// 下载策略: 从 sciagent.tech 服务器下载
|
|
938
|
+
console.log('');
|
|
939
|
+
console.log(' 尝试从服务器下载...');
|
|
940
|
+
let success = await downloadFromServer(platform, arch, CURRENT_VERSION);
|
|
941
|
+
|
|
942
|
+
// 如果当前版本失败,尝试回退版本
|
|
943
|
+
if (!success) {
|
|
944
|
+
for (const fallbackVersion of GITHUB_FALLBACK_VERSIONS) {
|
|
945
|
+
if (fallbackVersion === CURRENT_VERSION) continue;
|
|
946
|
+
console.log(` 尝试回退版本 v${fallbackVersion}...`);
|
|
947
|
+
success = await downloadFromServer(platform, arch, fallbackVersion);
|
|
948
|
+
if (success) break;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (!success) {
|
|
953
|
+
console.error('');
|
|
954
|
+
console.error('╔══════════════════════════════════════════════════════════╗');
|
|
955
|
+
console.error('║ Manual Installation Required ║');
|
|
956
|
+
console.error('╚══════════════════════════════════════════════════════════╝');
|
|
957
|
+
console.error('');
|
|
958
|
+
console.error(' All download sources failed. Please try again later or:');
|
|
959
|
+
console.error(` 1. Check your network connection`);
|
|
960
|
+
console.error(` 2. Visit: https://sciagent.tech`);
|
|
961
|
+
console.error('');
|
|
962
|
+
process.exit(1);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const verifyResult = checkBinaryInstalled(platform, arch);
|
|
966
|
+
if (verifyResult.installed) {
|
|
967
|
+
console.log(`\n✅ Platform binary installed successfully`);
|
|
968
|
+
} else {
|
|
969
|
+
console.error(`\n❌ Installation verification failed. Please install manually:`);
|
|
970
|
+
console.error(` npm install -g ${packageName}@${CURRENT_VERSION}`);
|
|
971
|
+
process.exit(1);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Windows: 确保 npm 全局 bin 目录在 PATH 中
|
|
976
|
+
if (process.platform === 'win32') {
|
|
977
|
+
const npmBinDir = path.join(process.env.APPDATA || '', 'npm');
|
|
978
|
+
const userPath = (process.env.PATH || '').split(path.delimiter);
|
|
979
|
+
if (!userPath.some(p => p.toLowerCase() === npmBinDir.toLowerCase())) {
|
|
980
|
+
try {
|
|
981
|
+
const { execSync } = require('child_process');
|
|
982
|
+
// 使用 PowerShell 永久添加到用户 PATH(无 1024 字符限制)
|
|
983
|
+
execSync(
|
|
984
|
+
`powershell -Command "[Environment]::SetEnvironmentVariable('Path', [Environment]::GetEnvironmentVariable('Path','User') + ';${npmBinDir}', 'User')"`,
|
|
985
|
+
{ stdio: 'pipe', timeout: 15000 }
|
|
986
|
+
);
|
|
987
|
+
console.log(`✅ Added "${npmBinDir}" to user PATH (restart terminal to take effect)`);
|
|
988
|
+
} catch (e) {
|
|
989
|
+
console.log(`⚠️ Could not add "${npmBinDir}" to PATH automatically.`);
|
|
990
|
+
console.log(` Please run this command manually:`);
|
|
991
|
+
console.log(` powershell -Command "[Environment]::SetEnvironmentVariable('Path', [Environment]::GetEnvironmentVariable('Path','User') + ';${npmBinDir}', 'User')"`);
|
|
992
|
+
console.log(` Then restart your terminal.`);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// 安装 CodeBuddy SDK(使用国内镜像)
|
|
998
|
+
await installCodebuddySdk();
|
|
999
|
+
|
|
1000
|
+
console.log('');
|
|
1001
|
+
console.log('Usage:');
|
|
1002
|
+
console.log(' sciagent # Start with default ports');
|
|
1003
|
+
console.log(' sciagent --port 8080 # Custom proxy port');
|
|
1004
|
+
console.log(' sciagent --no-browser # Don\'t open browser');
|
|
1005
|
+
console.log(' sciagent install-sdk # Install CodeBuddy SDK manually');
|
|
1006
|
+
console.log(' sciagent --help # Show help');
|
|
1007
|
+
console.log('');
|
|
1008
|
+
console.log('Documentation: https://gitee.com/garva/research-agent');
|
|
1009
|
+
console.log('');
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// 运行主函数
|
|
1013
|
+
main().catch(err => {
|
|
1014
|
+
console.error('Post install error:', err.message);
|
|
1015
|
+
process.exit(1);
|
|
1016
|
+
});
|