236 lines
7.2 KiB
JavaScript
236 lines
7.2 KiB
JavaScript
#!/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,缺失再回落 tar(libarchive 支持 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));
|
||
});
|