5460 lines
171 KiB
JavaScript
5460 lines
171 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
import readline from "node:readline";
|
||
import { mkdir } from "node:fs/promises";
|
||
import { appendFileSync } from "node:fs";
|
||
import { chromium } from "playwright-core";
|
||
|
||
const ERP_URL = process.env.DEWU_1688_ERP_URL || "https://www.erp321.com/epaas";
|
||
const USERNAME = process.env.DEWU_1688_ERP_USERNAME || "";
|
||
const PASSWORD = process.env.DEWU_1688_ERP_PASSWORD || "";
|
||
const BROWSER_CODE = process.env.DEWU_1688_BROWSER_CODE || "ERP-UNASSIGNED";
|
||
const PROFILE_DIR =
|
||
process.env.DEWU_1688_BROWSER_PROFILE ||
|
||
`/opt/dalongxia-chat-api/data/dewu-1688-browser-profiles/${BROWSER_CODE}`;
|
||
const EXECUTABLE =
|
||
process.env.DEWU_1688_CHROMIUM_PATH || "/snap/bin/chromium";
|
||
const HEADLESS = !["0", "false", "no"].includes(
|
||
String(process.env.DEWU_1688_HEADLESS || "1").toLowerCase(),
|
||
);
|
||
const ACTION_TIMEOUT = Number(process.env.DEWU_1688_ACTION_TIMEOUT_MS || 15000);
|
||
const ONLINE_VERIFY_TIMEOUT = Number(
|
||
process.env.DEWU_1688_ONLINE_VERIFY_TIMEOUT_MS || 20000,
|
||
);
|
||
const BATCH_VERIFY_TIMEOUT = Number(
|
||
process.env.DEWU_1688_BATCH_VERIFY_TIMEOUT_MS || 180000,
|
||
);
|
||
const BATCH_VERIFY_POLL_INTERVAL = Number(
|
||
process.env.DEWU_1688_BATCH_VERIFY_POLL_INTERVAL_MS || 30000,
|
||
);
|
||
const BATCH_VERIFY_MAX_ATTEMPTS = Number(
|
||
process.env.DEWU_1688_BATCH_VERIFY_MAX_ATTEMPTS || 3,
|
||
);
|
||
const DEBUG_LOG =
|
||
process.env.DEWU_1688_DEBUG_LOG ||
|
||
"/opt/dalongxia-chat-api/data/dewu-1688-worker.log";
|
||
|
||
let context;
|
||
let page;
|
||
|
||
function clean(value, limit = 300) {
|
||
return String(value || "").replace(/\s+/g, " ").trim().slice(0, limit);
|
||
}
|
||
|
||
function safeOrderFailure(message) {
|
||
const error = new Error(message);
|
||
error.safeToContinue = true;
|
||
return error;
|
||
}
|
||
|
||
function normalizedContactText(value) {
|
||
return String(value || "")
|
||
.toLowerCase()
|
||
.replace(/[^0-9a-z\u4e00-\u9fff]/g, "");
|
||
}
|
||
|
||
function withoutPrivacySuffix(value) {
|
||
return String(value || "")
|
||
.replace(/\s*[\[\uff3b]\d{4,8}[\]\uff3d]\s*$/u, "")
|
||
.trim();
|
||
}
|
||
|
||
function erpRecipientText(value) {
|
||
const source = withoutPrivacySuffix(value);
|
||
const chinese = (
|
||
source.match(/[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/gu) || []
|
||
).join("");
|
||
return String(chinese || source).trim().slice(0, 30);
|
||
}
|
||
|
||
function normalizedRecipientText(value) {
|
||
return normalizedContactText(withoutPrivacySuffix(value));
|
||
}
|
||
|
||
function privacyPhoneSuffix(value) {
|
||
const matched = String(value || "").match(/(?:-|\u2014|\uff0d)(\d{4})\s*$/u);
|
||
return matched ? matched[1] : "";
|
||
}
|
||
|
||
function normalizedPhoneDigits(value) {
|
||
const source = String(value || "").trim();
|
||
const privacyNumber = source.match(
|
||
/^\s*(1[3-9]\d{9})\s*(?:-|\u2014|\uff0d)\s*\d{4}\s*$/u,
|
||
);
|
||
return privacyNumber ? privacyNumber[1] : source.replace(/\D/g, "");
|
||
}
|
||
|
||
function phoneValueDiagnostic(value) {
|
||
const source = String(value || "").trim();
|
||
const digits = normalizedPhoneDigits(source);
|
||
return [
|
||
source ? "filled" : "empty",
|
||
isMaskedContactValue(source) ? "masked" : "plain",
|
||
`digits=${digits.length}`,
|
||
`tail=${digits ? digits.slice(-4) : "-"}`,
|
||
].join(":");
|
||
}
|
||
|
||
function isMaskedContactValue(value) {
|
||
return /[*\uff0a•●]{2,}/u.test(String(value || ""));
|
||
}
|
||
|
||
function normalizedMaskedRecipientPattern(value) {
|
||
return withoutPrivacySuffix(value)
|
||
.toLowerCase()
|
||
.replace(/[*\uff0a•●]+/gu, "*")
|
||
.replace(/[^0-9a-z\u4e00-\u9fff*]/gu, "");
|
||
}
|
||
|
||
function isMaskedRecipientValue(value) {
|
||
return /[*\uff0a•●]/u.test(String(value || ""));
|
||
}
|
||
|
||
function maskedRecipientMatchesExpected(actual, expected) {
|
||
const pattern = normalizedMaskedRecipientPattern(actual);
|
||
const normalizedExpected = normalizedRecipientText(expected);
|
||
if (!pattern.includes("*") || !normalizedExpected) return false;
|
||
const visibleText = pattern.replace(/\*/g, "");
|
||
if (!visibleText) return true;
|
||
const expression = pattern
|
||
.split("*")
|
||
.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
||
.join(".+");
|
||
return new RegExp(`^${expression}$`, "u").test(normalizedExpected);
|
||
}
|
||
|
||
function collapseRepeatedAddressSegments(value) {
|
||
let current = value;
|
||
for (let attempt = 0; attempt < 4; attempt += 1) {
|
||
const collapsed = current.replace(
|
||
/([\u4e00-\u9fff]{2,12}(?:省|市|区|县|镇|乡|街道))\1/gu,
|
||
"$1",
|
||
);
|
||
if (collapsed === current) break;
|
||
current = collapsed;
|
||
}
|
||
return current;
|
||
}
|
||
|
||
function canonicalAddressText(value, administrativeParts = []) {
|
||
let text = normalizedContactText(withoutPrivacySuffix(value));
|
||
const removable = new Set();
|
||
for (const part of administrativeParts) {
|
||
const normalized = normalizedContactText(part);
|
||
if (!normalized) continue;
|
||
removable.add(normalized);
|
||
const alias = normalized.replace(/(?:省|市|区|县)$/u, "");
|
||
if (alias.length >= 2) removable.add(alias);
|
||
}
|
||
for (const token of [...removable].sort((a, b) => b.length - a.length)) {
|
||
text = text.split(token).join("");
|
||
}
|
||
return collapseRepeatedAddressSegments(text);
|
||
}
|
||
|
||
function addressTextMatches(
|
||
expected,
|
||
actual,
|
||
administrativeParts = [],
|
||
) {
|
||
const expectedText = canonicalAddressText(expected, administrativeParts);
|
||
const actualText = canonicalAddressText(actual, administrativeParts);
|
||
const minimumComparableLength =
|
||
administrativeParts.filter((part) => normalizedContactText(part)).length >= 3
|
||
? 4
|
||
: 6;
|
||
return (
|
||
expectedText.length >= minimumComparableLength &&
|
||
actualText.length >= minimumComparableLength &&
|
||
(actualText.includes(expectedText) || expectedText.includes(actualText))
|
||
);
|
||
}
|
||
|
||
function contactTextMatchesOrder(order, actual) {
|
||
const text = normalizedContactText(actual);
|
||
const verified = order.__verifiedDelivery || {};
|
||
const recipient = normalizedRecipientText(
|
||
verified.recipient || order.recipient,
|
||
);
|
||
const phone = normalizedPhoneDigits(verified.phone || order.phone);
|
||
const privacySuffix = privacyPhoneSuffix(order.phone);
|
||
const address = verified.address || order.address;
|
||
return (
|
||
recipient.length > 0 &&
|
||
phone.length >= 7 &&
|
||
text.includes(recipient) &&
|
||
text.includes(phone) &&
|
||
(!privacySuffix || text.includes(privacySuffix)) &&
|
||
addressTextMatches(address, actual)
|
||
);
|
||
}
|
||
|
||
const SUBMISSION_RISK_STEPS = new Set([
|
||
"supplier.1688.save.started",
|
||
"supplier.1688.save.completed",
|
||
"supplier.1688.save_confirm.started",
|
||
"supplier.1688.save_confirm.completed",
|
||
"supplier.1688.native_confirm.started",
|
||
"supplier.1688.native_confirm.completed",
|
||
"supplier.1688.order.waiting",
|
||
"supplier.1688.verify.started",
|
||
"supplier.1688.submission.accepted",
|
||
"supplier.1688.confirm.started",
|
||
"supplier.1688.confirm.completed",
|
||
"supplier.1688.dialog.closed",
|
||
"supplier.1688.dialog.success",
|
||
]);
|
||
|
||
function stepMayHaveChangedExternalState(stepCode) {
|
||
const code = clean(stepCode, 100);
|
||
return (
|
||
code === "address.save.started" ||
|
||
code === "purchase.generate.submit.started" ||
|
||
code.startsWith("purchase.audit.submit.") ||
|
||
SUBMISSION_RISK_STEPS.has(code)
|
||
);
|
||
}
|
||
|
||
function publicFailure(error, fallback = "ERP 页面状态与预期不一致") {
|
||
const text = clean(error?.message || error, 180);
|
||
const mappings = [
|
||
[
|
||
/Existing purchase number verification failed/i,
|
||
"订单详情中的采购单号与预检记录不一致,本单已隔离待人工核对,后续订单继续处理",
|
||
],
|
||
[
|
||
/Existing purchase number was not loaded/i,
|
||
"订单详情暂未加载出已有采购单号,本单已隔离且不会重复生成采购单,后续订单继续处理",
|
||
],
|
||
[
|
||
/Generated purchase number was not found/i,
|
||
"采购单已提交生成,但暂未读取到采购单号,本单已隔离待人工核对,后续订单继续处理",
|
||
],
|
||
[
|
||
/Purchase is not waiting for audit/i,
|
||
"采购单状态不是待审核或已审核,本单已隔离待人工核对,后续订单继续处理",
|
||
],
|
||
[
|
||
/Purchase audit confirmation was not completed/i,
|
||
"采购单审核确认未完成,本单已隔离;采购单已保留,请人工核对审核状态",
|
||
],
|
||
[
|
||
/Purchase audit failed/i,
|
||
"ERP返回采购单审核失败,本单已隔离,请人工核对采购单;后续订单继续处理",
|
||
],
|
||
[
|
||
/purchase row container count is not one|Purchase row selection was not isolated/i,
|
||
"采购单行定位暂不稳定,本单已隔离待人工核对,后续订单继续处理",
|
||
],
|
||
[
|
||
/1688 online order (?:number was not found|verification (?:failed|timed out))/i,
|
||
"1688线上单号复核未通过,本单转入待复核,后续订单继续处理",
|
||
],
|
||
[
|
||
/1688 submission (?:was rejected|confirmation was not received)/i,
|
||
"ERP未返回1688下单成功确认,本单未完成,系统不会将其计为成功",
|
||
],
|
||
[
|
||
/Address parse verification failed/i,
|
||
"收货地址识别结果与原始资料不一致,本单已跳过且未保存,后续订单继续处理",
|
||
],
|
||
[
|
||
/Saved address verification failed/i,
|
||
"ERP保存后回读的收货信息与上传资料不一致,本单已阻止1688下单,后续订单继续处理",
|
||
],
|
||
[
|
||
/1688 (?:delivery address verification|default delivery address capture) failed/i,
|
||
"1688页面未切换到当前订单的收货地址,已阻止使用默认公司地址下单,后续订单继续处理",
|
||
],
|
||
[
|
||
/1688 account was not ready/i,
|
||
"1688账号暂未加载完成,本轮未重复提交下单,请稍后重试",
|
||
],
|
||
];
|
||
for (const [pattern, message] of mappings) {
|
||
if (pattern.test(text)) return message;
|
||
}
|
||
if (
|
||
!text ||
|
||
/(selector|locator|playwright|browsertype|chromium|chrome|timeout|stack|\/opt\/|\/snap\/|cookie|token|password|credential|iframe|frame|welcome|not found|ambiguous|unexpected|control|page\.goto|net::|call log|launchpersistentcontext|referenceerror|typeerror|is not defined|cannot read (?:properties|property))/i.test(
|
||
text,
|
||
)
|
||
) {
|
||
return fallback;
|
||
}
|
||
return text;
|
||
}
|
||
|
||
function logInternalError(error, scope = "worker") {
|
||
try {
|
||
const value = String(error?.stack || error?.message || error)
|
||
.replace(/\r?\n/g, " | ")
|
||
.slice(0, 4000);
|
||
appendFileSync(
|
||
DEBUG_LOG,
|
||
`${new Date().toISOString()} ${BROWSER_CODE} ${scope} ${value}\n`,
|
||
{ encoding: "utf8", mode: 0o600 },
|
||
);
|
||
} catch {
|
||
// Diagnostics must never change task behavior.
|
||
}
|
||
}
|
||
|
||
async function ensureBrowser() {
|
||
if (context && page && !page.isClosed()) return;
|
||
if (!USERNAME || !PASSWORD) {
|
||
throw new Error("ERP credentials are not configured");
|
||
}
|
||
await mkdir(PROFILE_DIR, { recursive: true });
|
||
context = await chromium.launchPersistentContext(PROFILE_DIR, {
|
||
executablePath: EXECUTABLE,
|
||
headless: HEADLESS,
|
||
viewport: { width: 1440, height: 960 },
|
||
args: ["--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu"],
|
||
});
|
||
context.setDefaultTimeout(ACTION_TIMEOUT);
|
||
const pages = context.pages();
|
||
page = pages[0] || (await context.newPage());
|
||
for (const extra of pages.slice(1)) {
|
||
await extra.close().catch(() => {});
|
||
}
|
||
context.on("page", async (candidate) => {
|
||
if (page && !page.isClosed() && candidate !== page) {
|
||
await candidate.close().catch(() => {});
|
||
}
|
||
});
|
||
}
|
||
|
||
async function firstVisible(locator) {
|
||
let count = 0;
|
||
try {
|
||
count = await locator.count();
|
||
} catch {
|
||
return [];
|
||
}
|
||
const visible = [];
|
||
for (let index = 0; index < count; index += 1) {
|
||
const item = locator.nth(index);
|
||
if (await item.isVisible().catch(() => false)) visible.push(item);
|
||
}
|
||
return visible;
|
||
}
|
||
|
||
async function oneVisible(locator, label) {
|
||
const visible = await firstVisible(locator);
|
||
if (visible.length !== 1) {
|
||
throw new Error(`${label} visible count ${visible.length}`);
|
||
}
|
||
return visible[0];
|
||
}
|
||
|
||
async function waitForOneVisible(locator, label, timeoutMs = 25000) {
|
||
const deadline = Date.now() + timeoutMs;
|
||
while (Date.now() < deadline) {
|
||
const visible = await firstVisible(locator);
|
||
if (visible.length === 1) return visible[0];
|
||
if (visible.length > 1) {
|
||
throw new Error(`${label} visible count ${visible.length}`);
|
||
}
|
||
await page.waitForTimeout(250);
|
||
}
|
||
throw new Error(`${label} visible count 0`);
|
||
}
|
||
|
||
async function clickExactText(frame, text, label = text) {
|
||
const item = await oneVisible(frame.getByText(text, { exact: true }), label);
|
||
await item.click();
|
||
}
|
||
|
||
async function dismissBlockingTips(frame) {
|
||
const tips = frame.locator("#TopPoolTip");
|
||
const count = await tips.count();
|
||
for (let index = 0; index < count; index += 1) {
|
||
const tip = tips.nth(index);
|
||
if (!(await tip.isVisible().catch(() => false))) continue;
|
||
await tip.evaluate((element) => {
|
||
const overlay = element.closest("div.contip") || element;
|
||
overlay.style.display = "none";
|
||
overlay.style.pointerEvents = "none";
|
||
});
|
||
}
|
||
}
|
||
|
||
async function waitForFrameWithVisibleTexts(
|
||
texts,
|
||
label,
|
||
timeoutMs = 25000,
|
||
) {
|
||
const deadline = Date.now() + timeoutMs;
|
||
while (Date.now() < deadline) {
|
||
const matches = [];
|
||
for (const candidate of page.frames()) {
|
||
let matched = true;
|
||
for (const text of texts) {
|
||
const visible = await firstVisible(
|
||
candidate.getByText(text, { exact: true }),
|
||
);
|
||
if (visible.length !== 1) {
|
||
matched = false;
|
||
break;
|
||
}
|
||
}
|
||
if (matched) matches.push(candidate);
|
||
}
|
||
if (matches.length === 1) return matches[0];
|
||
if (matches.length > 1) {
|
||
throw new Error(`${label} frame count ${matches.length}`);
|
||
}
|
||
await page.waitForTimeout(250);
|
||
}
|
||
throw new Error(`${label} frame not found`);
|
||
}
|
||
|
||
async function waitForPurchasePreviewFrame(timeoutMs = 25000) {
|
||
const deadline = Date.now() + timeoutMs;
|
||
while (Date.now() < deadline) {
|
||
const matches = [];
|
||
for (const candidate of page.frames()) {
|
||
const plaintext = await firstVisible(
|
||
candidate.getByText("明文", { exact: true }),
|
||
);
|
||
const generate = await firstVisible(
|
||
candidate.getByText("生成采购单", { exact: true }),
|
||
);
|
||
if (plaintext.length === 1 && generate.length === 1) {
|
||
matches.push(candidate);
|
||
}
|
||
if (
|
||
candidate.url().includes("/epaas-dialog-frame.html") &&
|
||
(await frameChainVisible(candidate).catch(() => false))
|
||
) {
|
||
const body = clean(
|
||
await candidate.locator("body").innerText().catch(() => ""),
|
||
500,
|
||
);
|
||
if (/不支持[\s\S]{0,120}生成采购单|生成采购单[\s\S]{0,120}不支持/.test(body)) {
|
||
const close = await firstVisible(
|
||
candidate.getByText("关闭", { exact: true }),
|
||
);
|
||
if (close.length === 1) await close[0].click().catch(() => {});
|
||
throw new Error(`ERP拒绝生成采购单:${body}`);
|
||
}
|
||
}
|
||
}
|
||
if (matches.length === 1) return matches[0];
|
||
if (matches.length > 1) {
|
||
throw new Error(`purchase preview frame count ${matches.length}`);
|
||
}
|
||
await page.waitForTimeout(250);
|
||
}
|
||
throw new Error("purchase preview frame not found");
|
||
}
|
||
|
||
async function closeVisibleEpaasDialogs(timeoutMs = 8000) {
|
||
const deadline = Date.now() + timeoutMs;
|
||
while (Date.now() < deadline) {
|
||
const visibleDialogs = [];
|
||
for (const candidate of page.frames()) {
|
||
if (
|
||
candidate.url().includes("/epaas-dialog-frame.html") &&
|
||
(await frameChainVisible(candidate).catch(() => false))
|
||
) {
|
||
visibleDialogs.push(candidate);
|
||
}
|
||
}
|
||
if (!visibleDialogs.length) return;
|
||
for (const candidate of visibleDialogs.reverse()) {
|
||
for (const label of ["确定", "确认", "关闭", "取消"]) {
|
||
const controls = await firstVisible(
|
||
candidate.getByText(label, { exact: true }),
|
||
);
|
||
if (controls.length === 1) {
|
||
await controls[0].click({ force: true }).catch(() => {});
|
||
await page.waitForTimeout(250);
|
||
break;
|
||
}
|
||
}
|
||
await candidate.locator("body").press("Escape").catch(() => {});
|
||
}
|
||
await page.keyboard.press("Escape").catch(() => {});
|
||
await page.waitForTimeout(250);
|
||
}
|
||
const body = [];
|
||
for (const candidate of page.frames()) {
|
||
if (
|
||
candidate.url().includes("/epaas-dialog-frame.html") &&
|
||
(await frameChainVisible(candidate).catch(() => false))
|
||
) {
|
||
body.push(
|
||
clean(
|
||
await candidate.locator("body").innerText().catch(() => ""),
|
||
500,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
if (!body.some(Boolean)) {
|
||
const blankOverlays = page.locator("iframe#epaasDialogFrame");
|
||
const count = await blankOverlays.count();
|
||
for (let index = 0; index < count; index += 1) {
|
||
const overlay = blankOverlays.nth(index);
|
||
if (!(await overlay.isVisible().catch(() => false))) continue;
|
||
await overlay.evaluate((element) => {
|
||
element.style.display = "none";
|
||
element.style.pointerEvents = "none";
|
||
element.setAttribute("aria-hidden", "true");
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
throw new Error(`ERP弹窗未关闭:${clean(body.join(" | "), 500)}`);
|
||
}
|
||
|
||
async function hideBlankEpaasPointerOverlays(navigationFrame) {
|
||
const overlays = page.locator("iframe#epaasDialogFrame");
|
||
const count = await overlays.count();
|
||
let hidden = 0;
|
||
for (let index = 0; index < count; index += 1) {
|
||
const overlay = overlays.nth(index);
|
||
if (!(await overlay.isVisible().catch(() => false))) continue;
|
||
const handle = await overlay.elementHandle().catch(() => null);
|
||
const candidate = handle
|
||
? await handle.contentFrame().catch(() => null)
|
||
: null;
|
||
if (!candidate || isDescendantFrame(candidate, navigationFrame)) continue;
|
||
const body = clean(
|
||
await candidate.locator("body").innerText().catch(() => ""),
|
||
500,
|
||
);
|
||
if (body) continue;
|
||
await overlay.evaluate((element) => {
|
||
element.style.display = "none";
|
||
element.style.pointerEvents = "none";
|
||
element.setAttribute("aria-hidden", "true");
|
||
});
|
||
hidden += 1;
|
||
}
|
||
return hidden;
|
||
}
|
||
|
||
async function waitForVisibleTextAcrossFrames(
|
||
text,
|
||
label,
|
||
timeoutMs = 25000,
|
||
exact = false,
|
||
) {
|
||
const deadline = Date.now() + timeoutMs;
|
||
while (Date.now() < deadline) {
|
||
const matches = [];
|
||
for (const candidate of page.frames()) {
|
||
if (!(await frameChainVisible(candidate).catch(() => false))) continue;
|
||
for (const item of await firstVisible(
|
||
candidate.getByText(text, { exact }),
|
||
)) {
|
||
matches.push({ frame: candidate, item });
|
||
}
|
||
}
|
||
if (matches.length === 1) return matches[0];
|
||
if (matches.length > 1) {
|
||
throw new Error(`${label} visible count ${matches.length}`);
|
||
}
|
||
await page.waitForTimeout(250);
|
||
}
|
||
throw new Error(`${label} not found`);
|
||
}
|
||
|
||
async function waitForVisibleLocatorAcrossFrames(
|
||
selector,
|
||
label,
|
||
timeoutMs = 25000,
|
||
) {
|
||
const deadline = Date.now() + timeoutMs;
|
||
while (Date.now() < deadline) {
|
||
const matches = [];
|
||
for (const candidate of page.frames()) {
|
||
if (!(await frameChainVisible(candidate).catch(() => false))) continue;
|
||
for (const item of await firstVisible(candidate.locator(selector))) {
|
||
matches.push({ frame: candidate, item });
|
||
}
|
||
}
|
||
if (matches.length === 1) return matches[0];
|
||
if (matches.length > 1) {
|
||
throw new Error(`${label} visible count ${matches.length}`);
|
||
}
|
||
await page.waitForTimeout(250);
|
||
}
|
||
throw new Error(`${label} not found`);
|
||
}
|
||
|
||
async function loginFormVisible() {
|
||
return page
|
||
.locator('input[placeholder="邮箱地址/手机号码"]')
|
||
.isVisible()
|
||
.catch(() => false);
|
||
}
|
||
|
||
async function otpFormVisible() {
|
||
return page
|
||
.locator('input[placeholder="输入验证码"]')
|
||
.isVisible()
|
||
.catch(() => false);
|
||
}
|
||
|
||
async function gotoErp() {
|
||
try {
|
||
await page.goto(ERP_URL, { waitUntil: "domcontentloaded" });
|
||
} catch (error) {
|
||
if (!/net::ERR_ABORTED/i.test(String(error?.message || error))) {
|
||
throw error;
|
||
}
|
||
await page.waitForTimeout(800);
|
||
}
|
||
}
|
||
|
||
function erpApplicationVisible() {
|
||
return page.frames().some(
|
||
(frame) =>
|
||
frame.url().includes("/erp-web-group/home/") ||
|
||
frame.url().includes("/app/order/order/list.aspx"),
|
||
);
|
||
}
|
||
|
||
async function submitCredentials() {
|
||
const account = page.locator('input[placeholder="邮箱地址/手机号码"]');
|
||
const password = page.locator('input[placeholder="请输入密码"]');
|
||
if ((await account.count()) !== 1 || (await password.count()) !== 1) {
|
||
throw new Error("Unexpected login form");
|
||
}
|
||
await account.fill(USERNAME);
|
||
await password.fill(PASSWORD);
|
||
const checkbox = page.locator('input[type="checkbox"]').first();
|
||
if ((await checkbox.count()) === 1 && !(await checkbox.isChecked())) {
|
||
await checkbox.check();
|
||
}
|
||
await clickExactText(page, "立即登录", "login button");
|
||
await page.waitForTimeout(700);
|
||
}
|
||
|
||
async function sendSmsIfNeeded(force = false) {
|
||
const resend = page.getByText(
|
||
/^(获取验证码|重新获取(?:\(\d+s\))?)$/,
|
||
{ exact: true },
|
||
);
|
||
const visible = await firstVisible(resend);
|
||
if (visible.length !== 1) {
|
||
throw new Error("Unexpected SMS control");
|
||
}
|
||
const label = clean(await visible[0].innerText(), 60);
|
||
const waitSeconds = Number(label.match(/\((\d+)s\)/)?.[1] || 0);
|
||
if (label === "获取验证码" || label === "重新获取") {
|
||
await visible[0].click();
|
||
await page.waitForTimeout(500);
|
||
return { sent: true, waitSeconds: 0 };
|
||
}
|
||
return { sent: false, waitSeconds, force };
|
||
}
|
||
|
||
async function ensureLoggedIn(otp = "", resendSms = false) {
|
||
await ensureBrowser();
|
||
let navigated = false;
|
||
if (!page.url().startsWith("https://www.erp321.com/")) {
|
||
await gotoErp();
|
||
navigated = true;
|
||
}
|
||
if (
|
||
!navigated &&
|
||
!erpApplicationVisible() &&
|
||
!(await loginFormVisible()) &&
|
||
!(await otpFormVisible())
|
||
) {
|
||
await gotoErp();
|
||
}
|
||
await page.waitForTimeout(800);
|
||
if (await loginFormVisible()) {
|
||
await submitCredentials();
|
||
}
|
||
if (await otpFormVisible()) {
|
||
if (!otp) {
|
||
const sms = await sendSmsIfNeeded(resendSms);
|
||
return {
|
||
status: "needs_sms",
|
||
message: sms.sent
|
||
? "ERP 已发送登录验证码。"
|
||
: sms.waitSeconds
|
||
? `验证码已发送,${sms.waitSeconds} 秒后可重新发送。`
|
||
: "验证码已发送,请检查登录手机号。",
|
||
};
|
||
}
|
||
const input = page.locator('input[placeholder="输入验证码"]');
|
||
await input.fill(otp);
|
||
await clickExactText(page, "确 定", "OTP confirmation");
|
||
await page.waitForTimeout(1600);
|
||
if (await otpFormVisible()) {
|
||
const errorText = clean(
|
||
await page.locator("body").innerText().catch(() => ""),
|
||
500,
|
||
);
|
||
if (/验证码.*(错误|失效|过期)|Fid field/i.test(errorText)) {
|
||
const sms = await sendSmsIfNeeded(true).catch(() => ({
|
||
sent: false,
|
||
waitSeconds: 0,
|
||
}));
|
||
return {
|
||
status: "needs_sms",
|
||
message: sms.sent
|
||
? "验证码已失效,ERP 已重新发送验证码。"
|
||
: sms.waitSeconds
|
||
? `验证码未通过,${sms.waitSeconds} 秒后可重新发送。`
|
||
: "验证码未通过,请回复“重新发送验证码”后再试。",
|
||
};
|
||
}
|
||
throw new Error("OTP was not accepted");
|
||
}
|
||
}
|
||
const deadline = Date.now() + 30000;
|
||
while (Date.now() < deadline) {
|
||
if (erpApplicationVisible()) return { status: "ok" };
|
||
if (await loginFormVisible()) {
|
||
throw new Error("Login form remained visible");
|
||
}
|
||
if (await otpFormVisible()) return { status: "needs_sms" };
|
||
await page.waitForTimeout(250);
|
||
}
|
||
throw new Error("ERP application did not become ready");
|
||
}
|
||
|
||
function frameByUrl(fragment) {
|
||
return page.frames().find((frame) => frame.url().includes(fragment));
|
||
}
|
||
|
||
async function frameChainVisible(frame) {
|
||
let current = frame;
|
||
while (current && current.parentFrame()) {
|
||
const element = await current.frameElement().catch(() => null);
|
||
if (!element || !(await element.isVisible().catch(() => false))) {
|
||
return false;
|
||
}
|
||
current = current.parentFrame();
|
||
}
|
||
return true;
|
||
}
|
||
|
||
async function visibleFrameByUrl(fragment) {
|
||
const candidates = page
|
||
.frames()
|
||
.filter((frame) => frame.url().includes(fragment));
|
||
for (const frame of candidates.reverse()) {
|
||
if (await frameChainVisible(frame)) return frame;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
async function dismissWelcomeDialogs() {
|
||
const welcome = frameByUrl("/erp-web-group/home/");
|
||
if (!welcome) return;
|
||
for (const selector of [
|
||
'button[aria-label="Close"]',
|
||
".ant-modal-close",
|
||
".anticon-close",
|
||
]) {
|
||
const candidates = welcome.locator(selector);
|
||
const visible = await firstVisible(candidates);
|
||
if (visible.length === 1) {
|
||
await visible[0].click({ force: true }).catch(() => {});
|
||
await welcome.waitForTimeout(200);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function readyOrderFrame() {
|
||
const candidates = page
|
||
.frames()
|
||
.filter((frame) => frame.url().includes("/app/order/order/list.aspx"))
|
||
.reverse();
|
||
for (const frame of candidates) {
|
||
if (!(await frameChainVisible(frame))) continue;
|
||
if ((await visibleOrderInputs(frame)).length) return frame;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
async function openOrderList() {
|
||
let orderFrame =
|
||
(await readyOrderFrame()) ||
|
||
(await visibleFrameByUrl("/app/order/order/list.aspx"));
|
||
if (orderFrame) {
|
||
const tabs = await firstVisible(
|
||
page.getByRole("tab", { name: "订单", exact: true }),
|
||
);
|
||
if (tabs.length === 1) await tabs[0].click().catch(() => {});
|
||
return (await readyOrderFrame()) || orderFrame;
|
||
}
|
||
await gotoErp();
|
||
await page.waitForTimeout(1200);
|
||
await dismissWelcomeDialogs();
|
||
const welcome = frameByUrl("/erp-web-group/home/");
|
||
if (!welcome) throw new Error("Welcome frame not found");
|
||
const candidates = [
|
||
...(await firstVisible(
|
||
page.getByRole("tab", { name: "订单", exact: true }),
|
||
)),
|
||
...(await firstVisible(welcome.getByText("订单", { exact: true }))),
|
||
];
|
||
if (!candidates.length) throw new Error("Order shortcut not found");
|
||
for (const candidate of candidates) {
|
||
await candidate.evaluate((element) => element.click()).catch(() => {});
|
||
const deadline = Date.now() + 12000;
|
||
while (Date.now() < deadline) {
|
||
orderFrame = await visibleFrameByUrl("/app/order/order/list.aspx");
|
||
if (orderFrame) return orderFrame;
|
||
await page.waitForTimeout(250);
|
||
}
|
||
}
|
||
throw new Error("Order list did not open");
|
||
}
|
||
|
||
async function visibleOrderInputs(orderFrame) {
|
||
const candidates = [];
|
||
for (const frame of page.frames()) {
|
||
if (!isDescendantFrame(frame, orderFrame)) continue;
|
||
const input = frame.locator(
|
||
"input#so_id, textarea#so_id, [contenteditable='true']#so_id",
|
||
);
|
||
if (
|
||
(await input.count()) === 1 &&
|
||
(await frameChainVisible(frame)) &&
|
||
(await input.isVisible().catch(() => false))
|
||
) {
|
||
candidates.push(input);
|
||
}
|
||
}
|
||
return candidates;
|
||
}
|
||
|
||
async function waitForOrderInputs(orderFrame) {
|
||
const readyDeadline = Date.now() + ACTION_TIMEOUT;
|
||
while (Date.now() < readyDeadline) {
|
||
const candidates = await visibleOrderInputs(orderFrame);
|
||
if (candidates.length) return candidates;
|
||
await orderFrame.waitForTimeout(250);
|
||
}
|
||
return [];
|
||
}
|
||
|
||
async function resetToOrderList() {
|
||
if (!context || !page || page.isClosed()) return;
|
||
const pages = context.pages();
|
||
for (const candidate of pages) {
|
||
if (candidate !== page) await candidate.close().catch(() => {});
|
||
}
|
||
if (await loginFormVisible() || (await otpFormVisible())) {
|
||
throw new Error("Browser returned to an authentication page");
|
||
}
|
||
await gotoErp();
|
||
await page.waitForTimeout(800);
|
||
await dismissWelcomeDialogs();
|
||
await openOrderList();
|
||
const orderTabs = await firstVisible(
|
||
page.getByRole("tab", { name: "订单", exact: true }),
|
||
);
|
||
if (orderTabs.length === 1) await orderTabs[0].click();
|
||
if (context.pages().length !== 1) {
|
||
throw new Error("Browser did not return to one main window");
|
||
}
|
||
}
|
||
|
||
async function warmBrowserAtOrderList() {
|
||
let lastError;
|
||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||
try {
|
||
const auth = await ensureLoggedIn();
|
||
if (auth.status !== "ok") return auth;
|
||
const orderFrame = await openOrderList();
|
||
const inputs = await waitForOrderInputs(orderFrame);
|
||
if (!inputs.length) throw new Error("Order input did not become ready");
|
||
if (context.pages().length !== 1) {
|
||
throw new Error("Warm browser has more than one window");
|
||
}
|
||
return {
|
||
status: "ok",
|
||
browser_reset: "order_list",
|
||
message: "ERP 浏览器已登录并停在订单页面。",
|
||
};
|
||
} catch (error) {
|
||
lastError = error;
|
||
await context?.close().catch(() => {});
|
||
context = undefined;
|
||
page = undefined;
|
||
if (attempt === 0) {
|
||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||
}
|
||
}
|
||
}
|
||
throw lastError || new Error("ERP browser warmup failed");
|
||
}
|
||
|
||
async function recycleBrowserToOrderList() {
|
||
await resetToOrderList();
|
||
await context.close();
|
||
context = undefined;
|
||
page = undefined;
|
||
const warmed = await warmBrowserAtOrderList();
|
||
if (warmed.status !== "ok") {
|
||
throw new Error("Browser authentication was not retained during recycle");
|
||
}
|
||
}
|
||
|
||
function isDescendantFrame(frame, parent) {
|
||
let current = frame;
|
||
while (current) {
|
||
if (current === parent) return true;
|
||
current = current.parentFrame();
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function searchOrder(orderFrame, orderId) {
|
||
const normalizedOrderId = clean(orderId, 40);
|
||
const maxSearchAttempts = 3;
|
||
for (
|
||
let attempt = 1;
|
||
attempt <= maxSearchAttempts;
|
||
attempt += 1
|
||
) {
|
||
const candidates = await waitForOrderInputs(orderFrame);
|
||
if (!candidates.length) throw new Error("Online order input not found");
|
||
for (const input of candidates.reverse()) {
|
||
await input.fill(normalizedOrderId);
|
||
await input.press("Enter");
|
||
const deadline = Date.now() + (attempt === 1 ? 6000 : 10000);
|
||
while (Date.now() < deadline) {
|
||
const visible = await firstVisible(
|
||
orderFrame.locator(`text="${normalizedOrderId}"`),
|
||
);
|
||
if (visible.length === 1) return visible[0];
|
||
if (visible.length > 1) {
|
||
throw new Error("More than one order row matched");
|
||
}
|
||
await orderFrame.waitForTimeout(250);
|
||
}
|
||
}
|
||
if (attempt < maxSearchAttempts) {
|
||
await orderFrame.waitForTimeout(1000);
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
async function orderRowInfo(orderLocator, orderId) {
|
||
return orderLocator.evaluate((element, expectedOrderId) => {
|
||
let node = element;
|
||
let best = element;
|
||
while (node && node !== document.body) {
|
||
const text = (node.innerText || "").replace(/\s+/g, " ").trim();
|
||
const hasCheckbox = Boolean(
|
||
node.querySelector?.('input[type="checkbox"]'),
|
||
);
|
||
if (
|
||
text.includes(expectedOrderId) &&
|
||
text.length >= expectedOrderId.length &&
|
||
text.length <= 5000
|
||
) {
|
||
best = node;
|
||
if (hasCheckbox && /得物|1688一件代发/.test(text)) break;
|
||
}
|
||
node = node.parentElement;
|
||
}
|
||
const text = (best.innerText || "").replace(/\s+/g, " ").trim();
|
||
const rowElement = element.closest("._jt_row") || best;
|
||
const cellText = (dataId) => {
|
||
const cell = rowElement.querySelector?.(`[data-id="${dataId}"]`);
|
||
return (cell?.innerText || cell?.textContent || "")
|
||
.replace(/\s+/g, " ")
|
||
.trim();
|
||
};
|
||
const explicitOrderStatus = cellText("status");
|
||
const orderStatus =
|
||
explicitOrderStatus ||
|
||
(text.match(/异常\s*1688\s*一件代发/) || [""])[0] ||
|
||
(text.match(/已发货|已完成|已取消|交易关闭|已作废/) || [""])[0];
|
||
const explicitPurchaseOrderNo = cellText("po_id");
|
||
return {
|
||
text,
|
||
orderStatus,
|
||
purchaseOrderNo:
|
||
explicitPurchaseOrderNo.match(/^\d{5,20}$/)?.[0] || "",
|
||
shopName:
|
||
(text.match(/得物-[^\s]+|店铺-得物/) || [""])[0],
|
||
hasDropshipMarker: text.includes("1688一件代发"),
|
||
hasPurchaseTag: text.includes("采购订单"),
|
||
blockedLifecycleStatus:
|
||
(orderStatus.match(/已发货|已完成|已取消|交易关闭|已作废/) || [""])[0],
|
||
};
|
||
}, orderId);
|
||
}
|
||
|
||
function isErpOrderEditorFrame(frame) {
|
||
try {
|
||
const url = new URL(frame.url());
|
||
return (
|
||
(url.hostname === "erp321.com" ||
|
||
url.hostname.endsWith(".erp321.com")) &&
|
||
url.pathname.toLowerCase() ===
|
||
"/app/order/order/ordereditornew.aspx"
|
||
);
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function openOrderDetail(
|
||
orderFrame,
|
||
orderLocator,
|
||
expectedOrderId = "",
|
||
) {
|
||
const priorFrames = new Set(
|
||
page
|
||
.frames()
|
||
.filter((frame) => isErpOrderEditorFrame(frame))
|
||
);
|
||
await orderLocator.click({ button: "right" });
|
||
const menuItems = orderFrame.getByText("订单详情", { exact: true });
|
||
const visible = await firstVisible(menuItems);
|
||
if (visible.length !== 1) {
|
||
throw new Error("Order detail menu is ambiguous");
|
||
}
|
||
await visible[0].click();
|
||
const deadline = Date.now() + 20000;
|
||
while (Date.now() < deadline) {
|
||
const candidates = page
|
||
.frames()
|
||
.filter((frame) => isErpOrderEditorFrame(frame))
|
||
.reverse();
|
||
const visibleCandidates = [];
|
||
for (const candidate of candidates) {
|
||
if (await frameChainVisible(candidate).catch(() => false)) {
|
||
visibleCandidates.push(candidate);
|
||
}
|
||
}
|
||
const fresh = visibleCandidates.find(
|
||
(candidate) => !priorFrames.has(candidate),
|
||
);
|
||
if (fresh) return fresh;
|
||
if (expectedOrderId) {
|
||
for (const candidate of visibleCandidates) {
|
||
const body = clean(
|
||
await candidate.locator("body").innerText().catch(() => ""),
|
||
12000,
|
||
);
|
||
if (body.includes(expectedOrderId)) return candidate;
|
||
}
|
||
}
|
||
if (visibleCandidates.length === 1) return visibleCandidates[0];
|
||
await page.waitForTimeout(200);
|
||
}
|
||
throw new Error("Order detail did not open");
|
||
}
|
||
|
||
async function readOrderDetail(detailFrame) {
|
||
const body = clean(await detailFrame.locator("body").innerText(), 15000);
|
||
const purchaseMatch = body.match(/生成采购单号[::]?\s*(\d{5,20})/);
|
||
const value = async (selector) => {
|
||
const item = detailFrame.locator(selector);
|
||
return (await item.count()) === 1
|
||
? clean(await item.inputValue().catch(() => ""), 300)
|
||
: "";
|
||
};
|
||
return {
|
||
purchaseOrderNo: purchaseMatch?.[1] || "",
|
||
receiverState: await value("#receiver_state"),
|
||
receiverCity: await value("#receiver_city"),
|
||
receiverDistrict: await value("#receiver_district"),
|
||
receiverAddress: await value("#receiver_address"),
|
||
receiverName: await value("#receiver_name"),
|
||
logText: body,
|
||
};
|
||
}
|
||
|
||
async function waitForOrderDetailPurchase(
|
||
detailFrame,
|
||
expectedPurchaseOrderNo = "",
|
||
timeoutMs = 12000,
|
||
) {
|
||
const expected = clean(expectedPurchaseOrderNo, 40);
|
||
const deadline = Date.now() + timeoutMs;
|
||
let detail = {};
|
||
let nextScrollAt = 0;
|
||
while (Date.now() < deadline) {
|
||
if (Date.now() >= nextScrollAt) {
|
||
await detailFrame
|
||
.evaluate(() => {
|
||
window.scrollTo(0, document.documentElement.scrollHeight);
|
||
document.scrollingElement?.scrollTo(
|
||
0,
|
||
document.scrollingElement.scrollHeight,
|
||
);
|
||
for (const element of document.querySelectorAll("*")) {
|
||
if (element.scrollHeight > element.clientHeight + 20) {
|
||
element.scrollTop = element.scrollHeight;
|
||
}
|
||
}
|
||
const logSection = [...document.querySelectorAll("*")].find(
|
||
(element) =>
|
||
/订单操作进程及日志|生成采购单号/.test(
|
||
element.textContent || "",
|
||
),
|
||
);
|
||
logSection?.scrollIntoView({ block: "center" });
|
||
})
|
||
.catch(() => {});
|
||
nextScrollAt = Date.now() + 1000;
|
||
}
|
||
detail = await readOrderDetail(detailFrame);
|
||
const current = clean(detail.purchaseOrderNo, 40);
|
||
if (!expected && current) return detail;
|
||
if (expected && current === expected) return detail;
|
||
if (expected) {
|
||
const links = await firstVisible(
|
||
detailFrame.getByText(expected, { exact: true }),
|
||
);
|
||
if (links.length === 1) {
|
||
detail.purchaseOrderNo = expected;
|
||
return detail;
|
||
}
|
||
}
|
||
await detailFrame.waitForTimeout(250);
|
||
}
|
||
return detail;
|
||
}
|
||
|
||
async function closeOrderDetail(detailFrame) {
|
||
await detailFrame.locator("body").press("Escape").catch(() => {});
|
||
const deadline = Date.now() + 2500;
|
||
while (Date.now() < deadline) {
|
||
if (!(await frameChainVisible(detailFrame).catch(() => false))) return;
|
||
await page.waitForTimeout(150);
|
||
}
|
||
await closeVisibleEpaasDialogs().catch(() => {});
|
||
}
|
||
|
||
async function inspectSingleOrder(orderFrame, order, report = () => {}) {
|
||
const orderId = clean(order.order_id, 40);
|
||
report("order.search.started", "正在输入线上订单号并搜索", "started");
|
||
const orderLocator = await searchOrder(orderFrame, orderId);
|
||
if (!orderLocator) {
|
||
report("order.search.not_found", "线上订单号未匹配", "failed");
|
||
return {
|
||
order_id: orderId,
|
||
state: "not_found",
|
||
order_status: "未找到",
|
||
message: "线上订单号未匹配",
|
||
};
|
||
}
|
||
report("order.search.completed", "已定位订单行", "completed");
|
||
const row = await orderRowInfo(orderLocator, orderId);
|
||
report("order.status.read", "已读取订单状态", "completed");
|
||
let detail = {};
|
||
try {
|
||
report("order.detail.open.started", "正在打开订单详情", "started");
|
||
const detailFrame = await openOrderDetail(
|
||
orderFrame,
|
||
orderLocator,
|
||
orderId,
|
||
);
|
||
report("order.detail.open.completed", "订单详情已打开", "completed");
|
||
detail = await waitForOrderDetailPurchase(detailFrame, "", 1800);
|
||
report("order.detail.read", "已读取订单详情和操作日志", "completed");
|
||
await closeOrderDetail(detailFrame);
|
||
report("order.detail.closed", "订单详情已关闭", "completed");
|
||
} catch {
|
||
detail = {};
|
||
report("order.detail.read.skipped", "订单详情读取失败,继续核对订单行", "skipped");
|
||
}
|
||
const purchaseOrderNo =
|
||
clean(detail.purchaseOrderNo, 40) || clean(row.purchaseOrderNo, 40);
|
||
if (purchaseOrderNo || row.hasPurchaseTag) {
|
||
if (purchaseOrderNo) {
|
||
report(
|
||
"order.preflight.resume_purchase",
|
||
"检测到已有采购单,将从采购审核步骤继续",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
return {
|
||
order_id: orderId,
|
||
state: "resume_purchase",
|
||
shop_name: row.shopName,
|
||
order_status: "1688一件代发 / 已生成采购",
|
||
purchase_order_no: purchaseOrderNo,
|
||
current_address: clean(detail.receiverAddress, 120),
|
||
message: "继续处理现有采购单",
|
||
};
|
||
}
|
||
report(
|
||
"order.preflight.duplicate_blocked",
|
||
"检测到采购记录但无法确认采购单号,已阻止重复生成",
|
||
"failed",
|
||
);
|
||
return {
|
||
order_id: orderId,
|
||
state: "already_processed",
|
||
shop_name: row.shopName,
|
||
order_status: row.hasDropshipMarker
|
||
? "1688一件代发 / 已生成采购"
|
||
: "已生成采购",
|
||
purchase_order_no: purchaseOrderNo,
|
||
current_address: clean(detail.receiverAddress, 120),
|
||
message: "已存在采购单或采购标记",
|
||
};
|
||
}
|
||
if (!row.shopName.includes("得物")) {
|
||
return {
|
||
order_id: orderId,
|
||
state: "blocked",
|
||
shop_name: row.shopName,
|
||
order_status: "非得物订单",
|
||
message: "店铺不是得物渠道",
|
||
};
|
||
}
|
||
if (row.blockedLifecycleStatus) {
|
||
report(
|
||
"order.preflight.lifecycle_blocked",
|
||
"订单已进入不可发起采购的终态",
|
||
"failed",
|
||
);
|
||
return {
|
||
order_id: orderId,
|
||
state: "blocked",
|
||
shop_name: row.shopName,
|
||
order_status: row.blockedLifecycleStatus,
|
||
message: "订单已进入不可发起采购的终态",
|
||
};
|
||
}
|
||
if (
|
||
!row.hasDropshipMarker ||
|
||
(row.orderStatus && !row.orderStatus.includes("异常"))
|
||
) {
|
||
report(
|
||
"order.preflight.status_blocked",
|
||
"订单状态不符合异常1688一件代发",
|
||
"failed",
|
||
);
|
||
return {
|
||
order_id: orderId,
|
||
state: "blocked",
|
||
shop_name: row.shopName,
|
||
order_status: row.orderStatus || "状态未识别",
|
||
message: row.orderStatus
|
||
? `ERP状态不符合执行条件:${row.orderStatus}`
|
||
: "未读取到订单状态",
|
||
};
|
||
}
|
||
report(
|
||
"order.preflight.ready",
|
||
"订单状态符合异常1688一件代发",
|
||
"completed",
|
||
);
|
||
return {
|
||
order_id: orderId,
|
||
state: "ready",
|
||
shop_name: row.shopName,
|
||
order_status: row.orderStatus || "1688一件代发",
|
||
purchase_order_no: "",
|
||
current_address: clean(detail.receiverAddress, 120),
|
||
};
|
||
}
|
||
|
||
async function inspectBatch(payload) {
|
||
writeProgress({
|
||
phase: "inspect",
|
||
step_code: "auth.check.started",
|
||
step_state: "started",
|
||
message: "正在检查 ERP 登录状态",
|
||
});
|
||
const auth = await ensureLoggedIn(
|
||
clean(payload.otp, 12),
|
||
Boolean(payload.resend_sms),
|
||
);
|
||
if (auth.status !== "ok") {
|
||
writeProgress({
|
||
phase: "inspect",
|
||
step_code: "auth.check.waiting",
|
||
step_state: "waiting",
|
||
message: "ERP 登录等待短信验证码",
|
||
});
|
||
return auth;
|
||
}
|
||
writeProgress({
|
||
phase: "inspect",
|
||
step_code: "auth.check.completed",
|
||
step_state: "completed",
|
||
message: "ERP 登录状态正常",
|
||
});
|
||
writeProgress({
|
||
phase: "inspect",
|
||
step_code: "order.list.open.started",
|
||
step_state: "started",
|
||
message: "正在打开订单列表",
|
||
});
|
||
const orderFrame = await openOrderList();
|
||
writeProgress({
|
||
phase: "inspect",
|
||
step_code: "order.list.open.completed",
|
||
step_state: "completed",
|
||
message: "订单列表已就绪",
|
||
});
|
||
const orders = [];
|
||
const inputOrders = payload.orders || [];
|
||
for (let index = 0; index < inputOrders.length; index += 1) {
|
||
const order = inputOrders[index];
|
||
const orderContext = {
|
||
purchase_order_no: "",
|
||
online_1688_order_no: "",
|
||
external_state_may_have_changed: false,
|
||
submission_may_have_started: false,
|
||
last_step_code: "",
|
||
};
|
||
const report = (stepCode, message, stepState = "started", extra = {}) => {
|
||
if (extra.purchase_order_no) {
|
||
orderContext.purchase_order_no = clean(
|
||
extra.purchase_order_no,
|
||
60,
|
||
);
|
||
}
|
||
if (extra.online_1688_order_no) {
|
||
orderContext.online_1688_order_no = clean(
|
||
extra.online_1688_order_no,
|
||
80,
|
||
);
|
||
}
|
||
writeProgress({
|
||
phase: "inspect",
|
||
completed: index,
|
||
total: inputOrders.length,
|
||
order_index: index + 1,
|
||
order_total: inputOrders.length,
|
||
current_order_id: clean(order.order_id, 40),
|
||
step_code: stepCode,
|
||
step_state: stepState,
|
||
message,
|
||
purchase_order_no: orderContext.purchase_order_no,
|
||
online_1688_order_no: orderContext.online_1688_order_no,
|
||
...extra,
|
||
});
|
||
};
|
||
report("order.preflight.started", "开始核对当前订单", "started");
|
||
const result = await inspectSingleOrder(orderFrame, order, report);
|
||
orders.push(result);
|
||
writeProgress({
|
||
phase: "inspect",
|
||
completed: index + 1,
|
||
total: inputOrders.length,
|
||
order_index: index + 1,
|
||
order_total: inputOrders.length,
|
||
current_order_id: clean(order.order_id, 40),
|
||
step_code: "order.preflight.completed",
|
||
step_state:
|
||
result.state === "ready" || result.state === "resume_purchase"
|
||
? "completed"
|
||
: "skipped",
|
||
message:
|
||
result.state === "ready" || result.state === "resume_purchase"
|
||
? "当前订单预检通过"
|
||
: "当前订单预检结束,执行阶段将跳过",
|
||
purchase_order_no: clean(result.purchase_order_no, 60),
|
||
online_1688_order_no: clean(result.online_1688_order_no, 80),
|
||
});
|
||
}
|
||
return { status: "ok", orders };
|
||
}
|
||
|
||
async function fillAndSaveAddress(detailFrame, order, report = () => {}) {
|
||
const paste = await waitForOneVisible(
|
||
detailFrame.locator("#input_receiver_address"),
|
||
"address paste control",
|
||
);
|
||
const save = await waitForOneVisible(
|
||
detailFrame.locator("#save_btn"),
|
||
"address save control",
|
||
);
|
||
const expectedRecipient = erpRecipientText(order.recipient);
|
||
const composite = `${expectedRecipient} ${clean(
|
||
order.phone,
|
||
20,
|
||
)} ${clean(order.address, 300)}`;
|
||
report("address.paste.started", "正在粘贴收货资料", "started");
|
||
await paste.fill(composite);
|
||
report("address.paste.completed", "收货资料已粘贴", "completed");
|
||
report("address.parse.started", "正在识别填充收货地址", "started");
|
||
await clickExactText(detailFrame, "识别填充", "address parse");
|
||
const recipientControl = await waitForOneVisible(
|
||
detailFrame.locator("#receiver_name"),
|
||
"recipient control",
|
||
);
|
||
const addressControl = await waitForOneVisible(
|
||
detailFrame.locator("#receiver_address"),
|
||
"address control",
|
||
);
|
||
const stateControl = await waitForOneVisible(
|
||
detailFrame.locator("#receiver_state"),
|
||
"state control",
|
||
);
|
||
const cityControl = await waitForOneVisible(
|
||
detailFrame.locator("#receiver_city"),
|
||
"city control",
|
||
);
|
||
const districtControl = await waitForOneVisible(
|
||
detailFrame.locator("#receiver_district"),
|
||
"district control",
|
||
);
|
||
let phoneControl = null;
|
||
for (const selector of ["#receiver_mobile", "#receiver_phone"]) {
|
||
const controls = await firstVisible(detailFrame.locator(selector));
|
||
if (controls.length > 1) {
|
||
throw new Error("Phone control is ambiguous");
|
||
}
|
||
if (controls.length === 1) {
|
||
phoneControl = controls[0];
|
||
break;
|
||
}
|
||
}
|
||
const expectedPhoneDigits = normalizedPhoneDigits(clean(order.phone, 20));
|
||
const expectedPrivacySuffix = privacyPhoneSuffix(order.phone);
|
||
const deadline = Date.now() + 5000;
|
||
let verificationIssues = [];
|
||
let verifiedAddressState = null;
|
||
do {
|
||
const recipient = clean(await recipientControl.inputValue(), 30);
|
||
const parsedPhone = phoneControl
|
||
? clean(await phoneControl.inputValue(), 30)
|
||
: "";
|
||
let address = clean(await addressControl.inputValue(), 300);
|
||
const state = clean(await stateControl.inputValue(), 60);
|
||
const city = clean(await cityControl.inputValue(), 60);
|
||
const district = clean(await districtControl.inputValue(), 60);
|
||
if (
|
||
expectedPrivacySuffix &&
|
||
!normalizedContactText(address).includes(expectedPrivacySuffix)
|
||
) {
|
||
await addressControl.fill(`${withoutPrivacySuffix(address)}[${expectedPrivacySuffix}]`);
|
||
address = clean(await addressControl.inputValue(), 300);
|
||
}
|
||
verificationIssues = [];
|
||
verifiedAddressState = {
|
||
recipient,
|
||
phone: parsedPhone,
|
||
address: `${state}${city}${district}${address}`,
|
||
administrativeParts: [state, city, district],
|
||
};
|
||
if (
|
||
normalizedRecipientText(recipient) !==
|
||
normalizedRecipientText(expectedRecipient)
|
||
) {
|
||
verificationIssues.push("recipient");
|
||
}
|
||
if (normalizedPhoneDigits(parsedPhone) !== expectedPhoneDigits) {
|
||
verificationIssues.push("phone");
|
||
}
|
||
if (
|
||
expectedPrivacySuffix &&
|
||
!normalizedContactText(address).includes(expectedPrivacySuffix)
|
||
) {
|
||
verificationIssues.push("privacy_suffix");
|
||
}
|
||
if (!address) verificationIssues.push("address");
|
||
if (!state) verificationIssues.push("state");
|
||
if (!city) verificationIssues.push("city");
|
||
if (!district) verificationIssues.push("district");
|
||
if (
|
||
!addressTextMatches(
|
||
order.address,
|
||
`${state}${city}${district}${address}`,
|
||
[state, city, district],
|
||
)
|
||
) {
|
||
verificationIssues.push("full_address");
|
||
}
|
||
if (!verificationIssues.length) break;
|
||
await detailFrame.waitForTimeout(250);
|
||
} while (Date.now() < deadline);
|
||
if (
|
||
verificationIssues.length === 1 &&
|
||
verificationIssues[0] === "recipient"
|
||
) {
|
||
report(
|
||
"address.recipient.restore.started",
|
||
"ERP识别调整了收件人,正在按原始资料恢复",
|
||
"started",
|
||
);
|
||
const restoredRecipient = withoutPrivacySuffix(expectedRecipient);
|
||
await recipientControl.fill(restoredRecipient);
|
||
if (
|
||
normalizedRecipientText(await recipientControl.inputValue()) ===
|
||
normalizedRecipientText(restoredRecipient)
|
||
) {
|
||
verificationIssues = [];
|
||
if (verifiedAddressState) {
|
||
verifiedAddressState.recipient = restoredRecipient;
|
||
}
|
||
report(
|
||
"address.recipient.restore.completed",
|
||
"收件人已按原始资料恢复并复核通过",
|
||
"completed",
|
||
);
|
||
}
|
||
}
|
||
if (verificationIssues.length) {
|
||
throw safeOrderFailure(
|
||
`Address parse verification failed fields=${verificationIssues.join(",")}`,
|
||
);
|
||
}
|
||
order.__verifiedDelivery = verifiedAddressState;
|
||
report("address.parse.completed", "收货地址识别填充完成", "completed");
|
||
report("address.verify.completed", "收件人、手机号和地区字段校验通过", "completed");
|
||
report("address.save.started", "正在保存订单基本及收货地址信息", "started");
|
||
await save.click();
|
||
await detailFrame.waitForTimeout(800);
|
||
report("address.save.submitted", "收货地址保存已提交,等待回读复核", "completed");
|
||
}
|
||
|
||
async function readOrderAddressForm(detailFrame) {
|
||
const recipientControl = await waitForOneVisible(
|
||
detailFrame.locator("#receiver_name"),
|
||
"recipient control",
|
||
);
|
||
const addressControl = await waitForOneVisible(
|
||
detailFrame.locator("#receiver_address"),
|
||
"address control",
|
||
);
|
||
const stateControl = await waitForOneVisible(
|
||
detailFrame.locator("#receiver_state"),
|
||
"state control",
|
||
);
|
||
const cityControl = await waitForOneVisible(
|
||
detailFrame.locator("#receiver_city"),
|
||
"city control",
|
||
);
|
||
const districtControl = await waitForOneVisible(
|
||
detailFrame.locator("#receiver_district"),
|
||
"district control",
|
||
);
|
||
const phoneCandidates = [];
|
||
for (const selector of ["#receiver_mobile", "#receiver_phone"]) {
|
||
const controls = await firstVisible(detailFrame.locator(selector));
|
||
if (controls.length > 1) throw new Error("Phone control is ambiguous");
|
||
if (controls.length === 1) {
|
||
phoneCandidates.push({
|
||
selector,
|
||
value: clean(await controls[0].inputValue(), 30),
|
||
});
|
||
}
|
||
}
|
||
const populatedPhones = phoneCandidates.filter((item) => item.value);
|
||
const distinctPhones = new Set(
|
||
populatedPhones.map((item) => normalizedPhoneDigits(item.value)),
|
||
);
|
||
if (distinctPhones.size > 1) {
|
||
throw new Error("Visible phone controls contain conflicting values");
|
||
}
|
||
const selectedPhone = populatedPhones[0] || phoneCandidates[0] || null;
|
||
return {
|
||
recipient: clean(await recipientControl.inputValue(), 30),
|
||
phone: selectedPhone ? selectedPhone.value : "",
|
||
phoneControlsPresent: phoneCandidates.length,
|
||
phoneControlsAllEmpty: phoneCandidates.every((item) => !item.value),
|
||
phoneControlStates: phoneCandidates.map(
|
||
(item) => `${item.selector}:${phoneValueDiagnostic(item.value)}`,
|
||
),
|
||
address: clean(await addressControl.inputValue(), 300),
|
||
state: clean(await stateControl.inputValue(), 60),
|
||
city: clean(await cityControl.inputValue(), 60),
|
||
district: clean(await districtControl.inputValue(), 60),
|
||
};
|
||
}
|
||
|
||
function administrativePartMatchesAddress(address, part) {
|
||
const source = normalizedContactText(address);
|
||
const normalized = normalizedContactText(part);
|
||
if (!source || !normalized) return false;
|
||
const alias = normalized.replace(/(?:省|市|区|县)$/u, "");
|
||
return source.includes(normalized) || (alias.length >= 2 && source.includes(alias));
|
||
}
|
||
|
||
function persistedPhoneIsPermissionHidden(order, actual) {
|
||
const expectedPhone = normalizedPhoneDigits(order.phone);
|
||
const verifiedPhone = normalizedPhoneDigits(order.__verifiedDelivery?.phone);
|
||
return Boolean(
|
||
expectedPhone
|
||
&& verifiedPhone === expectedPhone
|
||
&& !String(actual.phone || "").trim()
|
||
&& Number(actual.phoneControlsPresent || 0) > 0
|
||
&& actual.phoneControlsAllEmpty === true
|
||
);
|
||
}
|
||
|
||
function orderAddressFormVerificationIssues(order, actual) {
|
||
const expectedPhone = normalizedPhoneDigits(order.phone);
|
||
const expectedPrivacySuffix = privacyPhoneSuffix(order.phone);
|
||
const expectedRecipient = String(
|
||
order.__verifiedDelivery?.recipient || erpRecipientText(order.recipient),
|
||
).trim().slice(0, 30);
|
||
const issues = [];
|
||
const recipientMatches = isMaskedRecipientValue(actual.recipient)
|
||
? maskedRecipientMatchesExpected(actual.recipient, expectedRecipient)
|
||
: normalizedRecipientText(actual.recipient) ===
|
||
normalizedRecipientText(expectedRecipient);
|
||
if (!recipientMatches) {
|
||
issues.push("recipient");
|
||
}
|
||
if (
|
||
!isMaskedContactValue(actual.phone) &&
|
||
normalizedPhoneDigits(actual.phone) !== expectedPhone
|
||
) {
|
||
issues.push("phone");
|
||
}
|
||
if (
|
||
expectedPrivacySuffix &&
|
||
!isMaskedContactValue(actual.address) &&
|
||
!normalizedContactText(actual.address).includes(expectedPrivacySuffix) &&
|
||
!normalizedContactText(actual.recipient).includes(expectedPrivacySuffix)
|
||
) {
|
||
issues.push("privacy_suffix");
|
||
}
|
||
for (const [name, value] of [
|
||
["state", actual.state],
|
||
["city", actual.city],
|
||
["district", actual.district],
|
||
]) {
|
||
if (!value || !administrativePartMatchesAddress(order.address, value)) {
|
||
issues.push(name);
|
||
}
|
||
}
|
||
if (!actual.address) {
|
||
issues.push("address");
|
||
} else if (
|
||
!isMaskedContactValue(actual.address) &&
|
||
!addressTextMatches(
|
||
order.address,
|
||
`${actual.state}${actual.city}${actual.district}${actual.address}`,
|
||
[actual.state, actual.city, actual.district],
|
||
)
|
||
) {
|
||
issues.push("full_address");
|
||
}
|
||
return issues;
|
||
}
|
||
|
||
async function verifyPersistedOrderAddress(orderFrame, order, report = () => {}) {
|
||
report(
|
||
"address.persisted.verify.started",
|
||
"正在重新打开订单回读收货信息",
|
||
"started",
|
||
);
|
||
const orderLocator = await searchOrder(orderFrame, order.order_id);
|
||
if (!orderLocator) throw safeOrderFailure("Saved address verification failed");
|
||
const detailFrame = await openOrderDetail(
|
||
orderFrame,
|
||
orderLocator,
|
||
order.order_id,
|
||
);
|
||
const deadline = Date.now() + 5000;
|
||
let persisted = null;
|
||
let verificationIssues = [];
|
||
do {
|
||
persisted = await readOrderAddressForm(detailFrame);
|
||
verificationIssues = orderAddressFormVerificationIssues(order, persisted);
|
||
if (!verificationIssues.length) break;
|
||
await detailFrame.waitForTimeout(250);
|
||
} while (Date.now() < deadline);
|
||
if (
|
||
verificationIssues.includes("phone")
|
||
&& persistedPhoneIsPermissionHidden(order, persisted)
|
||
) {
|
||
verificationIssues = verificationIssues.filter((issue) => issue !== "phone");
|
||
report(
|
||
"address.persisted.phone.hidden",
|
||
"ERP重新打开后按权限隐藏手机号,已沿用保存前核验结果",
|
||
"completed",
|
||
);
|
||
}
|
||
if (verificationIssues.length) {
|
||
const phoneDiagnostic = verificationIssues.includes("phone")
|
||
? ` expected=${phoneValueDiagnostic(order.phone)}`
|
||
+ ` actual=${phoneValueDiagnostic(persisted?.phone)}`
|
||
+ ` controls=${(persisted?.phoneControlStates || []).join(";") || "none"}`
|
||
: "";
|
||
throw safeOrderFailure(
|
||
`Saved address verification failed fields=${verificationIssues.join(",")}`
|
||
+ phoneDiagnostic,
|
||
);
|
||
}
|
||
const previous = order.__verifiedDelivery || {};
|
||
order.__verifiedDelivery = {
|
||
recipient: isMaskedRecipientValue(persisted.recipient)
|
||
? previous.recipient
|
||
: persisted.recipient,
|
||
phone: !persisted.phone || isMaskedContactValue(persisted.phone)
|
||
? previous.phone
|
||
: persisted.phone,
|
||
address: isMaskedContactValue(persisted.address)
|
||
? previous.address
|
||
: `${persisted.state}${persisted.city}${persisted.district}${persisted.address}`,
|
||
administrativeParts: [
|
||
persisted.state,
|
||
persisted.city,
|
||
persisted.district,
|
||
],
|
||
};
|
||
report(
|
||
"address.persisted.verify.completed",
|
||
"ERP保存后的收货信息与上传资料一致",
|
||
"completed",
|
||
);
|
||
await closeOrderDetail(detailFrame);
|
||
report("address.save.completed", "收货地址保存并回读复核成功", "completed");
|
||
}
|
||
|
||
async function rowCheckbox(orderLocator) {
|
||
const checkbox = orderLocator.locator(
|
||
"xpath=ancestor::*[.//input[@type='checkbox']][1]//input[@type='checkbox'][1]",
|
||
);
|
||
if ((await checkbox.count()) !== 1) {
|
||
throw new Error("Order row checkbox is ambiguous");
|
||
}
|
||
return checkbox;
|
||
}
|
||
|
||
async function generatePurchaseOrder(
|
||
orderFrame,
|
||
orderLocator,
|
||
report = () => {},
|
||
) {
|
||
report("purchase.order.select.started", "正在勾选订单", "started");
|
||
const checkbox = await rowCheckbox(orderLocator);
|
||
if (!(await checkbox.isChecked())) await checkbox.check();
|
||
report("purchase.order.select.completed", "订单已勾选", "completed");
|
||
await dismissBlockingTips(orderFrame);
|
||
const additional = orderFrame.locator("#__AppendTool_Btn");
|
||
const visibleAdditional = await oneVisible(additional, "additional menu");
|
||
report("purchase.additional.open.started", "正在打开附加菜单", "started");
|
||
await visibleAdditional.click();
|
||
report("purchase.additional.open.completed", "附加菜单已打开", "completed");
|
||
await dismissBlockingTips(orderFrame);
|
||
report(
|
||
"purchase.generate.menu.started",
|
||
"正在选择按订单生成采购单",
|
||
"started",
|
||
);
|
||
await clickExactText(orderFrame, "按订单生成采购单", "generate purchase");
|
||
report(
|
||
"purchase.preview.open.started",
|
||
"正在打开采购单预览",
|
||
"started",
|
||
);
|
||
const previewFrame = await waitForPurchasePreviewFrame();
|
||
report(
|
||
"purchase.preview.open.completed",
|
||
"采购单预览已打开",
|
||
"completed",
|
||
);
|
||
report(
|
||
"purchase.plaintext.enable.started",
|
||
"正在开启采购单明文",
|
||
"started",
|
||
);
|
||
await clickExactText(previewFrame, "明文", "plaintext toggle");
|
||
report(
|
||
"purchase.plaintext.enable.completed",
|
||
"采购单明文已开启",
|
||
"completed",
|
||
);
|
||
report(
|
||
"purchase.generate.submit.started",
|
||
"正在提交生成采购单",
|
||
"started",
|
||
);
|
||
await clickExactText(
|
||
previewFrame,
|
||
"生成采购单",
|
||
"generate purchase confirmation",
|
||
);
|
||
const success = await waitForVisibleTextAcrossFrames(
|
||
/订单生成采购单成功|生成采购单成功/,
|
||
"purchase success",
|
||
);
|
||
report(
|
||
"purchase.generate.success",
|
||
"系统提示采购单生成成功",
|
||
"completed",
|
||
);
|
||
const confirmButtons = success.frame.getByText(/^(确定|确认)$/);
|
||
const visible = await firstVisible(confirmButtons);
|
||
if (visible.length === 1) await visible[0].click();
|
||
await closeVisibleEpaasDialogs();
|
||
report(
|
||
"purchase.generate.dialog.closed",
|
||
"采购单成功提示已确认并关闭",
|
||
"completed",
|
||
);
|
||
}
|
||
|
||
async function debugAdditionalControls(payload) {
|
||
const auth = await ensureLoggedIn();
|
||
if (auth.status !== "ok") return auth;
|
||
const orderFrame = await openOrderList();
|
||
const orderId = clean(payload.order_id, 40);
|
||
const orderLocator = await searchOrder(orderFrame, orderId);
|
||
const checkbox = await rowCheckbox(orderLocator);
|
||
if (!(await checkbox.isChecked())) await checkbox.check();
|
||
const candidates = orderFrame.getByText("附加", { exact: true });
|
||
const diagnostics = [];
|
||
for (const item of await firstVisible(candidates)) {
|
||
diagnostics.push(
|
||
await item.evaluate((element) => {
|
||
const ancestors = [];
|
||
let node = element;
|
||
for (let depth = 0; node && depth < 5; depth += 1) {
|
||
ancestors.push({
|
||
tag: node.tagName,
|
||
id: node.id || "",
|
||
className:
|
||
typeof node.className === "string" ? node.className : "",
|
||
role: node.getAttribute("role") || "",
|
||
title: node.getAttribute("title") || "",
|
||
text: (node.innerText || node.textContent || "")
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.slice(0, 180),
|
||
});
|
||
node = node.parentElement;
|
||
}
|
||
return { ancestors };
|
||
}),
|
||
);
|
||
}
|
||
const additional = await oneVisible(
|
||
orderFrame.locator("#__AppendTool_Btn"),
|
||
"additional menu",
|
||
);
|
||
await dismissBlockingTips(orderFrame);
|
||
await additional.click();
|
||
await orderFrame.waitForTimeout(300);
|
||
await dismissBlockingTips(orderFrame);
|
||
const purchaseCandidates = orderFrame.getByText("按订单生成采购单", {
|
||
exact: true,
|
||
});
|
||
const purchaseDiagnostics = [];
|
||
for (const item of await firstVisible(purchaseCandidates)) {
|
||
purchaseDiagnostics.push(
|
||
await item.evaluate((element) => {
|
||
const ancestors = [];
|
||
let node = element;
|
||
for (let depth = 0; node && depth < 5; depth += 1) {
|
||
ancestors.push({
|
||
tag: node.tagName,
|
||
id: node.id || "",
|
||
className:
|
||
typeof node.className === "string" ? node.className : "",
|
||
role: node.getAttribute("role") || "",
|
||
title: node.getAttribute("title") || "",
|
||
text: (node.innerText || node.textContent || "")
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.slice(0, 180),
|
||
});
|
||
node = node.parentElement;
|
||
}
|
||
return { ancestors };
|
||
}),
|
||
);
|
||
}
|
||
const purchaseAction = await oneVisible(
|
||
purchaseCandidates,
|
||
"generate purchase",
|
||
);
|
||
await purchaseAction.click();
|
||
await orderFrame.waitForTimeout(3000);
|
||
const previewDiagnostics = {};
|
||
for (const text of ["明文", "生成采购单"]) {
|
||
const items = orderFrame.getByText(text, { exact: true });
|
||
const details = [];
|
||
for (const item of await firstVisible(items)) {
|
||
details.push(
|
||
await item.evaluate((element) => {
|
||
const ancestors = [];
|
||
let node = element;
|
||
for (let depth = 0; node && depth < 5; depth += 1) {
|
||
ancestors.push({
|
||
tag: node.tagName,
|
||
id: node.id || "",
|
||
className:
|
||
typeof node.className === "string" ? node.className : "",
|
||
role: node.getAttribute("role") || "",
|
||
title: node.getAttribute("title") || "",
|
||
text: (node.innerText || node.textContent || "")
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.slice(0, 180),
|
||
});
|
||
node = node.parentElement;
|
||
}
|
||
return { ancestors };
|
||
}),
|
||
);
|
||
}
|
||
previewDiagnostics[text] = details;
|
||
}
|
||
const previewFrames = [];
|
||
const frameSnapshots = [];
|
||
for (const candidateFrame of page.frames()) {
|
||
const frameResult = {
|
||
url: candidateFrame.url(),
|
||
plaintext_count: 0,
|
||
generate_count: 0,
|
||
};
|
||
frameResult.plaintext_count = (
|
||
await firstVisible(
|
||
candidateFrame.getByText("明文", { exact: true }),
|
||
)
|
||
).length;
|
||
frameResult.generate_count = (
|
||
await firstVisible(
|
||
candidateFrame.getByText("生成采购单", { exact: true }),
|
||
)
|
||
).length;
|
||
if (frameResult.plaintext_count || frameResult.generate_count) {
|
||
previewFrames.push(frameResult);
|
||
}
|
||
frameSnapshots.push({
|
||
url: candidateFrame.url(),
|
||
text: clean(
|
||
await candidateFrame
|
||
.locator("body")
|
||
.innerText()
|
||
.catch(() => ""),
|
||
5000,
|
||
),
|
||
});
|
||
}
|
||
const screenshotPath = `/opt/dalongxia-chat-api/data/${BROWSER_CODE}-purchase-preview.png`;
|
||
await page
|
||
.screenshot({ path: screenshotPath, fullPage: true })
|
||
.catch(() => {});
|
||
return {
|
||
status: "ok",
|
||
order_id: orderId,
|
||
visible_count: diagnostics.length,
|
||
diagnostics,
|
||
purchase_visible_count: purchaseDiagnostics.length,
|
||
purchase_diagnostics: purchaseDiagnostics,
|
||
preview_diagnostics: previewDiagnostics,
|
||
preview_frames: previewFrames,
|
||
frame_snapshots: frameSnapshots,
|
||
screenshot_path: screenshotPath,
|
||
};
|
||
}
|
||
|
||
async function openPurchaseFromDetail(detailFrame, purchaseOrderNo) {
|
||
const link = detailFrame.getByText(purchaseOrderNo, { exact: true });
|
||
const item = await oneVisible(link, "purchase log link");
|
||
const linkDiagnostics = await item.evaluate((element) => ({
|
||
href: element.closest("a")?.href || element.getAttribute("href") || "",
|
||
target:
|
||
element.closest("a")?.target || element.getAttribute("target") || "",
|
||
onclick:
|
||
element.closest("[onclick]")?.getAttribute("onclick") ||
|
||
element.getAttribute("onclick") ||
|
||
"",
|
||
}));
|
||
await item.click();
|
||
const deadline = Date.now() + 8000;
|
||
while (Date.now() < deadline) {
|
||
const frame = await visibleFrameByUrl(
|
||
"/app/scm/purchase/purchasemode.aspx",
|
||
);
|
||
if (frame) return frame;
|
||
await page.waitForTimeout(250);
|
||
}
|
||
|
||
logInternalError(
|
||
new Error(
|
||
"Purchase link did not open the expected frame; falling back to the " +
|
||
`validated purchase page ${JSON.stringify(linkDiagnostics)}`,
|
||
),
|
||
`purchase-navigation-${purchaseOrderNo}`,
|
||
);
|
||
await page.goto(
|
||
"https://www.erp321.com/app/scm/purchase/purchasemode.aspx?_c=jst-epaas&epaas=true",
|
||
{ waitUntil: "domcontentloaded", timeout: 45000 },
|
||
);
|
||
await page.waitForSelector("#_jt", {
|
||
state: "visible",
|
||
timeout: 30000,
|
||
});
|
||
const purchaseFrame = page.mainFrame();
|
||
const purchaseNumberInput = purchaseFrame.locator("#po_id");
|
||
await purchaseNumberInput.fill(purchaseOrderNo);
|
||
const searchButton = await oneVisible(
|
||
purchaseFrame.locator("a.search").filter({ hasText: "搜索" }),
|
||
"purchase search button",
|
||
);
|
||
await searchButton.click();
|
||
await waitForOneVisible(
|
||
purchaseFrame.getByText(purchaseOrderNo, { exact: true }),
|
||
"purchase row after direct navigation",
|
||
);
|
||
return purchaseFrame;
|
||
}
|
||
|
||
async function purchaseRowLocator(purchaseFrame, purchaseOrderNo) {
|
||
const purchase = await waitForOneVisible(
|
||
purchaseFrame.getByText(purchaseOrderNo, { exact: true }),
|
||
"purchase row",
|
||
);
|
||
const directRow = purchase.locator(
|
||
"xpath=ancestor-or-self::*[contains(concat(' ', normalize-space(@class), ' '), ' _jt_row ')][1]",
|
||
);
|
||
if ((await directRow.count()) === 1) return directRow;
|
||
|
||
const rows = purchaseFrame.locator("._jt_row");
|
||
const matchingRows = rows.filter({ hasText: purchaseOrderNo });
|
||
const visibleMatchingRows = await firstVisible(matchingRows);
|
||
if (visibleMatchingRows.length !== 1) {
|
||
throw new Error(
|
||
`purchase row container count is not one direct=${await directRow.count()}`
|
||
+ ` fallback=${visibleMatchingRows.length} visible=${await rows.count()}`,
|
||
);
|
||
}
|
||
return visibleMatchingRows[0];
|
||
}
|
||
|
||
async function readPurchaseRow(purchaseFrame, purchaseOrderNo) {
|
||
const purchase = await purchaseRowLocator(
|
||
purchaseFrame,
|
||
purchaseOrderNo,
|
||
);
|
||
return clean(
|
||
await purchase.evaluate((row) => {
|
||
return (row.innerText || row.textContent || "")
|
||
.replace(/\s+/g, " ")
|
||
.trim();
|
||
}),
|
||
5000,
|
||
);
|
||
}
|
||
|
||
function verified1688Order(rowBody) {
|
||
const onlineOrderNo =
|
||
String(rowBody || "").match(/\b\d{16,30}\b/)?.[0] || "";
|
||
const stateVerified =
|
||
/等待买家付款|等待卖家发货|等待供应商发货|下单成功/.test(
|
||
String(rowBody || ""),
|
||
);
|
||
return {
|
||
onlineOrderNo,
|
||
verified: Boolean(onlineOrderNo && stateVerified),
|
||
};
|
||
}
|
||
|
||
async function inspectPurchaseForOrder(payload) {
|
||
const auth = await ensureLoggedIn();
|
||
if (auth.status !== "ok") return auth;
|
||
const orderFrame = await openOrderList();
|
||
const orderId = clean(payload.order_id, 40);
|
||
const orderLocator = await searchOrder(orderFrame, orderId);
|
||
if (!orderLocator) throw new Error("Inspection order was not found");
|
||
const detailFrame = await openOrderDetail(
|
||
orderFrame,
|
||
orderLocator,
|
||
orderId,
|
||
);
|
||
const detail = await waitForOrderDetailPurchase(
|
||
detailFrame,
|
||
"",
|
||
12000,
|
||
);
|
||
const purchaseOrderNo = clean(detail.purchaseOrderNo, 40);
|
||
if (!purchaseOrderNo) {
|
||
return { status: "ok", order_id: orderId, purchase_order_no: "" };
|
||
}
|
||
if (payload.detail_only === true) {
|
||
return {
|
||
status: "ok",
|
||
order_id: orderId,
|
||
purchase_order_no: purchaseOrderNo,
|
||
detail_log_text: clean(detail.logText, 15000),
|
||
};
|
||
}
|
||
const purchaseFrame = await openPurchaseFromDetail(
|
||
detailFrame,
|
||
purchaseOrderNo,
|
||
);
|
||
const row = await readPurchaseRow(purchaseFrame, purchaseOrderNo);
|
||
return {
|
||
status: "ok",
|
||
order_id: orderId,
|
||
purchase_order_no: purchaseOrderNo,
|
||
online_1688_order_no: row.match(/\b\d{16,30}\b/)?.[0] || "",
|
||
row,
|
||
};
|
||
}
|
||
|
||
async function describeAuditTrigger(purchaseFrame) {
|
||
const exactMatches = await firstVisible(
|
||
purchaseFrame.getByText("审核生效", { exact: true }),
|
||
);
|
||
const diagnostics = [];
|
||
for (const item of exactMatches) {
|
||
diagnostics.push(
|
||
await item.evaluate((element) => {
|
||
const describe = (node) => {
|
||
const attribute = (name) =>
|
||
typeof node.getAttribute === "function"
|
||
? node.getAttribute(name) || ""
|
||
: "";
|
||
const rect =
|
||
typeof node.getBoundingClientRect === "function"
|
||
? node.getBoundingClientRect()
|
||
: { x: 0, y: 0, width: 0, height: 0 };
|
||
const jquery = window.jQuery || window.$;
|
||
const jqueryEvents =
|
||
jquery && typeof jquery._data === "function"
|
||
? Object.entries(jquery._data(node, "events") || {}).flatMap(
|
||
([type, handlers]) =>
|
||
Array.from(handlers || []).slice(0, 20).map((handler) => ({
|
||
type,
|
||
selector: handler.selector || "",
|
||
namespace: handler.namespace || "",
|
||
source: String(handler.handler || handler)
|
||
.replace(/\s+/g, " ")
|
||
.slice(0, 500),
|
||
})),
|
||
)
|
||
: [];
|
||
return {
|
||
tag: node.tagName || node.nodeName || "",
|
||
id: node.id || "",
|
||
className:
|
||
typeof node.className === "string"
|
||
? node.className.slice(0, 240)
|
||
: "",
|
||
role: attribute("role"),
|
||
title: attribute("title"),
|
||
onclick: attribute("onclick").slice(0, 500),
|
||
hasOnclickProperty: typeof node.onclick === "function",
|
||
onclickProperty: String(node.onclick || "")
|
||
.replace(/\s+/g, " ")
|
||
.slice(0, 500),
|
||
jqueryEvents,
|
||
text: (node.innerText || node.textContent || "")
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.slice(0, 240),
|
||
rect: {
|
||
x: Math.round(rect.x),
|
||
y: Math.round(rect.y),
|
||
width: Math.round(rect.width),
|
||
height: Math.round(rect.height),
|
||
},
|
||
};
|
||
};
|
||
const ancestors = [];
|
||
let node = element;
|
||
for (let depth = 0; node && depth < 8; depth += 1) {
|
||
ancestors.push(describe(node));
|
||
node = node.parentElement;
|
||
}
|
||
const descendants = Array.from(
|
||
element.parentElement?.querySelectorAll(
|
||
"button,a,[role='button'],[onclick]",
|
||
) || [],
|
||
)
|
||
.slice(0, 30)
|
||
.map(describe);
|
||
const delegatedEvents = [document, document.body]
|
||
.flatMap((node) => describe(node).jqueryEvents)
|
||
.filter((event) =>
|
||
/ding|OPTM_Tool_Confirm|optimize-filter/i.test(
|
||
`${event.selector} ${event.source}`,
|
||
),
|
||
)
|
||
.slice(0, 50);
|
||
const checkedRows = Array.from(
|
||
document.querySelectorAll(
|
||
"._jt_row input[type='checkbox']:checked",
|
||
),
|
||
).map((checkbox) => {
|
||
const row = checkbox.closest("._jt_row");
|
||
return {
|
||
rowText: (row?.innerText || row?.textContent || "")
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.slice(0, 240),
|
||
jqueryEvents: describe(checkbox).jqueryEvents,
|
||
};
|
||
});
|
||
const tableApi = window.jTable || null;
|
||
const tableApiKeys = tableApi
|
||
? Object.keys(tableApi)
|
||
.filter((key) => /select|check|row/i.test(key))
|
||
.slice(0, 100)
|
||
: [];
|
||
const visibleOverlays = Array.from(
|
||
document.body.querySelectorAll("*"),
|
||
)
|
||
.filter((node) => {
|
||
const style = window.getComputedStyle(node);
|
||
const rect = node.getBoundingClientRect();
|
||
const text = (node.innerText || node.textContent || "").trim();
|
||
return (
|
||
rect.width > 0 &&
|
||
rect.height > 0 &&
|
||
style.display !== "none" &&
|
||
style.visibility !== "hidden" &&
|
||
["fixed", "absolute"].includes(style.position) &&
|
||
Number.parseInt(style.zIndex || "0", 10) >= 100 &&
|
||
/审核|采购|选择|失败|错误|权限|确认|预警/.test(text)
|
||
);
|
||
})
|
||
.slice(0, 30)
|
||
.map((node) => ({
|
||
tag: node.tagName,
|
||
id: node.id || "",
|
||
className:
|
||
typeof node.className === "string"
|
||
? node.className.slice(0, 180)
|
||
: "",
|
||
text: (node.innerText || node.textContent || "")
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.slice(0, 500),
|
||
}));
|
||
const runtimeSources = {};
|
||
for (const name of [
|
||
"GetCurrentJTableRow",
|
||
"GetPoIdArray",
|
||
"ConfirmCfm",
|
||
]) {
|
||
runtimeSources[name] = String(window[name] || "")
|
||
.replace(/\s+/g, " ")
|
||
.slice(0, 8000);
|
||
}
|
||
return {
|
||
ancestors,
|
||
descendants,
|
||
delegatedEvents,
|
||
confirmV2Source: String(window.ConfirmV2 || "")
|
||
.replace(/\s+/g, " ")
|
||
.slice(0, 8000),
|
||
tableApiKeys,
|
||
checkedRows,
|
||
visibleOverlays,
|
||
runtimeSources,
|
||
parentHtml: (element.parentElement?.outerHTML || "").slice(0, 3000),
|
||
};
|
||
}),
|
||
);
|
||
}
|
||
return diagnostics;
|
||
}
|
||
|
||
async function inspectAuditForOrder(payload) {
|
||
const auth = await ensureLoggedIn();
|
||
if (auth.status !== "ok") return auth;
|
||
const orderFrame = await openOrderList();
|
||
const orderId = clean(payload.order_id, 40);
|
||
const orderLocator = await searchOrder(orderFrame, orderId);
|
||
if (!orderLocator) throw new Error("Inspection order was not found");
|
||
const detailFrame = await openOrderDetail(
|
||
orderFrame,
|
||
orderLocator,
|
||
orderId,
|
||
);
|
||
const detail = await waitForOrderDetailPurchase(detailFrame, "", 12000);
|
||
const purchaseOrderNo = clean(detail.purchaseOrderNo, 40);
|
||
if (!purchaseOrderNo) {
|
||
return { status: "ok", order_id: orderId, purchase_order_no: "" };
|
||
}
|
||
const purchaseFrame = await openPurchaseFromDetail(
|
||
detailFrame,
|
||
purchaseOrderNo,
|
||
);
|
||
const row = await readPurchaseRow(purchaseFrame, purchaseOrderNo);
|
||
return {
|
||
status: "ok",
|
||
order_id: orderId,
|
||
purchase_order_no: purchaseOrderNo,
|
||
audit_completed: purchaseAuditCompleted(row),
|
||
online_1688_order_no: row.match(/\b\d{16,30}\b/)?.[0] || "",
|
||
audit_diagnostics: await describeAuditTrigger(purchaseFrame),
|
||
};
|
||
}
|
||
|
||
async function inspectAuditForPurchase(payload) {
|
||
const auth = await ensureLoggedIn();
|
||
if (auth.status !== "ok") return auth;
|
||
const purchaseOrderNo = clean(payload.purchase_order_no, 40);
|
||
if (!purchaseOrderNo) {
|
||
throw new Error("Purchase number is required for inspection");
|
||
}
|
||
await page.goto(
|
||
"https://www.erp321.com/app/scm/purchase/purchasemode.aspx?_c=jst-epaas&epaas=true",
|
||
{ waitUntil: "domcontentloaded", timeout: 45000 },
|
||
);
|
||
await page.waitForSelector("#_jt", {
|
||
state: "visible",
|
||
timeout: 30000,
|
||
});
|
||
const purchaseFrame = page.mainFrame();
|
||
const purchase = await waitForOneVisible(
|
||
purchaseFrame.getByText(purchaseOrderNo, { exact: true }),
|
||
"purchase row",
|
||
);
|
||
const row = await readPurchaseRow(purchaseFrame, purchaseOrderNo);
|
||
const checkbox = purchase.locator(
|
||
"xpath=ancestor::*[.//input[@type='checkbox']][1]//input[@type='checkbox'][1]",
|
||
);
|
||
if ((await checkbox.count()) === 1) {
|
||
const rowCheckboxes = purchaseFrame.locator(
|
||
"._jt_row input[type='checkbox']",
|
||
);
|
||
const count = await rowCheckboxes.count();
|
||
for (let index = 0; index < count; index += 1) {
|
||
const item = rowCheckboxes.nth(index);
|
||
if (
|
||
(await item.isVisible().catch(() => false)) &&
|
||
(await item.isChecked().catch(() => false))
|
||
) {
|
||
await item.uncheck({ force: true });
|
||
}
|
||
}
|
||
await checkbox.check({ force: true });
|
||
}
|
||
const selectionState = await purchaseFrame.evaluate(() => {
|
||
const current =
|
||
typeof window.GetCurrentJTableRow === "function"
|
||
? window.GetCurrentJTableRow("待审核") || []
|
||
: [];
|
||
const checked =
|
||
window.jTable && typeof window.jTable.GetCheckedRows === "function"
|
||
? window.jTable.GetCheckedRows() || []
|
||
: [];
|
||
const summarize = (rows) =>
|
||
Array.from(rows || []).slice(0, 20).map((item) => {
|
||
const data = item?.Data || item || {};
|
||
return {
|
||
purchaseOrderNo: String(
|
||
data.po_id || data.purchase_order_no || "",
|
||
).slice(0, 60),
|
||
status: String(data.status || data.po_status || "").slice(0, 80),
|
||
};
|
||
});
|
||
return {
|
||
domCheckedCount: document.querySelectorAll(
|
||
"._jt_row input[type='checkbox']:checked",
|
||
).length,
|
||
checkedRows: summarize(checked),
|
||
currentRows: summarize(current),
|
||
};
|
||
});
|
||
return {
|
||
status: "ok",
|
||
purchase_order_no: purchaseOrderNo,
|
||
audit_completed: purchaseAuditCompleted(row),
|
||
online_1688_order_no: row.match(/\b\d{16,30}\b/)?.[0] || "",
|
||
selection_state: selectionState,
|
||
audit_diagnostics: await describeAuditTrigger(purchaseFrame),
|
||
};
|
||
}
|
||
|
||
async function inspect1688Navigation(payload) {
|
||
const auth = await ensureLoggedIn();
|
||
if (auth.status !== "ok") return auth;
|
||
const orderFrame = await openOrderList();
|
||
const orderId = clean(payload.order_id, 40);
|
||
const orderLocator = await searchOrder(orderFrame, orderId);
|
||
if (!orderLocator) throw new Error("Inspection order was not found");
|
||
const detailFrame = await openOrderDetail(
|
||
orderFrame,
|
||
orderLocator,
|
||
orderId,
|
||
);
|
||
const detail = await waitForOrderDetailPurchase(detailFrame, "", 12000);
|
||
const purchaseOrderNo = clean(detail.purchaseOrderNo, 40);
|
||
if (!purchaseOrderNo) {
|
||
return { status: "ok", order_id: orderId, purchase_order_no: "" };
|
||
}
|
||
const purchaseFrame = await openPurchaseFromDetail(
|
||
detailFrame,
|
||
purchaseOrderNo,
|
||
);
|
||
const rowBody = await readPurchaseRow(purchaseFrame, purchaseOrderNo);
|
||
await clearPurchaseResultOverlays(purchaseFrame);
|
||
const trigger = purchaseFrame.locator("#OPTM_Purhcase_Order_1688");
|
||
if ((await trigger.count()) !== 1) {
|
||
throw new Error("1688 toolbar entry is ambiguous");
|
||
}
|
||
await trigger.click();
|
||
await clickExactText(
|
||
purchaseFrame,
|
||
"1688一键下单",
|
||
"1688 one-click order",
|
||
);
|
||
const dropshipMatch = await waitForVisibleLocatorAcrossFrames(
|
||
"#purchaseAddress",
|
||
"1688 dropship option",
|
||
);
|
||
const accountState = await waitFor1688AccountReady(dropshipMatch.frame);
|
||
let priceContrast = null;
|
||
if (payload.check_price_contrast === true) {
|
||
priceContrast = await dropshipMatch.frame.evaluate(
|
||
() =>
|
||
new Promise((resolve) => {
|
||
if (typeof window._AsyncCallPage !== "function") {
|
||
resolve({ error: "async_call_unavailable" });
|
||
return;
|
||
}
|
||
const memberId = window.jQuery?.("#shopInfos").val() || "";
|
||
let settled = false;
|
||
const finish = (value) => {
|
||
if (settled) return;
|
||
settled = true;
|
||
resolve(value);
|
||
};
|
||
const timer = setTimeout(
|
||
() => finish({ error: "price_contrast_timeout" }),
|
||
20000,
|
||
);
|
||
window._AsyncCallPage(
|
||
"PriceContrast",
|
||
(value) => {
|
||
clearTimeout(timer);
|
||
finish({ value });
|
||
},
|
||
true,
|
||
memberId,
|
||
);
|
||
}),
|
||
);
|
||
}
|
||
if (payload.open_confirmation === true) {
|
||
const dropshipVisible = dropshipMatch.item;
|
||
if (!(await dropshipVisible.isChecked())) {
|
||
await dropshipVisible.check();
|
||
}
|
||
await dropshipMatch.frame.locator("#save_btn").click();
|
||
await dropshipMatch.frame.waitForTimeout(1200);
|
||
}
|
||
const controls = await dropshipMatch.frame.evaluate(() => {
|
||
const visible = (element) => {
|
||
const rect = element.getBoundingClientRect();
|
||
const style = window.getComputedStyle(element);
|
||
return (
|
||
rect.width > 0 &&
|
||
rect.height > 0 &&
|
||
style.display !== "none" &&
|
||
style.visibility !== "hidden"
|
||
);
|
||
};
|
||
const eventSummary = (element) => {
|
||
const events =
|
||
window.jQuery && typeof window.jQuery._data === "function"
|
||
? window.jQuery._data(element, "events") || {}
|
||
: {};
|
||
return Object.entries(events).flatMap(([type, handlers]) =>
|
||
Array.from(handlers || []).map((handler) => ({
|
||
type,
|
||
selector: String(handler?.selector || "").slice(0, 120),
|
||
source: String(handler?.handler || "").slice(0, 1000),
|
||
})),
|
||
);
|
||
};
|
||
const summarize = (element) => ({
|
||
tag: element.tagName,
|
||
id: element.id || "",
|
||
className: String(element.className || "").slice(0, 200),
|
||
text: String(element.innerText || element.textContent || "")
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.slice(0, 200),
|
||
disabled: Boolean(element.disabled),
|
||
onclick: String(element.getAttribute("onclick") || "").slice(0, 500),
|
||
events: eventSummary(element),
|
||
html: String(element.outerHTML || "").slice(0, 1200),
|
||
});
|
||
const saveControls = Array.from(
|
||
document.querySelectorAll("button,a,input,span,div"),
|
||
)
|
||
.filter(
|
||
(element) =>
|
||
visible(element) &&
|
||
String(element.innerText || element.value || element.textContent || "")
|
||
.replace(/\s+/g, "")
|
||
.trim() === "保存",
|
||
)
|
||
.slice(0, 20)
|
||
.map((element) => ({
|
||
element: summarize(element),
|
||
ancestors: Array.from(
|
||
(function* ancestors(start) {
|
||
let current = start.parentElement;
|
||
while (current && current !== document.body) {
|
||
yield current;
|
||
current = current.parentElement;
|
||
}
|
||
})(element),
|
||
)
|
||
.slice(0, 5)
|
||
.map(summarize),
|
||
}));
|
||
const formControls = Array.from(
|
||
document.querySelectorAll("input,select,textarea"),
|
||
)
|
||
.filter(visible)
|
||
.slice(0, 80)
|
||
.map((element) => ({
|
||
tag: element.tagName,
|
||
type: element.type || "",
|
||
id: element.id || "",
|
||
name: element.name || "",
|
||
checked: Boolean(element.checked),
|
||
disabled: Boolean(element.disabled),
|
||
selectedIndex:
|
||
typeof element.selectedIndex === "number"
|
||
? element.selectedIndex
|
||
: -1,
|
||
}));
|
||
const runtimeSources = {};
|
||
for (const name of [
|
||
"CfmPurchaseTo1688",
|
||
"SendTo1688",
|
||
"PurchaseTo1688",
|
||
"handlePurchaseResponse",
|
||
"PlaceOrder",
|
||
"Save",
|
||
]) {
|
||
if (typeof window[name] === "function") {
|
||
runtimeSources[name] = String(window[name]).slice(0, 5000);
|
||
}
|
||
}
|
||
const relatedGlobals = Object.keys(window)
|
||
.filter((name) => /1688|purchase.*order/i.test(name))
|
||
.filter((name) => typeof window[name] === "function")
|
||
.slice(0, 80);
|
||
return {
|
||
saveControls,
|
||
formControls,
|
||
runtimeSources,
|
||
relatedGlobals,
|
||
confirmSource: String(window.jQuery?.cfm || "")
|
||
.replace(/\s+/g, " ")
|
||
.slice(0, 5000),
|
||
messageSource: String(window.jQuery?.msg || "")
|
||
.replace(/\s+/g, " ")
|
||
.slice(0, 5000),
|
||
defaultPurchaseAddress:
|
||
typeof window.defaultPurchaseAddress === "boolean"
|
||
? window.defaultPurchaseAddress
|
||
: null,
|
||
bodyText: String(document.body?.innerText || "")
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.slice(0, 1500),
|
||
};
|
||
});
|
||
const visibleConfirmationLayers = [];
|
||
for (const candidate of page.frames()) {
|
||
if (!(await frameChainVisible(candidate).catch(() => false))) continue;
|
||
const details = await candidate.evaluate(() => {
|
||
const visible = (element) => {
|
||
const rect = element.getBoundingClientRect();
|
||
const style = window.getComputedStyle(element);
|
||
return (
|
||
rect.width > 0 &&
|
||
rect.height > 0 &&
|
||
style.display !== "none" &&
|
||
style.visibility !== "hidden"
|
||
);
|
||
};
|
||
const candidates = Array.from(
|
||
document.querySelectorAll(
|
||
"button,a,input,span,div,[id*='confirm'],[id*='cfm'],[class*='dialog']",
|
||
),
|
||
)
|
||
.filter(visible)
|
||
.filter((element) => {
|
||
const text = String(
|
||
element.innerText || element.value || element.textContent || "",
|
||
)
|
||
.replace(/\s+/g, " ")
|
||
.trim();
|
||
const marker = `${element.id || ""} ${element.className || ""}`;
|
||
return (
|
||
/确认|确定|取消|继续|一键代发|一键下单/.test(text) ||
|
||
/confirm|cfm|dialog/i.test(marker)
|
||
);
|
||
})
|
||
.slice(0, 80)
|
||
.map((element) => ({
|
||
tag: element.tagName,
|
||
id: element.id || "",
|
||
className: String(element.className || "").slice(0, 200),
|
||
text: String(
|
||
element.innerText || element.value || element.textContent || "",
|
||
)
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.slice(0, 300),
|
||
onclick: String(element.getAttribute("onclick") || "").slice(0, 500),
|
||
html: String(element.outerHTML || "").slice(0, 1200),
|
||
}));
|
||
return {
|
||
bodyText: String(document.body?.innerText || "")
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.slice(0, 1200),
|
||
candidates,
|
||
};
|
||
});
|
||
visibleConfirmationLayers.push({
|
||
url: candidate.url(),
|
||
...details,
|
||
});
|
||
}
|
||
const screenshotPath =
|
||
`/opt/dalongxia-chat-api/data/` +
|
||
`${clean(BROWSER_CODE, 40)}-1688-navigation.png`;
|
||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||
return {
|
||
status: "ok",
|
||
order_id: orderId,
|
||
purchase_order_no: purchaseOrderNo,
|
||
audit_completed: purchaseAuditCompleted(rowBody),
|
||
online_1688_order_no: rowBody.match(/\b\d{16,30}\b/)?.[0] || "",
|
||
account_state: accountState,
|
||
price_contrast: priceContrast,
|
||
controls,
|
||
visible_confirmation_layers: visibleConfirmationLayers,
|
||
screenshot_path: screenshotPath,
|
||
};
|
||
}
|
||
|
||
async function inspectOrderRow(payload) {
|
||
const auth = await ensureLoggedIn();
|
||
if (auth.status !== "ok") return auth;
|
||
const orderFrame = await openOrderList();
|
||
const orderId = clean(payload.order_id, 40);
|
||
const orderLocator = await searchOrder(orderFrame, orderId);
|
||
if (!orderLocator) {
|
||
return { status: "ok", order_id: orderId, found: false };
|
||
}
|
||
const row = await orderRowInfo(orderLocator, orderId);
|
||
const cells = await orderLocator.evaluate((element) => {
|
||
const rowElement = element.closest("._jt_row");
|
||
if (!rowElement) return [];
|
||
return Array.from(rowElement.querySelectorAll("._jt_cell")).map((cell) => ({
|
||
data_id: cell.getAttribute("data-id") || "",
|
||
cell_index: cell.getAttribute("cellindex") || "",
|
||
text: (cell.innerText || cell.textContent || "")
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.slice(0, 500),
|
||
}));
|
||
});
|
||
return {
|
||
status: "ok",
|
||
order_id: orderId,
|
||
found: true,
|
||
row,
|
||
cells,
|
||
};
|
||
}
|
||
|
||
function purchaseAuditCompleted(rowBody) {
|
||
return /已确认|已审核|审核通过/.test(String(rowBody || ""));
|
||
}
|
||
|
||
async function findAuditConfirmationControl() {
|
||
const matches = [];
|
||
const controlSelector = [
|
||
"button",
|
||
"a",
|
||
"[role='button']",
|
||
"input[type='button']",
|
||
"input[type='submit']",
|
||
".btn",
|
||
".x-btn",
|
||
].join(",");
|
||
for (const candidate of page.frames()) {
|
||
if (!(await frameChainVisible(candidate).catch(() => false))) continue;
|
||
const body = clean(
|
||
await candidate.locator("body").innerText().catch(() => ""),
|
||
1500,
|
||
);
|
||
const isAuditDialog =
|
||
candidate.url().includes("/epaas-dialog-frame.html") &&
|
||
/审核|生效/.test(body);
|
||
const frameMatches = [];
|
||
const controls = candidate.locator(controlSelector);
|
||
const count = await controls.count().catch(() => 0);
|
||
for (let index = 0; index < count; index += 1) {
|
||
const item = controls.nth(index);
|
||
if (!(await item.isVisible().catch(() => false))) continue;
|
||
const label = clean(
|
||
await item
|
||
.evaluate(
|
||
(element) =>
|
||
element.innerText ||
|
||
element.textContent ||
|
||
element.value ||
|
||
element.getAttribute("title") ||
|
||
"",
|
||
)
|
||
.catch(() => ""),
|
||
80,
|
||
);
|
||
const strongMatch =
|
||
/全部确认审核|全部审核|确认并审核|确认审核/.test(label);
|
||
const dialogMatch =
|
||
isAuditDialog && /^(确认|确定|继续|确认并继续)$/.test(label);
|
||
if (strongMatch || dialogMatch) {
|
||
frameMatches.push({ frame: candidate, item, label });
|
||
}
|
||
}
|
||
if (!frameMatches.length) {
|
||
for (const label of [
|
||
"全部确认审核",
|
||
"全部审核",
|
||
"确认并审核",
|
||
"确认审核",
|
||
]) {
|
||
const textMatches = await firstVisible(
|
||
candidate.getByText(label, { exact: true }),
|
||
);
|
||
for (const item of textMatches) {
|
||
frameMatches.push({ frame: candidate, item, label });
|
||
}
|
||
if (frameMatches.length) break;
|
||
}
|
||
}
|
||
if (!frameMatches.length && isAuditDialog) {
|
||
for (const label of ["确认", "确定", "继续", "确认并继续"]) {
|
||
const textMatches = await firstVisible(
|
||
candidate.getByText(label, { exact: true }),
|
||
);
|
||
for (const item of textMatches) {
|
||
frameMatches.push({ frame: candidate, item, label });
|
||
}
|
||
if (frameMatches.length) break;
|
||
}
|
||
}
|
||
matches.push(...frameMatches);
|
||
}
|
||
if (matches.length > 1) {
|
||
const exact = matches.filter((match) =>
|
||
/全部确认审核/.test(match.label),
|
||
);
|
||
if (exact.length === 1) return exact[0];
|
||
throw new Error(`audit confirmation control count ${matches.length}`);
|
||
}
|
||
return matches[0] || null;
|
||
}
|
||
|
||
async function purchaseAuditResult(purchaseFrame) {
|
||
const overlays = await firstVisible(
|
||
purchaseFrame.locator(".jmx-result-wrapper"),
|
||
);
|
||
for (const overlay of overlays) {
|
||
const body = clean(await overlay.innerText().catch(() => ""), 1000);
|
||
if (/失败|错误|未成功/.test(body)) {
|
||
throw new Error(`Purchase audit failed: ${body}`);
|
||
}
|
||
if (/成功|完成|已确认|已审核/.test(body)) {
|
||
return { success: true, body, overlay };
|
||
}
|
||
}
|
||
return { success: false, body: "", overlay: null };
|
||
}
|
||
|
||
async function hideAuditResultOverlay(result) {
|
||
if (!result?.overlay) return;
|
||
await result.overlay
|
||
.evaluate((element) => {
|
||
element.style.display = "none";
|
||
element.style.pointerEvents = "none";
|
||
})
|
||
.catch(() => {});
|
||
}
|
||
|
||
async function clearPurchaseResultOverlays(
|
||
purchaseFrame,
|
||
timeoutMs = 5000,
|
||
) {
|
||
const deadline = Date.now() + timeoutMs;
|
||
let cleared = false;
|
||
while (Date.now() < deadline) {
|
||
const overlays = await firstVisible(
|
||
purchaseFrame.locator(".jmx-result-wrapper"),
|
||
);
|
||
if (overlays.length) {
|
||
for (const overlay of overlays) {
|
||
await overlay
|
||
.evaluate((element) => {
|
||
element.style.display = "none";
|
||
element.style.pointerEvents = "none";
|
||
element.setAttribute("aria-hidden", "true");
|
||
})
|
||
.catch(() => {});
|
||
}
|
||
cleared = true;
|
||
await purchaseFrame.waitForTimeout(250);
|
||
continue;
|
||
}
|
||
if (cleared) return true;
|
||
await purchaseFrame.waitForTimeout(200);
|
||
}
|
||
return cleared;
|
||
}
|
||
|
||
async function describe1688AccountControls(navigationFrame) {
|
||
const controls = navigationFrame.locator(
|
||
[
|
||
"select",
|
||
"input",
|
||
"[role='combobox']",
|
||
"[id*='account' i]",
|
||
"[name*='account' i]",
|
||
"[class*='select' i]",
|
||
].join(","),
|
||
);
|
||
const diagnostics = [];
|
||
const count = Math.min(await controls.count(), 80);
|
||
for (let index = 0; index < count; index += 1) {
|
||
const item = controls.nth(index);
|
||
diagnostics.push(
|
||
await item
|
||
.evaluate((element) => {
|
||
const rect = element.getBoundingClientRect();
|
||
const style = window.getComputedStyle(element);
|
||
const visible =
|
||
rect.width > 0 &&
|
||
rect.height > 0 &&
|
||
style.display !== "none" &&
|
||
style.visibility !== "hidden";
|
||
const options =
|
||
element.tagName === "SELECT"
|
||
? Array.from(element.options || [])
|
||
.slice(0, 20)
|
||
.map((option) => ({
|
||
text: (option.textContent || "").trim().slice(0, 120),
|
||
selected: option.selected,
|
||
disabled: option.disabled,
|
||
}))
|
||
: [];
|
||
return {
|
||
tag: element.tagName,
|
||
id: element.id || "",
|
||
name: element.getAttribute("name") || "",
|
||
type: element.getAttribute("type") || "",
|
||
role: element.getAttribute("role") || "",
|
||
title: element.getAttribute("title") || "",
|
||
placeholder: element.getAttribute("placeholder") || "",
|
||
className:
|
||
typeof element.className === "string"
|
||
? element.className.slice(0, 180)
|
||
: "",
|
||
text: (element.innerText || element.textContent || "")
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.slice(0, 240),
|
||
parentText: (element.parentElement?.innerText || "")
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.slice(0, 240),
|
||
checked: "checked" in element ? element.checked : undefined,
|
||
hasValue:
|
||
"value" in element &&
|
||
String(element.value || "").trim().length > 0,
|
||
visible,
|
||
options,
|
||
};
|
||
})
|
||
.catch(() => null),
|
||
);
|
||
}
|
||
return diagnostics.filter(Boolean);
|
||
}
|
||
|
||
async function waitFor1688AccountReady(
|
||
navigationFrame,
|
||
timeoutMs = 8000,
|
||
) {
|
||
const deadline = Date.now() + timeoutMs;
|
||
const absenceDeadline = Date.now() + 2000;
|
||
let latest = [];
|
||
while (Date.now() < deadline) {
|
||
latest = await describe1688AccountControls(navigationFrame);
|
||
const candidates = latest.filter((item) => {
|
||
if (!item.visible) return false;
|
||
const label = [
|
||
item.id,
|
||
item.name,
|
||
item.title,
|
||
item.placeholder,
|
||
item.className,
|
||
item.parentText,
|
||
].join(" ");
|
||
return /1688.*(账号|账户)|(账号|账户).*1688/i.test(label);
|
||
});
|
||
if (!candidates.length) {
|
||
if (Date.now() >= absenceDeadline) {
|
||
return { detected: false, ready: true, diagnostics: latest };
|
||
}
|
||
} else {
|
||
const ready = candidates.some((item) => {
|
||
if (item.tag === "SELECT") {
|
||
return item.options.some(
|
||
(option) =>
|
||
option.selected &&
|
||
!option.disabled &&
|
||
option.text &&
|
||
!/请选择|选择账号|选择账户/.test(option.text),
|
||
);
|
||
}
|
||
return item.hasValue || Boolean(item.text);
|
||
});
|
||
if (ready) {
|
||
return { detected: true, ready: true, diagnostics: candidates };
|
||
}
|
||
}
|
||
await navigationFrame.waitForTimeout(250);
|
||
}
|
||
return { detected: true, ready: false, diagnostics: latest };
|
||
}
|
||
|
||
async function confirm1688DialogsAfterSave(
|
||
navigationFrame,
|
||
purchaseOrderNo,
|
||
report = () => {},
|
||
timeoutMs = 6000,
|
||
) {
|
||
const deadline = Date.now() + timeoutMs;
|
||
let quietSince = Date.now();
|
||
let confirmed = 0;
|
||
while (Date.now() < deadline) {
|
||
const dialogs = [];
|
||
for (const candidate of page.frames()) {
|
||
if (
|
||
!candidate.url().includes("/epaas-dialog-frame.html") ||
|
||
!(await frameChainVisible(candidate).catch(() => false)) ||
|
||
isDescendantFrame(navigationFrame, candidate)
|
||
) {
|
||
continue;
|
||
}
|
||
const body = clean(
|
||
await candidate.locator("body").innerText().catch(() => ""),
|
||
1000,
|
||
);
|
||
if (/1688|一键下单|一键代发|明文|密文/.test(body)) {
|
||
dialogs.push({ frame: candidate, body });
|
||
}
|
||
}
|
||
if (dialogs.length > 1) {
|
||
throw new Error(`1688 pre-save dialog count ${dialogs.length}`);
|
||
}
|
||
if (!dialogs.length) {
|
||
if (Date.now() - quietSince >= 1000) return confirmed;
|
||
await page.waitForTimeout(200);
|
||
continue;
|
||
}
|
||
const dialog = dialogs[0];
|
||
let control = null;
|
||
for (const label of [
|
||
"确认并继续",
|
||
"确认",
|
||
"确定",
|
||
"继续",
|
||
"是",
|
||
]) {
|
||
const controls = await firstVisible(
|
||
dialog.frame.getByText(label, { exact: true }),
|
||
);
|
||
if (controls.length > 1) {
|
||
throw new Error(`1688 pre-save confirmation count ${controls.length}`);
|
||
}
|
||
if (controls.length === 1) {
|
||
control = controls[0];
|
||
break;
|
||
}
|
||
}
|
||
if (!control) {
|
||
const accountDiagnostics = /1688账号不能为空/.test(dialog.body)
|
||
? await describe1688AccountControls(navigationFrame)
|
||
: [];
|
||
logInternalError(
|
||
new Error(
|
||
`1688 pre-save confirmation diagnostics ${dialog.body}; ` +
|
||
`account_controls=${JSON.stringify(accountDiagnostics)}`,
|
||
),
|
||
`1688-pre-save-${purchaseOrderNo}`,
|
||
);
|
||
throw new Error("1688 pre-save confirmation was not found");
|
||
}
|
||
report(
|
||
"supplier.1688.save_confirm.started",
|
||
"正在确认1688一件代发提示",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
await control.click({ force: true });
|
||
confirmed += 1;
|
||
report(
|
||
"supplier.1688.save_confirm.completed",
|
||
"1688一件代发提示已确认",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
quietSince = Date.now();
|
||
await page.waitForTimeout(500);
|
||
}
|
||
return confirmed;
|
||
}
|
||
|
||
async function click1688SaveWithOverlayRecovery(
|
||
navigationFrame,
|
||
purchaseOrderNo,
|
||
report = () => {},
|
||
) {
|
||
try {
|
||
await clickExactText(navigationFrame, "保存", "save 1688 navigation");
|
||
return;
|
||
} catch (error) {
|
||
const message = String(error?.message || error || "");
|
||
if (!/epaasDialogFrame|intercepts pointer events/i.test(message)) {
|
||
throw error;
|
||
}
|
||
const confirmed = await confirm1688DialogsAfterSave(
|
||
navigationFrame,
|
||
purchaseOrderNo,
|
||
report,
|
||
2500,
|
||
);
|
||
const hidden = await hideBlankEpaasPointerOverlays(navigationFrame);
|
||
if (!confirmed && !hidden) throw error;
|
||
report(
|
||
"supplier.1688.save.overlay_recovered",
|
||
"1688保存遮罩已安全处理,正在重试保存",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
await navigationFrame.waitForTimeout(250);
|
||
await clickExactText(navigationFrame, "保存", "save 1688 navigation");
|
||
}
|
||
}
|
||
|
||
async function setLegacyCheckboxChecked(locator) {
|
||
if (!(await locator.isChecked())) {
|
||
await locator.check();
|
||
}
|
||
const state = await locator.evaluate((element) => {
|
||
element.setAttribute("checked", "checked");
|
||
if (window.jQuery) {
|
||
window.jQuery(element).prop("checked", true);
|
||
window.jQuery(element).attr("checked", "checked");
|
||
}
|
||
return {
|
||
checked: Boolean(element.checked),
|
||
checkedAttribute: element.getAttribute("checked") || "",
|
||
};
|
||
});
|
||
if (!state.checked || state.checkedAttribute !== "checked") {
|
||
throw new Error("1688 dropship checkbox verification failed");
|
||
}
|
||
}
|
||
|
||
async function read1688DeliveryAddressState(frame, checkbox) {
|
||
return frame.evaluate((purchaseAddress) => {
|
||
const visible = (element) => {
|
||
const rect = element.getBoundingClientRect();
|
||
const style = window.getComputedStyle(element);
|
||
return (
|
||
rect.width > 0 &&
|
||
rect.height > 0 &&
|
||
style.display !== "none" &&
|
||
style.visibility !== "hidden"
|
||
);
|
||
};
|
||
const values = Array.from(
|
||
document.querySelectorAll("select,input:not([type='hidden']),textarea"),
|
||
)
|
||
.filter(visible)
|
||
.map((element) => {
|
||
if (element.tagName === "SELECT") {
|
||
return String(element.selectedOptions?.[0]?.textContent || "");
|
||
}
|
||
return String(element.value || "");
|
||
})
|
||
.map((value) => value.replace(/\s+/g, " ").trim())
|
||
.filter(Boolean);
|
||
return {
|
||
checked: Boolean(purchaseAddress?.checked),
|
||
values,
|
||
bodyText: String(document.body?.innerText || "")
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.slice(0, 5000),
|
||
};
|
||
}, await checkbox.elementHandle());
|
||
}
|
||
|
||
async function capture1688DefaultAddressState(frame, checkbox) {
|
||
if (await checkbox.isChecked()) {
|
||
await checkbox.uncheck();
|
||
}
|
||
const deadline = Date.now() + 5000;
|
||
let latest = { checked: true, values: [] };
|
||
while (Date.now() < deadline) {
|
||
latest = await read1688DeliveryAddressState(frame, checkbox);
|
||
if (!latest.checked && latest.values.length) {
|
||
await frame.waitForTimeout(400);
|
||
return read1688DeliveryAddressState(frame, checkbox);
|
||
}
|
||
await frame.waitForTimeout(200);
|
||
}
|
||
throw safeOrderFailure("1688 default delivery address capture failed");
|
||
}
|
||
|
||
function deliveryAddressMatchDiagnostics(order, currentValues) {
|
||
const values = currentValues || [];
|
||
const currentText = values.join(" ");
|
||
const text = normalizedContactText(currentText);
|
||
const verified = order.__verifiedDelivery || {};
|
||
const recipient = normalizedRecipientText(
|
||
verified.recipient || order.recipient,
|
||
);
|
||
const phone = normalizedPhoneDigits(verified.phone || order.phone);
|
||
const privacySuffix = privacyPhoneSuffix(order.phone);
|
||
const address = verified.address || order.address;
|
||
const administrativeParts = verified.administrativeParts || [];
|
||
const recipientMatch = recipient.length > 0 && text.includes(recipient);
|
||
const phoneMatch = phone.length >= 7 && text.includes(phone);
|
||
const privacySuffixMatch = Boolean(
|
||
privacySuffix && text.includes(privacySuffix),
|
||
);
|
||
const addressMatch = values.some((value) =>
|
||
addressTextMatches(address, value, administrativeParts),
|
||
);
|
||
return {
|
||
recipientMatch,
|
||
phoneMatch,
|
||
privacySuffixMatch,
|
||
addressMatch,
|
||
contactMatch: Boolean(
|
||
privacySuffix
|
||
? privacySuffixMatch && addressMatch
|
||
: recipientMatch && phoneMatch && addressMatch
|
||
),
|
||
};
|
||
}
|
||
|
||
function deliveryAddressStateMatchesOrder(
|
||
order,
|
||
currentValues,
|
||
defaultValues,
|
||
currentBodyText = "",
|
||
defaultBodyText = "",
|
||
) {
|
||
const currentContactValues = [
|
||
...(currentValues || []),
|
||
currentBodyText,
|
||
].filter(Boolean);
|
||
const currentText = currentContactValues.join(" ");
|
||
const currentFingerprint = normalizedContactText(currentText);
|
||
const defaultFingerprint = normalizedContactText(
|
||
[...(defaultValues || []), defaultBodyText].filter(Boolean).join(" "),
|
||
);
|
||
const diagnostics = deliveryAddressMatchDiagnostics(
|
||
order,
|
||
currentContactValues,
|
||
);
|
||
return Boolean(
|
||
diagnostics.contactMatch
|
||
&& currentFingerprint
|
||
&& currentFingerprint !== defaultFingerprint
|
||
);
|
||
}
|
||
|
||
async function waitFor1688BuyerAddress(
|
||
frame,
|
||
checkbox,
|
||
order,
|
||
forbiddenState,
|
||
) {
|
||
const deadline = Date.now() + 10000;
|
||
let latest = { checked: false, values: [] };
|
||
while (Date.now() < deadline) {
|
||
latest = await read1688DeliveryAddressState(frame, checkbox);
|
||
if (
|
||
latest.checked &&
|
||
deliveryAddressStateMatchesOrder(
|
||
order,
|
||
latest.values,
|
||
forbiddenState.values,
|
||
latest.bodyText,
|
||
forbiddenState.bodyText,
|
||
)
|
||
) {
|
||
return latest;
|
||
}
|
||
await frame.waitForTimeout(250);
|
||
}
|
||
const currentText = latest.values.join(" ");
|
||
const matchDiagnostics = deliveryAddressMatchDiagnostics(
|
||
order,
|
||
[...latest.values, latest.bodyText].filter(Boolean),
|
||
);
|
||
logInternalError(
|
||
new Error(
|
||
"1688 delivery address diagnostics "
|
||
+ JSON.stringify({
|
||
checked: latest.checked,
|
||
value_count: latest.values.length,
|
||
recipient_match: matchDiagnostics.recipientMatch,
|
||
phone_match: matchDiagnostics.phoneMatch,
|
||
privacy_suffix_match: matchDiagnostics.privacySuffixMatch,
|
||
address_match: matchDiagnostics.addressMatch,
|
||
differs_from_default:
|
||
normalizedContactText(currentText)
|
||
!== normalizedContactText(
|
||
[...(forbiddenState.values || []), forbiddenState.bodyText]
|
||
.filter(Boolean)
|
||
.join(" "),
|
||
),
|
||
}),
|
||
),
|
||
`1688-address-${order.order_id}`,
|
||
);
|
||
throw safeOrderFailure("1688 delivery address verification failed");
|
||
}
|
||
|
||
async function install1688AsyncRecorder(frame, purchaseOrderNo) {
|
||
await frame.evaluate((expectedPurchaseOrderNo) => {
|
||
const recorderKey = "__dewu1688AsyncRecorder";
|
||
const current = window[recorderKey];
|
||
if (current?.installed) {
|
||
current.records = [];
|
||
current.confirmations = [];
|
||
current.messages = [];
|
||
current.purchaseOrderNo = String(expectedPurchaseOrderNo || "");
|
||
return;
|
||
}
|
||
if (typeof window._AsyncCallPage !== "function") {
|
||
throw new Error("1688 async call recorder unavailable");
|
||
}
|
||
const recorder = {
|
||
installed: true,
|
||
records: [],
|
||
confirmations: [],
|
||
messages: [],
|
||
purchaseOrderNo: String(expectedPurchaseOrderNo || ""),
|
||
original: window._AsyncCallPage,
|
||
originalCfm: window.jQuery?.cfm,
|
||
originalMsg: window.jQuery?.msg,
|
||
};
|
||
const clone = (value) => {
|
||
try {
|
||
return JSON.parse(JSON.stringify(value));
|
||
} catch {
|
||
return String(value ?? "").slice(0, 4000);
|
||
}
|
||
};
|
||
window[recorderKey] = recorder;
|
||
window._AsyncCallPage = function (
|
||
method,
|
||
callback,
|
||
...args
|
||
) {
|
||
const wrappedCallback = function (...callbackArgs) {
|
||
recorder.records.push({
|
||
method: String(method || "").slice(0, 80),
|
||
response: clone(callbackArgs[0]),
|
||
receivedAt: Date.now(),
|
||
});
|
||
if (typeof callback === "function") {
|
||
return callback.apply(this, callbackArgs);
|
||
}
|
||
return undefined;
|
||
};
|
||
return recorder.original.call(
|
||
this,
|
||
method,
|
||
wrappedCallback,
|
||
...args
|
||
);
|
||
};
|
||
if (typeof recorder.originalCfm === "function") {
|
||
window.jQuery.cfm = function (
|
||
message,
|
||
callback,
|
||
...args
|
||
) {
|
||
const text = String(
|
||
message?.text || message?.html || message || "",
|
||
)
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.slice(0, 1200);
|
||
const expected = recorder.purchaseOrderNo;
|
||
const targetScopedConfirmation =
|
||
Boolean(expected && text.includes(expected)) &&
|
||
/分销|代发|下单|1688/.test(text) &&
|
||
!/失败|错误|不能|禁止|下架|库存不足|不支持|异常/.test(text);
|
||
const autoConfirm =
|
||
/^请确认一键下单更改成一键代发[??]?$/.test(text) ||
|
||
/^存在本地商品单价高于1688线上产品单价商品,?是否继续下单/.test(
|
||
text,
|
||
) ||
|
||
/^如您选择密文对淘宝店铺分销代发/.test(text) ||
|
||
targetScopedConfirmation;
|
||
const entry = {
|
||
message: text,
|
||
autoConfirmed: autoConfirm,
|
||
resolved: autoConfirm,
|
||
receivedAt: Date.now(),
|
||
};
|
||
recorder.confirmations.push(entry);
|
||
if (autoConfirm) {
|
||
setTimeout(() => {
|
||
if (typeof callback === "function") callback(true);
|
||
}, 0);
|
||
return true;
|
||
}
|
||
return recorder.originalCfm.call(
|
||
this,
|
||
message,
|
||
function (value) {
|
||
entry.resolved = true;
|
||
entry.accepted = Boolean(value);
|
||
if (typeof callback === "function") {
|
||
return callback.apply(this, arguments);
|
||
}
|
||
return undefined;
|
||
},
|
||
...args
|
||
);
|
||
};
|
||
}
|
||
if (typeof recorder.originalMsg === "function") {
|
||
window.jQuery.msg = function (message, ...args) {
|
||
recorder.messages.push({
|
||
message: String(
|
||
message?.text || message?.html || message || "",
|
||
)
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.slice(0, 1200),
|
||
receivedAt: Date.now(),
|
||
});
|
||
return recorder.originalMsg.call(this, message, ...args);
|
||
};
|
||
}
|
||
}, String(purchaseOrderNo || ""));
|
||
}
|
||
|
||
async function read1688RecorderState(frame) {
|
||
return frame.evaluate(() => {
|
||
const recorder = window.__dewu1688AsyncRecorder || {};
|
||
return {
|
||
records: Array.from(recorder.records || []).slice(-30),
|
||
confirmations: Array.from(
|
||
recorder.confirmations || [],
|
||
).slice(-20),
|
||
messages: Array.from(recorder.messages || []).slice(-20),
|
||
};
|
||
});
|
||
}
|
||
|
||
function exact1688SuccessMessage(messages, purchaseOrderNo) {
|
||
const expected = String(purchaseOrderNo || "");
|
||
return (messages || []).find((item) => {
|
||
const text = clean(item?.message ?? item, 1200);
|
||
return Boolean(
|
||
expected &&
|
||
text.includes(expected) &&
|
||
/成功/.test(text) &&
|
||
!/失败|错误/.test(text),
|
||
);
|
||
});
|
||
}
|
||
|
||
function parseMaybeJson(value) {
|
||
if (typeof value !== "string") return value;
|
||
try {
|
||
return JSON.parse(value);
|
||
} catch {
|
||
return value;
|
||
}
|
||
}
|
||
|
||
function inspect1688SubmissionRecords(records) {
|
||
for (const record of records || []) {
|
||
const method = clean(record?.method, 80);
|
||
const response = parseMaybeJson(record?.response);
|
||
if (method === "PriceContrast" && Array.isArray(response)) {
|
||
const blocked = response.find(
|
||
(item) =>
|
||
["0", "3"].includes(String(item?.Key ?? item?.key ?? "")) &&
|
||
clean(item?.Value ?? item?.value, 500),
|
||
);
|
||
if (blocked) {
|
||
return {
|
||
state: "rejected",
|
||
reason: clean(blocked.Value ?? blocked.value, 500),
|
||
};
|
||
}
|
||
}
|
||
if (method !== "PurchaseTo1688V3" || !response) continue;
|
||
if (response.isComfrimFenXiao) {
|
||
continue;
|
||
}
|
||
const results = Array.isArray(response?.poResults)
|
||
? response.poResults
|
||
: [];
|
||
const messages = results
|
||
.map((item) => clean(item?.Value ?? item?.value, 500))
|
||
.filter(Boolean);
|
||
if (messages.some((message) => /成功/.test(message))) {
|
||
return { state: "accepted", reason: messages.join(";") };
|
||
}
|
||
return {
|
||
state: "rejected",
|
||
reason:
|
||
messages.join(";") ||
|
||
(response.needSplit
|
||
? "采购单需要拆分"
|
||
: "ERP未返回下单成功"),
|
||
};
|
||
}
|
||
return { state: "pending", reason: "" };
|
||
}
|
||
|
||
function create1688NativeDialogGuard(
|
||
purchaseOrderNo,
|
||
report,
|
||
confirmationTexts,
|
||
) {
|
||
let tasks = [];
|
||
const handler = (dialog) => {
|
||
const task = (async () => {
|
||
const message = clean(dialog.message(), 1000);
|
||
if (message) confirmationTexts.push(message);
|
||
if (!/1688|一键|代发|下单|明文|密文|确认/.test(message)) {
|
||
await dialog.dismiss().catch(() => {});
|
||
return { accepted: false, message };
|
||
}
|
||
report(
|
||
"supplier.1688.native_confirm.started",
|
||
"正在确认浏览器1688下单提示",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
await dialog.accept();
|
||
report(
|
||
"supplier.1688.native_confirm.completed",
|
||
"浏览器1688下单提示已确认",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
return { accepted: true, message };
|
||
})().catch((error) => ({
|
||
accepted: false,
|
||
message: clean(error?.message || error, 500),
|
||
}));
|
||
tasks.push(task);
|
||
};
|
||
page.on("dialog", handler);
|
||
return {
|
||
async drain() {
|
||
while (tasks.length) {
|
||
const current = tasks;
|
||
tasks = [];
|
||
const results = await Promise.all(current);
|
||
if (results.some((result) => !result.accepted)) {
|
||
throw new Error("1688 native confirmation was not accepted");
|
||
}
|
||
}
|
||
},
|
||
stop() {
|
||
page.off("dialog", handler);
|
||
},
|
||
};
|
||
}
|
||
|
||
async function auditConfirmationDiagnostics() {
|
||
const frames = [];
|
||
const selector = [
|
||
"button",
|
||
"a",
|
||
"[role='button']",
|
||
"input[type='button']",
|
||
"input[type='submit']",
|
||
"[onclick]",
|
||
].join(",");
|
||
for (const candidate of page.frames()) {
|
||
if (!(await frameChainVisible(candidate).catch(() => false))) continue;
|
||
const labels = [];
|
||
const controls = candidate.locator(selector);
|
||
const count = Math.min(
|
||
await controls.count().catch(() => 0),
|
||
500,
|
||
);
|
||
for (let index = 0; index < count; index += 1) {
|
||
const item = controls.nth(index);
|
||
if (!(await item.isVisible().catch(() => false))) continue;
|
||
const label = clean(
|
||
await item
|
||
.evaluate(
|
||
(element) =>
|
||
element.innerText ||
|
||
element.textContent ||
|
||
element.value ||
|
||
element.getAttribute("title") ||
|
||
"",
|
||
)
|
||
.catch(() => ""),
|
||
120,
|
||
);
|
||
if (label && /审核|确认|确定|生效|继续/.test(label)) {
|
||
labels.push(label);
|
||
}
|
||
}
|
||
const isDialog = candidate.url().includes("/epaas-dialog-frame.html");
|
||
const dialogText = isDialog
|
||
? clean(
|
||
await candidate.locator("body").innerText().catch(() => ""),
|
||
1000,
|
||
)
|
||
: "";
|
||
if (labels.length || dialogText) {
|
||
let framePath = "";
|
||
try {
|
||
const parsed = new URL(candidate.url());
|
||
framePath = `${parsed.origin}${parsed.pathname}`;
|
||
} catch {
|
||
framePath = clean(candidate.url().split("?")[0], 300);
|
||
}
|
||
frames.push({
|
||
url: framePath,
|
||
labels: [...new Set(labels)].slice(0, 30),
|
||
dialog_text: dialogText,
|
||
});
|
||
}
|
||
}
|
||
return frames;
|
||
}
|
||
|
||
async function purchaseAuditPrimaryControl(purchaseFrame) {
|
||
const preferred = await firstVisible(
|
||
purchaseFrame.locator(
|
||
"#MM_OPTM_Tool_Confirm > .ding_db_txt, " +
|
||
"#MM_OPTM_Tool_Confirm .ding_db_txt",
|
||
),
|
||
);
|
||
if (preferred.length === 1) return preferred[0];
|
||
if (preferred.length > 1) {
|
||
throw new Error("Primary audit control is ambiguous");
|
||
}
|
||
const exact = await firstVisible(
|
||
purchaseFrame.getByText("审核生效", { exact: true }),
|
||
);
|
||
if (exact.length !== 1) throw new Error("Audit control is ambiguous");
|
||
return exact[0];
|
||
}
|
||
|
||
async function clickPurchaseAuditDropdownAction(purchaseFrame) {
|
||
const arrows = await firstVisible(
|
||
purchaseFrame.locator("#MM_OPTM_Tool_Confirm .ding_db_arrow"),
|
||
);
|
||
if (arrows.length !== 1) return false;
|
||
await arrows[0].click({ force: true });
|
||
await purchaseFrame.waitForTimeout(250);
|
||
const actions = await firstVisible(
|
||
purchaseFrame.locator(
|
||
"#MM_OPTM_Tool_Confirm " +
|
||
".ding_db_itembox[ding='OPTM_Tool_Confirm'] .ding_db_item",
|
||
),
|
||
);
|
||
if (actions.length !== 1) return false;
|
||
await actions[0].click({ force: true });
|
||
return true;
|
||
}
|
||
|
||
async function invokePurchaseAuditFunction(purchaseFrame) {
|
||
return purchaseFrame.evaluate(() => {
|
||
if (typeof window.ConfirmV2 !== "function") return false;
|
||
window.ConfirmV2();
|
||
return true;
|
||
});
|
||
}
|
||
|
||
async function invokeValidatedPurchaseAudit(purchaseFrame) {
|
||
return purchaseFrame.evaluate(async () => {
|
||
if (
|
||
typeof window.GetCurrentJTableRow !== "function" ||
|
||
typeof window.GetPoIdArray !== "function" ||
|
||
typeof window._ACP !== "function" ||
|
||
typeof window.ToConfirm !== "function"
|
||
) {
|
||
return { submitted: false, reason: "required_functions_unavailable" };
|
||
}
|
||
const checkedRows = window.GetCurrentJTableRow("待审核") || [];
|
||
if (checkedRows.length !== 1) {
|
||
return {
|
||
submitted: false,
|
||
reason: "selected_purchase_count_invalid",
|
||
selected_count: checkedRows.length,
|
||
};
|
||
}
|
||
const errorPurchases = [];
|
||
let purchaseIds = window.GetPoIdArray(checkedRows) || [];
|
||
let checkStep = 1;
|
||
const precheck = (ids, step) =>
|
||
new Promise((resolve) => {
|
||
let settled = false;
|
||
let timer = null;
|
||
const finish = (value) => {
|
||
if (settled) return;
|
||
settled = true;
|
||
if (timer) clearTimeout(timer);
|
||
resolve(value);
|
||
};
|
||
timer = setTimeout(
|
||
() => finish({ __timeout: true }),
|
||
20000,
|
||
);
|
||
try {
|
||
window._ACP(
|
||
"CheckBeforeConfirm",
|
||
(result) => finish(result || null),
|
||
ids.join(","),
|
||
step,
|
||
);
|
||
} catch (error) {
|
||
finish({
|
||
__error: String(error?.message || error || "precheck_failed"),
|
||
});
|
||
}
|
||
});
|
||
for (let attempt = 1; attempt <= 8; attempt += 1) {
|
||
if (!purchaseIds.length) {
|
||
return { submitted: false, reason: "no_valid_purchase_ids" };
|
||
}
|
||
const result = await precheck(purchaseIds, checkStep);
|
||
if (!result || result.__timeout || result.__error) {
|
||
return {
|
||
submitted: false,
|
||
reason: result?.__timeout
|
||
? "precheck_timeout"
|
||
: result?.__error || "precheck_empty",
|
||
};
|
||
}
|
||
errorPurchases.push(...(result.FailedPurs || []));
|
||
const warningIds = [
|
||
...new Set(
|
||
(result.WarningPurs || [])
|
||
.map((row) => row?.PoId)
|
||
.filter(Boolean),
|
||
),
|
||
];
|
||
const nextIds = [
|
||
...(result.SuccessPoId || []),
|
||
...warningIds,
|
||
];
|
||
const nextStep = Number(result.NextStep || 0);
|
||
if (nextStep > 0) {
|
||
purchaseIds = nextIds;
|
||
checkStep = nextStep;
|
||
continue;
|
||
}
|
||
if (!nextIds.length) {
|
||
return {
|
||
submitted: false,
|
||
reason: "precheck_rejected_all_purchases",
|
||
};
|
||
}
|
||
window.ToConfirm(
|
||
nextIds,
|
||
errorPurchases,
|
||
checkedRows,
|
||
result.Confirmed,
|
||
);
|
||
return {
|
||
submitted: true,
|
||
confirmed_count: nextIds.length,
|
||
warning_count: warningIds.length,
|
||
failed_count: errorPurchases.length,
|
||
};
|
||
}
|
||
return { submitted: false, reason: "precheck_step_limit" };
|
||
});
|
||
}
|
||
|
||
async function selectPurchaseRowForAudit(
|
||
purchaseFrame,
|
||
purchaseOrderNo,
|
||
) {
|
||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||
const rowCheckboxes = purchaseFrame.locator(
|
||
"._jt_row input[type='checkbox']",
|
||
);
|
||
const rowCheckboxCount = await rowCheckboxes.count();
|
||
for (let index = 0; index < rowCheckboxCount; index += 1) {
|
||
const item = rowCheckboxes.nth(index);
|
||
if (!(await item.isVisible().catch(() => false))) continue;
|
||
await item.evaluate((element) => {
|
||
element.checked = false;
|
||
element.removeAttribute("checked");
|
||
element.dispatchEvent(new Event("input", { bubbles: true }));
|
||
element.dispatchEvent(new Event("change", { bubbles: true }));
|
||
}).catch(() => {});
|
||
}
|
||
|
||
const targetRow = await purchaseRowLocator(
|
||
purchaseFrame,
|
||
purchaseOrderNo,
|
||
);
|
||
const selected = await targetRow.evaluate((row) => {
|
||
const checkbox = row.querySelector("input[type='checkbox']");
|
||
if (!checkbox) return false;
|
||
checkbox.checked = true;
|
||
checkbox.setAttribute("checked", "checked");
|
||
checkbox.dispatchEvent(new Event("input", { bubbles: true }));
|
||
checkbox.dispatchEvent(new Event("change", { bubbles: true }));
|
||
return checkbox.checked;
|
||
}).catch(() => false);
|
||
await purchaseFrame.waitForTimeout(200);
|
||
const selectedRows = await purchaseFrame
|
||
.locator("._jt_row")
|
||
.evaluateAll((rows) =>
|
||
rows
|
||
.filter((row) => {
|
||
const checkbox = row.querySelector("input[type='checkbox']");
|
||
return checkbox?.checked && row.offsetParent !== null;
|
||
})
|
||
.map((row) =>
|
||
String(row.innerText || row.textContent || "")
|
||
.replace(/\s+/g, " ")
|
||
.trim(),
|
||
),
|
||
);
|
||
if (
|
||
selected &&
|
||
selectedRows.length === 1 &&
|
||
selectedRows[0].includes(String(purchaseOrderNo))
|
||
) {
|
||
return;
|
||
}
|
||
if (attempt < 3) {
|
||
await purchaseFrame.waitForTimeout(500);
|
||
}
|
||
}
|
||
throw new Error("Purchase row selection was not isolated");
|
||
}
|
||
|
||
async function auditPurchase(
|
||
purchaseFrame,
|
||
purchaseOrderNo,
|
||
report = () => {},
|
||
) {
|
||
report(
|
||
"purchase.audit.status.started",
|
||
"正在核对采购单状态",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
const rowBody = await readPurchaseRow(purchaseFrame, purchaseOrderNo);
|
||
if (purchaseAuditCompleted(rowBody)) {
|
||
report(
|
||
"purchase.audit.already_completed",
|
||
"采购单已经审核生效,无需重复审核",
|
||
"skipped",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
return;
|
||
}
|
||
if (!rowBody.includes("待审核")) {
|
||
throw new Error("Purchase is not waiting for audit");
|
||
}
|
||
report(
|
||
"purchase.audit.status.completed",
|
||
"采购单状态确认为待审核",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
report(
|
||
"purchase.audit.select.started",
|
||
"正在勾选采购单",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
await selectPurchaseRowForAudit(purchaseFrame, purchaseOrderNo);
|
||
report(
|
||
"purchase.audit.select.completed",
|
||
"采购单已勾选",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
const auditTarget = await purchaseAuditPrimaryControl(purchaseFrame);
|
||
report(
|
||
"purchase.audit.submit.started",
|
||
"正在点击审核生效",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
let nativeDialogTask = null;
|
||
let nativeDialogReported = false;
|
||
let confirmationSubmitted = false;
|
||
let functionFallbackSubmitted = false;
|
||
let dropdownFallbackSubmitted = false;
|
||
let validatedFallbackSubmitted = false;
|
||
let auditResult = { success: false, body: "", overlay: null };
|
||
const nativeDialogHandler = (dialog) => {
|
||
nativeDialogTask = (async () => {
|
||
const message = clean(dialog.message(), 500);
|
||
if (!/审核|生效|确认/.test(message)) {
|
||
await dialog.dismiss().catch(() => {});
|
||
return { accepted: false, message };
|
||
}
|
||
await dialog.accept().catch(() => {});
|
||
return { accepted: true, message };
|
||
})();
|
||
};
|
||
page.on("dialog", nativeDialogHandler);
|
||
try {
|
||
await auditTarget.click({ force: true });
|
||
const auditTriggeredAt = Date.now();
|
||
report(
|
||
"purchase.audit.confirm.started",
|
||
"正在识别并处理审核确认",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
const deadline = Date.now() + 30000;
|
||
let nextRefreshAt = Date.now() + 3000;
|
||
while (Date.now() < deadline) {
|
||
if (nativeDialogTask) {
|
||
const nativeResult = await nativeDialogTask;
|
||
if (!nativeResult.accepted) {
|
||
throw new Error("Purchase audit confirmation was not completed");
|
||
}
|
||
confirmationSubmitted = true;
|
||
if (!nativeDialogReported) {
|
||
report(
|
||
"purchase.audit.confirm.native.completed",
|
||
"浏览器审核确认已提交",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
nativeDialogReported = true;
|
||
}
|
||
}
|
||
if (!confirmationSubmitted) {
|
||
const confirmAudit = await findAuditConfirmationControl();
|
||
if (confirmAudit) {
|
||
await confirmAudit.item.click({ force: true });
|
||
confirmationSubmitted = true;
|
||
report(
|
||
"purchase.audit.confirm.completed",
|
||
"采购单审核确认已提交",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
await purchaseFrame.waitForTimeout(500);
|
||
}
|
||
}
|
||
auditResult = await purchaseAuditResult(purchaseFrame);
|
||
const currentRowBody = await readPurchaseRow(
|
||
purchaseFrame,
|
||
purchaseOrderNo,
|
||
).catch(() => "");
|
||
if (purchaseAuditCompleted(currentRowBody)) {
|
||
if (!confirmationSubmitted) {
|
||
report(
|
||
"purchase.audit.confirm.not_required",
|
||
"ERP已直接完成审核,无需二次确认",
|
||
"skipped",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
}
|
||
report(
|
||
"purchase.audit.status.verified",
|
||
"已回读采购单状态,确认审核生效",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
break;
|
||
}
|
||
if (
|
||
!confirmationSubmitted &&
|
||
!nativeDialogTask &&
|
||
!auditResult.success &&
|
||
!functionFallbackSubmitted &&
|
||
Date.now() - auditTriggeredAt >= 2500
|
||
) {
|
||
functionFallbackSubmitted = true;
|
||
report(
|
||
"purchase.audit.submit.function.started",
|
||
"主审核按钮未响应,正在调用页面审核动作",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
const invoked = await invokePurchaseAuditFunction(purchaseFrame);
|
||
report(
|
||
invoked
|
||
? "purchase.audit.submit.function.completed"
|
||
: "purchase.audit.submit.function.skipped",
|
||
invoked
|
||
? "页面审核动作已调用"
|
||
: "页面审核动作当前不可用,继续尝试审核菜单",
|
||
invoked ? "completed" : "skipped",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
}
|
||
if (
|
||
!confirmationSubmitted &&
|
||
!nativeDialogTask &&
|
||
!auditResult.success &&
|
||
!dropdownFallbackSubmitted &&
|
||
Date.now() - auditTriggeredAt >= 5000
|
||
) {
|
||
dropdownFallbackSubmitted = true;
|
||
report(
|
||
"purchase.audit.submit.fallback.started",
|
||
"主审核按钮未响应,正在通过审核下拉项继续",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
const clicked = await clickPurchaseAuditDropdownAction(
|
||
purchaseFrame,
|
||
);
|
||
report(
|
||
clicked
|
||
? "purchase.audit.submit.fallback.completed"
|
||
: "purchase.audit.submit.fallback.skipped",
|
||
clicked
|
||
? "审核下拉项已提交"
|
||
: "审核下拉项当前不可用,继续等待页面响应",
|
||
clicked ? "completed" : "skipped",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
}
|
||
if (
|
||
!confirmationSubmitted &&
|
||
!nativeDialogTask &&
|
||
!auditResult.success &&
|
||
!validatedFallbackSubmitted &&
|
||
Date.now() - auditTriggeredAt >= 8000
|
||
) {
|
||
validatedFallbackSubmitted = true;
|
||
report(
|
||
"purchase.audit.submit.validated.started",
|
||
"审核确认层未出现,正在通过ERP预审结果提交目标采购单",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
const validated = await invokeValidatedPurchaseAudit(
|
||
purchaseFrame,
|
||
);
|
||
if (validated.submitted) {
|
||
confirmationSubmitted = true;
|
||
}
|
||
report(
|
||
validated.submitted
|
||
? "purchase.audit.submit.validated.completed"
|
||
: "purchase.audit.submit.validated.skipped",
|
||
validated.submitted
|
||
? "ERP预审通过,目标采购单审核已提交"
|
||
: "ERP预审未返回可提交结果,继续等待页面状态",
|
||
validated.submitted ? "completed" : "skipped",
|
||
{
|
||
purchase_order_no: purchaseOrderNo,
|
||
audit_reason: clean(validated.reason, 120),
|
||
},
|
||
);
|
||
}
|
||
if (confirmationSubmitted && Date.now() >= nextRefreshAt) {
|
||
await hideAuditResultOverlay(auditResult);
|
||
const searchControls = await firstVisible(
|
||
purchaseFrame.getByText("搜索", { exact: true }),
|
||
);
|
||
if (searchControls.length === 1) {
|
||
await searchControls[0].click().catch(() => {});
|
||
await purchaseFrame.waitForTimeout(900);
|
||
}
|
||
nextRefreshAt = Date.now() + 3000;
|
||
}
|
||
await purchaseFrame.waitForTimeout(250);
|
||
}
|
||
const verifiedRowBody = await readPurchaseRow(
|
||
purchaseFrame,
|
||
purchaseOrderNo,
|
||
).catch(() => "");
|
||
if (!purchaseAuditCompleted(verifiedRowBody)) {
|
||
const diagnostics = await auditConfirmationDiagnostics().catch(
|
||
() => [],
|
||
);
|
||
logInternalError(
|
||
new Error(
|
||
`Purchase audit confirmation diagnostics ${JSON.stringify(
|
||
diagnostics,
|
||
)}`,
|
||
),
|
||
`audit-controls-${purchaseOrderNo}`,
|
||
);
|
||
throw new Error("Purchase audit confirmation was not completed");
|
||
}
|
||
} finally {
|
||
page.off("dialog", nativeDialogHandler);
|
||
await hideAuditResultOverlay(auditResult);
|
||
}
|
||
if (await clearPurchaseResultOverlays(purchaseFrame)) {
|
||
report(
|
||
"purchase.audit.result.closed",
|
||
"采购单审核结果层已关闭",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
}
|
||
report(
|
||
"purchase.audit.success",
|
||
"采购单审核生效成功",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
}
|
||
|
||
async function place1688Order(
|
||
purchaseFrame,
|
||
purchaseOrderNo,
|
||
order,
|
||
report = () => {},
|
||
) {
|
||
report(
|
||
"supplier.1688.status.started",
|
||
"正在核对是否已有1688线上单号",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
let rowBody = await readPurchaseRow(purchaseFrame, purchaseOrderNo);
|
||
const existingOrder = verified1688Order(rowBody);
|
||
if (existingOrder.verified) {
|
||
const online = existingOrder.onlineOrderNo;
|
||
report(
|
||
"supplier.1688.already_completed",
|
||
"已存在1688线上单号,无需重复下单",
|
||
"skipped",
|
||
{
|
||
purchase_order_no: purchaseOrderNo,
|
||
online_1688_order_no: online,
|
||
},
|
||
);
|
||
return { onlineOrderNo: online, reused: true };
|
||
}
|
||
await clearPurchaseResultOverlays(purchaseFrame);
|
||
const trigger = purchaseFrame.locator("#OPTM_Purhcase_Order_1688");
|
||
if ((await trigger.count()) !== 1) {
|
||
throw new Error("1688 toolbar entry is ambiguous");
|
||
}
|
||
report(
|
||
"supplier.sync.open.started",
|
||
"正在打开同步供应商菜单",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
await trigger.click();
|
||
report(
|
||
"supplier.sync.open.completed",
|
||
"同步供应商菜单已打开",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
report(
|
||
"supplier.1688.menu.started",
|
||
"正在选择1688一键下单",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
await clickExactText(purchaseFrame, "1688一键下单", "1688 one-click order");
|
||
report(
|
||
"supplier.1688.navigation.started",
|
||
"正在打开1688下单导航",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
const dropshipMatch = await waitForVisibleLocatorAcrossFrames(
|
||
"#purchaseAddress",
|
||
"1688 dropship option",
|
||
);
|
||
report(
|
||
"supplier.1688.navigation.completed",
|
||
"1688下单导航已打开",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
report(
|
||
"supplier.1688.account.waiting",
|
||
"正在等待1688账号加载",
|
||
"waiting",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
const accountState = await waitFor1688AccountReady(dropshipMatch.frame);
|
||
if (!accountState.ready) {
|
||
logInternalError(
|
||
new Error(
|
||
`1688 account was not ready; ` +
|
||
`account_controls=${JSON.stringify(accountState.diagnostics)}`,
|
||
),
|
||
`1688-account-${purchaseOrderNo}`,
|
||
);
|
||
throw new Error("1688 account was not ready");
|
||
}
|
||
report(
|
||
"supplier.1688.account.ready",
|
||
accountState.detected
|
||
? "1688账号已加载"
|
||
: "1688下单导航已完成加载",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
const dropshipVisible = dropshipMatch.item;
|
||
report(
|
||
"supplier.1688.address_guard.started",
|
||
"正在记录并禁用公司默认收货地址",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
const defaultAddressState = await capture1688DefaultAddressState(
|
||
dropshipMatch.frame,
|
||
dropshipVisible,
|
||
);
|
||
report(
|
||
"supplier.1688.dropship.started",
|
||
"正在勾选1688下单一件代发",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
await setLegacyCheckboxChecked(dropshipVisible);
|
||
await waitFor1688BuyerAddress(
|
||
dropshipMatch.frame,
|
||
dropshipVisible,
|
||
order,
|
||
defaultAddressState,
|
||
);
|
||
report(
|
||
"supplier.1688.address_guard.completed",
|
||
"1688收货信息已匹配当前订单,且已排除公司默认地址",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
report(
|
||
"supplier.1688.dropship.completed",
|
||
"1688一件代发已勾选,收货信息已切换为当前订单",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
await install1688AsyncRecorder(
|
||
dropshipMatch.frame,
|
||
purchaseOrderNo,
|
||
);
|
||
const confirmationTexts = [];
|
||
let latestAsyncRecords = [];
|
||
let latestRecorderState = {
|
||
records: [],
|
||
confirmations: [],
|
||
messages: [],
|
||
};
|
||
let submissionAccepted = false;
|
||
let submissionAcceptedReported = false;
|
||
const nativeDialogGuard = create1688NativeDialogGuard(
|
||
purchaseOrderNo,
|
||
report,
|
||
confirmationTexts,
|
||
);
|
||
try {
|
||
report(
|
||
"supplier.1688.save.started",
|
||
"正在保存1688下单导航",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
await click1688SaveWithOverlayRecovery(
|
||
dropshipMatch.frame,
|
||
purchaseOrderNo,
|
||
report,
|
||
);
|
||
await nativeDialogGuard.drain();
|
||
await confirm1688DialogsAfterSave(
|
||
dropshipMatch.frame,
|
||
purchaseOrderNo,
|
||
report,
|
||
15000,
|
||
);
|
||
await nativeDialogGuard.drain();
|
||
report(
|
||
"supplier.1688.save.completed",
|
||
"1688下单保存操作已提交",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
report(
|
||
"supplier.1688.order.waiting",
|
||
"正在等待1688下单确认和线上单号",
|
||
"waiting",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
report(
|
||
"supplier.1688.verify.started",
|
||
"正在回读采购单并复核1688线上单号和下单状态",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
const onlineDeadline = Date.now() + ONLINE_VERIFY_TIMEOUT;
|
||
let nextRefreshAt = Date.now() + 5000;
|
||
while (Date.now() < onlineDeadline) {
|
||
await nativeDialogGuard.drain();
|
||
const currentRecorderState = await read1688RecorderState(
|
||
dropshipMatch.frame,
|
||
).catch(() => null);
|
||
if (currentRecorderState) {
|
||
latestRecorderState = currentRecorderState;
|
||
if (currentRecorderState.records.length) {
|
||
latestAsyncRecords = currentRecorderState.records;
|
||
}
|
||
}
|
||
const submission = inspect1688SubmissionRecords(latestAsyncRecords);
|
||
if (submission.state === "rejected") {
|
||
logInternalError(
|
||
new Error(
|
||
`1688 submission was rejected: ${submission.reason}`,
|
||
),
|
||
`1688-submit-${purchaseOrderNo}`,
|
||
);
|
||
throw new Error("1688 submission was rejected");
|
||
}
|
||
if (submission.state === "accepted") {
|
||
submissionAccepted = true;
|
||
if (!submissionAcceptedReported) {
|
||
submissionAcceptedReported = true;
|
||
report(
|
||
"supplier.1688.submission.accepted",
|
||
"ERP已确认接收1688下单请求",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
}
|
||
}
|
||
const recordedSuccess = exact1688SuccessMessage(
|
||
latestRecorderState.messages,
|
||
purchaseOrderNo,
|
||
);
|
||
if (submissionAccepted && recordedSuccess) {
|
||
await closeVisibleEpaasDialogs().catch(() => {});
|
||
report(
|
||
"supplier.1688.dialog.success",
|
||
"ERP已确认目标采购单下单成功",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
report(
|
||
"supplier.1688.verify.completed",
|
||
"ERP后端回执和目标采购单成功提示复核通过",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
report(
|
||
"supplier.1688.order.success",
|
||
"1688一件代发下单完成",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
return {
|
||
onlineOrderNo: "",
|
||
reused: false,
|
||
submissionAcknowledged: true,
|
||
submissionPurchaseOrderNo: purchaseOrderNo,
|
||
completionEvidence: "erp_backend_and_success_dialog",
|
||
};
|
||
}
|
||
rowBody = await readPurchaseRow(purchaseFrame, purchaseOrderNo);
|
||
const verification = verified1688Order(rowBody);
|
||
if (verification.verified) {
|
||
await closeVisibleEpaasDialogs().catch(() => {});
|
||
report(
|
||
"supplier.1688.verify.completed",
|
||
"1688线上单号和下单状态复核通过",
|
||
"completed",
|
||
{
|
||
purchase_order_no: purchaseOrderNo,
|
||
online_1688_order_no: verification.onlineOrderNo,
|
||
},
|
||
);
|
||
report(
|
||
"supplier.1688.order.success",
|
||
"1688一件代发下单完成",
|
||
"completed",
|
||
{
|
||
purchase_order_no: purchaseOrderNo,
|
||
online_1688_order_no: verification.onlineOrderNo,
|
||
},
|
||
);
|
||
return {
|
||
onlineOrderNo: verification.onlineOrderNo,
|
||
reused: false,
|
||
};
|
||
}
|
||
let confirmation;
|
||
for (const candidate of page.frames()) {
|
||
if (!(await frameChainVisible(candidate).catch(() => false))) continue;
|
||
const controls = await firstVisible(
|
||
candidate.locator("#confirm_confirm"),
|
||
);
|
||
if (!controls.length) continue;
|
||
if (controls.length > 1 || confirmation) {
|
||
throw new Error("1688 confirmation control is ambiguous");
|
||
}
|
||
confirmation = { frame: candidate, item: controls[0] };
|
||
}
|
||
if (confirmation) {
|
||
report(
|
||
"supplier.1688.confirm.started",
|
||
"正在确认1688下单弹窗",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
confirmationTexts.push(
|
||
clean(
|
||
await confirmation.frame.locator("body").innerText().catch(() => ""),
|
||
1000,
|
||
),
|
||
);
|
||
await confirmation.item.click();
|
||
report(
|
||
"supplier.1688.confirm.completed",
|
||
"1688下单确认已提交",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
await purchaseFrame.waitForTimeout(900);
|
||
continue;
|
||
}
|
||
let visibleDialog = false;
|
||
let visibleDialogBody = "";
|
||
let clickedDialog = false;
|
||
for (const candidate of page.frames()) {
|
||
if (
|
||
!candidate.url().includes("/epaas-dialog-frame.html") ||
|
||
!(await frameChainVisible(candidate).catch(() => false))
|
||
) {
|
||
continue;
|
||
}
|
||
visibleDialog = true;
|
||
const body = clean(
|
||
await candidate.locator("body").innerText().catch(() => ""),
|
||
1000,
|
||
);
|
||
visibleDialogBody = body;
|
||
if (body) confirmationTexts.push(body);
|
||
const dialogReportsSuccess = /下单成功/.test(body);
|
||
if (
|
||
dialogReportsSuccess &&
|
||
!body.includes(String(purchaseOrderNo))
|
||
) {
|
||
throw new Error("1688 success dialog purchase number mismatch");
|
||
}
|
||
const labels = dialogReportsSuccess
|
||
? ["关闭", "确定", "确认"]
|
||
: ["确认并继续", "确认", "确定", "继续"];
|
||
for (const label of labels) {
|
||
const controls = await firstVisible(
|
||
candidate.getByText(label, { exact: true }),
|
||
);
|
||
if (controls.length === 1) {
|
||
await controls[0].click({ force: true });
|
||
clickedDialog = true;
|
||
break;
|
||
}
|
||
}
|
||
if (clickedDialog) break;
|
||
}
|
||
if (visibleDialog) {
|
||
if (clickedDialog) {
|
||
report(
|
||
"supplier.1688.dialog.closed",
|
||
"1688下单结果弹窗已确认",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
}
|
||
if (clickedDialog && /下单成功/.test(visibleDialogBody)) {
|
||
const currentRecorderState = await read1688RecorderState(
|
||
dropshipMatch.frame,
|
||
).catch(() => null);
|
||
if (currentRecorderState) {
|
||
latestRecorderState = currentRecorderState;
|
||
if (currentRecorderState.records.length) {
|
||
latestAsyncRecords = currentRecorderState.records;
|
||
}
|
||
}
|
||
const dialogSubmission =
|
||
inspect1688SubmissionRecords(latestAsyncRecords);
|
||
if (dialogSubmission.state === "rejected") {
|
||
logInternalError(
|
||
new Error(
|
||
`1688 submission was rejected: ${dialogSubmission.reason}`,
|
||
),
|
||
`1688-submit-${purchaseOrderNo}`,
|
||
);
|
||
throw new Error("1688 submission was rejected");
|
||
}
|
||
if (dialogSubmission.state === "accepted") {
|
||
submissionAccepted = true;
|
||
if (!submissionAcceptedReported) {
|
||
submissionAcceptedReported = true;
|
||
report(
|
||
"supplier.1688.submission.accepted",
|
||
"ERP已确认接收1688下单请求",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
}
|
||
report(
|
||
"supplier.1688.dialog.success",
|
||
"ERP已确认目标采购单下单成功",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
report(
|
||
"supplier.1688.verify.completed",
|
||
"ERP后端回执和目标采购单成功弹窗复核通过",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
report(
|
||
"supplier.1688.order.success",
|
||
"1688一件代发下单完成",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
return {
|
||
onlineOrderNo: "",
|
||
reused: false,
|
||
submissionAcknowledged: true,
|
||
submissionPurchaseOrderNo: purchaseOrderNo,
|
||
completionEvidence: "erp_backend_and_success_dialog",
|
||
};
|
||
}
|
||
report(
|
||
"supplier.1688.dialog.success",
|
||
"ERP已提示下单成功,正在核对后端下单回执",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
}
|
||
if (
|
||
clickedDialog &&
|
||
!/下单成功/.test(visibleDialogBody) &&
|
||
/失败|错误|无法|不能为空|已下架|库存不足|不支持|异常/.test(
|
||
visibleDialogBody,
|
||
)
|
||
) {
|
||
logInternalError(
|
||
new Error(
|
||
`1688 submission was rejected: ${visibleDialogBody}`,
|
||
),
|
||
`1688-submit-${purchaseOrderNo}`,
|
||
);
|
||
throw new Error("1688 submission was rejected");
|
||
}
|
||
await purchaseFrame.waitForTimeout(clickedDialog ? 900 : 500);
|
||
continue;
|
||
}
|
||
if (Date.now() >= nextRefreshAt) {
|
||
const searchControls = await firstVisible(
|
||
purchaseFrame.getByText("搜索", { exact: true }),
|
||
);
|
||
if (searchControls.length === 1) {
|
||
await searchControls[0].click().catch(() => {});
|
||
await purchaseFrame.waitForTimeout(1200);
|
||
}
|
||
nextRefreshAt = Date.now() + 5000;
|
||
continue;
|
||
}
|
||
await purchaseFrame.waitForTimeout(400);
|
||
}
|
||
rowBody = await readPurchaseRow(purchaseFrame, purchaseOrderNo).catch(
|
||
() => "",
|
||
);
|
||
const finalVerification = verified1688Order(rowBody);
|
||
if (finalVerification.verified) {
|
||
report(
|
||
"supplier.1688.verify.completed",
|
||
"1688线上单号和下单状态复核通过",
|
||
"completed",
|
||
{
|
||
purchase_order_no: purchaseOrderNo,
|
||
online_1688_order_no: finalVerification.onlineOrderNo,
|
||
},
|
||
);
|
||
report(
|
||
"supplier.1688.order.success",
|
||
"1688一件代发下单完成",
|
||
"completed",
|
||
{
|
||
purchase_order_no: purchaseOrderNo,
|
||
online_1688_order_no: finalVerification.onlineOrderNo,
|
||
},
|
||
);
|
||
return {
|
||
onlineOrderNo: finalVerification.onlineOrderNo,
|
||
reused: false,
|
||
};
|
||
}
|
||
const finalSubmission =
|
||
inspect1688SubmissionRecords(latestAsyncRecords);
|
||
if (finalSubmission.state === "rejected") {
|
||
logInternalError(
|
||
new Error(
|
||
`1688 submission was rejected: ${finalSubmission.reason}`,
|
||
),
|
||
`1688-submit-${purchaseOrderNo}`,
|
||
);
|
||
throw new Error("1688 submission was rejected");
|
||
}
|
||
submissionAccepted =
|
||
submissionAccepted ||
|
||
finalSubmission.state === "accepted" ||
|
||
confirmationTexts.some((text) => /下单成功/.test(text));
|
||
const existingOnlineOrderNotice = [
|
||
...confirmationTexts,
|
||
...(latestRecorderState.messages || []).map(
|
||
(item) => item?.message || item?.text || "",
|
||
),
|
||
].some((text) => /已经存在线上订单号/.test(String(text || "")));
|
||
if (existingOnlineOrderNotice) {
|
||
report(
|
||
"supplier.1688.already_completed",
|
||
"ERP确认该采购单已存在线上订单,无需重复下单",
|
||
"skipped",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
report(
|
||
"supplier.1688.order.success",
|
||
"1688一件代发订单已存在,本次未重复创建",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
return {
|
||
onlineOrderNo: "",
|
||
reused: true,
|
||
submissionAcknowledged: true,
|
||
submissionPurchaseOrderNo: purchaseOrderNo,
|
||
completionEvidence: "erp_existing_online_order_notice",
|
||
};
|
||
}
|
||
if (!submissionAccepted) {
|
||
logInternalError(
|
||
new Error(
|
||
`1688 submission confirmation was not received; ` +
|
||
`async_records=${JSON.stringify(
|
||
latestAsyncRecords,
|
||
).slice(0, 3000)}; ` +
|
||
`confirmations=${JSON.stringify(
|
||
latestRecorderState.confirmations,
|
||
).slice(0, 2000)}; ` +
|
||
`messages=${JSON.stringify(
|
||
latestRecorderState.messages,
|
||
).slice(0, 2000)}; ` +
|
||
`dialogs=${JSON.stringify(confirmationTexts).slice(0, 2000)}`,
|
||
),
|
||
`1688-submit-${purchaseOrderNo}`,
|
||
);
|
||
throw new Error("1688 submission confirmation was not received");
|
||
}
|
||
report(
|
||
"supplier.1688.verify.deferred",
|
||
"1688下单已提交,转入整批复核队列",
|
||
"waiting",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
return {
|
||
onlineOrderNo: "",
|
||
reused: false,
|
||
verificationPending: true,
|
||
};
|
||
} finally {
|
||
nativeDialogGuard.stop();
|
||
await nativeDialogGuard.drain().catch(() => {});
|
||
}
|
||
}
|
||
|
||
async function applySingle(orderFrame, order, report) {
|
||
report(
|
||
"order.recheck.started",
|
||
"正在做执行前二次校验",
|
||
"started",
|
||
);
|
||
const preflight = await inspectSingleOrder(orderFrame, order, report);
|
||
report(
|
||
"order.recheck.completed",
|
||
"执行前二次校验已完成",
|
||
"completed",
|
||
);
|
||
if (preflight.state === "resume_purchase") {
|
||
const purchaseOrderNo = clean(preflight.purchase_order_no, 40);
|
||
report(
|
||
"purchase.resume.detail.started",
|
||
"正在打开订单详情核对现有采购单号",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
let detailFrame = null;
|
||
let verifiedPurchaseNo = "";
|
||
const maxVerificationAttempts = 5;
|
||
for (
|
||
let attempt = 1;
|
||
attempt <= maxVerificationAttempts;
|
||
attempt += 1
|
||
) {
|
||
const orderLocator = await searchOrder(orderFrame, order.order_id);
|
||
detailFrame = await openOrderDetail(
|
||
orderFrame,
|
||
orderLocator,
|
||
order.order_id,
|
||
);
|
||
const detail = await waitForOrderDetailPurchase(
|
||
detailFrame,
|
||
purchaseOrderNo,
|
||
12000,
|
||
);
|
||
verifiedPurchaseNo = clean(detail.purchaseOrderNo, 40);
|
||
if (verifiedPurchaseNo === purchaseOrderNo) break;
|
||
if (verifiedPurchaseNo || attempt === maxVerificationAttempts) break;
|
||
await closeOrderDetail(detailFrame);
|
||
detailFrame = null;
|
||
report(
|
||
"purchase.resume.detail.retry",
|
||
"采购单号尚未加载,正在重新打开订单详情核对",
|
||
"started",
|
||
{
|
||
purchase_order_no: purchaseOrderNo,
|
||
attempt: attempt + 1,
|
||
max_attempts: maxVerificationAttempts,
|
||
},
|
||
);
|
||
await orderFrame.waitForTimeout(1000);
|
||
}
|
||
if (!verifiedPurchaseNo) {
|
||
logInternalError(
|
||
new Error(
|
||
`Existing purchase number was not loaded expected=${purchaseOrderNo} ` +
|
||
`url=${detailFrame?.url() || "CLOSED"}`,
|
||
),
|
||
`purchase-number-${clean(order.order_id, 40)}`,
|
||
);
|
||
throw new Error("Existing purchase number was not loaded");
|
||
}
|
||
if (verifiedPurchaseNo !== purchaseOrderNo) {
|
||
logInternalError(
|
||
new Error(
|
||
`Existing purchase number verification failed expected=${purchaseOrderNo} ` +
|
||
`actual=${verifiedPurchaseNo} url=${detailFrame?.url() || "CLOSED"}`,
|
||
),
|
||
`purchase-number-${clean(order.order_id, 40)}`,
|
||
);
|
||
throw new Error("Existing purchase number verification failed");
|
||
}
|
||
report(
|
||
"purchase.resume.number.verified",
|
||
"现有采购单号核对通过",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
report(
|
||
"purchase.management.open.started",
|
||
"正在进入采购单管理页面",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
const purchaseFrame = await openPurchaseFromDetail(
|
||
detailFrame,
|
||
purchaseOrderNo,
|
||
);
|
||
report(
|
||
"purchase.management.open.completed",
|
||
"采购单管理页面已打开",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
await auditPurchase(purchaseFrame, purchaseOrderNo, report);
|
||
const placement = await place1688Order(
|
||
purchaseFrame,
|
||
purchaseOrderNo,
|
||
order,
|
||
report,
|
||
);
|
||
const onlineOrderNo = placement.onlineOrderNo;
|
||
if (placement.submissionAcknowledged) {
|
||
return {
|
||
order_id: order.order_id,
|
||
state: "completed",
|
||
purchase_order_no: purchaseOrderNo,
|
||
online_1688_order_no: "",
|
||
submission_acknowledged: true,
|
||
submission_purchase_order_no:
|
||
placement.submissionPurchaseOrderNo,
|
||
completion_evidence: placement.completionEvidence,
|
||
message: "完成",
|
||
};
|
||
}
|
||
if (placement.verificationPending) {
|
||
return {
|
||
order_id: order.order_id,
|
||
state: "verification_pending",
|
||
purchase_order_no: purchaseOrderNo,
|
||
online_1688_order_no: "",
|
||
message: "已提交1688下单,等待整批复核",
|
||
};
|
||
}
|
||
if (!/^\d{16,30}$/.test(onlineOrderNo)) {
|
||
throw new Error("1688 online order verification failed");
|
||
}
|
||
return {
|
||
order_id: order.order_id,
|
||
state: "completed",
|
||
purchase_order_no: purchaseOrderNo,
|
||
online_1688_order_no: onlineOrderNo,
|
||
message: "完成",
|
||
};
|
||
}
|
||
if (preflight.state !== "ready") {
|
||
return {
|
||
order_id: order.order_id,
|
||
state: "skipped",
|
||
safe_to_continue: true,
|
||
purchase_order_no: preflight.purchase_order_no || "",
|
||
message:
|
||
preflight.state === "already_processed"
|
||
? "已存在无法安全识别单号的采购记录,已阻止重复执行"
|
||
: preflight.message || "二次校验未通过",
|
||
};
|
||
}
|
||
let orderLocator = await searchOrder(orderFrame, order.order_id);
|
||
report("address.detail.open.started", "正在打开订单详情修改收货地址", "started");
|
||
let detailFrame = await openOrderDetail(
|
||
orderFrame,
|
||
orderLocator,
|
||
order.order_id,
|
||
);
|
||
report("address.detail.open.completed", "收货地址编辑页面已打开", "completed");
|
||
await fillAndSaveAddress(detailFrame, order, report);
|
||
await closeOrderDetail(detailFrame);
|
||
report("address.detail.closed", "收货地址编辑页面已关闭", "completed");
|
||
await verifyPersistedOrderAddress(orderFrame, order, report);
|
||
|
||
orderLocator = await searchOrder(orderFrame, order.order_id);
|
||
await generatePurchaseOrder(orderFrame, orderLocator, report);
|
||
|
||
report(
|
||
"purchase.number.detail.started",
|
||
"正在打开订单详情读取生成采购单号",
|
||
"started",
|
||
);
|
||
let purchaseOrderNo = "";
|
||
const maxPurchaseNumberAttempts = 5;
|
||
for (
|
||
let attempt = 1;
|
||
attempt <= maxPurchaseNumberAttempts;
|
||
attempt += 1
|
||
) {
|
||
orderLocator = await searchOrder(orderFrame, order.order_id);
|
||
detailFrame = await openOrderDetail(
|
||
orderFrame,
|
||
orderLocator,
|
||
order.order_id,
|
||
);
|
||
const detail = await waitForOrderDetailPurchase(
|
||
detailFrame,
|
||
"",
|
||
15000,
|
||
);
|
||
purchaseOrderNo = clean(detail.purchaseOrderNo, 40);
|
||
if (purchaseOrderNo) break;
|
||
await closeOrderDetail(detailFrame);
|
||
detailFrame = null;
|
||
if (attempt < maxPurchaseNumberAttempts) {
|
||
report(
|
||
"purchase.number.detail.retry",
|
||
"采购单号尚未回写,正在重新打开订单详情读取",
|
||
"started",
|
||
{
|
||
attempt: attempt + 1,
|
||
max_attempts: maxPurchaseNumberAttempts,
|
||
},
|
||
);
|
||
await orderFrame.waitForTimeout(1500);
|
||
}
|
||
}
|
||
if (!purchaseOrderNo) throw new Error("Generated purchase number was not found");
|
||
report(
|
||
"purchase.number.read",
|
||
"已从订单操作日志读取采购单号",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
report(
|
||
"purchase.management.open.started",
|
||
"正在进入采购单管理页面",
|
||
"started",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
const purchaseFrame = await openPurchaseFromDetail(
|
||
detailFrame,
|
||
purchaseOrderNo,
|
||
);
|
||
report(
|
||
"purchase.management.open.completed",
|
||
"采购单管理页面已打开",
|
||
"completed",
|
||
{ purchase_order_no: purchaseOrderNo },
|
||
);
|
||
await auditPurchase(purchaseFrame, purchaseOrderNo, report);
|
||
const placement = await place1688Order(
|
||
purchaseFrame,
|
||
purchaseOrderNo,
|
||
order,
|
||
report,
|
||
);
|
||
const onlineOrderNo = placement.onlineOrderNo;
|
||
if (placement.submissionAcknowledged) {
|
||
return {
|
||
order_id: order.order_id,
|
||
state: "completed",
|
||
purchase_order_no: purchaseOrderNo,
|
||
online_1688_order_no: "",
|
||
submission_acknowledged: true,
|
||
submission_purchase_order_no:
|
||
placement.submissionPurchaseOrderNo,
|
||
completion_evidence: placement.completionEvidence,
|
||
message: "完成",
|
||
};
|
||
}
|
||
if (placement.verificationPending) {
|
||
return {
|
||
order_id: order.order_id,
|
||
state: "verification_pending",
|
||
purchase_order_no: purchaseOrderNo,
|
||
online_1688_order_no: "",
|
||
message: "已提交1688下单,等待整批复核",
|
||
};
|
||
}
|
||
if (!/^\d{16,30}$/.test(onlineOrderNo)) {
|
||
throw new Error("1688 online order verification failed");
|
||
}
|
||
return {
|
||
order_id: order.order_id,
|
||
state: "completed",
|
||
purchase_order_no: purchaseOrderNo,
|
||
online_1688_order_no: onlineOrderNo,
|
||
message: "完成",
|
||
};
|
||
}
|
||
|
||
async function read1688VerificationForOrder(result) {
|
||
const orderId = clean(result.order_id, 40);
|
||
const expectedPurchaseOrderNo = clean(result.purchase_order_no, 40);
|
||
await resetToOrderList();
|
||
const orderFrame = await openOrderList();
|
||
const orderLocator = await searchOrder(orderFrame, orderId);
|
||
if (!orderLocator) throw new Error("Verification order was not found");
|
||
const detailFrame = await openOrderDetail(
|
||
orderFrame,
|
||
orderLocator,
|
||
orderId,
|
||
);
|
||
const detail = await waitForOrderDetailPurchase(
|
||
detailFrame,
|
||
expectedPurchaseOrderNo,
|
||
15000,
|
||
);
|
||
const actualPurchaseOrderNo = clean(detail.purchaseOrderNo, 40);
|
||
if (actualPurchaseOrderNo !== expectedPurchaseOrderNo) {
|
||
throw new Error("Verification purchase number mismatch");
|
||
}
|
||
const purchaseFrame = await openPurchaseFromDetail(
|
||
detailFrame,
|
||
expectedPurchaseOrderNo,
|
||
);
|
||
const rowBody = await readPurchaseRow(
|
||
purchaseFrame,
|
||
expectedPurchaseOrderNo,
|
||
);
|
||
return verified1688Order(rowBody);
|
||
}
|
||
|
||
async function reconcile1688Order(payload) {
|
||
const orderId = clean(payload.order_id, 40);
|
||
const purchaseOrderNo = clean(payload.purchase_order_no, 60);
|
||
if (!/^\d{12,30}$/.test(orderId) || !purchaseOrderNo) {
|
||
throw new Error("Reconciliation order identity is invalid");
|
||
}
|
||
writeProgress({
|
||
phase: "reconcile",
|
||
current_order_id: orderId,
|
||
step_code: "supplier.1688.background_verify.started",
|
||
step_state: "started",
|
||
message: "正在后台补录1688线上单号",
|
||
purchase_order_no: purchaseOrderNo,
|
||
online_1688_order_no: "",
|
||
});
|
||
const auth = await ensureLoggedIn();
|
||
if (auth.status !== "ok") {
|
||
return {
|
||
...auth,
|
||
order_id: orderId,
|
||
purchase_order_no: purchaseOrderNo,
|
||
verified: false,
|
||
online_1688_order_no: "",
|
||
};
|
||
}
|
||
const verification = await read1688VerificationForOrder({
|
||
order_id: orderId,
|
||
purchase_order_no: purchaseOrderNo,
|
||
});
|
||
writeProgress({
|
||
phase: "reconcile",
|
||
current_order_id: orderId,
|
||
step_code: verification.verified
|
||
? "supplier.1688.background_verify.completed"
|
||
: "supplier.1688.background_verify.waiting",
|
||
step_state: verification.verified ? "completed" : "waiting",
|
||
message: verification.verified
|
||
? "1688线上单号已补录"
|
||
: "1688线上单号尚未回写,稍后继续复核",
|
||
purchase_order_no: purchaseOrderNo,
|
||
online_1688_order_no: verification.onlineOrderNo || "",
|
||
});
|
||
return {
|
||
status: "ok",
|
||
order_id: orderId,
|
||
purchase_order_no: purchaseOrderNo,
|
||
verified: verification.verified,
|
||
online_1688_order_no: verification.onlineOrderNo || "",
|
||
};
|
||
}
|
||
|
||
async function verifyPending1688Orders(
|
||
results,
|
||
progressOffset,
|
||
progressTotal,
|
||
) {
|
||
let pending = results.filter(
|
||
(result) => result.state === "verification_pending",
|
||
);
|
||
if (!pending.length) return;
|
||
writeProgress({
|
||
phase: "apply",
|
||
completed: progressOffset + results.length - pending.length,
|
||
total: progressTotal,
|
||
current_order_id: "",
|
||
step_code: "supplier.1688.batch_verify.started",
|
||
step_state: "started",
|
||
message: `整批提交完成,开始复核 ${pending.length} 条1688线上单号`,
|
||
purchase_order_no: "",
|
||
online_1688_order_no: "",
|
||
});
|
||
const deadline = Date.now() + BATCH_VERIFY_TIMEOUT;
|
||
let attempt = 0;
|
||
while (
|
||
pending.length &&
|
||
Date.now() < deadline &&
|
||
attempt < BATCH_VERIFY_MAX_ATTEMPTS
|
||
) {
|
||
attempt += 1;
|
||
for (const result of [...pending]) {
|
||
if (Date.now() >= deadline) break;
|
||
writeProgress({
|
||
phase: "apply",
|
||
completed: progressOffset + results.filter(
|
||
(item) => item.state === "completed",
|
||
).length,
|
||
total: progressTotal,
|
||
current_order_id: clean(result.order_id, 40),
|
||
step_code: "supplier.1688.batch_verify.order.started",
|
||
step_state: "started",
|
||
message: `正在进行第 ${attempt} 轮1688线上单号复核`,
|
||
purchase_order_no: clean(result.purchase_order_no, 60),
|
||
online_1688_order_no: "",
|
||
});
|
||
let verification = { onlineOrderNo: "", verified: false };
|
||
try {
|
||
verification = await read1688VerificationForOrder(result);
|
||
} catch (error) {
|
||
logInternalError(
|
||
error,
|
||
`batch-verify-${clean(result.order_id, 40)}`,
|
||
);
|
||
}
|
||
if (verification.verified) {
|
||
result.state = "completed";
|
||
result.online_1688_order_no = verification.onlineOrderNo;
|
||
result.message = "完成";
|
||
writeProgress({
|
||
phase: "apply",
|
||
completed: progressOffset + results.filter(
|
||
(item) => item.state === "completed",
|
||
).length,
|
||
total: progressTotal,
|
||
current_order_id: clean(result.order_id, 40),
|
||
step_code: "supplier.1688.batch_verify.order.completed",
|
||
step_state: "completed",
|
||
message: "1688线上单号和下单状态复核通过",
|
||
purchase_order_no: clean(result.purchase_order_no, 60),
|
||
online_1688_order_no: verification.onlineOrderNo,
|
||
});
|
||
} else {
|
||
writeProgress({
|
||
phase: "apply",
|
||
completed: progressOffset + results.filter(
|
||
(item) => item.state === "completed",
|
||
).length,
|
||
total: progressTotal,
|
||
current_order_id: clean(result.order_id, 40),
|
||
step_code: "supplier.1688.batch_verify.order.waiting",
|
||
step_state: "waiting",
|
||
message: "1688线上单号尚未回写,稍后继续自动复核",
|
||
purchase_order_no: clean(result.purchase_order_no, 60),
|
||
online_1688_order_no: "",
|
||
});
|
||
}
|
||
}
|
||
pending = results.filter(
|
||
(result) => result.state === "verification_pending",
|
||
);
|
||
if (pending.length && Date.now() < deadline) {
|
||
await new Promise((resolve) =>
|
||
setTimeout(resolve, BATCH_VERIFY_POLL_INTERVAL),
|
||
);
|
||
}
|
||
}
|
||
for (const result of pending) {
|
||
result.state = "stopped";
|
||
result.message = "1688线上单号暂未确认,本单已停止并转后续核对";
|
||
writeProgress({
|
||
phase: "apply",
|
||
completed: progressOffset + results.filter(
|
||
(item) => item.state === "completed",
|
||
).length,
|
||
total: progressTotal,
|
||
current_order_id: clean(result.order_id, 40),
|
||
step_code: "supplier.1688.batch_verify.order.failed",
|
||
step_state: "failed",
|
||
message: result.message,
|
||
purchase_order_no: clean(result.purchase_order_no, 60),
|
||
online_1688_order_no: "",
|
||
});
|
||
}
|
||
writeProgress({
|
||
phase: "apply",
|
||
completed: progressOffset + results.filter(
|
||
(item) => item.state === "completed",
|
||
).length,
|
||
total: progressTotal,
|
||
current_order_id: "",
|
||
step_code: pending.length
|
||
? "supplier.1688.batch_verify.failed"
|
||
: "supplier.1688.batch_verify.completed",
|
||
step_state: pending.length ? "failed" : "completed",
|
||
message: pending.length
|
||
? `整批复核结束,${pending.length} 条未通过`
|
||
: "整批1688线上单号复核全部通过",
|
||
purchase_order_no: "",
|
||
online_1688_order_no: "",
|
||
});
|
||
}
|
||
|
||
async function applyBatch(payload) {
|
||
writeProgress({
|
||
phase: "apply",
|
||
step_code: "auth.check.started",
|
||
step_state: "started",
|
||
message: "正在检查 ERP 登录状态",
|
||
});
|
||
const auth = await ensureLoggedIn();
|
||
if (auth.status !== "ok") {
|
||
writeProgress({
|
||
phase: "apply",
|
||
step_code: "auth.check.waiting",
|
||
step_state: "waiting",
|
||
message: "ERP 登录等待短信验证码",
|
||
});
|
||
return auth;
|
||
}
|
||
writeProgress({
|
||
phase: "apply",
|
||
step_code: "auth.check.completed",
|
||
step_state: "completed",
|
||
message: "ERP 登录状态正常",
|
||
});
|
||
writeProgress({
|
||
phase: "apply",
|
||
step_code: "order.list.open.started",
|
||
step_state: "started",
|
||
message: "正在打开订单列表",
|
||
});
|
||
let orderFrame = await openOrderList();
|
||
writeProgress({
|
||
phase: "apply",
|
||
step_code: "order.list.open.completed",
|
||
step_state: "completed",
|
||
message: "订单列表已就绪",
|
||
});
|
||
const results = [];
|
||
const inputOrders = payload.orders || [];
|
||
const progressOffset = Number(payload.progress_offset || 0);
|
||
const progressTotal = Number(
|
||
payload.progress_total || inputOrders.length,
|
||
);
|
||
for (let index = 0; index < inputOrders.length; index += 1) {
|
||
const order = inputOrders[index];
|
||
const orderContext = {
|
||
purchase_order_no: "",
|
||
online_1688_order_no: "",
|
||
};
|
||
const report = (
|
||
stepCode,
|
||
message,
|
||
stepState = "started",
|
||
extra = {},
|
||
) => {
|
||
orderContext.last_step_code = stepCode;
|
||
if (stepMayHaveChangedExternalState(stepCode)) {
|
||
orderContext.external_state_may_have_changed = true;
|
||
}
|
||
if (SUBMISSION_RISK_STEPS.has(stepCode)) {
|
||
orderContext.submission_may_have_started = true;
|
||
}
|
||
if (extra.purchase_order_no) {
|
||
orderContext.purchase_order_no = clean(
|
||
extra.purchase_order_no,
|
||
60,
|
||
);
|
||
}
|
||
if (extra.online_1688_order_no) {
|
||
orderContext.online_1688_order_no = clean(
|
||
extra.online_1688_order_no,
|
||
80,
|
||
);
|
||
}
|
||
writeProgress({
|
||
phase: "apply",
|
||
completed: progressOffset + index,
|
||
total: progressTotal,
|
||
order_index: progressOffset + index + 1,
|
||
order_total: progressTotal,
|
||
current_order_id: clean(order.order_id, 40),
|
||
step_code: stepCode,
|
||
step_state: stepState,
|
||
message,
|
||
purchase_order_no: orderContext.purchase_order_no,
|
||
online_1688_order_no: orderContext.online_1688_order_no,
|
||
...extra,
|
||
});
|
||
};
|
||
try {
|
||
report("order.apply.started", "开始处理当前订单", "started");
|
||
const result = await applySingle(orderFrame, order, report);
|
||
results.push(result);
|
||
writeProgress({
|
||
phase: "apply",
|
||
completed: progressOffset + index + 1,
|
||
total: progressTotal,
|
||
order_index: progressOffset + index + 1,
|
||
order_total: progressTotal,
|
||
current_order_id: clean(order.order_id, 40),
|
||
step_code:
|
||
result.state === "completed"
|
||
? "order.apply.completed"
|
||
: result.state === "verification_pending"
|
||
? "order.apply.awaiting_verification"
|
||
: "order.apply.skipped",
|
||
step_state:
|
||
result.state === "completed"
|
||
? "completed"
|
||
: result.state === "verification_pending"
|
||
? "waiting"
|
||
: "skipped",
|
||
message:
|
||
result.state === "verification_pending"
|
||
? "当前订单已提交,继续处理下一条并在整批结束后复核"
|
||
: result.state === "skipped" && result.safe_to_continue
|
||
? "当前订单已安全跳过,继续处理后续订单"
|
||
: "当前订单处理完成",
|
||
purchase_order_no: clean(result.purchase_order_no, 60),
|
||
online_1688_order_no: clean(
|
||
result.online_1688_order_no,
|
||
80,
|
||
),
|
||
});
|
||
if (
|
||
!["completed", "verification_pending"].includes(result.state) &&
|
||
!(result.state === "skipped" && result.safe_to_continue)
|
||
) {
|
||
return { status: "stopped", orders: results };
|
||
}
|
||
if (index < inputOrders.length - 1) {
|
||
report(
|
||
"browser.recycle.started",
|
||
"正在复位浏览器以处理下一条订单",
|
||
"started",
|
||
{ completed: progressOffset + index + 1 },
|
||
);
|
||
await recycleBrowserToOrderList();
|
||
report(
|
||
"browser.recycle.completed",
|
||
"浏览器已回到订单列表",
|
||
"completed",
|
||
{ completed: progressOffset + index + 1 },
|
||
);
|
||
orderFrame = await openOrderList();
|
||
}
|
||
} catch (error) {
|
||
logInternalError(error, `apply-${clean(order.order_id, 40)}`);
|
||
const explicitlySafe = error?.safeToContinue === true;
|
||
const verificationPending = Boolean(
|
||
orderContext.submission_may_have_started &&
|
||
orderContext.purchase_order_no,
|
||
);
|
||
const needsManualReview = Boolean(
|
||
verificationPending ||
|
||
orderContext.external_state_may_have_changed ||
|
||
orderContext.purchase_order_no,
|
||
);
|
||
const failureMessage = publicFailure(
|
||
error,
|
||
explicitlySafe && !needsManualReview
|
||
? "当前订单保存前校验未通过,已跳过并继续后续订单"
|
||
: verificationPending
|
||
? "当前订单的1688提交结果不明确,已转入待复核;后续订单继续处理"
|
||
: needsManualReview
|
||
? "当前订单执行异常,已隔离待人工复核;后续订单继续处理"
|
||
: "当前订单执行异常,已跳过并继续后续订单",
|
||
);
|
||
report(
|
||
verificationPending
|
||
? "order.apply.awaiting_verification"
|
||
: "order.apply.skipped",
|
||
failureMessage,
|
||
verificationPending ? "waiting" : "skipped",
|
||
);
|
||
results.push({
|
||
order_id: clean(order.order_id, 40),
|
||
state: verificationPending ? "verification_pending" : "skipped",
|
||
safe_to_continue: true,
|
||
needs_manual_review: needsManualReview,
|
||
retry_automatically: false,
|
||
purchase_order_no: orderContext.purchase_order_no,
|
||
online_1688_order_no: orderContext.online_1688_order_no,
|
||
message: failureMessage,
|
||
});
|
||
if (index < inputOrders.length - 1) {
|
||
report(
|
||
"browser.recycle.started",
|
||
"正在复位浏览器以处理下一条订单",
|
||
"started",
|
||
{ completed: progressOffset + index + 1 },
|
||
);
|
||
try {
|
||
await recycleBrowserToOrderList();
|
||
report(
|
||
"browser.recycle.completed",
|
||
"浏览器已回到订单列表",
|
||
"completed",
|
||
{ completed: progressOffset + index + 1 },
|
||
);
|
||
orderFrame = await openOrderList();
|
||
} catch (recycleError) {
|
||
logInternalError(
|
||
recycleError,
|
||
`recycle-${clean(order.order_id, 40)}`,
|
||
);
|
||
report(
|
||
"browser.recycle.failed",
|
||
"浏览器复位失败,当前工作器无法继续后续订单",
|
||
"failed",
|
||
{ completed: progressOffset + index + 1 },
|
||
);
|
||
return { status: "stopped", orders: results };
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
}
|
||
await verifyPending1688Orders(
|
||
results,
|
||
progressOffset,
|
||
progressTotal,
|
||
);
|
||
return {
|
||
status: results.every((result) => result.state === "completed")
|
||
? "ok"
|
||
: results.every(
|
||
(result) =>
|
||
result.state === "completed" ||
|
||
(result.state === "skipped" && result.safe_to_continue),
|
||
)
|
||
? "partial"
|
||
: "stopped",
|
||
orders: results,
|
||
};
|
||
}
|
||
|
||
async function handleRequest(request) {
|
||
const action = clean(request.action, 40);
|
||
const payload = request.payload || {};
|
||
if (action === "warmup") {
|
||
const result = await warmBrowserAtOrderList();
|
||
result.browser_code = BROWSER_CODE;
|
||
return result;
|
||
}
|
||
if (action === "debug_additional") {
|
||
return debugAdditionalControls(payload);
|
||
}
|
||
if (action === "inspect_purchase") {
|
||
return inspectPurchaseForOrder(payload);
|
||
}
|
||
if (action === "inspect_audit") {
|
||
return inspectAuditForOrder(payload);
|
||
}
|
||
if (action === "inspect_purchase_no") {
|
||
return inspectAuditForPurchase(payload);
|
||
}
|
||
if (action === "inspect_1688_nav") {
|
||
return inspect1688Navigation(payload);
|
||
}
|
||
if (action === "inspect_row") {
|
||
return inspectOrderRow(payload);
|
||
}
|
||
if (
|
||
action === "inspect" ||
|
||
action === "apply" ||
|
||
action === "reconcile"
|
||
) {
|
||
const result =
|
||
action === "inspect"
|
||
? await inspectBatch(payload)
|
||
: action === "apply"
|
||
? await applyBatch(payload)
|
||
: await reconcile1688Order(payload);
|
||
result.browser_code = BROWSER_CODE;
|
||
if (result.status !== "needs_sms") {
|
||
try {
|
||
writeProgress({
|
||
phase: action,
|
||
current_order_id: "",
|
||
order_index: 0,
|
||
step_code: "browser.reset.started",
|
||
step_state: "started",
|
||
message: "正在关闭多余窗口并复位到订单页面",
|
||
});
|
||
const continuesToApply =
|
||
action === "inspect" &&
|
||
clean(result.status, 20) === "ok" &&
|
||
(result.orders || []).some((row) =>
|
||
["ready", "resume_purchase"].includes(clean(row.state, 30)),
|
||
);
|
||
if (continuesToApply) {
|
||
await resetToOrderList();
|
||
} else {
|
||
await recycleBrowserToOrderList();
|
||
}
|
||
result.browser_reset = "order_list";
|
||
writeProgress({
|
||
phase: action,
|
||
current_order_id: "",
|
||
order_index: 0,
|
||
step_code: "browser.reset.completed",
|
||
step_state: "completed",
|
||
message: "浏览器已复位到订单页面并可接收下一任务",
|
||
});
|
||
} catch (error) {
|
||
logInternalError(error, "reset");
|
||
writeProgress({
|
||
phase: action,
|
||
current_order_id: "",
|
||
order_index: 0,
|
||
step_code: "browser.reset.failed",
|
||
step_state: "failed",
|
||
message: "浏览器复位失败,需要重新初始化该浏览器",
|
||
});
|
||
result.status = result.status === "ok" ? "stopped" : result.status;
|
||
result.message =
|
||
result.message ||
|
||
"任务已停止,浏览器未能复位到订单页面,需人工检查后再接新任务。";
|
||
result.browser_reset = "failed";
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
if (action === "close") {
|
||
await context?.close().catch(() => {});
|
||
process.exit(0);
|
||
}
|
||
return { status: "error", message: "未知 ERP 操作" };
|
||
}
|
||
|
||
function writeResponse(response) {
|
||
process.stdout.write(
|
||
`${JSON.stringify({ type: "result", result: response })}\n`,
|
||
);
|
||
}
|
||
|
||
function writeProgress(progress) {
|
||
process.stdout.write(
|
||
`${JSON.stringify({
|
||
type: "progress",
|
||
browser_code: BROWSER_CODE,
|
||
...progress,
|
||
})}\n`,
|
||
);
|
||
}
|
||
|
||
const input = readline.createInterface({
|
||
input: process.stdin,
|
||
crlfDelay: Infinity,
|
||
});
|
||
|
||
input.on("line", async (line) => {
|
||
let request;
|
||
try {
|
||
request = JSON.parse(line);
|
||
} catch {
|
||
writeResponse({ status: "error", message: "请求格式错误" });
|
||
return;
|
||
}
|
||
try {
|
||
const result = await handleRequest(request);
|
||
writeResponse(result);
|
||
} catch (error) {
|
||
logInternalError(error, clean(request?.action, 20) || "request");
|
||
writeResponse({
|
||
status: "error",
|
||
message: publicFailure(error, "ERP 操作失败,未继续执行"),
|
||
});
|
||
}
|
||
});
|
||
|
||
process.on("SIGTERM", async () => {
|
||
await context?.close().catch(() => {});
|
||
process.exit(0);
|
||
});
|