From c8783a2cef14842fac62e48f68847094ca9a04d9 Mon Sep 17 00:00:00 2001 From: Misaka Date: Sat, 4 Apr 2026 13:19:41 +0800 Subject: [PATCH] feat(logging-p0): add full-step logging to ERP automation and unify capturePageContext Add ~45 structured log calls across extractor-core, cleaner, and erp-auth to cover all automation steps (navigation, query, download, material processing). Enhance capturePageContext with a step parameter for precise failure localization, and fix missing capturePageContext calls in error handlers. Co-Authored-By: Claude Opus 4.6 --- src/main/services/erp/cleaner.ts | 24 +++++++++++++-- src/main/services/erp/erp-auth.ts | 19 ++++++++++-- src/main/services/erp/erp-error-context.ts | 8 ++++- src/main/services/erp/extractor-core.ts | 34 ++++++++++++++++++++-- 4 files changed, 76 insertions(+), 9 deletions(-) diff --git a/src/main/services/erp/cleaner.ts b/src/main/services/erp/cleaner.ts index ba420b4..bb293be 100644 --- a/src/main/services/erp/cleaner.ts +++ b/src/main/services/erp/cleaner.ts @@ -366,7 +366,8 @@ export class CleanerService { totalMaterials, dryRun, orderNumbers: input.orderNumbers, - materialCodes: input.materialCodes + materialCodes: input.materialCodes, + ...(popupPage ? await capturePageContext(popupPage, undefined, 'cleaner.outerCatch') : {}) }) result.errors.push(`Clean failed: ${message}`) } finally { @@ -388,34 +389,44 @@ export class CleanerService { const { page, mainFrame } = session await mainFrame.locator('i').first().click() + log.debug('导航: 已点击菜单图标') const popupPromise = page.waitForEvent('popup') await mainFrame.getByTitle('离散生产订单维护', { exact: true }).first().click() const popupPage = await popupPromise + log.debug('导航: 弹出窗口已打开') const forwardFrameLocator = popupPage.locator('#forwardFrame') const fFrame = forwardFrameLocator.contentFrame() + log.debug('导航: 已获取 forwardFrame') const innerFrameLocator = fFrame.locator('#mainiframe') await innerFrameLocator.waitFor({ state: 'visible', timeout: 30000 }) const workFrame = innerFrameLocator.contentFrame() + log.debug('导航: 已获取内部工作框架') await workFrame.locator('#hot-key-head_list').waitFor({ state: 'visible', timeout: 30000 }) + log.info('已导航到清理页面') return { popupPage, workFrame } } private async setupQueryInterface(innerFrame: FrameLocator): Promise { await innerFrame.locator('.search-name-wrapper > .iconfont').click() + log.debug('查询界面: 已点击搜索图标') await innerFrame.getByText('订单号查询').click() + log.debug('查询界面: 已点击订单号查询') await innerFrame.getByRole('tab', { name: '全部' }).click() + log.debug('查询界面: 已切换到全部标签页') const inputEl = innerFrame.locator('#rc_select_0') await inputEl.fill('5000') await inputEl.press('Enter') + log.debug('查询界面: 已设置查询限制为5000') } private async queryOrders(workFrame: FrameLocator, orderNumbers: string[]): Promise { + log.debug('开始查询订单', { orderCount: orderNumbers.length }) const textbox = workFrame.getByRole('textbox', { name: '生产订单号' }) await textbox.fill(orderNumbers.join(',')) await workFrame.locator('.search-component-searchBtn').click() @@ -435,6 +446,8 @@ export class CleanerService { result.push({ rowIndex, orderNumber }) } + log.debug('查询结果收集完成', { rowCount: result.length }) + return result } @@ -507,7 +520,7 @@ export class CleanerService { } log.error('Failed to locate material plan menu item', { - attemptedSelectors: candidates.length + selectorsAttempted: candidates.length }) throw new Error('无法定位”备料计划”菜单项(可能菜单结构已变化)') } @@ -542,7 +555,9 @@ export class CleanerService { const detailInnerFrame = await detailInnerLocator.contentFrame() if (!detailInnerFrame) { - log.error('Failed to access detail inner frame') + log.error('Failed to access detail inner frame', { + ...(await capturePageContext(detailPage, undefined, 'processDetail.detailInnerFrame')) + }) throw new Error('Failed to access detail inner frame') } @@ -656,6 +671,7 @@ export class CleanerService { if (deleteSuccess) { detail.materialsDeleted += 1 + log.debug('物料已删除', { orderNumber, materialCode, rowNumber: currentRow }) } continue } @@ -668,6 +684,7 @@ export class CleanerService { materialCode, deleteSet }) + log.debug('物料已跳过', { orderNumber, materialCode, reason }) detail.skippedMaterials.push({ materialCode, materialName, @@ -691,6 +708,7 @@ export class CleanerService { if (!dryRun && detail.materialsDeleted > 0) { await saveButtonLocator.click() await saveButtonLocator.waitFor({ state: 'hidden', timeout: 60000 }) + log.info('订单修改已保存', { orderNumber, materialsDeleted: detail.materialsDeleted }) } } diff --git a/src/main/services/erp/erp-auth.ts b/src/main/services/erp/erp-auth.ts index db79567..609dbf8 100644 --- a/src/main/services/erp/erp-auth.ts +++ b/src/main/services/erp/erp-auth.ts @@ -30,6 +30,8 @@ export class ErpAuthService { return this.session } + log.info('开始ERP登录', { url: this.config.url }) + // Launch browser with SSL certificate errors ignored const browser = await chromium.launch({ headless: this.config.headless ?? false, // Use config or default to false @@ -42,6 +44,8 @@ export class ErpAuthService { ] }) + log.debug('浏览器已启动', { headless: this.config.headless ?? false }) + const context = await browser.newContext({ acceptDownloads: true, viewport: { width: 1920, height: 1080 }, @@ -56,6 +60,8 @@ export class ErpAuthService { const loginUrl = `${this.config.url}/yonbip/resources/uap/rbac/login/main/index.html` await page.goto(loginUrl) + log.debug('已导航到登录页面') + // Wait for page to load await page.waitForLoadState('domcontentloaded', { timeout: PAGE_LOAD_TIMEOUT }) @@ -69,6 +75,7 @@ export class ErpAuthService { // This is the main working frame for all subsequent operations const frameLocator = page.locator('#forwardFrame') const contentFrame = await frameLocator.contentFrame() + log.debug('已获取 forwardFrame') if (!contentFrame) { log.error('Failed to access forwardFrame content frame', { @@ -85,7 +92,8 @@ export class ErpAuthService { 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) + error: e instanceof Error ? e.message : String(e), + ...(await capturePageContext(page, undefined, 'login.username')) }) throw new Error(`Failed to find username input: ${e}`) } @@ -95,7 +103,8 @@ export class ErpAuthService { 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) + error: e instanceof Error ? e.message : String(e), + ...(await capturePageContext(page, undefined, 'login.password')) }) throw new Error(`Failed to find password input: ${e}`) } @@ -105,7 +114,8 @@ export class ErpAuthService { await contentFrame.getByRole('button', { name: '登录' }).click() } catch (e) { log.error('Failed to click login button', { - error: e instanceof Error ? e.message : String(e) + error: e instanceof Error ? e.message : String(e), + ...(await capturePageContext(page, undefined, 'login.button')) }) throw new Error(`Failed to click login button: ${e}`) } @@ -125,6 +135,8 @@ export class ErpAuthService { isLoggedIn: true } + log.info('ERP会话已建立') + return this.session } @@ -176,6 +188,7 @@ export class ErpAuthService { */ async close(): Promise { if (this.session) { + log.info('正在关闭ERP会话') await this.session.context.close() await this.session.browser.close() this.session = null diff --git a/src/main/services/erp/erp-error-context.ts b/src/main/services/erp/erp-error-context.ts index 0bbdacd..ca65fde 100644 --- a/src/main/services/erp/erp-error-context.ts +++ b/src/main/services/erp/erp-error-context.ts @@ -11,6 +11,7 @@ export interface ErpErrorContext { pageUrl?: string frameHierarchy?: Array<{ name: string; url: string }> targetSelector?: string + step?: string } /** @@ -22,7 +23,8 @@ export interface ErpErrorContext { */ export async function capturePageContext( page: Page, - targetSelector?: string + targetSelector?: string, + step?: string ): Promise { const ctx: ErpErrorContext = {} @@ -43,5 +45,9 @@ export async function capturePageContext( ctx.targetSelector = targetSelector } + if (step) { + ctx.step = step + } + return ctx } diff --git a/src/main/services/erp/extractor-core.ts b/src/main/services/erp/extractor-core.ts index 2b6a7c8..11daad0 100644 --- a/src/main/services/erp/extractor-core.ts +++ b/src/main/services/erp/extractor-core.ts @@ -31,6 +31,11 @@ export class ExtractorCore { } const totalBatches = this.createBatches(input.orderNumbers, input.batchSize).length + log.info('开始下载所有批次', { + totalOrders: input.orderNumbers.length, + totalBatches, + batchSize: input.batchSize + }) const totalPoints = 1 + totalBatches + 2 const progressPerPoint = 100 / totalPoints @@ -63,10 +68,16 @@ export class ExtractorCore { result.downloadedFiles.push(filePath) } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error' + log.error('批次下载失败', { batchIndex: i, totalBatches, error: message }) result.errors.push(`Batch ${i + 1}: ${message}`) } } + log.info('所有批次下载完成', { + downloadedCount: result.downloadedFiles.length, + errorCount: result.errors.length + }) + return result } @@ -89,20 +100,23 @@ export class ExtractorCore { // Step 1: Click menu icon (Python line 266) // main_frame is #forwardFrame.content_frame returned from login await mainFrame.locator('i').first().click() + log.debug('导航: 已点击菜单图标') // Step 2: Click discrete material plan menu item and expect popup (Python lines 267-271) const popupPromise = page.waitForEvent('popup') await mainFrame.getByTitle('离散备料计划维护', { exact: true }).first().click() const popupPage = await popupPromise + log.debug('导航: 弹出窗口已打开') // Step 3 & 4: Get nested frame structure in popup window (Python lines 273-276) // popup page contains #forwardFrame, which contains #mainiframe const forwardFrameLocator = popupPage.locator('#forwardFrame') const fFrame = await forwardFrameLocator.contentFrame() + log.debug('导航: 已获取 forwardFrame') if (!fFrame) { log.error('Failed to access popup forward frame', { - ...(await capturePageContext(popupPage)) + ...(await capturePageContext(popupPage, undefined, 'navigate.forwardFrame')) }) throw new Error('Failed to access popup forward frame') } @@ -110,15 +124,20 @@ export class ExtractorCore { const innerFrameLocator = fFrame.locator('#mainiframe') await innerFrameLocator.waitFor({ state: 'visible', timeout: 15000 }) const workFrame = await innerFrameLocator.contentFrame() + log.debug('导航: 已获取内部工作框架') if (!workFrame) { - log.error('Failed to access inner work frame') + log.error('Failed to access inner work frame', { + ...(await capturePageContext(popupPage, undefined, 'navigate.innerFrame')) + }) throw new Error('Failed to access inner work frame') } // Step 5: Setup query interface (Python line 278) await this.setupQueryInterface(workFrame) + log.info('提取器页面导航完成') + return { popupPage, workFrame } } @@ -129,17 +148,21 @@ export class ExtractorCore { private async setupQueryInterface(innerFrame: any): Promise { // Click search icon (Python line 233) await innerFrame.locator('.search-name-wrapper > .iconfont').click() + log.debug('查询界面: 已点击搜索图标') // Click "订单号查询" menu item (Python line 234) await innerFrame.getByText('订单号查询').click() + log.debug('查询界面: 已点击订单号查询') // Click "全部" tab (Python line 235) await innerFrame.getByRole('tab', { name: '全部' }).click() + log.debug('查询界面: 已切换到全部标签页') // Set limit to 5000 (Python lines 237-239) const inputBox = innerFrame.locator('#rc_select_0') await inputBox.fill('5000') await inputBox.press('Enter') + log.debug('查询界面: 已设置查询限制为5000') } /** @@ -155,16 +178,21 @@ export class ExtractorCore { _totalBatches: number, downloadDir: string ): Promise { + log.info('开始下载批次', { batchIndex: batchIndex + 1, orderCount: orderNumbers.length }) + // Fill order numbers (Python lines 143-145) const textbox = workFrame.getByRole('textbox', { name: '来源生产订单号' }) await textbox.fill('') await textbox.fill(orderNumbers.join(',')) + log.debug('已填入订单号', { orderCount: orderNumbers.length }) // Click search button (Python line 147) await workFrame.locator('.search-component-searchBtn').click() + log.debug('已点击搜索按钮') // Wait for loading (Python lines 148-153) await this.waitForLoading(workFrame) + log.debug('查询加载完成') // Click first row checkbox (Python line 155) await workFrame.getByRole('row', { name: '序号' }).getByLabel('').click() @@ -189,6 +217,8 @@ export class ExtractorCore { const download = await downloadPromise await download.saveAs(downloadPath) + log.info('批次下载完成', { batchIndex: batchIndex + 1, downloadPath }) + return downloadPath }