236 lines
7.2 KiB
JavaScript
Raw Normal View History

2026-05-20 21:39:12 +08:00
#!/usr/bin/env node
// 检测 netabrowser-cli chromium 是否就位;缺失则下载 + 解压到 packages/netabrowser-cli/chromium/win64/
// 覆盖变量:
// NETA_CHROMIUM_PATH 已就位的 chrome.exe 绝对路径(优先级最高,存在即直接复用)
// NETA_CHROMIUM_URL 自定义下载 zip 地址
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const https = require('node:https');
const http = require('node:http');
const { URL } = require('node:url');
const { spawnSync } = require('node:child_process');
const DEFAULT_URL =
'https://ai-flow.bj.bcebos.com/browers-cli/ungoogled-chromium_144.0.7559.132-1.1_windows_x64.zip?responseContentDisposition=attachment';
function log(msg) {
process.stderr.write(`[ensure-chromium] ${msg}\n`);
}
function fail(msg, code = 1) {
process.stderr.write(`[ensure-chromium] ERROR: ${msg}\n`);
process.exit(code);
}
function resolveTargetDir() {
if (process.env.NETA_CHROMIUM_PATH) {
// 用户指定了 chrome.exe 路径,目标目录 = 其上级
return path.dirname(process.env.NETA_CHROMIUM_PATH);
}
// dev 模式scripts/ → skill dir → skills/ → backend/ → packages/ → packages/netabrowser-cli/chromium/win64
return path.resolve(__dirname, '../../../../netabrowser-cli/chromium/win64');
}
function chromeExeIn(dir) {
return path.join(dir, 'chrome.exe');
}
function alreadyInstalled(dir) {
try {
const stat = fs.statSync(chromeExeIn(dir));
return stat.isFile() && stat.size > 1024 * 1024; // > 1MB 才算有效
} catch {
return false;
}
}
function httpGet(urlStr, redirectsLeft = 5) {
return new Promise((resolve, reject) => {
const url = new URL(urlStr);
const mod = url.protocol === 'http:' ? http : https;
const req = mod.get(
urlStr,
{
headers: {
'User-Agent': 'netabrowser-cli/ensure-chromium',
Accept: '*/*',
},
},
(res) => {
const status = res.statusCode || 0;
if (status >= 300 && status < 400 && res.headers.location) {
res.resume();
if (redirectsLeft <= 0) return reject(new Error('redirect loop'));
const next = new URL(res.headers.location, urlStr).toString();
log(`redirect → ${next}`);
resolve(httpGet(next, redirectsLeft - 1));
return;
}
if (status < 200 || status >= 300) {
res.resume();
return reject(new Error(`HTTP ${status} from ${urlStr}`));
}
resolve(res);
}
);
req.on('error', reject);
req.setTimeout(60_000, () => {
req.destroy(new Error('connect timeout'));
});
});
}
function humanBytes(n) {
if (!Number.isFinite(n)) return '?';
if (n > 1024 ** 3) return (n / 1024 ** 3).toFixed(2) + ' GiB';
if (n > 1024 ** 2) return (n / 1024 ** 2).toFixed(2) + ' MiB';
if (n > 1024) return (n / 1024).toFixed(2) + ' KiB';
return n + ' B';
}
async function download(url, destFile) {
log(`downloading ${url}`);
log(`${destFile}`);
const res = await httpGet(url);
const total = parseInt(String(res.headers['content-length'] || '0'), 10);
const out = fs.createWriteStream(destFile);
let received = 0;
let lastReport = Date.now();
await new Promise((resolve, reject) => {
res.on('data', (chunk) => {
received += chunk.length;
const now = Date.now();
if (now - lastReport > 2000) {
lastReport = now;
const pct = total ? ((received / total) * 100).toFixed(1) + '%' : '';
log(` ${humanBytes(received)}${total ? ' / ' + humanBytes(total) : ''} ${pct}`);
}
});
res.on('error', reject);
out.on('error', reject);
out.on('finish', resolve);
res.pipe(out);
});
log(`downloaded ${humanBytes(received)}`);
}
function extractZip(zipFile, destDir) {
fs.mkdirSync(destDir, { recursive: true });
if (process.platform === 'win32') {
// Windows直接用 PowerShell Expand-Archive避开 Git Bash 里 tar 把 C: 当主机的老问题
log(`extracting (PowerShell Expand-Archive) → ${destDir}`);
const ps = [
'-NoProfile',
'-ExecutionPolicy',
'Bypass',
'-Command',
`Expand-Archive -Path '${zipFile.replace(/'/g, "''")}' -DestinationPath '${destDir.replace(
/'/g,
"''"
)}' -Force`,
];
const r = spawnSync('powershell.exe', ps, {
stdio: ['ignore', 'inherit', 'inherit'],
});
if (r.status === 0) return;
fail(`extract failed: powershell exit=${r.status}`);
return;
}
// 非 Windows优先 unzip缺失再回落 tarlibarchive 支持 zip
log(`extracting (unzip) → ${destDir}`);
const r1 = spawnSync('unzip', ['-q', '-o', zipFile, '-d', destDir], {
stdio: ['ignore', 'inherit', 'inherit'],
});
if (r1.status === 0) return;
log(`unzip failed (status=${r1.status}); fallback tar`);
const r2 = spawnSync('tar', ['-xf', zipFile, '-C', destDir], {
stdio: ['ignore', 'inherit', 'inherit'],
});
if (r2.status === 0) return;
fail(`extract failed: unzip exit=${r1.status}, tar exit=${r2.status}`);
}
function findChromeDir(root) {
// 压缩包里 chrome.exe 可能在 root/ 或 root/<子目录>/
const direct = chromeExeIn(root);
if (fs.existsSync(direct)) return root;
const entries = fs.readdirSync(root, { withFileTypes: true });
for (const ent of entries) {
if (!ent.isDirectory()) continue;
const sub = path.join(root, ent.name);
if (fs.existsSync(chromeExeIn(sub))) return sub;
}
return null;
}
function moveContents(srcDir, targetDir) {
fs.mkdirSync(targetDir, { recursive: true });
for (const name of fs.readdirSync(srcDir)) {
const from = path.join(srcDir, name);
const to = path.join(targetDir, name);
try {
fs.rmSync(to, { recursive: true, force: true });
} catch {}
fs.renameSync(from, to);
}
}
function rmrf(p) {
try {
fs.rmSync(p, { recursive: true, force: true });
} catch {}
}
async function main() {
const targetDir = resolveTargetDir();
if (alreadyInstalled(targetDir)) {
log(`already installed: ${chromeExeIn(targetDir)}`);
return;
}
if (process.env.NETA_CHROMIUM_PATH) {
// 用户显式指定路径但文件缺失 — 不自动下载到别处,直接报错
fail(
`NETA_CHROMIUM_PATH=${process.env.NETA_CHROMIUM_PATH} but file missing. ` +
`Unset it to trigger auto-download to the default location.`
);
}
const url = process.env.NETA_CHROMIUM_URL || DEFAULT_URL;
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'neta-chromium-'));
const zipFile = path.join(tmpRoot, 'chromium.zip');
const extractDir = path.join(tmpRoot, 'extract');
try {
await download(url, zipFile);
extractZip(zipFile, extractDir);
const chromeDir = findChromeDir(extractDir);
if (!chromeDir) {
fail(`chrome.exe not found in extracted archive: ${extractDir}`);
}
log(`moving ${chromeDir}${targetDir}`);
moveContents(chromeDir, targetDir);
if (!alreadyInstalled(targetDir)) {
fail(`post-install check failed: ${chromeExeIn(targetDir)} missing or too small`);
}
log(`installed ok → ${chromeExeIn(targetDir)}`);
} finally {
rmrf(tmpRoot);
}
}
main().catch((err) => {
fail(err && err.stack ? err.stack : String(err));
});