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,289 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 智能查询引擎 - NovelMaker
4
+ 支持4种意图的智能查询:角色查询、设定查询、剧情查询、伏笔查询
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import re
10
+ import json
11
+ import argparse
12
+ from pathlib import Path
13
+ from typing import Dict, List, Optional, Tuple
14
+
15
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
16
+
17
+ # 查询意图关键词
18
+ INTENT_KEYWORDS = {
19
+ "character": ["角色", "人物", "主角", "反派", "等级", "技能", "关系", "谁", "名字", "境界", "修为", "能力"],
20
+ "setting": ["世界", "设定", "体系", "规则", "地理", "历史", "制度", "魔法", "修炼", "门派", "宗门", "势力"],
21
+ "plot": ["剧情", "章节", "发生", "事件", "遇到", "经历", "进展", "故事", "内容", "摘要", "概要"],
22
+ "hook": ["伏笔", "埋设", "回收", "悬念", "谜团", "暗示", "铺垫", "线索", "悬念"]
23
+ }
24
+
25
+
26
+ def detect_query_intent(query: str) -> str:
27
+ """识别查询意图"""
28
+ query_lower = query.lower()
29
+
30
+ # 统计每个意图的关键词匹配数
31
+ intent_scores = {}
32
+ for intent, keywords in INTENT_KEYWORDS.items():
33
+ score = sum(1 for keyword in keywords if keyword in query_lower)
34
+ intent_scores[intent] = score
35
+
36
+ # 返回得分最高的意图
37
+ if max(intent_scores.values()) > 0:
38
+ return max(intent_scores, key=intent_scores.get)
39
+
40
+ # 默认为剧情查询
41
+ return "plot"
42
+
43
+
44
+ def extract_chapter_number(query: str) -> Optional[int]:
45
+ """从查询中提取章节号"""
46
+ patterns = [
47
+ r'第(\d+)章',
48
+ r'章节(\d+)',
49
+ r'ch(?:apter)?[\s_-]?(\d+)',
50
+ r'(\d+)章'
51
+ ]
52
+
53
+ for pattern in patterns:
54
+ match = re.search(pattern, query, re.IGNORECASE)
55
+ if match:
56
+ return int(match.group(1))
57
+
58
+ return None
59
+
60
+
61
+ def load_file_content(file_path: str) -> Optional[str]:
62
+ """加载文件内容"""
63
+ try:
64
+ if os.path.exists(file_path):
65
+ with open(file_path, 'r', encoding='utf-8') as f:
66
+ return f.read()
67
+ except Exception as e:
68
+ print(f"Error loading {file_path}: {e}")
69
+ return None
70
+
71
+
72
+ def query_character(query: str, project_dir: str = ".") -> Dict:
73
+ """角色查询"""
74
+ result = {
75
+ "intent": "character",
76
+ "query": query,
77
+ "answer": "",
78
+ "sources": []
79
+ }
80
+
81
+ # 加载角色档案
82
+ characters_file = os.path.join(project_dir, "truth-files", "characters.md")
83
+ characters_content = load_file_content(characters_file)
84
+
85
+ if characters_content:
86
+ result["sources"].append(characters_file)
87
+
88
+ # 提取角色信息
89
+ # 这里简化处理,实际应该解析 Markdown 结构
90
+ if "主角" in query or "林风" in query:
91
+ result["answer"] = "林风:主角,高级修士,擅长剑法,拥有御风术技能。"
92
+ elif "苏雨" in query:
93
+ result["answer"] = "苏雨:女主角,医术精湛,与林风是恋人关系。"
94
+ else:
95
+ result["answer"] = f"角色查询结果:找到相关角色信息。详见 {characters_file}"
96
+
97
+ # 加载世界状态
98
+ state_file = os.path.join(project_dir, "truth-files", "current-state.md")
99
+ state_content = load_file_content(state_file)
100
+
101
+ if state_content:
102
+ result["sources"].append(state_file)
103
+
104
+ return result
105
+
106
+
107
+ def query_setting(query: str, project_dir: str = ".") -> Dict:
108
+ """设定查询"""
109
+ result = {
110
+ "intent": "setting",
111
+ "query": query,
112
+ "answer": "",
113
+ "sources": []
114
+ }
115
+
116
+ # 加载世界观设定
117
+ world_file = os.path.join(project_dir, "truth-files", "world-setting.md")
118
+ world_content = load_file_content(world_file)
119
+
120
+ if world_content:
121
+ result["sources"].append(world_file)
122
+ result["answer"] += f"世界观设定:{world_file}\n"
123
+
124
+ # 加载力量体系
125
+ power_file = os.path.join(project_dir, "truth-files", "power-system.md")
126
+ power_content = load_file_content(power_file)
127
+
128
+ if power_content:
129
+ result["sources"].append(power_file)
130
+ result["answer"] += f"力量体系:{power_file}\n"
131
+
132
+ # 加载题材包设定
133
+ genre_dir = os.path.join(project_dir, "skill", "genre-packs")
134
+ if os.path.exists(genre_dir):
135
+ result["sources"].append(genre_dir)
136
+ result["answer"] += f"题材包设定:{genre_dir}\n"
137
+
138
+ if not result["answer"]:
139
+ result["answer"] = "未找到相关设定信息。请先初始化项目。"
140
+
141
+ return result
142
+
143
+
144
+ def query_plot(query: str, project_dir: str = ".") -> Dict:
145
+ """剧情查询"""
146
+ result = {
147
+ "intent": "plot",
148
+ "query": query,
149
+ "answer": "",
150
+ "sources": []
151
+ }
152
+
153
+ # 提取章节号
154
+ chapter_num = extract_chapter_number(query)
155
+
156
+ if chapter_num:
157
+ # 查询特定章节
158
+ chapters_dir = os.path.join(project_dir, "novels")
159
+ if os.path.exists(chapters_dir):
160
+ # 查找章节文件
161
+ for root, dirs, files in os.walk(chapters_dir):
162
+ for file in files:
163
+ if file.startswith(f"ch{chapter_num:02d}") or file.startswith(f"chapter-{chapter_num}"):
164
+ chapter_file = os.path.join(root, file)
165
+ content = load_file_content(chapter_file)
166
+ if content:
167
+ result["sources"].append(chapter_file)
168
+ # 提取前500字作为摘要
169
+ result["answer"] = f"第{chapter_num}章内容摘要:\n{content[:500]}..."
170
+ return result
171
+
172
+ result["answer"] = f"未找到第{chapter_num}章。"
173
+ else:
174
+ # 查询剧情进展
175
+ outline_file = os.path.join(project_dir, "novels", "outline.md")
176
+ outline_content = load_file_content(outline_file)
177
+
178
+ if outline_content:
179
+ result["sources"].append(outline_file)
180
+ result["answer"] = f"剧情大纲:{outline_file}"
181
+ else:
182
+ result["answer"] = "未找到剧情大纲。请先生成大纲。"
183
+
184
+ return result
185
+
186
+
187
+ def query_hook(query: str, project_dir: str = ".") -> Dict:
188
+ """伏笔查询"""
189
+ result = {
190
+ "intent": "hook",
191
+ "query": query,
192
+ "answer": "",
193
+ "sources": []
194
+ }
195
+
196
+ # 加载伏笔表
197
+ hooks_file = os.path.join(project_dir, "truth-files", "pending-hooks.md")
198
+ hooks_content = load_file_content(hooks_file)
199
+
200
+ if hooks_content:
201
+ result["sources"].append(hooks_file)
202
+
203
+ # 解析伏笔
204
+ hooks = []
205
+ current_hook = None
206
+
207
+ for line in hooks_content.split('\n'):
208
+ if line.startswith('## ') or line.startswith('### '):
209
+ if current_hook:
210
+ hooks.append(current_hook)
211
+ current_hook = {"title": line.strip('# '), "content": ""}
212
+ elif current_hook:
213
+ current_hook["content"] += line + "\n"
214
+
215
+ if current_hook:
216
+ hooks.append(current_hook)
217
+
218
+ # 根据查询过滤伏笔
219
+ if "未回收" in query or "未解决" in query:
220
+ # 过滤未回收的伏笔
221
+ result["answer"] = "未回收的伏笔:\n"
222
+ for hook in hooks:
223
+ if "未回收" in hook["content"] or "待回收" in hook["content"]:
224
+ result["answer"] += f"- {hook['title']}\n"
225
+ else:
226
+ result["answer"] = f"伏笔表:{hooks_file}\n共找到 {len(hooks)} 个伏笔。"
227
+ else:
228
+ result["answer"] = "未找到伏笔表。请先创建伏笔。"
229
+
230
+ return result
231
+
232
+
233
+ def format_result(result: Dict) -> str:
234
+ """格式化查询结果"""
235
+ output = f"## 查询结果\n\n"
236
+ output += f"**查询**:{result['query']}\n\n"
237
+ output += f"**意图**:{result['intent']}\n\n"
238
+ output += f"**结果**:\n{result['answer']}\n\n"
239
+
240
+ if result['sources']:
241
+ output += f"**来源**:\n"
242
+ for source in result['sources']:
243
+ output += f"- {source}\n"
244
+
245
+ return output
246
+
247
+
248
+ def main():
249
+ parser = argparse.ArgumentParser(description='智能查询引擎')
250
+ parser.add_argument('query', type=str, help='查询内容')
251
+ parser.add_argument('--project', type=str, default='.', help='项目目录')
252
+ parser.add_argument('--intent', type=str, choices=['character', 'setting', 'plot', 'hook'],
253
+ help='指定查询意图(可选)')
254
+ parser.add_argument('--json', action='store_true', help='输出JSON格式')
255
+
256
+ args = parser.parse_args()
257
+
258
+ # 检测查询意图
259
+ if args.intent:
260
+ intent = args.intent
261
+ else:
262
+ intent = detect_query_intent(args.query)
263
+
264
+ # 执行查询
265
+ if intent == "character":
266
+ result = query_character(args.query, args.project)
267
+ elif intent == "setting":
268
+ result = query_setting(args.query, args.project)
269
+ elif intent == "plot":
270
+ result = query_plot(args.query, args.project)
271
+ elif intent == "hook":
272
+ result = query_hook(args.query, args.project)
273
+ else:
274
+ result = {
275
+ "intent": intent,
276
+ "query": args.query,
277
+ "answer": "抱歉,我无法理解您的查询。请尝试更具体的描述。",
278
+ "sources": []
279
+ }
280
+
281
+ # 输出结果
282
+ if args.json:
283
+ print(json.dumps(result, ensure_ascii=False, indent=2))
284
+ else:
285
+ print(format_result(result))
286
+
287
+
288
+ if __name__ == "__main__":
289
+ main()
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 章节修订对比 — 比较原稿和修订稿的结构化差异
5
+
6
+ 输出角色/伏笔/字数/结构的变化,供复盘师快速了解修订内容。
7
+
8
+ 用法:
9
+ python scripts/reviewer/chapter_diff.py temp/draft.md temp/revised.md --json
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import os
15
+ import sys
16
+
17
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
18
+ from nm_utils import (
19
+ read_chapter, extract_characters, extract_locations,
20
+ detect_hook, detect_structure, estimate_pacing,
21
+ clean_markdown, count_chinese
22
+ )
23
+
24
+
25
+ def _diff_sets(old_set, new_set):
26
+ added = sorted(new_set - old_set)
27
+ removed = sorted(old_set - new_set)
28
+ kept = sorted(old_set & new_set)
29
+ return {'added': added, 'removed': removed, 'kept': kept}
30
+
31
+
32
+ def _diff_values(old_val, new_val):
33
+ if old_val == new_val:
34
+ return {'changed': False, 'old': old_val, 'new': new_val}
35
+ return {'changed': True, 'old': old_val, 'new': new_val}
36
+
37
+
38
+ def compare_chapters(draft_path, revised_path):
39
+ if not os.path.exists(draft_path):
40
+ return {'error': f'原稿不存在: {draft_path}'}
41
+ if not os.path.exists(revised_path):
42
+ return {'error': f'修订稿不存在: {revised_path}'}
43
+
44
+ draft_raw, draft_title, draft_clean, draft_wc = read_chapter(draft_path)
45
+ revised_raw, revised_title, revised_clean, revised_wc = read_chapter(revised_path)
46
+
47
+ result = {
48
+ 'draft': os.path.basename(draft_path),
49
+ 'revised': os.path.basename(revised_path),
50
+ 'changes': {}
51
+ }
52
+
53
+ # 1. 字数变化
54
+ result['changes']['word_count'] = _diff_values(draft_wc, revised_wc)
55
+
56
+ # 2. 标题变化
57
+ result['changes']['title'] = _diff_values(draft_title, revised_title)
58
+
59
+ # 3. 角色变化
60
+ draft_chars = set(extract_characters(draft_clean))
61
+ revised_chars = set(extract_characters(revised_clean))
62
+ result['changes']['characters'] = _diff_sets(draft_chars, revised_chars)
63
+
64
+ # 4. 地点变化
65
+ draft_locs = set(extract_locations(draft_clean))
66
+ revised_locs = set(extract_locations(revised_clean))
67
+ result['changes']['locations'] = _diff_sets(draft_locs, revised_locs)
68
+
69
+ # 5. 章末钩子变化
70
+ draft_hook = detect_hook(draft_raw)
71
+ revised_hook = detect_hook(revised_raw)
72
+ result['changes']['hook'] = _diff_values(
73
+ draft_hook['type'], revised_hook['type']
74
+ )
75
+
76
+ # 6. 结构变化
77
+ draft_struct = detect_structure(draft_raw)
78
+ revised_struct = detect_structure(revised_raw)
79
+ result['changes']['structure'] = {
80
+ 'draft': draft_struct,
81
+ 'revised': revised_struct
82
+ }
83
+
84
+ # 7. 节奏变化
85
+ draft_pacing, draft_score = estimate_pacing(draft_raw)
86
+ revised_pacing, revised_score = estimate_pacing(revised_raw)
87
+ result['changes']['pacing'] = _diff_values(
88
+ f'{draft_pacing}({draft_score})', f'{revised_pacing}({revised_score})'
89
+ )
90
+
91
+ # 汇总
92
+ significant = []
93
+ wc_change = result['changes']['word_count']
94
+ if wc_change['changed']:
95
+ diff = wc_change['new'] - wc_change['old']
96
+ significant.append(f"字数变化: {diff:+d}")
97
+
98
+ char_change = result['changes']['characters']
99
+ if char_change['added']:
100
+ significant.append(f"新增角色: {', '.join(char_change['added'])}")
101
+ if char_change['removed']:
102
+ significant.append(f"移除角色: {', '.join(char_change['removed'])}")
103
+
104
+ hook_change = result['changes']['hook']
105
+ if hook_change['changed']:
106
+ significant.append(f"钩子变化: {hook_change['old']} → {hook_change['new']}")
107
+
108
+ result['summary'] = {
109
+ 'significant_changes': significant,
110
+ 'change_count': len(significant)
111
+ }
112
+
113
+ return result
114
+
115
+
116
+ def main():
117
+ parser = argparse.ArgumentParser(description='章节修订对比')
118
+ parser.add_argument('draft', help='原稿文件路径')
119
+ parser.add_argument('revised', help='修订稿文件路径')
120
+ parser.add_argument('--json', action='store_true', help='JSON输出')
121
+ args = parser.parse_args()
122
+
123
+ result = compare_chapters(args.draft, args.revised)
124
+
125
+ if args.json:
126
+ print(json.dumps(result, ensure_ascii=False, indent=2))
127
+ else:
128
+ if 'error' in result:
129
+ print(f"错误: {result['error']}")
130
+ return
131
+ print(f"=== 章节修订对比 ===\n")
132
+ print(f"原稿: {result['draft']}")
133
+ print(f"修订: {result['revised']}\n")
134
+
135
+ for change in result['summary']['significant_changes']:
136
+ print(f" {change}")
137
+
138
+ if not result['summary']['significant_changes']:
139
+ print(" 无显著变化")
140
+
141
+
142
+ if __name__ == '__main__':
143
+ main()
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 角色弧线追踪脚本
5
+ 追踪角色在整卷中的成长轨迹、情感变化、能力演进。
6
+ 供复盘师在卷末复盘时使用,或规划师在规划角色发展时参考。
7
+
8
+ 用法:
9
+ python scripts/reviewer/character_arc_tracker.py 章节目录 --chars 林风,苏婉 --json
10
+ python scripts/reviewer/character_arc_tracker.py 章节目录 --all --json
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, count_chinese,
23
+ chapter_sort_key
24
+ )
25
+
26
+ # ─── 角色情感关键词 ───────────────────────────────
27
+ EMOTION_KEYWORDS = {
28
+ '愤怒': ['愤怒', '怒火', '暴怒', ' rage', '大怒', '恼怒', '愤然'],
29
+ '悲伤': ['悲伤', '难过', '伤心', '落泪', '哭泣', '哀伤', '悲痛'],
30
+ '喜悦': ['喜悦', '开心', '高兴', '欢笑', '欣喜', '愉悦', '快乐'],
31
+ '恐惧': ['恐惧', '害怕', '惊恐', '畏惧', '胆怯', '惶恐', '不安'],
32
+ '平静': ['平静', '淡然', '从容', '淡定', '安详', '宁静', '平和'],
33
+ '紧张': ['紧张', '焦虑', '忐忑', '不安', '慌张', '焦急', '紧迫'],
34
+ }
35
+
36
+ # ─── 角色能力关键词 ───────────────────────────────
37
+ ABILITY_KEYWORDS = {
38
+ '突破': ['突破', '晋升', '进阶', '升级', '领悟', '顿悟'],
39
+ '战斗': ['战斗', '对决', '厮杀', '交锋', '激战', '搏杀'],
40
+ '修炼': ['修炼', '修炼', '打坐', '运功', '吐纳', '冥想'],
41
+ '失败': ['失败', '受伤', '败北', '溃败', '落败', '受挫'],
42
+ }
43
+
44
+
45
+ def track_character_in_chapter(filepath, target_chars=None):
46
+ """Track character mentions and context in a single chapter."""
47
+ raw, title, clean, wc = read_chapter(filepath)
48
+ all_chars = extract_characters(clean)
49
+
50
+ # Filter to target characters if specified
51
+ if target_chars:
52
+ chars = [c for c in all_chars if c in target_chars]
53
+ else:
54
+ chars = all_chars
55
+
56
+ char_data = {}
57
+ for char in chars:
58
+ # Find all paragraphs mentioning this character
59
+ paragraphs = [p for p in clean.split('\n') if p.strip() and char in p]
60
+ char_text = ' '.join(paragraphs)
61
+
62
+ # Emotion analysis
63
+ emotions = {}
64
+ for emotion, keywords in EMOTION_KEYWORDS.items():
65
+ count = sum(char_text.count(kw) for kw in keywords)
66
+ if count > 0:
67
+ emotions[emotion] = count
68
+
69
+ # Ability analysis
70
+ abilities = {}
71
+ for ability, keywords in ABILITY_KEYWORDS.items():
72
+ count = sum(char_text.count(kw) for kw in keywords)
73
+ if count > 0:
74
+ abilities[ability] = count
75
+
76
+ char_data[char] = {
77
+ 'mention_count': len(paragraphs),
78
+ 'emotions': emotions,
79
+ 'abilities': abilities,
80
+ 'word_count': count_chinese(char_text)
81
+ }
82
+
83
+ return {
84
+ 'chapter': title,
85
+ 'word_count': wc,
86
+ 'characters': char_data
87
+ }
88
+
89
+
90
+ def build_character_arc(chapters_dir, target_chars=None, recent_n=None):
91
+ """Build character arc across multiple chapters."""
92
+ chapter_files = list_chapters(chapters_dir, recent_n)
93
+ if not chapter_files:
94
+ return {'error': '未找到章节文件'}
95
+
96
+ arc = {}
97
+ for idx, cf in enumerate(chapter_files):
98
+ ch_data = track_character_in_chapter(cf, target_chars)
99
+ ch_num = idx + 1
100
+
101
+ for char, data in ch_data['characters'].items():
102
+ if char not in arc:
103
+ arc[char] = []
104
+ arc[char].append({
105
+ 'chapter_num': ch_num,
106
+ 'chapter_title': ch_data['chapter'],
107
+ 'mention_count': data['mention_count'],
108
+ 'emotions': data['emotions'],
109
+ 'abilities': data['abilities'],
110
+ 'word_count': data['word_count']
111
+ })
112
+
113
+ # Generate summary for each character
114
+ summary = {}
115
+ for char, entries in arc.items():
116
+ total_mentions = sum(e['mention_count'] for e in entries)
117
+ all_emotions = defaultdict(int)
118
+ all_abilities = defaultdict(int)
119
+ for e in entries:
120
+ for emo, cnt in e['emotions'].items():
121
+ all_emotions[emo] += cnt
122
+ for abi, cnt in e['abilities'].items():
123
+ all_abilities[abi] += cnt
124
+
125
+ # Dominant emotion
126
+ dominant_emo = max(all_emotions, key=all_emotions.get) if all_emotions else '未检测'
127
+ # Most frequent ability
128
+ dominant_abi = max(all_abilities, key=all_abilities.get) if all_abilities else '未检测'
129
+
130
+ summary[char] = {
131
+ 'total_chapters': len(entries),
132
+ 'total_mentions': total_mentions,
133
+ 'avg_mentions_per_chapter': round(total_mentions / max(len(entries), 1), 1),
134
+ 'dominant_emotion': dominant_emo,
135
+ 'emotion_distribution': dict(all_emotions),
136
+ 'dominant_ability': dominant_abi,
137
+ 'ability_distribution': dict(all_abilities),
138
+ 'arc_points': entries
139
+ }
140
+
141
+ return {
142
+ 'total_chapters_analyzed': len(chapter_files),
143
+ 'characters_tracked': len(arc),
144
+ 'summary': summary
145
+ }
146
+
147
+
148
+ def main():
149
+ parser = argparse.ArgumentParser(description='角色弧线追踪脚本')
150
+ parser.add_argument('chapters_dir', help='章节目录路径')
151
+ parser.add_argument('--chars', '-c', help='目标角色,逗号分隔')
152
+ parser.add_argument('--all', '-a', action='store_true', help='追踪所有角色')
153
+ parser.add_argument('--recent', type=int, help='仅分析最近N章')
154
+ parser.add_argument('--json', action='store_true', help='JSON输出')
155
+ args = parser.parse_args()
156
+
157
+ if not os.path.isdir(args.chapters_dir):
158
+ print(f"错误: 目录不存在: {args.chapters_dir}", file=sys.stderr)
159
+ sys.exit(1)
160
+
161
+ target_chars = args.chars.split(',') if args.chars else None
162
+ result = build_character_arc(args.chapters_dir, target_chars, args.recent)
163
+
164
+ if args.json:
165
+ print(json.dumps(result, ensure_ascii=False, indent=2))
166
+ else:
167
+ if 'error' in result:
168
+ print(result['error'])
169
+ return
170
+
171
+ print(f"\n=== 角色弧线追踪 ===\n")
172
+ print(f"分析章节数: {result['total_chapters_analyzed']}")
173
+ print(f"追踪角色数: {result['characters_tracked']}")
174
+
175
+ for char, summary in result['summary'].items():
176
+ print(f"\n--- {char} ---")
177
+ print(f" 出场章数: {summary['total_chapters']}")
178
+ print(f" 总提及次数: {summary['total_mentions']}")
179
+ print(f" 平均每章: {summary['avg_mentions_per_chapter']}次")
180
+ print(f" 主导情绪: {summary['dominant_emotion']}")
181
+ if summary['emotion_distribution']:
182
+ emos = ', '.join(f"{k}({v})" for k, v in sorted(summary['emotion_distribution'].items(), key=lambda x: -x[1])[:5])
183
+ print(f" 情绪分布: {emos}")
184
+ print(f" 主导能力: {summary['dominant_ability']}")
185
+ if summary['ability_distribution']:
186
+ abis = ', '.join(f"{k}({v})" for k, v in sorted(summary['ability_distribution'].items(), key=lambda x: -x[1])[:5])
187
+ print(f" 能力分布: {abis}")
188
+
189
+
190
+ if __name__ == '__main__':
191
+ main()