@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
@@ -0,0 +1,794 @@
1
+ /**
2
+ * v2/export-markdown.js —— 把阶段 JSON 导出成供另一个 LLM 阅读的 Markdown。
3
+ *
4
+ * 导出不是复制目录:读者应该看到章节、人物、关系、冲突等内容的
5
+ * Markdown 结构,而不是一堆和源目录完全相同的 JSON 文件。
6
+ */
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { asArray, readJson, stageDir, stageFile } from './core.js';
10
+ import { contractFor } from './contracts.js';
11
+
12
+ const STAGE_FILE_ORDER = {
13
+ '10-scale': ['scale.json'],
14
+ '20-macro': ['world.json', 'story.json', 'volumes.json', 'lines.json', 'endings.json', 'emotion.json', 'roles.json'],
15
+ '30-cast': ['characters.json', 'factions.json', 'locations.json', 'items.json', 'conflicts.json', 'foreshadows.json', 'climaxes.json', 'payoffs.json', 'relations.json', 'information.json'],
16
+ '40-rhythm': ['chapter_map.json', 'index.json'],
17
+ '50-writing': ['ledger.json'],
18
+ '60-deslop': [],
19
+ };
20
+
21
+ const KEY_LABELS = {
22
+ // 通用
23
+ schema_version: '格式版本', stage: '阶段', status: '状态', checked_at: '检查时间',
24
+ files: '文件', errors: '错误', warnings: '警告', id: '编号', name: '名称',
25
+ title: '标题', type: '类型', chapter_id: '章节编号', display_no: '章节序号',
26
+ chapter: '章节', chapters: '章节列表', content: '内容', description: '描述',
27
+ note: '备注', reason: '原因', value: '值', values: '值列表', label: '标签',
28
+ order: '顺序', path: '路径', method: '方法', function: '功能', question: '问题',
29
+ answer: '答案', goal: '目标', stakes: '赌注', theme: '主题', tone: '基调',
30
+ premise: '前提', promise: '承诺', recognition: '认知', measure: '度量',
31
+ limits: '限制', limit: '限制', cost: '代价', impact: '影响', scope: '范围',
32
+ rules: '规则', exceptions: '例外', layers: '层级', axes: '维度',
33
+ // 数量与公式
34
+ formula_version: '公式版本', genre_profile: '题材配置', capacity: '规模参数',
35
+ target_chapters: '目标章数', target_words: '目标总字数', avg_words_per_chapter: '平均每章字数',
36
+ base_words_per_chapter: '基准每章字数', chapter_scale: '章节字数换算系数',
37
+ structure: '结构', volume_count: '卷数', branch_count: '支线数', total_acts: '总幕数',
38
+ volumes: '卷列表', acts: '幕列表', act: '幕', act_id: '幕编号', act_index: '幕序号',
39
+ act_count: '幕数', act_chapter_sum: '幕章数合计', chapter_start: '起始章',
40
+ chapter_end: '结束章', chapter_sum: '章节数合计', ratio: '占比', quotas: '配额',
41
+ distribution: '分布', rhythm_budget: '节奏预算', hooks: '钩子', reveals: '揭示',
42
+ reversals: '反转', emotional_peaks: '情绪峰值', golden_three: '黄金三章',
43
+ hook_strength: '钩子强度', must_establish: '必须建立', min: '最低值', target: '目标值',
44
+ base_range: '基础区间', base_range_low: '基础区间下界', base_range_high: '基础区间上界',
45
+ adjusted_range: '调整后区间', range_low: '区间下界', range_high: '区间上界',
46
+ formula: '公式', field: '字段', scale_field: '数量字段', genre_multiplier: '题材倍率',
47
+ deviation_reason: '偏离理由', derivation_reasons: '推导理由', derivations: '推导结果',
48
+ chosen: '已选值', constraints: '约束', no_content_fields: '禁止内容字段',
49
+ climax_chapters: '高潮章', payoff_chapters: '爽点章', main_lines: '主线',
50
+ main_line: '主线', branches: '支线', branch: '支线', endings: '结局',
51
+ information: '信息差', information_gaps: '信息差',
52
+ // 宏观
53
+ world: '世界', world_rules: '世界规则', world_setting: '世界设定',
54
+ power_system: '力量体系', social_order: '社会秩序', taboos: '禁忌',
55
+ core_imagery: '核心意象', reader_promise: '读者承诺', anti_tropes: '反套路',
56
+ core_question: '核心问题', core_conflict: '核心冲突', turning_points: '转折点',
57
+ end_state: '结束状态', internal_conflict: '内部冲突', romance_constraint: '情感线约束',
58
+ roles: '角色功能位', role_id: '功能位编号', role_slot: '功能位', macro_goal: '宏观目标',
59
+ arc_question: '弧光问题', expected_strength: '预期强度', participants: '参与者',
60
+ emotional_arc: '情绪弧', emotion: '情绪', phase: '阶段', intensity: '强度',
61
+ peaks: '峰值', arc: '弧光', growth: '成长', direction: '方向', level: '层级',
62
+ required_foreshadow_recovery: '必须回收的伏笔', volume: '卷', volume_id: '卷编号',
63
+ core_goal: '核心目标', escalation_path: '升级路径', resolution_plan: '解决计划',
64
+ residual_impact: '残余影响', node_type: '节点类型', resolution: '收束',
65
+ // 实体
66
+ characters: '人物', character: '人物', protagonist: '主角', core_supporting: '核心配角',
67
+ volume_supporting: '卷级配角', core_antagonist: '核心反派', volume_antagonist: '卷级反派',
68
+ role_slot: '功能位', faction: '势力', description: '描述', flaw: '缺陷',
69
+ golden_finger: '金手指', information_state: '信息状态', known: '已知', unknown: '未知',
70
+ false_belief: '误信', catchphrase: '口头禅', rhythm: '节奏', traits: '特征',
71
+ exit: '退场', short_term: '短期目标', medium_term: '中期目标', long_term: '长期目标',
72
+ arc: '弧光', stages: '阶段', planned_change: '计划变化', question: '问题',
73
+ factions: '势力', faction_relations: '势力关系', faction_membership: '成员关系',
74
+ relations: '关系', character_relations: '人物关系', memberships: '成员关系',
75
+ from: '从', to: '到', kind: '关系类型', initial_state: '初始状态', trigger: '触发',
76
+ changes: '变化', position: '位置', resources: '资源', territory: '地盘',
77
+ internal_conflict: '内部冲突', narrative_function: '叙事功能', locations: '地点',
78
+ items: '道具', owner: '拥有者', category: '分类', conflicts: '冲突',
79
+ foreshadows: '伏笔', climaxes: '高潮', payoffs: '爽点', pleasures: '爽点',
80
+ clues: '线索', setup: '埋设', payoff_target: '回收目标', recovery_requirement: '回收要求',
81
+ red_herrings: '误导线索', payoff: '兑现', required_setup: '所需铺垫',
82
+ emotional_effect: '情绪效果', scale: '规模', info_name: '信息名称',
83
+ who_knows: '知道的人', who_does_not_know: '不知道的人', resolution_method: '揭示方式',
84
+ writing_ban: '写作禁令', setup: '铺垫', required_setup: '所需铺垫',
85
+ // 节奏 / 章节
86
+ chapter_map: '章节地图', chapters: '章节', why: '为什么', how: '怎么写',
87
+ main_line_nodes: '主线节点', branch_nodes: '支线节点', conflict: '冲突',
88
+ function_main: '主功能', function_aux: '辅助功能', turn: '转折', ending_hook: '章尾钩子',
89
+ foreshadow_actions: '伏笔动作', payoff_actions: '爽点动作', information_gaps: '信息差动作',
90
+ ending_actions: '结局动作', action: '动作', detail: '细节', state: '状态',
91
+ pov: '视角人物', cast: '出场人物', mentioned: '提及人物', time: '时间',
92
+ story_day: '故事日', clock: '时钟', duration_minutes: '持续分钟数',
93
+ word_target: '字数目标', hook: '钩子', strength: '强度',
94
+ // 细纲
95
+ chapter: '章节', number: '章号', entry_state: '进入状态', exit_state: '离开状态',
96
+ locations: '地点', sensory: '感官', role_in_chapter: '本章角色', entry: '进入',
97
+ exit: '离开', knowledge: '知识状态', scenes: '场景节拍', no: '序号',
98
+ goal: '目标', obstacle: '阻碍', outcome: '结果', emotion: '情绪',
99
+ information_gap: '信息差', foreshadow: '伏笔', payoff: '爽点',
100
+ must_write: '必须写', must_not_write: '绝对不能写', ending: '收束',
101
+ continuity: '连续性', from_prev: '承接上一章', to_next: '衔接下一章',
102
+ open_threads: '未闭合线索', coverage: '覆盖矩阵', hard_constraints: '硬约束',
103
+ soft_suggestions: '软建议', scene_split: '场景拆分', emotion_trend: '情绪趋势',
104
+ description_focus: '描写重点', pace: '节奏', dialogue_ratio: '对话比例',
105
+ information_state: '信息状态', knowledge_state: '知识状态',
106
+ // 台账 / 导出 / 知识时间线
107
+ ch: '章', knowledge_changes: '知识变化', gained: '获得', lost: '失去',
108
+ foreshadow_status: '伏笔状态', information_status: '信息差状态',
109
+ ending_hook: '章尾钩子', word_count: '字数',
110
+ subject_type: '对象类型', subject_id: '对象编号', change: '变化',
111
+ knowledge: '知道什么', coverage: '掌握程度', source_character: '来源人物',
112
+ source_event: '来源事件', scene: '场景', evidence: '依据',
113
+ initial_knowledge: '初始知识', knowledge_events: '知识事件',
114
+ from_state: '原状态', to_state: '新状态',
115
+ };
116
+
117
+ const TOKEN_LABELS = {
118
+ structure: '结构', branch: '支线', volume: '卷', count: '数量', sum: '合计',
119
+ character: '人物', characters: '人物', elements: '要素', supporting: '配角',
120
+ antagonist: '反派', protagonist: '主角', fixed: '固定规则', custom: '自定义公式',
121
+ conflict: '冲突', conflicts: '冲突', phase: '阶段', item: '道具', items: '道具',
122
+ location: '地点', locations: '地点', faction: '势力', factions: '势力',
123
+ foreshadow: '伏笔', foreshadows: '伏笔', climax: '高潮', climaxes: '高潮',
124
+ pleasure: '爽点', pleasures: '爽点', world: '世界', rule: '规则', rules: '规则',
125
+ counts: '数量', info: '信息', information: '信息', gaps: '差',
126
+ relations: '关系', relation: '关系', character_relations: '人物关系',
127
+ faction_relations: '势力关系', memberships: '成员关系', membership: '成员关系',
128
+ main: '主线', line: '线', lines: '线', nodes: '节点', node: '节点',
129
+ start: '开始', end: '结束', high: '上界', low: '下界', range: '区间',
130
+ base: '基础', adjusted: '调整后', target: '目标', value: '值', values: '值列表',
131
+ chapter: '章节', chapters: '章节', id: '编号', name: '名称', type: '类型',
132
+ role: '功能', slot: '位', goal: '目标', stakes: '赌注', theme: '主题',
133
+ tone: '基调', premise: '前提', promise: '承诺', recognition: '认知',
134
+ measure: '度量', limits: '限制', limit: '限制', cost: '代价', impact: '影响',
135
+ scope: '范围', layers: '层级', axes: '维度', description: '描述', content: '内容',
136
+ note: '备注', reason: '原因', value: '值', label: '标签', order: '顺序',
137
+ path: '路径', method: '方法', function: '功能', question: '问题',
138
+ answer: '答案', setup: '铺垫', clues: '线索', payoff: '兑现', target: '目标',
139
+ red: '误导', herrings: '线索', required: '所需', emotional: '情绪',
140
+ effect: '效果', info_name: '信息名称', who: '谁', knows: '知道',
141
+ does: '不', not: '不', know: '知道', resolution: '揭示', writing: '写作',
142
+ ban: '禁令', chapter_map: '章节地图', main_line: '主线', branch_nodes: '支线节点',
143
+ main_line_nodes: '主线节点', function_main: '主功能', function_aux: '辅助功能',
144
+ turn: '转折', ending_hook: '章尾钩子', foreshadow_actions: '伏笔动作',
145
+ payoff_actions: '爽点动作', information_gaps: '信息差', ending_actions: '结局动作',
146
+ action: '动作', detail: '细节', state: '状态', pov: '视角人物', cast: '出场人物',
147
+ mentioned: '提及人物', time: '时间', story_day: '故事日', clock: '时钟',
148
+ duration_minutes: '持续分钟数', word_target: '字数目标', hook: '钩子',
149
+ strength: '强度', entry_state: '进入状态', exit_state: '离开状态',
150
+ role_in_chapter: '本章角色', entry: '进入', exit: '离开', knowledge: '知识状态',
151
+ scenes: '场景节拍', no: '序号', goal: '目标', obstacle: '阻碍', outcome: '结果',
152
+ emotion: '情绪', information_gap: '信息差', foreshadow: '伏笔',
153
+ must_write: '必须写', must_not_write: '绝对不能写', ending: '收束',
154
+ continuity: '连续性', from_prev: '承接上一章', to_next: '衔接下一章',
155
+ open_threads: '未闭合线索', coverage: '覆盖矩阵', hard_constraints: '硬约束',
156
+ soft_suggestions: '软建议', scene_split: '场景拆分', emotion_trend: '情绪趋势',
157
+ description_focus: '描写重点', pace: '节奏', dialogue_ratio: '对话比例',
158
+ information_state: '信息状态', knowledge_state: '知识状态', ch: '章',
159
+ knowledge_changes: '知识变化', gained: '获得', lost: '失去',
160
+ foreshadow_status: '伏笔状态', information_status: '信息差状态', word_count: '字数',
161
+ subject_type: '对象类型', subject_id: '对象编号', change: '变化',
162
+ knowledge_events: '知识事件', initial_knowledge: '初始知识',
163
+ source_character: '来源人物', source_event: '来源事件', scene: '场景',
164
+ evidence: '依据', from_state: '原状态', to_state: '新状态',
165
+ };
166
+
167
+ const VALUE_LABELS = {
168
+ created: '已创建', progressed: '推进中', resolved: '已解决',
169
+ planted: '已埋设', recovered: '已回收', reinforced: '已强化',
170
+ partial: '部分知道', full: '完全知道', unknown: '不知道', false: '误信',
171
+ witnessed: '亲眼看到', told: '他人告知', inferred: '推断', document: '文书',
172
+ rumor: '传闻', experience: '亲身经历', none: '无', other: '其他',
173
+ book: '全书', volume: '卷', serial: '连载', core: '核心',
174
+ phase: '阶段', branch: '支线', small: '小', medium: '中', large: '大',
175
+ draft: '草稿', passed: '已通过', stale: '过期', archived: '已归档',
176
+ learns: '获知', partially_learns: '部分获知', confirms: '确认',
177
+ corrects: '纠正', forgets: '遗忘', misinforms: '误信',
178
+ information: '信息差', foreshadow: '伏笔',
179
+ 'ROLE-PROTAGONIST': '主角', 'ROLE-CORE-SUPPORT-01': '核心配角',
180
+ };
181
+
182
+ // 少量容易漏掉的业务键与枚举键,单独覆盖,避免被通用拆分规则拼成半英文。
183
+ Object.assign(KEY_LABELS, {
184
+ voice: '语言风格', exit: '退场', interweaves: '交织节点', turning: '转折点',
185
+ hard_limits: '硬性限制', core: '核心', large: '大', medium: '中', small: '小', character_arc: '人物弧光',
186
+ social: '社会', symbolic: '象征', aliases: '别名', benchmark: '对标',
187
+ book_name: '书名', created_at: '创建时间', channel_model: '频道模型',
188
+ charTotal: '人物总数', core_supporting_appearance_per_10: '核心配角每 10 章出场次数',
189
+ protagonist_appearance_per_10: '主角每 10 章出场次数', default_words_per_chapter: '默认每章字数',
190
+ distribution_weights: '分布权重', applies_to_field: '适用字段', apply: '应用',
191
+ default: '默认', profile: '题材配置', profiles: '题材配置列表',
192
+ character_elements: '人物要素', climax_elements: '高潮要素', conflict_elements: '冲突要素',
193
+ ending_elements: '结局要素', faction_elements: '势力要素', foreshadow_elements: '伏笔要素',
194
+ item_elements: '道具要素', location_elements: '地点要素', pleasure_elements: '爽点要素',
195
+ world_rule_counts: '世界规则数量', volume_ratio_sum: '卷占比合计',
196
+ volume_antagonist_count: '卷级反派数量', volume_supporting_count: '卷级配角数量',
197
+ large_climax_count: '大高潮数量', medium_climax_count: '中高潮数量', small_climax_count: '小高潮数量',
198
+ branch_conflict_count: '交织冲突数量', phase_conflict_count: '阶段冲突数量',
199
+ ending_count: '结局数量', faction_count: '势力数量', faction_relation_count: '势力关系数量',
200
+ large_foreshadow_count: '大伏笔数量', medium_foreshadow_count: '中伏笔数量', small_foreshadow_count: '小伏笔数量',
201
+ branch_item_count: '支线道具数量', core_item_count: '核心道具数量', symbolic_item_count: '象征道具数量',
202
+ key_location_count: '关键地点数量', large_pleasure_count: '大爽点数量', medium_pleasure_count: '中爽点数量',
203
+ small_pleasure_count: '小爽点数量', main_line_count: '主线数量',
204
+ large_climax: '大高潮', medium_climax: '中高潮', small_climax: '小高潮',
205
+ large_foreshadow: '大伏笔', medium_foreshadow: '中伏笔', small_foreshadow: '小伏笔',
206
+ core_setting: '核心设定', base_formula_words_per_chapter: '公式基准每章字数',
207
+ custom_formula_rule: '自定义公式规则', default_profile: '默认题材配置',
208
+ emotion_jump_warning: '情绪跳变警告', climax_gap_chapters: '高潮间隔章数',
209
+ flatline_chapters: '平流章数', expr_high: '表达式上界', expr_low: '表达式下界',
210
+ BC: '基础章数', CA: '当前幕章数', CC: '冲突数', CS: '章节规模', T: '总章数',
211
+ V: '卷数', P: '平台系数', n: '数量', Math: '数学函数',
212
+ channel: '频道', fallback: '回退配置', farming: '种田', first: '第一段',
213
+ genre: '题材', gt25: '大于 25 章', lt10: '小于 10 章', last: '最后一段',
214
+ middle: '中间段', missing: '缺失项', new_entities: '新增实体', notes: '备注',
215
+ one_line_pitch: '一句话卖点', opening_chapters: '开篇章数',
216
+ opening_hook_multiplier: '开篇钩子倍率', overrides: '覆盖规则',
217
+ payoff_gap_chapters: '爽点间隔章数', pleasure_density_multiplier: '爽点密度倍率',
218
+ positioning: '定位', preferred_chapters_per_volume: '每卷推荐章数',
219
+ profile_file: '题材配置文件', profile_path: '题材配置路径', profile_rule: '题材匹配规则',
220
+ quality: '质量', reader_value: '读者价值', references: '参考资料',
221
+ relationships: '关系', rounding: '取整规则', scale_field_rule: '数量字段规则',
222
+ selling_points: '卖点', shuangwen: '爽文', special_rules: '特殊规则',
223
+ structure_feature: '结构特征', suspense: '悬疑', version: '版本',
224
+ volume_length_multiplier: '卷长倍率', volume_supporting_retention: '卷级配角留存率',
225
+ hook_density_per_chapter: '每章钩子密度', formula_with_multiplier: '带倍率公式',
226
+ multiplier_category: '倍率分类', multipliers: '倍率',
227
+ small_foreshadow_recovery: '小伏笔回收', medium_foreshadow_recovery: '中伏笔回收',
228
+ large_foreshadow_recovery: '大伏笔回收',
229
+ });
230
+
231
+ function translateValue(value) {
232
+ if (typeof value !== 'string') return value;
233
+ return VALUE_LABELS[value] || value;
234
+ }
235
+
236
+ function translateKey(key) {
237
+ const raw = String(key);
238
+ if (KEY_LABELS[raw]) return KEY_LABELS[raw];
239
+ if (/^V\d{3}$/.test(raw)) return '第 ' + Number(raw.slice(1)) + ' 卷';
240
+ if (raw.includes('.')) {
241
+ return raw.split('.').map((part) => KEY_LABELS[part] || TOKEN_LABELS[part] || part).join(' · ');
242
+ }
243
+ const parts = raw.split('_').filter(Boolean);
244
+ if (parts.length > 1) {
245
+ const translated = parts.map((part) => TOKEN_LABELS[part]);
246
+ if (translated.every(Boolean)) return translated.join('');
247
+ return raw;
248
+ }
249
+ return TOKEN_LABELS[raw] || raw;
250
+ }
251
+
252
+ const STAGE_LABEL = {
253
+ '10-scale': '数量表',
254
+ '20-macro': '宏观设定',
255
+ '30-cast': '人物与实体',
256
+ '40-rhythm': '章节地图与细纲',
257
+ '50-writing': '正文与台账',
258
+ '60-deslop': '去味报告',
259
+ };
260
+
261
+ function heading(level, text) {
262
+ return '#'.repeat(Math.max(1, Math.min(6, level))) + ' ' + text;
263
+ }
264
+
265
+ function isObject(value) {
266
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
267
+ }
268
+
269
+ function isScalar(value) {
270
+ return value === null || value === undefined || ['string', 'number', 'boolean'].includes(typeof value);
271
+ }
272
+
273
+ function isScalarArray(value) {
274
+ return Array.isArray(value) && value.every(isScalar);
275
+ }
276
+
277
+ function cleanObject(value) {
278
+ if (!isObject(value)) return value;
279
+ return Object.fromEntries(Object.entries(value).filter(([key]) => key !== '_meta'));
280
+ }
281
+
282
+ function oneLine(value) {
283
+ if (value === null || value === undefined || value === '') return '—';
284
+ if (typeof value === 'boolean') return value ? '是' : '否';
285
+ if (Array.isArray(value)) return value.map(oneLine).join('、') || '—';
286
+ if (isObject(value)) return JSON.stringify(value);
287
+ const text = String(value).replace(/\r?\n/g, ' ').trim();
288
+ return text ? translateValue(text) : '—';
289
+ }
290
+
291
+ function labelOf(item, index = 0) {
292
+ if (!isObject(item)) return '第 ' + (index + 1) + ' 项';
293
+ const id = item.id || item.chapter_id;
294
+ const name = item.name || item.title || item.label || item.info_name;
295
+ if (id && name) return id + ' ' + name;
296
+ if (id) return String(id);
297
+ if (name) return String(name);
298
+ const chap = item.chapter_id || (item.display_no !== undefined ? '第 ' + item.display_no + ' 章' : '');
299
+ return chap || ('第 ' + (index + 1) + ' 项');
300
+ }
301
+
302
+ function fieldLine(key, value, depth = 0) {
303
+ const pad = ' '.repeat(depth);
304
+ if (isScalar(value) || isScalarArray(value)) return [pad + '- **' + translateKey(key) + '**:' + oneLine(value)];
305
+ return [];
306
+ }
307
+
308
+ /** 实体内部字段:标量用列表,数组/对象继续缩进。 */
309
+ function renderFields(obj, depth, lines) {
310
+ const data = cleanObject(obj) || {};
311
+ for (const [key, value] of Object.entries(data)) {
312
+ if (value === undefined) continue;
313
+ if (isScalar(value) || isScalarArray(value)) {
314
+ lines.push(...fieldLine(key, value, depth));
315
+ continue;
316
+ }
317
+ const pad = ' '.repeat(depth);
318
+ if (Array.isArray(value)) {
319
+ lines.push(pad + '- **' + translateKey(key) + '**:');
320
+ if (!value.length) {
321
+ lines.push(' '.repeat(depth + 1) + '- (空)');
322
+ continue;
323
+ }
324
+ value.forEach((item, index) => {
325
+ if (isObject(item)) {
326
+ lines.push(' '.repeat(depth + 1) + '- **' + labelOf(item, index) + '**');
327
+ renderFields(item, depth + 2, lines);
328
+ } else {
329
+ lines.push(' '.repeat(depth + 1) + '- ' + oneLine(item));
330
+ }
331
+ });
332
+ continue;
333
+ }
334
+ if (isObject(value)) {
335
+ lines.push(pad + '- **' + translateKey(key) + '**:');
336
+ renderFields(value, depth + 1, lines);
337
+ }
338
+ }
339
+ }
340
+
341
+ function renderSection(key, value, level, lines) {
342
+ lines.push(heading(level, translateKey(key)));
343
+ lines.push('');
344
+ if (Array.isArray(value)) {
345
+ if (!value.length) {
346
+ lines.push('(空)');
347
+ lines.push('');
348
+ return;
349
+ }
350
+ value.forEach((item, index) => {
351
+ if (isObject(item)) {
352
+ lines.push(heading(level + 1, labelOf(item, index)));
353
+ lines.push('');
354
+ renderFields(item, 0, lines);
355
+ lines.push('');
356
+ } else {
357
+ lines.push('- ' + oneLine(item));
358
+ }
359
+ });
360
+ return;
361
+ }
362
+ if (isObject(value)) {
363
+ const entries = Object.entries(cleanObject(value) || {});
364
+ const objectMap = entries.length > 0 && entries.every(([, child]) => isObject(child));
365
+ if (objectMap) {
366
+ for (const [childKey, child] of entries) {
367
+ const childLabel = child.label || translateKey(childKey);
368
+ lines.push(heading(level + 1, childLabel));
369
+ lines.push('');
370
+ lines.push('- **键名**:`' + childKey + '`');
371
+ renderFields(child, 0, lines);
372
+ lines.push('');
373
+ }
374
+ } else {
375
+ renderFields(value, 0, lines);
376
+ lines.push('');
377
+ }
378
+ return;
379
+ }
380
+ lines.push(oneLine(value));
381
+ lines.push('');
382
+ }
383
+
384
+ function renderTopLevel(data, level, lines) {
385
+ if (Array.isArray(data)) {
386
+ renderSection('条目', data, level, lines);
387
+ return;
388
+ }
389
+ const obj = cleanObject(data) || {};
390
+ const scalars = [];
391
+ const composites = [];
392
+ for (const [key, value] of Object.entries(obj)) {
393
+ if (value === undefined) continue;
394
+ if (isScalar(value) || isScalarArray(value)) scalars.push([key, value]);
395
+ else composites.push([key, value]);
396
+ }
397
+ if (scalars.length) {
398
+ for (const [key, value] of scalars) lines.push(...fieldLine(key, value));
399
+ lines.push('');
400
+ }
401
+ for (const [key, value] of composites) renderSection(key, value, level, lines);
402
+ }
403
+
404
+ function indented(lines, prefix = ' ') {
405
+ return lines.map((line) => prefix + line);
406
+ }
407
+
408
+ function actionLines(actions) {
409
+ const out = [];
410
+ for (const action of asArray(actions)) {
411
+ if (!isObject(action)) {
412
+ out.push('- ' + oneLine(action));
413
+ continue;
414
+ }
415
+ out.push('- **' + (action.id || '条目') + '**');
416
+ if (action.action !== undefined) out.push(' - ' + translateKey('action') + ':' + translateValue(action.action));
417
+ if (action.state !== undefined) out.push(' - ' + translateKey('state') + ':' + translateValue(action.state));
418
+ if (action.detail !== undefined) out.push(' - ' + translateKey('detail') + ':' + oneLine(action.detail));
419
+ const rest = { ...action };
420
+ delete rest.id;
421
+ delete rest.action;
422
+ delete rest.state;
423
+ delete rest.detail;
424
+ if (Object.keys(rest).length) renderFields(rest, 1, out);
425
+ }
426
+ return out;
427
+ }
428
+
429
+ function knowledgeEventLines(event) {
430
+ const out = [];
431
+ if (!isObject(event)) return ['- ' + oneLine(event)];
432
+ out.push('- **' + [event.character, event.subject_id].filter(Boolean).join(' / ') + '**');
433
+ if (event.subject_type) out.push(' - 对象类型:' + oneLine(event.subject_type));
434
+ if (event.change) out.push(' - 变化:' + oneLine(event.change));
435
+ if (event.coverage) out.push(' - 掌握程度:' + oneLine(event.coverage));
436
+ if (event.knowledge) out.push(' - 知道什么:' + oneLine(event.knowledge));
437
+ if (event.how) out.push(' - 获知方式:' + oneLine(event.how));
438
+ if (event.source_character) out.push(' - 来源人物:' + oneLine(event.source_character));
439
+ if (event.scene) out.push(' - 场景:' + oneLine(event.scene));
440
+ if (event.evidence) out.push(' - 依据:' + oneLine(event.evidence));
441
+ return out;
442
+ }
443
+
444
+ function renderChapter(chapter, lines, level = 3) {
445
+ const c = chapter || {};
446
+ lines.push(heading(level, '第' + (c.display_no || '?') + '章 ' + (c.title || '')));
447
+ lines.push('');
448
+ lines.push('- **章节编号**:' + (c.chapter_id || '—'));
449
+ lines.push('- **章节序号**:' + (c.display_no ?? '—'));
450
+ lines.push('- **卷**:' + (c.volume || '—'));
451
+ lines.push('- **幕**:' + (c.act || '—'));
452
+ lines.push('- **视角人物**:' + (c.pov || '—'));
453
+ lines.push('- **出场人物**:' + oneLine(c.cast));
454
+ if (asArray(c.mentioned).length) lines.push('- **提及人物**:' + oneLine(c.mentioned));
455
+ lines.push('- **地点**:' + oneLine(c.locations));
456
+ if (c.time) {
457
+ if (c.time.story_day !== undefined) lines.push('- **故事日**:' + c.time.story_day);
458
+ if (c.time.clock) lines.push('- **时钟**:' + c.time.clock);
459
+ if (c.time.duration_minutes !== undefined) lines.push('- **持续分钟数**:' + c.time.duration_minutes);
460
+ }
461
+ const why = c.why || {};
462
+ lines.push('- **为什么**:');
463
+ lines.push(' - 主线节点:' + oneLine(why.main_line_nodes));
464
+ lines.push(' - 支线节点:' + oneLine(why.branch_nodes));
465
+ lines.push(' - 冲突:' + oneLine(why.conflict));
466
+ lines.push(' - 主题:' + oneLine(why.theme));
467
+ lines.push(' - 赌注:' + oneLine(why.stakes));
468
+ const how = c.how || {};
469
+ lines.push('- **怎么写**:');
470
+ lines.push(' - 主功能:' + oneLine(how.function_main));
471
+ if (how.function_aux) lines.push(' - 辅功能:' + oneLine(how.function_aux));
472
+ lines.push(' - 转折:' + oneLine(how.turn));
473
+ lines.push(' - 章尾钩子:' + oneLine(how.ending_hook));
474
+ const actionBlocks = [
475
+ ['伏笔动作', c.foreshadow_actions],
476
+ ['爽点动作', c.payoff_actions],
477
+ ['信息差', c.information_gaps],
478
+ ['结局落点', c.ending_actions],
479
+ ];
480
+ for (const [label, actions] of actionBlocks) {
481
+ if (!asArray(actions).length) continue;
482
+ lines.push('- **' + label + '**:');
483
+ lines.push(...indented(actionLines(actions)));
484
+ }
485
+ if (asArray(c.knowledge_events).length) {
486
+ lines.push('- **知识事件**:');
487
+ lines.push(...indented(asArray(c.knowledge_events).flatMap(knowledgeEventLines)));
488
+ }
489
+ if (c.emotion) {
490
+ if (c.emotion.phase !== undefined) lines.push('- **情绪阶段**:' + oneLine(c.emotion.phase));
491
+ if (c.emotion.value !== undefined) lines.push('- **情绪强度**:' + c.emotion.value);
492
+ }
493
+ if (c.hook) {
494
+ if (c.hook.type !== undefined) lines.push('- **钩子类型**:' + oneLine(c.hook.type));
495
+ if (c.hook.strength !== undefined) lines.push('- **钩子强度**:' + translateValue(c.hook.strength));
496
+ }
497
+ if (c.word_target !== undefined) lines.push('- **字数目标**:' + c.word_target);
498
+ lines.push('');
499
+ }
500
+
501
+ function renderChapterMap(data, lines, level = 3) {
502
+ const chapters = asArray(data && data.chapters);
503
+ lines.push(heading(level, '章节地图'));
504
+ lines.push('');
505
+ lines.push('> 共 ' + chapters.length + ' 章。每章按 5W 展开,供后续细纲/正文读取。');
506
+ lines.push('');
507
+ for (const chapter of chapters) renderChapter(chapter, lines, level + 1);
508
+ }
509
+
510
+ function renderChapterIndex(data, lines, level = 3) {
511
+ const chapters = asArray(data && data.chapters);
512
+ lines.push(heading(level, '章节索引'));
513
+ lines.push('');
514
+ lines.push('| 章 | 章节编号 | 标题 | 卷 | 幕 | 细纲路径 | 状态 |');
515
+ lines.push('|---|---|---|---|---|---|---|');
516
+ for (const c of chapters) {
517
+ lines.push('| ' + [c.display_no, c.chapter_id, c.title, c.volume, c.act, c.path, c.status]
518
+ .map((v) => oneLine(v).replace(/\|/g, '\\|')).join(' | ') + ' |');
519
+ }
520
+ lines.push('');
521
+ }
522
+
523
+ function renderScene(scene, index, lines) {
524
+ lines.push(heading(4, '场景 ' + (index + 1)));
525
+ lines.push('');
526
+ renderFields(scene, 0, lines);
527
+ lines.push('');
528
+ }
529
+
530
+ function renderOutline(data, lines, level = 3) {
531
+ const chapter = (data && data.chapter) || data || {};
532
+ lines.push(heading(level, '第' + (chapter.number || chapter.display_no || '?') + '章 ' + (chapter.title || '')));
533
+ lines.push('');
534
+ lines.push('- **章节编号**:' + (chapter.chapter_id || '—'));
535
+ lines.push('- **卷**:' + (chapter.volume || '—'));
536
+ lines.push('- **幕**:' + (chapter.act || '—'));
537
+ lines.push('- **字数目标**:' + (chapter.word_target ?? '—'));
538
+ if (chapter.pov) {
539
+ const pov = chapter.pov;
540
+ lines.push('- **视角人物**:' + (pov.id || pov.name || '—'));
541
+ if (pov.entry_state) lines.push('- **视角进入状态**:' + oneLine(pov.entry_state));
542
+ if (pov.exit_state) lines.push('- **视角离开状态**:' + oneLine(pov.exit_state));
543
+ }
544
+ if (chapter.time) {
545
+ if (isObject(chapter.time)) {
546
+ if (chapter.time.story_day !== undefined) lines.push('- **故事日**:' + chapter.time.story_day);
547
+ if (chapter.time.clock) lines.push('- **时钟**:' + chapter.time.clock);
548
+ if (chapter.time.duration_minutes !== undefined) lines.push('- **持续分钟数**:' + chapter.time.duration_minutes);
549
+ if (chapter.time.chronology) lines.push('- **叙事顺序**:' + oneLine(chapter.time.chronology));
550
+ } else {
551
+ lines.push('- **时间**:' + oneLine(chapter.time));
552
+ }
553
+ }
554
+ if (chapter.locations) {
555
+ const locs = Array.isArray(chapter.locations) ? chapter.locations : [chapter.locations];
556
+ lines.push('- **地点**:');
557
+ for (const loc of locs) {
558
+ if (isObject(loc)) lines.push(' - ' + [loc.id, loc.name, loc.state ? '(' + oneLine(loc.state) + ')' : ''].filter(Boolean).join(' '));
559
+ else lines.push(' - ' + oneLine(loc));
560
+ }
561
+ }
562
+ if (asArray(chapter.cast).length) {
563
+ lines.push('- **出场人物**:');
564
+ for (const person of chapter.cast) {
565
+ if (isObject(person)) {
566
+ lines.push(' - **' + labelOf(person) + '**');
567
+ if (person.role_in_chapter) lines.push(' - 本章角色:' + oneLine(person.role_in_chapter));
568
+ if (person.entry) lines.push(' - 进入:' + oneLine(person.entry));
569
+ if (person.exit) lines.push(' - 离开:' + oneLine(person.exit));
570
+ if (person.knowledge) {
571
+ lines.push(' - 知识状态:');
572
+ for (const item of asArray(person.knowledge)) {
573
+ if (isObject(item)) {
574
+ lines.push(' - **' + [item.subject_id, item.character].filter(Boolean).join(' / ') + '**');
575
+ for (const [k, v] of Object.entries(item)) {
576
+ if (['subject_id', 'character'].includes(k)) continue;
577
+ lines.push(' - ' + translateKey(k) + ':' + oneLine(v));
578
+ }
579
+ } else {
580
+ lines.push(' - ' + oneLine(item));
581
+ }
582
+ }
583
+ }
584
+ } else {
585
+ lines.push(' - ' + oneLine(person));
586
+ }
587
+ }
588
+ }
589
+ if (chapter.why) {
590
+ lines.push('- **为什么**:');
591
+ renderFields(chapter.why, 1, lines);
592
+ }
593
+ if (asArray(chapter.knowledge_events).length) {
594
+ lines.push('- **知识事件**:');
595
+ lines.push(...indented(asArray(chapter.knowledge_events).flatMap(knowledgeEventLines)));
596
+ }
597
+ const how = chapter.how || {};
598
+ const scenes = asArray(how.scenes);
599
+ if (scenes.length) {
600
+ lines.push('- **场景节拍**:');
601
+ lines.push('');
602
+ for (let i = 0; i < scenes.length; i += 1) renderScene(scenes[i], i, lines);
603
+ }
604
+ if (how.ending) {
605
+ lines.push('- **本章收束**:');
606
+ if (isObject(how.ending)) {
607
+ if (how.ending.type) lines.push(' - 类型:' + oneLine(how.ending.type));
608
+ if (how.ending.text) lines.push(' - 文本:' + oneLine(how.ending.text));
609
+ if (how.ending.hook) lines.push(' - 钩子:' + oneLine(how.ending.hook));
610
+ } else {
611
+ lines.push(' - ' + oneLine(how.ending));
612
+ }
613
+ }
614
+ if (chapter.continuity) {
615
+ lines.push('- **连续性**:');
616
+ renderFields(chapter.continuity, 1, lines);
617
+ }
618
+ if (chapter.coverage) {
619
+ lines.push('- **覆盖矩阵**:');
620
+ renderFields(chapter.coverage, 1, lines);
621
+ }
622
+ if (chapter.hard_constraints) {
623
+ lines.push('- **硬约束**:');
624
+ renderFields(chapter.hard_constraints, 1, lines);
625
+ }
626
+ if (chapter.soft_suggestions) {
627
+ lines.push('- **软建议**:');
628
+ renderFields(chapter.soft_suggestions, 1, lines);
629
+ }
630
+ lines.push('');
631
+ }
632
+
633
+ function bookNameOf(projectRoot) {
634
+ const input = readJson(path.join(projectRoot, '设定', '00-baseline', 'input.json'), null)
635
+ || readJson(stageFile(projectRoot, '00-baseline', 'input.json'), null);
636
+ return (input && input.book_name) || '';
637
+ }
638
+
639
+ function stageFiles(projectRoot, stageId) {
640
+ const dir = stageDir(projectRoot, stageId);
641
+ const root = fs.readdirSync(dir, { withFileTypes: true })
642
+ .filter((e) => e.isFile() && e.name.endsWith('.json') && e.name !== 'bundle.json' && !e.name.startsWith('.'))
643
+ .map((e) => e.name);
644
+ const preferred = STAGE_FILE_ORDER[stageId] || [];
645
+ const ordered = preferred.filter((name) => root.includes(name));
646
+ const rest = root.filter((name) => !ordered.includes(name)).sort();
647
+ const files = [...ordered, ...rest];
648
+ const extra = [];
649
+ if (stageId === '40-rhythm') {
650
+ const outlineDir = path.join(dir, 'outlines');
651
+ let names = [];
652
+ try { names = fs.readdirSync(outlineDir).filter((n) => n.endsWith('.json')).sort(); } catch { names = []; }
653
+ for (const name of names) extra.push({ rel: 'outlines/' + name, json: true });
654
+ }
655
+ if (stageId === '50-writing') {
656
+ const chapterDir = path.join(dir, 'chapters');
657
+ let names = [];
658
+ try { names = fs.readdirSync(chapterDir).filter((n) => n.endsWith('.md')).sort(); } catch { names = []; }
659
+ for (const name of names) extra.push({ rel: 'chapters/' + name, markdown: true });
660
+ }
661
+ if (stageId === '60-deslop') {
662
+ for (const sub of ['reports', 'revisions']) {
663
+ const base = path.join(dir, sub);
664
+ let names = [];
665
+ try { names = fs.readdirSync(base).sort(); } catch { names = []; }
666
+ for (const name of names) {
667
+ if (name.endsWith('.md')) extra.push({ rel: sub + '/' + name, markdown: true });
668
+ else if (name.endsWith('.json')) extra.push({ rel: sub + '/' + name, json: true });
669
+ }
670
+ }
671
+ }
672
+ return {
673
+ dir,
674
+ files: [...files.map((name) => ({ rel: name, json: true })), ...extra],
675
+ };
676
+ }
677
+
678
+ /**
679
+ * 生成阶段汇总 Markdown。
680
+ * @returns {{markdown:string, files:string[], stageId:string, title:string}}
681
+ */
682
+ function sourceFileLabel(files) {
683
+ const grouped = [];
684
+ const top = [];
685
+ let outlines = 0;
686
+ let chapters = 0;
687
+ for (const file of files) {
688
+ if (file.rel.startsWith('outlines/')) outlines += 1;
689
+ else if (file.rel.startsWith('chapters/')) chapters += 1;
690
+ else top.push(file.rel);
691
+ }
692
+ for (const name of top) grouped.push('`' + name + '`');
693
+ if (outlines) grouped.push('`outlines/`(' + outlines + ' 个细纲)');
694
+ if (chapters) grouped.push('`chapters/`(' + chapters + ' 篇正文)');
695
+ return grouped.join('、');
696
+ }
697
+
698
+ export function renderStageMarkdown(projectRoot, stageId) {
699
+ const contract = contractFor(stageId);
700
+ const label = STAGE_LABEL[stageId] || (contract && contract.title) || stageId;
701
+ const { files } = stageFiles(projectRoot, stageId);
702
+ const lines = [];
703
+ lines.push('# ' + stageId + ' 阶段导出:' + label);
704
+ lines.push('');
705
+ lines.push('> 本文件由 fancy-story-builder 2.0 从阶段 JSON 生成,供其它 LLM 直接阅读;不是源目录 JSON 文件的复制。');
706
+ lines.push('');
707
+ const book = bookNameOf(projectRoot);
708
+ if (book) lines.push('- **项目**:' + book);
709
+ lines.push('- **阶段**:`' + stageId + '`(' + label + ')');
710
+ lines.push('- **导出时间**:' + new Date().toISOString());
711
+ lines.push('- **源文件**:' + sourceFileLabel(files));
712
+ if (contract && contract.purpose) lines.push('- **阶段目的**:' + contract.purpose);
713
+ if (contract && asArray(contract.readOrder).length) lines.push('- **建议读法**:' + contract.readOrder.join(' → '));
714
+ lines.push('');
715
+ lines.push('---');
716
+ lines.push('');
717
+
718
+ let rendered = 0;
719
+ for (const file of files) {
720
+ const full = path.join(stageDir(projectRoot, stageId), file.rel);
721
+ if (file.json) {
722
+ const data = readJson(full, null);
723
+ if (data === null) continue;
724
+ lines.push('## `' + file.rel + '`');
725
+ lines.push('');
726
+ if (file.rel === 'chapter_map.json') renderChapterMap(data, lines, 3);
727
+ else if (file.rel === 'index.json') renderChapterIndex(data, lines, 3);
728
+ else if (file.rel.startsWith('outlines/')) renderOutline(data, lines, 3);
729
+ else renderTopLevel(data, 3, lines);
730
+ lines.push('---');
731
+ lines.push('');
732
+ rendered += 1;
733
+ } else if (file.markdown) {
734
+ const text = fs.readFileSync(full, 'utf-8');
735
+ lines.push('## `' + file.rel + '`');
736
+ lines.push('');
737
+ lines.push(text.trim());
738
+ lines.push('');
739
+ lines.push('---');
740
+ lines.push('');
741
+ rendered += 1;
742
+ }
743
+ }
744
+ if (!rendered) {
745
+ lines.push('(当前阶段没有可导出的 JSON 或正文文件。)');
746
+ lines.push('');
747
+ }
748
+ return {
749
+ markdown: lines.join('\n'),
750
+ files: files.map((f) => f.rel),
751
+ stageId,
752
+ title: label,
753
+ };
754
+ }
755
+
756
+ /** 只导出单章细纲,附带章节地图中该章的条目。 */
757
+ export function renderOutlineMarkdown(projectRoot, chapterId) {
758
+ const dir = stageDir(projectRoot, '40-rhythm');
759
+ const outlineFile = path.join(dir, 'outlines', chapterId + '.json');
760
+ const outline = readJson(outlineFile, null);
761
+ if (!outline) return null;
762
+ const map = readJson(path.join(dir, 'chapter_map.json'), null);
763
+ const chapter = asArray(map && map.chapters).find((c) => String(c.chapter_id) === String(chapterId));
764
+ const lines = [];
765
+ lines.push('# 40-rhythm 单章细纲导出:' + chapterId);
766
+ lines.push('');
767
+ lines.push('> 本文件由 fancy-story-builder 2.0 从章节地图和单章细纲 JSON 生成,供写作模型直接阅读。');
768
+ lines.push('');
769
+ const book = bookNameOf(projectRoot);
770
+ if (book) lines.push('- **项目**:' + book);
771
+ lines.push('- **章节**:' + chapterId);
772
+ lines.push('- **导出时间**:' + new Date().toISOString());
773
+ lines.push('');
774
+ lines.push('---');
775
+ lines.push('');
776
+ if (chapter) {
777
+ lines.push('## 章节地图条目');
778
+ lines.push('');
779
+ renderChapter(chapter, lines, 3);
780
+ lines.push('---');
781
+ lines.push('');
782
+ }
783
+ lines.push('## 细纲');
784
+ lines.push('');
785
+ renderOutline(outline, lines, 3);
786
+ return {
787
+ markdown: lines.join('\n'),
788
+ chapterId,
789
+ chapter,
790
+ outline,
791
+ files: ['chapter_map.json', 'outlines/' + chapterId + '.json'],
792
+ };
793
+ }
794
+