@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,400 @@
1
+ /**
2
+ * Project registry, project directories, and project-scoped database users.
3
+ *
4
+ * The registry lives in one shared SurrealDB database
5
+ * (`fancy_registry`). A project's novel data lives in its own database
6
+ * (`proj_<uuid>`), and the project directory only stores connection metadata:
7
+ *
8
+ * <workspace>/projects/<uuid>/.fancy-project.json
9
+ *
10
+ * The novel title is NOT duplicated on disk. Renaming a project updates the
11
+ * registry and the project's `story:main` record; the directory name (the
12
+ * UUID) never changes.
13
+ */
14
+
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import crypto from 'crypto';
18
+ import { Surreal, createRemoteEngines } from 'surrealdb';
19
+ import {
20
+ WorkspaceError,
21
+ atomicWriteJson,
22
+ readJsonFile,
23
+ localIsoString,
24
+ readSecrets,
25
+ readWorkspace,
26
+ resolveWorkspace,
27
+ setActiveProject,
28
+ workspacePaths,
29
+ getActiveProject,
30
+ } from './workspace.js';
31
+ import { rpcUrl, startServer } from './surreal-server.js';
32
+
33
+ export const PROJECT_SCHEMA_VERSION = 1;
34
+ export const PROJECT_CONFIG_FILE = '.fancy-project.json';
35
+ export const REGISTRY_TABLE = 'project';
36
+
37
+ export class ProjectError extends Error {
38
+ constructor(code, message, detail) {
39
+ super(message);
40
+ this.name = 'ProjectError';
41
+ this.code = code;
42
+ this.detail = detail;
43
+ }
44
+ }
45
+
46
+ export function projectRecordId(projectId) {
47
+ return 'project:' + String(projectId).replace(/-/g, '_');
48
+ }
49
+
50
+ export function databaseName(projectId) {
51
+ return 'proj_' + String(projectId).replace(/-/g, '');
52
+ }
53
+
54
+ export function databaseUser(projectId) {
55
+ return 'u_' + String(projectId).replace(/-/g, '').slice(0, 16);
56
+ }
57
+
58
+ export function projectConfigPath(dir) {
59
+ return path.join(dir, PROJECT_CONFIG_FILE);
60
+ }
61
+
62
+ export function readProjectConfig(dir) {
63
+ const file = projectConfigPath(dir);
64
+ const data = readJsonFile(file);
65
+ if (!data || typeof data !== 'object' || Array.isArray(data) || !data.project_id) return null;
66
+ return data;
67
+ }
68
+
69
+ export function writeProjectConfig(dir, config) {
70
+ fs.mkdirSync(dir, { recursive: true });
71
+ return atomicWriteJson(projectConfigPath(dir), {
72
+ schema_version: PROJECT_SCHEMA_VERSION,
73
+ ...config,
74
+ }, { mode: 0o600 });
75
+ }
76
+
77
+ export function projectDir(root, projectId) {
78
+ return workspacePaths(root).project(projectId);
79
+ }
80
+
81
+ function normalizeProject(row) {
82
+ if (!row) return null;
83
+ const topic = row.topic && typeof row.topic === 'object' ? row.topic : {};
84
+ return {
85
+ ...row,
86
+ project_id: String(row.project_id || ''),
87
+ name: String(row.name || topic.book_name || row.project_id || ''),
88
+ db_name: String(row.db_name || ''),
89
+ db_user: String(row.db_user || ''),
90
+ dir: String(row.dir || ''),
91
+ status: String(row.status || 'active'),
92
+ topic,
93
+ };
94
+ }
95
+
96
+ async function connectAdmin(root) {
97
+ // startServer verifies the root credentials, and moves the workspace to a
98
+ // free port if another (possibly stale) SurrealDB is squatting on ours.
99
+ await startServer(root);
100
+ const workspace = readWorkspace(root);
101
+ const secrets = readSecrets(root);
102
+ const db = new Surreal({ engines: { ...createRemoteEngines() } });
103
+ await db.connect(rpcUrl(workspace));
104
+ await db.signin({
105
+ username: secrets.surreal?.username || 'root',
106
+ password: secrets.surreal?.password,
107
+ });
108
+ return { db, workspace, secrets };
109
+ }
110
+
111
+ const REGISTRY_DDL = [
112
+ `DEFINE TABLE IF NOT EXISTS ${REGISTRY_TABLE} SCHEMAFULL;`,
113
+ `DEFINE FIELD IF NOT EXISTS project_id ON ${REGISTRY_TABLE} TYPE string;`,
114
+ `DEFINE FIELD IF NOT EXISTS name ON ${REGISTRY_TABLE} TYPE string;`,
115
+ `DEFINE FIELD IF NOT EXISTS db_name ON ${REGISTRY_TABLE} TYPE string;`,
116
+ `DEFINE FIELD IF NOT EXISTS db_user ON ${REGISTRY_TABLE} TYPE string;`,
117
+ `DEFINE FIELD IF NOT EXISTS dir ON ${REGISTRY_TABLE} TYPE string;`,
118
+ `DEFINE FIELD IF NOT EXISTS status ON ${REGISTRY_TABLE} TYPE string DEFAULT 'active';`,
119
+ `DEFINE FIELD IF NOT EXISTS topic ON ${REGISTRY_TABLE} TYPE object FLEXIBLE;`,
120
+ `DEFINE FIELD IF NOT EXISTS created_at ON ${REGISTRY_TABLE} TYPE datetime VALUE time::now();`,
121
+ `DEFINE FIELD IF NOT EXISTS updated_at ON ${REGISTRY_TABLE} TYPE datetime VALUE time::now();`,
122
+ `DEFINE INDEX IF NOT EXISTS project_id_unique ON ${REGISTRY_TABLE} FIELDS project_id UNIQUE;`,
123
+ `DEFINE INDEX IF NOT EXISTS project_name_unique ON ${REGISTRY_TABLE} FIELDS name UNIQUE;`,
124
+ ];
125
+
126
+ export async function withRegistry(root, fn) {
127
+ const { db, workspace } = await connectAdmin(root);
128
+ try {
129
+ await db.use({ namespace: workspace.surreal?.namespace || 'fancy', database: workspace.surreal?.registry_database || 'fancy_registry' });
130
+ for (const statement of REGISTRY_DDL) await db.query(statement);
131
+ return await fn(db, workspace);
132
+ } finally {
133
+ try { await db.close(); } catch { /* ignore */ }
134
+ }
135
+ }
136
+
137
+ export async function listProjects(root) {
138
+ return withRegistry(root, async (db) => {
139
+ const rows = await db.query(`SELECT * FROM ${REGISTRY_TABLE} WHERE status != 'deleted' ORDER BY name;`);
140
+ const list = Array.isArray(rows) && Array.isArray(rows[0]) ? rows[0] : [];
141
+ return list.map(normalizeProject);
142
+ });
143
+ }
144
+
145
+ export async function getProject(root, nameOrId) {
146
+ const key = String(nameOrId || '').trim();
147
+ if (!key) return null;
148
+ const projects = await listProjects(root);
149
+ const lower = key.toLowerCase();
150
+ return projects.find((row) => row.name === key
151
+ || row.project_id === key
152
+ || row.db_name === key
153
+ || row.dir === key
154
+ || path.basename(row.dir) === key
155
+ || row.name.toLowerCase() === lower)
156
+ || projects.find((row) => row.name.includes(key) || row.project_id.startsWith(key))
157
+ || null;
158
+ }
159
+
160
+ export async function findProjects(root, query) {
161
+ const key = String(query || '').trim();
162
+ const projects = await listProjects(root);
163
+ if (!key) return projects;
164
+ const lower = key.toLowerCase();
165
+ return projects.filter((row) => row.name.toLowerCase().includes(lower)
166
+ || row.project_id.startsWith(key)
167
+ || row.db_name === key
168
+ || path.basename(row.dir) === key);
169
+ }
170
+
171
+ async function assertNameAvailable(db, name, exceptId = null) {
172
+ const rows = await db.query(
173
+ `SELECT project_id, name FROM ${REGISTRY_TABLE} WHERE name = $name AND status != 'deleted';`,
174
+ { name },
175
+ );
176
+ const list = Array.isArray(rows) && Array.isArray(rows[0]) ? rows[0] : [];
177
+ const conflict = list.find((row) => row.project_id !== exceptId);
178
+ if (conflict) {
179
+ throw new ProjectError('PROJECT_NAME_EXISTS', '小说名已被占用:' + name + '(已有项目 UUID:' + conflict.project_id + ')。请更换小说名后重试。');
180
+ }
181
+ }
182
+
183
+ export async function createProject(root, opts = {}) {
184
+ const title = String(opts.title || opts.name || '').trim();
185
+ if (!title) throw new ProjectError('PROJECT_TITLE_REQUIRED', '创建项目时必须提供小说名。');
186
+ const projectId = opts.projectId || crypto.randomUUID();
187
+ const dbName = databaseName(projectId);
188
+ const dbUser = databaseUser(projectId);
189
+ const password = crypto.randomBytes(24).toString('base64url');
190
+ const dir = projectDir(root, projectId);
191
+ const topic = opts.topic && typeof opts.topic === 'object' ? opts.topic : {};
192
+
193
+ const result = await withRegistry(root, async (db, workspace) => {
194
+ await assertNameAvailable(db, title);
195
+ const namespace = workspace.surreal?.namespace || 'fancy';
196
+ await db.query(`DEFINE DATABASE IF NOT EXISTS ${dbName};`);
197
+ // DEFINE USER ... ON DATABASE is resolved against the connection's
198
+ // currently selected database. Switch to the project database before
199
+ // defining its user, then switch back to the registry.
200
+ await db.use({ namespace, database: dbName });
201
+ await db.query(`DEFINE USER IF NOT EXISTS ${dbUser} ON DATABASE PASSWORD '${password}' ROLES VIEWER;`);
202
+ await db.use({ namespace, database: workspace.surreal?.registry_database || 'fancy_registry' });
203
+ const created = await db.query(
204
+ `CREATE ${REGISTRY_TABLE} SET
205
+ project_id = $project_id,
206
+ name = $name,
207
+ db_name = $db_name,
208
+ db_user = $db_user,
209
+ dir = $dir,
210
+ status = 'active',
211
+ topic = $topic;`,
212
+ { project_id: projectId, name: title, db_name: dbName, db_user: dbUser, dir, topic },
213
+ );
214
+ const row = Array.isArray(created) && Array.isArray(created[0]) ? created[0][0] : null;
215
+ return { row: normalizeProject(row), namespace };
216
+ });
217
+
218
+ fs.mkdirSync(dir, { recursive: true });
219
+ fs.mkdirSync(path.join(dir, '导出'), { recursive: true });
220
+ fs.mkdirSync(path.join(dir, '设定'), { recursive: true });
221
+ writeProjectConfig(dir, {
222
+ project_id: projectId,
223
+ workspace: root,
224
+ database: dbName,
225
+ namespace: result.namespace,
226
+ db_user: dbUser,
227
+ created_at: localIsoString(),
228
+ });
229
+
230
+ const secrets = readSecrets(root);
231
+ secrets.projects = secrets.projects || {};
232
+ secrets.projects[projectId] = { username: dbUser, password };
233
+ atomicWriteJson(path.join(root, '.fancy', 'secrets.json'), secrets, { mode: 0o600 });
234
+
235
+ if (opts.makeActive !== false) setActiveProject(root, projectId);
236
+ return getProject(root, projectId);
237
+ }
238
+
239
+ export async function updateProjectTopic(root, nameOrId, topic) {
240
+ const project = await getProject(root, nameOrId);
241
+ if (!project) throw new ProjectError('PROJECT_NOT_FOUND', '未找到匹配的项目:' + nameOrId + '。可运行 /fancy-project list 查看当前工作区的项目。');
242
+ const value = topic && typeof topic === 'object' ? topic : {};
243
+ await withRegistry(root, async (db) => {
244
+ await db.query(
245
+ `UPDATE ${REGISTRY_TABLE} SET topic = $topic, updated_at = time::now() WHERE project_id = $id;`,
246
+ { topic: value, id: project.project_id },
247
+ );
248
+ });
249
+ try {
250
+ const { openStoryGraph } = await import('../plugins/fancy-story-builder/storydb/client.js');
251
+ const sdb = await openStoryGraph(project.dir, { project, admin: true });
252
+ try {
253
+ await sdb.query(
254
+ `UPDATE story:main SET payload.topic = $topic, title = $title, updated_at = time::now();`,
255
+ { topic: value, title: value.book_name || value.title || project.name },
256
+ );
257
+ } finally {
258
+ await sdb.close();
259
+ }
260
+ } catch { /* registry is authoritative for lookup */ }
261
+ return getProject(root, project.project_id);
262
+ }
263
+
264
+ export async function renameProject(root, nameOrId, newTitle) {
265
+ const title = String(newTitle || '').trim();
266
+ if (!title) throw new ProjectError('PROJECT_TITLE_REQUIRED', '新小说名不能为空。');
267
+ const project = await getProject(root, nameOrId);
268
+ if (!project) throw new ProjectError('PROJECT_NOT_FOUND', '未找到匹配的项目:' + nameOrId + '。可运行 /fancy-project list 查看当前工作区的项目。');
269
+ const topic = {
270
+ ...(project.topic || {}),
271
+ book_name: title,
272
+ title,
273
+ };
274
+ await withRegistry(root, async (db) => {
275
+ await assertNameAvailable(db, title, project.project_id);
276
+ await db.query(
277
+ `UPDATE ${REGISTRY_TABLE} SET name = $name, topic = $topic, updated_at = time::now()
278
+ WHERE project_id = $id;`,
279
+ { name: title, topic, id: project.project_id },
280
+ );
281
+ });
282
+ // Keep the story record in sync; if the project DB is unavailable, the
283
+ // registry rename still wins and the next open will repair it.
284
+ try {
285
+ const { openStoryGraph } = await import('../plugins/fancy-story-builder/storydb/client.js');
286
+ const sdb = await openStoryGraph(project.dir, { admin: true });
287
+ try {
288
+ await sdb.query('UPDATE story:main SET title = $title, updated_at = time::now();', { title });
289
+ } finally {
290
+ await sdb.close();
291
+ }
292
+ } catch { /* registry is authoritative for lookup */ }
293
+ return getProject(root, project.project_id);
294
+ }
295
+
296
+ export async function deleteProject(root, nameOrId, opts = {}) {
297
+ const project = await getProject(root, nameOrId);
298
+ if (!project) throw new ProjectError('PROJECT_NOT_FOUND', '未找到匹配的项目:' + nameOrId + '。可运行 /fancy-project list 查看当前工作区的项目。');
299
+ if (opts.confirm !== true) {
300
+ throw new ProjectError('CONFIRMATION_REQUIRED', '删除项目需要明确确认(confirm=true)。', { project });
301
+ }
302
+ await withRegistry(root, async (db) => {
303
+ await db.query(`REMOVE DATABASE ${project.db_name};`);
304
+ await db.query(`UPDATE ${REGISTRY_TABLE} SET status = 'deleted', updated_at = time::now() WHERE project_id = $id;`, { id: project.project_id });
305
+ });
306
+ const dir = path.resolve(project.dir || projectDir(root, project.project_id));
307
+ if (opts.purge === true) {
308
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
309
+ } else if (fs.existsSync(dir)) {
310
+ const trash = path.join(workspacePaths(root).fancy, 'trash', `${project.project_id}-${Date.now()}`);
311
+ fs.mkdirSync(path.dirname(trash), { recursive: true });
312
+ try { fs.renameSync(dir, trash); } catch { /* ignore */ }
313
+ }
314
+ const active = getActiveProject(root);
315
+ if (active === project.project_id) setActiveProject(root, null);
316
+ return { project, removed: true, purged: opts.purge === true };
317
+ }
318
+
319
+ export async function setProjectActive(root, nameOrId) {
320
+ const project = await getProject(root, nameOrId);
321
+ if (!project) throw new ProjectError('PROJECT_NOT_FOUND', '未找到匹配的项目:' + nameOrId + '。可运行 /fancy-project list 查看当前工作区的项目。');
322
+ setActiveProject(root, project.project_id);
323
+ return project;
324
+ }
325
+
326
+ export function projectFromCwd(root, cwd) {
327
+ if (!cwd) return null;
328
+ let current = path.resolve(cwd);
329
+ const workspace = path.resolve(root);
330
+ for (;;) {
331
+ const config = readProjectConfig(current);
332
+ if (config) return { config, dir: current };
333
+ if (current === workspace) return null;
334
+ const parent = path.dirname(current);
335
+ if (parent === current) return null;
336
+ current = parent;
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Resolve a project without requiring the registry when the cwd already points
342
+ * at a project directory. If only the workspace is known, use the active
343
+ * project or, when there is exactly one, that project.
344
+ */
345
+ export async function resolveProject(opts = {}) {
346
+ const root = opts.workspace || resolveWorkspace({ cwd: opts.cwd, explicit: opts.explicitWorkspace, required: true });
347
+ const fromCwd = projectFromCwd(root, opts.cwd || process.cwd());
348
+ if (fromCwd) {
349
+ let record = null;
350
+ try { record = await getProject(root, fromCwd.config.project_id); } catch { /* registry may be down */ }
351
+ return {
352
+ ...(record || {}),
353
+ project_id: fromCwd.config.project_id,
354
+ name: (record && record.name) || fromCwd.config.project_id,
355
+ db_name: fromCwd.config.database,
356
+ db_user: fromCwd.config.db_user,
357
+ dir: fromCwd.dir,
358
+ config: fromCwd.config,
359
+ workspace: root,
360
+ };
361
+ }
362
+
363
+ if (opts.project) {
364
+ const project = await getProject(root, opts.project);
365
+ if (!project) throw new ProjectError('PROJECT_NOT_FOUND', '未找到匹配的项目:' + opts.project + '。可运行 /fancy-project list 查看当前工作区的项目。');
366
+ return { ...project, config: readProjectConfig(project.dir), workspace: root };
367
+ }
368
+
369
+ const active = getActiveProject(root);
370
+ if (active) {
371
+ const project = await getProject(root, active);
372
+ if (project) return { ...project, config: readProjectConfig(project.dir), workspace: root };
373
+ }
374
+
375
+ const projects = await listProjects(root);
376
+ if (projects.length === 1) {
377
+ return { ...projects[0], config: readProjectConfig(projects[0].dir), workspace: root };
378
+ }
379
+ if (!projects.length) {
380
+ throw new ProjectError('PROJECT_NOT_FOUND', '当前工作区尚未创建任何项目。请先运行 /fancy-topic setup 创建项目。');
381
+ }
382
+ throw new ProjectError(
383
+ 'PROJECT_AMBIGUOUS',
384
+ '当前工作区存在多个项目,无法自动选择。请使用 --project <小说名|UUID> 指定项目,或先运行 /fancy-project use <小说名|UUID> 设置当前项目。',
385
+ { projects: projects.map((p) => ({ id: p.project_id, name: p.name })) },
386
+ );
387
+ }
388
+
389
+ export async function ensureProjectSchemaReady(project, opts = {}) {
390
+ const { openStoryGraph } = await import('../plugins/fancy-story-builder/storydb/client.js');
391
+ const sdb = await openStoryGraph(project.dir, { admin: true, ...opts });
392
+ try {
393
+ return sdb;
394
+ } catch (err) {
395
+ await sdb.close();
396
+ throw err;
397
+ }
398
+ }
399
+
400
+ export { WorkspaceError };