feat: integrate Playwright download dialog into startup flow

This commit is contained in:
Misaka_Company
2026-03-24 16:02:31 +08:00
parent a711781f21
commit fbd62fe390
8 changed files with 101 additions and 16 deletions

View File

@@ -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 { try {
fs.mkdirSync(browsersPath, { recursive: true }) fs.mkdirSync(browsersPath, { recursive: true })
} catch (error) { } catch (error) {
@@ -43,7 +47,7 @@ export function ensurePlaywrightRuntime(browsersPath: string): void {
const chromiumPath = fs.existsSync(newChromiumPath) ? newChromiumPath : oldChromiumPath const chromiumPath = fs.existsSync(newChromiumPath) ? newChromiumPath : oldChromiumPath
if (fs.existsSync(chromiumPath)) { if (fs.existsSync(chromiumPath)) {
return return true
} }
let foundRevision = false let foundRevision = false
@@ -64,22 +68,14 @@ export function ensurePlaywrightRuntime(browsersPath: string): void {
} }
if (foundRevision) { 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( console.warn(
'Playwright browser not found. Available:', 'Playwright browser not found. Available:',
fs.existsSync(browsersPath) ? fs.readdirSync(browsersPath) : 'none' fs.existsSync(browsersPath) ? fs.readdirSync(browsersPath) : 'none'
) )
return false
} }
export async function initializeMainProcessServices(): Promise<void> { export async function initializeMainProcessServices(): Promise<void> {

View File

@@ -12,7 +12,8 @@ app.whenReady().then(async () => {
setupProcessGuards() setupProcessGuards()
registerMainWindowLifecycle() registerMainWindowLifecycle()
const playwrightBrowsersPath = configurePlaywrightBrowsersPath() const playwrightBrowsersPath = configurePlaywrightBrowsersPath()
ensurePlaywrightRuntime(playwrightBrowsersPath) const browsersExist = ensurePlaywrightRuntime(playwrightBrowsersPath)
console.log('Playwright browsers exist:', browsersExist)
await initializeMainProcessServices() await initializeMainProcessServices()
setupElectronRuntime() setupElectronRuntime()

View File

@@ -3,7 +3,8 @@
* Handles browser download requests from renderer process * 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 { IPC_CHANNELS } from '../../shared/ipc-channels'
import { withErrorHandling, type IpcResult } from './index' import { withErrorHandling, type IpcResult } from './index'
import { DownloadService } from '../services/playwright-browser' import { DownloadService } from '../services/playwright-browser'
@@ -31,12 +32,51 @@ function createS3Client(): S3Client {
}) })
} }
/**
* Check if Playwright browsers are installed
*/
async function checkBrowsersExist(): Promise<boolean> {
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 * Track active download for cancellation
*/ */
let activeDownload: { service: DownloadService; cancelled: boolean } | null = null let activeDownload: { service: DownloadService; cancelled: boolean } | null = null
export function registerPlaywrightBrowserHandlers(): void { export function registerPlaywrightBrowserHandlers(): void {
ipcMain.handle(IPC_CHANNELS.PLAYWRIGHT_BROWSER_CHECK, async (): Promise<IpcResult<boolean>> => {
return withErrorHandling(async () => {
return checkBrowsersExist()
}, 'playwright-browser:check')
})
ipcMain.handle( ipcMain.handle(
IPC_CHANNELS.PLAYWRIGHT_BROWSER_DOWNLOAD, IPC_CHANNELS.PLAYWRIGHT_BROWSER_DOWNLOAD,
async (event: IpcMainInvokeEvent): Promise<IpcResult<void>> => { async (event: IpcMainInvokeEvent): Promise<IpcResult<void>> => {

View File

@@ -4,6 +4,10 @@ import type { IpcResult } from '../../main/types/ipc.types'
import type { DownloadProgress } from '../index.d' import type { DownloadProgress } from '../index.d'
export const playwrightBrowserApi = { export const playwrightBrowserApi = {
check: async (): Promise<IpcResult<boolean>> => {
return ipcRenderer.invoke(IPC_CHANNELS.PLAYWRIGHT_BROWSER_CHECK)
},
download: async (): Promise<IpcResult<void>> => { download: async (): Promise<IpcResult<void>> => {
return ipcRenderer.invoke(IPC_CHANNELS.PLAYWRIGHT_BROWSER_DOWNLOAD) return ipcRenderer.invoke(IPC_CHANNELS.PLAYWRIGHT_BROWSER_DOWNLOAD)
}, },

View File

@@ -151,6 +151,7 @@ export interface DownloadProgress {
} }
export interface PlaywrightBrowserAPI { export interface PlaywrightBrowserAPI {
check: () => Promise<IpcResult<boolean>>
download: () => Promise<IpcResult<void>> download: () => Promise<IpcResult<void>>
cancel: () => Promise<IpcResult<void>> cancel: () => Promise<IpcResult<void>>
onProgress: (callback: (data: DownloadProgress) => void) => () => void onProgress: (callback: (data: DownloadProgress) => void) => () => void

View File

@@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import { AuthenticatedAppShell } from './components/app/AuthenticatedAppShell' import { AuthenticatedAppShell } from './components/app/AuthenticatedAppShell'
import { UnauthenticatedApp } from './components/app/UnauthenticatedApp' import { UnauthenticatedApp } from './components/app/UnauthenticatedApp'
import PlaywrightDownloadDialog from './components/PlaywrightDownloadDialog'
import { useAppBootstrap } from './hooks/useAppBootstrap' import { useAppBootstrap } from './hooks/useAppBootstrap'
function App(): React.JSX.Element { function App(): React.JSX.Element {
@@ -22,6 +23,8 @@ function App(): React.JSX.Element {
updateCatalog, updateCatalog,
showUpdateDialog, showUpdateDialog,
setShowUpdateDialog, setShowUpdateDialog,
showPlaywrightDownload,
setShowPlaywrightDownload,
showError, showError,
handleLogin, handleLogin,
handleLoginCancel, handleLoginCancel,
@@ -34,8 +37,23 @@ function App(): React.JSX.Element {
refreshUpdateDialogState refreshUpdateDialogState
} = useAppBootstrap() } = useAppBootstrap()
const handlePlaywrightDownloadComplete = React.useCallback(() => {
setShowPlaywrightDownload(false)
}, [setShowPlaywrightDownload])
const shouldShowLogout = currentUser?.userType === 'Admin' || isSwitchedByAdmin const shouldShowLogout = currentUser?.userType === 'Admin' || isSwitchedByAdmin
// Show Playwright download dialog first (before authentication check)
if (showPlaywrightDownload) {
return (
<PlaywrightDownloadDialog
isOpen={showPlaywrightDownload}
onClose={() => {}}
onDownloadComplete={handlePlaywrightDownloadComplete}
/>
)
}
if (!isAuthenticated) { if (!isAuthenticated) {
return ( return (
<UnauthenticatedApp <UnauthenticatedApp

View File

@@ -37,6 +37,7 @@ export function useAppBootstrap() {
const [updateStatus, setUpdateStatus] = useState<UpdateStatus | null>(null) const [updateStatus, setUpdateStatus] = useState<UpdateStatus | null>(null)
const [updateCatalog, setUpdateCatalog] = useState<UpdateDialogCatalog | null>(null) const [updateCatalog, setUpdateCatalog] = useState<UpdateDialogCatalog | null>(null)
const [showUpdateDialog, setShowUpdateDialog] = useState(false) const [showUpdateDialog, setShowUpdateDialog] = useState(false)
const [showPlaywrightDownload, setShowPlaywrightDownload] = useState(false)
const authInitializationStartedRef = useRef(false) const authInitializationStartedRef = useRef(false)
@@ -120,7 +121,28 @@ export function useAppBootstrap() {
authInitializationStartedRef.current = true authInitializationStartedRef.current = true
logger.info('=== Initializing auth... ===') logger.info('=== Initializing auth... ===')
// 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() 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]) }, [initializeAuth, logger])
useEffect(() => { useEffect(() => {
@@ -295,6 +317,8 @@ export function useAppBootstrap() {
updateCatalog, updateCatalog,
showUpdateDialog, showUpdateDialog,
setShowUpdateDialog, setShowUpdateDialog,
showPlaywrightDownload,
setShowPlaywrightDownload,
showError, showError,
refreshUpdateState, refreshUpdateState,
refreshUpdateCatalog, refreshUpdateCatalog,

View File

@@ -110,7 +110,8 @@ export const IPC_CHANNELS = {
// Playwright Browser // Playwright Browser
PLAYWRIGHT_BROWSER_DOWNLOAD: 'playwright-browser:download', PLAYWRIGHT_BROWSER_DOWNLOAD: 'playwright-browser:download',
PLAYWRIGHT_BROWSER_CANCEL: 'playwright-browser:cancel', 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 } as const
/** /**