import type { Page } from "playwright"; import type { QuoteItem } from "@/modules/providers/quote-provider"; import { isRpaRequireQuoteRates, isRpaVisualRushMode } from "@/lib/rpa/env"; import { RpaError } from "@/modules/rpa/errors"; import { mapPageTextToErrorCode } from "@/modules/rpa/error-mapper"; import { captureQuoteNetworkResponses } from "@/workers/rpa/quote-capture/quote-network-capture"; import { decodeQuotePayloads } from "@/workers/rpa/quote-capture/payload-decode"; import { validateQuoteSchema } from "@/workers/rpa/quote-capture/quote-schema-validator"; import { submitQuoteForm } from "@/workers/rpa/quote-capture/thin-browser-driver"; import { logQuoteDebugRaw } from "@/workers/rpa/quote-capture/quote-debug"; import type { QuoteCaptureSource, QuoteSubmitOptions } from "@/workers/rpa/quote-capture/types"; export type ScrapeQuotesMeta = { source: QuoteCaptureSource; contractCount: number; }; /** * Contract-driven pipeline: * ThinBrowserDriver(submit) → QuoteNetworkCapture(shape) → QuoteContractGuard → QuoteSchemaValidator */ export async function runQuoteCapturePipeline( page: Page, options?: QuoteSubmitOptions, ): Promise<{ items: QuoteItem[]; meta: ScrapeQuotesMeta }> { if (!options?.widgetFallbackAfterDirectFailure) { throw new RpaError( "RPA_DATA_INVALID", "Widget 报价仅允许在 Direct 明确失败后由 quote-strategy 调用", { retryable: false }, ); } console.log("[rpa] quote-capture: Widget fallback pipeline start"); const signals = await captureQuoteNetworkResponses(page, () => submitQuoteForm(page, options), ); const bodyText = await page.locator("body").innerText().catch(() => ""); const textCode = mapPageTextToErrorCode(bodyText); if (textCode) { throw new RpaError(textCode, `结果页:${textCode}`, { retryable: false }); } const decoded = decodeQuotePayloads(signals); if (!decoded || decoded.items.length === 0) { if ( isRpaVisualRushMode() && !isRpaRequireQuoteRates() && signals.resultUrl.includes("sign-up-quote") ) { console.log( "[rpa] visual-rush: sign-up-quote 已落页,解析为空仍视为验收通过", ); return { items: [], meta: { source: "lenient", contractCount: signals.contractPayloads.length }, }; } logQuoteDebugRaw(signals); throw new RpaError( "RPA_DATA_INVALID", `未捕获满足契约的报价响应(contract=${signals.contractPayloads.length},json=${signals.jsonPayloads.length},url=${signals.resultUrl})`, { retryable: true }, ); } const items = validateQuoteSchema(decoded.items); console.log( `[rpa] quote-capture: source=${decoded.source} contract=${signals.contractPayloads.length}`, ); return { items, meta: { source: decoded.source, contractCount: signals.contractPayloads.length, }, }; }