import { RpaError } from "@/modules/rpa/errors"; import { captureSelectionTrace, drainSelectionTraces, formatSelectionTraceEvent, logSelectionTrace, } from "@/workers/rpa/selection-trace"; import { AxelSelectionBridge, formatPlaceCommitFailure, normalizeAxelPlaceId, } from "@/workers/rpa/axel-selection-bridge"; import { commitPlaceWithStrategy } from "@/workers/rpa/fix-place"; import { clickMothershipStyledSuggestion, reopenStyledSuggestionDropdown, tryStyledKeyboardCommit, waitForStyledSuggestionRows, } from "@/workers/rpa/mothership-styled-suggestion-click"; import { dismissCookieConsent, scrollQuoteWidgetIntoView, } from "@/workers/rpa/page-prep"; import { isRpaVisualRushMode, isDualAddressPatchEnabled } from "@/lib/rpa/env"; import { commitViaAxelPlacePrefetch, fetchAxelPlaceLocation } from "@/workers/rpa/axel-place-prefetch"; import { applyDeliveryDualAddressPatch, applyPickupDualAddressPatch, preservePickupInWidget, releasePickupInputInWidget, } from "@/workers/rpa/inject/dual-address-patch"; import { dismissAddressSuggestionDropdown, isAddressPresentInWidget, isPickupReadyForDeliveryInput, lockPickupIntoChip, readWidgetCombinedText, releasePickupFieldForDelivery, widgetContainsConfirmedAddress, } from "@/workers/rpa/address-commit"; import { buildAddressBindingLabels, clickSuggestionByLabel, ensureAddressSuggestions, ensureSuggestionDropdownOpen, selectAddressWithPlaceBinding, } from "@/workers/rpa/address-suggestions"; import { AddressCommitStateMachine } from "@/workers/rpa/address-adapter/commit-state-machine"; import { createEmptyDiagnostics, buildNetworkDiagnostics, formatDiagnosticsFailure, logAddressSelectDiagnostics, resolvePlaceFailureStatus, } from "@/workers/rpa/address-adapter/diagnostics"; import { isAddressSuggestionDropdownOpen, waitForAddressSuggestionLocked, } from "@/workers/rpa/address-suggestions"; import type { AddressAdapter, AddressCommitResult, AddressSelectDiagnostics, SelectAddressParams, } from "@/workers/rpa/address-adapter/types"; export class MotherShipAddressAdapter implements AddressAdapter { private readonly stateMachine = new AddressCommitStateMachine(); private bridge: AxelSelectionBridge | null = null; private result: AddressCommitResult | null = null; private diagnostics: AddressSelectDiagnostics = createEmptyDiagnostics(""); getCommitState() { return this.stateMachine.getState(); } getCommittedAddress(): AddressCommitResult | null { return this.result; } getDiagnostics(): AddressSelectDiagnostics { return { ...this.diagnostics, commitState: this.stateMachine.getState(), }; } async selectAddress(params: SelectAddressParams): Promise { const { ctx, side, address, street } = params; const page = ctx.page; const label = address.mothershipDisplayLabel?.trim(); if (!label) { this.fail( params, "selection_failed", "须在 MotherShip 多候选地址中由用户确认后再询价", ); } const queryHint = { street: address.street, city: address.city ?? "", state: address.state ?? "", zip: address.zip ?? "", }; this.diagnostics = createEmptyDiagnostics(label!, "EMPTY"); this.diagnostics.selectionTraces = []; this.stateMachine.transition("TYPING"); this.bridge = new AxelSelectionBridge(); this.bridge.attach(page); this.bridge.clearCandidates(); this.diagnostics.events.push("bridge-attached"); try { await dismissCookieConsent(page); await scrollQuoteWidgetIntoView(page); await ensureAddressSuggestions(page, street, queryHint, { mothershipLabel: label!, preferConfirmedLabel: true, maxQueries: params.side === "delivery" && params.pickupAnchor ? 1 : undefined, }); this.diagnostics.events.push("suggestions-typed"); await this.recordSelectionTrace(page, params.side, "after-suggestions-typed"); if (!this.bridge.hasSearchResults()) { this.fail( params, "selection_failed", "axel/location/search 未返回 googlePlacesResults", ); } await this.bridge.waitForSearchResults( params.side === "delivery" && params.pickupAnchor ? 1_500 : 8_000, ); this.stateMachine.transition("SUGGESTIONS_READY"); const preknownPlaceId = params.address.selectedFromMothership ? (params.address.mothershipOptionId ?? params.address.placeId)?.trim() : ""; const preknownLabel = params.address.mothershipDisplayLabel?.trim() || label!; let binding = null; if (preknownPlaceId) { binding = { label: preknownLabel, placeId: preknownPlaceId, description: preknownLabel, }; this.diagnostics.events.push( `binding-preknown:${preknownPlaceId.slice(0, 16)}`, ); } else { for (const probeLabel of buildAddressBindingLabels(queryHint, label!)) { binding = this.bridge.resolveSelection(probeLabel, queryHint); if (binding?.placeId?.trim()) { break; } } } if (!binding?.placeId?.trim()) { this.fail( params, "selection_failed", `search 响应中未找到与确认项绑定的 placeId:${label}`, ); } this.diagnostics.placeId = binding!.placeId; this.diagnostics.address = label!; this.diagnostics.events.push( `binding-resolved:${binding!.placeId.slice(0, 16)}`, ); await this.recordSelectionTrace(page, params.side, "after-binding-resolved"); console.log( `[rpa] axel-bridge:${side} placeId=${binding!.placeId.slice(0, 16)}… desc=${binding!.description.slice(0, 48)}`, ); this.stateMachine.transition("SELECTING"); this.diagnostics.method = "mothership-place-binding"; this.stateMachine.transition("PLACE_LOADING"); const deliveryFast = params.side === "delivery" && Boolean(params.pickupAnchor?.placeId); const rush = isRpaVisualRushMode(); if (preknownPlaceId && !rush) { const syntheticFirst = await this.trySyntheticPreknownCommit( params, binding!, street, "preknown-first", ); if (syntheticFirst) { return syntheticFirst; } const deliveryPickupInfoEarly = params.side === "delivery" && params.pickupAnchor?.placeId ? { placeId: params.pickupAnchor.placeId, description: params.pickupAnchor.mothershipDisplayLabel ?? "", location: params.ctx.state.axelPlaceLocations.pickup ?? this.bridge!.getPlaceDetails(params.pickupAnchor.placeId) ?? {}, } : undefined; const earlyFix = await commitPlaceWithStrategy( page, this.bridge!, binding!.placeId, binding!.description, params.side, deliveryPickupInfoEarly, { input: street, query: queryHint }, ); if (earlyFix.ok) { await this.assertWidgetLockedAfterCommit( params, binding!.placeId, earlyFix.method, ); return this.succeed( params, binding!.placeId, binding!.description, earlyFix.method, ); } console.log( "[rpa] preknown-place: fix-place 未锁定,继续 styled 主路径", ); const syntheticEarly = await this.trySyntheticPreknownCommit( params, binding!, street, "preknown", ); if (syntheticEarly) { return syntheticEarly; } const keyboardPreknown = await tryStyledKeyboardCommit( page, street, this.bridge!, binding!.placeId, this.bridge!.getCandidateIndex(binding!.placeId), ); if (keyboardPreknown) { await this.assertWidgetLockedAfterCommit( params, binding!.placeId, "preknown-keyboard", ); return this.succeed( params, binding!.placeId, binding!.description, "preknown-keyboard", ); } const finalSynthetic = await this.trySyntheticPreknownCommit( params, binding!, street, "preknown-final", ); if (finalSynthetic) { return finalSynthetic; } this.fail( params, "selection_failed", `preknown 地址 commit 失败(已跳过误点路径):${label}`, ); } const styledRowMs = deliveryFast ? 350 : 2_500; const styledRowRetryMs = deliveryFast ? 1_000 : 8_000; const styledRowFinalMs = deliveryFast ? 1_500 : 10_000; let rowsReady = await waitForStyledSuggestionRows( page, [], queryHint, styledRowMs, ); if (!rowsReady) { await ensureSuggestionDropdownOpen( page, street, queryHint, binding!.description, ); rowsReady = await waitForStyledSuggestionRows( page, [], queryHint, styledRowRetryMs, ); } if (!rowsReady && !deliveryFast) { console.log("[rpa] styled-rows-miss: retry reopen before click"); await reopenStyledSuggestionDropdown( page, street, queryHint, binding!.description, ); rowsReady = await waitForStyledSuggestionRows( page, [], queryHint, styledRowFinalMs, ); } if (!rowsReady) { console.log("[rpa] styled-rows-miss: dropdown still empty before click"); } let styledClick: { clicked: boolean; reason?: string; matchedText?: string; method?: string; }; if (preknownPlaceId && !rush) { console.log( "[rpa] preknown-place: 跳过 legacyOnly 鼠标点选,走 keyboard/prefetch", ); styledClick = { clicked: false, reason: "preknown-skip-legacy-click" }; } else { styledClick = await clickMothershipStyledSuggestion( page, binding!.description, queryHint, { input: street, candidateIndex: this.bridge!.getCandidateIndex(binding!.placeId), skipReopen: true, side: params.side, bridge: this.bridge!, placeId: binding!.placeId, legacyOnly: true, fastClick: true, placeCommitTimeoutMs: deliveryFast ? 2_000 : 8_000, }, ); } if (rush && styledClick.clicked) { return this.succeed( params, binding!.placeId, binding!.description, styledClick.method ?? "mothership-styled-click-rush", ); } if (!styledClick.clicked) { console.log( `[rpa] mothership-styled-click: 未点选 reason=${styledClick.reason ?? "unknown"}`, ); if (rush) { const prefetch = await commitViaAxelPlacePrefetch( page, this.bridge!, binding!.placeId, binding!.description, street, { query: queryHint, candidateIndex: this.bridge!.getCandidateIndex(binding!.placeId), }, ); if ( prefetch.ok && (await this.bridge!.waitForPlaceCommit(binding!.placeId, 8_000)) ) { await this.assertWidgetLockedAfterCommit( params, binding!.placeId, "axel-place-prefetch-rush", ); return this.succeed( params, binding!.placeId, binding!.description, "axel-place-prefetch-rush", ); } const labelClicked = await clickSuggestionByLabel( page, binding!.description, queryHint, ); if (labelClicked) { return this.succeed( params, binding!.placeId, binding!.description, "click-suggestion-by-label-rush", ); } this.fail( params, "selection_failed", "visual-rush:地址联想未点中", ); } await dismissCookieConsent(page); await scrollQuoteWidgetIntoView(page, { force: true }); await ensureSuggestionDropdownOpen( page, street, queryHint, binding!.description, ); const keyboardCommitted = await tryStyledKeyboardCommit( page, street, this.bridge!, binding!.placeId, this.bridge!.getCandidateIndex(binding!.placeId), ); if (!keyboardCommitted) { console.log("[rpa] mothership-styled-keyboard:miss"); const prefetch = await commitViaAxelPlacePrefetch( page, this.bridge!, binding!.placeId, binding!.description, street, { query: queryHint, candidateIndex: this.bridge!.getCandidateIndex(binding!.placeId), }, ); if ( prefetch.ok && (await this.bridge!.waitForPlaceCommit(binding!.placeId, 8_000)) ) { await this.assertWidgetLockedAfterCommit( params, binding!.placeId, "axel-place-prefetch", ); return this.succeed( params, binding!.placeId, binding!.description, "axel-place-prefetch", ); } } if (keyboardCommitted) { await this.assertWidgetLockedAfterCommit( params, binding!.placeId, "mothership-styled-keyboard", ); return this.succeed( params, binding!.placeId, binding!.description, "mothership-styled-keyboard", ); } const labelClicked = await clickSuggestionByLabel( page, binding!.description, queryHint, ); if (labelClicked) { const labelCommitted = await this.bridge!.waitForPlaceCommit( binding!.placeId, 8_000, ); if (labelCommitted) { await this.assertWidgetLockedAfterCommit( params, binding!.placeId, "click-suggestion-by-label", ); return this.succeed( params, binding!.placeId, binding!.description, "click-suggestion-by-label", ); } } } if (styledClick.clicked) { const styledCommitted = await this.bridge!.waitForPlaceCommit( binding!.placeId, 10_000, ); if (styledCommitted) { await this.assertWidgetLockedAfterCommit( params, binding!.placeId, "mothership-styled-click", ); return this.succeed( params, binding!.placeId, binding!.description, "mothership-styled-click", ); } console.log( `[rpa] mothership-styled-click: place 请求未观测到,检查视觉状态…`, ); const allowVisualBypass = !params.address.selectedFromMothership; if (allowVisualBypass) { const visuallyConfirmed = await isAddressPresentInWidget( params.ctx.page, params.widgetSelector, params.address, ); if (visuallyConfirmed) { console.log( `[rpa] mothership-styled-click: 视觉已确认「${params.address.mothershipDisplayLabel?.slice(0, 32)}」,跳过网络等待`, ); await this.assertWidgetLockedAfterCommit( params, binding!.placeId, "mothership-styled-click-visual", ); return this.succeed( params, binding!.placeId, binding!.description, "mothership-styled-click-visual", ); } } console.log( `[rpa] mothership-styled-click: 视觉也未确认,降级 fix-place/legacy`, ); } const deliveryPickupInfo = params.side === "delivery" && params.pickupAnchor?.placeId ? { placeId: params.pickupAnchor.placeId, description: params.pickupAnchor.mothershipDisplayLabel ?? "", location: params.ctx.state.axelPlaceLocations.pickup ?? this.bridge!.getPlaceDetails(params.pickupAnchor.placeId) ?? {}, } : undefined; const fixResult = await commitPlaceWithStrategy( page, this.bridge!, binding!.placeId, binding!.description, params.side, deliveryPickupInfo, { input: street, query: queryHint }, ); if (fixResult.ok) { await this.assertWidgetLockedAfterCommit( params, binding!.placeId, fixResult.method, ); return this.succeed( params, binding!.placeId, binding!.description, fixResult.method, ); } console.log( `[rpa] fix-place: ${fixResult.method} 未通过 widget 锁定闸门,继续 legacy`, ); try { await selectAddressWithPlaceBinding( page, this.bridge, binding!, queryHint, street, { side: params.side, widgetSelector: params.widgetSelector, pickupAnchor: params.side === "delivery" ? params.pickupAnchor : undefined, confirmedAddress: params.address, ctx: params.ctx, }, ); } catch (error) { if (error instanceof RpaError) { const diag = this.bridge.getPlaceCommitDiagnostics(binding!.placeId); this.diagnostics.placeRequestObserved = diag.placeNetworkLog.length > 0; const status = error.message.includes("未发起 GET place 请求") ? "no_place_request" : error.message.includes("placeId 不匹配") ? "place_id_mismatch" : error.message.includes("HTTP") ? "place_http_error" : "timeout"; this.fail( params, status, formatDiagnosticsFailure({ ...this.diagnostics, status, commitState: "FAILED", }), ); } throw error; } if (this.bridge.hasPlaceCommit(binding!.placeId)) { await this.assertWidgetLockedAfterCommit(params, binding!.placeId, "legacy"); return this.succeed( params, binding!.placeId, binding!.description, "legacy", ); } const diag = this.bridge.getPlaceCommitDiagnostics(binding!.placeId); this.diagnostics.placeRequestObserved = diag.placeNetworkLog.length > 0; const placeLog = diag.placeNetworkLog; const acceptedIds = this.collectAcceptedPlaceIds(params, binding!.placeId); const { httpOk: httpOkForAccepted } = this.bridgeCommitForAcceptedIds(acceptedIds); const status = resolvePlaceFailureStatus( placeLog.length > 0, placeLog.some((e) => e.status >= 200) && !httpOkForAccepted, placeLog.some((e) => e.status >= 400), true, ); const reason = formatPlaceCommitFailure(binding!.placeId, [], diag); console.log(`[rpa] axel-place-gate: ${reason}`); const syntheticFallback = await this.trySyntheticPreknownCommit( params, binding!, street, "legacy-fallback", ); if (syntheticFallback) { return syntheticFallback; } this.fail( params, status, formatDiagnosticsFailure({ ...this.diagnostics, status, commitState: "FAILED", }), ); } finally { this.bridge?.detach(); this.bridge = null; } return this.result!; } async waitForCommit(): Promise { if (!this.result?.committed) { throw new RpaError( "ADDRESS_NOT_CONFIRMED", formatDiagnosticsFailure(this.getDiagnostics()), { retryable: true }, ); } return this.result; } private async trySyntheticPreknownCommit( params: SelectAddressParams, binding: { placeId: string; description: string }, street: import("playwright").Locator, methodTag: string, ): Promise { if (!params.address.selectedFromMothership) { return null; } const preknown = params.address.mothershipOptionId?.trim() || params.address.placeId?.trim(); if (!preknown) { return null; } const fetched = await fetchAxelPlaceLocation( params.ctx.page, binding.placeId, ); if (!fetched.ok || !fetched.location) { return null; } const normalized = normalizeAxelPlaceId(binding.placeId); this.bridge!.setPlaceDetails(normalized, fetched.location); this.bridge!.recordSyntheticPlaceCommit(normalized, fetched.status); params.ctx.state.axelPlaceLocations[params.side] = fetched.location; if (isDualAddressPatchEnabled()) { if (params.side === "pickup") { await applyPickupDualAddressPatch(params.ctx.page, { placeId: binding.placeId, location: fetched.location, description: binding.description, }); await preservePickupInWidget(params.ctx.page); await releasePickupInputInWidget(params.ctx.page); await lockPickupIntoChip( params.ctx, params.widgetSelector, params.street, ); await params.ctx.page.waitForTimeout(600); await releasePickupFieldForDelivery( params.ctx, params.widgetSelector, params.address, params.street, ); } else if ( params.side === "delivery" && params.pickupAnchor?.placeId && params.ctx.state.axelPlaceLocations.pickup ) { await applyDeliveryDualAddressPatch( params.ctx.page, { placeId: params.pickupAnchor.placeId, location: params.ctx.state.axelPlaceLocations.pickup, description: params.pickupAnchor.mothershipDisplayLabel?.trim() || params.pickupAnchor.formattedAddress?.trim() || params.pickupAnchor.street, }, { placeId: binding.placeId, location: fetched.location, description: binding.description, }, ); } } else { await street.fill(binding.description).catch(() => undefined); await params.ctx.page.keyboard.press("Escape").catch(() => undefined); await params.ctx.page.keyboard.press("Tab").catch(() => undefined); await params.ctx.page.waitForTimeout(400); } console.log( `[rpa] preknown-synthetic: GET place/${normalized.slice(0, 16)} 合成 commit`, ); await this.assertWidgetLockedAfterCommit( params, binding.placeId, `${methodTag}-synthetic`, ); return this.succeed( params, binding.placeId, binding.description, `${methodTag}-synthetic`, ); } private collectAcceptedPlaceIds( params: SelectAddressParams, bindingPlaceId: string, ): string[] { const ids = new Set(); const add = (raw?: string) => { const normalized = normalizeAxelPlaceId(raw?.trim() ?? ""); if (normalized) { ids.add(normalized); } }; add(bindingPlaceId); add(params.address.mothershipOptionId); add(params.address.placeId); for (const candidate of this.bridge?.getCandidates() ?? []) { add(candidate.placeId); } return [...ids]; } private bridgeCommitForAcceptedIds(acceptedIds: string[]): { observed: boolean; httpOk: boolean; } { const bridge = this.bridge; if (!bridge || acceptedIds.length === 0) { return { observed: false, httpOk: false }; } return { observed: acceptedIds.some((id) => bridge.hasPlaceCommit(id)), httpOk: acceptedIds.some((id) => bridge.hasSuccessfulPlaceNetworkRequest(id), ), }; } private async assertWidgetLockedAfterCommit( params: SelectAddressParams, placeId: string, fixMethod: string, ): Promise { const bridge = this.bridge; const acceptedIds = this.collectAcceptedPlaceIds(params, placeId); const networkDiag = bridge ? buildNetworkDiagnostics( bridge, placeId, params.side === "delivery" ? params.pickupAnchor?.placeId : undefined, ) : null; const commitGate = this.bridgeCommitForAcceptedIds(acceptedIds); this.diagnostics.placeNetworkEvents = networkDiag?.placeNetworkEvents; this.diagnostics.pickupLostAfterDelivery = networkDiag?.pickupLostAfterDelivery; this.diagnostics.placeRequestObserved = commitGate.observed; const observedPlaceIds = networkDiag?.observedPlaceIds ?? []; const placeRequestObserved = commitGate.observed; const placeRequestHttpOk = commitGate.httpOk; console.log("[rpa] place-commit-diagnostic", { side: params.side, placeId, commitState: this.stateMachine.getState(), placeRequestObserved, placeRequestHttpOk, observedPlaceIds: observedPlaceIds.map((id) => id.slice(0, 24)), fixMethod, pickupLostAfterDelivery: networkDiag?.pickupLostAfterDelivery ?? false, }); // 快速通道:visual / synthetic(dual-patch vault)放宽网络 place 闸门 const isVisualConfirmed = fixMethod.includes("-visual"); const isSyntheticCommit = fixMethod.includes("-synthetic"); const relaxedGate = isVisualConfirmed || isSyntheticCommit; if (!placeRequestObserved && !relaxedGate) { this.fail( params, "no_place_request", formatDiagnosticsFailure({ ...this.diagnostics, status: "no_place_request", commitState: "FAILED", }), ); } if (!placeRequestHttpOk && !relaxedGate) { this.fail( params, "place_http_error", formatDiagnosticsFailure({ ...this.diagnostics, status: "place_http_error", commitState: "FAILED", }), ); } if (relaxedGate) { console.log( `[rpa] place-commit: ${isSyntheticCommit ? "synthetic" : "视觉"}确认模式,跳过网络请求检查 (observed=${placeRequestObserved}, ok=${placeRequestHttpOk})`, ); } let dropdownClosed = await waitForAddressSuggestionLocked( params.ctx.page, 2_000, ); let dropdownOpen = await isAddressSuggestionDropdownOpen(params.ctx.page); if (!dropdownClosed || dropdownOpen) { await dismissAddressSuggestionDropdown(params.ctx.page, params.street).catch( () => undefined, ); await params.ctx.page.keyboard.press("Escape").catch(() => undefined); await params.ctx.page.waitForTimeout(400); dropdownClosed = await waitForAddressSuggestionLocked(params.ctx.page, 2_000); dropdownOpen = await isAddressSuggestionDropdownOpen(params.ctx.page); } if ((!dropdownClosed || dropdownOpen) && !placeRequestHttpOk && !relaxedGate) { this.fail( params, "selection_failed", formatDiagnosticsFailure({ ...this.diagnostics, status: "selection_failed", commitState: "FAILED", method: fixMethod, }) + ";联想下拉未收起", ); } else if (dropdownOpen) { console.log( "[rpa] place-commit: place 200 已观测,忽略 styled 下拉误报", ); } if (params.side === "delivery" && params.pickupAnchor) { const pickupStillInWidget = await isAddressPresentInWidget( params.ctx.page, params.widgetSelector, params.pickupAnchor, ); this.diagnostics.pickupLostAfterDelivery = !pickupStillInWidget; if (!pickupStillInWidget) { this.fail( params, "selection_failed", formatDiagnosticsFailure({ ...this.diagnostics, status: "selection_failed", commitState: "FAILED", pickupLostAfterDelivery: true, }) + ";填派送后提货地址从 widget 消失", ); } } else if (networkDiag?.pickupLostAfterDelivery) { this.diagnostics.pickupLostAfterDelivery = false; } if (params.side === "pickup") { let locked = await isPickupReadyForDeliveryInput( params.ctx.page, params.widgetSelector, params.address, ); if (!locked && placeRequestHttpOk) { await dismissAddressSuggestionDropdown(params.ctx.page).catch( () => undefined, ); await lockPickupIntoChip( params.ctx, params.widgetSelector, params.street, ); await params.ctx.page.waitForTimeout(600); locked = await isPickupReadyForDeliveryInput( params.ctx.page, params.widgetSelector, params.address, ); } if (!locked && placeRequestHttpOk) { locked = await releasePickupFieldForDelivery( params.ctx, params.widgetSelector, params.address, params.street, ); } if (!locked && placeRequestHttpOk) { const widgetText = await readWidgetCombinedText( params.ctx.page, params.widgetSelector, ); if (widgetContainsConfirmedAddress(widgetText, params.address)) { console.log( "[rpa] place-commit: place 200 且 widget 已含提货地址,放行 chip 闸门", ); return; } } if (!locked && placeRequestHttpOk) { console.log( "[rpa] place-commit: place 200 已观测,放行 pickup(后续 settle 打开派送侧)", ); return; } if (!locked) { this.fail( params, "selection_failed", formatDiagnosticsFailure({ ...this.diagnostics, status: "selection_failed", commitState: "FAILED", method: fixMethod, }) + ";widget 未锁定(单框仍含提货全文,无独立派送框)", ); } return; } if ( !(await isAddressPresentInWidget( params.ctx.page, params.widgetSelector, params.address, )) ) { if (placeRequestHttpOk) { await dismissAddressSuggestionDropdown(params.ctx.page).catch( () => undefined, ); await params.ctx.page.keyboard.press("Escape").catch(() => undefined); await params.ctx.page.keyboard.press("Tab").catch(() => undefined); const deadline = Date.now() + 4_000; while (Date.now() < deadline) { if ( await isAddressPresentInWidget( params.ctx.page, params.widgetSelector, params.address, ) ) { return; } await params.ctx.page.waitForTimeout(200); } if (placeRequestHttpOk) { console.log( "[rpa] place-commit: delivery place 200 已观测,widget 文案未同步仍接受网络 commit", ); return; } } this.fail( params, "selection_failed", formatDiagnosticsFailure({ ...this.diagnostics, status: "selection_failed", commitState: "FAILED", }) + ";派送地址未写入 widget", ); } } private succeed( params: SelectAddressParams, placeId: string, formattedAddress: string, method?: string, ): AddressCommitResult { this.mergeBufferedSelectionTraces(); this.stateMachine.transition("COMMITTED"); this.diagnostics.status = "ok"; this.diagnostics.method = method ?? this.diagnostics.method; this.diagnostics.placeRequestObserved = this.bridge?.hasSuccessfulPlaceNetworkRequest(placeId) ?? false; this.diagnostics.commitState = "COMMITTED"; const details = this.bridge?.getPlaceDetails(placeId); params.ctx.state.axelPlaceLocations[params.side] = details ?? { placeId, formattedAddress, }; this.result = { success: true, placeId, formattedAddress, committed: true, }; logAddressSelectDiagnostics(params.side, this.getDiagnostics()); return this.result; } private async recordSelectionTrace( page: import("playwright").Page, side: "pickup" | "delivery", label: string, ): Promise { const snapshot = await captureSelectionTrace(page, label); this.diagnostics.selectionTraces = [ ...(this.diagnostics.selectionTraces ?? []), snapshot, ]; this.diagnostics.events.push(formatSelectionTraceEvent(snapshot)); logSelectionTrace(side, snapshot); } private mergeBufferedSelectionTraces(): void { const drained = drainSelectionTraces(); if (!drained.length) { return; } this.diagnostics.selectionTraces = [ ...(this.diagnostics.selectionTraces ?? []), ...drained, ]; for (const snapshot of drained) { this.diagnostics.events.push(formatSelectionTraceEvent(snapshot)); } } private fail( params: SelectAddressParams, status: AddressSelectDiagnostics["status"], detail: string, ): never { this.mergeBufferedSelectionTraces(); this.stateMachine.transition("FAILED"); this.diagnostics.status = status; this.diagnostics.commitState = "FAILED"; this.diagnostics.events.push(`failed:${status}`); logAddressSelectDiagnostics(params.side, this.getDiagnostics()); throw new RpaError("ADDRESS_NOT_CONFIRMED", detail, { retryable: status !== "selection_failed", }); } }