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,169 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ NovelMaker 安装脚本(Python 版)
4
+
5
+ 当 npx 不可用时的备选安装方案。
6
+
7
+ 使用方式:
8
+ python scripts/common/install.py [--ide trae|claude|cursor|...] [--uninstall]
9
+ """
10
+
11
+ import sys
12
+ import argparse
13
+ from pathlib import Path
14
+
15
+
16
+ # 支持的 IDE 列表
17
+ TARGETS = [
18
+ {"name": "Claude Code", "dir": ".claude/skills/novel-maker", "detect": [".claude"]},
19
+ {"name": "Cursor", "dir": ".cursor/rules/novel-maker", "detect": [".cursor", ".cursorrules"]},
20
+ {"name": "Trae", "dir": ".trae/skills/novel-maker", "detect": [".trae"]},
21
+ {"name": "Windsurf", "dir": ".windsurf/skills/novel-maker", "detect": [".windsurf"]},
22
+ {"name": "Gemini CLI", "dir": ".gemini/skills/novel-maker", "detect": ["GEMINI.md"]},
23
+ {"name": "Codex CLI", "dir": ".codex/skills/novel-maker", "detect": [".codex"]},
24
+ {"name": "OpenCode", "dir": ".opencode/skills/novel-maker", "detect": [".opencode"]},
25
+ {"name": "Aider", "dir": ".aider/skills/novel-maker", "detect": [".aider"]},
26
+ {"name": "Hermes Agent", "dir": ".hermes/skills/novel-maker", "detect": [".hermes", "HERMES.md"]},
27
+ {"name": "Qwen Code", "dir": ".qwen/skills/novel-maker", "detect": [".qwen"]},
28
+ {"name": "Claw Code", "dir": ".claw/skills/novel-maker", "detect": [".claw", "CLAW.md"]},
29
+ {"name": "Qoder", "dir": ".qoder/skills/novel-maker", "detect": [".qoder"]},
30
+ {"name": "Antigravity", "dir": ".agents/skills/novel-maker", "detect": [".agents"]},
31
+ {"name": "OpenClaw", "dir": "skills/novel-maker", "detect": [".openclaw"]},
32
+ {"name": "Kiro", "dir": ".kiro/steering/novel-maker", "detect": [".kiro"]},
33
+ {"name": "VS Code", "dir": ".github/superpowers/novel-maker", "detect": [".github/copilot-instructions.md"]},
34
+ {"name": "DeerFlow", "dir": "skills/custom/novel-maker", "detect": ["deer_flow"]},
35
+ {"name": "Copilot CLI", "dir": ".claude/skills/novel-maker", "detect": [".claude"]},
36
+ ]
37
+
38
+ # 工具别名
39
+ TOOL_ALIASES = {
40
+ "claude": "Claude Code",
41
+ "cursor": "Cursor",
42
+ "trae": "Trae",
43
+ "windsurf": "Windsurf",
44
+ "gemini": "Gemini CLI",
45
+ "codex": "Codex CLI",
46
+ "opencode": "OpenCode",
47
+ "aider": "Aider",
48
+ "hermes": "Hermes Agent",
49
+ "qwen": "Qwen Code",
50
+ "claw": "Claw Code",
51
+ "qoder": "Qoder",
52
+ "antigravity": "Antigravity",
53
+ "openclaw": "OpenClaw",
54
+ "kiro": "Kiro",
55
+ }
56
+
57
+
58
+ def detect_ides():
59
+ """检测当前项目中的 IDE"""
60
+ detected = []
61
+ cwd = Path.cwd()
62
+
63
+ for target in TARGETS:
64
+ for detect in target["detect"]:
65
+ if (cwd / detect).exists():
66
+ detected.append(target)
67
+ break
68
+
69
+ return detected
70
+
71
+
72
+ def copy_dir(src, dest):
73
+ """递归复制目录"""
74
+ import shutil
75
+ src = Path(src)
76
+ dest = Path(dest)
77
+ dest.mkdir(parents=True, exist_ok=True)
78
+
79
+ for item in src.iterdir():
80
+ if item.name == '.DS_Store':
81
+ continue
82
+ dest_item = dest / item.name
83
+ if item.is_dir():
84
+ copy_dir(item, dest_item)
85
+ else:
86
+ shutil.copy2(item, dest_item)
87
+
88
+
89
+ def install_skill(target):
90
+ """安装技能到指定 IDE"""
91
+ cwd = Path.cwd()
92
+ skill_dir = cwd / "skill"
93
+
94
+ if not skill_dir.exists():
95
+ print(f"❌ 错误:未找到 skill/ 目录")
96
+ return False
97
+
98
+ dest_dir = cwd / target["dir"]
99
+ print(f" 安装到 {target['name']}: {target['dir']}")
100
+
101
+ try:
102
+ copy_dir(skill_dir, dest_dir)
103
+ print(f" ✅ {target['name']} 安装成功")
104
+ return True
105
+ except Exception as e:
106
+ print(f" ❌ {target['name']} 安装失败: {e}")
107
+ return False
108
+
109
+
110
+ def uninstall_skill():
111
+ """卸载技能"""
112
+ import shutil
113
+ cwd = Path.cwd()
114
+
115
+ print("🗑️ 卸载 NovelMaker...\n")
116
+
117
+ for target in TARGETS:
118
+ dest_dir = cwd / target["dir"]
119
+ if dest_dir.exists():
120
+ shutil.rmtree(dest_dir)
121
+ print(f" ✅ {target['name']} 已卸载")
122
+
123
+ print("\n✅ 卸载完成")
124
+
125
+
126
+ def main():
127
+ parser = argparse.ArgumentParser(description="NovelMaker 安装脚本")
128
+ parser.add_argument("--ide", help="指定 IDE 的类型")
129
+ parser.add_argument("--uninstall", action="store_true", help="卸载技能")
130
+ args = parser.parse_args()
131
+
132
+ print("\n NovelMaker v2.0.0 - 全能网文写作助手")
133
+ print(" 6角色协作架构,用说话的方式写小说\n")
134
+
135
+ if args.uninstall:
136
+ uninstall_skill()
137
+ return
138
+
139
+ if args.ide:
140
+ tool_name = TOOL_ALIASES.get(args.ide.lower())
141
+ if not tool_name:
142
+ print(f"❌ 未知的工具: {args.ide}")
143
+ print(f"支持的: {', '.join(TOOL_ALIASES.keys())}")
144
+ sys.exit(1)
145
+ targets = [t for t in TARGETS if t["name"] == tool_name]
146
+ else:
147
+ targets = detect_ides()
148
+
149
+ if not targets:
150
+ print("❌ 未检测到 IDE,请使用 --ide 参数指定")
151
+ print(f"支持的: {', '.join(TOOL_ALIASES.keys())}")
152
+ sys.exit(1)
153
+
154
+ print(f"检测到 {len(targets)} 个 IDE:\n")
155
+ for t in targets:
156
+ print(f" - {t['name']}")
157
+ print()
158
+
159
+ for target in targets:
160
+ install_skill(target)
161
+
162
+ print("\n✅ 安装完成!")
163
+ print("\n在 IDE 聊天框输入以下指令开始创作:")
164
+ print(" /novel-maker init 开始写小说")
165
+ print(" /novel-maker help 查看帮助\n")
166
+
167
+
168
+ if __name__ == "__main__":
169
+ main()
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ NovelMaker 脚本公共工具模块
5
+ 提供所有脚本共用的函数,消除重复代码。
6
+ """
7
+
8
+ import os
9
+ import re
10
+ import sys
11
+ import json
12
+ from collections import Counter
13
+
14
+ # ─── 字符编码兼容 ───────────────────────────────
15
+ if sys.platform == 'win32':
16
+ import io
17
+ if hasattr(sys.stdout, 'buffer'):
18
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
19
+ if hasattr(sys.stderr, 'buffer'):
20
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
21
+
22
+ # ─── Markdown 清理 ───────────────────────────────
23
+ _MARKDOWN_PATTERNS = [
24
+ (r'#{1,6}\s*', ''),
25
+ (r'\*\*(.*?)\*\*', r'\1'),
26
+ (r'\*(.*?)\*', r'\1'),
27
+ (r'~~(.*?)~~', r'\1'),
28
+ (r'`(.*?)`', r'\1'),
29
+ (r'\[(.*?)\]\(.*?\)', r'\1'),
30
+ (r'>\s?', ''),
31
+ (r'-{3,}', ''),
32
+ (r'\*{3,}', ''),
33
+ ]
34
+
35
+
36
+ def clean_markdown(text):
37
+ """Remove markdown formatting from text."""
38
+ for pattern, repl in _MARKDOWN_PATTERNS:
39
+ text = re.sub(pattern, repl, text)
40
+ return text
41
+
42
+
43
+ def count_chinese(text):
44
+ """Count Chinese characters in text."""
45
+ return len(re.findall(r'[\u4e00-\u9fff]', text))
46
+
47
+
48
+ def extract_title(content):
49
+ """Extract chapter title from markdown heading."""
50
+ m = re.search(r'^#\s*(.+)$', content, re.MULTILINE)
51
+ return m.group(1).strip() if m else None
52
+
53
+
54
+ def read_chapter(filepath):
55
+ """Read a chapter file and return (raw_text, title, clean_text, word_count)."""
56
+ with open(filepath, 'r', encoding='utf-8') as f:
57
+ raw = f.read()
58
+ title = extract_title(raw)
59
+ clean = clean_markdown(raw)
60
+ wc = count_chinese(clean)
61
+ return raw, title, clean, wc
62
+
63
+
64
+ def extract_content_from_chapter(file_path):
65
+ """Extract body content after the chapter heading line."""
66
+ with open(file_path, 'r', encoding='utf-8') as f:
67
+ content = f.read()
68
+ lines = content.split('\n')
69
+ content_start = 0
70
+ for i, line in enumerate(lines):
71
+ if line.startswith('#') and '章' in line:
72
+ content_start = i + 1
73
+ break
74
+ return '\n'.join(lines[content_start:])
75
+
76
+
77
+ # ─── 角色提取 ───────────────────────────────
78
+ _STOP_PREFIXES = ('他', '她', '我', '你', '它', '这', '那', '一', '几', '每', '各', '有', '是', '不', '又', '也', '就', '便', '被', '把', '向', '对', '从', '跟', '将', '让', '却', '只')
79
+ _STOP_SUFFIXES = ('道', '说', '问', '笑', '喊', '怒', '叫', '哭', '叹', '喃', '咕', '叨', '呼', '喝')
80
+ _STOP_WORDS = {
81
+ '说道', '问道', '喊道', '笑道', '怒道', '冷冷道', '淡淡道', '低声道', '大声道',
82
+ '心中', '忽然', '这个时候', '就在这', '只听得', '眼看', '原来',
83
+ '可以', '没有', '已经', '这个', '那个', '什么', '怎么', '为什么',
84
+ '如果', '因为', '所以', '但是', '而且', '然后', '虽然', '不过',
85
+ '只是', '还是', '或者', '一定', '可能', '应该', '必须', '能够',
86
+ '他们', '自己', '我们', '你们', '这些', '那些', '所有', '每个',
87
+ '现在', '刚才', '以前', '以后', '之前', '之后', '一直', '终于',
88
+ '看着', '听到', '发现', '感觉', '觉得', '知道', '想到', '看到',
89
+ '继续', '开始', '已经', '准备', '打算', '决定', '选择', '同意',
90
+ '掌门', '宗主', '长老', '堂主', '舵主',
91
+ '摆了摆手', '点了点头', '摇了摇头', '挥了挥手', '拱了拱手',
92
+ '老者淡淡', '少年笑', '中年笑', '大汉道', '老妪道', '少妇道',
93
+ }
94
+ _GENERIC_TITLES = ('老者', '少年', '中年', '大汉', '老妪', '少妇', '壮汉', '妇人', '男子', '女子')
95
+
96
+
97
+ def extract_characters(text):
98
+ """Extract character names from dialogue patterns, filtering noise."""
99
+ candidates = set()
100
+ for m in re.finditer(r'([\u4e00-\u9fff]{2,4})(?:说道|问道|喊道|笑道|怒道|冷冷道|淡淡道|低声道|大声道|心想|暗道|思忖|暗想|心说)', text):
101
+ candidates.add(m.group(1))
102
+ for m in re.finditer(r'([\u4e00-\u9fff]{2,4})[::]["\u201c]', text):
103
+ candidates.add(m.group(1))
104
+ for m in re.finditer(r'["\u201d]([\u4e00-\u9fff]{2,4})[说问道喊笑怒冷淡淡低大]', text):
105
+ candidates.add(m.group(1))
106
+
107
+ filtered = []
108
+ for c in sorted(candidates):
109
+ if c in _STOP_WORDS:
110
+ continue
111
+ if c.startswith(_STOP_PREFIXES):
112
+ continue
113
+ if c.endswith(_STOP_SUFFIXES) and len(c) <= 3:
114
+ continue
115
+ if len(c) >= 4 and re.search(r'[了的得地着过]', c[1:-1]):
116
+ continue
117
+ if any(c.startswith(t) for t in _GENERIC_TITLES):
118
+ continue
119
+ filtered.append(c)
120
+ return filtered
121
+
122
+
123
+ def extract_locations(text):
124
+ """Extract location names from movement/position patterns."""
125
+ locs = set()
126
+ for m in re.finditer(r'(?:在|到|去|进入|来到|回到)([\u4e00-\u9fff]{2,6})(?:中|里|内|外|前|后|上|下|边|旁)?(?:[,。!?\s]|$)', text):
127
+ locs.add(m.group(1))
128
+ return sorted(locs)[:10]
129
+
130
+
131
+ # ─── 钩子检测 ───────────────────────────────
132
+ _HOOK_CUES = {
133
+ '揭示': ['原来', '竟然是', '才发现', '终于明白', '真相', '秘密', '身份'],
134
+ '危机': ['突然', '忽然', '就在这时', '猛地', '危险', '不好', '糟糕', '惊变', '杀意'],
135
+ '选择': ['怎么办', '选哪', '两难', '不得', '必须', '只能', '要么', '是否'],
136
+ '期待': ['下次', '明天', '等着', '一定', '会回来', '再来', '不会放'],
137
+ '反转': ['没想到', '不料', '却', '反而', '居然', '竟'],
138
+ '悬念': ['到底', '究竟', '为什么', '怎么', '是否', '会不会', '难道'],
139
+ }
140
+
141
+ _HOOK_PATTERN_MAP = {
142
+ "揭示": re.compile(r'(原来|竟然|居然|没想到|真相|秘密|隐藏|真相大白|揭露)'),
143
+ "危机": re.compile(r'(危险|危机|追杀|暗杀|陷阱|包围|生死|命悬一线|千钧一发)'),
144
+ "选择": re.compile(r'(选择|决定|犹豫|两难|何去何从|必须做出)'),
145
+ "期待": re.compile(r'(明天|明天就|明天开始|等着|等着我|三天后|约定|届时)'),
146
+ "反转": re.compile(r'(但是|然而|却|没想到|谁知|岂料|不料|偏偏|可是)'),
147
+ "悬念": re.compile(r'(他不知道|她没看到|没有人知道|神秘|未知|谜团|未解|尚未)'),
148
+ }
149
+
150
+
151
+ def detect_hook(text, last_n=400):
152
+ """Detect chapter ending hook type from text tail."""
153
+ tail = ''.join(re.findall(r'[\u4e00-\u9fff,。!?、:""\u201c\u201d\u2026\uff01\uff1f]', text[-last_n:]))
154
+ found = {}
155
+ for ctype, keywords in _HOOK_CUES.items():
156
+ score = sum(1 for kw in keywords if kw in tail)
157
+ if score:
158
+ found[ctype] = score
159
+ best = max(found, key=found.get) if found else '未检测'
160
+ return {'type': best, 'cues': found, 'tail_preview': tail[-80:] if tail else ''}
161
+
162
+
163
+ def detect_hook_type_from_patterns(text):
164
+ """Detect hook type using compiled regex patterns (for batch scanning)."""
165
+ scores = {}
166
+ for hook_type, pattern in _HOOK_PATTERN_MAP.items():
167
+ scores[hook_type] = len(pattern.findall(text))
168
+ if not scores or max(scores.values()) == 0:
169
+ return "未知"
170
+ return max(scores, key=scores.get)
171
+
172
+
173
+ # ─── 章节排序 ───────────────────────────────
174
+ _CHAPTER_PATTERN = re.compile(r'第([零一二三四五六七八九十百\d]+)章.*\.md$')
175
+ _CN_MAP = {'零': 0, '一': 1, '二': 2, '三': 3, '四': 4,
176
+ '五': 5, '六': 6, '七': 7, '八': 8, '九': 9, '十': 10}
177
+
178
+
179
+ def chapter_sort_key(filepath):
180
+ """Sort key for chapter filenames (handles Chinese numerals)."""
181
+ m = _CHAPTER_PATTERN.search(os.path.basename(filepath))
182
+ if m:
183
+ num_str = m.group(1)
184
+ if num_str.isdigit():
185
+ return int(num_str)
186
+ val = 0
187
+ for ch in num_str:
188
+ val = val * 10 + _CN_MAP.get(ch, 0)
189
+ return val
190
+ return 9999
191
+
192
+
193
+ def list_chapters(chapters_dir, recent_n=None):
194
+ """List chapter files in a directory, sorted by chapter number."""
195
+ if not os.path.isdir(chapters_dir):
196
+ return []
197
+ files = [os.path.join(chapters_dir, f) for f in os.listdir(chapters_dir)
198
+ if _CHAPTER_PATTERN.search(f)]
199
+ files.sort(key=chapter_sort_key)
200
+ if recent_n:
201
+ files = files[-recent_n:]
202
+ return files
203
+
204
+
205
+ # ─── 摘要生成 ───────────────────────────────
206
+ def generate_summary(text, first_n=150, last_n=200):
207
+ """Generate head/tail summary from chapter text."""
208
+ clean = clean_markdown(text)
209
+ chinese_only = ''.join(re.findall(r'[\u4e00-\u9fff,。!?、:""\u201c\u201d]', clean))
210
+ head = chinese_only[:first_n]
211
+ tail = chinese_only[-last_n:] if len(chinese_only) > last_n else chinese_only[first_n:]
212
+ return {'开头': head, '结尾': tail}
213
+
214
+
215
+ # ─── 结构检测 ───────────────────────────────
216
+ def detect_structure(text):
217
+ """Detect chapter structure: dialogue/action/description ratios."""
218
+ lines = [l.strip() for l in text.split('\n') if l.strip()]
219
+ dialogue_lines = sum(1 for l in lines if re.search(r'["\u201c\u201d]|[\u300c\u300d]|[::][\u201c]', l))
220
+ action_keywords = ['打', '砍', '刺', '踢', '冲', '跑', '跳', '飞', '击', '轰', '斩', '劈', '砸', '撞']
221
+ description_keywords = ['阳光', '月光', '山', '水', '云', '风', '雨', '雪', '花', '树', '雾', '光', '影',
222
+ '宫殿', '阁楼', '庭院', '房间', '大厅', '森林', '沙漠', '河流', '山谷']
223
+ action_count = sum(1 for l in lines if any(kw in l for kw in action_keywords))
224
+ desc_count = sum(1 for l in lines if any(kw in l for kw in description_keywords))
225
+ total = len(lines)
226
+ return {
227
+ 'dialogue_pct': round(dialogue_lines / total * 100, 1) if total else 0,
228
+ 'action_pct': round(action_count / total * 100, 1) if total else 0,
229
+ 'description_pct': round(desc_count / total * 100, 1) if total else 0,
230
+ 'total_paragraphs': total
231
+ }
232
+
233
+
234
+ # ─── 节奏评级 ───────────────────────────────
235
+ _HIGHEST_INTENSITY_KEYWORDS = ['生死', '对决', '终极', '决战', '毁灭', '崩塌', '同归于尽', '最后一击']
236
+ _HIGH_INTENSITY_KEYWORDS = ['战斗', '突破', '打脸', '击杀', '轰杀', '爆发', '碾压', '秒杀']
237
+ _MID_INTENSITY_KEYWORDS = ['准备', '计划', '修炼', '提升', '领悟', '探索']
238
+ _LOW_INTENSITY_KEYWORDS = ['日常', '对话', '闲聊', '休息', '散步', '喝茶']
239
+
240
+
241
+ def estimate_pacing(text):
242
+ """Estimate chapter pacing level S1-S5 based on content keywords."""
243
+ clean = clean_markdown(text)
244
+ lines = [l.strip() for l in clean.split('\n') if l.strip()]
245
+ total = max(len(lines), 1)
246
+ highest = sum(1 for l in lines if any(kw in l for kw in _HIGHEST_INTENSITY_KEYWORDS))
247
+ high = sum(1 for l in lines if any(kw in l for kw in _HIGH_INTENSITY_KEYWORDS))
248
+ mid = sum(1 for l in lines if any(kw in l for kw in _MID_INTENSITY_KEYWORDS))
249
+ low = sum(1 for l in lines if any(kw in l for kw in _LOW_INTENSITY_KEYWORDS))
250
+ score = highest * 5 + high * 4 + mid * 3 + low * 1
251
+ avg = score / total
252
+ if avg >= 2.5:
253
+ return 'S5', 5
254
+ elif avg >= 1.8:
255
+ return 'S4', 4
256
+ elif avg >= 1.2:
257
+ return 'S3', 3
258
+ elif avg >= 0.6:
259
+ return 'S2', 2
260
+ else:
261
+ return 'S1', 1
262
+
263
+
264
+ # ─── Markdown 大纲解析 ───────────────────────────────
265
+ def parse_outline_headings(text, max_level=4):
266
+ """Parse markdown outline into a tree of headings."""
267
+ tree = []
268
+ for line in text.split('\n'):
269
+ m = re.match(r'^(#{1,' + str(max_level) + r'})\s+(.+)', line)
270
+ if m:
271
+ level = len(m.group(1))
272
+ title = m.group(2).strip()
273
+ tree.append({'level': level, 'title': title})
274
+ return tree
275
+
276
+
277
+ # ─── Truth文件读取 ───────────────────────────────
278
+ def read_truth_section(filepath):
279
+ """Read a truth file and return structured sections."""
280
+ if not os.path.exists(filepath):
281
+ return None
282
+ with open(filepath, 'r', encoding='utf-8') as f:
283
+ text = f.read()
284
+ sections = {}
285
+ current_section = 'header'
286
+ sections[current_section] = []
287
+ for line in text.split('\n'):
288
+ if line.startswith('## '):
289
+ current_section = line[3:].strip()
290
+ sections[current_section] = []
291
+ elif line.startswith('# '):
292
+ current_section = line[2:].strip()
293
+ sections[current_section] = []
294
+ else:
295
+ sections[current_section].append(line)
296
+ return {k: '\n'.join(v).strip() for k, v in sections.items() if v}
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env python3
2
+ """NovelMaker 技能验证脚本
3
+
4
+ 验证所有技能文件的完整性和正确性:
5
+ - Python 脚本语法检查
6
+ - 文件引用完整性检查
7
+ - 角色定义完整性检查
8
+ - 题材包完整性检查
9
+ - 弧线模板完整性检查
10
+ """
11
+
12
+ import ast
13
+ import os
14
+ import re
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ SKILL_DIR = Path(__file__).resolve().parent.parent.parent
19
+ PROJECT_DIR = SKILL_DIR.parent
20
+
21
+
22
+ def check_python_syntax():
23
+ """检查所有 Python 脚本的语法"""
24
+ results = []
25
+ scripts_dir = SKILL_DIR / "scripts"
26
+ for py_file in sorted(scripts_dir.rglob("*.py")):
27
+ try:
28
+ with open(py_file, encoding="utf-8") as f:
29
+ ast.parse(f.read())
30
+ results.append(("PASS", py_file.name, "语法正确"))
31
+ except SyntaxError as e:
32
+ results.append(("FAIL", py_file.name, f"语法错误: {e.msg} 行{e.lineno}"))
33
+ return results
34
+
35
+
36
+ def check_markdown_references():
37
+ """检查 SKILL.md 和 QUICK-REF.md 中的文件引用"""
38
+ results = []
39
+ patterns = [
40
+ (SKILL_DIR / "SKILL.md", r'\[.*?\]\(([^)]+)\)'),
41
+ (SKILL_DIR / "QUICK-REF.md", r'`scripts/(\S+\.py)`'),
42
+ ]
43
+
44
+ for md_file, pattern in patterns:
45
+ if not md_file.exists():
46
+ results.append(("FAIL", md_file.name, "文件不存在"))
47
+ continue
48
+
49
+ with open(md_file, encoding="utf-8") as f:
50
+ content = f.read()
51
+
52
+ for match in re.finditer(pattern, content):
53
+ ref = match.group(1)
54
+ if ref.startswith(("http://", "https://", "#")):
55
+ continue
56
+ ref_path = md_file.parent / ref
57
+ if not ref_path.exists():
58
+ results.append(("WARN", md_file.name, f"断裂引用: {ref}"))
59
+ else:
60
+ results.append(("PASS", md_file.name, f"引用有效: {ref}"))
61
+ return results
62
+
63
+
64
+ def check_agents():
65
+ """检查角色定义完整性"""
66
+ results = []
67
+ agents_dir = SKILL_DIR / "agents"
68
+ expected = ["coordinator.md", "planner.md", "writer.md",
69
+ "auditor.md", "reviser.md", "reviewer.md"]
70
+
71
+ for agent in expected:
72
+ agent_path = agents_dir / agent
73
+ if not agent_path.exists():
74
+ results.append(("FAIL", f"agents/{agent}", "文件不存在"))
75
+ continue
76
+ with open(agent_path, encoding="utf-8") as f:
77
+ content = f.read()
78
+ required_sections = ["职责", "触发"]
79
+ missing = [s for s in required_sections if s not in content]
80
+ if missing:
81
+ results.append(("WARN", f"agents/{agent}", f"缺少章节: {', '.join(missing)}"))
82
+ else:
83
+ results.append(("PASS", f"agents/{agent}", "定义完整"))
84
+ return results
85
+
86
+
87
+ def check_genre_packs():
88
+ """检查题材包完整性"""
89
+ results = []
90
+ packs_dir = SKILL_DIR / "genre-packs"
91
+ required_files = ["rules.md", "templates.md", "arc-types.md"]
92
+
93
+ for pack_dir in sorted(packs_dir.iterdir()):
94
+ if not pack_dir.is_dir() or pack_dir.name.startswith("_"):
95
+ continue
96
+ for req_file in required_files:
97
+ file_path = pack_dir / req_file
98
+ if not file_path.exists():
99
+ results.append(("FAIL", f"genre-packs/{pack_dir.name}/{req_file}", "文件不存在"))
100
+ else:
101
+ results.append(("PASS", f"genre-packs/{pack_dir.name}/{req_file}", "存在"))
102
+ return results
103
+
104
+
105
+ def check_arc_templates():
106
+ """检查弧线模板完整性"""
107
+ results = []
108
+ templates_dir = SKILL_DIR / "arc-templates"
109
+
110
+ for genre_dir in sorted(templates_dir.iterdir()):
111
+ if not genre_dir.is_dir():
112
+ continue
113
+ md_files = list(genre_dir.glob("*.md"))
114
+ if not md_files:
115
+ results.append(("WARN", f"arc-templates/{genre_dir.name}", "目录为空"))
116
+ else:
117
+ results.append(("PASS", f"arc-templates/{genre_dir.name}", f"{len(md_files)}个模板"))
118
+ return results
119
+
120
+
121
+ def check_rules():
122
+ """检查规则文件完整性"""
123
+ results = []
124
+ rules_dir = SKILL_DIR / "rules"
125
+ expected = ["anti-ai-expressions.md", "character-voice.md", "consistency-check.md", "smart-query.md"]
126
+
127
+ for rule in expected:
128
+ rule_path = rules_dir / rule
129
+ if not rule_path.exists():
130
+ results.append(("FAIL", f"rules/{rule}", "文件不存在"))
131
+ else:
132
+ results.append(("PASS", f"rules/{rule}", "存在"))
133
+ return results
134
+
135
+
136
+ def check_templates():
137
+ """检查模板文件完整性"""
138
+ results = []
139
+ templates_dir = SKILL_DIR / "templates"
140
+ required = ["constitution.md", "outline.md", "chapter.md", "character-profile.md"]
141
+
142
+ for tpl in required:
143
+ tpl_path = templates_dir / tpl
144
+ if not tpl_path.exists():
145
+ results.append(("FAIL", f"templates/{tpl}", "文件不存在"))
146
+ else:
147
+ results.append(("PASS", f"templates/{tpl}", "存在"))
148
+ return results
149
+
150
+
151
+ def check_hooks():
152
+ """检查 Hook 文件完整性"""
153
+ results = []
154
+ hooks_dir = SKILL_DIR / "hooks"
155
+ expected = ["context-injection.md", "intent-detection.md", "chapter-complete.md",
156
+ "review-trigger.md", "summary-trigger.md"]
157
+
158
+ for hook in expected:
159
+ hook_path = hooks_dir / hook
160
+ if not hook_path.exists():
161
+ results.append(("FAIL", f"hooks/{hook}", "文件不存在"))
162
+ else:
163
+ results.append(("PASS", f"hooks/{hook}", "存在"))
164
+ return results
165
+
166
+
167
+ def run_all_checks():
168
+ """运行所有检查"""
169
+ checks = [
170
+ ("Python 脚本语法", check_python_syntax),
171
+ ("Markdown 文件引用", check_markdown_references),
172
+ ("角色定义", check_agents),
173
+ ("题材包", check_genre_packs),
174
+ ("弧线模板", check_arc_templates),
175
+ ("规则文件", check_rules),
176
+ ("模板文件", check_templates),
177
+ ("Hook 文件", check_hooks),
178
+ ]
179
+
180
+ total_pass = 0
181
+ total_warn = 0
182
+ total_fail = 0
183
+
184
+ print("=" * 60)
185
+ print("NovelMaker 技能验证报告")
186
+ print("=" * 60)
187
+
188
+ for check_name, check_func in checks:
189
+ results = check_func()
190
+ passes = [r for r in results if r[0] == "PASS"]
191
+ warns = [r for r in results if r[0] == "WARN"]
192
+ fails = [r for r in results if r[0] == "FAIL"]
193
+
194
+ total_pass += len(passes)
195
+ total_warn += len(warns)
196
+ total_fail += len(fails)
197
+
198
+ status = "✓" if not fails else "✗"
199
+ print(f"\n{status} {check_name} ({len(passes)}通过 {len(warns)}警告 {len(fails)}失败)")
200
+
201
+ for level, name, msg in fails:
202
+ print(f" ✗ {name}: {msg}")
203
+ for level, name, msg in warns:
204
+ print(f" ⚠ {name}: {msg}")
205
+
206
+ print("\n" + "=" * 60)
207
+ print(f"总计: {total_pass}通过 {total_warn}警告 {total_fail}失败")
208
+ print("=" * 60)
209
+
210
+ return total_fail == 0
211
+
212
+
213
+ if __name__ == "__main__":
214
+ success = run_all_checks()
215
+ sys.exit(0 if success else 1)