fix(#124): consolidate color discovery diagnostics
This commit is contained in:
@@ -11,8 +11,8 @@ android {
|
||||
applicationId = "cn.ilapage.goauto.agent"
|
||||
minSdk = 23
|
||||
targetSdk = 34
|
||||
versionCode = 25
|
||||
versionName = "0.9.12"
|
||||
versionCode = 26
|
||||
versionName = "0.9.13"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -54,4 +54,5 @@ dependencies {
|
||||
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("org.json:json:20240303")
|
||||
testImplementation("org.xerial:sqlite-jdbc:3.46.1.0")
|
||||
}
|
||||
|
||||
+35
-31
@@ -885,7 +885,9 @@ class PddProductDetailCollector(
|
||||
val diagnosticRowValues = linkedMapOf<Int, MutableSet<String>>()
|
||||
val diagnosticParsedValues = linkedSetOf<String>()
|
||||
var diagnosticNonClickableCandidates = 0
|
||||
var diagnosticSelectedValues = 0
|
||||
var diagnosticInitialSelectedColors = 0
|
||||
var diagnosticInitialSelectedSizes = 0
|
||||
var diagnosticSelectedSummaryPresent = false
|
||||
var diagnosticHorizontalSwipes = 0
|
||||
var diagnosticTermination = AgentDiagnosticReason.COLOR_FOUND
|
||||
|
||||
@@ -895,10 +897,6 @@ class PddProductDetailCollector(
|
||||
}
|
||||
val values = rows.flatten()
|
||||
diagnosticParsedValues += values.map(VisibleSpecValue::text)
|
||||
diagnosticSelectedValues = maxOf(
|
||||
diagnosticSelectedValues,
|
||||
values.count { it.node.selected || it.node.checked },
|
||||
)
|
||||
diagnosticNonClickableCandidates = maxOf(
|
||||
diagnosticNonClickableCandidates,
|
||||
visibleNonClickableColorCandidateCount(screen, config, values.map(VisibleSpecValue::text).toSet()),
|
||||
@@ -911,7 +909,9 @@ class PddProductDetailCollector(
|
||||
rowValueCounts = diagnosticRowValues.mapValues { it.value.size },
|
||||
parsedValueCount = diagnosticParsedValues.size,
|
||||
nonClickableCandidateCount = diagnosticNonClickableCandidates,
|
||||
selectedValueCount = diagnosticSelectedValues,
|
||||
initialSelectedColorCount = diagnosticInitialSelectedColors,
|
||||
initialSelectedSizeCount = diagnosticInitialSelectedSizes,
|
||||
selectedSummaryPresent = diagnosticSelectedSummaryPresent,
|
||||
horizontalSwipes = diagnosticHorizontalSwipes,
|
||||
elapsedMs = now() - diagnosticStartedAt,
|
||||
)
|
||||
@@ -921,11 +921,19 @@ class PddProductDetailCollector(
|
||||
moveColorsToStart(goodsId, config, evidence, deadline)?.let { return it }
|
||||
val initial = parse(goodsId, config, evidence)
|
||||
initial.problem?.let { return failure(it.code, it.message) }
|
||||
diagnosticInitialSelectedColors = initial.dimensions.filter { it.key == "color" }.flatMap { it.values }
|
||||
.count { it.node.selected || it.node.checked }
|
||||
diagnosticInitialSelectedSizes = initial.dimensions.filter { it.key == "size" }.flatMap { it.values }
|
||||
.count { it.node.selected || it.node.checked }
|
||||
diagnosticSelectedSummaryPresent = initial.selectedSummary != null
|
||||
val initialRows = colorRows(initial)
|
||||
observeColorDiscovery(initial, initialRows)
|
||||
val rowCount = initialRows.size
|
||||
if (rowCount == 0) {
|
||||
finishColorDiscovery(AgentDiagnosticReason.COLOR_EDGE_REACHED)
|
||||
finishColorDiscovery(
|
||||
if (diagnosticNonClickableCandidates > 0) AgentDiagnosticReason.COLOR_VALUES_NOT_CLICKABLE
|
||||
else AgentDiagnosticReason.COLOR_EDGE_REACHED,
|
||||
)
|
||||
return null
|
||||
}
|
||||
val attempted = mutableSetOf<String>()
|
||||
@@ -1017,34 +1025,30 @@ class PddProductDetailCollector(
|
||||
rowValueCounts: Map<Int, Int>,
|
||||
parsedValueCount: Int,
|
||||
nonClickableCandidateCount: Int,
|
||||
selectedValueCount: Int,
|
||||
initialSelectedColorCount: Int,
|
||||
initialSelectedSizeCount: Int,
|
||||
selectedSummaryPresent: Boolean,
|
||||
horizontalSwipes: Int,
|
||||
elapsedMs: Long,
|
||||
) {
|
||||
if (taskId <= 0) return
|
||||
fun record(metricReason: AgentDiagnosticReason, attempt: Int = 0, count: Int) {
|
||||
diagnostic(
|
||||
AgentDiagnosticEvent(
|
||||
taskId = taskId,
|
||||
stage = AgentDiagnosticStage.COLOR_DISCOVERY,
|
||||
reason = metricReason,
|
||||
attempt = attempt,
|
||||
elapsedMs = elapsedMs,
|
||||
candidateCount = count,
|
||||
),
|
||||
)
|
||||
}
|
||||
rowValueCounts.forEach { (rowIndex, count) ->
|
||||
record(AgentDiagnosticReason.COLOR_ROW_VALUE_COUNT, attempt = rowIndex + 1, count = count)
|
||||
}
|
||||
record(AgentDiagnosticReason.COLOR_FOUND, count = parsedValueCount)
|
||||
record(AgentDiagnosticReason.COLOR_VALUES_NOT_CLICKABLE, count = nonClickableCandidateCount)
|
||||
record(AgentDiagnosticReason.COLOR_SELECTED_VALUE_COUNT, count = selectedValueCount)
|
||||
record(AgentDiagnosticReason.COLOR_HORIZONTAL_SWIPE_COUNT, count = horizontalSwipes)
|
||||
record(AgentDiagnosticReason.COLOR_VERTICAL_SWIPE_COUNT, count = 0)
|
||||
if (reason != AgentDiagnosticReason.COLOR_FOUND) {
|
||||
record(reason, attempt = horizontalSwipes, count = parsedValueCount)
|
||||
}
|
||||
diagnostic(
|
||||
AgentDiagnosticEvent(
|
||||
taskId = taskId,
|
||||
stage = AgentDiagnosticStage.COLOR_DISCOVERY,
|
||||
reason = reason,
|
||||
elapsedMs = elapsedMs,
|
||||
colorRowCount = rowValueCounts.size,
|
||||
colorRowValueCounts = rowValueCounts.takeIf { it.isNotEmpty() }
|
||||
?.toSortedMap()?.values?.joinToString(","),
|
||||
clickableColorCount = parsedValueCount,
|
||||
nonClickableColorCandidateCount = nonClickableCandidateCount,
|
||||
initialSelectedColorCount = initialSelectedColorCount,
|
||||
initialSelectedSizeCount = initialSelectedSizeCount,
|
||||
selectedSummaryPresent = selectedSummaryPresent,
|
||||
horizontalSwipeCount = horizontalSwipes,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun visibleNonClickableColorCandidateCount(
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package cn.ilapage.goauto.agent.persistence
|
||||
|
||||
internal object AgentDiagnosticSchema {
|
||||
const val VERSION = 2
|
||||
|
||||
val colorDiagnosticColumns = linkedMapOf(
|
||||
"color_row_count" to "INTEGER",
|
||||
"color_row_value_counts" to "TEXT",
|
||||
"clickable_color_count" to "INTEGER",
|
||||
"non_clickable_color_candidate_count" to "INTEGER",
|
||||
"initial_selected_color_count" to "INTEGER",
|
||||
"initial_selected_size_count" to "INTEGER",
|
||||
"selected_summary_present" to "INTEGER",
|
||||
"horizontal_swipe_count" to "INTEGER",
|
||||
)
|
||||
|
||||
val createTableSql =
|
||||
"""CREATE TABLE agent_diagnostic (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id INTEGER NOT NULL,
|
||||
stage TEXT NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
attempt INTEGER NOT NULL,
|
||||
elapsed_ms INTEGER NOT NULL,
|
||||
package_matched INTEGER,
|
||||
activity_matched INTEGER,
|
||||
candidate_count INTEGER,
|
||||
target_class_name TEXT,
|
||||
target_clickable INTEGER,
|
||||
clickable_ancestor_depth INTEGER,
|
||||
target_zone TEXT,
|
||||
clipboard_poll_count INTEGER,
|
||||
clipboard_mime_accepted INTEGER,
|
||||
clipboard_item_count INTEGER,
|
||||
clipboard_content_length INTEGER,
|
||||
clipboard_timestamp_fresh INTEGER,
|
||||
color_row_count INTEGER,
|
||||
color_row_value_counts TEXT,
|
||||
clickable_color_count INTEGER,
|
||||
non_clickable_color_candidate_count INTEGER,
|
||||
initial_selected_color_count INTEGER,
|
||||
initial_selected_size_count INTEGER,
|
||||
selected_summary_present INTEGER,
|
||||
horizontal_swipe_count INTEGER,
|
||||
agent_version TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
)""".trimIndent()
|
||||
|
||||
fun v2MigrationStatements(oldVersion: Int, newVersion: Int, existingColumns: Set<String>): List<String> {
|
||||
if (oldVersion >= 2 || newVersion < 2) return emptyList()
|
||||
return colorDiagnosticColumns.mapNotNull { (name, definition) ->
|
||||
if (name in existingColumns) null else "ALTER TABLE agent_diagnostic ADD COLUMN $name $definition"
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
-30
@@ -43,10 +43,6 @@ enum class AgentDiagnosticReason {
|
||||
COLOR_SWIPE_FAILED,
|
||||
COLOR_CONTAINER_UNAVAILABLE,
|
||||
COLOR_VALUES_NOT_CLICKABLE,
|
||||
COLOR_ROW_VALUE_COUNT,
|
||||
COLOR_SELECTED_VALUE_COUNT,
|
||||
COLOR_HORIZONTAL_SWIPE_COUNT,
|
||||
COLOR_VERTICAL_SWIPE_COUNT,
|
||||
PACKAGE_MISMATCH,
|
||||
ACTIVITY_MISMATCH,
|
||||
SELECTOR_MISMATCH,
|
||||
@@ -88,6 +84,14 @@ data class AgentDiagnosticEvent(
|
||||
val clipboardItemCount: Int? = null,
|
||||
val clipboardContentLength: Int? = null,
|
||||
val clipboardTimestampFresh: Boolean? = null,
|
||||
val colorRowCount: Int? = null,
|
||||
val colorRowValueCounts: String? = null,
|
||||
val clickableColorCount: Int? = null,
|
||||
val nonClickableColorCandidateCount: Int? = null,
|
||||
val initialSelectedColorCount: Int? = null,
|
||||
val initialSelectedSizeCount: Int? = null,
|
||||
val selectedSummaryPresent: Boolean? = null,
|
||||
val horizontalSwipeCount: Int? = null,
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
)
|
||||
|
||||
@@ -109,34 +113,22 @@ internal object AgentDiagnosticRetentionPolicy {
|
||||
|
||||
class AgentDiagnosticStore(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
db.execSQL(
|
||||
"""CREATE TABLE agent_diagnostic (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id INTEGER NOT NULL,
|
||||
stage TEXT NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
attempt INTEGER NOT NULL,
|
||||
elapsed_ms INTEGER NOT NULL,
|
||||
package_matched INTEGER,
|
||||
activity_matched INTEGER,
|
||||
candidate_count INTEGER,
|
||||
target_class_name TEXT,
|
||||
target_clickable INTEGER,
|
||||
clickable_ancestor_depth INTEGER,
|
||||
target_zone TEXT,
|
||||
clipboard_poll_count INTEGER,
|
||||
clipboard_mime_accepted INTEGER,
|
||||
clipboard_item_count INTEGER,
|
||||
clipboard_content_length INTEGER,
|
||||
clipboard_timestamp_fresh INTEGER,
|
||||
agent_version TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
)""".trimIndent(),
|
||||
)
|
||||
db.execSQL(AgentDiagnosticSchema.createTableSql)
|
||||
db.execSQL("CREATE INDEX idx_agent_diagnostic_task ON agent_diagnostic(task_id, id)")
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = Unit
|
||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
val existing = columnNames(db)
|
||||
AgentDiagnosticSchema.v2MigrationStatements(oldVersion, newVersion, existing).forEach(db::execSQL)
|
||||
}
|
||||
|
||||
private fun columnNames(db: SQLiteDatabase): Set<String> =
|
||||
db.rawQuery("PRAGMA table_info(agent_diagnostic)", null).use { cursor ->
|
||||
val nameIndex = cursor.getColumnIndexOrThrow("name")
|
||||
buildSet {
|
||||
while (cursor.moveToNext()) add(cursor.getString(nameIndex))
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun record(event: AgentDiagnosticEvent) {
|
||||
@@ -162,6 +154,16 @@ class AgentDiagnosticStore(context: Context) : SQLiteOpenHelper(context, DATABAS
|
||||
putNullableInt("clipboard_item_count", event.clipboardItemCount)
|
||||
putNullableInt("clipboard_content_length", event.clipboardContentLength)
|
||||
putNullableBoolean("clipboard_timestamp_fresh", event.clipboardTimestampFresh)
|
||||
putNullableInt("color_row_count", event.colorRowCount)
|
||||
event.colorRowValueCounts?.takeIf(ROW_VALUE_COUNTS_PATTERN::matches)?.let {
|
||||
put("color_row_value_counts", it.take(MAX_ROW_VALUE_COUNTS_CHARS))
|
||||
}
|
||||
putNullableInt("clickable_color_count", event.clickableColorCount)
|
||||
putNullableInt("non_clickable_color_candidate_count", event.nonClickableColorCandidateCount)
|
||||
putNullableInt("initial_selected_color_count", event.initialSelectedColorCount)
|
||||
putNullableInt("initial_selected_size_count", event.initialSelectedSizeCount)
|
||||
putNullableBoolean("selected_summary_present", event.selectedSummaryPresent)
|
||||
putNullableInt("horizontal_swipe_count", event.horizontalSwipeCount)
|
||||
put("agent_version", BuildConfig.VERSION_NAME)
|
||||
put("created_at", event.createdAt)
|
||||
}
|
||||
@@ -187,8 +189,10 @@ class AgentDiagnosticStore(context: Context) : SQLiteOpenHelper(context, DATABAS
|
||||
|
||||
companion object {
|
||||
private const val DATABASE_NAME = "goauto_diagnostics.db"
|
||||
private const val DATABASE_VERSION = 1
|
||||
private const val DATABASE_VERSION = AgentDiagnosticSchema.VERSION
|
||||
private const val MAX_CLASS_NAME_CHARS = 160
|
||||
private const val MAX_ROW_VALUE_COUNTS_CHARS = 160
|
||||
private val ROW_VALUE_COUNTS_PATTERN = Regex("[0-9]+(?:,[0-9]+)*")
|
||||
private val ALLOWED_ZONES = setOf("top-left", "top-center", "top-right", "middle", "bottom")
|
||||
}
|
||||
}
|
||||
|
||||
+79
-12
@@ -227,6 +227,29 @@ class PddProductDetailCollectorTest {
|
||||
assertEquals("card/caption", color.node.path)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectedStateFromDescendantIsAlreadyNormalizedByParser() {
|
||||
val snapshot = UiSnapshot(
|
||||
PDD_PACKAGE,
|
||||
ACTIVITY,
|
||||
listOf(
|
||||
node("content", "", 0, 0, 1080, 2200, resourceId = "android:id/content", className = "android.widget.FrameLayout"),
|
||||
node("summary", "已选:A色", 20, 300, 700, 360),
|
||||
node("title", "确认款式", 20, 370, 300, 410),
|
||||
node("color-heading", "颜色分类", 20, 420, 300, 470),
|
||||
node("color", "A色", 36, 500, 340, 650, clickable = true),
|
||||
node("color/selected", "选中标记", 36, 500, 80, 540, selected = true, parentPath = "color"),
|
||||
node("size-heading", "尺码", 20, 700, 300, 750),
|
||||
node("size", "M", 36, 780, 220, 850, clickable = true),
|
||||
node("confirm", "确定", 0, 2000, 1080, 2150, clickable = true),
|
||||
),
|
||||
)
|
||||
|
||||
val parsed = PddScreenParser.parse(snapshot, config(), GOODS_ID, evidence())
|
||||
|
||||
assertTrue(parsed.dimensions.first { it.key == "color" }.values.single().node.selected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun quickConfirmationExposesOnlyCurrentSpecAsRecoveryEntry() {
|
||||
val snapshot = UiSnapshot(
|
||||
@@ -545,6 +568,7 @@ class PddProductDetailCollectorTest {
|
||||
prices = colors.associateWith { 1000L },
|
||||
rowSize = 2,
|
||||
nonClickableColorCandidates = listOf("隐藏候选"),
|
||||
selectedSummaryPrefix = "请选择",
|
||||
)
|
||||
var clock = 0L
|
||||
|
||||
@@ -557,20 +581,50 @@ class PddProductDetailCollectorTest {
|
||||
).collect(GOODS_ID, rule())
|
||||
|
||||
assertTrue(result.successful)
|
||||
val colorEvents = events.filter { it.stage == AgentDiagnosticStage.COLOR_DISCOVERY }
|
||||
assertEquals(listOf(2, 2), colorEvents.filter { it.reason == AgentDiagnosticReason.COLOR_ROW_VALUE_COUNT }.map { it.candidateCount })
|
||||
assertEquals(4, colorEvents.single { it.reason == AgentDiagnosticReason.COLOR_FOUND }.candidateCount)
|
||||
assertEquals(1, colorEvents.single { it.reason == AgentDiagnosticReason.COLOR_VALUES_NOT_CLICKABLE }.candidateCount)
|
||||
assertEquals(1, colorEvents.single { it.reason == AgentDiagnosticReason.COLOR_SELECTED_VALUE_COUNT }.candidateCount)
|
||||
assertTrue(requireNotNull(colorEvents.single { it.reason == AgentDiagnosticReason.COLOR_HORIZONTAL_SWIPE_COUNT }.candidateCount) > 0)
|
||||
assertEquals(0, colorEvents.single { it.reason == AgentDiagnosticReason.COLOR_VERTICAL_SWIPE_COUNT }.candidateCount)
|
||||
assertTrue(colorEvents.any { it.reason == AgentDiagnosticReason.COLOR_EDGE_REACHED })
|
||||
val persistedText = colorEvents.joinToString()
|
||||
val event = events.single { it.stage == AgentDiagnosticStage.COLOR_DISCOVERY }
|
||||
assertEquals(AgentDiagnosticReason.COLOR_EDGE_REACHED, event.reason)
|
||||
assertEquals(2, event.colorRowCount)
|
||||
assertEquals("2,2", event.colorRowValueCounts)
|
||||
assertEquals(4, event.clickableColorCount)
|
||||
assertEquals(1, event.nonClickableColorCandidateCount)
|
||||
assertEquals(0, event.initialSelectedColorCount)
|
||||
assertEquals(0, event.initialSelectedSizeCount)
|
||||
assertEquals(false, event.selectedSummaryPresent)
|
||||
assertTrue(requireNotNull(event.horizontalSwipeCount) > 0)
|
||||
val persistedText = event.toString()
|
||||
colors.forEach { assertFalse(persistedText.contains(it)) }
|
||||
assertFalse(persistedText.contains("隐藏候选"))
|
||||
assertFalse(persistedText.contains("10.00"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `color discovery records initial color and size selection before agent clicks`() {
|
||||
val events = mutableListOf<AgentDiagnosticEvent>()
|
||||
val driver = FakeCollectorDriver(
|
||||
colors = listOf("红色", "蓝色"),
|
||||
sizes = listOf("S", "M"),
|
||||
initialSelectedColor = "红色",
|
||||
initialSelectedSize = "M",
|
||||
)
|
||||
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(1, event.initialSelectedColorCount)
|
||||
assertEquals(1, event.initialSelectedSizeCount)
|
||||
assertEquals(true, event.selectedSummaryPresent)
|
||||
assertTrue(driver.clickedLabels.size > 1)
|
||||
assertEquals(1, event.initialSelectedColorCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `color diagnostic write failure does not change collection result`() {
|
||||
var failureCount = 0
|
||||
@@ -1151,6 +1205,9 @@ class PddProductDetailCollectorTest {
|
||||
private val orderRestorePagesBeforeSpecs: Int = 0,
|
||||
private val sizeHeadingLabel: String = "尺码",
|
||||
private val nonClickableColorCandidates: List<String> = emptyList(),
|
||||
private val initialSelectedColor: String? = null,
|
||||
private val initialSelectedSize: String? = null,
|
||||
private val selectedSummaryPrefix: String = "已选",
|
||||
) : PddCollectorDriver {
|
||||
var captureCount = 0
|
||||
var clickCount = 0
|
||||
@@ -1160,7 +1217,7 @@ class PddProductDetailCollectorTest {
|
||||
var backCount = 0
|
||||
var entryClickCount = 0
|
||||
var restoreGestures = 0
|
||||
private var selected: String? = null
|
||||
private var selected: String? = initialSelectedColor
|
||||
private var previousSelected: String? = null
|
||||
private var horizontalPage = 0
|
||||
private var verticalPage = 0
|
||||
@@ -1216,7 +1273,7 @@ class PddProductDetailCollectorTest {
|
||||
node("shop", "测试旗舰店", 20, 190, 300, 240),
|
||||
node("sales", "已拼1.2万件", 20, 250, 300, 300),
|
||||
node("reviews", "商品评价(1.2万)", 320, 250, 650, 300),
|
||||
node("selected", "已选 ${displayedSelected().orEmpty()}", 20, 320, 700, 370),
|
||||
node("selected", "$selectedSummaryPrefix ${displayedSelected().orEmpty()}", 20, 320, 700, 370),
|
||||
node("panel-title", "确认款式", 20, 370, 300, 410),
|
||||
node("scroll", "", 0, 380, 1080, 1900, scrollable = scrollablePanel),
|
||||
)
|
||||
@@ -1254,7 +1311,17 @@ class PddProductDetailCollectorTest {
|
||||
}
|
||||
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")
|
||||
nodes += node(
|
||||
"scroll/size-$size-$captureCount",
|
||||
size,
|
||||
30 + index * 230,
|
||||
770,
|
||||
220 + index * 230,
|
||||
840,
|
||||
clickable = true,
|
||||
selected = size == initialSelectedSize,
|
||||
parentPath = "scroll",
|
||||
)
|
||||
}
|
||||
if (extraDimension) {
|
||||
nodes += node("scroll/capacity-heading", "容量", 20, 900, 300, 950, parentPath = "scroll")
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package cn.ilapage.goauto.agent.persistence
|
||||
|
||||
import java.sql.Connection
|
||||
import java.sql.DriverManager
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class AgentDiagnosticStoreMigrationTest {
|
||||
@Test
|
||||
fun freshDatabaseCreatesV2ColumnsAndAcceptsSingleColorDiagnosticRecord() = withDatabase { db ->
|
||||
db.createStatement().use { it.execute(AgentDiagnosticSchema.createTableSql) }
|
||||
|
||||
assertTrue(columnNames(db).containsAll(AgentDiagnosticSchema.colorDiagnosticColumns.keys))
|
||||
db.prepareStatement(
|
||||
"""INSERT INTO agent_diagnostic (
|
||||
task_id, stage, reason, attempt, elapsed_ms,
|
||||
color_row_count, color_row_value_counts, clickable_color_count,
|
||||
non_clickable_color_candidate_count, initial_selected_color_count,
|
||||
initial_selected_size_count, selected_summary_present, horizontal_swipe_count,
|
||||
agent_version, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""".trimIndent(),
|
||||
).use { statement ->
|
||||
listOf(
|
||||
124L,
|
||||
AgentDiagnosticStage.COLOR_DISCOVERY.name,
|
||||
AgentDiagnosticReason.COLOR_EDGE_REACHED.name,
|
||||
0,
|
||||
350L,
|
||||
2,
|
||||
"2,2",
|
||||
4,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
"0.9.13",
|
||||
1_000L,
|
||||
).forEachIndexed { index, value -> statement.setObject(index + 1, value) }
|
||||
assertEquals(1, statement.executeUpdate())
|
||||
}
|
||||
|
||||
db.createStatement().use { statement ->
|
||||
statement.executeQuery(
|
||||
"SELECT color_row_count, color_row_value_counts, clickable_color_count, " +
|
||||
"non_clickable_color_candidate_count, initial_selected_color_count, initial_selected_size_count, " +
|
||||
"selected_summary_present, horizontal_swipe_count FROM agent_diagnostic",
|
||||
).use { row ->
|
||||
assertTrue(row.next())
|
||||
assertEquals(2, row.getInt(1))
|
||||
assertEquals("2,2", row.getString(2))
|
||||
assertEquals(4, row.getInt(3))
|
||||
assertEquals(1, row.getInt(4))
|
||||
assertEquals(0, row.getInt(5))
|
||||
assertEquals(1, row.getInt(6))
|
||||
assertEquals(1, row.getInt(7))
|
||||
assertEquals(2, row.getInt(8))
|
||||
assertTrue(!row.next())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun v1MigrationPreservesRowsAndIsIdempotent() = withDatabase { db ->
|
||||
db.createStatement().use { statement ->
|
||||
statement.execute(CREATE_V1_TABLE_SQL)
|
||||
statement.execute(
|
||||
"INSERT INTO agent_diagnostic " +
|
||||
"(task_id, stage, reason, attempt, elapsed_ms, agent_version, created_at) " +
|
||||
"VALUES (106, 'DETAIL_ENTRY', 'DETAIL_ENTRY_MATCHED', 1, 10, '0.9.12', 1000)",
|
||||
)
|
||||
}
|
||||
|
||||
migrateV1ToV2(db)
|
||||
|
||||
assertTrue(columnNames(db).containsAll(AgentDiagnosticSchema.colorDiagnosticColumns.keys))
|
||||
assertEquals(1, rowCount(db))
|
||||
assertTrue(AgentDiagnosticSchema.v2MigrationStatements(1, 2, columnNames(db)).isEmpty())
|
||||
|
||||
migrateV1ToV2(db)
|
||||
|
||||
assertEquals(1, rowCount(db))
|
||||
}
|
||||
|
||||
private fun migrateV1ToV2(db: Connection) {
|
||||
AgentDiagnosticSchema.v2MigrationStatements(1, 2, columnNames(db)).forEach { sql ->
|
||||
db.createStatement().use { it.execute(sql) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun columnNames(db: Connection): Set<String> =
|
||||
db.createStatement().use { statement ->
|
||||
statement.executeQuery("PRAGMA table_info(agent_diagnostic)").use { rows ->
|
||||
buildSet {
|
||||
while (rows.next()) add(rows.getString("name"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun rowCount(db: Connection): Int =
|
||||
db.createStatement().use { statement ->
|
||||
statement.executeQuery("SELECT COUNT(*) FROM agent_diagnostic").use { rows ->
|
||||
check(rows.next())
|
||||
rows.getInt(1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun withDatabase(block: (Connection) -> Unit) {
|
||||
Class.forName("org.sqlite.JDBC")
|
||||
DriverManager.getConnection("jdbc:sqlite::memory:").use(block)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val CREATE_V1_TABLE_SQL =
|
||||
"""CREATE TABLE agent_diagnostic (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id INTEGER NOT NULL,
|
||||
stage TEXT NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
attempt INTEGER NOT NULL,
|
||||
elapsed_ms INTEGER NOT NULL,
|
||||
package_matched INTEGER,
|
||||
activity_matched INTEGER,
|
||||
candidate_count INTEGER,
|
||||
target_class_name TEXT,
|
||||
target_clickable INTEGER,
|
||||
clickable_ancestor_depth INTEGER,
|
||||
target_zone TEXT,
|
||||
clipboard_poll_count INTEGER,
|
||||
clipboard_mime_accepted INTEGER,
|
||||
clipboard_item_count INTEGER,
|
||||
clipboard_content_length INTEGER,
|
||||
clipboard_timestamp_fresh INTEGER,
|
||||
agent_version TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
)""".trimIndent()
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Troubleshooting
|
||||
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Troubleshooting
|
||||
wiki_revision: 6c4352598e1152ed562e998aa641f38415ac71c7
|
||||
synchronized_at: 2026-08-19T01:17:13Z
|
||||
wiki_revision: a8d4790811aa825c78830cb9c71c6b21723e4584
|
||||
synchronized_at: 2026-08-28T03:35:05Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 故障排查
|
||||
@@ -69,3 +69,19 @@ ColorOS 开启“安装增强防护”时,电脑端未知来源 APK 可能不
|
||||
## 规则疑似失效
|
||||
|
||||
复制现有规则创建或编辑可用规则,用测试任务验证。删除旧规则只阻止创建新任务,已经创建的任务仍按自身快照执行。
|
||||
|
||||
## Android 颜色采集诊断
|
||||
|
||||
Agent 0.9.13 起,颜色发现阶段在本地 `goauto_diagnostics.db` 的 `agent_diagnostic` 表保存一条脱敏 `COLOR_DISCOVERY` 记录。数据库最多保留最近 50 条、最长 7 天;不得将数据库文件上传工单。
|
||||
|
||||
记录只包含颜色行数、各行值数量、可点击颜色数、不可点击候选数、首次颜色点击前的颜色/尺码选中数、是否存在“已选”摘要、横向滑动次数、终止原因和耗时,不保存规格文案、价格、摘要原文、坐标、链接、goods_id、控件树、XML 或截图。
|
||||
|
||||
只对 Debug APK 使用以下读取流程:
|
||||
|
||||
```powershell
|
||||
adb shell run-as cn.ilapage.goauto.agent ls databases
|
||||
adb exec-out run-as cn.ilapage.goauto.agent cat databases/goauto_diagnostics.db > agent-diagnostics.db
|
||||
sqlite3 -readonly agent-diagnostics.db "SELECT task_id,reason,color_row_count,color_row_value_counts,clickable_color_count,non_clickable_color_candidate_count,initial_selected_color_count,initial_selected_size_count,selected_summary_present,horizontal_swipe_count,elapsed_ms,agent_version,created_at FROM agent_diagnostic WHERE stage='COLOR_DISCOVERY' ORDER BY id DESC LIMIT 1;"
|
||||
```
|
||||
|
||||
读取时记录设备、Agent 版本、任务号和规则快照;工单只回写查询得到的脱敏聚合数值。读取完成后删除本地导出副本。正式 APK 若不允许 `run-as`,停止排查并确认安全的只读诊断出口,不通过放宽应用安全配置或上传完整数据库绕过。
|
||||
|
||||
Reference in New Issue
Block a user