Просмотр исходного кода

feat: load_context增强——章纲+摘要+主角+约束+伏笔概要

lingfengQAQ 2 месяцев назад
Родитель
Сommit
2d6762e948

+ 61 - 7
webnovel-writer/scripts/data_modules/memory_contract_adapter.py

@@ -110,17 +110,71 @@ class MemoryContractAdapter:
         )
         )
 
 
     def load_context(self, chapter: int, budget_tokens: int = 4000) -> ContextPack:
     def load_context(self, chapter: int, budget_tokens: int = 4000) -> ContextPack:
+        sections: Dict[str, Any] = {}
+
+        # 1. MemoryOrchestrator 基础包
         try:
         try:
             orch = self._memory_orchestrator()
             orch = self._memory_orchestrator()
             pack = orch.build_memory_pack(chapter)
             pack = orch.build_memory_pack(chapter)
-            return ContextPack(
-                chapter=chapter,
-                sections=pack,
-                budget_used_tokens=0,  # orchestrator 不计 token,由调用者按需裁剪
-            )
+            sections["memory_pack"] = pack
+        except Exception as e:
+            logger.warning("load_context: orchestrator failed: %s", e)
+
+        # 2. 章纲摘要
+        try:
+            from chapter_outline_loader import load_chapter_outline
+            outline = load_chapter_outline(self.config.project_root, chapter, max_chars=1500)
+            if outline and not outline.startswith("⚠️"):
+                sections["outline"] = outline
+        except Exception as e:
+            logger.warning("load_context: outline failed: %s", e)
+
+        # 3. 最近摘要
+        try:
+            summaries = {}
+            for prev_ch in range(max(1, chapter - 2), chapter):
+                text = self.read_summary(prev_ch)
+                if text:
+                    summaries[f"ch{prev_ch:04d}"] = text[:500]
+            if summaries:
+                sections["recent_summaries"] = summaries
+        except Exception as e:
+            logger.warning("load_context: summaries failed: %s", e)
+
+        # 4. 主角状态 + 进度
+        try:
+            sm = self._state_manager()
+            sm._load_state()
+            protagonist = sm._state.get("protagonist_state")
+            if protagonist:
+                sections["protagonist"] = protagonist
+            progress = sm._state.get("progress")
+            if progress:
+                sections["progress"] = progress
+        except Exception as e:
+            logger.warning("load_context: state failed: %s", e)
+
+        # 5. 活跃约束(world_rules 前 5 条)
+        try:
+            rules = self.query_rules()
+            if rules:
+                sections["active_rules"] = [r.to_dict() for r in rules[:5]]
         except Exception as e:
         except Exception as e:
-            logger.warning("load_context failed: %s", e)
-            return ContextPack(chapter=chapter)
+            logger.warning("load_context: rules failed: %s", e)
+
+        # 6. 紧急伏笔(前 3 条)
+        try:
+            loops = self.get_open_loops()
+            if loops:
+                sections["urgent_loops"] = [l.to_dict() for l in loops[:3]]
+        except Exception as e:
+            logger.warning("load_context: loops failed: %s", e)
+
+        return ContextPack(
+            chapter=chapter,
+            sections=sections,
+            budget_used_tokens=0,
+        )
 
 
     def query_entity(self, entity_id: str) -> Optional[EntitySnapshot]:
     def query_entity(self, entity_id: str) -> Optional[EntitySnapshot]:
         try:
         try:

+ 51 - 0
webnovel-writer/scripts/data_modules/tests/test_memory_contract_adapter.py

@@ -206,6 +206,57 @@ class TestLoadContext:
         assert isinstance(pack, ContextPack)
         assert isinstance(pack, ContextPack)
         assert pack.chapter == 10
         assert pack.chapter == 10
 
 
+    def test_load_context_includes_protagonist(self, tmp_path):
+        cfg = _make_project(tmp_path)
+        state = {
+            "progress": {"current_chapter": 9},
+            "protagonist_state": {"location": "迦南学院", "power": {"realm": "斗师"}},
+        }
+        cfg.state_file.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
+
+        adapter = MemoryContractAdapter(cfg)
+        pack = adapter.load_context(10)
+        assert "protagonist" in pack.sections
+        assert pack.sections["protagonist"]["location"] == "迦南学院"
+        assert "progress" in pack.sections
+
+    def test_load_context_includes_recent_summaries(self, tmp_path):
+        cfg = _make_project(tmp_path)
+        summary_dir = cfg.webnovel_dir / "summaries"
+        summary_dir.mkdir(parents=True, exist_ok=True)
+        (summary_dir / "ch0008.md").write_text("第8章摘要内容", encoding="utf-8")
+        (summary_dir / "ch0009.md").write_text("第9章摘要内容", encoding="utf-8")
+
+        adapter = MemoryContractAdapter(cfg)
+        pack = adapter.load_context(10)
+        assert "recent_summaries" in pack.sections
+        assert "ch0008" in pack.sections["recent_summaries"]
+        assert "ch0009" in pack.sections["recent_summaries"]
+
+    def test_load_context_includes_rules_and_loops(self, tmp_path):
+        cfg = _make_project(tmp_path)
+        from data_modules.memory.schema import MemoryItem
+        from data_modules.memory.store import ScratchpadManager
+
+        store = ScratchpadManager(cfg)
+        store.upsert_item(MemoryItem(
+            id="rule-1", layer="semantic", category="world_rule",
+            subject="力量体系", field="异火", value="23种",
+            status="active", source_chapter=1,
+        ))
+        store.upsert_item(MemoryItem(
+            id="ol-1", layer="semantic", category="open_loop",
+            subject="三年之约", field="", value="萧炎与纳兰嫣然三年之约",
+            status="active", source_chapter=1,
+        ))
+
+        adapter = MemoryContractAdapter(cfg)
+        pack = adapter.load_context(10)
+        assert "active_rules" in pack.sections
+        assert len(pack.sections["active_rules"]) == 1
+        assert "urgent_loops" in pack.sections
+        assert len(pack.sections["urgent_loops"]) == 1
+
 
 
 class TestCommitChapter:
 class TestCommitChapter:
     def test_commit_chapter_basic(self, tmp_path):
     def test_commit_chapter_basic(self, tmp_path):