5460 lines
171 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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);
});