novelforge-agent 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -12
- package/dist/src/cli/index.js +10 -2
- package/dist/src/core/contextBuilder.js +32 -0
- package/dist/src/core/projectDiscovery.js +1 -0
- package/dist/src/core/projectOps.js +7 -1
- package/dist/src/core/projectStore.js +4 -1
- package/dist/src/core/prompts/en-US.js +130 -3
- package/dist/src/core/prompts/zh-CN.js +130 -3
- package/dist/src/core/schemas.js +23 -0
- package/dist/src/core/steps/architectureExtension.js +72 -0
- package/dist/src/core/steps/chapterReview.js +2 -1
- package/dist/src/core/steps/index.js +4 -0
- package/dist/src/core/steps/memoryCard.js +22 -1
- package/dist/src/core/steps/storyBible.js +1 -1
- package/dist/src/core/steps/styleGuide.js +12 -0
- package/dist/src/core/workflow.js +2 -0
- package/dist/src/mcp/tools.js +36 -8
- package/package.json +1 -1
- package/src/cli/index.ts +11 -3
- package/src/core/contextBuilder.ts +30 -0
- package/src/core/projectDiscovery.ts +2 -0
- package/src/core/projectOps.ts +9 -1
- package/src/core/projectStore.ts +8 -1
- package/src/core/prompts/en-US.ts +132 -3
- package/src/core/prompts/types.ts +2 -0
- package/src/core/prompts/zh-CN.ts +132 -3
- package/src/core/schemas.ts +25 -0
- package/src/core/steps/architectureExtension.ts +88 -0
- package/src/core/steps/chapterReview.ts +2 -1
- package/src/core/steps/index.ts +4 -0
- package/src/core/steps/memoryCard.ts +23 -1
- package/src/core/steps/storyBible.ts +1 -1
- package/src/core/steps/styleGuide.ts +13 -0
- package/src/core/types.ts +32 -0
- package/src/core/workflow.ts +2 -0
- package/src/mcp/tools.ts +35 -8
|
@@ -79,6 +79,56 @@ Rules:
|
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
function buildStyleGuidePrompt(input: PromptBuildInput): BuiltPrompt {
|
|
83
|
+
return {
|
|
84
|
+
purpose: 'style_guide',
|
|
85
|
+
expectedFormat: 'JSON matching StyleGuideSchema',
|
|
86
|
+
prompt: `You are the style editor for a long-form novel. From the user prompt, metadata, and story bible, create a style guide that chapter writing and review can enforce over the whole project.
|
|
87
|
+
|
|
88
|
+
## User Prompt
|
|
89
|
+
${input.state.initialPrompt}
|
|
90
|
+
|
|
91
|
+
${input.context ? `## Existing Context\n${input.context}\n` : ''}## Output Requirements
|
|
92
|
+
Output valid JSON only, in this shape:
|
|
93
|
+
{
|
|
94
|
+
"narrativeVoice": "Narration person, POV distance, narrator texture, emotional temperature",
|
|
95
|
+
"pacing": "Rules for openings, transitions, conflict movement, and chapter-end hooks",
|
|
96
|
+
"diction": "Word choice, sentence density, genre terminology boundaries",
|
|
97
|
+
"dialogueRules": [
|
|
98
|
+
"Rules for core character dialogue length, tone, subtext, and forms of address"
|
|
99
|
+
],
|
|
100
|
+
"prohibitedPatterns": [
|
|
101
|
+
"Patterns to avoid: modern memes, explanatory narration, lore dumping, voice drift, etc."
|
|
102
|
+
],
|
|
103
|
+
"proseRhythm": {
|
|
104
|
+
"sentenceRhythm": "How short, medium, and long sentences should be used; short sentences should serve turns, danger, or emotional landings, not default narration",
|
|
105
|
+
"paragraphing": "Paragraphs should form complete narrative units; avoid consecutive one-sentence paragraphs and line breaks used as fake rhythm",
|
|
106
|
+
"interiorityMode": "How interiority should be refracted through action, hesitation, and sensory response; avoid frequent direct explanation of thoughts",
|
|
107
|
+
"emphasisBudget": "Budget for repetition, dashes, isolated short sentences, and other emphasis tools",
|
|
108
|
+
"antiPatterns": [
|
|
109
|
+
"3 or more consecutive one-sentence short paragraphs",
|
|
110
|
+
"many short sentences used to simulate tension",
|
|
111
|
+
"explaining psychology immediately after every action",
|
|
112
|
+
"repeating the same sentence pattern to create fake rhythm"
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
"sampleParagraph": "A 120-250 word target-style sample. Do not turn it into plot outline.",
|
|
116
|
+
"consistencyChecks": [
|
|
117
|
+
"Concrete checks future chapter reviews should use to detect style drift"
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
Rules:
|
|
122
|
+
- Match genre, premise, character identities, and reader expectations.
|
|
123
|
+
- Do not rely on abstract adjectives only; every field must guide actual prose.
|
|
124
|
+
- proseRhythm must not be fixed word-count rules; describe reviewable rhythm principles and anti-patterns.
|
|
125
|
+
- sampleParagraph demonstrates prose texture only. Do not reveal future plot.
|
|
126
|
+
- prohibitedPatterns must contain at least 3 entries; consistencyChecks must contain at least 3 entries.
|
|
127
|
+
- proseRhythm.antiPatterns must contain at least 4 entries.
|
|
128
|
+
${strictJsonOutputRules()}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
82
132
|
function buildArchitecturePrompt(input: PromptBuildInput): BuiltPrompt {
|
|
83
133
|
return {
|
|
84
134
|
purpose: 'architecture',
|
|
@@ -89,7 +139,7 @@ return {
|
|
|
89
139
|
${input.state.initialPrompt}
|
|
90
140
|
|
|
91
141
|
## Goals
|
|
92
|
-
-
|
|
142
|
+
- Whole-book target is about ${input.state.plannedTotalChapters ?? input.state.targetChapters} chapters; generate only the first ${input.state.targetChapters} chapter architectures in this first batch.
|
|
93
143
|
- The full-book architecture should define the long-term main line and ending direction.
|
|
94
144
|
- Volume architecture should define phase conflict, climax, and volume-end hooks.
|
|
95
145
|
- Chapter architecture must cover only what should happen in that chapter and must not reveal later concrete events early.
|
|
@@ -131,6 +181,7 @@ Output valid JSON only, in this shape:
|
|
|
131
181
|
|
|
132
182
|
Rules:
|
|
133
183
|
- chapters.length must be at least ${input.state.targetChapters}.
|
|
184
|
+
- chapters do not need to cover the whole book; when writing reaches the boundary, the workflow will request architecture_extension.
|
|
134
185
|
- chapterNumber must start at 1 and increase contiguously.
|
|
135
186
|
- volumeId must reference an id from volumes.
|
|
136
187
|
- volumePacing must provide one pacing board for every volume.
|
|
@@ -139,6 +190,72 @@ ${strictJsonOutputRules()}`,
|
|
|
139
190
|
};
|
|
140
191
|
}
|
|
141
192
|
|
|
193
|
+
function buildArchitectureExtensionPrompt(input: PromptBuildInput): BuiltPrompt {
|
|
194
|
+
const start = input.state.currentChapter;
|
|
195
|
+
const total = input.state.plannedTotalChapters ?? input.state.targetChapters;
|
|
196
|
+
const end = Math.min(total, start + input.state.targetChapters - 1);
|
|
197
|
+
return {
|
|
198
|
+
purpose: 'architecture_extension',
|
|
199
|
+
expectedFormat: 'JSON matching ArchitectureExtensionPayloadSchema',
|
|
200
|
+
prompt: `You are the chief architect for a long-form novel. The manuscript has reached the edge of the existing chapter plan; extend the architecture from current continuity.
|
|
201
|
+
|
|
202
|
+
## Extension Range
|
|
203
|
+
- Start at chapter ${start}.
|
|
204
|
+
- This batch should plan through chapter ${end} at most.
|
|
205
|
+
- The whole-book target ends at chapter ${total}.
|
|
206
|
+
|
|
207
|
+
## Extension Principles
|
|
208
|
+
- Do not rewrite existing chapter architecture; append only new chapter architecture.
|
|
209
|
+
- New chapters must follow recent memory, the character state table, active foreshadow threads, and volume pacing boards.
|
|
210
|
+
- If the next chapters enter a new volume, add volumes and volumePacing. If they remain in an existing volume, you may provide an updated pacing board for that volume.
|
|
211
|
+
- If the full-book direction needs adjustment because of written material, include fullUpdate. fullUpdate must be a complete replacement for architecture/full.md, not a change note.
|
|
212
|
+
- Chapter architecture must cover only what should happen in that chapter and must not reveal later concrete events early.
|
|
213
|
+
|
|
214
|
+
${input.context ? `## Existing Context\n${input.context}\n` : ''}## Output Requirements
|
|
215
|
+
Output valid JSON only, in this shape:
|
|
216
|
+
{
|
|
217
|
+
"fullUpdate": "optional complete updated full-book architecture",
|
|
218
|
+
"volumes": [
|
|
219
|
+
{
|
|
220
|
+
"id": "v2",
|
|
221
|
+
"title": "New or updated volume title",
|
|
222
|
+
"summary": "Volume goal, conflict, climax, and end hook",
|
|
223
|
+
"order": 2
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
"volumePacing": [
|
|
227
|
+
{
|
|
228
|
+
"volumeId": "v2",
|
|
229
|
+
"start": "Volume starting state",
|
|
230
|
+
"promise": "Volume promise",
|
|
231
|
+
"keyTurns": ["Key turn 1", "Key turn 2"],
|
|
232
|
+
"midpoint": "Midpoint turn",
|
|
233
|
+
"climax": "Volume climax",
|
|
234
|
+
"payoffs": ["Planned payoffs"],
|
|
235
|
+
"lingeringMysteries": ["Lingering mysteries"]
|
|
236
|
+
}
|
|
237
|
+
],
|
|
238
|
+
"chapters": [
|
|
239
|
+
{
|
|
240
|
+
"chapterNumber": ${start},
|
|
241
|
+
"title": "Chapter title",
|
|
242
|
+
"volumeId": "v1",
|
|
243
|
+
"summary": "Chapter plot summary",
|
|
244
|
+
"requiredBeats": ["Required beat 1"]
|
|
245
|
+
}
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
Rules:
|
|
250
|
+
- chapters[0].chapterNumber must equal ${start}.
|
|
251
|
+
- chapterNumber must increase contiguously and must not exceed ${total}.
|
|
252
|
+
- chapters.length should be ${end - start + 1} unless the book has reached its ending.
|
|
253
|
+
- requiredBeats must include at least one concrete, actionable beat.
|
|
254
|
+
- volumeId must reference an existing volume id or a volume id supplied in this response.
|
|
255
|
+
${strictJsonOutputRules()}`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
142
259
|
function buildChapterPrompt(input: PromptBuildInput): BuiltPrompt {
|
|
143
260
|
const ch = input.state.currentChapter;
|
|
144
261
|
const isFirstChapter = ch <= 1;
|
|
@@ -148,7 +265,7 @@ function buildChapterPrompt(input: PromptBuildInput): BuiltPrompt {
|
|
|
148
265
|
prompt: `You are a professional long-form fiction writer. Write chapter ${ch} directly.
|
|
149
266
|
|
|
150
267
|
## Priority Order
|
|
151
|
-
1. Strictly follow the current chapter architecture, user additions, story bible hard constraints, and previous-chapter continuity.
|
|
268
|
+
1. Strictly follow the current chapter architecture, user additions, story bible hard constraints, style guide, and previous-chapter continuity.
|
|
152
269
|
2. Use relevant memory, prior text evidence, and active foreshadow threads.
|
|
153
270
|
3. Treat full-book and volume plans as distant planning context only. Do not write concrete future events early.
|
|
154
271
|
|
|
@@ -163,6 +280,8 @@ ${isFirstChapter
|
|
|
163
280
|
- The chapter must end on a clear hook: cliffhanger, mystery, emotional resonance, reveal, or volume close — per the chapter architecture endHookFocus. Default: cliffhanger.
|
|
164
281
|
|
|
165
282
|
## Style
|
|
283
|
+
- Enforce the Style Guide from context. Treat sampleParagraph as prose texture only; do not copy its content.
|
|
284
|
+
- Enforce Style Guide.proseRhythm: short sentences, one-line paragraphs, repeated sentences, and dashes are emphasis tools, not default narration. Ordinary narration should form natural sentence groups.
|
|
166
285
|
- Match the novel's genre, world, character identities, and emotional tone.
|
|
167
286
|
- Natural, stable, readable language; prioritize narrative progress, character work, and emotional accumulation.
|
|
168
287
|
- Dialogue fits each character's identity, relationship, and situation.
|
|
@@ -258,7 +377,7 @@ ${strictJsonOutputRules()}`,
|
|
|
258
377
|
}
|
|
259
378
|
|
|
260
379
|
function buildContinuityReviewPrompt(input: PromptBuildInput): BuiltPrompt {
|
|
261
|
-
const end = Math.max(input.state.targetChapters, input.state.currentChapter - 1);
|
|
380
|
+
const end = Math.max(input.state.plannedTotalChapters ?? input.state.targetChapters, input.state.currentChapter - 1);
|
|
262
381
|
return {
|
|
263
382
|
purpose: 'continuity_review',
|
|
264
383
|
expectedFormat: 'JSON matching ContinuityReviewSchema',
|
|
@@ -308,6 +427,8 @@ ${input.context ? `## Review Context\n${input.context}\n` : ''}## Review Focus
|
|
|
308
427
|
- Whether every requiredBeat is fulfilled; missing beats must appear in acceptance.requiredBeats.missingBeats.
|
|
309
428
|
- Whether this chapter advances the main line, character state, or active foreshadow threads. If it is static, at least one of narrativeProgress/characterProgress/foreshadowProgress must fail.
|
|
310
429
|
- Whether it violates the story bible, character state table, volume pacing board, or prior memory.
|
|
430
|
+
- Whether it violates the Style Guide: narrative voice, sentence density, genre diction, dialogue rules, or prohibited patterns.
|
|
431
|
+
- Whether it violates Style Guide.proseRhythm: excessive short-sentence density, consecutive one-sentence paragraphs, fake rhythm through line breaks, overly direct interior explanation, or repeated sentence patterns.
|
|
311
432
|
- Whether the ending has a clear hook that matches the chapter architecture endHookFocus.
|
|
312
433
|
- Whether it repeats prior chapter beats, conflict patterns, reveals, or dialogue functions.
|
|
313
434
|
- Character voice, motivation, and state vs the story bible and prior memory.
|
|
@@ -343,6 +464,10 @@ Output valid JSON only, in this shape:
|
|
|
343
464
|
"status": "pass | fail",
|
|
344
465
|
"evidence": "Whether it matches the story bible, character state table, and world rules"
|
|
345
466
|
},
|
|
467
|
+
"proseRhythm": {
|
|
468
|
+
"status": "pass | fail",
|
|
469
|
+
"evidence": "Whether it follows Style Guide.proseRhythm; explain whether short sentences, one-line paragraphs, repetition, and interior explanation are controlled"
|
|
470
|
+
},
|
|
346
471
|
"endingHook": {
|
|
347
472
|
"status": "pass | fail",
|
|
348
473
|
"evidence": "The ending hook passage and its function"
|
|
@@ -440,8 +565,12 @@ function buildPromptForStep(input: PromptBuildInput): BuiltPrompt {
|
|
|
440
565
|
return buildMetadataPrompt(input);
|
|
441
566
|
case 'story_bible':
|
|
442
567
|
return buildStoryBiblePrompt(input);
|
|
568
|
+
case 'style_guide':
|
|
569
|
+
return buildStyleGuidePrompt(input);
|
|
443
570
|
case 'architecture':
|
|
444
571
|
return buildArchitecturePrompt(input);
|
|
572
|
+
case 'architecture_extension':
|
|
573
|
+
return buildArchitectureExtensionPrompt(input);
|
|
445
574
|
case 'chapter':
|
|
446
575
|
return buildChapterPrompt(input);
|
|
447
576
|
case 'memory_card':
|
|
@@ -79,6 +79,56 @@ ${input.context ? `## 已有上下文\n${input.context}\n` : ''}## 输出结构
|
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
function buildStyleGuidePrompt(input: PromptBuildInput): BuiltPrompt {
|
|
83
|
+
return {
|
|
84
|
+
purpose: 'style_guide',
|
|
85
|
+
expectedFormat: 'JSON matching StyleGuideSchema',
|
|
86
|
+
prompt: `你是一名长篇小说文风主编。请根据用户提示、metadata 和故事圣经,生成可被每章写作与审稿长期执行的风格圣经。
|
|
87
|
+
|
|
88
|
+
## 用户提示词
|
|
89
|
+
${input.state.initialPrompt}
|
|
90
|
+
|
|
91
|
+
${input.context ? `## 已有上下文\n${input.context}\n` : ''}## 输出要求
|
|
92
|
+
请只输出合法 JSON,格式如下:
|
|
93
|
+
{
|
|
94
|
+
"narrativeVoice": "叙事人称、视角距离、旁白气质、情绪温度",
|
|
95
|
+
"pacing": "开章、转场、冲突推进、章末钩子的节奏规则",
|
|
96
|
+
"diction": "词汇选择、句式密度、题材术语使用边界",
|
|
97
|
+
"dialogueRules": [
|
|
98
|
+
"核心人物对白的长度、语气、潜台词、称谓规则"
|
|
99
|
+
],
|
|
100
|
+
"prohibitedPatterns": [
|
|
101
|
+
"禁止出现的现代梗、解释型旁白、设定堆砌、口吻漂移等"
|
|
102
|
+
],
|
|
103
|
+
"proseRhythm": {
|
|
104
|
+
"sentenceRhythm": "短句、中句、长句的使用原则;短句应服务转折、危险、情绪落点,而不是默认叙述单位",
|
|
105
|
+
"paragraphing": "段落应形成完整叙事单元;避免连续单句成段、靠频繁换行制造伪节奏",
|
|
106
|
+
"interiorityMode": "心理活动如何通过动作、迟疑、感官反应折射;避免频繁直接解释人物想法",
|
|
107
|
+
"emphasisBudget": "重复句、破折号、孤立短句等强调资源的使用预算",
|
|
108
|
+
"antiPatterns": [
|
|
109
|
+
"连续 3 个以上单句短段",
|
|
110
|
+
"用大量短句模拟紧张感",
|
|
111
|
+
"每个动作后立刻解释心理",
|
|
112
|
+
"重复同一句式制造伪节奏"
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
"sampleParagraph": "一段 120-250 字的目标风格示例,不要写成剧情大纲",
|
|
116
|
+
"consistencyChecks": [
|
|
117
|
+
"后续章节审稿时用于判断风格是否跑偏的具体检查项"
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
要求:
|
|
122
|
+
- 风格必须匹配 genre、premise、人物身份和目标读者预期。
|
|
123
|
+
- 不要只写抽象形容词;每个字段都要能指导实际行文。
|
|
124
|
+
- proseRhythm 不要写成固定字数规则;要描述可审稿的节奏原则和反模式。
|
|
125
|
+
- sampleParagraph 只展示语言质感,不要提前泄露后续剧情。
|
|
126
|
+
- prohibitedPatterns 至少 3 条,consistencyChecks 至少 3 条。
|
|
127
|
+
- proseRhythm.antiPatterns 至少 4 条。
|
|
128
|
+
${strictJsonOutputRules()}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
82
132
|
function buildArchitecturePrompt(input: PromptBuildInput): BuiltPrompt {
|
|
83
133
|
return {
|
|
84
134
|
purpose: 'architecture',
|
|
@@ -89,7 +139,7 @@ return {
|
|
|
89
139
|
${input.state.initialPrompt}
|
|
90
140
|
|
|
91
141
|
## 目标
|
|
92
|
-
-
|
|
142
|
+
- 全本目标约 ${input.state.plannedTotalChapters ?? input.state.targetChapters} 章;本次只生成首批 ${input.state.targetChapters} 个章架构。
|
|
93
143
|
- 全本架构负责长期主线和结局方向。
|
|
94
144
|
- 卷架构负责阶段冲突、高潮和卷尾钩子。
|
|
95
145
|
- 章架构必须只覆盖本章应发生的内容,不要提前泄露后续具体事件。
|
|
@@ -131,6 +181,7 @@ ${input.context ? `## 已有上下文\n${input.context}\n` : ''}## 输出要求
|
|
|
131
181
|
|
|
132
182
|
要求:
|
|
133
183
|
- chapters.length 必须大于等于 ${input.state.targetChapters}。
|
|
184
|
+
- chapters 不需要一次覆盖全本;后续写到边界时会进入 architecture_extension 续规划。
|
|
134
185
|
- chapterNumber 从 1 开始连续递增。
|
|
135
186
|
- volumeId 必须引用 volumes 中存在的 id。
|
|
136
187
|
- volumePacing 必须为每个 volume 提供节奏板。
|
|
@@ -139,6 +190,72 @@ ${strictJsonOutputRules()}`,
|
|
|
139
190
|
};
|
|
140
191
|
}
|
|
141
192
|
|
|
193
|
+
function buildArchitectureExtensionPrompt(input: PromptBuildInput): BuiltPrompt {
|
|
194
|
+
const start = input.state.currentChapter;
|
|
195
|
+
const total = input.state.plannedTotalChapters ?? input.state.targetChapters;
|
|
196
|
+
const end = Math.min(total, start + input.state.targetChapters - 1);
|
|
197
|
+
return {
|
|
198
|
+
purpose: 'architecture_extension',
|
|
199
|
+
expectedFormat: 'JSON matching ArchitectureExtensionPayloadSchema',
|
|
200
|
+
prompt: `你是一名长篇小说总架构师。当前已写到既有章纲边界,请基于已有内容续写后续章架构。
|
|
201
|
+
|
|
202
|
+
## 续规划范围
|
|
203
|
+
- 从第 ${start} 章开始。
|
|
204
|
+
- 本批最多规划到第 ${end} 章。
|
|
205
|
+
- 全本目标到第 ${total} 章结束。
|
|
206
|
+
|
|
207
|
+
## 续规划原则
|
|
208
|
+
- 不要改写已经存在的章节架构;只追加新的 chapter architecture。
|
|
209
|
+
- 新增章节必须承接最近记忆、角色状态表、活跃伏笔和卷级节奏板。
|
|
210
|
+
- 如果后续章节进入新卷,可以新增 volumes 和 volumePacing;如果仍在旧卷,可以补充/更新该卷节奏板。
|
|
211
|
+
- 如果全本方向因已写内容需要微调,可以输出 fullUpdate;fullUpdate 必须是可覆盖 architecture/full.md 的完整更新版,而不是变更说明。
|
|
212
|
+
- 章架构必须只覆盖本章应发生的内容,不要提前泄露更后面的具体事件。
|
|
213
|
+
|
|
214
|
+
${input.context ? `## 已有上下文\n${input.context}\n` : ''}## 输出要求
|
|
215
|
+
请只输出合法 JSON,格式如下:
|
|
216
|
+
{
|
|
217
|
+
"fullUpdate": "可选:完整更新后的全本架构 Markdown/文本",
|
|
218
|
+
"volumes": [
|
|
219
|
+
{
|
|
220
|
+
"id": "v2",
|
|
221
|
+
"title": "新增或更新卷标题",
|
|
222
|
+
"summary": "本卷目标、冲突、高潮和卷尾钩子",
|
|
223
|
+
"order": 2
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
"volumePacing": [
|
|
227
|
+
{
|
|
228
|
+
"volumeId": "v2",
|
|
229
|
+
"start": "本卷起点",
|
|
230
|
+
"promise": "本卷承诺",
|
|
231
|
+
"keyTurns": ["关键转折1", "关键转折2"],
|
|
232
|
+
"midpoint": "中点转折",
|
|
233
|
+
"climax": "本卷高潮",
|
|
234
|
+
"payoffs": ["计划回收点"],
|
|
235
|
+
"lingeringMysteries": ["遗留悬念"]
|
|
236
|
+
}
|
|
237
|
+
],
|
|
238
|
+
"chapters": [
|
|
239
|
+
{
|
|
240
|
+
"chapterNumber": ${start},
|
|
241
|
+
"title": "章标题",
|
|
242
|
+
"volumeId": "v1",
|
|
243
|
+
"summary": "本章剧情摘要",
|
|
244
|
+
"requiredBeats": ["必须完成的情节点1"]
|
|
245
|
+
}
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
要求:
|
|
250
|
+
- chapters[0].chapterNumber 必须等于 ${start}。
|
|
251
|
+
- chapterNumber 必须连续递增,且不能超过 ${total}。
|
|
252
|
+
- chapters.length 建议为 ${end - start + 1},除非已经到全本结尾。
|
|
253
|
+
- requiredBeats 至少 1 条,且必须具体可执行。
|
|
254
|
+
- volumeId 必须引用已有或本次新增 volumes 中存在的 id。
|
|
255
|
+
${strictJsonOutputRules()}`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
142
259
|
function buildChapterPrompt(input: PromptBuildInput): BuiltPrompt {
|
|
143
260
|
const ch = input.state.currentChapter;
|
|
144
261
|
const isFirstChapter = ch <= 1;
|
|
@@ -148,7 +265,7 @@ function buildChapterPrompt(input: PromptBuildInput): BuiltPrompt {
|
|
|
148
265
|
prompt: `你是一位擅长创作长篇网络小说的职业作者。请直接完成第 ${ch} 章正文。
|
|
149
266
|
|
|
150
267
|
## 执行优先级
|
|
151
|
-
1. 先严格遵守"
|
|
268
|
+
1. 先严格遵守"本章架构、用户补充要求、故事圣经硬约束、风格圣经、上一章承接"。
|
|
152
269
|
2. 再参考"历史相关记忆、历史原文证据、活跃伏笔"保证一致性。
|
|
153
270
|
3. 最后才参考"全本/本卷远场规划",且不得提前写出尚未发生的情节。
|
|
154
271
|
|
|
@@ -163,6 +280,8 @@ ${isFirstChapter
|
|
|
163
280
|
- 章末必须有清晰的"钩子":可以是悬念、反转、剧情承诺、情绪余韵或卷末高潮——按本章架构 endHookFocus 字段决定。如果未指定,默认用悬念。
|
|
164
281
|
|
|
165
282
|
## 风格
|
|
283
|
+
- 严格执行上下文中的 Style Guide;sampleParagraph 只作为语言质感参考,不要复写其内容。
|
|
284
|
+
- 严格执行 Style Guide.proseRhythm:短句、单句段、重复句、破折号是强调资源,不是默认叙述单位;常规叙述要形成自然句群。
|
|
166
285
|
- 文风必须与本书题材、世界观、人物身份、情感基调一致。
|
|
167
286
|
- 语言自然、稳定、可读,优先服务叙事推进、人物塑造和情绪积累。
|
|
168
287
|
- 对话符合人物身份、关系和处境;重要情绪通过动作、神态、节奏、潜台词体现。
|
|
@@ -257,7 +376,7 @@ ${strictJsonOutputRules()}`,
|
|
|
257
376
|
}
|
|
258
377
|
|
|
259
378
|
function buildContinuityReviewPrompt(input: PromptBuildInput): BuiltPrompt {
|
|
260
|
-
const end = Math.max(input.state.targetChapters, input.state.currentChapter - 1);
|
|
379
|
+
const end = Math.max(input.state.plannedTotalChapters ?? input.state.targetChapters, input.state.currentChapter - 1);
|
|
261
380
|
return {
|
|
262
381
|
purpose: 'continuity_review',
|
|
263
382
|
expectedFormat: 'JSON matching ContinuityReviewSchema',
|
|
@@ -307,6 +426,8 @@ ${input.context ? `## 审阅上下文\n${input.context}\n` : ''}## 审阅重点
|
|
|
307
426
|
- requiredBeats 是否全部完成;缺失项必须写入 acceptance.requiredBeats.missingBeats。
|
|
308
427
|
- 本章是否推进主线、人物状态或活跃伏笔;如果完全原地踏步,narrativeProgress/characterProgress/foreshadowProgress 至少一项必须 fail。
|
|
309
428
|
- 是否违反故事圣经、角色状态表、卷级节奏板或历史记忆。
|
|
429
|
+
- 是否违反 Style Guide:叙事声音、句式密度、题材词汇、对白规则和禁用模式。
|
|
430
|
+
- 是否违反 Style Guide.proseRhythm:短句密度过高、连续单句短段、靠换行制造伪节奏、心理解释过直白、重复同一句式。
|
|
310
431
|
- 章末是否有清晰钩子,且符合本章 endHookFocus。
|
|
311
432
|
- 是否重复之前章节已经完成的桥段、冲突结构、信息揭示或对话功能。
|
|
312
433
|
- 人物声音、动机、状态是否符合故事圣经与历史记忆。
|
|
@@ -342,6 +463,10 @@ ${input.context ? `## 审阅上下文\n${input.context}\n` : ''}## 审阅重点
|
|
|
342
463
|
"status": "pass | fail",
|
|
343
464
|
"evidence": "是否符合故事圣经、角色状态表和世界规则"
|
|
344
465
|
},
|
|
466
|
+
"proseRhythm": {
|
|
467
|
+
"status": "pass | fail",
|
|
468
|
+
"evidence": "是否符合 Style Guide.proseRhythm;说明短句/单句段/重复句/心理解释是否被合理控制"
|
|
469
|
+
},
|
|
345
470
|
"endingHook": {
|
|
346
471
|
"status": "pass | fail",
|
|
347
472
|
"evidence": "章末钩子的具体段落和作用"
|
|
@@ -439,8 +564,12 @@ function buildPromptForStep(input: PromptBuildInput): BuiltPrompt {
|
|
|
439
564
|
return buildMetadataPrompt(input);
|
|
440
565
|
case 'story_bible':
|
|
441
566
|
return buildStoryBiblePrompt(input);
|
|
567
|
+
case 'style_guide':
|
|
568
|
+
return buildStyleGuidePrompt(input);
|
|
442
569
|
case 'architecture':
|
|
443
570
|
return buildArchitecturePrompt(input);
|
|
571
|
+
case 'architecture_extension':
|
|
572
|
+
return buildArchitectureExtensionPrompt(input);
|
|
444
573
|
case 'chapter':
|
|
445
574
|
return buildChapterPrompt(input);
|
|
446
575
|
case 'memory_card':
|
package/src/core/schemas.ts
CHANGED
|
@@ -15,6 +15,23 @@ export const NovelMetadataSchema = z.object({
|
|
|
15
15
|
coreCast: z.array(CoreCastMemberSchema).min(1),
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
+
export const StyleGuideSchema = z.object({
|
|
19
|
+
narrativeVoice: z.string().min(1),
|
|
20
|
+
pacing: z.string().min(1),
|
|
21
|
+
diction: z.string().min(1),
|
|
22
|
+
dialogueRules: z.array(z.string().min(1)).min(1),
|
|
23
|
+
prohibitedPatterns: z.array(z.string().min(1)).min(1),
|
|
24
|
+
proseRhythm: z.object({
|
|
25
|
+
sentenceRhythm: z.string().min(1),
|
|
26
|
+
paragraphing: z.string().min(1),
|
|
27
|
+
interiorityMode: z.string().min(1),
|
|
28
|
+
emphasisBudget: z.string().min(1),
|
|
29
|
+
antiPatterns: z.array(z.string().min(1)).min(1),
|
|
30
|
+
}),
|
|
31
|
+
sampleParagraph: z.string().min(1),
|
|
32
|
+
consistencyChecks: z.array(z.string().min(1)).min(1),
|
|
33
|
+
});
|
|
34
|
+
|
|
18
35
|
export const VolumeArchitectureSchema = z.object({
|
|
19
36
|
id: z.string().min(1),
|
|
20
37
|
title: z.string().min(1),
|
|
@@ -61,6 +78,13 @@ export const ArchitecturePayloadSchema = z.object({
|
|
|
61
78
|
chapters: z.array(ChapterArchitectureSchema).min(1),
|
|
62
79
|
});
|
|
63
80
|
|
|
81
|
+
export const ArchitectureExtensionPayloadSchema = z.object({
|
|
82
|
+
fullUpdate: z.string().min(1).optional(),
|
|
83
|
+
volumes: z.array(VolumeArchitectureSchema).optional(),
|
|
84
|
+
volumePacing: z.array(VolumePacingBoardSchema).optional(),
|
|
85
|
+
chapters: z.array(ChapterArchitectureSchema).min(1),
|
|
86
|
+
});
|
|
87
|
+
|
|
64
88
|
export const ThreadActionSchema = z.object({
|
|
65
89
|
kind: z.enum(['plant', 'build', 'pay', 'drop']),
|
|
66
90
|
threadId: z.string().min(1).optional(),
|
|
@@ -181,6 +205,7 @@ export const ChapterAcceptanceGateSchema = z.object({
|
|
|
181
205
|
characterProgress: ChapterAcceptanceCheckSchema,
|
|
182
206
|
foreshadowProgress: ChapterAcceptanceCheckSchema,
|
|
183
207
|
storyBibleConsistency: ChapterAcceptanceCheckSchema,
|
|
208
|
+
proseRhythm: ChapterAcceptanceCheckSchema,
|
|
184
209
|
endingHook: ChapterAcceptanceCheckSchema,
|
|
185
210
|
repetition: ChapterAcceptanceCheckSchema,
|
|
186
211
|
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { ArchitectureExtensionPayloadSchema } from '../schemas.js';
|
|
4
|
+
import {
|
|
5
|
+
AgentState,
|
|
6
|
+
ChapterArchitecture,
|
|
7
|
+
VolumeArchitecture,
|
|
8
|
+
VolumePacingBoard,
|
|
9
|
+
} from '../types.js';
|
|
10
|
+
import { saveJsonFile, saveMarkdownFile } from '../projectStore.js';
|
|
11
|
+
import { StepHandler, parseJson } from './types.js';
|
|
12
|
+
|
|
13
|
+
async function readJsonArray<T>(projectPath: string, relativePath: string): Promise<T[]> {
|
|
14
|
+
try {
|
|
15
|
+
const raw = await readFile(join(projectPath, relativePath), 'utf8');
|
|
16
|
+
const parsed = JSON.parse(raw);
|
|
17
|
+
return Array.isArray(parsed) ? parsed as T[] : [];
|
|
18
|
+
} catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function assertContiguousExtension(
|
|
24
|
+
state: AgentState,
|
|
25
|
+
existing: ChapterArchitecture[],
|
|
26
|
+
nextChapters: ChapterArchitecture[]
|
|
27
|
+
): void {
|
|
28
|
+
const expectedStart = state.currentChapter;
|
|
29
|
+
const total = state.plannedTotalChapters ?? state.targetChapters;
|
|
30
|
+
const existingNumbers = new Set(existing.map((chapter) => chapter.chapterNumber));
|
|
31
|
+
|
|
32
|
+
for (let index = 0; index < nextChapters.length; index += 1) {
|
|
33
|
+
const chapter = nextChapters[index];
|
|
34
|
+
const expected = expectedStart + index;
|
|
35
|
+
if (chapter.chapterNumber !== expected) {
|
|
36
|
+
throw new Error(`architecture_extension chapters must start at chapter ${expectedStart} and be contiguous; expected ${expected}, got ${chapter.chapterNumber}`);
|
|
37
|
+
}
|
|
38
|
+
if (chapter.chapterNumber > total) {
|
|
39
|
+
throw new Error(`architecture_extension chapter ${chapter.chapterNumber} exceeds plannedTotalChapters ${total}`);
|
|
40
|
+
}
|
|
41
|
+
if (existingNumbers.has(chapter.chapterNumber)) {
|
|
42
|
+
throw new Error(`architecture_extension cannot overwrite existing chapter architecture ${chapter.chapterNumber}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mergeByKey<T>(existing: T[], incoming: T[] | undefined, keyOf: (item: T) => string): T[] {
|
|
48
|
+
const map = new Map<string, T>();
|
|
49
|
+
for (const item of existing) map.set(keyOf(item), item);
|
|
50
|
+
for (const item of incoming ?? []) map.set(keyOf(item), item);
|
|
51
|
+
return [...map.values()];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const architectureExtensionHandler: StepHandler = async (state, content) => {
|
|
55
|
+
const parsed = ArchitectureExtensionPayloadSchema.parse(parseJson(content));
|
|
56
|
+
const existingChapters = await readJsonArray<ChapterArchitecture>(state.projectPath, 'architecture/chapters.json');
|
|
57
|
+
assertContiguousExtension(state, existingChapters, parsed.chapters);
|
|
58
|
+
|
|
59
|
+
const chapters = [...existingChapters, ...parsed.chapters]
|
|
60
|
+
.sort((a, b) => a.chapterNumber - b.chapterNumber);
|
|
61
|
+
const existingVolumes = await readJsonArray<VolumeArchitecture>(state.projectPath, 'architecture/volumes.json');
|
|
62
|
+
const volumes = mergeByKey(existingVolumes, parsed.volumes, (volume) => volume.id)
|
|
63
|
+
.sort((a, b) => a.order - b.order);
|
|
64
|
+
const existingPacing = await readJsonArray<VolumePacingBoard>(state.projectPath, 'architecture/volume-pacing.json');
|
|
65
|
+
const volumePacing = mergeByKey(existingPacing, parsed.volumePacing, (board) => board.volumeId);
|
|
66
|
+
|
|
67
|
+
const savedPaths = [
|
|
68
|
+
await saveJsonFile(state.projectPath, 'architecture/chapters.json', chapters),
|
|
69
|
+
await saveJsonFile(state.projectPath, 'architecture/volumes.json', volumes),
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const hasVolumePacing = Boolean(parsed.volumePacing || existingPacing.length);
|
|
73
|
+
if (hasVolumePacing) {
|
|
74
|
+
savedPaths.push(await saveJsonFile(state.projectPath, 'architecture/volume-pacing.json', volumePacing));
|
|
75
|
+
}
|
|
76
|
+
if (parsed.fullUpdate) {
|
|
77
|
+
savedPaths.push(await saveMarkdownFile(state.projectPath, 'architecture/full.md', parsed.fullUpdate));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
savedPaths,
|
|
82
|
+
fileEntries: {
|
|
83
|
+
architecture: 'architecture/chapters.json',
|
|
84
|
+
...(hasVolumePacing ? { volumePacing: 'architecture/volume-pacing.json' } : {}),
|
|
85
|
+
},
|
|
86
|
+
next: { kind: 'linear', nextStep: 'chapter' },
|
|
87
|
+
};
|
|
88
|
+
};
|
|
@@ -6,6 +6,7 @@ import { StepHandler, parseJson } from './types.js';
|
|
|
6
6
|
|
|
7
7
|
export const chapterReviewHandler: StepHandler = async (state, content) => {
|
|
8
8
|
const parsed = ChapterReviewSchema.parse(parseJson(content));
|
|
9
|
+
const hasFailedAcceptance = Object.values(parsed.acceptance).some((check) => check.status === 'fail');
|
|
9
10
|
const target = state.pendingAction?.chapterNumber ?? parsed.chapterNumber;
|
|
10
11
|
const relative = join('reviews/chapter', chapterReviewFileName(target));
|
|
11
12
|
const path = await saveJsonFile(state.projectPath, relative, parsed);
|
|
@@ -17,7 +18,7 @@ export const chapterReviewHandler: StepHandler = async (state, content) => {
|
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
if (parsed.status === 'clean') {
|
|
21
|
+
if (parsed.status === 'clean' && !hasFailedAcceptance) {
|
|
21
22
|
return {
|
|
22
23
|
savedPaths: [path],
|
|
23
24
|
fileEntries: { [`review-chapter-${target}`]: relative },
|
package/src/core/steps/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { WorkflowStep } from '../types.js';
|
|
2
|
+
import { architectureExtensionHandler } from './architectureExtension.js';
|
|
2
3
|
import { architectureHandler } from './architecture.js';
|
|
3
4
|
import { chapterHandler } from './chapter.js';
|
|
4
5
|
import { chapterReviewHandler } from './chapterReview.js';
|
|
@@ -7,6 +8,7 @@ import { continuityReviewHandler } from './continuityReview.js';
|
|
|
7
8
|
import { crossChapterReviewHandler } from './crossChapterReview.js';
|
|
8
9
|
import { memoryCardHandler } from './memoryCard.js';
|
|
9
10
|
import { novelMetadataHandler } from './novelMetadata.js';
|
|
11
|
+
import { styleGuideHandler } from './styleGuide.js';
|
|
10
12
|
import { storyBibleHandler } from './storyBible.js';
|
|
11
13
|
import { StepHandler } from './types.js';
|
|
12
14
|
|
|
@@ -15,7 +17,9 @@ export type { StepApplyNext, StepApplyResult, StepHandler } from './types.js';
|
|
|
15
17
|
export const STEP_HANDLERS: Partial<Record<WorkflowStep, StepHandler>> = {
|
|
16
18
|
novel_metadata: novelMetadataHandler,
|
|
17
19
|
story_bible: storyBibleHandler,
|
|
20
|
+
style_guide: styleGuideHandler,
|
|
18
21
|
architecture: architectureHandler,
|
|
22
|
+
architecture_extension: architectureExtensionHandler,
|
|
19
23
|
chapter: chapterHandler,
|
|
20
24
|
memory_card: memoryCardHandler,
|
|
21
25
|
continuity_review: continuityReviewHandler,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
1
2
|
import { join } from 'node:path';
|
|
2
3
|
import { MemoryCardSchema } from '../schemas.js';
|
|
3
4
|
import { saveJsonFile } from '../projectStore.js';
|
|
@@ -7,6 +8,19 @@ import { ingestMemoryCardThreads } from '../threadStore.js';
|
|
|
7
8
|
import { applyCharacterUpdates } from '../characterStore.js';
|
|
8
9
|
import { StepHandler, parseJson } from './types.js';
|
|
9
10
|
|
|
11
|
+
async function maxPlannedChapter(projectPath: string): Promise<number> {
|
|
12
|
+
try {
|
|
13
|
+
const raw = await readFile(join(projectPath, 'architecture/chapters.json'), 'utf8');
|
|
14
|
+
const chapters = JSON.parse(raw) as Array<{ chapterNumber?: number }>;
|
|
15
|
+
return chapters.reduce((max, chapter) => {
|
|
16
|
+
const value = Number(chapter.chapterNumber);
|
|
17
|
+
return Number.isFinite(value) && value > max ? value : max;
|
|
18
|
+
}, 0);
|
|
19
|
+
} catch {
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
export const memoryCardHandler: StepHandler = async (state, content) => {
|
|
11
25
|
const parsed = MemoryCardSchema.parse(parseJson(content));
|
|
12
26
|
const relative = join('memory', memoryFileName(state.currentChapter));
|
|
@@ -15,12 +29,20 @@ export const memoryCardHandler: StepHandler = async (state, content) => {
|
|
|
15
29
|
await ingestMemoryCardThreads(state.projectPath, state.currentChapter, parsed.threadActions);
|
|
16
30
|
await applyCharacterUpdates(state.projectPath, state.currentChapter, parsed.characterUpdates);
|
|
17
31
|
const nextChapter = state.currentChapter + 1;
|
|
32
|
+
const plannedTotalChapters = state.plannedTotalChapters ?? state.targetChapters;
|
|
33
|
+
const plannedMax = await maxPlannedChapter(state.projectPath);
|
|
34
|
+
const nextStep =
|
|
35
|
+
nextChapter > plannedTotalChapters
|
|
36
|
+
? 'continuity_review'
|
|
37
|
+
: nextChapter > plannedMax
|
|
38
|
+
? 'architecture_extension'
|
|
39
|
+
: 'chapter';
|
|
18
40
|
return {
|
|
19
41
|
savedPaths: [path],
|
|
20
42
|
fileEntries: { [`memory-${state.currentChapter}`]: relative },
|
|
21
43
|
next: {
|
|
22
44
|
kind: 'linear',
|
|
23
|
-
nextStep
|
|
45
|
+
nextStep,
|
|
24
46
|
statePatch: { currentChapter: nextChapter },
|
|
25
47
|
},
|
|
26
48
|
};
|
|
@@ -9,6 +9,6 @@ export const storyBibleHandler: StepHandler = async (state, content) => {
|
|
|
9
9
|
return {
|
|
10
10
|
savedPaths: [path],
|
|
11
11
|
fileEntries: { storyBible: 'story-bible.md' },
|
|
12
|
-
next: { kind: 'linear', nextStep: '
|
|
12
|
+
next: { kind: 'linear', nextStep: 'style_guide' },
|
|
13
13
|
};
|
|
14
14
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { StyleGuideSchema } from '../schemas.js';
|
|
2
|
+
import { saveJsonFile } from '../projectStore.js';
|
|
3
|
+
import { StepHandler, parseJson } from './types.js';
|
|
4
|
+
|
|
5
|
+
export const styleGuideHandler: StepHandler = async (state, content) => {
|
|
6
|
+
const parsed = StyleGuideSchema.parse(parseJson(content));
|
|
7
|
+
const path = await saveJsonFile(state.projectPath, 'style-guide.json', parsed);
|
|
8
|
+
return {
|
|
9
|
+
savedPaths: [path],
|
|
10
|
+
fileEntries: { styleGuide: 'style-guide.json' },
|
|
11
|
+
next: { kind: 'linear', nextStep: 'architecture' },
|
|
12
|
+
};
|
|
13
|
+
};
|