fix(agent): search horizontally for exact purchase size (#230)

This commit is contained in:
QiuSW
2026-09-07 09:55:07 +08:00
parent 7b9fcfb8c1
commit 4e6afc2d25
3 changed files with 131 additions and 28 deletions
@@ -823,18 +823,22 @@ class PurchaseRehearsalExecutor(
currentSignature = inspected.signature
}
}
if (dimension == "color") {
locateExactColorHorizontally(input, target)?.let { return it }
if (dimension == "color" || dimension == "size") {
locateExactDimensionHorizontally(input, dimension, target)?.let { return it }
}
return SpecLookup(failure = failure(SPEC_TARGET_NOT_VISIBLE, "有界搜索后未找到精确规格"))
}
/**
* Compatibility fallback for horizontally paged PDD color rows. The
* Compatibility fallback for horizontally paged PDD color or size rows. The
* established visible/vertical lookup above remains the primary path, so
* already successful purchases never enter this branch.
*/
private fun locateExactColorHorizontally(input: PurchaseExecutionInput, target: String): SpecLookup? {
private fun locateExactDimensionHorizontally(
input: PurchaseExecutionInput,
dimension: String,
target: String,
): SpecLookup? {
val swipeLimit = DEFAULT_COLLECTOR.limits.getValue("specHorizontalSwipes")
val stableReadLimit = DEFAULT_COLLECTOR.limits.getValue("stableEdgeReads")
data class ColorInspection(
@@ -857,7 +861,7 @@ class PurchaseRehearsalExecutor(
"",
)
}
val values = screen.dimensions.filter { it.key == "color" }.flatMap { it.values }
val values = screen.dimensions.filter { it.key == dimension }.flatMap { it.values }
val exact = values.filter { it.text == target }
val lookup = when {
exact.size > 1 -> SpecLookup(failure = failure(SPEC_TARGET_AMBIGUOUS, "精确规格匹配到多个控件"))
@@ -865,7 +869,7 @@ class PurchaseRehearsalExecutor(
exact.size == 1 -> SpecLookup(failure = failure(SPEC_SAFE_TARGET_MISSING, "精确规格当前不可安全点击"))
else -> null
}
val rows = specColorRows(values)
val rows = specValueRows(values)
val signature = rows.flatten().joinToString("|") { value ->
val bounds = value.node.bounds
"${value.text}:${bounds.left},${bounds.top},${bounds.right},${bounds.bottom}:${value.available}"
@@ -878,7 +882,7 @@ class PurchaseRehearsalExecutor(
var verticalRecoverySwipes = 0
var previousVerticalSignature: String? = null
var verticalStableReads = 0
while (inspected.rows.none { it.size > 1 } && verticalRecoverySwipes < swipeLimit) {
while (horizontalSpecRow(inspected.screen, inspected.rows) == null && verticalRecoverySwipes < swipeLimit) {
val container = inspected.screen.specPanelContainer ?: break
if (!driver.swipePurchaseIn(container, SwipeDirection.DOWN, 350)) break
verticalRecoverySwipes++
@@ -890,21 +894,21 @@ class PurchaseRehearsalExecutor(
if (verticalStableReads >= stableReadLimit) break
}
val initialRow = inspected.rows.firstOrNull { it.size > 1 }
val initialRow = horizontalSpecRow(inspected.screen, inspected.rows)
?: return SpecLookup(failure = failure(
SPEC_TARGET_NOT_VISIBLE,
"有界搜索后未找到精确规格 [dimension=color;horizontalSwipes=0;verticalRecoverySwipes=$verticalRecoverySwipes;visibleCandidates=${inspected.rows.flatten().size};reason=noHorizontalRow]",
"有界搜索后未找到精确规格 [dimension=$dimension;horizontalSwipes=0;verticalRecoverySwipes=$verticalRecoverySwipes;visibleCandidates=${inspected.rows.flatten().size};reason=noHorizontalRow]",
))
var horizontalSwipes = 0
var previousSignature: String? = null
var stableReads = 0
while (horizontalSwipes < swipeLimit) {
val anchor = inspected.rows.firstOrNull { it.size > 1 }?.firstOrNull()?.node ?: initialRow.first().node
val anchor = horizontalSpecRow(inspected.screen, inspected.rows)?.firstOrNull()?.node ?: initialRow.first().node
if (!driver.swipeSpecRow(anchor, SwipeDirection.RIGHT)) {
return SpecLookup(failure = failure(
SPEC_TARGET_NOT_VISIBLE,
"有界搜索后未找到精确规格 [dimension=color;horizontalSwipes=$horizontalSwipes;verticalRecoverySwipes=$verticalRecoverySwipes;visibleCandidates=${inspected.rows.flatten().size};reason=restoreSwipeFailed]",
"有界搜索后未找到精确规格 [dimension=$dimension;horizontalSwipes=$horizontalSwipes;verticalRecoverySwipes=$verticalRecoverySwipes;visibleCandidates=${inspected.rows.flatten().size};reason=restoreSwipeFailed]",
))
}
horizontalSwipes++
@@ -919,15 +923,15 @@ class PurchaseRehearsalExecutor(
previousSignature = null
stableReads = 0
while (horizontalSwipes < swipeLimit * 2) {
val anchor = inspected.rows.firstOrNull { it.size > 1 }?.firstOrNull()?.node
val anchor = horizontalSpecRow(inspected.screen, inspected.rows)?.firstOrNull()?.node
?: return SpecLookup(failure = failure(
SPEC_TARGET_NOT_VISIBLE,
"有界搜索后未找到精确规格 [dimension=color;horizontalSwipes=$horizontalSwipes;verticalRecoverySwipes=$verticalRecoverySwipes;visibleCandidates=${inspected.rows.flatten().size};reason=rowLost]",
"有界搜索后未找到精确规格 [dimension=$dimension;horizontalSwipes=$horizontalSwipes;verticalRecoverySwipes=$verticalRecoverySwipes;visibleCandidates=${inspected.rows.flatten().size};reason=rowLost]",
))
if (!driver.swipeSpecRow(anchor, SwipeDirection.LEFT)) {
return SpecLookup(failure = failure(
SPEC_TARGET_NOT_VISIBLE,
"有界搜索后未找到精确规格 [dimension=color;horizontalSwipes=$horizontalSwipes;verticalRecoverySwipes=$verticalRecoverySwipes;visibleCandidates=${inspected.rows.flatten().size};reason=searchSwipeFailed]",
"有界搜索后未找到精确规格 [dimension=$dimension;horizontalSwipes=$horizontalSwipes;verticalRecoverySwipes=$verticalRecoverySwipes;visibleCandidates=${inspected.rows.flatten().size};reason=searchSwipeFailed]",
))
}
horizontalSwipes++
@@ -940,11 +944,29 @@ class PurchaseRehearsalExecutor(
}
return SpecLookup(failure = failure(
SPEC_TARGET_NOT_VISIBLE,
"有界搜索后未找到精确规格 [dimension=color;horizontalSwipes=$horizontalSwipes;verticalRecoverySwipes=$verticalRecoverySwipes;visibleCandidates=${inspected.rows.flatten().size};reason=edgeReached]",
"有界搜索后未找到精确规格 [dimension=$dimension;horizontalSwipes=$horizontalSwipes;verticalRecoverySwipes=$verticalRecoverySwipes;visibleCandidates=${inspected.rows.flatten().size};reason=edgeReached]",
))
}
private fun specColorRows(values: List<VisibleSpecValue>): List<List<VisibleSpecValue>> {
private fun horizontalSpecRow(
screen: ParsedPddScreen,
rows: List<List<VisibleSpecValue>>,
): List<VisibleSpecValue>? = rows.firstOrNull { row ->
row.size > 1 || row.any { value -> hasDedicatedHorizontalAncestor(screen, value.node) }
}
private fun hasDedicatedHorizontalAncestor(screen: ParsedPddScreen, source: SnapshotNode): Boolean {
val byPath = screen.sourceNodes.associateBy(SnapshotNode::path)
var current: SnapshotNode? = source
while (current != null) {
if (current.path == screen.specPanelContainer?.path) return false
if (current.scrollable && current.bounds.width > current.bounds.height) return true
current = current.parentPath?.let(byPath::get)
}
return false
}
private fun specValueRows(values: List<VisibleSpecValue>): List<List<VisibleSpecValue>> {
val sorted = values.sortedWith(compareBy({ it.node.bounds.centerY }, { it.node.bounds.left }))
val rows = mutableListOf<MutableList<VisibleSpecValue>>()
val centers = mutableListOf<Int>()