Files
2026-07-13 19:57:46 +08:00

340 lines
10 KiB
JavaScript

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,
};