fix(#124): consolidate color discovery diagnostics

This commit is contained in:
QiuSW
2026-08-28 11:36:42 +08:00
parent 9bb19b4d0f
commit d718c957bc
7 changed files with 363 additions and 77 deletions
+3 -2
View File
@@ -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")
}
@@ -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(
@@ -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"
}
}
}
@@ -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")
}
}
@@ -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")
@@ -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()
}
}
+18 -2
View File
@@ -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`,停止排查并确认安全的只读诊断出口,不通过放宽应用安全配置或上传完整数据库绕过。