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.

171 lines
5.3 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.

import { Prisma } from "@prisma/client";
import { QUOTE_VALIDITY_MS } from "@/lib/constants/quote";
import { getFlockMinQuotes } from "@/lib/flock/env";
import { isFlockNonRetryableFailure } from "@/lib/flock/exclusive-lock";
import { prisma } from "@/lib/prisma";
import { getMarkupRule } from "@/modules/pricing/engine";
import { getConfidenceScore } from "@/modules/quote/confidence";
import { handleRpaFailure } from "@/modules/quote/fallback-orchestrator";
import type { FlockQuoteJobData } from "@/modules/flock/rpa-queue";
import { buildFlockStoredQuotes } from "@/modules/flock/quote-storage";
import { RpaError } from "@/modules/rpa/errors";
import {
isProviderLoginFailureMessage,
PROVIDER_LOGIN_FAILED_USER_MESSAGE,
} from "@/modules/rpa/provider-login-message";
import { recordPostDone, safeRecord } from "@/modules/metrics/collector";
import {
closeCircuit,
isCircuitOpen,
recordRpaFailure,
recordRpaSuccess,
} from "@/modules/cache/circuit-breaker";
import { runFlockQuoteRpa } from "@/workers/rpa/flock/run-quote";
import { heartbeat, isWorkerPaused } from "@/workers/rpa/worker-state";
import {
buildFlockProgressPayload,
mapFlockErrorToStage,
parseFlockProgress,
shouldOverwriteFlockFailureStage,
type FlockRpaStage,
} from "@/lib/flock/rpa-progress";
async function writeFlockRpaStage(
quoteId: string,
stage: FlockRpaStage,
): Promise<void> {
try {
await prisma.quoteRecord.update({
where: { quoteId },
data: {
quotesJson: buildFlockProgressPayload(stage) as unknown as Prisma.InputJsonValue,
},
});
} catch (error) {
console.warn(
`[flock-job] 写进度失败 stage=${stage}:`,
error instanceof Error ? error.message : error,
);
}
}
async function readFlockProgressStage(
quoteId: string,
): Promise<string | null> {
try {
const row = await prisma.quoteRecord.findUnique({
where: { quoteId },
select: { quotesJson: true },
});
return parseFlockProgress(row?.quotesJson)?.stage ?? null;
} catch {
return null;
}
}
export async function processFlockQuoteJob(
data: FlockQuoteJobData,
workerId: string,
options?: { isFinalAttempt?: boolean },
): Promise<void> {
await heartbeat(workerId);
if (await isCircuitOpen()) {
throw new RpaError("PAGE_LOAD_TIMEOUT", "RPA 熔断中,暂缓消费", {
retryable: true,
});
}
if (await isWorkerPaused(workerId)) {
throw new RpaError("RPA_CAPTCHA", "Worker 已暂停", { retryable: true });
}
try {
// 每轮(含 BullMQ 重试)从互斥锁阶段开始;勿在重试时把阶段回退逻辑搞乱——这里确实要重新抢锁
await writeFlockRpaStage(data.quoteId, "lock");
const result = await runFlockQuoteRpa(data.input, {
customerId: data.customerId,
onStage: (stage) => writeFlockRpaStage(data.quoteId, stage),
});
const minQuotes = getFlockMinQuotes();
if (!result.ok || result.quotes.length < minQuotes) {
const msg = result.errorMessage ?? "Flock 报价提取失败";
if (isProviderLoginFailureMessage(msg)) {
throw new RpaError(
"PROVIDER_LOGIN_FAILED",
PROVIDER_LOGIN_FAILED_USER_MESSAGE,
{ retryable: false },
);
}
// 上限/拒号/互斥忙:禁止 BullMQ 二次空烧(否则加重 IP 风控且拖慢 embed
throw new RpaError("RPA_DATA_INVALID", msg, {
retryable: !isFlockNonRetryableFailure(msg),
});
}
const markupRule = await getMarkupRule(data.customerId);
const storedQuotes = buildFlockStoredQuotes(
result.quotes,
markupRule,
result.reference,
);
const validUntil = new Date(Date.now() + QUOTE_VALIDITY_MS);
await prisma.quoteRecord.update({
where: { quoteId: data.quoteId },
data: {
status: "done",
sourceType: "rpa",
isRealtime: true,
confidenceScore: getConfidenceScore("rpa"),
markupPercent:
markupRule.type === "percent" ? markupRule.percent : 0,
quotesJson: storedQuotes as unknown as Prisma.InputJsonValue,
validUntil,
errorCode: null,
errorMessage: null,
},
});
safeRecord(() =>
recordPostDone({
quoteId: data.quoteId,
sourceType: "rpa",
isRealtime: true,
}),
);
await recordRpaSuccess();
await closeCircuit();
} catch (error) {
await recordRpaFailure();
const rawMsg = error instanceof Error ? error.message : String(error);
const mapped = mapFlockErrorToStage(rawMsg);
const currentStage = await readFlockProgressStage(data.quoteId);
if (shouldOverwriteFlockFailureStage(currentStage, mapped)) {
await writeFlockRpaStage(data.quoteId, mapped.stage);
} else if (currentStage) {
console.warn(
`[flock-job] 保留进度 stage=${currentStage}(不因未匹配错误回退到 ${mapped.stage}err=${rawMsg.slice(0, 120)}`,
);
}
const isFinalAttempt = options?.isFinalAttempt ?? true;
const canRetry =
error instanceof RpaError && error.retryable && !isFinalAttempt;
if (canRetry) throw error;
await handleRpaFailure(
data.quoteId,
data.cargoHash,
data.customerId,
error,
{ skipStaleFallback: true },
);
if (error instanceof RpaError && !error.retryable) return;
throw error;
}
}