const LOGIN_REQUIRED_PATTERNS = [ /loginlt\.html/i, /用户登录/, /账\s*号/, /密\s*码/, /验证\s*码/, /立即登[录陆]/, /还没有登录/, /尚未登录/, /登录超时/, /登陆超时/, /重新登[录陆]/, /用户登录/, /USERS\s+LOGIN/i, /立即登[录陆]/, ]; const LOGGED_IN_PATTERNS = [ /业务操作/, /独立团计划表/, /散拼团计划表/, /财务操作/, /统计中心/, /合作商管理/, /退出/, /涓氬姟鎿嶄綔/, /鐙珛鍥㈣鍒掕〃/, /鏁f嫾鍥㈣鍒掕〃/, /閫€鍑?/, ]; function normalizeText(value) { return String(value || '').replace(/\s+/g, ' ').trim(); } function isLoginRequiredText(value) { const text = normalizeText(value); return LOGIN_REQUIRED_PATTERNS.some((pattern) => pattern.test(text)); } function isLoggedInText(value) { const text = normalizeText(value); return LOGGED_IN_PATTERNS.some((pattern) => pattern.test(text)); } function classifyErpSession({ bodyText = '', dialogMessage = '', url = '' } = {}) { const normalizedBody = normalizeText(bodyText); const normalizedDialog = normalizeText(dialogMessage); if (isLoggedInText(normalizedBody) && !isLoginRequiredText(normalizedBody)) { return { ok: true, reason: 'logged_in', message: 'ERP session appears logged in', details: { url }, }; } const combined = normalizeText([normalizedDialog, normalizedBody, url].filter(Boolean).join('\n')); if (isLoginRequiredText(combined)) { return { ok: false, reason: 'erp_login_required', message: 'ERP login/session timed out; please log in again before running automation.', details: { url, dialogMessage: normalizedDialog, bodyPreview: normalizedBody.slice(0, 500) }, }; } return { ok: false, reason: 'erp_session_unknown', message: 'ERP login/session not detected', details: { url, dialogMessage: normalizedDialog, bodyPreview: normalizedBody.slice(0, 500) }, }; } function installErpDialogHandler(page, sink = null) { const state = { messages: [], loginRequired: false }; page.on('dialog', async (dialog) => { const message = dialog.message(); const record = { type: dialog.type(), message }; state.messages.push(record); if (sink) sink(record); if (isLoginRequiredText(message)) state.loginRequired = true; await dialog.accept().catch(() => {}); }); return state; } function dialogCursor(dialogState = null) { return dialogState && Array.isArray(dialogState.messages) ? dialogState.messages.length : 0; } function dialogMessagesSince(dialogState = null, cursor = 0) { if (!dialogState || !Array.isArray(dialogState.messages)) return []; const start = Math.max(0, Number(cursor) || 0); return dialogState.messages.slice(start); } function assertNoNewErpLoginRequired(dialogState = null, cursor = 0, source = '') { const newMessages = dialogMessagesSince(dialogState, cursor); const dialogMessage = newMessages.map((item) => item.message || '').join('\n'); if (!isLoginRequiredText(dialogMessage)) { return { ok: true, messages: newMessages }; } const classification = classifyErpSession({ dialogMessage, }); throw createErpSessionError(classification, source); } async function readAllPageText(page) { const chunks = []; for (const frame of page.frames()) { const text = await frame.evaluate(() => document.body.innerText.replace(/\s+/g, ' ').trim()).catch(() => ''); if (text) chunks.push(text); } return chunks.join('\n'); } function dialogText(dialogState = null) { return dialogState && dialogState.messages ? dialogState.messages.map((item) => item.message).join('\n') : ''; } async function classifyPageSession(page, dialogState = null, source = '') { let bodyText = ''; try { bodyText = await readAllPageText(page); } catch (error) { if (isBrowserClosedError(error)) throw createBrowserClosedDuringLoginError(error, source); } let url = ''; try { url = page.url(); } catch (error) { if (isBrowserClosedError(error)) throw createBrowserClosedDuringLoginError(error, source); } return classifyErpSession({ bodyText, dialogMessage: dialogText(dialogState), url, }); } function siblingContextPages(page) { try { if (!page || typeof page.context !== 'function') return []; const context = page.context(); if (!context || typeof context.pages !== 'function') return []; return context.pages().filter((candidate) => candidate && candidate !== page); } catch { return []; } } async function classifyManualLoginPages(page, dialogState = null, source = '') { const pages = [page, ...siblingContextPages(page)]; let lastClassification = null; for (const candidate of pages) { const classification = await classifyPageSession( candidate, candidate === page ? dialogState : null, source ); if (classification.ok) { return { ...classification, details: { ...(classification.details || {}), detectedPageUrl: classification.details && classification.details.url, }, }; } if (!lastClassification || lastClassification.reason !== 'erp_login_required') { lastClassification = classification; } } return lastClassification; } function createErpSessionError(classification, source = '') { const error = new Error(source ? `${classification.message} (${source})` : classification.message); error.code = classification.reason; error.details = classification.details; return error; } function isBrowserClosedError(error) { return /Target page, context or browser has been closed|Target closed|Browser closed|Session closed/i.test( String(error && error.message || error || '') ); } function createBrowserClosedDuringLoginError(error, source = '') { return createErpSessionError({ ok: false, reason: 'erp_browser_closed_during_login', message: 'ERP Chrome was closed while waiting for login; reopen the ERP Chrome window, log in, then retry the same operation.', details: { source, originalMessage: String(error && error.message || error || ''), }, }, source); } async function assertErpSessionActive(page, dialogState = null, source = '') { const classification = await classifyPageSession(page, dialogState, source); if (!classification.ok) { throw createErpSessionError(classification, source); } return classification; } async function assertNoErpLoginRequired(page, dialogState = null, source = '') { const classification = await classifyPageSession(page, dialogState, source); if (classification.reason === 'erp_login_required') { throw createErpSessionError(classification, source); } return classification; } async function waitForErpManualLogin(page, dialogState = null, options = {}) { const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 10 * 60 * 1000; const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 2000; const source = options.source || 'erp.manualLogin'; const deadline = Date.now() + timeoutMs; let lastClassification = null; let lastLogAt = 0; while (Date.now() < deadline) { lastClassification = await classifyManualLoginPages(page, dialogState, source); if (lastClassification.ok) return lastClassification; if (Date.now() - lastLogAt > 15000) { console.error(`[ERP login] Waiting for manual login in Chrome (${source})...`); lastLogAt = Date.now(); } try { await page.waitForTimeout(pollMs); } catch (error) { if (isBrowserClosedError(error)) throw createBrowserClosedDuringLoginError(error, source); throw error; } } throw createErpSessionError( lastClassification || { reason: 'erp_login_timeout', message: 'ERP manual login timed out; please log in again before running automation.', details: { url: page.url() }, }, source ); } function normalizeUrlPath(value) { try { return new URL(value).pathname.toLowerCase(); } catch (_error) { return String(value || '').split('?')[0].toLowerCase(); } } function isAtTargetUrl(pageUrl, targetUrl) { if (!targetUrl) return true; return normalizeUrlPath(pageUrl) === normalizeUrlPath(targetUrl); } async function ensureErpSessionReady(page, dialogState = null, options = {}) { const source = options.source || 'erp.ensureSession'; const targetUrl = options.targetUrl || ''; const gotoTarget = async () => { if (!targetUrl) return; await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 45000 }).catch(() => {}); await page.waitForTimeout(1200).catch(() => {}); }; const initial = await assertNoErpLoginRequired(page, dialogState, source).catch((error) => { if (error && error.code === 'erp_login_required') return null; throw error; }); if (initial && initial.ok && isAtTargetUrl(page.url(), targetUrl)) return initial; if (initial && initial.ok) { await gotoTarget(); const target = await assertNoErpLoginRequired(page, dialogState, source).catch((error) => { if (error && error.code === 'erp_login_required') return null; throw error; }); if (target && target.ok) return target; } if (initial && initial.reason === 'erp_session_unknown') { console.error(`[ERP login] Waiting for ERP target page/session to become ready for ${source}.`); } else { console.error(`[ERP login] ERP requires login for ${source}. Fill the captcha and click login in the opened Chrome window.`); } await waitForErpManualLogin(page, dialogState, options); await gotoTarget(); const final = await assertNoErpLoginRequired(page, dialogState, source).catch((error) => { if (error && error.code === 'erp_login_required') return null; throw error; }); if (final && final.ok) return final; console.error(`[ERP login] Target page still requires login for ${source}; waiting once more in the same Chrome window.`); await waitForErpManualLogin(page, dialogState, options); await gotoTarget(); const ready = await assertNoErpLoginRequired(page, dialogState, source); if (ready && ready.ok) return ready; throw createErpSessionError(ready, source); } module.exports = { normalizeText, isLoginRequiredText, isLoggedInText, classifyErpSession, installErpDialogHandler, dialogCursor, dialogMessagesSince, assertNoNewErpLoginRequired, readAllPageText, assertErpSessionActive, assertNoErpLoginRequired, waitForErpManualLogin, ensureErpSessionReady, normalizeUrlPath, isAtTargetUrl, createErpSessionError, isBrowserClosedError, };