import { Prisma } from "@prisma/client"; import { writeAlert } from "@/modules/alert/service"; import { getL3 } from "@/modules/cache/redis-cache"; import { QUOTE_VALIDITY_MS } from "@/lib/constants/quote"; import { prisma } from "@/lib/prisma"; import { getConfidenceScore } from "@/modules/quote/confidence"; import { applyMarkupToQuotes, getMarkupRule, type RawQuoteTier, } from "@/modules/pricing/engine"; import { prepareMotherShipStorageQuotes } from "@/modules/quote/quote-completeness"; import { recordPostDone, safeRecord } from "@/modules/metrics/collector"; import { RpaError } from "@/modules/rpa/errors"; import { isEntryErrorCode } from "@/modules/rpa/error-mapper"; import { resolveQuoteFailureCode, resolveQuoteFailureMessage, truncateQuoteErrorMessage, type QuoteFailureCode, } from "@/modules/quote/quote-error-messages"; import type { L2CachePayload } from "@/modules/quote/types"; import { recordQuoteQueryOutcome } from "@/modules/quote/query-log"; export type FallbackResult = | { status: "done"; sourceType: "stale"; isRealtime: false; quotes: ReturnType; } | { status: "failed"; errorCode: QuoteFailureCode; errorMessage: string; }; /** 入口类错误不重试,直接降级(task-129) */ export function shouldConsumeRpaRetry(error: unknown): boolean { if (error instanceof RpaError && isEntryErrorCode(error.code)) { return false; } return true; } /** * RPA 失败 → L3 降级完整编排(task-092) * 不抛异常阻断主流程 */ export async function handleRpaFailure( quoteId: string, cargoHash: string, customerId: string, error: unknown, options?: { skipStaleFallback?: boolean }, ): Promise { const l3 = options?.skipStaleFallback ? null : await getL3(cargoHash); const errorMessage = error instanceof Error ? error.message : String(error ?? "unknown"); const failureCode = resolveQuoteFailureCode(error); const userMessage = truncateQuoteErrorMessage( resolveQuoteFailureMessage(error, failureCode), ); if (l3?.quotes?.length) { let uiQuotes: RawQuoteTier[]; try { uiQuotes = prepareMotherShipStorageQuotes(l3.quotes as RawQuoteTier[]); } catch { await prisma.quoteRecord.update({ where: { quoteId }, data: { status: "failed", errorCode: failureCode, errorMessage: userMessage, sourceType: null, isRealtime: false, }, }); await writeAlert("RPA_FAILED", { quoteId, cargoHash, detail: { reason: "L3 数据不完整", error: errorMessage }, }); await recordQuoteQueryOutcome(quoteId, "failed", { failureReason: userMessage, errorCode: "QUOTE_UNAVAILABLE", }); return { status: "failed", errorCode: "QUOTE_UNAVAILABLE", errorMessage: userMessage, }; } const markupRule = await getMarkupRule(customerId); const markedQuotes = applyMarkupToQuotes(uiQuotes, markupRule); const validUntil = new Date(Date.now() + QUOTE_VALIDITY_MS); await prisma.quoteRecord.update({ where: { quoteId }, data: { status: "done", sourceType: "stale", isRealtime: false, confidenceScore: getConfidenceScore("stale"), markupPercent: markupRule.type === "percent" ? markupRule.percent : 0, quotesJson: markedQuotes as unknown as Prisma.InputJsonValue, validUntil, errorCode: null, }, }); await writeAlert("STALE_FALLBACK", { quoteId, cargoHash, detail: { reason: errorMessage, customer_id: customerId }, }); await recordQuoteQueryOutcome(quoteId, "stale", { failureReason: userMessage, sourceType: "stale", tierCount: markedQuotes.length, }); safeRecord(() => recordPostDone({ quoteId, sourceType: "stale", isRealtime: false, }), ); return { status: "done", sourceType: "stale", isRealtime: false, quotes: markedQuotes, }; } await prisma.quoteRecord.update({ where: { quoteId }, data: { status: "failed", errorCode: failureCode, errorMessage: userMessage, isRealtime: false, }, }); await writeAlert("RPA_FAILED", { quoteId, cargoHash, detail: { reason: errorMessage, customer_id: customerId }, }); await recordQuoteQueryOutcome(quoteId, "failed", { failureReason: userMessage, errorCode: failureCode, }); return { status: "failed", errorCode: failureCode, errorMessage: userMessage }; }