340 lines
10 KiB
JavaScript
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,
|
|
};
|