Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bec8c84321 | ||
|
|
7ea34e32c1 | ||
|
|
d1ddb04959 | ||
|
|
ca815c30de |
@@ -11,8 +11,8 @@ android {
|
||||
applicationId = "cn.ilapage.goauto.agent"
|
||||
minSdk = 23
|
||||
targetSdk = 34
|
||||
versionCode = 75
|
||||
versionName = "0.9.62"
|
||||
versionCode = 78
|
||||
versionName = "0.9.65"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
||||
+22
-4
@@ -491,8 +491,11 @@ class PurchaseLiveAutomation(
|
||||
restoreFinalEvidenceInCurrentPanel(savedEvidence, expected, suffix)
|
||||
} else {
|
||||
if (!driver.backPurchase()) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "地址保存后无法返回订单页面,未创建订单")
|
||||
waitFor("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存后无法返回订单页面,未创建订单") {
|
||||
hasFinalSavedAddressEvidence(it, expected, suffix)
|
||||
val returned = waitFor("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存后无法返回订单页面,未创建订单") {
|
||||
hasFinalSavedAddressEvidence(it, expected, suffix) || isPurchaseConfirmationPanel(it)
|
||||
}
|
||||
if (!hasFinalSavedAddressEvidence(returned, expected, suffix)) {
|
||||
restoreFinalEvidenceInCurrentPanel(returned, expected, suffix)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -521,9 +524,10 @@ class PurchaseLiveAutomation(
|
||||
if (!isPurchaseConfirmationPanel(snapshot)) {
|
||||
fail("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存后采购面板发生变化,未创建订单")
|
||||
}
|
||||
val panels = purchasePanelScrollTargets(snapshot)
|
||||
val panels = savedAddressPanelScrollTargets(snapshot)
|
||||
if (panels.size != 1) {
|
||||
fail("PURCHASE_ADDRESS_ENTRY_AMBIGUOUS", "地址保存后没有找到唯一的规格面板滚动区域,未创建订单")
|
||||
val reason = if (panels.isEmpty()) "未找到可用的规格面板滚动区域" else "找到多个规格面板滚动区域"
|
||||
fail("PURCHASE_ADDRESS_ENTRY_AMBIGUOUS", "地址保存后${reason},未创建订单")
|
||||
}
|
||||
val panel = panels.single()
|
||||
val signature = viewportSignature(snapshot, panel)
|
||||
@@ -591,6 +595,20 @@ class PurchaseLiveAutomation(
|
||||
fail(code, message)
|
||||
}
|
||||
|
||||
private fun savedAddressPanelScrollTargets(snapshot: UiSnapshot): List<SnapshotNode> {
|
||||
val panel = PddScreenParser.parse(snapshot, PurchaseRehearsalExecutor.DEFAULT_COLLECTOR, "", null)
|
||||
.specPanelContainer ?: return purchasePanelScrollTargets(snapshot)
|
||||
val screenWidth = snapshot.nodes.maxOfOrNull { it.bounds.right }?.coerceAtLeast(1) ?: 1
|
||||
val screenHeight = snapshot.nodes.maxOfOrNull { it.bounds.bottom }?.coerceAtLeast(1) ?: 1
|
||||
return snapshot.nodes.filter { node ->
|
||||
node.visible && node.enabled && node.scrollable &&
|
||||
node.bounds.width * 100 >= screenWidth * 60 &&
|
||||
node.bounds.height * 100 >= screenHeight * 20 &&
|
||||
node.bounds.left >= panel.bounds.left && node.bounds.right <= panel.bounds.right &&
|
||||
node.bounds.top >= panel.bounds.top && node.bounds.bottom <= panel.bounds.bottom
|
||||
}.distinctBy { it.path }
|
||||
}
|
||||
|
||||
private fun purchasePanelScrollTargets(snapshot: UiSnapshot): List<SnapshotNode> {
|
||||
val screenWidth = snapshot.nodes.maxOfOrNull { it.bounds.right }?.coerceAtLeast(1) ?: 1
|
||||
val screenHeight = snapshot.nodes.maxOfOrNull { it.bounds.bottom }?.coerceAtLeast(1) ?: 1
|
||||
|
||||
+48
-14
@@ -96,6 +96,7 @@ class PurchaseRehearsalExecutor(
|
||||
) {
|
||||
private var purchasePanelContext: PurchasePanelContext? = null
|
||||
private var pageIdentity: Pair<String?, String?> = null to null
|
||||
private var entryRecoveryReason = "not_checked"
|
||||
|
||||
fun execute(input: PurchaseExecutionInput, rule: PurchaseRule, supportedCapabilities: Set<String>): PurchaseExecutionOutcome {
|
||||
purchasePanelContext = null
|
||||
@@ -400,7 +401,7 @@ class PurchaseRehearsalExecutor(
|
||||
if (refreshed.specPanelOpen) return null
|
||||
val recoveryTarget = recoverableSpecEntry(screen, refreshed, action, beforeIdentity)
|
||||
if (recoveryTarget == null) {
|
||||
panelDiagnostic("entryRecovery=rejected;pageChanged=${wait.changed};${panelEvidence(refreshed)}")
|
||||
panelDiagnostic("entryRecovery=rejected;reason=$entryRecoveryReason;pageChanged=${wait.changed};${panelEvidence(refreshed)}")
|
||||
return failure(
|
||||
SPEC_PANEL_EVIDENCE_NOT_MATCHED,
|
||||
"规格入口点击后页面已变化,但规格面板强证据不足 [${panelEvidence(wait.screen)}]",
|
||||
@@ -443,18 +444,38 @@ class PurchaseRehearsalExecutor(
|
||||
action: PurchaseAction,
|
||||
identity: Pair<String?, String?>,
|
||||
): SnapshotNode? {
|
||||
if (identity.first == null || identity.second == null || identity != pageIdentity ||
|
||||
!after.isPddPackage || !after.pageEvidenceMatched || after.problem != null || after.reviewPageOpen ||
|
||||
after.specPanelOpen || after.hasCloseControl || after.hasPaymentArea || after.hasOrderSubmitAction ||
|
||||
after.hasQuantityControls || after.hasSelectionSummary ||
|
||||
before.summary.title.isNullOrBlank() || before.summary.title != after.summary.title ||
|
||||
before.specEntrySource == null || before.specEntrySource != after.specEntrySource ||
|
||||
before.specEntry?.label != after.specEntry?.label ||
|
||||
after.explicitSpecEntryCount > 1 || after.nestedSpecEntryCount > 1
|
||||
) return null
|
||||
entryRecoveryReason = when {
|
||||
identity.first == null || identity.second == null -> "identity_missing"
|
||||
identity != pageIdentity -> "identity_changed"
|
||||
!after.isPddPackage || !after.pageEvidenceMatched -> "product_context_missing"
|
||||
after.problem != null || after.reviewPageOpen -> "page_problem"
|
||||
after.specPanelOpen -> "panel_open"
|
||||
after.hasCloseControl -> "close_control"
|
||||
after.hasPaymentArea -> "payment_area"
|
||||
after.hasOrderSubmitAction -> "order_action"
|
||||
after.hasQuantityControls -> "quantity_controls"
|
||||
after.hasSelectionSummary -> "selection_summary"
|
||||
before.summary.title.isNullOrBlank() -> "title_missing"
|
||||
before.summary.title != after.summary.title -> "title_changed"
|
||||
after.specEntry == null -> "entry_missing"
|
||||
before.specEntrySource == null -> "entry_source_missing"
|
||||
before.specEntrySource != after.specEntrySource -> "entry_source_changed"
|
||||
before.specEntry?.label != after.specEntry.label -> "entry_label_changed"
|
||||
after.explicitSpecEntryCount > 1 || after.nestedSpecEntryCount > 1 -> "entry_ambiguous"
|
||||
else -> "eligible"
|
||||
}
|
||||
if (entryRecoveryReason != "eligible") return null
|
||||
val anchor = after.specEntry ?: return null
|
||||
if (action.textAliases?.let { specEntryMatchesAliases(after, anchor, it) } == false) return null
|
||||
return (after.specEntryClickTarget ?: anchor).takeIf { it.visible && it.enabled }
|
||||
if (action.textAliases?.let { specEntryMatchesAliases(after, anchor, it) } == false) {
|
||||
entryRecoveryReason = "rule_alias_mismatch"
|
||||
return null
|
||||
}
|
||||
val target = after.specEntryClickTarget ?: anchor
|
||||
if (!target.visible || !target.enabled) {
|
||||
entryRecoveryReason = "entry_unavailable"
|
||||
return null
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
private data class SpecPanelWait(
|
||||
@@ -582,6 +603,7 @@ class PurchaseRehearsalExecutor(
|
||||
): PurchaseExecutionOutcome? {
|
||||
val initial = currentScreen(input)
|
||||
initial.problem?.let { return failure(it.code, it.message) }
|
||||
unavailableExactSpec(initial, dimension, target)?.let { return it }
|
||||
if (rememberExactSelection(initial, dimension, target, selectionProofs)) return null
|
||||
|
||||
val lookup = locateExactSpec(input, dimension, target)
|
||||
@@ -832,6 +854,18 @@ class PurchaseRehearsalExecutor(
|
||||
|
||||
private data class SpecLookup(val node: SnapshotNode? = null, val failure: PurchaseExecutionOutcome? = null)
|
||||
|
||||
private fun unavailableExactSpec(screen: ParsedPddScreen, dimension: String, target: String): PurchaseExecutionOutcome? {
|
||||
if (!screen.specPanelOpen) return null
|
||||
val exact = screen.dimensions.filter { it.key == dimension }.flatMap { it.values }.filter { it.text == target }
|
||||
return if (exact.size == 1 && !exact.single().available) unavailableSpec(dimension) else null
|
||||
}
|
||||
|
||||
private fun unavailableSpec(dimension: String): PurchaseExecutionOutcome {
|
||||
val role = if (dimension == "color") "颜色" else "尺码"
|
||||
panelDiagnostic("specLookup=unavailable;dimension=$dimension;reason=exact_target_unavailable")
|
||||
return failure(SPEC_SAFE_TARGET_MISSING, "目标${role}已售罄或当前不可选,未创建订单")
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches only parsed, selectable values in the already-open spec panel.
|
||||
* A short downward pass first restores the top when a previous action left
|
||||
@@ -868,7 +902,7 @@ class PurchaseRehearsalExecutor(
|
||||
}
|
||||
if (exact.size == 1 && exact.single().available) return Inspection(SpecLookup(node = exact.single().node), "", screen.specPanelContainer)
|
||||
if (exact.size == 1) {
|
||||
return Inspection(SpecLookup(failure = failure(SPEC_SAFE_TARGET_MISSING, "精确规格当前不可安全点击")), "", screen.specPanelContainer)
|
||||
return Inspection(SpecLookup(failure = unavailableSpec(dimension)), "", screen.specPanelContainer)
|
||||
}
|
||||
val signature = screen.dimensions.joinToString("|") { item ->
|
||||
"${item.key}:${item.values.joinToString(",") { value -> "${value.text}:${value.available}" }}"
|
||||
@@ -933,7 +967,7 @@ class PurchaseRehearsalExecutor(
|
||||
val lookup = when {
|
||||
exact.size > 1 -> SpecLookup(failure = failure(SPEC_TARGET_AMBIGUOUS, "精确规格匹配到多个控件"))
|
||||
exact.size == 1 && exact.single().available -> SpecLookup(node = exact.single().node)
|
||||
exact.size == 1 -> SpecLookup(failure = failure(SPEC_SAFE_TARGET_MISSING, "精确规格当前不可安全点击"))
|
||||
exact.size == 1 -> SpecLookup(failure = unavailableSpec(dimension))
|
||||
else -> null
|
||||
}
|
||||
val rows = specValueRows(values)
|
||||
|
||||
@@ -141,6 +141,72 @@ class PurchaseLiveAutomationTest {
|
||||
assertEquals(0, driver.submitClicks)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saved address back into short spec panel restores without aspect ratio restriction`() {
|
||||
val driver = LiveDriver(savedTransitionWithoutLegacyContext = true, backReturnsToSpecPanel = true,
|
||||
savedPanelBounds = NodeBounds(0, 400, 1080, 1325))
|
||||
val automation = PurchaseLiveAutomation(driver, pause = {})
|
||||
val address = automation.updateShippingAddress("_cg81")
|
||||
|
||||
assertEquals("_cg81", automation.finalConfirmation(input().copy(addressSuffix = "_cg81"), address).addressSuffix)
|
||||
assertEquals(1, driver.backCount)
|
||||
assertEquals(1, driver.scopedSwipes)
|
||||
assertEquals(0, driver.submitClicks)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `short saved spec panel still stops when unchanged`() {
|
||||
val driver = LiveDriver(saveReturnsToSpecPanel = true, savedSpecPanelRecoveryStuck = true,
|
||||
savedPanelBounds = NodeBounds(0, 400, 1080, 1325))
|
||||
val error = runCatching { PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg81") }
|
||||
.exceptionOrNull() as PurchaseLiveException
|
||||
assertEquals("PURCHASE_ADDRESS_SAVE_TIMEOUT", error.code)
|
||||
assertEquals(0, driver.backCount)
|
||||
assertTrue(driver.scopedSwipes in 1..5)
|
||||
assertEquals(0, driver.submitClicks)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `missing and duplicate saved panel scroll regions fail distinctly without gestures`() {
|
||||
for (count in listOf(0, 2)) {
|
||||
val driver = LiveDriver(saveReturnsToSpecPanel = true, savedPanelScrollCount = count)
|
||||
val error = runCatching { PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg81") }
|
||||
.exceptionOrNull() as PurchaseLiveException
|
||||
assertEquals("PURCHASE_ADDRESS_ENTRY_AMBIGUOUS", error.code)
|
||||
assertTrue(error.message.orEmpty().contains(if (count == 0) "未找到可用" else "找到多个"))
|
||||
assertEquals(0, driver.scopedSwipes)
|
||||
assertEquals(0, driver.submitClicks)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saved address back into spec panel restores final evidence with one back`() {
|
||||
val driver = LiveDriver(savedTransitionWithoutLegacyContext = true, backReturnsToSpecPanel = true)
|
||||
val automation = PurchaseLiveAutomation(driver, pause = {})
|
||||
|
||||
val address = automation.updateShippingAddress("_cg81")
|
||||
val final = automation.finalConfirmation(input().copy(addressSuffix = "_cg81"), address)
|
||||
|
||||
assertEquals("_cg81", final.addressSuffix)
|
||||
assertEquals(1, driver.backCount)
|
||||
assertEquals(1, driver.scopedSwipes)
|
||||
assertEquals(0, driver.submitClicks)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saved address back into stuck spec panel fails without another back or submit`() {
|
||||
val driver = LiveDriver(savedTransitionWithoutLegacyContext = true, backReturnsToSpecPanel = true,
|
||||
savedSpecPanelRecoveryStuck = true)
|
||||
|
||||
val error = runCatching { PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg81") }
|
||||
.exceptionOrNull() as PurchaseLiveException
|
||||
|
||||
assertEquals("PURCHASE_ADDRESS_SAVE_TIMEOUT", error.code)
|
||||
assertEquals(1, driver.backCount)
|
||||
assertTrue(driver.scopedSwipes in 1..5)
|
||||
assertEquals(0, driver.submitClicks)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saved address returning to spec panel is recovered without pressing back`() {
|
||||
val driver = LiveDriver(saveReturnsToSpecPanel = true)
|
||||
@@ -645,6 +711,9 @@ class PurchaseLiveAutomationTest {
|
||||
private val savedTransitionWithoutLegacyContext: Boolean = false,
|
||||
private val savedTransitionHidesSuffix: Boolean = false,
|
||||
private val saveReturnsToSpecPanel: Boolean = false,
|
||||
private val backReturnsToSpecPanel: Boolean = false,
|
||||
private val savedPanelBounds: NodeBounds = NodeBounds(0, 400, 1080, 2100),
|
||||
private val savedPanelScrollCount: Int = 1,
|
||||
private val savedSpecPanelRecoveryStuck: Boolean = false,
|
||||
private val duplicatePhoneNodesSameCard: Boolean = false,
|
||||
private val duplicateSemanticAddressCards: Boolean = false,
|
||||
@@ -708,7 +777,7 @@ class PurchaseLiveAutomationTest {
|
||||
))
|
||||
"post-save-spec" -> snapshot(listOf(
|
||||
node("root", "", bounds = NodeBounds(0, 0, 1080, 2200)),
|
||||
node("panel", "", scrollable = true, bounds = NodeBounds(0, 400, 1080, 2100)),
|
||||
node("panel", "", scrollable = true, bounds = savedPanelBounds),
|
||||
node("panel-title", "确认款式", bounds = NodeBounds(20, 396, 300, 430)),
|
||||
node("panel/price", "¥20.00", parentPath = "panel", bounds = NodeBounds(20, 460, 300, 520)),
|
||||
node("panel/selected", "已选 黑色 XL", parentPath = "panel", bounds = NodeBounds(20, 540, 700, 600)),
|
||||
@@ -720,7 +789,8 @@ class PurchaseLiveAutomationTest {
|
||||
node("panel/confirm", "确定", clickable = true, parentPath = "panel", bounds = NodeBounds(20, 1200, 500, 1280)),
|
||||
node("submit-parent", "", clickable = true, bounds = NodeBounds(20, 1900, 1000, 2100)),
|
||||
node("submit", "提交订单", parentPath = "submit-parent", bounds = NodeBounds(520, 1940, 980, 2040)),
|
||||
))
|
||||
).map { if (it.path == "panel" && savedPanelScrollCount == 0) it.copy(enabled = false) else it } +
|
||||
if (savedPanelScrollCount > 1) listOf(node("panel/duplicate", "", scrollable = true, bounds = savedPanelBounds, parentPath = "panel")) else emptyList())
|
||||
"order" -> snapshot(listOf(node("status", "待付款"), node("order", "订单号:PDD-202608210001"), node("time", "下单时间:2026-08-21 10:30:00"), node("pay", "立即支付", clickable = true)))
|
||||
"order-folded" -> snapshot(listOf(node("status", "待付款"), node("pay", "立即支付", clickable = true)))
|
||||
"order-folded-payment-activity" -> UiSnapshot(PDD, "com.xunmeng.pinduoduo.app_pay.core.PayActivity", listOf(
|
||||
@@ -863,6 +933,7 @@ class PurchaseLiveAutomationTest {
|
||||
page = when (page) {
|
||||
"chooser" -> "payment"
|
||||
"payment" -> if (orderEvidenceBelowFold) "order-folded" else "order"
|
||||
"saved-transition" -> if (backReturnsToSpecPanel) "post-save-spec" else "confirmation"
|
||||
else -> "confirmation"
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -930,12 +930,34 @@ class PurchaseRehearsalExecutorTest {
|
||||
@Test
|
||||
fun `changed entry identity or unsafe page never receives recovery tap`() {
|
||||
for (mode in listOf("missing", "duplicate", "activity", "payment")) {
|
||||
val diagnostics = mutableListOf<String>()
|
||||
val driver = FakePurchaseDriver(entryActionHasEffect = false, dynamicProduct = true,
|
||||
afterEntryMutation = mode, specTapResult = FreshActionResult.SUCCESS)
|
||||
val outcome = PurchaseRehearsalExecutor(driver, { true }, { null }, pause = {})
|
||||
val outcome = PurchaseRehearsalExecutor(driver, { true }, { null }, pause = {}, panelDiagnostic = diagnostics::add)
|
||||
.execute(input(), PurchaseRuleParser.parse(rule()), PurchaseAgentCapabilities.supported)
|
||||
assertEquals(mode, "failed", outcome.resultType)
|
||||
assertEquals(mode, 0, driver.specTapCount)
|
||||
val expected = mapOf("missing" to "entry_missing", "duplicate" to "entry_ambiguous",
|
||||
"activity" to "identity_changed", "payment" to "payment_area").getValue(mode)
|
||||
assertTrue(diagnostics.toString(), diagnostics.any { it.contains("entryRecovery=rejected;reason=$expected;") })
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unavailable exact color stops without selection click or searching even when selected`() {
|
||||
for (selected in listOf(null, "黑色")) {
|
||||
val driver = FakePurchaseDriver(colors = listOf("黑色", "白色"),
|
||||
unavailableColors = setOf("黑色")).apply { color = selected }
|
||||
val logs = mutableListOf<String>()
|
||||
val outcome = PurchaseRehearsalExecutor(driver, { true }, { null }, pause = {}, panelDiagnostic = logs::add)
|
||||
.execute(input(), PurchaseRuleParser.parse(rule()), PurchaseAgentCapabilities.supported)
|
||||
assertEquals("PURCHASE_SPEC_SAFE_TARGET_MISSING", outcome.errorCode)
|
||||
assertEquals("目标颜色已售罄或当前不可选,未创建订单", outcome.message)
|
||||
assertFalse(driver.clicked.contains("黑色"))
|
||||
assertEquals(0, driver.specTapCount)
|
||||
assertEquals(0, driver.swipeCount)
|
||||
assertTrue(logs.any { it == "specLookup=unavailable;dimension=color;reason=exact_target_unavailable" })
|
||||
assertFalse(logs.any { it.contains("黑色") })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1385,6 +1407,7 @@ class PurchaseRehearsalExecutorTest {
|
||||
private val pullDownSucceeds: Boolean = true,
|
||||
private val loseEvidenceAfterPull: Boolean = false,
|
||||
private val unavailableSizes: Set<String> = emptySet(),
|
||||
private val unavailableColors: Set<String> = emptySet(),
|
||||
allSpecsUnavailable: Boolean = false,
|
||||
private val nonScrollablePanel: Boolean = false,
|
||||
private val confirmationPanelKind: String? = null,
|
||||
@@ -1561,7 +1584,7 @@ class PurchaseRehearsalExecutorTest {
|
||||
540,
|
||||
clickable = true,
|
||||
selected = selectedColor == value,
|
||||
enabled = !allSpecsUnavailable,
|
||||
enabled = !allSpecsUnavailable && value !in unavailableColors,
|
||||
parentPath = colorParent,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Business-Rules-and-Glossary
|
||||
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Business-Rules-and-Glossary.-
|
||||
wiki_revision: eda7ac819c2147201d749cbef9400bd916822b87
|
||||
synchronized_at: 2026-09-08T10:09:39Z
|
||||
wiki_revision: fdcce403423aa799d1517ac75fb38da636ef5217
|
||||
synchronized_at: 2026-09-09T01:22:58Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 业务规则与术语
|
||||
@@ -494,3 +494,10 @@ Web 唯一展示位置为“采集采购 → SYB 同步记录”:列表状态
|
||||
实现 93aab6a,Android 0.9.62/versionCode 75;已构建并授权覆盖安装,真实采购效果待验收。仅在颜色维度完整精确目标已选中、但存在非目标已选候选时,识别同一卡片重复表达:卡片必须是滚动容器直接子级的可点击非滚动 android.view.ViewGroup,节点父链明确、可见启用且边界包含;其他冲突节点仅可为该卡片自身或其中的 ImageView。另一文字选项、另一张卡片、多个精确目标、目标未选中、父链/边界不明均不适用例外。额外核对内存源节点以拒绝被解析器按同名去重隐藏的跨卡片目标;不保存原始树。
|
||||
|
||||
不删除规格数字、不模糊匹配、不改变通用解析器、尺码确认、摘要兜底、入口/滚动/地址/价格/创建订单。没有非目标选中冲突时继续原判断;仅把同卡片容器/图片的重复选中表达从冲突中排除,不新增点击。
|
||||
|
||||
|
||||
## 精确规格不可用与入口恢复诊断(#245)
|
||||
|
||||
实现 ca815c3,Android 0.9.63/versionCode 76;已通过单元测试并构建,未安装、未进行真实采购验收。入口恢复沿用 #243 条件,拒绝时通过 entryRecovery=rejected;reason=<固定原因码> 区分页面身份、商品上下文、面板/支付区域、数量/摘要、标题、入口来源/文案、歧义、规则别名及可用性;不放宽条件,不新增点击。
|
||||
|
||||
在规格面板识别出唯一完整精确候选且 available=false 时,立即返回既有 PURCHASE_SPEC_SAFE_TARGET_MISSING,提示目标颜色/尺码已售罄或当前不可选,未创建订单;已选中目标同样不能绕过不可用检查。只记录 dimension 和 exact_target_unavailable 固定结构证据,不记录规格原文或原始树。原有纵向与横向定位在此条件下本就停止,不能将本改动描述为修复“已识别售罄仍继续搜索”。未识别精确候选时继续既有有界搜索,不推断售罄,不替换规格;解析器、匹配规则、滚动预算、地址与下单流程不变。
|
||||
|
||||
Reference in New Issue
Block a user