|
|
/** 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<string, unknown>;
|
|
|
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 = /(?<![.\d])(\d{4,5}\.\d{2})(?![.\d])/g;
|
|
|
let m: RegExpExecArray | null;
|
|
|
while ((m = re.exec(text)) !== null) {
|
|
|
const n = asNumber(m[1]);
|
|
|
if (n >= 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<string>();
|
|
|
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");
|
|
|
}
|