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.
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/bin/novel-maker.js +229 -0
- package/package.json +33 -0
- package/skill/CHANGELOG.md +82 -0
- package/skill/QUICK-REF.md +168 -0
- package/skill/README.md +75 -0
- package/skill/SKILL.md +715 -0
- package/skill/agents/README.md +59 -0
- package/skill/agents/auditor.md +234 -0
- package/skill/agents/coordinator.md +150 -0
- package/skill/agents/planner.md +220 -0
- package/skill/agents/reviewer.md +249 -0
- package/skill/agents/reviser.md +144 -0
- package/skill/agents/writer.md +213 -0
- package/skill/arc-templates/README.md +43 -0
- package/skill/arc-templates/apocalypse/ability.md +81 -0
- package/skill/arc-templates/apocalypse/faction.md +81 -0
- package/skill/arc-templates/apocalypse/humanity.md +81 -0
- package/skill/arc-templates/apocalypse/survival.md +81 -0
- package/skill/arc-templates/game/competition.md +81 -0
- package/skill/arc-templates/game/dungeon.md +81 -0
- package/skill/arc-templates/game/guild-war.md +81 -0
- package/skill/arc-templates/game/leveling.md +80 -0
- package/skill/arc-templates/general/challenge.md +44 -0
- package/skill/arc-templates/general/conflict.md +71 -0
- package/skill/arc-templates/general/explore.md +71 -0
- package/skill/arc-templates/general/growth.md +71 -0
- package/skill/arc-templates/general/mystery.md +71 -0
- package/skill/arc-templates/general/relation.md +71 -0
- package/skill/arc-templates/history/battle.md +71 -0
- package/skill/arc-templates/history/politics.md +71 -0
- package/skill/arc-templates/history/reform.md +71 -0
- package/skill/arc-templates/infinite-flow/boss.md +71 -0
- package/skill/arc-templates/infinite-flow/dungeon.md +71 -0
- package/skill/arc-templates/infinite-flow/enhance.md +71 -0
- package/skill/arc-templates/infinite-flow/team.md +71 -0
- package/skill/arc-templates/mystery/case.md +71 -0
- package/skill/arc-templates/mystery/deduction.md +71 -0
- package/skill/arc-templates/mystery/twist.md +71 -0
- package/skill/arc-templates/romance/angst.md +80 -0
- package/skill/arc-templates/romance/chase.md +71 -0
- package/skill/arc-templates/romance/slow-burn.md +71 -0
- package/skill/arc-templates/romance/sweet.md +80 -0
- package/skill/arc-templates/sci-fi/awakening.md +81 -0
- package/skill/arc-templates/sci-fi/breakthrough.md +81 -0
- package/skill/arc-templates/sci-fi/contact.md +81 -0
- package/skill/arc-templates/sci-fi/exploration.md +81 -0
- package/skill/arc-templates/urban/business.md +71 -0
- package/skill/arc-templates/urban/revenge.md +71 -0
- package/skill/arc-templates/urban/rise.md +71 -0
- package/skill/arc-templates/urban/romance.md +71 -0
- package/skill/arc-templates/western-fantasy/adventure.md +80 -0
- package/skill/arc-templates/western-fantasy/kingdom.md +71 -0
- package/skill/arc-templates/western-fantasy/magic-awakening.md +71 -0
- package/skill/arc-templates/western-fantasy/racial-conflict.md +71 -0
- package/skill/arc-templates/wuxia/breakthrough.md +71 -0
- package/skill/arc-templates/wuxia/grudge.md +71 -0
- package/skill/arc-templates/wuxia/hero-path.md +71 -0
- package/skill/arc-templates/wuxia/sect-war.md +71 -0
- package/skill/arc-templates/xianxia/breakthrough.md +71 -0
- package/skill/arc-templates/xianxia/dungeon.md +71 -0
- package/skill/arc-templates/xianxia/tournament.md +71 -0
- package/skill/arc-templates/xianxia/tribulation.md +71 -0
- package/skill/docs/examples.md +61 -0
- package/skill/docs/faq.md +81 -0
- package/skill/docs/installation.md +87 -0
- package/skill/docs/quickstart.md +83 -0
- package/skill/genre-packs/README.md +47 -0
- package/skill/genre-packs/_default/arc-types.md +153 -0
- package/skill/genre-packs/_default/rules.md +56 -0
- package/skill/genre-packs/_default/templates.md +135 -0
- package/skill/genre-packs/apocalypse/arc-types.md +109 -0
- package/skill/genre-packs/apocalypse/rules.md +113 -0
- package/skill/genre-packs/apocalypse/settings.md +106 -0
- package/skill/genre-packs/apocalypse/templates.md +192 -0
- package/skill/genre-packs/game/arc-types.md +109 -0
- package/skill/genre-packs/game/rules.md +113 -0
- package/skill/genre-packs/game/settings.md +103 -0
- package/skill/genre-packs/game/templates.md +173 -0
- package/skill/genre-packs/history/arc-types.md +109 -0
- package/skill/genre-packs/history/rules.md +107 -0
- package/skill/genre-packs/history/settings.md +126 -0
- package/skill/genre-packs/history/templates.md +179 -0
- package/skill/genre-packs/infinite-flow/arc-types.md +101 -0
- package/skill/genre-packs/infinite-flow/rules.md +75 -0
- package/skill/genre-packs/infinite-flow/settings.md +102 -0
- package/skill/genre-packs/infinite-flow/templates.md +226 -0
- package/skill/genre-packs/mystery/arc-types.md +109 -0
- package/skill/genre-packs/mystery/rules.md +107 -0
- package/skill/genre-packs/mystery/settings.md +103 -0
- package/skill/genre-packs/mystery/templates.md +178 -0
- package/skill/genre-packs/romance/arc-types.md +130 -0
- package/skill/genre-packs/romance/rules.md +88 -0
- package/skill/genre-packs/romance/settings.md +146 -0
- package/skill/genre-packs/romance/templates.md +245 -0
- package/skill/genre-packs/sci-fi/arc-types.md +109 -0
- package/skill/genre-packs/sci-fi/rules.md +113 -0
- package/skill/genre-packs/sci-fi/settings.md +99 -0
- package/skill/genre-packs/sci-fi/templates.md +170 -0
- package/skill/genre-packs/urban/arc-types.md +101 -0
- package/skill/genre-packs/urban/rules.md +75 -0
- package/skill/genre-packs/urban/settings.md +82 -0
- package/skill/genre-packs/urban/templates.md +212 -0
- package/skill/genre-packs/western-fantasy/arc-types.md +128 -0
- package/skill/genre-packs/western-fantasy/rules.md +88 -0
- package/skill/genre-packs/western-fantasy/settings.md +160 -0
- package/skill/genre-packs/western-fantasy/templates.md +225 -0
- package/skill/genre-packs/wuxia/arc-types.md +126 -0
- package/skill/genre-packs/wuxia/rules.md +86 -0
- package/skill/genre-packs/wuxia/settings.md +150 -0
- package/skill/genre-packs/wuxia/templates.md +195 -0
- package/skill/genre-packs/xianxia/arc-types.md +101 -0
- package/skill/genre-packs/xianxia/rules.md +74 -0
- package/skill/genre-packs/xianxia/settings.md +107 -0
- package/skill/genre-packs/xianxia/templates.md +202 -0
- package/skill/hooks/README.md +102 -0
- package/skill/hooks/chapter-complete.md +176 -0
- package/skill/hooks/context-injection.md +152 -0
- package/skill/hooks/intent-detection.md +183 -0
- package/skill/hooks/review-trigger.md +219 -0
- package/skill/hooks/summary-trigger.md +185 -0
- package/skill/references/act-guidance.md +228 -0
- package/skill/references/audit-core.md +130 -0
- package/skill/references/audit-dimensions.md +202 -0
- package/skill/references/character-voice-card.md +196 -0
- package/skill/references/consistency-checker.md +209 -0
- package/skill/references/content-expansion.md +68 -0
- package/skill/references/creative-constraints.md +200 -0
- package/skill/references/data-agent.md +286 -0
- package/skill/references/dialogue-writing.md +104 -0
- package/skill/references/editorial-perspective.md +166 -0
- package/skill/references/emotion-curve.md +127 -0
- package/skill/references/genre-rules.md +389 -0
- package/skill/references/golden-opening.md +81 -0
- package/skill/references/memory-system.md +288 -0
- package/skill/references/pacing-analysis.md +201 -0
- package/skill/references/platform-rules.md +244 -0
- package/skill/references/plot-structures.md +108 -0
- package/skill/references/reader-feedback.md +119 -0
- package/skill/references/rhythm-system.md +204 -0
- package/skill/references/style-imitation.md +193 -0
- package/skill/references/sweet-spot-tracking.md +182 -0
- package/skill/references/usage-guide.md +174 -0
- package/skill/references/writing-methods.md +169 -0
- package/skill/rules/anti-ai-expressions.md +206 -0
- package/skill/rules/character-voice.md +184 -0
- package/skill/rules/consistency-check.md +232 -0
- package/skill/rules/smart-query.md +263 -0
- package/skill/scripts/README.md +380 -0
- package/skill/scripts/auditor/chapter_transition.py +217 -0
- package/skill/scripts/auditor/consistency_scan.py +194 -0
- package/skill/scripts/auditor/dialogue_checker.py +194 -0
- package/skill/scripts/auditor/hook_report.py +115 -0
- package/skill/scripts/auditor/pacing_optimizer.py +303 -0
- package/skill/scripts/auditor/pacing_report.py +275 -0
- package/skill/scripts/auditor/pre_audit.py +203 -0
- package/skill/scripts/auditor/style_check.py +158 -0
- package/skill/scripts/auditor/worldbuilding_checker.py +637 -0
- package/skill/scripts/common/analyze.py +129 -0
- package/skill/scripts/common/init_guide.py +796 -0
- package/skill/scripts/common/install.py +169 -0
- package/skill/scripts/common/nm_utils.py +296 -0
- package/skill/scripts/common/validate.py +215 -0
- package/skill/scripts/coordinator/stats_report.py +165 -0
- package/skill/scripts/coordinator/volume_batch.py +121 -0
- package/skill/scripts/planner/outline_extractor.py +89 -0
- package/skill/scripts/planner/planner_context.py +220 -0
- package/skill/scripts/planner/query_engine.py +289 -0
- package/skill/scripts/reviewer/chapter_diff.py +143 -0
- package/skill/scripts/reviewer/character_arc_tracker.py +191 -0
- package/skill/scripts/reviewer/emotion_curve.py +340 -0
- package/skill/scripts/reviewer/foreshadowing_tracker.py +286 -0
- package/skill/scripts/reviewer/subplot_tracker.py +207 -0
- package/skill/scripts/reviewer/summary_generator.py +130 -0
- package/skill/scripts/reviewer/truth_diff.py +227 -0
- package/skill/scripts/reviewer/truth_manager.py +120 -0
- package/skill/scripts/test_scripts.py +366 -0
- package/skill/scripts/writer/build_write_context.py +255 -0
- package/skill/scripts/writer/chapter_info.py +67 -0
- package/skill/scripts/writer/check_wordcount.py +115 -0
- package/skill/scripts/writer/scene_builder.py +227 -0
- package/skill/scripts/writer/style_anchor.py +135 -0
- package/skill/styles/author-styles.md +53 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/skill/templates/INDEX.md +48 -0
- package/skill/templates/act-plan.md +72 -0
- package/skill/templates/chapter.md +53 -0
- package/skill/templates/character-profile.md +107 -0
- package/skill/templates/character-voice.md +106 -0
- package/skill/templates/constitution.md +90 -0
- package/skill/templates/emotional-arcs.md +37 -0
- package/skill/templates/hook-template.md +68 -0
- package/skill/templates/outline.md +155 -0
- package/skill/templates/plot-card.md +432 -0
- package/skill/templates/power-system.md +124 -0
- package/skill/templates/presets.json +69 -0
- package/skill/templates/review-report.md +135 -0
- package/skill/templates/scene-plan.md +221 -0
- package/skill/templates/scene-template.md +78 -0
- package/skill/templates/subplot-board.md +48 -0
- package/skill/templates/summary-10chapters.md +79 -0
- package/skill/templates/summary-50chapters.md +131 -0
- package/skill/templates/summary-volume.md +148 -0
- package/skill/templates/timeline.md +37 -0
- package/skill/templates/volume-plan.md +44 -0
- 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)
|