import type { Page } from "playwright"; import { isRpaAddressMockMode } from "@/lib/rpa/address-mode"; import { isRpaHumanSubmitStrict, isRpaVisualRushMode } from "@/lib/rpa/env"; import { clickFirstVisibleFromSpecs, getSelectorSpecs, } from "@/lib/rpa/selector-specs"; import { RpaError } from "@/modules/rpa/errors"; import { assertCargoCommittedState, assertBothAddressesLocked, } from "@/workers/rpa/cargo-lock"; import { debugBusinessStateSnapshot, waitForBusinessReadyState, } from "@/workers/rpa/quote-capture/quote-business-ready"; import { waitForPostSubmitDestination } from "@/workers/rpa/quote-capture/post-submit-navigation"; import type { QuoteSubmitOptions } from "@/workers/rpa/quote-capture/types"; const WIDGET_SELECTOR = "#react-quoter-landing-plugin"; /** * DOM 仅用于提交动作;填表由 kernel fillAddress / fillCargo 完成。 * submit 前必须通过 cargo committed + business-ready 闸门。 */ export async function submitQuoteForm( page: Page, options?: QuoteSubmitOptions, ): Promise { const rush = isRpaVisualRushMode(); const humanStrict = isRpaHumanSubmitStrict(); if (rush && !humanStrict) { console.log("[rpa] visual-rush: 跳过 submit 前全部闸门,直接点报价"); await page.keyboard.press("Escape").catch(() => undefined); await page.waitForTimeout(200); await page.keyboard.press("Escape").catch(() => undefined); await page.waitForTimeout(200); const clicked = await clickFirstVisibleFromSpecs( page, getSelectorSpecs("RPA_SELECTOR_SUBMIT"), ); if (!clicked) { throw new RpaError("STRUCT_CHANGE", "提交按钮不可见", { retryable: false, }); } const resultUrl = await waitForPostSubmitDestination(page); console.log(`[rpa] post-submit destination ok url=${resultUrl}`); return; } if (rush && humanStrict) { console.log("[rpa] human-submit-strict: rush 填表 + 完整 submit 闸门"); if (!isRpaAddressMockMode() && !options?.skipAddressLock) { await assertBothAddressesLocked(page, WIDGET_SELECTOR); } await assertCargoCommittedState(page, WIDGET_SELECTOR); const snapshot = await waitForBusinessReadyState(page, options); if (!snapshot.ready) { await debugBusinessStateSnapshot(page); throw new RpaError( "QUOTE_ENTRY_UNAVAILABLE", `禁止提交:business-ready=false(${snapshot.reasons.join(";")})`, { retryable: true }, ); } const clicked = await clickFirstVisibleFromSpecs( page, getSelectorSpecs("RPA_SELECTOR_SUBMIT"), ); if (!clicked) { throw new RpaError("STRUCT_CHANGE", "提交按钮不可见", { retryable: false }); } const resultUrl = await waitForPostSubmitDestination(page); console.log(`[rpa] post-submit destination ok url=${resultUrl}`); return; } if (!isRpaAddressMockMode() && !options?.skipAddressLock) { await assertBothAddressesLocked(page, WIDGET_SELECTOR); } else if (options?.skipAddressLock) { console.log("[rpa] axel-place-prefetch: 跳过 submit 前双地址锁定闸门"); } else { console.log("[rpa] address-mock: 跳过 submit 前双地址锁定闸门"); } await assertCargoCommittedState(page, WIDGET_SELECTOR); const snapshot = await waitForBusinessReadyState(page, options); if (!snapshot.ready) { await debugBusinessStateSnapshot(page); throw new RpaError( "QUOTE_ENTRY_UNAVAILABLE", `禁止提交:business-ready=false(${snapshot.reasons.join(";")})`, { retryable: true }, ); } const clicked = await clickFirstVisibleFromSpecs( page, getSelectorSpecs("RPA_SELECTOR_SUBMIT"), ); if (!clicked) { throw new RpaError("STRUCT_CHANGE", "提交按钮不可见", { retryable: false, }); } if (isRpaAddressMockMode()) { console.log( `[rpa] address-mock: 跳过 post-submit URL 等待(url=${page.url()})`, ); await page.waitForTimeout(1_500); return; } const resultUrl = await waitForPostSubmitDestination(page); console.log(`[rpa] post-submit destination ok url=${resultUrl}`); } export { debugBusinessStateSnapshot, waitForBusinessReadyState };