import type { Locator } from "playwright"; import { RPA_STREET_INPUT_CLICK_MS } from "@/lib/constants/rpa"; import { RpaError } from "@/modules/rpa/errors"; import { dismissAddressSuggestionDropdown } from "@/workers/rpa/address-commit"; import { AddressCommitStateMachine } from "@/workers/rpa/address-adapter/commit-state-machine"; import { createEmptyDiagnostics, logAddressSelectDiagnostics, } from "@/workers/rpa/address-adapter/diagnostics"; import type { AddressAdapter, AddressCommitResult, AddressSelectDiagnostics, SelectAddressParams, } from "@/workers/rpa/address-adapter/types"; const MOCK_STREET_FILL_MS = Math.min(RPA_STREET_INPUT_CLICK_MS, 5_000); /** 链路探通:仅 fill 街道文本,不等待 MotherShip 联想/place API */ async function mockTypeStreetInput( street: Locator, text: string, ): Promise { const value = text.replace(/\s+/g, " ").trim().slice(0, 160); await street.click({ timeout: MOCK_STREET_FILL_MS }).catch(() => undefined); await street.fill("", { timeout: MOCK_STREET_FILL_MS }).catch(() => undefined); await street .fill(value, { timeout: MOCK_STREET_FILL_MS }) .catch(() => undefined); await street.press("Escape").catch(() => undefined); await street.press("Tab").catch(() => undefined); } /** * mock 模式:快速填入地址文本并合成 commit,不触发 MotherShip place 闸门。 * 下游 cargo / submit / quote parser 仍真实执行。 */ export class MockAddressAdapter implements AddressAdapter { private readonly stateMachine = new AddressCommitStateMachine(); private result: AddressCommitResult | null = null; private diagnostics: AddressSelectDiagnostics = createEmptyDiagnostics(""); getCommitState() { return this.stateMachine.getState(); } getCommittedAddress(): AddressCommitResult | null { return this.result; } getDiagnostics(): AddressSelectDiagnostics { return { ...this.diagnostics, commitState: this.stateMachine.getState(), }; } async selectAddress(params: SelectAddressParams): Promise { const { ctx, side, address, street } = params; const page = ctx.page; const label = address.mothershipDisplayLabel?.trim() || address.formattedAddress?.trim() || address.street; const streetText = address.formattedAddress?.trim() || `${address.street}, ${address.city ?? ""}, ${address.state ?? ""} ${address.zip ?? ""}`.trim(); this.diagnostics = createEmptyDiagnostics(label, "EMPTY"); this.stateMachine.transition("TYPING"); this.diagnostics.method = "mock-fast-fill"; this.diagnostics.events.push("mock-mode-start"); await mockTypeStreetInput(street, streetText); await dismissAddressSuggestionDropdown(page, street); this.diagnostics.events.push("street-filled"); this.stateMachine.transition("COMMITTED"); const placeId = address.placeId?.trim() || `mock-place-${side}-${Date.now()}`; const formattedAddress = streetText || label; this.diagnostics.address = label; this.diagnostics.placeId = placeId; this.diagnostics.status = "ok"; this.diagnostics.placeRequestObserved = false; this.diagnostics.events.push("mock-commit-synthesized"); this.result = { success: true, placeId, formattedAddress, committed: true, }; logAddressSelectDiagnostics(side, this.getDiagnostics()); console.log( `[rpa] address-mock:${side} 已合成 commit placeId=${placeId}(跳过联想/place API)`, ); return this.result; } async waitForCommit(): Promise { if (!this.result?.committed) { throw new RpaError( "ADDRESS_NOT_CONFIRMED", "mock waitForCommit:selectAddress 未完成", { retryable: false }, ); } return this.result; } }