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