@waterwx/dsh-novel-forge 2.1.2-alpha → 2.1.2

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 (253) hide show
  1. package/README.md +278 -368
  2. package/lib/client.js +19568 -21381
  3. package/lib/index.js +4850 -4002
  4. package/lib/types/action-guard.d.ts +61 -0
  5. package/lib/types/ai-scan.d.ts +66 -12
  6. package/lib/types/assets.d.ts +7 -9
  7. package/lib/types/assistant.d.ts +25 -0
  8. package/lib/types/author-assets.d.ts +1 -3
  9. package/lib/types/bookshelf.d.ts +13 -3
  10. package/lib/types/client/api.d.ts +49 -29
  11. package/lib/types/client/chip-prefs.d.ts +36 -0
  12. package/lib/types/client/docx.d.ts +2 -2
  13. package/lib/types/client/llmLive.d.ts +12 -14
  14. package/lib/types/client/locales.d.ts +12 -67
  15. package/lib/types/client/mount.d.ts +0 -2
  16. package/lib/types/client/panel/AdaptModeView.d.ts +1 -1
  17. package/lib/types/client/panel/AiTag.d.ts +1 -0
  18. package/lib/types/client/panel/AssetsTab.d.ts +1 -1
  19. package/lib/types/client/panel/AssistantTab.d.ts +4 -1
  20. package/lib/types/client/panel/AuthorAssetsView.d.ts +1 -1
  21. package/lib/types/client/panel/AuthorHome.d.ts +12 -12
  22. package/lib/types/client/panel/CmdPalette.d.ts +12 -0
  23. package/lib/types/client/panel/CreateBookView.d.ts +1 -1
  24. package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +1 -1
  25. package/lib/types/client/panel/ImportModal.d.ts +1 -1
  26. package/lib/types/client/panel/LiveFeedLog.d.ts +8 -0
  27. package/lib/types/client/panel/ModelManager.d.ts +2 -2
  28. package/lib/types/client/panel/NovelPanel.d.ts +7 -2
  29. package/lib/types/client/panel/PageHeader.d.ts +18 -0
  30. package/lib/types/client/panel/ProgressConsole.d.ts +1 -1
  31. package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
  32. package/lib/types/client/panel/ReaderView.d.ts +1 -1
  33. package/lib/types/client/panel/RunPanel.d.ts +1 -1
  34. package/lib/types/client/panel/SettingsView.d.ts +13 -10
  35. package/lib/types/client/panel/ShelfView.d.ts +11 -2
  36. package/lib/types/client/panel/SubPage.d.ts +27 -0
  37. package/lib/types/client/panel/TensionView.d.ts +18 -0
  38. package/lib/types/client/panel/TimelineView.d.ts +18 -0
  39. package/lib/types/client/panel/WorldTab.d.ts +1 -1
  40. package/lib/types/client/panel/helpers.d.ts +5 -3
  41. package/lib/types/client/panel/market-radar-state.d.ts +56 -0
  42. package/lib/types/client/panel/outline-markdown.d.ts +112 -0
  43. package/lib/types/client/panel/status-badge.d.ts +13 -0
  44. package/lib/types/client/panel/storage-parse.d.ts +46 -0
  45. package/lib/types/client/panel/toast.d.ts +2 -0
  46. package/lib/types/client/panel/usePersistentState.d.ts +17 -0
  47. package/lib/types/client/panel/views.d.ts +21 -35
  48. package/lib/types/client/run-status.d.ts +21 -0
  49. package/lib/types/client/sidebar-entry.d.ts +0 -2
  50. package/lib/types/digest-parse.d.ts +29 -0
  51. package/lib/types/docx-core.d.ts +16 -0
  52. package/lib/types/docx.d.ts +2 -3
  53. package/lib/types/engine.d.ts +122 -103
  54. package/lib/types/home.d.ts +11 -0
  55. package/lib/types/index.d.ts +21 -13
  56. package/lib/types/llm-live.d.ts +62 -36
  57. package/lib/types/llm-retry.d.ts +38 -0
  58. package/lib/types/market-radar-scan.d.ts +0 -1
  59. package/lib/types/market-radar-sources.d.ts +1 -1
  60. package/lib/types/migrate-output.d.ts +25 -0
  61. package/lib/types/model-capability.d.ts +119 -0
  62. package/lib/types/model-request.d.ts +36 -0
  63. package/lib/types/output-budget.d.ts +86 -0
  64. package/lib/types/plotline-align.d.ts +35 -0
  65. package/lib/types/prompt-slots.d.ts +57 -0
  66. package/lib/types/prompting.d.ts +2 -27
  67. package/lib/types/protocol.d.ts +479 -595
  68. package/lib/types/revision.d.ts +76 -0
  69. package/lib/types/routes.d.ts +9 -3
  70. package/lib/types/run-selection.d.ts +51 -0
  71. package/lib/types/run.d.ts +31 -0
  72. package/lib/types/settings-persist.d.ts +59 -0
  73. package/lib/types/snapshots.d.ts +46 -0
  74. package/lib/types/stage-contract.d.ts +58 -0
  75. package/lib/types/stream-fallback.d.ts +60 -0
  76. package/lib/types/strip-headings.d.ts +33 -0
  77. package/lib/types/tension.d.ts +53 -0
  78. package/lib/types/timeline.d.ts +37 -0
  79. package/package.json +41 -28
  80. package/assets/styles/arcane-thick-3d.webp +0 -0
  81. package/assets/styles/chinese-ink.webp +0 -0
  82. package/assets/styles/clay-stopmotion-3d.webp +0 -0
  83. package/assets/styles/dark-ghibli.webp +0 -0
  84. package/assets/styles/dark-gothic-3d.webp +0 -0
  85. package/assets/styles/film-photo.webp +0 -0
  86. package/assets/styles/guofeng-heavy.webp +0 -0
  87. package/assets/styles/guoman-2d-gufeng.webp +0 -0
  88. package/assets/styles/guoman-3d-tianyuan.webp +0 -0
  89. package/assets/styles/guoman-3d-xuanhuan.webp +0 -0
  90. package/assets/styles/horror-game-3d.webp +0 -0
  91. package/assets/styles/hyperreal-3d.webp +0 -0
  92. package/assets/styles/japan-cel-dark.webp +0 -0
  93. package/assets/styles/korean-webtoon.webp +0 -0
  94. package/assets/styles/low-cyberpunk-3d.webp +0 -0
  95. package/assets/styles/nextgen-cg.webp +0 -0
  96. package/assets/styles/noir-film.webp +0 -0
  97. package/assets/styles/openworld-survival.webp +0 -0
  98. package/assets/styles/paper-cut-3d.webp +0 -0
  99. package/assets/styles/pixar-adult-3d.webp +0 -0
  100. package/assets/styles/pop-marvel-2d.webp +0 -0
  101. package/assets/styles/sandiao-qban.webp +0 -0
  102. package/assets/styles/shinkai-anime.webp +0 -0
  103. package/assets/styles/tactical-stealth.webp +0 -0
  104. package/assets/styles/urban-lightreal-3d.webp +0 -0
  105. package/assets/styles/vaporwave-3d.webp +0 -0
  106. package/assets/styles/vhs90.webp +0 -0
  107. package/lib/types/client/panel/BookshelfBar.d.ts +0 -11
  108. package/lib/types/client/panel/FlowGuide.d.ts +0 -22
  109. package/lib/types/client/panel/MangaRoleLibrary.d.ts +0 -20
  110. package/lib/types/client/panel/MangaWorkspace.d.ts +0 -12
  111. package/lib/types/client/panel/ModelPicker.d.ts +0 -11
  112. package/lib/types/client/panel/ProgressHomeView.d.ts +0 -5
  113. package/lib/types/client/panel/PropLibrary.d.ts +0 -8
  114. package/lib/types/client/panel/SceneLibrary.d.ts +0 -16
  115. package/lib/types/client/panel/StoryboardTab.d.ts +0 -19
  116. package/lib/types/client/panel/StyleCard.d.ts +0 -8
  117. package/lib/types/client/panel/StyleGallery.d.ts +0 -1
  118. package/lib/types/comic-presets.d.ts +0 -12
  119. package/lib/types/manga-genre-rules.d.ts +0 -22
  120. package/lib/types/prompt-utils.d.ts +0 -35
  121. package/lib/types/review-policy.d.ts +0 -97
  122. package/lib/types/shot-language.d.ts +0 -46
  123. package/lib/types/src/ai-scan.d.ts +0 -36
  124. package/lib/types/src/assets.d.ts +0 -37
  125. package/lib/types/src/assistant.d.ts +0 -45
  126. package/lib/types/src/author-assets.d.ts +0 -25
  127. package/lib/types/src/bookshelf.d.ts +0 -42
  128. package/lib/types/src/client/api.d.ts +0 -234
  129. package/lib/types/src/client/docx.d.ts +0 -15
  130. package/lib/types/src/client/index.d.ts +0 -14
  131. package/lib/types/src/client/locales.d.ts +0 -212
  132. package/lib/types/src/client/mount.d.ts +0 -9
  133. package/lib/types/src/client/panel/AdaptModeView.d.ts +0 -5
  134. package/lib/types/src/client/panel/AssetsTab.d.ts +0 -12
  135. package/lib/types/src/client/panel/AssistantTab.d.ts +0 -7
  136. package/lib/types/src/client/panel/AuthorAssetsView.d.ts +0 -4
  137. package/lib/types/src/client/panel/AuthorHome.d.ts +0 -32
  138. package/lib/types/src/client/panel/BookshelfBar.d.ts +0 -11
  139. package/lib/types/src/client/panel/CreateBookView.d.ts +0 -9
  140. package/lib/types/src/client/panel/FlowGuide.d.ts +0 -22
  141. package/lib/types/src/client/panel/GlobalAssetLibraryView.d.ts +0 -4
  142. package/lib/types/src/client/panel/ImportModal.d.ts +0 -7
  143. package/lib/types/src/client/panel/MangaRoleLibrary.d.ts +0 -20
  144. package/lib/types/src/client/panel/MangaWorkspace.d.ts +0 -12
  145. package/lib/types/src/client/panel/ModelManager.d.ts +0 -14
  146. package/lib/types/src/client/panel/NovelPanel.d.ts +0 -13
  147. package/lib/types/src/client/panel/ProgressConsole.d.ts +0 -17
  148. package/lib/types/src/client/panel/ProgressHomeView.d.ts +0 -5
  149. package/lib/types/src/client/panel/PropLibrary.d.ts +0 -8
  150. package/lib/types/src/client/panel/ReaderView.d.ts +0 -8
  151. package/lib/types/src/client/panel/ReasoningSection.d.ts +0 -10
  152. package/lib/types/src/client/panel/RunPanel.d.ts +0 -5
  153. package/lib/types/src/client/panel/SceneLibrary.d.ts +0 -16
  154. package/lib/types/src/client/panel/SettingsView.d.ts +0 -16
  155. package/lib/types/src/client/panel/ShelfView.d.ts +0 -15
  156. package/lib/types/src/client/panel/StoryboardTab.d.ts +0 -19
  157. package/lib/types/src/client/panel/StyleCard.d.ts +0 -8
  158. package/lib/types/src/client/panel/StyleGallery.d.ts +0 -1
  159. package/lib/types/src/client/panel/WorldTab.d.ts +0 -10
  160. package/lib/types/src/client/panel/controller.d.ts +0 -19
  161. package/lib/types/src/client/panel/helpers.d.ts +0 -18
  162. package/lib/types/src/client/panel/views.d.ts +0 -77
  163. package/lib/types/src/client/sidebar-entry.d.ts +0 -13
  164. package/lib/types/src/client/text.d.ts +0 -9
  165. package/lib/types/src/comic-presets.d.ts +0 -12
  166. package/lib/types/src/docx.d.ts +0 -19
  167. package/lib/types/src/engine.d.ts +0 -402
  168. package/lib/types/src/index.d.ts +0 -79
  169. package/lib/types/src/manga-genre-rules.d.ts +0 -22
  170. package/lib/types/src/novel-context.d.ts +0 -80
  171. package/lib/types/src/prompt-utils.d.ts +0 -35
  172. package/lib/types/src/protocol.d.ts +0 -2176
  173. package/lib/types/src/review-policy.d.ts +0 -97
  174. package/lib/types/src/routes.d.ts +0 -29
  175. package/lib/types/src/run.d.ts +0 -32
  176. package/lib/types/src/shot-language.d.ts +0 -46
  177. package/lib/types/src/story-beat-language.d.ts +0 -35
  178. package/lib/types/src/style-library.d.ts +0 -33
  179. package/lib/types/story-beat-language.d.ts +0 -35
  180. package/lib/types/style-library.d.ts +0 -33
  181. package/src/ai-scan.ts +0 -179
  182. package/src/assets.ts +0 -1774
  183. package/src/assistant.ts +0 -915
  184. package/src/author-assets.ts +0 -147
  185. package/src/bookshelf.ts +0 -167
  186. package/src/client/api.ts +0 -716
  187. package/src/client/css-modules.d.ts +0 -8
  188. package/src/client/docx.ts +0 -69
  189. package/src/client/index.ts +0 -34
  190. package/src/client/llmLive.ts +0 -101
  191. package/src/client/locales.ts +0 -418
  192. package/src/client/mount.tsx +0 -97
  193. package/src/client/mount.tsx._new +0 -97
  194. package/src/client/mount.tsx._nf +0 -97
  195. package/src/client/mount.tsx._tmp +0 -97
  196. package/src/client/panel/AdaptModeView.tsx +0 -535
  197. package/src/client/panel/AssetsTab.tsx +0 -458
  198. package/src/client/panel/AssistantTab.tsx +0 -339
  199. package/src/client/panel/AuthorAssetsView.tsx +0 -208
  200. package/src/client/panel/AuthorHome.tsx +0 -234
  201. package/src/client/panel/BookAnalysisView.tsx +0 -60
  202. package/src/client/panel/BookshelfBar.tsx +0 -170
  203. package/src/client/panel/CreateBookView.tsx +0 -371
  204. package/src/client/panel/DirectorView.tsx +0 -200
  205. package/src/client/panel/FlowGuide.tsx +0 -84
  206. package/src/client/panel/GlobalAssetLibraryView.tsx +0 -172
  207. package/src/client/panel/IdeaInspirationView.tsx +0 -71
  208. package/src/client/panel/ImportModal.tsx +0 -303
  209. package/src/client/panel/KnowledgeBaseView.tsx +0 -73
  210. package/src/client/panel/LiveFeedLog.tsx +0 -76
  211. package/src/client/panel/MangaRoleLibrary.tsx +0 -1002
  212. package/src/client/panel/MangaWorkspace.tsx +0 -715
  213. package/src/client/panel/MarketRadarView.tsx +0 -374
  214. package/src/client/panel/ModelManager.tsx +0 -205
  215. package/src/client/panel/NovelPanel.tsx +0 -4721
  216. package/src/client/panel/ProgressConsole.tsx +0 -63
  217. package/src/client/panel/ProgressHomeView.tsx +0 -87
  218. package/src/client/panel/PropLibrary.tsx +0 -175
  219. package/src/client/panel/ReaderView.tsx +0 -306
  220. package/src/client/panel/ReasoningSection.tsx +0 -31
  221. package/src/client/panel/RunPanel.tsx +0 -204
  222. package/src/client/panel/SceneLibrary.tsx +0 -453
  223. package/src/client/panel/SettingsView.tsx +0 -289
  224. package/src/client/panel/ShelfView.tsx +0 -242
  225. package/src/client/panel/StoryboardTab.tsx +0 -472
  226. package/src/client/panel/StyleCard.tsx +0 -90
  227. package/src/client/panel/StyleGallery.tsx +0 -58
  228. package/src/client/panel/WorldTab.tsx +0 -202
  229. package/src/client/panel/controller.ts +0 -45
  230. package/src/client/panel/helpers.ts +0 -67
  231. package/src/client/panel/panel.module.css +0 -4359
  232. package/src/client/panel/views.tsx +0 -314
  233. package/src/client/sidebar-entry.ts +0 -122
  234. package/src/client/text.ts +0 -28
  235. package/src/comic-presets.ts +0 -67
  236. package/src/docx.ts +0 -83
  237. package/src/engine.ts +0 -5636
  238. package/src/global-assets.ts +0 -59
  239. package/src/index.ts +0 -346
  240. package/src/llm-live.ts +0 -68
  241. package/src/manga-genre-rules.ts +0 -87
  242. package/src/market-radar-scan.ts +0 -41
  243. package/src/market-radar-sources.ts +0 -173
  244. package/src/novel-context.ts +0 -287
  245. package/src/prompt-utils.ts +0 -73
  246. package/src/prompting.ts +0 -159
  247. package/src/protocol.ts +0 -2565
  248. package/src/review-policy.ts +0 -161
  249. package/src/routes.ts +0 -3939
  250. package/src/run.ts +0 -338
  251. package/src/shot-language.ts +0 -181
  252. package/src/story-beat-language.ts +0 -130
  253. package/src/style-library.ts +0 -84
package/src/run.ts DELETED
@@ -1,338 +0,0 @@
1
- /**
2
- * 生产单(Production Run):批量章节生产的标准执行器。
3
- * 职责:计划补足 → 逐章生成(完整质量门)→ 被拒分级处理(豁免/修订+验证/待人工)→ 断点续跑。
4
- * 与路由层解耦:只依赖 engine 导出函数与磁盘状态,自身不碰 HTTP。
5
- * 串行纪律:单例执行器 + working 锁;每章重新从磁盘加载,写前合并易变字段。
6
- */
7
- import { existsSync, mkdirSync, readFileSync, copyFileSync, writeFileSync } from 'node:fs'
8
- import { join } from 'node:path'
9
- import type { Context } from '@deepseek-ai/cordis'
10
- import type { ChapterPlan, NovelConfig, ProjectState, ReviewReport, RunState } from './protocol.ts'
11
- import {
12
- loadProject,
13
- saveProject,
14
- mergeVolatileFromDisk,
15
- planChapters,
16
- generateChapterStream,
17
- summarizeAndExtractFacts,
18
- markForeshadowPlanted,
19
- reviewChapter,
20
- reviewChapterText,
21
- authorReviewChapter,
22
- autoLinkPlotlines,
23
- readChapterFile,
24
- rewriteChapterStream,
25
- chapterFileName,
26
- } from './engine.ts'
27
- import { loadBookshelf } from './bookshelf.ts'
28
-
29
- /** 生产单 checkpoint 文件名(放在书目录下)。 */
30
- function runStateFile(outputDir: string): string {
31
- return join(outputDir, 'run-state.json')
32
- }
33
-
34
- export class ProductionRunner {
35
- private state: RunState | null = null
36
- private working = false
37
- private pauseRequested = false
38
- private stopRequested = false
39
- /** 生产单绑定目录(start 时快照;所有读写固定用它,防止运行中切书导致写错目录)。 */
40
- private bookDir: string | null = null
41
-
42
- constructor(private deps: { ctx: Context; getConfig: () => NovelConfig }) {}
43
-
44
- /** 当前生产单状态(内存优先;web 重启后从磁盘恢复)。 */
45
- status(): RunState | null {
46
- if (this.state !== null) return this.state
47
- // 重启恢复:先找当前激活书目录,再扫书架所有书目录(防切书后找不到)。
48
- const candidates: string[] = [this.deps.getConfig().outputDir]
49
- const bookshelf = loadBookshelf()
50
- for (const b of bookshelf.books) if (!candidates.includes(b.outputDir)) candidates.push(b.outputDir)
51
- for (const outputDir of candidates) {
52
- const file = runStateFile(outputDir)
53
- if (!existsSync(file)) continue
54
- try {
55
- const raw = readFileSync(file, 'utf8')
56
- const parsed = JSON.parse(raw) as RunState
57
- if (parsed?.runId === undefined) continue
58
- // 重启恢复:若之前 running,则视为可续跑(loop 未在跑)。
59
- if (parsed.status === 'running') parsed.status = 'paused'
60
- this.state = parsed
61
- this.bookDir = outputDir
62
- return this.state
63
- } catch { /* 该目录 run-state 损坏则跳过 */ }
64
- }
65
- return null
66
- }
67
-
68
- private persist(): void {
69
- if (this.state === null) return
70
- const outputDir = this.bookDir ?? this.deps.getConfig().outputDir
71
- mkdirSync(outputDir, { recursive: true })
72
- writeFileSync(runStateFile(outputDir), JSON.stringify(this.state, null, 2), 'utf8')
73
- }
74
-
75
- private log(text: string): void {
76
- if (this.state === null) return
77
- this.state.log.push({ at: new Date().toISOString(), text })
78
- if (this.state.log.length > 300) this.state.log = this.state.log.slice(-300)
79
- this.state.updatedAt = new Date().toISOString()
80
- }
81
-
82
- /** 启动/续跑生产单:startNo..endNo 区间,endNo 超出计划时先自动补计划。 */
83
- async start(startNo: number, endNo: number, runDir?: string): Promise<RunState> {
84
- const config = this.deps.getConfig()
85
- if (this.working) throw new Error('生产单正在运行中,请先暂停或停止')
86
- const outputDir = runDir ?? config.outputDir
87
- let project = loadProject(outputDir)
88
- if (project === undefined) throw new Error('输出目录中没有项目')
89
-
90
- // 计划补足:目标区间超出已有计划 → 续写规划追加。
91
- if (endNo > project.chapters.length) {
92
- const need = endNo - project.chapters.length
93
- this.log(`计划不足,追加 ${need} 章计划…`)
94
- const chapters = await planChapters(this.deps.ctx, config, project, need, undefined, outputDir)
95
- mergeVolatileFromDisk(outputDir, project)
96
- project.chapters.push(...chapters)
97
- project.updatedAt = new Date().toISOString()
98
- saveProject(outputDir, project)
99
- this.log(`计划已追加,全书 ${project.chapters.length} 章`)
100
- }
101
-
102
- this.state = {
103
- runId: `run-${Date.now().toString(36)}`,
104
- startNo,
105
- endNo,
106
- status: 'running',
107
- currentNo: startNo,
108
- stats: { generated: 0, revised: 0, exempted: 0, regenerated: 0, error: 0 },
109
- pendingManual: [],
110
- log: [],
111
- startedAt: new Date().toISOString(),
112
- updatedAt: new Date().toISOString(),
113
- }
114
- this.pauseRequested = false
115
- this.stopRequested = false
116
- // 绑定生产单目录(快照):运行期间切书不影响本单的读写目标。
117
- this.bookDir = outputDir
118
- this.working = true
119
- this.persist()
120
- void this.loop()
121
- return this.state
122
- }
123
-
124
- pause(): void { this.pauseRequested = true }
125
- resume(): void {
126
- if (this.state === null || this.state.status !== 'paused') return
127
- this.state.status = 'running'
128
- this.working = true
129
- this.pauseRequested = false
130
- this.stopRequested = false
131
- this.persist()
132
- void this.loop()
133
- }
134
- stop(): void { this.stopRequested = true }
135
-
136
- /** 主循环:从 currentNo 扫描到 endNo,逐章处理;支持暂停/停止。 */
137
- private async loop(): Promise<void> {
138
- // 全部读写固定使用生产单绑定目录(bookDir),绝不跟随当前激活书。
139
- const config = this.deps.getConfig()
140
- const outputDir = this.bookDir ?? config.outputDir
141
- try {
142
- while (this.state !== null) {
143
- if (this.stopRequested) {
144
- this.stopRequested = false
145
- this.state.status = 'stopped'
146
- this.log('生产单已停止')
147
- this.persist()
148
- break
149
- }
150
- if (this.pauseRequested) {
151
- this.pauseRequested = false
152
- this.state.status = 'paused'
153
- this.log('生产单已暂停(随时可继续)')
154
- this.persist()
155
- break
156
- }
157
- const project = loadProject(outputDir)
158
- if (project === undefined) {
159
- this.state.status = 'error'
160
- this.state.error = '项目丢失'
161
- this.persist()
162
- break
163
- }
164
- // 找下一个需要处理的章(approved 快进)。
165
- let next: ChapterPlan | undefined
166
- for (let no = this.state.currentNo; no <= this.state.endNo; no++) {
167
- const ch = project.chapters.find(c => c.no === no)
168
- if (ch === undefined) continue
169
- if (ch.status === 'approved') { this.state.currentNo = no; continue }
170
- next = ch
171
- break
172
- }
173
- if (next === undefined) {
174
- this.state.status = 'done'
175
- this.log(`生产单完成:${this.state.startNo}-${this.state.endNo} 章处理完毕`)
176
- this.persist()
177
- break
178
- }
179
- this.state.currentNo = next.no
180
- await this.processChapter(project, next)
181
- this.persist()
182
- }
183
- } catch (error) {
184
- if (this.state !== null) {
185
- this.state.status = 'error'
186
- this.state.error = (error as Error).message
187
- this.log(`生产单异常:${(error as Error).message}`)
188
- this.persist()
189
- }
190
- } finally {
191
- this.working = false
192
- }
193
- }
194
-
195
- private async processChapter(project: ProjectState, chapter: ChapterPlan): Promise<void> {
196
- if (chapter.status === 'pending' || chapter.status === 'error' || chapter.status === 'written') {
197
- await this.produce(project, chapter)
198
- } else if (chapter.status === 'rejected') {
199
- await this.handleRejected(project, chapter)
200
- } else if (chapter.status === 'generating') {
201
- // 中断残留:复位重生成。
202
- chapter.status = 'pending'
203
- await this.produce(project, chapter)
204
- }
205
- }
206
-
207
- /** 完整质量门:生成 → 摘要+事实 → 伏笔标记 → 审稿 → 作者复盘。 */
208
- private async produce(project: ProjectState, chapter: ChapterPlan): Promise<void> {
209
- const { ctx, getConfig } = this.deps
210
- const config = getConfig()
211
- const outputDir = this.bookDir ?? config.outputDir
212
- const no = chapter.no
213
- const wasError = chapter.status === 'error'
214
- chapter.status = 'generating'
215
- chapter.error = undefined
216
- chapter.generatingAt = new Date().toISOString()
217
- mergeVolatileFromDisk(outputDir, project)
218
- saveProject(outputDir, project)
219
- this.log(`${wasError ? '重新生成' : '生成'} 第${no}章《${chapter.title}》…(模型 ${config.generateModel || config.model})`)
220
- try {
221
- for await (const step of generateChapterStream(ctx, config, project, outputDir, no)) { /* drain */ }
222
- try { await summarizeAndExtractFacts(ctx, config, project, outputDir, no) } catch (e) { console.warn('[dsh-novel-forge] run summary/facts:', (e as Error).message) }
223
- try { markForeshadowPlanted(project, outputDir, no) } catch (e) { console.warn('[dsh-novel-forge] run foreshadow:', (e as Error).message) }
224
- if (config.autoReview ?? true) {
225
- const report = await reviewChapter(ctx, config, project, outputDir, no)
226
- if (this.state !== null) this.state.stats[wasError ? 'regenerated' : 'generated']++
227
- this.log(`第${no}章 审稿 ${report.score}分 ${report.passed ? '✅ 通过' : '⚠️ 被拒'}(模型 ${config.reviewModel || config.model})`)
228
- } else {
229
- chapter.status = 'approved'
230
- mergeVolatileFromDisk(outputDir, project)
231
- saveProject(outputDir, project)
232
- }
233
- if (config.autoAuthorReview ?? true) {
234
- try {
235
- const body = readChapterFile(outputDir, chapter)
236
- if (body !== undefined) {
237
- let prevTail = ''
238
- if (no > 1) {
239
- const prev = project.chapters.find(c => c.no === no - 1)
240
- if (prev !== undefined) prevTail = (readChapterFile(outputDir, prev) ?? '').replace(/^#.*$/m, '').trim().slice(-600)
241
- }
242
- const review = await authorReviewChapter(ctx, config, project, no, body, prevTail)
243
- chapter.authorReview = review
244
- if (review.advancedLines !== undefined) autoLinkPlotlines(project, no, review.advancedLines)
245
- // 结果回灌:把本章状态变化/新线索写进编年录事实库,供后续计划与整本控制用。
246
- const backfillFacts: string[] = [...(review.stateChanges ?? []), ...(review.clues ?? [])]
247
- if (backfillFacts.length > 0) {
248
- project.facts ??= []
249
- for (const text of backfillFacts) project.facts.push({ chapterNo: no, text })
250
- }
251
- mergeVolatileFromDisk(outputDir, project)
252
- saveProject(outputDir, project)
253
- }
254
- } catch (e) { console.warn('[dsh-novel-forge] run author review:', (e as Error).message) }
255
- }
256
- } catch (error) {
257
- chapter.status = 'error'
258
- chapter.error = (error as Error).message
259
- mergeVolatileFromDisk(outputDir, project)
260
- saveProject(outputDir, project)
261
- if (this.state !== null) this.state.stats.error++
262
- this.log(`第${no}章 失败:${(error as Error).message}`)
263
- }
264
- }
265
-
266
- /** 被拒分级处理:无 high 豁免;有 high 按意见修订(最多 2 轮)+ 验证模式;仍不过 → 待人工。 */
267
- private async handleRejected(project: ProjectState, chapter: ChapterPlan): Promise<void> {
268
- const { ctx, getConfig } = this.deps
269
- const config = getConfig()
270
- const outputDir = this.bookDir ?? config.outputDir
271
- const no = chapter.no
272
- const report = chapter.review
273
- const highs = (report?.issues ?? []).filter(i => i.severity === 'high')
274
- if (highs.length === 0) {
275
- chapter.status = 'approved'
276
- project.updatedAt = new Date().toISOString()
277
- saveProject(outputDir, project)
278
- if (this.state !== null) this.state.stats.exempted++
279
- this.log(`第${no}章 豁免通过(无 high)`)
280
- return
281
- }
282
- this.log(`第${no}章 修订(${highs.length} 个 high)…`)
283
- for (let round = 1; round <= 2; round++) {
284
- const instr = '按审稿意见修订(优先处理):\n' + highs.map(h => `[${h.severity}] ${h.item}${h.suggestion !== '' ? `\n建议:${h.suggestion}` : ''}`).join('\n\n')
285
- try {
286
- for await (const step of rewriteChapterStream(ctx, config, project, outputDir, no, instr, undefined)) { /* drain */ }
287
- } catch (error) {
288
- this.log(`第${no}章 第${round}轮修订出错:${(error as Error).message}`)
289
- continue
290
- }
291
- // rewrite 已写盘(pendingDraft + saveProject),重载拿最新草稿。
292
- const fresh = loadProject(outputDir)
293
- const freshCh = fresh?.chapters.find(c => c.no === no)
294
- const draft = freshCh?.pendingDraft
295
- if (draft === undefined || draft.length < 50) {
296
- this.log(`第${no}章 第${round}轮草稿缺失,重试`)
297
- continue
298
- }
299
- const verify = await reviewChapterText(ctx, config, fresh!, draft, chapter.review)
300
- const highs2 = (verify.issues ?? []).filter(i => i.severity === 'high')
301
- if (verify.passed || highs2.length === 0) {
302
- // 应用草稿(与 draft/apply 路由同逻辑):备份原稿 → 落盘 → 定状态。
303
- this.applyDraft(fresh!, freshCh!, draft, verify)
304
- if (this.state !== null) this.state.stats.revised++
305
- this.log(`第${no}章 第${round}轮修订通过(${verify.score}分)`)
306
- return
307
- }
308
- this.log(`第${no}章 第${round}轮仍不过(${highs2.length} high)`)
309
- }
310
- if (this.state !== null) this.state.pendingManual.push(no)
311
- this.log(`第${no}章 ⚠️ 两轮修订仍不过 → 保留草稿待人工`)
312
- }
313
-
314
- private applyDraft(project: ProjectState, chapter: ChapterPlan, draft: string, report: ReviewReport): void {
315
- const config = this.deps.getConfig()
316
- const outputDir = this.bookDir ?? config.outputDir
317
- const fileName = chapterFileName(chapter)
318
- mkdirSync(outputDir, { recursive: true })
319
- const targetPath = join(outputDir, fileName)
320
- if (existsSync(targetPath)) {
321
- copyFileSync(targetPath, join(outputDir, `${fileName.replace(/\.md$/, '')}.bak.md`))
322
- }
323
- writeFileSync(targetPath, `# 第${chapter.no}章 ${chapter.title}\n\n${draft}\n`, 'utf8')
324
- chapter.pendingDraft = undefined
325
- chapter.chars = draft.length
326
- chapter.file = fileName
327
- if (typeof report.score === 'number') {
328
- chapter.review = report
329
- chapter.status = report.passed === true ? 'approved' : 'rejected'
330
- } else {
331
- chapter.status = 'written'
332
- chapter.review = undefined
333
- }
334
- chapter.error = undefined
335
- project.updatedAt = new Date().toISOString()
336
- saveProject(outputDir, project)
337
- }
338
- }
@@ -1,181 +0,0 @@
1
- /**
2
- * 镜头语言词库(分镜规范化基准)。
3
- * 分镜表的 shot/camera/composition/light 字段统一从这里取值(英文 id),
4
- * 生成提示词时用 zh 中文词块;适配英文平台时用 en。
5
- * 目的:让"景别/运镜/构图/光效"有标准词表,避免 LLM 自由发挥导致漂移。
6
- */
7
-
8
- /** 景别。 */
9
- export type ShotSizeId =
10
- | 'extreme_wide' | 'wide' | 'full' | 'medium'
11
- | 'medium_close' | 'close' | 'extreme_close' | 'big_extreme_close'
12
-
13
- /** 运镜/机位。 */
14
- export type CameraMoveId =
15
- | 'static' | 'dolly_in' | 'dolly_out' | 'pan_left' | 'pan_right'
16
- | 'track_left' | 'track_right' | 'follow' | 'pedestal_up' | 'pedestal_down'
17
- | 'orbit' | 'handheld' | 'low_angle' | 'high_angle' | 'over_shoulder'
18
-
19
- /** 构图。 */
20
- export type CompositionId =
21
- | 'rule_of_thirds' | 'center' | 'leading_line' | 'foreground'
22
- | 'low' | 'overhead' | 'symmetry'
23
-
24
- /** 光效。 */
25
- export type LightingId =
26
- | 'front' | 'side' | 'back' | 'top' | 'rembrandt'
27
- | 'neon' | 'hard' | 'soft' | 'mood' | 'contrast'
28
-
29
- export interface ShotLangEntry<T extends string> {
30
- id: T
31
- /** 中文(提示词/展示用)。 */
32
- zh: string
33
- /** 英文(英文平台适配用)。 */
34
- en: string
35
- /** 一句话说明(下拉/卡片)。 */
36
- hint: string
37
- }
38
-
39
- export const SHOT_SIZES: readonly ShotLangEntry<ShotSizeId>[] = [
40
- { id: 'extreme_wide', zh: '大远景', en: 'extreme wide shot', hint: '环境为主,人物很小' },
41
- { id: 'wide', zh: '远景', en: 'wide shot', hint: '人在景中,交代环境' },
42
- { id: 'full', zh: '全景', en: 'full shot', hint: '全身』人物整体' },
43
- { id: 'medium', zh: '中景', en: 'medium shot', hint: '膝部以上,叙事主力' },
44
- { id: 'medium_close', zh: '中近景', en: 'medium close-up', hint: '胸以上,表情+动作' },
45
- { id: 'close', zh: '近景', en: 'close-up', hint: '肩以上,突出表情' },
46
- { id: 'extreme_close', zh: '特写', en: 'extreme close-up', hint: '脸/物细节' },
47
- { id: 'big_extreme_close', zh: '大特写', en: 'big close-up', hint: '局部(眼/手/标志物)' },
48
- ]
49
-
50
- export const CAMERA_MOVES: readonly ShotLangEntry<CameraMoveId>[] = [
51
- { id: 'static', zh: '固定机位', en: 'static camera', hint: '机位不动' },
52
- { id: 'dolly_in', zh: '推近', en: 'dolly in', hint: '镜头向主体逼近' },
53
- { id: 'dolly_out', zh: '拉远', en: 'dolly out', hint: '镜头远离主体' },
54
- { id: 'pan_left', zh: '左摇', en: 'pan left', hint: '机位不动,镜头左转' },
55
- { id: 'pan_right', zh: '右摇', en: 'pan right', hint: '机位不动,镜头右转' },
56
- { id: 'track_left', zh: '左横移', en: 'track left', hint: '机位随主体左移' },
57
- { id: 'track_right', zh: '右横移', en: 'track right', hint: '机位随主体右移' },
58
- { id: 'follow', zh: '跟随', en: 'follow shot', hint: '镜头跟着主体运动' },
59
- { id: 'pedestal_up', zh: '升镜', en: 'pedestal up', hint: '机位抬高' },
60
- { id: 'pedestal_down', zh: '降镜', en: 'pedestal down', hint: '机位降低' },
61
- { id: 'orbit', zh: '环绕', en: 'orbit shot', hint: '镜头绕主体转' },
62
- { id: 'handheld', zh: '手持晃动', en: 'handheld', hint: '真实感/紧张感' },
63
- { id: 'low_angle', zh: '低机位仰拍', en: 'low angle', hint: '突出威严/压迫' },
64
- { id: 'high_angle', zh: '高机位俯拍', en: 'high angle', hint: '突出渺小/全知' },
65
- { id: 'over_shoulder', zh: '过肩镜头', en: 'over-the-shoulder', hint: '对话常用' },
66
- ]
67
-
68
- export const COMPOSITIONS: readonly ShotLangEntry<CompositionId>[] = [
69
- { id: 'rule_of_thirds', zh: '三分法', en: 'rule of thirds', hint: '主体偏三分之一处' },
70
- { id: 'center', zh: '中心对称', en: 'centered', hint: '主体居中' },
71
- { id: 'leading_line', zh: '引导线', en: 'leading lines', hint: '视线沿线条引向主体' },
72
- { id: 'foreground', zh: '前景遮挡', en: 'foreground framing', hint: '前景物框住主体' },
73
- { id: 'low', zh: '低机位', en: 'low camera', hint: '低于视线' },
74
- { id: 'overhead', zh: '俯拍', en: 'overhead', hint: '从上方俯视' },
75
- { id: 'symmetry', zh: '对称构图', en: 'symmetrical', hint: '左右镜像' },
76
- ]
77
-
78
- export const LIGHTINGS: readonly ShotLangEntry<LightingId>[] = [
79
- { id: 'front', zh: '顺光', en: 'front light', hint: '正面均匀照明' },
80
- { id: 'side', zh: '侧光', en: 'side light', hint: '明暗对比强' },
81
- { id: 'back', zh: '逆光', en: 'back light', hint: '轮廓光/剪影' },
82
- { id: 'top', zh: '顶光', en: 'top light', hint: '俯照硬朗' },
83
- { id: 'rembrandt', zh: '伦勃朗光', en: 'rembrandt lighting', hint: '侧逆光,经典人像' },
84
- { id: 'neon', zh: '霓虹光', en: 'neon lighting', hint: '赛博/夜景' },
85
- { id: 'hard', zh: '硬光', en: 'hard light', hint: '犀利阴影' },
86
- { id: 'soft', zh: '柔光', en: 'soft light', hint: '柔和过渡' },
87
- { id: 'mood', zh: '氛围光', en: 'mood lighting', hint: '情绪化色温' },
88
- { id: 'contrast', zh: '高反差', en: 'high contrast', hint: '黑白/强明暗' },
89
- ]
90
-
91
- export interface ShotLanguage {
92
- shot?: ShotSizeId
93
- camera: CameraMoveId[]
94
- composition?: CompositionId
95
- light: LightingId[]
96
- }
97
-
98
- /** 从中文文本归一化到景别(处理旧数据/LLM 口语)。未知回退 medium。 */
99
- export function normalizeShotSize(text: string | undefined): ShotSizeId {
100
- if (text === undefined || text === '') return 'medium'
101
- const t = text.trim()
102
- if (t.includes('大远景')) return 'extreme_wide'
103
- if (t.includes('远景')) return 'wide'
104
- if (t.includes('全景')) return 'full'
105
- if (t.includes('中近景') || t.includes('中景')) return 'medium'
106
- if (t.includes('近景') || t.includes('胸')) return 'close'
107
- if (t.includes('大特写')) return 'big_extreme_close'
108
- if (t.includes('特写')) return 'extreme_close'
109
- return 'medium'
110
- }
111
-
112
- /** 从中文文本归一化到运镜列表。 */
113
- export function normalizeCameras(text: string | undefined): CameraMoveId[] {
114
- if (text === undefined || text === '') return ['static']
115
- const t = text.trim()
116
- const out: CameraMoveId[] = []
117
- const push = (id: CameraMoveId) => { if (!out.includes(id)) out.push(id) }
118
- if (t.includes('推近') || t.includes('推进')) push('dolly_in')
119
- if (t.includes('拉远') || t.includes('拉出')) push('dolly_out')
120
- if (t.includes('左摇')) push('pan_left')
121
- if (t.includes('右摇')) push('pan_right')
122
- if (t.includes('横移')) push(t.includes('左') ? 'track_left' : 'track_right')
123
- if (t.includes('跟随') || t.includes('跟拍')) push('follow')
124
- if (t.includes('升降') || t.includes('升') || t.includes('降')) push('pedestal_up')
125
- if (t.includes('环绕')) push('orbit')
126
- if (t.includes('手持') || t.includes('晃动')) push('handheld')
127
- if (t.includes('低机位') || t.includes('仰拍') || t.includes('仰')) push('low_angle')
128
- if (t.includes('高机位') || t.includes('俯拍') || t.includes('俯')) push('high_angle')
129
- if (t.includes('过肩')) push('over_shoulder')
130
- if (out.length === 0) push('static')
131
- return out
132
- }
133
-
134
- /** 从中文文本归一化到构图(可选)。 */
135
- export function normalizeComposition(text: string | undefined): CompositionId | undefined {
136
- if (text === undefined || text === '') return undefined
137
- const t = text.trim()
138
- if (t.includes('三分')) return 'rule_of_thirds'
139
- if (t.includes('中心') || t.includes('居中')) return 'center'
140
- if (t.includes('引导')) return 'leading_line'
141
- if (t.includes('前景')) return 'foreground'
142
- if (t.includes('低机位')) return 'low'
143
- if (t.includes('俯拍') || t.includes('俯视')) return 'overhead'
144
- if (t.includes('对称')) return 'symmetry'
145
- return undefined
146
- }
147
-
148
- /** 从中文文本归一化到光效列表。 */
149
- export function normalizeLightings(text: string | undefined): LightingId[] {
150
- if (text === undefined || text === '') return ['soft']
151
- const t = text.trim()
152
- const out: LightingId[] = []
153
- const push = (id: LightingId) => { if (!out.includes(id)) out.push(id) }
154
- if (t.includes('顺光')) push('front')
155
- if (t.includes('侧光') || t.includes('伦勃朗')) push(t.includes('伦勃朗') ? 'rembrandt' : 'side')
156
- if (t.includes('逆光')) push('back')
157
- if (t.includes('顶光')) push('top')
158
- if (t.includes('霓虹')) push('neon')
159
- if (t.includes('硬光')) push('hard')
160
- if (t.includes('柔光')) push('soft')
161
- if (t.includes('氛围') || t.includes('情绪')) push('mood')
162
- if (t.includes('高反差')) push('contrast')
163
- if (out.length === 0) push('soft')
164
- return out
165
- }
166
-
167
- /** 取词条中文(ZHs 合并句子)。 */
168
- export function sizeZh(id: ShotSizeId | undefined): string {
169
- return SHOT_SIZES.find(e => e.id === id)?.zh ?? '中景'
170
- }
171
- export function cameraZh(ids: CameraMoveId[] | undefined): string {
172
- if (ids === undefined || ids.length === 0) return '固定机位'
173
- return CAMERA_MOVES.filter(e => ids.includes(e.id)).map(e => e.zh).join(' + ')
174
- }
175
- export function compoZh(id: CompositionId | undefined): string {
176
- return COMPOSITIONS.find(e => e.id === id)?.zh ?? ''
177
- }
178
- export function lightZh(ids: LightingId[] | undefined): string {
179
- if (ids === undefined || ids.length === 0) return '柔光'
180
- return LIGHTINGS.filter(e => ids.includes(e.id)).map(e => e.zh).join(' + ')
181
- }
@@ -1,130 +0,0 @@
1
- /**
2
- * 编剧词库(剧情骨架规范化基准)。
3
- * 骨架的 function(叙事功能)与 emotion(情绪走向)统一从这里取值(英文 id),
4
- * 生成提示词/展示时用 zh 中文。
5
- * 目的:让"叙事功能/情绪"有标准词表,避免 LLM 自由发挥导致情绪链漂移。
6
- */
7
-
8
- /** 叙事功能(对应节拍在剧情中的作用)。 */
9
- export type StoryFunctionId =
10
- | 'exposition' | 'conflict' | 'turn' | 'climax'
11
- | 'resolve' | 'foreshadow' | 'character'
12
-
13
- /** 情绪词(低→高能量的情绪阶,角色在节拍中的主要情绪点)。 */
14
- export type EmotionId =
15
- // 起:低能量
16
- | 'calm' | 'indifferent' | 'expectant' | 'curious' | 'alert'
17
- // 承:蓄势
18
- | 'suppressed' | 'enduring' | 'worried' | 'irritable' | 'uneasy'
19
- // 转:激化
20
- | 'terrified' | 'angry' | 'collapsing' | 'resolute' | 'grieved'
21
- // 合:落点
22
- | 'relieved' | 'bittersweet' | 'triumphant' | 'reborn' | 'numb'
23
-
24
- export interface BeatLangEntry<T extends string> {
25
- id: T
26
- /** 中文(展示用)。 */
27
- zh: string
28
- /** 英文(英文平台适配用)。 */
29
- en: string
30
- /** 一句话说明。 */
31
- hint: string
32
- }
33
-
34
- /** 叙事功能词表。 */
35
- export const STORY_FUNCTIONS: readonly BeatLangEntry<StoryFunctionId>[] = [
36
- { id: 'exposition', zh: '铺垫', en: 'exposition', hint: '介绍背景/人物/悬念' },
37
- { id: 'conflict', zh: '冲突', en: 'conflict', hint: '矛盾显现/加剧' },
38
- { id: 'turn', zh: '转折', en: 'turning point', hint: '剧情反转/方向改变' },
39
- { id: 'climax', zh: '高潮', en: 'climax', hint: '冲突顶峰/揭示' },
40
- { id: 'resolve', zh: '收束', en: 'resolution', hint: '矛盾化解/结果' },
41
- { id: 'foreshadow', zh: '伏笔', en: 'foreshadowing', hint: '埋下后续线索' },
42
- { id: 'character', zh: '人物塑造', en: 'characterization', hint: '展现人物特质/转变' },
43
- ]
44
-
45
- /** 情绪词表(按能量/阶段排序)。 */
46
- export const EMOTIONS: readonly BeatLangEntry<EmotionId>[] = [
47
- // 起:低能量
48
- { id: 'calm', zh: '平静', en: 'calm', hint: '无波澜' },
49
- { id: 'indifferent', zh: '淡然', en: 'indifferent', hint: '不在乎' },
50
- { id: 'expectant', zh: '期待', en: 'expectant', hint: '有所期盼' },
51
- { id: 'curious', zh: '好奇', en: 'curious', hint: '想要探究' },
52
- { id: 'alert', zh: '警觉', en: 'alert', hint: '察觉到异样' },
53
- // 承:蓄势
54
- { id: 'suppressed', zh: '压抑', en: 'suppressed', hint: '情绪被压制' },
55
- { id: 'enduring', zh: '隐忍', en: 'enduring', hint: '强忍着' },
56
- { id: 'worried', zh: '担忧', en: 'worried', hint: '担心后果' },
57
- { id: 'irritable', zh: '焦躁', en: 'irritable', hint: '烦躁不耐' },
58
- { id: 'uneasy', zh: '不安', en: 'uneasy', hint: '心里没底' },
59
- // 转:激化
60
- { id: 'terrified', zh: '惊惧', en: 'terrified', hint: '极度恐惧' },
61
- { id: 'angry', zh: '愤怒', en: 'angry', hint: '强烈不满' },
62
- { id: 'collapsing', zh: '崩溃', en: 'collapsing', hint: '情绪失控' },
63
- { id: 'resolute', zh: '决绝', en: 'resolute', hint: '下定狠心' },
64
- { id: 'grieved', zh: '痛心', en: 'grieved', hint: '悲伤心痛' },
65
- // 合:落点
66
- { id: 'relieved', zh: '释然', en: 'relieved', hint: '放下包袱' },
67
- { id: 'bittersweet', zh: '悲凉', en: 'bittersweet', hint: '苦涩无奈' },
68
- { id: 'triumphant', zh: '得意', en: 'triumphant', hint: '占据上风' },
69
- { id: 'reborn', zh: '重生', en: 'reborn', hint: '脱胎换骨' },
70
- { id: 'numb', zh: '麻木', en: 'numb', hint: '失去感知' },
71
- ]
72
-
73
- export interface StoryBeatLanguage {
74
- function: StoryFunctionId
75
- emotion: EmotionId[]
76
- }
77
-
78
- /** 从中文归一化叙事功能(未知回退 exposition)。 */
79
- export function normalizeStoryFunction(text: string | undefined): StoryFunctionId {
80
- if (text === undefined || text === '') return 'exposition'
81
- const t = text.trim()
82
- if (t.includes('铺垫')) return 'exposition'
83
- if (t.includes('冲突')) return 'conflict'
84
- if (t.includes('转折')) return 'turn'
85
- if (t.includes('高潮')) return 'climax'
86
- if (t.includes('收束')) return 'resolve'
87
- if (t.includes('伏笔')) return 'foreshadow'
88
- if (t.includes('人物塑造')) return 'character'
89
- return 'exposition'
90
- }
91
-
92
- /** 从中文文本归一化情绪词列表(可含→箭头链)。 */
93
- export function normalizeEmotions(text: string | undefined): EmotionId[] {
94
- if (text === undefined || text === '') return ['calm']
95
- const t = text.trim()
96
- const out: EmotionId[] = []
97
- const push = (id: EmotionId) => { if (!out.includes(id)) out.push(id) }
98
- if (t.includes('平静')) push('calm')
99
- if (t.includes('淡然')) push('indifferent')
100
- if (t.includes('期待')) push('expectant')
101
- if (t.includes('好奇')) push('curious')
102
- if (t.includes('警觉')) push('alert')
103
- if (t.includes('压抑')) push('suppressed')
104
- if (t.includes('隐忍')) push('enduring')
105
- if (t.includes('担忧')) push('worried')
106
- if (t.includes('焦躁')) push('irritable')
107
- if (t.includes('不安')) push('uneasy')
108
- if (t.includes('惊惧')) push('terrified')
109
- if (t.includes('愤怒')) push('angry')
110
- if (t.includes('崩溃')) push('collapsing')
111
- if (t.includes('决绝')) push('resolute')
112
- if (t.includes('痛心')) push('grieved')
113
- if (t.includes('释然')) push('relieved')
114
- if (t.includes('悲凉')) push('bittersweet')
115
- if (t.includes('得意')) push('triumphant')
116
- if (t.includes('重生')) push('reborn')
117
- if (t.includes('麻木')) push('numb')
118
- if (out.length === 0) push('calm')
119
- return out
120
- }
121
-
122
- /** 叙事功能中文。 */
123
- export function functionZh(id: StoryFunctionId | undefined): string {
124
- return STORY_FUNCTIONS.find(e => e.id === id)?.zh ?? '铺垫'
125
- }
126
- /** 情绪中文(箭头连接)。 */
127
- export function emotionZh(ids: EmotionId[] | undefined): string {
128
- if (ids === undefined || ids.length === 0) return '平静'
129
- return EMOTIONS.filter(e => ids.includes(e.id)).map(e => e.zh).join('→')
130
- }