@cloud411716/fancy-webnovel 2.2.0 → 3.0.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 (211) hide show
  1. package/client.js +201 -15
  2. package/cordis.patch.yml +5 -0
  3. package/docs/douyin-plugin.md +118 -0
  4. package/docs/material-library/README.md +404 -0
  5. package/docs/material-library/import.js +437 -0
  6. package/docs/material-library/prompt.md +370 -0
  7. package/docs/material-library/queries.surql +94 -0
  8. package/docs/material-library/sample.jsonl +14 -0
  9. package/docs/material-library/schema.surql +46 -0
  10. package/docs/mcp-profile.example.yml +24 -0
  11. package/docs/story-graph-design.md +602 -0
  12. package/docs/story-graph-validation.md +81 -0
  13. package/docs/workspace-mcp.md +284 -0
  14. package/index.js +11 -7
  15. package/lib/atomic-write.js +109 -0
  16. package/lib/chapter-order.js +100 -0
  17. package/lib/confirmation.js +80 -0
  18. package/lib/db-init.js +123 -0
  19. package/lib/deployed-state.js +11 -11
  20. package/lib/direction-labels.js +265 -0
  21. package/lib/error-codes.js +31 -31
  22. package/lib/infra.js +29 -4
  23. package/lib/material-db.js +149 -0
  24. package/lib/path-guard.js +150 -0
  25. package/lib/projects.js +400 -0
  26. package/lib/scraper-utils.js +140 -0
  27. package/lib/surreal-server.js +338 -0
  28. package/lib/topic-decision.js +860 -0
  29. package/lib/topic-fields.js +251 -0
  30. package/lib/topic-schema.js +225 -0
  31. package/lib/workspace.js +501 -0
  32. package/package.json +29 -4
  33. package/plugins/fancy-analyze/analyze-definitions.md +519 -0
  34. package/plugins/fancy-analyze/index.js +1128 -77
  35. package/plugins/fancy-analyze/ingest-coverage.js +107 -0
  36. package/plugins/fancy-analyze/schema-analyze.md +355 -107
  37. package/plugins/fancy-analyze/scripts/append-summary-chapter.js +612 -0
  38. package/plugins/fancy-analyze/summary-progress.js +71 -7
  39. package/plugins/fancy-analyze/templates.js +437 -139
  40. package/plugins/fancy-bootstrap/index.js +276 -131
  41. package/plugins/fancy-bootstrap/scripts/init.js +59 -25
  42. package/plugins/fancy-bootstrap/templates.js +35 -18
  43. package/plugins/fancy-db/index.js +172 -0
  44. package/plugins/fancy-douyin/deploy.js +134 -0
  45. package/plugins/fancy-douyin/index.js +551 -0
  46. package/plugins/fancy-douyin/prompts/01-analysis-prompt.md +84 -0
  47. package/plugins/fancy-douyin/scripts/crawl-note.cjs +194 -0
  48. package/plugins/fancy-douyin/scripts/lib.cjs +197 -0
  49. package/plugins/fancy-douyin/scripts/scan-account.cjs +128 -0
  50. package/plugins/fancy-douyin/scripts/start-chrome.cjs +203 -0
  51. package/plugins/fancy-douyin/state.js +197 -0
  52. package/plugins/fancy-douyin/templates.js +127 -0
  53. package/plugins/fancy-export/exporter.js +805 -0
  54. package/plugins/fancy-export/index.js +63 -0
  55. package/plugins/fancy-import/import-driver.js +29 -6
  56. package/plugins/fancy-import/index.js +13 -5
  57. package/plugins/fancy-import/prompts/01-import-prompt.md +39 -14
  58. package/plugins/fancy-import/templates.js +17 -8
  59. package/plugins/fancy-material/exporter.js +153 -0
  60. package/plugins/fancy-material/genres.js +256 -0
  61. package/plugins/fancy-material/index.js +835 -0
  62. package/plugins/fancy-material/inventory.js +143 -0
  63. package/plugins/fancy-material/kinds.js +313 -0
  64. package/plugins/fancy-material/queue.js +55 -0
  65. package/plugins/fancy-material/scripts/query.js +254 -0
  66. package/plugins/fancy-material/templates.js +258 -0
  67. package/plugins/fancy-material/tool-deploy.js +138 -0
  68. package/plugins/fancy-material/validate.js +149 -0
  69. package/plugins/fancy-project/index.js +133 -0
  70. package/plugins/fancy-scan/existing-block.js +100 -0
  71. package/plugins/fancy-scan/index.js +410 -21
  72. package/plugins/fancy-scan/prompts/01-analysis-prompt.md +302 -0
  73. package/plugins/fancy-scan/scripts/append-block.js +725 -0
  74. package/plugins/fancy-scan/scripts/report-validate.js +824 -0
  75. package/plugins/fancy-scan/scripts/run-scan.js +169 -22
  76. package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +145 -16
  77. package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +179 -49
  78. package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +38 -20
  79. package/plugins/fancy-scan/templates/blocks.js +155 -0
  80. package/plugins/fancy-scan/templates/platforms.js +372 -0
  81. package/plugins/fancy-scan/templates/report-schema.js +90 -0
  82. package/plugins/fancy-scan/templates.js +545 -146
  83. package/plugins/fancy-scan/tool-deploy.js +335 -0
  84. package/plugins/fancy-scan/wait-turn.js +46 -0
  85. package/plugins/fancy-story-builder/db/README.md +106 -0
  86. package/plugins/fancy-story-builder/db/client.js +228 -0
  87. package/plugins/fancy-story-builder/db/commands.js +145 -0
  88. package/plugins/fancy-story-builder/db/ids.js +39 -0
  89. package/plugins/fancy-story-builder/db/import.js +798 -0
  90. package/plugins/fancy-story-builder/db/project.js +299 -0
  91. package/plugins/fancy-story-builder/db/query.js +361 -0
  92. package/plugins/fancy-story-builder/db/schema.js +417 -0
  93. package/plugins/fancy-story-builder/db/stage-sync.js +238 -0
  94. package/plugins/fancy-story-builder/flesh-output.js +19 -6
  95. package/plugins/fancy-story-builder/index.js +4105 -396
  96. package/plugins/fancy-story-builder/patch-driver.js +390 -0
  97. package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +29 -19
  98. package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +72 -10
  99. package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +40 -5
  100. package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +37 -0
  101. package/plugins/fancy-story-builder/purpose-template.md +42 -66
  102. package/plugins/fancy-story-builder/references/formulas-skeleton.json +229 -56
  103. package/plugins/fancy-story-builder/references/genre-profiles.json +3514 -0
  104. package/plugins/fancy-story-builder/references/glossary.md +15 -6
  105. package/plugins/fancy-story-builder/references/index.md +97 -6
  106. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +1 -1
  107. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +25 -7
  108. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +5 -1
  109. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +6 -2
  110. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +15 -5
  111. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +1 -1
  112. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +2 -1
  113. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +1 -1
  114. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +19 -1
  115. package/plugins/fancy-story-builder/schema-setting.json +1100 -0
  116. package/plugins/fancy-story-builder/schema-setting.md +201 -293
  117. package/plugins/fancy-story-builder/scripts/brief-validate.js +164 -0
  118. package/plugins/fancy-story-builder/scripts/content-quality.js +4 -1
  119. package/plugins/fancy-story-builder/scripts/flesh-validate.js +135 -12
  120. package/plugins/fancy-story-builder/scripts/graph-utils.js +146 -47
  121. package/plugins/fancy-story-builder/scripts/reference-log.js +110 -0
  122. package/plugins/fancy-story-builder/scripts/rhythm-validate.js +100 -4
  123. package/plugins/fancy-story-builder/scripts/schema-pages.js +2668 -0
  124. package/plugins/fancy-story-builder/scripts/semantic-lint.js +5 -1
  125. package/plugins/fancy-story-builder/scripts/skeleton-validate.js +158 -17
  126. package/plugins/fancy-story-builder/scripts/stage-batches.js +36 -2
  127. package/plugins/fancy-story-builder/scripts/stage-contract.js +12 -0
  128. package/plugins/fancy-story-builder/scripts/story-artifacts.js +933 -0
  129. package/plugins/fancy-story-builder/scripts/upstream-query.js +1 -1
  130. package/plugins/fancy-story-builder/scripts/v2/baseline.js +112 -0
  131. package/plugins/fancy-story-builder/scripts/v2/cast.js +517 -0
  132. package/plugins/fancy-story-builder/scripts/v2/context.js +434 -0
  133. package/plugins/fancy-story-builder/scripts/v2/contracts.js +493 -0
  134. package/plugins/fancy-story-builder/scripts/v2/core.js +419 -0
  135. package/plugins/fancy-story-builder/scripts/v2/engine.js +1068 -0
  136. package/plugins/fancy-story-builder/scripts/v2/export-markdown.js +794 -0
  137. package/plugins/fancy-story-builder/scripts/v2/genre.js +195 -0
  138. package/plugins/fancy-story-builder/scripts/v2/knowledge.js +321 -0
  139. package/plugins/fancy-story-builder/scripts/v2/macro.js +409 -0
  140. package/plugins/fancy-story-builder/scripts/v2/manifest.js +538 -0
  141. package/plugins/fancy-story-builder/scripts/v2/migrate.js +137 -0
  142. package/plugins/fancy-story-builder/scripts/v2/prompts.js +1335 -0
  143. package/plugins/fancy-story-builder/scripts/v2/rhythm.js +618 -0
  144. package/plugins/fancy-story-builder/scripts/v2/scale.js +843 -0
  145. package/plugins/fancy-story-builder/scripts/v2/snapshot.js +121 -0
  146. package/plugins/fancy-story-builder/scripts/v2/stages.js +273 -0
  147. package/plugins/fancy-story-builder/scripts/v2/writing.js +216 -0
  148. package/plugins/fancy-story-builder/storydb/README.md +271 -0
  149. package/plugins/fancy-story-builder/storydb/cli.js +458 -0
  150. package/plugins/fancy-story-builder/storydb/client.js +351 -0
  151. package/plugins/fancy-story-builder/storydb/compile.js +452 -0
  152. package/plugins/fancy-story-builder/storydb/context.js +288 -0
  153. package/plugins/fancy-story-builder/storydb/definitions.js +699 -0
  154. package/plugins/fancy-story-builder/storydb/driver.js +106 -0
  155. package/plugins/fancy-story-builder/storydb/dsh-tools.js +313 -0
  156. package/plugins/fancy-story-builder/storydb/export.js +175 -0
  157. package/plugins/fancy-story-builder/storydb/ids.js +31 -0
  158. package/plugins/fancy-story-builder/storydb/impact.js +170 -0
  159. package/plugins/fancy-story-builder/storydb/import.js +559 -0
  160. package/plugins/fancy-story-builder/storydb/mcp.js +99 -0
  161. package/plugins/fancy-story-builder/storydb/prompts.js +952 -0
  162. package/plugins/fancy-story-builder/storydb/stage-command.js +379 -0
  163. package/plugins/fancy-story-builder/storydb/stage.js +426 -0
  164. package/plugins/fancy-story-builder/storydb/store.js +891 -0
  165. package/plugins/fancy-story-builder/storydb/tools.js +592 -0
  166. package/plugins/fancy-story-builder/storydb/validation.js +183 -0
  167. package/plugins/fancy-story-builder/templates/v2/10-scale/scale-template.json +144 -0
  168. package/plugins/fancy-story-builder/templates/v2/20-macro/emotion-template.json +15 -0
  169. package/plugins/fancy-story-builder/templates/v2/20-macro/endings-template.json +16 -0
  170. package/plugins/fancy-story-builder/templates/v2/20-macro/lines-template.json +28 -0
  171. package/plugins/fancy-story-builder/templates/v2/20-macro/roles-template.json +10 -0
  172. package/plugins/fancy-story-builder/templates/v2/20-macro/story-template.json +19 -0
  173. package/plugins/fancy-story-builder/templates/v2/20-macro/volumes-template.json +24 -0
  174. package/plugins/fancy-story-builder/templates/v2/20-macro/world-template.json +24 -0
  175. package/plugins/fancy-story-builder/templates/v2/30-cast/characters-template.json +49 -0
  176. package/plugins/fancy-story-builder/templates/v2/30-cast/climaxes-template.json +14 -0
  177. package/plugins/fancy-story-builder/templates/v2/30-cast/conflicts-template.json +30 -0
  178. package/plugins/fancy-story-builder/templates/v2/30-cast/factions-template.json +18 -0
  179. package/plugins/fancy-story-builder/templates/v2/30-cast/foreshadows-template.json +29 -0
  180. package/plugins/fancy-story-builder/templates/v2/30-cast/information-template.json +29 -0
  181. package/plugins/fancy-story-builder/templates/v2/30-cast/items-template.json +13 -0
  182. package/plugins/fancy-story-builder/templates/v2/30-cast/locations-template.json +12 -0
  183. package/plugins/fancy-story-builder/templates/v2/30-cast/payoffs-template.json +16 -0
  184. package/plugins/fancy-story-builder/templates/v2/30-cast/relations-template.json +44 -0
  185. package/plugins/fancy-story-builder/templates/v2/40-rhythm/chapter-map-template.json +81 -0
  186. package/plugins/fancy-story-builder/templates/v2/40-rhythm/outline-template.json +162 -0
  187. package/plugins/fancy-story-builder/templates/v2/50-writing/ledger-template.json +46 -0
  188. package/plugins/fancy-story-builder/templates/v2/60-deslop/report-template.json +10 -0
  189. 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 +267 -44
  190. 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 +121 -19
  191. 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 +129 -21
  192. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/266/344/273/226/345/256/236/344/275/223/346/250/241/346/235/277.json +62 -0
  193. 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 +76 -17
  194. 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 +56 -8
  195. 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 +63 -13
  196. 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 +34 -6
  197. 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 +81 -18
  198. 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 +202 -42
  199. 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 +160 -27
  200. 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 +217 -41
  201. 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 +215 -41
  202. package/plugins/fancy-story-builder/templates.js +272 -74
  203. package/plugins/fancy-story-builder/wiki-references.js +429 -93
  204. package/plugins/fancy-story-builder/writing-driver.js +336 -9
  205. package/plugins/fancy-topic/index.js +414 -61
  206. package/plugins/fancy-topic/prompts/01-exploration-prompt.md +115 -0
  207. package/plugins/fancy-topic/scripts/save-suggestion.js +219 -0
  208. package/plugins/fancy-topic/scripts/select-topic.js +106 -63
  209. package/plugins/fancy-topic/templates.js +225 -122
  210. package/plugins/fancy-topic/tool-deploy.js +200 -0
  211. package/lib/safe-rm.js +0 -94
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Genre discovery for the material library.
3
+ *
4
+ * Two sources:
5
+ * 1. A topic_decision file: use the genres listed under its "选题方向" section.
6
+ * 2. Fanqie scan data: the `## <genre> — N 本` headings contain the most
7
+ * complete genre list available in the workspace.
8
+ *
9
+ * If neither exists (for example when printing the prompt outside a workspace),
10
+ * fall back to the known Fanqie genre list. That keeps `prompt` usable as a
11
+ * standalone all-genres prompt.
12
+ */
13
+
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+ import { workspacePaths } from '#workspace';
17
+
18
+ export const FALLBACK_GENRES = [
19
+ '西方奇幻',
20
+ '东方仙侠',
21
+ '科幻末世',
22
+ '都市日常',
23
+ '都市修真',
24
+ '都市高武',
25
+ '历史古代',
26
+ '战神赘婿',
27
+ '都市种田',
28
+ '传统玄幻',
29
+ '历史脑洞',
30
+ '悬疑脑洞',
31
+ '都市脑洞',
32
+ '玄幻脑洞',
33
+ '悬疑灵异',
34
+ '抗战谍战',
35
+ '游戏体育',
36
+ '动漫衍生',
37
+ '男频衍生',
38
+ '古风世情',
39
+ '女频衍生',
40
+ '玄幻言情',
41
+ '种田',
42
+ '年代',
43
+ '现言脑洞',
44
+ '宫斗宅斗',
45
+ '古言脑洞',
46
+ '快穿',
47
+ '青春甜宠',
48
+ '星光璀璨',
49
+ '女频悬疑',
50
+ '职场婚恋',
51
+ '豪门总裁',
52
+ '民国言情',
53
+ ];
54
+
55
+ function unique(values) {
56
+ const seen = new Set();
57
+ const out = [];
58
+ for (const value of values) {
59
+ const text = String(value || '').trim();
60
+ if (!text || seen.has(text)) continue;
61
+ seen.add(text);
62
+ out.push(text);
63
+ }
64
+ return out;
65
+ }
66
+
67
+ function cleanGenre(value) {
68
+ return String(value || '')
69
+ .replace(/\*\*/g, '')
70
+ .replace(/^[\s\-*|]+/, '')
71
+ .replace(/[\s\-*|]+$/, '')
72
+ .replace(/[((].*?[))]/g, '')
73
+ .trim();
74
+ }
75
+
76
+ function splitGenres(value) {
77
+ return unique(String(value || '')
78
+ .split(/[、,,/;;]/)
79
+ .map(cleanGenre)
80
+ .filter((item) => item && !['全部', '通用', '其他', '未知'].includes(item)));
81
+ }
82
+
83
+ /**
84
+ * List all topic_decision_*.md files in <workspace>/扫榜结果, newest first.
85
+ *
86
+ * The material command lets the user pick one when several reports exist;
87
+ * it never merges the genre lists of multiple reports.
88
+ *
89
+ * @param {string} root workspace directory
90
+ * @returns {string[]} absolute file paths
91
+ */
92
+ export function listTopicDecisionFiles(root) {
93
+ const dir = workspacePaths(root).scan;
94
+ let names;
95
+ try {
96
+ names = fs.readdirSync(dir);
97
+ } catch {
98
+ return [];
99
+ }
100
+ return names
101
+ .filter((name) => /^topic_decision_.*\.md$/i.test(name))
102
+ .sort()
103
+ .reverse()
104
+ .map((name) => path.join(dir, name))
105
+ .filter((file) => {
106
+ try {
107
+ const stat = fs.lstatSync(file);
108
+ return stat.isFile() && !stat.isSymbolicLink();
109
+ } catch {
110
+ return false;
111
+ }
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Find the latest topic_decision_*.md in <workspace>/扫榜结果.
117
+ *
118
+ * @param {string} root workspace directory
119
+ * @returns {string|null}
120
+ */
121
+ export function latestTopicDecision(root) {
122
+ return listTopicDecisionFiles(root)[0] || null;
123
+ }
124
+
125
+ /**
126
+ * Extract genres from the "选题方向" section of a topic_decision report.
127
+ *
128
+ * Supports both the current markdown table (`方向 | 题材 | ...`) and simple
129
+ * bullet lines such as `- **题材**:仙侠、都市高武`.
130
+ *
131
+ * @param {string} text
132
+ * @returns {string[]}
133
+ */
134
+ export function parseTopicDecisionGenres(text) {
135
+ const source = String(text || '');
136
+ const lines = source.split(/\r?\n/);
137
+ let start = -1;
138
+ for (let i = 0; i < lines.length; i += 1) {
139
+ if (/^##\s*(?:[一二三四五六七八九十]+、)?选题方向(?:建议)?\s*$/.test(lines[i].trim())) {
140
+ start = i;
141
+ break;
142
+ }
143
+ }
144
+ if (start === -1) return [];
145
+
146
+ let end = lines.length;
147
+ for (let i = start + 1; i < lines.length; i += 1) {
148
+ if (/^##\s+/.test(lines[i])) {
149
+ end = i;
150
+ break;
151
+ }
152
+ }
153
+ const section = lines.slice(start + 1, end);
154
+ const genres = [];
155
+
156
+ // Markdown table: find the header that contains 题材.
157
+ let header = null;
158
+ let genreIndex = -1;
159
+ for (const line of section) {
160
+ const trimmed = line.trim();
161
+ if (!trimmed.startsWith('|')) continue;
162
+ const cells = trimmed.split('|').slice(1, -1).map((cell) => cell.trim());
163
+ if (!cells.length) continue;
164
+ const idx = cells.findIndex((cell) => cell.replace(/\*\*/g, '').trim() === '题材');
165
+ if (idx !== -1) {
166
+ header = cells;
167
+ genreIndex = idx;
168
+ break;
169
+ }
170
+ }
171
+ if (header && genreIndex !== -1) {
172
+ let inBody = false;
173
+ for (const line of section) {
174
+ const trimmed = line.trim();
175
+ if (!trimmed.startsWith('|')) continue;
176
+ const cells = trimmed.split('|').slice(1, -1).map((cell) => cell.trim());
177
+ if (!inBody) {
178
+ if (cells.every((cell) => /^:?-{2,}:?$/.test(cell.replace(/\s/g, '')))) {
179
+ inBody = true;
180
+ }
181
+ continue;
182
+ }
183
+ if (cells.length <= genreIndex) continue;
184
+ genres.push(...splitGenres(cells[genreIndex]));
185
+ }
186
+ }
187
+
188
+ // Bullet fallback, including lines like `- **题材**:仙侠`.
189
+ for (const line of section) {
190
+ const match = line.match(/^\s*[-*]\s*(?:\*\*)?题材(?:\*\*)?\s*[::]\s*(.+)$/);
191
+ if (match) genres.push(...splitGenres(match[1]));
192
+ }
193
+
194
+ return unique(genres);
195
+ }
196
+
197
+ /**
198
+ * Read the genre headings from Fanqie scan files.
199
+ *
200
+ * @param {string} root workspace directory
201
+ * @returns {string[]}
202
+ */
203
+ export function parseFanqieGenres(root) {
204
+ const dir = workspacePaths(root).scan;
205
+ let names;
206
+ try {
207
+ names = fs.readdirSync(dir);
208
+ } catch {
209
+ return [...FALLBACK_GENRES];
210
+ }
211
+ const files = names
212
+ .filter((name) => /^番茄.*\.md$/i.test(name))
213
+ .sort();
214
+ const genres = [];
215
+ for (const name of files) {
216
+ let text;
217
+ try {
218
+ text = fs.readFileSync(path.join(dir, name), 'utf8');
219
+ } catch {
220
+ continue;
221
+ }
222
+ for (const line of text.split(/\r?\n/)) {
223
+ const match = line.match(/^##\s+(.+?)\s+—\s+\d+\s+本\s*$/);
224
+ if (match) genres.push(...splitGenres(match[1]));
225
+ }
226
+ }
227
+ return genres.length ? unique(genres) : [...FALLBACK_GENRES];
228
+ }
229
+
230
+ /**
231
+ * Resolve the genres for a generation run.
232
+ *
233
+ * @param {string} root workspace directory
234
+ * @param {{topicFile?: string|null}} [options] explicit report selected by the user
235
+ * @returns {{mode: 'topic_decision'|'all_genres', genres: string[], topicFile: string|null}}
236
+ */
237
+ export function resolveMaterialGenres(root, options = {}) {
238
+ const topicFile = options.topicFile || latestTopicDecision(root);
239
+ if (topicFile) {
240
+ try {
241
+ const genres = parseTopicDecisionGenres(fs.readFileSync(topicFile, 'utf8'));
242
+ if (genres.length) return { mode: 'topic_decision', genres, topicFile };
243
+ } catch { /* fall through to all genres */ }
244
+ }
245
+ return { mode: 'all_genres', genres: parseFanqieGenres(root), topicFile: null };
246
+ }
247
+
248
+ /**
249
+ * All known genres, used by the `prompt` subcommand.
250
+ *
251
+ * @param {string|null} root
252
+ * @returns {string[]}
253
+ */
254
+ export function allKnownGenres(root) {
255
+ return root ? parseFanqieGenres(root) : [...FALLBACK_GENRES];
256
+ }