You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

163 lines
4.9 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 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<ReturnType<typeof createRpaBrowserContext>>,
timeoutMs: number,
): Promise<boolean> {
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<FlockRunQuoteResult> {
// 防御在深度:客户账密上下文下禁止走 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<typeof mapFlockFulfillmentOptionsToLines>[0],
);
const minQuotes = getFlockMinQuotes();
if (quotes.length < minQuotes) {
return {
ok: false,
quotes,
reference,
errorMessage: `FLOCK_DIRECT_NO_RATESfulfillmentOptions 映射后不足 ${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();
}
}