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.

222 lines
6.8 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 Freight get-a-quote RPAPRD §14 / 技术设计 §7.6
* DirectPOST 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<FlockRunQuoteResult> {
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: "34 个工作日(预估)",
reference: "FRG-MOCK",
},
];
}
async function runFlockDomPath(
input: FlockQuoteInput,
onStage?: FlockStageReporter,
): Promise<FlockRunQuoteResult> {
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<FlockRunQuoteResult> {
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/quotessource=${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,
};
}
}