You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

472 lines
14 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import type { QuoteRequest } from "@/modules/providers/quote-provider";
import { pageLocator } from "@/lib/rpa/locator";
import { isRpaAddressMockMode } from "@/lib/rpa/address-mode";
import { isRpaVisualRushMode } from "@/lib/rpa/env";
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";
import { isDualAddressPatchEnabled } from "@/lib/rpa/env";
export { widgetContainsConfirmedAddress } from "@/workers/rpa/address-commit";
type AddressFields = QuoteRequest["pickup"];
async function assertConfirmedAddressesInWidget(
ctx: RPAContext,
widgetSelector: string,
pickup: AddressFields,
delivery: AddressFields,
): Promise<void> {
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<void> {
const widgetSelector = ctx.selectors.resolve("RPA_SELECTOR_QUOTE_WIDGET");
const mockMode = isRpaAddressMockMode();
if (side === "delivery") {
if (!pickupAnchor) {
throw new RpaError(
"ADDRESS_NOT_CONFIRMED",
"填写派送地址前缺少提货锚点地址",
{ retryable: false },
);
}
const pickupVaultReady =
isDualAddressPatchEnabled() &&
Boolean(ctx.state.axelPlaceLocations.pickup);
if (!isRpaVisualRushMode()) {
if (!pickupVaultReady) {
await runKernelSubStep("fillAddress", "settlePickupBeforeDelivery", () =>
settlePickupBeforeDelivery(ctx, widgetSelector, pickupAnchor),
);
await runKernelSubStep(
"fillAddress",
"assertPickupStableBeforeDelivery",
() =>
assertDualAddressStable(ctx, widgetSelector, pickupAnchor, null),
);
} else {
console.log(
"[rpa] address-step: vault pickup 已注入,跳过 settlePickupBeforeDelivery",
);
await preservePickupInWidget(ctx.page);
await releasePickupInputInWidget(ctx.page);
await ctx.page.waitForTimeout(400);
}
} else {
console.log("[rpa] visual-rush: 跳过 settlePickupBeforeDelivery");
}
}
ctx.state.markAddressSide(side);
const street = await runKernelSubStep(
"fillAddress",
`${side}/openAddressSide`,
() => openAddressSide(ctx, side),
);
if (side === "delivery" && pickupAnchor && !mockMode && !isRpaVisualRushMode()) {
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();
const rush = isRpaVisualRushMode();
let commit;
try {
await adapter.selectAddress({
ctx,
side,
address,
street,
widgetSelector,
pickupAnchor: side === "delivery" ? pickupAnchor : undefined,
});
commit = await adapter.waitForCommit();
} catch (error) {
if (
rush ||
(side === "delivery" &&
!mockMode &&
pickupAnchor &&
(await isAddressesVisuallyCompleteForCargo(
ctx.page,
widgetSelector,
pickupAnchor,
address,
)))
) {
if (rush) {
console.log(
`[rpa] visual-rush: ${side} adapter 异常已忽略,继续下一步`,
);
} 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 (!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);
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<void> {
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 commitcargo/submit/quote 仍真实执行",
);
}
const rush = isRpaVisualRushMode();
if (rush) {
console.log("[rpa] visual-rush: 地址步仅填表+点选,跳过锁定/重填闸门");
}
await runKernelSubStep("fillAddress", "stabilizeLandingPage", () =>
stabilizeQuoteLandingPage(ctx),
);
await fillAddressWithCommitStep(ctx, "pickup", req.pickup);
if (!mockMode && !rush) {
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) {
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) {
await runKernelSubStep("fillAddress", "assertConfirmedAddressesInWidget", () =>
assertConfirmedAddressesInWidget(
ctx,
widgetSelector,
req.pickup,
req.delivery,
),
);
await runKernelSubStep("fillAddress", "assertBothAddressesLocked", () =>
assertBothAddressesLocked(ctx.page, widgetSelector),
);
}
}