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.
chajia/workers/rpa/priority1-job-handler.ts

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;
}
}