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,165 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 项目统计报告
5
+ 快速扫描 novels/ 和 .novel-maker/ 目录,输出完整的写作进度统计。
6
+ 供 `/novel-maker stats` 指令使用。
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import json
12
+ from collections import Counter
13
+
14
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
15
+ from nm_utils import (
16
+ list_chapters, read_chapter, count_chinese,
17
+ extract_characters, extract_locations, estimate_pacing
18
+ )
19
+
20
+
21
+ def scan_volume(volume_path):
22
+ """Scan a single volume directory."""
23
+ vol_name = os.path.basename(volume_path)
24
+ chapters_dir = os.path.join(volume_path, 'chapters')
25
+ if not os.path.isdir(chapters_dir):
26
+ return None
27
+
28
+ chapter_files = list_chapters(chapters_dir)
29
+ if not chapter_files:
30
+ return None
31
+
32
+ chapters = []
33
+ all_chars = Counter()
34
+ all_locs = Counter()
35
+ pacing_dist = Counter()
36
+ total_words = 0
37
+
38
+ for idx, filepath in enumerate(chapter_files):
39
+ raw, title, clean, wc = read_chapter(filepath)
40
+ pacing_label, _ = estimate_pacing(raw)
41
+ chars = extract_characters(clean)
42
+ locs = extract_locations(clean)
43
+
44
+ chapters.append({
45
+ 'num': idx + 1,
46
+ 'title': title,
47
+ 'words': wc,
48
+ 'pacing': pacing_label,
49
+ 'characters': chars[:4],
50
+ })
51
+
52
+ total_words += wc
53
+ for c in chars:
54
+ all_chars[c] += 1
55
+ for loc in locs:
56
+ all_locs[loc] += 1
57
+ pacing_dist[pacing_label] += 1
58
+
59
+ return {
60
+ 'name': vol_name,
61
+ 'total_chapters': len(chapters),
62
+ 'total_words': total_words,
63
+ 'avg_words': round(total_words / max(len(chapters), 1), 0),
64
+ 'chapters': chapters,
65
+ 'main_characters': [name for name, _ in all_chars.most_common(8)],
66
+ 'character_appearances': dict(all_chars.most_common(10)),
67
+ 'main_locations': [loc for loc, _ in all_locs.most_common(5)],
68
+ 'pacing_distribution': dict(pacing_dist),
69
+ }
70
+
71
+
72
+ def scan_project(novels_dir):
73
+ """Scan the entire novels directory."""
74
+ if not os.path.isdir(novels_dir):
75
+ return {'error': f'目录不存在: {novels_dir}'}
76
+
77
+ # Check for outline
78
+ outline_path = os.path.join(novels_dir, 'outline.md')
79
+ has_outline = os.path.exists(outline_path)
80
+ outline_word_count = 0
81
+ if has_outline:
82
+ with open(outline_path, 'r', encoding='utf-8') as f:
83
+ outline_word_count = count_chinese(f.read())
84
+
85
+ # Scan volumes
86
+ volumes = []
87
+ for entry in sorted(os.listdir(novels_dir)):
88
+ vol_path = os.path.join(novels_dir, entry)
89
+ if os.path.isdir(vol_path) and entry.startswith('volume'):
90
+ result = scan_volume(vol_path)
91
+ if result:
92
+ volumes.append(result)
93
+
94
+ # Aggregate stats
95
+ total_chapters = sum(v['total_chapters'] for v in volumes)
96
+ total_words = sum(v['total_words'] for v in volumes)
97
+
98
+ return {
99
+ 'has_outline': has_outline,
100
+ 'outline_words': outline_word_count,
101
+ 'volumes': len(volumes),
102
+ 'total_chapters': total_chapters,
103
+ 'total_words': total_words,
104
+ 'volume_details': volumes,
105
+ }
106
+
107
+
108
+ def main():
109
+ import argparse
110
+ parser = argparse.ArgumentParser(description="Project stats report")
111
+ parser.add_argument("novels_dir", nargs='?', default='novels', help="Novels directory")
112
+ parser.add_argument("--json", action="store_true", help="Output as JSON")
113
+ parser.add_argument("--volume", type=str, help="Show only specific volume")
114
+ args = parser.parse_args()
115
+
116
+ novels_dir = args.novels_dir
117
+ if not os.path.isdir(novels_dir):
118
+ # Try to find novels dir
119
+ base = os.path.dirname(os.path.abspath(novels_dir))
120
+ alt = os.path.join(base, 'novels')
121
+ if os.path.isdir(alt):
122
+ novels_dir = alt
123
+ else:
124
+ print(f"错误: 目录不存在 - {novels_dir}", file=sys.stderr)
125
+ sys.exit(1)
126
+
127
+ result = scan_project(novels_dir)
128
+
129
+ if args.json:
130
+ print(json.dumps(result, ensure_ascii=False, indent=2))
131
+ return
132
+
133
+ if 'error' in result:
134
+ print(result['error'])
135
+ return
136
+
137
+ print(f"\n{'=' * 60}")
138
+ print(f"📚 NovelMaker 项目统计")
139
+ print(f"{'=' * 60}")
140
+ print(f"大纲: {'✅ 已生成' if result['has_outline'] else '❌ 未生成'} ({result['outline_words']}字)")
141
+ print(f"卷数: {result['volumes']}")
142
+ print(f"总章节: {result['total_chapters']}")
143
+ print(f"总字数: {result['total_words']:,}")
144
+ print()
145
+
146
+ for vol in result['volume_details']:
147
+ if args.volume and vol['name'] != args.volume:
148
+ continue
149
+ print(f" 📖 {vol['name']}")
150
+ print(f" 章节: {vol['total_chapters']} 章 | 字数: {vol['total_words']:,} | 平均: {vol['avg_words']}字/章")
151
+ print(f" 主要角色: {', '.join(vol['main_characters'][:5])}")
152
+ print(f" 主要场景: {', '.join(vol['main_locations'][:3])}")
153
+ pacing = vol['pacing_distribution']
154
+ pacing_str = ' | '.join(f"{k}:{v}章" for k, v in sorted(pacing.items()))
155
+ print(f" 节奏分布: {pacing_str}")
156
+
157
+ if args.volume == vol['name']:
158
+ print(f"\n 章节明细:")
159
+ for ch in vol['chapters']:
160
+ print(f" 第{ch['num']}章: {ch['title']} ({ch['words']}字, {ch['pacing']})")
161
+ print()
162
+
163
+
164
+ if __name__ == "__main__":
165
+ main()
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 卷级批量章节分析
5
+ 批处理卷内所有(或最近N章),输出卷级汇总 JSON。
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import json
11
+ from collections import Counter
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,
16
+ extract_locations, detect_hook, detect_hook_type_from_patterns,
17
+ count_chinese
18
+ )
19
+
20
+ FORESHADOWING_KEYWORDS = {
21
+ '血脉': ['血脉', '祖先', '传承', '遗传', '血统'],
22
+ '身世': ['身世', '出身', '来历', '母亲', '父亲'],
23
+ '预言': ['预言', '命中注定', '天意', '卦象', '命格'],
24
+ '阴谋': ['阴谋', '暗算', '布局', '棋局', '幕后'],
25
+ '身份': ['身份', '真实身份', '秘密', '隐藏'],
26
+ '仇敌': ['仇人', '仇敌', '恩怨', '血海深仇', '宿敌'],
27
+ }
28
+
29
+
30
+ def analyze_volume(chapters_dir, recent_n=None):
31
+ chapter_files = list_chapters(chapters_dir, recent_n)
32
+ if not chapter_files:
33
+ return {'error': f'No chapters found in {chapters_dir}'}
34
+
35
+ chapter_summaries = []
36
+ all_characters = Counter()
37
+ foreshadowing_counter = Counter()
38
+ hook_type_counter = Counter()
39
+ character_matrix = {}
40
+
41
+ for idx, filepath in enumerate(chapter_files):
42
+ raw, title, clean, wc = read_chapter(filepath)
43
+ chars = extract_characters(clean)
44
+ hook = detect_hook_type_from_patterns(raw)
45
+
46
+ chapter_num = idx + 1
47
+ chapter_summaries.append({
48
+ 'chapter': chapter_num,
49
+ 'title': title,
50
+ 'word_count': wc,
51
+ 'within_range': 2200 <= wc <= 2800,
52
+ 'characters': chars[:6],
53
+ 'hook_type': hook,
54
+ })
55
+
56
+ for c in chars:
57
+ all_characters[c] += 1
58
+ hook_type_counter[hook] += 1
59
+ character_matrix[f'ch{chapter_num}'] = chars
60
+
61
+ for keyword in clean:
62
+ for ftype, keywords in FORESHADOWING_KEYWORDS.items():
63
+ if any(kw in keyword for kw in keywords):
64
+ foreshadowing_counter[ftype] += 1
65
+ break
66
+
67
+ main_characters = [name for name, _ in all_characters.most_common(8)]
68
+
69
+ return {
70
+ 'total_chapters': len(chapter_files),
71
+ 'total_words': sum(s['word_count'] for s in chapter_summaries),
72
+ 'avg_words': round(sum(s['word_count'] for s in chapter_summaries) / max(len(chapter_summaries), 1), 0),
73
+ 'chapters': chapter_summaries,
74
+ 'main_characters': main_characters,
75
+ 'character_matrix': character_matrix,
76
+ 'foreshadowing_summary': dict(foreshadowing_counter),
77
+ 'hook_distribution': dict(hook_type_counter),
78
+ }
79
+
80
+
81
+ def main():
82
+ if len(sys.argv) < 2:
83
+ print('用法: python volume_batch.py <卷章节目录路径> [--recent N] [--json]')
84
+ return
85
+ chapters_dir = sys.argv[1]
86
+ if not os.path.isdir(chapters_dir):
87
+ print(f'错误: 目录不存在 - {chapters_dir}')
88
+ return
89
+ recent_n = None
90
+ json_only = False
91
+ if '--recent' in sys.argv:
92
+ idx = sys.argv.index('--recent')
93
+ if idx + 1 < len(sys.argv):
94
+ recent_n = int(sys.argv[idx + 1])
95
+ json_only = '--json' in sys.argv
96
+
97
+ result = analyze_volume(chapters_dir, recent_n)
98
+
99
+ if json_only:
100
+ print(json.dumps(result, ensure_ascii=False, indent=2))
101
+ else:
102
+ print(f"=== 卷级批量分析 ===\n")
103
+ if 'error' in result:
104
+ print(result['error'])
105
+ return
106
+ print(f"总章节: {result['total_chapters']} 章")
107
+ print(f"总字数: {result['total_words']} 字")
108
+ print(f"平均每章: {result['avg_words']} 字\n")
109
+ print(f"主要角色: {', '.join(result['main_characters'][:8])}\n")
110
+ print(f"钩子类型分布:")
111
+ for ht, count in sorted(result.get('hook_distribution', {}).items(), key=lambda x: x[1], reverse=True):
112
+ bar = "█" * count
113
+ print(f" {ht}: {bar} ({count})")
114
+ print(f"\n章节概览:")
115
+ for ch in result['chapters']:
116
+ status = "✓" if ch['within_range'] else "⚠"
117
+ print(f" {status} 第{ch['chapter']}章: {ch['title']} ({ch['word_count']}字, 角色: {', '.join(ch['characters'][:3])})")
118
+
119
+
120
+ if __name__ == '__main__':
121
+ main()
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 大纲快速提取
5
+ 解析 outline.md 和 vol*/plan.md 的标题层级,输出结构化大纲树。
6
+ 供 `/novel-maker memory outline` 指令使用。
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 parse_outline_headings, count_chinese
15
+
16
+
17
+ def extract_outline(filepath):
18
+ """Extract structured outline from a markdown file."""
19
+ if not os.path.exists(filepath):
20
+ return None
21
+ with open(filepath, 'r', encoding='utf-8') as f:
22
+ text = f.read()
23
+ wc = count_chinese(text)
24
+ tree = parse_outline_headings(text)
25
+ return {'file': os.path.basename(filepath), 'words': wc, 'headings': tree}
26
+
27
+
28
+ def extract_all_outlines(base_dir):
29
+ """Extract all outline-related files."""
30
+ results = {}
31
+
32
+ # Main outline
33
+ outline_path = os.path.join(base_dir, 'outline.md')
34
+ if os.path.exists(outline_path):
35
+ results['outline'] = extract_outline(outline_path)
36
+
37
+ # Volume plans
38
+ for entry in sorted(os.listdir(base_dir)):
39
+ vol_path = os.path.join(base_dir, entry)
40
+ if os.path.isdir(vol_path) and entry.startswith('volume'):
41
+ plan_path = os.path.join(vol_path, 'plan.md')
42
+ if os.path.exists(plan_path):
43
+ results[entry] = extract_outline(plan_path)
44
+
45
+ # Act plans
46
+ act_path = os.path.join(vol_path, 'act-plan.md')
47
+ if os.path.exists(act_path):
48
+ results[f"{entry}/act-plan"] = extract_outline(act_path)
49
+
50
+ return results
51
+
52
+
53
+ def main():
54
+ import argparse
55
+ parser = argparse.ArgumentParser(description="Outline extractor")
56
+ parser.add_argument("base_dir", nargs='?', default='novels', help="Novels base directory")
57
+ parser.add_argument("--json", action="store_true", help="Output as JSON")
58
+ parser.add_argument("--file", type=str, help="Specific file to extract")
59
+ args = parser.parse_args()
60
+
61
+ if args.file:
62
+ if not os.path.isfile(args.file):
63
+ print(f"Error: {args.file} not found", file=sys.stderr)
64
+ sys.exit(1)
65
+ result = {args.file: extract_outline(args.file)}
66
+ else:
67
+ if not os.path.isdir(args.base_dir):
68
+ print(f"Error: {args.base_dir} is not a directory", file=sys.stderr)
69
+ sys.exit(1)
70
+ result = extract_all_outlines(args.base_dir)
71
+
72
+ if args.json:
73
+ print(json.dumps(result, ensure_ascii=False, indent=2))
74
+ else:
75
+ for key, val in result.items():
76
+ if val is None:
77
+ print(f"\n❌ {key}: 文件不存在")
78
+ continue
79
+ print(f"\n{'=' * 50}")
80
+ print(f"📄 {key} ({val['words']}字, {len(val['headings'])}个标题)")
81
+ print(f"{'=' * 50}")
82
+ for h in val['headings']:
83
+ indent = " " * (h['level'] - 1)
84
+ marker = {1: '📖', 2: '📑', 3: '📝', 4: '•'}.get(h['level'], '•')
85
+ print(f"{indent}{marker} {h['title']}")
86
+
87
+
88
+ if __name__ == "__main__":
89
+ main()
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 规划师上下文包 — 一键生成规划所需的精简上下文
5
+
6
+ 将规划师需要读取的 10+ 个文件压缩为一个结构化 JSON(~5000 token),
7
+ 替代手动读取全部真相文件+大纲(~30,000 token)。
8
+
9
+ 用法:
10
+ python scripts/planner/planner_context.py --volume 01 --json
11
+ python scripts/planner/planner_context.py --volume 01 --act 2 --json
12
+ """
13
+
14
+ import argparse
15
+ import json
16
+ import os
17
+ import sys
18
+
19
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
20
+ from nm_utils import (
21
+ read_truth_section, list_chapters, read_chapter,
22
+ extract_characters, generate_summary, parse_outline_headings,
23
+ chapter_sort_key
24
+ )
25
+
26
+ NOVEL_MAKER_DIR = '.novel-maker'
27
+ TRUTH_DIR = os.path.join(NOVEL_MAKER_DIR, 'truth-files')
28
+
29
+
30
+ def _find_project_root(start_path):
31
+ path = os.path.abspath(start_path)
32
+ for _ in range(10):
33
+ if os.path.isdir(os.path.join(path, NOVEL_MAKER_DIR)):
34
+ return path
35
+ parent = os.path.dirname(path)
36
+ if parent == path:
37
+ break
38
+ path = parent
39
+ return None
40
+
41
+
42
+ def _read_truth_compact(filepath, max_chars=600):
43
+ sections = read_truth_section(filepath)
44
+ if not sections:
45
+ return None
46
+ result = {}
47
+ for title, content in sections.items():
48
+ if title == 'header':
49
+ continue
50
+ lines = [l.strip() for l in content.split('\n') if l.strip() and not l.startswith('#')]
51
+ compact = '\n'.join(lines[:10])
52
+ if len(compact) > max_chars:
53
+ compact = compact[:max_chars] + '...'
54
+ if compact:
55
+ result[title] = compact
56
+ return result
57
+
58
+
59
+ def _get_volume_progress(volume_dir):
60
+ chapters_dir = os.path.join(volume_dir, 'chapters')
61
+ if not os.path.isdir(chapters_dir):
62
+ return {'total': 0, 'chapters': []}
63
+ all_chapters = list_chapters(chapters_dir)
64
+ chapters = []
65
+ total_wc = 0
66
+ for ch_path in all_chapters:
67
+ raw, title, clean, wc = read_chapter(ch_path)
68
+ ch_num = chapter_sort_key(ch_path)
69
+ chars = extract_characters(clean)[:5]
70
+ summary = generate_summary(raw, first_n=50, last_n=50)
71
+ chapters.append({
72
+ 'number': ch_num,
73
+ 'title': title,
74
+ 'word_count': wc,
75
+ 'characters': chars,
76
+ 'summary': summary
77
+ })
78
+ total_wc += wc
79
+ return {
80
+ 'total': len(chapters),
81
+ 'total_word_count': total_wc,
82
+ 'chapters': chapters[-5:] # 只返回最近5章
83
+ }
84
+
85
+
86
+ def _get_outline_context(outline_path, current_chapter=None):
87
+ if not os.path.exists(outline_path):
88
+ return None
89
+ with open(outline_path, encoding='utf-8') as f:
90
+ text = f.read()
91
+ headings = parse_outline_headings(text)
92
+ if current_chapter:
93
+ result = []
94
+ found = False
95
+ for h in headings:
96
+ if f'第{current_chapter}章' in h['title']:
97
+ found = True
98
+ if found:
99
+ result.append(h)
100
+ if len(result) >= 10:
101
+ break
102
+ if not result:
103
+ result = headings[-10:] if len(headings) > 10 else headings
104
+ else:
105
+ result = headings[-10:] if len(headings) > 10 else headings
106
+ return [{'level': h['level'], 'title': h['title']} for h in result]
107
+
108
+
109
+ def _get_active_hooks(truth_dir):
110
+ fpath = os.path.join(truth_dir, 'pending-hooks.md')
111
+ sections = read_truth_section(fpath)
112
+ if not sections:
113
+ return []
114
+ hooks = []
115
+ for title, content in sections.items():
116
+ if 'header' in title or '已回收' in title or '已解决' in title:
117
+ continue
118
+ lines = [l.strip() for l in content.split('\n') if l.strip() and not l.startswith('#')]
119
+ for line in lines:
120
+ if 'H[' in line or '伏笔' in line or '钩子' in line:
121
+ hooks.append(line[:100])
122
+ return hooks[:10]
123
+
124
+
125
+ def _get_active_subplots(truth_dir):
126
+ fpath = os.path.join(truth_dir, 'subplot-board.md')
127
+ sections = read_truth_section(fpath)
128
+ if not sections:
129
+ return []
130
+ subplots = []
131
+ for title, content in sections.items():
132
+ if '活跃' in title:
133
+ lines = [l.strip() for l in content.split('\n') if l.strip() and not l.startswith('#')]
134
+ for line in lines:
135
+ if line.startswith('-') or line.startswith('*'):
136
+ subplots.append(line[:100])
137
+ return subplots[:5]
138
+
139
+
140
+ def build_planner_context(volume='01', act=None, root=None, truth_dir=None):
141
+ if root:
142
+ pass
143
+ else:
144
+ root = _find_project_root('.')
145
+ if not root:
146
+ return {'error': '未找到 .novel-maker 目录'}
147
+
148
+ truth_path = truth_dir or os.path.join(root, TRUTH_DIR)
149
+ volume_dir = os.path.join(root, 'novels', f'volume-{volume.zfill(2)}')
150
+
151
+ context = {
152
+ 'meta': {'volume': volume, 'act': act},
153
+ 'volume_progress': {},
154
+ 'truth_files': {},
155
+ 'outline': None,
156
+ 'active_hooks': [],
157
+ 'active_subplots': []
158
+ }
159
+
160
+ # 卷进度
161
+ context['volume_progress'] = _get_volume_progress(volume_dir)
162
+
163
+ # 真相文件摘要
164
+ truth_files = {
165
+ 'characters': 'characters.md',
166
+ 'current_state': 'current-state.md',
167
+ 'world_setting': 'world-setting.md',
168
+ 'power_system': 'power-system.md',
169
+ 'emotional_arcs': 'emotional-arcs.md',
170
+ }
171
+ for key, filename in truth_files.items():
172
+ fpath = os.path.join(truth_path, filename)
173
+ compact = _read_truth_compact(fpath)
174
+ if compact:
175
+ context['truth_files'][key] = compact
176
+
177
+ # 大纲
178
+ outline_path = os.path.join(volume_dir, 'outline.md')
179
+ current_ch = context['volume_progress']['total'] + 1 if context['volume_progress']['total'] > 0 else None
180
+ context['outline'] = _get_outline_context(outline_path, current_ch)
181
+
182
+ # 活跃伏笔
183
+ context['active_hooks'] = _get_active_hooks(truth_path)
184
+
185
+ # 活跃支线
186
+ context['active_subplots'] = _get_active_subplots(truth_path)
187
+
188
+ return context
189
+
190
+
191
+ def main():
192
+ parser = argparse.ArgumentParser(description='规划师上下文包')
193
+ parser.add_argument('--volume', '-v', default='01', help='卷号(默认01)')
194
+ parser.add_argument('--act', '-a', type=int, help='当前幕号')
195
+ parser.add_argument('--root', help='项目根目录')
196
+ parser.add_argument('--truth-dir', help='真相文件目录')
197
+ parser.add_argument('--json', action='store_true', help='JSON输出')
198
+ args = parser.parse_args()
199
+
200
+ ctx = build_planner_context(args.volume, args.act, args.root, args.truth_dir)
201
+
202
+ if args.json:
203
+ print(json.dumps(ctx, ensure_ascii=False, indent=2))
204
+ else:
205
+ if 'error' in ctx:
206
+ print(f"错误: {ctx['error']}")
207
+ return
208
+ print(f"=== 规划师上下文: 卷{ctx['meta']['volume']} ===\n")
209
+ prog = ctx['volume_progress']
210
+ print(f"进度: {prog['total']}章, {prog.get('total_word_count', 0)}字")
211
+ if ctx['truth_files']:
212
+ print(f"真相文件: {len(ctx['truth_files'])}个")
213
+ if ctx['active_hooks']:
214
+ print(f"活跃伏笔: {len(ctx['active_hooks'])}个")
215
+ if ctx['active_subplots']:
216
+ print(f"活跃支线: {len(ctx['active_subplots'])}条")
217
+
218
+
219
+ if __name__ == '__main__':
220
+ main()