From bd4bc5f4f057555930bd4b3ded3c10c9546fd239 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Thu, 3 Sep 2026 11:44:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(agent):=20=E6=94=AF=E6=8C=81=E4=B8=8D?= =?UTF-8?q?=E5=8F=AF=E6=BB=9A=E5=8A=A8=E8=A7=84=E6=A0=BC=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=20(#202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../automation/PddProductDetailCollector.kt | 8 ++++++++ .../automation/PurchaseRehearsalExecutor.kt | 19 +++++++++++++++++-- .../agent/service/AgentForegroundService.kt | 1 + .../agent/PurchaseRehearsalExecutorTest.kt | 16 ++++++++++++++-- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt index 7176f2f..6357ff0 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt @@ -306,6 +306,14 @@ object PddScreenParser { orderConfirmationEvidence -> SpecPanelType.ORDER_CONFIRMATION panelScrollable != null && (hasSelectionSummary || hasSubmitHint || (hasPanelTitle && hasPanelAction)) -> SpecPanelType.NORMAL_SCROLLABLE hasSelectionSummary && hasPanelTitle && hasPanelAction -> SpecPanelType.NON_SCROLLABLE_CONFIRMATION + // Some PDD builds expose the complete selector as non-scrollable. + // Accept it only when two parsed dimensions and selectable values + // coexist with the selected summary, unique quantity controls and + // the unique lower-page order action; generic product/review pages + // cannot satisfy this combined evidence. + panelScrollable == null && headings.size >= 2 && dimensions.size >= 2 && + dimensions.sumOf { it.values.size } >= 2 && hasSelectionSummary && + hasQuantityControls && hasOrderSubmitAction -> SpecPanelType.NON_SCROLLABLE_CONFIRMATION else -> SpecPanelType.UNKNOWN } val panelOpen = specPanelType != SpecPanelType.UNKNOWN diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseRehearsalExecutor.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseRehearsalExecutor.kt index 3622eb4..20b08dc 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseRehearsalExecutor.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseRehearsalExecutor.kt @@ -51,6 +51,7 @@ class PurchaseRehearsalExecutor( private val probeSpecs: () -> String?, private val pause: (Long) -> Unit = Thread::sleep, private val stepChanged: (String) -> Unit = {}, + private val panelDiagnostic: (String) -> Unit = {}, private val beforeOrderSubmit: (FinalConfirmationEvidence) -> Unit = { throw PurchaseLiveException("PURCHASE_MODE_NOT_ALLOWED", "当前执行器没有正式采购授权") }, ) { fun execute(input: PurchaseExecutionInput, rule: PurchaseRule, supportedCapabilities: Set): PurchaseExecutionOutcome { @@ -123,7 +124,7 @@ class PurchaseRehearsalExecutor( } return failure } - applyPostAction(action)?.let { return it } + applyPostAction(input, action)?.let { return it } } if (input.phase == "spec_probe") return failure("PURCHASE_RULE_INVALID", "规格探测任务缺少 probeSpecs 动作") return if (input.executionMode == "rehearsal") PurchaseExecutionOutcome("rehearsal_completed", message = "商品、规格、数量和价格复核完成,已在下单前安全停止", actualUnitPriceCent = observedPrice) @@ -288,6 +289,7 @@ class PurchaseRehearsalExecutor( } repeat(30) { screen = currentScreen(input) + panelDiagnostic(panelEvidence(screen)) if (screen.reviewPageOpen) return leaveUnexpectedReviewPage(input) screen.problem?.let { return failure(it.code, it.message) } if (screen.specPanelOpen) return null @@ -296,6 +298,11 @@ class PurchaseRehearsalExecutor( return failure(SPEC_PANEL_NOT_OPENED, "点击后未识别到商品规格面板") } + private fun panelEvidence(screen: ParsedPddScreen): String = + "type=${screen.specPanelType};scrollables=${screen.panelScrollableCount};headings=${screen.panelHeadingCount};" + + "options=${screen.panelOptionCount};summary=${screen.hasSelectionSummary};quantity=${screen.hasQuantityControls};" + + "orderAction=${screen.hasOrderSubmitAction};pageEvidence=${screen.pageEvidenceMatched}" + private fun specEntryMatchesAliases(screen: ParsedPddScreen, candidate: SnapshotNode, aliases: List): Boolean { val prefix = "${candidate.path}/" return (sequenceOf(candidate) + screen.sourceNodes.asSequence().filter { it.path.startsWith(prefix) }) @@ -590,9 +597,17 @@ class PurchaseRehearsalExecutor( return null } - private fun applyPostAction(action: PurchaseAction): PurchaseExecutionOutcome? { + private fun applyPostAction(input: PurchaseExecutionInput, action: PurchaseAction): PurchaseExecutionOutcome? { if (action.waitAfterMs > 0) pause(action.waitAfterMs) action.swipeAfter?.let { swipe -> + // The stock purchase rule asks to reveal additional selector rows after + // opening the sheet. A fully-evidenced non-scrollable selector has no + // scroll target, and treating that absence as an action failure blocks + // an otherwise safe exact-spec flow. Keep all other configured swipes + // mandatory; this exception is limited to that confirmed panel state. + if (action.type == PurchaseActionType.OPEN_SPEC_PANEL && + currentScreen(input).specPanelType == SpecPanelType.NON_SCROLLABLE_CONFIRMATION + ) return null repeat(swipe.count) { index -> if (!driver.swipePurchase(swipe.direction, swipe.durationMs)) { return failure("RULE_ACTION_FAILED", "规则要求的有限滑动失败") diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt index 1885fee..4743593 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt @@ -473,6 +473,7 @@ class AgentForegroundService : Service() { openLink = { PddLinkLauncher(this).open(it) }, probeSpecs = { collectPurchaseProbe(accessibility, task, parsedRule) }, stepChanged = { step -> purchaseStore.updateStep(task.taskId, task.taskAttemptId, step) }, + panelDiagnostic = { evidence -> Log.i("GoAutoPurchasePanel", "task=${task.taskId};$evidence") }, beforeOrderSubmit = { evidence -> val boundaryRequestId = UUID.randomUUID().toString() val finalEvidence = JSONObject() diff --git a/android/app/src/test/java/cn/ilapage/goauto/agent/PurchaseRehearsalExecutorTest.kt b/android/app/src/test/java/cn/ilapage/goauto/agent/PurchaseRehearsalExecutorTest.kt index a841e64..30cee83 100644 --- a/android/app/src/test/java/cn/ilapage/goauto/agent/PurchaseRehearsalExecutorTest.kt +++ b/android/app/src/test/java/cn/ilapage/goauto/agent/PurchaseRehearsalExecutorTest.kt @@ -418,6 +418,16 @@ class PurchaseRehearsalExecutorTest { assertEquals(50, pauses.size) } + @Test + fun `open spec panel skips required follow-up swipe only for confirmed non-scrollable panel`() { + val driver = FakePurchaseDriver(nonScrollablePanel = true, purchaseSwipeSucceeds = false) + val outcome = PurchaseRehearsalExecutor(driver, { driver.browser = true; true }, { null }, pause = {}) + .execute(input(), PurchaseRuleParser.parse(rule()), PurchaseAgentCapabilities.supported) + + assertEquals("rehearsal_completed", outcome.resultType) + assertEquals(0, driver.swipeCount) + } + @Test fun `transient sold out page recovers before opening specs`() { val driver = FakePurchaseDriver(soldOut = true, recoverSoldOutAfterPull = true) @@ -732,6 +742,8 @@ class PurchaseRehearsalExecutorTest { private val loseEvidenceAfterPull: Boolean = false, private val unavailableSizes: Set = emptySet(), allSpecsUnavailable: Boolean = false, + private val nonScrollablePanel: Boolean = false, + private val purchaseSwipeSucceeds: Boolean = true, ) : PurchaseUiDriver { var browser = false var panel = false @@ -802,8 +814,8 @@ class PurchaseRehearsalExecutorTest { node("content", "", 0, 0, 1080, 2200), node("price", "¥${priceCent / 100}.${(priceCent % 100).toString().padStart(2, '0')}", 20, 300, 300, 360), node("title", "确认款式", 20, 396, 300, 430), - node("scroll", "", 0, 400, 1080, 950, scrollable = true), ) + if (!nonScrollablePanel) nodes += node("scroll", "", 0, 400, 1080, 950, scrollable = true) if (!hideSummary) { nodes += node("selected", "已选 $displayedSummary", 20, 365, 700, 395) } @@ -912,7 +924,7 @@ class PurchaseRehearsalExecutorTest { override fun swipePurchase(direction: SwipeDirection, durationMs: Long): Boolean { swipeCount++ if (direction == SwipeDirection.UP) upSwipeCount++ - return true + return purchaseSwipeSucceeds } override fun swipePurchaseIn(target: SnapshotNode, direction: SwipeDirection, durationMs: Long): Boolean =