/** * Flock Freight get-a-quote RPA(PRD §14 / 技术设计 §7.6) * Direct:POST api.flockfreight.com/user/quotes;失败可回退 DOM 填表 * 客户账密:强制 DOM 登录,跳过 Direct */ import { formatExternalSiteNavigationError, isTransientNavigationError, } from "@/lib/rpa/page-goto"; import { fetchFlockQuotesViaDirect } from "@/lib/flock/direct-quote"; import { getFlockLoginCredentials, getFlockQuoteMode, getFlockSessionMaxRotations, isFlockDirectQuoteEnabled, isFlockDomQuoteFallbackEnabled, isFlockWorkerReuseSession, } from "@/lib/flock/env"; import { getFlockLoginSource, hasEffectiveFlockLogin, runWithFlockLoginContext, } from "@/lib/flock/login-context"; import { withFlockExclusiveLock } from "@/lib/flock/exclusive-lock"; import { getCustomerProviderLogin, hasCustomerProviderCredential } from "@/modules/customer/provider-credentials"; import { runFlockVisualChain } from "@/workers/rpa/flock/visual-chain"; import { runFlockQuoteWithWorkerSession, shouldRotateFlockSession, } from "@/workers/rpa/flock/worker-session"; import type { FlockStageReporter } from "@/lib/flock/rpa-progress"; import { reportFlockStage } from "@/lib/flock/rpa-progress"; import type { FlockQuoteInput, FlockQuoteLine, FlockRunQuoteResult, } from "@/workers/rpa/flock/types"; export type { FlockStageReporter }; function isFlockQuoteLimitError(errorMessage?: string): boolean { return /flock_quote_limit|thank you for your interest/i.test( errorMessage ?? "", ); } /** 无痕全链:拒号/撞号时换邮箱重试(每轮新浏览器,对齐 batch 有效路径) */ export async function runFlockVisualChainWithRotations( input: FlockQuoteInput, onStage?: FlockStageReporter, ): Promise { const maxRotations = getFlockSessionMaxRotations(); let lastResult: FlockRunQuoteResult = { ok: false, quotes: [], errorMessage: "FLOCK_QUOTE_LIMIT:未执行查价", }; for (let rotation = 0; rotation < maxRotations; rotation += 1) { const result = await runFlockVisualChain({ input, onStage }); lastResult = result; if (result.ok) { return result; } if (isFlockQuoteLimitError(result.errorMessage)) { return result; } if (shouldRotateFlockSession(result.errorMessage)) { console.warn( `[flock-rpa] ${result.errorMessage?.slice(0, 80)}… 无痕换新邮箱 (${rotation + 1}/${maxRotations})`, ); continue; } return result; } return { ok: false, quotes: [], errorMessage: lastResult.errorMessage ?? `FLOCK_ACCOUNT_REJECTED:已换 ${maxRotations} 个邮箱仍无法查价`, }; } function mockFlockQuotes(): FlockQuoteLine[] { return [ { tier: "flock_direct", serviceLevel: "guaranteed", rateOption: "fastest", carrier: "Flock Freight", label: "FlockDirect®", totalUsd: 2330, transitDays: "4", transitDescription: "4 天(含周末)", reference: "FRG-MOCK", }, { tier: "standard", serviceLevel: "standard", rateOption: "lowest", carrier: "Flock Freight", label: "Standard", totalUsd: 769, transitDays: "3-4", transitDescription: "3–4 个工作日(预估)", reference: "FRG-MOCK", }, ]; } async function runFlockDomPath( input: FlockQuoteInput, onStage?: FlockStageReporter, ): Promise { if (hasEffectiveFlockLogin()) { console.log( `[flock-rpa] DOM 使用登录凭据 source=${getFlockLoginSource()},跳过邮箱轮换`, ); if (isFlockWorkerReuseSession()) { return runFlockQuoteWithWorkerSession(input, onStage); } return runFlockVisualChain({ input, onStage }); } if (isFlockWorkerReuseSession()) { return runFlockQuoteWithWorkerSession(input, onStage); } return runFlockVisualChainWithRotations(input, onStage); } export type RunFlockQuoteOptions = { customerId?: string; onStage?: FlockStageReporter; }; /** 执行 Flock 官网查价(Mock / Direct / DOM) */ export async function runFlockQuoteRpa( input: FlockQuoteInput, options?: RunFlockQuoteOptions, ): Promise { if (process.env.RPA_MOCK_MODE === "true") { return { ok: true, quotes: mockFlockQuotes(), reference: "FRG-MOCK" }; } const customerId = options?.customerId?.trim(); let customerLogin = customerId ? await getCustomerProviderLogin(customerId, "flock") : null; const onStage = options?.onStage; // 已配置账密但解密失败:禁止回落 Direct API(否则拿不到折扣价) if (customerId && !customerLogin) { const configured = await hasCustomerProviderCredential(customerId, "flock"); if (configured) { await reportFlockStage(onStage, "login"); return { ok: false, quotes: [], errorMessage: "FLOCK_LOGIN_FAILED:客户已配置 Flock 账密但解密失败,请检查 JWT_SECRET 后重新保存账密(禁止改走 Direct API)", }; } } const envLogin = getFlockLoginCredentials(); const login = customerLogin ?? envLogin; const source = customerLogin ? "customer" : envLogin ? "env" : null; try { return await withFlockExclusiveLock( "embed-worker", () => runWithFlockLoginContext(login, source, async () => { const mode = getFlockQuoteMode(); console.log( `[flock-rpa] quoteMode=${mode} hasLogin=${Boolean(login)} source=${source ?? "none"} path=${login ? "dom-login" : "direct_or_dom"}`, ); // 客户/环境账密:强制官网登录 DOM,禁止匿名 Direct API if (login) { console.log( `[flock-rpa] 已配置登录账密,强制 DOM 登录查价(禁止 Direct /user/quotes)source=${source}`, ); return runFlockDomPath(input, onStage); } if (isFlockDirectQuoteEnabled()) { const direct = await fetchFlockQuotesViaDirect(input); if (direct.ok) { await reportFlockStage(onStage, "done"); return direct; } console.warn( `[flock-rpa] Direct 失败: ${direct.errorMessage?.slice(0, 160) ?? "unknown"}`, ); if (!isFlockDomQuoteFallbackEnabled()) { return direct; } console.warn("[flock-rpa] 回退 DOM 填表路径…"); } return runFlockDomPath(input, onStage); }), { waitMs: 30_000, pollMs: 500 }, ); } 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, }; } }