From d718c957bc223bc2e55c4ee6bc42855e11237fb3 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Fri, 28 Aug 2026 11:36:42 +0800 Subject: [PATCH] fix(#124): consolidate color discovery diagnostics --- android/app/build.gradle.kts | 5 +- .../automation/PddProductDetailCollector.kt | 66 +++++---- .../persistence/AgentDiagnosticSchema.kt | 55 +++++++ .../agent/persistence/AgentDiagnosticStore.kt | 64 ++++---- .../agent/PddProductDetailCollectorTest.kt | 91 ++++++++++-- .../AgentDiagnosticStoreMigrationTest.kt | 139 ++++++++++++++++++ docs/06-troubleshooting.md | 20 ++- 7 files changed, 363 insertions(+), 77 deletions(-) create mode 100644 android/app/src/main/java/cn/ilapage/goauto/agent/persistence/AgentDiagnosticSchema.kt create mode 100644 android/app/src/test/java/cn/ilapage/goauto/agent/persistence/AgentDiagnosticStoreMigrationTest.kt diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 6d09516..b2f9b63 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -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") } diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt index 63a510b..faa5467 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/PddProductDetailCollector.kt @@ -885,7 +885,9 @@ class PddProductDetailCollector( val diagnosticRowValues = linkedMapOf>() val diagnosticParsedValues = linkedSetOf() 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() @@ -1017,34 +1025,30 @@ class PddProductDetailCollector( rowValueCounts: Map, 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( diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/persistence/AgentDiagnosticSchema.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/persistence/AgentDiagnosticSchema.kt new file mode 100644 index 0000000..fab4c66 --- /dev/null +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/persistence/AgentDiagnosticSchema.kt @@ -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): List { + 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" + } + } +} diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/persistence/AgentDiagnosticStore.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/persistence/AgentDiagnosticStore.kt index e087bef..7d39dbe 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/persistence/AgentDiagnosticStore.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/persistence/AgentDiagnosticStore.kt @@ -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 = + 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") } } diff --git a/android/app/src/test/java/cn/ilapage/goauto/agent/PddProductDetailCollectorTest.kt b/android/app/src/test/java/cn/ilapage/goauto/agent/PddProductDetailCollectorTest.kt index 062df62..5dda7f1 100644 --- a/android/app/src/test/java/cn/ilapage/goauto/agent/PddProductDetailCollectorTest.kt +++ b/android/app/src/test/java/cn/ilapage/goauto/agent/PddProductDetailCollectorTest.kt @@ -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() + 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 = 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") diff --git a/android/app/src/test/java/cn/ilapage/goauto/agent/persistence/AgentDiagnosticStoreMigrationTest.kt b/android/app/src/test/java/cn/ilapage/goauto/agent/persistence/AgentDiagnosticStoreMigrationTest.kt new file mode 100644 index 0000000..710c9fd --- /dev/null +++ b/android/app/src/test/java/cn/ilapage/goauto/agent/persistence/AgentDiagnosticStoreMigrationTest.kt @@ -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 = + 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() + } +} diff --git a/docs/06-troubleshooting.md b/docs/06-troubleshooting.md index f9c7dcb..f9986a5 100644 --- a/docs/06-troubleshooting.md +++ b/docs/06-troubleshooting.md @@ -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 # 故障排查 @@ -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`,停止排查并确认安全的只读诊断出口,不通过放宽应用安全配置或上传完整数据库绕过。