You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
95 lines
3.0 KiB
95 lines
3.0 KiB
import type { Page } from "playwright";
|
|
import { RPA_PAGE_GOTO_TIMEOUT_MS } from "@/lib/constants/rpa";
|
|
|
|
const GOTO_STRATEGIES = ["domcontentloaded", "commit", "load"] as const;
|
|
|
|
function sleep(ms: number): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
export function readPageGotoTimeoutMs(): number {
|
|
const raw = process.env.RPA_PAGE_GOTO_TIMEOUT_MS?.trim();
|
|
if (!raw) {
|
|
return RPA_PAGE_GOTO_TIMEOUT_MS;
|
|
}
|
|
const parsed = Number(raw);
|
|
return Number.isFinite(parsed) && parsed >= 10_000
|
|
? parsed
|
|
: RPA_PAGE_GOTO_TIMEOUT_MS;
|
|
}
|
|
|
|
function readGotoRetryCount(): number {
|
|
const raw = process.env.RPA_GOTO_RETRY_COUNT?.trim();
|
|
const parsed = Number(raw);
|
|
if (Number.isFinite(parsed) && parsed >= 1 && parsed <= 5) {
|
|
return Math.floor(parsed);
|
|
}
|
|
return 3;
|
|
}
|
|
|
|
/** 网络层瞬时错误 — 可重试 */
|
|
export function isTransientNavigationError(error: unknown): boolean {
|
|
const msg = error instanceof Error ? error.message : String(error ?? "");
|
|
return /ERR_CONNECTION_RESET|ERR_CONNECTION_CLOSED|ERR_NETWORK_CHANGED|ERR_TIMED_OUT|ECONNRESET|ETIMEDOUT|NS_ERROR_NET_RESET|net::ERR_/i.test(
|
|
msg,
|
|
);
|
|
}
|
|
|
|
/** Priority1 等外站导航失败 — 用户可读中文 */
|
|
export function formatExternalSiteNavigationError(
|
|
siteLabel: string,
|
|
error: unknown,
|
|
): string {
|
|
const detail = error instanceof Error ? error.message : String(error ?? "");
|
|
if (/ERR_CONNECTION_RESET|ECONNRESET/i.test(detail)) {
|
|
return (
|
|
`无法连接 ${siteLabel}(连接被重置)。` +
|
|
"请确认本机或服务器可访问该官网,或联系管理员检查网络与代理配置后重试。"
|
|
);
|
|
}
|
|
if (/ERR_TIMED_OUT|ETIMEDOUT|Timeout/i.test(detail)) {
|
|
return (
|
|
`打开 ${siteLabel} 超时。` +
|
|
"请稍后重试,或联系管理员检查网络与代理配置。"
|
|
);
|
|
}
|
|
return `无法打开 ${siteLabel},请稍后重试或联系管理员`;
|
|
}
|
|
|
|
/**
|
|
* page.goto 多策略 + 瞬时网络错误重试(与 MotherShip quote-page-adapter 一致并增强)
|
|
*/
|
|
export async function gotoWithResilience(
|
|
page: Page,
|
|
url: string,
|
|
options?: { timeoutMs?: number },
|
|
): Promise<void> {
|
|
const timeout = options?.timeoutMs ?? readPageGotoTimeoutMs();
|
|
const maxAttempts = readGotoRetryCount();
|
|
let lastError: Error | undefined;
|
|
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
for (const waitUntil of GOTO_STRATEGIES) {
|
|
try {
|
|
await page.goto(url, { waitUntil, timeout });
|
|
return;
|
|
} catch (error) {
|
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
}
|
|
}
|
|
|
|
const transient = isTransientNavigationError(lastError);
|
|
if (attempt < maxAttempts && transient) {
|
|
const backoffMs = 1_500 * attempt;
|
|
console.warn(
|
|
`[page-goto] 第 ${attempt}/${maxAttempts} 次打开失败,${backoffMs}ms 后重试: ${url}`,
|
|
);
|
|
await sleep(backoffMs);
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
throw lastError ?? new Error(`无法打开 ${url}`);
|
|
}
|