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));
|
|||
|
|
});
|