/** Priority1 公开询价页报价提取(Feathery custom_request + 页面摘要) */ export type Priority1QuoteLine = { rank: number; carrier: string; carrierCode: string; totalUsd: number; transitDays: number; deliveryDate: string | null; expirationDate: string | null; serviceLevel: string; quoteId: number | null; caboUrl: string | null; source: "feathery-summary" | "visible-dom" | "api-all" | "dom" | "ftl-calendar" | "legacy-tier"; }; type ApiRateQuote = { id?: number; carrierName?: string; carrierCode?: string; transitDays?: number; deliveryDate?: string; expirationDate?: string; serviceLevel?: string; serviceLevelDescription?: string; rateQuoteDetail?: { total?: number }; }; function asNumber(v: unknown): number { if (typeof v === "number" && Number.isFinite(v)) return v; if (typeof v === "string") { const n = Number(v.replace(/,/g, "")); if (Number.isFinite(n)) return n; } return 0; } function parseCustomRequestPayload(body: unknown): Priority1QuoteLine[] { if (!body || typeof body !== "object") return []; const data = (body as { data?: { id?: number; rateQuotes?: ApiRateQuote[] } }) .data; const list = data?.rateQuotes; if (!Array.isArray(list)) return []; return list .map((q, idx) => ({ rank: idx + 1, carrier: String(q.carrierName ?? "").trim(), carrierCode: String(q.carrierCode ?? "").trim(), totalUsd: asNumber(q.rateQuoteDetail?.total), transitDays: asNumber(q.transitDays), deliveryDate: q.deliveryDate ?? null, expirationDate: q.expirationDate ?? null, serviceLevel: String( q.serviceLevelDescription ?? q.serviceLevel ?? "", ).trim(), quoteId: typeof data?.id === "number" ? data.id : null, caboUrl: typeof data?.id === "number" ? `https://dashboard.priority1.com/ltl/quotes/details/${data.id}` : null, source: "api-all" as const, })) .filter((q) => q.carrier && q.totalUsd > 0); } function parseFeatherySummaryPayload(body: unknown): Priority1QuoteLine[] { if (!body || typeof body !== "object") return []; const o = body as Record; const out: Priority1QuoteLine[] = []; const slots = [ { prefix: "LTL_best_price_quote", rank: 1, }, { prefix: "LTL_standard_rate_quote", rank: 2, }, { prefix: "LTL_second_rate_quote", rank: 3, }, { prefix: "LTL_expedited_quote", rank: 4, }, ]; for (const slot of slots) { const carrier = o[`${slot.prefix}_carrier`]; const cost = o[`${slot.prefix}_cost`]; if (!carrier || !cost) continue; out.push({ rank: slot.rank, carrier: String(carrier), carrierCode: "", totalUsd: asNumber(cost), transitDays: asNumber(o[`${slot.prefix}_transit`]), deliveryDate: o[`${slot.prefix}_del_date`] ? String(o[`${slot.prefix}_del_date`]) : null, expirationDate: o[`${slot.prefix}_exp_date`] ? String(o[`${slot.prefix}_exp_date`]) : null, serviceLevel: slot.prefix.includes("expedited") ? "Expedited" : "Standard", quoteId: typeof o.quote_id === "number" ? o.quote_id : null, caboUrl: typeof o.cabo_url === "string" ? o.cabo_url : typeof o.quote_id === "number" ? `https://dashboard.priority1.com/ltl/quotes/details/${o.quote_id}` : null, source: "feathery-summary", }); } /** FTL 日历页 — quote_00 / quote_01 等槽位 */ const ftlSlots = Object.keys(o).filter((k) => /^quote_\d+$/i.test(k)); for (let i = 0; i < ftlSlots.length; i += 1) { const key = ftlSlots[i]!; const cost = o[key]; const n = asNumber(cost); if (n <= 0 || String(cost).includes("{{")) continue; out.push({ rank: out.length + 1, carrier: "FTL 日历报价", carrierCode: "", totalUsd: n, transitDays: 0, deliveryDate: null, expirationDate: null, serviceLevel: "FTL", quoteId: typeof o.quote_id === "number" ? o.quote_id : null, caboUrl: typeof o.cabo_url === "string" ? o.cabo_url : null, source: "feathery-summary", }); } return out; } /** FTL 报价页 — YOUR INSTANT QUOTE OPTIONS 日历表格(价格常无 $ 前缀) */ export function parseFtlCalendarQuotes(text: string): Priority1QuoteLine[] { if (!/YOUR INSTANT QUOTE OPTIONS|INSTANT QUOTE OPTIONS/i.test(text)) { return []; } const out: Priority1QuoteLine[] = []; const pairRe = /\b(?:SUN|MON|TUE|WED|THU|FRI|SAT)\b\s+(\d{1,2})\s+(\d{4,5}\.\d{2})/gi; let pair: RegExpExecArray | null; while ((pair = pairRe.exec(text)) !== null) { const day = Number(pair[1]); const totalUsd = asNumber(pair[2]); if (!Number.isFinite(day) || day < 1 || day > 31) continue; if (totalUsd < 200 || totalUsd > 100_000) continue; out.push({ rank: out.length + 1, carrier: "FTL 日历报价", carrierCode: "", totalUsd, transitDays: 0, deliveryDate: `DAY:${day}`, expirationDate: null, serviceLevel: "FTL Calendar", quoteId: null, caboUrl: null, source: "ftl-calendar" as const, }); } if (out.length > 0) { return out; } const prices: number[] = []; const re = /(?= 200 && n <= 100_000) { prices.push(n); } } const unique = [...new Set(prices)].sort((a, b) => a - b); return unique.map((totalUsd, idx) => ({ rank: idx + 1, carrier: "FTL 日历报价", carrierCode: "", totalUsd, transitDays: 0, deliveryDate: null, expirationDate: null, serviceLevel: "FTL Calendar", quoteId: null, caboUrl: null, source: "ftl-calendar" as const, })); } /** 站点无即时报价、需销售人工确认时的标准返回文案 */ export const PRIORITY1_MANUAL_FOLLOWUP_MESSAGE = "表单已提交,站点判定需人工确认细节后报价"; export type Priority1QuoteOutcome = "instant_quote" | "manual_followup" | "no_result"; /** FTL 温控 / Expedited 等 — 「MORE INFORMATION NEEDED」终态(无金额) */ export function isPriority1ManualFollowupPage(domText: string): boolean { return ( /MORE\s+INFORMATION\s+NEEDED/i.test(domText) && /freight\s+specialist|Confirm\s+Details\s+&\s+Lock\s+In\s+Your\s+Rate|REQUEST\s+NEW\s+QUOTE/i.test( domText, ) ); } export function resolvePriority1SubmitOutcome( bodies: string[], domText: string, ): { outcome: Priority1QuoteOutcome; quotes: Priority1QuoteLine[]; message?: string; } { const quotes = extractPriority1VisibleQuotes(bodies, domText); if (quotes.length > 0) { return { outcome: "instant_quote", quotes }; } if (isPriority1ManualFollowupPage(domText)) { return { outcome: "manual_followup", quotes: [], message: PRIORITY1_MANUAL_FOLLOWUP_MESSAGE, }; } return { outcome: "no_result", quotes: [] }; } /** 页面是否已进入报价结果(LTL 卡片 / FTL 日历 / 含 $ 金额) */ export function isPriority1QuotePageReady(domText: string): boolean { if (/\$[\d,]+\.\d{2}/.test(domText)) return true; if (/QUOTE\s+\d+/i.test(domText) && /\$[\d,]+\.\d{2}/.test(domText)) return true; if (parseFtlCalendarQuotes(domText).length > 0) return true; if ( /YOUR INSTANT QUOTE OPTIONS|INSTANT QUOTE OPTIONS|REQUEST A NEW QUOTE/i.test( domText, ) && /\b\d{4,5}\.\d{2}\b/.test(domText) ) { return true; } return false; } function parseUsDashOrSlashDate(raw: string | null | undefined): string | null { if (!raw) return null; const s = raw.trim(); const dash = s.match(/^(\d{2})-(\d{2})-(\d{4})$/); if (dash) return `${dash[1]}/${dash[2]}/${dash[3]}`; const slash = s.match(/^(\d{2})\/(\d{2})\/(\d{4})$/); if (slash) return s; const iso = s.match(/^(\d{4})-(\d{2})-(\d{2})/); if (iso) return `${iso[2]}/${iso[3]}/${iso[1]}`; return s; } const UNRESOLVED_CARRIER_RE = /未解析承运商|\(页面/; function isUnresolvedCarrier(carrier: string | undefined | null): boolean { const c = String(carrier ?? "").trim(); return !c || UNRESOLVED_CARRIER_RE.test(c); } function isLtlFeatherySummaryLine(line: Priority1QuoteLine): boolean { return ( line.source === "feathery-summary" && line.carrier !== "FTL 日历报价" && !/^quote_\d+$/i.test(line.serviceLevel) && line.serviceLevel !== "FTL" ); } function pickOfficialUsd( summaryUsd: number, domUsd: number, domCarrierUnresolved: boolean, ): number { if (summaryUsd <= 0) return domUsd; if (domUsd <= 0) return summaryUsd; const drift = Math.abs(summaryUsd - domUsd) / summaryUsd; if (drift <= 0.02) return domUsd; return domCarrierUnresolved ? summaryUsd : domUsd; } function parseCarrierFromQuoteBlock(block: string): string { const afterQuote = block.match( /QUOTE\s+\d+\s+([A-Za-z][A-Za-z0-9\s&.'-]{1,48}?)\s+\$/i, ); if (afterQuote?.[1]) { return afterQuote[1].replace(/\s+/g, " ").trim(); } const beforeTransit = block.match( /STANDARD\s+LTL\s+(.+?)\s+Transit:/i, ); if (beforeTransit?.[1]) { const c = beforeTransit[1].replace(/\s+/g, " ").trim(); if (c && !/^QUOTE\s+\d/i.test(c)) return c; } const inline = block.match( /STANDARD\s+LTL\s+(.+?)\s+Quote Expiration Date/i, ); if (inline?.[1]) { const c = inline[1].replace(/\s+/g, " ").trim(); if (c && !/^QUOTE\s+\d/i.test(c)) return c; } const lines = block.split(/\n/).map((l) => l.trim()).filter(Boolean); for (let i = 0; i < lines.length; i += 1) { if (!/^STANDARD\s+LTL/i.test(lines[i]!)) continue; for (let j = i + 1; j < lines.length; j += 1) { const line = lines[j]!; if ( /^(Transit:|Quote Expiration|Estimated Delivery|\$|QUOTE\s+\d+)/i.test( line, ) ) { break; } if (line.length >= 2 && !/^STANDARD/i.test(line)) { return line.replace(/\s+/g, " ").trim(); } } } return ""; } function extractPriceFromQuoteBlock(block: string): number { const nearQuote = block.match( /QUOTE\s+\d+[\s\S]{0,120}?\$\s*([\d,]+\.\d{2})/i, ); if (nearQuote?.[1]) return asNumber(nearQuote[1]); const prices = [...block.matchAll(/\$\s*([\d,]+\.\d{2})/g)].map((m) => asNumber(m[1]), ); return prices.find((p) => p > 0) ?? 0; } function mergeLtlSummaryWithDomCards( summary: Priority1QuoteLine[], domCards: Priority1QuoteLine[], ): Priority1QuoteLine[] { const ltlSummary = summary .filter(isLtlFeatherySummaryLine) .sort((a, b) => a.rank - b.rank); if (ltlSummary.length === 0) return []; const sortedDom = [...domCards].sort((a, b) => a.rank - b.rank); return ltlSummary.map((s, idx) => { const dom = sortedDom[idx]; if (!dom) return s; return { ...s, rank: dom.rank, transitDays: dom.transitDays > 0 ? dom.transitDays : s.transitDays, deliveryDate: dom.deliveryDate ?? s.deliveryDate, expirationDate: dom.expirationDate ?? s.expirationDate, carrierCode: s.carrierCode || dom.carrierCode, source: "feathery-summary", }; }); } function enrichDomCardsFromNetwork( cards: Priority1QuoteLine[], bodies: string[], ): Priority1QuoteLine[] { if (cards.length === 0) return cards; const summary = extractFeatherySummaryQuotes(bodies) .filter(isLtlFeatherySummaryLine) .sort((a, b) => a.rank - b.rank); const api = extractPriority1AllApiQuotes(bodies); const sortedCards = [...cards].sort((a, b) => a.rank - b.rank); return sortedCards.map((card, idx) => { const unresolved = isUnresolvedCarrier(card.carrier); const match = summary.find((p) => p.rank === card.rank) ?? summary[idx] ?? (!unresolved ? api.find( (p) => p.carrier.toLowerCase() === card.carrier.toLowerCase(), ) : undefined) ?? api[idx]; if (!match) return card; return { ...card, carrier: unresolved ? match.carrier : card.carrier, carrierCode: card.carrierCode || match.carrierCode, totalUsd: pickOfficialUsd(match.totalUsd, card.totalUsd, unresolved), deliveryDate: card.deliveryDate ?? match.deliveryDate, expirationDate: card.expirationDate ?? match.expirationDate, transitDays: card.transitDays > 0 ? card.transitDays : match.transitDays, quoteId: card.quoteId ?? match.quoteId, caboUrl: card.caboUrl ?? match.caboUrl, }; }); } function parseVisibleQuoteCardsDom(text: string): Priority1QuoteLine[] { const out: Priority1QuoteLine[] = []; const blocks = text.split(/(?=QUOTE\s+\d+)/i).filter((b) => /QUOTE\s+\d+/i.test(b)); for (const block of blocks) { const rankM = block.match(/QUOTE\s+(\d+)/i); const totalUsd = extractPriceFromQuoteBlock(block); if (!rankM || totalUsd <= 0) continue; const transitM = block.match(/Transit:\s*(\d+)\s+Business\s+Days?/i); const expM = block.match( /Quote Expiration Date:\s*(\d{2}[-/]\d{2}[-/]\d{4})/i, ); const delM = block.match( /Estimated Delivery Date:\s*(\d{2}[-/]\d{2}[-/]\d{4})/i, ); const carrier = parseCarrierFromQuoteBlock(block); out.push({ rank: Number(rankM[1]), carrier: carrier || "(页面未解析承运商)", carrierCode: "", totalUsd, transitDays: transitM ? Number(transitM[1]) : 0, deliveryDate: parseUsDashOrSlashDate(delM?.[1]), expirationDate: parseUsDashOrSlashDate(expM?.[1]), serviceLevel: "STANDARD LTL", quoteId: null, caboUrl: null, source: "visible-dom", }); } return out.sort((a, b) => a.rank - b.rank); } function parseDomTextQuotes(text: string): Priority1QuoteLine[] { const prices = [...text.matchAll(/\$([\d,]+\.\d{2})/g)].map((m) => asNumber(m[1]), ); const unique = [...new Set(prices)].filter((p) => p > 0).sort((a, b) => a - b); return unique.map((totalUsd, idx) => ({ rank: idx + 1, carrier: "(页面未解析承运商)", carrierCode: "", totalUsd, transitDays: 0, deliveryDate: null, expirationDate: null, serviceLevel: "", quoteId: null, caboUrl: null, source: "dom", })); } export function extractPriority1AllApiQuotes( bodies: string[], ): Priority1QuoteLine[] { let apiQuotes: Priority1QuoteLine[] = []; for (const raw of bodies) { let parsed: unknown; try { parsed = JSON.parse(raw); } catch { continue; } const fromApi = parseCustomRequestPayload(parsed); if (fromApi.length > apiQuotes.length) { apiQuotes = fromApi; } } return dedupeQuotes(apiQuotes); } function extractFeatherySummaryQuotes(bodies: string[]): Priority1QuoteLine[] { const summaryQuotes: Priority1QuoteLine[] = []; for (const raw of bodies) { let parsed: unknown; try { parsed = JSON.parse(raw); } catch { continue; } summaryQuotes.push(...parseFeatherySummaryPayload(parsed)); } return dedupeQuotes(summaryQuotes); } /** 页面用户可见报价:可见 QUOTE 卡片 DOM > 摘要 > FTL 日历 > 全量 API */ export function extractPriority1VisibleQuotes( bodies: string[], domText = "", ): Priority1QuoteLine[] { const domCards = parseVisibleQuoteCardsDom(domText); const summary = extractFeatherySummaryQuotes(bodies); const ltlSummary = summary.filter(isLtlFeatherySummaryLine); if (ltlSummary.length > 0) { const merged = mergeLtlSummaryWithDomCards(ltlSummary, domCards); if (merged.length > 0) { return merged; } } if (domCards.length > 0) { return enrichDomCardsFromNetwork(domCards, bodies); } if (summary.length > 0) { return summary; } const ftlCalendar = parseFtlCalendarQuotes(domText); if (ftlCalendar.length > 0) { return ftlCalendar; } return extractPriority1AllApiQuotes(bodies).slice(0, 4).map((line, idx) => ({ ...line, rank: idx + 1, })); } export function extractPriority1QuotesFromNetworkBodies( bodies: string[], ): Priority1QuoteLine[] { return extractPriority1VisibleQuotes(bodies); } export function extractPriority1QuotesFromDomText(text: string): Priority1QuoteLine[] { const cards = parseVisibleQuoteCardsDom(text); if (cards.length > 0) return cards; const ftl = parseFtlCalendarQuotes(text); if (ftl.length > 0) return ftl; return parseDomTextQuotes(text); } export function resolvePriority1QuoteBundles( bodies: string[], domText = "", ): { visible: Priority1QuoteLine[]; allApi: Priority1QuoteLine[] } { return { visible: extractPriority1VisibleQuotes(bodies, domText), allApi: extractPriority1AllApiQuotes(bodies), }; } function dedupeQuotes(lines: Priority1QuoteLine[]): Priority1QuoteLine[] { const seen = new Set(); const out: Priority1QuoteLine[] = []; for (const line of lines) { const key = `${line.carrier}|${line.totalUsd}|${line.transitDays}`; if (seen.has(key)) continue; seen.add(key); out.push({ ...line, rank: out.length + 1 }); } return out; } export function formatPriority1QuoteOutcomeMessage( outcome: Priority1QuoteOutcome, message?: string, ): string { if (outcome === "manual_followup") { return message ?? PRIORITY1_MANUAL_FOLLOWUP_MESSAGE; } if (outcome === "instant_quote") return "(即时报价)"; return "(无报价结果)"; } export function formatPriority1QuotesTable(lines: Priority1QuoteLine[]): string { if (!lines.length) return "(无报价)"; const rows = lines.map( (q) => `${String(q.rank).padStart(2)}. ${q.carrier.padEnd(22)} $${q.totalUsd.toFixed(2).padStart(9)} ${q.transitDays}天 [${q.source}]`, ); return rows.join("\n"); }