@cloud411716/fancy-webnovel 2.2.0 → 3.0.3

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 (212) hide show
  1. package/README.md +65 -0
  2. package/client.js +201 -15
  3. package/cordis.patch.yml +5 -1
  4. package/docs/douyin-plugin.md +118 -0
  5. package/docs/material-library/README.md +404 -0
  6. package/docs/material-library/import.js +437 -0
  7. package/docs/material-library/prompt.md +370 -0
  8. package/docs/material-library/queries.surql +94 -0
  9. package/docs/material-library/sample.jsonl +14 -0
  10. package/docs/material-library/schema.surql +46 -0
  11. package/docs/mcp-profile.example.yml +24 -0
  12. package/docs/story-graph-design.md +602 -0
  13. package/docs/story-graph-validation.md +81 -0
  14. package/docs/workspace-mcp.md +284 -0
  15. package/index.js +12 -7
  16. package/lib/atomic-write.js +109 -0
  17. package/lib/chapter-order.js +100 -0
  18. package/lib/confirmation.js +80 -0
  19. package/lib/db-init.js +123 -0
  20. package/lib/deployed-state.js +11 -11
  21. package/lib/direction-labels.js +265 -0
  22. package/lib/error-codes.js +31 -31
  23. package/lib/infra.js +29 -4
  24. package/lib/material-db.js +149 -0
  25. package/lib/path-guard.js +150 -0
  26. package/lib/projects.js +400 -0
  27. package/lib/scraper-utils.js +140 -0
  28. package/lib/surreal-server.js +338 -0
  29. package/lib/topic-decision.js +860 -0
  30. package/lib/topic-fields.js +251 -0
  31. package/lib/topic-schema.js +225 -0
  32. package/lib/workspace.js +501 -0
  33. package/package.json +31 -7
  34. package/plugins/fancy-analyze/analyze-definitions.md +519 -0
  35. package/plugins/fancy-analyze/index.js +1128 -77
  36. package/plugins/fancy-analyze/ingest-coverage.js +107 -0
  37. package/plugins/fancy-analyze/schema-analyze.md +355 -107
  38. package/plugins/fancy-analyze/scripts/append-summary-chapter.js +612 -0
  39. package/plugins/fancy-analyze/summary-progress.js +71 -7
  40. package/plugins/fancy-analyze/templates.js +437 -139
  41. package/plugins/fancy-bootstrap/index.js +276 -131
  42. package/plugins/fancy-bootstrap/scripts/init.js +59 -25
  43. package/plugins/fancy-bootstrap/templates.js +35 -18
  44. package/plugins/fancy-db/index.js +172 -0
  45. package/plugins/fancy-douyin/deploy.js +134 -0
  46. package/plugins/fancy-douyin/index.js +551 -0
  47. package/plugins/fancy-douyin/prompts/01-analysis-prompt.md +84 -0
  48. package/plugins/fancy-douyin/scripts/crawl-note.cjs +194 -0
  49. package/plugins/fancy-douyin/scripts/lib.cjs +197 -0
  50. package/plugins/fancy-douyin/scripts/scan-account.cjs +128 -0
  51. package/plugins/fancy-douyin/scripts/start-chrome.cjs +203 -0
  52. package/plugins/fancy-douyin/state.js +197 -0
  53. package/plugins/fancy-douyin/templates.js +127 -0
  54. package/plugins/fancy-export/exporter.js +805 -0
  55. package/plugins/fancy-export/index.js +63 -0
  56. package/plugins/fancy-import/import-driver.js +29 -6
  57. package/plugins/fancy-import/index.js +13 -5
  58. package/plugins/fancy-import/prompts/01-import-prompt.md +39 -14
  59. package/plugins/fancy-import/templates.js +17 -8
  60. package/plugins/fancy-material/exporter.js +153 -0
  61. package/plugins/fancy-material/genres.js +256 -0
  62. package/plugins/fancy-material/index.js +835 -0
  63. package/plugins/fancy-material/inventory.js +145 -0
  64. package/plugins/fancy-material/kinds.js +313 -0
  65. package/plugins/fancy-material/queue.js +55 -0
  66. package/plugins/fancy-material/scripts/query.js +254 -0
  67. package/plugins/fancy-material/templates.js +258 -0
  68. package/plugins/fancy-material/tool-deploy.js +138 -0
  69. package/plugins/fancy-material/validate.js +149 -0
  70. package/plugins/fancy-project/index.js +133 -0
  71. package/plugins/fancy-scan/existing-block.js +100 -0
  72. package/plugins/fancy-scan/index.js +410 -21
  73. package/plugins/fancy-scan/prompts/01-analysis-prompt.md +302 -0
  74. package/plugins/fancy-scan/scripts/append-block.js +725 -0
  75. package/plugins/fancy-scan/scripts/report-validate.js +824 -0
  76. package/plugins/fancy-scan/scripts/run-scan.js +169 -22
  77. package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +145 -16
  78. package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +179 -49
  79. package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +38 -20
  80. package/plugins/fancy-scan/templates/blocks.js +155 -0
  81. package/plugins/fancy-scan/templates/platforms.js +372 -0
  82. package/plugins/fancy-scan/templates/report-schema.js +90 -0
  83. package/plugins/fancy-scan/templates.js +545 -146
  84. package/plugins/fancy-scan/tool-deploy.js +335 -0
  85. package/plugins/fancy-scan/wait-turn.js +46 -0
  86. package/plugins/fancy-story-builder/db/README.md +106 -0
  87. package/plugins/fancy-story-builder/db/client.js +228 -0
  88. package/plugins/fancy-story-builder/db/commands.js +145 -0
  89. package/plugins/fancy-story-builder/db/ids.js +39 -0
  90. package/plugins/fancy-story-builder/db/import.js +798 -0
  91. package/plugins/fancy-story-builder/db/project.js +299 -0
  92. package/plugins/fancy-story-builder/db/query.js +361 -0
  93. package/plugins/fancy-story-builder/db/schema.js +417 -0
  94. package/plugins/fancy-story-builder/db/stage-sync.js +238 -0
  95. package/plugins/fancy-story-builder/flesh-output.js +19 -6
  96. package/plugins/fancy-story-builder/index.js +4105 -396
  97. package/plugins/fancy-story-builder/patch-driver.js +390 -0
  98. package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +29 -19
  99. package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +72 -10
  100. package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +40 -5
  101. package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +37 -0
  102. package/plugins/fancy-story-builder/purpose-template.md +42 -66
  103. package/plugins/fancy-story-builder/references/formulas-skeleton.json +229 -56
  104. package/plugins/fancy-story-builder/references/genre-profiles.json +3514 -0
  105. package/plugins/fancy-story-builder/references/glossary.md +15 -6
  106. package/plugins/fancy-story-builder/references/index.md +97 -6
  107. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +1 -1
  108. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +25 -7
  109. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +5 -1
  110. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +6 -2
  111. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +15 -5
  112. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +1 -1
  113. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +2 -1
  114. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +1 -1
  115. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +19 -1
  116. package/plugins/fancy-story-builder/schema-setting.json +1100 -0
  117. package/plugins/fancy-story-builder/schema-setting.md +201 -293
  118. package/plugins/fancy-story-builder/scripts/brief-validate.js +164 -0
  119. package/plugins/fancy-story-builder/scripts/content-quality.js +4 -1
  120. package/plugins/fancy-story-builder/scripts/flesh-validate.js +135 -12
  121. package/plugins/fancy-story-builder/scripts/graph-utils.js +146 -47
  122. package/plugins/fancy-story-builder/scripts/reference-log.js +110 -0
  123. package/plugins/fancy-story-builder/scripts/rhythm-validate.js +100 -4
  124. package/plugins/fancy-story-builder/scripts/schema-pages.js +2668 -0
  125. package/plugins/fancy-story-builder/scripts/semantic-lint.js +5 -1
  126. package/plugins/fancy-story-builder/scripts/skeleton-validate.js +158 -17
  127. package/plugins/fancy-story-builder/scripts/stage-batches.js +36 -2
  128. package/plugins/fancy-story-builder/scripts/stage-contract.js +12 -0
  129. package/plugins/fancy-story-builder/scripts/story-artifacts.js +933 -0
  130. package/plugins/fancy-story-builder/scripts/upstream-query.js +1 -1
  131. package/plugins/fancy-story-builder/scripts/v2/baseline.js +112 -0
  132. package/plugins/fancy-story-builder/scripts/v2/cast.js +517 -0
  133. package/plugins/fancy-story-builder/scripts/v2/context.js +434 -0
  134. package/plugins/fancy-story-builder/scripts/v2/contracts.js +493 -0
  135. package/plugins/fancy-story-builder/scripts/v2/core.js +419 -0
  136. package/plugins/fancy-story-builder/scripts/v2/engine.js +1068 -0
  137. package/plugins/fancy-story-builder/scripts/v2/export-markdown.js +794 -0
  138. package/plugins/fancy-story-builder/scripts/v2/genre.js +195 -0
  139. package/plugins/fancy-story-builder/scripts/v2/knowledge.js +321 -0
  140. package/plugins/fancy-story-builder/scripts/v2/macro.js +409 -0
  141. package/plugins/fancy-story-builder/scripts/v2/manifest.js +538 -0
  142. package/plugins/fancy-story-builder/scripts/v2/migrate.js +137 -0
  143. package/plugins/fancy-story-builder/scripts/v2/prompts.js +1335 -0
  144. package/plugins/fancy-story-builder/scripts/v2/rhythm.js +618 -0
  145. package/plugins/fancy-story-builder/scripts/v2/scale.js +843 -0
  146. package/plugins/fancy-story-builder/scripts/v2/snapshot.js +121 -0
  147. package/plugins/fancy-story-builder/scripts/v2/stages.js +273 -0
  148. package/plugins/fancy-story-builder/scripts/v2/writing.js +216 -0
  149. package/plugins/fancy-story-builder/storydb/README.md +271 -0
  150. package/plugins/fancy-story-builder/storydb/cli.js +458 -0
  151. package/plugins/fancy-story-builder/storydb/client.js +351 -0
  152. package/plugins/fancy-story-builder/storydb/compile.js +452 -0
  153. package/plugins/fancy-story-builder/storydb/context.js +288 -0
  154. package/plugins/fancy-story-builder/storydb/definitions.js +699 -0
  155. package/plugins/fancy-story-builder/storydb/driver.js +106 -0
  156. package/plugins/fancy-story-builder/storydb/dsh-tools.js +313 -0
  157. package/plugins/fancy-story-builder/storydb/export.js +175 -0
  158. package/plugins/fancy-story-builder/storydb/ids.js +31 -0
  159. package/plugins/fancy-story-builder/storydb/impact.js +170 -0
  160. package/plugins/fancy-story-builder/storydb/import.js +559 -0
  161. package/plugins/fancy-story-builder/storydb/mcp.js +99 -0
  162. package/plugins/fancy-story-builder/storydb/prompts.js +952 -0
  163. package/plugins/fancy-story-builder/storydb/stage-command.js +379 -0
  164. package/plugins/fancy-story-builder/storydb/stage.js +426 -0
  165. package/plugins/fancy-story-builder/storydb/store.js +891 -0
  166. package/plugins/fancy-story-builder/storydb/tools.js +592 -0
  167. package/plugins/fancy-story-builder/storydb/validation.js +183 -0
  168. package/plugins/fancy-story-builder/templates/v2/10-scale/scale-template.json +144 -0
  169. package/plugins/fancy-story-builder/templates/v2/20-macro/emotion-template.json +15 -0
  170. package/plugins/fancy-story-builder/templates/v2/20-macro/endings-template.json +16 -0
  171. package/plugins/fancy-story-builder/templates/v2/20-macro/lines-template.json +28 -0
  172. package/plugins/fancy-story-builder/templates/v2/20-macro/roles-template.json +10 -0
  173. package/plugins/fancy-story-builder/templates/v2/20-macro/story-template.json +19 -0
  174. package/plugins/fancy-story-builder/templates/v2/20-macro/volumes-template.json +24 -0
  175. package/plugins/fancy-story-builder/templates/v2/20-macro/world-template.json +24 -0
  176. package/plugins/fancy-story-builder/templates/v2/30-cast/characters-template.json +49 -0
  177. package/plugins/fancy-story-builder/templates/v2/30-cast/climaxes-template.json +14 -0
  178. package/plugins/fancy-story-builder/templates/v2/30-cast/conflicts-template.json +30 -0
  179. package/plugins/fancy-story-builder/templates/v2/30-cast/factions-template.json +18 -0
  180. package/plugins/fancy-story-builder/templates/v2/30-cast/foreshadows-template.json +29 -0
  181. package/plugins/fancy-story-builder/templates/v2/30-cast/information-template.json +29 -0
  182. package/plugins/fancy-story-builder/templates/v2/30-cast/items-template.json +13 -0
  183. package/plugins/fancy-story-builder/templates/v2/30-cast/locations-template.json +12 -0
  184. package/plugins/fancy-story-builder/templates/v2/30-cast/payoffs-template.json +16 -0
  185. package/plugins/fancy-story-builder/templates/v2/30-cast/relations-template.json +44 -0
  186. package/plugins/fancy-story-builder/templates/v2/40-rhythm/chapter-map-template.json +81 -0
  187. package/plugins/fancy-story-builder/templates/v2/40-rhythm/outline-template.json +162 -0
  188. package/plugins/fancy-story-builder/templates/v2/50-writing/ledger-template.json +46 -0
  189. package/plugins/fancy-story-builder/templates/v2/60-deslop/report-template.json +10 -0
  190. 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
  191. 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
  192. 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
  193. 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
  194. 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
  195. 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
  196. 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
  197. 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
  198. 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
  199. 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
  200. 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
  201. 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
  202. 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
  203. package/plugins/fancy-story-builder/templates.js +272 -74
  204. package/plugins/fancy-story-builder/wiki-references.js +429 -93
  205. package/plugins/fancy-story-builder/writing-driver.js +336 -9
  206. package/plugins/fancy-topic/index.js +414 -61
  207. package/plugins/fancy-topic/prompts/01-exploration-prompt.md +115 -0
  208. package/plugins/fancy-topic/scripts/save-suggestion.js +219 -0
  209. package/plugins/fancy-topic/scripts/select-topic.js +106 -63
  210. package/plugins/fancy-topic/templates.js +225 -122
  211. package/plugins/fancy-topic/tool-deploy.js +200 -0
  212. package/lib/safe-rm.js +0 -94
@@ -1,68 +1,120 @@
1
1
  /**
2
2
  * wiki-references.js
3
- * 对标参考(对标/wiki/)的目录清单与注入文本
3
+ * 对标参考(对标/wiki/)的目录清单、阶段映射与注入文本。
4
4
  *
5
- * 抽出来单独成模块的理由:这段逻辑决定了"哪些对标文件真的可以读",
6
- * 必须能被直接测。放在 index.js 里只能靠跑整个插件来覆盖,测不动。
5
+ * 这里有三件事必须分开:
6
+ * 1. **有哪些目录**:22 个可能目录(14 个按章技法 + 8 个跨章节实体;没有源页的目录不会有总结)。
7
+ * 2. **哪个阶段/批次该看哪些**:阶段级清单 + 批次级重点。
8
+ * 3. **怎么保证模型真的看到**:除了列路径,还要把摘要正文注入批次提示词。
9
+ *
10
+ * 为什么第 3 点单独做:只给路径时,模型可以声称"已参考"但完全不读;
11
+ * 插件把摘要直接放进 prompt 后,"跳过参考文件"这件事在物理上就不成立了。
7
12
  */
8
- import { existsSync } from 'fs';
13
+ import { existsSync, readFileSync } from 'fs';
9
14
  import { join } from 'path';
10
15
 
11
16
  // 各目录的作用说明(注入提示词时用)
12
17
  export const DIRECTORY_DESCRIPTIONS = {
13
- '叙事视角': '叙事视角:分析视角选择和切换策略',
14
- '章节结构': '章节结构:分析章节的起承转合结构',
15
- '节奏韵律': '节奏韵律:分析叙事节奏的张弛控制',
16
- '对话技巧': '对话技巧:分析对话中的言外之意和潜台词',
17
- '描写技法': '描写技法:分析环境、动作,心理描写的技巧',
18
- '伏笔回收': '伏笔回收:分析伏笔的埋设和回收技巧',
19
- '世界观构建': '世界观构建:分析世界观设定的呈现方式',
20
- '角色弧光': '角色弧光:分析角色在不同章节的成长弧线变化',
21
- '情节机制': '情节机制:分析情节推进的内在逻辑',
22
- '开头钩子': '开头钩子:分析开篇吸引读者的技巧',
23
- '结尾收束': '结尾收束:分析章节结尾的收束技法',
24
- '句式变化': '句式变化:分析句子的长短和节奏变化',
25
- '段落布局': '段落布局:分析段落的疏密和布局',
26
- '场景转换': '场景转换:分析场景切换的技巧',
27
- '情绪渲染': '情绪渲染:分析情感氛围的营造方式',
28
- '张力构建': '张力构建:分析冲突和悬念的构建方式',
29
- '来源摘要': '来源摘要:章节的完整分析和要点提炼'
18
+ '叙事视角': '叙事视角与信息差:谁在看、读者知道多少、视角如何限制或释放信息',
19
+ '章节结构': '单章起承转合:一章内部如何组织冲突、转折、收束',
20
+ '节奏韵律': '叙事节奏:张弛、信息密度、呼吸位、加压与泄压',
21
+ '对话技巧': '对话功能:推进剧情、塑造人物、传递设定、制造潜台词',
22
+ '描写技法': '描写:环境、动作、心理、感官细节如何服务人物与制度',
23
+ '情节机制': '情节推进机制:信息差、资源门槛、因果链、金手指边界',
24
+ '开头钩子': '开篇/章首钩子:如何在前几百字制造问题与期待',
25
+ '结尾收束': '章末/卷末收束:如何留悬念、转目标、兑现情绪',
26
+ '句式变化': '句式节奏:长短句、问句、短句冲击、解释句铺陈',
27
+ '段落布局': '段落功能与视觉节奏:分段、独段、留白、信息焦点',
28
+ '场景转换': '场景切换:时空标定、焦点交接、转场如何推进调查或关系',
29
+ '情绪渲染': '情绪曲线:群体/个体、希望/挫败、自嘲/爆发、共情',
30
+ '张力构建': '张力与压力:信息缺口、金额/时限、门槛叠加、赌注',
31
+ '章节功能设计': '章节功能:开局、揭示、推进、转折、收束、下一门槛',
32
+ '人物关系设计': '关系设计:试探、互助、家庭分工、师徒、竞争、信任边界',
33
+ '势力结构': '势力设计:资源/武力双轨、组织信用、区域议价、成员动员',
34
+ '世界观设计': '世界观设计:规则、力量体系、测量/资格、阶层与制度',
35
+ '冲突设计': '冲突设计:公开节点、赌注、制度门槛、资源困境、组织对抗',
36
+ '主线支线结构': '主线/支线:核心目标稳定、难度结构改写、支线服务主线',
37
+ '分卷节奏': '分卷/幕结构:卷目标、卷内起伏、卷末升级',
38
+ '伏笔布局': '伏笔:埋设、分层揭示、预约兑现、传闻层、问题清单',
39
+ '角色弧光设计': '角色弧光:目标—成本—修正、缺陷、动摇不改、配角功能'
30
40
  };
31
41
 
32
- // 各阶段对应的 wiki 目录
33
- //
34
- // 映射依据:**这一层产物要回答什么问题**,再挑对标技法里对得上的那些。
35
- // 判据是"样本里的那招能不能直接指导本层的一个字段",不是"主题沾边"。
42
+ // 各阶段对应的 wiki 目录(阶段级“本阶段可以看哪些”,不是“本批必须全读”)
36
43
  //
37
- // 骨架:有哪些实体、怎么连、有多少、分几卷几幕
38
- // → 情节机制(主线支线怎么构成)/ 张力构建(冲突与门槛怎么叠)
39
- // → 伏笔回收(伏笔清单怎么埋)/ 角色弧光(成长型支线的走向)
40
- // → 结尾收束(结局怎么设计)/ 世界观构建(规则与势力怎么成体系)
41
- // 血肉:实体长什么样(角色卡、世界观、关系、情绪)
42
- // → 世界观构建 / 描写技法 / 对话技巧 / 场景转换 / 情绪渲染
43
- // → 角色弧光 / 伏笔回收 / 情节机制
44
- // 节奏:什么时间发生(升级、弧线、高潮钩子、爽点、伏笔密度、章节切分)
45
- // → 节奏韵律 / 张力构建 / 章节结构 / 伏笔回收 / 情绪渲染
46
- // → 开头钩子 / 结尾收束 / 情节机制 / 角色弧光
47
- // 写作:具体怎么写(场景、章节工作流、开篇、对话、句式、语气)
48
- // → 开头钩子 / 结尾收束 / 对话技巧 / 描写技法 / 段落布局 / 句式变化
49
- // → 叙事视角 / 场景转换 / 节奏韵律
44
+ // 判据:**这一层产物要回答什么问题**,不是“主题沾边”。
45
+ // skeleton:有哪些实体、怎么连、有多少、分几卷几幕
46
+ // flesh:实体长什么样(角色/关系/势力/世界/情绪/道具)
47
+ // rhythm:什么时间发生(升级、弧线、高潮、钩子、伏笔密度、章节切分)
48
+ // writing:具体怎么写(场景、视角、对话、描写、句式、段落、开篇、收束)
50
49
  export const PHASE_DIRECTORIES = {
51
- skeleton: ['世界观构建', '情节机制', '张力构建', '伏笔回收', '角色弧光', '结尾收束'],
52
- flesh: ['世界观构建', '描写技法', '对话技巧', '场景转换', '情绪渲染', '角色弧光', '伏笔回收', '情节机制'],
53
- rhythm: ['节奏韵律', '张力构建', '章节结构', '伏笔回收', '情绪渲染', '开头钩子', '结尾收束', '情节机制', '角色弧光'],
54
- // 写作阶段以前没有对应键 → readWikiReferences 会退回 skeleton 那套,
55
- // 结果是写章节时拿到的是骨架阶段的目录(角色弧光/伏笔回收…),
56
- // 真正该读的对话、句式、段落反而一个都没有(2026-10-27 查到)。
57
- writing: ['开头钩子', '结尾收束', '对话技巧', '描写技法', '段落布局', '句式变化', '叙事视角', '场景转换', '节奏韵律'],
58
- // 去味阶段不注入对标参考:改写的是"像不像人话",不是"写得好不好",
50
+ skeleton: [
51
+ '主线支线结构', '冲突设计', '世界观设计', '势力结构',
52
+ '人物关系设计', '角色弧光设计', '伏笔布局', '情节机制',
53
+ '张力构建', '分卷节奏'
54
+ ],
55
+ flesh: [
56
+ '角色弧光设计', '人物关系设计', '势力结构', '世界观设计',
57
+ '冲突设计', '情绪渲染', '描写技法', '对话技巧',
58
+ '伏笔布局', '情节机制', '主线支线结构'
59
+ ],
60
+ rhythm: [
61
+ '节奏韵律', '章节结构', '章节功能设计', '开头钩子',
62
+ '结尾收束', '张力构建', '冲突设计', '伏笔布局',
63
+ '角色弧光设计', '主线支线结构', '分卷节奏', '情绪渲染',
64
+ '情节机制', '场景转换'
65
+ ],
66
+ writing: [
67
+ '叙事视角', '场景转换', '对话技巧', '描写技法',
68
+ '句式变化', '段落布局', '情绪渲染', '节奏韵律',
69
+ '开头钩子', '结尾收束', '张力构建', '伏笔布局',
70
+ '章节结构'
71
+ ],
72
+ // 去味阶段不注入对标参考:改写的是“像不像人话”,不是“写得好不好”,
59
73
  // 拿别人的写法当参照反而会把旧稿的毛病一起抄进去。
60
- deslop: [],
61
- analyze: ['角色弧光', '伏笔回收', '结尾收束', '张力构建', '情节机制', '开头钩子', '节奏韵律', '世界观构建', '章节结构', '情绪渲染', '对话技巧', '场景转换', '描写技法']
74
+ deslop: []
75
+ };
76
+
77
+ /**
78
+ * 批次级映射:同一阶段里,不同产物该看哪几篇。
79
+ *
80
+ * 阶段级清单解决“本阶段大概有哪些参考”,批次级映射解决“这一批到底该看哪几篇”。
81
+ * 两者都保留:阶段级用于建立全局地图,批次级用于把摘要压到单批可消化的量。
82
+ */
83
+ export const BATCH_DIRECTORIES = {
84
+ skeleton: {
85
+ '框架': ['主线支线结构', '冲突设计', '世界观设计', '分卷节奏', '张力构建'],
86
+ '实体': ['角色弧光设计', '势力结构', '世界观设计', '人物关系设计', '冲突设计', '伏笔布局'],
87
+ '关系': ['人物关系设计', '势力结构', '冲突设计', '角色弧光设计'],
88
+ '层级': ['主线支线结构', '分卷节奏', '势力结构', '冲突设计']
89
+ },
90
+ flesh: {
91
+ '角色': ['角色弧光设计', '人物关系设计', '情绪渲染', '对话技巧'],
92
+ '冲突': ['冲突设计', '角色弧光设计', '张力构建'],
93
+ '势力': ['势力结构', '世界观设计', '人物关系设计'],
94
+ '剧情': ['主线支线结构', '情节机制', '伏笔布局', '人物关系设计'],
95
+ '地点': ['世界观设计', '描写技法', '场景转换'],
96
+ '结局': ['主线支线结构', '情绪渲染', '结尾收束'],
97
+ '世界观规则': ['世界观设计', '势力结构', '冲突设计'],
98
+ '关键道具': ['伏笔布局', '世界观设计', '描写技法']
99
+ },
100
+ rhythm: {
101
+ '章节规划': ['节奏韵律', '章节功能设计', '章节结构', '分卷节奏', '主线支线结构'],
102
+ '节奏参数': ['节奏韵律', '张力构建', '冲突设计', '情绪渲染'],
103
+ '时序映射': ['角色弧光设计', '主线支线结构', '伏笔布局', '分卷节奏'],
104
+ '章节细纲': ['开头钩子', '结尾收束', '章节结构', '章节功能设计', '张力构建', '伏笔布局']
105
+ },
106
+ writing: {
107
+ default: [
108
+ '叙事视角', '场景转换', '对话技巧', '描写技法',
109
+ '句式变化', '段落布局', '情绪渲染', '节奏韵律',
110
+ '开头钩子', '结尾收束', '张力构建', '伏笔布局',
111
+ '章节结构'
112
+ ]
113
+ }
62
114
  };
63
115
 
64
116
  /**
65
- * 各阶段的"插件指导目录"与本阶段重点。
117
+ * 各阶段的“插件指导目录”与本阶段重点。
66
118
  *
67
119
  * 用来在提示词里把两层参考分开讲清楚:
68
120
  * references/<阶段>指导/ = 高维方法论(这一层该怎么设计)
@@ -88,18 +140,40 @@ export const PHASE_GUIDANCE = {
88
140
  };
89
141
 
90
142
  /**
91
- * 找出当前阶段真正可读的对标总结文件。
92
- *
93
- * 只认 `<目录名>-总结.md`;目录在但总结文件不在的,记为 missing。
143
+ * 各阶段「方法论 vs 对标样本」的大致创作权重。
94
144
  *
95
- * @returns {{ available: Array<{dir:string, path:string}>, missing: string[] }}
145
+ * 不是硬配额:契约/选题/上游永远 100% 服从;这里的百分比只用于回答
146
+ * “当两者都可用时,创作判断更该听谁”。
96
147
  */
97
- export function collectWikiReferences(projectRoot, phase = 'skeleton') {
148
+ export const STAGE_REFERENCE_WEIGHTS = {
149
+ skeleton: { method: '60%', benchmark: '40%', note: '结构、实体、关系模式可借样本校准;配额、图结构、字段以方法论和校验器为准' },
150
+ flesh: { method: '55%', benchmark: '45%', note: '人物、关系、世界质感可多借样本;字段与引用完整性以契约为准' },
151
+ rhythm: { method: '50%', benchmark: '50%', note: '节奏最依赖市场手感;公式与联合约束仍是硬门槛' },
152
+ writing: { method: '65%', benchmark: '35%', note: 'craft 下限优先;样本只校准题材语气与桥段,防止照抄' }
153
+ };
154
+
155
+ /** 已登记的总结目录集合(测试用来防止映射里写出不存在的目录)。 */
156
+ export const KNOWN_SUMMARY_DIRS = Object.keys(DIRECTORY_DESCRIPTIONS);
157
+
158
+ /** 取出某阶段的目录清单(保留映射顺序)。 */
159
+ function phaseDirs(phase) {
160
+ return PHASE_DIRECTORIES[phase] || PHASE_DIRECTORIES.skeleton;
161
+ }
162
+
163
+ /** 取出某阶段某批次的重点目录;没有批次映射时退回阶段清单。 */
164
+ function batchDirs(phase, batchType) {
165
+ const map = BATCH_DIRECTORIES[phase];
166
+ if (map && batchType && map[batchType]) return map[batchType];
167
+ if (map && map.default) return map.default;
168
+ return phaseDirs(phase);
169
+ }
170
+
171
+ /** 按给定目录顺序检查总结文件是否存在。 */
172
+ function collectDirs(projectRoot, dirs) {
98
173
  const wikiDir = join(projectRoot, '对标', 'wiki');
99
- const phaseDirs = PHASE_DIRECTORIES[phase] || PHASE_DIRECTORIES.skeleton;
100
174
  const available = [];
101
175
  const missing = [];
102
- for (const dir of Array.from(phaseDirs).sort()) {
176
+ for (const dir of dirs) {
103
177
  const summaryFile = join(wikiDir, dir, dir + '-总结.md');
104
178
  if (existsSync(summaryFile)) {
105
179
  available.push({ dir, path: '对标/wiki/' + dir + '/' + dir + '-总结.md' });
@@ -111,9 +185,35 @@ export function collectWikiReferences(projectRoot, phase = 'skeleton') {
111
185
  }
112
186
 
113
187
  /**
114
- * 生成注入提示词的对标参考文本。
188
+ * 找出当前阶段真正可读的对标总结文件。
189
+ *
190
+ * 只认 `<目录名>-总结.md`;目录在但总结文件不在的,记为 missing。
191
+ * 返回顺序按 PHASE_DIRECTORIES 的映射顺序(方便摘要按优先级取前几篇)。
192
+ *
193
+ * @returns {{ available: Array<{dir:string, path:string}>, missing: string[] }}
194
+ */
195
+ export function collectWikiReferences(projectRoot, phase = 'skeleton') {
196
+ return collectDirs(projectRoot, phaseDirs(phase));
197
+ }
198
+
199
+ /**
200
+ * 找出当前阶段、当前批次真正可读的对标总结文件。
201
+ *
202
+ * 批次映射优先;没有映射时退回阶段清单。
203
+ *
204
+ * @param {string} projectRoot
205
+ * @param {string} phase
206
+ * @param {string} [batchType] 例如“框架/实体/关系/层级”“角色/冲突/…”“章节规划/… ”
207
+ * @returns {{ available: Array<{dir:string, path:string}>, missing: string[] }}
208
+ */
209
+ export function collectBatchWikiReferences(projectRoot, phase = 'skeleton', batchType = '') {
210
+ return collectDirs(projectRoot, batchDirs(phase, batchType));
211
+ }
212
+
213
+ /**
214
+ * 生成注入提示词的对标参考文本(只列路径,不展开正文)。
115
215
  *
116
- * 只注入**实际存在**的文件;缺失的目录明确告知"没有",避免 LLM 去读不存在的
216
+ * 只注入**实际存在**的文件;缺失的目录明确告知“没有”,避免 LLM 去读不存在的
117
217
  * 路径后自行编造内容。
118
218
  */
119
219
  export function readWikiReferences(projectRoot, phase = 'skeleton') {
@@ -136,69 +236,305 @@ export function readWikiReferences(projectRoot, phase = 'skeleton') {
136
236
  }
137
237
 
138
238
  /**
139
- * 生成"参考分层"说明段,注入到各阶段提示词的参考小节。
239
+ * 生成“参考优先级”说明段,注入到各阶段提示词的参考小节。
140
240
  *
141
- * ## 为什么要单独生成这一段
241
+ * 这一段的职责不是“多列几个文件”,而是把四种参考的**优先级和边界**讲死:
142
242
  *
143
- * 插件自带的 `references/<阶段>指导/` 和项目里的 `对标/wiki/` 是**两种性质
144
- * 完全不同**的参考,模型很容易混着用:
243
+ * 1. 硬契约(模板/glossary/公式/校验器/上游 JSON)—— 100% 服从
244
+ * 2. 本项目选题与上游产物 —— 创作主权;对标不能改设定
245
+ * 3. references/<阶段>指导/ —— 方法论,决定下限
246
+ * 4. 对标/wiki/ 总结 —— 市场样本,校准上限
145
247
  *
146
- * - `references/<阶段>指导/` = **高维方法论**:这一层该怎么设计的通则
147
- * (数量怎么定、实体卡写什么、节奏怎么排)。是"怎么想"。
148
- * - `对标/wiki/<技法>/<技法>-总结.md` = **市场验证过的落地样本**:
149
- * 从真实商业小说里提炼的"某招在成品里怎么生效"。是"怎么落地"。
150
- *
151
- * 混用的后果不是文风问题,是**设定污染**:拿别人的成品当规则 → 照抄专名、
152
- * 情节、句子、金手指 → 撞设定、涉嫌抄袭。所以这一段必须把两层的边界、
153
- * 优先级和红线一次讲清楚,并给出"本阶段该读哪几个"的明确清单。
154
- *
155
- * ## 为什么还必须注入到批次提示词
156
- *
157
- * 以前这份对标参考只挂在**系统提示词**上,而系统提示词那几个生成函数
158
- * (generateSkeletonSystemPrompt 等)**从来没有被调用** ——
159
- * 真正派给模型的是每批的批次提示词。结果就是:对标参考一次都没到过模型手里
160
- * (tt5 实测:39 条骨架批次指令里 0 条含"对标",整个会话 0 次"对标参考")。
248
+ * 为什么不能 50/50:方法论和样本回答的是不同问题。
249
+ * 方法论回答“这一层必须有什么、字段怎么成立”;样本回答“这一招在成品里怎么落地”。
250
+ * 把两者等权,模型就会拿样本里的桥段去覆盖模板/选题,最后写出一本“像对标但不像本项目”的书。
161
251
  *
162
252
  * @param {string} projectRoot 项目根目录
163
253
  * @param {string} phase 阶段(skeleton / flesh / rhythm / writing)
254
+ * @param {string} [batchType] 批次类型(可选;给了就额外列“本批优先”)
164
255
  * @returns {string} 注入文本(没有对标目录时返回空串)
165
256
  */
166
- export function renderBenchmarkGuidance(projectRoot, phase = 'skeleton') {
257
+ export function renderBenchmarkGuidance(projectRoot, phase = 'skeleton', batchType = '') {
167
258
  const guide = PHASE_GUIDANCE[phase];
168
259
  if (!guide) return '';
169
260
  const wikiDir = join(projectRoot, '对标', 'wiki');
170
261
  if (!existsSync(wikiDir)) return '';
171
262
 
172
- const { available, missing } = collectWikiReferences(projectRoot, phase);
263
+ const phaseRefs = collectWikiReferences(projectRoot, phase);
264
+ const batchRefs = batchType ? collectBatchWikiReferences(projectRoot, phase, batchType) : null;
265
+ const weights = STAGE_REFERENCE_WEIGHTS[phase] || STAGE_REFERENCE_WEIGHTS.skeleton;
173
266
 
174
267
  const lines = [];
175
- lines.push('### 参考分三层,各读各的,别混\n');
176
- lines.push('| 层 | 位置 | 是什么 | 怎么用 |');
268
+ lines.push('### 参考优先级(不是 50/50)');
269
+ lines.push('');
270
+ lines.push('| 优先级 | 参考 | 作用 | 冲突时怎么判 |');
177
271
  lines.push('|---|---|---|---|');
178
- lines.push('| 硬契约 | `模板/`、`references/glossary.md` | 字段、枚举、id 规则 —— 校验器就按它判 | **必读**,动笔前先对齐 |');
179
- lines.push('| 高维方法论 | `' + guide.guideDir + '/` | ' + guide.focus + '的通则 | **必读**,按本批文件选篇 |');
180
- lines.push('| 市场验证样本 | `对标/wiki/` 下的 `<技法名>-总结.md` | 真实商业小说里某一招怎么生效 | **选读**,用来校准手法 |');
272
+ lines.push('| 1 | `模板/`、`references/glossary.md`、`formulas-*.json`、校验器 | 字段、枚举、公式、结构 | **100% 服从**,不能拿样本改契约 |');
273
+ lines.push('| 2 | 本项目选题、`input_snapshot`、上游 JSON | 这本书的创作主权 | 对标不能改设定;硬门禁不能破 |');
274
+ lines.push('| 3 | `' + guide.guideDir + '/` | ' + guide.focus + '的通则 | **主参考**:决定下限与字段质量 |');
275
+ lines.push('| 4 | `对标/wiki/` 的 `<技法>-总结.md` | 真实商业小说里某一招怎么落地 | **校准参考**:只在可行域内选点 |');
276
+ lines.push('');
277
+ lines.push('**本阶段的创作权重**:方法论约 **' + weights.method + '**,对标样本约 **' + weights.benchmark + '**。' + weights.note + '。');
278
+ lines.push('');
279
+ lines.push('**匹配度折扣**:对标样本还要看它和本项目的距离——同题材 + 同平台 + 同读者层按上表;同题材不同平台约 ×0.7;不同题材约 ×0.3(只借通用结构,不硬套桥段)。');
181
280
  lines.push('');
182
- lines.push('**读法**:先读 `' + guide.guideDir + '/`(决定"该怎么想"),再按需翻对标样本(校准"怎么落地")。');
281
+ lines.push('**读法**:先读 `' + guide.guideDir + '/`(决定“该怎么想”);本批对标摘要只用于定位,**决策前必须核对原文件里的对应条目**,摘要不能替代原文。');
183
282
  lines.push('');
184
- lines.push('**红线**:对标样本是**别人的成品**,只学结构与手法 —— '
185
- + '禁止照搬专名、情节、句子、金手指设定;'
186
- + '样本里的角色名/地名/机构名一律不许进本项目。');
283
+ lines.push('**本批强制**:摘要正文会直接注入,但摘要只负责定位;写作时必须先在原文件中核对 **1–2 条与本批字段/场景直接相关**的原则,落到具体设计上,并把参考文件和条目写进 `设定/.reference-decisions.md`。不要只收藏路径,也不要声称“已读取”却没有体现。确实没有适用条目时按契约写,不要硬套。');
284
+ lines.push('');
285
+ lines.push('**红线**:对标样本是**别人的成品**,只学结构与手法——禁止照搬专名、情节、句子、金手指设定;样本里的角色名/地名/机构名一律不许进本项目。');
187
286
  lines.push('');
188
287
 
189
- if (available.length) {
190
- lines.push('**本阶段可读的对标文件**(读哪些由你判断,读后不重读):\n');
191
- for (const a of available) {
288
+ if (phaseRefs.available.length) {
289
+ lines.push('**本阶段可读的对标文件**(阶段级地图,不等于每批都要全读):\n');
290
+ for (const a of phaseRefs.available) {
192
291
  const desc = DIRECTORY_DESCRIPTIONS[a.dir] || '写作技法分析';
193
292
  lines.push('- `' + a.path + '` —— ' + desc);
194
293
  }
195
294
  lines.push('');
196
295
  } else {
197
- lines.push('**本阶段没有可读的对标文件**(对标/wiki 下还没有总结文件):不要凭空编造"对标分析",照插件指导写。\n');
296
+ lines.push('**本阶段没有可读的对标文件**(对标/wiki 下尚无总结文件):不要凭空编造“对标分析”,照插件指导写。\n');
198
297
  }
298
+
299
+ if (batchRefs) {
300
+ if (batchRefs.available.length) {
301
+ lines.push('**本批优先参考**(插件已按本批产物筛过;摘要正文会注入其中最相关的前几篇):\n');
302
+ for (const a of batchRefs.available) {
303
+ const desc = DIRECTORY_DESCRIPTIONS[a.dir] || '写作技法分析';
304
+ lines.push('- `' + a.path + '` —— ' + desc);
305
+ }
306
+ lines.push('');
307
+ } else {
308
+ lines.push('**本批没有匹配到可读的对标文件**:按方法论与硬契约写,不要临时扩大到无关题材的样本。\n');
309
+ }
310
+ }
311
+
312
+ const missing = Array.from(new Set([...(phaseRefs.missing || []), ...((batchRefs && batchRefs.missing) || [])]));
199
313
  if (missing.length) {
200
- lines.push('**以下对标目录本阶段没有总结文件,不要引用**:' + missing.join('、'));
314
+ lines.push('**以下对标目录本阶段/本批没有总结文件,不要引用**:' + missing.join('、'));
201
315
  lines.push('');
202
316
  }
203
317
  return lines.join('\n');
204
318
  }
319
+
320
+ /** 摘要注入的体量上限(按阶段)。 */
321
+ const DIGEST_LIMITS = {
322
+ skeleton: { maxFiles: 6, maxEntriesPerFile: 5, maxDetailFiles: 2, maxDetailEntries: 2, maxChars: 8000 },
323
+ flesh: { maxFiles: 4, maxEntriesPerFile: 8, maxDetailFiles: 2, maxDetailEntries: 2, maxChars: 7000 },
324
+ rhythm: { maxFiles: 6, maxEntriesPerFile: 6, maxDetailFiles: 2, maxDetailEntries: 2, maxChars: 8000 },
325
+ writing: { maxFiles: 10, maxEntriesPerFile: 6, maxDetailFiles: 2, maxDetailEntries: 1, maxChars: 9000 }
326
+ };
327
+
328
+ /** 截断文本,保留前 n 个字符。 */
329
+ function truncate(text, n) {
330
+ const s = String(text || '').replace(/\s+/g, ' ').trim();
331
+ if (s.length <= n) return s;
332
+ return s.slice(0, n).replace(/[,。;、,.;:\s]+$/, '') + '…';
333
+ }
334
+
335
+ /**
336
+ * 解析一个总结文件里的条目。
337
+ *
338
+ * 只解析 `## 核心技法` / `## 核心设计原则` 下的 `###` 条目;
339
+ * `- 来源章节:` / `- 来源实体:` 是登记字段,不进入摘要正文。
340
+ *
341
+ * @param {string} text
342
+ * @returns {Array<{title:string, fields:Object<string,string>}>}
343
+ */
344
+ export function parseSummaryEntries(text) {
345
+ const source = String(text || '');
346
+ const sectionRe = /^##\s+(核心技法|核心设计原则)\s*$/m;
347
+ const m = sectionRe.exec(source);
348
+ if (!m) return [];
349
+
350
+ const rest = source.slice(m.index + m[0].length);
351
+ const nextH2 = /^##\s+/m.exec(rest);
352
+ const body = nextH2 ? rest.slice(0, nextH2.index) : rest;
353
+ const chunks = body.split(/^###\s+/m).slice(1);
354
+ const entries = [];
355
+
356
+ for (const chunk of chunks) {
357
+ const lines = chunk.split('\n');
358
+ const title = String(lines[0] || '').trim();
359
+ if (!title) continue;
360
+ const fields = {};
361
+ for (const line of lines.slice(1)) {
362
+ const fm = /^-\s*([^:]+):\s*(.*)$/.exec(line.trim());
363
+ if (!fm) continue;
364
+ const key = fm[1].trim();
365
+ if (key.startsWith('来源')) continue; // 登记字段不进摘要
366
+ fields[key] = fm[2].trim();
367
+ }
368
+ entries.push({ title, fields });
369
+ }
370
+ return entries;
371
+ }
372
+
373
+ /** 判断这个总结文件是实体页总结还是按章技法总结。 */
374
+ function isEntityEntries(entries) {
375
+ return entries.some((e) => Object.prototype.hasOwnProperty.call(e.fields, '为什么这么设计'));
376
+ }
377
+
378
+ /** 条目的一句话索引(优先取“为什么/核心要点”)。 */
379
+ function entryOneLine(entry, entity) {
380
+ const keys = entity
381
+ ? ['为什么这么设计', '设计好处', '迁移到骨架/血肉/节奏']
382
+ : ['核心要点', '应用方式', '使用场景'];
383
+ for (const key of keys) {
384
+ if (entry.fields[key]) return truncate(entry.fields[key], 100);
385
+ }
386
+ return '';
387
+ }
388
+
389
+ /** 从批次里提取用于匹配条目的关键词。 */
390
+ function batchTokens(batch) {
391
+ const tokens = [];
392
+ const add = (v) => {
393
+ const s = String(v || '').trim();
394
+ if (s && s.length >= 2 && !tokens.includes(s)) tokens.push(s);
395
+ };
396
+ if (batch) {
397
+ add(batch.type);
398
+ add(batch.groupKey);
399
+ for (const item of (batch.items || [])) {
400
+ add(item.name);
401
+ add(item.type);
402
+ add(item.category);
403
+ add(item.level);
404
+ add(item.scale);
405
+ }
406
+ }
407
+ return tokens;
408
+ }
409
+
410
+ /**
411
+ * 选本批最该展开的条目。
412
+ *
413
+ * 没有语义模型可用,所以用“批次类型 / 条目名 / 类型词”做轻量匹配;
414
+ * 匹配不到时按原顺序取前几条(摘要本身已经按条目顺序组织)。
415
+ */
416
+ function selectDetailEntries(entries, entity, batch, limit) {
417
+ const tokens = batchTokens(batch);
418
+ const scored = entries.map((entry, index) => {
419
+ const hay = entry.title + ' ' + Object.values(entry.fields).join(' ');
420
+ let score = 0;
421
+ for (const t of tokens) {
422
+ if (entry.title.includes(t)) score += 5;
423
+ else if (hay.includes(t)) score += 2;
424
+ }
425
+ return { entry, index, score };
426
+ });
427
+ scored.sort((a, b) => (b.score - a.score) || (a.index - b.index));
428
+ return scored.slice(0, Math.max(1, limit)).map((x) => x.entry);
429
+ }
430
+
431
+ /** 把一个条目展开成摘要正文。 */
432
+ function renderEntryDetail(entry, entity) {
433
+ const lines = ['#### ' + entry.title];
434
+ const keys = entity
435
+ ? ['为什么这么设计', '设计好处', '如何服务剧情/人物/主题', '风险与反例', '迁移到骨架/血肉/节奏']
436
+ : ['核心要点', '使用场景', '应用方式'];
437
+ for (const key of keys) {
438
+ if (entry.fields[key]) lines.push('- ' + key + ':' + entry.fields[key]);
439
+ }
440
+ return lines.join('\n');
441
+ }
442
+
443
+ /**
444
+ * 生成“本批对标参考摘要”,直接注入批次提示词。
445
+ *
446
+ * 这是“防止模型跳过参考文件”的核心手段:
447
+ * - 只给路径:模型可以声称读过但完全不看;
448
+ * - 注入摘要:本批相关原则已经在上下文里,模型不需要(也不能假装)去读;
449
+ * - 保留原文件路径:需要更细的条目时仍可回读,但本批不依赖它。
450
+ *
451
+ * @param {string} projectRoot
452
+ * @param {string} phase
453
+ * @param {Object} [batch] 批次对象(至少含 type;可选 items/groupKey)
454
+ * @param {Object} [opts] 覆盖默认上限:maxFiles/maxEntriesPerFile/maxDetailFiles/maxDetailEntries/maxChars
455
+ * @returns {string}
456
+ */
457
+ export function renderBenchmarkDigest(projectRoot, phase = 'skeleton', batch = null, opts = {}) {
458
+ const wikiDir = join(projectRoot, '对标', 'wiki');
459
+ if (!existsSync(wikiDir)) return '';
460
+
461
+ const dirs = batchDirs(phase, batch && batch.type);
462
+ const { available } = collectDirs(projectRoot, dirs);
463
+ const limits = Object.assign({}, DIGEST_LIMITS[phase] || DIGEST_LIMITS.skeleton, opts || {});
464
+
465
+ if (available.length === 0) {
466
+ return [
467
+ '## 本批对标参考(已注入摘要;决策前仍需核对原文件)',
468
+ '',
469
+ '(本批没有可用的对标总结文件。按 `' + ((PHASE_GUIDANCE[phase] || {}).guideDir || 'references/') + '/` 与硬契约写,不要编造“对标分析”。)'
470
+ ].join('\n');
471
+ }
472
+
473
+ const selected = available.slice(0, Math.max(1, limits.maxFiles));
474
+ const indexBlocks = [];
475
+ const detailBlocks = [];
476
+
477
+ selected.forEach((entry, fileIndex) => {
478
+ let text = '';
479
+ try {
480
+ text = readFileSync(join(projectRoot, entry.path), 'utf-8');
481
+ } catch {
482
+ return;
483
+ }
484
+ const entries = parseSummaryEntries(text);
485
+ if (entries.length === 0) return;
486
+ const entity = isEntityEntries(entries);
487
+ const visible = entries.slice(0, Math.max(1, limits.maxEntriesPerFile));
488
+
489
+ const indexLines = [];
490
+ indexLines.push('### `' + entry.path + '`');
491
+ indexLines.push('');
492
+ indexLines.push('**原则索引**');
493
+ for (const e of visible) {
494
+ const one = entryOneLine(e, entity);
495
+ indexLines.push('- ' + e.title + (one ? ':' + one : ''));
496
+ }
497
+ if (entries.length > visible.length) {
498
+ indexLines.push('- ……还有 ' + (entries.length - visible.length) + ' 条(需要时读原文件)');
499
+ }
500
+ indexLines.push('');
501
+ indexBlocks.push(indexLines.join('\n'));
502
+
503
+ if (fileIndex < limits.maxDetailFiles) {
504
+ const details = selectDetailEntries(entries, entity, batch, limits.maxDetailEntries);
505
+ if (details.length) {
506
+ const dLines = ['**本批重点**', ''];
507
+ for (const d of details) dLines.push(renderEntryDetail(d, entity), '');
508
+ detailBlocks.push('### `' + entry.path + '` 的重点条目\n\n' + dLines.join('\n'));
509
+ }
510
+ }
511
+ });
512
+
513
+ const header = [
514
+ '## 本批对标参考(已注入摘要;决策前仍需核对原文件)',
515
+ '',
516
+ '> 下面是插件按本批任务从 `对标/wiki/` 筛出的原则索引和重点条目。它们是**校准样本**,不是硬规则:',
517
+ '> 与模板、选题、上游产物冲突时,以硬契约和本项目设定为准;禁止照搬专名、情节、句子。',
518
+ '> 这些摘要不是可选背景:本批至少选 1 条与本批字段/场景直接相关的原则,落到具体设计上;没有适用条目就按契约写,不要硬套。',
519
+ '> 摘要只覆盖本批重点,不能替代原文;做本批决策前必须核对原文件里的对应条目,并把参考文件和条目写进 `设定/.reference-decisions.md`。',
520
+ ''
521
+ ].join('\n');
522
+
523
+ if (indexBlocks.length === 0) {
524
+ return header + '(选中的总结文件里没有可解析的条目;按硬契约写,不要编造“对标分析”。)';
525
+ }
526
+
527
+ let out = header;
528
+ for (const block of indexBlocks) {
529
+ if (out.length + block.length > limits.maxChars) break;
530
+ out += block + '\n';
531
+ }
532
+ for (const block of detailBlocks) {
533
+ if (out.length + block.length > limits.maxChars) break;
534
+ out += block + '\n';
535
+ }
536
+ if (out.length > limits.maxChars) {
537
+ out = out.slice(0, limits.maxChars).replace(/[,。;、,.;:\s]+$/, '') + '\n\n(摘要已按上下文预算截断,需要完整条目时读原文件。)';
538
+ }
539
+ return out.trim();
540
+ }