#!/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)); });