@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,952 @@
1
+ /**
2
+ * Prompts for the storydb-native generation flow.
3
+ *
4
+ * Each stage prompt is self-contained:
5
+ * 1. what the model is producing
6
+ * 2. a command reference with purpose / when / example
7
+ * 3. a step-by-step workflow
8
+ * 4. one copyable `apply` payload
9
+ * 5. hard rules and recovery instructions
10
+ * 6. the stage-check completion gate
11
+ *
12
+ * The LLM writes through the storydb CLI. It never writes stage JSON files and
13
+ * never writes generator scripts.
14
+ */
15
+
16
+ import { EDGES } from './definitions.js';
17
+
18
+ const CLI = 'node <cli> --db <root>';
19
+
20
+
21
+ function json(value) {
22
+ return JSON.stringify(value || {}, null, 2);
23
+ }
24
+
25
+ function edgeList(tables) {
26
+ return tables
27
+ .map((table) => {
28
+ const edge = EDGES[table];
29
+ return edge ? '- `' + table + '`:' + edge.in + ' -> ' + edge.out : null;
30
+ })
31
+ .filter(Boolean)
32
+ .join('\n');
33
+ }
34
+
35
+ function setup() {
36
+ return `## 工具入口
37
+
38
+ 本次**不使用 bash、不使用 storydb CLI、不直接改数据库文件**。所有读写都通过工具:
39
+
40
+ - 读/分析:\`fancy_story_read\`,用 \`action\` 选择
41
+ \`status\` / \`schema\` / \`get\` / \`search\` / \`query\` / \`context\` / \`validate\` / \`stage_check\` / \`next\` / \`why\` / \`impact\`
42
+ - 写:阶段批量用 \`fancy_story_apply_batch\`;自然语言写用 \`fancy_story_apply\`(会弹窗确认)
43
+ - 导出:\`fancy_story_export\`
44
+ - 项目/数据库:\`fancy_workspace\`、\`fancy_db\`
45
+
46
+ 如果客户端是把 MCP 工具直接暴露出来,名字可能是 \`story_status\` / \`story_schema\` / \`story_apply\`,或带 \`mcp__fancy__\` 前缀;看到哪种就用哪种。不要自己拼 shell 命令。`;
47
+ }
48
+
49
+ function rules(options = {}) {
50
+ const perChapter = options.perChapter
51
+ ? '\n6. 章节用 \`fancy_story_apply_batch\` 的 \`perChapter: true\` + `chapters: [...]` 分批提交,每批 5-10 章;超过 10 章会被 `BATCH_TOO_LARGE` 拒绝。每章是一个独立事务;某章失败只回滚该章。'
52
+ : '';
53
+ const decisionNo = options.perChapter ? 7 : 6;
54
+ const parallelNo = decisionNo + 1;
55
+ const reportNo = decisionNo + 2;
56
+ const refs = options.refs
57
+ ? `\n## 参考资料\n\n先读项目里这些文件,再动手设计;不要凭记忆编造公式、节奏或文风要求。\n\n${options.refs}\n`
58
+ : '';
59
+ return `## 铁律
60
+
61
+ 1. 不写 \`设定/**/*.json\`,不运行旧的 \`build\` 命令,不读插件源码。
62
+ 2. 不要写 Node/Python/Shell 生成脚本,也不要用 \`python3 - <<\` / \`node -e\` 修改 JSON。要批量写入就分多次 \`fancy_story_apply_batch\`;章节用 \`perChapter: true\`,每批最多 10 章。
63
+ 3. 写入分两种:
64
+ - 当前是 \`/fancy-build\` 阶段:用 \`fancy_story_apply_batch\`。用户已经显式发起阶段,不需要逐批确认;工具内部会先预览再提交。
65
+ - 用户用自然语言要求改数据:用 \`fancy_story_apply\`;它会先预览并弹窗要求用户确认。
66
+ 一个 apply 是一个事务;失败会回滚,错误原文会返回给你。
67
+ 4. 先查再写。不要编造 ID;引用人物/势力/地点/伏笔/爽点/信息差之前,先用 \`fancy_story_read\`、\`fancy_story_read\` 或 \`fancy_story_read\` 确认。
68
+ 5. 缺细节可以先用 \`status: "seed"\` 或 \`status: "draft"\` 占位;不要把未完成的记录标成 \`complete\`。${perChapter}
69
+ ${decisionNo}. 每个 \`fancy_story_apply_batch\` 至少带一条 \`decisions\`(subject / text / reason),说明这次为什么这样写。决策和数据同事务。
70
+ ${parallelNo}. **不要使用 subagent 并行写同一个项目。** 服务端支持多客户端,但同一章的并发写入仍会互相覆盖;一个项目同一时间只让一个写手提交。
71
+ ${reportNo}. 不要把判断写成“需要你知道”“请确认”之类的聊天汇报。需要保留的判断写进 \`decisions\`;结束时只报告 \`fancy_story_read\` 结果和关键计数。
72
+
73
+ ## 表结构怎么查
74
+
75
+ 不要背表结构,也不要要求把整份 schema 塞进上下文。先查再用:
76
+
77
+ - 列出全部表:\`fancy_story_read {"action":"schema"}\`
78
+ - 看某张表:\`fancy_story_read {"action":"schema","table":"<表名>"}\`(字段、必填、枚举、关联边)
79
+ - 查几条记录:\`fancy_story_read {"action":"search","kind":"<表名>","where":{"字段":"值"},"limit":20}\`
80
+ - 只取需要的列:\`fancy_story_read {"action":"query","sql":"SELECT entity_id AS id, name, type FROM character ORDER BY entity_id"}\`
81
+ - 不要用 \`grep\` / \`awk\` / \`node -e\` 解析工具输出;工具返回的就是结构化 JSON。
82
+ - 边表也能 \`fancy_story_read\`(例如 \`fancy_story_read {"action":"search","kind":"foreshadow_payoff","limit":20}\`);边不要用 \`update\`,重新 \`link\` 同一条边会覆盖它的 data。
83
+
84
+ 实体表:\`story\`、\`scale\`、\`volume\`、\`act\`、\`world_doc\`、\`story_doc\`、\`line\`、\`line_node\`、\`ending\`、\`role_slot\`、\`character\`、\`faction\`、\`location\`、\`item\`、\`conflict\`、\`foreshadow\`、\`payoff\`、\`climax\`、\`information\`、\`chapter\`、\`outline\`、\`scene\`、\`chapter_text\`、\`handoff\`。
85
+
86
+ 边表:\`has_volume\`、\`has_act\`、\`has_chapter\`、\`chapter_cast\`、\`chapter_location\`、\`chapter_line_node\`、\`chapter_branch\`、\`chapter_conflict\`、\`chapter_climax\`、\`chapter_item\`、\`relation\`、\`faction_relation\`、\`membership\`、\`conflict_participant\`、\`climax_participant\`、\`foreshadow_payoff\`、\`foreshadow_action\`、\`payoff_action\`、\`info_action\`、\`ending_action\`、\`outline_scene\`、\`outline_cast\`、\`outline_location\`、\`outline_foreshadow\`、\`scene_cast\`、\`scene_location\`、\`scene_foreshadow\`。
87
+
88
+ ID 规则:
89
+
90
+ - \`create\` 的 ID 放在 \`data.id\`(人物、势力、地点、道具、冲突、伏笔、爽点、高潮、信息差);\`update\` 的 ID 放在 \`id\`。
91
+ - 卷/幕/线索/节点/结局/功能位用各自的 \`volume_id\`、\`act_id\`、\`line_id\`、\`node_id\`、\`ending_id\`、\`role_id\`。
92
+ - 章节用 \`chapter_id\`(如 \`CH341\`);细纲/正文/交接也用 \`chapter_id\`。
93
+ - \`scale\`、\`world_doc\`、\`story_doc\` 是单例,不需要 ID。
94
+ - 场景 \`scene\` 的 ID 自动是 \`SC_<chapter_id>_<scene_order>\`,例如 \`scene:SC_CH341_1\`。
95
+
96
+ ${refs}`;
97
+ }
98
+
99
+ function applyFormat() {
100
+ return `## apply 的格式
101
+
102
+ \`\`\`json
103
+ {
104
+ "workflow": "rhythm",
105
+ "summary": "第 341-342 章",
106
+ "ops": [
107
+ { "op": "create", "kind": "chapter", "data": { "chapter_id": "CH341" } },
108
+ { "op": "link", "edge": "chapter_cast", "from": "chapter:CH341", "to": "character:C001", "data": { "role": "pov" } },
109
+ { "op": "knowledge", "data": { "chapter_id": "CH341", "subject_type": "foreshadow", "subject_id": "FS001", "character_id": "C001", "change": "learns", "coverage": "partial" } }
110
+ ],
111
+ "decisions": [
112
+ { "subject": "chapter:CH341", "text": "第 341 章承担公开复核表的转折", "reason": "承接上一章的反对者" }
113
+ ]
114
+ }
115
+ \`\`\`
116
+
117
+ > 用 \`fancy_story_apply_batch\` 提交这份参数;它会先预览再写入。自然语言写入请改用 \`fancy_story_apply\`。
118
+
119
+ 可用的 op:
120
+
121
+ | op | 作用 | 关键字段 |
122
+ | --- | --- | --- |
123
+ | \`create\` | 新建或覆盖一条实体 | \`kind\`、\`data\` |
124
+ | \`update\` | 局部修改已有实体 | \`kind\`、\`id\`、\`data\` |
125
+ | \`link\` | 新建或覆盖一条边 | \`edge\`、\`from\`、\`to\`、\`data\` |
126
+ | \`unlink\` | 删除一条边 | \`edge\`、\`from\`、\`to\`,可加 \`data.action\` |
127
+ | \`knowledge\` | 记录一次知识变化 | \`data\`(见 knowledge_event 表) |`;
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // scale
132
+ // ---------------------------------------------------------------------------
133
+
134
+ export function buildScalePrompt(ctx) {
135
+ return `# scale:确定故事的规模骨架
136
+
137
+ 你要为一本网文确定:总章数、总字数、卷、幕、每卷/幕的章节区间和各类实体配额。结果写入 SurrealDB 故事图,不写文件。
138
+
139
+ ## 本阶段产出
140
+
141
+ - \`scale:main\`:容量、结构、配额、节奏预算。
142
+ - \`volume\`:每卷的 \`chapter_start\` / \`chapter_end\`。
143
+ - \`act\`:每幕的 \`chapter_start\` / \`chapter_end\`,挂到所属卷。
144
+ - 边:\`has_volume\`(story -> volume)、\`has_act\`(volume -> act)。
145
+
146
+ ${setup()}
147
+
148
+ ${rules({ refs: '- `references/formulas-skeleton.json`:数量公式和硬上限\n- `references/genre-profiles.json`:题材参数、卷长、倍率' })}
149
+
150
+ ## 工具速查
151
+
152
+ | 目的 | 工具调用 | 什么时候用 |
153
+ | --- | --- | --- |
154
+ | 看当前故事/规模 | \`fancy_story_read {"action":"status"}\` | 开始前,确认没有残留规模数据 |
155
+ | 看表结构 | \`fancy_story_read {"action":"schema","table":"scale"}\`、\`fancy_story_read {"action":"schema","table":"volume"}\`、\`fancy_story_read {"action":"schema","table":"act"}\` | 第一次写这三张表前 |
156
+ | 提交规模和卷/幕 | \`fancy_story_preview\` → \`fancy_story_apply_batch\` | 全部算好后一次提交 |
157
+ | 检查完成度 | \`fancy_story_read {"action":"stage_check","stage":"scale"}\` | 提交后,必须通过 |
158
+ | 看已写入的卷 | \`fancy_story_read {"action":"search","kind":"volume","limit":50}\` | 需要确认卷区间时 |
159
+
160
+ ## 执行步骤
161
+
162
+ 1. 如果项目里存在 \`references/formulas-skeleton.json\` 或 \`references/genre-profiles.json\`,先读它们,用里面的公式和题材参数计算;不存在就按选题和常识计算。
163
+ 2. 计算并写死这些数:
164
+ - \`target_chapters\`、\`target_words\`、\`avg_words_per_chapter\`
165
+ - 卷数、每卷的 \`chapter_start\` / \`chapter_end\`
166
+ - 每卷的幕数、每幕的 \`chapter_start\` / \`chapter_end\`
167
+ - 人物/势力/地点/道具/冲突/伏笔/高潮/爽点/结局的配额
168
+ 3. 用一次 \`apply\` 写入 \`scale:main\`、所有 \`volume\`、所有 \`act\`,以及 \`has_volume\` / \`has_act\` 边。
169
+ 4. 运行 \`fancy_story_read {"action":"stage_check","stage":"scale"}\`,直到 \`ok: true\`。
170
+
171
+ ## 选题
172
+
173
+ \`\`\`json
174
+ ${json(ctx.topic)}
175
+ \`\`\`
176
+
177
+ ## 边
178
+
179
+ - \`has_volume\`:story -> volume
180
+ - \`has_act\`:volume -> act
181
+
182
+ ## 可复制的提交示例
183
+
184
+ 把下面的数字换成你算出的结果。卷和幕的数量按实际增减。
185
+
186
+ 参数(用 \`fancy_story_apply_batch\` 提交;它会先预览再写入):
187
+
188
+ \`\`\`json
189
+ {
190
+ "workflow": "scale",
191
+ "summary": "500 章 / 5 卷 / 15 幕",
192
+ "ops": [
193
+ {
194
+ "op": "create",
195
+ "kind": "scale",
196
+ "data": {
197
+ "capacity": { "target_chapters": 500, "target_words": 1500000, "avg_words_per_chapter": 3000 },
198
+ "structure": { "volume_count": 5, "acts_per_volume": 3 },
199
+ "quotas": {
200
+ "characters": { "protagonist": 1, "core_supporting": 6, "volume_supporting": 30, "core_antagonist": 2, "volume_antagonist": 3 },
201
+ "factions": 8,
202
+ "locations": 30,
203
+ "items": { "core": 5, "branch": 20, "symbolic": 10 },
204
+ "conflicts": { "core": 1, "phase": 15, "branch": 30 },
205
+ "foreshadows": { "small": 60, "medium": 30, "large": 10 },
206
+ "climaxes": { "small": 20, "medium": 10, "large": 5 },
207
+ "pleasures": { "small": 80, "medium": 35, "large": 12 },
208
+ "endings": 3
209
+ },
210
+ "rhythm_budget": {},
211
+ "distribution": {},
212
+ "constraints": {},
213
+ "formulas": {},
214
+ "derivations": {},
215
+ "status": "draft"
216
+ }
217
+ },
218
+ { "op": "create", "kind": "volume", "data": { "volume_id": "V001", "title": "第一卷", "chapter_start": 1, "chapter_end": 100, "goal": "建立秩序", "status": "draft" } },
219
+ { "op": "create", "kind": "act", "data": { "act_id": "A001", "volume_id": "V001", "act_index": 1, "chapter_start": 1, "chapter_end": 33, "goal": "进入新环境", "status": "draft" } },
220
+ { "op": "link", "edge": "has_volume", "from": "story:main", "to": "volume:V001", "data": {} },
221
+ { "op": "link", "edge": "has_act", "from": "volume:V001", "to": "act:A001", "data": {} }
222
+ ],
223
+ "decisions": [
224
+ { "subject": "scale:main", "text": "按 500 章、5 卷、每卷 3 幕切分", "reason": "题材公式给出的中长线容量" }
225
+ ]
226
+ }
227
+ \`\`\`
228
+
229
+ ## 完成标准
230
+
231
+ - \`scale:main\` 存在,\`capacity\` / \`quotas\` / \`structure\` 不为空。
232
+ - 所有卷覆盖 \`1..target_chapters\`,卷内幕区间连续、不重叠。
233
+ - \`fancy_story_read {"action":"stage_check","stage":"scale"}\` 返回 \`ok: true\`。
234
+ `;
235
+ }
236
+
237
+ // ---------------------------------------------------------------------------
238
+ // macro
239
+ // ---------------------------------------------------------------------------
240
+
241
+ export function buildMacroPrompt(ctx) {
242
+ return `# macro:补世界观、故事、线索、结局和功能位
243
+
244
+ scale 已经给出卷/幕骨架。现在在骨架上补:世界观、故事核心、主线/支线、主线节点、结局、角色功能位,并给卷/幕补目标。
245
+
246
+ ## 本阶段产出
247
+
248
+ - \`world_doc\`:世界观前提、主题、规则、力量体系、社会秩序、禁忌、意象、读者承诺。
249
+ - \`story_doc\`:核心问题、核心冲突、结构、反套路、承诺。
250
+ - \`line\` + \`line_node\`:至少 1 条主线,主线拆成若干节点,节点落到真实卷/幕。
251
+ - \`ending\`:至少 1 个结局,写明要回收的伏笔 ID。
252
+ - \`role_slot\`:主角、核心配角、反派等功能位。
253
+ - 需要时给已有 \`volume\` / \`act\` 补 \`goal\`、\`core_conflict\`、\`emotional_arc\`、\`ending_hook\`。
254
+
255
+ ${setup()}
256
+
257
+ ${rules({ refs: '- `references/骨架指导/`:卷纲、幕纲、主支线的写法\n- `references/glossary.md`、`references/index.md`:术语和目录约定' })}
258
+
259
+ ## 工具速查
260
+
261
+ | 目的 | 工具调用 | 什么时候用 |
262
+ | --- | --- | --- |
263
+ | 看已有规模 | \`fancy_story_read {"action":"search","kind":"volume","limit":50}\`、\`fancy_story_read {"action":"search","kind":"act","limit":100}\` | 开始前,确认卷/幕 ID |
264
+ | 看表结构 | \`fancy_story_read {"action":"schema","table":"world_doc"}\`、\`schema story_doc\`、\`schema line\`、\`schema line_node\`、\`schema ending\`、\`schema role_slot\` | 写对应表前 |
265
+ | 看某个卷/幕 | \`fancy_story_read {"action":"get","kind":"volume","id":"V001"}\`、\`fancy_story_read {"action":"get","kind":"act","id":"A001"}\` | 需要补目标时 |
266
+ | 提交 | \`fancy_story_preview\` → \`fancy_story_apply_batch\` | 一批实体 + 边写完后 |
267
+ | 检查 | \`fancy_story_read {"action":"stage_check","stage":"macro"}\` | 提交后,必须通过 |
268
+
269
+ ## 执行步骤
270
+
271
+ 1. 先看 \`fancy_story_read {"action":"status"}\`,确认 scale 已存在、macro 还没有写完。
272
+ 2. 写 \`world_doc\`:世界观前提、主题、规则、力量体系、社会秩序、禁忌、意象、读者承诺。
273
+ 3. 写 \`story_doc\`:核心问题、核心冲突、结构、反套路、承诺。
274
+ 4. 写 1 条主线和若干支线;每条主线至少拆 3 个 \`line_node\`,节点要落到卷/幕。
275
+ 5. 写 \`ending\`:至少一个主结局,注明需要回收的伏笔 ID。
276
+ 6. 写 \`role_slot\`:主角、核心配角、反派等功能位,后面 cast 会按这些槽位建人物。
277
+ 7. 需要时用 \`update\` 给已有 \`volume\` / \`act\` 补 \`goal\`、\`core_conflict\`、\`emotional_arc\`、\`ending_hook\`。
278
+ 8. 运行 \`fancy_story_read {"action":"stage_check","stage":"macro"}\`,直到通过。
279
+
280
+ ## 上游 scale
281
+
282
+ \`\`\`json
283
+ ${json(ctx.scale)}
284
+ \`\`\`
285
+
286
+ ## 边
287
+
288
+ - \`has_volume\`:story -> volume(scale 已写)
289
+ - \`has_act\`:volume -> act(scale 已写)
290
+
291
+ ## 可复制的提交示例
292
+
293
+ 参数(用 \`fancy_story_apply_batch\` 提交;它会先预览再写入):
294
+
295
+ \`\`\`json
296
+ {
297
+ "workflow": "macro",
298
+ "summary": "世界观 / 主线 / 结局 / 功能位",
299
+ "ops": [
300
+ { "op": "create", "kind": "world_doc", "data": { "premise": "资源枯竭后的城市靠配给维持秩序", "theme": ["秩序", "公平"], "tone": "冷峻", "world_rules": ["配给表公开可查"], "status": "draft" } },
301
+ { "op": "create", "kind": "story_doc", "data": { "core_question": "公开的秩序能不能被真正执行", "core_conflict": "主角坚持公开,既得利益者要求私下分配", "status": "draft" } },
302
+ { "op": "create", "kind": "line", "data": { "line_id": "M001", "kind": "main", "name": "重建配给秩序", "goal": "让配给表公开且可复核", "status": "draft" } },
303
+ { "op": "create", "kind": "line_node", "data": { "node_id": "M001-01", "line_id": "M001", "volume": "V001", "act": "A001", "goal": "第一次公开复核", "status": "draft" } },
304
+ { "op": "create", "kind": "ending", "data": { "ending_id": "E001", "level": "主线", "type": "阶段性胜利", "direction": "秩序被保留,但代价被看见", "required_foreshadow_recovery": ["FS001"], "status": "draft" } },
305
+ { "op": "create", "kind": "role_slot", "data": { "role_id": "R001", "name": "主角", "slot_function": "推动公开复核", "quota_key": "protagonist", "status": "draft" } },
306
+ { "op": "update", "kind": "volume", "id": "V001", "data": { "goal": "建立新秩序", "core_conflict": "公开与私分的冲突" } }
307
+ ],
308
+ "decisions": [
309
+ { "subject": "line:M001", "text": "主线围绕公开配给表推进", "reason": "核心问题需要一个可被章节验证的动作" }
310
+ ]
311
+ }
312
+ \`\`\`
313
+
314
+ ## 完成标准
315
+
316
+ - \`world_doc\`、\`story_doc\` 各至少 1 条。
317
+ - 至少 1 条主线、3 个主线节点、1 个结局、若干 \`role_slot\`。
318
+ - 主线节点引用的卷/幕真实存在。
319
+ - \`fancy_story_read {"action":"stage_check","stage":"macro"}\` 返回 \`ok: true\`。
320
+ `;
321
+ }
322
+
323
+ // ---------------------------------------------------------------------------
324
+ // cast
325
+ // ---------------------------------------------------------------------------
326
+
327
+ export function buildCastPrompt(ctx) {
328
+ return `# cast:建立人物、势力、地点、冲突和伏笔网络
329
+
330
+ scale 给出配额,macro 给出主线/结局/功能位。现在按配额补齐实体和它们之间的关系,建立后面 rhythm 能直接引用的 ID 网络。
331
+
332
+ ## 本阶段产出
333
+
334
+ - 按配额补齐 \`character\`、\`faction\`、\`location\`、\`item\`、\`conflict\`、\`foreshadow\`、\`payoff\`、\`climax\`、\`information\`。
335
+ - 建立 \`relation\`、\`faction_relation\`、\`membership\`、\`conflict_participant\`、\`climax_participant\`、\`foreshadow_payoff\`。
336
+ - 人物绑定 \`role_slot\`;伏笔/爽点成对规划,具体章节动作留到 rhythm。
337
+
338
+ ${setup()}
339
+
340
+ ${rules({ refs: '- `references/血肉指导/`:人物、势力、冲突、伏笔的写法\n- `references/genre-profiles.json`、`references/glossary.md`:题材约束和术语' })}
341
+
342
+ ## 工具速查
343
+
344
+ | 目的 | 工具调用 | 什么时候用 |
345
+ | --- | --- | --- |
346
+ | 看总进度 | \`fancy_story_read {"action":"status"}\` | 每批写入后 |
347
+ | 看配额 | \`fancy_story_read {"action":"get","kind":"scale","id":"main"}\` | 开始前,确认还缺多少 |
348
+ | 看表结构 | \`fancy_story_read {"action":"schema","table":"character"}\` 等 | 第一次写某张表前 |
349
+ | 找已有实体 | \`fancy_story_read {"action":"search","kind":"character","where":{"type":"主角"}}\`、\`fancy_story_read {"action":"search","kind":"foreshadow","limit":200}\` | 避免重复 ID |
350
+ | 看单个实体 | \`fancy_story_read {"action":"get","kind":"character","id":"C001"}\` | 写关系前确认存在 |
351
+ | 提交 | \`fancy_story_preview\` → \`fancy_story_apply_batch\` | 每批 10-20 个实体 |
352
+ | 检查 | \`fancy_story_read {"action":"stage_check","stage":"cast"}\` | 每批后/结束 |
353
+
354
+ ## 执行步骤
355
+
356
+ 1. \`fancy_story_read {"action":"status"}\`,再 \`fancy_story_read {"action":"get","kind":"scale","id":"main"}\` 看配额。
357
+ 2. 按这个顺序写,先实体后关系:
358
+ - \`faction\` → \`location\` → \`character\`(绑定 \`role_slot\`)
359
+ - \`item\` → \`conflict\` → \`foreshadow\` → \`payoff\` → \`climax\` → \`information\`
360
+ - 最后写 \`relation\`、\`faction_relation\`、\`membership\`、\`conflict_participant\`、\`climax_participant\`、\`foreshadow_payoff\`
361
+ 3. 每个实体必须有稳定 ID。人物 \`role_slot\` 指向 macro 里已有的 \`role_slot.role_id\`。
362
+ 4. 伏笔和爽点要成对规划:每个 \`foreshadow\` 至少有 plant 和 reveal 的回收目标;每个 \`payoff\` 至少有 setup 和 payoff 的兑现方向。这里只写实体,不写具体章节动作(章节动作在 rhythm)。
363
+ 5. 每批 10-20 个实体提交一次;失败按错误原文改那一批。
364
+ 6. 全部写完后运行 \`fancy_story_read {"action":"stage_check","stage":"cast"}\`。数量不足会报 \`STAGE_INCOMPLETE\`,按 \`expected/actual\` 补。
365
+
366
+ ## 上游配额
367
+
368
+ \`\`\`json
369
+ ${json(ctx.quotas)}
370
+ \`\`\`
371
+
372
+ ## 上游 macro
373
+
374
+ \`\`\`json
375
+ ${json(ctx.macro)}
376
+ \`\`\`
377
+
378
+ ## 边
379
+
380
+ ${edgeList(['relation', 'faction_relation', 'membership', 'conflict_participant', 'climax_participant', 'foreshadow_payoff'])}
381
+
382
+ ## 可复制的提交示例
383
+
384
+ 参数(用 \`fancy_story_apply_batch\` 提交;它会先预览再写入):
385
+
386
+ \`\`\`json
387
+ {
388
+ "workflow": "cast",
389
+ "summary": "第一批:主角 / 核心配角 / 地点 / 伏笔",
390
+ "ops": [
391
+ { "op": "create", "kind": "character", "data": { "id": "C001", "name": "沈砚", "type": "主角", "role_slot": "R001", "goal": { "external": "查清配给表被改的真相", "internal": "证明自己不是靠关系上位" }, "flaw": "不信任体制内的人", "status": "draft" } },
392
+ { "op": "create", "kind": "character", "data": { "id": "C002", "name": "韩雨棠", "type": "核心配角", "role_slot": "R002", "goal": { "external": "保住复核组" }, "status": "draft" } },
393
+ { "op": "create", "kind": "location", "data": { "id": "L001", "name": "配给公示栏", "type": "公共空间", "narrative_function": "公开与私下的冲突现场", "status": "draft" } },
394
+ { "op": "create", "kind": "foreshadow", "data": { "id": "FS001", "name": "空白的来源栏", "type": "medium", "setup": "公示表上有一栏永远空白", "payoff_target": "配给来源被调包", "scope": "book", "status": "draft" } },
395
+ { "op": "create", "kind": "payoff", "data": { "id": "P001", "name": "当众念完复核表", "scale": "medium", "setup": "有人要求先私下看", "payoff": "主角坚持当场念完", "status": "draft" } },
396
+ { "op": "link", "edge": "relation", "from": "character:C001", "to": "character:C002", "data": { "kind": "合作", "initial_state": "互相试探" } },
397
+ { "op": "link", "edge": "foreshadow_payoff", "from": "foreshadow:FS001", "to": "payoff:P001", "data": { "relation": "揭示来源" } }
398
+ ],
399
+ "decisions": [
400
+ { "subject": "character:C001", "text": "主角设定为复核执行者", "reason": "主线需要一个能推动公开复核的人" }
401
+ ]
402
+ }
403
+ \`\`\`
404
+
405
+ ## 完成标准
406
+
407
+ - 各类实体数量达到 scale 配额。
408
+ - 所有边引用的实体都存在(DB 会拒绝悬空边)。
409
+ - 人物都绑定了 \`role_slot\`。
410
+ - \`fancy_story_read {"action":"stage_check","stage":"cast"}\` 返回 \`ok: true\`。
411
+ `;
412
+ }
413
+
414
+ // ---------------------------------------------------------------------------
415
+ // rhythm
416
+ // ---------------------------------------------------------------------------
417
+
418
+ export function buildRhythmPrompt(ctx) {
419
+ const target = (ctx.scale && ctx.scale.capacity && ctx.scale.capacity.target_chapters)
420
+ || (ctx.topic && ctx.topic.target_chapters)
421
+ || 0;
422
+ return `# rhythm:把设定落到每一章
423
+
424
+ 你要为第 1 章到第 ${target || '目标'} 章建立 \`chapter\` 节点,并写清每章的:谁出场、在哪、推进哪条线、制造什么冲突、埋/收哪个伏笔、铺垫/兑现哪个爽点、章末钩子。完成后 \`stage-check rhythm\` 必须通过。
425
+
426
+ ## 本阶段产出
427
+
428
+ - \`chapter\`:覆盖 \`1..target\`,\`display_no\` 连续。
429
+ - 每章至少 1 条 \`chapter_cast\`,POV 用 \`role: "pov"\`;至少 1 条 \`chapter_location\`。
430
+ - 按需要写 \`chapter_line_node\` / \`chapter_branch\` / \`chapter_conflict\`。
431
+ - 有高潮的章写 \`chapter_climax\`;有明确道具的章写 \`chapter_item\`。
432
+ - 按节奏写 \`foreshadow_action\` / \`payoff_action\` / \`info_action\`,必要时写 \`knowledge\`。
433
+ - 每章至少一条 \`decisions\`。
434
+
435
+ ${setup()}
436
+
437
+ ${rules({ perChapter: true, refs: '- `references/节奏指导/`:章节节奏、钩子、情绪曲线\n- `references/formulas-rhythm.json`、`references/genre-profiles.json`:节奏参数' })}
438
+
439
+ ## 工具速查
440
+
441
+ | 目的 | 工具调用 | 什么时候用 |
442
+ | --- | --- | --- |
443
+ | 看总进度 | \`fancy_story_read {"action":"status"}\` | 开始前、每批后 |
444
+ | 看已有章节 | \`fancy_story_read {"action":"search","kind":"chapter","limit":20}\` | 规划下一批前,避免重复 |
445
+ | 看卷/幕 ID | \`fancy_story_read {"action":"search","kind":"volume","limit":50}\`、\`fancy_story_read {"action":"search","kind":"act","limit":100}\` | 不确定章节归属时 |
446
+ | 查人物/伏笔/爽点 | \`fancy_story_read {"action":"get","kind":"character","id":"C001"}\`、\`fancy_story_read {"action":"get","kind":"foreshadow","id":"FS001"}\`、\`fancy_story_read {"action":"search","kind":"payoff","limit":500}\` | 需要具体设定时 |
447
+ | 看单章上下文 | \`fancy_story_read {"action":"context","chapter":341,"prev":3}\` | 写/修某一章前;会返回该章已有的动作和知识事件 |
448
+ | 看实体关系 | \`fancy_story_read {"action":"context","entity":"foreshadow:FS012"}\` | 不确定伏笔埋设/回收时 |
449
+ | 提交章节 | \`fancy_story_preview\` → \`fancy_story_apply_batch\`(\`perChapter: true\`) | 每 5-10 章提交一次 |
450
+ | 检查 | \`fancy_story_read {"action":"stage_check","stage":"rhythm"}\` | 每批后/结束,必须通过 |
451
+ | 修单条动作 | \`fancy_story_apply_batch {"ops":[{"op":"unlink","edge":"payoff_action","from":"chapter:CH341","to":"payoff:P001","data":{"action":"setup"}}]}\` | 只删动作,不重写整章时 |
452
+
453
+ ## 执行步骤
454
+
455
+ 1. 先 \`fancy_story_read {"action":"status"}\` 和 \`fancy_story_read {"action":"search","kind":"chapter","limit":20}\`,确认已有章节和下一批起点。
456
+ 2. 按卷、按幕、按章号递增规划。每章至少回答:
457
+ - 这一章推进哪条主线/支线节点?
458
+ - 谁出场,POV 是谁?
459
+ - 时间(\`story_day\`)和地点是什么?
460
+ - 这一章的核心冲突和转折是什么?
461
+ - 章末钩子是什么?连续 3 章不能同类型。
462
+ - 本章要埋/推进/回收哪些伏笔?要 setup/payoff 哪些爽点?要创建/推进/解决哪些信息差?
463
+ 3. 每 5-10 章提交一次 \`apply perChapter: true\`。每章是完整替换:默认会清掉该章原有的 cast/action 边,再写你这次提交的边,所以每章要把该章的边写全。
464
+ 4. 每批后运行 \`fancy_story_read {"action":"stage_check","stage":"rhythm"}\`。只修 \`errors\`;\`warnings\` 不阻断,但要看一眼。
465
+ 5. 最后确认章节覆盖 1..${target || 'target'}、每章至少 1 个 cast。
466
+
467
+ ## 上游规模
468
+
469
+ \`\`\`json
470
+ ${json(ctx.scale)}
471
+ \`\`\`
472
+
473
+ ## 上游 cast(只给数量;具体实体用 query/search 查)
474
+
475
+ \`\`\`json
476
+ ${json(ctx.cast && ctx.cast.counts)}
477
+ \`\`\`
478
+
479
+ ## 主线/支线节点
480
+
481
+ \`\`\`json
482
+ ${json({
483
+ lines: (ctx.macro && ctx.macro.lines) || [],
484
+ nodes: (ctx.macro && ctx.macro.nodes) || [],
485
+ })}
486
+ \`\`\`
487
+
488
+ ## 一张章节图要写的字段
489
+
490
+ 写之前运行 \`fancy_story_read {"action":"schema","table":"chapter"}\` 和 \`fancy_story_read {"action":"schema","table":"knowledge_event"}\`。必填字段以 schema 为准;本章 example 只展示写法。
491
+
492
+ ## 边
493
+
494
+ ${edgeList(['has_chapter', 'chapter_cast', 'chapter_location', 'chapter_line_node', 'chapter_branch', 'chapter_conflict', 'chapter_climax', 'chapter_item', 'foreshadow_action', 'payoff_action', 'info_action', 'ending_action'])}
495
+
496
+ ## 可复制的两章提交示例
497
+
498
+ 下面的 ID 只是示例,必须换成 DB 里真实存在的 ID。每章都要有自己的 \`decisions\`。
499
+
500
+ 参数(用 \`fancy_story_apply_batch\` 提交;它会先预览再写入):
501
+
502
+ \`\`\`json
503
+ {
504
+ "perChapter": true,
505
+ "workflow": "rhythm",
506
+ "summary": "第 341-342 章",
507
+ "chapters": [
508
+ {
509
+ "chapter": "CH341",
510
+ "ops": [
511
+ { "op": "create", "kind": "chapter", "data": {
512
+ "chapter_id": "CH341",
513
+ "display_no": 341,
514
+ "title": "第一张复核表",
515
+ "volume": "V004",
516
+ "act": "A012",
517
+ "pov": "C001",
518
+ "time": { "story_day": 318, "clock": "20:00-21:00", "duration_minutes": 60 },
519
+ "why": { "main_line_nodes": ["M001-12"], "branch_nodes": ["B003"], "conflict": ["X033"], "theme": "秩序要能被核对", "stakes": "配额表一旦公开就不能再改" },
520
+ "how": { "function_main": "推进重建秩序", "function_aux": "回收 FS012 的铺垫", "turn": "公开的配额表逼出第一个反对者", "ending_hook": "反对者拿出另一本账" },
521
+ "emotion": { "phase": "紧绷", "value": 4 },
522
+ "hook": { "type": "突然揭示", "strength": "强" },
523
+ "word_target": 2400,
524
+ "status": "draft"
525
+ } },
526
+ { "op": "link", "edge": "has_chapter", "from": "act:A012", "to": "chapter:CH341", "data": {} },
527
+ { "op": "link", "edge": "chapter_cast", "from": "chapter:CH341", "to": "character:C001", "data": { "role": "pov" } },
528
+ { "op": "link", "edge": "chapter_cast", "from": "chapter:CH341", "to": "character:C027", "data": { "role": "cast" } },
529
+ { "op": "link", "edge": "chapter_location", "from": "chapter:CH341", "to": "location:L011", "data": {} },
530
+ { "op": "link", "edge": "chapter_line_node", "from": "chapter:CH341", "to": "line_node:M001-12", "data": {} },
531
+ { "op": "link", "edge": "chapter_conflict", "from": "chapter:CH341", "to": "conflict:X033", "data": {} },
532
+ { "op": "link", "edge": "foreshadow_action", "from": "chapter:CH341", "to": "foreshadow:FS012", "data": { "action": "plant", "chapter_no": 341, "detail": "来源栏第一次空着" } },
533
+ { "op": "link", "edge": "payoff_action", "from": "chapter:CH341", "to": "payoff:P086", "data": { "action": "setup", "chapter_no": 341, "detail": "有人要求先私下看" } },
534
+ { "op": "knowledge", "data": { "chapter_id": "CH341", "chapter_no": 341, "subject_type": "foreshadow", "subject_id": "FS012", "character_id": "C001", "change": "learns", "coverage": "partial", "knowledge": "来源栏可能被改过", "how": "witnessed", "scene": "公示栏", "evidence": "空白栏" } }
535
+ ],
536
+ "decisions": [
537
+ { "subject": "chapter:CH341", "text": "公开复核表逼出第一个反对者", "reason": "把主线从查账推进到公开对抗" }
538
+ ]
539
+ },
540
+ {
541
+ "chapter": "CH342",
542
+ "ops": [
543
+ { "op": "create", "kind": "chapter", "data": {
544
+ "chapter_id": "CH342",
545
+ "display_no": 342,
546
+ "title": "另一本账",
547
+ "volume": "V004",
548
+ "act": "A012",
549
+ "pov": "C001",
550
+ "time": { "story_day": 318, "clock": "21:00-22:00", "duration_minutes": 60 },
551
+ "why": { "main_line_nodes": ["M001-12"], "conflict": ["X033"], "theme": "账本的真实性", "stakes": "两本账必须有一本假的" },
552
+ "how": { "function_main": "推进冲突", "turn": "反对者拿出另一本账", "ending_hook": "两本账的签名一致" },
553
+ "emotion": { "phase": "紧张", "value": 5 },
554
+ "hook": { "type": "两难抉择", "strength": "强" },
555
+ "word_target": 2400,
556
+ "status": "draft"
557
+ } },
558
+ { "op": "link", "edge": "has_chapter", "from": "act:A012", "to": "chapter:CH342", "data": {} },
559
+ { "op": "link", "edge": "chapter_cast", "from": "chapter:CH342", "to": "character:C001", "data": { "role": "pov" } },
560
+ { "op": "link", "edge": "chapter_cast", "from": "chapter:CH342", "to": "character:C024", "data": { "role": "cast" } },
561
+ { "op": "link", "edge": "chapter_location", "from": "chapter:CH342", "to": "location:L011", "data": {} },
562
+ { "op": "link", "edge": "chapter_line_node", "from": "chapter:CH342", "to": "line_node:M001-12", "data": {} },
563
+ { "op": "link", "edge": "foreshadow_action", "from": "chapter:CH342", "to": "foreshadow:FS012", "data": { "action": "progress", "chapter_no": 342, "detail": "另一本账也有同一个空白来源栏" } },
564
+ { "op": "knowledge", "data": { "chapter_id": "CH342", "chapter_no": 342, "subject_type": "foreshadow", "subject_id": "FS012", "character_id": "C001", "change": "confirms", "coverage": "full", "knowledge": "两本账都被人改过", "how": "inferred", "scene": "公示栏", "evidence": "相同的空白栏" } }
565
+ ],
566
+ "decisions": [
567
+ { "subject": "chapter:CH342", "text": "用两本账把冲突升级", "reason": "上一章只是公开,这一章要让公开本身出问题" }
568
+ ]
569
+ }
570
+ ]
571
+ }
572
+ \`\`\`
573
+
574
+ ## 每章最低要求
575
+
576
+ - \`chapter\` 的必填字段齐全,\`display_no\` 连续。
577
+ - 至少 1 条 \`chapter_cast\`,POV 用 \`role: "pov"\`;每条 \`chapter_cast\` 都必须带 \`role\`(\`pov\` / \`cast\` / \`mentioned\`)。
578
+ - 至少 1 条 \`chapter_location\`。
579
+ - 推进主线的章要有 \`chapter_line_node\`;冲突在本章实际发生时才写 \`chapter_conflict\`,纯背景/威胁不要挂冲突边。
580
+ - 冲突在本章实际出场的人物必须写 \`chapter_cast\`;\`conflict.participants\` 是整条冲突的参与者,不等于每章都出场。
581
+ - 伏笔动作只写 \`plant\` / \`progress\` / \`reveal\`;爽点动作只写 \`setup\` / \`payoff\`;信息差只写 \`created\` / \`progressed\` / \`resolved\`。
582
+ - 结局动作只写 \`ending_action\` + \`action: "close"\`,不要用 \`resolve\` 等其他值。
583
+ - 有高潮的章写 \`chapter_climax\`;有明确道具的章写 \`chapter_item\`。
584
+ - 知识事件只在人物对某事的知道程度发生变化时写;同一章、同一人物、同一对象只写一条。
585
+
586
+ ## 硬规则(违反会被 DB 拒绝)
587
+
588
+ - \`PAYOFF_ORDER\`:\`setup\` 必须早于 \`payoff\`。先写 setup 章,再写 payoff 章;不要先兑现后铺垫。
589
+ - \`FORESHADOW_ORDER\`:\`plant\` 必须早于 \`progress\` / \`reveal\`;\`reveal\` 之后不能再 \`plant\`。
590
+ - \`knowledge_rules\`:
591
+ - \`learns\` → \`partial\` 或 \`full\`
592
+ - \`partially_learns\` → \`partial\`
593
+ - \`confirms\` → \`full\`,且此前不能是 \`unknown\`
594
+ - \`corrects\` → \`partial\` 或 \`full\`
595
+ - \`forgets\` → \`unknown\`
596
+ - \`misinforms\` → \`false\`
597
+ - \`how: "told"\` 必须写 \`source_character_id\`
598
+ - \`HOOK_REPEAT\`:连续 3 章不能使用同一个 \`hook.type\`。
599
+ - \`TIME_REGRESSION\`:\`time.story_day\` 不能比上一章小;闪回必须写 \`time.flashback: true\`。
600
+ - \`CHAPTER_VOLUME\` / \`CHAPTER_ACT\`:章节引用的卷/幕必须已经存在。
601
+ - 所有 \`from\` / \`to\` 引用的实体必须已经存在,否则会报悬空引用。
602
+
603
+ ## 出错怎么修
604
+
605
+ - \`apply\` 返回 \`ok: false\`:看 \`error.message\` 和 \`detail.op_index\`,只改那个 op 后重新提交;不要重写整批。
606
+ - \`stage-check\` 返回 \`ok: false\`:只处理 \`errors\`;\`warnings\` 是提示,不阻断。
607
+ - \`PAYOFF_ORDER\`:用 \`fancy_story_read {"action":"context","entity":"payoff:Pxxx"}\` 看 setup/payoff 章号;把 setup 放到更早的章,或把 payoff 放到 setup 之后。重写某一章时直接重新提交该章(\`perChapter: true\` 会替换该章旧动作)。
608
+ - \`FORESHADOW_ORDER\`:先补 plant 章,再写 progress/reveal。
609
+ - \`RHYTHM_CAST\`:给该章补 \`chapter_cast\`。
610
+ - \`HOOK_REPEAT\`:改其中一章的 \`hook.type\`。
611
+ - 缺章:用 \`fancy_story_read {"action":"search","kind":"chapter","limit":50}\` 找缺口,只补缺的章。
612
+
613
+ ## 完成标准
614
+
615
+ - \`chapter\` 数量达到 target,\`display_no\` 覆盖 \`1..target\`。
616
+ - 每章至少 1 个 cast。
617
+ - 没有 \`PAYOFF_ORDER\` / \`FORESHADOW_ORDER\` / \`HOOK_REPEAT\` / \`TIME_REGRESSION\` 等硬错误。
618
+ - \`fancy_story_read {"action":"stage_check","stage":"rhythm"}\` 返回 \`ok: true\`。
619
+ `;
620
+ }
621
+
622
+ // ---------------------------------------------------------------------------
623
+ // outline
624
+ // ---------------------------------------------------------------------------
625
+
626
+ export function buildOutlinePrompt(ctx) {
627
+ return `# outline:给每一章写可执行的细纲
628
+
629
+ rhythm 已经有章节图。现在为每一章补 \`outline\` 和 \`scene\`:这一章必须写什么、不能写什么、分几个场景、每个场景的目标/阻碍/转折/结果、出场人物和地点。
630
+ ${ctx.selection && ctx.selection.all === false ? '\n## 本次范围\n\n本次只处理:' + ctx.selection.label + '。不要写其他章节;stage-check 也只检查这些章节。\n' : '\n## 本次范围\n\n本次处理全部章节。\n'}
631
+ ## 本阶段产出
632
+
633
+ 细纲不是“本章大概写什么”,而是正文写作的施工图。每章必须从 rhythm 图取全信息,不能只写两句概括。
634
+
635
+ - 每章一条 \`outline\`:\`pov\`、\`time\`、\`why\`、\`must_write\`、\`must_not_write\`、\`continuity\`、\`ending\`。
636
+ - \`must_write\` 至少 3-6 条具体事件;\`must_not_write\` 至少 2 条;不要写“推进剧情”“制造冲突”这种空话。
637
+ - 每个 \`outline\` 一般 2-4 个 \`scene\`,至少 1 个;每个场景写清目标、阻碍、转折、结果、地点、出场人物。
638
+ - 建 \`outline_scene\`、\`outline_cast\`、\`outline_location\`;有伏笔的章建 \`outline_foreshadow\`。
639
+ - 本章如果 rhythm 已安排伏笔、爽点、信息差、高潮、道具,必须在 \`must_write\` 或场景里逐条落地,不能遗漏。
640
+ - 正文要能只看这份细纲就写出来;写到写阶段还需要猜的内容,说明细纲不合格。
641
+
642
+ ${setup()}
643
+
644
+ ${rules({ perChapter: true, refs: '- `references/节奏指导/`:细纲的 must_write / must_not_write 和场景写法\n- `references/formulas-rhythm.json`:节奏参数' })}
645
+
646
+ ## 工具速查
647
+
648
+ | 目的 | 工具调用 | 什么时候用 |
649
+ | --- | --- | --- |
650
+ | 看总进度 | \`fancy_story_read {"action":"status"}\` | 开始前、每批后 |
651
+ | 看已有章节 | \`fancy_story_read {"action":"search","kind":"chapter","limit":20}\` | 规划下一批前 |
652
+ | 看单章上下文 | \`fancy_story_read {"action":"context","chapter":341,"prev":3}\` | 写每一章细纲前;包含章节图、cast、knowledge、已有 outline/scene |
653
+ | 看表结构 | \`fancy_story_read {"action":"schema","table":"outline"}\`、\`fancy_story_read {"action":"schema","table":"scene"}\` | 第一次写之前 |
654
+ | 提交 | \`fancy_story_preview\` → \`fancy_story_apply_batch\`(\`perChapter: true\`) | 每 5-10 章提交一次 |
655
+ | 检查 | \`fancy_story_read {"action":"stage_check","stage":"outline"}\` | 每批后/结束,必须通过 |
656
+
657
+ ## 执行步骤
658
+
659
+ 1. \`fancy_story_read {"action":"status"}\`,确认 rhythm 已完成。
660
+ 2. 用 \`fancy_story_read {"action":"search","kind":"chapter","limit":20}\` 分批取章节,再用 \`fancy_story_read {"action":"context","chapter":N,"prev":3}\` 取单章上下文。处理完一批再取下一批,不要把全部章节一次塞进上下文。
661
+ 3. 每章先从 \`context\` 读出并核对:POV、时间、情绪、钩子、cast、地点、主线/支线节点、冲突、伏笔动作、爽点动作、信息差动作、高潮、道具。
662
+ 4. 每章写:
663
+ - \`outline\`:\`chapter_id\`、\`number\`、\`title\`、\`pov\`、\`time\`、\`why\`、\`must_write\`、\`must_not_write\`、\`continuity\`、\`ending\`
664
+ - 2-4 个 \`scene\`:\`outline_id\`、\`scene_order\`、\`goal\`、\`obstacle\`、\`turn\`、\`outcome\`、\`location\`、\`cast\`
665
+ - \`outline_scene\`、\`outline_cast\`、\`outline_location\` 边;伏笔章加 \`outline_foreshadow\` 边
666
+ 5. 写完后回查:本章的每个 \`chapter_cast\` / \`chapter_location\` / \`chapter_conflict\` / \`foreshadow_action\` / \`payoff_action\` / \`info_action\` / \`chapter_climax\` / \`chapter_item\` 是否都在细纲里有对应描述。没有就补 \`must_write\` 或场景。
667
+ 6. 每 5-10 章提交一次 \`apply perChapter: true\`;每章是独立事务。
668
+ 7. 全部写完后运行 \`fancy_story_read {"action":"stage_check","stage":"outline"}\`。
669
+
670
+ ## 提交前自检
671
+
672
+ 每章逐项打勾,缺一项就不要提交:
673
+
674
+ - 出场人物:本章 \`chapter_cast\` 的每个人都出现了吗?
675
+ - 地点:本章 \`chapter_location\` 的地点都写到了吗?
676
+ - 伏笔:每个 \`foreshadow_action\` 的 plant/progress/reveal 都有对应情节吗?
677
+ - 爽点:每个 \`payoff_action\` 的 setup/payoff 都有对应情节吗?
678
+ - 信息差:每个 \`info_action\` 的建立/推进/解决都有对应情节吗?
679
+ - 高潮/道具:有 \`chapter_climax\`/\`chapter_item\` 时写进正文了吗?
680
+ - 情绪/钩子:本章 \`emotion\` 与 \`hook\` 和正文实际感受一致吗?
681
+ - 长度:\`word_count\` 达到 \`word_target\` 的 80% 以上吗?
682
+
683
+ ## 上游进度
684
+
685
+ \`\`\`json
686
+ ${json(ctx.summary)}
687
+ \`\`\`
688
+
689
+ ## 边
690
+
691
+ ${edgeList(['outline_scene', 'outline_cast', 'outline_location', 'outline_foreshadow'])}
692
+
693
+ ## 可复制的单章提交示例
694
+
695
+ 参数(用 \`fancy_story_apply_batch\` 提交;它会先预览再写入):
696
+
697
+ \`\`\`json
698
+ {
699
+ "perChapter": true,
700
+ "workflow": "outline",
701
+ "summary": "第 341 章细纲",
702
+ "chapters": [
703
+ {
704
+ "chapter": "CH341",
705
+ "ops": [
706
+ { "op": "create", "kind": "outline", "data": {
707
+ "chapter_id": "CH341",
708
+ "number": 341,
709
+ "title": "第一张复核表",
710
+ "pov": "C001",
711
+ "time": { "story_day": 318, "clock": "20:00-21:00", "duration_minutes": 60 },
712
+ "why": { "main_line_nodes": ["M001-12"], "conflict": ["X033"], "theme": "秩序要能被核对", "stakes": "配额表一旦公开就不能再改" },
713
+ "must_write": ["复核分配表的过程", "公开后第一个反对者的反应", "把 FS012 的来源栏空白拍在桌上", "P086 的 setup:有人要求先私下看"],
714
+ "must_not_write": ["不要用旁白总结制度", "不要让主角一次说服所有人"],
715
+ "continuity": { "from_prev": "承接上一章的反对者", "to_next": "账本被带走" },
716
+ "ending": { "type": "悬念", "text": "反对者把账本拍在桌上", "hook": "账本是从哪来的" },
717
+ "status": "draft"
718
+ } },
719
+ { "op": "create", "kind": "scene", "data": {
720
+ "outline_id": "CH341",
721
+ "scene_order": 1,
722
+ "goal": "把复核结果公开",
723
+ "obstacle": "有人要求先私下看",
724
+ "turn": "主角坚持当场念完",
725
+ "outcome": "公开成立,但埋下反对者",
726
+ "location": "L011",
727
+ "cast": ["C001", "C027"],
728
+ "status": "draft"
729
+ } },
730
+ { "op": "create", "kind": "scene", "data": {
731
+ "outline_id": "CH341",
732
+ "scene_order": 2,
733
+ "goal": "让反对者把另一本账拿出来",
734
+ "obstacle": "主角不能当众承认自己没核对过来源",
735
+ "turn": "账本上的签名和公示表一致",
736
+ "outcome": "两本账互相证明,冲突升级",
737
+ "location": "L011",
738
+ "cast": ["C001", "C027"],
739
+ "status": "draft"
740
+ } },
741
+ { "op": "link", "edge": "outline_scene", "from": "outline:CH341", "to": "scene:SC_CH341_1", "data": { "scene_order": 1 } },
742
+ { "op": "link", "edge": "outline_scene", "from": "outline:CH341", "to": "scene:SC_CH341_2", "data": { "scene_order": 2 } },
743
+ { "op": "link", "edge": "outline_cast", "from": "outline:CH341", "to": "character:C001", "data": { "role": "pov" } },
744
+ { "op": "link", "edge": "outline_location", "from": "outline:CH341", "to": "location:L011", "data": {} },
745
+ { "op": "link", "edge": "outline_foreshadow", "from": "outline:CH341", "to": "foreshadow:FS012", "data": { "intent": "plant" } }
746
+ ],
747
+ "decisions": [
748
+ { "subject": "outline:CH341", "text": "用公开念表作为本章核心动作", "reason": "承接 rhythm 的公开复核节点" }
749
+ ]
750
+ }
751
+ ]
752
+ }
753
+ \`\`\`
754
+
755
+ ## 完成标准
756
+
757
+ - 每一章都有 \`outline\`,并且 \`pov\` / \`time\` / \`why\` 来自或对齐 rhythm 章节图。
758
+ - 每个 \`outline\` 一般 2-4 个 \`scene\`,至少 1 个,且通过 \`outline_scene\` 连上。
759
+ - 每章都有 \`outline_cast\` 和 \`outline_location\`;有伏笔的章有 \`outline_foreshadow\`。
760
+ - \`must_write\` 至少 3 条具体事件,\`must_not_write\` 非空。
761
+ - rhythm 图里本章的伏笔、爽点、信息差、高潮、道具都能在细纲里找到落点。
762
+ - \`fancy_story_read {"action":"stage_check","stage":"outline"}\` 返回 \`ok: true\`。
763
+ `;
764
+ }
765
+
766
+ // ---------------------------------------------------------------------------
767
+ // writing
768
+ // ---------------------------------------------------------------------------
769
+
770
+ export function buildWritingPrompt(ctx) {
771
+ return `# write:按细纲写正文,并留下章末交接
772
+
773
+ 细纲已经写好。现在逐章写正文,写入 \`chapter_text\`,同时写 \`handoff\`,让下一章能接住地点、时间、人物和未结线索。
774
+ ${ctx.selection && ctx.selection.all === false ? '\n## 本次范围\n\n本次只处理:' + ctx.selection.label + '。不要写其他章节;stage-check 也只检查这些章节。\n' : '\n## 本次范围\n\n本次处理全部章节。\n'}
775
+ ## 本阶段产出
776
+
777
+ - 每章一条 \`chapter_text\`:正文 \`content\` + \`word_count\`。
778
+ - 每章一条 \`handoff\`:章末地点、时间、人物、未结线索、下一场提示、连贯性备注。
779
+ - 正文覆盖 \`outline.must_write\`,不违反 \`must_not_write\`,按 \`outline.ending\` 收束。
780
+
781
+ ${setup()}
782
+
783
+ ${rules({ perChapter: true, refs: '- `references/写作指导/`:正文写法、去 AI 味、章节纪律\n- `references/genre-profiles.json`:题材语气和字数基准' })}
784
+
785
+ ## 工具速查
786
+
787
+ | 目的 | 工具调用 | 什么时候用 |
788
+ | --- | --- | --- |
789
+ | 看总进度 | \`fancy_story_read {"action":"status"}\` | 开始前、每批后 |
790
+ | 看已有章节 | \`fancy_story_read {"action":"search","kind":"chapter","limit":20}\` | 规划下一批前 |
791
+ | 看单章上下文 | \`fancy_story_read {"action":"context","chapter":341,"prev":3}\` | 每章写正文前;包含细纲、场景、人物、地点、知识、前文结尾 |
792
+ | 查前文关键词 | \`fancy_story_read {"action":"context","chapter":341,"prev":3,"search":"账本"}\` | 不确定前文是否提过某物时 |
793
+ | 看表结构 | \`fancy_story_read {"action":"schema","table":"chapter_text"}\`、\`fancy_story_read {"action":"schema","table":"handoff"}\` | 第一次写之前 |
794
+ | 提交 | \`fancy_story_preview\` → \`fancy_story_apply_batch\`(\`perChapter: true\`) | 每 1-5 章提交一次 |
795
+ | 检查 | \`fancy_story_read {"action":"stage_check","stage":"writing"}\` | 每批后/结束,必须通过 |
796
+
797
+ ## 执行步骤
798
+
799
+ 1. \`fancy_story_read {"action":"status"}\`,确认 outline 已完成。
800
+ 2. 用 \`fancy_story_read {"action":"search","kind":"chapter","limit":20}\` 分批取章节;逐章运行 \`fancy_story_read {"action":"context","chapter":N,"prev":3}\`。
801
+ 3. 写前把 \`context\` 返回的以下内容逐项对一遍,写进正文:
802
+ - \`cast\`:每个出场人物都要出现,不能只写 POV。
803
+ - \`locations\`:本章地点要在场景里落地。
804
+ - \`actions\`:每个伏笔/爽点/信息差动作都要有具体情节,不能只在旁白里提一句。
805
+ - \`climaxes\` / \`items\`:有明确关联时写进正文;没有时不要凭空加。
806
+ - \`chapter.emotion\` / \`chapter.hook\` / \`chapter.how\`:情绪、钩子、转折、章末收束要对齐。
807
+ 4. 写正文:
808
+ - 覆盖 \`outline.must_write\` 的每个事件。
809
+ - 不违反 \`outline.must_not_write\`。
810
+ - 结尾按 \`outline.ending\` 收束。
811
+ - 正文长度必须达到 \`chapter.word_target\` 的 80% 以上;不要用 1000 字草稿冒充 2400 字章节。
812
+ - 如果上一章 handoff 的地点/时间与本章开场不一致,写过渡;不要无解释跳场景。
813
+ 5. 同一次提交里写 \`chapter_text\` 和 \`handoff\`。正文用 \`content\`,字数用 \`word_count\`(中文按非空白字符数估算)。
814
+ 6. 每 1-5 章提交一次;失败只回滚当前章。
815
+ 7. 全部写完后运行 \`fancy_story_read {"action":"stage_check","stage":"writing"}\`。
816
+
817
+ ## 提交前自检
818
+
819
+ 每章逐项打勾,缺一项就不要提交:
820
+
821
+ - 出场人物:本章 \`chapter_cast\` 的每个人都出现了吗?
822
+ - 地点:本章 \`chapter_location\` 的地点都写到了吗?
823
+ - 伏笔:每个 \`foreshadow_action\` 的 plant/progress/reveal 都有对应情节吗?
824
+ - 爽点:每个 \`payoff_action\` 的 setup/payoff 都有对应情节吗?
825
+ - 信息差:每个 \`info_action\` 的建立/推进/解决都有对应情节吗?
826
+ - 高潮/道具:有 \`chapter_climax\`/\`chapter_item\` 时写进正文了吗?
827
+ - 情绪/钩子:本章 \`emotion\` 与 \`hook\` 和正文实际感受一致吗?
828
+ - 长度:\`word_count\` 达到 \`word_target\` 的 80% 以上吗?
829
+
830
+ ## 上游进度
831
+
832
+ \`\`\`json
833
+ ${json(ctx.summary)}
834
+ \`\`\`
835
+
836
+ ## 可复制的单章提交示例
837
+
838
+ 参数(用 \`fancy_story_apply_batch\` 提交;它会先预览再写入):
839
+
840
+ \`\`\`json
841
+ {
842
+ "perChapter": true,
843
+ "workflow": "writing",
844
+ "summary": "第 341 章正文",
845
+ "chapters": [
846
+ {
847
+ "chapter": "CH341",
848
+ "ops": [
849
+ { "op": "create", "kind": "chapter_text", "data": {
850
+ "chapter_id": "CH341",
851
+ "display_no": 341,
852
+ "content": "# 第341章 第一张复核表\\n\\n……",
853
+ "word_count": 2400,
854
+ "status": "draft"
855
+ } },
856
+ { "op": "create", "kind": "handoff", "data": {
857
+ "chapter_id": "CH341",
858
+ "end_location": "L011",
859
+ "end_time": { "story_day": 318, "clock": "21:00" },
860
+ "end_characters": ["C001", "C027"],
861
+ "open_threads": ["账本的来源"],
862
+ "next_scene_hint": "反对者带着账本去找 C024",
863
+ "continuity_notes": "从公示栏转到档案楼,需要一个走路的过渡",
864
+ "status": "draft"
865
+ } }
866
+ ],
867
+ "decisions": [
868
+ { "subject": "chapter_text:CH341", "text": "把公开念表写成正面冲突", "reason": "细纲要求公开后立刻出现反对者" }
869
+ ]
870
+ }
871
+ ]
872
+ }
873
+ \`\`\`
874
+
875
+ ## 完成标准
876
+
877
+ - 每一章都有 \`chapter_text\`,\`word_count > 0\`。
878
+ - 每章有 \`handoff\`。
879
+ - 正文覆盖细纲的 \`must_write\`,不违反 \`must_not_write\`。
880
+ - \`fancy_story_read {"action":"stage_check","stage":"writing"}\` 返回 \`ok: true\`。
881
+ `;
882
+ }
883
+
884
+ // ---------------------------------------------------------------------------
885
+ // repair
886
+ // ---------------------------------------------------------------------------
887
+
888
+ export function buildRepairPrompt(stage, check, ctx = {}) {
889
+ const label = stage === 'writing' ? 'write' : stage;
890
+ const errors = (check.errors || []).slice(0, 20);
891
+ const pending = (check.pending || []).slice(0, 20);
892
+ const warnings = (check.warnings || []).slice(0, 10);
893
+ const prompt = `# 继续完成 ${label} 阶段
894
+
895
+ 上一次 \`stage-check ${stage}\` 没有通过。不要重写已经完成的内容,只修下面这些问题。
896
+
897
+ ## 命令入口
898
+
899
+ \`\`\`bash
900
+ fancy_story_read {"action":"stage_check","stage":"${stage}"}
901
+ \`\`\`
902
+
903
+ ## 必须修的错误
904
+
905
+ \`\`\`json
906
+ ${JSON.stringify(errors, null, 2)}
907
+ \`\`\`
908
+
909
+ ## 待办
910
+
911
+ \`\`\`json
912
+ ${JSON.stringify(pending, null, 2)}
913
+ \`\`\`
914
+
915
+ ## 可参考的警告(不阻断)
916
+
917
+ \`\`\`json
918
+ ${JSON.stringify(warnings, null, 2)}
919
+ \`\`\`
920
+
921
+ ## 做法
922
+
923
+ 1. 先看错误里的 \`scope\` / \`message\` / \`op_index\`,判断是哪条数据或哪个操作。
924
+ 2. 用 \`fancy_story_read\`、\`fancy_story_read {"action":"context","entity":"..."}\`、\`fancy_story_read {"action":"context","chapter":...}\` 或 \`fancy_story_read {"action":"query","sql":"SELECT ..."}\` 定位。
925
+ 3. 用 \`fancy_story_apply_batch\` 修;如果是章节问题,用 \`fancy_story_apply_batch(perChapter: true)\` 重新提交该章(会替换该章的 cast/action 边)。
926
+ 4. 失败时按 DB 返回的错误原文改,不要猜。
927
+ 5. 修完重新运行 \`fancy_story_read {"action":"stage_check","stage":"${stage}"}\`,直到 \`ok: true\`。
928
+ 6. 不要停下来等确认;不要写阶段 JSON;不要写生成脚本。`;
929
+ const withRefs = prompt
930
+ .replaceAll('<cli>', ctx.cliPath || '<cli>')
931
+ .replaceAll('<root>', ctx.projectRoot || '<root>');
932
+ if (ctx.wikiReferences) {
933
+ return withRefs + '\n\n## 共享对标参考\n\n以下文件在工作区 `' + ctx.workspaceRoot + '` 下(多个项目共享;路径相对工作区):\n\n' + ctx.wikiReferences + '\n';
934
+ }
935
+ return withRefs;
936
+ }
937
+
938
+ export function buildStagePrompt(stage, ctx) {
939
+ let prompt;
940
+ switch (stage) {
941
+ case 'scale': prompt = buildScalePrompt(ctx); break;
942
+ case 'macro': prompt = buildMacroPrompt(ctx); break;
943
+ case 'cast': prompt = buildCastPrompt(ctx); break;
944
+ case 'rhythm': prompt = buildRhythmPrompt(ctx); break;
945
+ case 'outline': prompt = buildOutlinePrompt(ctx); break;
946
+ case 'writing': prompt = buildWritingPrompt(ctx); break;
947
+ default: throw new Error('未知阶段:' + stage);
948
+ }
949
+ return prompt
950
+ .replaceAll('<cli>', ctx.cliPath || '<cli>')
951
+ .replaceAll('<root>', ctx.projectRoot || '<root>');
952
+ }