import type { QuoteRequest } from "@/modules/providers/quote-provider"; import { pageLocator } from "@/lib/rpa/locator"; import { isRpaAddressMockMode } from "@/lib/rpa/address-mode"; import { isRpaAddressRushMode, isDualAddressPatchEnabled, isBlindAddressFlowActive, isPreconfirmedMothershipQuote, isRpaBlindAddressFlow, resolveBlindAddressFlow, } from "@/lib/rpa/env"; import { isAddressChipVisuallyCommitted } from "@/workers/rpa/address-chip-visual"; import { RpaError } from "@/modules/rpa/errors"; import { assertRPAContext } from "@/workers/rpa/kernel/context-validator"; import type { RPAContext } from "@/workers/rpa/kernel/context"; import { createAddressAdapter, formatDiagnosticsFailure, } from "@/workers/rpa/address-adapter"; import { isAddressSuggestionDropdownOpen, getAddressConfirmationSurfaceText, } from "@/workers/rpa/address-suggestions"; import { openAddressSide } from "@/workers/rpa/address-side"; import { assertDualAddressStable, blurAddressEditing, dismissAddressSuggestionDropdown, ensureDualAddressCommitted, isAddressesVisuallyCompleteForCargo, isPickupReadyForDeliveryInput, lockPickupIntoChip, releasePickupFieldForDelivery, settlePickupBeforeDelivery, widgetContainsConfirmedAddress, } from "@/workers/rpa/address-commit"; import { runKernelSubStep } from "@/workers/rpa/kernel/step-guard"; import { stabilizeQuoteLandingPage } from "@/workers/rpa/page-prep"; import { assertBothAddressesLocked, settleAddressFieldsForCargo, } from "@/workers/rpa/cargo-lock"; import { applyDeliveryDualAddressPatch, applyPickupDualAddressPatch, preservePickupInWidget, releasePickupInputInWidget, } from "@/workers/rpa/inject/dual-address-patch"; export { widgetContainsConfirmedAddress } from "@/workers/rpa/address-commit"; type AddressFields = QuoteRequest["pickup"]; async function assertConfirmedAddressesInWidget( ctx: RPAContext, widgetSelector: string, pickup: AddressFields, delivery: AddressFields, ): Promise { const surfaceText = await getAddressConfirmationSurfaceText( ctx.page, widgetSelector, ); const fullText = await pageLocator(ctx.page, widgetSelector).innerText(); const widgetText = [surfaceText, fullText] .map((part) => part.replace(/\s+/g, " ").trim()) .filter(Boolean) .join(" "); // 快速通道:如果widget已经包含两个地址,信任视觉状态,忽略dropdown检测 const pickupPresent = widgetContainsConfirmedAddress(widgetText, pickup); const deliveryPresent = widgetContainsConfirmedAddress(widgetText, delivery); if (pickupPresent && deliveryPresent) { console.log("[rpa] assertConfirmedAddressesInWidget: 视觉快速通道 - 两地址已在widget中"); return; } if ( !surfaceText.trim() && (await isAddressSuggestionDropdownOpen(ctx.page)) ) { throw new RpaError( "ADDRESS_NOT_CONFIRMED", "地址联想下拉仍打开,未完成点选锁定", { retryable: true }, ); } if (!pickupPresent) { throw new RpaError( "ADDRESS_NOT_CONFIRMED", `widget 未体现已确认提货地址:${pickup.mothershipDisplayLabel}`, { retryable: true }, ); } if (!deliveryPresent) { throw new RpaError( "ADDRESS_NOT_CONFIRMED", `widget 未体现已确认派送地址:${delivery.mothershipDisplayLabel}`, { retryable: true }, ); } } /** 单侧填址 + commit 屏障(pickup 先锁定再开 delivery);业务流程仅调用 AddressAdapter */ export async function fillAddressWithCommitStep( ctx: RPAContext, side: "pickup" | "delivery", address: AddressFields, pickupAnchor?: AddressFields, ): Promise { const widgetSelector = ctx.selectors.resolve("RPA_SELECTOR_QUOTE_WIDGET"); const mockMode = isRpaAddressMockMode(); const rush = isRpaAddressRushMode(); const blind = isBlindAddressFlowActive(ctx); const rushOrBlind = rush || blind; if (side === "delivery") { if (!pickupAnchor) { throw new RpaError( "ADDRESS_NOT_CONFIRMED", "填写派送地址前缺少提货锚点地址", { retryable: false }, ); } const pickupVaultReady = isDualAddressPatchEnabled() && Boolean(ctx.state.axelPlaceLocations.pickup); if (!rushOrBlind) { if (pickupVaultReady) { console.log( "[rpa] address-step: vault pickup 已注入,打开派送侧前 settle", ); } await runKernelSubStep("fillAddress", "settlePickupBeforeDelivery", () => settlePickupBeforeDelivery(ctx, widgetSelector, pickupAnchor), ); if (!pickupVaultReady) { await runKernelSubStep( "fillAddress", "assertPickupStableBeforeDelivery", () => assertDualAddressStable(ctx, widgetSelector, pickupAnchor, null), ); } } else { console.log( blind ? "[rpa] blind-flow: 跳过 settlePickupBeforeDelivery" : "[rpa] visual-rush: 跳过 settlePickupBeforeDelivery", ); } } ctx.state.markAddressSide(side); const street = await runKernelSubStep( "fillAddress", `${side}/openAddressSide`, () => openAddressSide(ctx, side), ); if (side === "delivery" && pickupAnchor && !mockMode && !rushOrBlind) { const widgetText = await pageLocator(ctx.page, widgetSelector) .innerText() .catch(() => ""); if (!widgetContainsConfirmedAddress(widgetText, pickupAnchor)) { throw new RpaError( "ADDRESS_NOT_CONFIRMED", "打开派送侧后提货地址已从 widget 消失", { retryable: true }, ); } } if (!(await street.isEditable().catch(() => false))) { throw new RpaError( "STRUCT_CHANGE", `${side === "pickup" ? "提货" : "派送"}地址输入框不可编辑`, { retryable: false }, ); } await runKernelSubStep("fillAddress", `${side}/addressAdapter`, async () => { const adapter = createAddressAdapter(); let commit; try { await adapter.selectAddress({ ctx, side, address, street, widgetSelector, pickupAnchor: side === "delivery" ? pickupAnchor : undefined, }); if (!blind) { commit = await adapter.waitForCommit(); } } catch (error) { if (blind) { console.log(`[rpa] blind-flow: ${side} adapter 异常已忽略,继续下一步`); return; } const chipOk = await isAddressChipVisuallyCommitted( ctx.page, widgetSelector, address, ); if ( chipOk && (rush || (side === "delivery" && !mockMode && pickupAnchor && (await isAddressesVisuallyCompleteForCargo( ctx.page, widgetSelector, pickupAnchor, address, )))) ) { if (rush) { console.log( `[rpa] visual-rush: ${side} chip 视觉已确认,继续下一步`, ); } else { console.log( "[rpa] address-step: delivery commit 未通过但视觉双地址已齐,继续货物步", ); } if (!rush) { await dismissAddressSuggestionDropdown(ctx.page, street); await blurAddressEditing(ctx, widgetSelector); } return; } if (error instanceof RpaError) { const diag = adapter.getDiagnostics(); if (!error.message.includes("commitState=")) { throw new RpaError( "ADDRESS_NOT_CONFIRMED", formatDiagnosticsFailure(diag), { retryable: error.retryable }, ); } } throw error; } if (blind) { console.log(`[rpa] blind-flow: ${side} 完成,不校验 commit`); return; } if (!commit?.committed || adapter.getCommitState() !== "COMMITTED") { throw new RpaError( "ADDRESS_NOT_CONFIRMED", formatDiagnosticsFailure(adapter.getDiagnostics()), { retryable: true }, ); } const diag = adapter.getDiagnostics(); if (!mockMode && !rush && !diag.placeRequestObserved) { throw new RpaError( "ADDRESS_NOT_CONFIRMED", formatDiagnosticsFailure({ ...diag, status: "no_place_request", }), { retryable: true }, ); } if (side === "pickup" && !mockMode && !rush) { await lockPickupIntoChip(ctx, widgetSelector, street, address); await ctx.page.waitForTimeout(1000); let locked = await isPickupReadyForDeliveryInput( ctx.page, widgetSelector, address, ); if (!locked) { locked = await releasePickupFieldForDelivery( ctx, widgetSelector, address, street, ); } if (!locked && diag.placeRequestObserved) { console.log( "[rpa] address-step: pickup place 200 已观测,chip 未就绪仍继续", ); } else if (!locked) { console.log("[rpa] pickup-commit-diagnostic", { phase: "after-pickup-adapter", placeId: diag.placeId, commitState: diag.commitState, placeRequestObserved: diag.placeRequestObserved, method: diag.method, }); throw new RpaError( "ADDRESS_NOT_CONFIRMED", "Pickup commit 后 widget 未锁定(无独立派送框)", { retryable: true }, ); } if (!ctx.state.axelPlaceLocations.pickup) { throw new RpaError( "ADDRESS_NOT_CONFIRMED", "Pickup commit 后 axel place 详情未写入状态", { retryable: true }, ); } console.log("[rpa] address-step:pickup-locked", { placeId: diag.placeId.slice(0, 24), method: diag.method, placeRequestObserved: diag.placeRequestObserved, }); if (isDualAddressPatchEnabled() && ctx.state.axelPlaceLocations.pickup) { await applyPickupDualAddressPatch(ctx.page, { placeId: diag.placeId, location: ctx.state.axelPlaceLocations.pickup, description: address.mothershipDisplayLabel?.trim() || address.formattedAddress?.trim() || address.street, }); } } if (side === "pickup" && rush) { console.log("[rpa] visual-rush: pickup 已点选联想,跳过 chip 锁定轮询"); } if (side === "delivery" && !mockMode && !rush) { if ( isDualAddressPatchEnabled() && ctx.state.axelPlaceLocations.pickup && ctx.state.axelPlaceLocations.delivery && pickupAnchor ) { await applyDeliveryDualAddressPatch( ctx.page, { placeId: pickupAnchor.placeId ?? "", location: ctx.state.axelPlaceLocations.pickup, description: pickupAnchor.mothershipDisplayLabel?.trim() || pickupAnchor.formattedAddress?.trim() || pickupAnchor.street, }, { placeId: diag.placeId, location: ctx.state.axelPlaceLocations.delivery, description: address.mothershipDisplayLabel?.trim() || address.formattedAddress?.trim() || address.street, }, ); } if (diag.pickupLostAfterDelivery) { throw new RpaError( "ADDRESS_NOT_CONFIRMED", "填派送后提货 place commit 丢失(pickupLostAfterDelivery=true)", { retryable: true }, ); } if (!ctx.state.axelPlaceLocations.pickup || !ctx.state.axelPlaceLocations.delivery) { throw new RpaError( "ADDRESS_NOT_CONFIRMED", "双地址 commit 后 axel place 状态不完整", { retryable: true }, ); } console.log("[rpa] address-step:dual-address-committed", { pickupPlaceId: diag.placeId.slice(0, 24), pickupLostAfterDelivery: diag.pickupLostAfterDelivery ?? false, placeRequestObserved: diag.placeRequestObserved, }); } await dismissAddressSuggestionDropdown(ctx.page, street); await blurAddressEditing(ctx, widgetSelector); }); } /** Kernel 地址填表步骤(双地址分步 commit,禁止 quote_session 跳过) */ export async function fillAddressStep( ctx: RPAContext, req: QuoteRequest, ): Promise { assertRPAContext(ctx, "fillAddressStep"); const widgetSelector = ctx.selectors.resolve("RPA_SELECTOR_QUOTE_WIDGET"); const mockMode = isRpaAddressMockMode(); if (mockMode) { console.log( "[rpa] RPA_ADDRESS_MODE=mock:仅替代 MotherShip commit,cargo/submit/quote 仍真实执行", ); } ctx.state.blindAddressFlow = resolveBlindAddressFlow(req); const rush = isRpaAddressRushMode(); const blind = isBlindAddressFlowActive(ctx); if (blind) { console.log( isPreconfirmedMothershipQuote(req) && !isRpaBlindAddressFlow() ? "[rpa] blind-flow: MotherShip 候选已确认,填表+点选,仅最终验报价" : "[rpa] blind-flow: 地址步填表+点选,仅最终验报价", ); } else if (rush) { console.log("[rpa] visual-rush: 地址步仅填表+点选,跳过锁定/重填闸门"); } await runKernelSubStep("fillAddress", "stabilizeLandingPage", () => stabilizeQuoteLandingPage(ctx), ); await fillAddressWithCommitStep(ctx, "pickup", req.pickup); if (!mockMode && !rush && !blind) { const pickupVaultReady = isDualAddressPatchEnabled() && Boolean(ctx.state.axelPlaceLocations.pickup); if (isDualAddressPatchEnabled()) { await preservePickupInWidget(ctx.page); await releasePickupInputInWidget(ctx.page); await ctx.page.waitForTimeout(600); } if (!pickupVaultReady) { await runKernelSubStep("fillAddress", "assertPickupCommitted", () => assertDualAddressStable(ctx, widgetSelector, req.pickup, null), ); } else { console.log( "[rpa] address-step: preknown pickup 已注入 vault,跳过 assertPickupCommitted", ); } } await fillAddressWithCommitStep( ctx, "delivery", req.delivery, req.pickup, ); const visualReady = !mockMode && (await isAddressesVisuallyCompleteForCargo( ctx.page, widgetSelector, req.pickup, req.delivery, )); if (visualReady) { console.log( "[rpa] fillAddress: 视觉已确认双地址(widget/地图),直接进入货物步", ); } else if (!mockMode && !rush && !blind) { await runKernelSubStep("fillAddress", "ensureDualAddressCommitted", () => ensureDualAddressCommitted( ctx, widgetSelector, req.pickup, req.delivery, async (refillSide, refillAddress) => { await fillAddressWithCommitStep( ctx, refillSide, refillAddress, refillSide === "delivery" ? req.pickup : undefined, ); }, ), ); } await ctx.page.keyboard.press("Escape").catch(() => undefined); if (!rush) { await runKernelSubStep("fillAddress", "settleAddressEditing", () => ctx.state.settleAddressEditing(), ); await runKernelSubStep("fillAddress", "settleAddressFieldsForCargo", () => settleAddressFieldsForCargo(ctx.page, widgetSelector), ); } if (!mockMode && !visualReady && !rush && !blind) { await runKernelSubStep("fillAddress", "assertConfirmedAddressesInWidget", () => assertConfirmedAddressesInWidget( ctx, widgetSelector, req.pickup, req.delivery, ), ); await runKernelSubStep("fillAddress", "assertBothAddressesLocked", () => assertBothAddressesLocked(ctx.page, widgetSelector), ); } }