针对一台中继盒上某根探针的实时温度页面:屏幕中央巨大的数字显示当前温度,下方面板在未开始烹饪时显示本地化的 cookingStart 文案(英文资源为 START);开始后该位置改为计时器磁贴(显示已用时),点击该计时器磁贴才会取消当前会话,界面上不显示 STOP 文案。;连接丢失时下方面板会置灰禁用;主显并非一律显示最后已知温度:isConnectionLost 且未落入 docked / disconnected 分支时会进入 _ConnectionLostDisplay 显示灰阶最后读数;其中 isReconnecting=true 显示 Reconnecting…,否则显示 Connection lost。;recentlyLost(90 秒 grace 窗口)在 UI 上仍按在线态处理,不显示该断连组件;docked 或真正断连时显示红字提示。
补充:_pushTargetToDevice() 在编码目标温度时,华氏线值使用 truncate() 截断小数,摄氏线值使用 round() 四舍五入(TAPD #1003186,per Liang 2026-04-28:与 formatTempF 的 °F 显示去尾规则对齐,避免中继盒物理屏读数与 App 显示不一致)。因此像 139.5°F 这样的目标会以 139°F 下发,而不是 140°F。
lib/features/thermometer/presentation/pages/cooking_page.dart(1894 行)boosterProvider.family、probeByNumberProvider、cookingSessionsProvider、pendingCookParamsProvider.family、alarmServiceProvider、earlyWarnLevelProvider、temperatureUnitProvider(页面上的连接状态已从全局 connectionStateProvider 废弃,改为由 boosterProvider 的 isBleConnected / deviceStatus 派生,并参考 connectedBoostersProvider.cloudLiveDeviceIds 与 deviceConnectionManagerProvider.cloudActiveDeviceIds 判断云端可达性)BleDeviceService.setProbeTarget(transport-split 后的统一入口;legacy 走 LegacyBoosterTransport.setProbeTarget 写 0x55 [color] [unit] [temp] [MAC],CM4 走 Cm4BoosterTransport.setProbeTarget 写 ASCII SET_<color>=ABCDEFG(SET_BL/WH/BU/YE;末位 G 为 work-state,常见值包括 START 用的 G=1 与 KEEP 用的 G=2),详见 CM4 协议 §探针配置)(deviceId, probeNumber)(路由 arguments)/cooking(CookingPage.routeName)lib/app.dart 的 onGenerateRoute(line 218),需要 (String deviceId, ProbeNumber probeNumber) arguments补充:顶部两个 glass badge 不只显示百分比。当前这两个 badge 的标签文案在代码里仍是硬编码英文 Probe / Booster,没有走 ARB 本地化。连接正常时,Booster 充电会显示绿色 battery_charging_full 图标,且百分比文字也为绿色;非充电时若电量 <=20% 会显示 battery_alert 图标,其中 <=10% 为红色、11–20% 为橙色;断连时百分比降为次级灰色。
补充:当该 (deviceId, probeNumber) 没有 active session,且 pendingCookParamsProvider 里也没有已缓存参数时,本页类默认值是目标温度 122°F、肉种文案 Protein、_earlyWarnLevel = 1(即默认开启第一档提前预警,5°C/9°F)。随后 _restoreCookState() 才会用 active session 或 pending 参数覆盖这些初值。
顶部 _TopBar:返回箭头 + 居中设备名 + 两个 glass badge(探针电量 / 中继盒电量),断连时多一行 _ConnectionLostBanner。低电量 inline banner 已删(99cf247 / TAPD #1003093,Liang 2026-05-28)——原 _LowBatteryBanner(≤20% 红/橙 banner)+ 底部 _maybeWarnLowBattery snackbar(20/10/5/3% 阈值)+ _lastWarnedBatteryThreshold 状态机一并退役,与 告警与通知 改造同行:probe lowBattery 现复用共享的居中卡片 alarm popup(与所有 non-safety 报警一致),at-a-glance 留持久电量百分比 chip(顶部 glass badge)。中央占据大半屏的是巨大温度数字(76pt 粗体白);超下界显示蓝色 "LO",超上界显示橙色 "HI",BLE 链路真断时(displayState ∈ {docked, disconnected},per spec 2026-04-29 review extension:覆盖入仓 0x55AA、入仓后中继盒自动关机、电池耗尽、用户主动关机、BLE 断 + 90 秒宽限期满五种"手机已不再与中继盒通讯"的情况)切换到红字 "Booster box not connected to smartphone";宽限期内(recentlyLost)与用户手动重连中(reconnecting)保留 _ConnectionLostDisplay 显示 "last known" 灰阶温度(per Liang 早期 grace-UX 指令),无探针数据时红字 "Probe not reaching booster / Move the booster closer to the probe"(per spec 2026-04-30:可操作建议替换原描述性 "Probe not connected to booster box")。底部 _BottomPanel 是 6 格胶囊磁贴(分两行):目标温度(display-only,不可点,per TAPD 2026-05-19 / c31ebee——之前点 Target 会弹 QuickTargetSheet,现已撤销,改目标走 Adjust / Meat Presets 的 canonical 入口)、环境温度(Ambient)、计时器(点开 START/STOP)、温度图(点开 push 子页)、Adjust(推到预设页)、Meat(推到肉类预设)。底部面板在断连时整体置灰且不可点(AnimatedOpacity + IgnorePointer)。
_TopBar 的 IconButton.onPressed → Navigator.pop(context)Navigator.pop(context),并未在代码中硬编码为 Dashboard)Per TAPD 2026-05-19 / c31ebee:目标温度磁贴改为display-only——onTargetTap 传 null,_GlassTile 不挂手势 handler、无 ripple、无任何 tap 动作。原 _showTargetSheet helper 与 quick_target_sheet.dart import 一并从 cooking_page.dart 删掉;_BottomPanel.onTargetTap 字段类型放宽到 VoidCallback? 让 null 通过类型检查(其它字段签名不变)。改目标的 canonical 路径是下面 Action 5 (Adjust 磁贴) 与 Action 6 (Meat 磁贴)——Liang 想要的是 Target tile 只渲读数,bottom-sheet quick-picker 在测试中让用户误以为 read-out 自身可编辑、属反向 UX。QuickTargetSheet widget 自身仍存在,当前用于 meat_presets_page.dart 的自定义槽位编辑流程 _openCustomPicker,不是 protein preset tile 点击路径。
补充:启动新会话时,页面会调用 _pushTargetToDevice(workState: Cm4WorkState.start);对 CM4 而言,这一类 START 下发带 G=1 语义,用来启动中继盒的权威烹饪计时。另外,START 发送是 _lastPushedConfig 去重逻辑的例外:即使本次参数签名与上一次 Save 完全相同,也不会被 SET_TARGET_DEDUP 跳过,必须再次下发,才能真正启动中继盒侧的权威计时。相对地,Save 或中途 Adjust 触发的 _pushTargetToDevice() 不传 workState,注释明确说明 transport 会发送 KEEP(G=2),只更新参数,不改变烹饪状态。
无论是启动新会话还是结束当前会话,代码都会对该 deviceId 调用 alarmServiceProvider.reset(...)。这会清掉上一轮 cook 残留的 targetReached / dismissed 等 alarm 状态,避免旧告警跨会话遗留到新的一轮烹饪,或在会话结束后继续悬挂。
补充:会话结束后(包括用户取消,以及其他会话完成路径),代码会把刚结束会话的 targetTemp、presetName/meatType、earlyWarnLevel 与 isCustom 回写到 pendingCookParamsProvider((deviceId, pn))。因此用户下次再进入同一探针的烹饪页时,会默认恢复上一次使用的参数,而不是退回页面类默认值。
_BottomPanel.onTimerTap 或 _StartCookingTile.onTap → _toggleSessiononTap → _toggleSession
→ notifier.activeSession(deviceId, pn) 取活跃会话
→ 有活跃:DevLogService.logUserAction('SESSION_STOP') → _finishSession(cancelled)
→ notifier.endSession(deviceId, pn, cancelled)
→ cookLogProvider.addSession(finalized.copyWith(isActive: false, endTime, status))
→ 无活跃:DevLogService.logUserAction('SESSION_START') → notifier.startSession(...)
→ _clearPendingParams() (session 内已存)
→ _pushTargetToDevice(workState: Cm4WorkState.start)
CookingSession(isActive=true) 加入 cookingSessionsProvider;pendingCookParams 清空;目标温度推到设备endTime 设为现在,状态为 cancelled,随后通过 archiveCookLogOnFinalize(finalized, 'user_stop') 归档。已拥有的会话直接写入;镜像会话在同步后找不到发起端记录时仍会回填,已有发起端记录时由身份去重保留其所有权。补充:当前图表的 Y 轴不是按当前数据自适应缩放,而是固定为 0–120°C 的常量量程(代码内部以对应的华氏常量绘制,因为采样点以 °F 存储)。因此 target 线、early-warning 节点以及曲线高度都共享一套固定纵轴,和 cook log 详情图保持一致。
补充:图表页不只画温度曲线和目标线。若 early warning 已开启,还会额外显示一个 amber 图例项,并将 early-warning 温度作为 Y 轴节点绘出;同时 _TempGraphPainter 会在温度曲线首次达到或越过 early-warning 与 target 的采样点位置分别绘制橙色/绿色时间标记。
补充:X 轴时间刻度不是固定间隔,而是自适应 2–5 个刻度(_TempGraphPainter.paint 内 layoutTicks):从最多刻度数开始逐档尝试,只要相邻标签宽度会重叠就退一档,直到刻度不再挤在一起;两端点(0s 与总时长)永远保留,只丢中间刻度。TAPD #1003116(807aa20)修的是短时长图只画得出 "0s" 一个标签的问题(原固定 10s 最小间隔方案);TAPD #1003179(f332af7)修的是长烹饪时(如 "298m27s" 这类宽标签)右侧刻度互相挤压重叠的问题。
_BottomPanel.onGraphTap → _openGraph_TemperatureGraphPage(同一文件内的私有 widget,不是路由),显示历史温度曲线 + 目标线GestureDetector.onTap → navigateInstant(context, _TemperatureGraphPage(tempHistory, targetTempF, deviceId, probeNumber))
IconButton.onPressed → Navigator.pop),其他都是被动渲染——注意不是 fl_chart:_TempGraphPainter 是本文件内手写的 CustomPainter(Canvas 直绘坐标轴/网格/曲线/标记),cooking_page.dart 完全没有 import fl_chartprobeByNumberProvider 让新温度点实时加入l.graphTitle、图例 l.graphLegendProbe / l.graphLegendTarget(temp)、空图提示 l.graphWaitingForTelemetry(无点)/ l.graphCollectingData(仅 1 点)。_TempGraphPainter 在 paint 时拿不到 BuildContext,所以两条 hint 由 _TemperatureGraphPage.build 在外层取本地化字符串后通过构造器 waitingHint / collectingHint 参数 plumb 进 painter。 ⚠️ dcf2699(TAPD #1003115)后只剩 4 条:图表改为单样本直接画 dot(points.length == 1 分支),l.graphCollectingData + _TempGraphPainter.collectingHint 参数一并从 6 语 ARB 与 painter 拆除,仅在 points.isEmpty 时画 l.graphWaitingForTelemetry。同次接 _TemperatureGraphPage.build 改为:active session 在时优先用 cookSessionRecorderProvider 写入的 session.history 渲染整条曲线(page-independent recorder,详见 §状态变化时如何更新)、pre-cook fallback 到本页 _tempHistory、再追加 probe.internalTemp 一个 now 点;本页 _recordTemperature 加 5 s heartbeat(值未变 + now - last ≥ 5s 时也补一点),稳态探针不再卡在「Collecting data…」34 分钟(实测 log)。⚠️ 1c26e22 / TAPD #1003126 改 idle fallback 链:无 active session 时不再立即落到 _tempHistory,而是先看新增的 lastCookSessionProvider.family((deviceId, ProbeNumber))——newest-first 扫 cookLogProvider 找该探针最近一条已结束 cook(任何 status,包括 auto-stop-on-target-reached / cancelled / disconnected),有则冻结渲染那条 session.history 整曲线;只有该探针从未烹饪过才回退到本页 _tempHistory。Target / early-warn 用 historical 那条 cook 自己保存的 targetTemp / earlyWarnLevel,legend 与 painter 共用(与显示的曲线对齐),冻结历史时不追加 live now 点(避免拖一条尾巴到当前 idle 温让人误以为 cook 还在跑)。Cook log 跨 auto-finalize / 退页 / App 重启都持久化,所以这条 frozen 曲线一直在屏上直到用户按 START——按 Start 时 active 翻非空、新 cook 的 session.history 立刻接管(初始为空),同时 _toggleSession 启动分支主动 _tempHistory.clear() + _lastRecordedTemp = -1,防止旧 preview 点在中央 recorder 写第一个样本之前的短窗里 flash 出来。时间轴 tick 标签(30s / 2m05s)SI 符号未本地化,Liang 没标,留作 follow-up(要做时按同样的构造器 pattern 走)。补充:当前 Adjust 返回到烹饪页时,会先无条件执行 _syncEarlyWarnFromGlobal(),再判断用户是否真的保存了 PresetPage 结果。这意味着如果用户在 Adjust 内嵌的 Goal Setting grid 里只改了 early-warning chip、随后直接返回而没有点 Save,该 level 仍会被本页拉回,并补写到 pendingCookParamsProvider((deviceId, probeNumber)) 与当前 active session,避免后续 _savePendingParams() 用旧快照把它覆盖掉。
当前实现还会把自定义槽位标记一并带入/带回:进入 PresetPage 时传 initialIsCustom: _isCustomPreset,返回结果后再读取 result['isCustom'] 更新本页 _isCustomPreset。这一步很重要,因为后续 _resolveMeatType() 需要依赖该标记把自定义槽位稳定映射为 MeatType.custom,避免用户把自定义槽位重命名成 Beef 之类时被误当成固件内置预设。
_BottomPanel.onAdjustTap → _navigateToAdjustnavigateInstant<Map<String, dynamic>>(PresetPage(initialTargetTempF, initialMeat))
→ 返回结果 Map:{ targetTempF, meatType }
→ setState 更新 _targetTempF / _meatType
→ _savePendingParams + _syncTargetToSession + alarmServiceProvider.reset
补充:肉类预设网格会按中继盒家族变化:CM4 显示 8 种固件已知肉类和 1 个 Custom 槽位;Legacy 显示 12 种肉类和 4 个 Custom 槽位。
_BottomPanel.onMeatTap → _navigateToMeatPresetscooking_page._navigateToMeatPresets 把 widget.deviceId + widget.probeNumber 透到 MeatPresetsPage / PresetPage,chip 在 grid 内 tap 即时 commit;回 cooking 页时调 _syncEarlyWarnFromGlobal 从 earlyWarnLevelProvider 拉回本页 _earlyWarnLevel、再为命名路由 caller 补写 per-probe stores,避免后续 _savePendingParams 用 stale 快照覆盖已 commit 值。_navigateToMeatPresets → await navigateInstant(
MeatPresetsPage(family, deviceId, probeNumber))
→ _syncEarlyWarnFromGlobal() // 兜底回拉 chip tap 已 commit 的 level
→ pick: {meatType, targetTempF, isCustom} // earlyWarnLevel key 已在 8513d11 移除
→ await navigateInstant(
PresetPage(initialMeat, initialTargetTempF, family, deviceId, probeNumber))
→ _syncEarlyWarnFromGlobal() // 嵌套 Presets&Goals 内 chip 改值也要兜底
→ result: {targetTempF, meatType?, isCustom?}
→ setState 更新 _targetTempF / _meatType / _isCustomPreset
→ _savePendingParams + _syncTargetToSession + alarmServiceProvider.reset
earlyWarnLevelProvider 本地 seed 状态变更 + active session 的 earlyWarnLevel 落盘(per Liang TAPD #1003091 / 33b4e62 — alarm evaluator 自此读 per-session 值而非全局 provider,避免并发 (device, probe) 会话之间阈值串扰;下方 2026-05-09 spec note 末段"同步到 earlyWarnLevelProvider 让 evaluator 立即可用"在 33b4e62 后仅作 UI seed 同步,evaluator 不再消费)。⚠️ 8513d11 / TAPD #1003234 起 chip 落盘改在 grid 内 _setEarlyWarnLevel 完成(每次 tap 写 global provider + 有 context 时同时写 per-probe pending + 当前 session),cooking 页本侧只通过 _syncEarlyWarnFromGlobal 把本页 _earlyWarnLevel 与命名路由 caller 缺的 per-probe stores 拉齐——保证后续 _savePendingParams 不会用 stale 快照覆盖。Per Liang 2026-05-09(TAPD):earlyWarnLevel 必须持久化到 CookingSession 自身。之前 5°/10° 早期警告偏移有三处内存副本(page-local _earlyWarnLevel、global earlyWarnLevelProvider、pendingCookParamsProvider)但都不跨进程重启,会话对象本身又没带这个字段,所以中继盒断连 ~10 分钟后 Android 后台 OOM kill 掉进程、用户回到烹饪页时 earlyWarnLevelProvider 重置为 0、_restoreCookState 走 active-session 分支早返不恢复,alarm evaluator 读到 0 后剩余烹饪时间永久不再触发 earlyWarning1/2,肉类预设页 toggle 也显示为 off。修复:CookingSession 增 earlyWarnLevel 字段(fromJson 缺键 fallback 0 兼容旧持久化会话),_toggleSession 启动会话时把当前 _earlyWarnLevel 写入 session,肉类预设页返回新 level 时由 _navigateToMeatPresets 调 cookingSessionsProvider.notifier.updateEarlyWarnLevel(deviceId, probeNumber, level) 落盘,_restoreCookState active-session 分支早返之前先把 session 上的 level 写回 _earlyWarnLevel 并同步到 earlyWarnLevelProvider 让 evaluator 立即可用。
补充:当前断连相关 UI 文案也有多处硬编码英文,未走 ARB 本地化。_ConnectionLostDisplay 里的 Last reading、Reconnecting…、Connection lost,以及 _ConnectionLostBanner 按钮文案 Back to scan 都直接写在 cooking_page.dart 中。另外,顶部 _ConnectionLostBanner 的状态文本本身也同样是硬编码英文:重连态显示 Reconnecting…,非重连态显示 Connection lost,并非只有按钮文案未接 ARB。
_ConnectionLostBanner.onBackToScan → Navigator.pop(context)Navigator.pop(context),并未在代码中硬编码为 Dashboard)补充:当存在 active session 且 isConnectionLost 变为 true 时,页面会记录 _pausedAt 与 _elapsedBeforePause,计时器磁贴随后冻结在断连瞬间的已用时,并在文案末尾追加 ⏸;重连后 _pausedAt 被清空,计时器恢复按当前时间差继续显示。
除 watch 和 target 的 ref.listen 外,本页当前还有 3 个重要监听未在文档中说明:
pendingCookParamsProvider(...).meatType,当中继盒侧肉类选择经同步写回 provider 后,页面本地 _meatType 会自动跟随更新。boosterProvider(deviceId).select((b) => b.isBleConnected),一旦 BLE 真断开就把 _lastPushedConfig 清空,保证后续重连会强制重发目标配置。(deviceId, probeNumber) 是否仍有 active session;当会话从 active 变为 inactive 时同样清空 _lastPushedConfig,避免下一次同配置重新开始烹饪时被 dedup 误跳过。_buildContent 通过 ref.watch 订阅多个 Provider:temperatureUnitProvider、boosterProvider(deviceId)、cookingSessionsProvider、probeByNumberProvider((deviceId, pn))、probeSensorFlagsProvider((deviceId, pn))(连接状态已改为从 boosterProvider 的 isBleConnected / deviceStatus 派生,不再 watch 全局 connectionStateProvider) —— 任一变化触发整页重建。
ref.listen 在 :377-386 监听 pendingCookParamsProvider.targetTempF:当中继盒侧推送的目标温度(例:用户按了中继盒物理按钮、或 12B 的 preset alarm 与 App 不一致触发的 reconciliation——TAPD #5;reconciler 通过 BleDeviceService.onProbeTargetReconciled 回调写入此 provider)变化超过 0.5°F 时,_targetTempF 同步更新。0.5°F tolerance 防止 echo App 自己刚写的值。
每次新温度数据通过 _recordTemperature 只入栈本地 _tempHistory(cap 300 点,仅服务本页的实时迷你图表)。会话 history 不再由本页写入——b58946 / TAPD #1003099 起搬到独立的 page-independent cookSessionRecorderProvider(device_providers.dart 内、app.dart watch 保活),从 connectedBoostersProvider 监听遥测、对每个 active session 调 CookingSessionsNotifier.recordSample(...) 落盘,hybrid throttle(≤1 sample / 5 s、≥0.5 °F 或 30 s heartbeat、cap 500 点、超出按 2× decimation 保全 span 而非掐头),同时把 ambient temp 真实写入(之前 cooking-page-coupled addReading hardcoded 0)。这样多探针场景下用户没打开过的探针(CM2 White 等)也能录到完整曲线、烹饪日志详情页画线不缺段。
_timerTick 是一个 ValueNotifier<int>,在 initState 中由 Timer.periodic 每秒递增其 value,驱动计时器通过 ValueListenableBuilder 局部重建显示,而不触发整页 setState。
存在明确平台差异:_adaptiveBlur() 在 Android 直接走较高透明度纯色填充;所有非 Android 平台(包括 iOS 与 Web)都保留 BackdropFilter 模糊。;另外旧的 in-page 低电量 banner/snackbar 路径已删除,低电量改由共享 alarm popup + 顶部电量 badge 呈现。
cooking_page.dart 1894 行:建议拆 _BottomPanel / _TopBar / _TemperatureGraphPage / _GlassTile 等到独立 widget 文件,与 仓库结构 的重构候选一致。c072acf) 接通调用点(_pushTargetToDevice CM4 分支调 Cm4BoosterTransport.setProbeTarget → CM4Command.configureProbe);a3c9cf7 + Beta 2026-05-11 spec 收口编码——CD 槽是大写十六进制 00–FF(鸡 165°F → A5),守卫放宽到 > 0xFF;6e5dfb7 反向恢复 SET_BL/WH/BU/YE 色名前缀(cloud doc 2026-05-19 权威 + Liang 2026-05-16 HW 实测),详见 CM4 协议 §Q1 / §SET 前缀历史。_tempHistory 与 session history 双轨:本地数组(cap 300)+ session.history 都在记录,可能产生不一致 🔴 需要确认(语义是否有意——本地图表 vs 持久化历史)cookSessionRecorderProvider 写入(page-independent,详见 状态变化时如何更新),本地 _tempHistory(cap 300)仅服务本页的实时迷你图表、与持久化历史故意分离:本地图表追求 1 Hz 全粒度可视、session history 走 hybrid throttle 节省存储(≤1 sample / 5 s + ≥0.5 °F / 30 s heartbeat + cap 500 / 2× decimation)。两者数据点密度本就不同,不构成「不一致」bug。:263-266 硬编码英文 'Probe battery at 3% — replace now' 等);其他文案都通过 l.cookingXXX。_maybeWarnLowBattery + _LowBatteryBanner + _lastWarnedBatteryThreshold 状态机,与冗余的 in-page banner / bottom snackbar 一并退役;probe lowBattery 改走共享居中卡片 alarm popup(详见 告警与通知 §报警类型与优先级 表 lowBattery 行),at-a-glance 留持久电量 chip。i18n 问题随源头消失、不再需要单独处理。_pushTargetToDevice byte-identical dedup:本页持 _lastPushedConfig: String? 签名('<pn>|<tempByte>|<useCelsius>|<cm4WireE>|<cm4WireF>'),同一 config 重复 push 时记 SET_TARGET_DEDUP core log 后早返、跳过 transport 层发线、避免 Save→Start 时中继盒物理屏多 flash 一次。Build 里 ref.listen(boosterProvider(deviceId).select(isBleConnected)) 在 false 时清 signature——真断连(已过 90 s grace 由 isBleConnected 落定)后下次 reconnect 强制重 push。App 侧 alarm evaluator 独立于此(看 cachedTargets / session.target,不读 _lastPushedConfig),skip 不抑制任何 alarm。⚠️ 28c930e / TAPD #1003124 追加 cook-end 清 signature 路径:再加一个 ref.listen<bool>(cookingSessionsProvider.select((all) => all.any((s) => s.deviceId == deviceId && s.probeNumber == pn && s.isActive))),当 active 从 true 翻 false(用户取消 / session-complete 状态机收尾——62621923 / TAPD #1003117 Layer B 起 target-reached 不再立即 finalize、改 latch 节点 + arm trailing 录制窗,active 翻 false 由 4-条件状态机推进至 session-complete 时才发生,详见 状态模型 §烹饪会话)时把 _lastPushedConfig = null。原因:完成的 cook 触发中继盒报警后固件自身 disarm,下次同屏 Start 即使 config byte-identical 也必须重发才能 re-arm 中继盒;之前 dedup 直接 skip,仅 App 侧响、中继盒静默——TAPD #1003124 报告的"同探针二次烹饪中继盒不响"症状。Save → 首次 Start 路径不受 session-end 清签名逻辑影响,但 Start 使用 Cm4WorkState.start(G=1)时绝不会被 dedup,必须重新下发以启动中继盒侧权威计时。