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