import type { QuoteItem } from "@/modules/providers/quote-provider"; import { isAxelQuoteResponseUrl } from "@/workers/rpa/quote-capture/axel-contract"; import { satisfiesQuoteContract } from "@/workers/rpa/quote-capture/quote-contract-guard"; import { normalizeQuote } from "@/workers/rpa/quote-capture/quote-normalize"; import type { QuoteCaptureSignals, QuoteDecodeResult } from "@/workers/rpa/quote-capture/types"; type PartialTier = { serviceLevel: QuoteItem["serviceLevel"]; rateOption: QuoteItem["rateOption"]; rawFreight: number; surcharges: number; transitDays: string; transitDescription: string; carrier: string; }; const MAX_WALK_DEPTH = 24; const SERVICE_LEVEL_ALIASES: Record = { standard: "standard", guaranteed: "guaranteed", guarantee: "guaranteed", 标准: "standard", 保证送达: "guaranteed", }; const RATE_OPTION_ALIASES: Record = { lowest: "lowest", cheapest: "lowest", economy: "lowest", fastest: "fastest", expedited: "fastest", express: "fastest", bestvalue: "bestValue", 最低价格: "lowest", 最快递送: "fastest", 最优性价比: "bestValue", }; const PRICE_FIELD_KEYS = [ "price", "freight", "amount", "cost", "total", "rawfreight", "rawtotal", "rate", "value", "basequoteprice", "finalprice", ] as const; const TRANSIT_FIELD_KEYS = [ "transitdays", "transit_days", "estimatedtransitdays", "estimatedtransit", "deliverydays", "transit", "eta", "days", ] as const; function normalizeKey(key: string): string { return key.replace(/[^a-z0-9]/gi, "").toLowerCase(); } function keyToServiceLevel(key: string): QuoteItem["serviceLevel"] | null { const n = normalizeKey(key); return SERVICE_LEVEL_ALIASES[n] ?? null; } function keyToRateOption(key: string): QuoteItem["rateOption"] | null { const n = normalizeKey(key); return RATE_OPTION_ALIASES[n] ?? null; } function mapSubKeyToRateOption( _serviceLevel: QuoteItem["serviceLevel"], subKey: string, ): QuoteItem["rateOption"] | null { const ro = keyToRateOption(subKey); if (ro) { return ro; } if (normalizeKey(subKey) === "bestvalue") { return "bestValue"; } return null; } function readStringField(obj: Record, keys: readonly string[]): string | null { for (const key of keys) { const val = obj[key]; if (typeof val === "string" && val.trim()) { return val.trim(); } if (typeof val === "number" && Number.isFinite(val)) { return String(val); } } return null; } function readNumericFromKeys( obj: Record, keys: readonly string[], ): number | null { for (const want of keys) { for (const key of Object.keys(obj)) { if (normalizeKey(key) !== normalizeKey(want)) { continue; } const val = obj[key]; if (typeof val === "number" && val > 0) { return val; } if (typeof val === "string") { const m = val.replace(/,/g, "").match(/(\d+(?:\.\d+)?)/); if (m) { const n = Number(m[1]); if (n > 0) { return n; } } } } } return null; } function readPrice(obj: Record): number | null { // 为何:MotherShip UI 展示 finalPrice;price 常为承运商标价与 UI 不一致 return ( readNumericFromKeys(obj, ["finalprice"]) ?? readNumericFromKeys(obj, ["price"]) ); } function readTransit(obj: Record): string { const daysVal = obj.days ?? obj.transitDays; if (typeof daysVal === "number") { if (daysVal > 0) { return `Est. ${daysVal} business days`; } if (daysVal === 0) { return "0 business days"; } } const desc = readStringField(obj, [ "serviceCommitmentDescription", "transitDescription", "description", "label", ]); if (desc) { return desc; } for (const key of Object.keys(obj)) { const nk = normalizeKey(key); if (!TRANSIT_FIELD_KEYS.some((t) => nk.includes(t.replace(/_/g, "")))) { continue; } const val = obj[key]; if (typeof val === "string" && val.trim()) { return val.trim(); } if (typeof val === "number" && val > 0) { return `${val} business days`; } } return "—"; } function inferServiceLevel(obj: Record): QuoteItem["serviceLevel"] | null { const fields = ["serviceLevel", "service_level", "serviceType", "service_type", "type", "name", "tier"]; for (const key of fields) { const val = obj[key]; if (typeof val !== "string") { continue; } const lower = val.toLowerCase(); if (lower.includes("guarantee") || val.includes("保证")) { return "guaranteed"; } if (lower.includes("standard") || val.includes("标准")) { return "standard"; } } return null; } function inferRateOption(obj: Record): QuoteItem["rateOption"] | null { const fields = ["rateOption", "rate_option", "option", "speed", "name", "label", "serviceType", "service_type"]; for (const key of fields) { const val = obj[key]; if (typeof val !== "string") { continue; } const lower = val.toLowerCase(); if (lower.includes("fast") || lower.includes("exped") || val.includes("最快")) { return "fastest"; } if (lower.includes("low") || lower.includes("cheap") || val.includes("最低")) { return "lowest"; } if (lower.includes("bestvalue") || lower.includes("best value") || val.includes("最优")) { return "bestValue"; } } return null; } function readCarrierName(obj: Record): string { const carrierInfo = obj.carrierInfo; if ( carrierInfo != null && typeof carrierInfo === "object" && !Array.isArray(carrierInfo) ) { const name = readStringField(carrierInfo as Record, [ "name", "carrierName", ]); if (name) { return name; } } return ( readStringField(obj, ["carrierName", "carrier", "providerName"]) ?? "Mothership" ); } function pushTier( acc: PartialTier[], serviceLevel: QuoteItem["serviceLevel"], rateOption: QuoteItem["rateOption"], obj: Record, ): void { const rawFreight = readPrice(obj); if (rawFreight === null || rawFreight <= 0) { return; } const surcharges = typeof obj.surcharges === "number" && obj.surcharges >= 0 ? obj.surcharges : typeof obj.fees === "number" && obj.fees >= 0 ? obj.fees : 0; const transit = readTransit(obj); acc.push({ serviceLevel, rateOption, rawFreight, surcharges, transitDays: transit, transitDescription: transit, carrier: readCarrierName(obj), }); } function walkNestedServiceRates( obj: Record, acc: PartialTier[], depth: number, ): void { if (depth > MAX_WALK_DEPTH) { return; } for (const [key, value] of Object.entries(obj)) { const sl = keyToServiceLevel(key); if (!sl || typeof value !== "object" || value === null || Array.isArray(value)) { continue; } const nested = value as Record; const directRo = keyToRateOption(key); if (directRo) { pushTier(acc, sl, directRo, nested); continue; } for (const [subKey, subVal] of Object.entries(nested)) { const ro = mapSubKeyToRateOption(sl, subKey); if (!ro || typeof subVal !== "object" || subVal === null || Array.isArray(subVal)) { continue; } pushTier(acc, sl, ro, subVal as Record); } } } function walkJson(node: unknown, acc: PartialTier[], depth = 0): void { if (depth > MAX_WALK_DEPTH || node == null) { return; } if (Array.isArray(node)) { for (const item of node) { walkJson(item, acc, depth + 1); } return; } if (typeof node !== "object") { return; } const obj = node as Record; walkNestedServiceRates(obj, acc, depth); const sl = inferServiceLevel(obj); const ro = inferRateOption(obj); if (sl) { pushTier(acc, sl, ro ?? "lowest", obj); } for (const value of Object.values(obj)) { walkJson(value, acc, depth + 1); } } function dedupePartials(partials: PartialTier[]): PartialTier[] { const map = new Map(); for (const tier of partials) { const key = `${tier.serviceLevel}/${tier.rateOption}`; if (!map.has(key)) { map.set(key, tier); } } return [...map.values()]; } function partialsToQuoteItems(partials: PartialTier[]): QuoteItem[] { return dedupePartials(partials).map((tier) => ({ serviceLevel: tier.serviceLevel, rateOption: tier.rateOption, carrier: tier.carrier, transitDays: tier.transitDays, transitDescription: tier.transitDescription, rawFreight: tier.rawFreight, surcharges: tier.surcharges, rawTotal: tier.rawFreight + tier.surcharges, })); } /** UI 档位:叶节点 finalPrice/price;或经 id 关联 availableRates 解析完整对象 */ function resolveUiTierLeaf(tier: unknown): Record | null { if (tier == null || typeof tier !== "object" || Array.isArray(tier)) { return null; } const obj = tier as Record; if (readNumericFromKeys(obj, ["finalprice", "price"]) != null) { return obj; } return null; } function resolveTierFromRatesSlot( slotVal: unknown, availableRates: Record | null, ): Record | null { const leaf = resolveUiTierLeaf(slotVal); if (leaf) { return leaf; } if (availableRates == null) { return null; } let rateId: string | null = null; if (typeof slotVal === "string" && slotVal.trim()) { rateId = slotVal.trim(); } else if (slotVal != null && typeof slotVal === "object" && !Array.isArray(slotVal)) { const id = (slotVal as Record).id; if (typeof id === "string" && id.trim()) { rateId = id.trim(); } } if (!rateId) { return null; } const ref = availableRates[rateId]; if (ref == null || typeof ref !== "object" || Array.isArray(ref)) { return null; } return resolveUiTierLeaf(ref) ? (ref as Record) : null; } function hasMinimumTiers(items: QuoteItem[]): boolean { return items.some( (i) => i.serviceLevel === "standard" && i.rateOption === "lowest", ); } /** 解码 sign-up-quote?quote-details=(URL 编码 JSON,可能多层) */ export function decodeQuoteDetailsParam(raw: string): unknown | null { let current = raw.trim(); for (let attempt = 0; attempt < 4; attempt += 1) { if (current.startsWith("{") || current.startsWith("[")) { try { return JSON.parse(current) as unknown; } catch { return null; } } let progressed = false; try { const urlDecoded = decodeURIComponent(current); if (urlDecoded !== current) { current = urlDecoded; progressed = true; } } catch { /* 非 URL 编码 */ } if (!progressed) { try { const fromB64 = Buffer.from(current, "base64").toString("utf8"); if (fromB64 && fromB64 !== current) { current = fromB64; progressed = true; } } catch { /* 非 base64 */ } } if (!progressed) { break; } } if (current.startsWith("{") || current.startsWith("[")) { try { return JSON.parse(current) as unknown; } catch { return null; } } return null; } function extractTiersFromServiceBlock( partials: PartialTier[], serviceLevel: QuoteItem["serviceLevel"], block: Record, availableRates: Record | null, ): void { for (const [subKey, subVal] of Object.entries(block)) { const ro = mapSubKeyToRateOption(serviceLevel, subKey); if (!ro) { continue; } const tier = resolveTierFromRatesSlot(subVal, availableRates); if (tier) { pushTier(partials, serviceLevel, ro, tier); } } } function extractTiersFromRates( rates: Record, availableRates: Record | null, ): PartialTier[] { const partials: PartialTier[] = []; const standard = rates.standard; if (standard != null && typeof standard === "object" && !Array.isArray(standard)) { extractTiersFromServiceBlock( partials, "standard", standard as Record, availableRates, ); } const fastestTop = resolveTierFromRatesSlot(rates.fastest, availableRates); if ( fastestTop && !partials.some( (p) => p.serviceLevel === "standard" && p.rateOption === "fastest", ) ) { pushTier(partials, "standard", "fastest", fastestTop); } const guaranteed = rates.guaranteed; if ( guaranteed != null && typeof guaranteed === "object" && !Array.isArray(guaranteed) ) { extractTiersFromServiceBlock( partials, "guaranteed", guaranteed as Record, availableRates, ); } return partials; } function readAvailableRates(body: unknown): Record | null { if (body == null || typeof body !== "object" || Array.isArray(body)) { return null; } const data = (body as Record).data; if (data == null || typeof data !== "object" || Array.isArray(data)) { return null; } const availableRates = (data as Record).availableRates; if ( availableRates == null || typeof availableRates !== "object" || Array.isArray(availableRates) ) { return null; } return availableRates as Record; } function readRateDays(obj: Record): number { if (typeof obj.days === "number" && obj.days >= 0) { return obj.days; } return 999; } /** MotherShip UI 档位名仅以 rates.{standard|guaranteed} 下出现的子键为准 */ function listUiRateOptionsFromRatesBlock( rates: Record | undefined, serviceLevel: QuoteItem["serviceLevel"], ): Set | null { const block = rates?.[serviceLevel]; if (block == null || typeof block !== "object" || Array.isArray(block)) { return null; } const allowed = new Set(); for (const subKey of Object.keys(block as Record)) { const ro = mapSubKeyToRateOption(serviceLevel, subKey); if (ro) { allowed.add(ro); } } return allowed.size > 0 ? allowed : null; } function collectExplicitRateOptionsFromAvailable( availableRates: Record | null, serviceLevel: QuoteItem["serviceLevel"], ): Set { const allowed = new Set(); if (!availableRates) { return allowed; } for (const entry of Object.values(availableRates)) { if (entry == null || typeof entry !== "object" || Array.isArray(entry)) { continue; } const obj = entry as Record; const level = inferServiceLevel(obj) ?? "standard"; if (level !== serviceLevel) { continue; } const serviceType = String(obj.serviceType ?? "").toLowerCase(); if (!serviceType || serviceType === "customrate") { continue; } const ro = keyToRateOption(serviceType) ?? mapSubKeyToRateOption(serviceLevel, serviceType); if (ro) { allowed.add(ro); } } return allowed; } /** * 补全 availableRates 时允许的档位: * - guaranteed:仅 rates.guaranteed 下声明的子键(与 Widget Tab 一致) * - standard:rates.standard 子键 + availableRates 显式 serviceType(最快档常仅在此) */ function listAllowedRateOptionsForSupplement( rates: Record | undefined, availableRates: Record | null, serviceLevel: QuoteItem["serviceLevel"], ): Set | null { const fromRates = listUiRateOptionsFromRatesBlock(rates, serviceLevel); if (serviceLevel === "guaranteed") { return fromRates; } const fromExplicit = collectExplicitRateOptionsFromAvailable( availableRates, "standard", ); if (!fromRates && fromExplicit.size === 0) { return null; } return new Set([...(fromRates ?? []), ...fromExplicit]); } function isRateOptionAllowedForSupplement( rates: Record | undefined, availableRates: Record | null, serviceLevel: QuoteItem["serviceLevel"], rateOption: QuoteItem["rateOption"], ): boolean { const allowed = listAllowedRateOptionsForSupplement( rates, availableRates, serviceLevel, ); if (allowed == null) { return true; } return allowed.has(rateOption); } function hasPartialTier( partials: PartialTier[], serviceLevel: QuoteItem["serviceLevel"], rateOption: QuoteItem["rateOption"], ): boolean { return partials.some( (p) => p.serviceLevel === serviceLevel && p.rateOption === rateOption, ); } function mergePartials(target: PartialTier[], source: PartialTier[]): void { for (const tier of source) { if (!hasPartialTier(target, tier.serviceLevel, tier.rateOption)) { target.push(tier); } } } /** * data.availableRates 兜底: * - 显式 serviceType(lowest/fastest/bestValue)仅补 rates.* 已声明的 UI 槽位 * - customRate 池推断仅当 rates.{serviceLevel} 块未定义任何 UI 子键时启用 */ function extractTiersFromAvailableRates( body: unknown, ratesFromBody?: Record, ): PartialTier[] { const availableRates = readAvailableRates(body); if (!availableRates) { return []; } const partials: PartialTier[] = []; const customPools: Record< QuoteItem["serviceLevel"], Record[] > = { standard: [], guaranteed: [] }; for (const entry of Object.values(availableRates)) { if (entry == null || typeof entry !== "object" || Array.isArray(entry)) { continue; } const obj = entry as Record; if (!resolveUiTierLeaf(obj)) { continue; } const serviceLevel = inferServiceLevel(obj) ?? "standard"; const serviceType = String(obj.serviceType ?? "").toLowerCase(); const rateOption = keyToRateOption(serviceType) ?? mapSubKeyToRateOption(serviceLevel, serviceType); if (rateOption && serviceType !== "customrate") { if ( isRateOptionAllowedForSupplement( ratesFromBody, availableRates, serviceLevel, rateOption, ) ) { pushTier(partials, serviceLevel, rateOption, obj); } continue; } customPools[serviceLevel].push(obj); } for (const serviceLevel of ["standard", "guaranteed"] as const) { if (listUiRateOptionsFromRatesBlock(ratesFromBody, serviceLevel) != null) { continue; } const pool = customPools[serviceLevel]; if (pool.length === 0) { continue; } if (!hasPartialTier(partials, serviceLevel, "lowest")) { const cheapest = pool.reduce((best, cur) => (readPrice(cur) ?? Infinity) < (readPrice(best) ?? Infinity) ? cur : best, ); pushTier(partials, serviceLevel, "lowest", cheapest); } if (!hasPartialTier(partials, serviceLevel, "fastest")) { const fastest = pool.reduce((best, cur) => readRateDays(cur) < readRateDays(best) ? cur : best, ); pushTier(partials, serviceLevel, "fastest", fastest); } if (!hasPartialTier(partials, serviceLevel, "bestValue")) { const sorted = [...pool].sort( (a, b) => (readPrice(a) ?? 0) - (readPrice(b) ?? 0), ); const pick = sorted[1] ?? sorted[0]; if (pick) { pushTier(partials, serviceLevel, "bestValue", pick); } } } return partials; } function mergeAvailableRatesSupplement( partials: PartialTier[], body: unknown, rates: Record | undefined, ): void { mergePartials(partials, extractTiersFromAvailableRates(body, rates)); } function extractFromContractBodies( bodies: unknown[], ): QuoteItem[] | null { const partials: PartialTier[] = []; for (const body of bodies) { const canonical = normalizeQuote(body); if (!canonical) { continue; } const availableRates = readAvailableRates(body); partials.push(...extractTiersFromRates(canonical.rates, availableRates)); mergeAvailableRatesSupplement(partials, body, canonical.rates); } const items = partialsToQuoteItems(partials); return hasMinimumTiers(items) ? items : null; } /** 直连 axel/quote:rates.* 叶节点 + id 关联 availableRates + 显式 serviceType 补档 */ export function decodeAxelQuoteBodyLenient(body: unknown): QuoteItem[] | null { const partials: PartialTier[] = []; const canonical = normalizeQuote(body); const rates = canonical?.rates; const availableRates = readAvailableRates(body); if (rates) { partials.push(...extractTiersFromRates(rates, availableRates)); } mergeAvailableRatesSupplement(partials, body, rates); const items = partialsToQuoteItems(partials); return hasMinimumTiers(items) ? items : null; } function orderContractPayloads( payloads: QuoteCaptureSignals["contractPayloads"], ): QuoteCaptureSignals["contractPayloads"] { return [...payloads].sort((a, b) => { const rank = (url: string) => (isAxelQuoteResponseUrl(url) ? 0 : 1); return rank(a.url) - rank(b.url); }); } function tryLenientAxelDecode( signals: QuoteCaptureSignals, ): QuoteDecodeResult | null { const onSignUpQuote = signals.resultUrl.includes("sign-up-quote"); const candidates = [ ...orderContractPayloads(signals.contractPayloads), ...signals.jsonPayloads, ]; for (const payload of candidates) { if (!isAxelQuoteResponseUrl(payload.url) && !onSignUpQuote) { continue; } const items = decodeAxelQuoteBodyLenient(payload.body); if (items && hasMinimumTiers(items)) { console.log( `[rpa] quote-decode: lenient axel/quote url=${payload.url} tiers=${items.length}`, ); return { items, source: "lenient" }; } } return null; } /** 契约 JSON 响应 → 四档报价;sign-up-quote 页 axel/quote 结构不全时走 lenient */ export function decodeQuotePayloads( signals: QuoteCaptureSignals, ): QuoteDecodeResult | null { if (signals.contractPayloads.length === 0) { return tryLenientAxelDecode(signals); } const ordered = orderContractPayloads(signals.contractPayloads); const strictOk = ordered.every((p) => satisfiesQuoteContract(p.body)); if (strictOk) { const bodies = ordered.map((p) => p.body); const items = extractFromContractBodies(bodies); if (items && hasMinimumTiers(items)) { return { items, source: "contract" }; } } return tryLenientAxelDecode(signals); } /** @deprecated Network-First 架构禁止 DOM 内嵌 JSON 报价提取 */ export function decodeEmbeddedJsonBodies( _bodies: unknown[], ): QuoteDecodeResult | null { return null; }