Skip to content

ADR-198: 场景序列化异常的保存韧性

状态: 已实施(2026-07-31) 日期: 2026-07-28

背景

自动保存链路的两处致命缺陷已修复:

  • 并发写坏文件 → SaveLastScene 改原子写(temp + rename)+ sceneMu 互斥锁(commit c0393243)。
  • 退出丢最后一次 → cleanupAndFlushSave 改同步序列化直发 SaveLastScene,不再依赖 beforeunload 里不被 await 的 async Promise(commit bb4ed07c)。

审查保存机制时发现剩余的 P2 缺口:saveSceneImmediate()serializeScene() 若抛异常,整个 catchconsole.warn + toast,这次保存等于什么都没写。当多模块/多动作被高频(含 AI 并发)改动、场景处于中间态时,serializeScene() 更容易踩到 undefined/空引用而抛错,导致一次本应成功的自动保存被静默吞掉。

与前两处不同,这不是写坏已有存档(原子写已保证 last_scene.json 永远是完整的旧版或新版),而是新状态未能落盘——上一份存档仍在,下次启动会恢复到较旧的状态,表现为"部分改动丢失"。

决策

已实施方向①+②(2026-07-31):

  • 方向①分段容错serializeScene() 内把单个模型条目的组装抽为 serializeModel(inst),主循环逐个模型 try/catch——单条抛错时跳过该模型并 logWarn('scene:serialize', ...) 记录(模型名 + id),其余模型仍进入 models 数组。做到“能存多少存多少”,而非全有或全无。
  • 方向②失败可观测saveSceneImmediateserializeScene() 单独包一层 try/catch,序列化整体失败时上报 scene:serialize 诊断并 returnfinally 里的 trailing save 仍执行);到达外层 catch 即写盘(SaveLastScene)失败,日志文案区分为 FAILED (write)。两类失败不再混为一谈。
  • 方向③保底快照(回退到 undo 栈最近一次成功序列化结果)暂不实施——方向①已把“单条崩溃拖垮整次保存”的主因消除,方向③收益边际递减,如后续出现顶层字段序列化崩溃的实例再评估。

原始决策(2026-07-28,已被上文取代)

暂不修复,记录为已知技术债,触发概率低(需序列化过程本身崩溃,正常操作路径不会触发)。原子写已兜住“存档不被写坏”的底线,最坏后果仅是回退到上一份完整存档,不会出现无法解析的损坏文件。

备选方案

  • 立即修复分段容错:收益确实,但当前无复现用例,属于防御性编码;优先级低于正在推进的功能,暂缓。
  • 序列化失败时静默保留旧档(现状):即本 ADR 的决策,成本为零,最坏结果可接受(回退到完整旧档)。

影响

  • 涉及文件:frontend/src/scene/scene-serialize.tsserializeScene / 新增 serializeModel / saveSceneImmediate)。
  • 单元测试:frontend/src/__tests__/scene/scene-serialize-resilience.test.ts(模拟单个模型序列化抛错,验证其余模型仍落盘 + 空场景 + 全正常三例)。

相关文档

  • ADR-002:writeConfig 轻写 vs writeConfigAndRescan 全量(配置持久化拆分)
  • commit c0393243:SaveLastScene 原子写 + 互斥锁
  • commit bb4ed07c:退出刷盘改同步序列化直发
  • 知识卡:SaveLastScene 非原子写导致并发状态丢失(common_pitfalls)