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.

229 lines
6.2 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 { Page } from "playwright";
import type { CargoCommitProbe } from "@/workers/rpa/cargo-lock";
/** MotherShip 重量输入框与 API freight.weightLbs 均为单托 lb */
export const CARGO_WEIGHT_TOLERANCE_LB = 1.5;
export const CARGO_WEIGHT_TOLERANCE_KG = 0.5;
const LB_TO_KG = 2.20462;
export type MotherShipWeightDisplay = {
displayValue: string;
unit: "lb" | "kg";
weightLbPerPallet: number;
};
export type ExpectedCargoWeight = MotherShipWeightDisplay & {
palletCount: number;
};
export type LiveWeightFieldMeta = {
placeholder: string;
ariaLabel: string;
adjacentText: string;
unit: "lb" | "kg";
};
/** 仅从单个字段的 placeholder/label/相邻文案推断单位(禁止合并多 spec 字符串) */
export function inferWeightUnitFromFieldHint(hint: string): "lb" | "kg" {
const text = hint.trim();
if (!text) {
return "lb";
}
const hasKg = /(?:\bkg\b|公斤|千克)/i.test(text);
const hasLb = /(?:\blbs?\b|磅)/i.test(text);
if (hasKg && !hasLb) {
return "kg";
}
if (hasLb) {
return "lb";
}
return "lb";
}
/** 按页面实际单位生成填入值API 入参始终为单托 lb */
export function buildWeightDisplayForUnit(
weightLbPerPallet: number,
unit: "lb" | "kg",
): MotherShipWeightDisplay {
if (unit === "kg") {
const kg = Math.round((weightLbPerPallet / LB_TO_KG) * 100) / 100;
return {
displayValue: String(kg),
unit: "kg",
weightLbPerPallet,
};
}
return {
displayValue: String(weightLbPerPallet),
unit: "lb",
weightLbPerPallet,
};
}
/**
* @deprecated 仅单测/回退:必须传入**单个**可见字段 hint不得传 env 全量 specs
*/
export function resolveMotherShipWeightDisplay(
weightLbPerPallet: number,
singleFieldHint: string,
): MotherShipWeightDisplay {
const unit = inferWeightUnitFromFieldHint(singleFieldHint);
return buildWeightDisplayForUnit(weightLbPerPallet, unit);
}
/** 读取 widget 内当前可见、可编辑的重量输入框单位指示 */
export async function readLiveWeightFieldMeta(
page: Page,
widgetSelector: string,
): Promise<LiveWeightFieldMeta | null> {
return page.evaluate((sel) => {
const widget = document.querySelector(sel);
if (!widget) {
return null;
}
const inputs = widget.querySelectorAll<HTMLInputElement>("input");
for (const inp of inputs) {
const visible = inp.offsetParent !== null;
const editable =
visible &&
!inp.disabled &&
inp.readOnly === false &&
inp.getAttribute("aria-readonly") !== "true";
if (!editable) {
continue;
}
const placeholder = inp.placeholder ?? "";
const ariaLabel = inp.getAttribute("aria-label") ?? "";
const id = inp.id;
let adjacentText = "";
if (id) {
const label = widget.querySelector(`label[for="${id}"]`);
adjacentText = label?.textContent?.trim() ?? "";
}
const hint = `${placeholder} ${ariaLabel} ${adjacentText}`;
if (!/weight|重量|lbs|lb|kg|公斤|千克|磅/i.test(hint)) {
continue;
}
const hasKg = /(?:\bkg\b|公斤|千克)/i.test(hint);
const hasLb = /(?:\blbs?\b|磅)/i.test(hint);
let unit: "lb" | "kg" = "lb";
if (hasKg && !hasLb) {
unit = "kg";
}
return {
placeholder,
ariaLabel,
adjacentText,
unit,
};
}
return null;
}, widgetSelector);
}
/** 按 MotherShip 页面当前重量字段实际单位决定填入值(轮询至字段可见) */
export async function resolveMotherShipWeightDisplayFromPage(
page: Page,
widgetSelector: string,
weightLbPerPallet: number,
waitMs = 12_000,
): Promise<MotherShipWeightDisplay> {
const deadline = Date.now() + waitMs;
while (Date.now() < deadline) {
const live = await readLiveWeightFieldMeta(page, widgetSelector);
if (live) {
return buildWeightDisplayForUnit(weightLbPerPallet, live.unit);
}
await page.waitForTimeout(150);
}
return buildWeightDisplayForUnit(weightLbPerPallet, "lb");
}
function parseSummaryWeightLb(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;
}
function summaryWeightMatchesExpected(
summaryWeight: number,
expected: ExpectedCargoWeight,
): boolean {
const perPalletOk =
Math.abs(summaryWeight - expected.weightLbPerPallet) <=
CARGO_WEIGHT_TOLERANCE_LB;
const totalExpected = expected.weightLbPerPallet * expected.palletCount;
const totalOk =
Math.abs(summaryWeight - totalExpected) <=
CARGO_WEIGHT_TOLERANCE_LB * expected.palletCount;
if (expected.unit === "kg") {
const kg = Number(expected.displayValue);
return (
Math.abs(summaryWeight - kg) <= CARGO_WEIGHT_TOLERANCE_KG ||
Math.abs(summaryWeight - kg * expected.palletCount) <=
CARGO_WEIGHT_TOLERANCE_KG * expected.palletCount
);
}
return perPalletOk || totalOk;
}
/** widget 内重量/托盘是否与请求一致(单托语义) */
export function probeWeightMatchesExpected(
probe: CargoCommitProbe,
expected: ExpectedCargoWeight,
): boolean {
if (
probe.internalPalletCount !== null &&
probe.internalPalletCount !== expected.palletCount
) {
return false;
}
if (probe.internalWeight !== null && probe.internalWeight > 0) {
if (expected.unit === "kg") {
return (
Math.abs(probe.internalWeight - Number(expected.displayValue)) <=
CARGO_WEIGHT_TOLERANCE_KG
);
}
return (
Math.abs(probe.internalWeight - expected.weightLbPerPallet) <=
CARGO_WEIGHT_TOLERANCE_LB
);
}
const summaryWeight = parseSummaryWeightLb(probe.widgetTextPreview);
if (summaryWeight === null) {
return false;
}
return summaryWeightMatchesExpected(summaryWeight, expected);
}
/** session / storageState 残留货物摘要且与本次请求不一致 */
export function isStaleCargoState(
probe: CargoCommitProbe,
expected: ExpectedCargoWeight,
): boolean {
if (!probe.hasSummaryChip) {
return false;
}
return !probeWeightMatchesExpected(probe, expected);
}