@cloud411716/fancy-webnovel 1.0.45 → 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 (134) hide show
  1. package/client.js +183 -0
  2. package/cordis.patch.yml +0 -14
  3. package/index.js +21 -37
  4. package/lib/chinese-num.js +111 -0
  5. package/lib/deployed-state.js +141 -0
  6. package/lib/error-codes.js +104 -0
  7. package/lib/infra.js +569 -0
  8. package/lib/llm-wiki.js +109 -0
  9. package/lib/safe-rm.js +94 -0
  10. package/lib/scraper-utils.js +165 -0
  11. package/package.json +37 -5
  12. package/plugins/fancy-analyze/command-input.js +65 -0
  13. package/plugins/fancy-analyze/index.js +973 -0
  14. package/plugins/fancy-analyze/schema-analyze.md +379 -0
  15. package/plugins/fancy-analyze/schema-write.md +178 -0
  16. package/plugins/fancy-analyze/scripts/split_source.js +292 -0
  17. package/plugins/fancy-analyze/summary-progress.js +431 -0
  18. package/plugins/fancy-analyze/templates.js +881 -0
  19. package/plugins/fancy-bootstrap/index.js +168 -0
  20. package/plugins/fancy-bootstrap/scripts/init.js +143 -0
  21. package/plugins/fancy-bootstrap/scripts/path-utils.js +21 -0
  22. package/plugins/fancy-bootstrap/templates.js +54 -0
  23. package/plugins/fancy-import/import-core.js +1118 -0
  24. package/plugins/fancy-import/import-driver.js +581 -0
  25. package/plugins/fancy-import/index.js +730 -0
  26. package/plugins/fancy-import/prompts/01-import-prompt.md +106 -0
  27. package/plugins/fancy-import/templates.js +49 -0
  28. package/plugins/fancy-scan/index.js +319 -0
  29. package/plugins/fancy-scan/scripts/run-scan.js +520 -0
  30. package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +794 -0
  31. package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +339 -0
  32. package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +611 -0
  33. package/plugins/fancy-scan/templates.js +295 -0
  34. package/plugins/fancy-story-builder/deploy-manifest.js +239 -0
  35. package/plugins/fancy-story-builder/deslop-driver.js +821 -0
  36. package/plugins/fancy-story-builder/flesh-output.js +155 -0
  37. package/plugins/fancy-story-builder/index.js +3152 -0
  38. package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +553 -0
  39. package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +540 -0
  40. package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +504 -0
  41. package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +188 -0
  42. package/plugins/fancy-story-builder/prompts/05-deslop-prompt.md +96 -0
  43. package/plugins/fancy-story-builder/purpose-template.md +107 -0
  44. package/plugins/fancy-story-builder/references/formulas-rhythm.json +302 -0
  45. package/plugins/fancy-story-builder/references/formulas-skeleton.json +229 -0
  46. package/plugins/fancy-story-builder/references/glossary.md +585 -0
  47. package/plugins/fancy-story-builder/references/index.md +490 -0
  48. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/00-index.md +174 -0
  49. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/01-scene.md +150 -0
  50. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/02-chapter-workflow.md +99 -0
  51. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/03-opening-chapter.md +104 -0
  52. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/04-dialogue-mastery.md +274 -0
  53. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/05-genre-formulas.md +432 -0
  54. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/06-tone-ensemble.md +259 -0
  55. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/07-ledger-audit.md +222 -0
  56. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/08-diagnosis-revision.md +205 -0
  57. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/09-reading-power.md +135 -0
  58. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/10-deslop.md +129 -0
  59. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +98 -0
  60. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/01-upgrade-rhythm.md +300 -0
  61. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/02-arc-rhythm.md +168 -0
  62. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/03-highpoint-hooks.md +189 -0
  63. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/04-satisfaction.md +226 -0
  64. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/05-foreshadow-density.md +195 -0
  65. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/06-chapter-outline.md +226 -0
  66. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +163 -0
  67. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/00-index.md +100 -0
  68. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +239 -0
  69. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/02-worldbuilding.md +245 -0
  70. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +181 -0
  71. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/04-emotion-methods.md +286 -0
  72. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +177 -0
  73. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +91 -0
  74. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +208 -0
  75. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/02-packaging.md +135 -0
  76. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +305 -0
  77. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/04-conflict.md +328 -0
  78. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/05-quota.md +187 -0
  79. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +142 -0
  80. package/plugins/fancy-story-builder/rhythm-output.js +140 -0
  81. package/plugins/fancy-story-builder/schema-setting.md +324 -0
  82. package/plugins/fancy-story-builder/scripts/ai-patterns.js +619 -0
  83. package/plugins/fancy-story-builder/scripts/check-guide-compliance.js +289 -0
  84. package/plugins/fancy-story-builder/scripts/content-quality.js +413 -0
  85. package/plugins/fancy-story-builder/scripts/deslop-validate.js +143 -0
  86. package/plugins/fancy-story-builder/scripts/error-classes.js +112 -0
  87. package/plugins/fancy-story-builder/scripts/flesh-validate.js +1189 -0
  88. package/plugins/fancy-story-builder/scripts/formula-engine.js +112 -0
  89. package/plugins/fancy-story-builder/scripts/graph-utils.js +1385 -0
  90. package/plugins/fancy-story-builder/scripts/label-coverage.js +171 -0
  91. package/plugins/fancy-story-builder/scripts/prompt-drift-check.js +441 -0
  92. package/plugins/fancy-story-builder/scripts/reading-power.js +298 -0
  93. package/plugins/fancy-story-builder/scripts/rhythm-validate.js +1216 -0
  94. package/plugins/fancy-story-builder/scripts/semantic-lint.js +377 -0
  95. package/plugins/fancy-story-builder/scripts/skeleton-quota.js +145 -0
  96. package/plugins/fancy-story-builder/scripts/skeleton-validate.js +1832 -0
  97. package/plugins/fancy-story-builder/scripts/stage-batches.js +1371 -0
  98. package/plugins/fancy-story-builder/scripts/stage-contract.js +417 -0
  99. package/plugins/fancy-story-builder/scripts/upstream-query.js +198 -0
  100. package/plugins/fancy-story-builder/scripts/writing-rules.js +167 -0
  101. package/plugins/fancy-story-builder/scripts/writing-validate.js +682 -0
  102. package/plugins/fancy-story-builder/skeleton-output.js +199 -0
  103. package/plugins/fancy-story-builder/stage-driver.js +1049 -0
  104. package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//346/227/266/345/272/217/346/230/240/345/260/204/346/250/241/346/235/277.json +204 -0
  105. package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//347/253/240/350/212/202/347/273/206/347/272/262/346/250/241/346/235/277.json +34 -0
  106. package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//347/253/240/350/212/202/350/247/204/345/210/222/346/250/241/346/235/277.json +24 -0
  107. package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//350/212/202/345/245/217/345/217/202/346/225/260/346/250/241/346/235/277.json +27 -0
  108. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//344/270/226/347/225/214/350/247/202/350/247/204/345/210/231/346/250/241/346/235/277.json +66 -0
  109. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/263/351/224/256/351/201/223/345/205/267/346/250/241/346/235/277.json +76 -0
  110. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/206/262/347/252/201/346/250/241/346/235/277.json +72 -0
  111. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/211/247/346/203/205/346/250/241/346/235/277.json +37 -0
  112. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/212/277/345/212/233/346/250/241/346/235/277.json +53 -0
  113. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/234/260/347/202/271/346/250/241/346/235/277.json +40 -0
  114. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//347/273/223/345/261/200/346/250/241/346/235/277.json +79 -0
  115. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//350/247/222/350/211/262/346/250/241/346/235/277.json +132 -0
  116. package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/205/263/347/263/273/346/250/241/346/235/277.json +133 -0
  117. package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/256/236/344/275/223/346/250/241/346/235/277.json +160 -0
  118. package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/261/202/347/272/247/346/250/241/346/235/277.json +65 -0
  119. package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//346/241/206/346/236/266/346/250/241/346/235/277.json +134 -0
  120. package/plugins/fancy-story-builder/templates.js +1248 -0
  121. package/plugins/fancy-story-builder/wiki-references.js +204 -0
  122. package/plugins/fancy-story-builder/writing-driver.js +1061 -0
  123. package/plugins/fancy-topic/index.js +704 -0
  124. package/plugins/fancy-topic/scripts/select-topic.js +274 -0
  125. package/plugins/fancy-topic/templates.js +373 -0
  126. package/commands/bootstrap/index.js +0 -160
  127. package/commands/bootstrap/init-project.js +0 -121
  128. package/commands/scan/index.js +0 -275
  129. package/commands/scan/platforms/fanqie.js +0 -146
  130. package/commands/scan/platforms/jinjiang.js +0 -143
  131. package/commands/scan/platforms/qidian.js +0 -102
  132. package/commands/scan/platforms/qimao.js +0 -127
  133. package/commands/scan/scraper.js +0 -195
  134. package/infra.js +0 -160
@@ -0,0 +1,298 @@
1
+ /**
2
+ * reading-power.js —— 阅读动力的规则真源
3
+ *
4
+ * 这些规则原先散在旧版 skill 的两份 markdown 里(旧 fancy-write/references/
5
+ * hooks.md 与 reading-power.md),是纯给人读的规范,没法程序判定:
6
+ * · hooks.md —— 13 式章末钩子 + 7 式章首引子 + "严禁连续 3 章用同一种"
7
+ * · reading-power.md —— 5 大钩子分类 / 5 级强度 / 8 大爽点 / 7 种微兑现 / 30-40-30
8
+ *
9
+ * 现在落成**可判定的词表**:写作阶段每章把用了哪种钩子、含哪些微兑现写进章台账,
10
+ * writing-validate.js 拿这些词表逐条对账(见 validateChapter)。
11
+ *
12
+ * 设计要点:
13
+ * 1. 词表是**唯一真源**。旧 skill 里有三份互相漂移的副本(deslop 的 24 类、
14
+ * review 声称的"19 类"、代码里的 21 类),这里不再重复造第四份。
15
+ * 2. 每条都带 id。台账里记的是 id(不是中文名),改名不会让历史台账失效。
16
+ * 3. 判定与文案分离:这里只回答"哪些合法 / 哪些违规",
17
+ * "应该写成什么样"是提示词和写作指导的事。
18
+ */
19
+
20
+ /**
21
+ * 13 式章末钩子。
22
+ * 每章末必用其中一种 —— 这是"读者为什么要翻下一章"的答案。
23
+ */
24
+ export const TAIL_HOOK_TYPES = [
25
+ { id: 'sudden_reveal', name: '突然揭示', category: '悬念钩', desc: '一句话把读者以为知道的事掀翻' },
26
+ { id: 'urgent_crisis', name: '紧急危机', category: '危机钩', desc: '危险正在逼近,角色必须立刻行动' },
27
+ { id: 'unfinished_action', name: '未完成的动作', category: '选择钩', desc: '动作停在最要紧的一步' },
28
+ { id: 'identity_flip', name: '身份反转', category: '悬念钩', desc: '身份/立场的反转' },
29
+ { id: 'dilemma', name: '两难选择', category: '选择钩', desc: '二选一,且两边都有代价' },
30
+ { id: 'mystery_item', name: '神秘物品', category: '悬念钩', desc: '出现一个来历不明的物或线索' },
31
+ { id: 'time_limit', name: '时间限制', category: '危机钩', desc: '倒计时开始了' },
32
+ { id: 'promise_threat', name: '承诺威胁', category: '情绪钩', desc: '有人在预告/威胁后果' },
33
+ { id: 'odd_disappearance', name: '离奇消失', category: '悬念钩', desc: '人或物凭空不见' },
34
+ { id: 'implication', name: '言外之意', category: '情绪钩', desc: '话没说破,但读者听懂了' },
35
+ { id: 'imagery', name: '意象钩子', category: '悬念钩', desc: '用反复出现的意象收尾' },
36
+ { id: 'echo', name: '回声钩子', category: '情绪钩', desc: '结尾与开篇形成回响' },
37
+ { id: 'blank', name: '留白钩子', category: '悬念钩', desc: '停在关键信息之前,让读者自己脑补' }
38
+ ];
39
+
40
+ /**
41
+ * 7 式章首引子。
42
+ * 每章首必用其中一种 —— 决定读者第一段会不会滑走。
43
+ */
44
+ export const HEAD_HOOK_TYPES = [
45
+ { id: 'dialogue_suspense', name: '悬念对话引子', desc: '用一句对话把悬念抛出来' },
46
+ { id: 'inner_monologue', name: '内心独白引子', desc: '从角色的内心切进去' },
47
+ { id: 'contrast_scene', name: '反差场景引子', desc: '两个画面并置,反差制造张力' },
48
+ { id: 'unfinished_move', name: '未完成动作引子', desc: '动作停在半途' },
49
+ { id: 'imagery_foreshadow', name: '意象伏笔引子', desc: '用意象埋伏笔' },
50
+ { id: 'time_shift', name: '时间错位引子', desc: '时间跳跃制造疑问' },
51
+ { id: 'object_detail', name: '物件细节引子', desc: '从一个物件切进去' }
52
+ ];
53
+
54
+ /**
55
+ * 5 大钩子分类 —— 用来判断一章的钩子"有没有变化"。
56
+ * 同一分类连续用 3 章 = 钩子开始重复(比同一种更隐蔽,所以两层都要查)。
57
+ */
58
+ export const HOOK_CATEGORIES = ['危机钩', '悬念钩', '渴望钩', '情绪钩', '选择钩'];
59
+
60
+ /**
61
+ * 钩子强度 5 级。中间章用 1-2 级,高潮章用 3-5 级。
62
+ * 强度不是越高越好 —— 全书都是 5 级反而平。
63
+ */
64
+ export const HOOK_STRENGTHS = [
65
+ { id: 'curiosity', name: '好奇悬念', level: 1, usage: '中间章节' },
66
+ { id: 'concern', name: '关切悬念', level: 2, usage: '中间章节' },
67
+ { id: 'urgent', name: '迫切悬念', level: 3, usage: '高潮章节' },
68
+ { id: 'survival', name: '生存悬念', level: 4, usage: '高潮/结局前' },
69
+ { id: 'ultimate', name: '终极悬念', level: 5, usage: '全书结尾' }
70
+ ];
71
+
72
+ /**
73
+ * 8 大爽点模式。识别靠标识与结构,不靠词表 ——
74
+ * 这一项只能由模型判(LLM 报 finding),脚本只校验报上来的类型合法。
75
+ */
76
+ export const PAYOFF_PATTERNS = [
77
+ { id: 'flex_counter', name: '装逼打脸', en: 'Flex & Counter', arc: '嘲讽→反转→震惊' },
78
+ { id: 'underdog_reveal', name: '扮猪吃虎', en: 'Underdog Reveal', arc: '示弱→暴露→碾压' },
79
+ { id: 'underdog_victory', name: '越级反杀', en: 'Underdog Victory', arc: '差距→策略→逆转' },
80
+ { id: 'authority_challenge', name: '打脸权威', en: 'Authority Challenge', arc: '权威→挑战→成功' },
81
+ { id: 'villain_downfall', name: '反派翻车', en: 'Villain Downfall', arc: '得意→反杀→落幕' },
82
+ { id: 'sweet_surprise', name: '甜蜜超预期', en: 'Sweet Surprise', arc: '期待→超预期→升华' },
83
+ { id: 'misunderstanding', name: '迪化误解', en: 'Misunderstanding Elevation', arc: '随意行为→配角脑补升华' },
84
+ { id: 'identity_reveal', name: '身份掉马', en: 'Identity Reveal', arc: '身份伪装→关键时刻→揭露' }
85
+ ];
86
+
87
+ /**
88
+ * 7 种微兑现 —— 每章至少含 1 个。
89
+ * "微兑现"是让读者觉得这一章没白读的东西:哪怕很小,也得给一个。
90
+ */
91
+ export const MICRO_PAYOFFS = [
92
+ { id: 'info', name: '信息兑现', desc: '揭示了新信息或线索' },
93
+ { id: 'relation', name: '关系兑现', desc: '关系推进或确认' },
94
+ { id: 'ability', name: '能力兑现', desc: '能力提升或掌握新技能' },
95
+ { id: 'resource', name: '资源兑现', desc: '获得物品或资源' },
96
+ { id: 'recognition', name: '认可兑现', desc: '获得认可或面子' },
97
+ { id: 'emotion', name: '情绪兑现', desc: '情绪释放或共鸣' },
98
+ { id: 'clue', name: '线索兑现', desc: '伏笔回收或推进' }
99
+ ];
100
+
101
+ /**
102
+ * 30/40/30 结构 —— 一章内部的节奏配比。
103
+ * 这里的比例是"参考值",不作为硬判定(章节长度差异太大),但要在写作指导里讲清楚。
104
+ */
105
+ export const STRUCTURE_3030 = [
106
+ { id: 'setup', name: '铺垫', ratio: 0.3, role: '建立信息差、压力、期待' },
107
+ { id: 'delivery', name: '兑现', ratio: 0.4, role: '核心爽点执行' },
108
+ { id: 'aftermath', name: '余波', ratio: 0.3, role: '反应、收获、新期待' }
109
+ ];
110
+
111
+ /** 题材偏好的微兑现类型与每章数量(参考值,不作硬判定) */
112
+ export const GENRE_PAYOFF_PREFERENCE = {
113
+ 爽文: { types: ['ability', 'resource', 'recognition'], perChapter: '2-3' },
114
+ 言情: { types: ['relation', 'emotion', 'recognition'], perChapter: '1-2' },
115
+ 悬疑: { types: ['info', 'clue'], perChapter: '1-2' }
116
+ };
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // 判定辅助
120
+ // ---------------------------------------------------------------------------
121
+
122
+ const TAIL_BY_ID = new Map(TAIL_HOOK_TYPES.map((x) => [x.id, x]));
123
+ const TAIL_BY_NAME = new Map(TAIL_HOOK_TYPES.map((x) => [x.name, x]));
124
+ const HEAD_BY_ID = new Map(HEAD_HOOK_TYPES.map((x) => [x.id, x]));
125
+ const HEAD_BY_NAME = new Map(HEAD_HOOK_TYPES.map((x) => [x.name, x]));
126
+ const MICRO_BY_ID = new Map(MICRO_PAYOFFS.map((x) => [x.id, x]));
127
+ const MICRO_BY_NAME = new Map(MICRO_PAYOFFS.map((x) => [x.name, x]));
128
+
129
+ /**
130
+ * 查一个章末钩子(台账里可能写 id,也可能写中文名)。
131
+ * @returns {{id,name,category}|null}
132
+ */
133
+ export function resolveTailHook(value) {
134
+ if (typeof value !== 'string' || !value) return null;
135
+ const s = value.trim();
136
+ return TAIL_BY_ID.get(s) || TAIL_BY_NAME.get(s) || null;
137
+ }
138
+
139
+ /**
140
+ * 查一个章首引子。
141
+ * @returns {{id,name,desc}|null}
142
+ */
143
+ export function resolveHeadHook(value) {
144
+ if (typeof value !== 'string' || !value) return null;
145
+ const s = value.trim();
146
+ return HEAD_BY_ID.get(s) || HEAD_BY_NAME.get(s) || null;
147
+ }
148
+
149
+ /**
150
+ * 查一个微兑现类型。
151
+ * @returns {{id,name,desc}|null}
152
+ */
153
+ export function resolveMicroPayoff(value) {
154
+ if (typeof value !== 'string' || !value) return null;
155
+ const s = value.trim();
156
+ return MICRO_BY_ID.get(s) || MICRO_BY_NAME.get(s) || null;
157
+ }
158
+
159
+ /** 全部合法的章末钩子 id(给提示词/文档用) */
160
+ export function tailHookIds() {
161
+ return TAIL_HOOK_TYPES.map((x) => x.id);
162
+ }
163
+
164
+ /** 全部合法的章首引子 id */
165
+ export function headHookIds() {
166
+ return HEAD_HOOK_TYPES.map((x) => x.id);
167
+ }
168
+
169
+ /** 全部合法的微兑现 id */
170
+ export function microPayoffIds() {
171
+ return MICRO_PAYOFFS.map((x) => x.id);
172
+ }
173
+
174
+ /**
175
+ * 查连续重复的钩子。
176
+ *
177
+ * 规则(旧 fancy-write/references/hooks.md:77):
178
+ * "严禁连续 3 章用同一种" —— 同一 tail_hook 连续出现 3 章就报。
179
+ * 同理,"同一大分类连续 3 章"也算重复 —— 因为 13 式里有好几式同属悬念钩,
180
+ * 换了个名字但读者感受没变。
181
+ *
182
+ * @param {Array<{ch:number, hook_tail_type?:string}>} entries 章台账条目(按 ch 升序)
183
+ * @returns {{sameType:Array, sameCategory:Array}}
184
+ * sameType —— 连续 3 章用同一 tail_hook 的段落
185
+ * sameCategory —— 连续 3 章用同一大分类的段落
186
+ */
187
+ export function findHookRepeats(entries) { const list = (entries || [])
188
+ .filter((e) => e && e.ch !== undefined && e.ch !== null)
189
+ .slice()
190
+ .sort((a, b) => a.ch - b.ch);
191
+
192
+ const sameType = [];
193
+ const sameCategory = [];
194
+
195
+ let typeRun = [];
196
+ let catRun = [];
197
+
198
+ const flush = (run, out, keyOf, label) => {
199
+ if (run.length >= 3) {
200
+ out.push({
201
+ key: keyOf(run[0]),
202
+ label: label,
203
+ chapters: run.map((e) => e.ch),
204
+ value: keyOf(run[0])
205
+ });
206
+ }
207
+ };
208
+
209
+ for (let i = 0; i < list.length; i++) {
210
+ const cur = list[i];
211
+ const hook = resolveTailHook(cur.hook_tail_type);
212
+
213
+ // 同一种
214
+ if (hook) {
215
+ if (typeRun.length && typeRun[0]._hook.id !== hook.id) {
216
+ flush(typeRun, sameType, (e) => e._hook.id, 'hook_tail_type');
217
+ typeRun = [];
218
+ }
219
+ typeRun.push(Object.assign({}, cur, { _hook: hook }));
220
+ } else {
221
+ flush(typeRun, sameType, (e) => e._hook.id, 'hook_tail_type');
222
+ typeRun = [];
223
+ }
224
+
225
+ // 同一大分类
226
+ const cat = hook ? hook.category : null;
227
+ if (cat) {
228
+ if (catRun.length && catRun[0]._cat !== cat) {
229
+ flush(catRun, sameCategory, (e) => e._cat, 'hook_tail_category');
230
+ catRun = [];
231
+ }
232
+ catRun.push(Object.assign({}, cur, { _cat: cat }));
233
+ } else {
234
+ flush(catRun, sameCategory, (e) => e._cat, 'hook_tail_category');
235
+ catRun = [];
236
+ }
237
+ }
238
+ flush(typeRun, sameType, (e) => e._hook.id, 'hook_tail_type');
239
+ flush(catRun, sameCategory, (e) => e._cat, 'hook_tail_category');
240
+
241
+ return { sameType, sameCategory };
242
+ }
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // 清单生成(唯一真源 → 提示词/文档)
246
+ // ---------------------------------------------------------------------------
247
+
248
+ /**
249
+ * 生成"合法取值清单",给写作提示词动态注入。
250
+ *
251
+ * 为什么要动态生成而不是把清单写死在 markdown 里:
252
+ * 手写两份必然漂移 —— 旧 skill 正是这么坏的:
253
+ * deslop 的"24 类"、review 声称的"19 类"(实际 20 条)、
254
+ * 代码里实现的 21 类,三份互不一致,模型根本不知道以哪套为准。
255
+ *
256
+ * 这里清单只有一份(本文档),提示词、校验器、写作指导都从它派生。
257
+ *
258
+ * @returns {string} 可直接拼进提示词的 markdown 段
259
+ */
260
+ export function readingPowerLegend() {
261
+ const lines = [];
262
+ lines.push('---');
263
+ lines.push('');
264
+ lines.push('## 附:合法取值清单(由校验器判定,填错会被拦下)');
265
+ lines.push('');
266
+ lines.push('章台账里这三个字段只能填下面清单里的值(中文名或 id 都行):');
267
+ lines.push('');
268
+
269
+ lines.push('### `hook_head_type` —— 章首引子 7 式');
270
+ lines.push('');
271
+ lines.push('| id | 名称 |');
272
+ lines.push('|---|---|');
273
+ for (const h of HEAD_HOOK_TYPES) lines.push('| ' + h.id + ' | ' + h.name + ' |');
274
+ lines.push('');
275
+
276
+ lines.push('### `hook_tail_type` —— 章末钩子 13 式');
277
+ lines.push('');
278
+ lines.push('| id | 名称 | 大分类 |');
279
+ lines.push('|---|---|---|');
280
+ for (const h of TAIL_HOOK_TYPES) lines.push('| ' + h.id + ' | ' + h.name + ' | ' + h.category + ' |');
281
+ lines.push('');
282
+ lines.push('**不得连续 3 章用同一种**(同一大分类也不行):`HOOK_REPEAT`。');
283
+ lines.push('');
284
+
285
+ lines.push('### `micro_payoffs` —— 微兑现 7 种(每章至少 1 个)');
286
+ lines.push('');
287
+ lines.push('| id | 名称 |');
288
+ lines.push('|---|---|');
289
+ for (const m of MICRO_PAYOFFS) lines.push('| ' + m.id + ' | ' + m.name + ' |');
290
+ lines.push('');
291
+ lines.push('### `hook_strength` —— 钩子强度 5 级');
292
+ lines.push('');
293
+ lines.push('| id | 名称 | 级别 | 适用位置 |');
294
+ lines.push('|---|---|---|---|');
295
+ for (const s of HOOK_STRENGTHS) lines.push('| ' + s.id + ' | ' + s.name + ' | ' + s.level + ' | ' + s.usage + ' |');
296
+ lines.push('');
297
+ return lines.join('\n');
298
+ }