Appearance
ADR-198: 场景序列化异常的保存韧性
状态: 已实施(2026-07-31) 日期: 2026-07-28
背景
自动保存链路的两处致命缺陷已修复:
- 并发写坏文件 →
SaveLastScene改原子写(temp + rename)+sceneMu互斥锁(commitc0393243)。 - 退出丢最后一次 →
cleanupAndFlushSave改同步序列化直发SaveLastScene,不再依赖beforeunload里不被 await 的 async Promise(commitbb4ed07c)。
审查保存机制时发现剩余的 P2 缺口:saveSceneImmediate() 内 serializeScene() 若抛异常,整个 catch 只 console.warn + toast,这次保存等于什么都没写。当多模块/多动作被高频(含 AI 并发)改动、场景处于中间态时,serializeScene() 更容易踩到 undefined/空引用而抛错,导致一次本应成功的自动保存被静默吞掉。
与前两处不同,这不是写坏已有存档(原子写已保证 last_scene.json 永远是完整的旧版或新版),而是新状态未能落盘——上一份存档仍在,下次启动会恢复到较旧的状态,表现为"部分改动丢失"。
决策
已实施方向①+②(2026-07-31):
- 方向①分段容错:
serializeScene()内把单个模型条目的组装抽为serializeModel(inst),主循环逐个模型try/catch——单条抛错时跳过该模型并logWarn('scene:serialize', ...)记录(模型名 + id),其余模型仍进入models数组。做到“能存多少存多少”,而非全有或全无。 - 方向②失败可观测:
saveSceneImmediate把serializeScene()单独包一层try/catch,序列化整体失败时上报scene:serialize诊断并return(finally里的 trailing save 仍执行);到达外层 catch 即写盘(SaveLastScene)失败,日志文案区分为FAILED (write)。两类失败不再混为一谈。 - 方向③保底快照(回退到 undo 栈最近一次成功序列化结果)暂不实施——方向①已把“单条崩溃拖垮整次保存”的主因消除,方向③收益边际递减,如后续出现顶层字段序列化崩溃的实例再评估。
原始决策(2026-07-28,已被上文取代)
暂不修复,记录为已知技术债,触发概率低(需序列化过程本身崩溃,正常操作路径不会触发)。原子写已兜住“存档不被写坏”的底线,最坏后果仅是回退到上一份完整存档,不会出现无法解析的损坏文件。
备选方案
- 立即修复分段容错:收益确实,但当前无复现用例,属于防御性编码;优先级低于正在推进的功能,暂缓。
- 序列化失败时静默保留旧档(现状):即本 ADR 的决策,成本为零,最坏结果可接受(回退到完整旧档)。
影响
- 涉及文件:
frontend/src/scene/scene-serialize.ts(serializeScene/ 新增serializeModel/saveSceneImmediate)。 - 单元测试:
frontend/src/__tests__/scene/scene-serialize-resilience.test.ts(模拟单个模型序列化抛错,验证其余模型仍落盘 + 空场景 + 全正常三例)。
相关文档
- ADR-002:writeConfig 轻写 vs writeConfigAndRescan 全量(配置持久化拆分)
- commit
c0393243:SaveLastScene 原子写 + 互斥锁 - commit
bb4ed07c:退出刷盘改同步序列化直发 - 知识卡:
SaveLastScene 非原子写导致并发状态丢失(common_pitfalls)