import type { QuoteContractRates } from "@/workers/rpa/quote-capture/quote-contract-guard"; /** 归一化后的报价 rates(契约校验唯一输入) */ export type CanonicalQuote = { rates: QuoteContractRates; /** 解析来源路径(debug) */ sourcePath: string; }; function isRatesObject(value: unknown): value is QuoteContractRates { return value != null && typeof value === "object" && !Array.isArray(value); } function hasRatesBranchShape(rates: QuoteContractRates): boolean { return ( "standard" in rates || "guaranteed" in rates || "fastest" in rates ); } function readPath(obj: unknown, ...keys: string[]): unknown { let cur = obj; for (const key of keys) { if (cur == null || typeof cur !== "object" || Array.isArray(cur)) { return undefined; } cur = (cur as Record)[key]; } return cur; } type RatesCandidate = { rates: QuoteContractRates; sourcePath: string }; function tryRatesAt( rates: unknown, sourcePath: string, ): RatesCandidate | null { if (!isRatesObject(rates) || !hasRatesBranchShape(rates)) { return null; } return { rates, sourcePath }; } /** * 从 API body 解析 rates(支持常见嵌套变体)。 * body.rates | body.data.rates | body.result.rates | array[0].rates */ export function resolveRatesFromBody(body: unknown): RatesCandidate | null { if (body == null) { return null; } if (Array.isArray(body)) { if (body.length > 0) { const fromFirst = tryRatesAt( readPath(body[0], "rates"), "[0].rates", ); if (fromFirst) { return fromFirst; } } return null; } if (typeof body !== "object") { return null; } const candidates: RatesCandidate[] = []; const direct = tryRatesAt(readPath(body, "rates"), "rates"); if (direct) { candidates.push(direct); } const fromData = tryRatesAt(readPath(body, "data", "rates"), "data.rates"); if (fromData) { candidates.push(fromData); } const fromResult = tryRatesAt( readPath(body, "result", "rates"), "result.rates", ); if (fromResult) { candidates.push(fromResult); } return candidates[0] ?? null; } /** body → CanonicalQuote;无法解析 rates 时返回 null */ export function normalizeQuote(body: unknown): CanonicalQuote | null { const resolved = resolveRatesFromBody(body); if (!resolved) { return null; } return { rates: resolved.rates, sourcePath: resolved.sourcePath, }; }