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,207 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 支线追踪器
5
+ 追踪小说中的支线剧情发展状态,检测未回收的伏笔和断裂的支线。
6
+ 供复盘师在卷末复盘时使用,或规划师在规划新支线时参考。
7
+
8
+ 用法:
9
+ python scripts/reviewer/subplot_tracker.py 章节目录 --json
10
+ python scripts/reviewer/subplot_tracker.py 章节目录 --verbose
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, extract_characters, extract_locations,
23
+ detect_hook, count_chinese
24
+ )
25
+
26
+ # ─── 支线类型定义 ───────────────────────────────
27
+ SUBPLOT_PATTERNS = {
28
+ '感情线': {
29
+ 'keywords': ['喜欢', '爱', '情', '心动', '暗恋', '表白', '分手', '复合', '婚姻'],
30
+ 'indicators': ['两人独处', '眼神交流', '心跳加速', '脸红', '牵手']
31
+ },
32
+ '复仇线': {
33
+ 'keywords': ['仇', '恨', '报复', '凶手', '仇人', '血债', '讨回'],
34
+ 'indicators': ['调查真相', '寻找证据', '准备复仇', ' confrontation']
35
+ },
36
+ '成长线': {
37
+ 'keywords': ['修炼', '突破', '进阶', '领悟', '提升', '实力'],
38
+ 'indicators': ['闭关', '顿悟', '突破瓶颈', '新技能']
39
+ },
40
+ '阴谋线': {
41
+ 'keywords': ['阴谋', '计划', '暗中', '秘密', '隐藏', '真相'],
42
+ 'indicators': ['密谋', '布局', '暗中观察', '设局']
43
+ },
44
+ '冒险线': {
45
+ 'keywords': ['探险', '寻宝', '秘境', '遗迹', '未知', '危险'],
46
+ 'indicators': ['出发', '探索', '发现', '遭遇危险']
47
+ }
48
+ }
49
+
50
+
51
+ def detect_subplot_mentions(text):
52
+ """Detect subplot mentions in text."""
53
+ mentions = defaultdict(list)
54
+
55
+ for subplot_type, config in SUBPLOT_PATTERNS.items():
56
+ for keyword in config['keywords']:
57
+ if keyword in text:
58
+ mentions[subplot_type].append(keyword)
59
+ for indicator in config['indicators']:
60
+ if indicator in text:
61
+ mentions[subplot_type].append(f"[{indicator}]")
62
+
63
+ return dict(mentions)
64
+
65
+
66
+ def track_subplot_across_chapters(chapters_dir, recent_n=None):
67
+ """Track subplot development across chapters."""
68
+ chapter_files = list_chapters(chapters_dir, recent_n)
69
+ if not chapter_files:
70
+ return {'error': '未找到章节文件'}
71
+
72
+ subplot_timeline = defaultdict(list)
73
+ subplot_status = {}
74
+
75
+ for idx, cf in enumerate(chapter_files):
76
+ raw, title, clean, wc = read_chapter(cf)
77
+ ch_num = idx + 1
78
+
79
+ # Detect subplot mentions
80
+ mentions = detect_subplot_mentions(clean)
81
+
82
+ for subplot_type, keywords in mentions.items():
83
+ subplot_timeline[subplot_type].append({
84
+ 'chapter_num': ch_num,
85
+ 'chapter_title': title,
86
+ 'keywords': list(set(keywords))[:5], # Limit to 5 keywords
87
+ 'word_count': wc
88
+ })
89
+
90
+ # Analyze subplot status
91
+ for subplot_type, timeline in subplot_timeline.items():
92
+ total_chapters = len(timeline)
93
+ first_chapter = timeline[0]['chapter_num'] if timeline else 0
94
+ last_chapter = timeline[-1]['chapter_num'] if timeline else 0
95
+
96
+ # Determine status
97
+ if total_chapters == 0:
98
+ status = '未出现'
99
+ elif total_chapters == 1:
100
+ status = '刚引入'
101
+ elif last_chapter - first_chapter < 3:
102
+ status = '发展中'
103
+ elif total_chapters > len(chapter_files) * 0.3:
104
+ status = '主要支线'
105
+ else:
106
+ status = '间歇性出现'
107
+
108
+ subplot_status[subplot_type] = {
109
+ 'status': status,
110
+ 'total_chapters': total_chapters,
111
+ 'first_appearance': first_chapter,
112
+ 'last_appearance': last_chapter,
113
+ 'chapters_involved': [t['chapter_num'] for t in timeline],
114
+ 'timeline': timeline[-5:] # Last 5 appearances
115
+ }
116
+
117
+ return {
118
+ 'total_chapters_analyzed': len(chapter_files),
119
+ 'subplots_detected': len(subplot_timeline),
120
+ 'subplot_status': subplot_status,
121
+ 'subplot_timeline': dict(subplot_timeline)
122
+ }
123
+
124
+
125
+ def check_subplot_consistency(result):
126
+ """Check for subplot consistency issues."""
127
+ issues = []
128
+
129
+ for subplot_type, status in result.get('subplot_status', {}).items():
130
+ # Check for abandoned subplots
131
+ if status['status'] == '刚引入' and result['total_chapters_analyzed'] > 10:
132
+ issues.append({
133
+ 'type': '可能遗弃',
134
+ 'subplot': subplot_type,
135
+ 'message': f"{subplot_type}在第{status['first_appearance']}章引入后未再出现",
136
+ 'severity': 'warning'
137
+ })
138
+
139
+ # Check for inconsistent pacing
140
+ if status['total_chapters'] > 0:
141
+ chapters = status['chapters_involved']
142
+ gaps = [chapters[i+1] - chapters[i] for i in range(len(chapters)-1)]
143
+ if gaps and max(gaps) > 5:
144
+ issues.append({
145
+ 'type': '节奏不均',
146
+ 'subplot': subplot_type,
147
+ 'message': f"{subplot_type}章节间隔过大(最大{max(gaps)}章)",
148
+ 'severity': 'info'
149
+ })
150
+
151
+ return issues
152
+
153
+
154
+ def main():
155
+ parser = argparse.ArgumentParser(description='支线追踪器')
156
+ parser.add_argument('chapters_dir', help='章节目录路径')
157
+ parser.add_argument('--recent', type=int, help='仅分析最近N章')
158
+ parser.add_argument('--json', action='store_true', help='JSON输出')
159
+ parser.add_argument('--verbose', '-v', action='store_true', help='显示详细信息')
160
+ args = parser.parse_args()
161
+
162
+ if not os.path.isdir(args.chapters_dir):
163
+ print(f"错误: 目录不存在: {args.chapters_dir}", file=sys.stderr)
164
+ sys.exit(1)
165
+
166
+ result = track_subplot_across_chapters(args.chapters_dir, args.recent)
167
+
168
+ # Add consistency check
169
+ if not args.json:
170
+ issues = check_subplot_consistency(result)
171
+ result['consistency_issues'] = issues
172
+
173
+ if args.json:
174
+ print(json.dumps(result, ensure_ascii=False, indent=2))
175
+ else:
176
+ if 'error' in result:
177
+ print(result['error'])
178
+ return
179
+
180
+ print(f"\n=== 支线追踪报告 ===\n")
181
+ print(f"分析章节数: {result['total_chapters_analyzed']}")
182
+ print(f"检测支线数: {result['subplots_detected']}")
183
+
184
+ print(f"\n支线状态:")
185
+ for subplot_type, status in result['subplot_status'].items():
186
+ icon = '●' if status['status'] == '主要支线' else '○'
187
+ print(f" {icon} {subplot_type}: {status['status']}")
188
+ print(f" 出场章数: {status['total_chapters']}, 首次: 第{status['first_appearance']}章, 最近: 第{status['last_appearance']}章")
189
+
190
+ if result.get('consistency_issues'):
191
+ print(f"\n一致性问题:")
192
+ for issue in result['consistency_issues']:
193
+ icon = '' if issue['severity'] == 'warning' else 'ℹ'
194
+ print(f" {icon} {issue['subplot']}: {issue['message']}")
195
+
196
+ if args.verbose and result.get('subplot_timeline'):
197
+ print(f"\n详细时间线:")
198
+ for subplot_type, timeline in result['subplot_timeline'].items():
199
+ print(f"\n {subplot_type}:")
200
+ for entry in timeline[-5:]: # Last 5 entries
201
+ print(f" 第{entry['chapter_num']}章: {entry['chapter_title']}")
202
+ if entry['keywords']:
203
+ print(f" 关键词: {', '.join(entry['keywords'])}")
204
+
205
+
206
+ if __name__ == '__main__':
207
+ main()
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 阶段总结辅助脚本
5
+ 从各章结构化数据中提取摘要,拼接剧情时间线。
6
+ 供 `/novel-maker summary` 指令使用,AI 读取此输出代替读全文。
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import json
12
+
13
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
14
+ from nm_utils import (
15
+ list_chapters, read_chapter, extract_characters, extract_locations,
16
+ detect_hook, count_chinese, clean_markdown
17
+ )
18
+
19
+
20
+ def build_chapter_timeline(chapter_path):
21
+ """Extract a structured timeline entry from a single chapter."""
22
+ raw, title, clean, wc = read_chapter(chapter_path)
23
+ chars = extract_characters(clean)
24
+ locs = extract_locations(clean)
25
+ hook = detect_hook(raw)
26
+
27
+ # Extract first and last paragraph summaries
28
+ paragraphs = [p.strip() for p in clean.split('\n') if p.strip()]
29
+ first_para = paragraphs[0][:100] if paragraphs else ''
30
+ last_para = paragraphs[-1][-150:] if paragraphs else ''
31
+
32
+ return {
33
+ 'title': title,
34
+ 'words': wc,
35
+ 'characters': chars[:6],
36
+ 'locations': locs[:3],
37
+ 'hook_type': hook['type'],
38
+ 'first_para_summary': first_para,
39
+ 'last_para_summary': last_para,
40
+ }
41
+
42
+
43
+ def generate_summary(chapters_dir, start_n=1, end_n=None):
44
+ """Generate a structured summary for chapters start_n through end_n."""
45
+ all_chapters = list_chapters(chapters_dir)
46
+ if not all_chapters:
47
+ return {'error': 'No chapters found'}
48
+
49
+ if end_n is None:
50
+ end_n = len(all_chapters)
51
+ start_idx = max(0, start_n - 1)
52
+ end_idx = min(len(all_chapters), end_n)
53
+ target_chapters = all_chapters[start_idx:end_idx]
54
+
55
+ timeline = []
56
+ all_chars = set()
57
+ all_locs = set()
58
+
59
+ for cf in target_chapters:
60
+ entry = build_chapter_timeline(cf)
61
+ timeline.append(entry)
62
+ all_chars.update(entry['characters'])
63
+ all_locs.update(entry['locations'])
64
+
65
+ total_words = sum(t['words'] for t in timeline)
66
+
67
+ return {
68
+ 'chapter_range': f"第{start_n}-{start_n + len(timeline) - 1}章",
69
+ 'total_chapters': len(timeline),
70
+ 'total_words': total_words,
71
+ 'avg_words': round(total_words / max(len(timeline), 1), 0),
72
+ 'timeline': timeline,
73
+ 'characters_involved': sorted(all_chars),
74
+ 'locations_visited': sorted(all_locs),
75
+ }
76
+
77
+
78
+ def main():
79
+ import argparse
80
+ parser = argparse.ArgumentParser(description="Summary generator")
81
+ parser.add_argument("chapters_dir", help="Chapters directory")
82
+ parser.add_argument("--range", type=str, help="Chapter range, e.g. '1-10' or '11-20'", default=None)
83
+ parser.add_argument("--last", type=int, help="Last N chapters", default=None)
84
+ parser.add_argument("--json", action="store_true", help="Output as JSON")
85
+ args = parser.parse_args()
86
+
87
+ if not os.path.isdir(args.chapters_dir):
88
+ print(f"Error: {args.chapters_dir} is not a directory", file=sys.stderr)
89
+ sys.exit(1)
90
+
91
+ start_n = 1
92
+ end_n = None
93
+
94
+ if args.range:
95
+ parts = args.range.split('-')
96
+ if len(parts) == 2:
97
+ start_n = int(parts[0])
98
+ end_n = int(parts[1])
99
+ elif args.last:
100
+ all_chapters = list_chapters(args.chapters_dir)
101
+ start_n = max(1, len(all_chapters) - args.last + 1)
102
+
103
+ result = generate_summary(args.chapters_dir, start_n, end_n)
104
+
105
+ if args.json:
106
+ print(json.dumps(result, ensure_ascii=False, indent=2))
107
+ else:
108
+ if 'error' in result:
109
+ print(result['error'])
110
+ return
111
+
112
+ print(f"\n{'=' * 60}")
113
+ print(f"📋 阶段总结 ({result['chapter_range']})")
114
+ print(f"{'=' * 60}")
115
+ print(f"章节数: {result['total_chapters']}")
116
+ print(f"总字数: {result['total_words']:,}")
117
+ print(f"平均字数: {result['avg_words']}/章")
118
+ print(f"涉及角色: {', '.join(result['characters_involved'][:8])}")
119
+ print(f"涉及场景: {', '.join(result['locations_visited'][:5])}")
120
+ print(f"\n剧情时间线:")
121
+ for t in result['timeline']:
122
+ print(f"\n {t['title']} ({t['words']}字)")
123
+ print(f" 角色: {', '.join(t['characters'][:4])}")
124
+ print(f" 开头: {t['first_para_summary'][:60]}...")
125
+ print(f" 结尾: ...{t['last_para_summary'][-60:]}")
126
+ print(f" 钩子: {t['hook_type']}")
127
+
128
+
129
+ if __name__ == "__main__":
130
+ main()
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 真相文件变更检测 — 自动检测章节变更并生成更新 diff
5
+
6
+ 对比定稿章节与真相文件,检测需要更新的内容(新角色、新地点、
7
+ 伏笔变化、情感变化等),输出结构化 diff 供复盘师审核。
8
+
9
+ 用法:
10
+ python scripts/reviewer/truth_diff.py ch15.md --truth-dir .novel-maker/truth-files/
11
+ python scripts/reviewer/truth_diff.py ch15.md --prev ch14.md --truth-dir .novel-maker/truth-files/ --json
12
+ """
13
+
14
+ import argparse
15
+ import json
16
+ import os
17
+ import re
18
+ import sys
19
+
20
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
21
+ from nm_utils import (
22
+ read_chapter, extract_characters, extract_locations,
23
+ detect_hook, clean_markdown, read_truth_section
24
+ )
25
+
26
+
27
+ def _load_known_names(truth_dir, filename='characters.md'):
28
+ """从真相文件中提取已知角色名"""
29
+ fpath = os.path.join(truth_dir, filename)
30
+ sections = read_truth_section(fpath)
31
+ if not sections:
32
+ return set()
33
+ names = set()
34
+ for title, content in sections.items():
35
+ for m in re.finditer(r'\*\*([^*]{1,10})\*\*', content):
36
+ name = m.group(1).strip()
37
+ if len(name) >= 2 and not any(c in name for c in '()()'):
38
+ names.add(name)
39
+ for m in re.finditer(r'[::]\s*([^\s,,。]{2,6})', content):
40
+ name = m.group(1).strip()
41
+ if len(name) >= 2:
42
+ names.add(name)
43
+ return names
44
+
45
+
46
+ def _load_known_hooks(truth_dir, filename='pending-hooks.md'):
47
+ """从伏笔表中提取已知伏笔"""
48
+ fpath = os.path.join(truth_dir, filename)
49
+ sections = read_truth_section(fpath)
50
+ if not sections:
51
+ return {'pending': [], 'recovered': []}
52
+ hooks = {'pending': [], 'recovered': []}
53
+ for title, content in sections.items():
54
+ if '已回收' in title or '已解决' in title:
55
+ for m in re.finditer(r'H\[(\d+)\]', content):
56
+ hooks['recovered'].append(m.group(1))
57
+ elif 'header' not in title:
58
+ for m in re.finditer(r'H\[(\d+)\]', content):
59
+ hooks['pending'].append(m.group(1))
60
+ return hooks
61
+
62
+
63
+ def _load_known_locations(truth_dir, filename='world-setting.md'):
64
+ """从世界观设定中提取已知地点"""
65
+ fpath = os.path.join(truth_dir, filename)
66
+ sections = read_truth_section(fpath)
67
+ if not sections:
68
+ return set()
69
+ locs = set()
70
+ for title, content in sections.items():
71
+ if '地' in title or '区域' in title or '地点' in title:
72
+ for m in re.finditer(r'\*\*([^*]{2,8})\*\*', content):
73
+ locs.add(m.group(1))
74
+ return locs
75
+
76
+
77
+ def _detect_hook_keywords(text):
78
+ """检测伏笔相关关键词"""
79
+ hook_keywords = {
80
+ 'reveal': ['原来', '竟然', '居然', '真相', '秘密', '身份'],
81
+ 'setup': ['记住', '将来', '总有一天', '等着', '约定'],
82
+ 'foreshadow': ['似乎', '隐约', '感觉', '仿佛', '好像'],
83
+ }
84
+ found = {}
85
+ for htype, keywords in hook_keywords.items():
86
+ matches = [kw for kw in keywords if kw in text]
87
+ if matches:
88
+ found[htype] = matches
89
+ return found
90
+
91
+
92
+ def _detect_emotion_keywords(text):
93
+ """检测情感关键词"""
94
+ emotion_map = {
95
+ '愤怒': ['怒', '火', '恨', '咬牙', '攥拳', '瞪'],
96
+ '悲伤': ['泪', '哭', '泣', '哽咽', '心痛', '悲伤'],
97
+ '喜悦': ['笑', '喜', '乐', '高兴', '兴奋', '激动'],
98
+ '恐惧': ['怕', '恐', '惊', '颤', '抖', '畏惧'],
99
+ '惊讶': ['惊', '没想到', '居然', '竟然', '不敢相信'],
100
+ '平静': ['平静', '淡然', '从容', '沉稳', '泰然'],
101
+ }
102
+ found = {}
103
+ for emotion, keywords in emotion_map.items():
104
+ count = sum(1 for kw in keywords if kw in text)
105
+ if count >= 2:
106
+ found[emotion] = count
107
+ return found
108
+
109
+
110
+ def detect_changes(chapter_path, truth_dir, prev_chapter_path=None):
111
+ """检测章节变更"""
112
+ if not os.path.exists(chapter_path):
113
+ return {'error': f'章节文件不存在: {chapter_path}'}
114
+ if not os.path.isdir(truth_dir):
115
+ return {'error': f'真相文件目录不存在: {truth_dir}'}
116
+
117
+ raw, title, clean, wc = read_chapter(chapter_path)
118
+ changes = {
119
+ 'chapter': os.path.basename(chapter_path),
120
+ 'title': title,
121
+ 'word_count': wc,
122
+ 'detected': {}
123
+ }
124
+
125
+ # 1. 新角色检测
126
+ known_chars = _load_known_names(truth_dir)
127
+ chapter_chars = set(extract_characters(clean))
128
+ new_chars = chapter_chars - known_chars
129
+ if new_chars:
130
+ changes['detected']['new_characters'] = sorted(new_chars)
131
+
132
+ # 2. 新地点检测
133
+ known_locs = _load_known_locations(truth_dir)
134
+ chapter_locs = set(extract_locations(clean))
135
+ new_locs = chapter_locs - known_locs
136
+ if new_locs:
137
+ changes['detected']['new_locations'] = sorted(new_locs)
138
+
139
+ # 3. 伏笔检测
140
+ hook_keywords = _detect_hook_keywords(clean)
141
+ if hook_keywords:
142
+ changes['detected']['hook_keywords'] = hook_keywords
143
+
144
+ # 4. 章末钩子
145
+ hook = detect_hook(raw)
146
+ if hook['type'] not in ('未检测', '未知'):
147
+ changes['detected']['chapter_hook'] = {
148
+ 'type': hook['type'],
149
+ 'tail_preview': hook.get('tail_preview', '')
150
+ }
151
+
152
+ # 5. 情感变化
153
+ emotions = _detect_emotion_keywords(clean)
154
+ if emotions:
155
+ changes['detected']['emotions'] = emotions
156
+
157
+ # 6. 与前章对比
158
+ if prev_chapter_path and os.path.exists(prev_chapter_path):
159
+ prev_raw, prev_title, prev_clean, prev_wc = read_chapter(prev_chapter_path)
160
+ prev_chars = set(extract_characters(prev_clean))
161
+ prev_locs = set(extract_locations(prev_clean))
162
+ continuing_chars = chapter_chars & prev_chars
163
+ new_in_chapter = chapter_chars - prev_chars
164
+ changes['detected']['continuing_characters'] = sorted(continuing_chars)
165
+ if new_in_chapter:
166
+ changes['detected']['new_vs_prev'] = sorted(new_in_chapter)
167
+
168
+ # 7. 伏笔表状态
169
+ known_hooks = _load_known_hooks(truth_dir)
170
+ changes['detected']['hook_status'] = {
171
+ 'pending_count': len(known_hooks['pending']),
172
+ 'recovered_count': len(known_hooks['recovered'])
173
+ }
174
+
175
+ # 汇总
176
+ change_count = len([v for v in changes['detected'].values() if v])
177
+ changes['summary'] = {
178
+ 'total_changes': change_count,
179
+ 'has_new_characters': 'new_characters' in changes['detected'],
180
+ 'has_new_locations': 'new_locations' in changes['detected'],
181
+ 'has_hook_keywords': 'hook_keywords' in changes['detected'],
182
+ 'has_emotions': 'emotions' in changes['detected'],
183
+ }
184
+
185
+ return changes
186
+
187
+
188
+ def main():
189
+ parser = argparse.ArgumentParser(description='真相文件变更检测')
190
+ parser.add_argument('chapter', help='定稿章节文件路径')
191
+ parser.add_argument('--truth-dir', '-t', required=True, help='真相文件目录')
192
+ parser.add_argument('--prev', '-p', help='前一章文件路径')
193
+ parser.add_argument('--json', action='store_true', help='JSON输出')
194
+ args = parser.parse_args()
195
+
196
+ changes = detect_changes(args.chapter, args.truth_dir, args.prev)
197
+
198
+ if args.json:
199
+ print(json.dumps(changes, ensure_ascii=False, indent=2))
200
+ else:
201
+ if 'error' in changes:
202
+ print(f"错误: {changes['error']}")
203
+ return
204
+ print(f"=== 变更检测: {changes['chapter']} ({changes['title']}) ===\n")
205
+
206
+ detected = changes['detected']
207
+ if 'new_characters' in detected:
208
+ print(f"新角色: {', '.join(detected['new_characters'])}")
209
+ if 'new_locations' in detected:
210
+ print(f"新地点: {', '.join(detected['new_locations'])}")
211
+ if 'hook_keywords' in detected:
212
+ for htype, keywords in detected['hook_keywords'].items():
213
+ print(f"伏笔({htype}): {', '.join(keywords)}")
214
+ if 'chapter_hook' in detected:
215
+ print(f"章末钩子: {detected['chapter_hook']['type']}")
216
+ if 'emotions' in detected:
217
+ for emotion, count in detected['emotions'].items():
218
+ print(f"情感({emotion}): {count}次")
219
+ if 'continuing_characters' in detected:
220
+ print(f"延续角色: {', '.join(detected['continuing_characters'])}")
221
+
222
+ if changes['summary']['total_changes'] == 0:
223
+ print("未检测到需要更新的内容")
224
+
225
+
226
+ if __name__ == '__main__':
227
+ main()
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 真相文件管理器
5
+ 解析 truth-files 目录,快速查看角色、伏笔、世界观、力量体系等实体信息。
6
+ 供 `/novel-maker memory entity` 指令使用。
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import json
12
+ import re
13
+
14
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
15
+ from nm_utils import read_truth_section, count_chinese
16
+
17
+
18
+ TRUTH_FILES = {
19
+ 'characters': '角色档案',
20
+ 'world-setting': '世界观',
21
+ 'power-system': '力量体系',
22
+ 'current-state': '当前状态',
23
+ 'pending-hooks': '伏笔表',
24
+ 'constitution': '创作宪法',
25
+ }
26
+
27
+
28
+ def scan_truth_files(truth_dir):
29
+ """Scan all truth files and return structured data."""
30
+ if not os.path.isdir(truth_dir):
31
+ return {'error': f'目录不存在: {truth_dir}'}
32
+
33
+ result = {}
34
+ for key, label in TRUTH_FILES.items():
35
+ filepath = os.path.join(truth_dir, f'{key}.md')
36
+ if os.path.exists(filepath):
37
+ with open(filepath, 'r', encoding='utf-8') as f:
38
+ text = f.read()
39
+ wc = count_chinese(text)
40
+ sections = read_truth_section(filepath)
41
+ result[key] = {
42
+ 'label': label,
43
+ 'words': wc,
44
+ 'sections': list(sections.keys()) if sections else [],
45
+ 'sections_content': sections,
46
+ }
47
+
48
+ # Extract character names from characters.md
49
+ if 'characters' in result:
50
+ content = result['characters'].get('sections_content', {})
51
+ char_names = []
52
+ for section_text in content.values():
53
+ for m in re.finditer(r'[*\-]?\s*\*\*?([\u4e00-\u9fff]{2,4})\*\*?', section_text):
54
+ name = m.group(1)
55
+ if name not in ('角色', '人物', '主角', '配角', '反派', '姓名'):
56
+ char_names.append(name)
57
+ result['characters']['extracted_names'] = list(set(char_names))[:20]
58
+
59
+ # Extract hook info from pending-hooks.md
60
+ if 'pending-hooks' in result:
61
+ content = result['pending-hooks'].get('sections_content', {})
62
+ hooks = []
63
+ for section_text in content.values():
64
+ for m in re.finditer(r'\[([^\]]+)\]', section_text):
65
+ hooks.append(m.group(1))
66
+ result['pending-hooks']['extracted_hooks'] = hooks[:10]
67
+
68
+ return result
69
+
70
+
71
+ def main():
72
+ import argparse
73
+ parser = argparse.ArgumentParser(description="Truth files manager")
74
+ parser.add_argument("truth_dir", nargs='?', default=None, help="Truth files directory")
75
+ parser.add_argument("--json", action="store_true", help="Output as JSON")
76
+ parser.add_argument("--entity", type=str, help="Show specific entity (characters/world/power/hooks/state)")
77
+ args = parser.parse_args()
78
+
79
+ truth_dir = args.truth_dir
80
+ if truth_dir is None:
81
+ # Try common locations
82
+ for candidate in ['.novel-maker/truth-files', 'novels/truth-files', 'truth-files']:
83
+ if os.path.isdir(candidate):
84
+ truth_dir = candidate
85
+ break
86
+ if truth_dir is None:
87
+ print("错误: 未找到 truth-files 目录,请指定路径", file=sys.stderr)
88
+ sys.exit(1)
89
+
90
+ result = scan_truth_files(truth_dir)
91
+
92
+ if args.json:
93
+ print(json.dumps(result, ensure_ascii=False, indent=2))
94
+ return
95
+
96
+ if 'error' in result:
97
+ print(result['error'])
98
+ return
99
+
100
+ print(f"\n{'=' * 60}")
101
+ print(f"📁 真相文件管理 ({truth_dir})")
102
+ print(f"{'=' * 60}\n")
103
+
104
+ for key, info in result.items():
105
+ if args.entity and key != args.entity:
106
+ continue
107
+ print(f"📄 {info['label']} ({key}.md, {info['words']}字)")
108
+ if info['sections']:
109
+ print(f" 章节: {', '.join(info['sections'])}")
110
+ if key == 'characters' and info.get('extracted_names'):
111
+ print(f" 角色: {', '.join(info['extracted_names'][:10])}")
112
+ if key == 'pending-hooks' and info.get('extracted_hooks'):
113
+ print(f" 伏笔 ({len(info['extracted_hooks'])}条):")
114
+ for h in info['extracted_hooks'][:5]:
115
+ print(f" • {h}")
116
+ print()
117
+
118
+
119
+ if __name__ == "__main__":
120
+ main()