@waterwx/dsh-novel-forge 0.1.0 → 1.0.0-alpha

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 (180) hide show
  1. package/README.md +162 -111
  2. package/cordis.patch.yml +1 -1
  3. package/lib/client.js +54165 -2666
  4. package/lib/index.js +11895 -2211
  5. package/lib/types/ai-scan.d.ts +36 -0
  6. package/lib/types/assets.d.ts +19 -4
  7. package/lib/types/assistant.d.ts +6 -0
  8. package/lib/types/author-assets.d.ts +25 -0
  9. package/lib/types/bookshelf.d.ts +8 -1
  10. package/lib/types/client/api.d.ts +235 -3
  11. package/lib/types/client/index.d.ts +1 -1
  12. package/lib/types/client/llmLive.d.ts +33 -0
  13. package/lib/types/client/locales.d.ts +101 -28
  14. package/lib/types/client/panel/AdaptModeView.d.ts +5 -0
  15. package/lib/types/client/panel/AssetsTab.d.ts +6 -1
  16. package/lib/types/client/panel/AuthorAssetsView.d.ts +4 -0
  17. package/lib/types/client/panel/AuthorHome.d.ts +34 -0
  18. package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
  19. package/lib/types/client/panel/CreateBookView.d.ts +13 -0
  20. package/lib/types/client/panel/DirectorView.d.ts +7 -0
  21. package/lib/types/client/panel/FlowGuide.d.ts +22 -0
  22. package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +4 -0
  23. package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
  24. package/lib/types/client/panel/ImportModal.d.ts +7 -0
  25. package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
  26. package/lib/types/client/panel/LiveFeedLog.d.ts +5 -0
  27. package/lib/types/client/panel/MangaRoleLibrary.d.ts +20 -0
  28. package/lib/types/client/panel/MangaWorkspace.d.ts +12 -0
  29. package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
  30. package/lib/types/client/panel/ModelManager.d.ts +14 -0
  31. package/lib/types/client/panel/ModelPicker.d.ts +11 -0
  32. package/lib/types/client/panel/NovelPanel.d.ts +1 -1
  33. package/lib/types/client/panel/ProgressConsole.d.ts +17 -0
  34. package/lib/types/client/panel/ProgressHomeView.d.ts +5 -0
  35. package/lib/types/client/panel/PropLibrary.d.ts +8 -0
  36. package/lib/types/client/panel/ReaderView.d.ts +8 -0
  37. package/lib/types/client/panel/ReasoningSection.d.ts +10 -0
  38. package/lib/types/client/panel/RunPanel.d.ts +5 -0
  39. package/lib/types/client/panel/SceneLibrary.d.ts +16 -0
  40. package/lib/types/client/panel/SettingsView.d.ts +16 -0
  41. package/lib/types/client/panel/ShelfView.d.ts +15 -0
  42. package/lib/types/client/panel/StoryboardTab.d.ts +19 -0
  43. package/lib/types/client/panel/StyleCard.d.ts +8 -0
  44. package/lib/types/client/panel/StyleGallery.d.ts +1 -0
  45. package/lib/types/client/panel/WorldTab.d.ts +10 -0
  46. package/lib/types/client/panel/helpers.d.ts +10 -0
  47. package/lib/types/client/panel/views.d.ts +77 -0
  48. package/lib/types/client/run-status.d.ts +19 -0
  49. package/lib/types/client/text.d.ts +9 -0
  50. package/lib/types/comic-presets.d.ts +12 -0
  51. package/lib/types/engine.d.ts +250 -8
  52. package/lib/types/global-assets.d.ts +6 -0
  53. package/lib/types/index.d.ts +24 -2
  54. package/lib/types/llm-live.d.ts +44 -0
  55. package/lib/types/manga-genre-rules.d.ts +22 -0
  56. package/lib/types/market-radar-scan.d.ts +19 -0
  57. package/lib/types/market-radar-sources.d.ts +31 -0
  58. package/lib/types/novel-context.d.ts +80 -0
  59. package/lib/types/prompt-utils.d.ts +35 -0
  60. package/lib/types/prompting.d.ts +35 -0
  61. package/lib/types/protocol.d.ts +1453 -5
  62. package/lib/types/review-policy.d.ts +97 -0
  63. package/lib/types/run.d.ts +32 -0
  64. package/lib/types/shot-language.d.ts +46 -0
  65. package/lib/types/src/ai-scan.d.ts +36 -0
  66. package/lib/types/src/assets.d.ts +37 -0
  67. package/lib/types/src/assistant.d.ts +45 -0
  68. package/lib/types/src/author-assets.d.ts +25 -0
  69. package/lib/types/src/bookshelf.d.ts +42 -0
  70. package/lib/types/src/client/api.d.ts +234 -0
  71. package/lib/types/src/client/docx.d.ts +15 -0
  72. package/lib/types/src/client/index.d.ts +14 -0
  73. package/lib/types/src/client/locales.d.ts +212 -0
  74. package/lib/types/src/client/mount.d.ts +9 -0
  75. package/lib/types/src/client/panel/AdaptModeView.d.ts +5 -0
  76. package/lib/types/src/client/panel/AssetsTab.d.ts +12 -0
  77. package/lib/types/src/client/panel/AssistantTab.d.ts +7 -0
  78. package/lib/types/src/client/panel/AuthorAssetsView.d.ts +4 -0
  79. package/lib/types/src/client/panel/AuthorHome.d.ts +32 -0
  80. package/lib/types/src/client/panel/BookshelfBar.d.ts +11 -0
  81. package/lib/types/src/client/panel/CreateBookView.d.ts +9 -0
  82. package/lib/types/src/client/panel/FlowGuide.d.ts +22 -0
  83. package/lib/types/src/client/panel/GlobalAssetLibraryView.d.ts +4 -0
  84. package/lib/types/src/client/panel/ImportModal.d.ts +7 -0
  85. package/lib/types/src/client/panel/MangaRoleLibrary.d.ts +20 -0
  86. package/lib/types/src/client/panel/MangaWorkspace.d.ts +12 -0
  87. package/lib/types/src/client/panel/ModelManager.d.ts +14 -0
  88. package/lib/types/src/client/panel/NovelPanel.d.ts +13 -0
  89. package/lib/types/src/client/panel/ProgressConsole.d.ts +17 -0
  90. package/lib/types/src/client/panel/ProgressHomeView.d.ts +5 -0
  91. package/lib/types/src/client/panel/PropLibrary.d.ts +8 -0
  92. package/lib/types/src/client/panel/ReaderView.d.ts +8 -0
  93. package/lib/types/src/client/panel/ReasoningSection.d.ts +10 -0
  94. package/lib/types/src/client/panel/RunPanel.d.ts +5 -0
  95. package/lib/types/src/client/panel/SceneLibrary.d.ts +16 -0
  96. package/lib/types/src/client/panel/SettingsView.d.ts +16 -0
  97. package/lib/types/src/client/panel/ShelfView.d.ts +15 -0
  98. package/lib/types/src/client/panel/StoryboardTab.d.ts +19 -0
  99. package/lib/types/src/client/panel/StyleCard.d.ts +8 -0
  100. package/lib/types/src/client/panel/StyleGallery.d.ts +1 -0
  101. package/lib/types/src/client/panel/WorldTab.d.ts +10 -0
  102. package/lib/types/src/client/panel/controller.d.ts +19 -0
  103. package/lib/types/src/client/panel/helpers.d.ts +18 -0
  104. package/lib/types/src/client/panel/views.d.ts +77 -0
  105. package/lib/types/src/client/sidebar-entry.d.ts +13 -0
  106. package/lib/types/src/client/text.d.ts +9 -0
  107. package/lib/types/src/comic-presets.d.ts +12 -0
  108. package/lib/types/src/docx.d.ts +19 -0
  109. package/lib/types/src/engine.d.ts +402 -0
  110. package/lib/types/src/index.d.ts +79 -0
  111. package/lib/types/src/manga-genre-rules.d.ts +22 -0
  112. package/lib/types/src/novel-context.d.ts +80 -0
  113. package/lib/types/src/prompt-utils.d.ts +35 -0
  114. package/lib/types/src/protocol.d.ts +2176 -0
  115. package/lib/types/src/review-policy.d.ts +97 -0
  116. package/lib/types/src/routes.d.ts +29 -0
  117. package/lib/types/src/run.d.ts +32 -0
  118. package/lib/types/src/shot-language.d.ts +46 -0
  119. package/lib/types/src/story-beat-language.d.ts +35 -0
  120. package/lib/types/src/style-library.d.ts +33 -0
  121. package/lib/types/story-beat-language.d.ts +35 -0
  122. package/lib/types/style-library.d.ts +33 -0
  123. package/package.json +57 -42
  124. package/scripts/patch-schemastery.mjs +47 -0
  125. package/src/ai-scan.ts +179 -0
  126. package/src/assets.ts +1774 -518
  127. package/src/assistant.ts +915 -547
  128. package/src/author-assets.ts +147 -0
  129. package/src/bookshelf.ts +32 -2
  130. package/src/client/api.ts +666 -254
  131. package/src/client/index.ts +3 -1
  132. package/src/client/llmLive.ts +101 -0
  133. package/src/client/locales.ts +178 -31
  134. package/src/client/mount.tsx +1 -1
  135. package/src/client/panel/AdaptModeView.tsx +535 -0
  136. package/src/client/panel/AssetsTab.tsx +168 -51
  137. package/src/client/panel/AssistantTab.tsx +339 -188
  138. package/src/client/panel/AuthorAssetsView.tsx +208 -0
  139. package/src/client/panel/AuthorHome.tsx +234 -0
  140. package/src/client/panel/BookAnalysisView.tsx +60 -0
  141. package/src/client/panel/BookshelfBar.tsx +63 -9
  142. package/src/client/panel/CreateBookView.tsx +371 -0
  143. package/src/client/panel/DirectorView.tsx +200 -0
  144. package/src/client/panel/GlobalAssetLibraryView.tsx +172 -0
  145. package/src/client/panel/IdeaInspirationView.tsx +71 -0
  146. package/src/client/panel/ImportModal.tsx +303 -0
  147. package/src/client/panel/KnowledgeBaseView.tsx +73 -0
  148. package/src/client/panel/LiveFeedLog.tsx +67 -0
  149. package/src/client/panel/MarketRadarView.tsx +374 -0
  150. package/src/client/panel/ModelManager.tsx +205 -0
  151. package/src/client/panel/NovelPanel.tsx +4713 -990
  152. package/src/client/panel/ProgressConsole.tsx +63 -0
  153. package/src/client/panel/ProgressHomeView.tsx +87 -0
  154. package/src/client/panel/ReaderView.tsx +306 -0
  155. package/src/client/panel/ReasoningSection.tsx +31 -0
  156. package/src/client/panel/RunPanel.tsx +204 -0
  157. package/src/client/panel/SettingsView.tsx +265 -0
  158. package/src/client/panel/ShelfView.tsx +242 -0
  159. package/src/client/panel/WorldTab.tsx +202 -0
  160. package/src/client/panel/helpers.ts +50 -0
  161. package/src/client/panel/panel.module.css +4325 -526
  162. package/src/client/panel/views.tsx +346 -0
  163. package/src/client/run-status.ts +100 -0
  164. package/src/client/sidebar-entry.ts +1 -1
  165. package/src/client/text.ts +28 -0
  166. package/src/engine.ts +4230 -1019
  167. package/src/global-assets.ts +59 -0
  168. package/src/index.ts +123 -9
  169. package/src/llm-live.ts +68 -0
  170. package/src/market-radar-scan.ts +41 -0
  171. package/src/market-radar-sources.ts +173 -0
  172. package/src/novel-context.ts +287 -0
  173. package/src/prompt-utils.ts +73 -0
  174. package/src/prompting.ts +159 -0
  175. package/src/protocol.ts +1998 -539
  176. package/src/review-policy.ts +161 -0
  177. package/src/routes.ts +3277 -955
  178. package/src/run.ts +338 -0
  179. package/lib/client.js.map +0 -1
  180. package/lib/index.js.map +0 -1
package/src/run.ts ADDED
@@ -0,0 +1,338 @@
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
+ }