fix(#113): restore hidden PDD spec headings

This commit is contained in:
QiuSW
2026-08-27 17:29:24 +08:00
parent c782103b6f
commit bc9c637522
8 changed files with 326 additions and 11 deletions
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId = "cn.ilapage.goauto.agent"
minSdk = 23
targetSdk = 34
versionCode = 22
versionName = "0.9.9"
versionCode = 23
versionName = "0.9.10"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -379,6 +379,32 @@ class GoAutoAccessibilityService : AccessibilityService(), UiDriver, PddCollecto
return swipe(SemanticTarget.SPEC_PANEL, direction)
}
override fun pullDownSpecPanel(anchor: SnapshotNode): Boolean {
if (!anchor.scrollable) return false
val root = rootInActiveWindow ?: return false
val matches = mutableListOf<AccessibilityNodeInfo>()
walk(root) { node ->
val bounds = Rect().also(node::getBoundsInScreen)
if (
node.isVisibleToUser && node.isEnabled && node.isScrollable &&
node.className?.toString() == anchor.className &&
kotlin.math.abs(bounds.centerX() - anchor.bounds.centerX) <= 32 &&
kotlin.math.abs(bounds.centerY() - anchor.bounds.centerY) <= 32
) matches += node
}
if (matches.size != 1) return false
val bounds = Rect().also(matches.single()::getBoundsInScreen)
Log.i("GoAutoCollector", "spec-panel-restore anchored=true bounds=$bounds class=${matches.single().className}")
return swipeNode(
matches.single(),
SwipeDirection.DOWN,
durationMs = 550,
preferScrollAction = false,
downStartPercent = 55,
downEndPercent = 88,
)
}
override fun pullDownGoodsPage(): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Log.i("GoAutoCollector", "pull-down result=unsupported sdk=${Build.VERSION.SDK_INT}")
@@ -440,6 +466,8 @@ class GoAutoAccessibilityService : AccessibilityService(), UiDriver, PddCollecto
direction: SwipeDirection,
durationMs: Long = 450,
preferScrollAction: Boolean = true,
downStartPercent: Int = 25,
downEndPercent: Int = 75,
): Boolean {
val bounds = Rect().also(node::getBoundsInScreen)
if (bounds.width() < 2 || bounds.height() < 2) return false
@@ -458,7 +486,12 @@ class GoAutoAccessibilityService : AccessibilityService(), UiDriver, PddCollecto
val centerY = bounds.centerY()
val points = when (direction) {
SwipeDirection.UP -> listOf(centerX, bottom, centerX, top)
SwipeDirection.DOWN -> listOf(centerX, top, centerX, bottom)
SwipeDirection.DOWN -> listOf(
centerX,
bounds.top + bounds.height() * downStartPercent.coerceIn(1, 98) / 100,
centerX,
bounds.top + bounds.height() * downEndPercent.coerceIn(downStartPercent + 1, 99) / 100,
)
SwipeDirection.LEFT -> listOf(right, centerY, left, centerY)
SwipeDirection.RIGHT -> listOf(left, centerY, right, centerY)
}
@@ -65,13 +65,14 @@ interface PddCollectorDriver {
reason = FreshClickReason.UNKNOWN,
)
fun swipeSpec(direction: SwipeDirection, anchor: SnapshotNode? = null): Boolean
fun pullDownSpecPanel(anchor: SnapshotNode): Boolean = swipeSpec(SwipeDirection.DOWN, anchor)
fun pullDownGoodsPage(): Boolean
fun back(): Boolean
}
data class VisibleSpecValue(val text: String, val available: Boolean, val node: SnapshotNode)
data class VisibleDimension(val key: String, val name: String, val values: List<VisibleSpecValue>)
enum class SpecPanelType { UNKNOWN, NORMAL_SCROLLABLE, NON_SCROLLABLE_CONFIRMATION, QUICK_CONFIRMATION }
enum class SpecPanelType { UNKNOWN, NORMAL_SCROLLABLE, NON_SCROLLABLE_CONFIRMATION, QUICK_CONFIRMATION, ORDER_CONFIRMATION }
data class ParsedPddScreen(
val summary: ProductSummary,
@@ -95,6 +96,7 @@ data class ParsedPddScreen(
val panelOptionCount: Int,
val hasSelectionSummary: Boolean,
val hasQuantityControls: Boolean,
val hasOrderSubmitAction: Boolean,
val problem: PageProblem?,
val sourceNodes: List<SnapshotNode>,
) {
@@ -188,8 +190,25 @@ object PddScreenParser {
}
val hasClose = compactLabels.any { it in setOf("关闭", "關閉") }
val quickConfirmationEvidence = hasClose && hasSelectionSummary && hasPaymentArea && hasQuickBuy && hasQuantityControls
val orderSubmitCandidates = visibleNodes.asSequence()
.filter { it.visible && it.enabled && it.clickable && it.bounds.width > 0 && it.bounds.height > 0 }
.filter { it.bounds.centerY.toDouble() >= screenHeight * 0.75 }
.filter { node ->
val compact = (listOf(node.label) + descendants(node, visibleNodes).map(SnapshotNode::label))
.joinToString("") { it.replace(" ", "") }
compact.contains("提交订单") || compact.contains("提交訂單")
}
.toList()
.filter { candidate ->
orderSubmitCandidatesAncestorFree(candidate, visibleNodes)
}
val hasOrderSubmitAction = orderSubmitCandidates.size == 1
val orderConfirmationEvidence = hasClose && hasSelectionSummary && hasPaymentArea &&
hasQuantityControls && hasOrderSubmitAction && boundedScrollables.size == 1
val fallbackScrollable = boundedScrollables.singleOrNull().takeIf {
allHeadings.isEmpty() && hasSelectionSummary && hasPanelTitle && hasPanelAction
allHeadings.isEmpty() && (
(hasSelectionSummary && hasPanelTitle && hasPanelAction) || orderConfirmationEvidence
)
}
val panelScrollable = headedPanelScrollable ?: fallbackScrollable
val panelVisible = panelScrollable?.let { region ->
@@ -221,6 +240,7 @@ object PddScreenParser {
// selected value or restore a previously scrolled confirmation panel.
val specPanelType = when {
quickConfirmationEvidence -> SpecPanelType.QUICK_CONFIRMATION
orderConfirmationEvidence -> SpecPanelType.ORDER_CONFIRMATION
panelScrollable != null && (hasSelectionSummary || hasSubmitHint || (hasPanelTitle && hasPanelAction)) -> SpecPanelType.NORMAL_SCROLLABLE
hasSelectionSummary && hasPanelTitle && hasPanelAction -> SpecPanelType.NON_SCROLLABLE_CONFIRMATION
else -> SpecPanelType.UNKNOWN
@@ -281,6 +301,7 @@ object PddScreenParser {
panelOptionCount = dimensions.sumOf { it.values.size },
hasSelectionSummary = hasSelectionSummary,
hasQuantityControls = hasQuantityControls,
hasOrderSubmitAction = hasOrderSubmitAction,
problem = problem,
sourceNodes = visibleNodes,
)
@@ -291,6 +312,16 @@ object PddScreenParser {
return compact in excludedExactOptionLabels || excludedOptionWords.any { compact.contains(it) }
}
private fun orderSubmitCandidatesAncestorFree(candidate: SnapshotNode, source: List<SnapshotNode>): Boolean {
val candidatePrefix = "${candidate.path}/"
return source.none { node ->
node.path.startsWith(candidatePrefix) && node.visible && node.enabled && node.clickable &&
(listOf(node.label) + descendants(node, source).map(SnapshotNode::label))
.joinToString("") { it.replace(" ", "") }
.let { it.contains("提交订单") || it.contains("提交訂單") }
}
}
private fun SnapshotNode.matches(selector: NodeSelector): Boolean =
(selector.resourceId == null || resourceId == selector.resourceId) &&
(selector.text == null || text == selector.text) &&
@@ -588,6 +619,67 @@ class PddProductDetailCollector(
reviewPage = screen
break
}
if (screen.pageEvidenceMatched && screen.specPanelType == SpecPanelType.ORDER_CONFIRMATION) {
val maxRestores = minOf(3, config.limits.getValue("specVerticalSwipes"))
var restored = screen
var restoreAttempts = 0
var previousSignature = screenSignature(screen)
var stableReads = 0
var restoreReason = AgentDiagnosticReason.SPEC_PANEL_RESTORE_LIMIT
var restoreContainer = screen.specPanelContainer
if (restoreContainer == null) {
restoreReason = AgentDiagnosticReason.SPEC_PANEL_RESTORE_CONTAINER_UNAVAILABLE
} else {
while (restoreAttempts < maxRestores) {
val currentContainer = restoreContainer
if (currentContainer == null) {
restoreReason = AgentDiagnosticReason.SPEC_PANEL_RESTORE_CONTAINER_UNAVAILABLE
break
}
if (!driver.pullDownSpecPanel(currentContainer)) {
restoreReason = AgentDiagnosticReason.SPEC_PANEL_RESTORE_GESTURE_FAILED
break
}
restoreAttempts++
pause(350)
val refreshed = parse(goodsId, config, evidence)
panelAttempts++
lastPanelScreen = refreshed
refreshed.problem?.let { return failure(it.code, it.message) }
if (!refreshed.pageEvidenceMatched) {
return failure("RULE_NOT_MATCHED", "规格面板回顶期间离开 PDD 商品详情页")
}
restored = refreshed
if (refreshed.dimensions.isNotEmpty()) {
restoreReason = AgentDiagnosticReason.SPEC_PANEL_RESTORE_SUCCEEDED
break
}
restoreContainer = refreshed.specPanelContainer
if (restoreContainer == null) {
restoreReason = AgentDiagnosticReason.SPEC_PANEL_RESTORE_CONTAINER_UNAVAILABLE
break
}
val signature = screenSignature(refreshed)
stableReads = if (signature == previousSignature) stableReads + 1 else 0
previousSignature = signature
if (stableReads >= config.limits.getValue("stableEdgeReads")) {
restoreReason = AgentDiagnosticReason.SPEC_PANEL_RESTORE_EDGE
break
}
}
}
recordSpecPanelEntry(
restoreReason,
restored,
restoreAttempts,
now() - panelStartedAt,
screenChanged,
restoreAttempts > 0,
restoreReason == AgentDiagnosticReason.SPEC_PANEL_RESTORE_SUCCEEDED,
)
opened = if (restored.dimensions.isEmpty()) restored.copy(specPanelContainer = null) else restored
break
}
if (
screen.pageEvidenceMatched &&
screen.specPanelType == SpecPanelType.QUICK_CONFIRMATION &&
@@ -991,7 +1083,8 @@ class PddProductDetailCollector(
val shape = screen?.let {
"type=${it.specPanelType.name};scroll=${it.panelScrollableCount};head=${it.panelHeadingCount};" +
"option=${it.panelOptionCount};summary=${it.hasSelectionSummary.toInt()};" +
"quantity=${it.hasQuantityControls.toInt()};changed=${screenChanged.toInt()};" +
"quantity=${it.hasQuantityControls.toInt()};submit=${it.hasOrderSubmitAction.toInt()};" +
"changed=${screenChanged.toInt()};" +
"recovery=${when {
recoverySucceeded -> "success"
reason == AgentDiagnosticReason.SPEC_PANEL_RECOVERY_UNAVAILABLE -> "unavailable"
@@ -23,6 +23,11 @@ enum class AgentDiagnosticReason {
SPEC_PANEL_RECOVERED,
SPEC_PANEL_RECOVERY_UNAVAILABLE,
SPEC_PANEL_RECOVERY_FAILED,
SPEC_PANEL_RESTORE_SUCCEEDED,
SPEC_PANEL_RESTORE_EDGE,
SPEC_PANEL_RESTORE_LIMIT,
SPEC_PANEL_RESTORE_CONTAINER_UNAVAILABLE,
SPEC_PANEL_RESTORE_GESTURE_FAILED,
SPEC_PANEL_EVIDENCE_NOT_MATCHED,
SPEC_ENTRY_CLICK_NO_EFFECT,
SIZE_FOUND,
@@ -335,6 +335,67 @@ class PddProductDetailCollectorTest {
assertEquals(null, parsed.quickConfirmationEntry)
}
@Test
fun orderConfirmationUsesReadOnlySubmitEvidenceAndUniqueRecyclerView() {
val snapshot = orderConfirmationSnapshot()
val parsed = PddScreenParser.parse(snapshot, config(), GOODS_ID, evidence())
assertTrue(parsed.specPanelOpen)
assertEquals(SpecPanelType.ORDER_CONFIRMATION, parsed.specPanelType)
assertEquals("panel", parsed.specPanelContainer?.path)
assertTrue(parsed.dimensions.isEmpty())
assertTrue(parsed.hasOrderSubmitAction)
assertEquals(null, parsed.quickConfirmationEntry)
}
@Test
fun orderConfirmationRejectsAmbiguousScrollableContainers() {
val snapshot = orderConfirmationSnapshot().let {
it.copy(
nodes = it.nodes + node(
"secondary-panel",
"",
0,
700,
1080,
1300,
scrollable = true,
className = "androidx.recyclerview.widget.RecyclerView",
),
)
}
val parsed = PddScreenParser.parse(snapshot, config(), GOODS_ID, evidence())
assertFalse(parsed.specPanelOpen)
assertEquals(SpecPanelType.UNKNOWN, parsed.specPanelType)
assertEquals(null, parsed.specPanelContainer)
}
@Test
fun referenceCategoryIsParsedAsSizeAfterPanelRestore() {
val snapshot = UiSnapshot(
PDD_PACKAGE,
ACTIVITY,
listOf(
node("content", "", 0, 0, 1080, 2200, resourceId = "android:id/content", className = "android.widget.FrameLayout"),
node("summary", "已选:绿色 100", 20, 300, 700, 360),
node("title", "确认款式", 20, 370, 300, 420),
node("panel", "", 0, 430, 1080, 1800, scrollable = true),
node("panel/color-heading", "颜色分类", 20, 500, 300, 550, parentPath = "panel"),
node("panel/green", "绿色", 30, 580, 260, 650, clickable = true, parentPath = "panel"),
node("panel/size-heading", "参考分类", 20, 720, 300, 770, parentPath = "panel"),
node("panel/100", "100 建议身高90-100", 30, 800, 360, 870, clickable = true, parentPath = "panel"),
node("confirm", "确定", 0, 2000, 1080, 2150, clickable = true),
),
)
val parsed = PddScreenParser.parse(snapshot, config(), GOODS_ID, evidence())
assertEquals(listOf("100 建议身高90-100"), parsed.dimensions.single { it.key == "size" }.values.map { it.text })
}
@Test
fun boundedScrollablePanelUsesHeadingsInsideItsOwnRegion() {
val snapshot = UiSnapshot(
@@ -825,6 +886,92 @@ class PddProductDetailCollectorTest {
assertTrue(event.targetClassName?.contains("recovery=success") == true)
}
@Test
fun orderConfirmationUsesAtMostThreePanelGesturesAndFindsReferenceCategory() {
val events = mutableListOf<AgentDiagnosticEvent>()
val driver = FakeCollectorDriver(
colors = listOf("绿色"),
sizes = listOf("100 建议身高90-100"),
startWithPanelClosed = true,
orderConfirmationAfterEntry = true,
orderRestorePagesBeforeSpecs = 2,
sizeHeadingLabel = "参考分类",
)
var clock = 0L
val result = PddProductDetailCollector(
driver,
{ clock },
{ clock += it },
taskId = 82,
diagnostic = events::add,
).collect(GOODS_ID, rule())
assertTrue(result.successful)
assertEquals(2, driver.restoreGestures)
assertEquals(listOf("100 建议身高90-100"), result.payload?.dimensions?.single { it.key == "size" }?.values)
assertFalse(driver.clickedLabels.any { it.contains("已选") || it.contains("提交订单") || it.contains("支付") })
assertEquals(
AgentDiagnosticReason.SPEC_PANEL_RESTORE_SUCCEEDED,
events.single { it.stage == AgentDiagnosticStage.SPEC_PANEL_ENTRY }.reason,
)
}
@Test
fun orderConfirmationStopsAfterThreeChangingViewsWithoutGuessingActions() {
val events = mutableListOf<AgentDiagnosticEvent>()
val driver = FakeCollectorDriver(
startWithPanelClosed = true,
orderConfirmationAfterEntry = true,
orderRestorePagesBeforeSpecs = 10,
)
var clock = 0L
val result = PddProductDetailCollector(
driver,
{ clock },
{ clock += it },
taskId = 83,
diagnostic = events::add,
).collect(GOODS_ID, rule())
assertTrue(result.successful)
assertEquals(3, driver.restoreGestures)
assertEquals(listOf("单独购买"), driver.clickedLabels)
assertEquals(
AgentDiagnosticReason.SPEC_PANEL_RESTORE_LIMIT,
events.single { it.stage == AgentDiagnosticStage.SPEC_PANEL_ENTRY }.reason,
)
}
@Test
fun orderConfirmationStopsWhenBoundedRestoreGestureFails() {
val events = mutableListOf<AgentDiagnosticEvent>()
val driver = FakeCollectorDriver(
startWithPanelClosed = true,
orderConfirmationAfterEntry = true,
orderRestorePagesBeforeSpecs = 2,
verticalSwipeSucceeds = false,
)
var clock = 0L
val result = PddProductDetailCollector(
driver,
{ clock },
{ clock += it },
taskId = 84,
diagnostic = events::add,
).collect(GOODS_ID, rule())
assertTrue(result.successful)
assertEquals(0, driver.restoreGestures)
assertEquals(listOf("单独购买"), driver.clickedLabels)
assertEquals(
AgentDiagnosticReason.SPEC_PANEL_RESTORE_GESTURE_FAILED,
events.single { it.stage == AgentDiagnosticStage.SPEC_PANEL_ENTRY }.reason,
)
}
@Test
fun repeatedReviewMisrouteStopsAfterSingleRecovery() {
val driver = FakeCollectorDriver(startWithPanelClosed = true, alwaysEntryOpensReview = true)
@@ -931,6 +1078,9 @@ class PddProductDetailCollectorTest {
private val verticalSwipeSucceeds: Boolean = true,
private val quickConfirmationAfterEntry: Boolean = false,
private val hideQuickConfirmationSpec: Boolean = false,
private val orderConfirmationAfterEntry: Boolean = false,
private val orderRestorePagesBeforeSpecs: Int = 0,
private val sizeHeadingLabel: String = "尺码",
) : PddCollectorDriver {
var captureCount = 0
var clickCount = 0
@@ -939,6 +1089,7 @@ class PddProductDetailCollectorTest {
var pullDownCount = 0
var backCount = 0
var entryClickCount = 0
var restoreGestures = 0
private var selected: String? = null
private var previousSelected: String? = null
private var horizontalPage = 0
@@ -986,6 +1137,9 @@ class PddProductDetailCollectorTest {
add(node("buy-now", "现在买", 500, 1800, 1080, 2180, clickable = true))
})
}
if (orderConfirmationAfterEntry && restoreGestures < orderRestorePagesBeforeSpecs) {
return orderConfirmationSnapshot(restoreGestures)
}
val nodes = mutableListOf(
node("content", "", 0, 0, 1080, 2200, resourceId = "android:id/content", className = "android.widget.FrameLayout"),
node("title", "这是一个足够长的拼多多测试商品标题", 20, 100, 1000, 180),
@@ -1017,7 +1171,7 @@ class PddProductDetailCollectorTest {
)
}
}
if (!continuationPage) nodes += node("scroll/size-heading", "尺码", 20, 700, 300, 750, parentPath = "scroll")
if (!continuationPage) nodes += node("scroll/size-heading", sizeHeadingLabel, 20, 700, 300, 750, parentPath = "scroll")
sizePages[verticalPage.coerceAtMost(sizePages.lastIndex)].forEachIndexed { index, size ->
nodes += node("scroll/size-$size-$captureCount", size, 30 + index * 230, 770, 220 + index * 230, 840, clickable = true, parentPath = "scroll")
}
@@ -1086,6 +1240,13 @@ class PddProductDetailCollectorTest {
return true
}
override fun pullDownSpecPanel(anchor: SnapshotNode): Boolean {
if (!anchor.scrollable || !verticalSwipeSucceeds) return false
restoreGestures++
swipes += SwipeDirection.DOWN to anchor
return true
}
override fun pullDownGoodsPage(): Boolean { pullDownCount++; return true }
override fun back(): Boolean {
@@ -1119,6 +1280,24 @@ class PddProductDetailCollectorTest {
clickable, scrollable, selected, false, true, true,
)
private fun orderConfirmationSnapshot(row: Int = 0) = UiSnapshot(
PDD_PACKAGE,
ACTIVITY,
listOf(
node("content", "", 0, 0, 1080, 2328, resourceId = "android:id/content", className = "android.widget.FrameLayout"),
node("close", "关闭", 975, 273, 1050, 348, clickable = true, className = "android.widget.ImageView"),
node("summary", "已选:绿色两件套 100 建议身高90-100", 396, 654, 1053, 768),
node("quantity", "1", 480, 804, 567, 879, className = "android.widget.EditText"),
node("decrease", "减少数量", 396, 804, 474, 879, clickable = true, className = "android.widget.ImageView"),
node("increase", "增加数量", 567, 804, 645, 879, clickable = true, className = "android.widget.ImageView"),
node("panel", "", 0, 1052, 1080, 2079, scrollable = true, className = "androidx.recyclerview.widget.RecyclerView"),
node("panel/row", "确认内容$row", 20, 1200, 1000, 1280, clickable = true, parentPath = "panel"),
node("payment", "使用微信支付,更换先用后付可0元下单", 112, 2100, 930, 2157),
node("submit", "", 0, 2181, 1080, 2328, clickable = true, className = "android.widget.FrameLayout"),
node("submit/text", "提交订单 ¥9.82", 306, 2225, 627, 2284, parentPath = "submit"),
),
)
private fun evidence() = PageEvidence(PDD_PACKAGE, ACTIVITY, NodeSelector(resourceId = "android:id/content", className = "android.widget.FrameLayout"))
private fun detailSnapshot(
@@ -1135,7 +1314,7 @@ class PddProductDetailCollectorTest {
private fun config(maxSkuCount: Int = 500) = PddCollectorConfig(
"pddProductDetailV1", "safeBottomSpecEntryV1", "pddRmbPriceV1", "color",
listOf("颜色分类", "颜色", "款式"), listOf("尺码", "尺寸", "套餐"),
listOf("颜色分类", "颜色", "款式"), listOf("尺码", "尺寸", "套餐", "参考分类"),
mapOf("page" to 1000, "specPanel" to 1000, "selection" to 500, "price" to 500, "overall" to 10000),
mapOf("goodsPageVerticalSwipes" to 1, "specHorizontalSwipes" to 3, "specVerticalSwipes" to 3, "stableEdgeReads" to 1, "stablePriceReads" to 2, "maxSkuCount" to maxSkuCount),
)
+3 -2
View File
@@ -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: 2c2d84b24108217e4242337a6afc69db64ff924c
synchronized_at: 2026-08-27T06:57:31Z
wiki_revision: f3d85bf36ae7543aaa3fa1c6926e9bc0f221fb1e
synchronized_at: 2026-08-27T09:22:25Z
<!-- gitea-wiki-mirror:end -->
# 业务规则与术语
@@ -89,6 +89,7 @@ synchronized_at: 2026-08-27T06:57:31Z
- v2 任务首次未进入精确商品详情页时,可以按规则显式重开同一浏览器 URL 一次;登录、验证码、风控、无效链接和详情页内采集失败不触发该恢复。
- 假售罄恢复只使用主商品规格/购买强证据判断页面是否正常;顶部“相似商品”可以作为受控兜底证据,推荐卡片的标题、价格和销量不得冒充主商品证据。
- 商品规格遍历必须用已识别规格节点锁定横向颜色容器和纵向面板容器;颜色按视觉行蛇形遍历,滑动完成后重新读取节点,尺码只读并允许在标题滚出后沿已锁定容器续页。
- PDD 已选规格的订单确认形态只可由详情页、选择摘要、唯一数量控件、支付区、唯一底部提交动作和唯一主要滚动容器的组合证据确认;底部提交动作只读且永久禁止点击。Agent 只在该已确认容器内最多三次向下拖动回顶,每次重新读取节点;“参考分类”作为尺码的精确标题别名处理。
- Agent 可扩展,但规则必须按任务类型授权:采集规则不能创建订单,采购规则未来可以使用独立的创建订单能力,任何规则都不能付款。
## 采集任务
@@ -1,6 +1,7 @@
package rulecontract
import (
"bytes"
"strings"
"testing"
)
@@ -68,6 +69,9 @@ func TestBuiltInPDDProductDetailTemplateIsValid(t *testing.T) {
if strings.Join(required, ",") != "action.swipe.v1,collector.pdd.product-detail.v1,rule.schema.v2" {
t.Fatalf("unexpected template capabilities: %v", required)
}
if !bytes.Contains(raw, []byte(`"size":["尺码","尺寸","套餐","参考分类"]`)) {
t.Fatal("built-in template must classify 参考分类 as size")
}
}
func TestV2ValidatesTransientSoldOutRecovery(t *testing.T) {
+1 -1
View File
@@ -17,7 +17,7 @@ var pddProductDetailTemplate = json.RawMessage(`{
"hooks":{"afterSpecPanelOpen":[]},
"collector":{
"collectorId":"pddProductDetailV1","specEntryStrategy":"safeBottomSpecEntryV1","priceParser":"pddRmbPriceV1","priceGranularity":"color",
"dimensionAliases":{"color":["颜色分类","颜色","款式","颜色款式","花色"],"size":["尺码","尺寸","套餐"]},
"dimensionAliases":{"color":["颜色分类","颜色","款式","颜色款式","花色"],"size":["尺码","尺寸","套餐","参考分类"]},
"timeoutsMs":{"page":30000,"specPanel":10000,"selection":2000,"price":2000,"overall":180000},
"limits":{"goodsPageVerticalSwipes":3,"specHorizontalSwipes":12,"specVerticalSwipes":12,"stableEdgeReads":2,"stablePriceReads":2,"maxSkuCount":500}
}