把 AI 对话界面改成结构化录入界面,视觉上换掉聊天气泡只要改样式。但 HealthLoop 的实践证明:每次局部修复之后,下一种用户输入又会把聊天气泡带回来。这不是偶然,是重构姿势的必然结果。
现象
今日页视觉已经改成极简手帐:没有气泡、没有对话流,输入后应该出现的是字段补全弹层和候选确认。但实际运行时,用户消息、系统回复、重试卡、连续追问一个不少地出现了。修掉一种,另一种输入又把它带回来——像打地鼠。
排查:三层还在共同依赖旧模型
检查页面模板、状态和自动化测试,发现三层仍共同依赖 TodaySession.messages(消息数组):
- 页面直接循环渲染消息列表;
- 追问逻辑把输入框当作聊天回复框;
- 自动化测试明确要求
.conversation与.chat-bubble选择器必须存在。
之前的改动只替换了样式,没有改变交互模型。更讽刺的是第 3 点:测试反而在保护旧实现——你删掉气泡,测试红给你看,于是"正确地"把气泡加回来。
根因
产品目标已经从"AI 对话"变为"结构化辅助回填",但页面状态机和测试契约没有一起迁移。试图把每一种自然语言输入用局部判断堵住,只会形成越来越长的条件分支——而用户表达的开放性是条件分支覆盖不完的。
修法:状态机、数据、组件、测试一起迁移
- 定义统一录入状态机:识别 → 单字段补全 → 候选确认 → 保存 → 失败,五种状态,不再依赖"下一句话是什么";
- 保留旧消息结构作为兼容数据载体,通过纯适配器归约成唯一弹层视图,页面不再渲染消息列表——历史数据不用迁移,新交互只认弹层;
- 把补充选项、其他输入、取消、重试、二次确认收进独立弹层组件;
- 修改自动化测试:明确禁止聊天气泡回归(断言选择器不得出现),并覆盖弹层状态与操作绑定;
- 顺手把导航果冻位移动效从 260ms 收紧到项目约束的 220ms。
验证:typecheck 与全量 check 通过,今日会话、补全防重复、弹层状态、账号隔离等回归全绿。
姊妹坑:回归脚本把视觉规格也钉死了
同一轮暗色重构还踩了一个相邻的坑:tab bar 从悬浮胶囊改成标准底栏后,npm run check 挂了——检查脚本里有一条 assert.match(tabbarWxss, /height:\s*112rpx/)。视觉规格被写进了回归脚本。
处理原则:回归脚本应该锁"行为与约束"(热区最小高度、安全区适配),不该锁具体视觉值。断言改成校验 calc(100rpx + env(safe-area-inset-bottom)) 与 88rpx 热区。
教训
- 界面重构不能只换 DOM 类名和颜色。 产品交互模型变化时,状态机、持久化适配、组件边界、测试契约四者必须一起迁移,否则旧行为会被测试"正确地"恢复。
- 测试断言分两类:"产品不变量"(永远该锁)和"当前实现快照"(每换一次设计就要人工同步一次)。写断言之前先想清楚它守护的到底是什么。
- 打地鼠式修复的信号:同类问题反复以新形式出现。出现这个信号时,停下手里的局部修补,回去找共享的底层模型。