import type { AddressLookupInput, MothershipAddressCandidate, } from "@/modules/address/types"; import { AxelHttpClient } from "@/lib/axel/client"; import type { AxelSearchCandidate } from "@/lib/axel/pick-candidate"; import { labelToCandidate } from "@/workers/rpa/address-suggestions"; const UNAVAILABLE_REASON = "MotherShip 无法定位此地址,请选择其他候选或修改街道信息"; function buildSearchQuery(address: AddressLookupInput): string { return [address.street, address.city, address.state, address.zip] .map((part) => part?.trim()) .filter(Boolean) .join(", "); } function parseStateFromSecondary(secondary: string): { city: string; state: string; zip: string; } { const m = secondary.match(/^([^,]+),\s*([A-Z]{2})(?:\s+(\d{5}(?:-\d{4})?))?/); if (m) { return { city: m[1]!.trim(), state: m[2]!, zip: m[3] ?? "" }; } const parts = secondary.split(",").map((p) => p.trim()); return { city: parts[0] ?? "", state: parts[1]?.slice(0, 2) ?? "", zip: "", }; } /** axel search 行 → 候选;展示文案与 API description 一致 */ export function axelSearchRowToCandidate( row: AxelSearchCandidate, query: AddressLookupInput, ): MothershipAddressCandidate | null { const description = row.description.trim(); const optionId = row.placeId.trim(); if (!description || !optionId) { return null; } const parsed = labelToCandidate(description, query); if (parsed) { return { ...parsed, option_id: optionId, display_label: description, formatted_address: description, }; } const mainText = (row.mainText ?? description.split(",")[0] ?? "").trim(); const secondary = (row.secondaryText ?? "").trim(); const tail = parseStateFromSecondary(secondary); if (!mainText) { return null; } return { option_id: optionId, display_label: description, formatted_address: description, street: mainText, city: tail.city || query.city, state: tail.state || query.state, zip: tail.zip || query.zip, }; } function sortByStreetNumber( candidates: MothershipAddressCandidate[], query: AddressLookupInput, ): MothershipAddressCandidate[] { const streetNum = query.street.match(/^\d+/)?.[0]; if (!streetNum) { return candidates; } return [...candidates].sort((a, b) => { const aHas = `${a.street} ${a.display_label}`.includes(streetNum); const bHas = `${b.street} ${b.display_label}`.includes(streetNum); if (aHas === bHas) { return 0; } return aHas ? -1 : 1; }); } function dedupeByOptionId( candidates: MothershipAddressCandidate[], ): MothershipAddressCandidate[] { const seen = new Set(); const out: MothershipAddressCandidate[] = []; for (const row of candidates) { if (seen.has(row.option_id)) { continue; } seen.add(row.option_id); out.push(row); } return out; } async function verifyCandidate( client: AxelHttpClient, candidate: MothershipAddressCandidate, ): Promise { try { await client.fetchPlace(candidate.option_id); return { ...candidate, selectable: true }; } catch { return { ...candidate, selectable: false, unavailable_reason: UNAVAILABLE_REASON, }; } } async function verifyCandidates( client: AxelHttpClient, candidates: MothershipAddressCandidate[], ): Promise { return Promise.all(candidates.map((c) => verifyCandidate(client, c))); } function assertHasSelectable( candidates: MothershipAddressCandidate[], sideLabel: string, ): void { if (candidates.some((c) => c.selectable !== false)) { return; } throw new Error( `${sideLabel}地址在 MotherShip 均无法定位,请修改街道或城市后重试`, ); } export async function resolveAxelMothershipCandidates(input: { pickup: AddressLookupInput; delivery: AddressLookupInput; }): Promise<{ pickup_candidates: MothershipAddressCandidate[]; delivery_candidates: MothershipAddressCandidate[]; }> { const client = AxelHttpClient.fromStorageStatePath(); const [pickupRows, deliveryRows] = await Promise.all([ client.searchLocation(buildSearchQuery(input.pickup)), client.searchLocation(buildSearchQuery(input.delivery)), ]); const pickup_candidates = await verifyCandidates( client, sortByStreetNumber( dedupeByOptionId( pickupRows .map((row) => axelSearchRowToCandidate(row, input.pickup)) .filter((row): row is MothershipAddressCandidate => row != null), ), input.pickup, ), ); const delivery_candidates = await verifyCandidates( client, sortByStreetNumber( dedupeByOptionId( deliveryRows .map((row) => axelSearchRowToCandidate(row, input.delivery)) .filter((row): row is MothershipAddressCandidate => row != null), ), input.delivery, ), ); if (pickup_candidates.length === 0) { throw new Error("MotherShip 未返回提货地址候选"); } if (delivery_candidates.length === 0) { throw new Error("MotherShip 未返回派送地址候选"); } assertHasSelectable(pickup_candidates, "提货"); assertHasSelectable(delivery_candidates, "派送"); return { pickup_candidates, delivery_candidates }; }