import type { QuoteItem, QuoteRequest } from "@/modules/providers/quote-provider"; import { isWidgetQuoteFallbackEnabled } from "@/lib/rpa/env"; import { RpaError } from "@/modules/rpa/errors"; import type { RPAContext } from "@/workers/rpa/kernel/context"; import { scrapeQuotesViaDirectAxel } from "@/workers/rpa/quote-capture/axel-quote-direct"; import { scrapeQuotesStep } from "@/workers/rpa/kernel/steps/quote-step"; import { validateQuoteSchema } from "@/workers/rpa/quote-capture/quote-schema-validator"; import type { QuoteSubmitOptions } from "@/workers/rpa/quote-capture/types"; /** 报价获取来源(Direct 为唯一标准来源;Widget 仅 fallback) */ export type QuoteAcquisitionSource = "direct" | "widget"; export type QuoteAcquisitionResult = { items: QuoteItem[]; source: QuoteAcquisitionSource; }; function formatDirectFailure(error: unknown): string { if (error instanceof RpaError) { return `${error.code}: ${error.message}`; } if (error instanceof Error) { return error.message; } return String(error); } async function tryDirectQuoteInBrowser( ctx: RPAContext, req: QuoteRequest, ): Promise { const pickup = ctx.state.axelPlaceLocations.pickup; const delivery = ctx.state.axelPlaceLocations.delivery; if (!pickup || !delivery) { throw new RpaError( "RPA_DATA_INVALID", "Direct 报价缺少 axel place 详情(需先完成地址 commit)", { retryable: true }, ); } return scrapeQuotesViaDirectAxel( ctx.page, { pickup, delivery }, req, ); } /** * Direct 优先、Widget 兜底(强约束:Widget 仅在 Direct 明确失败后启动,禁止并行/预热)。 */ export async function captureQuotesDirectFirst( ctx: RPAContext, req: QuoteRequest, options?: QuoteSubmitOptions, ): Promise { let directError: unknown; console.log("[rpa] quote-strategy: 尝试 Direct 报价(主路径)"); try { const items = await tryDirectQuoteInBrowser(ctx, req); if (!items.length) { throw new RpaError( "RPA_DATA_INVALID", "Direct 报价返回 0 档", { retryable: true }, ); } validateQuoteSchema(items); console.log( `[rpa] quote-strategy: Direct 成功 tiers=${items.length}(不再调用 Widget)`, ); return { items, source: "direct" }; } catch (error) { directError = error; console.log( `[rpa] quote-strategy: Direct 失败 — ${formatDirectFailure(error).slice(0, 200)}`, ); } if (!isWidgetQuoteFallbackEnabled()) { if (directError instanceof RpaError) { throw directError; } throw new RpaError( "RPA_DATA_INVALID", formatDirectFailure(directError), { retryable: true }, ); } console.log( "[rpa] quote-strategy: 启动 Widget fallback(Direct 已明确失败)", ); const items = await scrapeQuotesStep(ctx, { ...options, widgetFallbackAfterDirectFailure: true, }); return { items, source: "widget" }; }