novel-maker 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +209 -0
  3. package/bin/novel-maker.js +229 -0
  4. package/package.json +33 -0
  5. package/skill/CHANGELOG.md +82 -0
  6. package/skill/QUICK-REF.md +168 -0
  7. package/skill/README.md +75 -0
  8. package/skill/SKILL.md +715 -0
  9. package/skill/agents/README.md +59 -0
  10. package/skill/agents/auditor.md +234 -0
  11. package/skill/agents/coordinator.md +150 -0
  12. package/skill/agents/planner.md +220 -0
  13. package/skill/agents/reviewer.md +249 -0
  14. package/skill/agents/reviser.md +144 -0
  15. package/skill/agents/writer.md +213 -0
  16. package/skill/arc-templates/README.md +43 -0
  17. package/skill/arc-templates/apocalypse/ability.md +81 -0
  18. package/skill/arc-templates/apocalypse/faction.md +81 -0
  19. package/skill/arc-templates/apocalypse/humanity.md +81 -0
  20. package/skill/arc-templates/apocalypse/survival.md +81 -0
  21. package/skill/arc-templates/game/competition.md +81 -0
  22. package/skill/arc-templates/game/dungeon.md +81 -0
  23. package/skill/arc-templates/game/guild-war.md +81 -0
  24. package/skill/arc-templates/game/leveling.md +80 -0
  25. package/skill/arc-templates/general/challenge.md +44 -0
  26. package/skill/arc-templates/general/conflict.md +71 -0
  27. package/skill/arc-templates/general/explore.md +71 -0
  28. package/skill/arc-templates/general/growth.md +71 -0
  29. package/skill/arc-templates/general/mystery.md +71 -0
  30. package/skill/arc-templates/general/relation.md +71 -0
  31. package/skill/arc-templates/history/battle.md +71 -0
  32. package/skill/arc-templates/history/politics.md +71 -0
  33. package/skill/arc-templates/history/reform.md +71 -0
  34. package/skill/arc-templates/infinite-flow/boss.md +71 -0
  35. package/skill/arc-templates/infinite-flow/dungeon.md +71 -0
  36. package/skill/arc-templates/infinite-flow/enhance.md +71 -0
  37. package/skill/arc-templates/infinite-flow/team.md +71 -0
  38. package/skill/arc-templates/mystery/case.md +71 -0
  39. package/skill/arc-templates/mystery/deduction.md +71 -0
  40. package/skill/arc-templates/mystery/twist.md +71 -0
  41. package/skill/arc-templates/romance/angst.md +80 -0
  42. package/skill/arc-templates/romance/chase.md +71 -0
  43. package/skill/arc-templates/romance/slow-burn.md +71 -0
  44. package/skill/arc-templates/romance/sweet.md +80 -0
  45. package/skill/arc-templates/sci-fi/awakening.md +81 -0
  46. package/skill/arc-templates/sci-fi/breakthrough.md +81 -0
  47. package/skill/arc-templates/sci-fi/contact.md +81 -0
  48. package/skill/arc-templates/sci-fi/exploration.md +81 -0
  49. package/skill/arc-templates/urban/business.md +71 -0
  50. package/skill/arc-templates/urban/revenge.md +71 -0
  51. package/skill/arc-templates/urban/rise.md +71 -0
  52. package/skill/arc-templates/urban/romance.md +71 -0
  53. package/skill/arc-templates/western-fantasy/adventure.md +80 -0
  54. package/skill/arc-templates/western-fantasy/kingdom.md +71 -0
  55. package/skill/arc-templates/western-fantasy/magic-awakening.md +71 -0
  56. package/skill/arc-templates/western-fantasy/racial-conflict.md +71 -0
  57. package/skill/arc-templates/wuxia/breakthrough.md +71 -0
  58. package/skill/arc-templates/wuxia/grudge.md +71 -0
  59. package/skill/arc-templates/wuxia/hero-path.md +71 -0
  60. package/skill/arc-templates/wuxia/sect-war.md +71 -0
  61. package/skill/arc-templates/xianxia/breakthrough.md +71 -0
  62. package/skill/arc-templates/xianxia/dungeon.md +71 -0
  63. package/skill/arc-templates/xianxia/tournament.md +71 -0
  64. package/skill/arc-templates/xianxia/tribulation.md +71 -0
  65. package/skill/docs/examples.md +61 -0
  66. package/skill/docs/faq.md +81 -0
  67. package/skill/docs/installation.md +87 -0
  68. package/skill/docs/quickstart.md +83 -0
  69. package/skill/genre-packs/README.md +47 -0
  70. package/skill/genre-packs/_default/arc-types.md +153 -0
  71. package/skill/genre-packs/_default/rules.md +56 -0
  72. package/skill/genre-packs/_default/templates.md +135 -0
  73. package/skill/genre-packs/apocalypse/arc-types.md +109 -0
  74. package/skill/genre-packs/apocalypse/rules.md +113 -0
  75. package/skill/genre-packs/apocalypse/settings.md +106 -0
  76. package/skill/genre-packs/apocalypse/templates.md +192 -0
  77. package/skill/genre-packs/game/arc-types.md +109 -0
  78. package/skill/genre-packs/game/rules.md +113 -0
  79. package/skill/genre-packs/game/settings.md +103 -0
  80. package/skill/genre-packs/game/templates.md +173 -0
  81. package/skill/genre-packs/history/arc-types.md +109 -0
  82. package/skill/genre-packs/history/rules.md +107 -0
  83. package/skill/genre-packs/history/settings.md +126 -0
  84. package/skill/genre-packs/history/templates.md +179 -0
  85. package/skill/genre-packs/infinite-flow/arc-types.md +101 -0
  86. package/skill/genre-packs/infinite-flow/rules.md +75 -0
  87. package/skill/genre-packs/infinite-flow/settings.md +102 -0
  88. package/skill/genre-packs/infinite-flow/templates.md +226 -0
  89. package/skill/genre-packs/mystery/arc-types.md +109 -0
  90. package/skill/genre-packs/mystery/rules.md +107 -0
  91. package/skill/genre-packs/mystery/settings.md +103 -0
  92. package/skill/genre-packs/mystery/templates.md +178 -0
  93. package/skill/genre-packs/romance/arc-types.md +130 -0
  94. package/skill/genre-packs/romance/rules.md +88 -0
  95. package/skill/genre-packs/romance/settings.md +146 -0
  96. package/skill/genre-packs/romance/templates.md +245 -0
  97. package/skill/genre-packs/sci-fi/arc-types.md +109 -0
  98. package/skill/genre-packs/sci-fi/rules.md +113 -0
  99. package/skill/genre-packs/sci-fi/settings.md +99 -0
  100. package/skill/genre-packs/sci-fi/templates.md +170 -0
  101. package/skill/genre-packs/urban/arc-types.md +101 -0
  102. package/skill/genre-packs/urban/rules.md +75 -0
  103. package/skill/genre-packs/urban/settings.md +82 -0
  104. package/skill/genre-packs/urban/templates.md +212 -0
  105. package/skill/genre-packs/western-fantasy/arc-types.md +128 -0
  106. package/skill/genre-packs/western-fantasy/rules.md +88 -0
  107. package/skill/genre-packs/western-fantasy/settings.md +160 -0
  108. package/skill/genre-packs/western-fantasy/templates.md +225 -0
  109. package/skill/genre-packs/wuxia/arc-types.md +126 -0
  110. package/skill/genre-packs/wuxia/rules.md +86 -0
  111. package/skill/genre-packs/wuxia/settings.md +150 -0
  112. package/skill/genre-packs/wuxia/templates.md +195 -0
  113. package/skill/genre-packs/xianxia/arc-types.md +101 -0
  114. package/skill/genre-packs/xianxia/rules.md +74 -0
  115. package/skill/genre-packs/xianxia/settings.md +107 -0
  116. package/skill/genre-packs/xianxia/templates.md +202 -0
  117. package/skill/hooks/README.md +102 -0
  118. package/skill/hooks/chapter-complete.md +176 -0
  119. package/skill/hooks/context-injection.md +152 -0
  120. package/skill/hooks/intent-detection.md +183 -0
  121. package/skill/hooks/review-trigger.md +219 -0
  122. package/skill/hooks/summary-trigger.md +185 -0
  123. package/skill/references/act-guidance.md +228 -0
  124. package/skill/references/audit-core.md +130 -0
  125. package/skill/references/audit-dimensions.md +202 -0
  126. package/skill/references/character-voice-card.md +196 -0
  127. package/skill/references/consistency-checker.md +209 -0
  128. package/skill/references/content-expansion.md +68 -0
  129. package/skill/references/creative-constraints.md +200 -0
  130. package/skill/references/data-agent.md +286 -0
  131. package/skill/references/dialogue-writing.md +104 -0
  132. package/skill/references/editorial-perspective.md +166 -0
  133. package/skill/references/emotion-curve.md +127 -0
  134. package/skill/references/genre-rules.md +389 -0
  135. package/skill/references/golden-opening.md +81 -0
  136. package/skill/references/memory-system.md +288 -0
  137. package/skill/references/pacing-analysis.md +201 -0
  138. package/skill/references/platform-rules.md +244 -0
  139. package/skill/references/plot-structures.md +108 -0
  140. package/skill/references/reader-feedback.md +119 -0
  141. package/skill/references/rhythm-system.md +204 -0
  142. package/skill/references/style-imitation.md +193 -0
  143. package/skill/references/sweet-spot-tracking.md +182 -0
  144. package/skill/references/usage-guide.md +174 -0
  145. package/skill/references/writing-methods.md +169 -0
  146. package/skill/rules/anti-ai-expressions.md +206 -0
  147. package/skill/rules/character-voice.md +184 -0
  148. package/skill/rules/consistency-check.md +232 -0
  149. package/skill/rules/smart-query.md +263 -0
  150. package/skill/scripts/README.md +380 -0
  151. package/skill/scripts/auditor/chapter_transition.py +217 -0
  152. package/skill/scripts/auditor/consistency_scan.py +194 -0
  153. package/skill/scripts/auditor/dialogue_checker.py +194 -0
  154. package/skill/scripts/auditor/hook_report.py +115 -0
  155. package/skill/scripts/auditor/pacing_optimizer.py +303 -0
  156. package/skill/scripts/auditor/pacing_report.py +275 -0
  157. package/skill/scripts/auditor/pre_audit.py +203 -0
  158. package/skill/scripts/auditor/style_check.py +158 -0
  159. package/skill/scripts/auditor/worldbuilding_checker.py +637 -0
  160. package/skill/scripts/common/analyze.py +129 -0
  161. package/skill/scripts/common/init_guide.py +796 -0
  162. package/skill/scripts/common/install.py +169 -0
  163. package/skill/scripts/common/nm_utils.py +296 -0
  164. package/skill/scripts/common/validate.py +215 -0
  165. package/skill/scripts/coordinator/stats_report.py +165 -0
  166. package/skill/scripts/coordinator/volume_batch.py +121 -0
  167. package/skill/scripts/planner/outline_extractor.py +89 -0
  168. package/skill/scripts/planner/planner_context.py +220 -0
  169. package/skill/scripts/planner/query_engine.py +289 -0
  170. package/skill/scripts/reviewer/chapter_diff.py +143 -0
  171. package/skill/scripts/reviewer/character_arc_tracker.py +191 -0
  172. package/skill/scripts/reviewer/emotion_curve.py +340 -0
  173. package/skill/scripts/reviewer/foreshadowing_tracker.py +286 -0
  174. package/skill/scripts/reviewer/subplot_tracker.py +207 -0
  175. package/skill/scripts/reviewer/summary_generator.py +130 -0
  176. package/skill/scripts/reviewer/truth_diff.py +227 -0
  177. package/skill/scripts/reviewer/truth_manager.py +120 -0
  178. package/skill/scripts/test_scripts.py +366 -0
  179. package/skill/scripts/writer/build_write_context.py +255 -0
  180. package/skill/scripts/writer/chapter_info.py +67 -0
  181. package/skill/scripts/writer/check_wordcount.py +115 -0
  182. package/skill/scripts/writer/scene_builder.py +227 -0
  183. package/skill/scripts/writer/style_anchor.py +135 -0
  184. package/skill/styles/author-styles.md +53 -0
  185. package/skill/styles/authors//344/270/245/350/260/250/350/256/276/345/256/232/346/265/201//345/277/230/350/257/255.md +110 -0
  186. package/skill/styles/authors//344/270/245/350/260/250/350/256/276/345/256/232/346/265/201//347/210/261/346/275/234/346/260/264/347/232/204/344/271/214/350/264/274.md +110 -0
  187. package/skill/styles/authors//344/270/245/350/260/250/350/256/276/345/256/232/346/265/201//350/250/200/345/275/222/346/255/243/344/274/240.md +110 -0
  188. package/skill/styles/authors//345/244/232/347/245/236/350/257/235/347/203/255/350/241/200/346/265/201//344/270/211/344/271/235/351/237/263/345/237/237.md +108 -0
  189. package/skill/styles/authors//346/202/254/347/226/221/346/216/250/347/220/206/346/265/201//346/235/200/350/231/253/351/230/237/351/230/237/345/221/230.md +108 -0
  190. package/skill/styles/authors//346/220/236/347/254/221/345/271/275/351/273/230/346/265/201//344/270/211/345/244/251/344/270/244/350/247/211.md +110 -0
  191. package/skill/styles/authors//346/220/236/347/254/221/345/271/275/351/273/230/346/265/201//344/274/232/350/257/264/350/257/235/347/232/204/350/202/230/345/255/220.md +110 -0
  192. package/skill/styles/authors//346/220/236/347/254/221/345/271/275/351/273/230/346/265/201//345/215/226/346/212/245/345/260/217/351/203/216/345/220/233.md +108 -0
  193. package/skill/styles/authors//346/220/236/347/254/221/345/271/275/351/273/230/346/265/201//345/274/210/351/235/222/345/263/260.md +123 -0
  194. package/skill/styles/authors//347/203/255/350/241/200/345/215/207/347/272/247/346/265/201//345/224/220/345/256/266/344/270/211/345/260/221.md +109 -0
  195. package/skill/styles/authors//347/203/255/350/241/200/345/215/207/347/272/247/346/265/201//345/244/251/350/232/225/345/234/237/350/261/206.md +110 -0
  196. package/skill/styles/authors//347/203/255/350/241/200/345/215/207/347/272/247/346/265/201//346/210/221/345/220/203/350/245/277/347/272/242/346/237/277.md +108 -0
  197. package/skill/styles/authors//347/203/255/350/241/200/345/215/207/347/272/247/346/265/201//346/273/232/345/274/200.md +109 -0
  198. package/skill/styles/authors//347/203/255/350/241/200/345/215/207/347/272/247/346/265/201//350/276/260/344/270/234.md +109 -0
  199. package/skill/styles/authors//347/211/271/350/211/262/351/242/206/345/237/237/346/265/201//345/244/251/344/270/213/351/234/270/345/224/261.md +110 -0
  200. package/skill/styles/authors//347/211/271/350/211/262/351/242/206/345/237/237/346/265/201//346/234/210/345/205/263.md +108 -0
  201. package/skill/styles/authors//347/211/271/350/211/262/351/242/206/345/237/237/346/265/201//350/220/247/351/274/216.md +109 -0
  202. package/skill/styles/authors//347/211/271/350/211/262/351/242/206/345/237/237/346/265/201//350/235/264/350/235/266/350/223/235.md +112 -0
  203. package/skill/styles/authors//347/273/206/350/205/273/346/226/207/351/235/222/346/265/201//346/204/244/346/200/222/347/232/204/351/246/231/350/225/211.md +109 -0
  204. package/skill/styles/authors//347/273/206/350/205/273/346/226/207/351/235/222/346/265/201//347/203/275/347/201/253/346/210/217/350/257/270/344/276/257.md +109 -0
  205. package/skill/styles/authors//347/273/206/350/205/273/346/226/207/351/235/222/346/265/201//347/214/253/350/205/273.md +110 -0
  206. package/skill/styles/authors//347/273/206/350/205/273/346/226/207/351/235/222/346/265/201//350/200/263/346/240/271.md +109 -0
  207. package/skill/templates/INDEX.md +48 -0
  208. package/skill/templates/act-plan.md +72 -0
  209. package/skill/templates/chapter.md +53 -0
  210. package/skill/templates/character-profile.md +107 -0
  211. package/skill/templates/character-voice.md +106 -0
  212. package/skill/templates/constitution.md +90 -0
  213. package/skill/templates/emotional-arcs.md +37 -0
  214. package/skill/templates/hook-template.md +68 -0
  215. package/skill/templates/outline.md +155 -0
  216. package/skill/templates/plot-card.md +432 -0
  217. package/skill/templates/power-system.md +124 -0
  218. package/skill/templates/presets.json +69 -0
  219. package/skill/templates/review-report.md +135 -0
  220. package/skill/templates/scene-plan.md +221 -0
  221. package/skill/templates/scene-template.md +78 -0
  222. package/skill/templates/subplot-board.md +48 -0
  223. package/skill/templates/summary-10chapters.md +79 -0
  224. package/skill/templates/summary-50chapters.md +131 -0
  225. package/skill/templates/summary-volume.md +148 -0
  226. package/skill/templates/timeline.md +37 -0
  227. package/skill/templates/volume-plan.md +44 -0
  228. package/skill/templates/world-setting.md +151 -0
@@ -0,0 +1,637 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 世界观一致性检查脚本
5
+ 检测世界观设定的前后矛盾,包括力量体系、地理、历史、社会制度等。
6
+ 供审计师在审查时使用,或复盘师在卷末复盘时参考。
7
+
8
+ 用法:
9
+ python scripts/auditor/worldbuilding_checker.py 章节目录 --truth 真相文件目录 --json
10
+ python scripts/auditor/worldbuilding_checker.py 章节目录 --truth 真相文件目录 --recent 10
11
+ """
12
+
13
+ import argparse
14
+ import json
15
+ import os
16
+ import re
17
+ import sys
18
+ from collections import defaultdict
19
+
20
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
21
+ from nm_utils import (
22
+ list_chapters, read_chapter, count_chinese, clean_markdown,
23
+ read_truth_section
24
+ )
25
+
26
+ # ─── 世界观检查维度 ──────────────────────────────
27
+ WORLDBUILDING_DIMENSIONS = {
28
+ 'power_system': {
29
+ 'name': '力量体系',
30
+ 'keywords': ['等级', '境界', '修为', '实力', '突破', '晋升', '阶位', '段位'],
31
+ 'check_patterns': [
32
+ r'(\w+)\s*(?:从|由)\s*(\w+)\s*(?:突破|晋升|进阶)\s*(?:到|为)\s*(\w+)',
33
+ r'(\w+)\s*(?:是|为)\s*(\w+)\s*(?:级别|等级|境界)'
34
+ ]
35
+ },
36
+ 'geography': {
37
+ 'name': '地理设定',
38
+ 'keywords': ['城市', '国家', '大陆', '山脉', '河流', '森林', '沙漠', '海洋'],
39
+ 'check_patterns': [
40
+ r'(?:位于|在)\s*(\w+)\s*(?:的|方向|方位)',
41
+ r'(\w+)\s*(?:距离|相距)\s*(\w+)\s*(?:有|约)\s*(\d+)\s*(?:里|公里)'
42
+ ]
43
+ },
44
+ 'history': {
45
+ 'name': '历史设定',
46
+ 'keywords': ['年前', '年前', '历史', '传说', '古老', '过去', '曾经', '当年'],
47
+ 'check_patterns': [
48
+ r'(\d+)\s*(?:年前|百年前|千年前)',
49
+ r'(?:在|于)\s*(\w+)\s*(?:时期|时代|年代)'
50
+ ]
51
+ },
52
+ 'social_system': {
53
+ 'name': '社会制度',
54
+ 'keywords': ['宗门', '门派', '家族', '朝廷', '帝国', '王国', '组织', '势力'],
55
+ 'check_patterns': [
56
+ r'(\w+)\s*(?:是|属于)\s*(\w+)\s*(?:的|宗门|门派|家族)',
57
+ r'(\w+)\s*(?:担任|是)\s*(\w+)\s*(?:职位|职务|身份)'
58
+ ]
59
+ },
60
+ 'items': {
61
+ 'name': '物品设定',
62
+ 'keywords': ['宝物', '神器', '丹药', '武器', '装备', '秘籍', '功法'],
63
+ 'check_patterns': [
64
+ r'(\w+)\s*(?:具有|拥有)\s*(\w+)\s*(?:效果|功能|作用)',
65
+ r'(\w+)\s*(?:需要|要求)\s*(\w+)\s*(?:才能|方可)\s*(?:使用|激活)'
66
+ ]
67
+ }
68
+ }
69
+
70
+
71
+ def extract_worldbuilding_facts(text, dimension_config):
72
+ """Extract worldbuilding facts from text based on dimension config."""
73
+ facts = []
74
+
75
+ for pattern in dimension_config['check_patterns']:
76
+ matches = re.finditer(pattern, text)
77
+ for match in matches:
78
+ facts.append({
79
+ 'pattern': pattern,
80
+ 'match': match.group(0),
81
+ 'groups': match.groups(),
82
+ 'position': match.start()
83
+ })
84
+
85
+ return facts
86
+
87
+
88
+ def check_worldbuilding_consistency(chapters_dir, truth_dir=None, recent_n=None):
89
+ """Check worldbuilding consistency across chapters."""
90
+ chapter_files = list_chapters(chapters_dir, recent_n)
91
+ if not chapter_files:
92
+ return {'error': '未找到章节文件'}
93
+
94
+ # Load truth files if provided
95
+ truth_facts = {}
96
+ if truth_dir and os.path.isdir(truth_dir):
97
+ for dimension in WORLDBUILDING_DIMENSIONS.keys():
98
+ truth_file = os.path.join(truth_dir, f'{dimension}.md')
99
+ if os.path.exists(truth_file):
100
+ with open(truth_file, 'r', encoding='utf-8') as f:
101
+ truth_facts[dimension] = f.read()
102
+
103
+ # Analyze each chapter
104
+ chapter_facts = defaultdict(lambda: defaultdict(list))
105
+ issues = []
106
+
107
+ for idx, cf in enumerate(chapter_files):
108
+ raw, title, clean, wc = read_chapter(cf)
109
+ ch_num = idx + 1
110
+
111
+ for dimension, config in WORLDBUILDING_DIMENSIONS.items():
112
+ facts = extract_worldbuilding_facts(clean, config)
113
+ chapter_facts[ch_num][dimension] = facts
114
+
115
+ # Check against truth files
116
+ if dimension in truth_facts:
117
+ truth_content = truth_facts[dimension]
118
+ for fact in facts:
119
+ # Simple consistency check: verify key terms appear in truth
120
+ for group in fact['groups']:
121
+ if group and group not in truth_content:
122
+ issues.append({
123
+ 'type': '设定冲突',
124
+ 'dimension': config['name'],
125
+ 'chapter': ch_num,
126
+ 'chapter_title': title,
127
+ 'fact': fact['match'],
128
+ 'message': f"「{group}」未在真相文件「{dimension}.md」中找到",
129
+ 'severity': 'warning'
130
+ })
131
+
132
+ # Detect internal contradictions (same entity with different descriptions)
133
+ entity_descriptions = defaultdict(list)
134
+ for ch_num, dimensions in chapter_facts.items():
135
+ for dimension, facts in dimensions.items():
136
+ for fact in facts:
137
+ if fact['groups'] and len(fact['groups']) >= 2:
138
+ entity = fact['groups'][0]
139
+ description = fact['groups'][1]
140
+ entity_descriptions[entity].append({
141
+ 'chapter': ch_num,
142
+ 'dimension': dimension,
143
+ 'description': description
144
+ })
145
+
146
+ # Find contradictions
147
+ for entity, descriptions in entity_descriptions.items():
148
+ unique_descriptions = set(d['description'] for d in descriptions)
149
+ if len(unique_descriptions) > 1:
150
+ issues.append({
151
+ 'type': '内部矛盾',
152
+ 'dimension': WORLDBUILDING_DIMENSIONS[descriptions[0]['dimension']]['name'],
153
+ 'entity': entity,
154
+ 'descriptions': descriptions,
155
+ 'message': f"「{entity}」在不同章节有不同描述",
156
+ 'severity': 'error'
157
+ })
158
+
159
+ # Summary statistics
160
+ dimension_stats = {}
161
+ for dimension, config in WORLDBUILDING_DIMENSIONS.items():
162
+ total_facts = sum(len(chapter_facts[ch][dimension]) for ch in chapter_facts)
163
+ dimension_stats[dimension] = {
164
+ 'name': config['name'],
165
+ 'total_facts': total_facts,
166
+ 'chapters_with_facts': sum(1 for ch in chapter_facts if chapter_facts[ch][dimension])
167
+ }
168
+
169
+ return {
170
+ 'total_chapters': len(chapter_files),
171
+ 'dimensions_checked': len(WORLDBUILDING_DIMENSIONS),
172
+ 'dimension_stats': dimension_stats,
173
+ 'issues': issues,
174
+ 'issue_count': len(issues),
175
+ 'error_count': sum(1 for i in issues if i['severity'] == 'error'),
176
+ 'warning_count': sum(1 for i in issues if i['severity'] == 'warning')
177
+ }
178
+
179
+
180
+ def generate_fix_suggestions(issues, truth_facts=None):
181
+ """Generate automatic fix suggestions for worldbuilding issues."""
182
+ suggestions = []
183
+
184
+ # Fix templates for each issue type
185
+ fix_templates = {
186
+ '设定冲突': {
187
+ 'action': '更新真相文件或修正章节内容',
188
+ 'steps': [
189
+ '确认该设定是否为有意的新增设定',
190
+ '如果是新设定,将其添加到对应的真相文件中',
191
+ '如果是笔误,修正章节中的描述使其与真相文件一致'
192
+ ]
193
+ },
194
+ '内部矛盾': {
195
+ 'action': '统一实体描述,消除前后矛盾',
196
+ 'steps': [
197
+ '确定以哪个章节的描述为准(通常以首次出现为准)',
198
+ '修正后续章节中不一致的描述',
199
+ '将统一后的描述更新到真相文件中'
200
+ ]
201
+ }
202
+ }
203
+
204
+ for issue in issues:
205
+ issue_type = issue['type']
206
+ if issue_type not in fix_templates:
207
+ continue
208
+
209
+ template = fix_templates[issue_type]
210
+ suggestion = {
211
+ 'type': issue_type,
212
+ 'dimension': issue.get('dimension', '未知'),
213
+ 'message': issue['message'],
214
+ 'action': template['action'],
215
+ 'steps': template['steps'],
216
+ 'severity': issue['severity']
217
+ }
218
+
219
+ if issue_type == '设定冲突':
220
+ suggestion['missing_term'] = issue.get('fact', '')
221
+ suggestion['chapter'] = issue.get('chapter', '未知')
222
+ suggestion['fix_options'] = [
223
+ f"将「{issue.get('fact', '')}」添加到真相文件对应维度中",
224
+ f"修正第{issue.get('chapter', '?')}章中的描述使其与真相文件一致"
225
+ ]
226
+
227
+ elif issue_type == '内部矛盾':
228
+ entity = issue.get('entity', '')
229
+ descriptions = issue.get('descriptions', [])
230
+ suggestion['entity'] = entity
231
+ suggestion['conflict_count'] = len(descriptions)
232
+
233
+ # Build specific fix based on descriptions
234
+ if descriptions:
235
+ first_desc = descriptions[0]
236
+ suggestion['recommended_fix'] = (
237
+ f"以第{first_desc['chapter']}章的描述「{first_desc['description']}」为准,"
238
+ f"统一其他{len(descriptions)-1}处描述"
239
+ )
240
+ suggestion['conflict_details'] = [
241
+ {
242
+ 'chapter': d['chapter'],
243
+ 'description': d['description']
244
+ }
245
+ for d in descriptions
246
+ ]
247
+
248
+ suggestions.append(suggestion)
249
+
250
+ return suggestions
251
+
252
+
253
+ def auto_fix_truth_files(issues, truth_dir, dry_run=True):
254
+ """Auto-fix truth files by adding missing terms from chapters.
255
+
256
+ Args:
257
+ issues: List of detected issues
258
+ truth_dir: Path to truth files directory
259
+ dry_run: If True, only report what would be fixed; if False, actually modify files
260
+
261
+ Returns:
262
+ List of fix actions performed
263
+ """
264
+ fix_actions = []
265
+
266
+ if not truth_dir or not os.path.isdir(truth_dir):
267
+ return fix_actions
268
+
269
+ # Group issues by dimension
270
+ missing_by_dimension = defaultdict(set)
271
+ for issue in issues:
272
+ if issue['type'] == '设定冲突' and 'dimension' in issue:
273
+ # Extract dimension key from display name
274
+ dim_key = None
275
+ for k, v in WORLDBUILDING_DIMENSIONS.items():
276
+ if v['name'] == issue['dimension']:
277
+ dim_key = k
278
+ break
279
+
280
+ if dim_key and 'fact' in issue:
281
+ # Extract the missing term from the fact
282
+ fact_text = issue['fact']
283
+ # Try to extract key terms from the fact
284
+ for group in re.findall(r'[\u4e00-\u9fa5]+', fact_text):
285
+ if len(group) >= 2: # Only terms with 2+ chars
286
+ missing_by_dimension[dim_key].add(group)
287
+
288
+ # Add missing terms to truth files
289
+ for dim_key, terms in missing_by_dimension.items():
290
+ truth_file = os.path.join(truth_dir, f'{dim_key}.md')
291
+
292
+ if os.path.exists(truth_file):
293
+ with open(truth_file, 'r', encoding='utf-8') as f:
294
+ content = f.read()
295
+
296
+ # Check which terms are actually missing
297
+ new_terms = [t for t in terms if t not in content]
298
+
299
+ if new_terms:
300
+ if not dry_run:
301
+ # Check if auto-fix section already exists
302
+ auto_fix_header = '## 自动补充设定'
303
+ if auto_fix_header in content:
304
+ # Append to existing section
305
+ with open(truth_file, 'a', encoding='utf-8') as f:
306
+ for term in sorted(new_terms):
307
+ f.write(f'- {term}\n')
308
+ else:
309
+ # Create new section
310
+ with open(truth_file, 'a', encoding='utf-8') as f:
311
+ f.write('\n\n## 自动补充设定\n')
312
+ for term in sorted(new_terms):
313
+ f.write(f'- {term}\n')
314
+
315
+ fix_actions.append({
316
+ 'action': '补全真相文件',
317
+ 'dimension': WORLDBUILDING_DIMENSIONS[dim_key]['name'],
318
+ 'file': truth_file,
319
+ 'added_terms': sorted(new_terms),
320
+ 'count': len(new_terms),
321
+ 'status': '待修复(dry-run)' if dry_run else '已修复'
322
+ })
323
+
324
+ return fix_actions
325
+
326
+
327
+ def auto_fix_chapter_contradictions(issues, chapters_dir, dry_run=True):
328
+ """Auto-fix contradictions in chapter files by standardizing entity descriptions.
329
+
330
+ Args:
331
+ issues: List of detected issues
332
+ chapters_dir: Path to chapters directory
333
+ dry_run: If True, only report what would be fixed; if False, actually modify files
334
+
335
+ Returns:
336
+ List of fix actions performed or proposed
337
+ """
338
+ fix_actions = []
339
+
340
+ # Get chapter files
341
+ chapter_files = list_chapters(chapters_dir)
342
+ if not chapter_files:
343
+ return fix_actions
344
+
345
+ # Process internal contradictions
346
+ for issue in issues:
347
+ if issue['type'] != '内部矛盾':
348
+ continue
349
+
350
+ entity = issue.get('entity', '')
351
+ descriptions = issue.get('descriptions', [])
352
+
353
+ if not entity or len(descriptions) < 2:
354
+ continue
355
+
356
+ # Use first description as the canonical one
357
+ canonical = descriptions[0]
358
+ canonical_desc = canonical['description']
359
+
360
+ # Skip if description is too short (risk of false positives)
361
+ if len(canonical_desc) < 4:
362
+ continue
363
+
364
+ # Find chapters that need fixing
365
+ chapters_to_fix = []
366
+ for desc in descriptions[1:]:
367
+ if desc['description'] != canonical_desc:
368
+ chapters_to_fix.append({
369
+ 'chapter': desc['chapter'],
370
+ 'wrong_desc': desc['description'],
371
+ 'right_desc': canonical_desc
372
+ })
373
+
374
+ if chapters_to_fix:
375
+ fix_action = {
376
+ 'action': '修正章节矛盾',
377
+ 'entity': entity,
378
+ 'canonical_description': canonical_desc,
379
+ 'canonical_chapter': canonical['chapter'],
380
+ 'fixes': []
381
+ }
382
+
383
+ for fix_info in chapters_to_fix:
384
+ ch_idx = fix_info['chapter'] - 1
385
+ if 0 <= ch_idx < len(chapter_files):
386
+ ch_file = chapter_files[ch_idx]
387
+
388
+ if not dry_run:
389
+ # Read and fix the chapter
390
+ with open(ch_file, 'r', encoding='utf-8') as f:
391
+ content = f.read()
392
+
393
+ # Use word boundary matching to avoid false replacements
394
+ # Only replace if the wrong description appears as a distinct phrase
395
+ wrong = fix_info['wrong_desc']
396
+ right = fix_info['right_desc']
397
+
398
+ # Count occurrences to detect scope
399
+ count = content.count(wrong)
400
+ if count > 0 and count <= 3: # Only fix if 1-3 occurrences
401
+ fixed_content = content.replace(wrong, right)
402
+
403
+ if fixed_content != content:
404
+ with open(ch_file, 'w', encoding='utf-8') as f:
405
+ f.write(fixed_content)
406
+
407
+ fix_action['fixes'].append({
408
+ 'chapter': fix_info['chapter'],
409
+ 'file': ch_file,
410
+ 'replaced': wrong,
411
+ 'with': right,
412
+ 'occurrences': count,
413
+ 'status': '已修复'
414
+ })
415
+ else:
416
+ fix_action['fixes'].append({
417
+ 'chapter': fix_info['chapter'],
418
+ 'file': ch_file,
419
+ 'replaced': wrong,
420
+ 'with': right,
421
+ 'occurrences': count,
422
+ 'status': '跳过(出现次数过多,需人工确认)'
423
+ })
424
+ else:
425
+ fix_action['fixes'].append({
426
+ 'chapter': fix_info['chapter'],
427
+ 'file': ch_file,
428
+ 'replaced': fix_info['wrong_desc'],
429
+ 'with': fix_info['right_desc'],
430
+ 'status': '待修复(dry-run)'
431
+ })
432
+
433
+ if fix_action['fixes']:
434
+ fix_actions.append(fix_action)
435
+
436
+ return fix_actions
437
+
438
+
439
+ def generate_fix_report(truth_fixes, chapter_fixes, issues):
440
+ """Generate a structured fix report for reviewer.
441
+
442
+ Args:
443
+ truth_fixes: List of truth file fix actions
444
+ chapter_fixes: List of chapter fix actions
445
+ issues: Original list of issues
446
+
447
+ Returns:
448
+ Dict containing structured fix report
449
+ """
450
+ report = {
451
+ 'summary': {
452
+ 'total_issues': len(issues),
453
+ 'truth_files_fixed': len(truth_fixes),
454
+ 'chapters_fixed': len(chapter_fixes),
455
+ 'remaining_issues': 0
456
+ },
457
+ 'truth_file_fixes': truth_fixes,
458
+ 'chapter_fixes': chapter_fixes,
459
+ 'unfixed_issues': []
460
+ }
461
+
462
+ # Track which issues were actually fixed
463
+ fixed_issue_indices = set()
464
+
465
+ # Check truth file fixes
466
+ for fix in truth_fixes:
467
+ dim_name = fix.get('dimension')
468
+ added_terms = fix.get('added_terms', [])
469
+ for idx, issue in enumerate(issues):
470
+ if issue['type'] == '设定冲突' and issue.get('dimension') == dim_name:
471
+ fact_text = issue.get('fact', '')
472
+ for term in re.findall(r'[\u4e00-\u9fa5]+', fact_text):
473
+ if term in added_terms:
474
+ fixed_issue_indices.add(idx)
475
+ break
476
+
477
+ # Check chapter fixes
478
+ fixed_entities = set()
479
+ for fix in chapter_fixes:
480
+ if 'entity' in fix:
481
+ fixed_entities.add(fix['entity'])
482
+
483
+ for idx, issue in enumerate(issues):
484
+ if issue['type'] == '内部矛盾' and issue.get('entity') in fixed_entities:
485
+ fixed_issue_indices.add(idx)
486
+
487
+ # Calculate remaining issues
488
+ report['summary']['remaining_issues'] = len(issues) - len(fixed_issue_indices)
489
+
490
+ # Collect unfixed issues
491
+ for idx, issue in enumerate(issues):
492
+ if idx not in fixed_issue_indices:
493
+ report['unfixed_issues'].append(issue)
494
+
495
+ return report
496
+
497
+
498
+ def main():
499
+ parser = argparse.ArgumentParser(description='世界观一致性检查脚本')
500
+ parser.add_argument('chapters_dir', help='章节目录路径')
501
+ parser.add_argument('--truth', '-t', help='真相文件目录路径')
502
+ parser.add_argument('--recent', type=int, help='仅检查最近N章')
503
+ parser.add_argument('--json', action='store_true', help='JSON输出')
504
+ parser.add_argument('--fix', action='store_true', help='显示自动修复建议')
505
+ parser.add_argument('--apply', action='store_true', help='应用自动修复(需配合--truth)')
506
+ parser.add_argument('--dry-run', action='store_true', help='仅预览修复内容,不实际修改文件')
507
+ args = parser.parse_args()
508
+
509
+ if not os.path.isdir(args.chapters_dir):
510
+ print(f"错误: 目录不存在: {args.chapters_dir}", file=sys.stderr)
511
+ sys.exit(1)
512
+
513
+ result = check_worldbuilding_consistency(args.chapters_dir, args.truth, args.recent)
514
+
515
+ # Generate fix suggestions if requested
516
+ if args.fix and result.get('issues'):
517
+ result['fix_suggestions'] = generate_fix_suggestions(result['issues'])
518
+
519
+ # Apply auto-fix if requested
520
+ if args.apply and result.get('issues'):
521
+ if not args.truth:
522
+ print("错误: --apply 需要配合 --truth 参数使用", file=sys.stderr)
523
+ sys.exit(1)
524
+
525
+ # Load truth facts for reference
526
+ truth_facts = {}
527
+ if os.path.isdir(args.truth):
528
+ for dimension in WORLDBUILDING_DIMENSIONS.keys():
529
+ truth_file = os.path.join(args.truth, f'{dimension}.md')
530
+ if os.path.exists(truth_file):
531
+ with open(truth_file, 'r', encoding='utf-8') as f:
532
+ truth_facts[dimension] = f.read()
533
+
534
+ # Auto-fix truth files
535
+ dry_run = args.dry_run
536
+ truth_fixes = auto_fix_truth_files(result['issues'], args.truth, dry_run=dry_run)
537
+
538
+ # Auto-fix chapter contradictions
539
+ chapter_fixes = auto_fix_chapter_contradictions(result['issues'], args.chapters_dir, dry_run=dry_run)
540
+
541
+ # Generate fix report
542
+ fix_report = generate_fix_report(truth_fixes, chapter_fixes, result['issues'])
543
+
544
+ result['fix_report'] = fix_report
545
+
546
+ if args.json:
547
+ print(json.dumps(result, ensure_ascii=False, indent=2))
548
+ else:
549
+ if 'error' in result:
550
+ print(result['error'])
551
+ return
552
+
553
+ print(f"\n=== 世界观一致性检查 ===\n")
554
+ print(f"检查章节数: {result['total_chapters']}")
555
+ print(f"检查维度数: {result['dimensions_checked']}")
556
+ print(f"问题总数: {result['issue_count']}")
557
+ print(f" 错误: {result['error_count']}")
558
+ print(f" 警告: {result['warning_count']}")
559
+
560
+ print(f"\n各维度统计:")
561
+ for dimension, stats in result['dimension_stats'].items():
562
+ print(f" {stats['name']}: {stats['total_facts']}个设定, 涉及{stats['chapters_with_facts']}章")
563
+
564
+ if result['issues']:
565
+ print(f"\n问题详情:")
566
+ for issue in result['issues'][:20]: # Show first 20 issues
567
+ icon = '' if issue['severity'] == 'error' else '⚠'
568
+ print(f"\n {icon} [{issue['type']}] {issue['dimension']}")
569
+ print(f" {issue['message']}")
570
+ if 'chapter' in issue:
571
+ print(f" 章节: 第{issue['chapter']}章 {issue.get('chapter_title', '')}")
572
+ if 'fact' in issue:
573
+ print(f" 原文: {issue['fact'][:60]}...")
574
+
575
+ # Show fix suggestions if requested
576
+ if args.fix and result.get('fix_suggestions'):
577
+ print(f"\n{'='*60}")
578
+ print(f"自动修复建议:")
579
+ print(f"{'='*60}")
580
+ for i, suggestion in enumerate(result['fix_suggestions'], 1):
581
+ print(f"\n [{i}] {suggestion['type']} - {suggestion['dimension']}")
582
+ print(f" 问题: {suggestion['message']}")
583
+ print(f" 操作: {suggestion['action']}")
584
+ for j, step in enumerate(suggestion['steps'], 1):
585
+ print(f" {j}. {step}")
586
+
587
+ if 'fix_options' in suggestion:
588
+ print(f" 修复选项:")
589
+ for opt in suggestion['fix_options']:
590
+ print(f" - {opt}")
591
+
592
+ if 'recommended_fix' in suggestion:
593
+ print(f" 推荐修复: {suggestion['recommended_fix']}")
594
+ if 'conflict_details' in suggestion:
595
+ print(f" 冲突详情:")
596
+ for detail in suggestion['conflict_details']:
597
+ print(f" 第{detail['chapter']}章: {detail['description']}")
598
+
599
+ # Show fix report if applied
600
+ if args.apply and result.get('fix_report'):
601
+ report = result['fix_report']
602
+ print(f"\n{'='*60}")
603
+ print(f"自动修复报告:")
604
+ print(f"{'='*60}")
605
+ print(f"\n修复统计:")
606
+ print(f" 总问题数: {report['summary']['total_issues']}")
607
+ print(f" 真相文件修复: {report['summary']['truth_files_fixed']}")
608
+ print(f" 章节修复: {report['summary']['chapters_fixed']}")
609
+ print(f" 剩余问题: {report['summary']['remaining_issues']}")
610
+
611
+ if report['truth_file_fixes']:
612
+ print(f"\n真相文件修复详情:")
613
+ for fix in report['truth_file_fixes']:
614
+ status = fix.get('status', '已修复')
615
+ status_icon = '✓' if '已修复' in status else '○'
616
+ print(f" {status_icon} [{fix['dimension']}] 补全 {fix['count']} 个设定")
617
+ for term in fix['added_terms']:
618
+ print(f" - {term}")
619
+
620
+ if report['chapter_fixes']:
621
+ print(f"\n章节修复详情:")
622
+ for fix in report['chapter_fixes']:
623
+ print(f" [{fix['entity']}] 以第{fix['canonical_chapter']}章为准")
624
+ for ch_fix in fix['fixes']:
625
+ status_icon = '✓' if '已修复' in ch_fix['status'] else '○'
626
+ print(f" {status_icon} 第{ch_fix['chapter']}章: {ch_fix['replaced']} → {ch_fix['with']}")
627
+
628
+ if report['unfixed_issues']:
629
+ print(f"\n未修复问题(需人工处理):")
630
+ for issue in report['unfixed_issues'][:10]:
631
+ print(f" - [{issue['type']}] {issue['message']}")
632
+ else:
633
+ print(f"\n✓ 未检测到世界观一致性问题")
634
+
635
+
636
+ if __name__ == '__main__':
637
+ main()