@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
@@ -8,7 +8,9 @@ import { fileURLToPath } from 'url';
8
8
  import { renderInputSnapshot } from './scripts/graph-utils.js';
9
9
  import { pickContractSections } from './scripts/stage-contract.js';
10
10
  import { readingPowerLegend } from './scripts/reading-power.js';
11
+ import { referenceLogPromptBlock } from './scripts/reference-log.js';
11
12
  import { deslopLegend } from './scripts/ai-patterns.js';
13
+ import { normalizeLegacyTopicData, TOPIC_REQUIRED_FIELDS } from '#topic-fields';
12
14
 
13
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
14
16
  const REFERENCES_DIR = join(__dirname, 'references');
@@ -89,17 +91,53 @@ function joinField(v, sep = ';', fallback = '无') {
89
91
  * 那份快照是骨架校验器验过的,更权威。这里只在"骨架产物还没落盘"时兜底。
90
92
  */
91
93
  function snapshotFromTopicData(topicData) {
92
- const t = topicData || {};
93
- return {
94
- book_name: t.book_name,
95
- genre: t.genre,
96
- core_summary: t.core_summary,
97
- core_conflict: t.core_conflict,
98
- hard_constraints: Array.isArray(t.hard_constraints) ? t.hard_constraints : [],
99
- anti_tropes: Array.isArray(t.anti_tropes) ? t.anti_tropes : [],
100
- target_chapters: t.target_chapters,
101
- target_words: t.target_words
102
- };
94
+ const t = normalizeLegacyTopicData(topicData || {});
95
+ const out = {};
96
+ for (const id of TOPIC_REQUIRED_FIELDS) {
97
+ if (id === 'hard_constraints' || id === 'anti_tropes') {
98
+ out[id] = Array.isArray(t[id]) ? t[id] : [];
99
+ } else {
100
+ out[id] = t[id];
101
+ }
102
+ }
103
+ return out;
104
+ }
105
+
106
+ /**
107
+ * 把提示词里**行内反引号**包着的项目内路径改成绝对路径。
108
+ *
109
+ * ## 为什么必须改
110
+ *
111
+ * 插件把参考文件(词表 / 公式 / 阶段指导 / 字段模板)拷到**项目根**的
112
+ * `references/` 与 `模板/` 下,但提示词里写的是相对路径
113
+ * (`references/formulas-skeleton.json`)。
114
+ *
115
+ * 相对路径的前提是"当前目录 = 项目根"。这个前提一旦不成立,
116
+ * 路径就会被拼到别的目录下 —— 典型的是拼成
117
+ * `<项目根>/设定/references/formulas-skeleton.json`:
118
+ * `设定/` 是 llm-wiki 的项目目录,插件**从没**往那里放过参考文件,
119
+ * 于是读不到,模型要么空手写、要么以为参考文件缺失。
120
+ *
121
+ * 插件知道项目根,所以在运行时把路径换成绝对的:基准被钉死,
122
+ * 模型不用猜,工具用哪种读法都对。
123
+ *
124
+ * ## 只改行内反引号里的
125
+ *
126
+ * - **代码块里的命令保持相对**:`node references/upstream-query.js …`
127
+ * 是在项目根下执行的命令,绝对化会与"在项目根运行"的语义打架;
128
+ * - **产物路径保持相对**(`设定/raw/sources/…`):那是模型要写的地方,
129
+ * 相对项目根,和校验器读的路径一致;
130
+ * - **`对标/wiki/` 保持相对**:那是 llm-wiki 的项目,相对路径才是它要的形态。
131
+ *
132
+ * @param {string} text 提示词正文
133
+ * @param {string} projectRoot 项目根绝对路径(没有就原样返回)
134
+ * @returns {string} 绝对化后的正文
135
+ */
136
+ export function absolutizeInlinePaths(text, projectRoot) {
137
+ if (!text || !projectRoot) return text;
138
+ const root = String(projectRoot).replace(/\/+$/, '');
139
+ if (!root) return text;
140
+ return text.replace(/`((?:references|模板)\/[^`\n]*)`/g, (m, p) => '`' + root + '/' + p + '`');
103
141
  }
104
142
 
105
143
  /** 标量字段的安全取值:undefined/null 会导致 prompt 里出现字面量 "undefined" */
@@ -123,43 +161,67 @@ export const storyBuilder = {
123
161
  *
124
162
  * 数值给默认值,是为了让"调用方忘了传某个字段"退化成 0 而不是崩。
125
163
  */
126
- notify({ type, err, phase, id, fileCount, missingCount, queueCount, detail, source, extra,
164
+ notify({ type, err, phase, id, fileCount, missingCount, queueCount, detail, source, extra, stage, missing, next,
127
165
  count = 0, failed = [], written = 0, clean = 0, passed = 0, rolledBack = 0, errored = 0,
128
166
  reused = 0 }) {
129
167
  switch (type) {
168
+ case 'help':
169
+ return `📖 故事构建 用法
170
+
171
+ /fancy-build scale - 故事规模:总章数、总字数、卷、幕、配额
172
+ /fancy-build macro - 世界观、故事、主线/支线、结局、角色功能位
173
+ /fancy-build cast - 人物、势力、地点、道具、冲突、伏笔、爽点、高潮、信息差
174
+ /fancy-build rhythm - 章节地图:逐章写人物、地点、冲突、钩子、伏笔/爽点动作
175
+ /fancy-build outline - 细纲和场景;示例:outline CH001 / outline 1-10 / outline all
176
+ /fancy-build write - 正文和章末交接;示例:write CH001 / write 1-10 / write all
177
+ /fancy-build deslop - 去 AI 味;示例:deslop CH001 / deslop 1-10 / deslop all
178
+ /fancy-build help - 显示本帮助
179
+ `;
180
+
181
+ case 'v2_done':
182
+ return err || extra || '✅ 阶段完成。';
183
+
184
+ case 'v2_failed': {
185
+ const text = err || extra || '阶段未完成';
186
+ return /^[❌⚠️]/.test(text) ? text : '❌ ' + text;
187
+ }
188
+
189
+ case 'v2_info':
190
+ return err || extra || '';
191
+
130
192
  case 'cancelled':
131
- return '🚫 已取消';
193
+ return '🚫 操作已取消。';
132
194
 
133
195
  case 'no_provider':
134
- return '⚠️ 当前环境不支持交互式提问。请在 dsh-tui 中运行此命令,或确保 web UI 已正确加载交互组件。';
196
+ return '⚠️ 当前运行环境不支持交互式提问。请在 dsh-tui 中运行该命令,或确认 Web UI 已正确加载交互组件。';
135
197
 
136
198
  case 'no_project_root':
137
- return '❌ 无法确定项目目录:' + (err || '会话未提供 cwd') + '。请在项目目录下运行该命令。';
199
+ return '❌ 无法确定当前项目:' + (err || '会话未提供当前工作目录') + '。请在项目目录下运行,或用 /fancy-project use <名字|UUID> 指定当前项目。';
138
200
 
139
201
  case 'ask_failed':
140
- return '❌ 提问失败:' + (err || '未知错误');
202
+ return '❌ 交互提问失败:' + (err || '未知错误');
141
203
 
142
204
  case 'not_initialized':
143
- return '⚠️ 当前目录尚未初始化,请先运行 /fancy-bootstrap 进行初始化。';
205
+ return '⚠️ 当前没有可用项目。请先运行 /fancy-bootstrap 初始化工作区,再运行 /fancy-topic setup 创建项目。';
144
206
 
145
207
  case 'config_corrupt':
146
- return '❌ ' + (err || '.fancy-deployed 解析失败') + '';
208
+ return '❌ ' + (err || '无法解析 .fancy-deployed 状态文件。') + ' 请检查文件内容,或重新运行 /fancy-bootstrap 初始化工作区。';
147
209
 
148
210
  case 'no_project_root':
149
211
  return '❌ ' + (err || '无法确定项目目录');
150
212
 
151
213
  case 'no_topic':
152
- return '⚠️ 未找到选题信息。请先运行 /fancy-topic 进行选题。';
214
+ return '⚠️ 未找到选题信息。请先运行 /fancy-topic 完成选题。';
153
215
 
154
216
  case 'setup_id_unavailable': {
155
217
  // 分类原因 → 人话 + **针对该原因**的排查步骤。
156
- // 此前这里只有一句"未设置 setup_id,请先运行 /fancy-story-builder skeleton",
218
+ // 此前这里只有一句"未设置 setup_id,请先运行 /fancy-build skeleton",
157
219
  // 而用户正是运行着这条命令 —— 死胡同,且把真实原因(token 无效 / 服务没起)藏了。
158
220
  // 注意:步骤必须按原因分流。给"连不上服务"的人列一堆 token 步骤,
159
221
  // 比不给步骤更糟 —— 会把人引到错误的方向上去。
160
222
  const reasonText = {
161
223
  unauthorized: 'llm-wiki 拒绝了请求(token 无效或服务端未配置 token)',
162
- network: '连不上 llm-wiki 服务',
224
+ network: '无法连接 llm-wiki 服务',
163
225
  api_error: 'llm-wiki 返回了错误',
164
226
  unexpected_response: 'llm-wiki 返回了非预期结构'
165
227
  };
@@ -167,15 +229,15 @@ export const storyBuilder = {
167
229
  unauthorized: [
168
230
  '1. 在 llm-wiki 的「设置 → API + MCP」里填「访问令牌」(可直接点「生成新 token」)',
169
231
  '2. 保存设置后,把同一个 token 写进本项目 .fancy-deployed 的 llm_wiki_token 字段',
170
- '3. 确认 llm-wiki 在跑:curl -s -o /dev/null -w "%{http_code}" http://localhost:19828/api/v1/health'
232
+ '3. 确认 llm-wiki 正在运行:curl -s -o /dev/null -w "%{http_code}" http://localhost:19828/api/v1/health'
171
233
  ],
172
234
  network: [
173
- '1. 确认 llm-wiki 在跑:curl -s -o /dev/null -w "%{http_code}" http://localhost:19828/api/v1/health',
235
+ '1. 确认 llm-wiki 正在运行:curl -s -o /dev/null -w "%{http_code}" http://localhost:19828/api/v1/health',
174
236
  '2. 确认 .fancy-deployed 里的 llm_wiki_url 指向正确地址(默认 http://localhost:19828/api/v1)',
175
237
  '3. 启动 llm-wiki 后重试本命令'
176
238
  ],
177
239
  api_error: [
178
- '1. 看 llm-wiki 的日志/控制台输出,确认服务端报的具体错误',
240
+ '1. 查看 llm-wiki 日志或控制台输出,确认服务端返回的具体错误',
179
241
  '2. 确认 .fancy-deployed 里的 llm_wiki_url 与 llm_wiki_token 正确'
180
242
  ],
181
243
  unexpected_response: [
@@ -190,10 +252,10 @@ export const storyBuilder = {
190
252
  const head = '❌ 未能建立 llm-wiki 设定项目:' + (reasonText[err] || err || '未知原因');
191
253
  const detailLine = detail ? '\n详情:' + detail : '';
192
254
  const consequence =
193
- '\n\n后果:llm-wiki 侧还不认识本项目的「设定」目录,' +
194
- '本阶段产物不会被 ingest 纳管,后续 /fancy-story-builder wiki 与 analyze 阶段也会失败。';
255
+ '\n\n后果:llm-wiki 尚未识别本项目的「设定」目录,' +
256
+ '本阶段产物不会被 ingest 纳管,后续 /fancy-build maintain sync 与 maintain lint 也会失败。';
195
257
  const tail = '\n' + (steps.length + 1) +
196
- '. 处理好后重新执行本命令,插件会自动把「设定」目录建成为 llm-wiki 项目';
258
+ '. 处理完成后重新执行本命令,插件会自动将「设定」目录创建为 llm-wiki 项目。';
197
259
  return head + detailLine + consequence + '\n处理办法:\n' + steps.join('\n') + tail;
198
260
  }
199
261
 
@@ -207,10 +269,12 @@ export const storyBuilder = {
207
269
  }
208
270
 
209
271
  case 'deploy_failed':
210
- return '❌ 阶段文件部署失败:' + (err || '未知错误') + '。请检查磁盘空间与目录权限后重试。';
272
+ return '❌ 阶段文件部署失败:' + (err || '未知错误') + '。请检查磁盘空间和目录权限后重试。';
211
273
 
212
274
  case 'schema_copy_failed':
213
- return '⚠️ 设定目录准备不完整:' + (detail || '未知原因') + '。本阶段仍会继续,但 schema.md / purpose.md 可能缺失。';
275
+ return '❌ 设定目录准备失败:' + (detail || '未知原因') +
276
+ '。\n\n请确认 llm-wiki 正在运行,且 .fancy-deployed 里的 llm_wiki_url / llm_wiki_token 正确。' +
277
+ '插件会先通过 llm-wiki 重建“设定”项目;如果服务不可用,插件不会以空目录继续执行。';
214
278
 
215
279
  case 'llm_wiki_not_configured':
216
280
  return '⚠️ llm_wiki 未配置。请在 .fancy-deployed 中配置 llm_wiki_url 和 llm_wiki_token';
@@ -224,14 +288,14 @@ export const storyBuilder = {
224
288
  // "does not identify the settlement of any particular message")。
225
289
  // 说"阶段开始"会让用户以为模型已经在写;实际 DeepSeek 侧 401 / 限流 / 超时,
226
290
  // 插件这一侧完全无感,错误只会出现在会话流里。
227
- return `✅ ${phase}提示词已投递给会话模型,产物由模型生成。` + (extra ? '\n\n' + extra : '');
291
+ return `✅ ${phase}提示词已发送至会话模型,产物将由模型生成。` + (extra ? '\n\n' + extra : '');
228
292
 
229
293
  case 'flesh_incomplete':
230
294
  // 血肉阶段没跑完时不能用"阶段开始"的说法(更不能用 ✅):
231
295
  // 2026-10 tt5 实测过静默失败——少产出一个文件、全量校验从没跑过,
232
296
  // 而插件早就返回了"阶段开始"。现在血肉阶段改成插件逐批派活,
233
297
  // 没过的批次由插件用脚本判出来并回喂重试,仍没过就把清单摆出来。
234
- return '⚠️ 血肉阶段未完成:部分文件没通过校验,或全量校验(--all)没通过。'
298
+ return '❌ 血肉阶段未完成:部分文件未通过校验,或全量校验(--all)未通过。'
235
299
  + (extra ? '\n\n' + extra : '');
236
300
 
237
301
  case 'flesh_complete':
@@ -239,17 +303,22 @@ export const storyBuilder = {
239
303
  return '✅ 血肉阶段完成:8 个 JSON 齐全,全量校验(--all)已通过。'
240
304
  + (extra ? '\n\n' + extra : '');
241
305
 
306
+ case 'stage_dependency_missing':
307
+ return '❌ ' + (stage || '当前') + '阶段未启动:前置的' + (missing || '上游') + '阶段尚未生成。'
308
+ + (detail ? '(' + detail + ')' : '')
309
+ + '\n\n请先执行 `' + (next || '/fancy-build help') + '`,等该阶段完成并通过校验后,再执行当前命令。';
310
+
242
311
  case 'upstream_not_ready':
243
312
  // 节奏阶段的上游门禁没过:继续跑只会产出废数据,必须在这里挡住
244
- return '⚠️ 节奏阶段未启动:' + (err || '上游阶段未通过全量校验')
245
- + '\n\n节奏阶段依赖骨架 + 血肉两个上游,且两者的全量校验(--all)都必须已通过。'
246
- + '请先跑完上游阶段并确认各自的 .stage-status.json 里 all_check.status = PASSED,再跑节奏阶段。'
313
+ return '❌ 节奏阶段未启动:' + (err || '上游阶段未通过全量校验')
314
+ + '\n\n节奏阶段依赖骨架和血肉两个上游,且两者的全量校验(--all)都必须已通过。'
315
+ + '请先完成上游阶段,并确认各自的 .stage-status.json 中 all_check.status = PASSED,再执行节奏阶段。'
247
316
  + (extra ? '\n\n' + extra : '');
248
317
 
249
318
  case 'rhythm_incomplete':
250
319
  // 节奏阶段没跑完时不能用"阶段开始"的说法(更不能用 ✅):
251
320
  // 节奏的绝大多数校验是跨文件口径的,单文件过了不代表阶段过了。
252
- return '⚠️ 节奏阶段未完成:部分批次没通过校验,或全量校验(--all)没通过。'
321
+ return '❌ 节奏阶段未完成:部分批次未通过校验,或全量校验(--all)未通过。'
253
322
  + (extra ? '\n\n' + extra : '');
254
323
 
255
324
  case 'rhythm_complete':
@@ -259,7 +328,7 @@ export const storyBuilder = {
259
328
  case 'skeleton_incomplete':
260
329
  // 骨架阶段没跑完时不能用"阶段开始"的说法(更不能用 ✅):
261
330
  // 引用完整性 / 图连通性 / 配额一致性都要全量校验才判得了。
262
- return '⚠️ 骨架阶段未完成:部分批次没通过校验,或全量校验(--all)没通过。'
331
+ return '❌ 骨架阶段未完成:部分批次未通过校验,或全量校验(--all)未通过。'
263
332
  + (extra ? '\n\n' + extra : '');
264
333
 
265
334
  case 'skeleton_complete':
@@ -272,46 +341,48 @@ export const storyBuilder = {
272
341
  // 用户会以为没有需要写的章,实际可能是全书都没写。
273
342
  return '✅ 写作阶段完成:新写 ' + count + ' 章' +
274
343
  (reused > 0 ? ',复用 ' + reused + ' 章(已复检通过)' : '') +
275
- '。' + (extra ? '\n\n' + extra : '');
344
+ ';全部章节逐章校验通过。' + (extra ? '\n\n' + extra : '');
276
345
 
277
346
  case 'writing_incomplete':
278
- return '⚠️ 写作阶段结束,但有 ' + failed.length + ' 章未通过校验(已写 ' + written + ' 章' +
347
+ return '❌ 写作阶段结束,但有 ' + failed.length + ' 章未通过校验(已写 ' + written + ' 章' +
279
348
  (reused > 0 ? ',复用 ' + reused + ' 章(已复检通过)' : '') +
280
349
  ')。未通过章号:' + failed.join('、') +
281
- '。请修好对应章节后重新执行本命令,会自动跳过已写好的章。' + (extra ? '\n\n' + extra : '');
350
+ '。请修复对应章节后重新执行本命令;已完成的章节将自动跳过。' + (extra ? '\n\n' + extra : '');
282
351
 
283
352
  case 'deslop_no_chapters':
284
- return '⚠️ 去味阶段没有可处理的章节:写作目录里找不到 `第NNN章.md` 文件。'
285
- + '(目录:' + (extra || '未知') + ')请先运行 /fancy-story-builder writing 写出正文。';
353
+ return '❌ 去味阶段没有可处理的章节:写作目录中未找到 `第NNN章.md` 文件。'
354
+ + '(目录:' + (extra || '未知') + ')请先运行 /fancy-build write all 生成正文。';
286
355
 
287
356
  case 'deslop_done':
288
- return `✅ 去味阶段完成:${count} 章已处理(${clean} 章本来就干净,${passed} 章改写后复扫通过)。`
357
+ return `✅ 去味阶段完成:${count} 章已处理(${clean} 章原本已符合要求,${passed} 章改写后复扫通过)。`
289
358
  + (extra ? '\n\n' + extra : '');
290
359
 
291
360
  case 'deslop_incomplete':
292
- return `⚠️ 去味阶段结束,但有 ${failed.length} 章没改干净(其中 ${rolledBack} 章改动超 35% 已被回滚,${errored} 章执行出错)。`
361
+ return `❌ 去味阶段结束,但有 ${failed.length} 章未通过去味校验(其中 ${rolledBack} 章改动超 35% 已被回滚,${errored} 章执行出错)。`
293
362
  + '问题章号:' + (failed.length ? failed.join('、') : '无') + '。'
294
363
  + '原文已备份在 `设定/raw/sources/写作/.revisions/`,报告见 `审查报告/`。'
295
364
  + (extra ? '\n\n' + extra : '');
296
365
 
297
366
  case 'no_subcommand':
298
- return `⚠️ 请指定子命令:
299
- /fancy-story-builder skeleton - 骨架阶段
300
- /fancy-story-builder flesh - 血肉阶段
301
- /fancy-story-builder rhythm - 节奏阶段
302
- /fancy-story-builder writing - 写作阶段(按节奏的章节细纲逐章写)
303
- /fancy-story-builder deslop - 去 AI 味阶段(逐章检测、改写、复扫)
304
- /fancy-story-builder wiki - 对设定文件执行 ingest
305
- /fancy-story-builder analyze - 分析阶段`;
367
+ return `⚠️ 请指定阶段:
368
+
369
+ /fancy-build scale
370
+ /fancy-build macro
371
+ /fancy-build cast
372
+ /fancy-build rhythm
373
+ /fancy-build outline [CH001 | 1-10 | all]
374
+ /fancy-build write [CH001 | 1-10 | all]
375
+ /fancy-build deslop [CH001 | 1-10 | all]
376
+ /fancy-build help`;
306
377
 
307
378
  case 'no_sources_dir':
308
- return '⚠️ 未找到 raw/sources 目录。请先执行骨架/血肉/节奏阶段生成设定文件。';
379
+ return '⚠️ 未找到 raw/sources 目录。请先执行骨架、血肉或节奏阶段,生成设定文件。';
309
380
 
310
381
  case 'no_files_to_ingest':
311
- return '⚠️ raw/sources 目录下没有找到可 ingest 的文件。';
382
+ return '⚠️ raw/sources 目录中未找到可 ingest 的文件。';
312
383
 
313
384
  case 'ingest_started':
314
- return `✅ 已启动 ingest,共 ${fileCount} 个文件正在处理。请稍后执行 /fancy-story-builder analyze 进行分析。` + (extra ? '\n\n' + extra : '');
385
+ return `✅ 已启动 ingest,共 ${fileCount} 个文件正在处理。稍后可执行 /fancy-build maintain lint 进行检查。` + (extra ? '\n\n' + extra : '');
315
386
 
316
387
  case 'ingest_failed':
317
388
  return '❌ ingest 失败:' + (err || '未知错误');
@@ -319,6 +390,32 @@ export const storyBuilder = {
319
390
  case 'ingest_not_ready':
320
391
  return `⚠️ ingest 尚未完成。当前有 ${missingCount} 个文件未处理,${queueCount} 个任务进行中。请等待 ingest 结束后再执行分析。`;
321
392
 
393
+ case 'sync_done':
394
+ return '✅ 故事对象已同步到 llm_wiki。' + (extra ? '\n\n' + extra : '');
395
+
396
+ case 'sync_failed':
397
+ return '❌ 同步到 llm_wiki 失败。' + (extra ? '\n\n' + extra : '');
398
+
399
+ case 'query_done':
400
+ return extra || '💬 查询完成';
401
+
402
+ case 'lint_done':
403
+ return '❌ 检查发现问题' + (extra ? '\n\n' + extra : '') +
404
+ '\n\n💡 可执行 `/fancy-build fix`:插件会把问题归因到骨架/血肉/节奏,发给 LLM 修复、跑阶段校验并重新 sync。';
405
+
406
+ case 'lint_clean':
407
+ return '✅ 检查通过' + (extra ? '\n\n' + extra : '');
408
+
409
+ case 'stage_materialize_failed':
410
+ return '❌ ' + (phase || '当前') + '阶段数据已完成,但同步到 llm_wiki 失败。'
411
+ + (extra ? '\n\n' + extra : '');
412
+
413
+ case 'patch_done':
414
+ return extra || '✅ patch 完成';
415
+
416
+ case 'patch_cancelled':
417
+ return '已取消 patch,未修改任何文件。';
418
+
322
419
  default:
323
420
  return String(type);
324
421
  }
@@ -332,10 +429,17 @@ export const storyBuilder = {
332
429
  id: 'subcommand',
333
430
  question: '请选择要执行的阶段:',
334
431
  options: [
335
- { label: 'skeleton', description: '骨架阶段:生成核心冲突、角色、框架' },
336
- { label: 'flesh', description: '血肉阶段:补充世界观、势力、物品、地点' },
337
- { label: 'rhythm', description: '节奏阶段:规划分卷、伏笔、细纲' },
338
- { label: 'analyze', description: '分析阶段:检查结构问题' },
432
+ { label: 'scale', description: '10-scale:只算章/卷/幕/配额/节奏预算' },
433
+ { label: 'macro', description: '20-macro:世界观、卷纲、幕纲、主支线、结局、角色功能位' },
434
+ { label: 'cast', description: '30-cast:人物、势力、地点、冲突、伏笔、爽点、关系、信息差' },
435
+ { label: 'rhythm', description: '40-rhythm:章节地图;--full 同时生成全部细纲' },
436
+ { label: 'outline', description: '40-rhythm:单章细纲 outline CH001 / outline all' },
437
+ { label: 'writing', description: '50-writing:按单章细纲写正文' },
438
+ { label: 'sync', description: '同步阶段闭包到 llm_wiki(首次正式同步在 30-cast)' },
439
+ { label: 'query', description: '向 llm_wiki 提问:query <问题>' },
440
+ { label: 'lint', description: 'schema / link / 语义检查(30-cast 通过后)' },
441
+ { label: 'patch', description: '按 manifest 依赖图修订:patch <意见>' },
442
+ { label: 'help', description: '显示全部子命令说明' },
339
443
  ],
340
444
  };
341
445
  },
@@ -349,7 +453,7 @@ export const storyBuilder = {
349
453
  genre,
350
454
  target_words,
351
455
  target_chapters,
352
- core_summary,
456
+ positioning,
353
457
  core_conflict,
354
458
  hard_constraints,
355
459
  anti_tropes,
@@ -363,7 +467,7 @@ export const storyBuilder = {
363
467
  prompt = prompt.replace(/\$\{book_name\}/g, textField(book_name));
364
468
  prompt = prompt.replace(/\$\{target_words\}/g, textField(target_words));
365
469
  prompt = prompt.replace(/\$\{target_chapters\}/g, textField(target_chapters));
366
- prompt = prompt.replace(/\$\{core_summary\}/g, textField(core_summary));
470
+ prompt = prompt.replace(/\$\{positioning\}/g, textField(positioning));
367
471
  prompt = prompt.replace(/\$\{core_conflict\}/g, textField(core_conflict));
368
472
  prompt = prompt.replace(/\$\{hard_constraints\}/g, joinField(hard_constraints));
369
473
  prompt = prompt.replace(/\$\{anti_tropes\}/g, joinField(anti_tropes));
@@ -379,7 +483,7 @@ export const storyBuilder = {
379
483
  * 缺省时退回 topicData 拼一份(骨架产物还没落盘的兜底)。
380
484
  */
381
485
  generateFleshSystemPrompt({ topicData, wikiReferences, inputSnapshot }) {
382
- const { book_name, genre, target_chapters, core_summary, hard_constraints, anti_tropes } = topicData;
486
+ const { book_name, genre, target_chapters, positioning, hard_constraints, anti_tropes } = topicData;
383
487
 
384
488
  // 读取血肉阶段提示词模板(prompts/02-flesh-prompt.md)
385
489
  let prompt = cleanPromptForLLM(readBuiltInReference('02-flesh-prompt.md'));
@@ -388,7 +492,6 @@ export const storyBuilder = {
388
492
  prompt = prompt.replace(/\$\{book_name\}/g, textField(book_name));
389
493
  prompt = prompt.replace(/\$\{genre\}/g, textField(genre));
390
494
  prompt = prompt.replace(/\$\{target_chapters\}/g, textField(target_chapters));
391
- prompt = prompt.replace(/\$\{core_summary\}/g, textField(core_summary));
392
495
  // 血肉 prompt 文件暂未引用,但保持幂等
393
496
  prompt = prompt.replace(/\$\{hard_constraints\}/g, joinField(hard_constraints));
394
497
  prompt = prompt.replace(/\$\{anti_tropes\}/g, joinField(anti_tropes));
@@ -432,9 +535,12 @@ export const storyBuilder = {
432
535
 
433
536
  const contract = readBuiltInReference('02-flesh-prompt.md');
434
537
  const snapshot = o.inputSnapshot || snapshotFromTopicData(o.topicData || {});
435
- const withSnapshot = contract
436
- .replace(/\$\{input_snapshot_block\}/g, renderInputSnapshot(snapshot))
437
- .replace(/\$\{benchmark_refs_block\}/g, o.benchmarkRefs || '');
538
+ const withSnapshot = absolutizeInlinePaths(
539
+ contract
540
+ .replace(/\$\{input_snapshot_block\}/g, renderInputSnapshot(snapshot))
541
+ .replace(/\$\{benchmark_refs_block\}/g, o.benchmarkRefs || ''),
542
+ o.projectRoot
543
+ );
438
544
 
439
545
  // 首轮带全量契约;重试轮只带"本文件字段契约 + 质量 + 错误码速查",
440
546
  // 任务输入在首轮已经建立,后续轮不必重复。
@@ -451,6 +557,13 @@ export const storyBuilder = {
451
557
  parts.push('');
452
558
  parts.push('---');
453
559
  parts.push('');
560
+ if (o.benchmarkDigest) {
561
+ // 摘要正文必须每轮都在:重试轮不抽 refs 小节,但参考不能跟着消失。
562
+ parts.push(String(o.benchmarkDigest).trim());
563
+ parts.push('');
564
+ parts.push('---');
565
+ parts.push('');
566
+ }
454
567
  parts.push(this.buildFleshBatchTask({ batch, attempt, failures: o.failures, relations: o.relations, stageDir: o.stageDir, buildDir: o.buildDir }));
455
568
 
456
569
  return parts.join('\n');
@@ -484,9 +597,12 @@ export const storyBuilder = {
484
597
 
485
598
  const contract = readBuiltInReference('03-rhythm-prompt.md');
486
599
  const snapshot = o.inputSnapshot || snapshotFromTopicData(o.topicData || {});
487
- const withSnapshot = contract
488
- .replace(/\$\{input_snapshot_block\}/g, renderInputSnapshot(snapshot))
489
- .replace(/\$\{benchmark_refs_block\}/g, o.benchmarkRefs || '');
600
+ const withSnapshot = absolutizeInlinePaths(
601
+ contract
602
+ .replace(/\$\{input_snapshot_block\}/g, renderInputSnapshot(snapshot))
603
+ .replace(/\$\{benchmark_refs_block\}/g, o.benchmarkRefs || ''),
604
+ o.projectRoot
605
+ );
490
606
 
491
607
  const names = isRetry
492
608
  ? ['overview', 'file:' + batch.type, 'consistency', 'errors', 'forbidden']
@@ -497,6 +613,11 @@ export const storyBuilder = {
497
613
  }
498
614
 
499
615
  const parts = [picked.text, '', '---', ''];
616
+ if (o.benchmarkDigest) {
617
+ // 摘要正文必须每轮都在:重试轮不抽 refs 小节,但参考不能跟着消失。
618
+ parts.push(String(o.benchmarkDigest).trim());
619
+ parts.push('', '---', '');
620
+ }
500
621
  parts.push(this.buildRhythmBatchTask({
501
622
  batch: batch,
502
623
  attempt: attempt,
@@ -591,6 +712,21 @@ export const storyBuilder = {
591
712
  lines.push('');
592
713
  lines.push('**只写这些章,章号原样抄。**');
593
714
  lines.push('');
715
+ } else if (batch.rowsMode === 'optional') {
716
+ lines.push('## 本批负责的关系阶段(可 0 行)');
717
+ lines.push('');
718
+ lines.push('本批写 `' + batch.groupKey + '` 里 `' + (batch.ownerField || 'from') + '` 属于下表这些角色的行:');
719
+ lines.push('');
720
+ lines.push('| id | name |');
721
+ lines.push('|---|---|');
722
+ for (const item of batch.items || []) {
723
+ lines.push('| ' + (item.id || '-') + ' | ' + (item.name || '-') + ' |');
724
+ }
725
+ lines.push('');
726
+ lines.push('**没有关系变化就写空数组 `[]`**;有关系变化才写行,不要为了凑数硬编。');
727
+ lines.push('每行只写一个阶段:`from` / `to` / `stage` / `state` / `start_chapter`,可选 `end_chapter` / `trigger_event` / `description`。');
728
+ lines.push('同一对角色按 `start_chapter` 严格递增;`from` 必须是上表里的角色 id。');
729
+ lines.push('');
594
730
  } else if (!batch.wholeObject) {
595
731
  lines.push('## 本批要写的条目(' + batch.count + ' 条)');
596
732
  lines.push('');
@@ -627,6 +763,7 @@ export const storyBuilder = {
627
763
  lines.push('');
628
764
  }
629
765
 
766
+ lines.push(referenceLogPromptBlock('节奏', batch));
630
767
  lines.push('## 交付');
631
768
  lines.push('');
632
769
  lines.push('写完 `' + shardPath + '` 就结束本轮。');
@@ -731,6 +868,7 @@ export const storyBuilder = {
731
868
  lines.push('');
732
869
  }
733
870
 
871
+ lines.push(referenceLogPromptBlock('血肉', batch));
734
872
  lines.push('## 交付');
735
873
  lines.push('');
736
874
  lines.push('写完 `' + shardPath + '` 就结束本轮。');
@@ -782,10 +920,13 @@ export const storyBuilder = {
782
920
 
783
921
  const contract = readBuiltInReference('01-skeleton-prompt.md');
784
922
  const snapshot = o.inputSnapshot || snapshotFromTopicData(o.topicData || {});
785
- const withSnapshot = contract
786
- .replace(/\$\{input_snapshot_block\}/g, renderInputSnapshot(snapshot))
787
- // 参考分层说明(插件指导 vs 对标样本)+ 本阶段可读的对标文件清单
788
- .replace(/\$\{benchmark_refs_block\}/g, o.benchmarkRefs || '');
923
+ const withSnapshot = absolutizeInlinePaths(
924
+ contract
925
+ .replace(/\$\{input_snapshot_block\}/g, renderInputSnapshot(snapshot))
926
+ // 参考分层说明(插件指导 vs 对标样本)+ 本阶段可读的对标文件清单
927
+ .replace(/\$\{benchmark_refs_block\}/g, o.benchmarkRefs || ''),
928
+ o.projectRoot
929
+ );
789
930
 
790
931
  const names = isRetry
791
932
  ? ['overview', 'file:' + batch.type, 'consistency', 'errors', 'forbidden']
@@ -796,12 +937,18 @@ export const storyBuilder = {
796
937
  }
797
938
 
798
939
  const parts = [picked.text, '', '---', ''];
940
+ if (o.benchmarkDigest) {
941
+ // 摘要正文必须每轮都在:重试轮不抽 refs 小节,但参考不能跟着消失。
942
+ parts.push(String(o.benchmarkDigest).trim());
943
+ parts.push('', '---', '');
944
+ }
799
945
  parts.push(this.buildSkeletonBatchTask({
800
946
  batch: batch,
801
947
  attempt: attempt,
802
948
  failures: o.failures,
803
949
  idTables: o.idTables,
804
950
  extraContext: o.extraContext,
951
+ existingEntities: o.existingEntities,
805
952
  stageDir: o.stageDir,
806
953
  buildDir: o.buildDir
807
954
  }));
@@ -1026,6 +1173,21 @@ export const storyBuilder = {
1026
1173
  lines.push('');
1027
1174
  lines.push('**id 和 name 原样抄,不要改一个字符。**');
1028
1175
  lines.push('');
1176
+ } else if (batch.rowsMode === 'optional') {
1177
+ lines.push('## 本批负责的关系阶段(可 0 行)');
1178
+ lines.push('');
1179
+ lines.push('本批写 `' + batch.groupKey + '` 里 `' + (batch.ownerField || 'from') + '` 属于下表这些角色的行:');
1180
+ lines.push('');
1181
+ lines.push('| id | name | 类型 |');
1182
+ lines.push('|---|---|---|');
1183
+ for (const item of batch.items || []) {
1184
+ lines.push('| ' + (item.id || '-') + ' | ' + (item.name || '-') + ' | ' + (item.type || item.category || '-') + ' |');
1185
+ }
1186
+ lines.push('');
1187
+ lines.push('**没有关系变化就写空数组 `[]`**;有关系变化才写行,不要为了凑数硬编。');
1188
+ lines.push('每行只写一个阶段:`from` / `to` / `stage` / `state` / `start_chapter`,可选 `end_chapter` / `trigger_event` / `description`。');
1189
+ lines.push('同一对角色按 `start_chapter` 严格递增;`from` 必须是上表里的角色 id。');
1190
+ lines.push('');
1029
1191
  } else if (batch.rowsMode === 'semantic') {
1030
1192
  // 行数由关系本身决定:**不能说"写 N 条"**。
1031
1193
  // 以前统一写"本批要写的条目(N 条)…每行一条",模型把 N 当成真实行数,
@@ -1068,6 +1230,20 @@ export const storyBuilder = {
1068
1230
  lines.push('');
1069
1231
  }
1070
1232
 
1233
+ if (o.existingEntities && o.existingEntities.length) {
1234
+ lines.push('## 已经存在的实体(同一实体必须复用,不要换名新建)');
1235
+ lines.push('');
1236
+ lines.push('这些是前面批次已经写进实体清单的 id + name。写本批之前先对照:');
1237
+ lines.push('');
1238
+ lines.push('- 要写的实体已经在表里 → **复用原 id 和 name**,不要改一个字,也不要再建一条。');
1239
+ lines.push('- 同一个人的本名、称呼、绰号、关系称谓(哥哥/父亲/老师/老板等)→ 只保留**本名/正式名**那一条,不要为称呼另建实体。');
1240
+ lines.push('- 同一势力的全称、简称、集团/公司后缀 → 只保留已经存在的正式名,不要重复建。');
1241
+ lines.push('- 表里没有、且确实是新实体 → 才按本批配额新建。');
1242
+ lines.push('');
1243
+ for (const line of o.existingEntities) lines.push(line);
1244
+ lines.push('');
1245
+ }
1246
+
1071
1247
  if (o.idTables && o.idTables.length) {
1072
1248
  lines.push('## 可引用的 id(只能从下表里选)');
1073
1249
  lines.push('');
@@ -1084,6 +1260,20 @@ export const storyBuilder = {
1084
1260
  lines.push('');
1085
1261
  }
1086
1262
 
1263
+ if (Array.isArray(batch.optionalArrays) && batch.optionalArrays.indexOf('other_entities') >= 0) {
1264
+ lines.push('## 题材扩展实体(可选,0-8 条)');
1265
+ lines.push('');
1266
+ lines.push('同一个分片里还要写 `other_entities` 数组。它**不计入上面的条目数**:');
1267
+ lines.push('如果题材里有灵魂、神魂、器灵、妖灵、鬼魂、概念体等**不适合当人物**的存在,就在这里登记;没有就写空数组 `[]`。');
1268
+ lines.push('');
1269
+ lines.push('- id 从 `O001` 起连续编号,三位数字;不要占用人物/势力/道具的 id。');
1270
+ lines.push('- `kind` 写具体类别(灵魂/神魂/器灵/妖灵/鬼魂/概念体/其他),不要只写“其他”。');
1271
+ lines.push('- 只登记会在故事里实际出场、且需要独立关系的存在;不要把普通人物换个名字塞进来。');
1272
+ lines.push('- 它们与人物/其他实体的关系由 `关系.json` 的 `other_relations` 表达(如 `附身`、`寄宿`、`共生`、`镇压`)。');
1273
+ lines.push('');
1274
+ }
1275
+
1276
+ lines.push(referenceLogPromptBlock('骨架', batch));
1087
1277
  lines.push('## 交付');
1088
1278
  lines.push('');
1089
1279
  lines.push('写完 `' + shardPath + '` 就结束本轮。');
@@ -1134,7 +1324,7 @@ export const storyBuilder = {
1134
1324
  * 写哪些文件、写成什么格式、什么会被校验拦下、收到错误后怎么改。
1135
1325
  * 流程(读什么、校验什么、推进到哪一章)由 writing-driver.js 负责。
1136
1326
  */
1137
- generateWritingContract({ topicData, benchmarkRefs }) {
1327
+ generateWritingContract({ topicData, benchmarkRefs, benchmarkDigest, projectRoot }) {
1138
1328
  const { book_name, target_chapters } = topicData || {};
1139
1329
 
1140
1330
  // 读取写作阶段提示词模板(prompts/04-writing-prompt.md)
@@ -1146,6 +1336,12 @@ export const storyBuilder = {
1146
1336
  // 参考分层说明(插件指导 vs 对标样本)+ 本阶段可读的对标文件清单
1147
1337
  prompt = prompt.replace(/\$\{benchmark_refs_block\}/g, benchmarkRefs || '');
1148
1338
  prompt = prompt.replace(/\$\{[a-z_]+\}/g, '');
1339
+ if (benchmarkDigest) {
1340
+ // 摘要正文直接注入,避免模型只收藏路径、写作时完全不看。
1341
+ prompt = prompt + '\n\n' + String(benchmarkDigest).trim();
1342
+ }
1343
+ // 参考文件路径绝对化(基准钉死,见 absolutizeInlinePaths 的说明)
1344
+ prompt = absolutizeInlinePaths(prompt, projectRoot);
1149
1345
 
1150
1346
  // 追加"合法取值清单" —— 从 reading-power.js(唯一真源)动态生成。
1151
1347
  //
@@ -1167,7 +1363,7 @@ export const storyBuilder = {
1167
1363
  * 写死一份必然漂移:旧 skill 就坏在这儿(deslop 说 24 类、review 说 19 类、
1168
1364
  * 代码实现 21 类,三份对不上,模型根本不知道以哪套为准)。
1169
1365
  */
1170
- generateDeslopContract({ topicData } = {}) {
1366
+ generateDeslopContract({ topicData, projectRoot } = {}) {
1171
1367
  const { book_name } = topicData || {};
1172
1368
 
1173
1369
  // 读取去味阶段提示词模板(prompts/05-deslop-prompt.md)
@@ -1175,6 +1371,8 @@ export const storyBuilder = {
1175
1371
 
1176
1372
  prompt = prompt.replace(/\$\{book_name\}/g, book_name || '');
1177
1373
  prompt = prompt.replace(/\$\{[a-z_]+\}/g, '');
1374
+ // 参考文件路径绝对化(基准钉死,见 absolutizeInlinePaths 的说明)
1375
+ prompt = absolutizeInlinePaths(prompt, projectRoot);
1178
1376
 
1179
1377
  // 追加"AI 模式清单" —— 每一类都带改法,模型照着改
1180
1378
  prompt = prompt + '\n\n' + deslopLegend();