@waterwx/dsh-novel-forge 0.1.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 (50) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +180 -0
  3. package/cordis.patch.yml +13 -0
  4. package/lib/client.js +3924 -0
  5. package/lib/client.js.map +1 -0
  6. package/lib/index.js +3120 -0
  7. package/lib/index.js.map +1 -0
  8. package/lib/types/assets.d.ts +35 -0
  9. package/lib/types/assistant.d.ts +43 -0
  10. package/lib/types/bookshelf.d.ts +35 -0
  11. package/lib/types/client/api.d.ts +68 -0
  12. package/lib/types/client/docx.d.ts +15 -0
  13. package/lib/types/client/index.d.ts +14 -0
  14. package/lib/types/client/locales.d.ts +139 -0
  15. package/lib/types/client/mount.d.ts +9 -0
  16. package/lib/types/client/panel/AssetsTab.d.ts +7 -0
  17. package/lib/types/client/panel/AssistantTab.d.ts +7 -0
  18. package/lib/types/client/panel/BookshelfBar.d.ts +11 -0
  19. package/lib/types/client/panel/NovelPanel.d.ts +13 -0
  20. package/lib/types/client/panel/controller.d.ts +19 -0
  21. package/lib/types/client/panel/helpers.d.ts +8 -0
  22. package/lib/types/client/sidebar-entry.d.ts +13 -0
  23. package/lib/types/docx.d.ts +19 -0
  24. package/lib/types/engine.d.ts +95 -0
  25. package/lib/types/index.d.ts +55 -0
  26. package/lib/types/protocol.d.ts +521 -0
  27. package/lib/types/routes.d.ts +29 -0
  28. package/package.json +105 -0
  29. package/src/assets.ts +518 -0
  30. package/src/assistant.ts +547 -0
  31. package/src/bookshelf.ts +137 -0
  32. package/src/client/api.ts +254 -0
  33. package/src/client/css-modules.d.ts +8 -0
  34. package/src/client/docx.ts +69 -0
  35. package/src/client/index.ts +34 -0
  36. package/src/client/locales.ts +271 -0
  37. package/src/client/mount.tsx +97 -0
  38. package/src/client/panel/AssetsTab.tsx +341 -0
  39. package/src/client/panel/AssistantTab.tsx +188 -0
  40. package/src/client/panel/BookshelfBar.tsx +116 -0
  41. package/src/client/panel/NovelPanel.tsx +990 -0
  42. package/src/client/panel/controller.ts +45 -0
  43. package/src/client/panel/helpers.ts +17 -0
  44. package/src/client/panel/panel.module.css +894 -0
  45. package/src/client/sidebar-entry.ts +122 -0
  46. package/src/docx.ts +83 -0
  47. package/src/engine.ts +1019 -0
  48. package/src/index.ts +184 -0
  49. package/src/protocol.ts +539 -0
  50. package/src/routes.ts +955 -0
package/src/assets.ts ADDED
@@ -0,0 +1,518 @@
1
+ /**
2
+ * Writing assets — 题材基底库 / 推进模式库 / 反 AI 规则 / 写法引擎.
3
+ *
4
+ * Ported from AI-Novel-Writing-Assistant (Apache-2.0) built-in seed data:
5
+ * - 8 preset style templates (DEFAULT_STYLE_TEMPLATES)
6
+ * - 12 anti-AI rules with concrete detect patterns (DEFAULT_ANTI_AI_RULES)
7
+ * - genre tree + progression mode seeds
8
+ * Assets persist with the project (novel-project.json) and are injected into
9
+ * generation / planning / review prompts, and available to the AI assistant.
10
+ */
11
+
12
+ import type { AntiAiRule, GenreNode, ProgressionMode, ProjectAssets, StyleAsset, StyleTemplate } from './protocol.ts'
13
+
14
+ // ------------------------------------------------------ built-in style templates
15
+
16
+ /** 预置写法模板(来自 AI-Novel-Writing-Assistant 内置 DEFAULT_STYLE_TEMPLATES)。 */
17
+ export const BUILTIN_STYLE_TEMPLATES: StyleTemplate[] = [
18
+ {
19
+ key: 'power-up-escalation',
20
+ name: '爽文递进推进流',
21
+ description: '持续升级冲突和收益点,强化目标推进与爽点兑现。',
22
+ category: '爽文流',
23
+ applicableGenres: ['都市', '玄幻', '热血'],
24
+ proseRules: [
25
+ '围绕目标推进,尽快兑现局部收益;每段都要有目标推进或爽点兑现。',
26
+ '保持明确因果和节奏抬升,场景单元按「目标→阻碍→压制→反转收益」推进。',
27
+ '优先冲突和结果,少停留;段尾用钩子收束。',
28
+ ],
29
+ dialogueRules: [
30
+ '角色表达直接,情绪跟随胜负切换。',
31
+ '对话承担推进与信息功能,但保留角色自己的语气差异。',
32
+ ],
33
+ languageRules: [
34
+ '句式清晰,减少无效分散信息。',
35
+ '直接、明确,不做无谓铺垫。',
36
+ ],
37
+ rhythmRules: [
38
+ '快节奏,段落密度中等,动作先于解释。',
39
+ '尽快兑现局部收益,避免拖沓。',
40
+ ],
41
+ defaultAntiAiRuleKeys: ['禁止总结主题', '对话纯功能推进', '连续三段解释性叙事'],
42
+ },
43
+ {
44
+ key: 'bottom-loop-reality',
45
+ name: '底层循环现实流',
46
+ description: '通过碎片化生活与反复落空表现人物困境。',
47
+ category: '现实流',
48
+ applicableGenres: ['都市', '现实', '成长'],
49
+ proseRules: [
50
+ '以时间推进和现实落差构成叙事张力,结尾不解决核心困境。',
51
+ '场景单元按「行为→落差→自我合理化」推进。',
52
+ '以碎片化生活推进,不做总括式回顾。',
53
+ ],
54
+ dialogueRules: [
55
+ '人物情绪通过动作和嘴硬表达,允许短促口语化台词。',
56
+ '对话保留生活杂音与无效信息。',
57
+ ],
58
+ languageRules: [
59
+ '语言粗粝、口语化,允许生活杂音与不完整句。',
60
+ '句子变化度高,允许无意义细节。',
61
+ ],
62
+ rhythmRules: [
63
+ '段落密实,动作先于解释。',
64
+ '中快节奏,允许碎片化流动。',
65
+ ],
66
+ defaultAntiAiRuleKeys: ['禁止解释型心理描写', '禁止段尾升华', '鼓励无意义小动作', '鼓励现实落差', '鼓励嘴硬补偿'],
67
+ },
68
+ {
69
+ key: 'suspense-pressure',
70
+ name: '悬疑压迫递增流',
71
+ description: '通过信息遮蔽、细节异常和压力叠加制造不安感。',
72
+ category: '悬疑流',
73
+ applicableGenres: ['悬疑', '惊悚', '现实'],
74
+ proseRules: [
75
+ '以异常细节、信息差和节奏收束推动悬念层层加压。',
76
+ '场景单元按「现场细节→异常→误判→新风险」推进。',
77
+ '优先制造信息缺口和压迫氛围。',
78
+ ],
79
+ dialogueRules: [
80
+ '角色反应克制,恐惧通过反应显现。',
81
+ '对话保留克制感,不解释恐惧来源。',
82
+ ],
83
+ languageRules: [
84
+ '细节精确,保留少量噪音增强现场感。',
85
+ '克制、中等偏高句变化。',
86
+ ],
87
+ rhythmRules: [
88
+ '通过节奏收束和信息延迟制造压力。',
89
+ '中速,段落密度中等偏高。',
90
+ ],
91
+ defaultAntiAiRuleKeys: ['禁止解释型心理描写', '禁止总结主题', '段落长度过于整齐', '鼓励现实落差'],
92
+ },
93
+ {
94
+ key: 'emotional-tension',
95
+ name: '情绪拉扯流',
96
+ description: '通过错位表达、停顿和误读制造关系张力。',
97
+ category: '情感流',
98
+ applicableGenres: ['言情', '都市', '群像'],
99
+ proseRules: [
100
+ '人物不直说核心情绪,靠误读、停顿和反应推动关系变化。',
101
+ '场景单元按「动作→言外之意→误读→回避」推进。',
102
+ '以关系错位推进,而非直接说明。',
103
+ ],
104
+ dialogueRules: [
105
+ '情绪通过停顿、动作和言外之意体现。',
106
+ '对话充满潜台词与试探。',
107
+ ],
108
+ languageRules: [
109
+ '语言自然,允许留白与停顿。',
110
+ '句子变化度高,允许无意义细节。',
111
+ ],
112
+ rhythmRules: [
113
+ '给关系反应留空间,但避免空洞抒情。',
114
+ '中慢节奏,段落密度中等。',
115
+ ],
116
+ defaultAntiAiRuleKeys: ['禁止直接说教', '禁止段尾升华', '对话纯功能推进', '鼓励无意义小动作'],
117
+ },
118
+ {
119
+ key: 'ensemble-weave',
120
+ name: '群像交织流',
121
+ description: '以多人行动线和视角差异交织推进事件。',
122
+ category: '群像流',
123
+ applicableGenres: ['群像', '都市', '悬疑'],
124
+ proseRules: [
125
+ '多角色并行推进,但每个角色的表达和认知范围必须区分清楚。',
126
+ '多线并进,但视角切换要受控。',
127
+ ],
128
+ dialogueRules: [
129
+ '不同角色口吻必须拉开差异,避免所有人说话一样。',
130
+ ],
131
+ languageRules: [
132
+ '保持角色差异,句式变化度高。',
133
+ '减少无效分散信息。',
134
+ ],
135
+ rhythmRules: [
136
+ '多线交织但节奏不乱,平衡推进。',
137
+ '动作先于解释。',
138
+ ],
139
+ defaultAntiAiRuleKeys: ['对话纯功能推进', '句式重复率过高', '禁止总结主题'],
140
+ },
141
+ {
142
+ key: 'immersive-daily',
143
+ name: '日常浸没流',
144
+ description: '通过生活细节和细微情绪变化建立持续沉浸感。',
145
+ category: '日常流',
146
+ applicableGenres: ['日常', '治愈', '都市'],
147
+ proseRules: [
148
+ '重场景体验和关系温度,核心情绪通过场景自然流出。',
149
+ '允许保留生活性动作和无效信息。',
150
+ ],
151
+ dialogueRules: [
152
+ '人物表达自然,不用高强度戏剧句。',
153
+ '对话保留生活气息。',
154
+ ],
155
+ languageRules: [
156
+ '保留生活细节和杂音,不追求工整。',
157
+ '口语化,句子变化中等偏高。',
158
+ ],
159
+ rhythmRules: [
160
+ '慢节奏沉浸,但避免空转。',
161
+ '允许碎片化流动。',
162
+ ],
163
+ defaultAntiAiRuleKeys: ['禁止段尾升华', '段落长度过于整齐', '鼓励无意义小动作'],
164
+ },
165
+ {
166
+ key: 'cold-professional',
167
+ name: '冷峻专业流',
168
+ description: '以专业事实和行业细节压住情绪,形成克制压力感。',
169
+ category: '专业流',
170
+ applicableGenres: ['职场', '现实', '悬疑'],
171
+ proseRules: [
172
+ '行业事实和程序细节优先,情绪不直说,信息密度高于抒情密度。',
173
+ '场景单元按「事实→动作→专业判断→后果」推进。',
174
+ '让专业事实承担叙事重量。',
175
+ ],
176
+ dialogueRules: [
177
+ '情绪藏在专业动作和事实选择里。',
178
+ '对话以信息性表达为主,克制。',
179
+ ],
180
+ languageRules: [
181
+ '术语和事实优先,避免廉价金句。',
182
+ '正式、克制的语言。',
183
+ ],
184
+ rhythmRules: [
185
+ '信息密度高,但不铺张解释。',
186
+ '平衡节奏,段落密度中等偏高。',
187
+ ],
188
+ defaultAntiAiRuleKeys: ['禁止直接说教', '禁止总结主题', '句式重复率过高'],
189
+ },
190
+ {
191
+ key: 'absurd-dark-humor',
192
+ name: '荒诞黑色幽默流',
193
+ description: '通过反差、冷感观察和荒诞细节制造黑色幽默。',
194
+ category: '黑色幽默',
195
+ applicableGenres: ['都市', '黑色幽默', '现实'],
196
+ proseRules: [
197
+ '用反差和荒诞细节放大现实困境,笑点和压迫感同时存在。',
198
+ '场景单元按「现实细节→荒诞偏差→冷反应」推进。',
199
+ '依赖反差和冷感观察,而非热闹吐槽。',
200
+ ],
201
+ dialogueRules: [
202
+ '情绪藏在冷反应和嘴硬里。',
203
+ '台词冷面、口语化,允许自嘲与转移。',
204
+ ],
205
+ languageRules: [
206
+ '允许夹带荒诞杂质和冷幽默节奏。',
207
+ '口语化,句子变化度高。',
208
+ ],
209
+ rhythmRules: [
210
+ '反差点要快落地,不要解释笑点。',
211
+ '平衡节奏,段落密度中等偏高。',
212
+ ],
213
+ defaultAntiAiRuleKeys: ['禁止解释型心理描写', '禁止段尾升华', '鼓励现实落差', '鼓励嘴硬补偿'],
214
+ },
215
+ ]
216
+
217
+ // ---------------------------------------------------------- built-in rules
218
+
219
+ /** 内置全局反 AI 规则(来自 AI-Novel-Writing-Assistant 内置 DEFAULT_ANTI_AI_RULES)。 */
220
+ export const BUILTIN_ANTI_AI_RULES: AntiAiRule[] = [
221
+ {
222
+ name: '禁止解释型心理描写',
223
+ avoid: '直接使用"他感到""他意识到""他明白了"等句式解释人物心理。',
224
+ fix: '把心理解释改成动作、语气、停顿、环境反应或结果。',
225
+ detectPatterns: ['他感到', '她感到', '他意识到', '她意识到', '他明白了', '她明白了'],
226
+ builtin: true,
227
+ },
228
+ {
229
+ name: '禁止段尾升华',
230
+ avoid: '在段尾或收尾处用总结句升华主题(如"生活就是""命运总会""说到底")。',
231
+ fix: '删除升华句,回到具体动作、现场或悬而未决的处境。',
232
+ detectPatterns: ['生活就是', '命运总会', '归根结底', '说到底', '这就是'],
233
+ builtin: true,
234
+ },
235
+ {
236
+ name: '禁止总结主题',
237
+ avoid: '把段落写成总结中心思想或提炼人生道理(如"这说明""这意味着")。',
238
+ fix: '删掉主题总结,让信息通过事件和结果自然显现。',
239
+ detectPatterns: ['这说明', '这意味着', '归根结底', '其实就是'],
240
+ builtin: true,
241
+ },
242
+ {
243
+ name: '禁止直接说教',
244
+ avoid: '作者替角色或读者做直接价值判断和说教(如"我们都应该""人总要学会")。',
245
+ fix: '改成角色具体处境或对话,不做抽象说教。',
246
+ detectPatterns: ['我们都应该', '人总要学会', '真正重要的是'],
247
+ builtin: true,
248
+ },
249
+ {
250
+ name: '段落长度过于整齐',
251
+ avoid: '段落长度和节奏过于平均,产生 AI 作文感。',
252
+ fix: '打破段落长度均衡,让句子和段落有自然起伏。',
253
+ detectPatterns: [],
254
+ builtin: true,
255
+ },
256
+ {
257
+ name: '连续三段解释性叙事',
258
+ avoid: '连续几段只有解释没有动作,削弱现场感。',
259
+ fix: '插入动作、对话、环境反馈,减少连段说明。',
260
+ detectPatterns: [],
261
+ builtin: true,
262
+ },
263
+ {
264
+ name: '对话纯功能推进',
265
+ avoid: '对话只有信息推进,没有人物语气和生活噪音(如"告诉你""我们现在要")。',
266
+ fix: '补入停顿、绕弯、语气差异和无效信息。',
267
+ detectPatterns: ['告诉你', '我们现在要', '接下来就'],
268
+ builtin: true,
269
+ },
270
+ {
271
+ name: '句式重复率过高',
272
+ avoid: '连续句式过于整齐(如"首先""然后""接着""最后"),显得机械。',
273
+ fix: '拉开句式长度和起句方式,打散结构。',
274
+ detectPatterns: ['首先', '然后', '接着', '最后'],
275
+ builtin: true,
276
+ },
277
+ {
278
+ name: 'AI 高频套话',
279
+ avoid: '滥用"不禁""仿佛""一时间""不由得""顿时""然而""缓缓""轻轻""微微""似乎""终于"等模式词及套路比喻。',
280
+ fix: '用具体、有画面感的表达替换套话;每个比喻都应当是新造的。',
281
+ detectPatterns: ['不禁', '仿佛', '一时间', '不由得', '顿时', '缓缓', '轻轻', '微微'],
282
+ builtin: true,
283
+ },
284
+ {
285
+ name: '鼓励无意义小动作',
286
+ avoid: '(鼓励类)全篇缺少真实但不推动主线的小动作,人物显得空洞。',
287
+ fix: '补入挠头、点烟、抠包装、挪椅子等小动作,增加人味与生活感。',
288
+ detectPatterns: [],
289
+ builtin: true,
290
+ },
291
+ {
292
+ name: '鼓励现实落差',
293
+ avoid: '(鼓励类)人物预期和现实结果完全一致,缺少落差。',
294
+ fix: '补出人物预期与实际结果之间的差距,制造张力。',
295
+ detectPatterns: [],
296
+ builtin: true,
297
+ },
298
+ {
299
+ name: '鼓励嘴硬补偿',
300
+ avoid: '(鼓励类)人物吃瘪后没有维持体面的反应。',
301
+ fix: '给角色补一句嘴硬找补或自我合理化,保持人设温度。',
302
+ detectPatterns: [],
303
+ builtin: true,
304
+ },
305
+ ]
306
+
307
+ /** 内置题材基底库(常用网文题材树,跨书复用)。 */
308
+ export const BUILTIN_GENRE_LIBRARY: GenreNode[] = [
309
+ {
310
+ name: '仙侠修真',
311
+ description: '以修仙境界、宗门斗争、法宝丹药为核心,读者期待从凡人到强者的成长与长生问道。',
312
+ children: [
313
+ { name: '凡人流', description: '资质平凡、步步为营,靠资源积累与心机博弈逆袭,强调真实感与代入感。', children: [] },
314
+ { name: '苟道流', description: '主角苟且发育、藏锋敛芒,坐收渔利,强调生存智慧与反差爽点。', children: [] },
315
+ { name: '争霸流', description: '宗门、王朝或大陆争锋,主角由弱到强整合势力,强调格局与权谋。', children: [] },
316
+ ],
317
+ },
318
+ {
319
+ name: '都市异能',
320
+ description: '现代都市背景叠加超能力,读者期待隐藏身份、扮猪吃虎与日常反差。',
321
+ children: [
322
+ { name: '异能升级', description: '觉醒超能力后不断变强,隐藏于都市,遇敌碾压。', children: [] },
323
+ { name: '重生复仇', description: '重生回到过去,利用先知先觉改变命运、清算仇敌。', children: [] },
324
+ { name: '商业经营', description: '以超能力或见识经商扩张,建立商业帝国,强调经营爽感。', children: [] },
325
+ ],
326
+ },
327
+ {
328
+ name: '悬疑推理',
329
+ description: '以谜题、案件与真相揭露为核心,读者期待线索层层展开与反转。',
330
+ children: [
331
+ { name: '本格推理', description: '公平线索、逻辑推演,读者可与主角一同解谜。', children: [] },
332
+ { name: '刑侦探案', description: '警察或侦探视角连续破案,案件串联主线,强调现实与人性。', children: [] },
333
+ { name: '无限流', description: '主角穿梭于不同副本世界解谜求生,副本之间累积成长。', children: [] },
334
+ ],
335
+ },
336
+ {
337
+ name: '玄幻奇幻',
338
+ description: '异世界或架空大陆的冒险成长,读者期待宏大世界观、奇遇与战力突破。',
339
+ children: [
340
+ { name: '学院流', description: '入学修炼、同窗竞争、大赛扬名,强调青春感与阶梯式打脸。', children: [] },
341
+ { name: '废柴逆袭', description: '开局废柴受辱,觉醒金手指后一路逆袭打脸,强调反差与爽点。', children: [] },
342
+ { name: '诸天万界', description: '穿越诸天世界收集资源与能力,强调世界多样性与成长曲线。', children: [] },
343
+ ],
344
+ },
345
+ {
346
+ name: '历史军事',
347
+ description: '以历史时代为背景的争霸、谋略或军旅故事,读者期待权谋博弈与时代质感。',
348
+ children: [
349
+ { name: '王朝争霸', description: '乱世崛起、招贤纳士、逐鹿天下,强调战略与人心。', children: [] },
350
+ { name: '穿越种田', description: '穿越古代发展生产、经营家族,强调建设感与生活细节。', children: [] },
351
+ ],
352
+ },
353
+ {
354
+ name: '末世科幻',
355
+ description: '末世危机或科幻设定下的生存与重建,读者期待资源管理、危机升级与人性考验。',
356
+ children: [
357
+ { name: '基地经营', description: '建立基地、收集资源、抵御危机,强调建设与扩张。', children: [] },
358
+ { name: '进化觉醒', description: '末世异变中觉醒能力不断进化,强调战力成长与危机求生。', children: [] },
359
+ ],
360
+ },
361
+ ]
362
+
363
+ /** 内置常用推进模式。 */
364
+ export const BUILTIN_PROGRESSION_MODES: ProgressionMode[] = [
365
+ {
366
+ name: '升级变强',
367
+ driver: '主角的实力、境界或能力持续增长,读者期待每次突破带来的碾压与认可。',
368
+ readerExpectation: '每隔几章有一次明确的实力提升或打脸兑现;大境界突破要有仪式感。',
369
+ payoffs: ['突破境界', '学会新技能', '越级战胜强敌', '当众打脸质疑者'],
370
+ risks: ['升级重复套路', '战力膨胀失控', '无铺垫强行突破'],
371
+ primary: false,
372
+ },
373
+ {
374
+ name: '经营扩张',
375
+ driver: '主角的产业、势力或领地不断扩张,资源复利滚雪球。',
376
+ readerExpectation: '经营投入有可感知的回报,扩张遇到新挑战并解决。',
377
+ payoffs: ['新产业上线', '规模翻倍', '吞并对手', '资源闭环成型'],
378
+ risks: ['过程枯燥', '扩张无阻力', '数值失衡'],
379
+ primary: false,
380
+ },
381
+ {
382
+ name: '解谜揭露',
383
+ driver: '主线谜团(身世、阴谋、世界观真相)持续牵引读者,每揭开一层又引出更深一层。',
384
+ readerExpectation: '定期有真相碎片放出,回收旧伏笔、埋设新伏笔。',
385
+ payoffs: ['伏笔回收', '身份揭露', '阴谋浮出水面', '反转打脸'],
386
+ risks: ['谜题拖太久', '伏笔忘记回收', '反转生硬'],
387
+ primary: false,
388
+ },
389
+ {
390
+ name: '渔翁得利',
391
+ driver: '强敌相互厮杀,主角躲在暗处观察、收割,风险由他人承担、果实由主角获取。',
392
+ readerExpectation: '冲突升级时主角以最小代价获取最大收益,且不暴露自身。',
393
+ payoffs: ['坐收渔利', '捡漏宝物', '敌人两败俱伤', '信息差获利'],
394
+ risks: ['重复套路', '收割太轻易', '主角全程无风险'],
395
+ primary: false,
396
+ },
397
+ {
398
+ name: '关系拉扯',
399
+ driver: '人物关系(知己、对手、师徒、情感线)的张力与变化持续推动剧情。',
400
+ readerExpectation: '关系有进有退、有误会与和解,情绪起伏带动阅读欲。',
401
+ payoffs: ['关系升温', '信任建立', '背叛与挽回', '并肩作战'],
402
+ risks: ['情感线停滞', '工业糖精', '为虐而虐'],
403
+ primary: false,
404
+ },
405
+ ]
406
+
407
+ // ------------------------------------------------------------- persistence
408
+
409
+ /** 默认(空)项目写作资产。 */
410
+ export function emptyProjectAssets(): ProjectAssets {
411
+ return {
412
+ auxiliaryProgressions: [],
413
+ antiAiRules: [],
414
+ styleAssets: [],
415
+ }
416
+ }
417
+
418
+ /** 合并项目资产与内置库:返回「生效的反 AI 规则」(内置全局 + 项目自定义)。 */
419
+ export function effectiveAntiAiRules(assets: ProjectAssets | undefined): AntiAiRule[] {
420
+ const custom = assets?.antiAiRules ?? []
421
+ const customNames = new Set(custom.map(r => r.name))
422
+ return [...BUILTIN_ANTI_AI_RULES.filter(r => !customNames.has(r.name)), ...custom]
423
+ }
424
+
425
+ /** 把生效规则渲染成提示词块。 */
426
+ export function renderAntiAiRules(assets: ProjectAssets | undefined): string {
427
+ const rules = effectiveAntiAiRules(assets)
428
+ if (rules.length === 0) return ''
429
+ return [
430
+ '==================== 反 AI 规则(写作时必须遵守的表达边界) ====================',
431
+ ...rules.map(r => `- ${r.name}:避免——${r.avoid};修正——${r.fix}`),
432
+ ].join('\n')
433
+ }
434
+
435
+ /** 渲染题材与推进模式提示词块。 */
436
+ export function renderGenreAndProgression(assets: ProjectAssets | undefined): string {
437
+ const sections: string[] = []
438
+ if (assets?.genre !== undefined) {
439
+ sections.push('==================== 题材基底(本书的题材定位与读者期待) ====================')
440
+ sections.push(`题材:${assets.genre.name}`)
441
+ if (assets.genre.description !== '') sections.push(`读者期待:${assets.genre.description}`)
442
+ const walk = (node: GenreNode, depth: number): void => {
443
+ for (const child of node.children) {
444
+ sections.push(`${' '.repeat(depth)}- ${child.name}:${child.description}`)
445
+ walk(child, depth + 1)
446
+ }
447
+ }
448
+ walk(assets.genre, 1)
449
+ }
450
+ const modes = [
451
+ ...(assets?.primaryProgression !== undefined ? [assets.primaryProgression] : []),
452
+ ...(assets?.auxiliaryProgressions ?? []),
453
+ ]
454
+ if (modes.length > 0) {
455
+ sections.push('==================== 推进模式(读者为什么继续看) ====================')
456
+ for (const mode of modes) {
457
+ const tag = mode.primary ? '(主推进)' : '(辅助)'
458
+ sections.push(`- 模式「${mode.name}」${tag}:驱动力——${mode.driver}`)
459
+ sections.push(` 读者期待:${mode.readerExpectation}`)
460
+ if (mode.payoffs.length > 0) sections.push(` 常见兑现:${mode.payoffs.join('、')}`)
461
+ if (mode.risks.length > 0) sections.push(` 节奏风险(避免):${mode.risks.join('、')}`)
462
+ }
463
+ }
464
+ return sections.join('\n')
465
+ }
466
+
467
+ /** 渲染写法资产提示词块。 */
468
+ export function renderStyleAssets(assets: ProjectAssets | undefined): string {
469
+ const styles = assets?.styleAssets ?? []
470
+ if (styles.length === 0) return ''
471
+ const sections: string[] = ['==================== 写法资产(本书的叙事风格约束) ====================']
472
+ for (const style of styles) {
473
+ sections.push(`【${style.name}】`)
474
+ if (style.proseRules.length > 0) sections.push('叙述与节奏:\n' + style.proseRules.map(r => `- ${r}`).join('\n'))
475
+ if (style.dialogueRules.length > 0) sections.push('台词风格:\n' + style.dialogueRules.map(r => `- ${r}`).join('\n'))
476
+ if (style.descriptionRules.length > 0) sections.push('描写与情绪:\n' + style.descriptionRules.map(r => `- ${r}`).join('\n'))
477
+ if (style.boundaries.length > 0) sections.push('表达边界:\n' + style.boundaries.map(r => `- ${r}`).join('\n'))
478
+ }
479
+ return sections.join('\n')
480
+ }
481
+
482
+ /** 渲染全部写作资产提示词(供生成/规划/审稿注入)。 */
483
+ export function renderAllAssets(assets: ProjectAssets | undefined): string {
484
+ const parts = [
485
+ renderGenreAndProgression(assets),
486
+ renderStyleAssets(assets),
487
+ renderAntiAiRules(assets),
488
+ ].filter(part => part !== '')
489
+ return parts.join('\n\n')
490
+ }
491
+
492
+ /** 预置写法模板 → 可直接绑定的 StyleAsset。 */
493
+ export function styleTemplateToAsset(template: StyleTemplate): StyleAsset {
494
+ return {
495
+ name: template.name,
496
+ proseRules: [...template.proseRules, ...template.rhythmRules.map(r => `节奏:${r}`)],
497
+ dialogueRules: template.dialogueRules,
498
+ descriptionRules: template.languageRules,
499
+ boundaries: [`模板「${template.name}」适用题材:${template.applicableGenres.join('、')}`, '不要违背模板的叙事单元结构与节奏约束'],
500
+ createdAt: new Date().toISOString(),
501
+ }
502
+ }
503
+
504
+ // ------------------------------------------------------- style extraction
505
+
506
+ /** 写法引擎:从样本文本提取风格资产的系统提示词。 */
507
+ export function styleEngineSystemPrompt(): string {
508
+ return [
509
+ '你是一位资深网文文风分析师。你会收到一段样本文本,请提炼出可复用的叙事风格规则,供后续章节保持同一种味道。',
510
+ '要求:',
511
+ '1. 从样本中归纳,不要泛泛而谈;每条规则都要能落到具体写法(句式、用词、视角、节奏、对话方式、描写密度)。',
512
+ '2. 台词风格要说明角色说话的语气特征与常用表达方式。',
513
+ '3. 表达边界要写明这段风格「不会怎么做」(如:不用华丽辞藻、不写长段心理独白、不用成语堆砌)。',
514
+ '4. 输出必须是合法 JSON 对象,不要输出任何其他文字。',
515
+ 'JSON 结构:',
516
+ '{"proseRules": ["叙述视角与句式节奏规则"], "dialogueRules": ["台词风格规则"], "descriptionRules": ["描写密度与情绪表达规则"], "boundaries": ["表达边界"]}',
517
+ ].join('\n')
518
+ }