From fbd62fe39044c871d37f001ff6b57985ebc9d85b Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Tue, 24 Mar 2026 16:02:31 +0800 Subject: [PATCH] feat: integrate Playwright download dialog into startup flow --- src/main/bootstrap/runtime.ts | 20 +++++------ src/main/index.ts | 3 +- src/main/ipc/playwright-browser.ts | 42 ++++++++++++++++++++++- src/preload/api/browser-download.ts | 4 +++ src/preload/index.d.ts | 1 + src/renderer/src/App.tsx | 18 ++++++++++ src/renderer/src/hooks/useAppBootstrap.ts | 26 +++++++++++++- src/shared/ipc-channels.ts | 3 +- 8 files changed, 101 insertions(+), 16 deletions(-) diff --git a/src/main/bootstrap/runtime.ts b/src/main/bootstrap/runtime.ts index c441980..8a5e46f 100644 --- a/src/main/bootstrap/runtime.ts +++ b/src/main/bootstrap/runtime.ts @@ -31,7 +31,11 @@ export function setupElectronRuntime(): void { }) } -export function ensurePlaywrightRuntime(browsersPath: string): void { +/** + * Check if Playwright browsers are installed + * @returns true if browsers exist, false otherwise + */ +export function ensurePlaywrightRuntime(browsersPath: string): boolean { try { fs.mkdirSync(browsersPath, { recursive: true }) } catch (error) { @@ -43,7 +47,7 @@ export function ensurePlaywrightRuntime(browsersPath: string): void { const chromiumPath = fs.existsSync(newChromiumPath) ? newChromiumPath : oldChromiumPath if (fs.existsSync(chromiumPath)) { - return + return true } let foundRevision = false @@ -64,22 +68,14 @@ export function ensurePlaywrightRuntime(browsersPath: string): void { } if (foundRevision) { - return + return true } - dialog.showErrorBox( - '浏览器文件未找到', - `Playwright 浏览器文件不存在。\n\n` + - `期望路径:${newChromiumPath}\n` + - `或:${oldChromiumPath}\n\n` + - `当前目录内容:${fs.existsSync(browsersPath) ? fs.readdirSync(browsersPath).join(', ') : '目录不存在'}\n\n` + - `请运行以下命令安装浏览器:\n` + - `npx playwright install chromium` - ) console.warn( 'Playwright browser not found. Available:', fs.existsSync(browsersPath) ? fs.readdirSync(browsersPath) : 'none' ) + return false } export async function initializeMainProcessServices(): Promise { diff --git a/src/main/index.ts b/src/main/index.ts index 2d34fb4..7a6b52e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -12,7 +12,8 @@ app.whenReady().then(async () => { setupProcessGuards() registerMainWindowLifecycle() const playwrightBrowsersPath = configurePlaywrightBrowsersPath() - ensurePlaywrightRuntime(playwrightBrowsersPath) + const browsersExist = ensurePlaywrightRuntime(playwrightBrowsersPath) + console.log('Playwright browsers exist:', browsersExist) await initializeMainProcessServices() setupElectronRuntime() diff --git a/src/main/ipc/playwright-browser.ts b/src/main/ipc/playwright-browser.ts index 161e3b5..c7fb970 100644 --- a/src/main/ipc/playwright-browser.ts +++ b/src/main/ipc/playwright-browser.ts @@ -3,7 +3,8 @@ * Handles browser download requests from renderer process */ -import { ipcMain, IpcMainInvokeEvent } from 'electron' +import { app, ipcMain, IpcMainInvokeEvent } from 'electron' +import { join } from 'path' import { IPC_CHANNELS } from '../../shared/ipc-channels' import { withErrorHandling, type IpcResult } from './index' import { DownloadService } from '../services/playwright-browser' @@ -31,12 +32,51 @@ function createS3Client(): S3Client { }) } +/** + * Check if Playwright browsers are installed + */ +async function checkBrowsersExist(): Promise { + const fs = await import('fs') + const browsersPath = join(app.getPath('userData'), 'ms-playwright') + const newChromiumPath = join(browsersPath, 'chromium-1208', 'chrome-win64', 'chrome.exe') + const oldChromiumPath = join(browsersPath, 'chromium-win32', 'chrome.exe') + const chromiumPath = fs.default.existsSync(newChromiumPath) ? newChromiumPath : oldChromiumPath + + if (fs.default.existsSync(chromiumPath)) { + return true + } + + let foundRevision = false + try { + const entries = fs.default.readdirSync(browsersPath) + for (const entry of entries) { + if (entry.startsWith('chromium-') && !entry.includes('headless')) { + const revisionPath = join(browsersPath, entry, 'chrome-win64', 'chrome.exe') + if (fs.default.existsSync(revisionPath)) { + foundRevision = true + break + } + } + } + } catch { + // Ignore browser directory probing failures + } + + return foundRevision +} + /** * Track active download for cancellation */ let activeDownload: { service: DownloadService; cancelled: boolean } | null = null export function registerPlaywrightBrowserHandlers(): void { + ipcMain.handle(IPC_CHANNELS.PLAYWRIGHT_BROWSER_CHECK, async (): Promise> => { + return withErrorHandling(async () => { + return checkBrowsersExist() + }, 'playwright-browser:check') + }) + ipcMain.handle( IPC_CHANNELS.PLAYWRIGHT_BROWSER_DOWNLOAD, async (event: IpcMainInvokeEvent): Promise> => { diff --git a/src/preload/api/browser-download.ts b/src/preload/api/browser-download.ts index 4b251e1..f7ef215 100644 --- a/src/preload/api/browser-download.ts +++ b/src/preload/api/browser-download.ts @@ -4,6 +4,10 @@ import type { IpcResult } from '../../main/types/ipc.types' import type { DownloadProgress } from '../index.d' export const playwrightBrowserApi = { + check: async (): Promise> => { + return ipcRenderer.invoke(IPC_CHANNELS.PLAYWRIGHT_BROWSER_CHECK) + }, + download: async (): Promise> => { return ipcRenderer.invoke(IPC_CHANNELS.PLAYWRIGHT_BROWSER_DOWNLOAD) }, diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 12aff8a..3d6142c 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -151,6 +151,7 @@ export interface DownloadProgress { } export interface PlaywrightBrowserAPI { + check: () => Promise> download: () => Promise> cancel: () => Promise> onProgress: (callback: (data: DownloadProgress) => void) => () => void diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index fc59b79..aff2ed5 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,6 +1,7 @@ import React from 'react' import { AuthenticatedAppShell } from './components/app/AuthenticatedAppShell' import { UnauthenticatedApp } from './components/app/UnauthenticatedApp' +import PlaywrightDownloadDialog from './components/PlaywrightDownloadDialog' import { useAppBootstrap } from './hooks/useAppBootstrap' function App(): React.JSX.Element { @@ -22,6 +23,8 @@ function App(): React.JSX.Element { updateCatalog, showUpdateDialog, setShowUpdateDialog, + showPlaywrightDownload, + setShowPlaywrightDownload, showError, handleLogin, handleLoginCancel, @@ -34,8 +37,23 @@ function App(): React.JSX.Element { refreshUpdateDialogState } = useAppBootstrap() + const handlePlaywrightDownloadComplete = React.useCallback(() => { + setShowPlaywrightDownload(false) + }, [setShowPlaywrightDownload]) + const shouldShowLogout = currentUser?.userType === 'Admin' || isSwitchedByAdmin + // Show Playwright download dialog first (before authentication check) + if (showPlaywrightDownload) { + return ( + {}} + onDownloadComplete={handlePlaywrightDownloadComplete} + /> + ) + } + if (!isAuthenticated) { return ( (null) const [updateCatalog, setUpdateCatalog] = useState(null) const [showUpdateDialog, setShowUpdateDialog] = useState(false) + const [showPlaywrightDownload, setShowPlaywrightDownload] = useState(false) const authInitializationStartedRef = useRef(false) @@ -120,7 +121,28 @@ export function useAppBootstrap() { authInitializationStartedRef.current = true logger.info('=== Initializing auth... ===') - void initializeAuth() + + // Check if Playwright browsers are installed + const checkPlaywrightBrowsers = async () => { + try { + const result = await window.electron.playwrightBrowser.check() + if (result.success && !result.data) { + logger.info('Playwright browsers not found, showing download dialog') + setShowPlaywrightDownload(true) + } else { + logger.info('Playwright browsers found, continuing auth') + void initializeAuth() + } + } catch (error) { + logger.error('Failed to check Playwright browsers', { + error: error instanceof Error ? error.message : String(error) + }) + // Continue with auth even if check fails + void initializeAuth() + } + } + + void checkPlaywrightBrowsers() }, [initializeAuth, logger]) useEffect(() => { @@ -295,6 +317,8 @@ export function useAppBootstrap() { updateCatalog, showUpdateDialog, setShowUpdateDialog, + showPlaywrightDownload, + setShowPlaywrightDownload, showError, refreshUpdateState, refreshUpdateCatalog, diff --git a/src/shared/ipc-channels.ts b/src/shared/ipc-channels.ts index 957be3e..ca267ce 100644 --- a/src/shared/ipc-channels.ts +++ b/src/shared/ipc-channels.ts @@ -110,7 +110,8 @@ export const IPC_CHANNELS = { // Playwright Browser PLAYWRIGHT_BROWSER_DOWNLOAD: 'playwright-browser:download', PLAYWRIGHT_BROWSER_CANCEL: 'playwright-browser:cancel', - PLAYWRIGHT_BROWSER_PROGRESS: 'playwright-browser:progress' + PLAYWRIGHT_BROWSER_PROGRESS: 'playwright-browser:progress', + PLAYWRIGHT_BROWSER_CHECK: 'playwright-browser:check' } as const /**