|
|
import type { Page } from "playwright";
|
|
|
import { pageLocator } from "@/lib/rpa/locator";
|
|
|
import { RpaError } from "@/modules/rpa/errors";
|
|
|
import { isCargoEditorOpen, isCargoFieldsVisible } from "@/workers/rpa/cargo-editor";
|
|
|
import {
|
|
|
type ExpectedCargoWeight,
|
|
|
probeWeightMatchesExpected,
|
|
|
} from "@/workers/rpa/cargo-weight";
|
|
|
import { isAddressesVisuallyCompleteForCargo } from "@/workers/rpa/address-commit";
|
|
|
import { dismissCookieConsent } from "@/workers/rpa/page-prep";
|
|
|
|
|
|
const POLL_MS = 200;
|
|
|
const COMMIT_WAIT_MS = 12_000;
|
|
|
const ADDRESS_SETTLE_MS = 8_000;
|
|
|
|
|
|
const CARGO_PLACEHOLDER_RE =
|
|
|
/pallet|weight|length|width|height|托盘|重量|长度|宽度|高度/i;
|
|
|
|
|
|
export type CargoCommitProbe = {
|
|
|
hasSummaryChip: boolean;
|
|
|
visibleEditableCargoInputs: number;
|
|
|
editorOpen: boolean;
|
|
|
internalPalletCount: number | null;
|
|
|
internalWeight: number | null;
|
|
|
widgetTextPreview: string;
|
|
|
};
|
|
|
|
|
|
/** 货物 committed 校验期望(单托 weightLb + palletCount) */
|
|
|
export type ExpectedCargoCommit = {
|
|
|
palletCount?: number;
|
|
|
weight?: ExpectedCargoWeight;
|
|
|
};
|
|
|
|
|
|
const INVALID_WEIGHT_SUMMARY_RE = /-\s*(?:Lbs|kg|公斤)\b/i;
|
|
|
|
|
|
function parseWeightFromSummary(text: string): number | null {
|
|
|
const patterns = [
|
|
|
/x\s*(\d{2,5})\s*Lbs/i,
|
|
|
/(\d{2,5})Lbs/i,
|
|
|
/\b(\d{2,5})\s*(?:lbs?|kg|公斤)\b/i,
|
|
|
];
|
|
|
for (const re of patterns) {
|
|
|
const m = text.match(re);
|
|
|
if (m) {
|
|
|
const n = Number(m[1]);
|
|
|
if (Number.isFinite(n) && n > 0) {
|
|
|
return n;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
export function hasValidWeightInSummary(text: string): boolean {
|
|
|
if (INVALID_WEIGHT_SUMMARY_RE.test(text)) {
|
|
|
return false;
|
|
|
}
|
|
|
return parseWeightFromSummary(text) !== null;
|
|
|
}
|
|
|
|
|
|
function cargoValuesMatchExpected(
|
|
|
probe: CargoCommitProbe,
|
|
|
expected?: ExpectedCargoCommit,
|
|
|
): boolean {
|
|
|
if (!expected) {
|
|
|
return true;
|
|
|
}
|
|
|
if (
|
|
|
expected.palletCount !== undefined &&
|
|
|
probe.internalPalletCount !== null &&
|
|
|
probe.internalPalletCount !== expected.palletCount
|
|
|
) {
|
|
|
return false;
|
|
|
}
|
|
|
if (expected.weight && !probeWeightMatchesExpected(probe, expected.weight)) {
|
|
|
return false;
|
|
|
}
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
/** 纯函数:是否已达 committed(非仅 UI 可见) */
|
|
|
export function evaluateCargoCommitted(
|
|
|
probe: CargoCommitProbe,
|
|
|
expected?: ExpectedCargoCommit | number,
|
|
|
): boolean {
|
|
|
const normalized: ExpectedCargoCommit | undefined =
|
|
|
typeof expected === "number" ? { palletCount: expected } : expected;
|
|
|
|
|
|
const summaryComplete =
|
|
|
probe.hasSummaryChip &&
|
|
|
!INVALID_WEIGHT_SUMMARY_RE.test(probe.widgetTextPreview) &&
|
|
|
hasValidWeightInSummary(probe.widgetTextPreview) &&
|
|
|
(/\d+W,\s*x\s*\d+L/i.test(probe.widgetTextPreview) ||
|
|
|
(probe.internalPalletCount !== null && probe.internalPalletCount > 0));
|
|
|
|
|
|
if (summaryComplete) {
|
|
|
if (
|
|
|
normalized?.palletCount !== undefined &&
|
|
|
probe.internalPalletCount !== null &&
|
|
|
probe.internalPalletCount !== normalized.palletCount
|
|
|
) {
|
|
|
return false;
|
|
|
}
|
|
|
if (normalized?.weight && !probeWeightMatchesExpected(probe, normalized.weight)) {
|
|
|
return false;
|
|
|
}
|
|
|
if (normalized?.palletCount !== undefined && probe.internalPalletCount !== null) {
|
|
|
return probe.internalPalletCount === normalized.palletCount;
|
|
|
}
|
|
|
return cargoValuesMatchExpected(probe, normalized);
|
|
|
}
|
|
|
|
|
|
if (probe.visibleEditableCargoInputs > 0) {
|
|
|
return false;
|
|
|
}
|
|
|
if (probe.editorOpen && probe.visibleEditableCargoInputs > 0) {
|
|
|
return false;
|
|
|
}
|
|
|
if (!probe.hasSummaryChip) {
|
|
|
return false;
|
|
|
}
|
|
|
if (INVALID_WEIGHT_SUMMARY_RE.test(probe.widgetTextPreview)) {
|
|
|
return false;
|
|
|
}
|
|
|
const palletOk =
|
|
|
probe.internalPalletCount !== null && probe.internalPalletCount > 0;
|
|
|
const weightOk =
|
|
|
probe.internalWeight !== null && probe.internalWeight > 0;
|
|
|
const weightSummaryOk = hasValidWeightInSummary(probe.widgetTextPreview);
|
|
|
const dimOk = /\d+W,\s*x\s*\d+L/i.test(probe.widgetTextPreview);
|
|
|
if (!palletOk && !dimOk) {
|
|
|
return false;
|
|
|
}
|
|
|
if (!weightOk && !weightSummaryOk) {
|
|
|
return false;
|
|
|
}
|
|
|
if (normalized?.palletCount !== undefined && palletOk) {
|
|
|
if (probe.internalPalletCount !== normalized.palletCount) {
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
if (normalized?.weight && !probeWeightMatchesExpected(probe, normalized.weight)) {
|
|
|
return false;
|
|
|
}
|
|
|
return cargoValuesMatchExpected(probe, normalized);
|
|
|
}
|
|
|
|
|
|
export async function probeCargoCommitState(
|
|
|
page: Page,
|
|
|
widgetSelector: string,
|
|
|
): Promise<CargoCommitProbe> {
|
|
|
return page.evaluate(
|
|
|
({ sel, cargoPhReSource }) => {
|
|
|
const widget = document.querySelector(sel);
|
|
|
if (!widget) {
|
|
|
return {
|
|
|
hasSummaryChip: false,
|
|
|
visibleEditableCargoInputs: 0,
|
|
|
editorOpen: false,
|
|
|
internalPalletCount: null,
|
|
|
internalWeight: null,
|
|
|
widgetTextPreview: "",
|
|
|
};
|
|
|
}
|
|
|
|
|
|
const cargoPhRe = new RegExp(cargoPhReSource, "i");
|
|
|
const innerText = (
|
|
|
(widget as HTMLElement).innerText ??
|
|
|
widget.textContent ??
|
|
|
""
|
|
|
)
|
|
|
.replace(/\s+/g, " ")
|
|
|
.trim();
|
|
|
|
|
|
const hasSummaryChip =
|
|
|
/\d+\s*托盘s?/i.test(innerText) ||
|
|
|
/\d+\s*pallets?/i.test(innerText) ||
|
|
|
/\d+W,\s*x\s*\d+L/i.test(innerText) ||
|
|
|
/\b\d{2,5}\s*(lbs?|kg|公斤)\b/i.test(innerText);
|
|
|
|
|
|
let visibleEditableCargoInputs = 0;
|
|
|
let internalPalletCount: number | null = null;
|
|
|
let internalWeight: number | null = null;
|
|
|
|
|
|
const inputs = widget.querySelectorAll<HTMLInputElement>("input");
|
|
|
for (const inp of inputs) {
|
|
|
const ph = inp.placeholder ?? "";
|
|
|
if (!cargoPhRe.test(ph)) {
|
|
|
continue;
|
|
|
}
|
|
|
const val = inp.value?.trim() ?? "";
|
|
|
if (/托盘|pallet/i.test(ph) && val) {
|
|
|
const n = Number(val);
|
|
|
if (Number.isFinite(n) && n > 0) {
|
|
|
internalPalletCount = n;
|
|
|
}
|
|
|
}
|
|
|
if (/weight|重量|公斤|lbs|kg/i.test(ph) && val) {
|
|
|
const n = Number(val);
|
|
|
if (Number.isFinite(n) && n > 0) {
|
|
|
internalWeight = n;
|
|
|
}
|
|
|
}
|
|
|
const visible = inp.offsetParent !== null;
|
|
|
const editable =
|
|
|
visible &&
|
|
|
!inp.disabled &&
|
|
|
inp.readOnly === false &&
|
|
|
inp.getAttribute("aria-readonly") !== "true";
|
|
|
if (editable) {
|
|
|
visibleEditableCargoInputs += 1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const editorOpen = Boolean(
|
|
|
document.querySelector(
|
|
|
'[role="dialog"][aria-modal="true"], [aria-modal="true"][data-state="open"]',
|
|
|
),
|
|
|
);
|
|
|
|
|
|
return {
|
|
|
hasSummaryChip,
|
|
|
visibleEditableCargoInputs,
|
|
|
editorOpen,
|
|
|
internalPalletCount:
|
|
|
internalPalletCount ??
|
|
|
(() => {
|
|
|
const m = innerText.match(/(\d+)\s*(?:pallet|托盘)s?/i);
|
|
|
return m ? Number(m[1]) : null;
|
|
|
})(),
|
|
|
internalWeight:
|
|
|
internalWeight ??
|
|
|
(() => {
|
|
|
const patterns = [
|
|
|
/x\s*(\d{2,5})\s*Lbs/i,
|
|
|
/(\d{2,5})Lbs/i,
|
|
|
/\b(\d{2,5})\s*(?:lbs?|kg|公斤)\b/i,
|
|
|
];
|
|
|
for (const re of patterns) {
|
|
|
const m = innerText.match(re);
|
|
|
if (m) {
|
|
|
const n = Number(m[1]);
|
|
|
if (Number.isFinite(n) && n > 0) {
|
|
|
return n;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return null;
|
|
|
})(),
|
|
|
widgetTextPreview: innerText.slice(0, 220),
|
|
|
};
|
|
|
},
|
|
|
{ sel: widgetSelector, cargoPhReSource: CARGO_PLACEHOLDER_RE.source },
|
|
|
);
|
|
|
}
|
|
|
|
|
|
async function landingPopoverHasCargo(page: Page): Promise<boolean> {
|
|
|
return page.evaluate(() => {
|
|
|
const pallet = document.querySelector<HTMLInputElement>(
|
|
|
'input[placeholder*="How many pallets" i]',
|
|
|
);
|
|
|
const weight = document.querySelector<HTMLInputElement>(
|
|
|
'input[placeholder*="Weight" i]',
|
|
|
);
|
|
|
return Boolean(pallet?.value?.trim() && weight?.value?.trim());
|
|
|
});
|
|
|
}
|
|
|
|
|
|
export async function isCargoCommittedState(
|
|
|
page: Page,
|
|
|
widgetSelector: string,
|
|
|
expected?: ExpectedCargoCommit | number,
|
|
|
): Promise<boolean> {
|
|
|
const normalized: ExpectedCargoCommit | undefined =
|
|
|
typeof expected === "number" ? { palletCount: expected } : expected;
|
|
|
if (normalized && (await landingPopoverMatchesExpected(page, normalized))) {
|
|
|
return true;
|
|
|
}
|
|
|
if (!normalized && (await landingPopoverHasCargo(page))) {
|
|
|
return true;
|
|
|
}
|
|
|
const editorOpen = await isCargoEditorOpen(page);
|
|
|
const probe = await probeCargoCommitState(page, widgetSelector);
|
|
|
return evaluateCargoCommitted(
|
|
|
{ ...probe, editorOpen: probe.editorOpen || editorOpen },
|
|
|
expected,
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/** 地址搜索框是否仍可编辑 */
|
|
|
export async function isAddressSearchEditable(
|
|
|
page: Page,
|
|
|
widgetSelector: string,
|
|
|
): Promise<boolean> {
|
|
|
return page.evaluate((sel) => {
|
|
|
const widget = document.querySelector(sel);
|
|
|
if (!widget) {
|
|
|
return true;
|
|
|
}
|
|
|
const inputs = widget.querySelectorAll<HTMLInputElement>("input");
|
|
|
for (const inp of inputs) {
|
|
|
const ph = inp.placeholder ?? "";
|
|
|
if (!/search|搜索/i.test(ph)) {
|
|
|
continue;
|
|
|
}
|
|
|
const visible = inp.offsetParent !== null;
|
|
|
const editable =
|
|
|
!inp.disabled &&
|
|
|
inp.readOnly === false &&
|
|
|
inp.getAttribute("aria-readonly") !== "true";
|
|
|
if (visible && editable) {
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
return false;
|
|
|
}, widgetSelector);
|
|
|
}
|
|
|
|
|
|
/** 双地址必须已写入 widget(进入货物步前) */
|
|
|
export async function assertBothAddressesLocked(
|
|
|
page: Page,
|
|
|
widgetSelector: string,
|
|
|
): Promise<void> {
|
|
|
if (await isAddressesVisuallyCompleteForCargo(page, widgetSelector)) {
|
|
|
console.log("[rpa] assertBothAddressesLocked: 视觉已确认双地址,跳过网络闸门");
|
|
|
return;
|
|
|
}
|
|
|
const deadline = Date.now() + ADDRESS_SETTLE_MS;
|
|
|
while (Date.now() < deadline) {
|
|
|
if (await isAddressesLockedForSubmit(page, widgetSelector)) {
|
|
|
return;
|
|
|
}
|
|
|
await page.waitForTimeout(POLL_MS);
|
|
|
}
|
|
|
const text = await page
|
|
|
.locator(widgetSelector)
|
|
|
.innerText()
|
|
|
.catch(() => "");
|
|
|
throw new RpaError(
|
|
|
"ADDRESS_NOT_CONFIRMED",
|
|
|
`双地址未锁定,无法进入货物步(preview=${text.slice(0, 120)})`,
|
|
|
{ retryable: true },
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/** 纯函数:双地址是否已锁定(供单测) */
|
|
|
export function evaluateAddressesLockedForSubmit(
|
|
|
widgetText: string,
|
|
|
addressSearchEditable: boolean,
|
|
|
): boolean {
|
|
|
if (addressSearchEditable) {
|
|
|
return false;
|
|
|
}
|
|
|
const normalized = widgetText.replace(/\s+/g, " ");
|
|
|
const zipCount = (normalized.match(/\b\d{5}(?:-\d{4})?\b/g) ?? []).length;
|
|
|
const stateCount = (normalized.match(/,\s*[A-Z]{2}\b/g) ?? []).length;
|
|
|
const streetMarkers = normalized.match(/\d{3,5}\s+\S+/g) ?? [];
|
|
|
return stateCount >= 2 || zipCount >= 2 || streetMarkers.length >= 2;
|
|
|
}
|
|
|
|
|
|
/** submit 前地址是否已锁定(必须双地址证据,禁止单州码弱放行) */
|
|
|
export async function isAddressesLockedForSubmit(
|
|
|
page: Page,
|
|
|
widgetSelector: string,
|
|
|
): Promise<boolean> {
|
|
|
const text = await page
|
|
|
.locator(widgetSelector)
|
|
|
.innerText()
|
|
|
.catch(() => "");
|
|
|
const addressSearchEditable = await isAddressSearchEditable(
|
|
|
page,
|
|
|
widgetSelector,
|
|
|
);
|
|
|
return evaluateAddressesLockedForSubmit(text, addressSearchEditable);
|
|
|
}
|
|
|
|
|
|
/** 收起内联货物步以便 submit(不点「货运详情」避免重置向导) */
|
|
|
export async function collapseInlineCargoForSubmit(
|
|
|
page: Page,
|
|
|
widgetSelector: string,
|
|
|
): Promise<void> {
|
|
|
const widget = pageLocator(page, widgetSelector);
|
|
|
|
|
|
for (const pattern of [/\d+W,\s*x\s*\d+L/i, /\d+\s*托盘s?/i, /\d+\s*pallets?/i]) {
|
|
|
const summary = widget.getByText(pattern).first();
|
|
|
if (await summary.isVisible().catch(() => false)) {
|
|
|
await summary
|
|
|
.click({ timeout: 3_000, force: true })
|
|
|
.catch(() => undefined);
|
|
|
await page.waitForTimeout(350);
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (await isCargoFieldsVisible(page, widgetSelector)) {
|
|
|
await page.keyboard.press("Escape").catch(() => undefined);
|
|
|
await widget
|
|
|
.click({ position: { x: 300, y: 60 }, timeout: 3_000, force: true })
|
|
|
.catch(() => undefined);
|
|
|
await page.waitForTimeout(300);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/** 尝试提交内联货物步(不点击地址 chip / 步骤标签,避免重置向导) */
|
|
|
export async function attemptCargoCommit(
|
|
|
page: Page,
|
|
|
widgetSelector: string,
|
|
|
): Promise<void> {
|
|
|
const widget = pageLocator(page, widgetSelector);
|
|
|
|
|
|
await page.keyboard.press("Tab").catch(() => undefined);
|
|
|
await page.keyboard.press("Enter").catch(() => undefined);
|
|
|
await page.waitForTimeout(250);
|
|
|
|
|
|
const summaryPatterns = [
|
|
|
/\d+\s*托盘s?/i,
|
|
|
/\d+\s*pallets?/i,
|
|
|
/\d+W,\s*x\s*\d+L/i,
|
|
|
];
|
|
|
for (const pattern of summaryPatterns) {
|
|
|
const summary = widget.getByText(pattern).first();
|
|
|
if (await summary.isVisible().catch(() => false)) {
|
|
|
await summary
|
|
|
.click({ timeout: 3_000, force: true })
|
|
|
.catch(() => undefined);
|
|
|
await page.waitForTimeout(400);
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
await page.keyboard.press("Escape").catch(() => undefined);
|
|
|
await widget
|
|
|
.click({ position: { x: 280, y: 220 }, timeout: 3_000, force: true })
|
|
|
.catch(() => undefined);
|
|
|
await page.waitForTimeout(300);
|
|
|
}
|
|
|
|
|
|
async function landingPopoverMatchesExpected(
|
|
|
page: Page,
|
|
|
expected: ExpectedCargoCommit,
|
|
|
): Promise<boolean> {
|
|
|
const state = await page.evaluate(() => {
|
|
|
const pallet = document.querySelector<HTMLInputElement>(
|
|
|
'input[placeholder*="How many pallets" i]',
|
|
|
);
|
|
|
const weight = document.querySelector<HTMLInputElement>(
|
|
|
'input[placeholder*="Weight" i]',
|
|
|
);
|
|
|
return {
|
|
|
pallet: pallet?.value?.trim() ?? "",
|
|
|
weight: weight?.value?.trim() ?? "",
|
|
|
};
|
|
|
});
|
|
|
if (!state.pallet || !state.weight) {
|
|
|
return false;
|
|
|
}
|
|
|
if (
|
|
|
expected.palletCount !== undefined &&
|
|
|
Number(state.pallet) !== expected.palletCount
|
|
|
) {
|
|
|
return false;
|
|
|
}
|
|
|
if (expected.weight) {
|
|
|
const actual = Number(state.weight);
|
|
|
const target = Number(expected.weight.displayValue);
|
|
|
if (Number.isFinite(actual) && Number.isFinite(target) && actual !== target) {
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
export async function waitForCargoCommittedState(
|
|
|
page: Page,
|
|
|
widgetSelector: string,
|
|
|
expected?: ExpectedCargoCommit | number,
|
|
|
): Promise<CargoCommitProbe> {
|
|
|
const normalized: ExpectedCargoCommit | undefined =
|
|
|
typeof expected === "number" ? { palletCount: expected } : expected;
|
|
|
|
|
|
if (normalized && (await landingPopoverMatchesExpected(page, normalized))) {
|
|
|
console.log("[rpa] cargo-commit: ok(landing Freight details 弹层已填)");
|
|
|
return probeCargoCommitState(page, widgetSelector);
|
|
|
}
|
|
|
|
|
|
const deadline = Date.now() + COMMIT_WAIT_MS;
|
|
|
let last: CargoCommitProbe | null = null;
|
|
|
|
|
|
while (Date.now() < deadline) {
|
|
|
const probe = await probeCargoCommitState(page, widgetSelector);
|
|
|
const editorOpen = await isCargoEditorOpen(page);
|
|
|
const merged = { ...probe, editorOpen: probe.editorOpen || editorOpen };
|
|
|
last = merged;
|
|
|
|
|
|
if (normalized && (await landingPopoverMatchesExpected(page, normalized))) {
|
|
|
console.log("[rpa] cargo-commit: ok(landing Freight details 弹层已填)");
|
|
|
return merged;
|
|
|
}
|
|
|
|
|
|
if (evaluateCargoCommitted(merged, normalized)) {
|
|
|
console.log(
|
|
|
`[rpa] cargo-commit: ok pallet=${merged.internalPalletCount ?? "n/a"} weight=${merged.internalWeight ?? "n/a"} editable=${merged.visibleEditableCargoInputs}`,
|
|
|
);
|
|
|
return merged;
|
|
|
}
|
|
|
|
|
|
await page.waitForTimeout(POLL_MS);
|
|
|
}
|
|
|
|
|
|
const failed = last ?? (await probeCargoCommitState(page, widgetSelector));
|
|
|
console.log(`[rpa] cargo-commit: FAIL ${JSON.stringify(failed)}`);
|
|
|
const weightHint = normalized?.weight
|
|
|
? ` expectedWeight=${normalized.weight.weightLbPerPallet}lb/pallet`
|
|
|
: "";
|
|
|
throw new RpaError(
|
|
|
normalized?.weight &&
|
|
|
failed.hasSummaryChip &&
|
|
|
!probeWeightMatchesExpected(failed, normalized.weight)
|
|
|
? "CARGO_WEIGHT_MISMATCH"
|
|
|
: "CARGO_NOT_COMMITTED",
|
|
|
normalized?.weight &&
|
|
|
failed.hasSummaryChip &&
|
|
|
!probeWeightMatchesExpected(failed, normalized.weight)
|
|
|
? `货物重量与请求不一致(widget=${failed.internalWeight ?? failed.widgetTextPreview.slice(0, 80)}${weightHint})`
|
|
|
: `货物未进入 committed 态(summary=${failed.hasSummaryChip} editable=${failed.visibleEditableCargoInputs} pallet=${failed.internalPalletCount})`,
|
|
|
{ retryable: true },
|
|
|
);
|
|
|
}
|
|
|
|
|
|
export async function assertCargoCommittedState(
|
|
|
page: Page,
|
|
|
widgetSelector: string,
|
|
|
expected?: ExpectedCargoCommit | number,
|
|
|
): Promise<void> {
|
|
|
if (await isCargoCommittedState(page, widgetSelector, expected)) {
|
|
|
return;
|
|
|
}
|
|
|
const probe = await probeCargoCommitState(page, widgetSelector);
|
|
|
throw new RpaError(
|
|
|
"CARGO_NOT_COMMITTED",
|
|
|
`submit 前货物未 committed(${JSON.stringify(probe)})`,
|
|
|
{ retryable: true },
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/** 收起地址编辑态,确保进入货物步前无未锁定 street 输入 */
|
|
|
export async function settleAddressFieldsForCargo(
|
|
|
page: Page,
|
|
|
widgetSelector: string,
|
|
|
): Promise<void> {
|
|
|
await dismissCookieConsent(page);
|
|
|
const widget = pageLocator(page, widgetSelector);
|
|
|
const deadline = Date.now() + ADDRESS_SETTLE_MS;
|
|
|
|
|
|
while (Date.now() < deadline) {
|
|
|
if (await isAddressesVisuallyCompleteForCargo(page, widgetSelector)) {
|
|
|
console.log("[rpa] settleAddressFieldsForCargo: 视觉双地址已就绪,进入货物步");
|
|
|
return;
|
|
|
}
|
|
|
if (await isAddressesLockedForSubmit(page, widgetSelector)) {
|
|
|
return;
|
|
|
}
|
|
|
const hasEditableStreet = await page.evaluate((sel) => {
|
|
|
const root = document.querySelector(sel);
|
|
|
if (!root) {
|
|
|
return true;
|
|
|
}
|
|
|
const inputs = root.querySelectorAll<HTMLInputElement>("input");
|
|
|
for (const inp of inputs) {
|
|
|
const ph = inp.placeholder ?? "";
|
|
|
if (!/search|搜索/i.test(ph)) {
|
|
|
continue;
|
|
|
}
|
|
|
const visible = inp.offsetParent !== null;
|
|
|
const editable =
|
|
|
!inp.disabled &&
|
|
|
inp.readOnly === false &&
|
|
|
inp.getAttribute("aria-readonly") !== "true";
|
|
|
if (visible && editable) {
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
return false;
|
|
|
}, widgetSelector);
|
|
|
|
|
|
if (!hasEditableStreet) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
await page.keyboard.press("Escape").catch(() => undefined);
|
|
|
await widget
|
|
|
.click({ position: { x: 24, y: 24 }, timeout: 3_000, force: true })
|
|
|
.catch(() => undefined);
|
|
|
await page.waitForTimeout(350);
|
|
|
}
|
|
|
|
|
|
throw new RpaError(
|
|
|
"STRUCT_CHANGE",
|
|
|
"地址编辑态未收起,无法进入货物步",
|
|
|
{ retryable: true },
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/** @deprecated 使用 isCargoCommittedState */
|
|
|
export async function isCargoSummaryLocked(
|
|
|
page: Page,
|
|
|
widgetSelector: string,
|
|
|
expectedPalletCount?: number,
|
|
|
): Promise<boolean> {
|
|
|
return isCargoCommittedState(page, widgetSelector, expectedPalletCount);
|
|
|
}
|
|
|
|
|
|
/** 关闭 cargo popover / drawer */
|
|
|
export async function closeCargoEditor(
|
|
|
page: Page,
|
|
|
widgetSelector?: string,
|
|
|
): Promise<void> {
|
|
|
if (widgetSelector) {
|
|
|
const widget = pageLocator(page, widgetSelector);
|
|
|
await page.keyboard.press("Escape").catch(() => undefined);
|
|
|
await widget
|
|
|
.click({ position: { x: 180, y: 100 }, timeout: 3_000, force: true })
|
|
|
.catch(() => undefined);
|
|
|
await page.waitForTimeout(400);
|
|
|
}
|
|
|
|
|
|
if (!(await isCargoEditorOpen(page))) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
await page.keyboard.press("Escape").catch(() => undefined);
|
|
|
await page.waitForTimeout(400);
|
|
|
|
|
|
if (!(await isCargoEditorOpen(page))) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const dismissTargets = [
|
|
|
page.locator("#react-quoter-landing-plugin").first(),
|
|
|
page.getByRole("button", { name: /done|save|apply|确认|完成/i }).first(),
|
|
|
];
|
|
|
|
|
|
for (const loc of dismissTargets) {
|
|
|
if (!(await loc.isVisible().catch(() => false))) {
|
|
|
continue;
|
|
|
}
|
|
|
await loc.click({ timeout: 3_000, force: true }).catch(() => undefined);
|
|
|
await page.waitForTimeout(400);
|
|
|
if (!(await isCargoEditorOpen(page))) {
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
await page.mouse.click(12, 12).catch(() => undefined);
|
|
|
await page.waitForTimeout(400);
|
|
|
}
|
|
|
|
|
|
/** @deprecated 使用 waitForCargoCommittedState */
|
|
|
export async function waitForCargoSummaryLocked(
|
|
|
page: Page,
|
|
|
widgetSelector: string,
|
|
|
expectedPalletCount?: number,
|
|
|
): Promise<void> {
|
|
|
await waitForCargoCommittedState(page, widgetSelector, expectedPalletCount);
|
|
|
}
|