novelforge-agent 0.1.1 → 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 +45 -18
- package/dist/src/cli/index.js +81 -4
- package/dist/src/core/bibleStore.js +36 -0
- package/dist/src/core/characterStore.js +74 -0
- package/dist/src/core/contextBuilder.js +76 -1
- package/dist/src/core/fileNames.js +4 -0
- package/dist/src/core/index.js +4 -0
- package/dist/src/core/projectDiscovery.js +1 -0
- package/dist/src/core/projectOps.js +193 -0
- package/dist/src/core/projectStore.js +15 -1
- package/dist/src/core/prompts/en-US.js +247 -16
- package/dist/src/core/prompts/zh-CN.js +246 -15
- package/dist/src/core/retrieval/index.js +8 -0
- package/dist/src/core/schemas.js +121 -1
- package/dist/src/core/steps/architecture.js +7 -1
- package/dist/src/core/steps/architectureExtension.js +72 -0
- package/dist/src/core/steps/chapter.js +11 -1
- package/dist/src/core/steps/chapterReview.js +26 -1
- package/dist/src/core/steps/chapterRevision.js +17 -0
- package/dist/src/core/steps/index.js +4 -0
- package/dist/src/core/steps/memoryCard.js +26 -1
- package/dist/src/core/steps/novelMetadata.js +4 -2
- package/dist/src/core/steps/storyBible.js +1 -1
- package/dist/src/core/steps/styleGuide.js +12 -0
- package/dist/src/core/threadStore.js +150 -0
- package/dist/src/core/workflow.js +5 -3
- package/dist/src/mcp/tools.js +228 -20
- package/package.json +5 -1
- package/src/cli/index.ts +84 -3
- package/src/core/bibleStore.ts +57 -0
- package/src/core/characterStore.ts +93 -0
- package/src/core/contextBuilder.ts +74 -4
- package/src/core/fileNames.ts +5 -0
- package/src/core/index.ts +4 -0
- package/src/core/projectDiscovery.ts +2 -0
- package/src/core/projectOps.ts +251 -0
- package/src/core/projectStore.ts +19 -1
- package/src/core/prompts/en-US.ts +258 -25
- package/src/core/prompts/types.ts +4 -1
- package/src/core/prompts/zh-CN.ts +250 -17
- package/src/core/retrieval/index.ts +10 -0
- package/src/core/schemas.ts +133 -1
- package/src/core/steps/architecture.ts +7 -1
- package/src/core/steps/architectureExtension.ts +88 -0
- package/src/core/steps/chapter.ts +11 -1
- package/src/core/steps/chapterReview.ts +28 -1
- package/src/core/steps/chapterRevision.ts +18 -0
- package/src/core/steps/index.ts +4 -0
- package/src/core/steps/memoryCard.ts +27 -1
- package/src/core/steps/novelMetadata.ts +4 -2
- package/src/core/steps/storyBible.ts +1 -1
- package/src/core/steps/styleGuide.ts +13 -0
- package/src/core/threadStore.ts +173 -0
- package/src/core/types.ts +134 -1
- package/src/core/workflow.ts +5 -3
- package/src/mcp/tools.ts +351 -21
|
@@ -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
|
- 章架构必须只覆盖本章应发生的内容,不要提前泄露后续具体事件。
|
|
@@ -106,6 +156,18 @@ ${input.context ? `## 已有上下文\n${input.context}\n` : ''}## 输出要求
|
|
|
106
156
|
"order": 1
|
|
107
157
|
}
|
|
108
158
|
],
|
|
159
|
+
"volumePacing": [
|
|
160
|
+
{
|
|
161
|
+
"volumeId": "v1",
|
|
162
|
+
"start": "本卷起点:主角/世界/冲突处于什么状态",
|
|
163
|
+
"promise": "本卷向读者承诺的核心看点或问题",
|
|
164
|
+
"keyTurns": ["关键转折1", "关键转折2"],
|
|
165
|
+
"midpoint": "本卷中点转折或认知变化",
|
|
166
|
+
"climax": "本卷高潮",
|
|
167
|
+
"payoffs": ["本卷计划回收的伏笔或承诺"],
|
|
168
|
+
"lingeringMysteries": ["卷末仍要保留的悬念"]
|
|
169
|
+
}
|
|
170
|
+
],
|
|
109
171
|
"chapters": [
|
|
110
172
|
{
|
|
111
173
|
"chapterNumber": 1,
|
|
@@ -119,38 +181,121 @@ ${input.context ? `## 已有上下文\n${input.context}\n` : ''}## 输出要求
|
|
|
119
181
|
|
|
120
182
|
要求:
|
|
121
183
|
- chapters.length 必须大于等于 ${input.state.targetChapters}。
|
|
184
|
+
- chapters 不需要一次覆盖全本;后续写到边界时会进入 architecture_extension 续规划。
|
|
122
185
|
- chapterNumber 从 1 开始连续递增。
|
|
123
186
|
- volumeId 必须引用 volumes 中存在的 id。
|
|
187
|
+
- volumePacing 必须为每个 volume 提供节奏板。
|
|
124
188
|
- requiredBeats 至少 1 条,且必须具体可执行。
|
|
125
189
|
${strictJsonOutputRules()}`,
|
|
126
190
|
};
|
|
127
191
|
}
|
|
128
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
|
+
|
|
129
259
|
function buildChapterPrompt(input: PromptBuildInput): BuiltPrompt {
|
|
130
|
-
|
|
260
|
+
const ch = input.state.currentChapter;
|
|
261
|
+
const isFirstChapter = ch <= 1;
|
|
262
|
+
return {
|
|
131
263
|
purpose: 'chapter',
|
|
132
264
|
expectedFormat: 'Markdown',
|
|
133
|
-
prompt: `你是一位擅长创作长篇网络小说的职业作者。请直接完成第 ${
|
|
265
|
+
prompt: `你是一位擅长创作长篇网络小说的职业作者。请直接完成第 ${ch} 章正文。
|
|
134
266
|
|
|
135
267
|
## 执行优先级
|
|
136
|
-
1.
|
|
137
|
-
2.
|
|
138
|
-
3.
|
|
139
|
-
|
|
140
|
-
##
|
|
268
|
+
1. 先严格遵守"本章架构、用户补充要求、故事圣经硬约束、风格圣经、上一章承接"。
|
|
269
|
+
2. 再参考"历史相关记忆、历史原文证据、活跃伏笔"保证一致性。
|
|
270
|
+
3. 最后才参考"全本/本卷远场规划",且不得提前写出尚未发生的情节。
|
|
271
|
+
|
|
272
|
+
## 字数目标
|
|
273
|
+
- 默认目标 3000 字(±20%)。如果本章架构里指定了 targetWords,按那个目标。
|
|
274
|
+
- 不要为了凑字数注水;也不要为了简洁牺牲冲突推进。
|
|
275
|
+
|
|
276
|
+
## 结构要求
|
|
277
|
+
${isFirstChapter
|
|
278
|
+
? '- 这是第 1 章,不需要"上回提要"。开篇直接立人物、立情境。'
|
|
279
|
+
: '- 章首需要 2-3 句"上回提要"或"承接段",让没读上一章的读者能续上(除非本章架构 requireRecap=false)。要自然带入,不要写成"上一章里……"的元叙述。'}
|
|
280
|
+
- 章末必须有清晰的"钩子":可以是悬念、反转、剧情承诺、情绪余韵或卷末高潮——按本章架构 endHookFocus 字段决定。如果未指定,默认用悬念。
|
|
281
|
+
|
|
282
|
+
## 风格
|
|
283
|
+
- 严格执行上下文中的 Style Guide;sampleParagraph 只作为语言质感参考,不要复写其内容。
|
|
284
|
+
- 严格执行 Style Guide.proseRhythm:短句、单句段、重复句、破折号是强调资源,不是默认叙述单位;常规叙述要形成自然句群。
|
|
141
285
|
- 文风必须与本书题材、世界观、人物身份、情感基调一致。
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
-
|
|
286
|
+
- 语言自然、稳定、可读,优先服务叙事推进、人物塑造和情绪积累。
|
|
287
|
+
- 对话符合人物身份、关系和处境;重要情绪通过动作、神态、节奏、潜台词体现。
|
|
288
|
+
- 场景描写有必要的感官细节与氛围支撑,但篇幅服务剧情。
|
|
289
|
+
- POV 严格按本章架构 povCharacter(如有),中途不切换视角。
|
|
146
290
|
|
|
147
291
|
## 执行规则
|
|
148
292
|
- 只写本章架构明确覆盖的内容,不得提前写后续章节具体事件或人物揭示。
|
|
149
|
-
-
|
|
293
|
+
- 不得新增本章架构未授权的主要人物;功能性角色轻描淡写。
|
|
150
294
|
- 所有人物称谓、物品、场景、能力、时间线必须与既有设定一致。
|
|
151
295
|
- 如果上一章结尾仍在动作、对话或同一场景中,本章开头必须连续衔接。
|
|
296
|
+
- 活跃伏笔列表中的条目本章可以"推进"或"回收",但**不得无声无息地删除**——若选择不触碰也要让它仍然成立。
|
|
152
297
|
- 禁止无代价越级碾压、强行降智配角、突兀机械反转、硬灌设定、空洞抒情。
|
|
153
|
-
-
|
|
298
|
+
- 禁止总结腔、条目腔、说教腔,不要输出解释性前言或"我修改了什么"之类的元文本。
|
|
154
299
|
|
|
155
300
|
${input.context ? `## 生成上下文\n${input.context}\n` : ''}## 输出要求
|
|
156
301
|
- 输出 Markdown。
|
|
@@ -160,7 +305,7 @@ ${input.context ? `## 生成上下文\n${input.context}\n` : ''}## 输出要求
|
|
|
160
305
|
}
|
|
161
306
|
|
|
162
307
|
function buildMemoryPrompt(input: PromptBuildInput): BuiltPrompt {
|
|
163
|
-
return {
|
|
308
|
+
return {
|
|
164
309
|
purpose: 'memory_card',
|
|
165
310
|
expectedFormat: 'JSON matching MemoryCardSchema',
|
|
166
311
|
prompt: `你是一名长篇小说连续性编辑。请从第 ${input.state.currentChapter} 章正文中提取记忆卡。
|
|
@@ -191,19 +336,47 @@ ${input.context ? `## 当前章上下文\n${input.context}\n` : ''}## 输出要
|
|
|
191
336
|
"after": "变化后"
|
|
192
337
|
}
|
|
193
338
|
],
|
|
194
|
-
"openThreads": ["尚未解决的伏笔、承诺、危险或疑问"]
|
|
339
|
+
"openThreads": ["尚未解决的伏笔、承诺、危险或疑问"],
|
|
340
|
+
"wordCount": 本章实际字数(整数估算即可),
|
|
341
|
+
"threadActions": [
|
|
342
|
+
{
|
|
343
|
+
"kind": "plant | build | pay | drop",
|
|
344
|
+
"threadId": "已有伏笔的 id(如果是 plant 留空让系统分配;build/pay/drop 必须填活跃伏笔列表里的 id)",
|
|
345
|
+
"description": "新伏笔的描述(plant 时必填)或本章如何推进/回收/丢弃这条伏笔的一句话"
|
|
346
|
+
}
|
|
347
|
+
],
|
|
348
|
+
"characterUpdates": [
|
|
349
|
+
{
|
|
350
|
+
"name": "人物姓名",
|
|
351
|
+
"role": "角色定位(如本章确认或改变)",
|
|
352
|
+
"goal": "本章结束时的当前目标",
|
|
353
|
+
"belief": "本章结束时影响其行动的核心认知/信念",
|
|
354
|
+
"relationships": [
|
|
355
|
+
{ "name": "相关人物", "dynamic": "本章结束时的关系状态" }
|
|
356
|
+
],
|
|
357
|
+
"abilities": ["本章结束时确认拥有的能力、资源或限制"],
|
|
358
|
+
"secrets": ["本章结束时仍未公开或只被部分人知道的秘密"],
|
|
359
|
+
"emotionalState": "本章结束时的情绪状态"
|
|
360
|
+
}
|
|
361
|
+
]
|
|
195
362
|
}
|
|
196
363
|
|
|
197
364
|
要求:
|
|
198
365
|
- 只记录已经在本章发生或被确认的信息。
|
|
199
366
|
- 不要推测后续剧情。
|
|
200
367
|
- facts 和 stateChanges 要具体,便于后续章节引用。
|
|
368
|
+
- wordCount 用中文字符数(去掉空格和 Markdown 标记),近似估算即可。
|
|
369
|
+
- threadActions 是关键:
|
|
370
|
+
· 任何"上下文里 Active Foreshadow Threads"列出的活跃伏笔,如果本章推进了,请发 kind="build";如果本章正式回收/兑现,请发 kind="pay";如果本章决定放弃,请发 kind="drop"。这三种情况 threadId 必填。
|
|
371
|
+
· 本章新埋设的伏笔,请发 kind="plant",description 写清楚是什么伏笔。
|
|
372
|
+
· 活跃伏笔本章没动也没关系,不需要为它发动作;但**不要悄悄删除**——只要不发 drop,它就继续保留活跃。
|
|
373
|
+
- characterUpdates 用于维护独立角色状态表:只输出本章有明确变化或被重新确认的重要人物;目标、信念、关系、能力、秘密、情绪状态必须以本章结尾为准。
|
|
201
374
|
${strictJsonOutputRules()}`,
|
|
202
375
|
};
|
|
203
376
|
}
|
|
204
377
|
|
|
205
378
|
function buildContinuityReviewPrompt(input: PromptBuildInput): BuiltPrompt {
|
|
206
|
-
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);
|
|
207
380
|
return {
|
|
208
381
|
purpose: 'continuity_review',
|
|
209
382
|
expectedFormat: 'JSON matching ContinuityReviewSchema',
|
|
@@ -249,6 +422,14 @@ return {
|
|
|
249
422
|
prompt: `你是一名严格的章节审稿编辑。请审阅指定章节是否存在内部问题以及与既有设定的冲突。
|
|
250
423
|
|
|
251
424
|
${input.context ? `## 审阅上下文\n${input.context}\n` : ''}## 审阅重点
|
|
425
|
+
- 这是强制章节验收门槛:只要任一验收项 fail,status 必须是 "issues_found",不能进入下一章。
|
|
426
|
+
- requiredBeats 是否全部完成;缺失项必须写入 acceptance.requiredBeats.missingBeats。
|
|
427
|
+
- 本章是否推进主线、人物状态或活跃伏笔;如果完全原地踏步,narrativeProgress/characterProgress/foreshadowProgress 至少一项必须 fail。
|
|
428
|
+
- 是否违反故事圣经、角色状态表、卷级节奏板或历史记忆。
|
|
429
|
+
- 是否违反 Style Guide:叙事声音、句式密度、题材词汇、对白规则和禁用模式。
|
|
430
|
+
- 是否违反 Style Guide.proseRhythm:短句密度过高、连续单句短段、靠换行制造伪节奏、心理解释过直白、重复同一句式。
|
|
431
|
+
- 章末是否有清晰钩子,且符合本章 endHookFocus。
|
|
432
|
+
- 是否重复之前章节已经完成的桥段、冲突结构、信息揭示或对话功能。
|
|
252
433
|
- 人物声音、动机、状态是否符合故事圣经与历史记忆。
|
|
253
434
|
- 世界规则、物品归属、能力边界是否被破坏。
|
|
254
435
|
- 时间线、地点、与上一章结尾的衔接是否一致。
|
|
@@ -260,6 +441,41 @@ ${input.context ? `## 审阅上下文\n${input.context}\n` : ''}## 审阅重点
|
|
|
260
441
|
{
|
|
261
442
|
"chapterNumber": ${chapter},
|
|
262
443
|
"status": "clean",
|
|
444
|
+
"acceptance": {
|
|
445
|
+
"requiredBeats": {
|
|
446
|
+
"status": "pass | fail",
|
|
447
|
+
"evidence": "逐条说明 requiredBeats 完成证据",
|
|
448
|
+
"missingBeats": []
|
|
449
|
+
},
|
|
450
|
+
"narrativeProgress": {
|
|
451
|
+
"status": "pass | fail",
|
|
452
|
+
"evidence": "本章如何推进主线/阶段目标"
|
|
453
|
+
},
|
|
454
|
+
"characterProgress": {
|
|
455
|
+
"status": "pass | fail",
|
|
456
|
+
"evidence": "本章如何改变或确认关键人物目标、信念、关系、能力、秘密或情绪"
|
|
457
|
+
},
|
|
458
|
+
"foreshadowProgress": {
|
|
459
|
+
"status": "pass | fail",
|
|
460
|
+
"evidence": "本章如何埋设、推进、回收或有意识保留伏笔"
|
|
461
|
+
},
|
|
462
|
+
"storyBibleConsistency": {
|
|
463
|
+
"status": "pass | fail",
|
|
464
|
+
"evidence": "是否符合故事圣经、角色状态表和世界规则"
|
|
465
|
+
},
|
|
466
|
+
"proseRhythm": {
|
|
467
|
+
"status": "pass | fail",
|
|
468
|
+
"evidence": "是否符合 Style Guide.proseRhythm;说明短句/单句段/重复句/心理解释是否被合理控制"
|
|
469
|
+
},
|
|
470
|
+
"endingHook": {
|
|
471
|
+
"status": "pass | fail",
|
|
472
|
+
"evidence": "章末钩子的具体段落和作用"
|
|
473
|
+
},
|
|
474
|
+
"repetition": {
|
|
475
|
+
"status": "pass | fail",
|
|
476
|
+
"evidence": "是否重复既有桥段;无重复也要说明依据"
|
|
477
|
+
}
|
|
478
|
+
},
|
|
263
479
|
"issues": [
|
|
264
480
|
{
|
|
265
481
|
"severity": "low | medium | high",
|
|
@@ -274,6 +490,8 @@ ${input.context ? `## 审阅上下文\n${input.context}\n` : ''}## 审阅重点
|
|
|
274
490
|
要求:
|
|
275
491
|
- 没有问题时 status 为 "clean",issues 输出空数组。
|
|
276
492
|
- 有问题时 status 为 "issues_found"。
|
|
493
|
+
- 只有所有 acceptance 项都是 "pass" 时,status 才能为 "clean"。
|
|
494
|
+
- 任一 acceptance 项为 "fail" 时,必须在 issues 中写出对应问题与修复建议。
|
|
277
495
|
- evidence 必须具体,不能写"疑似"、"可能"。
|
|
278
496
|
${strictJsonOutputRules()}`,
|
|
279
497
|
};
|
|
@@ -346,8 +564,12 @@ function buildPromptForStep(input: PromptBuildInput): BuiltPrompt {
|
|
|
346
564
|
return buildMetadataPrompt(input);
|
|
347
565
|
case 'story_bible':
|
|
348
566
|
return buildStoryBiblePrompt(input);
|
|
567
|
+
case 'style_guide':
|
|
568
|
+
return buildStyleGuidePrompt(input);
|
|
349
569
|
case 'architecture':
|
|
350
570
|
return buildArchitecturePrompt(input);
|
|
571
|
+
case 'architecture_extension':
|
|
572
|
+
return buildArchitectureExtensionPrompt(input);
|
|
351
573
|
case 'chapter':
|
|
352
574
|
return buildChapterPrompt(input);
|
|
353
575
|
case 'memory_card':
|
|
@@ -360,6 +582,17 @@ function buildPromptForStep(input: PromptBuildInput): BuiltPrompt {
|
|
|
360
582
|
return buildChapterRevisionPrompt(input);
|
|
361
583
|
case 'cross_chapter_review':
|
|
362
584
|
return buildCrossChapterReviewPrompt(input);
|
|
585
|
+
case 'story_bible_amend':
|
|
586
|
+
return {
|
|
587
|
+
purpose: 'story_bible',
|
|
588
|
+
expectedFormat: 'Markdown',
|
|
589
|
+
prompt: `请基于当前已有故事圣经与本次反馈,输出"修订后的完整故事圣经 Markdown"。
|
|
590
|
+
|
|
591
|
+
${input.context ? `## 修订上下文\n${input.context}\n` : ''}## 输出要求
|
|
592
|
+
- 输出完整的 story-bible.md 内容,覆盖式替换旧版(旧版会自动归档到 story-bible-versions/)。
|
|
593
|
+
- 保留旧版仍然成立的内容,仅修改 / 新增 / 删除有明确依据的部分。
|
|
594
|
+
- 不要输出 diff、变更说明、bullet 总结,直接输出新 bible 全文。`,
|
|
595
|
+
};
|
|
363
596
|
case 'complete':
|
|
364
597
|
return {
|
|
365
598
|
purpose: 'continuity_review',
|
|
@@ -95,6 +95,16 @@ export async function indexMemoryCard(projectPath: string, chapterNumber: number
|
|
|
95
95
|
await upsert(projectPath, (existing) => existing === id, chunkMemoryCard(chapterNumber, card));
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
export async function removeChapterFromIndex(projectPath: string, chapterNumber: number): Promise<void> {
|
|
99
|
+
const prefix = `chapter:${chapterNumber}:`;
|
|
100
|
+
await upsert(projectPath, (id) => id.startsWith(prefix), []);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function removeMemoryCardFromIndex(projectPath: string, chapterNumber: number): Promise<void> {
|
|
104
|
+
const id = `memory:${chapterNumber}`;
|
|
105
|
+
await upsert(projectPath, (existing) => existing === id, []);
|
|
106
|
+
}
|
|
107
|
+
|
|
98
108
|
export async function retrieve(projectPath: string, query: string, options: RetrieveOptions = {}): Promise<RetrievalHit[]> {
|
|
99
109
|
if (!query.trim()) return [];
|
|
100
110
|
const bundle = await loadBundle(projectPath);
|
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),
|
|
@@ -22,20 +39,100 @@ export const VolumeArchitectureSchema = z.object({
|
|
|
22
39
|
order: z.number().int().positive(),
|
|
23
40
|
});
|
|
24
41
|
|
|
42
|
+
export const VolumePacingBoardSchema = z.object({
|
|
43
|
+
volumeId: z.string().min(1),
|
|
44
|
+
start: z.string().min(1),
|
|
45
|
+
promise: z.string().min(1),
|
|
46
|
+
keyTurns: z.array(z.string().min(1)).min(1),
|
|
47
|
+
midpoint: z.string().min(1),
|
|
48
|
+
climax: z.string().min(1),
|
|
49
|
+
payoffs: z.array(z.string().min(1)),
|
|
50
|
+
lingeringMysteries: z.array(z.string().min(1)),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const EndHookFocusSchema = z.enum([
|
|
54
|
+
'cliffhanger',
|
|
55
|
+
'mystery',
|
|
56
|
+
'emotional',
|
|
57
|
+
'reveal',
|
|
58
|
+
'volume_close',
|
|
59
|
+
'gentle',
|
|
60
|
+
]);
|
|
61
|
+
|
|
25
62
|
export const ChapterArchitectureSchema = z.object({
|
|
26
63
|
chapterNumber: z.number().int().positive(),
|
|
27
64
|
title: z.string().min(1),
|
|
28
65
|
volumeId: z.string().min(1),
|
|
29
66
|
summary: z.string().min(1),
|
|
30
67
|
requiredBeats: z.array(z.string().min(1)).min(1),
|
|
68
|
+
targetWords: z.number().int().positive().optional(),
|
|
69
|
+
requireRecap: z.boolean().optional(),
|
|
70
|
+
endHookFocus: EndHookFocusSchema.optional(),
|
|
71
|
+
povCharacter: z.string().min(1).optional(),
|
|
31
72
|
});
|
|
32
73
|
|
|
33
74
|
export const ArchitecturePayloadSchema = z.object({
|
|
34
75
|
full: z.string().min(1),
|
|
35
76
|
volumes: z.array(VolumeArchitectureSchema).min(1),
|
|
77
|
+
volumePacing: z.array(VolumePacingBoardSchema).optional(),
|
|
36
78
|
chapters: z.array(ChapterArchitectureSchema).min(1),
|
|
37
79
|
});
|
|
38
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
|
+
|
|
88
|
+
export const ThreadActionSchema = z.object({
|
|
89
|
+
kind: z.enum(['plant', 'build', 'pay', 'drop']),
|
|
90
|
+
threadId: z.string().min(1).optional(),
|
|
91
|
+
description: z.string().min(1),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export const ThreadStatusSchema = z.enum(['planted', 'building', 'paid', 'dropped']);
|
|
95
|
+
|
|
96
|
+
export const ThreadSchema = z.object({
|
|
97
|
+
id: z.string().min(1),
|
|
98
|
+
description: z.string().min(1),
|
|
99
|
+
status: ThreadStatusSchema,
|
|
100
|
+
plantedAt: z.number().int().positive(),
|
|
101
|
+
lastTouchedAt: z.number().int().positive(),
|
|
102
|
+
plannedPayoffAt: z.number().int().positive().optional(),
|
|
103
|
+
paidOffAt: z.number().int().positive().optional(),
|
|
104
|
+
droppedAt: z.number().int().positive().optional(),
|
|
105
|
+
notes: z.string().optional(),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export const CharacterRelationshipStateSchema = z.object({
|
|
109
|
+
name: z.string().min(1),
|
|
110
|
+
dynamic: z.string().min(1),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export const CharacterStateSchema = z.object({
|
|
114
|
+
name: z.string().min(1),
|
|
115
|
+
role: z.string().min(1).optional(),
|
|
116
|
+
goal: z.string().min(1),
|
|
117
|
+
belief: z.string().min(1),
|
|
118
|
+
relationships: z.array(CharacterRelationshipStateSchema),
|
|
119
|
+
abilities: z.array(z.string().min(1)),
|
|
120
|
+
secrets: z.array(z.string().min(1)),
|
|
121
|
+
emotionalState: z.string().min(1),
|
|
122
|
+
lastUpdatedAt: z.number().int().nonnegative(),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export const CharacterStateUpdateSchema = z.object({
|
|
126
|
+
name: z.string().min(1),
|
|
127
|
+
role: z.string().min(1).optional(),
|
|
128
|
+
goal: z.string().min(1).optional(),
|
|
129
|
+
belief: z.string().min(1).optional(),
|
|
130
|
+
relationships: z.array(CharacterRelationshipStateSchema).optional(),
|
|
131
|
+
abilities: z.array(z.string().min(1)).optional(),
|
|
132
|
+
secrets: z.array(z.string().min(1)).optional(),
|
|
133
|
+
emotionalState: z.string().min(1).optional(),
|
|
134
|
+
});
|
|
135
|
+
|
|
39
136
|
export const MemoryCardSchema = z.object({
|
|
40
137
|
summary: z.string().min(1),
|
|
41
138
|
keyEvents: z.array(z.string().min(1)),
|
|
@@ -55,6 +152,9 @@ export const MemoryCardSchema = z.object({
|
|
|
55
152
|
after: z.string().min(1),
|
|
56
153
|
})),
|
|
57
154
|
openThreads: z.array(z.string().min(1)),
|
|
155
|
+
wordCount: z.number().int().nonnegative().optional(),
|
|
156
|
+
threadActions: z.array(ThreadActionSchema).optional(),
|
|
157
|
+
characterUpdates: z.array(CharacterStateUpdateSchema).optional(),
|
|
58
158
|
});
|
|
59
159
|
|
|
60
160
|
export const ContinuityReviewSchema = z.object({
|
|
@@ -73,15 +173,47 @@ export const ContinuityReviewSchema = z.object({
|
|
|
73
173
|
|
|
74
174
|
export const ChapterReviewIssueSchema = z.object({
|
|
75
175
|
severity: z.enum(['low', 'medium', 'high']),
|
|
76
|
-
category: z.enum([
|
|
176
|
+
category: z.enum([
|
|
177
|
+
'character',
|
|
178
|
+
'world',
|
|
179
|
+
'timeline',
|
|
180
|
+
'item',
|
|
181
|
+
'knowledge',
|
|
182
|
+
'pacing',
|
|
183
|
+
'style',
|
|
184
|
+
'architecture',
|
|
185
|
+
'plot',
|
|
186
|
+
'foreshadow',
|
|
187
|
+
'hook',
|
|
188
|
+
'repetition',
|
|
189
|
+
]),
|
|
77
190
|
description: z.string().min(1),
|
|
78
191
|
evidence: z.string().min(1),
|
|
79
192
|
suggestion: z.string().min(1),
|
|
80
193
|
});
|
|
81
194
|
|
|
195
|
+
export const ChapterAcceptanceCheckSchema = z.object({
|
|
196
|
+
status: z.enum(['pass', 'fail']),
|
|
197
|
+
evidence: z.string().min(1),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
export const ChapterAcceptanceGateSchema = z.object({
|
|
201
|
+
requiredBeats: ChapterAcceptanceCheckSchema.extend({
|
|
202
|
+
missingBeats: z.array(z.string().min(1)),
|
|
203
|
+
}),
|
|
204
|
+
narrativeProgress: ChapterAcceptanceCheckSchema,
|
|
205
|
+
characterProgress: ChapterAcceptanceCheckSchema,
|
|
206
|
+
foreshadowProgress: ChapterAcceptanceCheckSchema,
|
|
207
|
+
storyBibleConsistency: ChapterAcceptanceCheckSchema,
|
|
208
|
+
proseRhythm: ChapterAcceptanceCheckSchema,
|
|
209
|
+
endingHook: ChapterAcceptanceCheckSchema,
|
|
210
|
+
repetition: ChapterAcceptanceCheckSchema,
|
|
211
|
+
});
|
|
212
|
+
|
|
82
213
|
export const ChapterReviewSchema = z.object({
|
|
83
214
|
chapterNumber: z.number().int().positive(),
|
|
84
215
|
status: z.enum(['clean', 'issues_found']),
|
|
216
|
+
acceptance: ChapterAcceptanceGateSchema,
|
|
85
217
|
issues: z.array(ChapterReviewIssueSchema),
|
|
86
218
|
});
|
|
87
219
|
|
|
@@ -9,9 +9,15 @@ export const architectureHandler: StepHandler = async (state, content) => {
|
|
|
9
9
|
await saveJsonFile(state.projectPath, 'architecture/volumes.json', parsed.volumes),
|
|
10
10
|
await saveJsonFile(state.projectPath, 'architecture/chapters.json', parsed.chapters),
|
|
11
11
|
];
|
|
12
|
+
if (parsed.volumePacing) {
|
|
13
|
+
savedPaths.push(await saveJsonFile(state.projectPath, 'architecture/volume-pacing.json', parsed.volumePacing));
|
|
14
|
+
}
|
|
12
15
|
return {
|
|
13
16
|
savedPaths,
|
|
14
|
-
fileEntries: {
|
|
17
|
+
fileEntries: {
|
|
18
|
+
architecture: 'architecture/chapters.json',
|
|
19
|
+
...(parsed.volumePacing ? { volumePacing: 'architecture/volume-pacing.json' } : {}),
|
|
20
|
+
},
|
|
15
21
|
next: { kind: 'linear', nextStep: 'chapter' },
|
|
16
22
|
};
|
|
17
23
|
};
|
|
@@ -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
|
+
};
|