/** * Flock Direct:浏览器只用于拿匿名会话 cookie,报价走 POST /user/quotes(跳过填表 DOM) */ import { FLOCK_APP_ORIGIN, FLOCK_DIRECT_QUOTE_TIMEOUT_MS, FLOCK_QUOTES_URL, DEFAULT_FLOCK_USER_AGENT, } from "@/lib/flock/constants"; import { getFlockMinQuotes, getFlockQuoteUrl } from "@/lib/flock/env"; import { mustForceFlockAccountLogin } from "@/lib/flock/login-context"; import { mapFlockFulfillmentOptionsToLines } from "@/lib/flock/map-fulfillment-options"; import { buildFlockQuotesApiRequest } from "@/lib/flock/quote-payload"; import { createRpaBrowserContext } from "@/lib/rpa/browser-context"; import { launchRpaBrowser } from "@/lib/rpa/browser-launch"; import { formatExternalSiteNavigationError, gotoWithResilience, isTransientNavigationError, } from "@/lib/rpa/page-goto"; import { waitForFlockQuoteForm } from "@/workers/rpa/flock/form-interactions"; import type { FlockQuoteInput, FlockRunQuoteResult, } from "@/workers/rpa/flock/types"; function randomDeviceFingerprint(): string { const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let out = ""; for (let i = 0; i < 20; i += 1) { out += alphabet[Math.floor(Math.random() * alphabet.length)]!; } return out; } async function waitForSessionCookies( context: Awaited>, timeoutMs: number, ): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const cookies = await context.cookies(); const hasAnon = cookies.some((c) => c.name === "anonymous_token"); const hasJwt = cookies.some((c) => c.name === "ff_jwt"); if (hasAnon || hasJwt) return true; await new Promise((r) => setTimeout(r, 250)); } return false; } /** Direct 询价:bootstrap 匿名 cookie → POST /user/quotes → 映射两档 */ export async function fetchFlockQuotesViaDirect( input: FlockQuoteInput, ): Promise { // 防御在深度:客户账密上下文下禁止走 Direct if (mustForceFlockAccountLogin()) { return { ok: false, quotes: [], errorMessage: "FLOCK_PATH_DENIED:客户已绑定官网账密,禁止 Direct /user/quotes,必须 DOM 登录查价", }; } const headless = process.env.RPA_HEADLESS !== "false"; const browser = await launchRpaBrowser({ headless, incognito: true }); try { const context = await createRpaBrowserContext(browser, { userAgent: DEFAULT_FLOCK_USER_AGENT, extraHTTPHeaders: { "Accept-Language": "en-US", }, }); const page = await context.newPage(); const url = getFlockQuoteUrl(); try { await gotoWithResilience(page, url); } catch (error) { const raw = error instanceof Error ? error.message : String(error); return { ok: false, quotes: [], errorMessage: isTransientNavigationError({ message: raw }) ? formatExternalSiteNavigationError("Flock Freight 官网", { message: raw, }) : raw, }; } await waitForFlockQuoteForm(page).catch(() => false); const ready = await waitForSessionCookies(context, 15_000); if (!ready) { return { ok: false, quotes: [], errorMessage: "FLOCK_DIRECT_NO_SESSION:未拿到 anonymous_token/ff_jwt(入口可能变更)", }; } const payload = buildFlockQuotesApiRequest(input); const fingerprint = randomDeviceFingerprint(); const response = await context.request.post(FLOCK_QUOTES_URL, { timeout: FLOCK_DIRECT_QUOTE_TIMEOUT_MS, headers: { Accept: "application/json", "Content-Type": "application/json", Origin: FLOCK_APP_ORIGIN, Referer: `${FLOCK_APP_ORIGIN}/`, "x-device-fingerprint": fingerprint, }, data: payload, }); const status = response.status(); const text = await response.text(); if (status < 200 || status >= 300) { return { ok: false, quotes: [], errorMessage: `FLOCK_DIRECT_HTTP_${status}:${text.slice(0, 240)}`, }; } let body: unknown; try { body = JSON.parse(text) as unknown; } catch { return { ok: false, quotes: [], errorMessage: "FLOCK_DIRECT_BAD_JSON:报价响应无法解析", }; } const { quotes, reference } = mapFlockFulfillmentOptionsToLines( body as Parameters[0], ); const minQuotes = getFlockMinQuotes(); if (quotes.length < minQuotes) { return { ok: false, quotes, reference, errorMessage: `FLOCK_DIRECT_NO_RATES:fulfillmentOptions 映射后不足 ${minQuotes} 档`, }; } console.log( `[flock-direct] OK ref=${reference ?? "-"} ${quotes .map((q) => `${q.label}:$${q.totalUsd}`) .join(", ")}`, ); return { ok: true, quotes, reference }; } finally { await browser.close(); } }