Prechádzať zdrojové kódy

fix: merge chapter status updates under state lock

lingfengQAQ 3 týždňov pred
rodič
commit
7089d40004

+ 17 - 1
webnovel-writer/scripts/data_modules/state_manager.py

@@ -144,6 +144,7 @@ class StateManager:
         self._pending_disambiguation_pending: List[Dict[str, Any]] = []
         self._pending_progress_chapter: Optional[int] = None
         self._pending_progress_words_delta: int = 0
+        self._pending_chapter_status: Dict[str, str] = {}
         self._pending_chapter_meta: Dict[str, Any] = {}
 
         # v5.1 引入: 缓存待同步到 SQLite 的数据
@@ -246,6 +247,7 @@ class StateManager:
                 self._pending_chapter_meta,
                 self._pending_progress_chapter is not None,
                 self._pending_progress_words_delta != 0,
+                self._pending_chapter_status,
             ]
         )
         if not has_pending:
@@ -283,6 +285,18 @@ class StateManager:
 
                     progress["last_updated"] = self._now_progress_timestamp()
 
+                if self._pending_chapter_status:
+                    progress = disk_state.get("progress", {})
+                    if not isinstance(progress, dict):
+                        progress = {}
+                        disk_state["progress"] = progress
+                    chapter_status = progress.get("chapter_status")
+                    if not isinstance(chapter_status, dict):
+                        chapter_status = {}
+                        progress["chapter_status"] = chapter_status
+                    chapter_status.update(self._pending_chapter_status)
+                    progress["last_updated"] = self._now_progress_timestamp()
+
                 # v5.1 引入: 强制使用 SQLite 模式,移除大数据字段
                 # 确保 state.json 中不存在这些膨胀字段
                 for field in ["entities_v3", "alias_index", "state_changes", "structured_relationships"]:
@@ -373,6 +387,7 @@ class StateManager:
                 self._pending_chapter_meta.clear()
                 self._pending_progress_chapter = None
                 self._pending_progress_words_delta = 0
+                self._pending_chapter_status.clear()
 
                 # SQLite 侧 pending:成功后清空,失败则恢复快照(避免静默丢数据)
                 if sqlite_sync_ok:
@@ -660,7 +675,8 @@ class StateManager:
         progress = self._state.setdefault("progress", {})
         chapter_status = progress.setdefault("chapter_status", {})
         chapter_status[str(chapter)] = status
-        self._save_state()
+        self._pending_chapter_status[str(chapter)] = status
+        self.save_state()
 
     def _save_state(self) -> None:
         """直接持久化当前内存状态到 state.json(轻量写入,不走 pending 合并)。"""

+ 16 - 0
webnovel-writer/scripts/data_modules/tests/test_chapter_status.py

@@ -68,6 +68,22 @@ def test_set_chapter_status_idempotent(state_project):
     assert sm.get_chapter_status(5) == "chapter_drafted"
 
 
+def test_set_chapter_status_persists_without_direct_save(state_project, monkeypatch):
+    sm = _make_manager(state_project)
+    sm._load_state()
+
+    def boom():
+        raise AssertionError("_save_state should not be called")
+
+    monkeypatch.setattr(sm, "_save_state", boom)
+
+    sm.set_chapter_status(5, "chapter_drafted")
+
+    saved = json.loads((state_project / ".webnovel" / "state.json").read_text(encoding="utf-8"))
+    assert saved["progress"]["chapter_status"]["5"] == "chapter_drafted"
+    assert sm.get_chapter_status(5) == "chapter_drafted"
+
+
 def test_set_chapter_status_invalid(state_project):
     sm = _make_manager(state_project)
     sm._load_state()

+ 29 - 0
webnovel-writer/scripts/data_modules/tests/test_state_manager_extra.py

@@ -396,6 +396,35 @@ def test_save_state_progress_and_disambiguation_merge(temp_project):
     assert len(saved["disambiguation_pending"]) == 1
 
 
+def test_set_chapter_status_preserves_existing_disk_state(temp_project):
+    temp_project.state_file.write_text(
+        json.dumps({"progress": {"current_chapter": 4}}, ensure_ascii=False),
+        encoding="utf-8",
+    )
+
+    manager = StateManager(temp_project, enable_sqlite_sync=False)
+
+    external_state = {
+        "progress": {
+            "current_chapter": 8,
+            "chapter_status": {"3": "chapter_committed"},
+        },
+        "disambiguation_warnings": [{"chapter": 4, "mention": "宗主"}],
+    }
+    temp_project.state_file.write_text(
+        json.dumps(external_state, ensure_ascii=False),
+        encoding="utf-8",
+    )
+
+    manager.set_chapter_status(5, "chapter_drafted")
+
+    saved = json.loads(temp_project.state_file.read_text(encoding="utf-8"))
+    assert saved["progress"]["current_chapter"] == 8
+    assert saved["progress"]["chapter_status"]["3"] == "chapter_committed"
+    assert saved["progress"]["chapter_status"]["5"] == "chapter_drafted"
+    assert saved["disambiguation_warnings"] == [{"chapter": 4, "mention": "宗主"}]
+
+
 def test_sync_to_sqlite_exceptions_and_no_sql_manager(temp_project, monkeypatch):
     manager = StateManager(temp_project)
     manager._pending_progress_chapter = 1