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.
151 lines
4.2 KiB
151 lines
4.2 KiB
import { Prisma } from "@prisma/client";
|
|
import { QUOTE_VALIDITY_MS } from "@/lib/constants/quote";
|
|
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 { Priority1QuoteJobData } from "@/modules/priority1/rpa-queue";
|
|
import { buildPriority1StoredQuotes } from "@/modules/priority1/quote-storage";
|
|
import { RpaError } from "@/modules/rpa/errors";
|
|
import { recordPostDone, safeRecord } from "@/modules/metrics/collector";
|
|
import {
|
|
closeCircuit,
|
|
isCircuitOpen,
|
|
recordRpaFailure,
|
|
recordRpaSuccess,
|
|
} from "@/modules/cache/circuit-breaker";
|
|
import { PRIORITY1_MANUAL_FOLLOWUP_ERROR_CODE } from "@/modules/priority1/constants";
|
|
import { PRIORITY1_MANUAL_FOLLOWUP_MESSAGE } from "@/workers/rpa/priority1/quote-extract";
|
|
import { runPriority1QuoteRpa } from "@/workers/rpa/priority1/run-quote";
|
|
import { heartbeat, isWorkerPaused } from "@/workers/rpa/worker-state";
|
|
|
|
async function completePriority1ManualFollowup(
|
|
quoteId: string,
|
|
message: string,
|
|
): Promise<void> {
|
|
await prisma.quoteRecord.update({
|
|
where: { quoteId },
|
|
data: {
|
|
status: "done",
|
|
sourceType: "rpa",
|
|
isRealtime: true,
|
|
confidenceScore: getConfidenceScore("rpa"),
|
|
markupPercent: 0,
|
|
quotesJson: Prisma.JsonNull,
|
|
validUntil: null,
|
|
errorCode: PRIORITY1_MANUAL_FOLLOWUP_ERROR_CODE,
|
|
errorMessage: message,
|
|
},
|
|
});
|
|
|
|
safeRecord(() =>
|
|
recordPostDone({
|
|
quoteId,
|
|
sourceType: "rpa",
|
|
isRealtime: true,
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function processPriority1QuoteJob(
|
|
data: Priority1QuoteJobData,
|
|
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 {
|
|
const result = await runPriority1QuoteRpa(data.input);
|
|
if (!result.ok) {
|
|
throw new RpaError(
|
|
"RPA_DATA_INVALID",
|
|
result.errorMessage ?? "Priority1 报价提取失败",
|
|
{ retryable: true },
|
|
);
|
|
}
|
|
|
|
if (result.quoteOutcome === "manual_followup") {
|
|
await completePriority1ManualFollowup(
|
|
data.quoteId,
|
|
result.message ?? PRIORITY1_MANUAL_FOLLOWUP_MESSAGE,
|
|
);
|
|
await recordRpaSuccess();
|
|
await closeCircuit();
|
|
return;
|
|
}
|
|
|
|
if (result.quotes.length === 0) {
|
|
throw new RpaError(
|
|
"RPA_DATA_INVALID",
|
|
result.errorMessage ?? "Priority1 报价提取失败",
|
|
{ retryable: true },
|
|
);
|
|
}
|
|
|
|
const markupRule = await getMarkupRule(data.customerId);
|
|
const storedQuotes = buildPriority1StoredQuotes(
|
|
result.quotes,
|
|
markupRule,
|
|
data.input.shipmentType,
|
|
);
|
|
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 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;
|
|
}
|
|
}
|