import type { QuoteItem, QuoteRequest } from "@/modules/providers/quote-provider"; import { RpaError } from "@/modules/rpa/errors"; import { AxelHttpClient } from "@/lib/axel/client"; import { pickBestSearchCandidate } from "@/lib/axel/pick-candidate"; import { buildAxelShipmentPayload, type AxelPlaceLocation, } from "@/lib/axel/shipment"; import { normalizeAxelPlaceId } from "@/workers/rpa/axel-selection-bridge"; import { decodeAxelQuoteBodyLenient } from "@/workers/rpa/quote-capture/payload-decode"; import { normalizeQuoteItems } from "@/workers/rpa/quote-capture/quote-schema-validator"; function buildAddressQuery( addr: QuoteRequest["pickup"], ): string { const fromFormatted = addr.formattedAddress?.trim(); if (fromFormatted) { return fromFormatted; } return [addr.street, addr.city, addr.state, addr.zip] .map((part) => part?.trim()) .filter(Boolean) .join(", "); } function resolvePlaceId(addr: QuoteRequest["pickup"]): string | null { const optionId = addr.mothershipOptionId?.trim(); if (optionId && !optionId.startsWith("ms_")) { return optionId; } const placeId = addr.placeId?.trim(); if (placeId && placeId.startsWith("ChIJ")) { return placeId; } return null; } /** embed-demo / RPA Worker:按用户确认的 placeId 直连 axel/quote */ export async function fetchAxelQuoteItems(req: QuoteRequest): Promise { const client = await AxelHttpClient.create(); type ResolvedSide = { placeId: string; description: string; place?: AxelPlaceLocation; }; async function resolveSide( addr: QuoteRequest["pickup"], sideLabel: string, ): Promise { const knownPlaceId = resolvePlaceId(addr); const description = addr.mothershipDisplayLabel?.trim() || addr.formattedAddress?.trim() || buildAddressQuery(addr); if (knownPlaceId) { try { const place = await client.fetchPlace(knownPlaceId); return { placeId: knownPlaceId, description, place }; } catch { throw new RpaError( "ADDRESS_NOT_CONFIRMED", `${sideLabel}地址在 MotherShip 无法定位,请重新选择候选`, { retryable: true }, ); } } const query = buildAddressQuery(addr); const candidates = await client.searchLocation(query); const picked = pickBestSearchCandidate(query, candidates); if (!picked?.placeId) { throw new RpaError( "ADDRESS_SUGGESTION_NOT_FOUND", `未找到${sideLabel}地址候选:${query}`, { retryable: true }, ); } const place = await client.fetchPlace(picked.placeId); return { placeId: picked.placeId, description: picked.description, place, }; } const [pickupSide, deliverySide] = await Promise.all([ resolveSide(req.pickup, "提货"), resolveSide(req.delivery, "派送"), ]); const pickup = pickupSide.place ?? await client.fetchPlace(pickupSide.placeId); const delivery = deliverySide.place ?? await client.fetchPlace(deliverySide.placeId); const shipment = buildAxelShipmentPayload({ pickup, delivery, pickupDescription: pickupSide.description, deliveryDescription: deliverySide.description, cargo: { palletCount: req.palletCount, weightLb: req.weightLb, lengthIn: req.dimsIn.l, widthIn: req.dimsIn.w, heightIn: req.dimsIn.h, }, }); const body = await client.postQuote(shipment); const decoded = decodeAxelQuoteBodyLenient(body); if (!decoded?.length) { throw new RpaError( "RPA_DATA_INVALID", "axel/quote 响应无法解析为报价档位", { retryable: true }, ); } console.log( `[rpa] direct axel quote: pickup=${normalizeAxelPlaceId(pickupSide.placeId)} delivery=${normalizeAxelPlaceId(deliverySide.placeId)} tiers=${decoded.length}`, ); return normalizeQuoteItems(decoded); }