@waterwx/dsh-novel-forge 2.0.0-alpha.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.md +286 -285
  2. package/lib/client.js +19561 -19439
  3. package/lib/index.js +8159 -4456
  4. package/lib/types/action-guard.d.ts +61 -0
  5. package/lib/types/ai-scan.d.ts +0 -6
  6. package/lib/types/assets.d.ts +20 -9
  7. package/lib/types/assistant.d.ts +29 -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 +136 -27
  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 +31 -0
  14. package/lib/types/client/locales.d.ts +15 -70
  15. package/lib/types/client/mount.d.ts +0 -2
  16. package/lib/types/client/panel/AiTag.d.ts +1 -0
  17. package/lib/types/client/panel/AssistantTab.d.ts +4 -1
  18. package/lib/types/client/panel/AuthorHome.d.ts +13 -11
  19. package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
  20. package/lib/types/client/panel/CmdPalette.d.ts +12 -0
  21. package/lib/types/client/panel/CreateBookView.d.ts +5 -1
  22. package/lib/types/client/panel/DirectorView.d.ts +7 -0
  23. package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
  24. package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
  25. package/lib/types/client/panel/LiveFeedLog.d.ts +13 -0
  26. package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
  27. package/lib/types/client/panel/ModelManager.d.ts +2 -2
  28. package/lib/types/client/panel/NovelPanel.d.ts +6 -1
  29. package/lib/types/client/panel/PageHeader.d.ts +18 -0
  30. package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
  31. package/lib/types/client/panel/SettingsView.d.ts +12 -9
  32. package/lib/types/client/panel/ShelfView.d.ts +10 -1
  33. package/lib/types/client/panel/SubPage.d.ts +27 -0
  34. package/lib/types/client/panel/TensionView.d.ts +18 -0
  35. package/lib/types/client/panel/TimelineView.d.ts +18 -0
  36. package/lib/types/client/panel/helpers.d.ts +5 -3
  37. package/lib/types/client/panel/market-radar-state.d.ts +56 -0
  38. package/lib/types/client/panel/toast.d.ts +2 -0
  39. package/lib/types/client/panel/views.d.ts +21 -35
  40. package/lib/types/client/run-status.d.ts +21 -0
  41. package/lib/types/client/sidebar-entry.d.ts +0 -2
  42. package/lib/types/docx-core.d.ts +16 -0
  43. package/lib/types/docx.d.ts +2 -3
  44. package/lib/types/engine.d.ts +78 -103
  45. package/lib/types/global-assets.d.ts +6 -0
  46. package/lib/types/home.d.ts +11 -0
  47. package/lib/types/index.d.ts +26 -12
  48. package/lib/types/llm-live.d.ts +70 -0
  49. package/lib/types/llm-retry.d.ts +38 -0
  50. package/lib/types/market-radar-scan.d.ts +18 -0
  51. package/lib/types/market-radar-sources.d.ts +31 -0
  52. package/lib/types/migrate-output.d.ts +25 -0
  53. package/lib/types/model-capability.d.ts +93 -0
  54. package/lib/types/output-budget.d.ts +86 -0
  55. package/lib/types/prompt-slots.d.ts +57 -0
  56. package/lib/types/prompting.d.ts +10 -0
  57. package/lib/types/protocol.d.ts +816 -601
  58. package/lib/types/revision.d.ts +76 -0
  59. package/lib/types/routes.d.ts +9 -3
  60. package/lib/types/run.d.ts +23 -1
  61. package/lib/types/settings-persist.d.ts +59 -0
  62. package/lib/types/snapshots.d.ts +46 -0
  63. package/lib/types/stage-contract.d.ts +58 -0
  64. package/lib/types/tension.d.ts +53 -0
  65. package/lib/types/timeline.d.ts +37 -0
  66. package/package.json +50 -25
  67. package/src/action-guard.ts +127 -0
  68. package/src/ai-scan.ts +0 -24
  69. package/src/assets.ts +1801 -879
  70. package/src/assistant.ts +1098 -770
  71. package/src/author-assets.ts +131 -147
  72. package/src/bookshelf.ts +193 -167
  73. package/src/client/api.ts +767 -617
  74. package/src/client/chip-prefs.ts +121 -0
  75. package/src/client/docx.ts +32 -69
  76. package/src/client/index.ts +2 -0
  77. package/src/client/llmLive.ts +111 -0
  78. package/src/client/locales.ts +15 -274
  79. package/src/client/mount.tsx +3 -2
  80. package/src/client/panel/AdaptModeView.tsx +535 -520
  81. package/src/client/panel/AiTag.tsx +7 -0
  82. package/src/client/panel/AssetsTab.tsx +448 -458
  83. package/src/client/panel/AssistantTab.tsx +355 -301
  84. package/src/client/panel/AuthorAssetsView.tsx +205 -207
  85. package/src/client/panel/AuthorHome.tsx +309 -204
  86. package/src/client/panel/BookAnalysisView.tsx +63 -0
  87. package/src/client/panel/CmdPalette.tsx +102 -0
  88. package/src/client/panel/CreateBookView.tsx +366 -352
  89. package/src/client/panel/DirectorView.tsx +200 -0
  90. package/src/client/panel/GlobalAssetLibraryView.tsx +168 -172
  91. package/src/client/panel/IdeaInspirationView.tsx +76 -0
  92. package/src/client/panel/ImportModal.tsx +302 -302
  93. package/src/client/panel/KnowledgeBaseView.tsx +125 -0
  94. package/src/client/panel/LiveFeedLog.tsx +204 -0
  95. package/src/client/panel/MarketRadarView.tsx +415 -0
  96. package/src/client/panel/ModelManager.tsx +205 -205
  97. package/src/client/panel/NovelPanel.tsx +3144 -2205
  98. package/src/client/panel/PageHeader.tsx +37 -0
  99. package/src/client/panel/ProgressConsole.tsx +63 -64
  100. package/src/client/panel/PromptSlotsView.tsx +97 -0
  101. package/src/client/panel/ReaderView.tsx +306 -306
  102. package/src/client/panel/RunPanel.tsx +56 -24
  103. package/src/client/panel/SettingsView.tsx +175 -106
  104. package/src/client/panel/ShelfView.tsx +63 -39
  105. package/src/client/panel/SubPage.tsx +42 -0
  106. package/src/client/panel/TensionView.tsx +212 -0
  107. package/src/client/panel/TimelineView.tsx +208 -0
  108. package/src/client/panel/WorldTab.tsx +202 -202
  109. package/src/client/panel/helpers.ts +7 -5
  110. package/src/client/panel/market-radar-state.ts +79 -0
  111. package/src/client/panel/panel.module.css +5767 -4360
  112. package/src/client/panel/toast.ts +51 -0
  113. package/src/client/panel/views.tsx +316 -314
  114. package/src/client/run-status.ts +171 -0
  115. package/src/client/sidebar-entry.ts +3 -3
  116. package/src/docx-core.ts +53 -0
  117. package/src/docx.ts +44 -83
  118. package/src/engine.ts +1197 -1638
  119. package/src/global-assets.ts +58 -0
  120. package/src/home.ts +33 -0
  121. package/src/index.ts +181 -101
  122. package/src/llm-live.ts +245 -0
  123. package/src/llm-retry.ts +77 -0
  124. package/src/market-radar-scan.ts +35 -0
  125. package/src/market-radar-sources.ts +173 -0
  126. package/src/migrate-output.ts +76 -0
  127. package/src/model-capability.ts +215 -0
  128. package/src/novel-context.ts +286 -287
  129. package/src/output-budget.ts +147 -0
  130. package/src/prompt-slots.ts +125 -0
  131. package/src/prompting.ts +26 -0
  132. package/src/protocol.ts +835 -614
  133. package/src/revision.ts +283 -0
  134. package/src/routes.ts +1286 -910
  135. package/src/run.ts +113 -17
  136. package/src/settings-persist.ts +114 -0
  137. package/src/snapshots.ts +174 -0
  138. package/src/stage-contract.ts +186 -0
  139. package/src/tension.ts +251 -0
  140. package/src/timeline.ts +159 -0
  141. package/assets/styles/arcane-thick-3d.webp +0 -0
  142. package/assets/styles/chinese-ink.webp +0 -0
  143. package/assets/styles/clay-stopmotion-3d.webp +0 -0
  144. package/assets/styles/dark-ghibli.webp +0 -0
  145. package/assets/styles/dark-gothic-3d.webp +0 -0
  146. package/assets/styles/film-photo.webp +0 -0
  147. package/assets/styles/guofeng-heavy.webp +0 -0
  148. package/assets/styles/guoman-2d-gufeng.webp +0 -0
  149. package/assets/styles/guoman-3d-tianyuan.webp +0 -0
  150. package/assets/styles/guoman-3d-xuanhuan.webp +0 -0
  151. package/assets/styles/horror-game-3d.webp +0 -0
  152. package/assets/styles/hyperreal-3d.webp +0 -0
  153. package/assets/styles/japan-cel-dark.webp +0 -0
  154. package/assets/styles/korean-webtoon.webp +0 -0
  155. package/assets/styles/low-cyberpunk-3d.webp +0 -0
  156. package/assets/styles/nextgen-cg.webp +0 -0
  157. package/assets/styles/noir-film.webp +0 -0
  158. package/assets/styles/openworld-survival.webp +0 -0
  159. package/assets/styles/paper-cut-3d.webp +0 -0
  160. package/assets/styles/pixar-adult-3d.webp +0 -0
  161. package/assets/styles/pop-marvel-2d.webp +0 -0
  162. package/assets/styles/sandiao-qban.webp +0 -0
  163. package/assets/styles/shinkai-anime.webp +0 -0
  164. package/assets/styles/tactical-stealth.webp +0 -0
  165. package/assets/styles/urban-lightreal-3d.webp +0 -0
  166. package/assets/styles/vaporwave-3d.webp +0 -0
  167. package/assets/styles/vhs90.webp +0 -0
  168. package/lib/types/client/panel/BookshelfBar.d.ts +0 -11
  169. package/lib/types/client/panel/FlowGuide.d.ts +0 -22
  170. package/lib/types/client/panel/MangaRoleLibrary.d.ts +0 -20
  171. package/lib/types/client/panel/MangaWorkspace.d.ts +0 -12
  172. package/lib/types/client/panel/ModelPicker.d.ts +0 -11
  173. package/lib/types/client/panel/ProgressHomeView.d.ts +0 -5
  174. package/lib/types/client/panel/PropLibrary.d.ts +0 -8
  175. package/lib/types/client/panel/SceneLibrary.d.ts +0 -16
  176. package/lib/types/client/panel/StoryboardTab.d.ts +0 -19
  177. package/lib/types/client/panel/StyleCard.d.ts +0 -8
  178. package/lib/types/client/panel/StyleGallery.d.ts +0 -1
  179. package/lib/types/comic-presets.d.ts +0 -12
  180. package/lib/types/manga-genre-rules.d.ts +0 -22
  181. package/lib/types/prompt-utils.d.ts +0 -35
  182. package/lib/types/review-policy.d.ts +0 -97
  183. package/lib/types/shot-language.d.ts +0 -46
  184. package/lib/types/story-beat-language.d.ts +0 -35
  185. package/lib/types/style-library.d.ts +0 -33
  186. package/skills/chapter-batch/SKILL.md +0 -81
  187. package/src/client/mount.tsx._new +0 -97
  188. package/src/client/mount.tsx._nf +0 -97
  189. package/src/client/mount.tsx._tmp +0 -97
  190. package/src/client/panel/BookshelfBar.tsx +0 -170
  191. package/src/client/panel/FlowGuide.tsx +0 -84
  192. package/src/client/panel/MangaRoleLibrary.tsx +0 -1002
  193. package/src/client/panel/MangaWorkspace.tsx +0 -715
  194. package/src/client/panel/ProgressHomeView.tsx +0 -87
  195. package/src/client/panel/PropLibrary.tsx +0 -175
  196. package/src/client/panel/SceneLibrary.tsx +0 -453
  197. package/src/client/panel/StoryboardTab.tsx +0 -472
  198. package/src/client/panel/StyleCard.tsx +0 -90
  199. package/src/client/panel/StyleGallery.tsx +0 -58
  200. package/src/comic-presets.ts +0 -67
  201. package/src/manga-genre-rules.ts +0 -87
  202. package/src/prompt-utils.ts +0 -73
  203. package/src/review-policy.ts +0 -161
  204. package/src/shot-language.ts +0 -181
  205. package/src/story-beat-language.ts +0 -130
  206. package/src/style-library.ts +0 -84
@@ -1,287 +1,286 @@
1
- /**
2
- * 统一上下文组装与相关设定检索
3
- *
4
- * 取代各阶段各自拼装 prompt,消除「生成看得到、审稿看不到」的断层。
5
- * 所有阶段通过 buildChapterContext 获取同一来源的事实与设定。
6
- */
7
-
8
- import type { ProjectState, ChapterPlan, StoryBible, RoleRecord, ChapterFact, Foreshadow, Plotline } from './protocol'
9
- import { readFileSync, existsSync } from 'node:fs'
10
- import { join } from 'node:path'
11
-
12
- export interface ChapterContext {
13
- /** 平台合规红线(永远全量) */
14
- complianceRedLines: string[]
15
- /** 本书写作红线(永远全量) */
16
- bookRedLines: string[]
17
- /** 当前章相关角色卡(全量信息) */
18
- relevantRoles: Array<{ name: string; role: string; traits: string[]; goals?: string; knowledge?: string[] }>
19
- /** 当前章相关道藏规则 */
20
- relevantWorldRules: string[]
21
- /** 活跃剧情线 */
22
- activePlotlines: Plotline[]
23
- /** 未回收伏笔 */
24
- activeForeshadows: Foreshadow[]
25
- /** 上一章结尾原文 */
26
- prevChapterTail: string
27
- /** 上一章摘要 */
28
- prevChapterSummary?: string
29
- /** 最近事实(近因记忆,top20) */
30
- recentFacts: string[]
31
- /** 相关旧事实(按本章 beats 检索,top15) */
32
- relatedFacts: string[]
33
- /** 当前卷大纲 */
34
- currentVolumeOutline: string
35
- /** 本章 beats */
36
- beats: string
37
- /** 本章标题 */
38
- chapterTitle: string
39
- }
40
-
41
- export interface ContextBuildOptions {
42
- /** 阶段:writing/review/revise/polish/plan */
43
- stage?: 'writing' | 'review' | 'revise' | 'polish' | 'plan'
44
- /** 是否注入完整角色卡(false 时只注入角色名+定位) */
45
- fullRoleCards?: boolean
46
- /** 相关事实检索上限 */
47
- relatedFactsLimit?: number
48
- /** 最近事实数量 */
49
- recentFactsLimit?: number
50
- }
51
-
52
- /**
53
- * 相关事实检索:trigram 重合度 + 角色名命中加权 + 近因加权
54
- * 从 generateChapterStream 抽取,扩展为通用函数。
55
- */
56
- export function retrieveRelatedFacts(
57
- facts: ChapterFact[],
58
- beatsText: string,
59
- roleNames: string[],
60
- limit = 15,
61
- ): string[] {
62
- if (facts.length === 0) return []
63
-
64
- const trigrams = (s: string): Set<string> => {
65
- const out = new Set<string>()
66
- for (let i = 0; i + 3 <= s.length; i++) {
67
- const tri = s.slice(i, i + 3)
68
- if (tri.trim() !== '') out.add(tri)
69
- }
70
- return out
71
- }
72
-
73
- const beatsTri = trigrams(beatsText)
74
- const beatRoles = roleNames.filter(n => beatsText.includes(n))
75
-
76
- const recentTexts = new Set(facts.slice(-20).map(f => f.text))
77
-
78
- return facts
79
- .map((f, idx) => {
80
- const head = f.text.slice(0, 80)
81
- let score = 0
82
- for (const tri of trigrams(head)) if (beatsTri.has(tri)) score += 1
83
- if (beatRoles.length > 0) {
84
- for (const n of beatRoles) if (head.includes(n)) score += 8
85
- }
86
- // 近因加权:越新越优先(封顶 40 章)
87
- score += Math.min(idx, 40) / 10
88
- return { f, score }
89
- })
90
- .filter(x => x.score >= 3)
91
- .sort((a, b) => b.score - a.score)
92
- .slice(0, limit)
93
- .map(x => `[第${x.f.chapterNo}章] ${x.f.text}`)
94
- .filter(t => !recentTexts.has(t.slice(t.indexOf(']') + 2)))
95
- }
96
-
97
- /**
98
- * 相关角色检索:beats 中出现的角色 + 主角/主要反派永远全量
99
- */
100
- export function retrieveRelevantRoles(
101
- bible: StoryBible | undefined,
102
- roles: RoleRecord[] | undefined,
103
- beatsText: string,
104
- ): ChapterContext['relevantRoles'] {
105
- const result: ChapterContext['relevantRoles'] = []
106
- const seen = new Set<string>()
107
-
108
- // 主角/主要反派永远全量
109
- const alwaysRoles = (bible?.characters ?? []).filter(c =>
110
- c.role === 'protagonist' || c.role === 'antagonist'
111
- )
112
- for (const card of alwaysRoles) {
113
- if (seen.has(card.name)) continue
114
- seen.add(card.name)
115
- result.push({
116
- name: card.name,
117
- role: card.role,
118
- traits: card.traits,
119
- goals: card.goals !== '' ? card.goals : undefined,
120
- knowledge: Array.isArray(card.knowledge) && card.knowledge.length > 0 ? card.knowledge : undefined,
121
- })
122
- }
123
-
124
- // beats 中出现的角色
125
- const beatRoles = (bible?.characters ?? []).filter(c => beatsText.includes(c.name))
126
- for (const card of beatRoles) {
127
- if (seen.has(card.name)) continue
128
- seen.add(card.name)
129
- result.push({
130
- name: card.name,
131
- role: card.role,
132
- traits: card.traits,
133
- goals: card.goals !== '' ? card.goals : undefined,
134
- knowledge: Array.isArray(card.knowledge) && card.knowledge.length > 0 ? card.knowledge : undefined,
135
- })
136
- }
137
-
138
- // 角色库补充(不在道藏里的)
139
- for (const r of roles ?? []) {
140
- if (seen.has(r.name)) continue
141
- if (!beatsText.includes(r.name) && r.roleLabel !== 'protagonist' && r.roleLabel !== 'antagonist') continue
142
- seen.add(r.name)
143
- result.push({
144
- name: r.name,
145
- role: r.roleLabel,
146
- traits: Array.isArray(r.traits) ? r.traits : [],
147
- goals: r.goals !== undefined && r.goals !== '' ? r.goals : undefined,
148
- })
149
- }
150
-
151
- return result
152
- }
153
-
154
- /**
155
- * 构建章节统一上下文
156
- *
157
- * 各阶段调用此函数获取同一来源的事实与设定,消除上下文断层。
158
- * 检索结果不足时自动回退全量,保证不丢失关键信息。
159
- */
160
- export function buildChapterContext(
161
- project: ProjectState,
162
- chapter: ChapterPlan,
163
- outputDir: string,
164
- options: ContextBuildOptions = {},
165
- ): ChapterContext {
166
- const {
167
- stage = 'writing',
168
- fullRoleCards = true,
169
- relatedFactsLimit = 15,
170
- recentFactsLimit = 20,
171
- } = options
172
-
173
- const bible = project.bible
174
- const allFacts = project.facts ?? []
175
- const roleNames = (project.roles ?? []).map(r => r.name).filter((n): n is string => typeof n === 'string' && n !== '')
176
-
177
- // 上一章结尾 + 摘要
178
- let prevChapterTail = ''
179
- let prevChapterSummary: string | undefined
180
- const prev = project.chapters.find(c => c.no === chapter.no - 1)
181
- if (prev?.file !== undefined) {
182
- try {
183
- const prevPath = join(outputDir, prev.file)
184
- if (existsSync(prevPath)) {
185
- const text = readFileSync(prevPath, 'utf8')
186
- prevChapterTail = text.replace(/^#\s+.*$/m, '').trim().slice(-900)
187
- }
188
- } catch { /* 文件缺失时忽略 */ }
189
- }
190
- prevChapterSummary = prev?.summary
191
-
192
- // 事实:最近 + 相关
193
- const recentFacts = allFacts.slice(-recentFactsLimit).map(f => `[第${f.chapterNo}章] ${f.text}`)
194
- const relatedFacts = retrieveRelatedFacts(allFacts, chapter.beats, roleNames, relatedFactsLimit)
195
-
196
- // 角色:相关角色
197
- const relevantRoles = retrieveRelevantRoles(bible, project.roles, chapter.beats)
198
-
199
- // 道藏规则:全部(规则数量通常不多,全量注入更安全)
200
- const relevantWorldRules = bible?.worldRules ?? []
201
-
202
- // 活跃剧情线
203
- const activePlotlines = (project.plotlines ?? []).filter(l =>
204
- l.status === 'active' || l.status === 'paused'
205
- )
206
-
207
- // 未回收伏笔
208
- const activeForeshadows = (project.foreshadows ?? []).filter(f =>
209
- f.status === 'planted' || f.status === 'progressing'
210
- )
211
-
212
- // 当前卷大纲
213
- const volumeNo = chapter.volume ?? 0
214
- const volume = project.volumes?.find(v => v.no === volumeNo)
215
- const currentVolumeOutline = volume !== undefined
216
- ? `第${volume.no}卷《${volume.title}》:${volume.summary}`
217
- : ''
218
-
219
- return {
220
- complianceRedLines: [], // 由调用方注入 COMPLIANCE_REDLINES
221
- bookRedLines: bible?.redLines ?? [],
222
- relevantRoles: fullRoleCards ? relevantRoles : relevantRoles.map(r => ({ name: r.name, role: r.role, traits: [] })),
223
- relevantWorldRules,
224
- activePlotlines,
225
- activeForeshadows,
226
- prevChapterTail,
227
- prevChapterSummary,
228
- recentFacts,
229
- relatedFacts,
230
- currentVolumeOutline,
231
- beats: chapter.beats,
232
- chapterTitle: chapter.title,
233
- }
234
- }
235
-
236
- /**
237
- * 将上下文渲染为 prompt 文本块
238
- * 各阶段可按需选择注入哪些块。
239
- */
240
- export function renderContextBlocks(ctx: ChapterContext): {
241
- rolesBlock: string
242
- worldRulesBlock: string
243
- factsBlock: string
244
- plotlinesBlock: string
245
- foreshadowsBlock: string
246
- continuityBlock: string
247
- redLinesBlock: string
248
- } {
249
- const roleName = { protagonist: '主角', supporting: '配角', antagonist: '反派', other: '其他', female_lead: '女主', female_support: '女配', support: '配角', extra: '路人' } as Record<string, string>
250
-
251
- const rolesBlock = ctx.relevantRoles.length > 0
252
- ? '==================== 相关角色卡 ====================\n' +
253
- ctx.relevantRoles.map(r =>
254
- `- ${r.name}(${roleName[r.role] ?? r.role}):${r.traits.join('、')}${r.goals ? `;目标:${r.goals}` : ''}${r.knowledge ? `\n 已知信息:${r.knowledge.join(';')}(未列出的该角色不知道)` : ''}`
255
- ).join('\n')
256
- : ''
257
-
258
- const worldRulesBlock = ctx.relevantWorldRules.length > 0
259
- ? '==================== 世界规则 ====================\n' + ctx.relevantWorldRules.map(r => `- ${r}`).join('\n')
260
- : ''
261
-
262
- const factsBlock = (ctx.recentFacts.length > 0 || ctx.relatedFacts.length > 0)
263
- ? '==================== 事实库(最近 + 相关) ====================\n' +
264
- (ctx.recentFacts.length > 0 ? `【最近 ${ctx.recentFacts.length} 条】\n${ctx.recentFacts.join('\n')}` : '') +
265
- (ctx.relatedFacts.length > 0 ? `\n【相关旧事实 ${ctx.relatedFacts.length} 条】\n${ctx.relatedFacts.join('\n')}` : '')
266
- : ''
267
-
268
- const plotlinesBlock = ctx.activePlotlines.length > 0
269
- ? '==================== 活跃剧情线 ====================\n' +
270
- ctx.activePlotlines.map(l => `- [${l.kind}${l.status === 'paused' ? '·暂停' : ''}] ${l.name}:${l.goal}${l.progress ? `(进度:${l.progress})` : ''}`).join('\n')
271
- : ''
272
-
273
- const foreshadowsBlock = ctx.activeForeshadows.length > 0
274
- ? '==================== 活跃伏笔 ====================\n' +
275
- ctx.activeForeshadows.map(f => `- [${f.status === 'planted' ? '已埋设' : '推进中'}] ${f.description}${f.targetChapter ? `(预计第${f.targetChapter}章回收)` : ''}`).join('\n')
276
- : ''
277
-
278
- const continuityBlock = ctx.prevChapterTail !== ''
279
- ? `==================== 上一章结尾(紧接此状态继续) ====================\n${ctx.prevChapterTail}`
280
- : ''
281
-
282
- const redLinesBlock = ctx.bookRedLines.length > 0
283
- ? '==================== 本书红线 ====================\n' + ctx.bookRedLines.map(r => `- ${r}`).join('\n')
284
- : ''
285
-
286
- return { rolesBlock, worldRulesBlock, factsBlock, plotlinesBlock, foreshadowsBlock, continuityBlock, redLinesBlock }
287
- }
1
+ /**
2
+ * 统一上下文组装与相关设定检索
3
+ *
4
+ * 取代各阶段各自拼装 prompt,消除「生成看得到、审稿看不到」的断层。
5
+ * 所有阶段通过 buildChapterContext 获取同一来源的事实与设定。
6
+ */
7
+
8
+ import type { ProjectState, ChapterPlan, StoryBible, RoleRecord, ChapterFact, Foreshadow, Plotline } from './protocol'
9
+ import { readFileSync, existsSync } from 'node:fs'
10
+ import { join } from 'node:path'
11
+
12
+ export interface ChapterContext {
13
+ /** 平台合规红线(永远全量) */
14
+ complianceRedLines: string[]
15
+ /** 本书写作红线(永远全量) */
16
+ bookRedLines: string[]
17
+ /** 当前章相关角色卡(全量信息) */
18
+ relevantRoles: Array<{ name: string; role: string; traits: string[]; goals?: string; knowledge?: string[] }>
19
+ /** 当前章相关道藏规则 */
20
+ relevantWorldRules: string[]
21
+ /** 活跃剧情线 */
22
+ activePlotlines: Plotline[]
23
+ /** 未回收伏笔 */
24
+ activeForeshadows: Foreshadow[]
25
+ /** 上一章结尾原文 */
26
+ prevChapterTail: string
27
+ /** 上一章摘要 */
28
+ prevChapterSummary?: string
29
+ /** 最近事实(近因记忆,top20) */
30
+ recentFacts: string[]
31
+ /** 相关旧事实(按本章 beats 检索,top15) */
32
+ relatedFacts: string[]
33
+ /** 当前卷大纲 */
34
+ currentVolumeOutline: string
35
+ /** 本章 beats */
36
+ beats: string
37
+ /** 本章标题 */
38
+ chapterTitle: string
39
+ }
40
+
41
+ export interface ContextBuildOptions {
42
+ /** 阶段:writing/review/revise/polish/plan */
43
+ stage?: 'writing' | 'review' | 'revise' | 'polish' | 'plan'
44
+ /** 是否注入完整角色卡(false 时只注入角色名+定位) */
45
+ fullRoleCards?: boolean
46
+ /** 相关事实检索上限 */
47
+ relatedFactsLimit?: number
48
+ /** 最近事实数量 */
49
+ recentFactsLimit?: number
50
+ }
51
+
52
+ /**
53
+ * 相关事实检索:trigram 重合度 + 角色名命中加权 + 近因加权
54
+ * 从 generateChapterStream 抽取,扩展为通用函数。
55
+ */
56
+ export function retrieveRelatedFacts(
57
+ facts: ChapterFact[],
58
+ beatsText: string,
59
+ roleNames: string[],
60
+ limit = 15,
61
+ ): string[] {
62
+ if (facts.length === 0) return []
63
+
64
+ const trigrams = (s: string): Set<string> => {
65
+ const out = new Set<string>()
66
+ for (let i = 0; i + 3 <= s.length; i++) {
67
+ const tri = s.slice(i, i + 3)
68
+ if (tri.trim() !== '') out.add(tri)
69
+ }
70
+ return out
71
+ }
72
+
73
+ const beatsTri = trigrams(beatsText)
74
+ const beatRoles = roleNames.filter(n => beatsText.includes(n))
75
+
76
+ const recentTexts = new Set(facts.slice(-20).map(f => f.text))
77
+
78
+ return facts
79
+ .map((f, idx) => {
80
+ const head = f.text.slice(0, 80)
81
+ let score = 0
82
+ for (const tri of trigrams(head)) if (beatsTri.has(tri)) score += 1
83
+ if (beatRoles.length > 0) {
84
+ for (const n of beatRoles) if (head.includes(n)) score += 8
85
+ }
86
+ // 近因加权:越新越优先(封顶 40 章)
87
+ score += Math.min(idx, 40) / 10
88
+ return { f, score }
89
+ })
90
+ .filter(x => x.score >= 3)
91
+ .sort((a, b) => b.score - a.score)
92
+ .slice(0, limit)
93
+ .map(x => `[第${x.f.chapterNo}章] ${x.f.text}`)
94
+ .filter(t => !recentTexts.has(t.slice(t.indexOf(']') + 2)))
95
+ }
96
+
97
+ /**
98
+ * 相关角色检索:beats 中出现的角色 + 主角/主要反派永远全量
99
+ */
100
+ export function retrieveRelevantRoles(
101
+ bible: StoryBible | undefined,
102
+ roles: RoleRecord[] | undefined,
103
+ beatsText: string,
104
+ ): ChapterContext['relevantRoles'] {
105
+ const result: ChapterContext['relevantRoles'] = []
106
+ const seen = new Set<string>()
107
+
108
+ // 主角/主要反派永远全量
109
+ const alwaysRoles = (bible?.characters ?? []).filter(c =>
110
+ c.role === 'protagonist' || c.role === 'antagonist'
111
+ )
112
+ for (const card of alwaysRoles) {
113
+ if (seen.has(card.name)) continue
114
+ seen.add(card.name)
115
+ result.push({
116
+ name: card.name,
117
+ role: card.role,
118
+ traits: card.traits,
119
+ goals: card.goals !== '' ? card.goals : undefined,
120
+ knowledge: Array.isArray(card.knowledge) && card.knowledge.length > 0 ? card.knowledge : undefined,
121
+ })
122
+ }
123
+
124
+ // beats 中出现的角色
125
+ const beatRoles = (bible?.characters ?? []).filter(c => beatsText.includes(c.name))
126
+ for (const card of beatRoles) {
127
+ if (seen.has(card.name)) continue
128
+ seen.add(card.name)
129
+ result.push({
130
+ name: card.name,
131
+ role: card.role,
132
+ traits: card.traits,
133
+ goals: card.goals !== '' ? card.goals : undefined,
134
+ knowledge: Array.isArray(card.knowledge) && card.knowledge.length > 0 ? card.knowledge : undefined,
135
+ })
136
+ }
137
+
138
+ // 角色库补充(不在道藏里的)
139
+ for (const r of roles ?? []) {
140
+ if (seen.has(r.name)) continue
141
+ if (!beatsText.includes(r.name) && r.roleLabel !== 'protagonist' && r.roleLabel !== 'antagonist') continue
142
+ seen.add(r.name)
143
+ result.push({
144
+ name: r.name,
145
+ role: r.roleLabel,
146
+ traits: Array.isArray(r.traits) ? r.traits : [],
147
+ goals: r.goals !== undefined && r.goals !== '' ? r.goals : undefined,
148
+ })
149
+ }
150
+
151
+ return result
152
+ }
153
+
154
+ /**
155
+ * 构建章节统一上下文
156
+ *
157
+ * 各阶段调用此函数获取同一来源的事实与设定,消除上下文断层。
158
+ * 检索结果不足时自动回退全量,保证不丢失关键信息。
159
+ */
160
+ export function buildChapterContext(
161
+ project: ProjectState,
162
+ chapter: ChapterPlan,
163
+ outputDir: string,
164
+ options: ContextBuildOptions = {},
165
+ ): ChapterContext {
166
+ const {
167
+ fullRoleCards = true,
168
+ relatedFactsLimit = 15,
169
+ recentFactsLimit = 20,
170
+ } = options
171
+
172
+ const bible = project.bible
173
+ const allFacts = project.facts ?? []
174
+ const roleNames = (project.roles ?? []).map(r => r.name).filter((n): n is string => typeof n === 'string' && n !== '')
175
+
176
+ // 上一章结尾 + 摘要
177
+ let prevChapterTail = ''
178
+ let prevChapterSummary: string | undefined
179
+ const prev = project.chapters.find(c => c.no === chapter.no - 1)
180
+ if (prev?.file !== undefined) {
181
+ try {
182
+ const prevPath = join(outputDir, prev.file)
183
+ if (existsSync(prevPath)) {
184
+ const text = readFileSync(prevPath, 'utf8')
185
+ prevChapterTail = text.replace(/^#\s+.*$/m, '').trim().slice(-900)
186
+ }
187
+ } catch { /* 文件缺失时忽略 */ }
188
+ }
189
+ prevChapterSummary = prev?.summary
190
+
191
+ // 事实:最近 + 相关
192
+ const recentFacts = allFacts.slice(-recentFactsLimit).map(f => `[第${f.chapterNo}章] ${f.text}`)
193
+ const relatedFacts = retrieveRelatedFacts(allFacts, chapter.beats, roleNames, relatedFactsLimit)
194
+
195
+ // 角色:相关角色
196
+ const relevantRoles = retrieveRelevantRoles(bible, project.roles, chapter.beats)
197
+
198
+ // 道藏规则:全部(规则数量通常不多,全量注入更安全)
199
+ const relevantWorldRules = bible?.worldRules ?? []
200
+
201
+ // 活跃剧情线
202
+ const activePlotlines = (project.plotlines ?? []).filter(l =>
203
+ l.status === 'active' || l.status === 'paused'
204
+ )
205
+
206
+ // 未回收伏笔
207
+ const activeForeshadows = (project.foreshadows ?? []).filter(f =>
208
+ f.status === 'planted' || f.status === 'progressing'
209
+ )
210
+
211
+ // 当前卷大纲
212
+ const volumeNo = chapter.volume ?? 0
213
+ const volume = project.volumes?.find(v => v.no === volumeNo)
214
+ const currentVolumeOutline = volume !== undefined
215
+ ? `第${volume.no}卷《${volume.title}》:${volume.summary}`
216
+ : ''
217
+
218
+ return {
219
+ complianceRedLines: [], // 由调用方注入 COMPLIANCE_REDLINES
220
+ bookRedLines: bible?.redLines ?? [],
221
+ relevantRoles: fullRoleCards ? relevantRoles : relevantRoles.map(r => ({ name: r.name, role: r.role, traits: [] })),
222
+ relevantWorldRules,
223
+ activePlotlines,
224
+ activeForeshadows,
225
+ prevChapterTail,
226
+ prevChapterSummary,
227
+ recentFacts,
228
+ relatedFacts,
229
+ currentVolumeOutline,
230
+ beats: chapter.beats,
231
+ chapterTitle: chapter.title,
232
+ }
233
+ }
234
+
235
+ /**
236
+ * 将上下文渲染为 prompt 文本块
237
+ * 各阶段可按需选择注入哪些块。
238
+ */
239
+ export function renderContextBlocks(ctx: ChapterContext): {
240
+ rolesBlock: string
241
+ worldRulesBlock: string
242
+ factsBlock: string
243
+ plotlinesBlock: string
244
+ foreshadowsBlock: string
245
+ continuityBlock: string
246
+ redLinesBlock: string
247
+ } {
248
+ const roleName = { protagonist: '主角', supporting: '配角', antagonist: '反派', other: '其他', female_lead: '女主', female_support: '女配', support: '配角', extra: '路人' } as Record<string, string>
249
+
250
+ const rolesBlock = ctx.relevantRoles.length > 0
251
+ ? '==================== 相关角色卡 ====================\n' +
252
+ ctx.relevantRoles.map(r =>
253
+ `- ${r.name}(${roleName[r.role] ?? r.role}):${r.traits.join('、')}${r.goals ? `;目标:${r.goals}` : ''}${r.knowledge ? `\n 已知信息:${r.knowledge.join(';')}(未列出的该角色不知道)` : ''}`
254
+ ).join('\n')
255
+ : ''
256
+
257
+ const worldRulesBlock = ctx.relevantWorldRules.length > 0
258
+ ? '==================== 世界规则 ====================\n' + ctx.relevantWorldRules.map(r => `- ${r}`).join('\n')
259
+ : ''
260
+
261
+ const factsBlock = (ctx.recentFacts.length > 0 || ctx.relatedFacts.length > 0)
262
+ ? '==================== 事实库(最近 + 相关) ====================\n' +
263
+ (ctx.recentFacts.length > 0 ? `【最近 ${ctx.recentFacts.length} 条】\n${ctx.recentFacts.join('\n')}` : '') +
264
+ (ctx.relatedFacts.length > 0 ? `\n【相关旧事实 ${ctx.relatedFacts.length} 条】\n${ctx.relatedFacts.join('\n')}` : '')
265
+ : ''
266
+
267
+ const plotlinesBlock = ctx.activePlotlines.length > 0
268
+ ? '==================== 活跃剧情线 ====================\n' +
269
+ ctx.activePlotlines.map(l => `- [${l.kind}${l.status === 'paused' ? '·暂停' : ''}] ${l.name}:${l.goal}${l.progress ? `(进度:${l.progress})` : ''}`).join('\n')
270
+ : ''
271
+
272
+ const foreshadowsBlock = ctx.activeForeshadows.length > 0
273
+ ? '==================== 活跃伏笔 ====================\n' +
274
+ ctx.activeForeshadows.map(f => `- [${f.status === 'planted' ? '已埋设' : '推进中'}] ${f.description}${f.targetChapter ? `(预计第${f.targetChapter}章回收)` : ''}`).join('\n')
275
+ : ''
276
+
277
+ const continuityBlock = ctx.prevChapterTail !== ''
278
+ ? `==================== 上一章结尾(紧接此状态继续) ====================\n${ctx.prevChapterTail}`
279
+ : ''
280
+
281
+ const redLinesBlock = ctx.bookRedLines.length > 0
282
+ ? '==================== 本书红线 ====================\n' + ctx.bookRedLines.map(r => `- ${r}`).join('\n')
283
+ : ''
284
+
285
+ return { rolesBlock, worldRulesBlock, factsBlock, plotlinesBlock, foreshadowsBlock, continuityBlock, redLinesBlock }
286
+ }