@waterwx/dsh-novel-forge 2.0.1-alpha → 2.1.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 (135) hide show
  1. package/README.md +97 -2
  2. package/lib/client.js +3291 -752
  3. package/lib/index.js +3557 -237
  4. package/lib/types/assets.d.ts +16 -3
  5. package/lib/types/assistant.d.ts +4 -0
  6. package/lib/types/client/api.d.ts +89 -0
  7. package/lib/types/client/llmLive.d.ts +33 -0
  8. package/lib/types/client/locales.d.ts +3 -3
  9. package/lib/types/client/panel/AdaptModeView.d.ts +1 -1
  10. package/lib/types/client/panel/AssetsTab.d.ts +1 -1
  11. package/lib/types/client/panel/AssistantTab.d.ts +1 -1
  12. package/lib/types/client/panel/AuthorAssetsView.d.ts +1 -1
  13. package/lib/types/client/panel/AuthorHome.d.ts +4 -2
  14. package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
  15. package/lib/types/client/panel/BookshelfBar.d.ts +1 -1
  16. package/lib/types/client/panel/CreateBookView.d.ts +6 -2
  17. package/lib/types/client/panel/DirectorView.d.ts +7 -0
  18. package/lib/types/client/panel/FlowGuide.d.ts +1 -1
  19. package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +1 -1
  20. package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
  21. package/lib/types/client/panel/ImportModal.d.ts +1 -1
  22. package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
  23. package/lib/types/client/panel/LiveFeedLog.d.ts +5 -0
  24. package/lib/types/client/panel/MangaRoleLibrary.d.ts +1 -1
  25. package/lib/types/client/panel/MangaWorkspace.d.ts +1 -1
  26. package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
  27. package/lib/types/client/panel/NovelPanel.d.ts +2 -2
  28. package/lib/types/client/panel/ProgressConsole.d.ts +1 -1
  29. package/lib/types/client/panel/ProgressHomeView.d.ts +1 -1
  30. package/lib/types/client/panel/PropLibrary.d.ts +1 -1
  31. package/lib/types/client/panel/ReaderView.d.ts +1 -1
  32. package/lib/types/client/panel/RunPanel.d.ts +1 -1
  33. package/lib/types/client/panel/SceneLibrary.d.ts +1 -1
  34. package/lib/types/client/panel/SettingsView.d.ts +1 -1
  35. package/lib/types/client/panel/ShelfView.d.ts +1 -1
  36. package/lib/types/client/panel/StoryboardTab.d.ts +1 -1
  37. package/lib/types/client/panel/StyleCard.d.ts +1 -1
  38. package/lib/types/client/panel/StyleGallery.d.ts +1 -1
  39. package/lib/types/client/panel/WorldTab.d.ts +1 -1
  40. package/lib/types/engine.d.ts +29 -1
  41. package/lib/types/global-assets.d.ts +6 -0
  42. package/lib/types/index.d.ts +6 -0
  43. package/lib/types/llm-live.d.ts +44 -0
  44. package/lib/types/market-radar-scan.d.ts +19 -0
  45. package/lib/types/market-radar-sources.d.ts +31 -0
  46. package/lib/types/prompting.d.ts +35 -0
  47. package/lib/types/protocol.d.ts +364 -8
  48. package/lib/types/run.d.ts +1 -1
  49. package/lib/types/src/ai-scan.d.ts +36 -0
  50. package/lib/types/src/assets.d.ts +37 -0
  51. package/lib/types/src/assistant.d.ts +45 -0
  52. package/lib/types/src/author-assets.d.ts +25 -0
  53. package/lib/types/src/bookshelf.d.ts +42 -0
  54. package/lib/types/src/client/api.d.ts +234 -0
  55. package/lib/types/src/client/docx.d.ts +15 -0
  56. package/lib/types/src/client/index.d.ts +14 -0
  57. package/lib/types/src/client/locales.d.ts +212 -0
  58. package/lib/types/src/client/mount.d.ts +9 -0
  59. package/lib/types/src/client/panel/AdaptModeView.d.ts +5 -0
  60. package/lib/types/src/client/panel/AssetsTab.d.ts +12 -0
  61. package/lib/types/src/client/panel/AssistantTab.d.ts +7 -0
  62. package/lib/types/src/client/panel/AuthorAssetsView.d.ts +4 -0
  63. package/lib/types/src/client/panel/AuthorHome.d.ts +32 -0
  64. package/lib/types/src/client/panel/BookshelfBar.d.ts +11 -0
  65. package/lib/types/src/client/panel/CreateBookView.d.ts +9 -0
  66. package/lib/types/src/client/panel/FlowGuide.d.ts +22 -0
  67. package/lib/types/src/client/panel/GlobalAssetLibraryView.d.ts +4 -0
  68. package/lib/types/src/client/panel/ImportModal.d.ts +7 -0
  69. package/lib/types/src/client/panel/MangaRoleLibrary.d.ts +20 -0
  70. package/lib/types/src/client/panel/MangaWorkspace.d.ts +12 -0
  71. package/lib/types/src/client/panel/ModelManager.d.ts +14 -0
  72. package/lib/types/src/client/panel/NovelPanel.d.ts +13 -0
  73. package/lib/types/src/client/panel/ProgressConsole.d.ts +17 -0
  74. package/lib/types/src/client/panel/ProgressHomeView.d.ts +5 -0
  75. package/lib/types/src/client/panel/PropLibrary.d.ts +8 -0
  76. package/lib/types/src/client/panel/ReaderView.d.ts +8 -0
  77. package/lib/types/src/client/panel/ReasoningSection.d.ts +10 -0
  78. package/lib/types/src/client/panel/RunPanel.d.ts +5 -0
  79. package/lib/types/src/client/panel/SceneLibrary.d.ts +16 -0
  80. package/lib/types/src/client/panel/SettingsView.d.ts +16 -0
  81. package/lib/types/src/client/panel/ShelfView.d.ts +15 -0
  82. package/lib/types/src/client/panel/StoryboardTab.d.ts +19 -0
  83. package/lib/types/src/client/panel/StyleCard.d.ts +8 -0
  84. package/lib/types/src/client/panel/StyleGallery.d.ts +1 -0
  85. package/lib/types/src/client/panel/WorldTab.d.ts +10 -0
  86. package/lib/types/src/client/panel/controller.d.ts +19 -0
  87. package/lib/types/src/client/panel/helpers.d.ts +18 -0
  88. package/lib/types/src/client/panel/views.d.ts +77 -0
  89. package/lib/types/src/client/sidebar-entry.d.ts +13 -0
  90. package/lib/types/src/client/text.d.ts +9 -0
  91. package/lib/types/src/comic-presets.d.ts +12 -0
  92. package/lib/types/src/docx.d.ts +19 -0
  93. package/lib/types/src/engine.d.ts +402 -0
  94. package/lib/types/src/index.d.ts +79 -0
  95. package/lib/types/src/manga-genre-rules.d.ts +22 -0
  96. package/lib/types/src/novel-context.d.ts +80 -0
  97. package/lib/types/src/prompt-utils.d.ts +35 -0
  98. package/lib/types/src/protocol.d.ts +2176 -0
  99. package/lib/types/src/review-policy.d.ts +97 -0
  100. package/lib/types/src/routes.d.ts +29 -0
  101. package/lib/types/src/run.d.ts +32 -0
  102. package/lib/types/src/shot-language.d.ts +46 -0
  103. package/lib/types/src/story-beat-language.d.ts +35 -0
  104. package/lib/types/src/style-library.d.ts +33 -0
  105. package/package.json +1 -1
  106. package/src/assets.ts +1774 -879
  107. package/src/assistant.ts +154 -9
  108. package/src/client/api.ts +105 -6
  109. package/src/client/llmLive.ts +101 -0
  110. package/src/client/locales.ts +4 -4
  111. package/src/client/panel/AdaptModeView.tsx +18 -3
  112. package/src/client/panel/AssistantTab.tsx +41 -3
  113. package/src/client/panel/AuthorHome.tsx +51 -20
  114. package/src/client/panel/BookAnalysisView.tsx +60 -0
  115. package/src/client/panel/CreateBookView.tsx +22 -3
  116. package/src/client/panel/DirectorView.tsx +200 -0
  117. package/src/client/panel/IdeaInspirationView.tsx +71 -0
  118. package/src/client/panel/KnowledgeBaseView.tsx +73 -0
  119. package/src/client/panel/LiveFeedLog.tsx +76 -0
  120. package/src/client/panel/MarketRadarView.tsx +374 -0
  121. package/src/client/panel/NovelPanel.tsx +117 -16
  122. package/src/client/panel/ProgressConsole.tsx +5 -6
  123. package/src/client/panel/SettingsView.tsx +3 -0
  124. package/src/client/panel/panel.module.css +2 -3
  125. package/src/engine.ts +459 -23
  126. package/src/global-assets.ts +59 -0
  127. package/src/index.ts +17 -1
  128. package/src/llm-live.ts +68 -0
  129. package/src/market-radar-scan.ts +41 -0
  130. package/src/market-radar-sources.ts +173 -0
  131. package/src/prompting.ts +159 -0
  132. package/src/protocol.ts +340 -8
  133. package/src/routes.ts +579 -59
  134. package/src/run.ts +15 -8
  135. package/skills/chapter-batch/SKILL.md +0 -81
package/lib/index.js CHANGED
@@ -20,6 +20,49 @@ const BUILTIN_STYLE_TEMPLATES = [
20
20
  "玄幻",
21
21
  "热血"
22
22
  ],
23
+ tags: [
24
+ "推进感",
25
+ "收益点",
26
+ "冲突升级"
27
+ ],
28
+ analysisMarkdown: "每段都要有目标推进或爽点兑现,保持明确因果和节奏抬升。",
29
+ narrative: {
30
+ progressionMode: "goal_driven",
31
+ sceneUnitPattern: [
32
+ "目标",
33
+ "阻碍",
34
+ "压制",
35
+ "反转收益"
36
+ ],
37
+ multiPov: false,
38
+ looping: false,
39
+ endingStyle: "hook",
40
+ summary: "围绕目标推进,尽快兑现局部收益。"
41
+ },
42
+ character: {
43
+ allowSelfReflection: true,
44
+ emotionExpression: "dialogue_and_action",
45
+ defenseMechanisms: [],
46
+ facePriority: false,
47
+ dialogueStyle: "direct",
48
+ summary: "角色表达直接,情绪跟随胜负切换。"
49
+ },
50
+ language: {
51
+ register: "direct",
52
+ roughness: .55,
53
+ allowIncompleteSentences: false,
54
+ allowSwearing: false,
55
+ sentenceVariation: "medium",
56
+ allowUselessDetails: false,
57
+ summary: "句式清晰,减少无效分散信息。"
58
+ },
59
+ rhythm: {
60
+ pace: "fast",
61
+ paragraphDensity: "medium",
62
+ allowFragmentedFlow: false,
63
+ actionOverExplanation: true,
64
+ summary: "优先冲突和结果,少停留。"
65
+ },
23
66
  proseRules: [
24
67
  "围绕目标推进,尽快兑现局部收益;每段都要有目标推进或爽点兑现。",
25
68
  "保持明确因果和节奏抬升,场景单元按「目标→阻碍→压制→反转收益」推进。",
@@ -29,9 +72,9 @@ const BUILTIN_STYLE_TEMPLATES = [
29
72
  languageRules: ["句式清晰,减少无效分散信息。", "直接、明确,不做无谓铺垫。"],
30
73
  rhythmRules: ["快节奏,段落密度中等,动作先于解释。", "尽快兑现局部收益,避免拖沓。"],
31
74
  defaultAntiAiRuleKeys: [
32
- "禁止总结主题",
33
- "对话纯功能推进",
34
- "连续三段解释性叙事"
75
+ "forbid-theme-summary",
76
+ "risk-dialogue-too-functional",
77
+ "risk-three-paragraphs-exposition"
35
78
  ]
36
79
  },
37
80
  {
@@ -44,6 +87,52 @@ const BUILTIN_STYLE_TEMPLATES = [
44
87
  "现实",
45
88
  "成长"
46
89
  ],
90
+ tags: [
91
+ "第一人称",
92
+ "口语化",
93
+ "碎片叙事"
94
+ ],
95
+ analysisMarkdown: "以时间推进和现实落差构成叙事张力,结尾不解决核心困境。",
96
+ narrative: {
97
+ progressionMode: "time_sequence",
98
+ sceneUnitPattern: [
99
+ "行为",
100
+ "落差",
101
+ "自我合理化"
102
+ ],
103
+ multiPov: false,
104
+ looping: true,
105
+ endingStyle: "unresolved",
106
+ summary: "以碎片化生活推进,不做总括式回顾。"
107
+ },
108
+ character: {
109
+ allowSelfReflection: false,
110
+ emotionExpression: "behavior_only",
111
+ defenseMechanisms: [
112
+ "嘴硬",
113
+ "转移",
114
+ "自我合理化"
115
+ ],
116
+ facePriority: true,
117
+ dialogueStyle: "short_colloquial",
118
+ summary: "人物情绪通过动作和嘴硬表达。"
119
+ },
120
+ language: {
121
+ register: "colloquial",
122
+ roughness: .8,
123
+ allowIncompleteSentences: true,
124
+ allowSwearing: true,
125
+ sentenceVariation: "high",
126
+ allowUselessDetails: true,
127
+ summary: "语言粗粝、口语化,允许生活杂音。"
128
+ },
129
+ rhythm: {
130
+ pace: "medium_fast",
131
+ paragraphDensity: "high",
132
+ allowFragmentedFlow: true,
133
+ actionOverExplanation: true,
134
+ summary: "段落密实,动作先于解释。"
135
+ },
47
136
  proseRules: [
48
137
  "以时间推进和现实落差构成叙事张力,结尾不解决核心困境。",
49
138
  "场景单元按「行为→落差→自我合理化」推进。",
@@ -53,11 +142,11 @@ const BUILTIN_STYLE_TEMPLATES = [
53
142
  languageRules: ["语言粗粝、口语化,允许生活杂音与不完整句。", "句子变化度高,允许无意义细节。"],
54
143
  rhythmRules: ["段落密实,动作先于解释。", "中快节奏,允许碎片化流动。"],
55
144
  defaultAntiAiRuleKeys: [
56
- "禁止解释型心理描写",
57
- "禁止段尾升华",
58
- "鼓励无意义小动作",
59
- "鼓励现实落差",
60
- "鼓励嘴硬补偿"
145
+ "forbid-explicit-psychology",
146
+ "forbid-ending-elevation",
147
+ "encourage-useless-action",
148
+ "encourage-reality-gap",
149
+ "encourage-hard-mouth-compensation"
61
150
  ]
62
151
  },
63
152
  {
@@ -70,6 +159,49 @@ const BUILTIN_STYLE_TEMPLATES = [
70
159
  "惊悚",
71
160
  "现实"
72
161
  ],
162
+ tags: [
163
+ "压迫感",
164
+ "信息差",
165
+ "异常细节"
166
+ ],
167
+ analysisMarkdown: "以异常细节、信息差和节奏收束推动悬念层层加压。",
168
+ narrative: {
169
+ progressionMode: "mystery_escalation",
170
+ sceneUnitPattern: [
171
+ "现场细节",
172
+ "异常",
173
+ "误判",
174
+ "新风险"
175
+ ],
176
+ multiPov: false,
177
+ looping: false,
178
+ endingStyle: "suspense",
179
+ summary: "优先制造信息缺口和压迫氛围。"
180
+ },
181
+ character: {
182
+ allowSelfReflection: true,
183
+ emotionExpression: "reaction_only",
184
+ defenseMechanisms: ["压抑"],
185
+ facePriority: false,
186
+ dialogueStyle: "restrained",
187
+ summary: "角色反应克制,恐惧通过反应显现。"
188
+ },
189
+ language: {
190
+ register: "restrained",
191
+ roughness: .45,
192
+ allowIncompleteSentences: true,
193
+ allowSwearing: false,
194
+ sentenceVariation: "medium_high",
195
+ allowUselessDetails: true,
196
+ summary: "细节精确,保留少量噪音增强现场感。"
197
+ },
198
+ rhythm: {
199
+ pace: "medium",
200
+ paragraphDensity: "medium_high",
201
+ allowFragmentedFlow: true,
202
+ actionOverExplanation: true,
203
+ summary: "通过节奏收束和信息延迟制造压力。"
204
+ },
73
205
  proseRules: [
74
206
  "以异常细节、信息差和节奏收束推动悬念层层加压。",
75
207
  "场景单元按「现场细节→异常→误判→新风险」推进。",
@@ -79,10 +211,10 @@ const BUILTIN_STYLE_TEMPLATES = [
79
211
  languageRules: ["细节精确,保留少量噪音增强现场感。", "克制、中等偏高句变化。"],
80
212
  rhythmRules: ["通过节奏收束和信息延迟制造压力。", "中速,段落密度中等偏高。"],
81
213
  defaultAntiAiRuleKeys: [
82
- "禁止解释型心理描写",
83
- "禁止总结主题",
84
- "段落长度过于整齐",
85
- "鼓励现实落差"
214
+ "forbid-explicit-psychology",
215
+ "forbid-theme-summary",
216
+ "risk-even-paragraph-length",
217
+ "encourage-reality-gap"
86
218
  ]
87
219
  },
88
220
  {
@@ -95,6 +227,53 @@ const BUILTIN_STYLE_TEMPLATES = [
95
227
  "都市",
96
228
  "群像"
97
229
  ],
230
+ tags: [
231
+ "误读",
232
+ "拉扯",
233
+ "停顿感"
234
+ ],
235
+ analysisMarkdown: "人物不直说核心情绪,靠误读、停顿和反应推动关系变化。",
236
+ narrative: {
237
+ progressionMode: "relationship_push_pull",
238
+ sceneUnitPattern: [
239
+ "动作",
240
+ "言外之意",
241
+ "误读",
242
+ "回避"
243
+ ],
244
+ multiPov: false,
245
+ looping: false,
246
+ endingStyle: "emotional_hook",
247
+ summary: "以关系错位推进,而非直接说明。"
248
+ },
249
+ character: {
250
+ allowSelfReflection: true,
251
+ emotionExpression: "subtext",
252
+ defenseMechanisms: [
253
+ "回避",
254
+ "试探",
255
+ "嘴硬"
256
+ ],
257
+ facePriority: true,
258
+ dialogueStyle: "subtext_heavy",
259
+ summary: "情绪通过停顿、动作和言外之意体现。"
260
+ },
261
+ language: {
262
+ register: "natural",
263
+ roughness: .35,
264
+ allowIncompleteSentences: true,
265
+ allowSwearing: false,
266
+ sentenceVariation: "high",
267
+ allowUselessDetails: true,
268
+ summary: "语言自然,允许留白与停顿。"
269
+ },
270
+ rhythm: {
271
+ pace: "medium_slow",
272
+ paragraphDensity: "medium",
273
+ allowFragmentedFlow: true,
274
+ actionOverExplanation: false,
275
+ summary: "给关系反应留空间,但避免空洞抒情。"
276
+ },
98
277
  proseRules: [
99
278
  "人物不直说核心情绪,靠误读、停顿和反应推动关系变化。",
100
279
  "场景单元按「动作→言外之意→误读→回避」推进。",
@@ -104,10 +283,10 @@ const BUILTIN_STYLE_TEMPLATES = [
104
283
  languageRules: ["语言自然,允许留白与停顿。", "句子变化度高,允许无意义细节。"],
105
284
  rhythmRules: ["给关系反应留空间,但避免空洞抒情。", "中慢节奏,段落密度中等。"],
106
285
  defaultAntiAiRuleKeys: [
107
- "禁止直接说教",
108
- "禁止段尾升华",
109
- "对话纯功能推进",
110
- "鼓励无意义小动作"
286
+ "forbid-direct-preaching",
287
+ "forbid-ending-elevation",
288
+ "risk-dialogue-too-functional",
289
+ "encourage-useless-action"
111
290
  ]
112
291
  },
113
292
  {
@@ -120,14 +299,56 @@ const BUILTIN_STYLE_TEMPLATES = [
120
299
  "都市",
121
300
  "悬疑"
122
301
  ],
302
+ tags: [
303
+ "多角色",
304
+ "交织",
305
+ "信息流动"
306
+ ],
307
+ analysisMarkdown: "多角色并行推进,但每个角色的表达和认知范围必须区分清楚。",
308
+ narrative: {
309
+ progressionMode: "multi_thread",
310
+ sceneUnitPattern: [
311
+ "角色动作",
312
+ "局部信息",
313
+ "交叉影响"
314
+ ],
315
+ multiPov: true,
316
+ looping: false,
317
+ endingStyle: "cross_hook",
318
+ summary: "多线并进,但视角切换要受控。"
319
+ },
320
+ character: {
321
+ allowSelfReflection: true,
322
+ emotionExpression: "mixed",
323
+ defenseMechanisms: [],
324
+ facePriority: false,
325
+ dialogueStyle: "distinct_by_role",
326
+ summary: "不同角色口吻必须拉开差异。"
327
+ },
328
+ language: {
329
+ register: "flexible",
330
+ roughness: .45,
331
+ allowIncompleteSentences: true,
332
+ allowSwearing: false,
333
+ sentenceVariation: "high",
334
+ allowUselessDetails: false,
335
+ summary: "保持角色差异,避免所有人说话一样。"
336
+ },
337
+ rhythm: {
338
+ pace: "balanced",
339
+ paragraphDensity: "medium",
340
+ allowFragmentedFlow: false,
341
+ actionOverExplanation: true,
342
+ summary: "多线交织但节奏不乱。"
343
+ },
123
344
  proseRules: ["多角色并行推进,但每个角色的表达和认知范围必须区分清楚。", "多线并进,但视角切换要受控。"],
124
345
  dialogueRules: ["不同角色口吻必须拉开差异,避免所有人说话一样。"],
125
346
  languageRules: ["保持角色差异,句式变化度高。", "减少无效分散信息。"],
126
347
  rhythmRules: ["多线交织但节奏不乱,平衡推进。", "动作先于解释。"],
127
348
  defaultAntiAiRuleKeys: [
128
- "对话纯功能推进",
129
- "句式重复率过高",
130
- "禁止总结主题"
349
+ "risk-dialogue-too-functional",
350
+ "risk-repeated-sentence-structure",
351
+ "forbid-theme-summary"
131
352
  ]
132
353
  },
133
354
  {
@@ -140,14 +361,56 @@ const BUILTIN_STYLE_TEMPLATES = [
140
361
  "治愈",
141
362
  "都市"
142
363
  ],
364
+ tags: [
365
+ "生活感",
366
+ "沉浸",
367
+ "细碎细节"
368
+ ],
369
+ analysisMarkdown: "允许保留生活性动作和无效信息,但核心情绪仍要通过场景自然流出。",
370
+ narrative: {
371
+ progressionMode: "scene_immersion",
372
+ sceneUnitPattern: [
373
+ "动作",
374
+ "环境",
375
+ "关系反应"
376
+ ],
377
+ multiPov: false,
378
+ looping: false,
379
+ endingStyle: "soft_open",
380
+ summary: "重场景体验和关系温度。"
381
+ },
382
+ character: {
383
+ allowSelfReflection: true,
384
+ emotionExpression: "light_behavior",
385
+ defenseMechanisms: [],
386
+ facePriority: false,
387
+ dialogueStyle: "daily_natural",
388
+ summary: "人物表达自然,不用高强度戏剧句。"
389
+ },
390
+ language: {
391
+ register: "colloquial",
392
+ roughness: .25,
393
+ allowIncompleteSentences: true,
394
+ allowSwearing: false,
395
+ sentenceVariation: "medium_high",
396
+ allowUselessDetails: true,
397
+ summary: "保留生活细节和杂音,不追求工整。"
398
+ },
399
+ rhythm: {
400
+ pace: "slow",
401
+ paragraphDensity: "medium",
402
+ allowFragmentedFlow: true,
403
+ actionOverExplanation: false,
404
+ summary: "慢节奏沉浸,但避免空转。"
405
+ },
143
406
  proseRules: ["重场景体验和关系温度,核心情绪通过场景自然流出。", "允许保留生活性动作和无效信息。"],
144
407
  dialogueRules: ["人物表达自然,不用高强度戏剧句。", "对话保留生活气息。"],
145
408
  languageRules: ["保留生活细节和杂音,不追求工整。", "口语化,句子变化中等偏高。"],
146
409
  rhythmRules: ["慢节奏沉浸,但避免空转。", "允许碎片化流动。"],
147
410
  defaultAntiAiRuleKeys: [
148
- "禁止段尾升华",
149
- "段落长度过于整齐",
150
- "鼓励无意义小动作"
411
+ "forbid-ending-elevation",
412
+ "risk-even-paragraph-length",
413
+ "encourage-useless-action"
151
414
  ]
152
415
  },
153
416
  {
@@ -160,6 +423,49 @@ const BUILTIN_STYLE_TEMPLATES = [
160
423
  "现实",
161
424
  "悬疑"
162
425
  ],
426
+ tags: [
427
+ "专业细节",
428
+ "克制",
429
+ "事实压情绪"
430
+ ],
431
+ analysisMarkdown: "行业事实和程序细节优先,情绪不直说,信息密度高于抒情密度。",
432
+ narrative: {
433
+ progressionMode: "fact_driven",
434
+ sceneUnitPattern: [
435
+ "事实",
436
+ "动作",
437
+ "专业判断",
438
+ "后果"
439
+ ],
440
+ multiPov: false,
441
+ looping: false,
442
+ endingStyle: "pressure_continue",
443
+ summary: "让专业事实承担叙事重量。"
444
+ },
445
+ character: {
446
+ allowSelfReflection: false,
447
+ emotionExpression: "suppressed",
448
+ defenseMechanisms: ["克制"],
449
+ facePriority: false,
450
+ dialogueStyle: "informational",
451
+ summary: "情绪藏在专业动作和事实选择里。"
452
+ },
453
+ language: {
454
+ register: "professional",
455
+ roughness: .2,
456
+ allowIncompleteSentences: false,
457
+ allowSwearing: false,
458
+ sentenceVariation: "medium",
459
+ allowUselessDetails: false,
460
+ summary: "术语和事实优先,避免廉价金句。"
461
+ },
462
+ rhythm: {
463
+ pace: "balanced",
464
+ paragraphDensity: "medium_high",
465
+ allowFragmentedFlow: false,
466
+ actionOverExplanation: true,
467
+ summary: "信息密度高,但不铺张解释。"
468
+ },
163
469
  proseRules: [
164
470
  "行业事实和程序细节优先,情绪不直说,信息密度高于抒情密度。",
165
471
  "场景单元按「事实→动作→专业判断→后果」推进。",
@@ -169,9 +475,9 @@ const BUILTIN_STYLE_TEMPLATES = [
169
475
  languageRules: ["术语和事实优先,避免廉价金句。", "正式、克制的语言。"],
170
476
  rhythmRules: ["信息密度高,但不铺张解释。", "平衡节奏,段落密度中等偏高。"],
171
477
  defaultAntiAiRuleKeys: [
172
- "禁止直接说教",
173
- "禁止总结主题",
174
- "句式重复率过高"
478
+ "forbid-direct-preaching",
479
+ "forbid-theme-summary",
480
+ "risk-repeated-sentence-structure"
175
481
  ]
176
482
  },
177
483
  {
@@ -184,6 +490,48 @@ const BUILTIN_STYLE_TEMPLATES = [
184
490
  "黑色幽默",
185
491
  "现实"
186
492
  ],
493
+ tags: [
494
+ "荒诞",
495
+ "反差",
496
+ "冷感"
497
+ ],
498
+ analysisMarkdown: "用反差和荒诞细节放大现实困境,笑点和压迫感同时存在。",
499
+ narrative: {
500
+ progressionMode: "contrast_driven",
501
+ sceneUnitPattern: [
502
+ "现实细节",
503
+ "荒诞偏差",
504
+ "冷反应"
505
+ ],
506
+ multiPov: false,
507
+ looping: false,
508
+ endingStyle: "bitter_aftertaste",
509
+ summary: "依赖反差和冷感观察,而非热闹吐槽。"
510
+ },
511
+ character: {
512
+ allowSelfReflection: false,
513
+ emotionExpression: "deadpan",
514
+ defenseMechanisms: ["自嘲", "转移"],
515
+ facePriority: true,
516
+ dialogueStyle: "deadpan_colloquial",
517
+ summary: "情绪藏在冷反应和嘴硬里。"
518
+ },
519
+ language: {
520
+ register: "colloquial",
521
+ roughness: .5,
522
+ allowIncompleteSentences: true,
523
+ allowSwearing: true,
524
+ sentenceVariation: "high",
525
+ allowUselessDetails: true,
526
+ summary: "允许夹带荒诞杂质和冷幽默节奏。"
527
+ },
528
+ rhythm: {
529
+ pace: "balanced",
530
+ paragraphDensity: "medium_high",
531
+ allowFragmentedFlow: true,
532
+ actionOverExplanation: true,
533
+ summary: "反差点要快落地,不要解释笑点。"
534
+ },
187
535
  proseRules: [
188
536
  "用反差和荒诞细节放大现实困境,笑点和压迫感同时存在。",
189
537
  "场景单元按「现实细节→荒诞偏差→冷反应」推进。",
@@ -193,10 +541,10 @@ const BUILTIN_STYLE_TEMPLATES = [
193
541
  languageRules: ["允许夹带荒诞杂质和冷幽默节奏。", "口语化,句子变化度高。"],
194
542
  rhythmRules: ["反差点要快落地,不要解释笑点。", "平衡节奏,段落密度中等偏高。"],
195
543
  defaultAntiAiRuleKeys: [
196
- "禁止解释型心理描写",
197
- "禁止段尾升华",
198
- "鼓励现实落差",
199
- "鼓励嘴硬补偿"
544
+ "forbid-explicit-psychology",
545
+ "forbid-ending-elevation",
546
+ "encourage-reality-gap",
547
+ "encourage-hard-mouth-compensation"
200
548
  ]
201
549
  },
202
550
  {
@@ -218,9 +566,9 @@ const BUILTIN_STYLE_TEMPLATES = [
218
566
  languageRules: ["战斗场景用短句和动作词,突破场景用感官描写(光/声/压力)。", "减少修炼过程的流水账,聚焦关键节点和突破瞬间。"],
219
567
  rhythmRules: ["突破节奏先抑后扬,压抑段不超过3段,突破段要快且有冲击力。", "每章至少一个战力或境界的明确进展点。"],
220
568
  defaultAntiAiRuleKeys: [
221
- "禁止总结主题",
222
- "禁止段尾升华",
223
- "连续三段解释性叙事"
569
+ "forbid-theme-summary",
570
+ "forbid-ending-elevation",
571
+ "risk-three-paragraphs-exposition"
224
572
  ]
225
573
  },
226
574
  {
@@ -247,9 +595,9 @@ const BUILTIN_STYLE_TEMPLATES = [
247
595
  languageRules: ["打脸场景用短句和动作,震惊场景用群像反应。", "避免\"全场寂静\"\"众人哗然\"等套话,用具体人物反应代替。"],
248
596
  rhythmRules: ["快节奏,打脸间隔不超过3章,每章至少一个小反转。", "装逼要克制,主角不主动炫耀,被动展露更有爽感。"],
249
597
  defaultAntiAiRuleKeys: [
250
- "禁止总结主题",
251
- "对话纯功能推进",
252
- "AI 高频套话"
598
+ "forbid-theme-summary",
599
+ "risk-dialogue-too-functional",
600
+ "risk-ai-cliche-high"
253
601
  ]
254
602
  },
255
603
  {
@@ -276,9 +624,9 @@ const BUILTIN_STYLE_TEMPLATES = [
276
624
  languageRules: ["正式、克制的语言,避免口语化和现代网络用语。", "用细节(眼神/手势/器物)暗示人物真实想法。"],
277
625
  rhythmRules: ["中慢节奏,布局段可以慢,但反转和收网段要快。", "每3-5章一个小高潮(布局见效或反制成功)。"],
278
626
  defaultAntiAiRuleKeys: [
279
- "禁止解释型心理描写",
280
- "禁止直接说教",
281
- "对话纯功能推进"
627
+ "forbid-explicit-psychology",
628
+ "forbid-direct-preaching",
629
+ "risk-dialogue-too-functional"
282
630
  ]
283
631
  },
284
632
  {
@@ -305,9 +653,9 @@ const BUILTIN_STYLE_TEMPLATES = [
305
653
  languageRules: ["温暖、细腻的语言,多用感官细节(温度/气味/触感)。", "避免\"心如鹿撞\"\"脸红心跳\"等套话,用具体动作代替。"],
306
654
  rhythmRules: ["中慢节奏,日常段可以慢,但心动瞬间要聚焦和放大。", "每5章一个关系突破(牵手/拥抱/表白等)。"],
307
655
  defaultAntiAiRuleKeys: [
308
- "禁止段尾升华",
309
- "禁止解释型心理描写",
310
- "鼓励无意义小动作"
656
+ "forbid-ending-elevation",
657
+ "forbid-explicit-psychology",
658
+ "encourage-useless-action"
311
659
  ]
312
660
  },
313
661
  {
@@ -334,9 +682,9 @@ const BUILTIN_STYLE_TEMPLATES = [
334
682
  languageRules: ["比赛场景用短句和动作词,节奏快,有画面感。", "操作描述要专业且准确,避免外行话。"],
335
683
  rhythmRules: ["快节奏,比赛段要紧凑,日常训练段可以稍缓。", "每场比赛至少一个逆转或高光时刻,每章一个小燃点。"],
336
684
  defaultAntiAiRuleKeys: [
337
- "禁止总结主题",
338
- "连续三段解释性叙事",
339
- "AI 高频套话"
685
+ "forbid-theme-summary",
686
+ "risk-three-paragraphs-exposition",
687
+ "risk-ai-cliche-high"
340
688
  ]
341
689
  },
342
690
  {
@@ -363,9 +711,9 @@ const BUILTIN_STYLE_TEMPLATES = [
363
711
  languageRules: ["口语化、轻快的语言,允许夸张和无厘头。", "吐槽用括号或单独段落,不与叙事混淆。"],
364
712
  rhythmRules: ["快节奏,笑点密集,不拖沓。", "每段不超过3句,长段落要拆。"],
365
713
  defaultAntiAiRuleKeys: [
366
- "禁止段尾升华",
367
- "禁止直接说教",
368
- "鼓励无意义小动作"
714
+ "forbid-ending-elevation",
715
+ "forbid-direct-preaching",
716
+ "encourage-useless-action"
369
717
  ]
370
718
  },
371
719
  {
@@ -396,9 +744,9 @@ const BUILTIN_STYLE_TEMPLATES = [
396
744
  ],
397
745
  rhythmRules: ["慢节奏铺垫,快节奏爆发,爆发后留余悸。", "每3章一个小高潮(异常确认或危机爆发)。"],
398
746
  defaultAntiAiRuleKeys: [
399
- "禁止解释型心理描写",
400
- "禁止总结主题",
401
- "段落长度过于整齐"
747
+ "forbid-explicit-psychology",
748
+ "forbid-theme-summary",
749
+ "risk-even-paragraph-length"
402
750
  ]
403
751
  },
404
752
  {
@@ -425,9 +773,9 @@ const BUILTIN_STYLE_TEMPLATES = [
425
773
  languageRules: ["精确、理性的语言,避免模糊和情绪化表达。", "技术描述要具体且可想象,不堆砌名词。"],
426
774
  rhythmRules: ["中慢节奏,技术推演段可以慢,但行动段要快。", "每5章一个技术突破或危机解决。"],
427
775
  defaultAntiAiRuleKeys: [
428
- "禁止直接说教",
429
- "禁止总结主题",
430
- "连续三段解释性叙事"
776
+ "forbid-direct-preaching",
777
+ "forbid-theme-summary",
778
+ "risk-three-paragraphs-exposition"
431
779
  ]
432
780
  }
433
781
  ];
@@ -445,7 +793,12 @@ const BUILTIN_ANTI_AI_RULES = [
445
793
  "他明白了",
446
794
  "她明白了"
447
795
  ],
448
- builtin: true
796
+ builtin: true,
797
+ key: "forbid-explicit-psychology",
798
+ severity: "forbidden",
799
+ riskLevel: "high",
800
+ autoRewrite: true,
801
+ globalBaselineEnabled: true
449
802
  },
450
803
  {
451
804
  name: "禁止段尾升华",
@@ -458,7 +811,12 @@ const BUILTIN_ANTI_AI_RULES = [
458
811
  "说到底",
459
812
  "这就是"
460
813
  ],
461
- builtin: true
814
+ builtin: true,
815
+ key: "forbid-ending-elevation",
816
+ severity: "forbidden",
817
+ riskLevel: "high",
818
+ autoRewrite: true,
819
+ globalBaselineEnabled: true
462
820
  },
463
821
  {
464
822
  name: "禁止总结主题",
@@ -470,7 +828,12 @@ const BUILTIN_ANTI_AI_RULES = [
470
828
  "归根结底",
471
829
  "其实就是"
472
830
  ],
473
- builtin: true
831
+ builtin: true,
832
+ key: "forbid-theme-summary",
833
+ severity: "forbidden",
834
+ riskLevel: "high",
835
+ autoRewrite: true,
836
+ globalBaselineEnabled: true
474
837
  },
475
838
  {
476
839
  name: "禁止直接说教",
@@ -481,21 +844,36 @@ const BUILTIN_ANTI_AI_RULES = [
481
844
  "人总要学会",
482
845
  "真正重要的是"
483
846
  ],
484
- builtin: true
847
+ builtin: true,
848
+ key: "forbid-direct-preaching",
849
+ severity: "forbidden",
850
+ riskLevel: "medium",
851
+ autoRewrite: true,
852
+ globalBaselineEnabled: true
485
853
  },
486
854
  {
487
855
  name: "段落长度过于整齐",
488
856
  avoid: "段落长度和节奏过于平均,产生 AI 作文感。",
489
857
  fix: "打破段落长度均衡,让句子和段落有自然起伏。",
490
858
  detectPatterns: [],
491
- builtin: true
859
+ builtin: true,
860
+ key: "risk-even-paragraph-length",
861
+ severity: "risk",
862
+ riskLevel: "medium",
863
+ autoRewrite: false,
864
+ globalBaselineEnabled: true
492
865
  },
493
866
  {
494
867
  name: "连续三段解释性叙事",
495
868
  avoid: "连续几段只有解释没有动作,削弱现场感。",
496
869
  fix: "插入动作、对话、环境反馈,减少连段说明。",
497
870
  detectPatterns: [],
498
- builtin: true
871
+ builtin: true,
872
+ key: "risk-three-paragraphs-exposition",
873
+ severity: "risk",
874
+ riskLevel: "high",
875
+ autoRewrite: false,
876
+ globalBaselineEnabled: true
499
877
  },
500
878
  {
501
879
  name: "对话纯功能推进",
@@ -506,7 +884,12 @@ const BUILTIN_ANTI_AI_RULES = [
506
884
  "我们现在要",
507
885
  "接下来就"
508
886
  ],
509
- builtin: true
887
+ builtin: true,
888
+ key: "risk-dialogue-too-functional",
889
+ severity: "forbidden",
890
+ riskLevel: "high",
891
+ autoRewrite: true,
892
+ globalBaselineEnabled: true
510
893
  },
511
894
  {
512
895
  name: "句式重复率过高",
@@ -518,7 +901,12 @@ const BUILTIN_ANTI_AI_RULES = [
518
901
  "接着",
519
902
  "最后"
520
903
  ],
521
- builtin: true
904
+ builtin: true,
905
+ key: "risk-repeated-sentence-structure",
906
+ severity: "forbidden",
907
+ riskLevel: "high",
908
+ autoRewrite: true,
909
+ globalBaselineEnabled: true
522
910
  },
523
911
  {
524
912
  name: "AI 高频套话",
@@ -534,28 +922,48 @@ const BUILTIN_ANTI_AI_RULES = [
534
922
  "轻轻",
535
923
  "微微"
536
924
  ],
537
- builtin: true
925
+ builtin: true,
926
+ key: "risk-ai-cliche-high",
927
+ severity: "forbidden",
928
+ riskLevel: "high",
929
+ autoRewrite: true,
930
+ globalBaselineEnabled: true
538
931
  },
539
932
  {
540
933
  name: "鼓励无意义小动作",
541
934
  avoid: "(鼓励类)全篇缺少真实但不推动主线的小动作,人物显得空洞。",
542
935
  fix: "补入挠头、点烟、抠包装、挪椅子等小动作,增加人味与生活感。",
543
936
  detectPatterns: [],
544
- builtin: true
937
+ builtin: true,
938
+ key: "encourage-useless-action",
939
+ severity: "encourage",
940
+ riskLevel: "low",
941
+ autoRewrite: false,
942
+ globalBaselineEnabled: false
545
943
  },
546
944
  {
547
945
  name: "鼓励现实落差",
548
946
  avoid: "(鼓励类)人物预期和现实结果完全一致,缺少落差。",
549
947
  fix: "补出人物预期与实际结果之间的差距,制造张力。",
550
948
  detectPatterns: [],
551
- builtin: true
949
+ builtin: true,
950
+ key: "encourage-reality-gap",
951
+ severity: "encourage",
952
+ riskLevel: "low",
953
+ autoRewrite: false,
954
+ globalBaselineEnabled: false
552
955
  },
553
956
  {
554
957
  name: "鼓励嘴硬补偿",
555
958
  avoid: "(鼓励类)人物吃瘪后没有维持体面的反应。",
556
959
  fix: "给角色补一句嘴硬找补或自我合理化,保持人设温度。",
557
960
  detectPatterns: [],
558
- builtin: true
961
+ builtin: true,
962
+ key: "encourage-hard-mouth-compensation",
963
+ severity: "encourage",
964
+ riskLevel: "low",
965
+ autoRewrite: false,
966
+ globalBaselineEnabled: false
559
967
  },
560
968
  {
561
969
  name: "禁止心中暗道/脑海浮现",
@@ -569,7 +977,12 @@ const BUILTIN_ANTI_AI_RULES = [
569
977
  "心中暗想",
570
978
  "心里暗道"
571
979
  ],
572
- builtin: true
980
+ builtin: true,
981
+ key: "forbid-inner-voice",
982
+ severity: "forbidden",
983
+ riskLevel: "high",
984
+ autoRewrite: true,
985
+ globalBaselineEnabled: true
573
986
  },
574
987
  {
575
988
  name: "禁止仿佛/好像比喻滥用",
@@ -582,35 +995,60 @@ const BUILTIN_ANTI_AI_RULES = [
582
995
  "宛如",
583
996
  "好似"
584
997
  ],
585
- builtin: true
998
+ builtin: true,
999
+ key: "forbid-simile-overuse",
1000
+ severity: "forbidden",
1001
+ riskLevel: "high",
1002
+ autoRewrite: true,
1003
+ globalBaselineEnabled: true
586
1004
  },
587
1005
  {
588
1006
  name: "禁止过度排比堆砌",
589
1007
  avoid: "连续3句以上相同句式或排比结构,显得机械和刻意。",
590
1008
  fix: "打散句式,用长短句交替,排比不超过2句。",
591
1009
  detectPatterns: [],
592
- builtin: true
1010
+ builtin: true,
1011
+ key: "forbid-over-parallelism",
1012
+ severity: "forbidden",
1013
+ riskLevel: "high",
1014
+ autoRewrite: true,
1015
+ globalBaselineEnabled: true
593
1016
  },
594
1017
  {
595
1018
  name: "禁止角色全知泄露",
596
1019
  avoid: "角色说出或知道他/她不可能知道的信息,破坏视角一致性。",
597
1020
  fix: "严格遵守视角限制,角色只能基于已有信息行动和判断。",
598
1021
  detectPatterns: [],
599
- builtin: true
1022
+ builtin: true,
1023
+ key: "forbid-omniscient-leak",
1024
+ severity: "forbidden",
1025
+ riskLevel: "high",
1026
+ autoRewrite: true,
1027
+ globalBaselineEnabled: true
600
1028
  },
601
1029
  {
602
1030
  name: "鼓励环境细节锚定",
603
1031
  avoid: "(鼓励类)情绪和氛围缺少具体环境物件锚定,全靠抽象形容词。",
604
1032
  fix: "用具体物件、光线、声音、温度等环境细节承载情绪,不直接说\"紧张\"\"悲伤\"。",
605
1033
  detectPatterns: [],
606
- builtin: true
1034
+ builtin: true,
1035
+ key: "encourage-env-anchor",
1036
+ severity: "encourage",
1037
+ riskLevel: "low",
1038
+ autoRewrite: false,
1039
+ globalBaselineEnabled: false
607
1040
  },
608
1041
  {
609
1042
  name: "鼓励角色语言差异化",
610
1043
  avoid: "(鼓励类)所有角色说话风格雷同,没有口头禅、用词习惯或句式差异。",
611
1044
  fix: "给每个主要角色设定独特的语言习惯(口头禅/用词偏好/句式长短),对话时严格区分。",
612
1045
  detectPatterns: [],
613
- builtin: true
1046
+ builtin: true,
1047
+ key: "encourage-voice-diff",
1048
+ severity: "encourage",
1049
+ riskLevel: "low",
1050
+ autoRewrite: false,
1051
+ globalBaselineEnabled: false
614
1052
  }
615
1053
  ];
616
1054
  /** 内置题材基底库(常用网文题材树,跨书复用)。 */
@@ -618,6 +1056,8 @@ const BUILTIN_GENRE_LIBRARY = [
618
1056
  {
619
1057
  name: "仙侠修真",
620
1058
  description: "以修仙境界、宗门斗争、法宝丹药为核心,读者期待从凡人到强者的成长与长生问道。",
1059
+ id: "genre_xianxia",
1060
+ template: "强调修行路径、因果代价与宗门/仙途抉择。",
621
1061
  children: [
622
1062
  {
623
1063
  name: "凡人流",
@@ -639,6 +1079,8 @@ const BUILTIN_GENRE_LIBRARY = [
639
1079
  {
640
1080
  name: "都市异能",
641
1081
  description: "现代都市背景叠加超能力,读者期待隐藏身份、扮猪吃虎与日常反差。",
1082
+ id: "genre_urban_power",
1083
+ template: "强调异能觉醒后的能力增长、都市隐藏与碾压打脸。",
642
1084
  children: [
643
1085
  {
644
1086
  name: "异能升级",
@@ -660,6 +1102,8 @@ const BUILTIN_GENRE_LIBRARY = [
660
1102
  {
661
1103
  name: "悬疑推理",
662
1104
  description: "以谜题、案件与真相揭露为核心,读者期待线索层层展开与反转。",
1105
+ id: "genre_mystery",
1106
+ template: "强调公平线索、逻辑推演与谜局层层递进、回收。",
663
1107
  children: [
664
1108
  {
665
1109
  name: "本格推理",
@@ -681,6 +1125,8 @@ const BUILTIN_GENRE_LIBRARY = [
681
1125
  {
682
1126
  name: "玄幻奇幻",
683
1127
  description: "异世界或架空大陆的冒险成长,读者期待宏大世界观、奇遇与战力突破。",
1128
+ id: "genre_fantasy",
1129
+ template: "强调金手指、打脸逆袭与世界/境界阶梯式扩展。",
684
1130
  children: [
685
1131
  {
686
1132
  name: "学院流",
@@ -702,6 +1148,8 @@ const BUILTIN_GENRE_LIBRARY = [
702
1148
  {
703
1149
  name: "历史军事",
704
1150
  description: "以历史时代为背景的争霸、谋略或军旅故事,读者期待权谋博弈与时代质感。",
1151
+ id: "genre_history",
1152
+ template: "强调乱世崛起、战略人心与时代质感。",
705
1153
  children: [{
706
1154
  name: "王朝争霸",
707
1155
  description: "乱世崛起、招贤纳士、逐鹿天下,强调战略与人心。",
@@ -715,6 +1163,8 @@ const BUILTIN_GENRE_LIBRARY = [
715
1163
  {
716
1164
  name: "末世科幻",
717
1165
  description: "末世危机或科幻设定下的生存与重建,读者期待资源管理、危机升级与人性考验。",
1166
+ id: "genre_apocalypse",
1167
+ template: "强调末世压力、基地建设与能力进化中的危机求生。",
718
1168
  children: [{
719
1169
  name: "基地经营",
720
1170
  description: "建立基地、收集资源、抵御危机,强调建设与扩张。",
@@ -728,6 +1178,8 @@ const BUILTIN_GENRE_LIBRARY = [
728
1178
  {
729
1179
  name: "都市生活",
730
1180
  description: "现代都市日常背景,无超能力或超能力为辅,读者期待身份反差、职场逆袭与生活烟火气。",
1181
+ id: "genre_urban_life",
1182
+ template: "强调性格反差、生活细节与烟火气中的人情与反转。",
731
1183
  children: [
732
1184
  {
733
1185
  name: "赘婿逆袭",
@@ -764,6 +1216,8 @@ const BUILTIN_GENRE_LIBRARY = [
764
1216
  {
765
1217
  name: "言情",
766
1218
  description: "以情感关系为核心,读者期待心动、拉扯、甜虐交织与情感归宿。",
1219
+ id: "genre_romance",
1220
+ template: "强调情感拉扯、关系升温与情绪节拍。",
767
1221
  children: [
768
1222
  {
769
1223
  name: "现言甜宠",
@@ -795,6 +1249,8 @@ const BUILTIN_GENRE_LIBRARY = [
795
1249
  {
796
1250
  name: "游戏竞技",
797
1251
  description: "以游戏或体育竞技为舞台,读者期待操作碾压、战术博弈与冠军荣耀。",
1252
+ id: "genre_esports",
1253
+ template: "强调操作细节、团队配合与赛事热血、逆转。",
798
1254
  children: [
799
1255
  {
800
1256
  name: "电竞荣耀",
@@ -821,6 +1277,8 @@ const BUILTIN_GENRE_LIBRARY = [
821
1277
  {
822
1278
  name: "二次元轻小说",
823
1279
  description: "ACG风格叙事,节奏轻快,设定新奇,读者期待脑洞展开与角色萌点。",
1280
+ id: "genre_light_novel",
1281
+ template: "强调新奇设定、角色个性与轻松反套路的喜剧节奏。",
824
1282
  children: [
825
1283
  {
826
1284
  name: "异世界穿越",
@@ -847,6 +1305,8 @@ const BUILTIN_GENRE_LIBRARY = [
847
1305
  {
848
1306
  name: "武侠",
849
1307
  description: "江湖侠义背景,武功秘籍、门派恩怨、家国情怀,读者期待侠气与江湖质感。",
1308
+ id: "genre_wuxia",
1309
+ template: "强调江湖恩怨、武功描写与侠义气的抉择。",
850
1310
  children: [
851
1311
  {
852
1312
  name: "传统武侠",
@@ -868,6 +1328,8 @@ const BUILTIN_GENRE_LIBRARY = [
868
1328
  {
869
1329
  name: "现实题材",
870
1330
  description: "贴近现实生活的行业/职场故事,读者期待专业质感、人性刻画与现实共鸣。",
1331
+ id: "genre_realistic",
1332
+ template: "强调行业专业、人性博弈与现实质感。",
871
1333
  children: [
872
1334
  {
873
1335
  name: "职场商战",
@@ -894,6 +1356,8 @@ const BUILTIN_GENRE_LIBRARY = [
894
1356
  {
895
1357
  name: "恐怖灵异",
896
1358
  description: "超自然/惊悚背景,读者期待氛围营造、悬念反转与心理恐惧。",
1359
+ id: "genre_horror",
1360
+ template: "强调氛围营造、心理恐惧与规则/文化诡异。",
897
1361
  children: [
898
1362
  {
899
1363
  name: "灵异惊悚",
@@ -920,6 +1384,8 @@ const BUILTIN_GENRE_LIBRARY = [
920
1384
  {
921
1385
  name: "科幻",
922
1386
  description: "未来/太空/科技背景,读者期待宏大设定、技术想象与文明思辨。",
1387
+ id: "genre_sci_fi",
1388
+ template: "强调科技想象、宇宙尺度与文明/命运抉择。",
923
1389
  children: [
924
1390
  {
925
1391
  name: "星际文明",
@@ -1030,6 +1496,1217 @@ const BUILTIN_PROGRESSION_MODES = [
1030
1496
  "为虐而虐"
1031
1497
  ],
1032
1498
  primary: false
1499
+ },
1500
+ {
1501
+ key: "story_mode_power_root",
1502
+ name: "爽感推进",
1503
+ template: "强调优势展示、局势翻转和清晰的读者爽点兑现。",
1504
+ driver: "通过主角优势兑现、认知反差和局势翻转持续制造爽感。",
1505
+ readerExpectation: "读者持续看到压制、立威、反转和规则被改写的满足感。",
1506
+ payoffs: [
1507
+ "立威",
1508
+ "打破质疑",
1509
+ "扩大影响力",
1510
+ "阶段性碾压"
1511
+ ],
1512
+ risks: ["主角长期被动", "爽点迟迟不兑现"],
1513
+ primary: true,
1514
+ progressionUnits: [
1515
+ "立威",
1516
+ "打破质疑",
1517
+ "扩大影响力",
1518
+ "阶段性碾压"
1519
+ ],
1520
+ allowedConflictForms: [
1521
+ "身份压制",
1522
+ "认知偏差",
1523
+ "权力挑战",
1524
+ "规则重写"
1525
+ ],
1526
+ forbiddenConflictForms: ["长期弱势求生", "持续吃瘪不反击"],
1527
+ conflictCeiling: "high",
1528
+ chapterUnit: "每章推进一次压制、反转或立威结果。",
1529
+ volumeReward: "卷末形成更大范围的承认、恐惧、臣服或秩序改写。",
1530
+ mandatorySignals: [
1531
+ "优势感",
1532
+ "反差感",
1533
+ "立威场面"
1534
+ ],
1535
+ antiSignals: ["主角长期被动", "爽点迟迟不兑现"],
1536
+ resolutionStyle: "尽快兑现主角优势,让冲突成为展示力量与地位的舞台。"
1537
+ },
1538
+ {
1539
+ key: "story_mode_build_root",
1540
+ name: "建设经营",
1541
+ template: "让世界随着主角行动而变得更丰富、更稳固、更有回报。",
1542
+ driver: "通过积累、扩张、经营和建设成果持续制造成就感。",
1543
+ readerExpectation: "读者能看到资源变多、地盘变稳、系统变完整的满足感。",
1544
+ payoffs: [
1545
+ "积累资源",
1546
+ "建设节点",
1547
+ "经营升级",
1548
+ "阶段性收成"
1549
+ ],
1550
+ risks: ["主驱动变成纯战斗文", "长期看不到收成"],
1551
+ primary: true,
1552
+ progressionUnits: [
1553
+ "积累资源",
1554
+ "建设节点",
1555
+ "经营升级",
1556
+ "阶段性收成"
1557
+ ],
1558
+ allowedConflictForms: [
1559
+ "资源压力",
1560
+ "经营竞争",
1561
+ "发展阻碍"
1562
+ ],
1563
+ forbiddenConflictForms: ["反派长期压过建设主线", "全靠大战推进"],
1564
+ conflictCeiling: "medium",
1565
+ chapterUnit: "每章推进一个可见的经营或建设单位。",
1566
+ volumeReward: "卷末给出一个更稳、更大、更丰富的成果面貌。",
1567
+ mandatorySignals: [
1568
+ "积累",
1569
+ "建设过程",
1570
+ "成果展示"
1571
+ ],
1572
+ antiSignals: ["主驱动变成纯战斗文", "长期看不到收成"],
1573
+ resolutionStyle: "优先用经营、组织、建设和资源调度化解问题。"
1574
+ },
1575
+ {
1576
+ key: "story_mode_healing_root",
1577
+ name: "日常治愈",
1578
+ template: "矛盾可以有,但不能破坏回暖感和陪伴感。",
1579
+ driver: "通过陪伴、修复、生活细节和关系回暖持续吸引读者。",
1580
+ readerExpectation: "读者反复获得安稳、温柔、回暖和情绪落地的满足感。",
1581
+ payoffs: [
1582
+ "生活日常",
1583
+ "陪伴修复",
1584
+ "小问题解决",
1585
+ "关系回温"
1586
+ ],
1587
+ risks: ["高压主线喧宾夺主", "治愈感被连续打断"],
1588
+ primary: true,
1589
+ progressionUnits: [
1590
+ "生活日常",
1591
+ "陪伴修复",
1592
+ "小问题解决",
1593
+ "关系回温"
1594
+ ],
1595
+ allowedConflictForms: [
1596
+ "低到中烈度困境",
1597
+ "关系误差",
1598
+ "生活压力"
1599
+ ],
1600
+ forbiddenConflictForms: [
1601
+ "持续高压对抗",
1602
+ "黑化式推进",
1603
+ "无休止背刺"
1604
+ ],
1605
+ conflictCeiling: "low",
1606
+ chapterUnit: "每章围绕一个生活片段或情绪修复点展开。",
1607
+ volumeReward: "卷末让读者感到人物状态更稳、更暖、更愿意继续生活。",
1608
+ mandatorySignals: [
1609
+ "生活感",
1610
+ "安抚点",
1611
+ "关系回暖"
1612
+ ],
1613
+ antiSignals: ["高压主线喧宾夺主", "治愈感被连续打断"],
1614
+ resolutionStyle: "优先通过陪伴、理解、日常行动和小范围修复化解问题。"
1615
+ },
1616
+ {
1617
+ key: "story_mode_comedy_root",
1618
+ name: "喜剧整活",
1619
+ template: "轻松不是语气标签,而是章节结构里持续兑现的笑点机制。",
1620
+ driver: "通过反差、包袱、误会和整活节奏持续制造轻松爽快感。",
1621
+ readerExpectation: "读者频繁获得笑点、反套路和情绪减压体验。",
1622
+ payoffs: [
1623
+ "设包袱",
1624
+ "误会升级",
1625
+ "反差回收",
1626
+ "局面失控"
1627
+ ],
1628
+ risks: ["长时间严肃无包袱", "笑点只靠口癖和段子堆砌"],
1629
+ primary: true,
1630
+ progressionUnits: [
1631
+ "设包袱",
1632
+ "误会升级",
1633
+ "反差回收",
1634
+ "局面失控"
1635
+ ],
1636
+ allowedConflictForms: [
1637
+ "误会",
1638
+ "反差",
1639
+ "社死",
1640
+ "整活翻车"
1641
+ ],
1642
+ forbiddenConflictForms: ["长篇沉重压抑线长期占主导", "笑点没有回收"],
1643
+ conflictCeiling: "medium",
1644
+ chapterUnit: "每章至少要推进一个有效笑点结构。",
1645
+ volumeReward: "卷末形成更大的整活名场面或误会共同体。",
1646
+ mandatorySignals: [
1647
+ "反差",
1648
+ "回收",
1649
+ "轻松释放"
1650
+ ],
1651
+ antiSignals: ["长时间严肃无包袱", "笑点只靠口癖和段子堆砌"],
1652
+ resolutionStyle: "通过包袱回收和失控场面完成释放。"
1653
+ },
1654
+ {
1655
+ key: "story_mode_mystery_root",
1656
+ name: "悬念博弈",
1657
+ template: "读者要持续感觉自己在往更深的真相推进。",
1658
+ driver: "通过信息差、推演和博弈升级持续制造想追下去的欲望。",
1659
+ readerExpectation: "读者不断获得谜面推进、推理快感和布局回收的满足。",
1660
+ payoffs: [
1661
+ "抛出疑点",
1662
+ "收集线索",
1663
+ "推演验证",
1664
+ "局势反转"
1665
+ ],
1666
+ risks: ["谜团只堆不解", "答案靠天降"],
1667
+ primary: true,
1668
+ progressionUnits: [
1669
+ "抛出疑点",
1670
+ "收集线索",
1671
+ "推演验证",
1672
+ "局势反转"
1673
+ ],
1674
+ allowedConflictForms: [
1675
+ "信息差",
1676
+ "隐藏动机",
1677
+ "智性对抗"
1678
+ ],
1679
+ forbiddenConflictForms: ["为了保密故意不讲理", "纯体力对抗吃掉推演感"],
1680
+ conflictCeiling: "high",
1681
+ chapterUnit: "每章推进一个新疑点或一个旧疑点的验证。",
1682
+ volumeReward: "卷末揭开一层更大的真相或完成一次关键博弈。",
1683
+ mandatorySignals: [
1684
+ "线索",
1685
+ "推演",
1686
+ "反制"
1687
+ ],
1688
+ antiSignals: ["谜团只堆不解", "答案靠天降"],
1689
+ resolutionStyle: "通过证据链、推演和布局完成反制。"
1690
+ },
1691
+ {
1692
+ key: "story_mode_relationship_root",
1693
+ name: "关系情感",
1694
+ template: "重点不是单纯堆冲突,而是让关系状态持续变化并兑现读者期待。",
1695
+ driver: "通过人物关系的靠近、拉扯、错位与回收,持续制造追读动力。",
1696
+ readerExpectation: "读者不断获得关系变化、情感张力和关键情绪兑现。",
1697
+ payoffs: [
1698
+ "关系建立",
1699
+ "情绪拉扯",
1700
+ "信任变化",
1701
+ "节点兑现"
1702
+ ],
1703
+ risks: ["关系原地踏步", "只有设定没有互动"],
1704
+ primary: true,
1705
+ progressionUnits: [
1706
+ "关系建立",
1707
+ "情绪拉扯",
1708
+ "信任变化",
1709
+ "节点兑现"
1710
+ ],
1711
+ allowedConflictForms: [
1712
+ "关系误差",
1713
+ "情感错位",
1714
+ "现实阻力",
1715
+ "价值观摩擦"
1716
+ ],
1717
+ forbiddenConflictForms: [
1718
+ "关系线长期停滞",
1719
+ "无意义狗血反复打转",
1720
+ "情感推进被其他线长期吞没"
1721
+ ],
1722
+ conflictCeiling: "medium",
1723
+ chapterUnit: "每章推进一个关系节点、情绪变化或彼此认知变化。",
1724
+ volumeReward: "卷末让关键关系发生明确变化或完成一次高价值情感兑现。",
1725
+ mandatorySignals: [
1726
+ "关系张力",
1727
+ "情绪节点",
1728
+ "兑现感"
1729
+ ],
1730
+ antiSignals: ["关系原地踏步", "只有设定没有互动"],
1731
+ resolutionStyle: "通过情绪交流、关系行动和关键选择完成推进或修复。"
1732
+ },
1733
+ {
1734
+ key: "story_mode_growth_root",
1735
+ name: "成长冒险",
1736
+ template: "读者需要持续感知到人物能力、认知或世界边界在扩展。",
1737
+ driver: "通过成长曲线、探索推进和阶段突破持续制造前进感。",
1738
+ readerExpectation: "读者反复获得变强、解锁新区域和达成新门槛的满足。",
1739
+ payoffs: [
1740
+ "获得目标",
1741
+ "挑战升级",
1742
+ "突破瓶颈",
1743
+ "进入新区域"
1744
+ ],
1745
+ risks: ["升级口头化", "冒险线停摆"],
1746
+ primary: true,
1747
+ progressionUnits: [
1748
+ "获得目标",
1749
+ "挑战升级",
1750
+ "突破瓶颈",
1751
+ "进入新区域"
1752
+ ],
1753
+ allowedConflictForms: [
1754
+ "试炼",
1755
+ "探索风险",
1756
+ "阶段门槛",
1757
+ "成长阵痛"
1758
+ ],
1759
+ forbiddenConflictForms: [
1760
+ "长期停滞不成长",
1761
+ "只有升级数值没有体验变化",
1762
+ "探索线长期缺席"
1763
+ ],
1764
+ conflictCeiling: "high",
1765
+ chapterUnit: "每章推进一个成长动作、一次探索发现或一段闯关反馈。",
1766
+ volumeReward: "卷末让人物到达更高阶段,或打开一片更大的冒险空间。",
1767
+ mandatorySignals: [
1768
+ "成长感",
1769
+ "突破感",
1770
+ "世界扩展"
1771
+ ],
1772
+ antiSignals: ["升级口头化", "冒险线停摆"],
1773
+ resolutionStyle: "通过训练、实践、探索和阶段性突破完成破局。"
1774
+ },
1775
+ {
1776
+ key: "story_mode_invincible",
1777
+ name: "无敌流",
1778
+ template: "尽快立住主角上限,重点写压制、破局、立威和改规则。",
1779
+ driver: "让主角以明显优势碾压阻力,并不断刷新他人认知。",
1780
+ readerExpectation: "读者每隔几章都能看到一次明确的压制和地位确立。",
1781
+ payoffs: [
1782
+ "展示底牌",
1783
+ "压制对手",
1784
+ "扩大威慑",
1785
+ "重写规则"
1786
+ ],
1787
+ risks: ["主角像普通升级文一样长期弱势", "核心优势被故意封死"],
1788
+ primary: false,
1789
+ progressionUnits: [
1790
+ "展示底牌",
1791
+ "压制对手",
1792
+ "扩大威慑",
1793
+ "重写规则"
1794
+ ],
1795
+ allowedConflictForms: [
1796
+ "身份误判",
1797
+ "势力挑衅",
1798
+ "高手试探"
1799
+ ],
1800
+ forbiddenConflictForms: ["长期躲藏发育", "长时间无法反击的受虐剧情"],
1801
+ conflictCeiling: "high",
1802
+ chapterUnit: "单章围绕一次压制、试探或立威展开。",
1803
+ volumeReward: "卷末形成新的权力格局或更大范围的承认。",
1804
+ mandatorySignals: [
1805
+ "强者气场",
1806
+ "围观震撼",
1807
+ "越级压制"
1808
+ ],
1809
+ antiSignals: ["主角像普通升级文一样长期弱势", "核心优势被故意封死"],
1810
+ resolutionStyle: "冲突以快速反制和高位碾压收束。"
1811
+ },
1812
+ {
1813
+ key: "story_mode_face_slap",
1814
+ name: "打脸流",
1815
+ template: "先蓄势误判,再精准反转,打脸要及时回收。",
1816
+ driver: "通过他人误判和后续反转持续兑现高频打脸快感。",
1817
+ readerExpectation: "读者不断看到轻视者被现场回收和反噬。",
1818
+ payoffs: [
1819
+ "误判铺垫",
1820
+ "身份反转",
1821
+ "公开回收",
1822
+ "舆论发酵"
1823
+ ],
1824
+ risks: ["只挨打不回收", "回收太轻没有爽点"],
1825
+ primary: false,
1826
+ progressionUnits: [
1827
+ "误判铺垫",
1828
+ "身份反转",
1829
+ "公开回收",
1830
+ "舆论发酵"
1831
+ ],
1832
+ allowedConflictForms: [
1833
+ "轻视",
1834
+ "公开羞辱",
1835
+ "资源争夺"
1836
+ ],
1837
+ forbiddenConflictForms: ["铺垫过长却没有回收", "打脸后没有实际影响"],
1838
+ conflictCeiling: "medium",
1839
+ chapterUnit: "单章重点制造一次误判与回收闭环。",
1840
+ volumeReward: "卷末主角从被看轻者转为不可忽视的中心人物。",
1841
+ mandatorySignals: [
1842
+ "误判",
1843
+ "反转",
1844
+ "当场回收"
1845
+ ],
1846
+ antiSignals: ["只挨打不回收", "回收太轻没有爽点"],
1847
+ resolutionStyle: "在最需要证明自己的场合完成反转和回收。"
1848
+ },
1849
+ {
1850
+ key: "story_mode_misread",
1851
+ name: "迪化流",
1852
+ template: "误会要层层放大,并不断变成对主角有利的局面。",
1853
+ driver: "利用他人对主角的过度解读制造持续失控的优势局面。",
1854
+ readerExpectation: "读者反复获得‘别人自己脑补过头’的反差快感。",
1855
+ payoffs: [
1856
+ "误读",
1857
+ "脑补升级",
1858
+ "群体扩散",
1859
+ "误会兑现"
1860
+ ],
1861
+ risks: ["误会强行", "所有人都很快看穿"],
1862
+ primary: false,
1863
+ progressionUnits: [
1864
+ "误读",
1865
+ "脑补升级",
1866
+ "群体扩散",
1867
+ "误会兑现"
1868
+ ],
1869
+ allowedConflictForms: [
1870
+ "信息不对称",
1871
+ "误会连锁",
1872
+ "群体解读偏差"
1873
+ ],
1874
+ forbiddenConflictForms: ["直接解释清楚", "误会只持续一次就结束"],
1875
+ conflictCeiling: "medium",
1876
+ chapterUnit: "单章围绕一次误解升级和意外收益展开。",
1877
+ volumeReward: "卷末误读体系形成稳定共识或传奇形象。",
1878
+ mandatorySignals: [
1879
+ "脑补",
1880
+ "失控传播",
1881
+ "主角被动受益"
1882
+ ],
1883
+ antiSignals: ["误会强行", "所有人都很快看穿"],
1884
+ resolutionStyle: "让误会自然滚大,并转化为主角资源或声望。"
1885
+ },
1886
+ {
1887
+ key: "story_mode_secret_identity",
1888
+ name: "马甲流",
1889
+ template: "马甲要各有功能,掉马风险要形成持续钩子。",
1890
+ driver: "通过多重身份切换、隐藏与掉马风险制造连续张力。",
1891
+ readerExpectation: "读者不断看到身份差、信息差和掉马边缘的刺激感。",
1892
+ payoffs: [
1893
+ "建立马甲",
1894
+ "切换身份",
1895
+ "险些掉马",
1896
+ "掉马兑现"
1897
+ ],
1898
+ risks: ["身份线长期静止", "马甲只是名字不同没有用途"],
1899
+ primary: false,
1900
+ progressionUnits: [
1901
+ "建立马甲",
1902
+ "切换身份",
1903
+ "险些掉马",
1904
+ "掉马兑现"
1905
+ ],
1906
+ allowedConflictForms: [
1907
+ "身份隐藏",
1908
+ "关系错位",
1909
+ "能力来源误判"
1910
+ ],
1911
+ forbiddenConflictForms: ["所有马甲功能重复", "掉马没有后果"],
1912
+ conflictCeiling: "medium",
1913
+ chapterUnit: "单章至少推进一次身份利用或风险逼近。",
1914
+ volumeReward: "卷末形成关键掉马或更复杂的身份网。",
1915
+ mandatorySignals: [
1916
+ "信息差",
1917
+ "身份切换",
1918
+ "掉马钩子"
1919
+ ],
1920
+ antiSignals: ["身份线长期静止", "马甲只是名字不同没有用途"],
1921
+ resolutionStyle: "围绕身份切换解决问题,再把压力转移到下一层掉马风险。"
1922
+ },
1923
+ {
1924
+ key: "story_mode_farming",
1925
+ name: "种田流",
1926
+ template: "重点写资源循环、土地或据点经营、关系熟化和阶段性收成。",
1927
+ driver: "通过一点点把生活与生产盘活,给读者稳定的回暖和积累感。",
1928
+ readerExpectation: "读者持续获得‘日子越来越好’和‘家底越来越厚’的满足感。",
1929
+ payoffs: [
1930
+ "播种准备",
1931
+ "生产积累",
1932
+ "邻里互动",
1933
+ "收成兑现"
1934
+ ],
1935
+ risks: ["只剩打怪和阴谋", "看不到生活改善"],
1936
+ primary: false,
1937
+ progressionUnits: [
1938
+ "播种准备",
1939
+ "生产积累",
1940
+ "邻里互动",
1941
+ "收成兑现"
1942
+ ],
1943
+ allowedConflictForms: [
1944
+ "资源短缺",
1945
+ "天气与环境压力",
1946
+ "小范围利益摩擦"
1947
+ ],
1948
+ forbiddenConflictForms: ["长期高压生死线", "无止境的大反派主线"],
1949
+ conflictCeiling: "low",
1950
+ chapterUnit: "单章围绕一项劳作、一项建设或一段生活改善展开。",
1951
+ volumeReward: "卷末形成一个明显更稳定、更温暖的生活阶段。",
1952
+ mandatorySignals: [
1953
+ "生活感",
1954
+ "劳作细节",
1955
+ "阶段收成"
1956
+ ],
1957
+ antiSignals: ["只剩打怪和阴谋", "看不到生活改善"],
1958
+ resolutionStyle: "通过勤劳、组织、互助和经营慢慢解决问题。"
1959
+ },
1960
+ {
1961
+ key: "story_mode_management",
1962
+ name: "经营流",
1963
+ template: "强调决策、运营、扩张、口碑和阶段性经营结果。",
1964
+ driver: "通过经营决策和组织扩张持续制造上升感。",
1965
+ readerExpectation: "读者反复看到业务跑起来、组织成型和口碑增长。",
1966
+ payoffs: [
1967
+ "找到突破口",
1968
+ "搭建流程",
1969
+ "扩大规模",
1970
+ "赢得市场"
1971
+ ],
1972
+ risks: ["只讲结果不讲过程", "经营主线被反派线吃掉"],
1973
+ primary: false,
1974
+ progressionUnits: [
1975
+ "找到突破口",
1976
+ "搭建流程",
1977
+ "扩大规模",
1978
+ "赢得市场"
1979
+ ],
1980
+ allowedConflictForms: [
1981
+ "经营竞争",
1982
+ "资源调配",
1983
+ "团队磨合"
1984
+ ],
1985
+ forbiddenConflictForms: ["无关的恋爱狗血长期抢戏", "经营过程被一笔带过"],
1986
+ conflictCeiling: "medium",
1987
+ chapterUnit: "单章推进一个经营动作或组织问题。",
1988
+ volumeReward: "卷末形成业务升级、品牌确立或组织跃迁。",
1989
+ mandatorySignals: [
1990
+ "策略感",
1991
+ "经营动作",
1992
+ "结果反馈"
1993
+ ],
1994
+ antiSignals: ["只讲结果不讲过程", "经营主线被反派线吃掉"],
1995
+ resolutionStyle: "靠策略、运营、分工和节奏管理解决问题。"
1996
+ },
1997
+ {
1998
+ key: "story_mode_territory_building",
1999
+ name: "领地建设",
2000
+ template: "重点写领地升级、人口汇聚、规则建立和防线稳固。",
2001
+ driver: "通过把一块地、一座城或一个据点逐步经营成型,持续制造扩张与稳固的成就感。",
2002
+ readerExpectation: "读者不断看到领地变强、秩序成型和安全感提升。",
2003
+ payoffs: [
2004
+ "招募与安置",
2005
+ "设施建设",
2006
+ "规则落地",
2007
+ "外部试探回击"
2008
+ ],
2009
+ risks: ["只有战争没有建设", "领地存在感越来越弱"],
2010
+ primary: false,
2011
+ progressionUnits: [
2012
+ "招募与安置",
2013
+ "设施建设",
2014
+ "规则落地",
2015
+ "外部试探回击"
2016
+ ],
2017
+ allowedConflictForms: [
2018
+ "资源紧张",
2019
+ "边界摩擦",
2020
+ "管理压力",
2021
+ "治安危机"
2022
+ ],
2023
+ forbiddenConflictForms: ["领地长期形同虚设", "主线被纯个人恩怨彻底带走"],
2024
+ conflictCeiling: "medium",
2025
+ chapterUnit: "单章推进一个领地建设动作、治理问题或外部试探应对。",
2026
+ volumeReward: "卷末让领地规模、秩序或影响力出现清晰跃升。",
2027
+ mandatorySignals: [
2028
+ "据点成长",
2029
+ "治理动作",
2030
+ "秩序成型"
2031
+ ],
2032
+ antiSignals: ["只有战争没有建设", "领地存在感越来越弱"],
2033
+ resolutionStyle: "优先通过建设、治理、组织和局部反制稳定局面。"
2034
+ },
2035
+ {
2036
+ key: "story_mode_family_management",
2037
+ name: "家族经营",
2038
+ template: "把资源经营、家族分工和内部关系变化一起写活。",
2039
+ driver: "通过家业扩张、家族关系重组和责任传承维持故事推进。",
2040
+ readerExpectation: "读者既能看到家底变厚,也能看到家族成员关系越来越成体系。",
2041
+ payoffs: [
2042
+ "家业调整",
2043
+ "成员磨合",
2044
+ "资源积累",
2045
+ "家族地位提升"
2046
+ ],
2047
+ risks: ["只剩吵架不见经营", "家族线没有传承感"],
2048
+ primary: false,
2049
+ progressionUnits: [
2050
+ "家业调整",
2051
+ "成员磨合",
2052
+ "资源积累",
2053
+ "家族地位提升"
2054
+ ],
2055
+ allowedConflictForms: [
2056
+ "家务分歧",
2057
+ "利益分配",
2058
+ "外部竞争",
2059
+ "代际观念冲突"
2060
+ ],
2061
+ forbiddenConflictForms: ["狗血撕裂长期压倒经营线", "家族成员只有工具功能没有关系变化"],
2062
+ conflictCeiling: "medium",
2063
+ chapterUnit: "单章推进一次家业动作、一场家族互动或一项责任兑现。",
2064
+ volumeReward: "卷末让家族更稳固、更有凝聚力,或完成一次地位跃升。",
2065
+ mandatorySignals: [
2066
+ "家族协作",
2067
+ "家业动作",
2068
+ "关系重组"
2069
+ ],
2070
+ antiSignals: ["只剩吵架不见经营", "家族线没有传承感"],
2071
+ resolutionStyle: "通过协商、分工、经营结果和共同应对危机完成修复。"
2072
+ },
2073
+ {
2074
+ key: "story_mode_healing_daily",
2075
+ name: "治愈日常",
2076
+ template: "保持低烈度困境和高密度安抚点,让读者愿意停留。",
2077
+ driver: "靠日常陪伴和细小修复慢慢把人物带回稳定状态。",
2078
+ readerExpectation: "读者每几章都获得一次情绪被抚平的体验。",
2079
+ payoffs: [
2080
+ "日常互动",
2081
+ "情绪安抚",
2082
+ "关系熟化",
2083
+ "生活改善"
2084
+ ],
2085
+ risks: ["冲突升级成高压大戏", "读完一章只剩焦虑"],
2086
+ primary: false,
2087
+ progressionUnits: [
2088
+ "日常互动",
2089
+ "情绪安抚",
2090
+ "关系熟化",
2091
+ "生活改善"
2092
+ ],
2093
+ allowedConflictForms: [
2094
+ "生活小挫折",
2095
+ "关系别扭",
2096
+ "旧伤回响"
2097
+ ],
2098
+ forbiddenConflictForms: ["大反派压顶", "长期极端痛苦不回收"],
2099
+ conflictCeiling: "low",
2100
+ chapterUnit: "每章围绕一个情绪节点和一个安抚点展开。",
2101
+ volumeReward: "卷末让角色和读者都感到状态明显回暖。",
2102
+ mandatorySignals: [
2103
+ "安抚感",
2104
+ "生活细节",
2105
+ "温柔互动"
2106
+ ],
2107
+ antiSignals: ["冲突升级成高压大戏", "读完一章只剩焦虑"],
2108
+ resolutionStyle: "让人物通过陪伴、倾听和具体行动被一点点治好。"
2109
+ },
2110
+ {
2111
+ key: "story_mode_shop_daily",
2112
+ name: "小店日常",
2113
+ template: "顾客、邻里和经营琐事都应成为温柔推进器。",
2114
+ driver: "通过店铺经营、来客故事和社区互动形成持续新鲜感。",
2115
+ readerExpectation: "读者既能看到经营变化,也能得到温暖的陌生人故事。",
2116
+ payoffs: [
2117
+ "来客事件",
2118
+ "经营调整",
2119
+ "社区互动",
2120
+ "日常收束"
2121
+ ],
2122
+ risks: ["只剩经营数据没有人情味", "店铺存在感越来越弱"],
2123
+ primary: false,
2124
+ progressionUnits: [
2125
+ "来客事件",
2126
+ "经营调整",
2127
+ "社区互动",
2128
+ "日常收束"
2129
+ ],
2130
+ allowedConflictForms: [
2131
+ "小店经营压力",
2132
+ "人情摩擦",
2133
+ "生活琐事"
2134
+ ],
2135
+ forbiddenConflictForms: ["连续恶性打压", "重悬疑或高压阴谋主导"],
2136
+ conflictCeiling: "medium",
2137
+ chapterUnit: "每章推进一次来客互动或经营小事件。",
2138
+ volumeReward: "卷末让小店更有人气、更有归属感。",
2139
+ mandatorySignals: [
2140
+ "小店空间感",
2141
+ "来客故事",
2142
+ "社区温度"
2143
+ ],
2144
+ antiSignals: ["只剩经营数据没有人情味", "店铺存在感越来越弱"],
2145
+ resolutionStyle: "通过待人接物、经营调整和社区互助自然化解。"
2146
+ },
2147
+ {
2148
+ key: "story_mode_slow_life",
2149
+ name: "慢生活",
2150
+ template: "让风景、劳作、饮食和相处方式都成为安稳体验的一部分。",
2151
+ driver: "通过缓慢但持续的生活改善与身心安定感推动读者停留。",
2152
+ readerExpectation: "读者反复获得放松、沉浸和‘终于缓下来’的满足感。",
2153
+ payoffs: [
2154
+ "日常作息",
2155
+ "环境熟悉",
2156
+ "生活改善",
2157
+ "情绪回稳"
2158
+ ],
2159
+ risks: ["为了刺激频繁硬拗大冲突", "节奏越来越躁没有呼吸感"],
2160
+ primary: false,
2161
+ progressionUnits: [
2162
+ "日常作息",
2163
+ "环境熟悉",
2164
+ "生活改善",
2165
+ "情绪回稳"
2166
+ ],
2167
+ allowedConflictForms: [
2168
+ "轻微不适应",
2169
+ "生活小困扰",
2170
+ "关系疏离后的磨合"
2171
+ ],
2172
+ forbiddenConflictForms: ["持续高压倒计时", "强反派长期追杀或压顶阴谋"],
2173
+ conflictCeiling: "low",
2174
+ chapterUnit: "单章围绕一个生活片段、一项微小改善或一次情绪缓冲展开。",
2175
+ volumeReward: "卷末让人物真正拥有一个更稳、更舒服的生活状态。",
2176
+ mandatorySignals: [
2177
+ "生活秩序",
2178
+ "松弛感",
2179
+ "细节沉浸"
2180
+ ],
2181
+ antiSignals: ["为了刺激频繁硬拗大冲突", "节奏越来越躁没有呼吸感"],
2182
+ resolutionStyle: "通过时间、陪伴、规律生活和小步修复慢慢化解问题。"
2183
+ },
2184
+ {
2185
+ key: "story_mode_companion_healing",
2186
+ name: "陪伴疗愈",
2187
+ template: "矛盾要服务于靠近、信任和修复,而不是不断撕裂。",
2188
+ driver: "通过陪伴关系逐步建立、安全感逐步累积来带动情绪兑现。",
2189
+ readerExpectation: "读者不断获得被理解、被陪着走过低谷的温柔满足。",
2190
+ payoffs: [
2191
+ "接近与试探",
2192
+ "日常照料",
2193
+ "情绪松动",
2194
+ "关系确认"
2195
+ ],
2196
+ risks: ["治愈线突然变虐恋拉扯", "陪伴关系没有实质推进"],
2197
+ primary: false,
2198
+ progressionUnits: [
2199
+ "接近与试探",
2200
+ "日常照料",
2201
+ "情绪松动",
2202
+ "关系确认"
2203
+ ],
2204
+ allowedConflictForms: [
2205
+ "旧伤回避",
2206
+ "沟通迟滞",
2207
+ "生活压力",
2208
+ "短暂误解"
2209
+ ],
2210
+ forbiddenConflictForms: [
2211
+ "反复背刺",
2212
+ "虐点长时间不回收",
2213
+ "关系恶性拉扯失控"
2214
+ ],
2215
+ conflictCeiling: "low",
2216
+ chapterUnit: "单章推进一次陪伴动作、一次关系松动或一次情绪落地。",
2217
+ volumeReward: "卷末让关键关系显著升温,人物更愿意相信他人和生活。",
2218
+ mandatorySignals: [
2219
+ "陪伴感",
2220
+ "信任增长",
2221
+ "修复落地"
2222
+ ],
2223
+ antiSignals: ["治愈线突然变虐恋拉扯", "陪伴关系没有实质推进"],
2224
+ resolutionStyle: "以耐心、陪伴、行动支持和情绪回应完成修复。"
2225
+ },
2226
+ {
2227
+ key: "story_mode_comedy",
2228
+ name: "搞笑流",
2229
+ template: "笑点必须结构化出现,不能只靠零散金句。",
2230
+ driver: "靠高频包袱、反差和失控场面维持阅读快乐。",
2231
+ readerExpectation: "读者每章都能收获明确的轻松和好笑点。",
2232
+ payoffs: [
2233
+ "包袱铺设",
2234
+ "反差升级",
2235
+ "笑点回收",
2236
+ "场面翻车"
2237
+ ],
2238
+ risks: ["整段都很严肃", "只有吐槽没有结构"],
2239
+ primary: false,
2240
+ progressionUnits: [
2241
+ "包袱铺设",
2242
+ "反差升级",
2243
+ "笑点回收",
2244
+ "场面翻车"
2245
+ ],
2246
+ allowedConflictForms: [
2247
+ "误会",
2248
+ "社死",
2249
+ "身份反差",
2250
+ "认知错位"
2251
+ ],
2252
+ forbiddenConflictForms: ["长时间沉重苦情线", "笑点稀薄又不推进剧情"],
2253
+ conflictCeiling: "medium",
2254
+ chapterUnit: "每章围绕至少一个有效包袱闭环展开。",
2255
+ volumeReward: "卷末形成标志性名场面或持续流传的笑料。",
2256
+ mandatorySignals: [
2257
+ "包袱",
2258
+ "回收",
2259
+ "场面感"
2260
+ ],
2261
+ antiSignals: ["整段都很严肃", "只有吐槽没有结构"],
2262
+ resolutionStyle: "用回收和升级继续抬高笑点而不是转沉重。"
2263
+ },
2264
+ {
2265
+ key: "story_mode_misunderstanding_comedy",
2266
+ name: "误会喜剧",
2267
+ template: "误会要层层放大,但每次都要带来新的局面。",
2268
+ driver: "通过多方信息差导致的误会扩散维持喜剧节奏。",
2269
+ readerExpectation: "读者不断看到误会越滚越大却越有趣的效果。",
2270
+ payoffs: [
2271
+ "误会建立",
2272
+ "多方误读",
2273
+ "失控扩散",
2274
+ "喜剧回收"
2275
+ ],
2276
+ risks: ["误会无后劲", "澄清过快导致节奏熄火"],
2277
+ primary: false,
2278
+ progressionUnits: [
2279
+ "误会建立",
2280
+ "多方误读",
2281
+ "失控扩散",
2282
+ "喜剧回收"
2283
+ ],
2284
+ allowedConflictForms: [
2285
+ "误会",
2286
+ "错位沟通",
2287
+ "集体误读"
2288
+ ],
2289
+ forbiddenConflictForms: ["误会太快澄清", "误会只是重复同一个梗"],
2290
+ conflictCeiling: "medium",
2291
+ chapterUnit: "每章推进一层新的误解或回收节点。",
2292
+ volumeReward: "卷末形成一场大型误会名场面。",
2293
+ mandatorySignals: [
2294
+ "信息差",
2295
+ "误读",
2296
+ "扩散"
2297
+ ],
2298
+ antiSignals: ["误会无后劲", "澄清过快导致节奏熄火"],
2299
+ resolutionStyle: "让误会在最搞笑的位置回收,或者升级成更大笑点。"
2300
+ },
2301
+ {
2302
+ key: "story_mode_absurd_subversion",
2303
+ name: "沙雕反套路",
2304
+ template: "反套路不只是拆预期,还要持续给出更好玩的新局面。",
2305
+ driver: "通过不断拆解常规预期,再给出更离谱但更成立的结果维持新鲜感。",
2306
+ readerExpectation: "读者持续获得‘居然还能这么来’的惊喜和轻松感。",
2307
+ payoffs: [
2308
+ "建立预期",
2309
+ "突然拧转",
2310
+ "离谱升级",
2311
+ "反套路回收"
2312
+ ],
2313
+ risks: ["为了搞怪牺牲可读性", "段子化堆叠没有故事推进"],
2314
+ primary: false,
2315
+ progressionUnits: [
2316
+ "建立预期",
2317
+ "突然拧转",
2318
+ "离谱升级",
2319
+ "反套路回收"
2320
+ ],
2321
+ allowedConflictForms: [
2322
+ "反差",
2323
+ "错位",
2324
+ "整活失控",
2325
+ "设定玩梗"
2326
+ ],
2327
+ forbiddenConflictForms: [
2328
+ "只剩无意义发疯",
2329
+ "离谱但没有逻辑支点",
2330
+ "连续沉重正剧化"
2331
+ ],
2332
+ conflictCeiling: "medium",
2333
+ chapterUnit: "每章至少推进一个预期建立与反套路回收闭环。",
2334
+ volumeReward: "卷末形成高传播度的离谱名场面或反套路高潮。",
2335
+ mandatorySignals: [
2336
+ "意外感",
2337
+ "反套路",
2338
+ "回收闭环"
2339
+ ],
2340
+ antiSignals: ["为了搞怪牺牲可读性", "段子化堆叠没有故事推进"],
2341
+ resolutionStyle: "通过更高一层的反套路回收局面,而不是落回沉重正统冲突。"
2342
+ },
2343
+ {
2344
+ key: "story_mode_mystery_inference",
2345
+ name: "悬疑推演",
2346
+ template: "每段推演都要建立在清晰线索上,不要纯作者强解。",
2347
+ driver: "让读者跟着线索与推演不断靠近真相。",
2348
+ readerExpectation: "读者持续获得‘拼图正在成型’的智性满足。",
2349
+ payoffs: [
2350
+ "提出疑点",
2351
+ "补充线索",
2352
+ "推演收束",
2353
+ "揭开真相"
2354
+ ],
2355
+ risks: ["故弄玄虚", "结论和线索脱节"],
2356
+ primary: false,
2357
+ progressionUnits: [
2358
+ "提出疑点",
2359
+ "补充线索",
2360
+ "推演收束",
2361
+ "揭开真相"
2362
+ ],
2363
+ allowedConflictForms: [
2364
+ "线索冲突",
2365
+ "证词矛盾",
2366
+ "隐藏动机"
2367
+ ],
2368
+ forbiddenConflictForms: ["关键证据凭空出现", "为了拖延故意含糊"],
2369
+ conflictCeiling: "medium",
2370
+ chapterUnit: "每章推进一个关键疑点的认知变化。",
2371
+ volumeReward: "卷末揭穿核心谜面或打开更大的悬念入口。",
2372
+ mandatorySignals: [
2373
+ "线索感",
2374
+ "逻辑链",
2375
+ "真相逼近"
2376
+ ],
2377
+ antiSignals: ["故弄玄虚", "结论和线索脱节"],
2378
+ resolutionStyle: "通过线索联动和逻辑推演完成局面突破。"
2379
+ },
2380
+ {
2381
+ key: "story_mode_mind_game",
2382
+ name: "智斗流",
2383
+ template: "对手必须像对手,博弈必须可感知。",
2384
+ driver: "通过多轮布局和拆招形成高黏性的智斗快感。",
2385
+ readerExpectation: "读者不断看到算计、反算和预判回收。",
2386
+ payoffs: [
2387
+ "布局",
2388
+ "试探",
2389
+ "反制",
2390
+ "收网"
2391
+ ],
2392
+ risks: ["胜负没有博弈过程", "对手工具化"],
2393
+ primary: false,
2394
+ progressionUnits: [
2395
+ "布局",
2396
+ "试探",
2397
+ "反制",
2398
+ "收网"
2399
+ ],
2400
+ allowedConflictForms: [
2401
+ "策略对抗",
2402
+ "心理博弈",
2403
+ "资源算计"
2404
+ ],
2405
+ forbiddenConflictForms: ["对手太蠢", "所有胜利都靠主角开挂信息"],
2406
+ conflictCeiling: "medium",
2407
+ chapterUnit: "每章推进一轮试探、布子或拆招。",
2408
+ volumeReward: "卷末完成一次大局收网或更高阶对手登场。",
2409
+ mandatorySignals: [
2410
+ "布局感",
2411
+ "对手压迫",
2412
+ "反制回收"
2413
+ ],
2414
+ antiSignals: ["胜负没有博弈过程", "对手工具化"],
2415
+ resolutionStyle: "通过提前布局和关键反制赢下局面。"
2416
+ },
2417
+ {
2418
+ key: "story_mode_survival_game",
2419
+ name: "生存博弈",
2420
+ template: "高压可以存在,但必须服务于规则推演、抉择代价和局势反制。",
2421
+ driver: "通过资源稀缺、环境压力和规则对抗持续制造必须做出选择的张力。",
2422
+ readerExpectation: "读者反复获得局势求解、代价权衡和绝境翻盘的紧绷满足。",
2423
+ payoffs: [
2424
+ "压力逼近",
2425
+ "规则试探",
2426
+ "资源争夺",
2427
+ "短线反制"
2428
+ ],
2429
+ risks: ["危机廉价化", "总是同一类困境循环"],
2430
+ primary: false,
2431
+ progressionUnits: [
2432
+ "压力逼近",
2433
+ "规则试探",
2434
+ "资源争夺",
2435
+ "短线反制"
2436
+ ],
2437
+ allowedConflictForms: [
2438
+ "资源稀缺",
2439
+ "规则压迫",
2440
+ "环境威胁",
2441
+ "群体博弈"
2442
+ ],
2443
+ forbiddenConflictForms: [
2444
+ "纯靠运气通关",
2445
+ "只有肉搏没有策略",
2446
+ "长期重复同一种危机"
2447
+ ],
2448
+ conflictCeiling: "high",
2449
+ chapterUnit: "单章推进一轮生存压力、规则发现或关键抉择。",
2450
+ volumeReward: "卷末完成一次阶段求生成功、规则突破或阵营格局改写。",
2451
+ mandatorySignals: [
2452
+ "资源压力",
2453
+ "选择代价",
2454
+ "规则利用"
2455
+ ],
2456
+ antiSignals: ["危机廉价化", "总是同一类困境循环"],
2457
+ resolutionStyle: "依靠规则理解、局势判断和有限资源配置完成破局。"
2458
+ },
2459
+ {
2460
+ key: "story_mode_romantic_tension",
2461
+ name: "恋爱拉扯",
2462
+ template: "拉扯要有温差和推进,不能只靠误会拖延。",
2463
+ driver: "通过双向吸引与现实阻力之间的反复拉扯维持情感张力。",
2464
+ readerExpectation: "读者不断看到关系升温、试探失控和情绪回收。",
2465
+ payoffs: [
2466
+ "试探靠近",
2467
+ "边界碰撞",
2468
+ "情绪失衡",
2469
+ "关系推进"
2470
+ ],
2471
+ risks: ["只有误会拖延", "没有实质关系变化"],
2472
+ primary: false,
2473
+ progressionUnits: [
2474
+ "试探靠近",
2475
+ "边界碰撞",
2476
+ "情绪失衡",
2477
+ "关系推进"
2478
+ ],
2479
+ allowedConflictForms: [
2480
+ "暧昧错位",
2481
+ "表达失败",
2482
+ "现实阻力",
2483
+ "价值观差异"
2484
+ ],
2485
+ forbiddenConflictForms: [
2486
+ "故意降智不说人话",
2487
+ "拖太久不推进",
2488
+ "关系线完全被外部主线压没"
2489
+ ],
2490
+ conflictCeiling: "medium",
2491
+ chapterUnit: "单章推进一个情感试探、认知变化或关系边界松动。",
2492
+ volumeReward: "卷末让关系进入新阶段,或完成一次高浓度情绪确认。",
2493
+ mandatorySignals: [
2494
+ "暧昧感",
2495
+ "双向张力",
2496
+ "靠近兑现"
2497
+ ],
2498
+ antiSignals: ["只有误会拖延", "没有实质关系变化"],
2499
+ resolutionStyle: "通过互动升级、情绪承认和行动选择完成关系推进。"
2500
+ },
2501
+ {
2502
+ key: "story_mode_chasing_wife",
2503
+ name: "追妻火葬场",
2504
+ template: "核心是追偿和重建,不是单方面反复伤害。",
2505
+ driver: "通过失去后的追偿、悔改和关系重建制造强烈情绪牵引。",
2506
+ readerExpectation: "读者持续获得‘该追的在追、该还的在还、该痛的有回应’的满足。",
2507
+ payoffs: [
2508
+ "后悔显现",
2509
+ "追偿行动",
2510
+ "关系试炼",
2511
+ "重建兑现"
2512
+ ],
2513
+ risks: ["只会口头道歉", "关系重建没有成本"],
2514
+ primary: false,
2515
+ progressionUnits: [
2516
+ "后悔显现",
2517
+ "追偿行动",
2518
+ "关系试炼",
2519
+ "重建兑现"
2520
+ ],
2521
+ allowedConflictForms: [
2522
+ "信任断裂",
2523
+ "旧伤反扑",
2524
+ "补偿落差",
2525
+ "现实阻碍"
2526
+ ],
2527
+ forbiddenConflictForms: [
2528
+ "追偿只停留在嘴上",
2529
+ "二次伤害无限循环",
2530
+ "被追一方完全失去主体性"
2531
+ ],
2532
+ conflictCeiling: "medium",
2533
+ chapterUnit: "单章推进一次追偿动作、一次旧伤碰撞或一次关系松动。",
2534
+ volumeReward: "卷末形成清晰的关系修复阶段或强力情绪回收。",
2535
+ mandatorySignals: [
2536
+ "悔意落地",
2537
+ "行动补偿",
2538
+ "边界重建"
2539
+ ],
2540
+ antiSignals: ["只会口头道歉", "关系重建没有成本"],
2541
+ resolutionStyle: "用持续行动、代价承担和边界尊重推进关系重建。"
2542
+ },
2543
+ {
2544
+ key: "story_mode_ensemble_bond",
2545
+ name: "群像羁绊",
2546
+ template: "群像不是名单堆砌,而是每个角色都要在关系网里发挥作用。",
2547
+ driver: "通过多人互动和羁绊变化让故事始终有新的关系火花。",
2548
+ readerExpectation: "读者不断获得角色之间的新连接、站队变化和情感共振。",
2549
+ payoffs: [
2550
+ "关系串联",
2551
+ "群体事件",
2552
+ "立场变化",
2553
+ "羁绊加深"
2554
+ ],
2555
+ risks: ["角色只报名字不干活", "群像线断裂成单人独角戏"],
2556
+ primary: false,
2557
+ progressionUnits: [
2558
+ "关系串联",
2559
+ "群体事件",
2560
+ "立场变化",
2561
+ "羁绊加深"
2562
+ ],
2563
+ allowedConflictForms: [
2564
+ "立场差异",
2565
+ "责任分歧",
2566
+ "情感错位",
2567
+ "团队压力"
2568
+ ],
2569
+ forbiddenConflictForms: [
2570
+ "群像角色长期工具化",
2571
+ "只剩主角独走",
2572
+ "关系网不发生变化"
2573
+ ],
2574
+ conflictCeiling: "medium",
2575
+ chapterUnit: "单章推进一个群体互动节点或关键角色对之间的关系变化。",
2576
+ volumeReward: "卷末让群像结构更稳,或让队伍/关系网进入新阶段。",
2577
+ mandatorySignals: [
2578
+ "群体互动",
2579
+ "关系联动",
2580
+ "角色互相成就"
2581
+ ],
2582
+ antiSignals: ["角色只报名字不干活", "群像线断裂成单人独角戏"],
2583
+ resolutionStyle: "通过团队互动、共同经历和角色间的选择完成关系演化。"
2584
+ },
2585
+ {
2586
+ key: "story_mode_growth_leveling",
2587
+ name: "升级成长",
2588
+ template: "升级不只写结果,还要写门槛、代价和兑现。",
2589
+ driver: "通过清晰可感的成长曲线和阶段突破持续拉动阅读。",
2590
+ readerExpectation: "读者不断看到能力、地位或认知层级获得实质提升。",
2591
+ payoffs: [
2592
+ "积累条件",
2593
+ "卡点受阻",
2594
+ "完成突破",
2595
+ "展示提升"
2596
+ ],
2597
+ risks: ["升级像流水账", "能力提升没有剧情后果"],
2598
+ primary: false,
2599
+ progressionUnits: [
2600
+ "积累条件",
2601
+ "卡点受阻",
2602
+ "完成突破",
2603
+ "展示提升"
2604
+ ],
2605
+ allowedConflictForms: [
2606
+ "瓶颈",
2607
+ "试炼",
2608
+ "资源不足",
2609
+ "同层竞争"
2610
+ ],
2611
+ forbiddenConflictForms: [
2612
+ "成长全靠旁白略过",
2613
+ "突破没有代价和铺垫",
2614
+ "升级后没有任何兑现"
2615
+ ],
2616
+ conflictCeiling: "high",
2617
+ chapterUnit: "单章推进一次积累、一次卡点试探或一次成长兑现。",
2618
+ volumeReward: "卷末完成一个清晰等级、境界或阶段跃迁。",
2619
+ mandatorySignals: [
2620
+ "成长门槛",
2621
+ "突破反馈",
2622
+ "阶段变化"
2623
+ ],
2624
+ antiSignals: ["升级像流水账", "能力提升没有剧情后果"],
2625
+ resolutionStyle: "通过积累、训练、实战和关键顿悟完成成长突破。"
2626
+ },
2627
+ {
2628
+ key: "story_mode_adventure_exploration",
2629
+ name: "探索冒险",
2630
+ template: "探索要带来真正的新信息、新选择和新危险。",
2631
+ driver: "通过不断进入未知区域、接触新规则和发现新秘密保持阅读新鲜感。",
2632
+ readerExpectation: "读者持续获得开图、发现与未知感兑现。",
2633
+ payoffs: [
2634
+ "进入新区域",
2635
+ "发现新规则",
2636
+ "遭遇风险",
2637
+ "带出新认知"
2638
+ ],
2639
+ risks: ["探索空转", "新区域没有存在意义"],
2640
+ primary: false,
2641
+ progressionUnits: [
2642
+ "进入新区域",
2643
+ "发现新规则",
2644
+ "遭遇风险",
2645
+ "带出新认知"
2646
+ ],
2647
+ allowedConflictForms: [
2648
+ "环境威胁",
2649
+ "未知规则",
2650
+ "探索竞争",
2651
+ "文明差异"
2652
+ ],
2653
+ forbiddenConflictForms: [
2654
+ "场景只是换皮",
2655
+ "探索没有新信息产出",
2656
+ "连续宅在原地不推进世界边界"
2657
+ ],
2658
+ conflictCeiling: "high",
2659
+ chapterUnit: "单章推进一次新发现、一次环境应对或一次地图探索。",
2660
+ volumeReward: "卷末打开新的区域层级、秘密入口或冒险目标。",
2661
+ mandatorySignals: [
2662
+ "未知感",
2663
+ "发现感",
2664
+ "地图扩展"
2665
+ ],
2666
+ antiSignals: ["探索空转", "新区域没有存在意义"],
2667
+ resolutionStyle: "通过观察、应对、适应和局部突破打开更深层探索。"
2668
+ },
2669
+ {
2670
+ key: "story_mode_dungeon_challenge",
2671
+ name: "副本闯关",
2672
+ template: "副本不是重复打怪,要写清目标、机制、风险和通关奖励。",
2673
+ driver: "通过阶段性关卡挑战和规则破解形成连续的推进快感。",
2674
+ readerExpectation: "读者不断获得过关、破局和奖励兑现的满足。",
2675
+ payoffs: [
2676
+ "进入副本",
2677
+ "识别机制",
2678
+ "破解难点",
2679
+ "通关结算"
2680
+ ],
2681
+ risks: ["关卡同质化", "只剩数值堆砌没有机制"],
2682
+ primary: false,
2683
+ progressionUnits: [
2684
+ "进入副本",
2685
+ "识别机制",
2686
+ "破解难点",
2687
+ "通关结算"
2688
+ ],
2689
+ allowedConflictForms: [
2690
+ "关卡机制",
2691
+ "时间压力",
2692
+ "队伍配合",
2693
+ "资源限制"
2694
+ ],
2695
+ forbiddenConflictForms: [
2696
+ "副本机制全靠硬抗",
2697
+ "通关后没有奖励反馈",
2698
+ "副本重复到没有新意"
2699
+ ],
2700
+ conflictCeiling: "high",
2701
+ chapterUnit: "单章推进一个关卡节点、一个机制破解或一次通关反馈。",
2702
+ volumeReward: "卷末完成一段完整副本征程,获得显著奖励或更高阶入口。",
2703
+ mandatorySignals: [
2704
+ "机制感",
2705
+ "闯关反馈",
2706
+ "奖励兑现"
2707
+ ],
2708
+ antiSignals: ["关卡同质化", "只剩数值堆砌没有机制"],
2709
+ resolutionStyle: "通过规则识别、团队配合和关键执行完成通关。"
1033
2710
  }
1034
2711
  ];
1035
2712
  /** 剧情桥段库:可复用情节套路(作者阅读经验沉淀,非某本书的剧情线)。 */
@@ -1194,22 +2871,71 @@ function emptyProjectAssets() {
1194
2871
  /** 合并项目资产与内置库:返回「生效的反 AI 规则」(内置全局 + 项目自定义)。 */
1195
2872
  function effectiveAntiAiRules(assets) {
1196
2873
  const custom = assets?.antiAiRules ?? [];
1197
- const customNames = new Set(custom.map((r) => r.name));
1198
- return [...BUILTIN_ANTI_AI_RULES.filter((r) => !customNames.has(r.name)), ...custom];
2874
+ const keyOf = (r) => r.key ?? r.name;
2875
+ const customKeys = new Set(custom.filter((r) => keyOf(r) !== "").map(keyOf));
2876
+ return [...BUILTIN_ANTI_AI_RULES.filter((r) => !customKeys.has(keyOf(r))), ...custom];
2877
+ }
2878
+ /** 内置库种子化 upsert(对齐上游 SystemResourceBootstrapService 精神)。
2879
+ * missing_only 仅补齐缺失的全局基线规则;sync_existing 还会按 key 刷新已内置规则的结构化字段。 */
2880
+ function ensureBuiltinAssets(assets, mode = "missing_only") {
2881
+ const base = assets ?? emptyProjectAssets();
2882
+ const keyOf = (r) => r.key ?? r.name;
2883
+ const existing = base.antiAiRules ?? [];
2884
+ const byKey = new Map(existing.map((r) => [keyOf(r), r]));
2885
+ const out = [];
2886
+ let changed = false;
2887
+ for (const r of existing) {
2888
+ const builtin = BUILTIN_ANTI_AI_RULES.find((b) => (b.key ?? b.name) === keyOf(r));
2889
+ if (mode === "sync_existing" && builtin !== void 0) {
2890
+ const refreshed = {
2891
+ ...builtin,
2892
+ name: r.name,
2893
+ avoid: r.avoid,
2894
+ fix: r.fix,
2895
+ detectPatterns: r.detectPatterns ?? builtin.detectPatterns,
2896
+ enabled: r.enabled ?? builtin.enabled,
2897
+ scope: r.scope
2898
+ };
2899
+ out.push(refreshed);
2900
+ if (JSON.stringify(refreshed) !== JSON.stringify(r)) changed = true;
2901
+ byKey.set(keyOf(r), refreshed);
2902
+ } else out.push(r);
2903
+ }
2904
+ for (const b of BUILTIN_ANTI_AI_RULES) if (b.globalBaselineEnabled === true && !byKey.has(b.key ?? b.name)) {
2905
+ out.push(b);
2906
+ byKey.set(b.key ?? b.name, b);
2907
+ changed = true;
2908
+ }
2909
+ if (!changed) return base;
2910
+ return {
2911
+ ...base,
2912
+ antiAiRules: out
2913
+ };
1199
2914
  }
1200
- /** 把生效规则渲染成提示词块(禁止/鼓励分列,压缩省 token)。 */
2915
+ /** 把生效规则渲染成提示词块(禁止/风险/鼓励三档分列,压缩省 token)。 */
1201
2916
  function renderAntiAiRules(assets) {
1202
2917
  const rules = effectiveAntiAiRules(assets).filter((r) => r.enabled !== false);
1203
2918
  if (rules.length === 0) return "";
1204
2919
  const clip = (value, max) => value.length > max ? value.slice(0, max) + "…" : value;
1205
- const isEncourage = (r) => r.severity === "encourage" || r.name.startsWith("鼓励") || r.avoid.startsWith("(鼓励类)");
1206
- const forbidden = rules.filter((r) => !isEncourage(r));
1207
- const encourage = rules.filter(isEncourage);
2920
+ const category = (r) => {
2921
+ if (r.severity === "encourage" || r.name.startsWith("鼓励") || r.avoid.startsWith("(鼓励类)")) return "encourage";
2922
+ if (r.severity === "risk") return "risk";
2923
+ return "forbidden";
2924
+ };
2925
+ const instruction = (r) => r.promptInstruction !== void 0 && r.promptInstruction !== "" ? r.promptInstruction : r.avoid;
2926
+ const forbidden = rules.filter((r) => category(r) === "forbidden");
2927
+ const risk = rules.filter((r) => category(r) === "risk");
2928
+ const encourage = rules.filter((r) => category(r) === "encourage");
2929
+ const editLine = (r) => r.fix !== "" ? `;修正——${clip(r.fix, 50)}` : "";
1208
2930
  const lines = [];
1209
2931
  lines.push("==================== 反 AI 规则(写作时必须遵守的表达边界) ====================");
1210
2932
  if (forbidden.length > 0) {
1211
2933
  lines.push("禁止类(命中即问题,审稿时列为 high/medium):");
1212
- for (const r of forbidden) lines.push(`- ${r.name}:避免——${clip(r.avoid, 90)}${r.fix !== "" ? `;修正——${clip(r.fix, 50)}` : ""}`);
2934
+ for (const r of forbidden) lines.push(`- ${r.name}:${clip(instruction(r), 90)}${editLine(r)}${r.riskLevel !== void 0 && r.riskLevel !== "high" ? `(严重度 ${r.riskLevel})` : ""}`);
2935
+ }
2936
+ if (risk.length > 0) {
2937
+ lines.push("风险类(可能扣分,审稿按风险等级提示,不硬性阻塞):");
2938
+ for (const r of risk) lines.push(`- ${r.name}:${clip(instruction(r), 90)}(${r.riskLevel ?? "medium"}${r.autoRewrite === false ? " · 不自动改写" : ""})${editLine(r)}`);
1213
2939
  }
1214
2940
  if (encourage.length > 0) {
1215
2941
  lines.push("鼓励类(希望出现,不命中不算错,审稿时只作低优先级建议、不阻塞通过):");
@@ -1224,9 +2950,11 @@ function renderGenreAndProgression(assets) {
1224
2950
  sections.push("==================== 题材基底(本书的题材定位与读者期待) ====================");
1225
2951
  sections.push(`题材:${assets.genre.name}`);
1226
2952
  if (assets.genre.description !== "") sections.push(`读者期待:${assets.genre.description}`);
2953
+ if (assets.genre.template !== void 0 && assets.genre.template !== "") sections.push(`写法指引:${assets.genre.template}`);
1227
2954
  const walk = (node, depth) => {
1228
2955
  for (const child of node.children) {
1229
2956
  sections.push(`${" ".repeat(depth)}- ${child.name}:${child.description}`);
2957
+ if (child.template !== void 0 && child.template !== "") sections.push(`${" ".repeat(depth + 1)}写法指引:${child.template}`);
1230
2958
  walk(child, depth + 1);
1231
2959
  }
1232
2960
  };
@@ -1237,8 +2965,22 @@ function renderGenreAndProgression(assets) {
1237
2965
  sections.push("==================== 推进模式(读者为什么继续看) ====================");
1238
2966
  for (const mode of modes) {
1239
2967
  const tag = mode.primary ? "(主推进)" : "(辅助)";
2968
+ const progressionUnits = mode.progressionUnits ?? [];
2969
+ const allowedConflictForms = mode.allowedConflictForms ?? [];
2970
+ const forbiddenConflictForms = mode.forbiddenConflictForms ?? [];
2971
+ const mandatorySignals = mode.mandatorySignals ?? [];
2972
+ const antiSignals = mode.antiSignals ?? [];
1240
2973
  sections.push(`- 模式「${mode.name}」${tag}:驱动力——${mode.driver}`);
1241
2974
  sections.push(` 读者期待:${mode.readerExpectation}`);
2975
+ if (mode.template !== void 0 && mode.template !== "") sections.push(` 写法指引:${mode.template}`);
2976
+ if (progressionUnits.length > 0) sections.push(` 推进单位:${progressionUnits.join(" → ")}`);
2977
+ if (mode.conflictCeiling !== void 0) sections.push(` 冲突上限:${mode.conflictCeiling}`);
2978
+ if (allowedConflictForms.length > 0) sections.push(` 允许冲突:${allowedConflictForms.join("、")}`);
2979
+ if (forbiddenConflictForms.length > 0) sections.push(` 避免冲突:${forbiddenConflictForms.join("、")}`);
2980
+ if (mode.chapterUnit !== void 0 && mode.chapterUnit !== "") sections.push(` 单章单位:${mode.chapterUnit}`);
2981
+ if (mode.volumeReward !== void 0 && mode.volumeReward !== "") sections.push(` 卷末回报:${mode.volumeReward}`);
2982
+ if (mandatorySignals.length > 0) sections.push(` 必达信号:${mandatorySignals.join("、")}`);
2983
+ if (antiSignals.length > 0) sections.push(` 跑偏信号:${antiSignals.join("、")}`);
1242
2984
  if (mode.payoffs.length > 0) sections.push(` 常见兑现:${mode.payoffs.join("、")}`);
1243
2985
  if (mode.risks.length > 0) sections.push(` 节奏风险(避免):${mode.risks.join("、")}`);
1244
2986
  }
@@ -1268,17 +3010,67 @@ function renderAllAssets(assets) {
1268
3010
  renderAntiAiRules(assets)
1269
3011
  ].filter((part) => part !== "").join("\n\n");
1270
3012
  }
1271
- /** 写法引擎:从样本文本提取风格资产的系统提示词。 */
3013
+ /** 起始风格画像库:无样本文本也能快速绑定一套写法(对齐上游 DEFAULT_STARTER_STYLE_PROFILES)。 */
3014
+ const BUILTIN_STARTER_STYLE_PROFILES = [
3015
+ {
3016
+ key: "starter-power-up",
3017
+ templateKey: "power-up-escalation",
3018
+ name: "我的默认爽文推进写法",
3019
+ description: "适合第一次开书先跑顺目标推进、爽点兑现和章节收益点,后续可直接在此基础上微调。"
3020
+ },
3021
+ {
3022
+ key: "starter-suspense",
3023
+ templateKey: "suspense-pressure",
3024
+ name: "我的默认悬疑压迫写法",
3025
+ description: "适合异常、规则、调查和危险逼近类故事,先帮你把压迫感和信息差稳住。"
3026
+ },
3027
+ {
3028
+ key: "starter-emotional",
3029
+ templateKey: "emotional-tension",
3030
+ name: "我的默认情绪拉扯写法",
3031
+ description: "适合关系推进、误读拉扯和情绪兑现类故事,先有一套能直接开写的关系型表达底座。"
3032
+ },
3033
+ {
3034
+ key: "starter-daily",
3035
+ templateKey: "immersive-daily",
3036
+ name: "我的默认日常浸没写法",
3037
+ description: "适合治愈、陪伴、生活经营和轻缓成长类故事,优先保证生活感和沉浸感。"
3038
+ }
3039
+ ];
3040
+ /** 根据指纹风险推荐仿写预设:low→imitate / medium→balanced / high→transfer。 */
3041
+ function recommendStylePreset(fingerprintRisk) {
3042
+ if (fingerprintRisk === "low") return "imitate";
3043
+ if (fingerprintRisk === "high") return "transfer";
3044
+ return "balanced";
3045
+ }
3046
+ /** 写法引擎:从样本文本提取风格资产的系统提示词(含 preset / 指纹 / 净化管线)。 */
1272
3047
  function styleEngineSystemPrompt() {
1273
3048
  return [
1274
- "你是一位资深网文文风分析师。你会收到一段样本文本,请提炼出可复用的叙事风格规则,供后续章节保持同一种味道。",
3049
+ "你是一位资深网文文风分析师。你会收到一段样本文本,请提炼出可复用的叙事风格规则,供后续章节保持同一种味道,同时给出「仿写预设」与「净化」建议。",
1275
3050
  "要求:",
1276
3051
  "1. 从样本中归纳,不要泛泛而谈;每条规则都要能落到具体写法(句式、用词、视角、节奏、对话方式、描写密度)。",
1277
3052
  "2. 台词风格要说明角色说话的语气特征与常用表达方式。",
1278
3053
  "3. 表达边界要写明这段风格「不会怎么做」(如:不用华丽辞藻、不写长段心理独白、不用成语堆砌)。",
1279
- "4. 输出必须是合法 JSON 对象,不要输出任何其他文字。",
3054
+ "4. 给出 writingGuidance:净化后的写作指引,即把规则改写成可安全用于生成的具体指导(去掉会暴露来源的具体人名/地名/独特句式)。",
3055
+ "5. 给出 forbiddenEntities:样本文本里不可照搬的原作特有实体与标志性表达(人名、地名、核心设定句、独特口头禅等)。",
3056
+ "6. 判断预设 preset:imitate=高保真仿写(保留全部特征),balanced=平衡(保留可迁移特征、降指纹),transfer=迁移(只取骨架、彻底去指纹)。",
3057
+ "7. 判断 fingerprintRisk:low/medium/high,表示照搬本样本会被识别为抄原作的风险。",
3058
+ "8. 输出必须是合法 JSON 对象,不要输出任何其他文字。",
1280
3059
  "JSON 结构:",
1281
- "{\"proseRules\": [\"叙述视角与句式节奏规则\"], \"dialogueRules\": [\"台词风格规则\"], \"descriptionRules\": [\"描写密度与情绪表达规则\"], \"boundaries\": [\"表达边界\"]}"
3060
+ "{\"proseRules\": [\"叙述视角与句式节奏规则\"], \"dialogueRules\": [\"台词风格规则\"], \"descriptionRules\": [\"描写密度与情绪表达规则\"], \"boundaries\": [\"表达边界\"], \"preset\": \"imitate|balanced|transfer\", \"fingerprintRisk\": \"low|medium|high\", \"writingGuidance\": [\"净化后的写作指引\"], \"forbiddenEntities\": [\"不可照搬的原作特有实体\"]}"
3061
+ ].join("\n");
3062
+ }
3063
+ /** 写作公式提取系统提示词(分层:basic 骨架 / standard 完整 / deep 逐句细化)。 */
3064
+ function styleFormulaSystemPrompt(depth) {
3065
+ return [
3066
+ "你是一位网文写作公式提炼师。你会收到一段样本文本,请把它浓缩成\"这套文怎么写\"的写作公式。",
3067
+ depth === "basic" ? "只提取最核心的叙事骨架与推进公式(1 段即可)。" : depth === "deep" ? "逐句/逐段细化:开场如何起、对话如何写、节奏如何控、爽点如何递进(分小节列出)。" : "提取完整可复用的写法公式(含结构、句式、节奏、对话、爽点密度)。",
3068
+ "要求:",
3069
+ "1. 聚焦 focusAreas 指定的重点(如 开场/对话/节奏/爽点密度/段落结构)。",
3070
+ "2. formula 用 Markdown 分小节;applyGuidance 说明生成/改写时如何套用(1-3 句)。",
3071
+ "3. 只输出合法 JSON 对象,不要任何其他文字。",
3072
+ "JSON 结构:",
3073
+ "{\"name\": \"公式名\", \"focusAreas\": [\"重点域\"], \"formula\": \"Markdown 公式正文\", \"applyGuidance\": \"套用指引\"}"
1282
3074
  ].join("\n");
1283
3075
  }
1284
3076
  //#endregion
@@ -1715,6 +3507,103 @@ function countOccurrences(text, word) {
1715
3507
  return count;
1716
3508
  }
1717
3509
  //#endregion
3510
+ //#region src/llm-live.ts
3511
+ const MAX_BUFFER = 400;
3512
+ let buffer = [];
3513
+ let listeners = /* @__PURE__ */ new Set();
3514
+ let seq = 0;
3515
+ function nextSessionId() {
3516
+ seq++;
3517
+ return `ll-${Date.now().toString(36)}-${seq}`;
3518
+ }
3519
+ function subscribeLiveFeed(listener) {
3520
+ for (const frame of buffer) listener(frame);
3521
+ listeners.add(listener);
3522
+ return () => {
3523
+ listeners.delete(listener);
3524
+ };
3525
+ }
3526
+ function emitLive(frame) {
3527
+ buffer.push(frame);
3528
+ if (buffer.length > MAX_BUFFER) buffer = buffer.slice(-400);
3529
+ for (const l of listeners) l(frame);
3530
+ }
3531
+ [
3532
+ "你是中文长篇网络小说写作助手。",
3533
+ "你的任务是根据当前章节任务,生成可直接阅读的正文,而不是提纲或解释。",
3534
+ "",
3535
+ "【叙事视角】{{slot.writer.pov}}",
3536
+ "",
3537
+ "【任务边界】只输出章节正文,不输出标题、提纲、解释或任何额外文本。",
3538
+ "",
3539
+ "【核心约束】",
3540
+ "0. 以本章任务、人物状态、伏笔指令和连续性上下文为准,避免提前揭示未来答案或写到后续章节事件。",
3541
+ "1. 必须推进新的剧情动作,本章必须发生实质变化(局面、关系、信息、风险、决策至少一项)。",
3542
+ "2. 不得写成总结、复盘、解释性段落为主的章节,正文必须以「正在发生」的内容为主。",
3543
+ "3. 不得引入新的核心角色、世界规则或与上下文冲突的重大设定。",
3544
+ "",
3545
+ "【结构要求】",
3546
+ "1. 开头必须迅速进入当前情境,不得长时间铺垫背景或复述上一章。",
3547
+ "2. 中段必须出现推进、变化或对抗,不能平铺直叙维持同一状态。",
3548
+ "3. 本章至少出现一次明确的「状态变化」(信息反转、局面升级、关系变化、风险上升或计划转向)。",
3549
+ "4. {{slot.writer.endingHookPreference}}",
3550
+ "",
3551
+ "【篇幅要求】",
3552
+ "本章目标长度:约 {{input.targetWordCount}} 字;可接受区间:{{input.minWordCount}}-{{input.maxWordCount}} 字。",
3553
+ "篇幅不够时必须继续推进新的有效情节、冲突、对话和动作,而不是草率收尾。",
3554
+ "禁止靠重复回顾、空泛心理独白、无信息量描写硬凑字数。",
3555
+ "",
3556
+ "【连续性约束】",
3557
+ "1. 章节开头必须与 recent_chapters 明显区分,禁止复用相同开场模式。",
3558
+ "2. 允许短回调,但不得大段复述已发生事件,不得复制上下文原句。",
3559
+ "3. 必须延续当前人物状态与局面,不得让角色行为失去动机或连续性。",
3560
+ "",
3561
+ "【表达要求】",
3562
+ "1. {{slot.writer.tonePreference}}",
3563
+ "2. 优先使用具体动作、对话与可感知细节推进,而不是抽象概述。",
3564
+ "3. {{slot.writer.antiAiRules}}",
3565
+ "4. 对话应服务推进或冲突,不得成为填充内容。",
3566
+ "",
3567
+ "【输出前自查】",
3568
+ "在生成正文前,先内部确认:读者回报、关键转折和章末净变化是否可见,旧钩子责任是否回应,",
3569
+ "结尾钩子是否成立,义务合约是否兑现,人物硬事实是否违背。确认通过后再开始输出,不需要在正文中输出核查结果。"
3570
+ ].join("\n");
3571
+ [
3572
+ "小说:{{input.novelTitle}}",
3573
+ "章节:第 {{input.chapterOrder}} 章 {{input.chapterTitle}}",
3574
+ "",
3575
+ "【书级合约】{{context.book_contract}}",
3576
+ "【章节任务】{{context.chapter_mission}}",
3577
+ "【读者体验合同】{{context.reader_experience}}",
3578
+ "【人物硬事实】{{context.character_hard_facts}}",
3579
+ "【本章义务合约】{{context.obligation_contract}}",
3580
+ "【卷级窗口】{{context.volume_window}}",
3581
+ "【出场角色子集】{{context.participant_subset}}",
3582
+ "【当前局面】{{context.local_state}}",
3583
+ "【风格合约】{{context.style_contract}}",
3584
+ "【额外写法约束】{{slot.writer.customConstraints}}",
3585
+ "",
3586
+ "只输出章节正文。"
3587
+ ].join("\n");
3588
+ /** 把官方 writer 骨架渲染成可附加到现有系统提示词末尾的约束/自查块。 */
3589
+ function renderOfficialChapterWriterSkeleton(meta) {
3590
+ const pov = meta.pov ?? "第三人称有限视角,严格跟随主角所见所知。";
3591
+ const tone = meta.tonePreference ?? "文风贴合本书设定,用具体细节与动作推进。";
3592
+ const hook = meta.endingHookPreference ?? "章末留一个明确的钩子(新信息、新风险或未闭合的选择)。";
3593
+ const antiAi = meta.antiAiRules ?? "";
3594
+ return [
3595
+ "==================== 官方生成骨架(必达 / 禁止 / 输出前自查) ====================",
3596
+ "【叙事视角】" + pov,
3597
+ "【本章必达】本章必须发生实质变化(局面、关系、信息、风险、决策至少一项);开头迅速进入情境,禁止复述上一章。",
3598
+ "【禁止事项】不得写总结/复盘/解释性段落为主;不得引入新的核心角色或与上下文冲突的设定;不得为空凑字数。",
3599
+ "【篇幅】目标 " + meta.targetChars + " 字,区间 " + meta.minChars + "-" + meta.maxChars + " 字;不够就继续推进有效情节,禁止草草收尾。",
3600
+ "【结尾】" + hook,
3601
+ "【表达】优先用具体动作、对话与可感知细节推进;" + tone,
3602
+ "【反 AI】" + antiAi,
3603
+ "【输出前自查】先确认:读者回报、关键转折、章末净变化是否可见,旧钩子责任是否回应,人物硬事实是否违背;确认通过后再输出,正文中不要输出核查结果。"
3604
+ ].join("\n");
3605
+ }
3606
+ //#endregion
1718
3607
  //#region src/novel-context.ts
1719
3608
  /**
1720
3609
  * 相关事实检索:trigram 重合度 + 角色名命中加权 + 近因加权
@@ -2335,6 +4224,20 @@ const NOVEL_API = {
2335
4224
  bible: "/api/dsh-novel-forge/bible",
2336
4225
  assets: "/api/dsh-novel-forge/assets",
2337
4226
  styleEngine: "/api/dsh-novel-forge/style-engine",
4227
+ styleFormula: "/api/dsh-novel-forge/style-formula",
4228
+ styleDetect: "/api/dsh-novel-forge/style-detect",
4229
+ knowledge: "/api/dsh-novel-forge/knowledge",
4230
+ bookAnalysis: "/api/dsh-novel-forge/book-analysis",
4231
+ ideaInspiration: "/api/dsh-novel-forge/idea-inspiration",
4232
+ ideaInspirationMarket: "/api/dsh-novel-forge/idea-inspiration/market",
4233
+ director: "/api/dsh-novel-forge/director",
4234
+ directorTodos: "/api/dsh-novel-forge/director/todos",
4235
+ llmLive: "/api/dsh-novel-forge/llm-live/stream",
4236
+ marketRadar: "/api/dsh-novel-forge/market-radar",
4237
+ marketRadarScan: "/api/dsh-novel-forge/market-radar/scan",
4238
+ marketRadarApply: "/api/dsh-novel-forge/market-radar/apply",
4239
+ marketRadarSync: "/api/dsh-novel-forge/market-radar/foundation-sync",
4240
+ marketRadarBrief: "/api/dsh-novel-forge/market-radar/brief",
2338
4241
  generate: "/api/dsh-novel-forge/generate",
2339
4242
  review: "/api/dsh-novel-forge/review",
2340
4243
  rewrite: "/api/dsh-novel-forge/rewrite",
@@ -2562,7 +4465,7 @@ const COMPLIANCE_REDLINES = [
2562
4465
  "9. 不得出现法律法规禁止的其他内容。"
2563
4466
  ];
2564
4467
  /** 审稿维度取值(与 review-policy.ts 的 REVIEW_DIMENSIONS 对齐,用于归一化模型输出的 dimension 字段)。 */
2565
- const REVIEW_DIMENSION_IDS = /* @__PURE__ */ new Set([
4468
+ const REVIEW_DIMENSION_IDS = new Set([
2566
4469
  "character",
2567
4470
  "setting",
2568
4471
  "redline",
@@ -3064,6 +4967,23 @@ function createProject(outline, outlinePath) {
3064
4967
  }
3065
4968
  /** One complete non-streaming LLM call. */
3066
4969
  async function complete(ctx, config, options) {
4970
+ const liveLabel = options.liveLabel ?? "LLM 调用";
4971
+ const sessionId = nextSessionId();
4972
+ emitLive({
4973
+ type: "session_started",
4974
+ sessionId,
4975
+ label: liveLabel,
4976
+ model: options.model || config.model,
4977
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4978
+ context: { interactionId: sessionId }
4979
+ });
4980
+ emitLive({
4981
+ type: "phase_changed",
4982
+ sessionId,
4983
+ phase: "streaming",
4984
+ phaseMessage: "模型正在返回内容",
4985
+ at: (/* @__PURE__ */ new Date()).toISOString()
4986
+ });
3067
4987
  const messages = [createUserMessage({
3068
4988
  content: [{
3069
4989
  type: "text",
@@ -3076,7 +4996,7 @@ async function complete(ctx, config, options) {
3076
4996
  })];
3077
4997
  const request = {
3078
4998
  provider: config.provider,
3079
- model: config.model,
4999
+ model: options.model || config.model,
3080
5000
  messages,
3081
5001
  system: options.system,
3082
5002
  maxTokens: options.maxTokens ?? config.maxTokens,
@@ -3095,8 +5015,26 @@ async function complete(ctx, config, options) {
3095
5015
  const reasoning = blocks.filter((block) => block.type === "reasoning").map((block) => block.text).join("\n").trim();
3096
5016
  if (reasoning !== "") text = reasoning;
3097
5017
  }
5018
+ emitLive({
5019
+ type: "session_completed",
5020
+ sessionId,
5021
+ totalChars: text.length,
5022
+ preview: text.slice(0, 320),
5023
+ at: (/* @__PURE__ */ new Date()).toISOString(),
5024
+ phase: text === "" ? "failed" : "completed"
5025
+ });
3098
5026
  return text;
3099
5027
  }
5028
+ /** 解析 JSON 数组;失败或为空时给模型一次修复重试(对齐上游 structuredInvokeRepair 精神)。 */
5029
+ async function completeJsonArray(ctx, config, options, parse) {
5030
+ let parsed = parse(await complete(ctx, config, options));
5031
+ if (Array.isArray(parsed) && parsed.length > 0) return parsed;
5032
+ const parsed2 = parse(await complete(ctx, config, {
5033
+ ...options,
5034
+ user: options.user + "\n\n注意:你上一次输出不是合法且非空的 JSON 数组。请重新输出:只输出一个合法 JSON 数组(不要 Markdown、不要解释、不要思考过程、不要代码块标记)。"
5035
+ }));
5036
+ return Array.isArray(parsed2) ? parsed2 : [];
5037
+ }
3100
5038
  /**
3101
5039
  * Parse a JSON value out of a model response. Multi-level tolerance because
3102
5040
  * models are sloppy: prose around the JSON, ```json fences, a truncated tail,
@@ -3357,9 +5295,10 @@ function planSystemPrompt(volumes) {
3357
5295
  "3. 严格遵循大纲的人设、金手指规则、战力体系与世界观设定,不得自行发明冲突设定。",
3358
5296
  "4. 输出必须是合法的 JSON 数组,不要输出任何其他文字或 Markdown 代码块标记。",
3359
5297
  "5. 数组每个元素格式:{\"title\": \"章节标题(10字以内,有网文感)\", \"beats\": \"结构化剧情要点(150-250字,必须包含四段,段间用换行分隔):\\n本章目标:本章要完成的核心推进;\\n剧情要点:主要情节的起承转合(2-4 句);\\n爽点/钩子:本章的爽点兑现或情绪钩子;\\n结尾钩子:本章结尾为下一章埋下的悬念\"}",
5298
+ "6. 每个章节对象可额外包含以下可选字段(尽量给出,缺失则跳过):mustAdvance(数组,本章必须推进的局面/关系/信息/风险/决策变化);mustPreserve(数组,本章必须保持不破坏的项);characterHardFacts(数组,本章不可违背的人物硬事实:身份/阵营/境界/当前位置/知情度);endingHook(字符串,本章结尾钩子要求);obligation(字符串,本章义务合约一句话)。",
3360
5299
  "重要:beats 字段内部必须使用 \\n 转义表示换行(JSON 字符串内不得有真实换行符),其余字符串值也不得包含真实换行符,JSON 必须在一段内完整结束。",
3361
5300
  "重要:直接输出 JSON 结果本身,不要把思考过程或推理内容写在输出里。",
3362
- volumes !== void 0 && volumes.length > 0 ? ["\n全书分卷结构(规划章节时需落在对应卷内):"].concat(volumes.map((v) => `第${v.no}卷《${v.title}》:${v.summary}(章节 ${v.chapterStart}-${v.chapterEnd})`)).join("\n") : ""
5301
+ volumes !== void 0 && volumes.length > 0 ? ["\n全书分卷结构(规划章节时需落在对应卷内):"].concat(volumes.map((v) => `第${v.no}卷《${v.title}》:${v.summary}(章节 ${v.chapterStart}-${v.chapterEnd})` + (v.strategy !== void 0 && v.strategy !== "" ? `\n 卷战略:${v.strategy}` : "") + (v.pacing !== void 0 && v.pacing !== "" ? `\n 卷节奏板:${v.pacing}` : ""))).join("\n") : ""
3363
5302
  ].join("\n");
3364
5303
  }
3365
5304
  /** Build the writing system prompt (bible + outline + active foreshadows).
@@ -3454,6 +5393,15 @@ function writeSystemPrompt(project, targetChars, lengthRule) {
3454
5393
  sections.push("==================== 内容合规红线(平台硬性要求,最高优先级,违反即失败) ====================");
3455
5394
  sections.push(COMPLIANCE_REDLINES.join("\n"));
3456
5395
  sections.push("以上九条为硬性底线,任何情况下不得以任何形式出现或影射;若剧情确需涉及(如批判、反讽),只能以明确否定、揭露、批判的立场呈现,且不得展开细节。");
5396
+ sections.push(renderOfficialChapterWriterSkeleton({
5397
+ targetChars: target,
5398
+ minChars: lo,
5399
+ maxChars: hi,
5400
+ pov: "第三人称有限视角,严格跟随主角所见所知。",
5401
+ endingHookPreference: "章末留一个明确钩子(新信息、新风险或未闭合选择)。",
5402
+ tonePreference: "动作、对话、心理交替推进;重大信息用对话/动作/发现呈现。",
5403
+ antiAiRules: "严格遵循上方「反 AI 规则」与写法资产,避免套话与 AI 腔。"
5404
+ }));
3457
5405
  return sections.join("\n");
3458
5406
  }
3459
5407
  /**
@@ -3487,6 +5435,19 @@ async function planChapters(ctx, config, project, chapterCount, volumeNo, output
3487
5435
  const user = [
3488
5436
  "请为下面这部小说规划章节。",
3489
5437
  volume !== void 0 ? `本次只规划第 ${volume.no} 卷《${volume.title}》的章节:\n${volume.summary}` : continuation ? `本书已有 ${existing.length} 章已规划/已写作(见下方「已有章节」)。请规划**后续**章节:从第 ${startNo} 章开始。` : "请规划全书开篇章节。",
5438
+ (() => {
5439
+ const c = project.bookContract;
5440
+ const primary = project.assets?.primaryProgression;
5441
+ const aux = project.assets?.auxiliaryProgressions ?? [];
5442
+ const genre = project.assets?.genre;
5443
+ const parts = [];
5444
+ if (c?.promise !== void 0 && c.promise !== "") parts.push(`书籍承诺:${c.promise}`);
5445
+ if ((c?.primaryModeName ?? primary?.name) !== void 0) parts.push(`主推进模式:${c?.primaryModeName ?? primary.name}`);
5446
+ if (aux.length > 0) parts.push(`辅助推进模式:${aux.map((a) => a.name).join("、")}`);
5447
+ if ((c?.tone ?? genre?.template) !== void 0) parts.push(`文风基调:${c?.tone ?? genre.template}`);
5448
+ if (c?.targetPlatform !== void 0 && c.targetPlatform !== "") parts.push(`目标平台:${c.targetPlatform}`);
5449
+ return parts.length > 0 ? "【开书定盘】(章节规划须符合此定位)\n" + parts.join("\n") : "";
5450
+ })(),
3490
5451
  continuation ? (() => {
3491
5452
  const eventLines = existing.slice(-20).filter((c) => c.summary !== void 0 && c.summary.trim() !== "").map((c) => "第" + c.no + "章《" + c.title + "》:" + c.summary.slice(0, 80));
3492
5453
  return "【续写硬性要求】已有章节的剧情不得重写或重复,章节标题也不得与已有章节重复。\n" + (eventLines.length > 0 ? "以下情节已在已有章节中发生过(最近 " + eventLines.length + " 章摘要),后续章节**绝对不得重写或重复**:\n" + eventLines.join("\n") : "已有章节的剧情不得重写或重复(无章节摘要时以编年录为准)。") + "\n若本次规划已进入大纲的收尾区间(接近全书规划总章数),最后 5-10 章必须按大纲推进到大结局(终极抉择/清算/双结局等),**禁止以悬念、逃离、未解之谜收尾**——收尾区间按大纲卷定位判断,不以当前剧情是否\"感觉像结尾\"为准。";
@@ -3501,15 +5462,18 @@ async function planChapters(ctx, config, project, chapterCount, volumeNo, output
3501
5462
  "",
3502
5463
  `请规划 ${chapterCount} 章。输出 JSON 数组(不要输出其他文字):`
3503
5464
  ].join("\n");
3504
- const parsed = parseJsonArray(await complete(ctx, config, {
5465
+ const parsed = await completeJsonArray(ctx, config, {
3505
5466
  system: planSystemPrompt(project.volumes) + (continuation ? "\n重要:本次是**续写规划**——已有章节的剧情不得重写或重复,新章节标题不得与已有章节标题相同,新章节的剧情必须从上一章结尾自然接续(人物状态、时间线、地点衔接一致)。" : ""),
3506
5467
  user,
3507
5468
  temperature: .7,
3508
- maxTokens: Math.max(config.maxTokens, 4e4)
3509
- }));
5469
+ maxTokens: Math.max(config.maxTokens, 4e4),
5470
+ liveLabel: "章节规划"
5471
+ }, (t) => parseJsonArray(t));
3510
5472
  const chapters = [];
3511
5473
  const existingNos = new Set(existing.map((c) => c.no));
3512
5474
  const existingTitles = new Set(existing.map((c) => c.title));
5475
+ const strArr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.trim() !== "") : [];
5476
+ const str = (v) => typeof v === "string" ? v.trim() : "";
3513
5477
  let cursor = startNo;
3514
5478
  for (const item of parsed) {
3515
5479
  if (chapters.length >= chapterCount) break;
@@ -3521,18 +5485,56 @@ async function planChapters(ctx, config, project, chapterCount, volumeNo, output
3521
5485
  if (title !== "" && existingTitles.has(title)) continue;
3522
5486
  while (existingNos.has(cursor)) cursor++;
3523
5487
  const no = cursor++;
5488
+ const pd = Array.isArray(entry.payoffDirectives) ? entry.payoffDirectives.filter((v) => typeof v === "object" && v !== null).map((p) => ({
5489
+ no: typeof p.no === "number" ? p.no : void 0,
5490
+ operation: [
5491
+ "seed",
5492
+ "touch",
5493
+ "pressure",
5494
+ "partial_reveal",
5495
+ "payoff",
5496
+ "forbid"
5497
+ ].includes(p.operation) ? p.operation : void 0,
5498
+ text: str(p.text).slice(0, 120)
5499
+ })).filter((p) => p.text !== "" || p.operation !== void 0).slice(0, 4) : void 0;
3524
5500
  chapters.push({
3525
5501
  no,
3526
5502
  volume: volumeOf(no, project.volumes),
3527
5503
  title: title || `第${no}章`,
3528
5504
  beats,
3529
5505
  targetChars: config.chapterChars,
3530
- status: "pending"
5506
+ status: "pending",
5507
+ mustAdvance: strArr(entry.mustAdvance).slice(0, 4),
5508
+ mustPreserve: strArr(entry.mustPreserve).slice(0, 4),
5509
+ characterHardFacts: strArr(entry.characterHardFacts).slice(0, 6),
5510
+ payoffDirectives: pd,
5511
+ endingHook: str(entry.endingHook).slice(0, 120) || void 0,
5512
+ obligation: str(entry.obligation).slice(0, 200) || void 0
3531
5513
  });
3532
5514
  }
3533
5515
  if (chapters.length === 0) throw new Error("章节计划生成失败:模型没有返回有效章节");
3534
5516
  return chapters;
3535
5517
  }
5518
+ /** 书内知识库检索:按查询词匹配文档标题/内容,返回相关片段(供生成/规划注入)。 */
5519
+ function retrieveKnowledge(project, query, topN = 3) {
5520
+ const docs = project.knowledgeDocs ?? [];
5521
+ if (docs.length === 0) return "";
5522
+ const terms = (query ?? "").split(/[\s,,。;;、/]+/).map((t) => t.trim()).filter((t) => t.length >= 2);
5523
+ if (terms.length === 0) return "";
5524
+ const scored = docs.map((d) => {
5525
+ let score = 0;
5526
+ for (const t of terms) {
5527
+ if (d.title.includes(t)) score += 4;
5528
+ if (d.content.includes(t)) score += 1;
5529
+ }
5530
+ return {
5531
+ d,
5532
+ score
5533
+ };
5534
+ }).filter((x) => x.score > 0).sort((a, b) => b.score - a.score).slice(0, topN);
5535
+ if (scored.length === 0) return "";
5536
+ return "【书内知识库参考(与本章相关,写作时须遵守/可引用)】\n" + scored.map((x) => `- 《${x.d.title}》:${x.d.content.slice(0, 300)}`).join("\n");
5537
+ }
3536
5538
  /** The review system prompt. */
3537
5539
  function reviewSystemPrompt(project) {
3538
5540
  const bible = project.bible;
@@ -3549,8 +5551,10 @@ function reviewSystemPrompt(project) {
3549
5551
  "8. 呈现方式:整章是否纯内心推理铺陈(无对话/无对抗,推理全靠解说);反派是否纯背景板无行动;重要配角是否无名标签化(瘦高个/灰衣人全程代称)——命中即列为问题。",
3550
5552
  "9. 内容合规(最高优先级):逐条核对下方「内容合规红线」,任何一条命中(含影射、暗示、详细描写)必须列为 high,并给出改写建议。",
3551
5553
  "输出必须是合法 JSON 对象,不要输出任何其他文字:",
3552
- "{\"score\": 0-100的整数, \"verdict\": \"一句话总评\", \"issues\": [{\"severity\": \"high|medium|low\", \"dimension\": \"character|setting|redline|writing|pacing|logic|anti-ai|presentation|compliance\", \"item\": \"问题描述\", \"suggestion\": \"修改建议\"}]}",
5554
+ "{\"score\": 0-100的整数, \"riskScore\": 0-100的整数(越高越需人工处理,可结合本地AI味指数), \"verdict\": \"一句话总评\", \"issues\": [{\"severity\": \"high|medium|low\", \"dimension\": \"character|setting|redline|writing|pacing|logic|anti-ai|presentation|compliance\", \"item\": \"问题描述\", \"suggestion\": \"修改建议\", \"ruleName\": \"命中的反AI规则名(见反AI规则清单)\", \"ruleType\": \"forbidden|risk|encourage\", \"category\": \"套话|句式|段落|心理|设定|节奏|对话|其他\", \"excerpt\": \"命中的原文摘录(不超过50字)\", \"reason\": \"判定理由\", \"canAutoRewrite\": true|false}]}",
3553
5555
  "维度 dimension 与上方 9 个审查维度一一对应:人设=character、设定=setting、红线=redline、文笔=writing、节奏=pacing、逻辑=logic、反AI=anti-ai、呈现=presentation、合规=compliance。每条 issue 都必须填 dimension。",
5556
+ "反 AI 类 issue 尽量给出 ruleName/ruleType/category/excerpt/reason/canAutoRewrite,便于统计与自动改写。",
5557
+ "AI 套话高频模板词示例(集中出现必须整体降密度):仿佛、似乎、极其、完美、深不见底、形成了、莫名、无法形容、难以言喻、精心雕琢、肤光胜雪、眉目如画、歌舞升平、觥筹交错、妙语连珠、不可名状、另一层真相、命运、真相。",
3554
5558
  "重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。",
3555
5559
  "重要:直接输出 JSON 结果本身,不要把思考过程写在输出里。"
3556
5560
  ];
@@ -3600,7 +5604,9 @@ async function reviewChapter(ctx, config, project, outputDir, chapterNo) {
3600
5604
  system: reviewSystemPrompt(project),
3601
5605
  user,
3602
5606
  temperature: .3,
3603
- maxTokens: Math.max(config.maxTokens, 8e3)
5607
+ maxTokens: Math.max(config.maxTokens, 8e3),
5608
+ model: config.reviewModel,
5609
+ liveLabel: "审稿"
3604
5610
  }));
3605
5611
  const issues = Array.isArray(raw.issues) ? raw.issues.filter((v) => typeof v === "object" && v !== null).map((entry) => ({
3606
5612
  severity: [
@@ -3610,9 +5616,20 @@ async function reviewChapter(ctx, config, project, outputDir, chapterNo) {
3610
5616
  ].includes(entry.severity) ? entry.severity : "medium",
3611
5617
  dimension: typeof entry.dimension === "string" && REVIEW_DIMENSION_IDS.has(entry.dimension) ? entry.dimension : void 0,
3612
5618
  item: typeof entry.item === "string" ? entry.item : "",
3613
- suggestion: typeof entry.suggestion === "string" ? entry.suggestion : ""
5619
+ suggestion: typeof entry.suggestion === "string" ? entry.suggestion : "",
5620
+ ruleName: typeof entry.ruleName === "string" ? entry.ruleName : void 0,
5621
+ ruleType: [
5622
+ "forbidden",
5623
+ "risk",
5624
+ "encourage"
5625
+ ].includes(entry.ruleType) ? entry.ruleType : void 0,
5626
+ category: typeof entry.category === "string" ? entry.category : void 0,
5627
+ excerpt: typeof entry.excerpt === "string" ? entry.excerpt.slice(0, 80) : void 0,
5628
+ reason: typeof entry.reason === "string" ? entry.reason.slice(0, 200) : void 0,
5629
+ canAutoRewrite: typeof entry.canAutoRewrite === "boolean" ? entry.canAutoRewrite : void 0
3614
5630
  })).filter((issue) => issue.item !== "") : [];
3615
5631
  const score = typeof raw.score === "number" ? Math.max(0, Math.min(100, Math.round(raw.score))) : 60;
5632
+ const riskScore = typeof raw.riskScore === "number" ? Math.max(0, Math.min(100, Math.round(raw.riskScore))) : void 0;
3616
5633
  const hasHigh = issues.some((i) => i.severity === "high");
3617
5634
  const softThreshold = Math.max(65, config.reviewPassScore - 5);
3618
5635
  const report = {
@@ -3620,6 +5637,9 @@ async function reviewChapter(ctx, config, project, outputDir, chapterNo) {
3620
5637
  passed: hasHigh ? score >= config.reviewPassScore : score >= softThreshold,
3621
5638
  verdict: typeof raw.verdict === "string" ? raw.verdict.slice(0, 200) : "",
3622
5639
  issues,
5640
+ riskScore,
5641
+ aiFlavor: aiScan.aiScore,
5642
+ aiPhrases: aiScan.clicheHits.slice(0, 8),
3623
5643
  reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
3624
5644
  };
3625
5645
  chapter.review = report;
@@ -3656,9 +5676,20 @@ async function reviewChapterText(ctx, config, project, text, previousReport) {
3656
5676
  ].includes(entry.severity) ? entry.severity : "medium",
3657
5677
  dimension: typeof entry.dimension === "string" && REVIEW_DIMENSION_IDS.has(entry.dimension) ? entry.dimension : void 0,
3658
5678
  item: typeof entry.item === "string" ? entry.item : "",
3659
- suggestion: typeof entry.suggestion === "string" ? entry.suggestion : ""
5679
+ suggestion: typeof entry.suggestion === "string" ? entry.suggestion : "",
5680
+ ruleName: typeof entry.ruleName === "string" ? entry.ruleName : void 0,
5681
+ ruleType: [
5682
+ "forbidden",
5683
+ "risk",
5684
+ "encourage"
5685
+ ].includes(entry.ruleType) ? entry.ruleType : void 0,
5686
+ category: typeof entry.category === "string" ? entry.category : void 0,
5687
+ excerpt: typeof entry.excerpt === "string" ? entry.excerpt.slice(0, 80) : void 0,
5688
+ reason: typeof entry.reason === "string" ? entry.reason.slice(0, 200) : void 0,
5689
+ canAutoRewrite: typeof entry.canAutoRewrite === "boolean" ? entry.canAutoRewrite : void 0
3660
5690
  })).filter((issue) => issue.item !== "") : [];
3661
5691
  const score = typeof raw.score === "number" ? Math.max(0, Math.min(100, Math.round(raw.score))) : 60;
5692
+ const riskScore = typeof raw.riskScore === "number" ? Math.max(0, Math.min(100, Math.round(raw.riskScore))) : void 0;
3662
5693
  let passed = issues.some((i) => i.severity === "high") ? score >= config.reviewPassScore : score >= Math.max(65, config.reviewPassScore - 5);
3663
5694
  if (previousReport !== void 0) {
3664
5695
  const hasHigh = issues.some((i) => i.severity === "high");
@@ -3675,6 +5706,9 @@ async function reviewChapterText(ctx, config, project, text, previousReport) {
3675
5706
  passed,
3676
5707
  verdict: typeof raw.verdict === "string" ? raw.verdict.slice(0, 200) : "",
3677
5708
  issues,
5709
+ riskScore,
5710
+ aiFlavor: aiScan.aiScore,
5711
+ aiPhrases: aiScan.clicheHits.slice(0, 8),
3678
5712
  reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
3679
5713
  };
3680
5714
  }
@@ -3711,8 +5745,12 @@ function authorReviewSystemPrompt() {
3711
5745
  "5. advancedLines:本章实际推进的剧情线名称数组——从「活跃剧情线」清单中选出推进了的线(名称必须与清单中的线名一字不差;没推进任何线则输出空数组)。",
3712
5746
  "6. continuity:与上一章结尾的衔接检查(人物位置/时间/伤势/资源/对话状态),发现问题要指出。",
3713
5747
  "7. trend:结合上一章复盘看近期节奏趋势(是否连续拖沓、爽点密度是否下降、是否需要调整)。",
5748
+ "8. stateChanges:本章发生的关键状态变化数组(人物状态/世界局面/关系/资源,每条约 20 字,最多 6 条),供整本与事实库回灌。",
5749
+ "9. newConflicts:本章新引入或明显升级的冲突数组(最多 4 条)。",
5750
+ "10. clues:本章埋下或推进的新线索/伏笔数组(最多 4 条)。",
5751
+ "11. absentRisks:本章缺席但按卷级职责应出场/该推进关系的重要角色,及其缺席带来的风险(最多 4 条)。",
3714
5752
  "输出必须是合法 JSON 对象,不要输出任何其他文字:",
3715
- "{\"hookHonored\": true或false, \"hookNote\": \"一句话\", \"endingHook\": 0-10整数, \"plotlineProgress\": \"一句话\", \"advancedLines\": [\"线名\"], \"continuity\": \"一句话\", \"trend\": \"一句话\"}",
5753
+ "{\"hookHonored\": true或false, \"hookNote\": \"一句话\", \"endingHook\": 0-10整数, \"plotlineProgress\": \"一句话\", \"advancedLines\": [\"线名\"], \"continuity\": \"一句话\", \"trend\": \"一句话\", \"stateChanges\": [\"状态变化\"], \"newConflicts\": [\"新冲突\"], \"clues\": [\"新线索\"], \"absentRisks\": [\"本章缺席但值得注意的角色及风险(卷级职责/缺席风险,最多4条)\"]}",
3716
5754
  "重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。",
3717
5755
  "重要:直接输出 JSON 结果本身,不要把思考过程写在输出里。"
3718
5756
  ].join("\n");
@@ -3743,6 +5781,7 @@ async function authorReviewChapter(ctx, config, project, chapterNo, body, prevTa
3743
5781
  }));
3744
5782
  const knownLineNames = new Set((project.plotlines ?? []).map((l) => l.name));
3745
5783
  const advancedLines = Array.isArray(raw.advancedLines) ? raw.advancedLines.filter((n) => typeof n === "string" && n.trim() !== "" && knownLineNames.has(n.trim())).map((n) => n.trim()) : [];
5784
+ const strArr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.trim() !== "").map((x) => x.slice(0, 120)) : [];
3746
5785
  return {
3747
5786
  hookHonored: raw.hookHonored === true,
3748
5787
  hookNote: typeof raw.hookNote === "string" ? raw.hookNote.slice(0, 200) : "",
@@ -3751,6 +5790,10 @@ async function authorReviewChapter(ctx, config, project, chapterNo, body, prevTa
3751
5790
  advancedLines,
3752
5791
  continuity: typeof raw.continuity === "string" ? raw.continuity.slice(0, 200) : "",
3753
5792
  trend: typeof raw.trend === "string" ? raw.trend.slice(0, 200) : "",
5793
+ stateChanges: strArr(raw.stateChanges).slice(0, 6),
5794
+ newConflicts: strArr(raw.newConflicts).slice(0, 4),
5795
+ clues: strArr(raw.clues).slice(0, 4),
5796
+ absentRisks: strArr(raw.absentRisks).slice(0, 4),
3754
5797
  reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
3755
5798
  };
3756
5799
  }
@@ -3785,7 +5828,7 @@ async function suggestPlotlines(ctx, config, project) {
3785
5828
  reasoning: config.analysisReasoning ?? "low"
3786
5829
  }));
3787
5830
  const lines = [];
3788
- const kinds = /* @__PURE__ */ new Set([
5831
+ const kinds = new Set([
3789
5832
  "main",
3790
5833
  "branch",
3791
5834
  "character",
@@ -3923,7 +5966,7 @@ async function extractRoles(ctx, config, project) {
3923
5966
  });
3924
5967
  raw = parseJsonArray(text);
3925
5968
  }
3926
- const labels = /* @__PURE__ */ new Set([
5969
+ const labels = new Set([
3927
5970
  "protagonist",
3928
5971
  "female_lead",
3929
5972
  "female_support",
@@ -4500,7 +6543,7 @@ async function extractRoleVisualFrom(ctx, config, project, outputDir, role, styl
4500
6543
  "街区"
4501
6544
  ];
4502
6545
  const identityTokens = (role.identity ?? "").split(/[,,。\s]+/).filter((t) => t.length >= 2);
4503
- const searchTokens = Array.from(/* @__PURE__ */ new Set([
6546
+ const searchTokens = Array.from(new Set([
4504
6547
  roleName,
4505
6548
  ...identityTokens,
4506
6549
  ...colorWords.filter((w) => roleText.includes(w)),
@@ -4867,7 +6910,7 @@ async function nominateMangaRoles(ctx, config, project, outputDir, chapterNo) {
4867
6910
  maxTokens: Math.max(config.maxTokens, 8e3),
4868
6911
  reasoning: config.analysisReasoning ?? "low"
4869
6912
  }));
4870
- const verdicts = /* @__PURE__ */ new Set([
6913
+ const verdicts = new Set([
4871
6914
  "matched",
4872
6915
  "ambiguous",
4873
6916
  "not_in_library"
@@ -5483,7 +7526,7 @@ async function designPlotlinePlan(ctx, config, project, health) {
5483
7526
  maxTokens: Math.max(config.maxTokens, 3e3)
5484
7527
  }));
5485
7528
  const suggestions = [];
5486
- const kinds = /* @__PURE__ */ new Set([
7529
+ const kinds = new Set([
5487
7530
  "main",
5488
7531
  "branch",
5489
7532
  "character",
@@ -5745,12 +7788,25 @@ async function* generateChapterStream(ctx, config, project, outputDir, chapterNo
5745
7788
  };
5746
7789
  }).filter((x) => x.score >= 3).sort((a, b) => b.score - a.score).slice(0, 15).map((x) => `[第${x.f.chapterNo}章] ${x.f.text}`).filter((t) => !recentSet.has(t.slice(t.indexOf("]") + 2)));
5747
7790
  const foreshadowHints = (project.foreshadows ?? []).filter((f) => f.status === "planned" && f.targetChapter !== void 0 && f.targetChapter > 0).filter((f) => Math.abs(f.targetChapter - chapterNo) <= 12).map((f) => `- ${f.description.slice(0, 120)}${f.targetChapter !== void 0 ? `(计划回收于第 ${f.targetChapter} 章)` : ""}`);
7791
+ const contractBlock = (() => {
7792
+ const parts = [];
7793
+ if ((chapter.mustAdvance?.length ?? 0) > 0) parts.push(`本章必达(必须推进):${chapter.mustAdvance.join(";")}`);
7794
+ if ((chapter.mustPreserve?.length ?? 0) > 0) parts.push(`本章保持(不得破坏):${chapter.mustPreserve.join(";")}`);
7795
+ if ((chapter.characterHardFacts?.length ?? 0) > 0) parts.push(`人物硬事实(不得违背):${chapter.characterHardFacts.join(";")}`);
7796
+ if ((chapter.payoffDirectives?.length ?? 0) > 0) parts.push(`伏笔指令:${chapter.payoffDirectives.map((p) => `${p.operation ?? "touch"}${p.no !== void 0 ? `(第${p.no}章)` : ""}${p.text !== void 0 && p.text !== "" ? ":" + p.text : ""}`).join(";")}`);
7797
+ if (chapter.endingHook !== void 0 && chapter.endingHook !== "") parts.push(`章末钩子要求:${chapter.endingHook}`);
7798
+ if (chapter.obligation !== void 0 && chapter.obligation !== "") parts.push(`本章义务合约:${chapter.obligation}`);
7799
+ return parts.length > 0 ? "==================== 本章合同(硬约束,必须满足) ====================\n" + parts.join("\n") : "";
7800
+ })();
7801
+ const knowledgeBlock = retrieveKnowledge(project, `${chapter.title} ${chapter.beats}`);
5748
7802
  const messages = [createUserMessage({
5749
7803
  content: [{
5750
7804
  type: "text",
5751
7805
  text: [
5752
7806
  `现在写第 ${chapter.no} 章,标题《${chapter.title}》。`,
5753
7807
  `本章剧情要点:${chapter.beats}`,
7808
+ contractBlock,
7809
+ knowledgeBlock,
5754
7810
  "",
5755
7811
  foreshadowHints.length > 0 ? `本章附近需顺势埋下以下暗线(自然带过,不喧宾夺主,1-2 句即可,但细节要可辨识、与描述吻合):\n${foreshadowHints.join("\n")}` : "",
5756
7812
  recentFacts.length > 0 ? `本书已确立的事实(新写内容不得与之矛盾):\n${recentFacts.join("\n")}` : "",
@@ -5768,7 +7824,7 @@ async function* generateChapterStream(ctx, config, project, outputDir, chapterNo
5768
7824
  })];
5769
7825
  const request = {
5770
7826
  provider: config.provider,
5771
- model: config.model,
7827
+ model: config.generateModel || config.model,
5772
7828
  messages,
5773
7829
  system: writeSystemPrompt(project, chapter.targetChars || config.chapterChars),
5774
7830
  maxTokens: Math.max(config.maxTokens, 2e4),
@@ -6315,7 +8371,7 @@ async function analyzeAdaptation(ctx, config, text) {
6315
8371
  user: [
6316
8372
  "你是一位资深网文编辑兼改编策划。下面给你一部已完结/连载小说的若干章正文节选。请通读并输出该书的「原文设定卡片」与「可改范围矩阵」。",
6317
8373
  "输出合法 JSON 对象:",
6318
- "{\"bookName\": \"书名\", \"outline\": \"一句话主线梗概(100字内)\", \"dimensions\": [{\"key\":\"realm\",\"title\":\"大世界\",\"mutability\":\"big|small|free|locked|visual\",\"current\":\"当前值\",\"evidence\":\"证据(出现章节/频次)\",\"candidates\":[\"候选1\",\"候选2\"],\"impact\":\"改了会影响什么\",\"risk\":\"high|medium|low\"}]}",
8374
+ "{\"bookName\": \"书名\", \"outline\": \"一句话主线梗概(100字内)\", \"dimensions\": [{\"key\":\"realm\",\"title\":\"大世界\",\"mutability\":\"big|small|free|locked|visual\",\"current\":\"当前值\",\"evidence\":\"证据(出现章节/频次)\",\"candidates\":[{\"name\":\"候选体系名\",\"desc\":\"该体系一句话说明\"}],\"impact\":\"改了会影响什么\",\"risk\":\"high|medium|low\"}]}",
6319
8375
  "dimensions 至少覆盖以下维度(key/title):realm 大世界、cultivation 修为体系、protagonist 主角、goldenFinger 金手指、supporting 配角与势力人物名、faction 势力/组织、style 文风与叙事、ending 结局走向、timeline 时间线/编年、foreshadow 伏笔/暗线。",
6320
8376
  "mutability 取值:locked=建议保留、big=可改影响大、small=可改影响小、free=可自由改、visual=仅视觉包装。",
6321
8377
  "每个 dimension.current 必须忠于文本,能引用原文就用原文(尤其角色名/境界名/势力名/金手指名)。",
@@ -6358,13 +8414,26 @@ function normalizeAdaptationDimension(d) {
6358
8414
  "medium",
6359
8415
  "low"
6360
8416
  ].includes(d.risk) ? d.risk : "medium";
8417
+ const candidates = (Array.isArray(d.candidates) ? d.candidates : []).map((x) => {
8418
+ if (typeof x === "string") return x.trim() !== "" ? { name: x.trim() } : null;
8419
+ if (typeof x === "object" && x !== null) {
8420
+ const o = x;
8421
+ const name = typeof o.name === "string" ? o.name.trim() : "";
8422
+ const desc = typeof o.desc === "string" ? o.desc.trim() : void 0;
8423
+ return name !== "" ? {
8424
+ name,
8425
+ desc
8426
+ } : null;
8427
+ }
8428
+ return null;
8429
+ }).filter((x) => x !== null);
6361
8430
  return {
6362
8431
  key,
6363
8432
  title,
6364
8433
  mutability,
6365
8434
  current: typeof d.current === "string" ? d.current : "",
6366
8435
  evidence: typeof d.evidence === "string" ? d.evidence : void 0,
6367
- candidates: Array.isArray(d.candidates) ? d.candidates.filter((x) => typeof x === "string") : [],
8436
+ candidates,
6368
8437
  impact: typeof d.impact === "string" ? d.impact : "",
6369
8438
  risk
6370
8439
  };
@@ -6917,7 +8986,9 @@ async function auditBatch(ctx, config, project, outputDir, batch) {
6917
8986
  "只输出 JSON 数组。"
6918
8987
  ].filter((s) => s !== "").join("\n\n"),
6919
8988
  temperature: .2,
6920
- maxTokens: Math.max(config.maxTokens, 12e3)
8989
+ maxTokens: Math.max(config.maxTokens, 12e3),
8990
+ model: config.auditModel,
8991
+ liveLabel: "质检查询"
6921
8992
  }));
6922
8993
  const issues = [];
6923
8994
  for (const entry of parsed) {
@@ -7281,7 +9352,8 @@ async function suggestForeshadows(ctx, config, project) {
7281
9352
  user,
7282
9353
  temperature: .5,
7283
9354
  maxTokens: Math.max(config.maxTokens, 12e3),
7284
- reasoning: config.analysisReasoning ?? "low"
9355
+ reasoning: config.analysisReasoning ?? "low",
9356
+ liveLabel: "伏笔建议"
7285
9357
  }));
7286
9358
  const existing = new Set(project.foreshadows.map((f) => f.description));
7287
9359
  const created = [];
@@ -7312,18 +9384,288 @@ async function extractStyleAsset(ctx, config, sampleText) {
7312
9384
  system: styleEngineSystemPrompt(),
7313
9385
  user,
7314
9386
  temperature: .3,
7315
- maxTokens: Math.max(config.maxTokens, 12e3)
9387
+ maxTokens: Math.max(config.maxTokens, 12e3),
9388
+ liveLabel: "写法提取"
7316
9389
  }));
7317
9390
  const strArray = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.trim() !== "") : [];
9391
+ const fingerprintRisk = [
9392
+ "low",
9393
+ "medium",
9394
+ "high"
9395
+ ].includes(raw.fingerprintRisk) ? raw.fingerprintRisk : "medium";
7318
9396
  const result = {
7319
- proseRules: strArray(raw.proseRules),
7320
- dialogueRules: strArray(raw.dialogueRules),
7321
- descriptionRules: strArray(raw.descriptionRules),
7322
- boundaries: strArray(raw.boundaries)
9397
+ proseRules: strArray(raw.proseRules).slice(0, 40),
9398
+ dialogueRules: strArray(raw.dialogueRules).slice(0, 40),
9399
+ descriptionRules: strArray(raw.descriptionRules).slice(0, 40),
9400
+ boundaries: strArray(raw.boundaries).slice(0, 30),
9401
+ preset: [
9402
+ "imitate",
9403
+ "balanced",
9404
+ "transfer"
9405
+ ].includes(raw.preset) ? raw.preset : recommendStylePreset(fingerprintRisk),
9406
+ fingerprintRisk,
9407
+ writingGuidance: strArray(raw.writingGuidance).slice(0, 20),
9408
+ forbiddenEntities: strArray(raw.forbiddenEntities).slice(0, 30)
7323
9409
  };
7324
9410
  if (result.proseRules.length + result.dialogueRules.length + result.descriptionRules.length + result.boundaries.length === 0) throw new Error("写法提取失败:模型没有返回有效规则");
7325
9411
  return result;
7326
9412
  }
9413
+ /** 分层提取写作公式(basic/standard/deep)。 */
9414
+ async function extractStyleFormula(ctx, config, sampleText, depth) {
9415
+ const user = `请提炼下面这段样本文本的写作公式:\n\n${sampleText}`;
9416
+ const raw = parseJsonObject(await complete(ctx, config, {
9417
+ system: styleFormulaSystemPrompt(depth),
9418
+ user,
9419
+ temperature: .3,
9420
+ maxTokens: Math.max(config.maxTokens, 8e3),
9421
+ liveLabel: "公式提取"
9422
+ }));
9423
+ const formula = typeof raw.formula === "string" ? raw.formula.trim() : "";
9424
+ if (formula === "") throw new Error("公式提取失败:模型没有返回有效 formula");
9425
+ return {
9426
+ name: typeof raw.name === "string" ? raw.name.trim().slice(0, 40) : `公式 ${Date.now().toString(36)}`,
9427
+ focusAreas: Array.isArray(raw.focusAreas) ? raw.focusAreas.filter((x) => typeof x === "string" && x.trim() !== "").map((x) => x.slice(0, 20)).slice(0, 6) : [],
9428
+ formula,
9429
+ applyGuidance: typeof raw.applyGuidance === "string" ? raw.applyGuidance.trim().slice(0, 300) : ""
9430
+ };
9431
+ }
9432
+ /** 市场雷达系统提示(对齐上游 marketRadar 分析师角色)。 */
9433
+ function marketRadarSystemPrompt() {
9434
+ return [
9435
+ "你是中文网络文学市场分析师。只分析输入中的公开榜单元数据或用户给出的市场线索,不补写作品正文,不假装知道未提供的信息。",
9436
+ "只根据输入归纳热门题材和市场信号;对未提供的信息不要臆测。",
9437
+ "重点分析:热门题材组合、主角身份、金手指机制、开篇危机、关系卖点、标题句式、拥挤套路和差异化机会。",
9438
+ "kind 只能使用 genre、protagonist、advantage、opening、relationship、title_pattern、opportunity、crowding,不得创造近义枚举值。",
9439
+ "榜单高频不等于适合照搬。机会建议必须说明读者满足点,同时避开直接复制具体作品。",
9440
+ "productionFoundation 的题材/推进模式优先引用「资源库」中已有的 existingId(名称一字不差);只有确无合适资产时才给出新资产(existingId 为 null)。",
9441
+ "creativeBrief 严禁复用榜单作品的人名、专有设定、简介句子和完整书名;只能提炼读者需求、爽点机制和结构机会,不得输出任何具体人名/作品名,一律使用身份或通用场景称谓。"
9442
+ ].join("\n");
9443
+ }
9444
+ /** 题材雷达:输入平台/题材/榜单文本 → 信号 + 生产底座 + 开书创意。 */
9445
+ async function runMarketRadar(ctx, config, req) {
9446
+ const genreCatalog = BUILTIN_GENRE_LIBRARY.map((g, i) => `- [${i}] ${g.name}:${g.description}${g.template !== void 0 ? `(写法:${g.template})` : ""}`).join("\n");
9447
+ const modeCatalog = BUILTIN_PROGRESSION_MODES.map((m, i) => `- [${i}] ${m.name}:${m.driver} / 期待:${m.readerExpectation}`).join("\n");
9448
+ const user = [
9449
+ req.platform !== void 0 && req.platform !== "" ? `目标平台:${req.platform}` : "",
9450
+ req.genre !== void 0 && req.genre !== "" ? `目标题材:${req.genre}` : "",
9451
+ req.keywords !== void 0 && req.keywords !== "" ? `关键词:${req.keywords}` : "",
9452
+ req.candidates !== void 0 && req.candidates.length > 0 ? "【上榜记录(真实榜单)】\n" + req.candidates.map((c, i) => `- ${i + 1}.《${c.title}》${c.author !== void 0 && c.author !== "" ? `(${c.author})` : ""}${(c.tags?.length ?? 0) > 0 ? ` [${c.tags.join("、")}]` : ""}${c.category !== void 0 && c.category !== "" ? ` ${c.category}` : ""}${c.synopsis !== void 0 && c.synopsis !== "" ? `:${c.synopsis.slice(0, 140)}` : ""}`).join("\n") : req.feedText !== void 0 && req.feedText !== "" ? "【榜单/市场线索】\n" + req.feedText.slice(0, 6e3) : "(无榜单数据,请基于平台/题材/关键词与你的市场知识归纳)",
9453
+ "",
9454
+ "现有题材基底库:\n" + (genreCatalog || "空"),
9455
+ "",
9456
+ "现有推进模式库:\n" + (modeCatalog || "空"),
9457
+ "",
9458
+ "输出必须是合法 JSON 对象,不要输出任何其他文字。",
9459
+ "JSON 结构:{\"signals\":[{\"id\":\"短横线稳定id\",\"kind\":\"genre|protagonist|advantage|opening|relationship|title_pattern|opportunity|crowding\",\"title\":\"一句话\",\"detail\":\"说明(含读者满足点)\",\"direction\":\"current|rising|stable|falling\",\"recommended\":true|false}], \"productionFoundation\":{\"genre\":{\"existingId\":null或资源名,\"name\":\"题材名\",\"description\":\"说明\",\"template\":\"写法指引\"},\"primaryStoryMode\":{\"existingId\":null或资源名,\"name\":\"模式名\",\"driver\":\"驱动力\",\"readerExpectation\":\"读者期待\"},\"secondaryStoryMode\":{同primary,可null或省略}}}"
9460
+ ].filter((s) => s !== "").join("\n\n");
9461
+ const raw = parseJsonObject(await complete(ctx, config, {
9462
+ system: marketRadarSystemPrompt(),
9463
+ user,
9464
+ temperature: .4,
9465
+ maxTokens: Math.max(config.maxTokens, 8e3),
9466
+ liveLabel: "题材雷达"
9467
+ }));
9468
+ const signalAs = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "object" && x !== null).map((x) => ({
9469
+ id: typeof x.id === "string" ? x.id.slice(0, 40) : `sig-${Math.random().toString(36).slice(2, 7)}`,
9470
+ kind: [
9471
+ "genre",
9472
+ "protagonist",
9473
+ "advantage",
9474
+ "opening",
9475
+ "relationship",
9476
+ "title_pattern",
9477
+ "opportunity",
9478
+ "crowding"
9479
+ ].includes(x.kind) ? x.kind : "genre",
9480
+ title: typeof x.title === "string" ? x.title.slice(0, 60) : "未命名信号",
9481
+ detail: typeof x.detail === "string" ? x.detail.slice(0, 300) : "",
9482
+ direction: [
9483
+ "current",
9484
+ "rising",
9485
+ "stable",
9486
+ "falling"
9487
+ ].includes(x.direction) ? x.direction : void 0,
9488
+ recommended: typeof x.recommended === "boolean" ? x.recommended : void 0
9489
+ })) : [];
9490
+ const pf = raw.productionFoundation ?? {};
9491
+ const genre = pf.genre ?? {};
9492
+ const primary = pf.primaryStoryMode ?? {};
9493
+ const secondary = pf.secondaryStoryMode ?? {};
9494
+ const genreIds = new Set(BUILTIN_GENRE_LIBRARY.map((g) => g.name));
9495
+ const modeIds = new Set(BUILTIN_PROGRESSION_MODES.map((m) => m.name));
9496
+ const productionFoundation = {
9497
+ genre: {
9498
+ existingId: typeof genre.existingId === "string" && genre.existingId !== "" && genreIds.has(genre.existingId) ? genre.existingId : void 0,
9499
+ name: typeof genre.name === "string" ? genre.name.slice(0, 40) : "未定题材",
9500
+ description: typeof genre.description === "string" ? genre.description.slice(0, 300) : "",
9501
+ template: typeof genre.template === "string" ? genre.template.slice(0, 200) : void 0
9502
+ },
9503
+ primaryStoryMode: {
9504
+ existingId: typeof primary.existingId === "string" && primary.existingId !== "" && modeIds.has(primary.existingId) ? primary.existingId : void 0,
9505
+ name: typeof primary.name === "string" ? primary.name.slice(0, 40) : "未定模式",
9506
+ driver: typeof primary.driver === "string" ? primary.driver.slice(0, 200) : "",
9507
+ readerExpectation: typeof primary.readerExpectation === "string" ? primary.readerExpectation.slice(0, 200) : ""
9508
+ },
9509
+ secondaryStoryMode: Object.keys(secondary).length > 0 && typeof secondary.name === "string" && secondary.name !== "" ? {
9510
+ existingId: typeof secondary.existingId === "string" && secondary.existingId !== "" && modeIds.has(secondary.existingId) ? secondary.existingId : void 0,
9511
+ name: secondary.name.slice(0, 40),
9512
+ driver: typeof secondary.driver === "string" ? secondary.driver.slice(0, 200) : "",
9513
+ readerExpectation: typeof secondary.readerExpectation === "string" ? secondary.readerExpectation.slice(0, 200) : ""
9514
+ } : void 0
9515
+ };
9516
+ return {
9517
+ signals: signalAs(raw.signals),
9518
+ productionFoundation
9519
+ };
9520
+ }
9521
+ const MARKET_MODE_HINT = {
9522
+ follow_hot: "优先贴合当前热门满足点,但仍禁止复制具体作品。",
9523
+ differentiate: "保留热门读者满足点,同时至少替换主角身份、舞台或金手指机制中的一项。",
9524
+ light: "市场信号只作次要参考,用户自身想法和已选题材优先。"
9525
+ };
9526
+ /** 开书创意简报:用选中的市场信号 + 影响模式生成可执行 constraint / creative seed。 */
9527
+ async function runMarketCreativeBrief(ctx, config, req) {
9528
+ const user = [
9529
+ `影响模式:${req.influenceMode}`,
9530
+ MARKET_MODE_HINT[req.influenceMode] ?? "",
9531
+ req.signals.length > 0 ? "选中的市场信号:\n" + req.signals.map((s) => `- [${s.kind}] ${s.title}:${s.detail}`).join("\n") : "(无选中信号,请结合题材与推进模式补齐)",
9532
+ "",
9533
+ "输出必须是合法 JSON 对象,不要输出任何其他文字。",
9534
+ "JSON 结构:{\"promptBlock\":\"可直接指导题材/金手指/首章爆点/整书方向的约束\",\"openingIdea\":\"一段可直接开书的中文起始想法(主角身份、金手指或核心优势、开局事件、近期目标;不输出标题/大纲/Markdown)\",\"coreAdvantage\":\"主角能做什么(含触发条件/使用边界/成长方向或代价至少一项)\",\"bookSellingPoint\":\"读者持续追读的核心满足点\",\"first30ChapterPromise\":\"前30章必须兑现的阶段结果/关系变化/能力成长\"}",
9535
+ "严禁复用榜单作品的人名、专有设定、简介句子和完整书名;一律使用身份或通用场景称谓,不得输出具体人名/作品名。"
9536
+ ].filter((s) => s !== "").join("\n\n");
9537
+ const cb = parseJsonObject(await complete(ctx, config, {
9538
+ system: marketRadarSystemPrompt(),
9539
+ user,
9540
+ temperature: .5,
9541
+ maxTokens: Math.max(config.maxTokens, 4e3),
9542
+ liveLabel: "开书创意"
9543
+ }));
9544
+ return {
9545
+ promptBlock: typeof cb.promptBlock === "string" ? cb.promptBlock.slice(0, 800) : "",
9546
+ openingIdea: typeof cb.openingIdea === "string" ? cb.openingIdea.slice(0, 800) : "",
9547
+ coreAdvantage: typeof cb.coreAdvantage === "string" ? cb.coreAdvantage.slice(0, 600) : "",
9548
+ bookSellingPoint: typeof cb.bookSellingPoint === "string" ? cb.bookSellingPoint.slice(0, 400) : "",
9549
+ first30ChapterPromise: typeof cb.first30ChapterPromise === "string" ? cb.first30ChapterPromise.slice(0, 400) : ""
9550
+ };
9551
+ }
9552
+ /** 书分析/拆书:输入一本书/章节文本 → 卖点/结构/可借鉴点/风险。 */
9553
+ async function runBookAnalysis(ctx, config, req) {
9554
+ const raw = parseJsonObject(await complete(ctx, config, {
9555
+ system: ["你是资深中文网文拆书编辑。分析输入的一本书或章节文本,提炼可复用的创作思路。", "严禁照搬具体作品的人名/专有设定/简介句子;只能提炼读者需求、结构手法和风险。"].join("\n"),
9556
+ user: [
9557
+ "请分析下面文本:\n" + (req.text ?? "").slice(0, 8e3),
9558
+ "输出必须是合法 JSON 对象。",
9559
+ "JSON 结构:{\"sellingPoints\":[\"读者为什么追(卖点)\"],\"structure\":[\"可复用叙事结构/节奏/单元\"],\"lessons\":[\"可借鉴手法/套路\"],\"risks\":[\"易踩坑/风险\"]}"
9560
+ ].filter((s) => s !== "").join("\n\n"),
9561
+ temperature: .4,
9562
+ maxTokens: Math.max(config.maxTokens, 4e3),
9563
+ liveLabel: "书分析"
9564
+ }));
9565
+ const arr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.trim() !== "").map((x) => x.slice(0, 200)) : [];
9566
+ return {
9567
+ sellingPoints: arr(raw.sellingPoints),
9568
+ structure: arr(raw.structure),
9569
+ lessons: arr(raw.lessons),
9570
+ risks: arr(raw.risks)
9571
+ };
9572
+ }
9573
+ /** 创意灵感:一句话/题材 → 多方向开书灵感。 */
9574
+ async function runIdeaInspiration(ctx, config, req) {
9575
+ const count = Math.max(1, Math.min(10, req.count ?? 5));
9576
+ const raw = parseJsonObject(await complete(ctx, config, {
9577
+ system: ["你是中文网文开书灵感策划。根据用户一句话/题材方向,给出多个可开书的差异化创意。", "严禁输出具体人名/作品名;用身份/通用称谓。每个创意都要有明确钩子、题材、视角、长期兑现。"].join("\n"),
9578
+ user: [
9579
+ `我的方向:${req.idea ?? ""}`,
9580
+ `请给 ${count} 个开书创意。`,
9581
+ "输出必须是合法 JSON 对象。",
9582
+ "JSON 结构:{\"ideas\":[{\"title\":\"书名/门面\",\"hook\":\"一句话钩子/开局爆点\",\"genre\":\"题材\",\"pov\":\"主角视角/身份\",\"payoff\":\"长期追读兑现\"}]}"
9583
+ ].filter((s) => s !== "").join("\n\n"),
9584
+ temperature: .8,
9585
+ maxTokens: Math.max(config.maxTokens, 4e3),
9586
+ liveLabel: "创意灵感"
9587
+ }));
9588
+ const str = (v) => typeof v === "string" ? v.trim().slice(0, 200) : "";
9589
+ const arr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "object" && x !== null).map((x) => ({
9590
+ title: str(x.title),
9591
+ hook: str(x.hook),
9592
+ genre: str(x.genre),
9593
+ pov: str(x.pov),
9594
+ payoff: str(x.payoff)
9595
+ })) : [];
9596
+ return { ideas: arr(raw.ideas).slice(0, count) };
9597
+ }
9598
+ /** 雷达→灵感:基于市场信号/生产底座/创意简报,生成贴合市场的开书灵感。 */
9599
+ async function runMarketIdeaInspiration(ctx, config, req) {
9600
+ const count = Math.max(1, Math.min(10, req.count ?? 5));
9601
+ const system = ["你是中文网文开书灵感策划,擅长从市场榜单分析中提炼可开书方向。", "严禁输出具体人名/作品名;用身份/通用称谓。每个创意都要有明确钩子、题材、视角、长期兑现,且要与市场分析呼应但做差异化。"].join("\n");
9602
+ const signalBlock = (req.signals ?? []).map((s) => `- [${s.kind}] ${s.title}:${s.detail}`).join("\n") || "(无)";
9603
+ const f = req.foundation;
9604
+ const foundationBlock = f !== void 0 ? `题材:${f.genre.name}(${f.genre.description})\n主推进:${f.primaryStoryMode.name}(${f.primaryStoryMode.driver})${f.secondaryStoryMode !== void 0 ? `\n副推进:${f.secondaryStoryMode.name}` : ""}` : "(无)";
9605
+ const b = req.brief;
9606
+ const briefBlock = b !== void 0 ? `创作约束:${b.promptBlock}\n开篇想法:${b.openingIdea}\n核心优势:${b.coreAdvantage}\n追读卖点:${b.bookSellingPoint}\n前30章承诺:${b.first30ChapterPromise}` : "(无)";
9607
+ const raw = parseJsonObject(await complete(ctx, config, {
9608
+ system,
9609
+ user: [
9610
+ "基于以下市场分析,给出多个【差异化、可开书】的灵感:",
9611
+ `市场信号:\n${signalBlock}`,
9612
+ `生产底座:\n${foundationBlock}`,
9613
+ `创意简报:\n${briefBlock}`,
9614
+ `请给 ${count} 个开书灵感。`,
9615
+ "输出必须是合法 JSON 对象。",
9616
+ "JSON 结构:{\"ideas\":[{\"title\":\"书名/门面\",\"hook\":\"一句话钩子/开局爆点\",\"genre\":\"题材\",\"pov\":\"主角视角/身份\",\"payoff\":\"长期追读兑现\"}]}"
9617
+ ].filter((s) => s !== "").join("\n\n"),
9618
+ temperature: .8,
9619
+ maxTokens: Math.max(config.maxTokens, 4e3),
9620
+ liveLabel: "市场灵感"
9621
+ }));
9622
+ const str = (v) => typeof v === "string" ? v.trim().slice(0, 200) : "";
9623
+ const arr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "object" && x !== null).map((x) => ({
9624
+ title: str(x.title),
9625
+ hook: str(x.hook),
9626
+ genre: str(x.genre),
9627
+ pov: str(x.pov),
9628
+ payoff: str(x.payoff)
9629
+ })) : [];
9630
+ return { ideas: arr(raw.ideas).slice(0, count) };
9631
+ }
9632
+ /** 自动导演编排建议:基于全书上下文,给出下一卷/阶段编排 + 修复再平衡。 */
9633
+ async function runDirectorAdvice(ctx, config, project, req) {
9634
+ const done = project.chapters.filter((c) => c.status !== "pending" && c.status !== "generating");
9635
+ const last = done[done.length - 1];
9636
+ const volumesBlock = (project.volumes ?? []).map((v) => `第${v.no}卷《${v.title}》:${v.summary}`).join("\n") || "无";
9637
+ const plotlinesBlock = (project.plotlines ?? []).filter((l) => l.status === "active" || l.status === "paused").map((l) => `[${l.kind}] ${l.name}:${l.goal}${l.progress ? `(${l.progress})` : ""}`).join("\n") || "无";
9638
+ const foreBlock = (project.foreshadows ?? []).filter((f) => f.status === "planted" || f.status === "progressing").map((f) => `- ${f.description}${f.targetChapter ? `(约${f.targetChapter}章回收)` : ""}`).join("\n") || "无";
9639
+ const recentFacts = (project.facts ?? []).slice(-8).map((f) => `[第${f.chapterNo}章] ${f.text}`).join("\n");
9640
+ const raw = parseJsonObject(await complete(ctx, config, {
9641
+ system: "你是一位长篇网文自动导演,负责整本编排、节奏、风险与再平衡。只给出可执行建议,不输出正文。",
9642
+ user: [
9643
+ `书名:《${project.bookName}》`,
9644
+ req.focus !== void 0 && req.focus !== "" ? `聚焦:${req.focus}` : "",
9645
+ `当前进度:已写 ${done.length} 章,最后一章《${last?.title ?? "无"}》摘要:${last?.summary ?? ""}`,
9646
+ "",
9647
+ `分卷:\n${volumesBlock}`,
9648
+ `剧情线:\n${plotlinesBlock}`,
9649
+ `活跃伏笔:\n${foreBlock}`,
9650
+ `最近事实:\n${recentFacts}`,
9651
+ "",
9652
+ "请作为本书的自动导演,给出下一卷/下一阶段的编排建议:续写方向、阶段弧光、节奏板、风险提示、需要修复/再平衡的点。",
9653
+ "输出必须是合法 JSON 对象。",
9654
+ "JSON 结构:{\"summary\":\"总体判断一句话\",\"nextArc\":[\"下一阶段关键剧情节点(每条约一句话)\"],\"pacing\":\"节奏板(起承转合/爽点密度/章节节奏)\",\"risks\":[\"风险/跑偏提示\"],\"fixes\":[\"需要修复/再平衡的点\"]}"
9655
+ ].filter((s) => s !== "").join("\n\n"),
9656
+ temperature: .4,
9657
+ maxTokens: Math.max(config.maxTokens, 4e3),
9658
+ liveLabel: "自动导演"
9659
+ }));
9660
+ const arr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.trim() !== "").map((x) => x.slice(0, 200)) : [];
9661
+ return {
9662
+ summary: typeof raw.summary === "string" ? raw.summary.slice(0, 300) : "",
9663
+ nextArc: arr(raw.nextArc),
9664
+ pacing: typeof raw.pacing === "string" ? raw.pacing.slice(0, 400) : "",
9665
+ risks: arr(raw.risks),
9666
+ fixes: arr(raw.fixes)
9667
+ };
9668
+ }
7327
9669
  /** Export the whole book as one txt/md file. */
7328
9670
  function exportBook(outputDir, project, format) {
7329
9671
  const parts = [];
@@ -7707,19 +10049,20 @@ var ProductionRunner = class {
7707
10049
  this.state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7708
10050
  }
7709
10051
  /** 启动/续跑生产单:startNo..endNo 区间,endNo 超出计划时先自动补计划。 */
7710
- async start(startNo, endNo) {
10052
+ async start(startNo, endNo, runDir) {
7711
10053
  const config = this.deps.getConfig();
7712
10054
  if (this.working) throw new Error("生产单正在运行中,请先暂停或停止");
7713
- let project = loadProject(config.outputDir);
10055
+ const outputDir = runDir ?? config.outputDir;
10056
+ let project = loadProject(outputDir);
7714
10057
  if (project === void 0) throw new Error("输出目录中没有项目");
7715
10058
  if (endNo > project.chapters.length) {
7716
10059
  const need = endNo - project.chapters.length;
7717
10060
  this.log(`计划不足,追加 ${need} 章计划…`);
7718
- const chapters = await planChapters(this.deps.ctx, config, project, need, void 0, config.outputDir);
7719
- mergeVolatileFromDisk(config.outputDir, project);
10061
+ const chapters = await planChapters(this.deps.ctx, config, project, need, void 0, outputDir);
10062
+ mergeVolatileFromDisk(outputDir, project);
7720
10063
  project.chapters.push(...chapters);
7721
10064
  project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7722
- saveProject(config.outputDir, project);
10065
+ saveProject(outputDir, project);
7723
10066
  this.log(`计划已追加,全书 ${project.chapters.length} 章`);
7724
10067
  }
7725
10068
  this.state = {
@@ -7742,7 +10085,7 @@ var ProductionRunner = class {
7742
10085
  };
7743
10086
  this.pauseRequested = false;
7744
10087
  this.stopRequested = false;
7745
- this.bookDir = config.outputDir;
10088
+ this.bookDir = outputDir;
7746
10089
  this.working = true;
7747
10090
  this.persist();
7748
10091
  this.loop();
@@ -7842,7 +10185,7 @@ var ProductionRunner = class {
7842
10185
  chapter.generatingAt = (/* @__PURE__ */ new Date()).toISOString();
7843
10186
  mergeVolatileFromDisk(outputDir, project);
7844
10187
  saveProject(outputDir, project);
7845
- this.log(wasError ? `第${no}章《${chapter.title}》重新生成…` : `第${no}章《${chapter.title}》生成…`);
10188
+ this.log(`${wasError ? "重新生成" : "生成"} 第${no}章《${chapter.title}》…(模型 ${config.generateModel || config.model})`);
7846
10189
  try {
7847
10190
  for await (const step of generateChapterStream(ctx, config, project, outputDir, no));
7848
10191
  try {
@@ -7858,7 +10201,7 @@ var ProductionRunner = class {
7858
10201
  if (config.autoReview ?? true) {
7859
10202
  const report = await reviewChapter(ctx, config, project, outputDir, no);
7860
10203
  if (this.state !== null) this.state.stats[wasError ? "regenerated" : "generated"]++;
7861
- this.log(`第${no}章 审稿 ${report.score}分 ${report.passed ? "✅ 通过" : "⚠️ 被拒"}`);
10204
+ this.log(`第${no}章 审稿 ${report.score}分 ${report.passed ? "✅ 通过" : "⚠️ 被拒"}(模型 ${config.reviewModel || config.model})`);
7862
10205
  } else {
7863
10206
  chapter.status = "approved";
7864
10207
  mergeVolatileFromDisk(outputDir, project);
@@ -7875,6 +10218,14 @@ var ProductionRunner = class {
7875
10218
  const review = await authorReviewChapter(ctx, config, project, no, body, prevTail);
7876
10219
  chapter.authorReview = review;
7877
10220
  if (review.advancedLines !== void 0) autoLinkPlotlines(project, no, review.advancedLines);
10221
+ const backfillFacts = [...review.stateChanges ?? [], ...review.clues ?? []];
10222
+ if (backfillFacts.length > 0) {
10223
+ project.facts ??= [];
10224
+ for (const text of backfillFacts) project.facts.push({
10225
+ chapterNo: no,
10226
+ text
10227
+ });
10228
+ }
7878
10229
  mergeVolatileFromDisk(outputDir, project);
7879
10230
  saveProject(outputDir, project);
7880
10231
  }
@@ -7965,7 +10316,7 @@ var ProductionRunner = class {
7965
10316
  * walk the XML with a tiny tokenizer — no heavyweight XML/DOM dependency.
7966
10317
  */
7967
10318
  /** Decode the handful of XML entities docx bodies actually use. */
7968
- function decodeEntities(text) {
10319
+ function decodeEntities$1(text) {
7969
10320
  return text.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&apos;/g, "'").replace(/&amp;/g, "&").replace(/&nbsp;/g, " ");
7970
10321
  }
7971
10322
  /**
@@ -7992,7 +10343,7 @@ function extractDocxText(buffer) {
7992
10343
  const runs = [];
7993
10344
  for (const match of segment.matchAll(/<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>|<w:tab\b[^>]*\/>|<w:br\b[^>]*\/>/g)) if (match[0].startsWith("<w:tab")) runs.push(" ");
7994
10345
  else if (match[0].startsWith("<w:br")) runs.push("\n");
7995
- else runs.push(decodeEntities(match[1] ?? ""));
10346
+ else runs.push(decodeEntities$1(match[1] ?? ""));
7996
10347
  const line = runs.join("").replace(/\u00a0/g, " ").trimEnd();
7997
10348
  paragraphs.push(line);
7998
10349
  }
@@ -8033,7 +10384,7 @@ function readOutlineFromDocx(path) {
8033
10384
  /** History file name inside the output dir. */
8034
10385
  const ASSISTANT_HISTORY_FILE = "novel-assistant.jsonl";
8035
10386
  /** Max tool-call rounds per user turn (safety bound). */
8036
- const MAX_TOOL_ROUNDS = 6;
10387
+ const MAX_TOOL_ROUNDS = 4;
8037
10388
  /** Load the persisted conversation (empty when none). */
8038
10389
  function loadAssistantHistory(outputDir) {
8039
10390
  const file = join(outputDir, ASSISTANT_HISTORY_FILE);
@@ -8156,11 +10507,12 @@ function assistantSystemPrompt(project) {
8156
10507
  "工作规则(严格遵守):",
8157
10508
  "1. 全量知情:回答和修改必须基于项目真实数据,禁止编造书中不存在的设定。需要完整信息时,先调用 book_overview 获取全书上下文(总纲全文/道藏/大世界/编年录/全部章节要点/暗线/卷首语);需要某章正文用 chapter_text。",
8158
10509
  "2. 修改流程:改前用一句话说明意图 → 执行工具 → 改后简要汇报。",
8159
- "3. 连锁维护:改动可能波及其它位置(其它章节、设定、编年录、卷首语)时,执行后主动调用 impact_analysis 分析影响面,并把「必须同步」的项一并处理或明确提示作者逐项确认。",
10510
+ "3. 连锁维护(只用于大改动,不要滥用):改动**整段大纲/道藏规则/章节正文**这类可能冲突的大改动时,才主动调用 impact_analysis;**新增知识库文档、记待办、加剧情线、改暗线状态**这类轻量增改,做完直接一句话汇报即可,禁止再连锁调用其它工具。",
8160
10511
  "4. 删除红线:删除章节、清空设定等破坏性操作必须等作者明确同意。",
8161
- "5. 品质门槛:建议必须具体——指出问题在哪一章、哪一段、哪一句,并给出可落地的改法;禁止\"建议增强冲突\"这类空话。",
8162
- "6. 设定忠诚:忠于总纲、道藏、大世界、编年录;发现书中已有内容与设定冲突时,主动指出并给修正方案。",
8163
- "7. 中文回复,简洁有干货。",
10512
+ "5. 收敛执行(最重要):每轮只做用户明确要求的那一件事。执行完立即用一句话汇报并结束本轮;**禁止**重复调用已做过的工具、**禁止**为了\"保险/确认/顺便\"再调用无关工具。若用户说\"记住这个设定/记一条待办/加一条剧情线\",就调对应工具一次 → 汇报 → 停。",
10513
+ "6. 品质门槛:建议必须具体——指出问题在哪一章、哪一段、哪一句,并给出可落地的改法;禁止\"建议增强冲突\"这类空话。",
10514
+ "7. 设定忠诚:忠于总纲、道藏、大世界、编年录;发现书中已有内容与设定冲突时,主动指出并给修正方案。",
10515
+ "8. 中文回复,简洁有干货。",
8164
10516
  "",
8165
10517
  "可用工具:",
8166
10518
  "- book_overview:{\"scope\": \"recent|full|volume:2\"(可选,默认 recent)}。返回全书上下文包(总纲/道藏/大世界/章节要点/编年录/暗线/卷首语)。recent=最近30章;full=全部章节(书很长时慎用);volume:N=只看第N卷。",
@@ -8181,6 +10533,18 @@ function assistantSystemPrompt(project) {
8181
10533
  "- assets_set_genre:{\"name\": \"题材名\", \"description\": \"题材说明(可选)\"}。设置本书题材基底。",
8182
10534
  "- assets_set_progression:{\"name\": \"模式名\", \"driver\": \"驱动力\", \"primary\": true/false}。设置主/辅助推进模式。",
8183
10535
  "- assets_add_rule:{\"name\": \"规则名(可选)\", \"avoid\": \"要避免的表达问题\", \"fix\": \"修正方向(可选)}。新增反 AI 规则。",
10536
+ "- book_analysis:{\"scope\": \"recent|full|volume:N\"(可选)}。拆书:提炼本书卖点/结构/可借鉴/风险(分析当前书,不照搬外部作品)。",
10537
+ "- director_advice:{\"focus\": \"聚焦方向(可选)\"}。自动导演:基于全书给出下一阶段剧情节点/节奏板/风险/修复建议。",
10538
+ "- knowledge_add:{\"title\": \"标题\", \"content\": \"内容\"}。往本书知识库加一条自由参考文档(生成时会被检索注入)。",
10539
+ "- knowledge_search:{\"query\": \"关键词\"}。在本书知识库检索相关内容。",
10540
+ "- plotline_list:无参数。查看本书当前剧情线。",
10541
+ "- plotline_add:{\"name\": \"线名\", \"goal\": \"目标\", \"kind\": \"main|branch|character|mystery(可选)\"}。新增一条剧情线。",
10542
+ "- director_todo_add:{\"text\": \"待办内容\", \"source\": \"risk|fix(可选)\"}。把一条风险/修复记成导演待办。",
10543
+ "- director_todo_list:无参数。查看本书导演待办。",
10544
+ "- knowledge_list:无参数。**列出本书全部知识库文档**(标题+内容)。作者说\"收集/汇总/列出所有知识库\"时调用它。",
10545
+ "- breakdown:{\"scope\": \"recent|all|volume:N\"(可选,默认 recent), \"preset\": \"quick|standard\"(可选)}。书内拆书分析:对本书已写章节做结构/人物/文风/卖点体检。",
10546
+ "- audit:无参数。全书一致性质检(分批扫描章节+设定+事实库,聚合矛盾)。",
10547
+ "- blurb:{\"partial\": \"已写开头(可选)\"}。AI 生成/补全小说简介并保存到本书。",
8184
10548
  "",
8185
10549
  "回答质量要求(非常重要):",
8186
10550
  "- 具体:回答必须引用项目里的真实内容(人名、境界、章节、暗线、设定),禁止空泛套话。快照里没有的信息,先调用工具获取(chapter_text / outline_text)再回答。",
@@ -8200,7 +10564,7 @@ function assistantSystemPrompt(project) {
8200
10564
  "- 铁律:只要你想执行任何操作,你的【整个回复】必须只包含一个动作标签,禁止先说话、禁止解释\"我要做什么\"、禁止铺垫——直接输出标签。",
8201
10565
  "- 如果你收到「格式提示」(宿主说你没有输出动作标签):你的下一条回复必须只输出动作标签,禁止再解释、禁止再道歉、禁止再描述计划。",
8202
10566
  "- 工具调用是自动的:你输出标签后,宿主会执行并把结果反馈给你,你再基于结果继续。",
8203
- "- 每次回复最多调用 1 个动作;执行结果会反馈给你,你可以继续讨论或再调用。",
10567
+ "- 每次回复最多调用 1 个动作;**完成用户本轮要求后立即用一句话汇报并停止,不要继续追加工具调用**(除非用户在下一轮明确要求)。",
8204
10568
  "- 需要先看总纲/章节再决定怎么改?那就先输出一个 outline_text / chapter_text 的标签,等结果回来。",
8205
10569
  "- chapter_rewrite 的 target 参数:从章节正文中复制一小段(一句话或几句话即可),不要带换行、不要带引号,取连续文本片段。",
8206
10570
  "- 如果工具执行失败(例如片段未找到),根据错误信息修正参数后自动重试一次,不要直接放弃或让作者手动操作。",
@@ -8430,6 +10794,140 @@ async function* executeAction(ctx, config, project, outputDir, name, args) {
8430
10794
  saveProject(outputDir, project);
8431
10795
  return `已新增反 AI 规则「${name !== "" ? name : avoid.slice(0, 20)}」`;
8432
10796
  }
10797
+ case "book_analysis": {
10798
+ const scopeArg = str(args.scope);
10799
+ const result = await runBookAnalysis(ctx, config, { text: bookOverview(project, scopeArg === "full" ? "full" : scopeArg === "all" ? "full" : /^volume:(\d+)$/.test(scopeArg) ? Number(scopeArg.slice(7)) : "recent") });
10800
+ return [
10801
+ "拆书结果:",
10802
+ "卖点:",
10803
+ ...(result.sellingPoints ?? []).map((s) => `- ${s}`),
10804
+ "结构:",
10805
+ ...(result.structure ?? []).map((s) => `- ${s}`),
10806
+ "可借鉴:",
10807
+ ...(result.lessons ?? []).map((s) => `- ${s}`),
10808
+ "风险:",
10809
+ ...(result.risks ?? []).map((s) => `- ${s}`)
10810
+ ].join("\n");
10811
+ }
10812
+ case "director_advice": {
10813
+ const result = await runDirectorAdvice(ctx, config, project, { focus: str(args.focus) });
10814
+ return [
10815
+ "自动导演建议:",
10816
+ `总体判断:${result.summary}`,
10817
+ "下一阶段节点:",
10818
+ ...(result.nextArc ?? []).map((s) => `- ${s}`),
10819
+ `节奏板:${result.pacing}`,
10820
+ "风险提示:",
10821
+ ...(result.risks ?? []).map((s) => `- ${s}`),
10822
+ "需要修复/再平衡:",
10823
+ ...(result.fixes ?? []).map((s) => `- ${s}`)
10824
+ ].join("\n");
10825
+ }
10826
+ case "knowledge_add": {
10827
+ const title = str(args.title).trim();
10828
+ const content = str(args.content).trim();
10829
+ if (title === "" || content === "") throw new Error("knowledge_add 需要 title 和 content");
10830
+ project.knowledgeDocs ??= [];
10831
+ project.knowledgeDocs.push({
10832
+ id: `kd-${Date.now().toString(36)}`,
10833
+ title,
10834
+ content,
10835
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
10836
+ });
10837
+ project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
10838
+ saveProject(outputDir, project);
10839
+ return `已加入知识库「${title}」(当前 ${project.knowledgeDocs.length} 篇)`;
10840
+ }
10841
+ case "knowledge_search": {
10842
+ const q = str(args.query).trim();
10843
+ if (q === "") throw new Error("knowledge_search 需要 query");
10844
+ const ql = q.toLowerCase();
10845
+ const hits = (project.knowledgeDocs ?? []).filter((d) => d.title.toLowerCase().includes(ql) || d.content.toLowerCase().includes(ql)).slice(-5);
10846
+ if (hits.length === 0) return "知识库中未找到相关内容。";
10847
+ return `知识库中与「${q}」相关(${hits.length} 篇):\n` + hits.map((d) => `- 【${d.title}】\n${d.content.slice(0, 400)}`).join("\n");
10848
+ }
10849
+ case "plotline_list": {
10850
+ const lines = project.plotlines ?? [];
10851
+ if (lines.length === 0) return "本书还没有剧情线。";
10852
+ return `本书剧情线(${lines.length} 条):\n` + lines.map((l) => `- [${l.status}] ${l.name}(${l.kind}):${l.goal}${l.progress !== "" ? `|${l.progress}` : ""}`).join("\n");
10853
+ }
10854
+ case "plotline_add": {
10855
+ const name = str(args.name).trim();
10856
+ const goal = str(args.goal).trim();
10857
+ if (name === "" || goal === "") throw new Error("plotline_add 需要 name 和 goal");
10858
+ const kindArg = str(args.kind);
10859
+ const kind = [
10860
+ "main",
10861
+ "branch",
10862
+ "character",
10863
+ "mystery"
10864
+ ].includes(kindArg) ? kindArg : "branch";
10865
+ const statusArg = str(args.status);
10866
+ const status = [
10867
+ "active",
10868
+ "paused",
10869
+ "resolved",
10870
+ "abandoned"
10871
+ ].includes(statusArg) ? statusArg : "active";
10872
+ project.plotlines ??= [];
10873
+ project.plotlines.push({
10874
+ id: `pl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
10875
+ name,
10876
+ kind,
10877
+ goal,
10878
+ progress: str(args.progress),
10879
+ status,
10880
+ chapters: Array.isArray(args.chapters) ? args.chapters : [],
10881
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
10882
+ });
10883
+ project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
10884
+ saveProject(outputDir, project);
10885
+ return `已加入剧情线「${name}」(当前 ${project.plotlines.length} 条)`;
10886
+ }
10887
+ case "director_todo_add": {
10888
+ const text = str(args.text).trim();
10889
+ if (text === "") throw new Error("director_todo_add 需要 text");
10890
+ const source = args.source === "fix" ? "fix" : "risk";
10891
+ project.todos ??= [];
10892
+ project.todos.unshift({
10893
+ id: `td-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
10894
+ text,
10895
+ source,
10896
+ done: false,
10897
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
10898
+ });
10899
+ project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
10900
+ saveProject(outputDir, project);
10901
+ return `已加入导演待办「${text.slice(0, 30)}」(当前 ${project.todos.length} 条)`;
10902
+ }
10903
+ case "director_todo_list": {
10904
+ const todos = project.todos ?? [];
10905
+ if (todos.length === 0) return "还没有导演待办。";
10906
+ return `导演待办(${todos.length} 条):\n` + todos.map((t) => `- ${t.done ? "[已处理]" : "[待处理]"} ${t.text}`).join("\n");
10907
+ }
10908
+ case "knowledge_list": {
10909
+ const docs = project.knowledgeDocs ?? [];
10910
+ if (docs.length === 0) return "本书知识库目前为空。";
10911
+ return `本书知识库(${docs.length} 篇):\n` + docs.map((d, i) => `[${i + 1}]《${d.title}》\n${d.content}`).join("\n\n");
10912
+ }
10913
+ case "breakdown": {
10914
+ const result = await breakdownBook(ctx, config, project, outputDir, str(args.scope) === "all" ? "all" : /^volume:\d+$/.test(str(args.scope)) ? str(args.scope) : "recent", str(args.preset) === "standard" ? "standard" : "quick");
10915
+ const lines = [`拆书分析结果(扫描 ${result.chaptersScanned} 章):`];
10916
+ for (const s of result.sections ?? []) lines.push(`\n【${s.title}】\n${s.markdown}`);
10917
+ return lines.join("\n");
10918
+ }
10919
+ case "audit": {
10920
+ const issues = await auditBook(ctx, config, project, outputDir);
10921
+ if (issues.length === 0) return "全书质检:未发现明显矛盾。";
10922
+ return `全书质检发现 ${issues.length} 处问题:\n` + issues.map((it) => `- [${it.severity}] 第${it.chapterNo}章:${it.item}${it.suggestion !== "" ? ` → ${it.suggestion}` : ""}`).join("\n");
10923
+ }
10924
+ case "blurb": {
10925
+ const text = await generateBlurb(ctx, config, project, str(args.partial));
10926
+ project.blurb = text;
10927
+ project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
10928
+ saveProject(outputDir, project);
10929
+ return `简介已生成/更新:\n${text}`;
10930
+ }
8433
10931
  default: throw new Error(`未知工具 ${name}`);
8434
10932
  }
8435
10933
  }
@@ -8540,6 +11038,8 @@ async function* runAssistantTurn(ctx, config, project, outputDir, userMessage) {
8540
11038
  history.push(userEntry);
8541
11039
  appendHistory(outputDir, userEntry);
8542
11040
  let round = 0;
11041
+ /** 最大迭代次数(防空转的总保险,与「已执行动作数 round」分开,避免计数混淆)。 */
11042
+ let iterations = 0;
8543
11043
  /** 已提示模型输出动作标签的次数(0 = 尚未提示;超过上限则按纯文字回复结束,防死循环)。 */
8544
11044
  let nudged = 0;
8545
11045
  const MAX_NUDGES = 6;
@@ -8557,7 +11057,11 @@ async function* runAssistantTurn(ctx, config, project, outputDir, userMessage) {
8557
11057
  export_txt: /(导出|打包|下载|txt)/,
8558
11058
  assets_set_genre: /(题材)/,
8559
11059
  assets_set_progression: /(推进)/,
8560
- assets_add_rule: /(规则|文戒|反AI)/
11060
+ assets_add_rule: /(规则|文戒|反AI)/,
11061
+ knowledge_add: /(知识库|记住|补充|收进|参考|资料)/,
11062
+ plotline_add: /(剧情线|长线|加入剧情线|线名)/,
11063
+ director_todo_add: /(待办|风险|修复|记一下|记一条)/,
11064
+ blurb: /(简介|封面|小说简介)/
8561
11065
  };
8562
11066
  /** 本轮已放行过写操作:后续写操作(生成→审稿→修订闭环)不再逐个拦截。 */
8563
11067
  let writeUnlocked = false;
@@ -8573,7 +11077,7 @@ async function* runAssistantTurn(ctx, config, project, outputDir, userMessage) {
8573
11077
  return false;
8574
11078
  };
8575
11079
  for (;;) {
8576
- if (round++ > 20) break;
11080
+ if (iterations++ > 20) break;
8577
11081
  const reply = await chatOnce(ctx, config, system, history);
8578
11082
  if (reply.length > 120 && /^[0-9a-fA-F\s]+$/.test(reply.slice(0, 2e3))) {
8579
11083
  garbleCount++;
@@ -8598,7 +11102,7 @@ async function* runAssistantTurn(ctx, config, project, outputDir, userMessage) {
8598
11102
  const intendsAction = /(改|修改|修订|重写|替换|调整|生成|新增|删除|导出|看看|查看|调出|读一下|加上|加一个|去掉|删掉|把.+改成|定位|处理|转轨|检查|确认|搜索|找一下|列一下|查一下|查一遍|查一查|再查|查查|核实|核对|清点|盘点|看一下|看下|继续)/.test(reply);
8599
11103
  const mentionsTarget = /(编年录|道藏|暗线|总纲|卷首语|章节|正文|规则|红线|伏笔|简介|大纲|事实|设定|世界|角色|人物|第\s*\d+\s*章)/.test(reply);
8600
11104
  const strayTag = /<[a-z_-]*action[^>]*>/.test(reply);
8601
- if ((intendsAction || mentionsTarget || strayTag) && nudged < MAX_NUDGES) {
11105
+ if ((intendsAction || mentionsTarget || strayTag) && round === 0 && nudged < MAX_NUDGES) {
8602
11106
  const userWriteIntent = Object.values(WRITE_TOOL_KEYS).some((re) => re.test(userMessage));
8603
11107
  const nudge = nudged === 0 ? userWriteIntent ? "你的上一条回复表达了想操作项目的意图(或动作标签格式有误),因此没有执行任何操作。请直接输出动作标签来执行,格式必须为 <dsh-action name=\"工具名\">{\"参数\":值}</dsh-action>(注意拼写是 dsh-action,不是 dash-action;标签成对出现,参数为合法 JSON)。如果需要先看内容,先输出 outline_text 或 chapter_text 标签。" : "你刚才的回复看起来在讨论项目内容,但没有必要执行任何操作。如果用户只是在提问或闲聊,请直接以文字回答即可,不要输出动作标签,也不要自行调用任何写操作(生成/修订/删除等只有用户明确要求时才允许)。若确实需要先查看数据,最多使用只读工具(outline_text / chapter_text / book_overview / facts_query)。" : userWriteIntent ? `你第 ${nudged + 1} 次表达了操作意图但没有输出动作标签,因此仍未执行任何操作。铁律:你的【整个回复】现在必须只包含一个 <dsh-action> 标签(例如 <dsh-action name="chapter_text">{"no":1}</dsh-action>),禁止任何解释、铺垫或"我这就去"之类的文字。若你其实不打算执行任何操作,请明确回复「不执行」。` : `你第 ${nudged + 1} 次回复仍不需要执行操作。再次强调:用户没有要求修改,请直接给出文字回答(可以简短引用项目数据),不要输出动作标签。写操作只会在用户明确要求时被允许。`;
8604
11108
  nudged++;
@@ -8692,6 +11196,11 @@ async function* runAssistantTurn(ctx, config, project, outputDir, userMessage) {
8692
11196
  status: "done",
8693
11197
  detail: result.slice(0, 200)
8694
11198
  };
11199
+ yield {
11200
+ frame: "toolResult",
11201
+ name,
11202
+ text: result.slice(0, 4e3)
11203
+ };
8695
11204
  } catch (error) {
8696
11205
  result = `执行失败:${error.message}`;
8697
11206
  yield {
@@ -8843,7 +11352,7 @@ function importDefaultAuthorAssets() {
8843
11352
  updatedAt: now
8844
11353
  });
8845
11354
  };
8846
- const builtinKinds = /* @__PURE__ */ new Set([
11355
+ const builtinKinds = new Set([
8847
11356
  "genre",
8848
11357
  "antiAi",
8849
11358
  "progression",
@@ -8890,11 +11399,315 @@ function importDefaultAuthorAssets() {
8890
11399
  return library;
8891
11400
  }
8892
11401
  //#endregion
11402
+ //#region src/market-radar-sources.ts
11403
+ const MARKET_RADAR_SOURCES = [
11404
+ {
11405
+ platform: "fanqie",
11406
+ platformLabel: "番茄小说",
11407
+ listKey: "reading",
11408
+ listLabel: "阅读榜",
11409
+ channel: "general",
11410
+ sourceUrl: "https://fanqienovel.com/rank"
11411
+ },
11412
+ {
11413
+ platform: "fanqie",
11414
+ platformLabel: "番茄小说",
11415
+ listKey: "new_book",
11416
+ listLabel: "新书榜",
11417
+ channel: "general",
11418
+ sourceUrl: "https://fanqienovel.com/rank/1_1"
11419
+ },
11420
+ {
11421
+ platform: "qidian",
11422
+ platformLabel: "起点中文网",
11423
+ listKey: "hotsales",
11424
+ listLabel: "畅销榜",
11425
+ channel: "male",
11426
+ sourceUrl: "https://m.qidian.com/rank/hotsales/"
11427
+ },
11428
+ {
11429
+ platform: "qidian",
11430
+ platformLabel: "起点中文网",
11431
+ listKey: "monthly_ticket",
11432
+ listLabel: "月票榜",
11433
+ channel: "male",
11434
+ sourceUrl: "https://m.qidian.com/rank/yuepiao/"
11435
+ },
11436
+ {
11437
+ platform: "qidian",
11438
+ platformLabel: "起点中文网",
11439
+ listKey: "new_book",
11440
+ listLabel: "新书榜",
11441
+ channel: "male",
11442
+ sourceUrl: "https://m.qidian.com/rank/"
11443
+ },
11444
+ {
11445
+ platform: "jinjiang",
11446
+ platformLabel: "晋江文学城",
11447
+ listKey: "monthly",
11448
+ listLabel: "月度榜",
11449
+ channel: "female",
11450
+ sourceUrl: "https://m.jjwxc.net/rank/naturalmore/5"
11451
+ },
11452
+ {
11453
+ platform: "jinjiang",
11454
+ platformLabel: "晋江文学城",
11455
+ listKey: "quarterly",
11456
+ listLabel: "季度榜",
11457
+ channel: "female",
11458
+ sourceUrl: "https://m.jjwxc.net/rank/naturalmore/6"
11459
+ },
11460
+ {
11461
+ platform: "jinjiang",
11462
+ platformLabel: "晋江文学城",
11463
+ listKey: "new_author",
11464
+ listLabel: "新晋作者榜",
11465
+ channel: "female",
11466
+ sourceUrl: "https://m.jjwxc.net/rank/naturalmore/29"
11467
+ }
11468
+ ];
11469
+ const NAMED_ENTITIES = {
11470
+ amp: "&",
11471
+ apos: "'",
11472
+ gt: ">",
11473
+ lt: "<",
11474
+ nbsp: " ",
11475
+ quot: "\""
11476
+ };
11477
+ function decodeEntities(value) {
11478
+ return value.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (_, key) => {
11479
+ if (key.startsWith("#")) {
11480
+ const hexadecimal = key[1]?.toLowerCase() === "x";
11481
+ const code = Number.parseInt(key.slice(hexadecimal ? 2 : 1), hexadecimal ? 16 : 10);
11482
+ return Number.isFinite(code) ? String.fromCodePoint(code) : "";
11483
+ }
11484
+ return NAMED_ENTITIES[key.toLowerCase()] ?? "";
11485
+ });
11486
+ }
11487
+ function plainText(value) {
11488
+ return decodeEntities((value ?? "").replace(/<[^>]*>/g, " ")).replace(/\s+/g, " ").trim();
11489
+ }
11490
+ function absoluteUrl(base, value) {
11491
+ return new URL(value.startsWith("//") ? `https:${value}` : value, base).toString();
11492
+ }
11493
+ function extractTags(synopsis) {
11494
+ return (synopsis.match(/^[【〖\[]([^】〗\]]+)[】〗\]]/g) ?? []).flatMap((part) => plainText(part).replace(/^[【〖\[]|[】〗\]]$/g, "").split(/[++、|]/)).map((tag) => tag.trim()).filter(Boolean).slice(0, 12);
11495
+ }
11496
+ function hasPrivateUseCharacters(value) {
11497
+ return /[\uE000-\uF8FF]/u.test(value ?? "");
11498
+ }
11499
+ function parseFanqieRanking(html, source) {
11500
+ return (html.match(/<div class="rank-book-item">[\s\S]*?(?=<div class="rank-book-item">|<\/main>|<footer|<\/body>)/g) ?? []).slice(0, 30).flatMap((block, index) => {
11501
+ const titleMatch = block.match(/<div class="title">\s*<a href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/);
11502
+ if (!titleMatch) return [];
11503
+ const synopsis = plainText(block.match(/<div class="desc abstract[^>]*>([\s\S]*?)<\/div>/)?.[1]);
11504
+ const footer = plainText(block.match(/<div class="book-item-footer">([\s\S]*?)<\/div>/)?.[1]);
11505
+ return [{
11506
+ rank: Number(block.match(/book-item-index"><h1>(\d+)<\/h1>/)?.[1] ?? index + 1),
11507
+ title: plainText(titleMatch[2]),
11508
+ author: plainText(block.match(/<div class="author">[\s\S]*?<span[^>]*>([\s\S]*?)<\/span>/)?.[1]),
11509
+ tags: extractTags(synopsis),
11510
+ synopsis: synopsis.slice(0, 800),
11511
+ heatLabel: footer.match(/在读[^\s,。]*/)?.[0],
11512
+ serialStatus: footer.match(/连载中|已完结/)?.[0],
11513
+ sourceUrl: absoluteUrl(source.sourceUrl, titleMatch[1])
11514
+ }];
11515
+ });
11516
+ }
11517
+ function parseFanqieDetail(html, item) {
11518
+ const title = plainText(html.match(/<h1[^>]*>([\s\S]*?)<\/h1>/)?.[1]);
11519
+ const author = plainText(html.match(/author-name-text[^>]*>([\s\S]*?)<\/a>/)?.[1]);
11520
+ const synopsis = plainText(html.match(/<div class="page-abstract-content"[^>]*>\s*<p[^>]*>([\s\S]*?)<\/p>/)?.[1]);
11521
+ if (!title || hasPrivateUseCharacters(title)) throw new Error("作品详情页未提供可读书名");
11522
+ return {
11523
+ ...item,
11524
+ title,
11525
+ author: author && !hasPrivateUseCharacters(author) ? author : void 0,
11526
+ synopsis: synopsis && !hasPrivateUseCharacters(synopsis) ? synopsis.slice(0, 800) : void 0,
11527
+ tags: synopsis && !hasPrivateUseCharacters(synopsis) ? extractTags(synopsis) : []
11528
+ };
11529
+ }
11530
+ function parseQidianRanking(html, source) {
11531
+ const escapedLabel = source.listLabel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11532
+ const itemPattern = new RegExp(`<a[^>]+href="([^"]*\\/book\\/[^"?]+)[^"]*"[^>]*>[\\s\\S]*?<h2[^>]+title="${escapedLabel}第(\\d+)位"[^>]*>([\\s\\S]*?)<\\/h2>[\\s\\S]*?<p[^>]*class="[^"]*subTitle[^"]*"[^>]*>([\\s\\S]*?)<\\/p>[\\s\\S]*?<\\/a>`, "g");
11533
+ return Array.from(html.matchAll(itemPattern)).slice(0, 30).map((match) => {
11534
+ const parts = plainText(match[4]).split("·").map((part) => part.trim()).filter(Boolean);
11535
+ return {
11536
+ rank: Number(match[2]),
11537
+ title: plainText(match[3]),
11538
+ author: parts[0],
11539
+ category: parts[1],
11540
+ tags: parts[1] ? [parts[1]] : [],
11541
+ heatLabel: parts[2],
11542
+ sourceUrl: absoluteUrl(source.sourceUrl, match[1])
11543
+ };
11544
+ });
11545
+ }
11546
+ function parseJinjiangRanking(html, source) {
11547
+ return Array.from(html.matchAll(/<li[^>]*>\s*<a href="(\/book2\/\d+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<\/li>/g)).slice(0, 30).map((match, index) => ({
11548
+ rank: index + 1,
11549
+ title: plainText(match[2]),
11550
+ tags: [],
11551
+ sourceUrl: absoluteUrl(source.sourceUrl, match[1])
11552
+ }));
11553
+ }
11554
+ async function fetchHtml(url) {
11555
+ const response = await fetch(url, {
11556
+ headers: { "user-agent": "Mozilla/5.0 (compatible; AI-Novel-Market-Radar/1.0; public-ranking-metadata-only)" },
11557
+ signal: AbortSignal.timeout(2e4)
11558
+ });
11559
+ if (!response.ok) throw new Error(`榜单页面返回 HTTP ${response.status}`);
11560
+ const bytes = await response.arrayBuffer();
11561
+ const headerCharset = response.headers.get("content-type")?.match(/charset=([^;]+)/i)?.[1]?.trim();
11562
+ const metaCharset = new TextDecoder("latin1").decode(bytes.slice(0, 4096)).match(/charset\s*=\s*["']?([\w-]+)/i)?.[1];
11563
+ return new TextDecoder(headerCharset || metaCharset || "utf-8").decode(bytes);
11564
+ }
11565
+ async function hydrateFanqieItems(items) {
11566
+ const hydrated = [];
11567
+ for (let offset = 0; offset < items.length; offset += 4) {
11568
+ const batch = await Promise.all(items.slice(offset, offset + 4).map(async (item) => {
11569
+ if (![
11570
+ item.title,
11571
+ item.author,
11572
+ item.synopsis
11573
+ ].some(hasPrivateUseCharacters)) return item;
11574
+ try {
11575
+ return parseFanqieDetail(await fetchHtml(item.sourceUrl), item);
11576
+ } catch {
11577
+ return null;
11578
+ }
11579
+ }));
11580
+ hydrated.push(...batch.filter((item) => item !== null));
11581
+ }
11582
+ return hydrated;
11583
+ }
11584
+ async function collectMarketSource(source) {
11585
+ const html = await fetchHtml(source.sourceUrl);
11586
+ const parsed = {
11587
+ fanqie: parseFanqieRanking,
11588
+ qidian: parseQidianRanking,
11589
+ jinjiang: parseJinjiangRanking
11590
+ }[source.platform](html, source);
11591
+ const items = source.platform === "fanqie" ? await hydrateFanqieItems(parsed) : parsed;
11592
+ if (items.length === 0) throw new Error("榜单页面结构可能已变化,未识别到公开作品元数据");
11593
+ return items;
11594
+ }
11595
+ //#endregion
11596
+ //#region src/market-radar-scan.ts
11597
+ /**
11598
+ * 热门题材雷达:真实榜单扫榜(聚合各平台榜单源,容错)。
11599
+ */
11600
+ let lastScan = null;
11601
+ async function scanMarketRanking(platforms) {
11602
+ const wanted = new Set(platforms?.length ? platforms : [
11603
+ "fanqie",
11604
+ "qidian",
11605
+ "jinjiang"
11606
+ ]);
11607
+ const sources = MARKET_RADAR_SOURCES.filter((s) => wanted.has(s.platform));
11608
+ const groups = [];
11609
+ await Promise.all(sources.map(async (source) => {
11610
+ try {
11611
+ const items = await collectMarketSource(source);
11612
+ groups.push({
11613
+ platform: source.platform,
11614
+ platformLabel: source.platformLabel,
11615
+ listKey: source.listKey,
11616
+ listLabel: source.listLabel,
11617
+ status: "ok",
11618
+ items
11619
+ });
11620
+ } catch (error) {
11621
+ groups.push({
11622
+ platform: source.platform,
11623
+ platformLabel: source.platformLabel,
11624
+ listKey: source.listKey,
11625
+ listLabel: source.listLabel,
11626
+ status: "error",
11627
+ error: error.message,
11628
+ items: []
11629
+ });
11630
+ }
11631
+ }));
11632
+ lastScan = {
11633
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
11634
+ groups
11635
+ };
11636
+ return lastScan;
11637
+ }
11638
+ //#endregion
11639
+ //#region src/global-assets.ts
11640
+ /**
11641
+ * 全局资源库(跨书可复用的自定义题材/推进模式)。
11642
+ * 雷达「同步底座到全局资源库」写入此处;/assets 返回时与内置库合并展示。
11643
+ */
11644
+ const FILE = join(homedir(), ".dsh", "novel-forge-global-assets.json");
11645
+ let cache = null;
11646
+ function load() {
11647
+ if (cache !== null) return cache;
11648
+ try {
11649
+ const raw = readFileSync(FILE, "utf8");
11650
+ const parsed = JSON.parse(raw);
11651
+ cache = {
11652
+ genres: Array.isArray(parsed.genres) ? parsed.genres : [],
11653
+ modes: Array.isArray(parsed.modes) ? parsed.modes : []
11654
+ };
11655
+ } catch {
11656
+ cache = {
11657
+ genres: [],
11658
+ modes: []
11659
+ };
11660
+ }
11661
+ return cache;
11662
+ }
11663
+ function persist() {
11664
+ if (cache === null) return;
11665
+ mkdirSync(join(homedir(), ".dsh"), { recursive: true });
11666
+ writeFileSync(FILE, JSON.stringify(cache, null, 2), "utf8");
11667
+ }
11668
+ function globalGenreLibrary() {
11669
+ return load().genres;
11670
+ }
11671
+ function globalProgressionLibrary() {
11672
+ return load().modes;
11673
+ }
11674
+ /** 新增/已有则跳过;返回 true=新增。 */
11675
+ function addGlobalGenre(g) {
11676
+ const lib = load();
11677
+ if (lib.genres.some((x) => x.name === g.name)) return false;
11678
+ lib.genres.push({
11679
+ ...g,
11680
+ children: g.children ?? []
11681
+ });
11682
+ persist();
11683
+ return true;
11684
+ }
11685
+ function addGlobalMode(m) {
11686
+ const lib = load();
11687
+ if (lib.modes.some((x) => x.name === m.name)) return false;
11688
+ lib.modes.push({
11689
+ ...m,
11690
+ payoffs: m.payoffs ?? [],
11691
+ risks: m.risks ?? [],
11692
+ primary: m.primary ?? false
11693
+ });
11694
+ persist();
11695
+ return true;
11696
+ }
11697
+ //#endregion
8893
11698
  //#region src/routes.ts
8894
11699
  /** Cap on JSON request bodies (generous: cover images travel as base64). */
8895
11700
  const MAX_JSON_BODY_BYTES = 64 * 1024 * 1024;
8896
11701
  /** 包内置风格效果图目录(assets/styles,随 npm 包分发)。 */
8897
11702
  const builtinStyleDir = fileURLToPath(new URL("../assets/styles/", import.meta.url));
11703
+ /** 解析请求级目标书目录:优先按请求携带的 bookId 查书架,否则回退全局 active 书目录。 */
11704
+ function resolveOutputDir(config, bookId) {
11705
+ if (bookId !== void 0 && bookId !== "") {
11706
+ const book = loadBookshelf().books.find((b) => b.id === bookId);
11707
+ if (book !== void 0 && book.outputDir !== "") return book.outputDir;
11708
+ }
11709
+ return config.outputDir;
11710
+ }
8898
11711
  /** Loopback-only fence (mirrors the family plugins' pairing routes). */
8899
11712
  function isLoopbackRequest(request) {
8900
11713
  const address = request.socket.remoteAddress;
@@ -8987,8 +11800,9 @@ function makeRoutes(deps) {
8987
11800
  handler: (req, res) => {
8988
11801
  if (!guard(req, res, "GET")) return;
8989
11802
  const config = getConfig();
8990
- seedBookshelfFromOutputDir(config.outputDir);
8991
- const project = loadProject(config.outputDir);
11803
+ const outputDir = resolveOutputDir(config, new URL(req.url ?? "/", "http://localhost").searchParams.get("bookId") ?? void 0);
11804
+ seedBookshelfFromOutputDir(outputDir);
11805
+ const project = loadProject(outputDir);
8992
11806
  if (project !== void 0) {
8993
11807
  const staleMs = 600 * 1e3;
8994
11808
  for (const c of project.chapters) if (c.status === "generating" && c.generatingAt !== void 0) {
@@ -8999,8 +11813,8 @@ function makeRoutes(deps) {
8999
11813
  c.generatingAt = void 0;
9000
11814
  }
9001
11815
  }
9002
- syncProjectWithDisk(project, config.outputDir);
9003
- saveProject(config.outputDir, project);
11816
+ syncProjectWithDisk(project, outputDir);
11817
+ saveProject(outputDir, project);
9004
11818
  }
9005
11819
  const slim = new URL(req.url ?? "/", "http://localhost").searchParams.get("slim") === "1";
9006
11820
  let projectPayload;
@@ -9125,8 +11939,13 @@ function makeRoutes(deps) {
9125
11939
  handler: async (req, res) => {
9126
11940
  if (!guard(req, res, "POST")) return;
9127
11941
  const body = await readJsonBody(req);
9128
- const config = getConfig();
9129
- const project = loadProject(config.outputDir);
11942
+ const config0 = getConfig();
11943
+ const outputDir = resolveOutputDir(config0, body?.bookId);
11944
+ const config = {
11945
+ ...config0,
11946
+ outputDir
11947
+ };
11948
+ const project = loadProject(outputDir);
9130
11949
  const outline = body?.outline?.trim() !== "" && body?.outline !== void 0 ? body.outline : project?.outline;
9131
11950
  if (outline === void 0 || outline.length < 50) {
9132
11951
  writeJson(res, 400, { error: "请先加载大纲" });
@@ -9138,7 +11957,7 @@ function makeRoutes(deps) {
9138
11957
  const next = project ?? createProject(outline);
9139
11958
  next.volumes = volumes;
9140
11959
  next.updatedAt = now;
9141
- saveProject(config.outputDir, next);
11960
+ saveProject(outputDir, next);
9142
11961
  writeJson(res, 200, { volumes });
9143
11962
  } catch (error) {
9144
11963
  writeJson(res, 500, { error: error.message });
@@ -9151,8 +11970,13 @@ function makeRoutes(deps) {
9151
11970
  handler: async (req, res) => {
9152
11971
  if (!guard(req, res, "POST")) return;
9153
11972
  const body = await readJsonBody(req);
9154
- const config = getConfig();
9155
- const project = loadProject(config.outputDir);
11973
+ const config0 = getConfig();
11974
+ const outputDir = resolveOutputDir(config0, body?.bookId);
11975
+ const config = {
11976
+ ...config0,
11977
+ outputDir
11978
+ };
11979
+ const project = loadProject(outputDir);
9156
11980
  const outline = body?.outline?.trim() !== "" && body?.outline !== void 0 ? body.outline : project?.outline;
9157
11981
  if (outline === void 0 || outline.length < 50) {
9158
11982
  writeJson(res, 400, { error: "请先加载大纲(或粘贴大纲文本)" });
@@ -9184,10 +12008,18 @@ function makeRoutes(deps) {
9184
12008
  path: NOVEL_API.generate,
9185
12009
  handler: async (req, res) => {
9186
12010
  if (!guard(req, res, "POST")) return;
9187
- const config = getConfig();
9188
- const project = requireProject(res);
9189
- if (project === void 0) return;
12011
+ const config0 = getConfig();
9190
12012
  const body = await readJsonBody(req);
12013
+ const outputDir = resolveOutputDir(config0, body?.bookId);
12014
+ const config = {
12015
+ ...config0,
12016
+ outputDir
12017
+ };
12018
+ const project = loadProject(outputDir);
12019
+ if (project === void 0) {
12020
+ writeJson(res, 400, { error: "输出目录中没有项目" });
12021
+ return;
12022
+ }
9191
12023
  const rawNo = body?.chapterNo;
9192
12024
  if (!Number.isInteger(rawNo) || rawNo === void 0 || rawNo < 1) {
9193
12025
  writeJson(res, 400, { error: "chapterNo 须为正整数" });
@@ -9217,17 +12049,46 @@ function makeRoutes(deps) {
9217
12049
  const send = (frame) => {
9218
12050
  res.write(JSON.stringify(frame) + "\n");
9219
12051
  };
12052
+ const liveSession = nextSessionId();
12053
+ emitLive({
12054
+ type: "session_started",
12055
+ sessionId: liveSession,
12056
+ label: "正文生成",
12057
+ model: config.generateModel || config.model,
12058
+ at: (/* @__PURE__ */ new Date()).toISOString(),
12059
+ context: {
12060
+ interactionId: liveSession,
12061
+ taskId: `ch-${no}`
12062
+ }
12063
+ });
12064
+ emitLive({
12065
+ type: "phase_changed",
12066
+ sessionId: liveSession,
12067
+ phase: "streaming",
12068
+ phaseMessage: "模型正在返回正文",
12069
+ at: (/* @__PURE__ */ new Date()).toISOString()
12070
+ });
9220
12071
  try {
9221
12072
  send({
9222
12073
  type: "start",
9223
12074
  no,
9224
12075
  title: chapter.title
9225
12076
  });
9226
- for await (const step of generateChapterStream(ctx, config, project, config.outputDir, no)) if (step.frame === "delta") send({
9227
- type: "delta",
9228
- text: step.text
9229
- });
9230
- else if (step.frame === "done") send({
12077
+ let genChars = 0;
12078
+ for await (const step of generateChapterStream(ctx, config, project, config.outputDir, no)) if (step.frame === "delta") {
12079
+ send({
12080
+ type: "delta",
12081
+ text: step.text
12082
+ });
12083
+ genChars += step.text.length;
12084
+ emitLive({
12085
+ type: "output_delta",
12086
+ sessionId: liveSession,
12087
+ content: step.text,
12088
+ totalChars: genChars,
12089
+ at: (/* @__PURE__ */ new Date()).toISOString()
12090
+ });
12091
+ } else if (step.frame === "done") send({
9231
12092
  type: "done",
9232
12093
  no,
9233
12094
  file: step.file,
@@ -9235,6 +12096,14 @@ function makeRoutes(deps) {
9235
12096
  title: chapter.title,
9236
12097
  warn: step.warn
9237
12098
  });
12099
+ emitLive({
12100
+ type: "session_completed",
12101
+ sessionId: liveSession,
12102
+ totalChars: genChars,
12103
+ preview: "",
12104
+ at: (/* @__PURE__ */ new Date()).toISOString(),
12105
+ phase: "completed"
12106
+ });
9238
12107
  try {
9239
12108
  await summarizeAndExtractFacts(ctx, config, project, config.outputDir, no);
9240
12109
  } catch (error) {
@@ -9300,17 +12169,25 @@ function makeRoutes(deps) {
9300
12169
  path: NOVEL_API.review,
9301
12170
  handler: async (req, res) => {
9302
12171
  if (!guard(req, res, "POST")) return;
9303
- const config = getConfig();
9304
- const project = requireProject(res);
9305
- if (project === void 0) return;
12172
+ const config0 = getConfig();
9306
12173
  const body = await readJsonBody(req);
12174
+ const outputDir = resolveOutputDir(config0, body?.bookId);
12175
+ const config = {
12176
+ ...config0,
12177
+ outputDir
12178
+ };
12179
+ const project = loadProject(outputDir);
12180
+ if (project === void 0) {
12181
+ writeJson(res, 400, { error: "输出目录中没有项目" });
12182
+ return;
12183
+ }
9307
12184
  if (!Number.isInteger(body?.chapterNo)) {
9308
12185
  writeJson(res, 400, { error: "chapterNo 须为正整数" });
9309
12186
  return;
9310
12187
  }
9311
12188
  const no = body.chapterNo;
9312
12189
  try {
9313
- writeJson(res, 200, { report: await reviewChapter(ctx, config, project, config.outputDir, no) });
12190
+ writeJson(res, 200, { report: await reviewChapter(ctx, config, project, outputDir, no) });
9314
12191
  } catch (error) {
9315
12192
  writeJson(res, 500, { error: error.message });
9316
12193
  }
@@ -9321,10 +12198,18 @@ function makeRoutes(deps) {
9321
12198
  path: NOVEL_API.rewrite,
9322
12199
  handler: async (req, res) => {
9323
12200
  if (!guard(req, res, "POST")) return;
9324
- const config = getConfig();
9325
- const project = requireProject(res);
9326
- if (project === void 0) return;
12201
+ const config0 = getConfig();
9327
12202
  const body = await readJsonBody(req);
12203
+ const outputDir = resolveOutputDir(config0, body?.bookId);
12204
+ const config = {
12205
+ ...config0,
12206
+ outputDir
12207
+ };
12208
+ const project = loadProject(outputDir);
12209
+ if (project === void 0) {
12210
+ writeJson(res, 400, { error: "输出目录中没有项目" });
12211
+ return;
12212
+ }
9328
12213
  if (!Number.isInteger(body?.chapterNo)) {
9329
12214
  writeJson(res, 400, { error: "chapterNo 须为正整数" });
9330
12215
  return;
@@ -9368,10 +12253,18 @@ function makeRoutes(deps) {
9368
12253
  path: NOVEL_API.polish,
9369
12254
  handler: async (req, res) => {
9370
12255
  if (!guard(req, res, "POST")) return;
9371
- const config = getConfig();
9372
- const project = requireProject(res);
9373
- if (project === void 0) return;
12256
+ const config0 = getConfig();
9374
12257
  const body = await readJsonBody(req);
12258
+ const outputDir = resolveOutputDir(config0, body?.bookId);
12259
+ const config = {
12260
+ ...config0,
12261
+ outputDir
12262
+ };
12263
+ const project = loadProject(outputDir);
12264
+ if (project === void 0) {
12265
+ writeJson(res, 400, { error: "输出目录中没有项目" });
12266
+ return;
12267
+ }
9375
12268
  if (!Number.isInteger(body?.chapterNo)) {
9376
12269
  writeJson(res, 400, { error: "chapterNo 须为正整数" });
9377
12270
  return;
@@ -9513,10 +12406,18 @@ function makeRoutes(deps) {
9513
12406
  path: NOVEL_API.foreshadow,
9514
12407
  handler: async (req, res) => {
9515
12408
  if (!guard(req, res, "POST")) return;
9516
- const config = getConfig();
9517
- const project = requireProject(res);
9518
- if (project === void 0) return;
12409
+ const config0 = getConfig();
9519
12410
  const body = await readJsonBody(req);
12411
+ const outputDir = resolveOutputDir(config0, body?.bookId);
12412
+ const config = {
12413
+ ...config0,
12414
+ outputDir
12415
+ };
12416
+ const project = loadProject(outputDir);
12417
+ if (project === void 0) {
12418
+ writeJson(res, 400, { error: "输出目录中没有项目" });
12419
+ return;
12420
+ }
9520
12421
  try {
9521
12422
  if (body?.suggest === true) {
9522
12423
  const created = await suggestForeshadows(ctx, config, project);
@@ -9563,10 +12464,19 @@ function makeRoutes(deps) {
9563
12464
  path: NOVEL_API.exportBook,
9564
12465
  handler: async (req, res) => {
9565
12466
  if (!guard(req, res, "POST")) return;
9566
- const config = getConfig();
9567
- const project = requireProject(res);
9568
- if (project === void 0) return;
9569
- const format = (await readJsonBody(req))?.format === "md" ? "md" : "txt";
12467
+ const config0 = getConfig();
12468
+ const body = await readJsonBody(req);
12469
+ const outputDir = resolveOutputDir(config0, body?.bookId);
12470
+ const config = {
12471
+ ...config0,
12472
+ outputDir
12473
+ };
12474
+ const project = loadProject(outputDir);
12475
+ if (project === void 0) {
12476
+ writeJson(res, 400, { error: "输出目录中没有项目" });
12477
+ return;
12478
+ }
12479
+ const format = body?.format === "md" ? "md" : "txt";
9570
12480
  try {
9571
12481
  writeJson(res, 200, { ...exportBook(config.outputDir, project, format) });
9572
12482
  } catch (error) {
@@ -9580,9 +12490,13 @@ function makeRoutes(deps) {
9580
12490
  handler: async (req, res) => {
9581
12491
  if (!guard(req, res, "GET")) return;
9582
12492
  const config = getConfig();
9583
- const project = requireProject(res);
9584
- if (project === void 0) return;
9585
12493
  const url = new URL(req.url ?? "/", "http://localhost");
12494
+ const outputDir = resolveOutputDir(config, url.searchParams.get("bookId") ?? void 0);
12495
+ const project = loadProject(outputDir);
12496
+ if (project === void 0) {
12497
+ writeJson(res, 400, { error: "输出目录中没有项目" });
12498
+ return;
12499
+ }
9586
12500
  const rawNo = Number(url.searchParams.get("no") ?? "0");
9587
12501
  if (!Number.isInteger(rawNo) || rawNo < 1) {
9588
12502
  writeJson(res, 400, { error: "no 须为正整数" });
@@ -9593,7 +12507,7 @@ function makeRoutes(deps) {
9593
12507
  writeJson(res, 404, { error: `章节 ${rawNo} 不在计划中` });
9594
12508
  return;
9595
12509
  }
9596
- const markdown = readChapterFile(config.outputDir, chapter);
12510
+ const markdown = readChapterFile(outputDir, chapter);
9597
12511
  if (markdown === void 0) {
9598
12512
  writeJson(res, 404, { error: `章节 ${rawNo} 尚未生成` });
9599
12513
  return;
@@ -9683,9 +12597,14 @@ function makeRoutes(deps) {
9683
12597
  handler: async (req, res) => {
9684
12598
  if (!guard(req, res, "POST")) return;
9685
12599
  const config = getConfig();
9686
- const project = requireProject(res);
9687
- if (project === void 0) return;
9688
- const message = (await readJsonBody(req))?.message?.trim();
12600
+ const body = await readJsonBody(req);
12601
+ const outputDir = resolveOutputDir(config, body?.bookId);
12602
+ const project = loadProject(outputDir);
12603
+ if (project === void 0) {
12604
+ writeJson(res, 400, { error: "输出目录中没有项目" });
12605
+ return;
12606
+ }
12607
+ const message = body?.message?.trim();
9689
12608
  if (message === void 0 || message === "") {
9690
12609
  writeJson(res, 400, { error: "消息不能为空" });
9691
12610
  return;
@@ -9700,7 +12619,7 @@ function makeRoutes(deps) {
9700
12619
  res.write(JSON.stringify(frame) + "\n");
9701
12620
  };
9702
12621
  try {
9703
- for await (const step of runAssistantTurn(ctx, config, project, config.outputDir, message)) if (step.frame === "delta") send({
12622
+ for await (const step of runAssistantTurn(ctx, config, project, outputDir, message)) if (step.frame === "delta") send({
9704
12623
  type: "delta",
9705
12624
  text: step.text
9706
12625
  });
@@ -9715,6 +12634,11 @@ function makeRoutes(deps) {
9715
12634
  name: step.name,
9716
12635
  text: step.text
9717
12636
  });
12637
+ else if (step.frame === "toolResult") send({
12638
+ type: "toolResult",
12639
+ name: step.name,
12640
+ text: step.text
12641
+ });
9718
12642
  send({ type: "done" });
9719
12643
  res.end();
9720
12644
  } catch (error) {
@@ -9733,16 +12657,16 @@ function makeRoutes(deps) {
9733
12657
  path: NOVEL_API.assistantHistory,
9734
12658
  handler: (req, res) => {
9735
12659
  if (!guard(req, res, "GET")) return;
9736
- writeJson(res, 200, { messages: loadAssistantHistory(getConfig().outputDir) });
12660
+ writeJson(res, 200, { messages: loadAssistantHistory(resolveOutputDir(getConfig(), new URL(req.url ?? "/", "http://localhost").searchParams.get("bookId") ?? void 0)) });
9737
12661
  }
9738
12662
  };
9739
12663
  /** 清空助手对话记录。 */
9740
12664
  const assistantClearRoute = {
9741
12665
  kind: "exact",
9742
12666
  path: NOVEL_API.assistantClear,
9743
- handler: (req, res) => {
12667
+ handler: async (req, res) => {
9744
12668
  if (!guard(req, res, "POST")) return;
9745
- clearAssistantHistory(getConfig().outputDir);
12669
+ clearAssistantHistory(resolveOutputDir(getConfig(), (await readJsonBody(req))?.bookId));
9746
12670
  writeJson(res, 200, { ok: true });
9747
12671
  }
9748
12672
  };
@@ -9759,10 +12683,20 @@ function makeRoutes(deps) {
9759
12683
  return;
9760
12684
  }
9761
12685
  const config = getConfig();
9762
- const project = loadProject(config.outputDir);
9763
- const projectAssets = project?.assets ?? emptyProjectAssets();
12686
+ let postBody;
9764
12687
  if (req.method === "POST") {
9765
- const body = await readJsonBody(req);
12688
+ postBody = await readJsonBody(req);
12689
+ if (postBody === void 0) {
12690
+ writeJson(res, 400, { error: "无效的 JSON" });
12691
+ return;
12692
+ }
12693
+ }
12694
+ const queryBookId = new URL(req.url ?? "/", "http://localhost").searchParams.get("bookId") ?? void 0;
12695
+ const outputDir = resolveOutputDir(config, postBody?.bookId ?? queryBookId);
12696
+ const project = loadProject(outputDir);
12697
+ const projectAssets = ensureBuiltinAssets(project?.assets ?? emptyProjectAssets());
12698
+ if (req.method === "POST") {
12699
+ const body = postBody;
9766
12700
  if (body === void 0) {
9767
12701
  writeJson(res, 400, { error: "无效的 JSON" });
9768
12702
  return;
@@ -9779,14 +12713,15 @@ function makeRoutes(deps) {
9779
12713
  projectAssets.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9780
12714
  project.assets = projectAssets;
9781
12715
  project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9782
- saveProject(config.outputDir, project);
12716
+ saveProject(outputDir, project);
9783
12717
  }
9784
12718
  writeJson(res, 200, {
9785
12719
  projectAssets,
9786
- genreLibrary: BUILTIN_GENRE_LIBRARY,
12720
+ genreLibrary: [...BUILTIN_GENRE_LIBRARY, ...globalGenreLibrary()],
9787
12721
  antiAiLibrary: BUILTIN_ANTI_AI_RULES,
9788
12722
  styleTemplates: BUILTIN_STYLE_TEMPLATES,
9789
- progressionLibrary: BUILTIN_PROGRESSION_MODES,
12723
+ progressionLibrary: [...BUILTIN_PROGRESSION_MODES, ...globalProgressionLibrary()],
12724
+ starterStyleProfiles: BUILTIN_STARTER_STYLE_PROFILES,
9790
12725
  plotBeatLibrary: BUILTIN_PLOT_BEATS
9791
12726
  });
9792
12727
  }
@@ -9826,6 +12761,406 @@ function makeRoutes(deps) {
9826
12761
  }
9827
12762
  }
9828
12763
  };
12764
+ const styleFormulaRoute = {
12765
+ kind: "exact",
12766
+ path: NOVEL_API.styleFormula,
12767
+ handler: async (req, res) => {
12768
+ if (!guard(req, res, "POST")) return;
12769
+ const config = getConfig();
12770
+ const project = loadProject(config.outputDir);
12771
+ const body = await readJsonBody(req);
12772
+ const sample = body?.sampleText?.trim();
12773
+ if (sample === void 0 || sample.length < 50) {
12774
+ writeJson(res, 400, { error: "样本文本过短(<50 字符),请粘贴一段能代表目标风格的文字" });
12775
+ return;
12776
+ }
12777
+ const depth = [
12778
+ "basic",
12779
+ "standard",
12780
+ "deep"
12781
+ ].includes(body?.depth) ? body.depth : "standard";
12782
+ try {
12783
+ const formula = await extractStyleFormula(ctx, config, sample, depth);
12784
+ const styleFormula = {
12785
+ key: `sf-${Date.now().toString(36)}`,
12786
+ ...formula,
12787
+ depth,
12788
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
12789
+ };
12790
+ if (project !== void 0) {
12791
+ project.assets ??= emptyProjectAssets();
12792
+ project.assets.styleFormulas ??= [];
12793
+ project.assets.styleFormulas.push(styleFormula);
12794
+ project.assets.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
12795
+ project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
12796
+ saveProject(config.outputDir, project);
12797
+ }
12798
+ writeJson(res, 200, { styleFormula });
12799
+ } catch (error) {
12800
+ writeJson(res, 500, { error: error.message });
12801
+ }
12802
+ }
12803
+ };
12804
+ /** 正文实时 AI 味检测(本地确定性扫描,免 LLM)。 */
12805
+ const styleDetectRoute = {
12806
+ kind: "exact",
12807
+ path: NOVEL_API.styleDetect,
12808
+ handler: async (req, res) => {
12809
+ if (!guard(req, res, "POST")) return;
12810
+ const text = (await readJsonBody(req))?.text?.trim() ?? "";
12811
+ if (text.length < 20) {
12812
+ writeJson(res, 400, { error: "文本过短(<20 字符)" });
12813
+ return;
12814
+ }
12815
+ writeJson(res, 200, { scan: scanAiFlavor(text.slice(0, 2e4)) });
12816
+ }
12817
+ };
12818
+ /** 热门题材雷达:输入平台/题材/榜单文本 → 信号 + 生产底座 + 开书创意。 */
12819
+ const marketRadarRoute = {
12820
+ kind: "exact",
12821
+ path: NOVEL_API.marketRadar,
12822
+ handler: async (req, res) => {
12823
+ if (!guard(req, res, "POST")) return;
12824
+ const config = getConfig();
12825
+ const body = await readJsonBody(req);
12826
+ try {
12827
+ writeJson(res, 200, { result: await runMarketRadar(ctx, config, body ?? {}) });
12828
+ } catch (error) {
12829
+ writeJson(res, 500, { error: error.message });
12830
+ }
12831
+ }
12832
+ };
12833
+ /** 真实榜单扫榜:抓取公开榜单(容错),返回分组候选。 */
12834
+ const marketRadarScanRoute = {
12835
+ kind: "exact",
12836
+ path: NOVEL_API.marketRadarScan,
12837
+ handler: async (req, res) => {
12838
+ if (!guard(req, res, "POST")) return;
12839
+ const body = await readJsonBody(req);
12840
+ try {
12841
+ writeJson(res, 200, { result: await scanMarketRanking(body?.platforms) });
12842
+ } catch (error) {
12843
+ writeJson(res, 500, { error: error.message });
12844
+ }
12845
+ }
12846
+ };
12847
+ /** 把雷达生产底座一键应用到某本书(写入项目资产/开书定盘,供后续规划与生成遵守)。 */
12848
+ const marketRadarApplyRoute = {
12849
+ kind: "exact",
12850
+ path: NOVEL_API.marketRadarApply,
12851
+ handler: async (req, res) => {
12852
+ if (!guard(req, res, "POST")) return;
12853
+ const body = await readJsonBody(req);
12854
+ const pf = body?.foundation;
12855
+ if (pf === void 0) {
12856
+ writeJson(res, 400, { error: "缺少 foundation" });
12857
+ return;
12858
+ }
12859
+ const outputDir = resolveOutputDir(getConfig(), body?.bookId);
12860
+ const project = loadProject(outputDir);
12861
+ if (project === void 0) {
12862
+ writeJson(res, 400, { error: "输出目录中没有项目" });
12863
+ return;
12864
+ }
12865
+ const genreBuiltin = BUILTIN_GENRE_LIBRARY.find((g) => g.name === (pf.genre.existingId || pf.genre.name));
12866
+ const genreNode = genreBuiltin !== void 0 ? { ...genreBuiltin } : {
12867
+ name: pf.genre.name.slice(0, 40),
12868
+ description: pf.genre.description,
12869
+ template: pf.genre.template,
12870
+ children: []
12871
+ };
12872
+ const findMode = (m) => BUILTIN_PROGRESSION_MODES.find((x) => x.name === (m.existingId || m.name));
12873
+ const primary = findMode(pf.primaryStoryMode) ?? {
12874
+ name: pf.primaryStoryMode.name.slice(0, 40),
12875
+ driver: pf.primaryStoryMode.driver,
12876
+ readerExpectation: pf.primaryStoryMode.readerExpectation,
12877
+ payoffs: [],
12878
+ risks: [],
12879
+ primary: true
12880
+ };
12881
+ const aux = [];
12882
+ if (pf.secondaryStoryMode !== void 0) {
12883
+ const sec = findMode(pf.secondaryStoryMode) ?? {
12884
+ name: pf.secondaryStoryMode.name.slice(0, 40),
12885
+ driver: pf.secondaryStoryMode.driver,
12886
+ readerExpectation: pf.secondaryStoryMode.readerExpectation,
12887
+ payoffs: [],
12888
+ risks: [],
12889
+ primary: false
12890
+ };
12891
+ aux.push(sec);
12892
+ }
12893
+ project.assets ??= emptyProjectAssets();
12894
+ project.assets.genre = genreNode;
12895
+ project.assets.primaryProgression = primary;
12896
+ project.assets.auxiliaryProgressions = aux;
12897
+ project.bookContract = {
12898
+ promise: pf.genre.description.slice(0, 120),
12899
+ primaryModeName: pf.primaryStoryMode.name,
12900
+ secondaryModeNames: pf.secondaryStoryMode !== void 0 ? [pf.secondaryStoryMode.name] : [],
12901
+ tone: pf.genre.template,
12902
+ targetPlatform: void 0
12903
+ };
12904
+ project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
12905
+ saveProject(outputDir, project);
12906
+ writeJson(res, 200, {
12907
+ ok: true,
12908
+ bookName: project.bookName
12909
+ });
12910
+ }
12911
+ };
12912
+ /** 把雷达生产底座里的「新资产」同步进全局资源库(跨书复用;已有名字跳过)。 */
12913
+ const marketRadarSyncRoute = {
12914
+ kind: "exact",
12915
+ path: NOVEL_API.marketRadarSync,
12916
+ handler: async (req, res) => {
12917
+ if (!guard(req, res, "POST")) return;
12918
+ const pf = (await readJsonBody(req))?.foundation;
12919
+ if (pf === void 0) {
12920
+ writeJson(res, 400, { error: "缺少 foundation" });
12921
+ return;
12922
+ }
12923
+ const synced = {
12924
+ genre: false,
12925
+ primaryMode: false,
12926
+ secondaryMode: false
12927
+ };
12928
+ if (pf.genre.existingId === void 0 && pf.genre.name !== "") synced.genre = addGlobalGenre({
12929
+ name: pf.genre.name.slice(0, 40),
12930
+ description: pf.genre.description,
12931
+ template: pf.genre.template,
12932
+ children: []
12933
+ });
12934
+ if (pf.primaryStoryMode.existingId === void 0 && pf.primaryStoryMode.name !== "") synced.primaryMode = addGlobalMode({
12935
+ name: pf.primaryStoryMode.name.slice(0, 40),
12936
+ driver: pf.primaryStoryMode.driver,
12937
+ readerExpectation: pf.primaryStoryMode.readerExpectation,
12938
+ payoffs: [],
12939
+ risks: [],
12940
+ primary: true
12941
+ });
12942
+ if (pf.secondaryStoryMode !== void 0 && pf.secondaryStoryMode !== null && pf.secondaryStoryMode.existingId === void 0 && pf.secondaryStoryMode.name !== "") synced.secondaryMode = addGlobalMode({
12943
+ name: pf.secondaryStoryMode.name.slice(0, 40),
12944
+ driver: pf.secondaryStoryMode.driver,
12945
+ readerExpectation: pf.secondaryStoryMode.readerExpectation,
12946
+ payoffs: [],
12947
+ risks: [],
12948
+ primary: false
12949
+ });
12950
+ writeJson(res, 200, {
12951
+ ok: true,
12952
+ synced
12953
+ });
12954
+ }
12955
+ };
12956
+ /** 开书创意简报:用选中的市场信号 + 影响模式生成。 */
12957
+ const marketRadarBriefRoute = {
12958
+ kind: "exact",
12959
+ path: NOVEL_API.marketRadarBrief,
12960
+ handler: async (req, res) => {
12961
+ if (!guard(req, res, "POST")) return;
12962
+ const config = getConfig();
12963
+ const body = await readJsonBody(req);
12964
+ if (body === void 0 || body.influenceMode === void 0) {
12965
+ writeJson(res, 400, { error: "缺少 influenceMode" });
12966
+ return;
12967
+ }
12968
+ try {
12969
+ writeJson(res, 200, { creativeBrief: await runMarketCreativeBrief(ctx, config, body) });
12970
+ } catch (error) {
12971
+ writeJson(res, 500, { error: error.message });
12972
+ }
12973
+ }
12974
+ };
12975
+ /** 书内知识库:GET 读取 / POST 增删改(按书路由)。 */
12976
+ const knowledgeRoute = {
12977
+ kind: "exact",
12978
+ path: NOVEL_API.knowledge,
12979
+ handler: async (req, res) => {
12980
+ if (req.method === "POST") {
12981
+ if (!guard(req, res, "POST")) return;
12982
+ } else if (!guard(req, res, "GET")) return;
12983
+ const config = getConfig();
12984
+ const qurl = new URL(req.url ?? "/", "http://localhost");
12985
+ const body = req.method === "POST" ? await readJsonBody(req) : void 0;
12986
+ const outputDir = resolveOutputDir(config, body?.bookId ?? qurl.searchParams.get("bookId") ?? void 0);
12987
+ const project = loadProject(outputDir);
12988
+ if (project === void 0) {
12989
+ writeJson(res, 400, { error: "输出目录中没有项目" });
12990
+ return;
12991
+ }
12992
+ project.knowledgeDocs ??= [];
12993
+ if (req.method === "POST" && body !== void 0) {
12994
+ if (body.action === "add" && body.doc !== void 0) project.knowledgeDocs.push({
12995
+ id: body.doc.id ?? `kd-${Date.now().toString(36)}`,
12996
+ title: body.doc.title,
12997
+ content: body.doc.content,
12998
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
12999
+ });
13000
+ else if (body.action === "remove" && body.id !== void 0) project.knowledgeDocs = project.knowledgeDocs.filter((d) => d.id !== body.id);
13001
+ else if (body.action === "replace" && body.doc !== void 0) project.knowledgeDocs = project.knowledgeDocs.map((d) => d.id === body.doc.id ? {
13002
+ ...d,
13003
+ title: body.doc.title,
13004
+ content: body.doc.content,
13005
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13006
+ } : d);
13007
+ project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
13008
+ saveProject(outputDir, project);
13009
+ }
13010
+ writeJson(res, 200, { docs: project.knowledgeDocs });
13011
+ }
13012
+ };
13013
+ /** 书分析/拆书:输入文本 → 卖点/结构/可借鉴点/风险。 */
13014
+ const bookAnalysisRoute = {
13015
+ kind: "exact",
13016
+ path: NOVEL_API.bookAnalysis,
13017
+ handler: async (req, res) => {
13018
+ if (!guard(req, res, "POST")) return;
13019
+ const config = getConfig();
13020
+ const body = await readJsonBody(req);
13021
+ if (body === void 0 || (body.text ?? "").trim().length < 50) {
13022
+ writeJson(res, 400, { error: "文本过短(<50 字符)" });
13023
+ return;
13024
+ }
13025
+ try {
13026
+ writeJson(res, 200, { result: await runBookAnalysis(ctx, config, body) });
13027
+ } catch (error) {
13028
+ writeJson(res, 500, { error: error.message });
13029
+ }
13030
+ }
13031
+ };
13032
+ /** 创意灵感:一句话/题材 → 多方向开书灵感。 */
13033
+ const ideaInspirationRoute = {
13034
+ kind: "exact",
13035
+ path: NOVEL_API.ideaInspiration,
13036
+ handler: async (req, res) => {
13037
+ if (!guard(req, res, "POST")) return;
13038
+ const config = getConfig();
13039
+ const body = await readJsonBody(req);
13040
+ if (body === void 0 || (body.idea ?? "").trim().length < 2) {
13041
+ writeJson(res, 400, { error: "请填写你的方向/一句话想法" });
13042
+ return;
13043
+ }
13044
+ try {
13045
+ writeJson(res, 200, { result: await runIdeaInspiration(ctx, config, body) });
13046
+ } catch (error) {
13047
+ writeJson(res, 500, { error: error.message });
13048
+ }
13049
+ }
13050
+ };
13051
+ /** 雷达→灵感:基于市场信号/生产底座/创意简报生成开书灵感。 */
13052
+ const marketIdeaInspirationRoute = {
13053
+ kind: "exact",
13054
+ path: NOVEL_API.ideaInspirationMarket,
13055
+ handler: async (req, res) => {
13056
+ if (!guard(req, res, "POST")) return;
13057
+ const config = getConfig();
13058
+ const body = await readJsonBody(req);
13059
+ if (body === void 0) {
13060
+ writeJson(res, 400, { error: "缺少市场分析参数" });
13061
+ return;
13062
+ }
13063
+ try {
13064
+ writeJson(res, 200, { result: await runMarketIdeaInspiration(ctx, config, body) });
13065
+ } catch (error) {
13066
+ writeJson(res, 500, { error: error.message });
13067
+ }
13068
+ }
13069
+ };
13070
+ /** 自动导演编排建议:基于全书上下文。 */
13071
+ const directorRoute = {
13072
+ kind: "exact",
13073
+ path: NOVEL_API.director,
13074
+ handler: async (req, res) => {
13075
+ if (!guard(req, res, "POST")) return;
13076
+ const config = getConfig();
13077
+ const body = await readJsonBody(req);
13078
+ const project = loadProject(resolveOutputDir(config, body?.bookId));
13079
+ if (project === void 0) {
13080
+ writeJson(res, 400, { error: "输出目录中没有项目" });
13081
+ return;
13082
+ }
13083
+ try {
13084
+ writeJson(res, 200, { result: await runDirectorAdvice(ctx, config, project, body ?? {}) });
13085
+ } catch (error) {
13086
+ writeJson(res, 500, { error: error.message });
13087
+ }
13088
+ }
13089
+ };
13090
+ /** 自动导演「采纳」出的书内待办:GET 读取 / POST 增删勾选(按书路由)。 */
13091
+ const directorTodosRoute = {
13092
+ kind: "exact",
13093
+ path: NOVEL_API.directorTodos,
13094
+ handler: async (req, res) => {
13095
+ let body;
13096
+ if (req.method === "POST") {
13097
+ if (!guard(req, res, "POST")) return;
13098
+ body = await readJsonBody(req);
13099
+ } else if (!guard(req, res, "GET")) return;
13100
+ const outputDir = resolveOutputDir(getConfig(), body?.bookId);
13101
+ const project = loadProject(outputDir);
13102
+ if (project === void 0) {
13103
+ writeJson(res, 400, { error: "输出目录中没有项目" });
13104
+ return;
13105
+ }
13106
+ project.todos ??= [];
13107
+ if (req.method === "POST" && body !== void 0) {
13108
+ if (body.op === "add" && (body.text ?? "").trim() !== "") project.todos.unshift({
13109
+ id: `td-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
13110
+ text: body.text.trim(),
13111
+ source: body.source ?? "fix",
13112
+ done: false,
13113
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
13114
+ });
13115
+ else if (body.op === "toggle" && body.id !== void 0) project.todos = project.todos.map((t) => t.id === body.id ? {
13116
+ ...t,
13117
+ done: !t.done
13118
+ } : t);
13119
+ else if (body.op === "remove" && body.id !== void 0) project.todos = project.todos.filter((t) => t.id !== body.id);
13120
+ project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
13121
+ saveProject(outputDir, project);
13122
+ }
13123
+ writeJson(res, 200, { todos: project.todos });
13124
+ }
13125
+ };
13126
+ /** AI 创作实况:订阅 LLM 实时事件流(NDJSON;订阅时先重放最近缓冲)。 */
13127
+ const llmLiveRoute = {
13128
+ kind: "exact",
13129
+ path: NOVEL_API.llmLive,
13130
+ handler: async (req, res) => {
13131
+ if (req.method !== "GET") {
13132
+ writeJson(res, 405, { error: "GET only" });
13133
+ return;
13134
+ }
13135
+ if (!isLoopbackRequest(req)) {
13136
+ writeJson(res, 403, { error: "forbidden: loopback-only" });
13137
+ return;
13138
+ }
13139
+ res.writeHead(200, {
13140
+ "content-type": "text/event-stream; charset=utf-8",
13141
+ "cache-control": "no-cache",
13142
+ "x-accel-buffering": "no",
13143
+ "referrer-policy": "no-referrer"
13144
+ });
13145
+ const unsubscribe = subscribeLiveFeed((frame) => {
13146
+ try {
13147
+ res.write("data: " + JSON.stringify(frame) + "\n\n");
13148
+ } catch {}
13149
+ });
13150
+ const hb = setInterval(() => {
13151
+ try {
13152
+ res.write(": ping\n\n");
13153
+ } catch {}
13154
+ }, 25e3);
13155
+ await new Promise((resolve) => {
13156
+ res.on("close", () => {
13157
+ clearInterval(hb);
13158
+ unsubscribe();
13159
+ resolve();
13160
+ });
13161
+ });
13162
+ }
13163
+ };
9829
13164
  const bookshelfRoute = {
9830
13165
  kind: "exact",
9831
13166
  path: NOVEL_API.bookshelf,
@@ -10019,9 +13354,18 @@ function makeRoutes(deps) {
10019
13354
  path: NOVEL_API.audit,
10020
13355
  handler: async (req, res) => {
10021
13356
  if (!guard(req, res, "POST")) return;
10022
- const config = getConfig();
10023
- const project = requireProject(res);
10024
- if (project === void 0) return;
13357
+ const body = await readJsonBody(req);
13358
+ const config0 = getConfig();
13359
+ const outputDir = resolveOutputDir(config0, body?.bookId);
13360
+ const config = {
13361
+ ...config0,
13362
+ outputDir
13363
+ };
13364
+ const project = loadProject(outputDir);
13365
+ if (project === void 0) {
13366
+ writeJson(res, 400, { error: "输出目录中没有项目" });
13367
+ return;
13368
+ }
10025
13369
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
10026
13370
  auditState = {
10027
13371
  status: "running",
@@ -10048,7 +13392,8 @@ function makeRoutes(deps) {
10048
13392
  writeJson(res, 200, {
10049
13393
  issues,
10050
13394
  auditedChapters,
10051
- auditedAt: auditState.finishedAt
13395
+ auditedAt: auditState.finishedAt,
13396
+ model: config.auditModel || config.model
10052
13397
  });
10053
13398
  } catch (error) {
10054
13399
  auditState = {
@@ -10154,8 +13499,7 @@ function makeRoutes(deps) {
10154
13499
  writeJson(res, 200, { blurb: text });
10155
13500
  return;
10156
13501
  }
10157
- const partial = body?.partial?.trim() ?? "";
10158
- const blurb = await generateBlurb(ctx, config, project, partial);
13502
+ const blurb = await generateBlurb(ctx, config, project, body?.partial?.trim() ?? "");
10159
13503
  project.blurb = blurb;
10160
13504
  project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
10161
13505
  saveProject(config.outputDir, project);
@@ -11533,7 +14877,8 @@ function makeRoutes(deps) {
11533
14877
  handler: async (req, res) => {
11534
14878
  if (!guard(req, res, "POST")) return;
11535
14879
  const body = await readJsonBody(req);
11536
- const project = loadProject(getConfig().outputDir);
14880
+ const outputDir = resolveOutputDir(getConfig(), body?.bookId);
14881
+ const project = loadProject(outputDir);
11537
14882
  if (project === void 0) {
11538
14883
  writeJson(res, 400, { error: "输出目录中没有项目" });
11539
14884
  return;
@@ -11547,7 +14892,7 @@ function makeRoutes(deps) {
11547
14892
  return;
11548
14893
  }
11549
14894
  try {
11550
- writeJson(res, 200, await runner.start(startNo, endNo));
14895
+ writeJson(res, 200, await runner.start(startNo, endNo, outputDir));
11551
14896
  } catch (error) {
11552
14897
  writeJson(res, 409, { error: error.message });
11553
14898
  }
@@ -11939,6 +15284,20 @@ function makeRoutes(deps) {
11939
15284
  chapterSaveRoute,
11940
15285
  assetsRoute,
11941
15286
  styleEngineRoute,
15287
+ styleFormulaRoute,
15288
+ styleDetectRoute,
15289
+ marketRadarRoute,
15290
+ marketRadarScanRoute,
15291
+ marketRadarApplyRoute,
15292
+ marketRadarSyncRoute,
15293
+ marketRadarBriefRoute,
15294
+ knowledgeRoute,
15295
+ bookAnalysisRoute,
15296
+ ideaInspirationRoute,
15297
+ marketIdeaInspirationRoute,
15298
+ directorRoute,
15299
+ directorTodosRoute,
15300
+ llmLiveRoute,
11942
15301
  assistantRoute,
11943
15302
  assistantHistoryRoute,
11944
15303
  assistantClearRoute,
@@ -12041,8 +15400,7 @@ function makeRoutes(deps) {
12041
15400
  writeJson(res, 404, { error: "not found" });
12042
15401
  return;
12043
15402
  }
12044
- const ext = extname(file).slice(1);
12045
- const type = MIME_BY_EXT[ext] ?? "application/octet-stream";
15403
+ const type = MIME_BY_EXT[extname(file).slice(1)] ?? "application/octet-stream";
12046
15404
  res.writeHead(200, {
12047
15405
  "content-type": type,
12048
15406
  "cache-control": "public, max-age=31536000"
@@ -12084,6 +15442,9 @@ const Config = z.object({
12084
15442
  outputDir: z.string().default(join(homedir(), ".dsh", "novels")),
12085
15443
  provider: z.string().default("deepseek-official"),
12086
15444
  model: z.string().default("deepseek-v4-flash"),
15445
+ generateModel: z.string().default(""),
15446
+ reviewModel: z.string().default(""),
15447
+ auditModel: z.string().default(""),
12087
15448
  reasoningEffort: z.union([
12088
15449
  "off",
12089
15450
  "low",
@@ -12143,6 +15504,9 @@ function resolveConfig(value) {
12143
15504
  outputDir: value?.outputDir ?? DEFAULT_OUTPUT_DIR,
12144
15505
  provider: value?.provider ?? DEFAULT_PROVIDER,
12145
15506
  model: value?.model ?? DEFAULT_MODEL,
15507
+ generateModel: value?.generateModel,
15508
+ reviewModel: value?.reviewModel,
15509
+ auditModel: value?.auditModel,
12146
15510
  reasoningEffort: value?.reasoningEffort ?? DEFAULT_REASONING_EFFORT,
12147
15511
  analysisReasoning: value?.analysisReasoning ?? DEFAULT_ANALYSIS_REASONING,
12148
15512
  chapterChars: value?.chapterChars ?? DEFAULT_CHAPTER_CHARS,
@@ -12200,6 +15564,9 @@ function apply(ctx, config) {
12200
15564
  if (patch.outputDir !== void 0) next.outputDir = patch.outputDir;
12201
15565
  if (patch.provider !== void 0) next.provider = patch.provider;
12202
15566
  if (patch.model !== void 0) next.model = patch.model;
15567
+ if (patch.generateModel !== void 0) next.generateModel = patch.generateModel;
15568
+ if (patch.reviewModel !== void 0) next.reviewModel = patch.reviewModel;
15569
+ if (patch.auditModel !== void 0) next.auditModel = patch.auditModel;
12203
15570
  if (patch.reasoningEffort !== void 0) next.reasoningEffort = patch.reasoningEffort;
12204
15571
  if (patch.analysisReasoning !== void 0) next.analysisReasoning = patch.analysisReasoning;
12205
15572
  if (patch.chapterChars !== void 0) next.chapterChars = patch.chapterChars;
@@ -12261,54 +15628,7 @@ function apply(ctx, config) {
12261
15628
  },
12262
15629
  onChange: sync
12263
15630
  });
12264
- const skillsService = ctx.get("skills");
12265
- if (skillsService?.register !== void 0) {
12266
- const disposeSkill = skillsService.register({
12267
- name: "novel-forge-chapter-batch",
12268
- provider: "dsh-novel-forge",
12269
- description: "批量生成小说章节并值守处理审稿未过的章节(豁免/按意见修订/验证模式/重新生成),依赖 dsh-novel-forge 的 /api/dsh-novel-forge/* 路由。",
12270
- content: [
12271
- "# 小说章节批量生产与值守处理",
12272
- "",
12273
- "## 0. 值守纪律(最高优先级,违反即空转)",
12274
- "1. 启动后台任务后立即停下,不轮询、不连续 wait、不做无意义动作——后台任务完成时运行时自动通知。",
12275
- "2. 只有收到「任务完成」通知或用户新消息时,才继续下一步(读结果→处理未过→汇报)。",
12276
- "3. 等待期间可做与当前任务无关的其他有用工作,但不得为等任务而空转。",
12277
- "4. 任务因 web 重启中断时:待 3080 恢复后,复位卡死章节(generating/error → reset)再续跑,并向用户说明中断原因。",
12278
- "",
12279
- "## 1. 前置检查",
12280
- "1. `GET /api/dsh-novel-forge/status` 确认服务在跑,读项目章节数。",
12281
- "2. 列出目标区间 pending 章节号,避免重复生成已处理章节。",
12282
- "3. **串行执行,禁止并行写 project.json**(并发会互相覆盖状态,实测教训)。",
12283
- "",
12284
- "## 2. 批量生成",
12285
- "逐章串行调用 `POST /generate` `{ chapterNo, skipReview: false }`(走完整质量门:生成→摘要+编年录→审稿→复盘)。",
12286
- "响应为 NDJSON,找 review 帧取 score/passed。每章 2-5 分钟;失败重试 1 次;后台跑并告知预计时长。",
12287
- "用 Node 脚本(fetch 逐章循环),避免 PowerShell 转义坑,用完删除。",
12288
- "",
12289
- "## 3. 未过章节分级处理",
12290
- "### A. 无 high → 豁免通过:`POST /chapter/approve`(主观项不磨)",
12291
- "### B. 有 high 且明确 → 按意见修订 + 验证模式:",
12292
- "1. 拼指令 `按审稿意见修订(优先处理):\n[severity] item → suggestion`(high 优先,无 high 取前 3 medium)",
12293
- "2. `POST /rewrite` 产草稿;3. `POST /chapter/check` 带 `previousReport` 走验证模式(只核对原意见解决+只挑新增 high)",
12294
- "4. 判定 `passed || 无 high` 可接受;5. `POST /draft/apply` 带 passed 报告落盘 approved",
12295
- "6. 不可接受 → 第二轮修订(指令更精确)→ 再验证;每章最多修 2 轮,仍不过保留草稿待人工",
12296
- "### C. 结构性 high(修订改不好)→ `POST /draft/discard` 后 `POST /generate` 重写,剩主观项再走 A",
12297
- "### D. error 状态 → `POST /chapter/reset` 后 `POST /generate`",
12298
- "注意:approved 但 review 有 high 的保留不动;修订指令越精确越有效(具体数字/行为一次就过)。",
12299
- "",
12300
- "## 4. 收尾",
12301
- "`GET /status` 验证目标区间全 approved;可选刷新剧情线(`POST /plotlines` op=refresh,注意只读最近 8 章摘要);汇报通过/未过/失败与遗留项。",
12302
- "",
12303
- "## 5. 已知陷阱",
12304
- "- 并发写 project.json 互相覆盖 → 必须串行",
12305
- "- rewrite 草稿可能偶发不落盘 → 应用前检查 pendingDraft,不存在则重跑",
12306
- "- PowerShell 调 API 引号/中文转义是坑 → 用 Node 脚本",
12307
- "- 验证模式 500 偶发 → 重试一次"
12308
- ].join("\n")
12309
- });
12310
- ctx.effect(() => disposeSkill, "dsh-novel-forge: skill");
12311
- }
15631
+ ctx.get("skills");
12312
15632
  sync();
12313
15633
  }
12314
15634
  //#endregion