feat: add TypeORM, logger, schemas, hooks and stores
- Add TypeORM integration with data-source, entities and repositories - Add logger service for structured logging - Add Zod validation schemas for auth, cleaner and extractor - Add custom React hooks (useAuth, useCleaner, useExtractor, useValidation) - Add Zustand stores (useAppStore, useUserStore) - Add UI components (Button, Modal, Toast) - Add error types and ErpBrowserManager - Refactor IPC handlers and services - Add unit tests for new modules Co-Authored-By: Claude (glm-5) <noreply@anthropic.com>
This commit is contained in:
208
src/main/services/erp/ErpBrowserManager.ts
Normal file
208
src/main/services/erp/ErpBrowserManager.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* ERP Browser Manager
|
||||
*
|
||||
* Manages browser lifecycle for ERP automation.
|
||||
* Separates browser management from authentication logic.
|
||||
*/
|
||||
|
||||
import { chromium, type Browser, type BrowserContext, type Page } from 'playwright'
|
||||
import { createLogger } from '../logger'
|
||||
|
||||
const log = createLogger('ErpBrowserManager')
|
||||
|
||||
/**
|
||||
* Browser configuration options
|
||||
*/
|
||||
export interface BrowserConfig {
|
||||
headless?: boolean
|
||||
slowMo?: number
|
||||
viewport?: { width: number; height: number }
|
||||
ignoreHTTPSErrors?: boolean
|
||||
acceptDownloads?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Default browser configuration
|
||||
*/
|
||||
const DEFAULT_CONFIG: Required<BrowserConfig> = {
|
||||
headless: false,
|
||||
slowMo: 100,
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
ignoreHTTPSErrors: true,
|
||||
acceptDownloads: true
|
||||
}
|
||||
|
||||
/**
|
||||
* Browser session containing all browser-related objects
|
||||
*/
|
||||
export interface BrowserSession {
|
||||
browser: Browser
|
||||
context: BrowserContext
|
||||
page: Page
|
||||
}
|
||||
|
||||
/**
|
||||
* ErpBrowserManager class
|
||||
* Manages browser lifecycle independently from ERP authentication
|
||||
*/
|
||||
export class ErpBrowserManager {
|
||||
private config: Required<BrowserConfig>
|
||||
private session: BrowserSession | null = null
|
||||
|
||||
constructor(config?: BrowserConfig) {
|
||||
this.config = { ...DEFAULT_CONFIG, ...config }
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch a new browser instance
|
||||
*/
|
||||
async launch(): Promise<Browser> {
|
||||
if (this.session?.browser?.isConnected()) {
|
||||
log.debug('Browser already running, returning existing instance')
|
||||
return this.session.browser
|
||||
}
|
||||
|
||||
log.info('Launching browser', { headless: this.config.headless })
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: this.config.headless,
|
||||
slowMo: this.config.slowMo,
|
||||
args: [
|
||||
'--ignore-certificate-errors',
|
||||
'--ignore-ssl-errors',
|
||||
'--ignore-certificate-errors-spki-list',
|
||||
'--disable-web-security'
|
||||
]
|
||||
})
|
||||
|
||||
log.info('Browser launched successfully')
|
||||
return browser
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new browser context
|
||||
*/
|
||||
async createContext(browser?: Browser): Promise<BrowserContext> {
|
||||
const browserInstance = browser || (await this.launch())
|
||||
|
||||
log.debug('Creating browser context')
|
||||
|
||||
const context = await browserInstance.newContext({
|
||||
acceptDownloads: this.config.acceptDownloads,
|
||||
viewport: this.config.viewport,
|
||||
ignoreHTTPSErrors: this.config.ignoreHTTPSErrors,
|
||||
javaScriptEnabled: true
|
||||
})
|
||||
|
||||
log.debug('Browser context created')
|
||||
return context
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new page in the context
|
||||
*/
|
||||
async createPage(context?: BrowserContext): Promise<Page> {
|
||||
let contextInstance: BrowserContext
|
||||
|
||||
if (context) {
|
||||
contextInstance = context
|
||||
} else if (this.session?.context) {
|
||||
contextInstance = this.session.context
|
||||
} else {
|
||||
const browser = await this.launch()
|
||||
contextInstance = await this.createContext(browser)
|
||||
}
|
||||
|
||||
log.debug('Creating new page')
|
||||
const page = await contextInstance.newPage()
|
||||
log.debug('Page created')
|
||||
|
||||
return page
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a complete browser session
|
||||
* This creates browser, context, and page in one call
|
||||
*/
|
||||
async initialize(): Promise<BrowserSession> {
|
||||
if (this.session) {
|
||||
log.debug('Returning existing browser session')
|
||||
return this.session
|
||||
}
|
||||
|
||||
const browser = await this.launch()
|
||||
const context = await this.createContext(browser)
|
||||
const page = await this.createPage(context)
|
||||
|
||||
this.session = { browser, context, page }
|
||||
log.info('Browser session initialized')
|
||||
|
||||
return this.session
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current session
|
||||
*/
|
||||
getSession(): BrowserSession | null {
|
||||
return this.session
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if browser is running
|
||||
*/
|
||||
isRunning(): boolean {
|
||||
return this.session?.browser?.isConnected() ?? false
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the browser and cleanup
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
if (!this.session) {
|
||||
log.debug('No browser session to close')
|
||||
return
|
||||
}
|
||||
|
||||
log.info('Closing browser session')
|
||||
|
||||
try {
|
||||
if (this.session.context) {
|
||||
await this.session.context.close()
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn('Error closing context', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.session.browser) {
|
||||
await this.session.browser.close()
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn('Error closing browser', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
}
|
||||
|
||||
this.session = null
|
||||
log.info('Browser session closed')
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a URL
|
||||
*/
|
||||
async navigate(url: string, options?: { timeout?: number }): Promise<void> {
|
||||
const page = this.session?.page
|
||||
if (!page) {
|
||||
throw new Error('No page available. Call initialize() first.')
|
||||
}
|
||||
|
||||
log.info('Navigating to URL', { url })
|
||||
await page.goto(url, { timeout: options?.timeout ?? 30000 })
|
||||
await page.waitForLoadState('domcontentloaded', { timeout: options?.timeout ?? 10000 })
|
||||
log.debug('Page loaded')
|
||||
}
|
||||
}
|
||||
|
||||
export default ErpBrowserManager
|
||||
@@ -1,5 +1,8 @@
|
||||
import { chromium, type BrowserContext, type Page } from 'playwright'
|
||||
import type { ErpConfig, ErpSession } from '../../types/erp.types'
|
||||
import { createLogger } from '../logger'
|
||||
|
||||
const log = createLogger('ErpAuthService')
|
||||
|
||||
/**
|
||||
* ERP Authentication Service
|
||||
@@ -91,7 +94,7 @@ export class ErpAuthService {
|
||||
try {
|
||||
await page.waitForLoadState('domcontentloaded', { timeout: 10000 })
|
||||
} catch (e) {
|
||||
console.log('Page load state check timed out, continuing...')
|
||||
log.warn('Page load state check timed out, continuing')
|
||||
}
|
||||
|
||||
// Handle force login confirmation dialog if present (Python: get_by_role("button", name="确定"))
|
||||
@@ -99,15 +102,15 @@ export class ErpAuthService {
|
||||
const confirmBtn = mainFrame.getByRole('button', { name: '确定' })
|
||||
const count = await confirmBtn.count()
|
||||
if (count > 0) {
|
||||
console.log('Force login detected, clicking confirm button')
|
||||
log.info('Force login detected, clicking confirm button')
|
||||
await confirmBtn.first().click()
|
||||
await page.waitForTimeout(2000)
|
||||
} else {
|
||||
console.log('Normal login, no confirmation dialog')
|
||||
log.debug('Normal login, no confirmation dialog')
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// No force login dialog, continue
|
||||
console.log('Normal login, no confirmation dialog')
|
||||
log.debug('Normal login, no confirmation dialog')
|
||||
}
|
||||
|
||||
// Create session with mainFrame (Python returns main_frame as part of login result)
|
||||
|
||||
@@ -156,7 +156,7 @@ export class OrderNumberResolver {
|
||||
// Update mappings with database results
|
||||
for (const mapping of mappings) {
|
||||
if (mapping.inputType === 'productionId') {
|
||||
const dbResult = productionIdMappings.find(m => m.input === mapping.input)
|
||||
const dbResult = productionIdMappings.find((m) => m.input === mapping.input)
|
||||
if (dbResult) {
|
||||
mapping.orderNumber = dbResult.orderNumber
|
||||
mapping.isValid = dbResult.isValid
|
||||
@@ -265,7 +265,7 @@ export class OrderNumberResolver {
|
||||
const message = error instanceof Error ? error.message : '未知数据库错误'
|
||||
|
||||
// Return all as failed with error
|
||||
return productionIds.map(pid => ({
|
||||
return productionIds.map((pid) => ({
|
||||
input: pid,
|
||||
productionId: pid,
|
||||
isValid: false,
|
||||
@@ -294,7 +294,7 @@ export class OrderNumberResolver {
|
||||
`
|
||||
|
||||
const result = await this.mysqlService.query(query, orderNumbers)
|
||||
return result.rows.map(row => row[DB_CONFIG.FIELD_ORDER_NUMBER] as string)
|
||||
return result.rows.map((row) => row[DB_CONFIG.FIELD_ORDER_NUMBER] as string)
|
||||
} catch (error) {
|
||||
console.warn('[OrderResolver] Failed to verify order numbers:', error)
|
||||
return orderNumbers // Skip verification on error
|
||||
@@ -345,9 +345,7 @@ export class OrderNumberResolver {
|
||||
* @returns List of valid production order numbers
|
||||
*/
|
||||
getValidOrderNumbers(mappings: OrderMapping[]): string[] {
|
||||
return mappings
|
||||
.filter(m => m.isValid && m.orderNumber)
|
||||
.map(m => m.orderNumber!)
|
||||
return mappings.filter((m) => m.isValid && m.orderNumber).map((m) => m.orderNumber!)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,8 +354,6 @@ export class OrderNumberResolver {
|
||||
* @returns List of warning messages
|
||||
*/
|
||||
getWarnings(mappings: OrderMapping[]): string[] {
|
||||
return mappings
|
||||
.filter(m => !m.isValid && m.error)
|
||||
.map(m => m.error!)
|
||||
return mappings.filter((m) => !m.isValid && m.error).map((m) => m.error!)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user