@cloud411716/fancy-webnovel 1.0.46 → 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 (271) hide show
  1. package/client.js +369 -0
  2. package/cordis.patch.yml +4 -13
  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 +26 -38
  15. package/lib/atomic-write.js +109 -0
  16. package/lib/chapter-order.js +100 -0
  17. package/lib/chinese-num.js +111 -0
  18. package/lib/confirmation.js +80 -0
  19. package/lib/db-init.js +123 -0
  20. package/lib/deployed-state.js +141 -0
  21. package/lib/direction-labels.js +265 -0
  22. package/lib/error-codes.js +104 -0
  23. package/lib/infra.js +594 -0
  24. package/lib/llm-wiki.js +109 -0
  25. package/lib/material-db.js +149 -0
  26. package/lib/path-guard.js +150 -0
  27. package/lib/projects.js +400 -0
  28. package/lib/scraper-utils.js +305 -0
  29. package/lib/surreal-server.js +338 -0
  30. package/lib/topic-decision.js +860 -0
  31. package/lib/topic-fields.js +251 -0
  32. package/lib/topic-schema.js +225 -0
  33. package/lib/workspace.js +501 -0
  34. package/package.json +62 -5
  35. package/plugins/fancy-analyze/analyze-definitions.md +519 -0
  36. package/plugins/fancy-analyze/command-input.js +65 -0
  37. package/plugins/fancy-analyze/index.js +2024 -0
  38. package/plugins/fancy-analyze/ingest-coverage.js +107 -0
  39. package/plugins/fancy-analyze/schema-analyze.md +627 -0
  40. package/plugins/fancy-analyze/schema-write.md +178 -0
  41. package/plugins/fancy-analyze/scripts/append-summary-chapter.js +612 -0
  42. package/plugins/fancy-analyze/scripts/split_source.js +292 -0
  43. package/plugins/fancy-analyze/summary-progress.js +495 -0
  44. package/plugins/fancy-analyze/templates.js +1179 -0
  45. package/plugins/fancy-bootstrap/index.js +313 -0
  46. package/plugins/fancy-bootstrap/scripts/init.js +177 -0
  47. package/plugins/fancy-bootstrap/scripts/path-utils.js +21 -0
  48. package/plugins/fancy-bootstrap/templates.js +71 -0
  49. package/plugins/fancy-db/index.js +172 -0
  50. package/plugins/fancy-douyin/deploy.js +134 -0
  51. package/plugins/fancy-douyin/index.js +551 -0
  52. package/plugins/fancy-douyin/prompts/01-analysis-prompt.md +84 -0
  53. package/plugins/fancy-douyin/scripts/crawl-note.cjs +194 -0
  54. package/plugins/fancy-douyin/scripts/lib.cjs +197 -0
  55. package/plugins/fancy-douyin/scripts/scan-account.cjs +128 -0
  56. package/plugins/fancy-douyin/scripts/start-chrome.cjs +203 -0
  57. package/plugins/fancy-douyin/state.js +197 -0
  58. package/plugins/fancy-douyin/templates.js +127 -0
  59. package/plugins/fancy-export/exporter.js +805 -0
  60. package/plugins/fancy-export/index.js +63 -0
  61. package/plugins/fancy-import/import-core.js +1118 -0
  62. package/plugins/fancy-import/import-driver.js +604 -0
  63. package/plugins/fancy-import/index.js +738 -0
  64. package/plugins/fancy-import/prompts/01-import-prompt.md +131 -0
  65. package/plugins/fancy-import/templates.js +58 -0
  66. package/plugins/fancy-material/exporter.js +153 -0
  67. package/plugins/fancy-material/genres.js +256 -0
  68. package/plugins/fancy-material/index.js +835 -0
  69. package/plugins/fancy-material/inventory.js +143 -0
  70. package/plugins/fancy-material/kinds.js +313 -0
  71. package/plugins/fancy-material/queue.js +55 -0
  72. package/plugins/fancy-material/scripts/query.js +254 -0
  73. package/plugins/fancy-material/templates.js +258 -0
  74. package/plugins/fancy-material/tool-deploy.js +138 -0
  75. package/plugins/fancy-material/validate.js +149 -0
  76. package/plugins/fancy-project/index.js +133 -0
  77. package/plugins/fancy-scan/existing-block.js +100 -0
  78. package/plugins/fancy-scan/index.js +708 -0
  79. package/plugins/fancy-scan/prompts/01-analysis-prompt.md +302 -0
  80. package/plugins/fancy-scan/scripts/append-block.js +725 -0
  81. package/plugins/fancy-scan/scripts/report-validate.js +824 -0
  82. package/plugins/fancy-scan/scripts/run-scan.js +667 -0
  83. package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +923 -0
  84. package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +469 -0
  85. package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +629 -0
  86. package/plugins/fancy-scan/templates/blocks.js +155 -0
  87. package/plugins/fancy-scan/templates/platforms.js +372 -0
  88. package/plugins/fancy-scan/templates/report-schema.js +90 -0
  89. package/plugins/fancy-scan/templates.js +694 -0
  90. package/plugins/fancy-scan/tool-deploy.js +335 -0
  91. package/plugins/fancy-scan/wait-turn.js +46 -0
  92. package/plugins/fancy-story-builder/db/README.md +106 -0
  93. package/plugins/fancy-story-builder/db/client.js +228 -0
  94. package/plugins/fancy-story-builder/db/commands.js +145 -0
  95. package/plugins/fancy-story-builder/db/ids.js +39 -0
  96. package/plugins/fancy-story-builder/db/import.js +798 -0
  97. package/plugins/fancy-story-builder/db/project.js +299 -0
  98. package/plugins/fancy-story-builder/db/query.js +361 -0
  99. package/plugins/fancy-story-builder/db/schema.js +417 -0
  100. package/plugins/fancy-story-builder/db/stage-sync.js +238 -0
  101. package/plugins/fancy-story-builder/deploy-manifest.js +239 -0
  102. package/plugins/fancy-story-builder/deslop-driver.js +821 -0
  103. package/plugins/fancy-story-builder/flesh-output.js +168 -0
  104. package/plugins/fancy-story-builder/index.js +6861 -0
  105. package/plugins/fancy-story-builder/patch-driver.js +390 -0
  106. package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +563 -0
  107. package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +602 -0
  108. package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +539 -0
  109. package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +225 -0
  110. package/plugins/fancy-story-builder/prompts/05-deslop-prompt.md +96 -0
  111. package/plugins/fancy-story-builder/purpose-template.md +83 -0
  112. package/plugins/fancy-story-builder/references/formulas-rhythm.json +302 -0
  113. package/plugins/fancy-story-builder/references/formulas-skeleton.json +402 -0
  114. package/plugins/fancy-story-builder/references/genre-profiles.json +3514 -0
  115. package/plugins/fancy-story-builder/references/glossary.md +594 -0
  116. package/plugins/fancy-story-builder/references/index.md +581 -0
  117. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/00-index.md +174 -0
  118. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/01-scene.md +150 -0
  119. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/02-chapter-workflow.md +99 -0
  120. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/03-opening-chapter.md +104 -0
  121. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/04-dialogue-mastery.md +274 -0
  122. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/05-genre-formulas.md +432 -0
  123. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/06-tone-ensemble.md +259 -0
  124. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/07-ledger-audit.md +222 -0
  125. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/08-diagnosis-revision.md +205 -0
  126. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/09-reading-power.md +135 -0
  127. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/10-deslop.md +129 -0
  128. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +98 -0
  129. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/01-upgrade-rhythm.md +300 -0
  130. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/02-arc-rhythm.md +168 -0
  131. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/03-highpoint-hooks.md +189 -0
  132. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/04-satisfaction.md +226 -0
  133. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/05-foreshadow-density.md +195 -0
  134. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/06-chapter-outline.md +226 -0
  135. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +181 -0
  136. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/00-index.md +100 -0
  137. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +243 -0
  138. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/02-worldbuilding.md +245 -0
  139. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +185 -0
  140. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/04-emotion-methods.md +286 -0
  141. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +187 -0
  142. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +91 -0
  143. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +209 -0
  144. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/02-packaging.md +135 -0
  145. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +305 -0
  146. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/04-conflict.md +328 -0
  147. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/05-quota.md +187 -0
  148. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +160 -0
  149. package/plugins/fancy-story-builder/rhythm-output.js +140 -0
  150. package/plugins/fancy-story-builder/schema-setting.json +1100 -0
  151. package/plugins/fancy-story-builder/schema-setting.md +232 -0
  152. package/plugins/fancy-story-builder/scripts/ai-patterns.js +619 -0
  153. package/plugins/fancy-story-builder/scripts/brief-validate.js +164 -0
  154. package/plugins/fancy-story-builder/scripts/check-guide-compliance.js +289 -0
  155. package/plugins/fancy-story-builder/scripts/content-quality.js +416 -0
  156. package/plugins/fancy-story-builder/scripts/deslop-validate.js +143 -0
  157. package/plugins/fancy-story-builder/scripts/error-classes.js +112 -0
  158. package/plugins/fancy-story-builder/scripts/flesh-validate.js +1312 -0
  159. package/plugins/fancy-story-builder/scripts/formula-engine.js +112 -0
  160. package/plugins/fancy-story-builder/scripts/graph-utils.js +1484 -0
  161. package/plugins/fancy-story-builder/scripts/label-coverage.js +171 -0
  162. package/plugins/fancy-story-builder/scripts/prompt-drift-check.js +441 -0
  163. package/plugins/fancy-story-builder/scripts/reading-power.js +298 -0
  164. package/plugins/fancy-story-builder/scripts/reference-log.js +110 -0
  165. package/plugins/fancy-story-builder/scripts/rhythm-validate.js +1312 -0
  166. package/plugins/fancy-story-builder/scripts/schema-pages.js +2668 -0
  167. package/plugins/fancy-story-builder/scripts/semantic-lint.js +381 -0
  168. package/plugins/fancy-story-builder/scripts/skeleton-quota.js +145 -0
  169. package/plugins/fancy-story-builder/scripts/skeleton-validate.js +1973 -0
  170. package/plugins/fancy-story-builder/scripts/stage-batches.js +1405 -0
  171. package/plugins/fancy-story-builder/scripts/stage-contract.js +429 -0
  172. package/plugins/fancy-story-builder/scripts/story-artifacts.js +933 -0
  173. package/plugins/fancy-story-builder/scripts/upstream-query.js +198 -0
  174. package/plugins/fancy-story-builder/scripts/v2/baseline.js +112 -0
  175. package/plugins/fancy-story-builder/scripts/v2/cast.js +517 -0
  176. package/plugins/fancy-story-builder/scripts/v2/context.js +434 -0
  177. package/plugins/fancy-story-builder/scripts/v2/contracts.js +493 -0
  178. package/plugins/fancy-story-builder/scripts/v2/core.js +419 -0
  179. package/plugins/fancy-story-builder/scripts/v2/engine.js +1068 -0
  180. package/plugins/fancy-story-builder/scripts/v2/export-markdown.js +794 -0
  181. package/plugins/fancy-story-builder/scripts/v2/genre.js +195 -0
  182. package/plugins/fancy-story-builder/scripts/v2/knowledge.js +321 -0
  183. package/plugins/fancy-story-builder/scripts/v2/macro.js +409 -0
  184. package/plugins/fancy-story-builder/scripts/v2/manifest.js +538 -0
  185. package/plugins/fancy-story-builder/scripts/v2/migrate.js +137 -0
  186. package/plugins/fancy-story-builder/scripts/v2/prompts.js +1335 -0
  187. package/plugins/fancy-story-builder/scripts/v2/rhythm.js +618 -0
  188. package/plugins/fancy-story-builder/scripts/v2/scale.js +843 -0
  189. package/plugins/fancy-story-builder/scripts/v2/snapshot.js +121 -0
  190. package/plugins/fancy-story-builder/scripts/v2/stages.js +273 -0
  191. package/plugins/fancy-story-builder/scripts/v2/writing.js +216 -0
  192. package/plugins/fancy-story-builder/scripts/writing-rules.js +167 -0
  193. package/plugins/fancy-story-builder/scripts/writing-validate.js +682 -0
  194. package/plugins/fancy-story-builder/skeleton-output.js +199 -0
  195. package/plugins/fancy-story-builder/stage-driver.js +1049 -0
  196. package/plugins/fancy-story-builder/storydb/README.md +271 -0
  197. package/plugins/fancy-story-builder/storydb/cli.js +458 -0
  198. package/plugins/fancy-story-builder/storydb/client.js +351 -0
  199. package/plugins/fancy-story-builder/storydb/compile.js +452 -0
  200. package/plugins/fancy-story-builder/storydb/context.js +288 -0
  201. package/plugins/fancy-story-builder/storydb/definitions.js +699 -0
  202. package/plugins/fancy-story-builder/storydb/driver.js +106 -0
  203. package/plugins/fancy-story-builder/storydb/dsh-tools.js +313 -0
  204. package/plugins/fancy-story-builder/storydb/export.js +175 -0
  205. package/plugins/fancy-story-builder/storydb/ids.js +31 -0
  206. package/plugins/fancy-story-builder/storydb/impact.js +170 -0
  207. package/plugins/fancy-story-builder/storydb/import.js +559 -0
  208. package/plugins/fancy-story-builder/storydb/mcp.js +99 -0
  209. package/plugins/fancy-story-builder/storydb/prompts.js +952 -0
  210. package/plugins/fancy-story-builder/storydb/stage-command.js +379 -0
  211. package/plugins/fancy-story-builder/storydb/stage.js +426 -0
  212. package/plugins/fancy-story-builder/storydb/store.js +891 -0
  213. package/plugins/fancy-story-builder/storydb/tools.js +592 -0
  214. package/plugins/fancy-story-builder/storydb/validation.js +183 -0
  215. package/plugins/fancy-story-builder/templates/v2/10-scale/scale-template.json +144 -0
  216. package/plugins/fancy-story-builder/templates/v2/20-macro/emotion-template.json +15 -0
  217. package/plugins/fancy-story-builder/templates/v2/20-macro/endings-template.json +16 -0
  218. package/plugins/fancy-story-builder/templates/v2/20-macro/lines-template.json +28 -0
  219. package/plugins/fancy-story-builder/templates/v2/20-macro/roles-template.json +10 -0
  220. package/plugins/fancy-story-builder/templates/v2/20-macro/story-template.json +19 -0
  221. package/plugins/fancy-story-builder/templates/v2/20-macro/volumes-template.json +24 -0
  222. package/plugins/fancy-story-builder/templates/v2/20-macro/world-template.json +24 -0
  223. package/plugins/fancy-story-builder/templates/v2/30-cast/characters-template.json +49 -0
  224. package/plugins/fancy-story-builder/templates/v2/30-cast/climaxes-template.json +14 -0
  225. package/plugins/fancy-story-builder/templates/v2/30-cast/conflicts-template.json +30 -0
  226. package/plugins/fancy-story-builder/templates/v2/30-cast/factions-template.json +18 -0
  227. package/plugins/fancy-story-builder/templates/v2/30-cast/foreshadows-template.json +29 -0
  228. package/plugins/fancy-story-builder/templates/v2/30-cast/information-template.json +29 -0
  229. package/plugins/fancy-story-builder/templates/v2/30-cast/items-template.json +13 -0
  230. package/plugins/fancy-story-builder/templates/v2/30-cast/locations-template.json +12 -0
  231. package/plugins/fancy-story-builder/templates/v2/30-cast/payoffs-template.json +16 -0
  232. package/plugins/fancy-story-builder/templates/v2/30-cast/relations-template.json +44 -0
  233. package/plugins/fancy-story-builder/templates/v2/40-rhythm/chapter-map-template.json +81 -0
  234. package/plugins/fancy-story-builder/templates/v2/40-rhythm/outline-template.json +162 -0
  235. package/plugins/fancy-story-builder/templates/v2/50-writing/ledger-template.json +46 -0
  236. package/plugins/fancy-story-builder/templates/v2/60-deslop/report-template.json +10 -0
  237. 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 +427 -0
  238. 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
  239. 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
  240. 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
  241. 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 +168 -0
  242. 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 +184 -0
  243. 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
  244. 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 +131 -0
  245. 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 +85 -0
  246. 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 +103 -0
  247. 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 +68 -0
  248. 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 +142 -0
  249. 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 +292 -0
  250. 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 +266 -0
  251. 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 +336 -0
  252. 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
  253. 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 +308 -0
  254. package/plugins/fancy-story-builder/templates.js +1446 -0
  255. package/plugins/fancy-story-builder/wiki-references.js +540 -0
  256. package/plugins/fancy-story-builder/writing-driver.js +1388 -0
  257. package/plugins/fancy-topic/index.js +1057 -0
  258. package/plugins/fancy-topic/prompts/01-exploration-prompt.md +115 -0
  259. package/plugins/fancy-topic/scripts/save-suggestion.js +219 -0
  260. package/plugins/fancy-topic/scripts/select-topic.js +317 -0
  261. package/plugins/fancy-topic/templates.js +476 -0
  262. package/plugins/fancy-topic/tool-deploy.js +200 -0
  263. package/commands/bootstrap/index.js +0 -164
  264. package/commands/bootstrap/init-project.js +0 -121
  265. package/commands/scan/index.js +0 -283
  266. package/commands/scan/platforms/fanqie.js +0 -146
  267. package/commands/scan/platforms/jinjiang.js +0 -143
  268. package/commands/scan/platforms/qidian.js +0 -102
  269. package/commands/scan/platforms/qimao.js +0 -127
  270. package/commands/scan/scraper.js +0 -195
  271. package/infra.js +0 -160
@@ -0,0 +1,131 @@
1
+ # 01 导入执行须知
2
+
3
+ > 本文件是导入阶段的**产物契约**:从旧稿反推出什么文件、写成什么格式、什么会被拦下。
4
+ > 导入的因果方向和正向四阶段相反:
5
+ > **旧稿已经在那里了 → 你把它读成结构化产物**,而不是照配额从零造。
6
+ >
7
+ > 导入阶段由插件驱动:它扫目录、切章、算配额、跑校验器、把错误发回来。
8
+ > **你不需要自己决定写哪些文件,也不需要自己跑校验脚本。**
9
+
10
+ ---
11
+
12
+ ## 〇、路径口径
13
+
14
+ **项目根的绝对路径:`${projectRoot}`**
15
+
16
+ 本文件与每轮派活里出现的路径都相对于项目根,但**读文件、写文件一律用绝对路径**(`<项目根>/...`)——你的工作目录未必是项目根:用相对路径会读不到旧稿,也会把产物写到别的地方去,插件在项目根下找不到文件,只会回一句"文件没有生成"。
17
+
18
+ ---
19
+
20
+ ## 一、核心铁律
21
+
22
+ > 1. **只反推,不创作**。旧稿里没有的人物、势力、地点、伏笔**不许新增**。
23
+ > 你的任务是"把旧稿里已经有的东西整理成结构化数据",
24
+ > 不是"补全一部没写完的小说"。
25
+ > 旧稿确实没写到的部分**留空**,由插件在报告里列成待补清单。
26
+ >
27
+ > 2. **不改旧稿正文**。正文由插件直接落盘(保留原标题),
28
+ > 你只负责反推结构化产物。写了正文也算违反契约。
29
+ >
30
+ > 3. **不修改 `模板/` 和 `references/` 下的任何文件**。这两个目录是只读输入。
31
+ >
32
+ > 4. **只写本轮指定的文件**。本轮要写哪几个文件,驱动会在派活时列清楚。
33
+ > 不要顺手多写,也不要改别的阶段的文件。
34
+ >
35
+ > 5. **引用的实体只能用 id 引用**(如 `C001`),不要在关系里写名字。
36
+
37
+ ---
38
+
39
+ ## 二、产物清单
40
+
41
+ | 阶段 | 文件 | 落盘路径(绝对路径) |
42
+ |---|---|---|
43
+ | 骨架 | 实体.json | `${projectRoot}/设定/raw/sources/骨架/实体.json` |
44
+ | 骨架 | 关系.json | `${projectRoot}/设定/raw/sources/骨架/关系.json` |
45
+ | 骨架 | 层级.json | `${projectRoot}/设定/raw/sources/骨架/层级.json` |
46
+ | 血肉 | 角色.json 等 8 个 | `${projectRoot}/设定/raw/sources/血肉/<名字>.json` |
47
+ | 节奏 | 章节规划.json 等 4 个 | `${projectRoot}/设定/raw/sources/节奏/<名字>.json` |
48
+ | 写作 | 4 个台账 | `${projectRoot}/设定/raw/sources/写作/台账/<名字>.json` |
49
+
50
+ **`${projectRoot}/设定/raw/sources/骨架/框架.json` 由插件生成,不要写。**里面的 `quota` 是插件从旧稿实际规模
51
+ 反推出来的(推不出来的按公式补齐),你写的实体条数必须和它对得上。
52
+
53
+ ---
54
+
55
+ ## 三、怎么反推
56
+
57
+ ### 3.1 通用原则
58
+
59
+ 1. **先通读**:驱动会把旧稿的正文(或设定资料)摘要给你,先建立整体印象。
60
+ 2. **再归类**:把旧稿里出现过的东西按类型归到实体清单里。
61
+ 3. **只登记出现过的东西**:每个实体都要能在旧稿里找到出处
62
+ (角色有出场、势力有提及、地点有场景)。
63
+ 4. **拿不准就留空**:宁可少写一个,也别编一个。插件会把没写的列进待补清单。
64
+
65
+ ### 3.2 各阶段要点
66
+
67
+ | 阶段 | 反推什么 | 注意 |
68
+ |---|---|---|
69
+ | 骨架 | 实体 / 关系 / 层级 | 只登记"出现过"的;层级里的卷要对应旧稿实际的卷 |
70
+ | 血肉 | 每个实体"具体什么样" | 旧稿里写明的外貌/动机/口吻照抄;没写的留空字符串 |
71
+ | 节奏 | 什么时间发生什么 | 章节号对应旧稿的实际章号;旧稿没写的章不要编 |
72
+ | 台账 | 已发生的状态 | 章台账按 `ch` 一章一条;人物/伏笔/时间线照实记 |
73
+
74
+ ### 3.3 字段与枚举
75
+
76
+ - 字段结构、id 规则、枚举取值:**以 `模板/` 下对应模板为准**
77
+ (`${projectRoot}/模板/骨架/实体模板.json` 等,**用绝对路径读**)。
78
+ - 枚举词表(角色类型、冲突类型、伏笔类型…):**以 `${projectRoot}/references/glossary.md` 为准**。
79
+ 写错枚举值会被校验器拦下。
80
+
81
+ ---
82
+
83
+ ## 四、会被拦下的东西
84
+
85
+ | 类别 | 错误码 | 说明 |
86
+ |---|---|---|
87
+ | 必填 | `REQUIRED` | 必填字段为空 |
88
+ | 类型 | `TYPE` | 字段类型不对(该是数组写成了对象) |
89
+ | id 规则 | `ID_FORMAT` / `ID_DUP` | id 不符合 `C001` / `F001` 这类格式,或 id 重复 |
90
+ | 枚举 | `ENUM_INVALID` | 取值不在 glossary 的词表里 |
91
+ | 引用 | `REF_UNKNOWN` | 关系里引用了不存在的 id |
92
+ | 配额 | `QUOTA_MISMATCH` | 条数和 `框架.json` 里的 `quota` 对不上 |
93
+ | 结构 | `DERIVATION_MISSING` | `${projectRoot}/设定/raw/sources/骨架/框架.json` 缺推导记录(**这个由插件生成,你不用管**) |
94
+ | 内容 | `NAME_PLACEHOLDER` / `TEXT_PLACEHOLDER` / `TEXT_TOO_SHORT` / `TEXT_DUPLICATE` | 实体名是"主角1"这类标签、字段是"待补充"这类占位、字段太短、同质化 |
95
+ | 没落盘 | `NO_OUTPUT` | **插件没等到你写文件**:多半是路径写歪了(用了相对路径),也可能是这一轮超时或被中断 |
96
+ | 没落盘 | `READ` | 插件在项目根下没找到本轮要写的文件,或它不是合法 JSON |
97
+ | 校验器 | `VALIDATOR_CRASH` | 校验脚本自己崩了——**不是你的产物有问题**,照原样再交一次即可 |
98
+
99
+ **什么算这一轮通过**:插件跑完校验器,**没有回任何错误**就是通过。
100
+ 下一轮派活里如果带着「上一轮的校验错误」,就是没通过,按第五节处理。
101
+
102
+ ---
103
+
104
+ ## 五、收到校验错误后怎么改
105
+
106
+ 驱动会把校验错误原样发回来(`code` / `path` / `message` / `hint`)。按这个顺序处理:
107
+
108
+ 1. **只改错误指向的字段**。错误没提到的部分不要动。
109
+ 2. **按 `hint` 的方向改**,不要绕过。
110
+ 3. **不要为了通过校验而编造内容**。校验器拦下"实体名是占位名"时,正确做法是**删掉这个编出来的实体**,不是给它起个像样的名字。
111
+ 4. 改完**重新写整个文件**(产物是整份覆盖,不是打补丁)。
112
+
113
+ **上面几类特殊错误码另说**:
114
+
115
+ - `NO_OUTPUT` / `READ`(文件没落盘)→ **先确认你用的是绝对路径**
116
+ (派活里给的那条 `<项目根>/设定/raw/sources/...`),目录不存在就先建目录,然后把整个文件**重新写一遍**。
117
+ 不要因为"内容我已经想好了"就跳过写文件——
118
+ 没落盘 = 这一轮没做,下一次还会重来。
119
+ - `VALIDATOR_CRASH`(校验器自己崩了)→ 不用改产物,原样重交一次。
120
+
121
+ **同一轮最多试 3 次**(派活标题里写着"第 N 次尝试")。
122
+ 第 3 次还没通过,插件会停在这一轮并写进导入报告;这时候**不要自己接着往下写别的阶段**,等插件告诉你下一步。
123
+
124
+ **汇报格式**(每轮完成后给一段话):
125
+
126
+ ```
127
+ 本轮完成:骨架(实体.json / 关系.json / 层级.json)
128
+ - 实体 12 条:角色 5、冲突 3、势力 2、地点 2
129
+ - 关系 18 条、层级 1 卷 3 主线 5 支线
130
+ - 旧稿没写到的:地点的详细描述(已留空)
131
+ ```
@@ -0,0 +1,58 @@
1
+ /**
2
+ * fancy-import 插件的文案模板
3
+ */
4
+
5
+ export const importTemplates = {
6
+ /**
7
+ * 通知文案。
8
+ * @param {Object} o
9
+ * @param {string} o.type
10
+ */
11
+ notify({ type, err, extra, chapters, files, rounds, failed }) {
12
+ switch (type) {
13
+ case 'help':
14
+ return `📥 旧稿导入 用法
15
+
16
+ /fancy-import <旧稿目录或文件> 导入旧稿并自动切章
17
+ /fancy-import 默认读取项目内的 旧稿/ 目录
18
+ /fancy-import help 显示本帮助
19
+
20
+ 作用:把旧稿导入到 设定/raw/sources/,并纳入 llm_wiki 项目。`;
21
+
22
+ case 'no_project_root':
23
+ return '❌ 无法确定项目目录:' + (err || '会话未提供当前工作目录') + '。请在项目目录下运行该命令。';
24
+
25
+ case 'not_initialized':
26
+ return '⚠️ 当前目录不属于已初始化的 fancy-webnovel 工作区。请先运行 /fancy-bootstrap 初始化工作区。';
27
+
28
+ case 'no_source':
29
+ return '❌ 旧稿导入失败:' + (err || '未知错误');
30
+
31
+ case 'no_source_arg':
32
+ return '⚠️ 请指定要导入的旧稿路径:\n'
33
+ + '/fancy-import <旧稿目录或文件>\n'
34
+ + '(目录中的 .txt / .md 文件会被扫描,正文与设定资料将自动归类。)';
35
+
36
+ case 'deploy_failed':
37
+ return '❌ 导入阶段文件部署失败:' + (err || '未知错误') + '。请检查磁盘空间和目录权限后重试。';
38
+
39
+ case 'setup_id_unavailable':
40
+ return '❌ 未能建立 llm-wiki 设定项目:' + (err || '未知原因') +
41
+ '\n导入产物必须位于「设定」目录下才会被纳管。请先按提示完成 llm-wiki 配置。';
42
+
43
+ case 'import_done':
44
+ return '✅ 导入完成:' + chapters + ' 章旧稿已转成 story-builder 格式,' +
45
+ (rounds || 0) + ' 轮反推的**单文件校验**都通过(跨文件一致性由骨架阶段的 `--all` 校验把关)。' +
46
+ '报告见 `导入报告/导入报告.md`。' +
47
+ (extra ? '\n\n' + extra : '');
48
+
49
+ case 'import_incomplete':
50
+ return '⚠️ 旧稿导入已完成,但有 ' + (failed || 0) + ' 轮反推未通过校验。' +
51
+ '**当前导入产物不完整**,待补清单见 `导入报告/导入报告.md`。' +
52
+ (extra ? '\n\n' + extra : '');
53
+
54
+ default:
55
+ return extra || '';
56
+ }
57
+ }
58
+ };
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Material library Markdown export for atomic blocks.
3
+ *
4
+ * Every record is one small block (name + tags + genres + optional note), so
5
+ * the export is deliberately terse:
6
+ *
7
+ * ## 命名
8
+ * ### 地名(place_name)
9
+ * #### 归墟
10
+ * - **tags**:地名、海、尽头、仙侠
11
+ * - **genres**:仙侠、通用
12
+ */
13
+
14
+ import path from 'path';
15
+ import { atomicWriteFile } from '#atomic-write';
16
+ import { localIsoString } from '#workspace';
17
+ import { connectMaterial } from '#material-db';
18
+ import { KIND_GROUPS, KIND_LABELS } from './kinds.js';
19
+
20
+ const KIND_ORDER = KIND_GROUPS.flatMap((group) => group.kinds);
21
+ const GROUP_OF = new Map();
22
+ for (const group of KIND_GROUPS) {
23
+ for (const kind of group.kinds) GROUP_OF.set(kind, group.title);
24
+ }
25
+
26
+ function text(value) {
27
+ return String(value == null ? '' : value).replace(/\r?\n/g, ' ').trim();
28
+ }
29
+
30
+ function heading(value) {
31
+ return text(value).replace(/\s+/g, ' ').trim() || '(未命名)';
32
+ }
33
+
34
+ function valueText(value) {
35
+ if (value === undefined || value === null || value === '') return '—';
36
+ if (Array.isArray(value)) {
37
+ return value.length ? value.map((item) => text(item)).join('、') : '—';
38
+ }
39
+ return text(value);
40
+ }
41
+
42
+ /**
43
+ * Render atomic materials as Markdown.
44
+ *
45
+ * @param {Array<object>} records
46
+ * @param {{workspace?: string, database?: string, exported_at?: string}} [meta]
47
+ * @returns {string}
48
+ */
49
+ export function renderMaterialMarkdown(records, meta = {}) {
50
+ const list = Array.isArray(records) ? records : [];
51
+ const byKind = new Map();
52
+ for (const record of list) {
53
+ const kind = String(record && record.kind || 'unknown');
54
+ if (!byKind.has(kind)) byKind.set(kind, []);
55
+ byKind.get(kind).push(record);
56
+ }
57
+ for (const rows of byKind.values()) {
58
+ rows.sort((a, b) => String(a.name || '').localeCompare(String(b.name || ''), 'zh-CN'));
59
+ }
60
+
61
+ const lines = [];
62
+ lines.push('# 网文创作素材库');
63
+ lines.push('');
64
+ lines.push('> 导出时间:' + (meta.exported_at || localIsoString()));
65
+ if (meta.workspace) lines.push('> 工作区:' + meta.workspace);
66
+ if (meta.database) lines.push('> 数据库:' + meta.database);
67
+ lines.push('> 素材:' + list.length + ' 条 · 类型:' + byKind.size + ' 个');
68
+ lines.push('');
69
+
70
+ if (!list.length) {
71
+ lines.push('暂无素材。运行 `/fancy-material` 生成,或 `/fancy-material import <文件>` 导入。');
72
+ lines.push('');
73
+ return lines.join('\n');
74
+ }
75
+
76
+ lines.push('## 目录');
77
+ lines.push('');
78
+ for (const group of KIND_GROUPS) {
79
+ const rows = group.kinds.reduce((sum, kind) => sum + (byKind.get(kind)?.length || 0), 0);
80
+ if (!rows) continue;
81
+ lines.push(`- ${group.title}:${rows} 条`);
82
+ }
83
+ lines.push('');
84
+
85
+ for (const group of KIND_GROUPS) {
86
+ const groupRows = group.kinds.reduce((sum, kind) => sum + (byKind.get(kind)?.length || 0), 0);
87
+ if (!groupRows) continue;
88
+ lines.push('## ' + group.title);
89
+ lines.push('');
90
+ for (const kind of group.kinds) {
91
+ const rows = byKind.get(kind) || [];
92
+ if (!rows.length) continue;
93
+ lines.push(`### ${KIND_LABELS[kind] || kind}(${kind})`);
94
+ lines.push('');
95
+ for (const record of rows) {
96
+ lines.push('#### ' + heading(record.name));
97
+ lines.push('');
98
+ lines.push('- **tags**:' + valueText(record.tags));
99
+ lines.push('- **genres**:' + valueText(record.genres));
100
+ if (record.source) lines.push('- **source**:' + text(record.source));
101
+ if (record.note) lines.push('- **note**:' + text(record.note));
102
+ lines.push('');
103
+ }
104
+ }
105
+ }
106
+
107
+ // Include any unknown kinds at the end rather than silently dropping them.
108
+ const unknown = [...byKind.keys()].filter((kind) => !KIND_ORDER.includes(kind));
109
+ if (unknown.length) {
110
+ lines.push('## 其他');
111
+ lines.push('');
112
+ for (const kind of unknown) {
113
+ lines.push(`### ${kind}`);
114
+ lines.push('');
115
+ for (const record of byKind.get(kind)) {
116
+ lines.push('#### ' + heading(record.name));
117
+ lines.push('');
118
+ lines.push('- **tags**:' + valueText(record.tags));
119
+ lines.push('- **genres**:' + valueText(record.genres));
120
+ if (record.source) lines.push('- **source**:' + text(record.source));
121
+ if (record.note) lines.push('- **note**:' + text(record.note));
122
+ lines.push('');
123
+ }
124
+ }
125
+ }
126
+
127
+ return lines.join('\n').replace(/\n{3,}/g, '\n\n').replace(/\n*$/, '\n');
128
+ }
129
+
130
+ /**
131
+ * Query the material DB and write `<workspace>/素材库.md`.
132
+ *
133
+ * @param {string} root workspace directory
134
+ * @returns {Promise<{file: string, count: number, kinds: number, database: string}>}
135
+ */
136
+ export async function exportMaterials(root) {
137
+ const { db, database } = await connectMaterial(root);
138
+ try {
139
+ const rows = await db.query('SELECT kind, name, tags, genres, note, source FROM material ORDER BY kind, name;');
140
+ const records = Array.isArray(rows) && Array.isArray(rows[0]) ? rows[0] : [];
141
+ const file = path.join(path.resolve(root), '素材库.md');
142
+ const markdown = renderMaterialMarkdown(records, {
143
+ workspace: root,
144
+ database,
145
+ exported_at: localIsoString(),
146
+ });
147
+ atomicWriteFile(file, markdown, { mode: 0o644 });
148
+ const kinds = new Set(records.map((record) => record.kind).filter(Boolean));
149
+ return { file, count: records.length, kinds: kinds.size, database };
150
+ } finally {
151
+ try { await db.close(); } catch { /* ignore */ }
152
+ }
153
+ }
@@ -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
+ }