修复:PDD 底部购买栏双入口导致规格入口候选不唯一(最右优先) #209

Open
opened 2026-09-03 16:43:55 +08:00 by ila · 1 comment
Owner

来源与目标

2026-09-03 用户报告:本地服务已安装 #208 的 Debug APK(16:30 完成安装)后,采购任务 CG63 于 16:34 重试两次仍然失败,错误仍为「没有找到安全的商品规格入口」。用户原话:「已连上手机,最新agent,cg63重试采集报错如下,找出原因:没有找到安全的商品规格入口,可以参考"D:\chengma\cmautobuy\client"的代码实现的逻辑。」

目标:在不放宽到任意文字点击的前提下,让底部购买栏在出现「单独购买 + 发起拼单」双按钮时能确定性地选出唯一入口,而不是因候选不唯一直接失败。

当前事实(真机证据,2026-09-03)

任务与商品:

  • purchase_task #63(CG63),pdd_product_id=1325,goods_id=320573752790,spec_source=exact_match,目标规格「圖片色豹紋 / 均碼(建議40.0-62.5公斤)」,当前 status=failed。
  • purchase_task_attempt 中 #63 自 15:53 至 16:34 连续失败 8 次;其中 116、117 两次发生在 #208 的 APK 安装(16:30)之后,证明 #208 的嵌套规格行兼容未覆盖该页面形态。
  • 该任务执行的是规格探测(PurchaseRehearsalExecutor),非采集任务;同期 collection_task 最新记录停在 2026-09-01。

页面形态(USB 设备 3B65BD02H7F00000,屏幕 1080x2376,仅读取控件树,未点击、未下单、未支付):

  • 商品页自上而下为轮播图、价格、标题、服务标签、评价区,不存在任何「请选择 / 已选 + 规格词」的规格行。因此 explicitSpecEntry 与 nestedSpecEntry 均为空,只能落到底部购买栏兜底。

  • 底部购买栏为两个并列可点击 ViewGroup:

    入口 bounds clickable
    单独购买 [446,2166][685,2328] true
    发起拼单 [685,2166][1080,2328] true
  • 两者全部通过 safeBottomSpecEntries 的既有几何约束(centerX≥0.4W=432、centerY≥0.8H=1901、bottom≥0.88H=2091、width≥0.08W=86、height≤0.3H=713),因此 bottomSpecEntries.size == 2。

失败点(PddProductDetailCollector.kt):

val bottomSpecEntry = bottomSpecEntries.singleOrNull()   // 候选为 2 时返回 null

singleOrNull() 在双按钮下返回 null,specEntry 与 quickConfirmationEntry 同时为空,PurchaseRehearsalExecutor.openSpecPanel 的 candidates 为空集,直接返回 PURCHASE_SPEC_ENTRY_NOT_FOUND /「没有找到安全的商品规格入口」。

值得注意的是,safeBottomSpecEntries 内部已经计算了排序:

.sortedWith(compareBy<Pair<SafeSpecEntry, Long>> { it.second }.thenByDescending { it.first.clickTarget.bounds.centerX })

即按可点击区域面积升序、再按 centerX 降序,但该优先级随后被 singleOrNull() 丢弃。

参考实现对照(D:\chengma\cmautobuy\client\src\util\get_size_panle_coord.py):该实现遇到多候选时不放弃,而是按 10 + 3*(含价格数字) + y_ratio*2 + x_ratio 打分并取最大值。本场景两个按钮前三项相同,最终由 x_ratio 决出,即取最右侧入口。

诊断现状:specEntryEvidence 生成的标量证据(specEntryCandidates、explicit、nested、bottomPurchase 等计数)当前仅通过 AgentForegroundService.kt:476 写入 logcat(tag GoAutoPurchasePanel),不随失败结果回传服务端,导致后台只能看到一句无上下文的错误文案,本次必须接手机拉取控件树才能定位。

用户已确认的决策

底部购买栏出现多个合法候选时,按最右优先选择唯一入口,与 cmautobuy 参考实现的语义一致。

范围

  1. Android:safeBottomSpecEntries 的消费方由 singleOrNull() 改为在既有安全过滤全部通过的候选中确定性取唯一入口,排序调整为最右优先(centerX 降序;并列时以面积升序兜底),使排序结果与已确认决策一致。
  2. Android:specEntrySource 需能区分该入口是唯一候选还是多候选按优先级选出,便于后续排查。
  3. Android:SPEC_ENTRY_NOT_FOUND 与 SPEC_ENTRY_TARGET_AMBIGUOUS 的失败消息追加已有的标量诊断串,格式与同函数内 SPEC_PANEL_NOT_OPENED 的 [...] 既有先例一致。该串只含计数与布尔量,不含节点文本、路径、控件树或截图。
  4. 补充单测:覆盖「单独购买 + 发起拼单」双按钮页面产生唯一最右入口、单按钮页面行为不变、以及评价/订单/提交订单/支付区域仍被排除的拒绝形态。

非目标

  • 不修改服务端、采购任务状态机、采购规则快照或规则契约默认别名。
  • 不放宽 safeBottomSpecEntries 既有的几何约束、buyWords 语义要求、nonConfigurableClickDenylist 与评价上下文排除。
  • 不把标量诊断改造为结构化字段回传服务端接口(该项如需实施另建工单)。
  • 不自动重试 CG63、不创建订单、不执行支付、不使用 OCR/VLM、不保存原始控件树或截图。

已知风险

「单独购买」与「发起拼单」打开的是同一个规格面板,对本工单涉及的规格探测无差异;但同一段选择逻辑同时服务真实采购路径,届时两者对应不同成交形态与价格(本商品为 ¥27.9 与 ¥17.72)。本次按用户确认采用最右优先,该业务后果由人工承担。

PDD 新版详情页普遍不再暴露独立规格行,仅能由底部按钮进入规格面板,因此本形态预计影响面较广,而非单一商品。

方案与设计证据

属于恢复既有「打开安全规格面板」行为的 Android 缺陷修复,无新页面、导航或用户可见交互变化,不需要 UI 原型。设计证据为 PddProductDetailCollector 现有安全语义、上述真机控件树事实,以及 cmautobuy get_size_panel_coord 的同场景既有实现。

验收

  • 「单独购买 + 发起拼单」双按钮商品页能产生唯一规格入口,且命中最右侧按钮。
  • 单候选页面与既有测试快照行为不变。
  • 评价、评论、晒单、问答、订单、提交订单、支付区域仍不能成为候选。
  • 失败消息中的诊断串不含节点文本、路径、原始控件树或截图。
  • Android 单测与 :app:assembleDebug 通过。
  • 真机重试 CG63 需人工授权后单独进行,本工单实施阶段不执行下单或支付。

文档影响

需更新长期业务规则 Business-Rules-and-Glossary 中商品规格入口的安全边界描述,补充「底部购买入口多候选时按最右优先确定性选择」。按 Wiki-first 流程先改线上页面并在线回读 revision,再执行一次 sync 与一次 sync --check。

注意:仓库内 docs/12-syb-erp-interface.md 存在与本工单无关的既有改动,sync 的保护机制可能阻止同步(#208 已遇到同样情况)。不得重置或覆盖该无关改动,如被阻止则在本工单记录原因。

依赖

无阻塞依赖。与 #208(已实施,待验收)为同一 CG63 故障的不同成因,本工单处理 #208 未覆盖的底部双按钮形态。

实施分工

实施由 Codex 执行;本仓库 Gitea 无 codex 账号,指派人记为 ila。实施完成后由 Claude 按本工单验收标准审核。

## 来源与目标 2026-09-03 用户报告:本地服务已安装 #208 的 Debug APK(16:30 完成安装)后,采购任务 CG63 于 16:34 重试两次仍然失败,错误仍为「没有找到安全的商品规格入口」。用户原话:「已连上手机,最新agent,cg63重试采集报错如下,找出原因:没有找到安全的商品规格入口,可以参考"D:\chengma\cmautobuy\client"的代码实现的逻辑。」 目标:在不放宽到任意文字点击的前提下,让底部购买栏在出现「单独购买 + 发起拼单」双按钮时能确定性地选出唯一入口,而不是因候选不唯一直接失败。 ## 当前事实(真机证据,2026-09-03) 任务与商品: - `purchase_task` #63(CG63),`pdd_product_id=1325`,`goods_id=320573752790`,`spec_source=exact_match`,目标规格「圖片色豹紋 / 均碼(建議40.0-62.5公斤)」,当前 `status=failed`。 - `purchase_task_attempt` 中 #63 自 15:53 至 16:34 连续失败 8 次;其中 116、117 两次发生在 #208 的 APK 安装(16:30)之后,证明 #208 的嵌套规格行兼容未覆盖该页面形态。 - 该任务执行的是规格探测(`PurchaseRehearsalExecutor`),非采集任务;同期 `collection_task` 最新记录停在 2026-09-01。 页面形态(USB 设备 3B65BD02H7F00000,屏幕 1080x2376,仅读取控件树,未点击、未下单、未支付): - 商品页自上而下为轮播图、价格、标题、服务标签、评价区,**不存在任何「请选择 / 已选 + 规格词」的规格行**。因此 `explicitSpecEntry` 与 `nestedSpecEntry` 均为空,只能落到底部购买栏兜底。 - 底部购买栏为两个并列可点击 ViewGroup: | 入口 | bounds | clickable | |---|---|---| | 单独购买 | [446,2166][685,2328] | true | | 发起拼单 | [685,2166][1080,2328] | true | - 两者全部通过 `safeBottomSpecEntries` 的既有几何约束(centerX≥0.4W=432、centerY≥0.8H=1901、bottom≥0.88H=2091、width≥0.08W=86、height≤0.3H=713),因此 `bottomSpecEntries.size == 2`。 失败点(`PddProductDetailCollector.kt`): ```kotlin val bottomSpecEntry = bottomSpecEntries.singleOrNull() // 候选为 2 时返回 null ``` `singleOrNull()` 在双按钮下返回 null,`specEntry` 与 `quickConfirmationEntry` 同时为空,`PurchaseRehearsalExecutor.openSpecPanel` 的 `candidates` 为空集,直接返回 `PURCHASE_SPEC_ENTRY_NOT_FOUND` /「没有找到安全的商品规格入口」。 值得注意的是,`safeBottomSpecEntries` 内部**已经**计算了排序: ```kotlin .sortedWith(compareBy<Pair<SafeSpecEntry, Long>> { it.second }.thenByDescending { it.first.clickTarget.bounds.centerX }) ``` 即按可点击区域面积升序、再按 centerX 降序,但该优先级随后被 `singleOrNull()` 丢弃。 参考实现对照(`D:\chengma\cmautobuy\client\src\util\get_size_panle_coord.py`):该实现遇到多候选时不放弃,而是按 `10 + 3*(含价格数字) + y_ratio*2 + x_ratio` 打分并取最大值。本场景两个按钮前三项相同,最终由 `x_ratio` 决出,即**取最右侧入口**。 诊断现状:`specEntryEvidence` 生成的标量证据(`specEntryCandidates`、`explicit`、`nested`、`bottomPurchase` 等计数)当前仅通过 `AgentForegroundService.kt:476` 写入 logcat(tag `GoAutoPurchasePanel`),不随失败结果回传服务端,导致后台只能看到一句无上下文的错误文案,本次必须接手机拉取控件树才能定位。 ## 用户已确认的决策 底部购买栏出现多个合法候选时,**按最右优先**选择唯一入口,与 cmautobuy 参考实现的语义一致。 ## 范围 1. Android:`safeBottomSpecEntries` 的消费方由 `singleOrNull()` 改为在既有安全过滤全部通过的候选中确定性取唯一入口,排序调整为**最右优先**(centerX 降序;并列时以面积升序兜底),使排序结果与已确认决策一致。 2. Android:`specEntrySource` 需能区分该入口是唯一候选还是多候选按优先级选出,便于后续排查。 3. Android:`SPEC_ENTRY_NOT_FOUND` 与 `SPEC_ENTRY_TARGET_AMBIGUOUS` 的失败消息追加已有的标量诊断串,格式与同函数内 `SPEC_PANEL_NOT_OPENED` 的 `[...]` 既有先例一致。该串只含计数与布尔量,不含节点文本、路径、控件树或截图。 4. 补充单测:覆盖「单独购买 + 发起拼单」双按钮页面产生唯一最右入口、单按钮页面行为不变、以及评价/订单/提交订单/支付区域仍被排除的拒绝形态。 ## 非目标 - 不修改服务端、采购任务状态机、采购规则快照或规则契约默认别名。 - 不放宽 `safeBottomSpecEntries` 既有的几何约束、`buyWords` 语义要求、`nonConfigurableClickDenylist` 与评价上下文排除。 - 不把标量诊断改造为结构化字段回传服务端接口(该项如需实施另建工单)。 - 不自动重试 CG63、不创建订单、不执行支付、不使用 OCR/VLM、不保存原始控件树或截图。 ## 已知风险 「单独购买」与「发起拼单」打开的是同一个规格面板,对本工单涉及的规格探测无差异;但同一段选择逻辑同时服务真实采购路径,届时两者对应不同成交形态与价格(本商品为 ¥27.9 与 ¥17.72)。本次按用户确认采用最右优先,该业务后果由人工承担。 PDD 新版详情页普遍不再暴露独立规格行,仅能由底部按钮进入规格面板,因此本形态预计影响面较广,而非单一商品。 ## 方案与设计证据 属于恢复既有「打开安全规格面板」行为的 Android 缺陷修复,无新页面、导航或用户可见交互变化,不需要 UI 原型。设计证据为 `PddProductDetailCollector` 现有安全语义、上述真机控件树事实,以及 cmautobuy `get_size_panel_coord` 的同场景既有实现。 ## 验收 - 「单独购买 + 发起拼单」双按钮商品页能产生唯一规格入口,且命中最右侧按钮。 - 单候选页面与既有测试快照行为不变。 - 评价、评论、晒单、问答、订单、提交订单、支付区域仍不能成为候选。 - 失败消息中的诊断串不含节点文本、路径、原始控件树或截图。 - Android 单测与 `:app:assembleDebug` 通过。 - 真机重试 CG63 需人工授权后单独进行,本工单实施阶段不执行下单或支付。 ## 文档影响 需更新长期业务规则 `Business-Rules-and-Glossary` 中商品规格入口的安全边界描述,补充「底部购买入口多候选时按最右优先确定性选择」。按 Wiki-first 流程先改线上页面并在线回读 revision,再执行一次 `sync` 与一次 `sync --check`。 注意:仓库内 `docs/12-syb-erp-interface.md` 存在与本工单无关的既有改动,`sync` 的保护机制可能阻止同步(#208 已遇到同样情况)。不得重置或覆盖该无关改动,如被阻止则在本工单记录原因。 ## 依赖 无阻塞依赖。与 #208(已实施,待验收)为同一 CG63 故障的不同成因,本工单处理 #208 未覆盖的底部双按钮形态。 ## 实施分工 实施由 Codex 执行;本仓库 Gitea 无 codex 账号,指派人记为 ila。实施完成后由 Claude 按本工单验收标准审核。
ila self-assigned this 2026-09-03 16:43:55 +08:00
Author
Owner

已完成并推送,待验收。

实现

分支 feat/209-pdd-bottom-spec-entry,基于 fd013c6(#208)。实施由 Codex 执行,审核与收尾由 Claude 完成。

  • ac6a57e fix(agent): 底部规格入口最右优先 (#209)

    • safeBottomSpecEntries 的排序由「面积升序、centerX 降序」改为 compareByDescending { centerX }.thenBy { area },即最右优先、并列时可点击面积升序兜底。
    • 其消费方由 singleOrNull() 改为 firstOrNull(),多候选不再直接失败。
    • specEntrySource 区分来源:唯一候选仍为 bottom_purchase,多候选按优先级选出记为 bottom_purchase_rightmost。
    • SPEC_ENTRY_NOT_FOUND 与 SPEC_ENTRY_TARGET_AMBIGUOUS 的失败消息追加标量诊断串,写法沿用 #206 在 SPEC_PANEL_NOT_OPENED 上的既有先例。
    • 未放宽既有几何约束、buyWords 语义要求、nonConfigurableClickDenylist 与评价上下文排除。
  • 9e800ce fix(agent): 区分规格入口点击目标不唯一 (#209)

    • 审核发现的缺陷修正:点击阶段 FreshActionResult.AMBIGUOUS 分支原沿用「规格入口候选不唯一」文案,与同串证据中的 specEntryCandidates=1 自相矛盾。该处歧义来自控件树在点击时对同一目标的重复匹配,而非解析候选不唯一,故改为「规格入口点击目标不唯一」,并同步更新断言。

验证

审核阶段由 Claude 独立重跑,未复用实施阶段的构建缓存:

  • ./gradlew :app:testDebugUnitTest --rerun-tasks:260 通过,0 失败,0 错误,0 跳过。本工单新增的 4 个用例(bottomPurchaseEntriesPreferRightmostCandidate、equallyRightmostBottomPurchaseEntriesPreferSmallerArea、bottomPurchaseCandidatesStillExcludeReviewOrderAndPaymentContexts、spec entry failures include scalar diagnostics only)已确认实际执行。
  • ./gradlew :app:assembleDebug:BUILD SUCCESSFUL。
  • git diff --check、git diff HEAD~1 --check:clean。
  • python dev_scripts/harness.py check --strict:通过。
  • 提交内容核对:两个提交合计仅含 4 个 Android 文件,工作区既有无关改动(docs/12-syb-erp-interface.md、server/config/settings.yml 及 7 个未跟踪文件)未被修改、未被暂存、未被提交。

真机页面回放验证

用本工单立单时从 USB 设备 3B65BD02H7F00000 读取的 CG63 商品页控件树(1080x2376)离线回放新的选择逻辑:

候选数 = 2
  ¥27.9单独购买    bounds=(446,2166)-(685,2328)  centerX=565.5  area=38718
  ¥17.72发起拼单   bounds=(685,2166)-(1080,2328) centerX=882.5  area=63990

旧 singleOrNull()             -> null(即本工单要修复的失败)
新 firstOrNull() + centerX降序 -> ¥17.72发起拼单
specEntrySource               -> bottom_purchase_rightmost

结论:该页面形态可产生唯一规格入口,且命中最右侧按钮,与已确认决策一致。

需要注意,若只把 singleOrNull() 改为 firstOrNull() 而不调整原有排序,将命中「单独购买」而非最右;本次已同时调整排序,故结果正确。

文档

线上 Business-Rules-and-Glossary 已更新并在线回读,新 revision 276af363a24d663f78489da49c1c175493069711。新增内容:底部购买入口通过全部安全过滤后仍多候选时,按最右优先确定性选出唯一入口,记为 bottom_purchase_rightmost,唯一候选仍记为 bottom_purchase;并说明「单独购买」与「发起拼单」打开同一规格面板但对应不同成交形态与价格,最右优先属经人工确认的取舍。

python dev_scripts/harness.py sync 被保护机制阻止,本工单未导出镜像,原因是工作区存在与本工单无关的未提交改动 docs/12-syb-erp-interface.md。按仓库规则不得重置或覆盖无关改动,故未强制同步。这是继 #208 之后第二次被同一文件阻塞,docs/03-business-rules-and-glossary.md 的 wiki_revision 仍停在 9b195f3c578f980b808e6eb28f36e7bf5294e75b,已落后 #208 与本工单两个 revision。线上 Wiki 为权威源,内容正确;镜像滞后需在该无关改动处理后单独补同步。

另外 sync --check 报告 docs/02-architecture-and-code-map.md 的 wiki_revision 与内容均和线上不一致。该问题与本工单无关,为既有状态,未处理。

未执行

  • 未推送到 main,未合并。分支已推送至 origin/feat/209-pdd-bottom-spec-entry。
  • 未安装 APK 到真机,未领取或重试 CG63,未创建订单,未执行支付。
  • 真机验证需人工授权后单独进行。
已完成并推送,待验收。 ## 实现 分支 `feat/209-pdd-bottom-spec-entry`,基于 `fd013c6`(#208)。实施由 Codex 执行,审核与收尾由 Claude 完成。 - `ac6a57e fix(agent): 底部规格入口最右优先 (#209)` - `safeBottomSpecEntries` 的排序由「面积升序、centerX 降序」改为 `compareByDescending { centerX }.thenBy { area }`,即最右优先、并列时可点击面积升序兜底。 - 其消费方由 `singleOrNull()` 改为 `firstOrNull()`,多候选不再直接失败。 - `specEntrySource` 区分来源:唯一候选仍为 `bottom_purchase`,多候选按优先级选出记为 `bottom_purchase_rightmost`。 - `SPEC_ENTRY_NOT_FOUND` 与 `SPEC_ENTRY_TARGET_AMBIGUOUS` 的失败消息追加标量诊断串,写法沿用 #206 在 `SPEC_PANEL_NOT_OPENED` 上的既有先例。 - 未放宽既有几何约束、`buyWords` 语义要求、`nonConfigurableClickDenylist` 与评价上下文排除。 - `9e800ce fix(agent): 区分规格入口点击目标不唯一 (#209)` - 审核发现的缺陷修正:点击阶段 `FreshActionResult.AMBIGUOUS` 分支原沿用「规格入口候选不唯一」文案,与同串证据中的 `specEntryCandidates=1` 自相矛盾。该处歧义来自控件树在点击时对同一目标的重复匹配,而非解析候选不唯一,故改为「规格入口点击目标不唯一」,并同步更新断言。 ## 验证 审核阶段由 Claude 独立重跑,未复用实施阶段的构建缓存: - `./gradlew :app:testDebugUnitTest --rerun-tasks`:**260 通过,0 失败,0 错误,0 跳过**。本工单新增的 4 个用例(`bottomPurchaseEntriesPreferRightmostCandidate`、`equallyRightmostBottomPurchaseEntriesPreferSmallerArea`、`bottomPurchaseCandidatesStillExcludeReviewOrderAndPaymentContexts`、`spec entry failures include scalar diagnostics only`)已确认实际执行。 - `./gradlew :app:assembleDebug`:BUILD SUCCESSFUL。 - `git diff --check`、`git diff HEAD~1 --check`:clean。 - `python dev_scripts/harness.py check --strict`:通过。 - 提交内容核对:两个提交合计仅含 4 个 Android 文件,工作区既有无关改动(`docs/12-syb-erp-interface.md`、`server/config/settings.yml` 及 7 个未跟踪文件)未被修改、未被暂存、未被提交。 ## 真机页面回放验证 用本工单立单时从 USB 设备 3B65BD02H7F00000 读取的 CG63 商品页控件树(1080x2376)离线回放新的选择逻辑: ``` 候选数 = 2 ¥27.9单独购买 bounds=(446,2166)-(685,2328) centerX=565.5 area=38718 ¥17.72发起拼单 bounds=(685,2166)-(1080,2328) centerX=882.5 area=63990 旧 singleOrNull() -> null(即本工单要修复的失败) 新 firstOrNull() + centerX降序 -> ¥17.72发起拼单 specEntrySource -> bottom_purchase_rightmost ``` 结论:该页面形态可产生唯一规格入口,且命中最右侧按钮,与已确认决策一致。 需要注意,若只把 `singleOrNull()` 改为 `firstOrNull()` 而不调整原有排序,将命中「单独购买」而非最右;本次已同时调整排序,故结果正确。 ## 文档 线上 `Business-Rules-and-Glossary` 已更新并在线回读,新 revision `276af363a24d663f78489da49c1c175493069711`。新增内容:底部购买入口通过全部安全过滤后仍多候选时,按最右优先确定性选出唯一入口,记为 `bottom_purchase_rightmost`,唯一候选仍记为 `bottom_purchase`;并说明「单独购买」与「发起拼单」打开同一规格面板但对应不同成交形态与价格,最右优先属经人工确认的取舍。 `python dev_scripts/harness.py sync` **被保护机制阻止,本工单未导出镜像**,原因是工作区存在与本工单无关的未提交改动 `docs/12-syb-erp-interface.md`。按仓库规则不得重置或覆盖无关改动,故未强制同步。这是继 #208 之后第二次被同一文件阻塞,`docs/03-business-rules-and-glossary.md` 的 `wiki_revision` 仍停在 `9b195f3c578f980b808e6eb28f36e7bf5294e75b`,已落后 #208 与本工单两个 revision。线上 Wiki 为权威源,内容正确;镜像滞后需在该无关改动处理后单独补同步。 另外 `sync --check` 报告 `docs/02-architecture-and-code-map.md` 的 `wiki_revision` 与内容均和线上不一致。该问题与本工单无关,为既有状态,未处理。 ## 未执行 - 未推送到 main,未合并。分支已推送至 `origin/feat/209-pdd-bottom-spec-entry`。 - 未安装 APK 到真机,未领取或重试 CG63,未创建订单,未执行支付。 - 真机验证需人工授权后单独进行。
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OPC/goauto#209