Procházet zdrojové kódy

fix: 修复脚本健壮性和兼容性问题

- context_pack_builder: 章节匹配正则兼容空格/中英文冒号/标题级别
- state_manager: 修复 entities_v3 和 current 字段类型边界情况
- state_manager: Windows 平台 UTF-8 编码支持
- extract_chapter_context: 复用 find_chapter_file,修复 foreshadowing 路径

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
lingfengQAQ před 4 měsíci
rodič
revize
eabebd2a40

+ 17 - 4
.claude/scripts/context_pack_builder.py

@@ -190,9 +190,22 @@ class ContextPackBuilder:
             with open(vf, 'r', encoding='utf-8') as f:
             with open(vf, 'r', encoding='utf-8') as f:
                 content = f.read()
                 content = f.read()
 
 
-            # 查找章节标记
-            pattern = rf'第{chapter_num}章[^\n]*\n(.*?)(?=第\d+章|$)'
-            match = re.search(pattern, content, re.DOTALL)
+            # 查找章节标记(兼容空格/中英文冒号/不同标题级别)
+            # 常见格式:### 第 1 章:标题 或 ### 第1章: 标题
+            heading_pattern = (
+                rf"(?m)^#+\s*第\s*{chapter_num}\s*章[::][^\n]*\n"
+                rf".*?(?=^#+\s*第\s*\d+\s*章|^##\s|\Z)"
+            )
+            match = re.search(heading_pattern, content, re.DOTALL)
+            if match:
+                return match.group(0).strip()
+
+            # 兼容无标题级别的格式:第 1 章 标题
+            plain_pattern = (
+                rf"(?m)^第\s*{chapter_num}\s*章[^\n]*\n"
+                rf".*?(?=^第\s*\d+\s*章|\Z)"
+            )
+            match = re.search(plain_pattern, content, re.DOTALL)
             if match:
             if match:
                 return match.group(0).strip()
                 return match.group(0).strip()
 
 
@@ -546,7 +559,7 @@ class ContextPackBuilder:
 
 
 
 
 def main():
 def main():
-    parser = argparse.ArgumentParser(description="Context Pack Builder v5.1")
+    parser = argparse.ArgumentParser(description="Context Pack Builder v5.2")
     parser.add_argument("--chapter", type=int, required=True, help="章节编号")
     parser.add_argument("--chapter", type=int, required=True, help="章节编号")
     parser.add_argument("--project-root", metavar="PATH", help="项目根目录")
     parser.add_argument("--project-root", metavar="PATH", help="项目根目录")
     parser.add_argument("--output", metavar="FILE", help="输出文件路径(默认输出到 stdout)")
     parser.add_argument("--output", metavar="FILE", help="输出文件路径(默认输出到 stdout)")

+ 22 - 2
.claude/scripts/data_modules/state_manager.py

@@ -14,6 +14,7 @@ v5.1 变更:
 """
 """
 
 
 import json
 import json
+import sys
 from pathlib import Path
 from pathlib import Path
 from typing import Dict, List, Optional, Any
 from typing import Dict, List, Optional, Any
 from dataclasses import dataclass, field, asdict
 from dataclasses import dataclass, field, asdict
@@ -763,7 +764,13 @@ class StateManager:
         if not entity_type:
         if not entity_type:
             return
             return
 
 
-        entity = self._state["entities_v3"][entity_type].get(entity_id)
+        entities_v3 = self._state.get("entities_v3")
+        if not isinstance(entities_v3, dict):
+            entities_v3 = {t: {} for t in self.ENTITY_TYPES}
+            self._state["entities_v3"] = entities_v3
+        entities_v3.setdefault(entity_type, {})
+
+        entity = entities_v3[entity_type].get(entity_id)
         if entity:
         if entity:
             if entity.get("first_appearance", 0) == 0:
             if entity.get("first_appearance", 0) == 0:
                 entity["first_appearance"] = chapter
                 entity["first_appearance"] = chapter
@@ -1093,7 +1100,16 @@ class StateManager:
         if not entity:
         if not entity:
             return
             return
 
 
-        current = entity.get("current", {})
+        current = entity.get("current")
+        if not isinstance(current, dict):
+            current = entity.get("current_json", {})
+        if isinstance(current, str):
+            try:
+                current = json.loads(current) if current else {}
+            except (json.JSONDecodeError, TypeError):
+                current = {}
+        if not isinstance(current, dict):
+            current = {}
         protag = self._state.setdefault("protagonist_state", {})
         protag = self._state.setdefault("protagonist_state", {})
 
 
         # 同步境界
         # 同步境界
@@ -1214,4 +1230,8 @@ def main():
 
 
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":
+    if sys.platform == "win32":
+        import io
+        sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
+        sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
     main()
     main()

+ 10 - 19
.claude/scripts/extract_chapter_context.py

@@ -18,6 +18,11 @@ import re
 import sys
 import sys
 from pathlib import Path
 from pathlib import Path
 
 
+try:
+    from chapter_paths import find_chapter_file
+except ImportError:  # pragma: no cover
+    from scripts.chapter_paths import find_chapter_file
+
 
 
 def find_project_root(start_path: Path = None) -> Path:
 def find_project_root(start_path: Path = None) -> Path:
     """查找包含 .webnovel 目录的项目根目录"""
     """查找包含 .webnovel 目录的项目根目录"""
@@ -91,22 +96,7 @@ def extract_chapter_summary(project_root: Path, chapter_num: int) -> str:
     if summary:
     if summary:
         return summary
         return summary
 
 
-    volume_num = (chapter_num - 1) // 50 + 1
-    chapter_dir = project_root / "正文" / f"第{volume_num}卷"
-
-    # 尝试匹配章节文件
-    patterns = [
-        f"第{chapter_num:03d}章*.md",
-        f"第{chapter_num:04d}章*.md",
-    ]
-
-    chapter_file = None
-    for pattern in patterns:
-        matches = list(chapter_dir.glob(pattern))
-        if matches:
-            chapter_file = matches[0]
-            break
-
+    chapter_file = find_chapter_file(project_root, chapter_num)
     if not chapter_file or not chapter_file.exists():
     if not chapter_file or not chapter_file.exists():
         return f"⚠️ 第 {chapter_num} 章文件不存在"
         return f"⚠️ 第 {chapter_num} 章文件不存在"
 
 
@@ -165,9 +155,10 @@ def extract_state_summary(project_root: Path) -> str:
             summary_parts.append(f"**近5章Strand**: {strand_str}")
             summary_parts.append(f"**近5章Strand**: {strand_str}")
 
 
     # 活跃伏笔(只显示紧急的)
     # 活跃伏笔(只显示紧急的)
-    if "foreshadowing" in state:
-        fs = state["foreshadowing"]
-        active = [f for f in fs if f.get("status") == "active"]
+    plot_threads = state.get("plot_threads", {}) if isinstance(state.get("plot_threads"), dict) else {}
+    foreshadowing = plot_threads.get("foreshadowing", [])
+    if isinstance(foreshadowing, list) and foreshadowing:
+        active = [f for f in foreshadowing if f.get("status") in {"active", "未回收"}]
         urgent = [f for f in active if f.get("urgency", 0) > 50]
         urgent = [f for f in active if f.get("urgency", 0) > 50]
         if urgent:
         if urgent:
             urgent_list = [f"{f.get('content', '?')[:30]}... (紧急度:{f.get('urgency')})" for f in urgent[:3]]
             urgent_list = [f"{f.get('content', '?')[:30]}... (紧急度:{f.get('urgency')})" for f in urgent[:3]]