diff --git a/src/main/bootstrap/runtime.ts b/src/main/bootstrap/runtime.ts index ae3149c..d35176d 100644 --- a/src/main/bootstrap/runtime.ts +++ b/src/main/bootstrap/runtime.ts @@ -3,6 +3,9 @@ import fs from 'fs' import { join } from 'path' import { ConfigManager } from '../services/config/config-manager' import { UpdateService } from '../services/update/update-service' +import { createLogger } from '../services/logger' + +const log = createLogger('Bootstrap') export function configurePlaywrightBrowsersPath(): string { const browsersPath = join(app.getPath('userData'), 'ms-playwright') @@ -39,7 +42,7 @@ export function ensurePlaywrightRuntime(browsersPath: string): boolean { try { fs.mkdirSync(browsersPath, { recursive: true }) } catch (error) { - console.error('Failed to create browsers directory:', error) + log.error('Failed to create browsers directory', { error }) } const newChromiumPath = join(browsersPath, 'chromium-1208', 'chrome-win64', 'chrome.exe') @@ -57,7 +60,7 @@ export function ensurePlaywrightRuntime(browsersPath: string): boolean { if (entry.startsWith('chromium-') && !entry.includes('headless')) { const revisionPath = join(browsersPath, entry, 'chrome-win64', 'chrome.exe') if (fs.existsSync(revisionPath)) { - console.log('Found Chromium revision:', entry) + log.info('Found Chromium revision', { revision: entry }) foundRevision = true break } @@ -71,10 +74,10 @@ export function ensurePlaywrightRuntime(browsersPath: string): boolean { return true } - console.warn( - 'Playwright browser not found. Available:', - fs.existsSync(browsersPath) ? fs.readdirSync(browsersPath) : 'none' - ) + log.warn('Playwright browser not found', { + available: fs.existsSync(browsersPath) ? fs.readdirSync(browsersPath) : 'none', + browsersPath + }) return false } @@ -84,7 +87,7 @@ export async function initializeMainProcessServices(): Promise { await configManager.initialize() UpdateService.getInstance().initialize() } catch (error) { - console.error('Failed to initialize ConfigManager:', error) + log.error('Failed to initialize ConfigManager', { error }) } const { registerIpcHandlers } = await import('../ipc') diff --git a/src/main/index.ts b/src/main/index.ts index 7a6b52e..ea4b8ac 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -7,17 +7,20 @@ import { setupElectronRuntime } from './bootstrap/runtime' import { setupProcessGuards } from './bootstrap/process-guards' +import { createLogger } from './services/logger' + +const log = createLogger('App') app.whenReady().then(async () => { setupProcessGuards() registerMainWindowLifecycle() const playwrightBrowsersPath = configurePlaywrightBrowsersPath() const browsersExist = ensurePlaywrightRuntime(playwrightBrowsersPath) - console.log('Playwright browsers exist:', browsersExist) + log.info('Playwright browsers check', { browsersExist }) await initializeMainProcessServices() setupElectronRuntime() - ipcMain.on('ping', () => console.log('pong')) + ipcMain.on('ping', () => log.debug('pong')) createMainWindow() }) diff --git a/src/main/services/erp/ErpBrowserManager.ts b/src/main/services/erp/ErpBrowserManager.ts index f0a6276..cea42ce 100644 --- a/src/main/services/erp/ErpBrowserManager.ts +++ b/src/main/services/erp/ErpBrowserManager.ts @@ -195,6 +195,10 @@ export class ErpBrowserManager { async navigate(url: string, options?: { timeout?: number }): Promise { const page = this.session?.page if (!page) { + log.error('No page available for navigation', { + url, + hasSession: !!this.session + }) throw new Error('No page available. Call initialize() first.') } diff --git a/src/main/services/erp/cleaner.ts b/src/main/services/erp/cleaner.ts index 82f0fa5..ba420b4 100644 --- a/src/main/services/erp/cleaner.ts +++ b/src/main/services/erp/cleaner.ts @@ -4,6 +4,7 @@ import type { CleanerInput, CleanerResult, OrderCleanDetail } from '../../types/ import type { ErpSession } from '../../types/erp.types' import type { FrameLocator, Locator, Page } from 'playwright' import { createLogger, run, trackDuration } from '../logger' +import { capturePageContext } from './erp-error-context' const log = createLogger('CleanerService') @@ -505,7 +506,10 @@ export class CleanerService { } } - throw new Error('无法定位“备料计划”菜单项(可能菜单结构已变化)') + log.error('Failed to locate material plan menu item', { + attemptedSelectors: candidates.length + }) + throw new Error('无法定位”备料计划”菜单项(可能菜单结构已变化)') } private async processDetailPage(params: { @@ -527,6 +531,9 @@ export class CleanerService { const dFrame = await detailMainFrame.contentFrame() if (!dFrame) { + log.error('Failed to access detail page forward frame', { + ...(await capturePageContext(detailPage)) + }) throw new Error('Failed to access detail page forward frame') } @@ -535,6 +542,7 @@ export class CleanerService { const detailInnerFrame = await detailInnerLocator.contentFrame() if (!detailInnerFrame) { + log.error('Failed to access detail inner frame') throw new Error('Failed to access detail inner frame') } @@ -852,6 +860,7 @@ export class CleanerService { const rows = workFrame.locator('tbody tr') const rowCount = await rows.count() if (rowCount === 0) { + log.error('Retry query returned no results', { orderNumber, rowCount }) throw new Error('订单重试查询无结果') } diff --git a/src/main/services/erp/erp-auth.ts b/src/main/services/erp/erp-auth.ts index fa7631f..db79567 100644 --- a/src/main/services/erp/erp-auth.ts +++ b/src/main/services/erp/erp-auth.ts @@ -1,6 +1,7 @@ import { chromium } from 'playwright' import type { ErpConfig, ErpSession } from '../../types/erp.types' import { createLogger } from '../logger' +import { capturePageContext } from './erp-error-context' const log = createLogger('ErpAuthService') @@ -70,6 +71,9 @@ export class ErpAuthService { const contentFrame = await frameLocator.contentFrame() if (!contentFrame) { + log.error('Failed to access forwardFrame content frame', { + ...(await capturePageContext(page)) + }) throw new Error('Failed to access forwardFrame content frame') } @@ -80,6 +84,9 @@ export class ErpAuthService { try { await contentFrame.getByRole('textbox', { name: '用户名' }).fill(this.config.username) } catch (e) { + log.error('Failed to find username input', { + error: e instanceof Error ? e.message : String(e) + }) throw new Error(`Failed to find username input: ${e}`) } @@ -87,6 +94,9 @@ export class ErpAuthService { try { await contentFrame.getByRole('textbox', { name: '密码' }).fill(this.config.password) } catch (e) { + log.error('Failed to find password input', { + error: e instanceof Error ? e.message : String(e) + }) throw new Error(`Failed to find password input: ${e}`) } @@ -94,6 +104,9 @@ export class ErpAuthService { try { await contentFrame.getByRole('button', { name: '登录' }).click() } catch (e) { + log.error('Failed to click login button', { + error: e instanceof Error ? e.message : String(e) + }) throw new Error(`Failed to click login button: ${e}`) } @@ -138,6 +151,7 @@ export class ErpAuthService { const hasError = await errorLocator.isVisible() if (hasError) { + log.error('ERP login failed: incorrect username or password') throw new Error('ERP 登录失败:名称或密码错误') } @@ -149,6 +163,7 @@ export class ErpAuthService { const hasError = await errorLocator.isVisible().catch(() => false) if (hasError) { + log.error('ERP login failed: incorrect username or password (retry check)') throw new Error('ERP 登录失败:名称或密码错误') } @@ -172,6 +187,7 @@ export class ErpAuthService { */ getSession(): ErpSession { if (!this.session?.isLoggedIn) { + log.error('getSession called without active session') throw new Error('Not logged in. Call login() first.') } return this.session diff --git a/src/main/services/erp/erp-error-context.ts b/src/main/services/erp/erp-error-context.ts new file mode 100644 index 0000000..0bbdacd --- /dev/null +++ b/src/main/services/erp/erp-error-context.ts @@ -0,0 +1,47 @@ +/** + * ERP Error Context Capture + * + * Lightweight helper to capture Playwright page state when ERP operations fail. + * All capture calls are defensive — failures do not propagate to the caller. + */ + +import type { Page } from 'playwright' + +export interface ErpErrorContext { + pageUrl?: string + frameHierarchy?: Array<{ name: string; url: string }> + targetSelector?: string +} + +/** + * Capture the current state of a Playwright page for error logging. + * Returns a plain object safe for structured logging. + * + * @param page - The Playwright page to inspect + * @param targetSelector - Optional selector that was being targeted + */ +export async function capturePageContext( + page: Page, + targetSelector?: string +): Promise { + const ctx: ErpErrorContext = {} + + try { + ctx.pageUrl = page.url() + } catch { + // page may be closed or inaccessible + } + + try { + const frames = page.frames() + ctx.frameHierarchy = frames.map((f) => ({ name: f.name(), url: f.url() })) + } catch { + // frame enumeration may fail on detached pages + } + + if (targetSelector) { + ctx.targetSelector = targetSelector + } + + return ctx +} diff --git a/src/main/services/erp/extractor-core.ts b/src/main/services/erp/extractor-core.ts index 9a81526..2b6a7c8 100644 --- a/src/main/services/erp/extractor-core.ts +++ b/src/main/services/erp/extractor-core.ts @@ -6,6 +6,10 @@ import type { ExtractorCoreResult, ExtractionProgress } from '../../types/extractor.types' +import { createLogger } from '../logger' +import { capturePageContext } from './erp-error-context' + +const log = createLogger('ExtractorCore') /** * ExtractorCore - Handles all web page operations for data extraction @@ -97,6 +101,9 @@ export class ExtractorCore { const fFrame = await forwardFrameLocator.contentFrame() if (!fFrame) { + log.error('Failed to access popup forward frame', { + ...(await capturePageContext(popupPage)) + }) throw new Error('Failed to access popup forward frame') } @@ -105,6 +112,7 @@ export class ExtractorCore { const workFrame = await innerFrameLocator.contentFrame() if (!workFrame) { + log.error('Failed to access inner work frame') throw new Error('Failed to access inner work frame') } diff --git a/src/main/services/user/migration/add-erp-params-migration.ts b/src/main/services/user/migration/add-erp-params-migration.ts index 47b724f..021efaf 100644 --- a/src/main/services/user/migration/add-erp-params-migration.ts +++ b/src/main/services/user/migration/add-erp-params-migration.ts @@ -17,6 +17,9 @@ import { dirname } from 'path' import { ConfigManager } from '../../config/config-manager' import { MySqlService } from '../../database/mysql' import { SqlServerService } from '../../database/sql-server' +import { createLogger } from '../../logger' + +const log = createLogger('Migration') const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) @@ -136,7 +139,9 @@ async function runMySQLMigration(configManager: ConfigManager): Promise { await mysqlService.disconnect() } catch (error) { - console.error('✗ MySQL Migration failed:', error instanceof Error ? error.message : error) + log.error('MySQL Migration failed', { + error: error instanceof Error ? error.message : String(error) + }) if (mysqlService.isConnected()) { await mysqlService.disconnect() } @@ -192,7 +197,9 @@ async function runSqlServerMigration(configManager: ConfigManager): Promise { console.log('\n✅ Migration completed successfully!\n') } catch (error) { - console.error('\n❌ Migration failed:', error instanceof Error ? error.message : error) + log.error('Migration failed', { error: error instanceof Error ? error.message : String(error) }) process.exit(1) } } diff --git a/src/main/services/user/migration/run-migration.ts b/src/main/services/user/migration/run-migration.ts index ecccfa5..9ea1943 100644 --- a/src/main/services/user/migration/run-migration.ts +++ b/src/main/services/user/migration/run-migration.ts @@ -13,6 +13,9 @@ import * as fs from 'fs' import * as path from 'path' import yaml from 'js-yaml' import { z } from 'zod' +import { createLogger } from '../../logger' + +const log = createLogger('MigrationRunner') /** * MySQL configuration schema @@ -105,8 +108,10 @@ async function runMigration(): Promise { try { dbConfig = loadConfig(configPath) } catch (error) { - console.error('Failed to load config.yaml:', error instanceof Error ? error.message : error) - console.error('Please ensure config.yaml exists and contains valid MySQL configuration.') + log.error('Failed to load config', { + error: error instanceof Error ? error.message : String(error), + configPath + }) process.exit(1) } @@ -171,8 +176,7 @@ async function runMigration(): Promise { console.log(` ERP_Password = 'your_password'`) console.log(` WHERE ERP_URL IS NULL;\n`) } catch (error) { - console.error('\n❌ Migration failed with error:') - console.error(error) + log.error('Migration failed', { error }) console.error('\nTroubleshooting:') console.error('1. Check if MySQL server is running') console.error('2. Verify database credentials in config.yaml file') @@ -194,6 +198,6 @@ async function runMigration(): Promise { // Run migration runMigration().catch((error) => { - console.error('Unexpected error:', error) + log.error('Unexpected error', { error }) process.exit(1) }) diff --git a/src/main/services/user/session-manager.ts b/src/main/services/user/session-manager.ts index 9f3f38c..318b26c 100644 --- a/src/main/services/user/session-manager.ts +++ b/src/main/services/user/session-manager.ts @@ -9,6 +9,9 @@ */ import type { UserInfo } from '../../types/user.types' +import { createLogger } from '../logger' + +const log = createLogger('SessionManager') /** * Session Manager Class @@ -59,12 +62,12 @@ export class SessionManager { } return false } catch (error) { - console.error('[SessionManager] Login error:', error) + log.error('Login error', { error }) return false } finally { if (dao) { await dao.disconnect().catch((error) => { - console.error('[SessionManager] Login disconnect error:', error) + log.error('Login disconnect error', { error }) }) } } @@ -93,12 +96,12 @@ export class SessionManager { } return false } catch (error) { - console.error('[SessionManager] Silent login error:', error) + log.error('Silent login error', { error }) return false } finally { if (dao) { await dao.disconnect().catch((error) => { - console.error('[SessionManager] Silent login disconnect error:', error) + log.error('Silent login disconnect error', { error }) }) } } @@ -187,12 +190,12 @@ export class SessionManager { dao = new BIPUsersDAO() return await dao.getAllUsers() } catch (error) { - console.error('[SessionManager] Get all users error:', error) + log.error('Get all users error', { error }) return [] } finally { if (dao) { await dao.disconnect().catch((error) => { - console.error('[SessionManager] Get all users disconnect error:', error) + log.error('Get all users disconnect error', { error }) }) } }