@waterwx/dsh-novel-forge 1.2.0 → 1.3.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 (141) hide show
  1. package/README.md +236 -349
  2. package/lib/client.js +56064 -10565
  3. package/lib/index.js +13922 -5218
  4. package/lib/types/action-guard.d.ts +61 -0
  5. package/lib/types/ai-scan.d.ts +36 -0
  6. package/lib/types/assets.d.ts +24 -9
  7. package/lib/types/assistant.d.ts +29 -0
  8. package/lib/types/author-assets.d.ts +25 -0
  9. package/lib/types/bookshelf.d.ts +11 -1
  10. package/lib/types/client/api.d.ts +178 -10
  11. package/lib/types/client/docx.d.ts +2 -0
  12. package/lib/types/client/index.d.ts +1 -1
  13. package/lib/types/client/llmLive.d.ts +31 -0
  14. package/lib/types/client/locales.d.ts +28 -11
  15. package/lib/types/client/panel/AdaptModeView.d.ts +5 -0
  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/AuthorAssetsView.d.ts +4 -0
  19. package/lib/types/client/panel/AuthorHome.d.ts +34 -0
  20. package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
  21. package/lib/types/client/panel/CmdPalette.d.ts +12 -0
  22. package/lib/types/client/panel/CreateBookView.d.ts +5 -1
  23. package/lib/types/client/panel/DirectorView.d.ts +7 -0
  24. package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +4 -0
  25. package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
  26. package/lib/types/client/panel/ImportModal.d.ts +7 -0
  27. package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
  28. package/lib/types/client/panel/LiveFeedLog.d.ts +13 -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/NovelPanel.d.ts +6 -1
  32. package/lib/types/client/panel/PageHeader.d.ts +18 -0
  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/PromptSlotsView.d.ts +11 -0
  36. package/lib/types/client/panel/ReasoningSection.d.ts +10 -0
  37. package/lib/types/client/panel/SettingsView.d.ts +19 -0
  38. package/lib/types/client/panel/ShelfView.d.ts +12 -1
  39. package/lib/types/client/panel/SubPage.d.ts +27 -0
  40. package/lib/types/client/panel/TensionView.d.ts +14 -0
  41. package/lib/types/client/panel/TimelineView.d.ts +14 -0
  42. package/lib/types/client/panel/toast.d.ts +2 -0
  43. package/lib/types/client/panel/views.d.ts +25 -7
  44. package/lib/types/client/run-status.d.ts +19 -0
  45. package/lib/types/client/text.d.ts +9 -0
  46. package/lib/types/docx-core.d.ts +16 -0
  47. package/lib/types/docx.d.ts +2 -3
  48. package/lib/types/engine.d.ts +185 -41
  49. package/lib/types/global-assets.d.ts +6 -0
  50. package/lib/types/index.d.ts +22 -2
  51. package/lib/types/llm-live.d.ts +75 -0
  52. package/lib/types/llm-retry.d.ts +38 -0
  53. package/lib/types/market-radar-scan.d.ts +19 -0
  54. package/lib/types/market-radar-sources.d.ts +31 -0
  55. package/lib/types/migrate-output.d.ts +25 -0
  56. package/lib/types/novel-context.d.ts +80 -0
  57. package/lib/types/prompt-slots.d.ts +57 -0
  58. package/lib/types/prompt-utils.d.ts +35 -0
  59. package/lib/types/prompting.d.ts +10 -0
  60. package/lib/types/protocol.d.ts +1311 -125
  61. package/lib/types/review-policy.d.ts +97 -0
  62. package/lib/types/routes.d.ts +4 -0
  63. package/lib/types/run.d.ts +3 -1
  64. package/lib/types/snapshots.d.ts +46 -0
  65. package/lib/types/stage-contract.d.ts +60 -0
  66. package/lib/types/tension.d.ts +65 -0
  67. package/lib/types/timeline.d.ts +37 -0
  68. package/package.json +52 -27
  69. package/scripts/patch-schemastery.mjs +47 -0
  70. package/src/action-guard.ts +127 -0
  71. package/src/ai-scan.ts +179 -0
  72. package/src/assets.ts +1806 -520
  73. package/src/assistant.ts +1098 -770
  74. package/src/author-assets.ts +149 -0
  75. package/src/bookshelf.ts +187 -151
  76. package/src/client/api.ts +758 -410
  77. package/src/client/docx.ts +37 -69
  78. package/src/client/index.ts +3 -1
  79. package/src/client/llmLive.ts +111 -0
  80. package/src/client/locales.ts +418 -383
  81. package/src/client/mount.tsx +1 -1
  82. package/src/client/panel/AdaptModeView.tsx +535 -0
  83. package/src/client/panel/AiTag.tsx +7 -0
  84. package/src/client/panel/AssetsTab.tsx +448 -458
  85. package/src/client/panel/AssistantTab.tsx +355 -301
  86. package/src/client/panel/AuthorAssetsView.tsx +206 -0
  87. package/src/client/panel/AuthorHome.tsx +309 -0
  88. package/src/client/panel/BookAnalysisView.tsx +63 -0
  89. package/src/client/panel/CmdPalette.tsx +102 -0
  90. package/src/client/panel/CreateBookView.tsx +366 -348
  91. package/src/client/panel/DirectorView.tsx +200 -0
  92. package/src/client/panel/GlobalAssetLibraryView.tsx +168 -0
  93. package/src/client/panel/IdeaInspirationView.tsx +76 -0
  94. package/src/client/panel/ImportModal.tsx +303 -0
  95. package/src/client/panel/KnowledgeBaseView.tsx +125 -0
  96. package/src/client/panel/LiveFeedLog.tsx +204 -0
  97. package/src/client/panel/MarketRadarView.tsx +377 -0
  98. package/src/client/panel/ModelManager.tsx +205 -0
  99. package/src/client/panel/NovelPanel.tsx +5517 -4597
  100. package/src/client/panel/PageHeader.tsx +37 -0
  101. package/src/client/panel/ProgressConsole.tsx +63 -0
  102. package/src/client/panel/PromptSlotsView.tsx +97 -0
  103. package/src/client/panel/ReaderView.tsx +306 -306
  104. package/src/client/panel/ReasoningSection.tsx +31 -0
  105. package/src/client/panel/RunPanel.tsx +223 -204
  106. package/src/client/panel/SettingsView.tsx +340 -0
  107. package/src/client/panel/ShelfView.tsx +266 -236
  108. package/src/client/panel/SubPage.tsx +42 -0
  109. package/src/client/panel/TensionView.tsx +196 -0
  110. package/src/client/panel/TimelineView.tsx +192 -0
  111. package/src/client/panel/WorldTab.tsx +202 -202
  112. package/src/client/panel/panel.module.css +5764 -2941
  113. package/src/client/panel/toast.ts +51 -0
  114. package/src/client/panel/views.tsx +401 -314
  115. package/src/client/run-status.ts +143 -0
  116. package/src/client/sidebar-entry.ts +1 -1
  117. package/src/client/text.ts +28 -0
  118. package/src/docx-core.ts +53 -0
  119. package/src/docx.ts +44 -83
  120. package/src/engine.ts +4672 -2787
  121. package/src/global-assets.ts +59 -0
  122. package/src/index.ts +323 -252
  123. package/src/llm-live.ts +260 -0
  124. package/src/llm-retry.ts +77 -0
  125. package/src/market-radar-scan.ts +41 -0
  126. package/src/market-radar-sources.ts +173 -0
  127. package/src/migrate-output.ts +76 -0
  128. package/src/novel-context.ts +286 -0
  129. package/src/prompt-slots.ts +125 -0
  130. package/src/prompting.ts +26 -0
  131. package/src/protocol.ts +2383 -1182
  132. package/src/review-policy.ts +161 -0
  133. package/src/routes.ts +3724 -2151
  134. package/src/run.ts +346 -330
  135. package/src/snapshots.ts +174 -0
  136. package/src/stage-contract.ts +189 -0
  137. package/src/tension.ts +240 -0
  138. package/src/timeline.ts +159 -0
  139. package/lib/client.js.map +0 -1
  140. package/lib/index.js.map +0 -1
  141. package/src/client/panel/BookshelfBar.tsx +0 -170
@@ -0,0 +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
+ 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
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * 提示词槽位:给作者留几个**安全可改**的位置,用来微调本书的写作偏好。
3
+ *
4
+ * 设计边界(很重要):
5
+ * * 槽位只追加"作者偏好",**不允许覆盖**规则工程与合规红线——道藏、写作红线、
6
+ * 九条内容合规红线、反 AI 规则、阶段契约都由宿主锁定,槽位碰不到;
7
+ * * 槽位内容随项目保存(`novel-project.json`),换书即换槽位;
8
+ * * 每个槽位有长度上限,超长直接截断并标记(沿用「截断显式化」的规则)。
9
+ */
10
+
11
+ import type { ProjectState, PromptSlotId } from './protocol.ts'
12
+
13
+ /** 单个槽位的元信息。 */
14
+ export interface PromptSlotSpec {
15
+ id: PromptSlotId
16
+ /** 面板显示名。 */
17
+ label: string
18
+ /** 一句话说明它影响什么。 */
19
+ hint: string
20
+ /** 示例(面板占位符)。 */
21
+ placeholder: string
22
+ /** 字符上限。 */
23
+ maxChars: number
24
+ }
25
+
26
+ /** 允许作者编辑的槽位(白名单;不在此列表的提示词一律不可改)。 */
27
+ export const PROMPT_SLOTS: readonly PromptSlotSpec[] = [
28
+ {
29
+ id: 'styleExtra',
30
+ label: '文风补充',
31
+ hint: '追加到写作提示词末尾的风格偏好(句式、叙述习惯、用词倾向)。',
32
+ placeholder: '例如:多用短句和对话推进,少用成语;环境描写只写能影响动作的部分。',
33
+ maxChars: 1200,
34
+ },
35
+ {
36
+ id: 'antiAiExtra',
37
+ label: '反 AI 补充',
38
+ hint: '本书额外的"别这么写"清单,与内置反 AI 规则叠加(不能覆盖内置规则)。',
39
+ placeholder: '例如:不要每段都用比喻;不要用"空气中弥漫着"这类句式。',
40
+ maxChars: 1200,
41
+ },
42
+ {
43
+ id: 'dialogueExtra',
44
+ label: '对话补充',
45
+ hint: '角色说话方式的额外约束,写入时会与角色卡一起注入。',
46
+ placeholder: '例如:长辈说话带方言词;主角对熟人用绰号称呼。',
47
+ maxChars: 800,
48
+ },
49
+ {
50
+ id: 'endingHookExtra',
51
+ label: '章末钩子偏好',
52
+ hint: '对结尾钩子的额外要求(与章节计划里的 endingHook 叠加)。',
53
+ placeholder: '例如:结尾尽量落在一个未闭合的选择上,而不是新出现的敌人。',
54
+ maxChars: 600,
55
+ },
56
+ ]
57
+
58
+ /** 槽位 id → 规格。 */
59
+ const SLOT_BY_ID = new Map(PROMPT_SLOTS.map(s => [s.id, s] as const))
60
+
61
+ /** 单个槽位的渲染结果。 */
62
+ export interface RenderedSlot {
63
+ id: PromptSlotId
64
+ label: string
65
+ text: string
66
+ /** 是否因超长被截断。 */
67
+ truncated: boolean
68
+ /** 原文长度(截断前)。 */
69
+ chars: number
70
+ }
71
+
72
+ /**
73
+ * 读取并规整项目里的槽位内容(去空白、按上限截断)。
74
+ *
75
+ * @param project 项目状态。
76
+ * @returns 非空槽位列表(保持 PROMPT_SLOTS 的声明顺序)。
77
+ */
78
+ export function readPromptSlots(project: ProjectState): RenderedSlot[] {
79
+ const stored = project.promptSlots ?? {}
80
+ const out: RenderedSlot[] = []
81
+ for (const spec of PROMPT_SLOTS) {
82
+ const raw = (stored[spec.id] ?? '').trim()
83
+ if (raw === '') continue
84
+ const truncated = raw.length > spec.maxChars
85
+ out.push({
86
+ id: spec.id,
87
+ label: spec.label,
88
+ text: truncated ? raw.slice(0, spec.maxChars) : raw,
89
+ truncated,
90
+ chars: raw.length,
91
+ })
92
+ }
93
+ return out
94
+ }
95
+
96
+ /**
97
+ * 把槽位渲染成注入提示词的段落。
98
+ *
99
+ * @param project 项目状态。
100
+ * @returns 提示词片段;无槽位时返回空串。
101
+ */
102
+ export function renderPromptSlots(project: ProjectState): string {
103
+ const slots = readPromptSlots(project)
104
+ if (slots.length === 0) return ''
105
+ const lines = ['==================== 作者偏好(本书自定义,优先级低于道藏/红线/合规) ====================']
106
+ for (const slot of slots) {
107
+ lines.push(`【${slot.label}】${slot.text}${slot.truncated ? `(已按 ${SLOT_BY_ID.get(slot.id)?.maxChars ?? 0} 字上限截断,原 ${slot.chars} 字)` : ''}`)
108
+ }
109
+ lines.push('以上偏好只影响表达方式;与道藏、写作红线、内容合规红线冲突时,一律以后者为准。')
110
+ return lines.join('\n')
111
+ }
112
+
113
+ /**
114
+ * 校验并规整一次槽位写入。
115
+ *
116
+ * @param id 槽位 id。
117
+ * @param value 内容(空串表示清空该槽位)。
118
+ * @returns 归一化后的内容。
119
+ * @throws 槽位 id 不在白名单时抛错。
120
+ */
121
+ export function normalizeSlotValue(id: string, value: string): string {
122
+ const spec = SLOT_BY_ID.get(id as PromptSlotId)
123
+ if (spec === undefined) throw new Error(`未知提示词槽位:${id}`)
124
+ return value.replace(/\r\n/g, '\n').trim()
125
+ }
@@ -0,0 +1,26 @@
1
+ /** 章节写作约束:把写作骨架渲染成可附加到系统提示词末尾的必达/禁止/自查块。 */
2
+ export function renderChapterWriterSkeleton(meta: {
3
+ targetChars: number
4
+ minChars: number
5
+ maxChars: number
6
+ pov?: string
7
+ tonePreference?: string
8
+ endingHookPreference?: string
9
+ antiAiRules?: string
10
+ }): string {
11
+ const pov = meta.pov ?? '第三人称有限视角,严格跟随主角所见所知。'
12
+ const tone = meta.tonePreference ?? '文风贴合本书设定,用具体细节与动作推进。'
13
+ const hook = meta.endingHookPreference ?? '章末留一个明确的钩子(新信息、新风险或未闭合的选择)。'
14
+ const antiAi = meta.antiAiRules ?? ''
15
+ return [
16
+ '==================== 章节生成骨架(必达 / 禁止 / 输出前自查) ====================',
17
+ '【叙事视角】' + pov,
18
+ '【本章必达】本章必须发生实质变化——局面、关系、信息、风险、决策至少动一项;开篇直接进入情境,不复述上一章。',
19
+ '【禁止事项】不写以总结、复盘、解释为主的段落;不新增核心角色,不写入与上下文冲突的设定;不为凑字数注水。',
20
+ '【篇幅】目标 ' + meta.targetChars + ' 字,区间 ' + meta.minChars + '-' + meta.maxChars + ' 字;不够就继续推进有效情节,不许草草收尾。',
21
+ '【结尾】' + hook,
22
+ '【表达】用具体动作、对话与可感知细节往前推;' + tone,
23
+ '【反 AI】' + antiAi,
24
+ '【输出前自查】逐项确认:读者的回报、关键转折、章末净变化是否看得见?上一章留下的钩子有没有交代?人物的硬事实有没有被写反?确认无误再输出,正文里不要出现核查过程。',
25
+ ].join('\n')
26
+ }