Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
954552ed11 | ||
|
|
9e5f09abc5 |
@@ -30,6 +30,9 @@ android {
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
versionNameSuffix = "-370-diag1"
|
||||
}
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
|
||||
+75
-5
@@ -1392,6 +1392,12 @@ class PddProductDetailCollector(
|
||||
var diagnosticInitialSelectedSizes = 0
|
||||
var diagnosticSelectedSummaryPresent = false
|
||||
var diagnosticHorizontalSwipes = 0
|
||||
var diagnosticVerticalSwipes = 0
|
||||
var diagnosticTrailingEmptyReads = 0
|
||||
var diagnosticHorizontalUnknown = false
|
||||
// Exact seed sets can reconcile a later stable visit to the same row.
|
||||
// These signatures stay in memory; only the aggregate tri-state is saved.
|
||||
val diagnosticHorizontalRows = mutableMapOf<Set<String>, Boolean?>()
|
||||
var diagnosticTermination = AgentDiagnosticReason.COLOR_FOUND
|
||||
val imageAttempts = mutableSetOf<String>()
|
||||
|
||||
@@ -1448,6 +1454,13 @@ class PddProductDetailCollector(
|
||||
initialSelectedSizeCount = diagnosticInitialSelectedSizes,
|
||||
selectedSummaryPresent = diagnosticSelectedSummaryPresent,
|
||||
horizontalSwipes = diagnosticHorizontalSwipes,
|
||||
verticalSwipes = diagnosticVerticalSwipes,
|
||||
trailingEmptyReads = diagnosticTrailingEmptyReads,
|
||||
horizontalIncomplete = when {
|
||||
diagnosticHorizontalRows.values.any { it == true } -> true
|
||||
diagnosticHorizontalUnknown || diagnosticHorizontalRows.values.any { it == null } -> null
|
||||
else -> false
|
||||
},
|
||||
elapsedMs = now() - diagnosticStartedAt,
|
||||
)
|
||||
}
|
||||
@@ -1464,6 +1477,8 @@ class PddProductDetailCollector(
|
||||
val initialRows = colorRows(initial)
|
||||
observeColorDiscovery(initial, initialRows)
|
||||
if (initialRows.isEmpty()) {
|
||||
diagnosticTrailingEmptyReads = 1
|
||||
diagnosticHorizontalUnknown = true
|
||||
finishColorDiscovery(
|
||||
if (diagnosticNonClickableCandidates > 0) AgentDiagnosticReason.COLOR_VALUES_NOT_CLICKABLE
|
||||
else AgentDiagnosticReason.COLOR_EDGE_REACHED,
|
||||
@@ -1471,7 +1486,8 @@ class PddProductDetailCollector(
|
||||
return null
|
||||
}
|
||||
val attempted = mutableSetOf<String>()
|
||||
fun collectVisibleColors(): PddCollectorResult? {
|
||||
fun collectVisibleColors(onFirstRows: ((List<List<VisibleSpecValue>>) -> Unit)? = null): PddCollectorResult? {
|
||||
var firstRead = true
|
||||
while (true) {
|
||||
if (now() > deadline) return failure("RULE_NOT_MATCHED", "采集超过规则总超时")
|
||||
val screen = parse(goodsId, config, evidence)
|
||||
@@ -1479,6 +1495,10 @@ class PddProductDetailCollector(
|
||||
if (!screen.pageEvidenceMatched) return failure("RULE_NOT_MATCHED", "采集期间离开 PDD 商品详情页")
|
||||
screen.dimensions.filter { it.key == "unsupported" }.forEach { unsupported += it.name }
|
||||
val rows = colorRows(screen)
|
||||
if (firstRead) {
|
||||
onFirstRows?.invoke(rows)
|
||||
firstRead = false
|
||||
}
|
||||
observeColorDiscovery(screen, rows)
|
||||
rows.flatten().filter { SpecValueNormalizer.isSafeColor(it.text) }.forEach { value ->
|
||||
colors[value.text] = colors[value.text] == true || value.available
|
||||
@@ -1531,8 +1551,18 @@ class PddProductDetailCollector(
|
||||
rowSeeds.forEachIndexed { rowIndex, seed ->
|
||||
val moveRight = rowIndex % 2 == 0
|
||||
val horizontalSignatureReads = mutableMapOf<List<String>, Int>()
|
||||
var diagnosticPreviousSignature: List<String>? = null
|
||||
var diagnosticStableReads = 0
|
||||
var diagnosticMoved = false
|
||||
var diagnosticRowUncertain = false
|
||||
var diagnosticRowIncomplete: Boolean? = null
|
||||
for (horizontalPass in 0..config.limits.getValue("specHorizontalSwipes")) {
|
||||
collectVisibleColors()?.let { return it }
|
||||
var diagnosticBeforeClicks: List<String>? = null
|
||||
collectVisibleColors { observedRows ->
|
||||
diagnosticBeforeClicks = observedRows.filter { row -> row.any { it.text in seed } }
|
||||
.singleOrNull()?.sortedBy { it.node.bounds.left }
|
||||
?.let { if (moveRight) it else it.reversed() }?.let(::optionSignature)
|
||||
}?.let { return it }
|
||||
screen = parse(goodsId, config, evidence)
|
||||
screen.problem?.let { return failure(it.code, it.message) }
|
||||
if (!screen.pageEvidenceMatched) return failure("RULE_NOT_MATCHED", "采集期间离开 PDD 商品详情页")
|
||||
@@ -1541,21 +1571,49 @@ class PddProductDetailCollector(
|
||||
val matchedRow = rows.maxByOrNull { row -> row.count { it.text in seed } }
|
||||
?.takeIf { row -> row.any { it.text in seed } }
|
||||
if (matchedRow == null) {
|
||||
diagnosticHorizontalUnknown = true
|
||||
diagnosticTermination = AgentDiagnosticReason.COLOR_ROW_REFLOWED
|
||||
break
|
||||
}
|
||||
val currentRow = matchedRow.sortedBy { it.node.bounds.left }
|
||||
.let { if (moveRight) it else it.reversed() }
|
||||
val signature = optionSignature(currentRow)
|
||||
// Observe the frame immediately after the prior swipe,
|
||||
// before clicking newly visible values. A click reflow or
|
||||
// ambiguous row match cannot prove horizontal movement.
|
||||
if (diagnosticBeforeClicks == null || diagnosticBeforeClicks != signature) {
|
||||
diagnosticRowUncertain = true
|
||||
diagnosticHorizontalUnknown = true
|
||||
}
|
||||
if (!diagnosticRowUncertain && diagnosticPreviousSignature != null && signature != diagnosticPreviousSignature) {
|
||||
diagnosticMoved = true
|
||||
}
|
||||
diagnosticStableReads = if (signature == diagnosticPreviousSignature) diagnosticStableReads + 1 else 0
|
||||
diagnosticPreviousSignature = signature
|
||||
val signatureReads = (horizontalSignatureReads[signature] ?: 0) + 1
|
||||
horizontalSignatureReads[signature] = signatureReads
|
||||
if (signatureReads > config.limits.getValue("stableEdgeReads")) break
|
||||
if (horizontalPass == config.limits.getValue("specHorizontalSwipes")) break
|
||||
if (signatureReads > config.limits.getValue("stableEdgeReads")) {
|
||||
if (!diagnosticRowUncertain && diagnosticStableReads >= config.limits.getValue("stableEdgeReads")) {
|
||||
diagnosticRowIncomplete = false
|
||||
}
|
||||
break
|
||||
}
|
||||
if (horizontalPass == config.limits.getValue("specHorizontalSwipes")) {
|
||||
if (diagnosticMoved && !diagnosticRowUncertain) diagnosticRowIncomplete = true
|
||||
break
|
||||
}
|
||||
val direction = if (moveRight) SwipeDirection.LEFT else SwipeDirection.RIGHT
|
||||
if (!driver.swipeSpec(direction, currentRow.first().node)) break
|
||||
if (!driver.swipeSpec(direction, currentRow.first().node)) {
|
||||
if (diagnosticMoved && !diagnosticRowUncertain) diagnosticRowIncomplete = true
|
||||
break
|
||||
}
|
||||
diagnosticHorizontalSwipes++
|
||||
pause(350)
|
||||
}
|
||||
// An uncertain revisit cannot erase previously observed
|
||||
// unresolved movement; only reliable stability clears it.
|
||||
diagnosticHorizontalRows[seed] = diagnosticRowIncomplete
|
||||
?: diagnosticHorizontalRows[seed]?.takeIf { it }
|
||||
}
|
||||
collectVisibleColors()?.let { return it }
|
||||
screen = parse(goodsId, config, evidence)
|
||||
@@ -1564,6 +1622,10 @@ class PddProductDetailCollector(
|
||||
observeColorDiscovery(screen, rows)
|
||||
}
|
||||
|
||||
// One observation per outer discovery round, never per click or
|
||||
// stable-price reread. Empty parsed rows do not prove an edge.
|
||||
diagnosticTrailingEmptyReads = if (rows.isEmpty()) diagnosticTrailingEmptyReads + 1 else 0
|
||||
if (rows.isEmpty()) diagnosticHorizontalUnknown = true
|
||||
if (!discoverVertically) {
|
||||
diagnosticTermination = AgentDiagnosticReason.COLOR_EDGE_REACHED
|
||||
break
|
||||
@@ -1586,6 +1648,7 @@ class PddProductDetailCollector(
|
||||
}
|
||||
val anchor = rows.flatten().firstOrNull()?.node ?: specPanelContainer
|
||||
if (anchor == null) {
|
||||
diagnosticHorizontalUnknown = true
|
||||
diagnosticTermination = AgentDiagnosticReason.COLOR_CONTAINER_UNAVAILABLE
|
||||
break
|
||||
}
|
||||
@@ -1593,6 +1656,7 @@ class PddProductDetailCollector(
|
||||
diagnosticTermination = AgentDiagnosticReason.COLOR_SWIPE_FAILED
|
||||
break
|
||||
}
|
||||
diagnosticVerticalSwipes++
|
||||
pause(350)
|
||||
}
|
||||
finishColorDiscovery()
|
||||
@@ -1621,6 +1685,9 @@ class PddProductDetailCollector(
|
||||
initialSelectedSizeCount: Int,
|
||||
selectedSummaryPresent: Boolean,
|
||||
horizontalSwipes: Int,
|
||||
verticalSwipes: Int,
|
||||
trailingEmptyReads: Int,
|
||||
horizontalIncomplete: Boolean?,
|
||||
elapsedMs: Long,
|
||||
) {
|
||||
if (taskId <= 0) return
|
||||
@@ -1639,6 +1706,9 @@ class PddProductDetailCollector(
|
||||
initialSelectedSizeCount = initialSelectedSizeCount,
|
||||
selectedSummaryPresent = selectedSummaryPresent,
|
||||
horizontalSwipeCount = horizontalSwipes,
|
||||
colorVerticalSwipeCount = verticalSwipes,
|
||||
colorTrailingEmptyReadCount = trailingEmptyReads,
|
||||
colorHorizontalIncomplete = horizontalIncomplete,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
+14
-2
@@ -1,7 +1,7 @@
|
||||
package cn.ilapage.goauto.agent.persistence
|
||||
|
||||
internal object AgentDiagnosticSchema {
|
||||
const val VERSION = 4
|
||||
const val VERSION = 5
|
||||
|
||||
val failureSnapshotStatements = listOf(
|
||||
"""CREATE TABLE IF NOT EXISTS purchase_failure_snapshot (
|
||||
@@ -38,6 +38,12 @@ internal object AgentDiagnosticSchema {
|
||||
"horizontal_swipe_count" to "INTEGER",
|
||||
)
|
||||
|
||||
val colorDiscoveryOutcomeColumns = linkedMapOf(
|
||||
"color_vertical_swipe_count" to "INTEGER",
|
||||
"color_trailing_empty_read_count" to "INTEGER",
|
||||
"color_horizontal_incomplete" to "INTEGER",
|
||||
)
|
||||
|
||||
val createTableSql =
|
||||
"""CREATE TABLE agent_diagnostic (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -66,6 +72,9 @@ internal object AgentDiagnosticSchema {
|
||||
initial_selected_size_count INTEGER,
|
||||
selected_summary_present INTEGER,
|
||||
horizontal_swipe_count INTEGER,
|
||||
color_vertical_swipe_count INTEGER,
|
||||
color_trailing_empty_read_count INTEGER,
|
||||
color_horizontal_incomplete INTEGER,
|
||||
task_type TEXT,
|
||||
task_attempt_id TEXT,
|
||||
device_id INTEGER,
|
||||
@@ -87,5 +96,8 @@ internal object AgentDiagnosticSchema {
|
||||
(if (oldVersion < 3 && newVersion >= 3) purchaseDiagnosticColumns.mapNotNull { (name, definition) ->
|
||||
if (name in existingColumns) null else "ALTER TABLE agent_diagnostic ADD COLUMN $name $definition"
|
||||
} else emptyList()) +
|
||||
(if (oldVersion < 4 && newVersion >= 4) failureSnapshotStatements else emptyList())
|
||||
(if (oldVersion < 4 && newVersion >= 4) failureSnapshotStatements else emptyList()) +
|
||||
(if (oldVersion < 5 && newVersion >= 5) colorDiscoveryOutcomeColumns.mapNotNull { (name, definition) ->
|
||||
if (name in existingColumns) null else "ALTER TABLE agent_diagnostic ADD COLUMN $name $definition"
|
||||
} else emptyList())
|
||||
}
|
||||
|
||||
@@ -114,6 +114,12 @@ data class AgentDiagnosticEvent(
|
||||
val deviceId: Long? = null,
|
||||
val phase: String? = null,
|
||||
val ruleSnapshotHash: String? = null,
|
||||
/** Successfully dispatched UP swipes during this color discovery; null means unknown. */
|
||||
val colorVerticalSwipeCount: Int? = null,
|
||||
/** Consecutive empty parsed-color discovery rounds at termination, excluding click/price reads. */
|
||||
val colorTrailingEmptyReadCount: Int? = null,
|
||||
/** Required horizontal exploration still unresolved at termination; not needed or recovered is false. */
|
||||
val colorHorizontalIncomplete: Boolean? = null,
|
||||
)
|
||||
|
||||
class SafeAgentDiagnosticRecorder(
|
||||
@@ -197,6 +203,9 @@ class AgentDiagnosticStore(context: Context) : SQLiteOpenHelper(context, DATABAS
|
||||
putNullableInt("initial_selected_size_count", event.initialSelectedSizeCount)
|
||||
putNullableBoolean("selected_summary_present", event.selectedSummaryPresent)
|
||||
putNullableInt("horizontal_swipe_count", event.horizontalSwipeCount)
|
||||
putNullableInt("color_vertical_swipe_count", event.colorVerticalSwipeCount)
|
||||
putNullableInt("color_trailing_empty_read_count", event.colorTrailingEmptyReadCount)
|
||||
putNullableBoolean("color_horizontal_incomplete", event.colorHorizontalIncomplete)
|
||||
put("agent_version", BuildConfig.VERSION_NAME)
|
||||
put("created_at", event.createdAt)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,27 @@ import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class AgentDiagnosticRecorderTest {
|
||||
@Test
|
||||
fun colorDiscoveryOutcomeDefaultsToUnknownAndPreservesExplicitValues() {
|
||||
val event = AgentDiagnosticEvent(
|
||||
taskId = 370,
|
||||
stage = AgentDiagnosticStage.COLOR_DISCOVERY,
|
||||
reason = AgentDiagnosticReason.COLOR_EDGE_REACHED,
|
||||
)
|
||||
assertEquals(null, event.colorVerticalSwipeCount)
|
||||
assertEquals(null, event.colorTrailingEmptyReadCount)
|
||||
assertEquals(null, event.colorHorizontalIncomplete)
|
||||
val completed = event.copy(
|
||||
colorVerticalSwipeCount = 4,
|
||||
colorTrailingEmptyReadCount = 2,
|
||||
colorHorizontalIncomplete = false,
|
||||
)
|
||||
assertEquals(4, completed.colorVerticalSwipeCount)
|
||||
assertEquals(2, completed.colorTrailingEmptyReadCount)
|
||||
assertEquals(false, completed.colorHorizontalIncomplete)
|
||||
assertEquals(true, completed.copy(colorHorizontalIncomplete = true).colorHorizontalIncomplete)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun diagnosticsAreBoundedToFiftyRecordsAndSevenDays() {
|
||||
assertEquals(50, AgentDiagnosticRetentionPolicy.MAX_RECORDS)
|
||||
|
||||
+248
-3
@@ -1220,6 +1220,246 @@ class PddProductDetailCollectorTest {
|
||||
assertTrue(driver.swipes.any { it.first == SwipeDirection.UP })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `color discovery counts only its own successful vertical swipes`() {
|
||||
val colors = listOf("A色", "B色", "C色")
|
||||
val events = mutableListOf<AgentDiagnosticEvent>()
|
||||
val driver = FakeCollectorDriver(
|
||||
colors = colors,
|
||||
colorVerticalPages = listOf(colors.take(2), colors.takeLast(2)),
|
||||
sizePages = listOf(listOf("S"), listOf("M"), listOf("L")),
|
||||
rowSize = 1,
|
||||
hideColorHeadingAfterFirstVerticalPage = true,
|
||||
)
|
||||
var clock = 0L
|
||||
|
||||
val result = PddProductDetailCollector(driver, { clock }, { clock += it }, taskId = 124, diagnostic = events::add)
|
||||
.collect(GOODS_ID, rule(config().copy(timeoutsMs = config().timeoutsMs + ("overall" to 30000))))
|
||||
|
||||
assertTrue(result.successful)
|
||||
val event = events.single { it.stage == AgentDiagnosticStage.COLOR_DISCOVERY }
|
||||
assertEquals(2, event.colorVerticalSwipeCount)
|
||||
assertTrue(driver.swipes.count { it.first == SwipeDirection.UP } > requireNotNull(event.colorVerticalSwipeCount))
|
||||
assertTrue(driver.swipes.any { it.first == SwipeDirection.DOWN })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `color discovery failed vertical dispatch is not counted`() {
|
||||
val events = mutableListOf<AgentDiagnosticEvent>()
|
||||
val driver = FakeCollectorDriver(colors = listOf("A色", "B色"), rowSize = 1, verticalSwipeSucceeds = false)
|
||||
var clock = 0L
|
||||
|
||||
val result = PddProductDetailCollector(driver, { clock }, { clock += it }, taskId = 124, diagnostic = events::add)
|
||||
.collect(GOODS_ID, rule())
|
||||
|
||||
assertTrue(result.successful)
|
||||
val event = events.single { it.stage == AgentDiagnosticStage.COLOR_DISCOVERY }
|
||||
assertEquals(AgentDiagnosticReason.COLOR_SWIPE_FAILED, event.reason)
|
||||
assertEquals(0, event.colorVerticalSwipeCount)
|
||||
assertEquals(0, event.colorTrailingEmptyReadCount)
|
||||
assertEquals(false, event.colorHorizontalIncomplete)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `color discovery counts empty parsed rounds after heading scrolls away without counting price reads`() {
|
||||
val colors = listOf("A色", "B色", "C色", "D色")
|
||||
val events = mutableListOf<AgentDiagnosticEvent>()
|
||||
val driver = FakeCollectorDriver(
|
||||
colors = colors,
|
||||
colorVerticalPages = listOf(colors.take(2), colors.takeLast(2)),
|
||||
rowSize = 1,
|
||||
hideColorHeadingAfterFirstVerticalPage = true,
|
||||
priceDelayReads = colors.associateWith { 2 },
|
||||
)
|
||||
var clock = 0L
|
||||
|
||||
val result = PddProductDetailCollector(driver, { clock }, { clock += it }, taskId = 124, diagnostic = events::add)
|
||||
.collect(GOODS_ID, rule(config().copy(timeoutsMs = config().timeoutsMs + ("overall" to 30000))))
|
||||
|
||||
assertTrue(result.successful)
|
||||
// This fixture retains the options while their heading disappears. The
|
||||
// existing parser/traversal still misses the second page in this stage.
|
||||
assertEquals(colors.take(2), requireNotNull(result.payload).dimensions.first { it.key == "color" }.values)
|
||||
val event = events.single { it.stage == AgentDiagnosticStage.COLOR_DISCOVERY }
|
||||
assertEquals(2, event.colorTrailingEmptyReadCount)
|
||||
assertEquals(2, event.colorVerticalSwipeCount)
|
||||
assertEquals(null, event.colorHorizontalIncomplete)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `color discovery initially empty is one empty round with unknown horizontal state`() {
|
||||
val events = mutableListOf<AgentDiagnosticEvent>()
|
||||
var clock = 0L
|
||||
|
||||
val result = PddProductDetailCollector(
|
||||
FakeCollectorDriver(colors = emptyList()), { clock }, { clock += it }, taskId = 124, diagnostic = events::add,
|
||||
).collect(GOODS_ID, rule())
|
||||
|
||||
assertTrue(result.successful)
|
||||
val event = events.single { it.stage == AgentDiagnosticStage.COLOR_DISCOVERY }
|
||||
assertEquals(0, event.colorVerticalSwipeCount)
|
||||
assertEquals(1, event.colorTrailingEmptyReadCount)
|
||||
assertEquals(null, event.colorHorizontalIncomplete)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `color discovery resets trailing empty rounds after nonempty discovery`() {
|
||||
val colors = listOf("A色", "B色", "C色", "D色")
|
||||
val events = mutableListOf<AgentDiagnosticEvent>()
|
||||
val driver = FakeCollectorDriver(
|
||||
colors = colors,
|
||||
colorVerticalPages = listOf(colors.take(2), emptyList(), colors.takeLast(2)),
|
||||
rowSize = 1,
|
||||
)
|
||||
var clock = 0L
|
||||
|
||||
val result = PddProductDetailCollector(driver, { clock }, { clock += it }, taskId = 124, diagnostic = events::add)
|
||||
.collect(GOODS_ID, rule(config().copy(timeoutsMs = config().timeoutsMs + ("overall" to 30000))))
|
||||
|
||||
assertTrue(result.successful)
|
||||
assertEquals(colors, requireNotNull(result.payload).dimensions.first { it.key == "color" }.values)
|
||||
val event = events.single { it.stage == AgentDiagnosticStage.COLOR_DISCOVERY }
|
||||
assertEquals(3, event.colorVerticalSwipeCount)
|
||||
assertEquals(0, event.colorTrailingEmptyReadCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `color discovery stable horizontal rows are complete`() {
|
||||
val events = mutableListOf<AgentDiagnosticEvent>()
|
||||
var clock = 0L
|
||||
|
||||
val result = PddProductDetailCollector(
|
||||
FakeCollectorDriver(colors = listOf("A色", "B色")), { clock }, { clock += it }, taskId = 124, diagnostic = events::add,
|
||||
).collect(GOODS_ID, rule())
|
||||
|
||||
assertTrue(result.successful)
|
||||
val event = events.single { it.stage == AgentDiagnosticStage.COLOR_DISCOVERY }
|
||||
assertEquals(false, event.colorHorizontalIncomplete)
|
||||
assertEquals(0, event.colorTrailingEmptyReadCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `color discovery horizontal dispatch failure before movement stays unknown`() {
|
||||
val events = mutableListOf<AgentDiagnosticEvent>()
|
||||
val base = FakeCollectorDriver(colors = listOf("A色", "B色"))
|
||||
val driver = object : PddCollectorDriver by base {
|
||||
override fun swipeSpec(direction: SwipeDirection, anchor: SnapshotNode?): Boolean =
|
||||
if (direction == SwipeDirection.LEFT) false else base.swipeSpec(direction, anchor)
|
||||
}
|
||||
var clock = 0L
|
||||
|
||||
val result = PddProductDetailCollector(driver, { clock }, { clock += it }, taskId = 124, diagnostic = events::add)
|
||||
.collect(GOODS_ID, rule())
|
||||
|
||||
assertTrue(result.successful)
|
||||
val event = events.single { it.stage == AgentDiagnosticStage.COLOR_DISCOVERY }
|
||||
assertEquals(0, event.horizontalSwipeCount)
|
||||
assertEquals(null, event.colorHorizontalIncomplete)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `color discovery successful horizontal dispatch alone does not prove required exploration`() {
|
||||
val events = mutableListOf<AgentDiagnosticEvent>()
|
||||
var clock = 0L
|
||||
val limited = config().copy(limits = config().limits + mapOf("specHorizontalSwipes" to 1, "stableEdgeReads" to 2))
|
||||
|
||||
val result = PddProductDetailCollector(
|
||||
FakeCollectorDriver(colors = listOf("A色", "B色")), { clock }, { clock += it }, taskId = 124, diagnostic = events::add,
|
||||
).collect(GOODS_ID, rule(limited))
|
||||
|
||||
assertTrue(result.successful)
|
||||
val event = events.single { it.stage == AgentDiagnosticStage.COLOR_DISCOVERY }
|
||||
assertEquals(1, event.horizontalSwipeCount)
|
||||
assertEquals(null, event.colorHorizontalIncomplete)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `color discovery observed horizontal movement then failure is incomplete`() {
|
||||
val colors = listOf("A色", "B色", "C色")
|
||||
val events = mutableListOf<AgentDiagnosticEvent>()
|
||||
val base = FakeCollectorDriver(colors = colors, colorPages = listOf(colors.take(2), colors.takeLast(2)))
|
||||
var leftSwipes = 0
|
||||
val driver = object : PddCollectorDriver by base {
|
||||
override fun swipeSpec(direction: SwipeDirection, anchor: SnapshotNode?): Boolean {
|
||||
if (direction == SwipeDirection.LEFT && leftSwipes++ > 0) return false
|
||||
return base.swipeSpec(direction, anchor)
|
||||
}
|
||||
}
|
||||
var clock = 0L
|
||||
|
||||
val result = PddProductDetailCollector(driver, { clock }, { clock += it }, taskId = 124, diagnostic = events::add)
|
||||
.collect(GOODS_ID, rule())
|
||||
|
||||
assertTrue(result.successful)
|
||||
assertEquals(colors, requireNotNull(result.payload).dimensions.first { it.key == "color" }.values)
|
||||
val event = events.single { it.stage == AgentDiagnosticStage.COLOR_DISCOVERY }
|
||||
assertEquals(1, event.horizontalSwipeCount)
|
||||
assertEquals(true, event.colorHorizontalIncomplete)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `color discovery uncertain revisit preserves proven incomplete horizontal row`() {
|
||||
val colors = listOf("A色", "B色", "C色", "D色", "E色")
|
||||
val events = mutableListOf<AgentDiagnosticEvent>()
|
||||
val base = FakeCollectorDriver(
|
||||
colors = colors,
|
||||
colorPages = listOf(listOf("A色", "B色", "D色", "E色"), listOf("B色", "C色", "D色", "E色")),
|
||||
rowSize = 2,
|
||||
resetHorizontalPageOnVerticalSwipe = true,
|
||||
)
|
||||
var leftSwipes = 0
|
||||
val driver = object : PddCollectorDriver by base {
|
||||
override fun swipeSpec(direction: SwipeDirection, anchor: SnapshotNode?): Boolean {
|
||||
if (direction == SwipeDirection.LEFT && leftSwipes++ > 0) return false
|
||||
return base.swipeSpec(direction, anchor)
|
||||
}
|
||||
}
|
||||
var clock = 0L
|
||||
|
||||
val result = PddProductDetailCollector(driver, { clock }, { clock += it }, taskId = 124, diagnostic = events::add)
|
||||
.collect(GOODS_ID, rule())
|
||||
|
||||
assertTrue(result.successful)
|
||||
assertTrue(requireNotNull(result.payload).dimensions.first { it.key == "color" }.values.contains("C色"))
|
||||
assertTrue(leftSwipes > 2)
|
||||
// The first A/B visit moves then fails; later A/B visits fail before
|
||||
// movement. Unknown new evidence must not erase that unresolved work.
|
||||
assertEquals(true, events.single { it.stage == AgentDiagnosticStage.COLOR_DISCOVERY }.colorHorizontalIncomplete)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `color discovery observed horizontal movement at budget is incomplete`() {
|
||||
val colors = listOf("A色", "B色", "C色")
|
||||
val events = mutableListOf<AgentDiagnosticEvent>()
|
||||
var clock = 0L
|
||||
val limited = config().copy(limits = config().limits + ("specHorizontalSwipes" to 1))
|
||||
|
||||
val result = PddProductDetailCollector(
|
||||
FakeCollectorDriver(colors = colors, colorPages = listOf(colors.take(2), colors.takeLast(2))),
|
||||
{ clock }, { clock += it }, taskId = 124, diagnostic = events::add,
|
||||
).collect(GOODS_ID, rule(limited))
|
||||
|
||||
assertTrue(result.successful)
|
||||
assertEquals(true, events.single { it.stage == AgentDiagnosticStage.COLOR_DISCOVERY }.colorHorizontalIncomplete)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `color discovery disjoint horizontal reflow is unknown`() {
|
||||
val colors = listOf("A色", "B色", "C色", "D色")
|
||||
val events = mutableListOf<AgentDiagnosticEvent>()
|
||||
var clock = 0L
|
||||
|
||||
val result = PddProductDetailCollector(
|
||||
FakeCollectorDriver(colors = colors, colorPages = listOf(colors.take(2), colors.takeLast(2))),
|
||||
{ clock }, { clock += it }, taskId = 124, diagnostic = events::add,
|
||||
).collect(GOODS_ID, rule())
|
||||
|
||||
assertTrue(result.successful)
|
||||
val event = events.single { it.stage == AgentDiagnosticStage.COLOR_DISCOVERY }
|
||||
assertEquals(1, event.horizontalSwipeCount)
|
||||
assertEquals(null, event.colorHorizontalIncomplete)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `color discovery records only bounded aggregate evidence`() {
|
||||
val colors = listOf("A色", "B色", "C色", "D色")
|
||||
@@ -1857,6 +2097,7 @@ class PddProductDetailCollectorTest {
|
||||
sizes: List<String> = listOf("S"),
|
||||
private val colorPages: List<List<String>> = listOf(colors),
|
||||
private val colorVerticalPages: List<List<String>>? = null,
|
||||
private val resetHorizontalPageOnVerticalSwipe: Boolean = false,
|
||||
private val sizePages: List<List<String>> = listOf(sizes),
|
||||
private val prices: Map<String, Long> = colors.associateWith { 1000L },
|
||||
private val failedClicks: Set<String> = emptySet(),
|
||||
@@ -1867,6 +2108,7 @@ class PddProductDetailCollectorTest {
|
||||
private val rowSize: Int = 4,
|
||||
private val hideSelectedColorOption: Boolean = false,
|
||||
private val hideDimensionHeadingsAfterFirstVerticalPage: Boolean = false,
|
||||
private val hideColorHeadingAfterFirstVerticalPage: Boolean = false,
|
||||
private val soldOutUntilPulls: Int = 0,
|
||||
private val startWithPanelClosed: Boolean = false,
|
||||
private val firstEntryOpensReview: Boolean = false,
|
||||
@@ -1986,7 +2228,7 @@ class PddProductDetailCollectorTest {
|
||||
?: colorPages[horizontalPage.coerceAtMost(colorPages.lastIndex)]
|
||||
val colorRowCount = (visibleColors.size + rowSize - 1) / rowSize
|
||||
val sizeHeadingTop = maxOf(700, 470 + colorRowCount * 90 + 20)
|
||||
if (!continuationPage && !panelCollapsedNow) {
|
||||
if (!continuationPage && !panelCollapsedNow && !(hideColorHeadingAfterFirstVerticalPage && verticalPage > 0)) {
|
||||
nodes += node("scroll/color-heading", "颜色分类", 20, 400, 300, 450, parentPath = "scroll")
|
||||
}
|
||||
if (!continuationPage && !hideColorNow && !panelCollapsedNow) {
|
||||
@@ -2118,8 +2360,11 @@ class PddProductDetailCollectorTest {
|
||||
if ((direction == SwipeDirection.UP || direction == SwipeDirection.DOWN) && !verticalSwipeSucceeds) return false
|
||||
when (direction) {
|
||||
SwipeDirection.LEFT -> horizontalPage = (horizontalPage + 1).coerceAtMost(colorPages.lastIndex)
|
||||
SwipeDirection.UP -> verticalPage = (verticalPage + 1)
|
||||
.coerceAtMost(maxOf(sizePages.lastIndex, colorVerticalPages?.lastIndex ?: 0))
|
||||
SwipeDirection.UP -> {
|
||||
verticalPage = (verticalPage + 1)
|
||||
.coerceAtMost(maxOf(sizePages.lastIndex, colorVerticalPages?.lastIndex ?: 0))
|
||||
if (resetHorizontalPageOnVerticalSwipe) horizontalPage = 0
|
||||
}
|
||||
SwipeDirection.DOWN -> {
|
||||
verticalPage = (verticalPage - 1).coerceAtLeast(0)
|
||||
downSwipeCount++
|
||||
|
||||
+177
-1
@@ -3,10 +3,112 @@ package cn.ilapage.goauto.agent.persistence
|
||||
import java.sql.Connection
|
||||
import java.sql.DriverManager
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class AgentDiagnosticStoreMigrationTest {
|
||||
@Test
|
||||
fun freshV5DatabaseHasNullableColorDiscoveryOutcomeColumns() = withDatabase { db ->
|
||||
db.createStatement().use { it.execute(AgentDiagnosticSchema.createTableSql) }
|
||||
assertEquals(5, AgentDiagnosticSchema.VERSION)
|
||||
assertV5Columns(db)
|
||||
insertOldDiagnostic(db)
|
||||
assertOldDiagnosticPreserved(db)
|
||||
for (incomplete in listOf(0, 1)) {
|
||||
db.createStatement().use { statement ->
|
||||
statement.executeUpdate(
|
||||
"UPDATE agent_diagnostic SET color_vertical_swipe_count=4, " +
|
||||
"color_trailing_empty_read_count=2, color_horizontal_incomplete=$incomplete",
|
||||
)
|
||||
statement.executeQuery(
|
||||
"SELECT color_vertical_swipe_count, color_trailing_empty_read_count, " +
|
||||
"color_horizontal_incomplete FROM agent_diagnostic",
|
||||
).use { row ->
|
||||
assertTrue(row.next())
|
||||
assertEquals(4, row.getInt(1))
|
||||
assertEquals(2, row.getInt(2))
|
||||
assertEquals(incomplete, row.getInt(3))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun everySupportedVersionUpgradesToV5WithoutChangingExistingRows() {
|
||||
for (oldVersion in 1..4) withDatabase { db ->
|
||||
createOldDatabase(db, oldVersion)
|
||||
insertOldDiagnostic(db)
|
||||
val existingTables = snapshotTables(db)
|
||||
|
||||
migrate(db, oldVersion, 5)
|
||||
|
||||
assertV5Columns(db)
|
||||
assertOldDiagnosticPreserved(db)
|
||||
existingTables.forEach { (name, rows) -> assertEquals(name, rows, tableRows(db, name)) }
|
||||
assertTrue(columnNames(db).containsAll(AgentDiagnosticSchema.colorDiagnosticColumns.keys))
|
||||
assertTrue(columnNames(db).containsAll(AgentDiagnosticSchema.purchaseDiagnosticColumns.keys))
|
||||
assertEquals(3, snapshotTables(db).size)
|
||||
migrate(db, oldVersion, 5)
|
||||
assertOldDiagnosticPreserved(db)
|
||||
existingTables.forEach { (name, rows) -> assertEquals(name, rows, tableRows(db, name)) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun v4UpgradeAddsOnlyMissingColumnsAndKeepsExistingV5Values() = withDatabase { db ->
|
||||
createOldDatabase(db, 4)
|
||||
insertOldDiagnostic(db)
|
||||
db.createStatement().use {
|
||||
it.execute("ALTER TABLE agent_diagnostic ADD COLUMN color_vertical_swipe_count INTEGER")
|
||||
it.executeUpdate("UPDATE agent_diagnostic SET color_vertical_swipe_count=3")
|
||||
}
|
||||
val statements = AgentDiagnosticSchema.migrationStatements(4, 5, columnNames(db))
|
||||
assertEquals(2, statements.size)
|
||||
assertTrue(statements.all { it.startsWith("ALTER TABLE agent_diagnostic ADD COLUMN ") })
|
||||
migrate(db, 4, 5)
|
||||
assertV5Columns(db)
|
||||
db.createStatement().use { statement ->
|
||||
statement.executeQuery("SELECT color_vertical_swipe_count, color_trailing_empty_read_count, color_horizontal_incomplete FROM agent_diagnostic").use {
|
||||
assertTrue(it.next())
|
||||
assertEquals(3, it.getInt(1))
|
||||
assertNull(it.getObject(2))
|
||||
assertNull(it.getObject(3))
|
||||
}
|
||||
}
|
||||
assertTrue(AgentDiagnosticSchema.migrationStatements(4, 5, columnNames(db)).isEmpty())
|
||||
assertTrue(AgentDiagnosticSchema.migrationStatements(5, 5, columnNames(db)).isEmpty())
|
||||
assertTrue(AgentDiagnosticSchema.migrationStatements(4, 4, columnNames(db)).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun v5UpgradeSurvivesDatabaseReopen() {
|
||||
val file = java.io.File.createTempFile("goauto-diagnostic-migration-", ".db")
|
||||
try {
|
||||
Class.forName("org.sqlite.JDBC")
|
||||
DriverManager.getConnection("jdbc:sqlite:${file.absolutePath}").use { db ->
|
||||
createOldDatabase(db, 4)
|
||||
insertOldDiagnostic(db)
|
||||
migrate(db, 4, 5)
|
||||
db.createStatement().use { it.execute("PRAGMA user_version=5") }
|
||||
}
|
||||
DriverManager.getConnection("jdbc:sqlite:${file.absolutePath}").use { db ->
|
||||
assertV5Columns(db)
|
||||
assertOldDiagnosticPreserved(db)
|
||||
assertTrue(AgentDiagnosticSchema.migrationStatements(4, 5, columnNames(db)).isEmpty())
|
||||
assertEquals(3, snapshotTables(db).size)
|
||||
db.createStatement().use { statement ->
|
||||
statement.executeQuery("PRAGMA user_version").use {
|
||||
assertTrue(it.next())
|
||||
assertEquals(5, it.getInt(1))
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
check(file.delete()) { "Could not remove temporary migration test database" }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun v4AddsIndependentFailureSnapshotQueueWithoutChangingStructuredRows() = withDatabase { db ->
|
||||
db.createStatement().use {
|
||||
@@ -16,7 +118,6 @@ class AgentDiagnosticStoreMigrationTest {
|
||||
AgentDiagnosticSchema.migrationStatements(3, 4, columnNames(db)).forEach { sql ->
|
||||
db.createStatement().use { it.execute(sql) }
|
||||
}
|
||||
assertEquals(4, AgentDiagnosticSchema.VERSION)
|
||||
for (table in listOf("purchase_failure_snapshot", "purchase_failure_snapshot_queue", "purchase_failure_snapshot_context")) {
|
||||
db.prepareStatement("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?").use {
|
||||
it.setString(1, table)
|
||||
@@ -137,6 +238,80 @@ class AgentDiagnosticStoreMigrationTest {
|
||||
assertEquals(1, rowCount(db))
|
||||
}
|
||||
|
||||
private fun assertV5Columns(db: Connection) {
|
||||
val actual = mutableSetOf<String>()
|
||||
db.createStatement().use { statement ->
|
||||
statement.executeQuery("PRAGMA table_info(agent_diagnostic)").use { rows ->
|
||||
while (rows.next()) {
|
||||
val name = rows.getString("name")
|
||||
if (name in V5_COLUMNS) {
|
||||
actual.add(name)
|
||||
assertEquals(name, "INTEGER", rows.getString("type"))
|
||||
assertEquals(name, 0, rows.getInt("notnull"))
|
||||
assertNull(name, rows.getObject("dflt_value"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assertEquals(V5_COLUMNS, actual)
|
||||
}
|
||||
|
||||
private fun createOldDatabase(db: Connection, version: Int) {
|
||||
db.createStatement().use { it.execute(CREATE_V1_TABLE_SQL) }
|
||||
migrate(db, 1, version)
|
||||
if (version >= 4) db.createStatement().use {
|
||||
it.execute("INSERT INTO purchase_failure_snapshot VALUES ('attempt','test-origin',106,7,'spec_probe',1000,2000,'test','ready','{}',X'0102','test-fingerprint',2)")
|
||||
it.execute("INSERT INTO purchase_failure_snapshot_queue VALUES ('attempt','test-fingerprint',1500,2)")
|
||||
it.execute("INSERT INTO purchase_failure_snapshot_context VALUES ('attempt','test-origin','{}',1000)")
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertOldDiagnostic(db: Connection) {
|
||||
db.createStatement().use {
|
||||
it.execute("INSERT INTO agent_diagnostic (task_id,stage,reason,attempt,elapsed_ms,agent_version,created_at) VALUES (106,'COLOR_DISCOVERY','COLOR_EDGE_REACHED',2,10,'old',1000)")
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertOldDiagnosticPreserved(db: Connection) {
|
||||
assertEquals(1, rowCount(db))
|
||||
db.createStatement().use { statement ->
|
||||
statement.executeQuery("SELECT id,task_id,stage,reason,attempt,elapsed_ms,agent_version,created_at,color_vertical_swipe_count,color_trailing_empty_read_count,color_horizontal_incomplete FROM agent_diagnostic").use {
|
||||
assertTrue(it.next())
|
||||
assertEquals(1L, it.getLong(1))
|
||||
assertEquals(106L, it.getLong(2))
|
||||
assertEquals("COLOR_DISCOVERY", it.getString(3))
|
||||
assertEquals("COLOR_EDGE_REACHED", it.getString(4))
|
||||
assertEquals(2, it.getInt(5))
|
||||
assertEquals(10L, it.getLong(6))
|
||||
assertEquals("old", it.getString(7))
|
||||
assertEquals(1000L, it.getLong(8))
|
||||
for (index in 9..11) assertNull(it.getObject(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun snapshotTables(db: Connection): Map<String, List<List<String?>>> =
|
||||
db.createStatement().use { statement ->
|
||||
statement.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'purchase_failure_snapshot%'").use { rows ->
|
||||
buildList { while (rows.next()) add(rows.getString(1)) }
|
||||
}
|
||||
}.associateWith { tableRows(db, it) }
|
||||
|
||||
private fun tableRows(db: Connection, table: String): List<List<String?>> =
|
||||
db.createStatement().use { statement ->
|
||||
statement.executeQuery("SELECT * FROM $table ORDER BY attempt_id").use { rows ->
|
||||
buildList {
|
||||
while (rows.next()) add((1..rows.metaData.columnCount).map { rows.getString(it) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrate(db: Connection, oldVersion: Int, newVersion: Int) {
|
||||
AgentDiagnosticSchema.migrationStatements(oldVersion, newVersion, columnNames(db)).forEach { sql ->
|
||||
db.createStatement().use { it.execute(sql) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrateV1ToV2(db: Connection) {
|
||||
AgentDiagnosticSchema.v2MigrationStatements(1, 2, columnNames(db)).forEach { sql ->
|
||||
db.createStatement().use { it.execute(sql) }
|
||||
@@ -166,6 +341,7 @@ class AgentDiagnosticStoreMigrationTest {
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val V5_COLUMNS = setOf("color_vertical_swipe_count", "color_trailing_empty_read_count", "color_horizontal_incomplete")
|
||||
val CREATE_V1_TABLE_SQL =
|
||||
"""CREATE TABLE agent_diagnostic (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Architecture-and-Code-Map
|
||||
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Architecture-and-Code-Map.-
|
||||
wiki_revision: 6a6a1a9a13aaf5284e5a81a79f849ad28525ceba
|
||||
synchronized_at: 2026-10-08T07:10:44Z
|
||||
wiki_revision: 579f053988e3f67f287ff130f9c24a692d897191
|
||||
synchronized_at: 2026-10-09T02:06:38Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
<!-- gitea-wiki-mirror:start -->
|
||||
@@ -652,3 +652,24 @@ Web 唯一展示位置为“采集采购 → SYB 同步记录”:列表状态
|
||||
- Server `purchase/failure_snapshot*.go` 提供专用上传/安全摘要/管理员ZIP下载;`models/purchase_failure_snapshot.go` 独立私有表。追加迁移 `1791400000000_purchase_failure_snapshot.go` 保存 LONGBLOB ZIP、LONGTEXT manifest、attempt唯一约束及到期索引。
|
||||
- `access.AdminAPIs` 注册两个仅管理员读取接口;普通任务DTO和Client API不增加原始诊断字段。通用操作日志精确排除三个诊断端点,私有SQL读写使用静默logger。服务启动立即清理并每小时清理过期诊断,不依赖新任务。
|
||||
- Web `purchase-tasks/FailureSnapshotCell.vue` 是现有执行记录末尾200px管理员专属列,独立摘要查询、固定原因中文和附件下载;无原始XML在线预览。
|
||||
|
||||
|
||||
## Android 重购编排(#367)
|
||||
|
||||
实现绑定 `5430dc1`,工单分支尚未合并/安装/发布;无Server/Web、数据库或接口迁移。
|
||||
|
||||
- `TaskHistoryFragment` 把采购搜索栏入口改为“重购”,发送 PREPARE/CONFIRM/STOP、显示 Service 内存状态;一次确认与逐条摘要由 `RepurchaseState` 提供,不由 Fragment 驱动采购生命周期。
|
||||
- `RepurchaseBatchDiscovery` 注入既有设备历史查询和服务端HTTP Date时钟,负责全状态/50条页/30天窗口、相邻60秒边界、去重、完整性及重读首页;最多200页,不使用本地历史缓存执行重购。
|
||||
- `RepurchaseCoordinator` 通过小型 `RepurchasePort` 执行非阻塞状态推进;只调用原reset和只读详情,固定每任务requestId、expected attempt、10秒本地忙碌复核及15分钟单条上限。依靠原执行器的attemptNumber/UUID/phase/已知结果识别合法探测续行,拒绝外部更晚attempt冒充本轮结果。
|
||||
- `AgentForegroundService` 独立单线程scheduled executor每2秒推进编排,历史网络不阻塞心跳或taskExecutor;发现使用独享API客户端,reset只短暂预留既有TaskExecutionMutex。原scheduler继续唯一负责claim/start/真实PDD操作及Outbox,编排不持整轮锁、不新建执行器路径。
|
||||
- 新工作查询同时检查采集next、采购next及本设备活动状态历史,防止采购next优先返回本轮current而遮蔽其他pending。停止信号为原子roundId,配置/身份冻结,仅内存保存摘要;进程重启不恢复整轮,既有单任务恢复及失败私有诊断不变。
|
||||
- `AgentApiClient` 仅补读已有采购payload的attemptNumber和标准HTTP Date响应元数据;不添加服务端字段或API。Date缺失清空样本,不沿用手机时间。
|
||||
|
||||
## Android 颜色发现诊断追加(#370 第一阶段)
|
||||
|
||||
实现绑定 `9e5f09a`,独立工单分支,尚未合并 main;本阶段不修改颜色遍历和采购结果判定。
|
||||
|
||||
- `AgentDiagnosticSchema` 升至 v5,在 `agent_diagnostic` 追加可空 INTEGER `color_vertical_swipe_count`、`color_trailing_empty_read_count`、`color_horizontal_incomplete`;对应事件中的两个 Int? 和一个 Boolean?,Boolean 按 0/1 写入。新库包含列,v1–v4 逐级补齐并跳过已存在列,旧行新列为 NULL;不清表,不改独立采购失败快照、队列和 context。
|
||||
- `PddProductDetailCollector.collectColors` 只基于既有解析帧记录颜色阶段 UP 派发次数、空发现轮次和横向探索三态;不增加抓取、手势或等待。标签及签名只在内存使用,持久化仍为安全聚合。
|
||||
- 不新增 Server/Web API 字段、服务端迁移或权限;普通结果 payload/missing 与采购探测失败策略未改变。完整遍历与不完整候选拦截仍属 #370 后续待证据实施范围,不能把本地诊断值作为已实现的业务门禁。
|
||||
- 读取与三态限制见 Troubleshooting;旧版不保证降级打开 v5。JDBC 测试与实际 Android 安装升级分别验收,测试不代替真实竖向颜色采集验证。
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Troubleshooting
|
||||
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Troubleshooting
|
||||
wiki_revision: fee37efc79b644ecfa3237283dd45f0a09796f44
|
||||
synchronized_at: 2026-10-08T03:25:33Z
|
||||
wiki_revision: 6b370764320d1f5148fbc31f0ef6ce999370173d
|
||||
synchronized_at: 2026-10-09T02:06:57Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 故障排查
|
||||
@@ -76,7 +76,7 @@ Agent 0.9.13 起,颜色发现阶段在本地 `goauto_diagnostics.db` 的 `agen
|
||||
|
||||
记录只包含颜色行数、各行值数量、可点击颜色数、不可点击候选数、首次颜色点击前的颜色/尺码选中数、是否存在“已选”摘要、横向滑动次数、终止原因和耗时,不保存规格文案、价格、摘要原文、坐标、链接、goods_id、控件树、XML 或截图。
|
||||
|
||||
只对 Debug APK 使用以下读取流程:
|
||||
以下仅为 v4 之前纯结构化诊断库的历史读取方式。v4 及之后包含采购失败私有现场,不再按此导出整库;请使用下方“Android 颜色发现诊断 v5”中的定向只读方式:
|
||||
|
||||
```powershell
|
||||
adb shell run-as cn.ilapage.goauto.agent ls databases
|
||||
@@ -136,3 +136,25 @@ adb -s <serial> shell run-as cn.ilapage.goauto.agent sqlite3 -readonly databases
|
||||
- 完整订单证据出现就结束;超时保留原订单结果未知语义,不能因为等待更久就把任务改为可重试失败或自动重下单。
|
||||
- 固定期限从 Back 成功时起算,不是从提交订单或进入核单函数起算,也不会因新样本重置;capture 阻塞不能被该检查中断,待付款分支拥有独立预算。
|
||||
- 不因本次延长断言历史问题就是加载慢。延长期出现必须点击才能展开的入口仍不会新增点击;页面识别/挽留弹窗等原因需要真实诊断证据另行分析。#364 的失败现场诊断不属于本单实现。
|
||||
|
||||
## Android 颜色发现诊断 v5(#370 第一阶段)
|
||||
|
||||
实现绑定 `9e5f09a`(工单分支),Debug 标识 `0.9.69-370-diag1`,尚未合并 main。本阶段只补诊断和测试夹具,不改变遍历、停止条件、采集完成状态或采购探测决策;不能据此宣称竖向漏采已修复。
|
||||
|
||||
`COLOR_DISCOVERY` 的结束聚合新增三个可空列:
|
||||
|
||||
- `color_vertical_swipe_count`:本次颜色循环中成功派发的 UP 次数,排除归顶及尺码阶段;不证明视口实际移动。
|
||||
- `color_trailing_empty_read_count`:终止前连续解析不到颜色的外层发现轮数;每轮只记一次,点击与采价重读不计,非空轮清零,初始空为 1。
|
||||
- `color_horizontal_incomplete`:1 表示已观察到同一行签名变化后,必要横向探索因失败或预算耗尽仍未完成;0 表示当前已观察行无需横扫或取得连续稳定证据;NULL 表示无法确认,包括重排、空解析、缺少横向容器或仅成功派发但没有移动证据。同 seed 的未知回访不能清除旧的 1,可靠完成的 0 可以清除。0 不代表整个商品已采全,签名变化也不是物理滑动成功的独立证明。
|
||||
|
||||
只保存计数、布尔/NULL 和固定原因码,不保存标签、价格、坐标、原始控件树、截图或个人信息。每行累计数仍不是逐屏轨迹;缺少日志不证明没执行,错误/超时提前退出可能没有结束聚合。现有 50 条/7 天留存不变。
|
||||
|
||||
本地 SQLite v4→v5 只补三列;旧行新值保持 NULL,不删除旧诊断或采购失败快照/队列/context。旧程序不保证能降级打开 v5,不能靠卸载清数据回退。
|
||||
|
||||
`goauto_diagnostics.db` 从 v4 起还含采购失败私有现场,不能按早期“纯结构化库”的假设导出或上传整库。已授权的 Debug 设备有 sqlite3 时可只查询所需聚合字段;无 sqlite3 时只使用已确认的内存读取方式,避免整库落盘,不放宽应用权限。示例查询(须替换设备,且实际存在 sqlite3):
|
||||
|
||||
```powershell
|
||||
adb -s <serial> shell run-as cn.ilapage.goauto.agent sqlite3 -readonly databases/goauto_diagnostics.db "SELECT task_id,reason,color_vertical_swipe_count,color_trailing_empty_read_count,color_horizontal_incomplete FROM agent_diagnostic WHERE stage='COLOR_DISCOVERY' ORDER BY id DESC LIMIT 1;"
|
||||
```
|
||||
|
||||
排查须绑定同一设备、Agent/PDD 版本、任务与规则快照,并对照同次 App 布局。网页竖排与 App 横排不是同一复现场景;当前横排只能做横向兼容性回归。真机测试结果及待验证项以 #370 工单为准。
|
||||
|
||||
Reference in New Issue
Block a user