|
|
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<QuoteItem[]> {
|
|
|
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<QuoteAcquisitionResult> {
|
|
|
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" };
|
|
|
}
|