|
|
import { RpaError } from "@/modules/rpa/errors";
|
|
|
import { pageLocator } from "@/lib/rpa/locator";
|
|
|
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 { isRpaAddressRushMode, isRpaFastQuoteMode, isBlindAddressFlowActive, isDualAddressPatchEnabled, resolveAxelSearchWaitMs } 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 {
|
|
|
blindClickAddressSuggestion,
|
|
|
isAddressCommittedVisually,
|
|
|
mouseClickSuggestionUntilVisualConfirmed,
|
|
|
} from "@/workers/rpa/address-suggestion-mouse";
|
|
|
import {
|
|
|
buildAddressBindingLabels,
|
|
|
clickSuggestionByLabel,
|
|
|
ensureAddressSuggestions,
|
|
|
ensureSuggestionDropdownOpen,
|
|
|
resolveAddressAutocompleteInput,
|
|
|
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<AddressCommitResult> {
|
|
|
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 = resolveAddressAutocompleteInput(
|
|
|
{
|
|
|
street: address.street,
|
|
|
city: address.city ?? "",
|
|
|
state: address.state ?? "",
|
|
|
zip: address.zip ?? "",
|
|
|
},
|
|
|
{ mothershipLabel: label! },
|
|
|
);
|
|
|
|
|
|
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);
|
|
|
|
|
|
const isPreknown = Boolean(
|
|
|
params.address.selectedFromMothership &&
|
|
|
(params.address.mothershipOptionId ?? params.address.placeId)?.trim(),
|
|
|
);
|
|
|
|
|
|
await ensureAddressSuggestions(page, street, queryHint, {
|
|
|
mothershipLabel: label!,
|
|
|
preferConfirmedLabel: true,
|
|
|
maxQueries:
|
|
|
isPreknown || isRpaFastQuoteMode()
|
|
|
? 1
|
|
|
: 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(
|
|
|
resolveAxelSearchWaitMs(params.side, isPreknown),
|
|
|
);
|
|
|
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";
|
|
|
|
|
|
if (isBlindAddressFlowActive(params.ctx)) {
|
|
|
await blindClickAddressSuggestion(
|
|
|
page,
|
|
|
street,
|
|
|
params.address,
|
|
|
`${params.side}-blind`,
|
|
|
);
|
|
|
if (preknownPlaceId) {
|
|
|
const fetched = await fetchAxelPlaceLocation(page, binding!.placeId);
|
|
|
if (fetched.ok && fetched.location) {
|
|
|
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 (params.side === "pickup") {
|
|
|
await pageLocator(page, params.widgetSelector)
|
|
|
.getByText(/^Deliver to$/i)
|
|
|
.first()
|
|
|
.click({ force: true, timeout: 2_000 })
|
|
|
.catch(() => undefined);
|
|
|
await page.waitForTimeout(200);
|
|
|
}
|
|
|
return this.succeed(
|
|
|
params,
|
|
|
binding!.placeId,
|
|
|
binding!.description,
|
|
|
"blind-flow",
|
|
|
);
|
|
|
}
|
|
|
|
|
|
const rush = isRpaAddressRushMode();
|
|
|
|
|
|
// preknown + rush:GET place 预热 + recording 点选(跳过 8 轮 query / styled)
|
|
|
if (preknownPlaceId && rush) {
|
|
|
const fetched = await fetchAxelPlaceLocation(page, binding!.placeId);
|
|
|
if (fetched.ok && fetched.location) {
|
|
|
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;
|
|
|
this.diagnostics.events.push(
|
|
|
`preknown-rush-primed:${normalized.slice(0, 16)}`,
|
|
|
);
|
|
|
}
|
|
|
|
|
|
const rushMouseOk = await mouseClickSuggestionUntilVisualConfirmed(
|
|
|
page,
|
|
|
street,
|
|
|
params.widgetSelector,
|
|
|
params.address,
|
|
|
`${params.side}-rush-recording`,
|
|
|
{ reopenSuggestions: true },
|
|
|
);
|
|
|
if (rushMouseOk) {
|
|
|
await this.assertWidgetLockedAfterCommit(
|
|
|
params,
|
|
|
binding!.placeId,
|
|
|
"preknown-rush-recording-visual",
|
|
|
);
|
|
|
return this.succeed(
|
|
|
params,
|
|
|
binding!.placeId,
|
|
|
binding!.description,
|
|
|
"preknown-rush-recording",
|
|
|
);
|
|
|
}
|
|
|
|
|
|
const prefetch = await commitViaAxelPlacePrefetch(
|
|
|
page,
|
|
|
this.bridge!,
|
|
|
binding!.placeId,
|
|
|
binding!.description,
|
|
|
street,
|
|
|
{
|
|
|
query: queryHint,
|
|
|
candidateIndex: this.bridge!.getCandidateIndex(binding!.placeId),
|
|
|
},
|
|
|
);
|
|
|
const commitWaitMs = isRpaFastQuoteMode() ? 1_500 : 4_000;
|
|
|
if (prefetch.ok) {
|
|
|
const committed = await this.bridge!.waitForPlaceCommit(
|
|
|
binding!.placeId,
|
|
|
commitWaitMs,
|
|
|
);
|
|
|
if (!committed && prefetch.location) {
|
|
|
const normalized = normalizeAxelPlaceId(binding!.placeId);
|
|
|
this.bridge!.recordSyntheticPlaceCommit(normalized, 200);
|
|
|
params.ctx.state.axelPlaceLocations[params.side] = prefetch.location;
|
|
|
}
|
|
|
if (committed || prefetch.location) {
|
|
|
const visualAfterPrefetch = await isAddressCommittedVisually(
|
|
|
page,
|
|
|
params.widgetSelector,
|
|
|
params.address,
|
|
|
street,
|
|
|
);
|
|
|
if (!visualAfterPrefetch) {
|
|
|
console.log(
|
|
|
`[rpa] ${params.side}: prefetch 无 chip 视觉确认,继续点选 fallback`,
|
|
|
);
|
|
|
} else {
|
|
|
await this.assertWidgetLockedAfterCommit(
|
|
|
params,
|
|
|
binding!.placeId,
|
|
|
"axel-place-prefetch-rush-early",
|
|
|
);
|
|
|
return this.succeed(
|
|
|
params,
|
|
|
binding!.placeId,
|
|
|
binding!.description,
|
|
|
"axel-place-prefetch-rush-early",
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const mouseVisualOk = await mouseClickSuggestionUntilVisualConfirmed(
|
|
|
page,
|
|
|
street,
|
|
|
params.widgetSelector,
|
|
|
params.address,
|
|
|
`${params.side}-mouse-suggestion`,
|
|
|
);
|
|
|
if (mouseVisualOk) {
|
|
|
await this.assertWidgetLockedAfterCommit(
|
|
|
params,
|
|
|
binding!.placeId,
|
|
|
"mouse-suggestion-visual",
|
|
|
);
|
|
|
return this.succeed(
|
|
|
params,
|
|
|
binding!.placeId,
|
|
|
binding!.description,
|
|
|
"mouse-suggestion-visual",
|
|
|
);
|
|
|
}
|
|
|
|
|
|
this.stateMachine.transition("PLACE_LOADING");
|
|
|
|
|
|
const deliveryFast =
|
|
|
params.side === "delivery" && Boolean(params.pickupAnchor?.placeId);
|
|
|
|
|
|
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 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");
|
|
|
|
|
|
// 「随便选一个」降级:ArrowDown+Enter 后视觉确认
|
|
|
const anySelected = await selectFirstSuggestionVisual(
|
|
|
page,
|
|
|
street,
|
|
|
params.widgetSelector,
|
|
|
params.address,
|
|
|
);
|
|
|
if (anySelected) {
|
|
|
console.log("[rpa] any-suggestion-visual: 联想已点选并视觉确认,跳过 placeId 等待");
|
|
|
return this.succeed(
|
|
|
params,
|
|
|
binding!.placeId,
|
|
|
binding!.description,
|
|
|
"any-suggestion-visual",
|
|
|
);
|
|
|
}
|
|
|
|
|
|
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 请求未观测到,检查视觉状态…`,
|
|
|
);
|
|
|
|
|
|
// 视觉确认:无论是否 preknown,只要 widget 中已出现地址文本即可放行
|
|
|
const allowVisualBypass = true;
|
|
|
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<AddressCommitResult> {
|
|
|
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<AddressCommitResult | null> {
|
|
|
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,
|
|
|
params.address,
|
|
|
);
|
|
|
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<string>();
|
|
|
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<void> {
|
|
|
if (isBlindAddressFlowActive(params.ctx)) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const rush = isRpaAddressRushMode();
|
|
|
if (rush && fixMethod.includes("-visual")) {
|
|
|
console.log(
|
|
|
`[rpa] place-commit: rush chip 视觉已确认,跳过网络/下拉闸门 (${fixMethod})`,
|
|
|
);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
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) {
|
|
|
await dismissAddressSuggestionDropdown(params.ctx.page).catch(
|
|
|
() => undefined,
|
|
|
);
|
|
|
await lockPickupIntoChip(
|
|
|
params.ctx,
|
|
|
params.widgetSelector,
|
|
|
params.street,
|
|
|
params.address,
|
|
|
);
|
|
|
await params.ctx.page.waitForTimeout(600);
|
|
|
locked = await isPickupReadyForDeliveryInput(
|
|
|
params.ctx.page,
|
|
|
params.widgetSelector,
|
|
|
params.address,
|
|
|
);
|
|
|
}
|
|
|
if (!locked) {
|
|
|
locked = await releasePickupFieldForDelivery(
|
|
|
params.ctx,
|
|
|
params.widgetSelector,
|
|
|
params.address,
|
|
|
params.street,
|
|
|
);
|
|
|
}
|
|
|
if (!locked) {
|
|
|
this.fail(
|
|
|
params,
|
|
|
"selection_failed",
|
|
|
formatDiagnosticsFailure({
|
|
|
...this.diagnostics,
|
|
|
status: "selection_failed",
|
|
|
commitState: "FAILED",
|
|
|
method: fixMethod,
|
|
|
}) +
|
|
|
";chip 视觉未确认(须门牌街名行 + City, ST 行,且 Deliver to 可见)",
|
|
|
);
|
|
|
}
|
|
|
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<void> {
|
|
|
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",
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 「随便选一个」降级:先尝试按文字点击,再 ArrowDown→Enter 后视觉确认
|
|
|
* 不依赖 placeId API 回调,只做视觉确认
|
|
|
*/
|
|
|
async function selectFirstSuggestionVisual(
|
|
|
page: import("playwright").Page,
|
|
|
input: import("playwright").Locator,
|
|
|
widgetSelector: string,
|
|
|
address: Parameters<typeof isAddressPresentInWidget>[2],
|
|
|
): Promise<boolean> {
|
|
|
return mouseClickSuggestionUntilVisualConfirmed(
|
|
|
page,
|
|
|
input,
|
|
|
widgetSelector,
|
|
|
address,
|
|
|
"any-suggestion-visual",
|
|
|
);
|
|
|
}
|