Files
qiming/qimingclaw/crates/agent-electron-client/scripts/prepare/prepare-node.js

344 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Node.js 24 集成(所有平台)
*
* 为所有平台准备 resources/node/<platform-arch>/,供运行时使用。
*
* 参考 LobsterAI 方案https://github.com/netease-youdao/LobsterAI
*
* 1) 若已存在 resources/node/<platform-arch>/,则跳过
* 2) 否则从 nodejs.org 下载对应平台的包,解压到 <platform-arch>/
* 3) 可选:验证下载文件的 SHA256 校验和
*
* 平台 keydarwin-x64, darwin-arm64, win32-x64, win32-arm64, linux-x64, linux-arm64
*/
const path = require('path');
const fs = require('fs');
const https = require('https');
const crypto = require('crypto');
const { execSync } = require('child_process');
const { getProjectRoot } = require('../utils/project-paths');
const projectRoot = getProjectRoot();
const nodeRoot = path.join(projectRoot, 'resources', 'node');
// Node.js 版本
const NODE_VERSION = '24.14.0';
// 是否验证 SHA256可通过环境变量禁用SKIP_SHA256=1 npm run prepare:node
const SKIP_SHA256 = process.env.SKIP_SHA256 === '1';
// 平台 key
function getPlatformKey() {
const p = process.platform;
const a = process.env.TARGET_ARCH || process.arch;
return `${p}-${a}`;
}
// Node.js 官方 release 资源文件名
const NODE_ASSET_SUFFIX = {
'darwin-arm64': 'node-v${VERSION}-darwin-arm64.tar.xz',
'darwin-x64': 'node-v${VERSION}-darwin-x64.tar.xz',
'win32-x64': 'node-v${VERSION}-win-x64.zip',
'win32-arm64': 'node-v${VERSION}-win-arm64.zip',
'linux-x64': 'node-v${VERSION}-linux-x64.tar.xz',
'linux-arm64': 'node-v${VERSION}-linux-arm64.tar.xz',
};
/**
* 下载文件
*/
function download(url, filename) {
return new Promise((resolve, reject) => {
const file = path.join(nodeRoot, '.cache', filename);
if (fs.existsSync(file)) {
console.log(`[prepare-node] 使用缓存: ${filename}`);
resolve(file);
return;
}
fs.mkdirSync(path.dirname(file), { recursive: true });
const stream = fs.createWriteStream(file);
https.get(url, (res) => {
if (res.statusCode === 302 || res.statusCode === 301) {
const loc = res.headers.location;
const fullUrl = loc.startsWith('http') ? loc : new URL(loc, url).href;
stream.close();
fs.unlink(file, () => {});
return download(fullUrl, filename).then(resolve).catch(reject);
}
if (res.statusCode !== 200) {
stream.close();
fs.unlink(file, () => {});
return reject(new Error(`HTTP ${res.statusCode}`));
}
res.pipe(stream);
stream.on('finish', () => { stream.close(); resolve(file); });
stream.on('error', reject);
}).on('error', reject);
});
}
/**
* 计算文件的 SHA256 哈希值
*/
function computeSha256(filePath) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256');
const stream = fs.createReadStream(filePath);
stream.on('data', (chunk) => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex')));
stream.on('error', reject);
});
}
/**
* 获取 Node.js 官方 SHASUMS256.txt 内容
*/
async function fetchShasums(urlOverride) {
const url = urlOverride || `https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt`;
return new Promise((resolve, reject) => {
https.get(url, (res) => {
if (res.statusCode === 302 || res.statusCode === 301) {
const loc = res.headers.location;
const fullUrl = loc.startsWith('http') ? loc : new URL(loc, url).href;
return fetchShasums(fullUrl).then(resolve).catch(reject);
}
if (res.statusCode !== 200) {
return reject(new Error(`HTTP ${res.statusCode}`));
}
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => resolve(data));
res.on('error', reject);
}).on('error', reject);
});
}
/**
* 验证下载文件的 SHA256
*/
async function verifySha256(filePath, filename) {
if (SKIP_SHA256) {
console.log('[prepare-node] SHA256 校验已跳过 (SKIP_SHA256=1)');
return true;
}
try {
console.log('[prepare-node] 验证 SHA256 校验和...');
const shasums = await fetchShasums();
const expectedHash = shasums
.split('\n')
.find(line => line.endsWith(filename))
?.split(/\s+/)[0];
if (!expectedHash) {
console.warn('[prepare-node] ⚠️ 未找到对应的 SHA256 校验和,跳过验证');
return true;
}
const actualHash = await computeSha256(filePath);
if (actualHash !== expectedHash) {
console.error(`[prepare-node] ❌ SHA256 校验失败!`);
console.error(`[prepare-node] 期望: ${expectedHash}`);
console.error(`[prepare-node] 实际: ${actualHash}`);
return false;
}
console.log('[prepare-node] ✅ SHA256 校验通过');
return true;
} catch (err) {
console.warn('[prepare-node] ⚠️ SHA256 校验失败(网络问题?:', err.message);
// 网络问题时允许继续
return true;
}
}
/**
* 解压
*/
function extractArchive(archivePath, outDir) {
fs.mkdirSync(outDir, { recursive: true });
const ext = path.extname(archivePath).toLowerCase();
if (ext === '.zip') {
if (process.platform === 'win32') {
const psPath = archivePath.replace(/'/g, "''");
const psDest = outDir.replace(/'/g, "''");
execSync(
`powershell -NoProfile -Command "Expand-Archive -LiteralPath '${psPath}' -DestinationPath '${psDest}' -Force"`,
{ stdio: 'inherit' }
);
} else {
execSync(`unzip -o -q "${archivePath}" -d "${outDir}"`, { stdio: 'inherit' });
}
} else {
execSync(`tar -xJf "${archivePath}" -C "${outDir}"`, { stdio: 'inherit' });
}
}
/**
* 复制目录,保留符号链接
*/
function copyDirRecursiveWithSymlinks(src, dest) {
if (!fs.existsSync(src)) return;
fs.mkdirSync(dest, { recursive: true });
for (const name of fs.readdirSync(src)) {
const s = path.join(src, name);
const d = path.join(dest, name);
const stat = fs.lstatSync(s);
if (stat.isSymbolicLink()) {
const linkTarget = fs.readlinkSync(s);
fs.symlinkSync(linkTarget, d);
} else if (stat.isDirectory()) {
copyDirRecursiveWithSymlinks(s, d);
} else {
fs.copyFileSync(s, d);
}
}
}
async function prepareNode(key, suffix) {
const cacheDir = path.join(nodeRoot, '.cache');
const platformDir = path.join(nodeRoot, key);
// 检查是否已存在
const platformKeyFile = path.join(platformDir, '.platform-key');
if (fs.existsSync(platformDir)) {
if (fs.existsSync(platformKeyFile)) {
const existingKey = fs.readFileSync(platformKeyFile, 'utf-8').trim();
if (existingKey === key) {
console.log(`[prepare-node] Node.js ${NODE_VERSION} (${key}) 已存在且架构匹配,跳过`);
return;
}
console.log(`[prepare-node] 架构不匹配: 已有 ${existingKey}, 需要 ${key}, 清理并重新下载`);
fs.rmSync(platformDir, { recursive: true, force: true });
} else {
// No .platform-key — legacy, treat as matching
console.log(`[prepare-node] Node.js ${NODE_VERSION} (${key}) 已存在,跳过`);
return;
}
}
const filename = suffix.replace('${VERSION}', NODE_VERSION);
const downloadUrl = `https://nodejs.org/dist/v${NODE_VERSION}/${filename}`;
console.log(`[prepare-node] 下载 Node.js ${NODE_VERSION} (${filename})...`);
try {
const archivePath = await download(downloadUrl, filename);
// 验证 SHA256
const sha256Valid = await verifySha256(archivePath, filename);
if (!sha256Valid) {
fs.unlinkSync(archivePath);
console.error('[prepare-node] 下载文件已删除,请重试');
process.exit(1);
}
const extractDir = path.join(cacheDir, `node-extract-${key}`);
if (fs.existsSync(extractDir)) {
fs.rmSync(extractDir, { recursive: true, force: true });
}
console.log(`[prepare-node] 解压...`);
extractArchive(archivePath, extractDir);
// 移动到目标目录(保留符号链接)
const entries = fs.readdirSync(extractDir);
if (entries.length === 1) {
const inner = path.join(extractDir, entries[0]);
if (fs.statSync(inner).isDirectory()) {
copyDirRecursiveWithSymlinks(inner, platformDir);
}
}
// 清理不需要的文件以减小包体积
// 1. 删除 include 目录C++ 头文件,运行时不需要)
const includeDir = path.join(platformDir, 'include');
if (fs.existsSync(includeDir)) {
fs.rmSync(includeDir, { recursive: true, force: true });
console.log(`[prepare-node] 已删除 include/ 目录(运行时不需要)`);
}
// 2. 删除 CHANGELOG.md, LICENSE, README.md可选减小体积
['CHANGELOG.md', 'LICENSE', 'README.md'].forEach(file => {
const filePath = path.join(platformDir, file);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
});
// Windows: 统一目录结构,将 node.exe 移动到 bin/ 目录
// 这样所有平台都使用 resources/node/<platform>/bin/node 结构
if (key.startsWith('win32-')) {
const nodeExe = path.join(platformDir, 'node.exe');
const binDir = path.join(platformDir, 'bin');
if (fs.existsSync(nodeExe) && !fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true });
fs.renameSync(nodeExe, path.join(binDir, 'node.exe'));
// 移动相关文件到 bin/
['npm.cmd', 'npx.cmd', 'corepack.cmd', 'npm', 'npx', 'corepack'].forEach(cmd => {
const src = path.join(platformDir, cmd);
if (fs.existsSync(src)) {
fs.renameSync(src, path.join(binDir, cmd));
}
});
// 移动 node_modules 到 bin/ 目录npm.cmd 期望 node_modules 在同一目录)
const nodeModulesSrc = path.join(platformDir, 'node_modules');
const nodeModulesDest = path.join(binDir, 'node_modules');
if (fs.existsSync(nodeModulesSrc) && !fs.existsSync(nodeModulesDest)) {
fs.renameSync(nodeModulesSrc, nodeModulesDest);
}
console.log(`[prepare-node] 已将 Windows Node.js 重组为 bin/ 结构`);
}
}
// macOS: 对 Node.js 二进制进行 ad-hoc 签名,使其能继承主应用的辅助功能权限
// 避免用户需要单独授权 node 子进程
if (key.startsWith('darwin-')) {
const nodeBin = path.join(platformDir, 'bin', 'node');
if (fs.existsSync(nodeBin)) {
try {
execSync(`codesign --force --sign - "${nodeBin}"`, { stdio: 'inherit' });
console.log(`[prepare-node] 已对 Node.js 二进制进行 ad-hoc 签名`);
} catch (signErr) {
console.warn(`[prepare-node] ⚠️ 签名失败(不影响功能):`, signErr.message);
}
}
}
console.log(`[prepare-node] Node.js ${NODE_VERSION} (${key}) 准备完成!`);
// Write .platform-key marker
fs.writeFileSync(platformKeyFile, key, 'utf-8');
console.log(`[prepare-node] 已写入 .platform-key: ${key}`);
} catch (err) {
console.error(`[prepare-node] 下载或解压失败:`, err.message);
process.exit(1);
}
}
async function main() {
// 所有平台都需要内置 Node.js 24不依赖用户系统安装
const key = getPlatformKey();
const suffix = NODE_ASSET_SUFFIX[key];
if (!suffix) {
console.warn(`[prepare-node] 未支持的平台: ${key}`);
return;
}
// 创建目录
fs.mkdirSync(nodeRoot, { recursive: true });
await prepareNode(key, suffix);
}
main();