|
|
@@ -68,10 +68,140 @@ function getServerDir(): string {
|
|
|
return path.resolve(__dirname, '..', '..', 'server');
|
|
|
}
|
|
|
|
|
|
-function getPlaywrightBrowsersPath(): string | null {
|
|
|
- if (!isPackaged()) return null;
|
|
|
- const bundled = path.join((process as any).resourcesPath, 'playwright');
|
|
|
- return fs.existsSync(bundled) ? bundled : null;
|
|
|
+// 用户目录下的 Playwright 浏览器存放位置
|
|
|
+function getUserPlaywrightBrowsersPath(): string {
|
|
|
+ return path.join(app.getPath('userData'), 'playwright-browsers');
|
|
|
+}
|
|
|
+
|
|
|
+function getPlaywrightBrowsersPath(): string {
|
|
|
+ // 1) 已打包:固定使用用户目录,避免污染系统缓存;
|
|
|
+ // 2) 开发环境:返回用户目录或系统默认(Playwright 会自动 fallback)。
|
|
|
+ return getUserPlaywrightBrowsersPath();
|
|
|
+}
|
|
|
+
|
|
|
+function getCloakBrowserCacheDir(): string {
|
|
|
+ return path.join(app.getPath('userData'), 'cloakbrowser');
|
|
|
+}
|
|
|
+
|
|
|
+// 检查 Chromium 是否已下载到用户目录
|
|
|
+function isChromiumInstalled(): boolean {
|
|
|
+ const dir = getUserPlaywrightBrowsersPath();
|
|
|
+ if (!fs.existsSync(dir)) return false;
|
|
|
+ try {
|
|
|
+ const entries = fs.readdirSync(dir);
|
|
|
+ return entries.some((name: string) => name.startsWith('chromium'));
|
|
|
+ } catch {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 进度回调:phase 取值:
|
|
|
+// 'start' 开始下载
|
|
|
+// 'progress' 下载进度(percent 0-100)
|
|
|
+// 'done' 完成(ok 表示成功与否)
|
|
|
+export type PlaywrightInstallProgress =
|
|
|
+ | { phase: 'start'; message?: string }
|
|
|
+ | { phase: 'progress'; percent: number; message?: string }
|
|
|
+ | { phase: 'done'; ok: boolean; message?: string };
|
|
|
+
|
|
|
+let onPlaywrightProgress: ((p: PlaywrightInstallProgress) => void) | null = null;
|
|
|
+function setPlaywrightProgressHandler(handler: (p: PlaywrightInstallProgress) => void): void {
|
|
|
+ onPlaywrightProgress = handler;
|
|
|
+}
|
|
|
+function emitProgress(p: PlaywrightInstallProgress): void {
|
|
|
+ try {
|
|
|
+ onPlaywrightProgress?.(p);
|
|
|
+ } catch {
|
|
|
+ // ignore handler errors
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 解析 playwright CLI 输出中的下载百分比,例如:
|
|
|
+// "|████████████████ | 65% of 130.5 MiB"
|
|
|
+function parsePercent(line: string): number | null {
|
|
|
+ const m = line.match(/(\d{1,3})%/);
|
|
|
+ if (!m) return null;
|
|
|
+ const v = Number(m[1]);
|
|
|
+ if (Number.isNaN(v) || v < 0 || v > 100) return null;
|
|
|
+ return v;
|
|
|
+}
|
|
|
+
|
|
|
+// 触发 Node 子进程下载 Chromium
|
|
|
+function installChromiumOnFirstRun(): Promise<boolean> {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ if (isChromiumInstalled()) {
|
|
|
+ log('INFO', 'Playwright Chromium 已存在,跳过下载');
|
|
|
+ resolve(true);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const serverDir = getServerDir();
|
|
|
+ const browsersPath = getUserPlaywrightBrowsersPath();
|
|
|
+ fs.mkdirSync(browsersPath, { recursive: true });
|
|
|
+
|
|
|
+ const cliEntry = path.join(serverDir, 'node_modules', 'playwright', 'cli.js');
|
|
|
+ if (!fs.existsSync(cliEntry)) {
|
|
|
+ log('ERROR', 'playwright/cli.js not found, cannot install chromium');
|
|
|
+ emitProgress({ phase: 'done', ok: false, message: '未找到 playwright CLI' });
|
|
|
+ resolve(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ log('INFO', '首次启动:开始下载 Playwright Chromium 到', browsersPath);
|
|
|
+ emitProgress({ phase: 'start', message: '开始下载 Chromium 浏览器...' });
|
|
|
+
|
|
|
+ const child = spawn(process.execPath, [cliEntry, 'install', 'chromium'], {
|
|
|
+ cwd: serverDir,
|
|
|
+ env: {
|
|
|
+ ...(process.env as Record<string, string>),
|
|
|
+ ELECTRON_RUN_AS_NODE: '1',
|
|
|
+ PLAYWRIGHT_BROWSERS_PATH: browsersPath,
|
|
|
+ // 让 playwright 输出更详细的进度
|
|
|
+ DEBUG: process.env.DEBUG || '',
|
|
|
+ },
|
|
|
+ stdio: ['ignore', 'pipe', 'pipe'],
|
|
|
+ windowsHide: true,
|
|
|
+ });
|
|
|
+
|
|
|
+ let lastPercent = -1;
|
|
|
+ const handleLine = (raw: string): void => {
|
|
|
+ const line = raw.trim();
|
|
|
+ if (!line) return;
|
|
|
+ log('PW', line);
|
|
|
+ const percent = parsePercent(line);
|
|
|
+ if (percent !== null && percent !== lastPercent) {
|
|
|
+ lastPercent = percent;
|
|
|
+ emitProgress({ phase: 'progress', percent, message: line });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ child.stdout?.on('data', (chunk: Buffer) => chunk.toString().split(/\r?\n/).forEach(handleLine));
|
|
|
+ child.stderr?.on('data', (chunk: Buffer) => {
|
|
|
+ // playwright 通常把进度写到 stderr
|
|
|
+ chunk.toString().split(/\r?\n/).forEach((l: string) => {
|
|
|
+ const line = l.trim();
|
|
|
+ if (!line) return;
|
|
|
+ log('PW-ERR', line);
|
|
|
+ const percent = parsePercent(line);
|
|
|
+ if (percent !== null && percent !== lastPercent) {
|
|
|
+ lastPercent = percent;
|
|
|
+ emitProgress({ phase: 'progress', percent, message: line });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ child.on('exit', (code: number | null) => {
|
|
|
+ const ok = code === 0 && isChromiumInstalled();
|
|
|
+ log(ok ? 'INFO' : 'ERROR', `Playwright Chromium install exit code=${code}`);
|
|
|
+ emitProgress({ phase: 'done', ok, message: ok ? '浏览器下载完成' : `下载失败 (exit ${code})` });
|
|
|
+ resolve(ok);
|
|
|
+ });
|
|
|
+ child.on('error', (err: Error) => {
|
|
|
+ log('ERROR', 'Playwright install spawn error', err.message);
|
|
|
+ emitProgress({ phase: 'done', ok: false, message: err.message });
|
|
|
+ resolve(false);
|
|
|
+ });
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
function findNodeRunner(): { cmd: string; args: string[]; useElectronAsNode?: boolean } {
|
|
|
@@ -155,10 +285,10 @@ function startNodeServer(): void {
|
|
|
env.UPLOAD_PATH = uploadsDir;
|
|
|
}
|
|
|
|
|
|
- const playwrightPath = getPlaywrightBrowsersPath();
|
|
|
- if (playwrightPath) {
|
|
|
- env.PLAYWRIGHT_BROWSERS_PATH = playwrightPath;
|
|
|
+ if (isPackaged()) {
|
|
|
+ env.PLAYWRIGHT_BROWSERS_PATH = getPlaywrightBrowsersPath();
|
|
|
}
|
|
|
+ env.CLOAKBROWSER_CACHE_DIR = getCloakBrowserCacheDir();
|
|
|
|
|
|
log('INFO', 'Starting node service', cmd, args.join(' '));
|
|
|
const child = spawn(cmd, args, {
|
|
|
@@ -211,6 +341,15 @@ function startNodeServer(): void {
|
|
|
}
|
|
|
|
|
|
async function startLocalServices(): Promise<{ nodeOk: boolean }> {
|
|
|
+ // 在 packaged 模式下,首次启动时按需下载 Chromium
|
|
|
+ if (isPackaged()) {
|
|
|
+ try {
|
|
|
+ await installChromiumOnFirstRun();
|
|
|
+ } catch (err) {
|
|
|
+ log('ERROR', 'installChromiumOnFirstRun failed', err);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
if (!services.node.process) {
|
|
|
startNodeServer();
|
|
|
}
|
|
|
@@ -250,4 +389,5 @@ export {
|
|
|
stopLocalServices,
|
|
|
getServiceStatus,
|
|
|
getLogPath,
|
|
|
+ setPlaywrightProgressHandler,
|
|
|
};
|