e117fd2
AGENTS.md
任务详情(用户读取):颜色仅 白色 一项,尺码 均码 一项,缺失项仅 reviewCount。
白色
均码
reviewCount
由此可确定:
不是文案识别问题:缺失项没有 unsupported,说明颜色标题已被正确映射为 color 维度,不属于 #115 范畴。
unsupported
color
不是点击或采价失败:缺失项没有 selection: 与 price:,被采到的白色点击成功且价格正常。
selection:
price:
不是「发现多个但遍历中途退出」:PddProductDetailCollector.kt:916 在点击之前就把当前行全部可见值写入结果——
PddProductDetailCollector.kt:916
currentRow.forEach { rowColors[it.text] = rowColors[it.text] == true || it.available } val value = currentRow.firstOrNull { it.available && it.text !in attempted } ?: break
若初始视口存在多个可见颜色值,即使随后因 PDD 重排触发 :905-913 的提前 break,结果也应包含那些值。结果只有一项,说明解析阶段产出的 color 值本身就只有 1 个。
:905-913
reviewCount 缺失与颜色无关,属既有独立小缺口,本工单不处理。
collectColors
:873-946
val direction = if (moveRight) SwipeDirection.LEFT else SwipeDirection.RIGHT
SwipeDirection.UP
val rowCount = colorRows(initial).size
:888
collectSizes
:1016-1060
repeat(specVerticalSwipes + 1)
swipeSpec(SwipeDirection.UP, anchor)
swipeSpec
GoAutoAccessibilityService.kt:365-368
width > height
swipe(SemanticTarget.SPEC_PANEL, ...)
break
AgentDiagnosticStage
persistence/AgentDiagnosticStore.kt:9-18
DETAIL_ENTRY
SPEC_PANEL_ENTRY
SIZE_DISCOVERY
事实 3 把根因收敛到「解析阶段只产出 1 个 color 值」,但成因有两种,修复重点不同:
parse
实施顺序要求:先落地第 1 项诊断并取得一次真机记录,确认属于哪个分支后再实施对应修复;两个分支的修复不得在无证据的情况下同时盲改。
agent_diagnostic
COLOR_DISCOVERY
AgentDiagnosticReason
COLOR_FOUND
COLOR_EDGE_REACHED
COLOR_SCAN_LIMIT
COLOR_ROW_REFLOWED
COLOR_SWIPE_FAILED
COLOR_CONTAINER_UNAVAILABLE
COLOR_VALUES_NOT_CLICKABLE
SafeAgentDiagnosticRecorder
colorRows()
rowCount
available
completed_partial
rowIndex >= rows.size
attempted
limits
colorVerticalSwipes
specVerticalSwipes
collector
colorLayout: auto | horizontal | vertical | grid
auto
rulecontract
说明:布局不适合以规则为主力手段。当前采集规则是全局的(AgentManualCollectionSetting 指向单条规则,所有当前页面采集任务共用),而布局逐商品不同;把 colorLayout 写死会顾此失彼。因此以自动判定为主、规则覆盖为辅。
AgentManualCollectionSetting
colorLayout
诊断(可独立验收)
分支 A(若确认)
分支 B(若确认)
共同
cd android && .\gradlew.bat :app:testDebugUnitTest :app:assembleDebug
go test ./app/goauto/rulecontract/...
3619435
PddProductDetailCollector
GoAutoAccessibilityService
stableEdgeReads
Android-Agent-API-Contract
docs/08-agent-api-contract.md
limits.colorVerticalSwipes
collector.colorLayout
Business-Rules-and-Glossary
docs/03-business-rules-and-glossary.md
sync
sync --check
第 1 项诊断修订已由提交 d718c95 完成,等待 Debug APK 真机读取验收;第 2~5 项未实施。取得原问题商品的脱敏聚合证据并由用户确认分支前,不继续修改遍历、解析或手势逻辑。
d718c95
用户提示 /mnt/d/chengma/cmautobuy/client 已有处理同类问题的 Python 实现。已只读分析 client/src/pdd_collect_service.py(2819 行,当前工作树,未做提交基线核对,仅作行为参考)。结论如下。
/mnt/d/chengma/cmautobuy/client
client/src/pdd_collect_service.py
_collect_color_prices(:1631)与 GoAuto 的 collectColors 结构基本一致:
_collect_color_prices
:1631
first_rows = self._visible_color_rows(first_xml) row_count = len(first_rows) # 同样在第一帧冻结 for row_index in range(row_count): move_right = row_index % 2 == 0 # 同样只有横向蛇形遍历
纵向滚动只出现在 _clear_default_size_selection(:1866)与 _move_spec_panel_to_top(:1928),颜色遍历中没有任何纵向动作。
_clear_default_size_selection
:1866
_move_spec_panel_to_top
:1928
因此:若确认为分支 A(纵向布局),参考实现不提供可借鉴方案,该部分必须原创;实施时不得以「参考项目如此实现」为由跳过纵向发现能力。
行消失时明确失败,不静默(:1671):
:1671
if row_index >= len(rows): raise PddCollectError("PDD_DATA_SPEC_INCOMPLETE", f"颜色第 {row_index + 1} 行在滑动后消失")
GoAuto 在对应位置(PddProductDetailCollector.kt:905-913)是静默 break,导致本次「任务成功但只有一个颜色」的无声失败。本工单第 4 项(重排提前退出)应至少保证该情况可被诊断区分;是否升级为失败按实施时的证据决定,不得继续无声吞掉。
PddProductDetailCollector.kt:905-913
滑动区域选法更可靠(_horizontal_region :2773):选取最深的可滚动节点,且要求其子节点含 clickable,不依赖宽高比。GoAuto 的 GoAutoAccessibilityService.kt:365-368 按 width > height 筛选,在纵向或嵌套容器下直接失效。本工单第 7 项改为采用这一选法,优先级高于原文的「判定为纵向布局并跳过横向翻页」。
_horizontal_region
:2773
截断项过滤(_visible_color_rows :2650):
_visible_color_rows
:2650
widest = max(item.bounds[2] - item.bounds[0] for item in candidates) minimum_width = max(48, int(widest * 0.6))
过滤被屏幕边缘截断的半个按钮;自绘非滚动面板(按钮宽度随文字长度变化)时则保留全部候选,不做宽度过滤。GoAuto 的 colorRows(:1306-1323)没有这层,可能把截断项当成完整值。
colorRows
:1306-1323
滑动后等待视口真实变化(_wait_for_horizontal_change):GoAuto 目前是固定 pause(350),慢设备上可能读到旧视口。
_wait_for_horizontal_change
pause(350)
参考实现有一个 GoAuto 完全没有的前置步骤 _clear_default_size_selection(:1866):进入规格面板后,先定位默认已选中的尺码并取消选择(且只在唯一选中项明确时点击一次,页面不支持取消时不重复点击),随后 _move_spec_panel_to_top 回顶,再开始逐色采价。
该步骤的存在说明参考项目遇到过「默认选中态干扰规格遍历」的真实问题。本次任务结果为「白色 + 均码」两个维度各仅一个值,与该情形吻合:若进入面板时带默认选中态,部分颜色可能因当前选中尺码无货而不出现在可点击节点中,从而在解析阶段就只产出一个 color 值。
补充佐证:参考实现的 _top_level_clickable_options(:796)同样要求 clickable == "true",即原工单的分支 B(未选中项不可点击)在参考实现中同样会失败。这反过来提高了分支 C 的分量。
_top_level_clickable_options
:796
clickable == "true"
因此待证实分支由两个扩展为三个:
原方案第 1 项的诊断需增加一个字段:进入规格面板时已处于选中状态的维度值数量(按维度分别计数)。加上原有的「不可点击但可见的候选数量」,一次真机记录即可三选一:
诊断仍不得记录颜色文案、价格、控件树、坐标原文与截图。
目标、非目标、安全边界、验证方式、依赖与文档影响不变。实施顺序仍为:先落地诊断 → 取得一次真机记录 → 确认分支 → 实施对应修复。
开始实施第 1 项「颜色阶段诊断」。本次范围按用户明确指令收窄:仅新增脱敏、旁路保护的颜色诊断及测试;不修改 collectColors 的遍历逻辑,不修改 PddScreenParser.parse 判定条件,不修改 swipeSpec。完成后提交并停在待验收,不实施工单第 2~5 项。
PddScreenParser.parse
已按用户明确收窄的范围完成颜色阶段诊断,未实施第 2~5 项。
0.9.12
GoAutoAccessibilityService.swipeSpec
python dev_scripts/harness.py check --strict
.\scripts\verify.ps1 -Component android
android/app/build/outputs/apk/debug/app-debug.apk
9bb19b4d0f80e753c03689b5853128ad968b07b6
feat(#124): add color discovery diagnostics
origin/main
本阶段只增加 Agent 内部脱敏诊断枚举和落库事件,未改变业务规则、共享接口、配置、状态或操作方式,判定为无长期 Wiki 文档影响,按规则跳过 Wiki 更新与同步。
尚未执行 PKG110 真机采集;需要安装 0.9.12 后运行一次原问题商品,读取 COLOR_DISCOVERY 聚合记录,再决定 #124 后续应进入纵向布局、解析判定或其他分支。本工单保持打开,状态为待验收。
9bb19b4
范围遵守良好:未改动 collectColors 的遍历算法、parse 判定条件与 swipeSpec,符合「只做诊断」的约束。终止原因分支覆盖完整(reflow / edge / limit / swipe failed / container unavailable),并补充了单元测试。
但建议改完下列第 1、2、4 点再装机验收,否则本次真机很可能给出误导性读数。
当前实现:
diagnosticSelectedValues = maxOf( diagnosticSelectedValues, values.count { it.node.selected || it.node.checked }, )
只检查规格值节点自身的 selected / checked。PDD 大量自绘控件把选中态放在后代节点上,父节点两个标志均为 false。
selected
checked
参考实现对照(/mnt/d/chengma/cmautobuy/client/src/pdd_collect_service.py):
/mnt/d/chengma/cmautobuy/client/src/pdd_collect_service.py
_node_is_selected
:2688
return any(item.get("selected") == "true" or item.get("checked") == "true" for item in node.iter())
_color_selection_state_exposed
:2694
snapshot.selected_text
GoAuto 的 ParsedPddScreen 已有现成的 selectedSummary 字段,本次未使用。
ParsedPddScreen
selectedSummary
要求:选中判定改为遍历节点自身及后代;同时把 selectedSummary 是否非空(以及其中可识别的维度值数量)一并记入诊断。按当前写法,若 PDD 不暴露 selected,该字段将恒为 0,从而错误地排除分支 C —— 而分支 C 正是与本次「白色 + 均码」现象最吻合的假设。
recordColorDiscovery 把每个度量写成一条独立记录:每行一条 COLOR_ROW_VALUE_COUNT,加 5 条固定度量,再加终止原因。
recordColorDiscovery
COLOR_ROW_VALUE_COUNT
而 AgentDiagnosticStore.kt:104 的 MAX_RECORDS = 50 是全局上限,按 id 倒序保留(:172)。一次当前页面采集本就要写 LINK_RESOLUTION、DETAIL_ENTRY、SPEC_PANEL_ENTRY、SIZE_DISCOVERY,颜色阶段再吃掉近 10 条,跑三四次即会把此前各阶段记录全部冲掉,后续跨阶段排查将失去证据。
AgentDiagnosticStore.kt:104
MAX_RECORDS = 50
:172
LINK_RESOLUTION
要求:一次颜色采集只写 1 条记录;各项度量改为 AgentDiagnosticEvent 的独立数值字段(参照既有 clipboardPollCount / clipboardItemCount / clipboardContentLength 的做法新增列),不得塞进 reason 枚举。每行值数量可用一个「行数 + 各行值数的紧凑摘要」单字段承载,仍不含任何文案。
AgentDiagnosticEvent
clipboardPollCount
clipboardItemCount
clipboardContentLength
reason
record(AgentDiagnosticReason.COLOR_FOUND, count = parsedValueCount) // 作度量 if (reason != AgentDiagnosticReason.COLOR_FOUND) record(reason, ...) // 又作终止原因
读取时无法区分某条记录表示「终止原因为正常完成」还是「解析值总数」。按第 2 点合并为单条记录后该问题自然消除。
同时,COLOR_ROW_VALUE_COUNT、COLOR_SELECTED_VALUE_COUNT、COLOR_HORIZONTAL_SWIPE_COUNT、COLOR_VERTICAL_SWIPE_COUNT 属于数据而非原因,不应出现在 AgentDiagnosticReason 中,随第 2 点一并移除。其中 COLOR_VERTICAL_SWIPE_COUNT 当前恒写 0,属占位,本阶段可直接删除,待第 2 项(纵向发现)实施时再引入。
COLOR_SELECTED_VALUE_COUNT
COLOR_HORIZONTAL_SWIPE_COUNT
COLOR_VERTICAL_SWIPE_COUNT
原方案要求「颜色/尺码分开计」,当前仅统计 color 维度。分支 C 的典型形态是默认选中的尺码过滤掉了部分颜色的可用性——参考实现的前置步骤即名为 _clear_default_size_selection(:1866)。只统计颜色选中数会漏掉最关键的一半证据。
要求:颜色与尺码的已选中值数量分别记录。
visibleNonClickableColorCandidateCount
bounds.bottom
bounds.top
改完第 1、2、4 点后装机,对同一商品采集一次,goauto_diagnostics.db 中应有一条 COLOR_DISCOVERY 记录,可读出:解析出的可点击颜色值数、可见但不可点击的候选数、颜色已选中数、尺码已选中数、已选摘要是否非空、行数与各行值数、横向滑动次数、终止原因。本阶段不要求采集结果变好。
goauto_diagnostics.db
对评论 #5174 与提交 9bb19b4 复核后,确认“单次颜色阶段只写一条诊断”和“分别记录颜色/尺码初始选中数”的方向正确,但第 1 点部分代码判断不成立,且新增字段涉及本地数据库迁移。后续不得直接按 #5174 原文实施,应以本评论为准修订。
#5174 所述“当前实现只检查规格节点自身,未检查后代节点”不成立。
PddScreenParser.parse 在构造规范化节点时已经执行:
selected = node.selected || descendants.any(SnapshotNode::selected) checked = node.checked || descendants.any(SnapshotNode::checked)
因此 VisibleSpecValue.node.selected/checked 已包含后代节点状态,不需要在诊断层重复遍历后代。
VisibleSpecValue.node.selected/checked
当前实现的真实问题是:diagnosticSelectedValues 对整个颜色采集过程取最大值。Agent 自己点击颜色后,后续快照可能产生选中态,从而污染“进入面板时是否已默认选中”的证据。
diagnosticSelectedValues
修订要求:
selectedSummary != null
selection.selectedPrefixes
AgentDiagnosticStore 当前全局最多保留 50 条记录。一次颜色阶段写入多条度量事件会过快淘汰详情进入、规格面板、尺码发现等其他阶段证据,因此后续应调整为:
AgentDiagnosticStore
应删除以下“指标型原因”:
COLOR_FOUND 不再同时充当数量指标。正常结束时由实际终止原因表达,例如稳定边界为 COLOR_EDGE_REACHED、达到上限为 COLOR_SCAN_LIMIT。
单条 COLOR_DISCOVERY 建议包含以下脱敏字段:
row_count
row_value_counts
clickable_color_count
non_clickable_candidate_count
initial_selected_color_count
initial_selected_size_count
selected_summary_present
horizontal_swipe_count
elapsed_ms
本阶段没有颜色纵向发现,vertical_swipe_count 恒为 0,没有诊断价值,应暂不新增;待纵向发现分支正式实施时再加入。
vertical_swipe_count
继续禁止保存颜色/尺码文案、价格、摘要原文、坐标、链接、goods_id、控件树、XML 和截图。
上述独立字段需要修改 Agent 本地 SQLite agent_diagnostic 表,这与工单正文现有“不修改数据库结构”描述冲突,也属于项目规则要求人工确认的数据库迁移。
因此后续实施前必须:
DATABASE_VERSION
v1 → v2
ALTER TABLE ADD COLUMN
clipboard* / target* / candidateCount / attempt
在取得迁移确认前,不继续修改代码。
本阶段明确不执行颜色纵向滑动,因此单次诊断不能独立证明“纵向滑动后出现新行”,也不能保证一次记录完成 A/B/C 三选一。
诊断只能:
这些读数是诊断证据,不是充分因果证明,不得据此自动执行取消尺码、放宽解析或纵向滚动修复。
当前诊断只保存在 Agent 本地数据库,没有 Admin/API 查看入口。装机验收前必须在工单写明可复现的读取路径:
adb shell run-as <最终包名>
若正式 APK 不允许 run-as,则本工单必须另行确认安全的只读诊断出口;不能只声称“已落库”后直接验收。
run-as
selected/checked
提交 9bb19b4 已完成首版诊断,但按本评论发现的问题,暂不进入真机验收。下一步应先由用户确认是否允许 Agent 本地 SQLite v1 → v2 增量迁移;确认后仅修订第 1 项诊断,不实施 #124 第 2~5 项。
针对修订评论中的两处判断分歧,逐条核对代码后结论如下;同时用户已授权本地诊断库迁移。后续实施以本评论为准,覆盖 #5174 与修订评论中被推翻的部分。
#5174 称「当前实现只检查规格节点自身,未检查后代节点」,该判断不成立,予以撤回。
PddProductDetailCollector.kt:151-152 在 parse 构造规范化节点时已完成折叠:
PddProductDetailCollector.kt:151-152
selected = node.selected || descendants.any(SnapshotNode::selected), checked = node.checked || descendants.any(SnapshotNode::checked),
因此 VisibleSpecValue.node.selected/checked 已含后代状态,不需要在诊断层重复遍历后代,也不得为此改动 parse。
修订评论对真实问题的定位正确并予采纳:diagnosticSelectedValues 使用 maxOf 跨整个采集过程取最大值,Agent 自身点击后的快照会污染「进入面板时是否已默认选中」这一证据。按修订要求执行:初始选中态只取首次颜色点击之前的快照,颜色与尺码分别记录。
maxOf
修订评论称「selectedSummary 也可能匹配『请选择』,因此不能直接作为已选证据」,该判断不成立。
PddProductDetailCollector.kt:290-292:
PddProductDetailCollector.kt:290-292
selectedSummary = labels.firstOrNull { val compact = it.replace(" ", "") textAliases.selection.selectedPrefixes.any(compact::startsWith) }
RuleContract.kt:90-91 中两个前缀集是分开的:
RuleContract.kt:90-91
summaryPrefixes = ["已选", "请选择", "已選", "請選擇"]
selectedPrefixes = ["已选", "已選"]
selectedSummary 使用的是 selectedPrefixes,本就只匹配「已选」,不会匹配「请选择」。
selectedPrefixes
因此:
screen.selectedSummary != null
:1311
:1419
修订评论要求「不保存摘要原文、只记布尔值或数量」的部分予以保留,与安全边界一致。
用户已于 2026-08-28 明确授权 Agent 本地诊断库 goauto_diagnostics.db 执行 v1 → v2 增量迁移。授权范围与约束:
AgentDiagnosticStore.kt:190
:139
onUpgrade(...) = Unit
onCreate
onUpgrade
clipboard*
target*
candidateCount
attempt
据此,工单正文「非目标」中的「不修改数据库结构」需同步修订为「不修改服务端数据库与共享 API 字段;允许 Agent 本地诊断库新增可空列的增量迁移」。
修订评论中下列各项予以确认,无需再讨论:
按本评论修订第 1 项诊断实现,仍不实施 #124 第 2~5 项。完成后装机采集一次原问题商品,回写脱敏聚合读数(不上传数据库文件与任何规格文案),再据此确定进入 A / B / C 哪个分支。
继续实施第 1 项诊断修订。已确认用户本次指令授权 Agent 本地 goauto_diagnostics.db 执行受限的 v1 → v2 增量迁移。范围严格按 #5192:单条 COLOR_DISCOVERY、独立脱敏字段、初始颜色/尺码选中态、迁移与读取路径测试;不修改 collectColors 遍历算法、parse 判定条件或 swipeSpec,不实施第 2~5 项。
已按最新修订方案完成“颜色阶段诊断”,本次没有修改 collectColors 的颜色遍历逻辑、解析判定条件或 swipeSpec。
color_row_count
color_row_value_counts
non_clickable_color_candidate_count
PddProductDetailCollectorTest
AgentDiagnosticRecorderTest
AgentDiagnosticStoreMigrationTest
a8d4790811aa825c78830cb9c71c6b21723e4584
d718c957bc223bc2e55c4ee6bc42855e11237fb3
尚未在原问题商品和真机上读取本次聚合诊断。工单保持待验收;下一步仅安装 0.9.13 Debug APK、复现原商品并按 Wiki 命令读取最新一条脱敏 COLOR_DISCOVERY 记录。在用户确认诊断分支前,不实施 #124 第 2~5 项,也不修改遍历、解析或手势逻辑。
已按确认范围完成纵向/网格颜色发现、动态行发现与稳定终止,未扩展解析口径、服务端接口、规则结构或数据库。
.\gradlew.bat :app:testDebugUnitTest --tests cn.ilapage.goauto.agent.PddProductDetailCollectorTest
python dev_scripts/harness.py sync --check
git diff --check
新增回归覆盖单列纵向分页和网格纵向视口变化;既有横向颜色回归继续通过。
设备:PKG110;PDD 商品、规则和设备与任务 #87 相同;Agent 0.9.14。
1,1,1,1,1,1
shopName
PAGE_STABILITY/TARGET_NOT_FOUND
b330f2be68cb2eddb929db2cb6021cf9f9b527ad
c2a1363f
本轮未在其他设备/ROM 上复测,也未另找一个真实横向多颜色商品真机复测;横向路径由既有单元回归覆盖。工单保持打开,等待人工验收。
更正上一条中的提交标识:完整提交为 c2a1363561bb3495174f888f75643821fdb924e7(短哈希 c2a1363),已推送至 origin/main。
c2a1363561bb3495174f888f75643821fdb924e7
c2a1363
用户于 2026-08-28 明确确认本工单验收通过。按项目流程记录验收结论并关闭工单;本次仅更新工单状态,无新增长期文档事实,不重复同步 Wiki。
No dependencies set.
The note is not visible to the blocked user.
所属与来源
e117fd2)执行当前页面采集后反馈:采集结果缺少颜色分类,只采到一种颜色;任务未失败,手机停留在 PDD 商品详情页。用户提出「是不是这次的颜色不是横向而是纵向的」。AGENTS.md「Gitea 交互与工单最小读取」记录回退原因。已证实事实
任务详情(用户读取):颜色仅
白色一项,尺码均码一项,缺失项仅reviewCount。由此可确定:
不是文案识别问题:缺失项没有
unsupported,说明颜色标题已被正确映射为color维度,不属于 #115 范畴。不是点击或采价失败:缺失项没有
selection:与price:,被采到的白色点击成功且价格正常。不是「发现多个但遍历中途退出」:
PddProductDetailCollector.kt:916在点击之前就把当前行全部可见值写入结果——若初始视口存在多个可见颜色值,即使随后因 PDD 重排触发
:905-913的提前 break,结果也应包含那些值。结果只有一项,说明解析阶段产出的 color 值本身就只有 1 个。reviewCount缺失与颜色无关,属既有独立小缺口,本工单不处理。代码事实(提交
e117fd2复核)collectColors(:873-946)只有水平翻页:val direction = if (moveRight) SwipeDirection.LEFT else SwipeDirection.RIGHT;整个函数没有任何SwipeDirection.UP。val rowCount = colorRows(initial).size(:888),后续不再重新发现新行。collectSizes(:1016-1060)具备纵向扫描,repeat(specVerticalSwipes + 1)循环内执行swipeSpec(SwipeDirection.UP, anchor)并逐屏合并。颜色与尺码的发现能力不对称。swipeSpec的横向分支(GoAutoAccessibilityService.kt:365-368)只接受width > height的可滚动祖先;纵向容器下取不到目标,会退回全局swipe(SemanticTarget.SPEC_PANEL, ...),而该兜底同样按方向筛选,可能直接返回 false 导致break。AgentDiagnosticStage(persistence/AgentDiagnosticStore.kt:9-18)只有DETAIL_ENTRY、SPEC_PANEL_ENTRY、SIZE_DISCOVERY及分享/剪贴板相关阶段,颜色阶段没有任何诊断落库。叠加 ColorOS 默认限制 logcat,本次根因只能靠任务详情间接推断。待证实分支(实施前必须先用诊断确认)
事实 3 把根因收敛到「解析阶段只产出 1 个 color 值」,但成因有两种,修复重点不同:
parse的 spec value 判定条件。实施顺序要求:先落地第 1 项诊断并取得一次真机记录,确认属于哪个分支后再实施对应修复;两个分支的修复不得在无证据的情况下同时盲改。
目标
非目标
reviewCount缺失。agent_diagnostic表可空列增量迁移,不删除、不重建表、不清空旧记录。实施方案
1. 颜色阶段诊断(先行,独立可验证)
AgentDiagnosticStage新增COLOR_DISCOVERY;AgentDiagnosticReason新增区分下列结果的枚举:COLOR_FOUND、COLOR_EDGE_REACHED、COLOR_SCAN_LIMIT、COLOR_ROW_REFLOWED、COLOR_SWIPE_FAILED、COLOR_CONTAINER_UNAVAILABLE、COLOR_VALUES_NOT_CLICKABLE。SafeAgentDiagnosticRecorder旁路保护,写入失败不影响采集结果。2. 分支 A · 纵向发现(确认后实施)
collectColors中引入纵向发现循环,复用collectSizes的模式:滚动一屏 → 重新colorRows()→ 合并新行 → 直到稳定边界或次数上限。rowCount改为每轮重新计算,不再于第一帧冻结;已处理行以行内容签名而非索引标识,避免滚动后索引错位。swipeSpec横向分支增加兜底:取不到width > height的可滚动祖先时,不直接退回全局兜底,而是判定该维度为纵向布局并跳过横向翻页,避免无效手势与误滑。3. 分支 B · 规格值判定(确认后实施)
parse中 spec value 的接受条件:允许「自身不可点击但存在可点击祖先」或「带明确选中语义」的节点作为规格值,并在结果中如实标记available。completed_partial,并在缺失项如实标注。4. 重排导致的提前退出
:905-913的rowIndex >= rows.size分支不再直接 break:重新扫描当前所有行,对照attempted判断是否仍有未尝试且可用的值;确无剩余时才结束,并记录COLOR_ROW_REFLOWED。5. 规则参数
limits新增colorVerticalSwipes(颜色纵向发现次数上限),沿用既有边界校验口径;未提供时取specVerticalSwipes作为默认值,保证存量规则快照不失效。collector新增可选colorLayout: auto | horizontal | vertical | grid,默认auto,仅作为自动判定失灵时的临时覆盖手段。rulecontract同步新增校验,字段名与上下限与 Android 解析保持一致。安全边界
验收标准
诊断(可独立验收)
COLOR_DISCOVERY记录,可读出行数、每行值数、可点击值数、不可点击候选数、滚动次数与终止原因。分支 A(若确认)
分支 B(若确认)
available。共同
limits/colorLayout的存量规则快照行为不变。验证方式
cd android && .\gradlew.bat :app:testDebugUnitTest :app:assembleDebuggo test ./app/goauto/rulecontract/...依赖、并行与风险
e117fd2)、#116(3619435)已合并;#112、#113 已合并。PddProductDetailCollector或GoAutoAccessibilityService手势逻辑的工单并行。stableEdgeReads稳定边界与次数上限,并受规则总超时约束。文档影响
Android-Agent-API-Contract(docs/08-agent-api-contract.md):采集规则新增limits.colorVerticalSwipes与collector.colorLayout的说明与默认值。Business-Rules-and-Glossary(docs/03-business-rules-and-glossary.md):如颜色完整性判定口径发生变化则同步更新;仅补充发现能力时可记为无长期文档影响并说明原因。sync与一轮sync --check,把页面与 revision 写回本工单。状态
第 1 项诊断修订已由提交
d718c95完成,等待 Debug APK 真机读取验收;第 2~5 项未实施。取得原问题商品的脱敏聚合证据并由用户确认分支前,不继续修改遍历、解析或手势逻辑。补充:参考实现分析与第三个待证实分支
用户提示
/mnt/d/chengma/cmautobuy/client已有处理同类问题的 Python 实现。已只读分析client/src/pdd_collect_service.py(2819 行,当前工作树,未做提交基线核对,仅作行为参考)。结论如下。一、参考实现同样没有颜色纵向发现(重要,防止照抄)
_collect_color_prices(:1631)与 GoAuto 的collectColors结构基本一致:纵向滚动只出现在
_clear_default_size_selection(:1866)与_move_spec_panel_to_top(:1928),颜色遍历中没有任何纵向动作。因此:若确认为分支 A(纵向布局),参考实现不提供可借鉴方案,该部分必须原创;实施时不得以「参考项目如此实现」为由跳过纵向发现能力。
二、可直接借鉴的四个机制
行消失时明确失败,不静默(
:1671):GoAuto 在对应位置(
PddProductDetailCollector.kt:905-913)是静默break,导致本次「任务成功但只有一个颜色」的无声失败。本工单第 4 项(重排提前退出)应至少保证该情况可被诊断区分;是否升级为失败按实施时的证据决定,不得继续无声吞掉。滑动区域选法更可靠(
_horizontal_region:2773):选取最深的可滚动节点,且要求其子节点含 clickable,不依赖宽高比。GoAuto 的GoAutoAccessibilityService.kt:365-368按width > height筛选,在纵向或嵌套容器下直接失效。本工单第 7 项改为采用这一选法,优先级高于原文的「判定为纵向布局并跳过横向翻页」。截断项过滤(
_visible_color_rows:2650):过滤被屏幕边缘截断的半个按钮;自绘非滚动面板(按钮宽度随文字长度变化)时则保留全部候选,不做宽度过滤。GoAuto 的
colorRows(:1306-1323)没有这层,可能把截断项当成完整值。滑动后等待视口真实变化(
_wait_for_horizontal_change):GoAuto 目前是固定pause(350),慢设备上可能读到旧视口。三、新增待证实分支 C · 默认选中态干扰
参考实现有一个 GoAuto 完全没有的前置步骤
_clear_default_size_selection(:1866):进入规格面板后,先定位默认已选中的尺码并取消选择(且只在唯一选中项明确时点击一次,页面不支持取消时不重复点击),随后_move_spec_panel_to_top回顶,再开始逐色采价。该步骤的存在说明参考项目遇到过「默认选中态干扰规格遍历」的真实问题。本次任务结果为「白色 + 均码」两个维度各仅一个值,与该情形吻合:若进入面板时带默认选中态,部分颜色可能因当前选中尺码无货而不出现在可点击节点中,从而在解析阶段就只产出一个 color 值。
补充佐证:参考实现的
_top_level_clickable_options(:796)同样要求clickable == "true",即原工单的分支 B(未选中项不可点击)在参考实现中同样会失败。这反过来提高了分支 C 的分量。因此待证实分支由两个扩展为三个:
parse判定条件_clear_default_size_selection的安全约束(唯一选中项才点、只点一次、取消失败不重试)四、对应的诊断字段调整
原方案第 1 项的诊断需增加一个字段:进入规格面板时已处于选中状态的维度值数量(按维度分别计数)。加上原有的「不可点击但可见的候选数量」,一次真机记录即可三选一:
诊断仍不得记录颜色文案、价格、控件树、坐标原文与截图。
五、参考边界
六、正文其余部分
目标、非目标、安全边界、验证方式、依赖与文档影响不变。实施顺序仍为:先落地诊断 → 取得一次真机记录 → 确认分支 → 实施对应修复。
开始实施第 1 项「颜色阶段诊断」。本次范围按用户明确指令收窄:仅新增脱敏、旁路保护的颜色诊断及测试;不修改
collectColors的遍历逻辑,不修改PddScreenParser.parse判定条件,不修改swipeSpec。完成后提交并停在待验收,不实施工单第 2~5 项。第 1 项已实施,等待验收
已按用户明确收窄的范围完成颜色阶段诊断,未实施第 2~5 项。
实现
0.9.12(versionCode 25)。COLOR_DISCOVERY。agent_diagnostic表和通用数值字段记录脱敏聚合事件,没有新增数据库列或迁移:SafeAgentDiagnosticRecorder旁路写入;诊断持久化失败不会改变采集结果。明确未修改
collectColors的固定首帧行数、横向蛇形遍历、滚动上限或采集结果逻辑;仅在原有分支旁增加观察与终止原因记录。PddScreenParser.parse的规格值判定条件。GoAutoAccessibilityService.swipeSpec。验证
python dev_scripts/harness.py check --strict:通过。.\scripts\verify.ps1 -Component android:Debug/Release 单元测试通过,Debug APK 构建通过。android/app/build/outputs/apk/debug/app-debug.apk。提交
9bb19b4d0f80e753c03689b5853128ad968b07b6—feat(#124): add color discovery diagnosticsorigin/main。文档影响与未验证项
本阶段只增加 Agent 内部脱敏诊断枚举和落库事件,未改变业务规则、共享接口、配置、状态或操作方式,判定为无长期 Wiki 文档影响,按规则跳过 Wiki 更新与同步。
尚未执行 PKG110 真机采集;需要安装 0.9.12 后运行一次原问题商品,读取
COLOR_DISCOVERY聚合记录,再决定 #124 后续应进入纵向布局、解析判定或其他分支。本工单保持打开,状态为待验收。第 1 项(颜色阶段诊断)评审 — 提交
9bb19b4范围遵守良好:未改动
collectColors的遍历算法、parse判定条件与swipeSpec,符合「只做诊断」的约束。终止原因分支覆盖完整(reflow / edge / limit / swipe failed / container unavailable),并补充了单元测试。但建议改完下列第 1、2、4 点再装机验收,否则本次真机很可能给出误导性读数。
必须改
1. 已选中判定口径过窄,会把分支 C 误排除
当前实现:
只检查规格值节点自身的
selected/checked。PDD 大量自绘控件把选中态放在后代节点上,父节点两个标志均为 false。参考实现对照(
/mnt/d/chengma/cmautobuy/client/src/pdd_collect_service.py):_node_is_selected(:2688)遍历自身及全部后代:_color_selection_state_exposed(:2694)把已选摘要snapshot.selected_text作为首选证据,节点标志只作兜底。GoAuto 的
ParsedPddScreen已有现成的selectedSummary字段,本次未使用。要求:选中判定改为遍历节点自身及后代;同时把
selectedSummary是否非空(以及其中可识别的维度值数量)一并记入诊断。按当前写法,若 PDD 不暴露selected,该字段将恒为 0,从而错误地排除分支 C —— 而分支 C 正是与本次「白色 + 均码」现象最吻合的假设。2. 单次采集写入 8~10 条诊断,会挤掉其它阶段历史
recordColorDiscovery把每个度量写成一条独立记录:每行一条COLOR_ROW_VALUE_COUNT,加 5 条固定度量,再加终止原因。而
AgentDiagnosticStore.kt:104的MAX_RECORDS = 50是全局上限,按 id 倒序保留(:172)。一次当前页面采集本就要写LINK_RESOLUTION、DETAIL_ENTRY、SPEC_PANEL_ENTRY、SIZE_DISCOVERY,颜色阶段再吃掉近 10 条,跑三四次即会把此前各阶段记录全部冲掉,后续跨阶段排查将失去证据。要求:一次颜色采集只写 1 条记录;各项度量改为
AgentDiagnosticEvent的独立数值字段(参照既有clipboardPollCount/clipboardItemCount/clipboardContentLength的做法新增列),不得塞进reason枚举。每行值数量可用一个「行数 + 各行值数的紧凑摘要」单字段承载,仍不含任何文案。应该改
3.
COLOR_FOUND同时充当度量键与终止原因,读数有歧义读取时无法区分某条记录表示「终止原因为正常完成」还是「解析值总数」。按第 2 点合并为单条记录后该问题自然消除。
同时,
COLOR_ROW_VALUE_COUNT、COLOR_SELECTED_VALUE_COUNT、COLOR_HORIZONTAL_SWIPE_COUNT、COLOR_VERTICAL_SWIPE_COUNT属于数据而非原因,不应出现在AgentDiagnosticReason中,随第 2 点一并移除。其中COLOR_VERTICAL_SWIPE_COUNT当前恒写 0,属占位,本阶段可直接删除,待第 2 项(纵向发现)实施时再引入。4. 缺少尺码维度的选中数
原方案要求「颜色/尺码分开计」,当前仅统计
color维度。分支 C 的典型形态是默认选中的尺码过滤掉了部分颜色的可用性——参考实现的前置步骤即名为_clear_default_size_selection(:1866)。只统计颜色选中数会漏掉最关键的一半证据。要求:颜色与尺码的已选中值数量分别记录。
做得好的部分
visibleNonClickableColorCandidateCount的区间圈定合理:以颜色标题bounds.bottom为上界、下一个维度标题bounds.top为下界,并排除含后代的容器节点避免重复计数,字段质量满足分支判定需要。验收口径
改完第 1、2、4 点后装机,对同一商品采集一次,
goauto_diagnostics.db中应有一条COLOR_DISCOVERY记录,可读出:解析出的可点击颜色值数、可见但不可点击的候选数、颜色已选中数、尺码已选中数、已选摘要是否非空、行数与各行值数、横向滑动次数、终止原因。本阶段不要求采集结果变好。第 1 项评审修订(全栈复核结论)
对评论 #5174 与提交
9bb19b4复核后,确认“单次颜色阶段只写一条诊断”和“分别记录颜色/尺码初始选中数”的方向正确,但第 1 点部分代码判断不成立,且新增字段涉及本地数据库迁移。后续不得直接按 #5174 原文实施,应以本评论为准修订。一、纠正选中态判断
#5174 所述“当前实现只检查规格节点自身,未检查后代节点”不成立。
PddScreenParser.parse在构造规范化节点时已经执行:因此
VisibleSpecValue.node.selected/checked已包含后代节点状态,不需要在诊断层重复遍历后代。当前实现的真实问题是:
diagnosticSelectedValues对整个颜色采集过程取最大值。Agent 自己点击颜色后,后续快照可能产生选中态,从而污染“进入面板时是否已默认选中”的证据。修订要求:
selectedSummary != null不能直接作为已选证据,因为selectedSummary也可能匹配“请选择”。只有摘要按规则中的selection.selectedPrefixes(如“已选/已選”)开头时,才记录“存在已选摘要”。二、单条诊断记录方向确认
AgentDiagnosticStore当前全局最多保留 50 条记录。一次颜色阶段写入多条度量事件会过快淘汰详情进入、规格面板、尺码发现等其他阶段证据,因此后续应调整为:collectColors最多写入 1 条COLOR_DISCOVERY;reason只表示最终终止原因;应删除以下“指标型原因”:
COLOR_ROW_VALUE_COUNTCOLOR_SELECTED_VALUE_COUNTCOLOR_HORIZONTAL_SWIPE_COUNTCOLOR_VERTICAL_SWIPE_COUNTCOLOR_FOUND不再同时充当数量指标。正常结束时由实际终止原因表达,例如稳定边界为COLOR_EDGE_REACHED、达到上限为COLOR_SCAN_LIMIT。三、建议的单记录字段
单条
COLOR_DISCOVERY建议包含以下脱敏字段:row_count:识别到的颜色行数;row_value_counts:各行值数量的紧凑摘要,只允许数字和分隔符,并限制长度;clickable_color_count:解析到的可点击颜色值数量;non_clickable_candidate_count:颜色区间内可见但未解析为可点击规格值的候选数量;initial_selected_color_count:首次颜色点击前已选颜色值数量;initial_selected_size_count:首次颜色点击前已选尺码值数量;selected_summary_present:是否存在以selection.selectedPrefixes开头的已选摘要;horizontal_swipe_count:颜色发现阶段实际成功的横向滑动次数;elapsed_ms;reason保存唯一终止原因。本阶段没有颜色纵向发现,
vertical_swipe_count恒为 0,没有诊断价值,应暂不新增;待纵向发现分支正式实施时再加入。继续禁止保存颜色/尺码文案、价格、摘要原文、坐标、链接、goods_id、控件树、XML 和截图。
四、数据库迁移门禁
上述独立字段需要修改 Agent 本地 SQLite
agent_diagnostic表,这与工单正文现有“不修改数据库结构”描述冲突,也属于项目规则要求人工确认的数据库迁移。因此后续实施前必须:
DATABASE_VERSION从 1 升级到 2;v1 → v2的非破坏性ALTER TABLE ADD COLUMN迁移;clipboard* / target* / candidateCount / attempt等旧字段来塞入颜色指标。在取得迁移确认前,不继续修改代码。
五、诊断能力边界
本阶段明确不执行颜色纵向滑动,因此单次诊断不能独立证明“纵向滑动后出现新行”,也不能保证一次记录完成 A/B/C 三选一。
诊断只能:
这些读数是诊断证据,不是充分因果证明,不得据此自动执行取消尺码、放宽解析或纵向滚动修复。
六、真机读取与验收补充
当前诊断只保存在 Agent 本地数据库,没有 Admin/API 查看入口。装机验收前必须在工单写明可复现的读取路径:
adb shell run-as <最终包名>读取或导出goauto_diagnostics.db的具体命令;COLOR_DISCOVERY;若正式 APK 不允许
run-as,则本工单必须另行确认安全的只读诊断出口;不能只声称“已落库”后直接验收。七、修订后的测试要求
COLOR_DISCOVERY。selected/checked继续通过现有 Parser 规范化结果生效。v1 → v2迁移保留旧记录并补齐可空列。collectColors遍历、PddScreenParser.parse判定条件和swipeSpec仍不得改变。状态
提交
9bb19b4已完成首版诊断,但按本评论发现的问题,暂不进入真机验收。下一步应先由用户确认是否允许 Agent 本地 SQLitev1 → v2增量迁移;确认后仅修订第 1 项诊断,不实施 #124 第 2~5 项。争议裁决与迁移授权(以本评论为准)
针对修订评论中的两处判断分歧,逐条核对代码后结论如下;同时用户已授权本地诊断库迁移。后续实施以本评论为准,覆盖 #5174 与修订评论中被推翻的部分。
一、撤回 #5174 的第 1 点(我方判断有误)
#5174 称「当前实现只检查规格节点自身,未检查后代节点」,该判断不成立,予以撤回。
PddProductDetailCollector.kt:151-152在parse构造规范化节点时已完成折叠:因此
VisibleSpecValue.node.selected/checked已含后代状态,不需要在诊断层重复遍历后代,也不得为此改动parse。修订评论对真实问题的定位正确并予采纳:
diagnosticSelectedValues使用maxOf跨整个采集过程取最大值,Agent 自身点击后的快照会污染「进入面板时是否已默认选中」这一证据。按修订要求执行:初始选中态只取首次颜色点击之前的快照,颜色与尺码分别记录。二、纠正修订评论第一节第 3 条(该条判断有误)
修订评论称「
selectedSummary也可能匹配『请选择』,因此不能直接作为已选证据」,该判断不成立。PddProductDetailCollector.kt:290-292:RuleContract.kt:90-91中两个前缀集是分开的:summaryPrefixes = ["已选", "请选择", "已選", "請選擇"]selectedPrefixes = ["已选", "已選"]selectedSummary使用的是selectedPrefixes,本就只匹配「已选」,不会匹配「请选择」。因此:
selected_summary_present直接取screen.selectedSummary != null即可,不需要额外的前缀再判定;selectedSummary的取值语义。该字段还被:1311的颜色选中确认与:1419的屏幕签名使用,改动会溢出本工单范围并影响既有采价与稳定性判定。修订评论要求「不保存摘要原文、只记布尔值或数量」的部分予以保留,与安全边界一致。
三、数据库迁移授权
用户已于 2026-08-28 明确授权 Agent 本地诊断库
goauto_diagnostics.db执行v1 → v2增量迁移。授权范围与约束:agent_diagnostic表新增可空列,只允许ALTER TABLE ADD COLUMN。AgentDiagnosticStore.kt:190的DATABASE_VERSION由 1 升为 2;:139现为onUpgrade(...) = Unit空实现,必须替换为真实的v1 → v2迁移逻辑,否则升级后新列缺失将导致写入失败。onCreate」与「旧库升级走onUpgrade」两条路径的测试。clipboard*/target*/candidateCount/attempt等既有字段承载颜色指标。据此,工单正文「非目标」中的「不修改数据库结构」需同步修订为「不修改服务端数据库与共享 API 字段;允许 Agent 本地诊断库新增可空列的增量迁移」。
四、其余修订内容一并确认
修订评论中下列各项予以确认,无需再讨论:
collectColors最多写入 1 条COLOR_DISCOVERY;reason只表示唯一终止原因。COLOR_ROW_VALUE_COUNT、COLOR_SELECTED_VALUE_COUNT、COLOR_HORIZONTAL_SWIPE_COUNT、COLOR_VERTICAL_SWIPE_COUNT;COLOR_FOUND不再兼任数量指标。vertical_swipe_count本阶段不新增,待纵向发现分支实施时再加。selected_summary_present的取值方式)。run-as,需另行确认安全的只读导出方式,不得仅声称「已落库」即进入验收。五、下一步
按本评论修订第 1 项诊断实现,仍不实施 #124 第 2~5 项。完成后装机采集一次原问题商品,回写脱敏聚合读数(不上传数据库文件与任何规格文案),再据此确定进入 A / B / C 哪个分支。
继续实施第 1 项诊断修订。已确认用户本次指令授权 Agent 本地
goauto_diagnostics.db执行受限的v1 → v2增量迁移。范围严格按 #5192:单条COLOR_DISCOVERY、独立脱敏字段、初始颜色/尺码选中态、迁移与读取路径测试;不修改collectColors遍历算法、parse判定条件或swipeSpec,不实施第 2~5 项。第 1 项实施完成,等待真机验收
已按最新修订方案完成“颜色阶段诊断”,本次没有修改
collectColors的颜色遍历逻辑、解析判定条件或swipeSpec。实现
collectColors正常结束仅写入 1 条COLOR_DISCOVERY诊断记录。color_row_countcolor_row_value_countsclickable_color_countnon_clickable_color_candidate_countinitial_selected_color_countinitial_selected_size_countselected_summary_presenthorizontal_swipe_countselected_summary_present仅依据解析器已经识别的“已选”摘要;“请选择”不会误判。reason仅保留终止原因,不再用独立记录表达行数、选中数或滑动数。goauto_diagnostics.db由 v1 升级至 v2,使用只增加可空列的迁移;重复执行不会重复加列,旧记录保留。验证
PddProductDetailCollectorTestAgentDiagnosticRecorderTestAgentDiagnosticStoreMigrationTestpython dev_scripts/harness.py check --strict:通过。.\scripts\verify.ps1 -Component android:通过,Debug/Release 单元测试及 Debug APK 构建成功。android/app/build/outputs/apk/debug/app-debug.apk。文档与提交
a8d4790811aa825c78830cb9c71c6b21723e4584。sync/sync --check。d718c957bc223bc2e55c4ee6bc42855e11237fb3origin/main。未验证与停止点
尚未在原问题商品和真机上读取本次聚合诊断。工单保持待验收;下一步仅安装 0.9.13 Debug APK、复现原商品并按 Wiki 命令读取最新一条脱敏
COLOR_DISCOVERY记录。在用户确认诊断分支前,不实施 #124 第 2~5 项,也不修改遍历、解析或手势逻辑。实施完成,待验收
已按确认范围完成纵向/网格颜色发现、动态行发现与稳定终止,未扩展解析口径、服务端接口、规则结构或数据库。
实现
specVerticalSwipes与stableEdgeReads,通过连续稳定视口签名终止,并保留扫描上限、容器不可用、滑动失败等明确诊断原因。自动验证
.\gradlew.bat :app:testDebugUnitTest --tests cn.ilapage.goauto.agent.PddProductDetailCollectorTest:通过。.\scripts\verify.ps1 -Component android:通过;Debug/Release 共 162 个测试通过,Debug APK 构建成功。python dev_scripts/harness.py check --strict:通过。python dev_scripts/harness.py sync --check:通过。git diff --check:通过。新增回归覆盖单列纵向分页和网格纵向视口变化;既有横向颜色回归继续通过。
同商品真机验证
设备:PKG110;PDD 商品、规则和设备与任务 #87 相同;Agent 0.9.14。
completed_partial,最终结果仅 1 个颜色、1 条颜色价格、1 个完整 SKU。completed_partial,最终结果为 6 个颜色、6 条稳定颜色价格、6 个完整 SKU。COLOR_EDGE_REACHED;首屏 6 行,行值数1,1,1,1,1,1;可点击 6、不可点击候选 3;初始已选颜色/尺码/摘要均存在;横向滑动 0;耗时 11248ms。shopName、reviewCount,本工单颜色目标已验证。PAGE_STABILITY/TARGET_NOT_FOUND失败;关闭遗留面板后同商品任务 #89 成功完成,未作为颜色逻辑结果计入。文档与提交
Business-Rules-and-Glossary,revisionb330f2be68cb2eddb929db2cb6021cf9f9b527ad;已在线回读并同步本地镜像。c2a1363f(已推送到origin/main)。未验证范围
本轮未在其他设备/ROM 上复测,也未另找一个真实横向多颜色商品真机复测;横向路径由既有单元回归覆盖。工单保持打开,等待人工验收。
更正上一条中的提交标识:完整提交为
c2a1363561bb3495174f888f75643821fdb924e7(短哈希c2a1363),已推送至origin/main。ila referenced this issue2026-08-28 15:04:53 +08:00
用户于 2026-08-28 明确确认本工单验收通过。按项目流程记录验收结论并关闭工单;本次仅更新工单状态,无新增长期文档事实,不重复同步 Wiki。