import { Prisma } from "@prisma/client"; import { QUOTE_VALIDITY_MS } from "@/lib/constants/quote"; import { CAPTCHA_PAUSE_MINUTES } from "@/lib/constants/rpa"; import { prepareMotherShipStorageQuotes } from "@/modules/quote/quote-completeness"; import { detectDeviation } from "@/modules/alert/deviation-detector"; import { writeAlert } from "@/modules/alert/service"; import { closeCircuit, isCircuitOpen, recordRpaFailure, recordRpaSuccess, } from "@/modules/cache/circuit-breaker"; import { overwriteL1, setL2, setL3 } from "@/modules/cache/redis-cache"; import { prisma } from "@/lib/prisma"; import { applyMarkupToQuotes, getMarkupRule } from "@/modules/pricing/engine"; import { getConfidenceScore } from "@/modules/quote/confidence"; import { handleRpaFailure } from "@/modules/quote/fallback-orchestrator"; import type { QuoteJobData } from "@/modules/quote/rpa-queue"; import { NO_RETRY_CODES, RpaError } from "@/modules/rpa/errors"; import { isEntryErrorCode } from "@/modules/rpa/error-mapper"; import { cargoToQuoteRequest, quoteItemsToRawTiers, } from "@/modules/rpa/quote-mapper"; import { recordPostDone, safeRecord } from "@/modules/metrics/collector"; import { recordQuoteQueryOutcome } from "@/modules/quote/query-log"; import { mothershipProvider } from "@/workers/rpa/mothership"; import { heartbeat, isWorkerPaused, pauseWorker, } from "@/workers/rpa/worker-state"; async function writeRpaAlert( alertType: "RPA_CAPTCHA" | "STRUCT_CHANGE" | "SESSION_EXPIRED", quoteId: string, cargoHash: string, detail: Record, ): Promise { await writeAlert(alertType, { quoteId, cargoHash, detail }); } async function completeQuoteSuccess( data: QuoteJobData, rawQuotes: ReturnType, ): Promise { const uiQuotes = prepareMotherShipStorageQuotes(rawQuotes); const markupRule = await getMarkupRule(data.customerId); const markedQuotes = applyMarkupToQuotes(uiQuotes, markupRule); const validUntil = new Date(Date.now() + QUOTE_VALIDITY_MS); const cachePayload = { quotes: uiQuotes }; await setL2(data.cargoHash, cachePayload); await setL3(data.cargoHash, cachePayload); 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: markedQuotes as unknown as Prisma.InputJsonValue, validUntil, errorCode: null, }, }); await detectDeviation(data.cargoHash, uiQuotes, data.quoteId); const doneResponse = { quote_id: data.quoteId, status: "done" as const, source_type: "rpa" as const, is_realtime: true, }; await overwriteL1(data.requestId, { quoteId: data.quoteId, response: doneResponse, }); await recordQuoteQueryOutcome(data.quoteId, "success", { sourceType: "rpa", tierCount: markedQuotes.length, }); } /** 单条 RPA job 处理(task-029) */ export async function processQuoteJob( data: QuoteJobData, workerId: string, options?: { isFinalAttempt?: boolean }, ): Promise { 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 mothershipProvider.getQuote( cargoToQuoteRequest(data.cargo), ); const rawQuotes = quoteItemsToRawTiers(result.items); await completeQuoteSuccess(data, rawQuotes); safeRecord(() => recordPostDone({ quoteId: data.quoteId, sourceType: "rpa", isRealtime: true, }), ); await recordRpaSuccess(); await closeCircuit(); } catch (error) { if (error instanceof RpaError) { if (error.pauseWorker) { await pauseWorker(workerId, CAPTCHA_PAUSE_MINUTES); await writeRpaAlert("RPA_CAPTCHA", data.quoteId, data.cargoHash, { worker_id: workerId, message: error.message, }); } else if (error.code === "STRUCT_CHANGE") { await writeRpaAlert("STRUCT_CHANGE", data.quoteId, data.cargoHash, { message: error.message, }); } else if (error.code === "QUOTE_ENTRY_UNAVAILABLE") { await writeRpaAlert("STRUCT_CHANGE", data.quoteId, data.cargoHash, { message: error.message, entry_error: "QUOTE_ENTRY_UNAVAILABLE", }); } else if (error.code === "SESSION_EXPIRED") { await writeRpaAlert("SESSION_EXPIRED", data.quoteId, data.cargoHash, { message: error.message, }); } } const shouldCountFailure = !( error instanceof RpaError && (NO_RETRY_CODES.has(error.code) || isEntryErrorCode(error.code)) ); if (shouldCountFailure) { await recordRpaFailure(); } const isFinalAttempt = options?.isFinalAttempt ?? true; const canRetry = error instanceof RpaError && error.retryable && !isEntryErrorCode(error.code) && !isFinalAttempt; if (canRetry) { throw error; } await handleRpaFailure( data.quoteId, data.cargoHash, data.customerId, error, ); if (error instanceof RpaError && !error.retryable) { return; } throw error; } }