@actalk/inkos-core 1.3.5 → 1.3.6
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/dist/agent/agent-session.d.ts.map +1 -1
- package/dist/agent/agent-session.js +18 -4
- package/dist/agent/agent-session.js.map +1 -1
- package/dist/agent/agent-tools.d.ts +2 -0
- package/dist/agent/agent-tools.d.ts.map +1 -1
- package/dist/agent/agent-tools.js +16 -1
- package/dist/agent/agent-tools.js.map +1 -1
- package/dist/agent/context-transform.d.ts +3 -0
- package/dist/agent/context-transform.d.ts.map +1 -0
- package/dist/agent/context-transform.js +65 -0
- package/dist/agent/context-transform.js.map +1 -0
- package/dist/agent/index.d.ts +1 -0
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +1 -0
- package/dist/agent/index.js.map +1 -1
- package/dist/agents/architect.d.ts +40 -2
- package/dist/agents/architect.d.ts.map +1 -1
- package/dist/agents/architect.js +575 -207
- package/dist/agents/architect.js.map +1 -1
- package/dist/agents/chapter-analyzer.d.ts.map +1 -1
- package/dist/agents/chapter-analyzer.js +5 -4
- package/dist/agents/chapter-analyzer.js.map +1 -1
- package/dist/agents/consolidator.d.ts.map +1 -1
- package/dist/agents/consolidator.js +2 -2
- package/dist/agents/consolidator.js.map +1 -1
- package/dist/agents/continuity.d.ts.map +1 -1
- package/dist/agents/continuity.js +4 -3
- package/dist/agents/continuity.js.map +1 -1
- package/dist/agents/planner.d.ts.map +1 -1
- package/dist/agents/planner.js +4 -3
- package/dist/agents/planner.js.map +1 -1
- package/dist/agents/reviser.d.ts.map +1 -1
- package/dist/agents/reviser.js +5 -4
- package/dist/agents/reviser.js.map +1 -1
- package/dist/agents/writer.d.ts.map +1 -1
- package/dist/agents/writer.js +8 -7
- package/dist/agents/writer.js.map +1 -1
- package/dist/llm/provider.d.ts +13 -0
- package/dist/llm/provider.d.ts.map +1 -1
- package/dist/llm/provider.js +5 -1
- package/dist/llm/provider.js.map +1 -1
- package/dist/models/project.d.ts +8 -0
- package/dist/models/project.d.ts.map +1 -1
- package/dist/models/project.js +7 -0
- package/dist/models/project.js.map +1 -1
- package/dist/pipeline/runner.d.ts +22 -0
- package/dist/pipeline/runner.d.ts.map +1 -1
- package/dist/pipeline/runner.js +154 -5
- package/dist/pipeline/runner.js.map +1 -1
- package/dist/state/manager.d.ts.map +1 -1
- package/dist/state/manager.js +6 -0
- package/dist/state/manager.js.map +1 -1
- package/dist/utils/memory-retrieval.d.ts.map +1 -1
- package/dist/utils/memory-retrieval.js +2 -1
- package/dist/utils/memory-retrieval.js.map +1 -1
- package/dist/utils/outline-paths.d.ts +51 -0
- package/dist/utils/outline-paths.d.ts.map +1 -0
- package/dist/utils/outline-paths.js +243 -0
- package/dist/utils/outline-paths.js.map +1 -0
- package/package.json +1 -1
package/dist/agents/architect.js
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
import { BaseAgent } from "./base.js";
|
|
2
2
|
import { readGenreProfile } from "./rules-reader.js";
|
|
3
|
-
import { writeFile, mkdir } from "node:fs/promises";
|
|
3
|
+
import { writeFile, mkdir, rm } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { renderHookSnapshot } from "../utils/memory-retrieval.js";
|
|
6
|
+
/** 拆 markdown 的首部 YAML frontmatter 和正文。没有 frontmatter 时返回 frontmatter: null。 */
|
|
7
|
+
function extractYamlFrontmatter(raw) {
|
|
8
|
+
if (!raw)
|
|
9
|
+
return { frontmatter: null, body: "" };
|
|
10
|
+
const stripped = raw.replace(/^```(?:md|markdown|yaml)?\s*\n/, "").replace(/\n```\s*$/, "");
|
|
11
|
+
const leadingMatch = stripped.match(/^\s*---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
|
|
12
|
+
if (!leadingMatch) {
|
|
13
|
+
return { frontmatter: null, body: stripped };
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
frontmatter: `---\n${leadingMatch[1]}\n---`,
|
|
17
|
+
body: leadingMatch[2].trim(),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
6
20
|
export class ArchitectAgent extends BaseAgent {
|
|
7
21
|
get name() {
|
|
8
22
|
return "architect";
|
|
9
23
|
}
|
|
10
|
-
async generateFoundation(book, externalContext, reviewFeedback) {
|
|
24
|
+
async generateFoundation(book, externalContext, reviewFeedback, options) {
|
|
11
25
|
const { profile: gp, body: genreBody } = await readGenreProfile(this.ctx.projectRoot, book.genre);
|
|
12
26
|
const resolvedLanguage = book.language ?? gp.language;
|
|
13
27
|
const contextBlock = externalContext
|
|
14
28
|
? `\n\n## 外部指令\n以下是来自外部系统的创作指令,请将其融入设定中:\n\n${externalContext}\n`
|
|
15
29
|
: "";
|
|
16
30
|
const reviewFeedbackBlock = this.buildReviewFeedbackBlock(reviewFeedback, resolvedLanguage);
|
|
31
|
+
const revisePrompt = options?.reviseFrom
|
|
32
|
+
? this.buildRevisePrompt(options.reviseFrom)
|
|
33
|
+
: "";
|
|
17
34
|
const numericalBlock = gp.numericalSystem
|
|
18
35
|
? `- 有明确的数值/资源体系可追踪
|
|
19
36
|
- 在 book_rules 中定义 numericalSystemOverrides(hardCap、resourceTypes)`
|
|
@@ -24,106 +41,190 @@ export class ArchitectAgent extends BaseAgent {
|
|
|
24
41
|
const eraBlock = gp.eraResearch
|
|
25
42
|
? "- 需要年代考据支撑(在 book_rules 中设置 eraConstraints)"
|
|
26
43
|
: "";
|
|
27
|
-
const
|
|
28
|
-
?
|
|
29
|
-
|
|
30
|
-
|
|
44
|
+
const basePrompt = resolvedLanguage === "en"
|
|
45
|
+
? this.buildEnglishFoundationPrompt(book, gp, genreBody, contextBlock, reviewFeedbackBlock, numericalBlock, powerBlock, eraBlock)
|
|
46
|
+
: this.buildChineseFoundationPrompt(book, gp, genreBody, contextBlock, reviewFeedbackBlock, numericalBlock, powerBlock, eraBlock);
|
|
47
|
+
const systemPrompt = revisePrompt + basePrompt;
|
|
48
|
+
const langPrefix = resolvedLanguage === "en"
|
|
49
|
+
? `【LANGUAGE OVERRIDE】ALL output (story_frame, volume_map, roles, book_rules, pending_hooks) MUST be written in English. Character names, place names, and all prose must be in English. The === SECTION: === tags remain unchanged. Do NOT emit rhythm_principles or current_state sections — rhythm principles live inside the last paragraph of volume_map; environment/era anchors (when relevant) are woven into story_frame's world-tonal-ground paragraph.\n\n`
|
|
50
|
+
: "";
|
|
51
|
+
const userMessage = resolvedLanguage === "en"
|
|
52
|
+
? `Generate the complete foundation for a ${gp.name} novel titled "${book.title}". Write everything in English.`
|
|
53
|
+
: `请为标题为"${book.title}"的${gp.name}小说生成完整基础设定。`;
|
|
54
|
+
const response = await this.chat([
|
|
55
|
+
{ role: "system", content: langPrefix + systemPrompt },
|
|
56
|
+
{ role: "user", content: userMessage },
|
|
57
|
+
], { temperature: 0.8, maxTokens: 16384 });
|
|
58
|
+
return this.parseSections(response.content);
|
|
59
|
+
}
|
|
60
|
+
buildRevisePrompt(reviseFrom) {
|
|
61
|
+
return `你在把一本已有书的架构稿从条目式升级成段落式架构稿 + 一人一卡的角色目录。
|
|
31
62
|
|
|
32
|
-
|
|
33
|
-
Protagonist setup (identity / advantage / personality core / behavioral boundaries)
|
|
63
|
+
原书信息(条目式架构稿原文,这是权威内容,必须完整保留里面的世界观 / 角色 / 主线 / 伏笔):
|
|
34
64
|
|
|
35
|
-
|
|
36
|
-
|
|
65
|
+
【story_bible.md 全文】
|
|
66
|
+
${reviseFrom.storyBible || "(无)"}
|
|
37
67
|
|
|
38
|
-
|
|
39
|
-
|
|
68
|
+
【volume_outline.md 全文】
|
|
69
|
+
${reviseFrom.volumeOutline || "(无)"}
|
|
40
70
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- Keep the title clear, direct, and easy to understand
|
|
44
|
-
- Use a format that immediately signals genre and core appeal
|
|
45
|
-
- Avoid overly literary or misleading titles
|
|
46
|
-
|
|
47
|
-
Blurb method (within 300 words, choose one):
|
|
48
|
-
1. Open with conflict, then reveal the hook, then leave suspense
|
|
49
|
-
2. Summarize only the main line and keep a clear suspense gap
|
|
50
|
-
3. Use a miniature scene that captures the book's strongest pull
|
|
51
|
-
|
|
52
|
-
Core blurb principle:
|
|
53
|
-
- The blurb is product copy that must make readers want to click`
|
|
54
|
-
: `用结构化二级标题组织:
|
|
55
|
-
## 01_世界观
|
|
56
|
-
世界观设定、核心规则体系
|
|
71
|
+
【book_rules.md 全文】
|
|
72
|
+
${reviseFrom.bookRules || "(无)"}
|
|
57
73
|
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
【character_matrix.md 全文】
|
|
75
|
+
${reviseFrom.characterMatrix || "(无)"}
|
|
60
76
|
|
|
61
|
-
|
|
62
|
-
|
|
77
|
+
你的任务:
|
|
78
|
+
1. 把 story_bible 的内容重新组织成 4 段段落式 story_frame.md(主题 / 核心冲突 / 世界观底色 / 终局方向)
|
|
79
|
+
2. 把 volume_outline 的内容重新组织成 5 段 volume_map.md + 末尾 1 段节奏原则
|
|
80
|
+
3. 把 character_matrix 里的每个角色拆成一份 roles/主要角色/<name>.md 或 roles/次要角色/<name>.md
|
|
63
81
|
|
|
64
|
-
|
|
65
|
-
|
|
82
|
+
严格约束:
|
|
83
|
+
- 世界观设定、角色设定、主线走向、已埋下的伏笔一个字都不能丢
|
|
84
|
+
- 如果原内容里有 bullet 点,把它们连成段落;不要只是把 bullet 转成句号分隔
|
|
85
|
+
- 主次角色的判断依据:character_matrix 里已标注的如果有,沿用;没有的话,把在 volume_outline 里出现频次高、或者承担主线冲突的列为主要,其余为次要
|
|
86
|
+
- 基调 / 语气不要改变
|
|
87
|
+
- 如果原内容里留有空白或未展开项,保留为空,不要主动补全
|
|
66
88
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
- 书名必须简单扼要、通俗易懂,读者看到书名就能知道题材和主题
|
|
70
|
-
- 采用"题材+核心爽点+主角行为"的长书名格式,避免文艺化
|
|
71
|
-
- 融入平台当下热点词汇,吸引精准流量
|
|
72
|
-
- 禁止题材错位(都市文取玄幻书名会导致读者流失)
|
|
73
|
-
- 参考热榜书名风格:俏皮、通俗、有记忆点
|
|
74
|
-
|
|
75
|
-
简介方法论(300字内,三种写法任选其一):
|
|
76
|
-
1. 冲突开篇法:第一句抛困境/冲突,第二句亮金手指/核心能力,第三句留悬念
|
|
77
|
-
2. 高度概括法:只挑主线概括(不是全篇概括),必须留悬念
|
|
78
|
-
3. 小剧场法:提炼故事中最经典的桥段,作为引子
|
|
79
|
-
|
|
80
|
-
简介核心原则:
|
|
81
|
-
- 简介 = 产品宣传语,必须让读者产生"我要点开看"的冲动
|
|
82
|
-
- 可以从剧情设定、人设、或某个精彩片段切入
|
|
83
|
-
- 必须有噱头(如"凡是被写在笔记本上的名字,最后都得死")`;
|
|
84
|
-
const volumeOutlinePrompt = resolvedLanguage === "en"
|
|
85
|
-
? `Volume plan. For each volume include: title, chapter range, core conflict, key turning points, and payoff goal
|
|
89
|
+
用户额外要求:
|
|
90
|
+
${reviseFrom.userFeedback || "(无)"}
|
|
86
91
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
// -------------------------------------------------------------------------
|
|
97
|
+
// Phase 5 段落式 prompt — zh
|
|
98
|
+
// -------------------------------------------------------------------------
|
|
99
|
+
buildChineseFoundationPrompt(book, gp, genreBody, contextBlock, reviewFeedbackBlock, numericalBlock, powerBlock, eraBlock) {
|
|
100
|
+
return `你是这本书的总架构师。你的唯一输出是**段落密度的基础设定**——不是表格、不是 schema、不是条目化 bullet。这本书的"灵气"从你这里来。你的段落密度决定了后面 planner 能不能读出"稀疏 memo",writer 能不能写出活人,reviewer 能不能校准硬伤。${contextBlock}${reviewFeedbackBlock}
|
|
101
|
+
|
|
102
|
+
## 书籍元信息
|
|
103
|
+
- 平台:${book.platform}
|
|
104
|
+
- 题材:${gp.name}(${book.genre})
|
|
105
|
+
- 目标章数:${book.targetChapters}章
|
|
106
|
+
- 每章字数:${book.chapterWordCount}字
|
|
107
|
+
- 标题:${book.title}
|
|
108
|
+
|
|
109
|
+
## 题材底色
|
|
110
|
+
${genreBody}
|
|
111
|
+
|
|
112
|
+
## 产出约束(硬性)
|
|
113
|
+
${numericalBlock}
|
|
114
|
+
${powerBlock}
|
|
115
|
+
${eraBlock}
|
|
116
|
+
|
|
117
|
+
## 输出结构(5 个 SECTION,严格按 === SECTION: === 分块,不要漏任何一块)
|
|
118
|
+
|
|
119
|
+
## 去重铁律(必读)
|
|
120
|
+
禁止在多段里重复同一事实。主角的完整角色线只写在 roles;世界铁律只写在 story_frame.世界观底色;节奏原则只写在 volume_map 最后一段;角色当前现状只写在 roles.当前现状;初始钩子只写在 pending_hooks(startChapter=0 行)。**如果本书是年代文/历史同人/都市重生等需要年份、季节、重大历史事件作为锚点的题材**,把环境/时代锚自然织进 story_frame.世界观底色("1985 年 7 月,非典刚过"这类);**修仙/玄幻/系统等没有真实年份的题材直接省略**,不要硬凑。如果一个段落写了另一段的内容,删掉。
|
|
121
|
+
|
|
122
|
+
## 预算(超预算必删)
|
|
123
|
+
- story_frame ≤ 3000 字
|
|
124
|
+
- volume_map ≤ 5000 字
|
|
125
|
+
- roles 总 ≤ 8000 字
|
|
126
|
+
- book_rules ≤ 500 字(仅 YAML)
|
|
127
|
+
- pending_hooks ≤ 2000 字
|
|
128
|
+
|
|
129
|
+
=== SECTION: story_frame ===
|
|
130
|
+
|
|
131
|
+
这是段落式骨架。**4 段**,每段约 600-900 字,不要写表格,不要写 bullet list,写成能被人读下去的段落。段落标题用 \`## \` 开头,段落内部是正经段落。**主角的完整角色线不写在本 section;它的权威来源是 roles/主要角色/<主角>.md。** 本段只需一句指针:"本书主角是 X,完整角色线详见 roles/主要角色/X.md"。
|
|
132
|
+
|
|
133
|
+
### 段 1:主题与基调
|
|
134
|
+
写这本书到底讲的是什么——不是"讲主角如何从弱到强"这种空话,而是具体的命题("一个被时代按在泥里的人,如何选择不被改写"、"当所有人都在撒谎时,坚持记录真相要付出什么代价")。主题下面跟着基调——温情冷冽悲壮肃杀,哪一种?为什么是这种而不是另一种?结尾用一句话指向主角并引向 roles(例:"本书主角是林辞,完整角色线详见 roles/主要角色/林辞.md")。
|
|
135
|
+
|
|
136
|
+
### 段 2:核心冲突与对手定性
|
|
137
|
+
这本书的主要矛盾是什么?不是"正邪对抗",而是"因为 A 相信 X、B 相信 Y,所以他们一定会在某件事上对撞"。主要对手是谁(至少 2 个:一个显性对手 + 一个结构性对手/体制),他们的动机从哪里长出来。对手不是工具,对手有自己的逻辑。
|
|
138
|
+
|
|
139
|
+
### 段 3:世界观底色(铁律 + 质感 + 本书专属规则)
|
|
140
|
+
这个世界的运行规则是什么?3-5 条**不可违反的铁律**——以段落形式写出,不要 bullet。这个世界的质感是什么——湿的还是干的、快的还是慢的、噪的还是静的?给 writer 一个明确的感官锚。**这一段同时承担原先 book_rules 正文里写的"叙事视角 / 本书专属规则 / 核心冲突驱动"等内容**——全部合并到这里写一次就够,不要再去 book_rules 重复。
|
|
141
|
+
|
|
142
|
+
### 段 4:终局方向
|
|
143
|
+
这本书最后一章大概是什么感觉——不是"主角登顶"、"大结局"这种套话,而是**最后一个镜头**大致长什么样。主角最后在哪、做什么、身边有谁、心里想什么。这是给全书所有后面的规划一个远方靶子。
|
|
144
|
+
|
|
145
|
+
=== SECTION: volume_map ===
|
|
146
|
+
|
|
147
|
+
这是分卷段落式地图,**5 段主体 + 1 段节奏原则尾段**。**关键要求:只写到卷级内容**——写清楚每卷的主题、情绪曲线、卷间钩子、角色阶段目标、卷尾不可逆事件。**禁止指定具体章号任务**(不要写"第 17 章让他回家"这种章级布局)。章级规划是 Phase 3 planner 的职责,架构师只搭骨架、不编章目。
|
|
148
|
+
|
|
149
|
+
### 段 1:各卷主题与情绪曲线
|
|
150
|
+
有几卷?每卷的主题一句话,每卷的情绪曲线一段(哪里压、哪里爽、哪里冷、哪里暖)。不要机械的"第一卷打小怪第二卷打大怪",写情绪的流动。
|
|
151
|
+
|
|
152
|
+
### 段 2:卷间钩子与回收承诺
|
|
153
|
+
第 1 卷埋什么钩子、在哪一卷回收;第 2 卷埋什么、在哪一卷回收。段落式写,不要表格。**只写卷级**(如"第 1 卷埋的身世之谜在第 3 卷回收"),不要写具体章号。
|
|
154
|
+
|
|
155
|
+
### 段 3:角色阶段性目标
|
|
156
|
+
主角在第 1 卷末要到什么状态?第 2 卷末?每一卷结束时主角的身份/关系/能力/心境应该是什么。次要角色的阶段性变化也要点到(师父在第 2 卷会死、对手在第 3 卷会黑化等)。写阶段性,不写完整角色线(完整角色线在 roles)。
|
|
157
|
+
|
|
158
|
+
### 段 4:卷尾必须发生的改变
|
|
159
|
+
每一卷最后一章必须发生什么不可逆的事——权力结构改变、关系破裂、秘密暴露、主角身份重定位。写段落,一卷一段。**只写"必须发生什么",不指定是第几章**。
|
|
160
|
+
|
|
161
|
+
### 段 5:节奏原则(具体化 + 通用)
|
|
162
|
+
**这是节奏原则的唯一归宿,不再有独立 rhythm_principles section。** 本段输出 6 条节奏原则。**至少 3 条必须具体化到本书**(例:"前 30 章每 5 章一个小爽点"),其余可保留通用原则(例:"拒绝机械降神"、"高潮前 3-5 章埋伏笔")。具体化 + 通用混合是合法的。反面例子:"节奏要张弛有度"(废话)。正面例子:"前 30 章每 5 章一个小爽点,且小爽点必须落在章末 300 字内"。6 条各写 2-3 句,覆盖(顺序不强制、可替换同权重议题):
|
|
163
|
+
1. 高潮间距——本书大高潮之间最长多少章?(具体化优先)
|
|
164
|
+
2. 喘息频率——高压段多长必须插一章喘息?喘息章承担什么任务?
|
|
165
|
+
3. 钩子密度——每章章末留钩数量,主钩最多允许悬多少章?
|
|
166
|
+
4. 信息释放节奏——主线信息在前 1/3、中段、后 1/3 分别释放多少比例?(可通用)
|
|
167
|
+
5. 爽点节奏——爽点间距多少章一个?什么类型为主?(具体化优先)
|
|
168
|
+
6. 情感节点递进——情感关系每多少章必须有一次实质推进?
|
|
92
169
|
|
|
93
170
|
### 黄金三章法则(前三章必须遵循)
|
|
94
171
|
- 第1章:抛出核心冲突(主角立即面临困境/危机/选择),禁止大段背景灌输
|
|
95
172
|
- 第2章:展示金手指/核心能力(主角如何应对第1章的困境),让读者看到爽点预期
|
|
96
|
-
- 第3
|
|
97
|
-
const bookRulesPrompt = resolvedLanguage === "en"
|
|
98
|
-
? `Generate book_rules.md as YAML frontmatter plus narrative guidance:
|
|
99
|
-
\`\`\`
|
|
100
|
-
---
|
|
101
|
-
version: "1.0"
|
|
102
|
-
protagonist:
|
|
103
|
-
name: (protagonist name)
|
|
104
|
-
personalityLock: [(3-5 personality keywords)]
|
|
105
|
-
behavioralConstraints: [(3-5 behavioral constraints)]
|
|
106
|
-
genreLock:
|
|
107
|
-
primary: ${book.genre}
|
|
108
|
-
forbidden: [(2-3 forbidden style intrusions)]
|
|
109
|
-
${gp.numericalSystem ? `numericalSystemOverrides:
|
|
110
|
-
hardCap: (decide from the setting)
|
|
111
|
-
resourceTypes: [(core resource types)]` : ""}
|
|
112
|
-
prohibitions:
|
|
113
|
-
- (3-5 book-specific prohibitions)
|
|
114
|
-
chapterTypesOverride: []
|
|
115
|
-
fatigueWordsOverride: []
|
|
116
|
-
additionalAuditDimensions: []
|
|
117
|
-
enableFullCastTracking: false
|
|
118
|
-
---
|
|
173
|
+
- 第3章:明确短期目标(主角确立第一个具体可达成的目标),给读者追读理由
|
|
119
174
|
|
|
120
|
-
|
|
121
|
-
(Describe the narrative perspective and style)
|
|
175
|
+
=== SECTION: roles ===
|
|
122
176
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
177
|
+
一人一卡。**主角卡是本书角色线的唯一权威来源**——story_frame 不再写主角的完整角色线,writer/planner 都从这里读。用以下格式分隔:
|
|
178
|
+
|
|
179
|
+
---ROLE---
|
|
180
|
+
tier: major
|
|
181
|
+
name: <角色名>
|
|
182
|
+
---CONTENT---
|
|
183
|
+
(这里写段落式角色卡,下面的小标题必须全部出现,每段至少 3 行正经段落,不要写表格)
|
|
184
|
+
|
|
185
|
+
## 核心标签
|
|
186
|
+
(3-5 个关键词 + 一句话为什么是这些词)
|
|
187
|
+
|
|
188
|
+
## 反差细节
|
|
189
|
+
(1-2 个与核心标签反差的具体细节——"冷酷杀手但会给流浪猫留鱼骨"。反差细节是人物立体化的公式,必须有。)
|
|
190
|
+
|
|
191
|
+
## 人物小传(过往经历)
|
|
192
|
+
(一段段落,说这个人怎么变成现在这样。童年/重大事件/塑造性格的那件事。只写关键过往,简版。)
|
|
193
|
+
|
|
194
|
+
## 主角线(起点 → 终点 → 代价)
|
|
195
|
+
**只有主角必须写本段;其他 major 角色如果分量重也可以写,否则略过。**主角从哪里出发(身份、处境、核心缺陷、一开始最想要什么),到哪里落脚(最终变成什么样的人、拿到/失去什么),为了这个落脚他付出了什么不可逆的代价(关系、身体、信念、某段过去)。不要只写"变强"这种平面变化,要写**内在的位移**。写足写实。
|
|
196
|
+
|
|
197
|
+
## 当前现状(第 0 章初始状态)
|
|
198
|
+
(第 0 章时他在哪、做什么、处境如何、最近最烦心的事。**只写角色个人处境**——初始钩子写在 pending_hooks 的 startChapter=0 行;环境/时代锚(如果是需要年份的题材)织进 story_frame.世界观底色。不再有独立的 current_state section。)
|
|
199
|
+
|
|
200
|
+
## 关系网络
|
|
201
|
+
(与主角、与其他重要角色的关系——一句话一条,关系不是标签是动态。)
|
|
202
|
+
|
|
203
|
+
## 内在驱动
|
|
204
|
+
(他想要什么、为什么想要、愿意付出什么代价。)
|
|
205
|
+
|
|
206
|
+
## 成长变化
|
|
207
|
+
(他在这本书里会经历什么内在位移——变好变坏变复杂,落在哪里。非主角可短可长。)
|
|
208
|
+
|
|
209
|
+
---ROLE---
|
|
210
|
+
tier: major
|
|
211
|
+
name: <下一个主要角色>
|
|
212
|
+
---CONTENT---
|
|
213
|
+
...
|
|
214
|
+
|
|
215
|
+
(主要角色至少 3 个:主角 + 主要对手 + 主要协作者。建议 2-3 主 + 2-3 辅,不要灌水。质量 > 数量。)
|
|
216
|
+
|
|
217
|
+
---ROLE---
|
|
218
|
+
tier: minor
|
|
219
|
+
name: <次要角色名>
|
|
220
|
+
---CONTENT---
|
|
221
|
+
(次要角色简化版,只需要 4 个小标题:核心标签 / 反差细节 / 当前现状 / 与主角关系,每段 1-2 行即可)
|
|
222
|
+
|
|
223
|
+
(次要角色 3-5 个,按出场密度给。)
|
|
224
|
+
|
|
225
|
+
=== SECTION: book_rules ===
|
|
226
|
+
|
|
227
|
+
**只输出 YAML frontmatter 一块——零段落正文。** 所有的"叙事视角 / 本书专属规则 / 核心冲突驱动"等段落已经合并到 story_frame.世界观底色,不要在这里重复写。
|
|
127
228
|
\`\`\`
|
|
128
229
|
---
|
|
129
230
|
version: "1.0"
|
|
@@ -144,137 +245,318 @@ fatigueWordsOverride: []
|
|
|
144
245
|
additionalAuditDimensions: []
|
|
145
246
|
enableFullCastTracking: false
|
|
146
247
|
---
|
|
248
|
+
\`\`\`
|
|
147
249
|
|
|
148
|
-
|
|
149
|
-
(描述本书叙事视角和风格)
|
|
150
|
-
|
|
151
|
-
## 核心冲突驱动
|
|
152
|
-
(描述本书的核心矛盾和驱动力)
|
|
153
|
-
\`\`\``;
|
|
154
|
-
const currentStatePrompt = resolvedLanguage === "en"
|
|
155
|
-
? `Initial state card (Chapter 0), include:
|
|
156
|
-
| Field | Value |
|
|
157
|
-
| --- | --- |
|
|
158
|
-
| Current Chapter | 0 |
|
|
159
|
-
| Current Location | (starting location) |
|
|
160
|
-
| Protagonist State | (initial condition) |
|
|
161
|
-
| Current Goal | (first goal) |
|
|
162
|
-
| Current Constraint | (initial constraint) |
|
|
163
|
-
| Current Alliances | (initial relationships) |
|
|
164
|
-
| Current Conflict | (first conflict) |`
|
|
165
|
-
: `初始状态卡(第0章),包含:
|
|
166
|
-
| 字段 | 值 |
|
|
167
|
-
|------|-----|
|
|
168
|
-
| 当前章节 | 0 |
|
|
169
|
-
| 当前位置 | (起始地点) |
|
|
170
|
-
| 主角状态 | (初始状态) |
|
|
171
|
-
| 当前目标 | (第一个目标) |
|
|
172
|
-
| 当前限制 | (初始限制) |
|
|
173
|
-
| 当前敌我 | (初始关系) |
|
|
174
|
-
| 当前冲突 | (第一个冲突) |`;
|
|
175
|
-
const pendingHooksPrompt = resolvedLanguage === "en"
|
|
176
|
-
? `Initial hook pool (Markdown table):
|
|
177
|
-
| hook_id | start_chapter | type | status | last_advanced_chapter | expected_payoff | payoff_timing | notes |
|
|
250
|
+
=== SECTION: pending_hooks ===
|
|
178
251
|
|
|
179
|
-
|
|
180
|
-
- Column 5 must be a pure chapter number, never natural-language description
|
|
181
|
-
- During book creation, all planned hooks are still unapplied, so last_advanced_chapter = 0
|
|
182
|
-
- Column 7 must be one of: immediate / near-term / mid-arc / slow-burn / endgame
|
|
183
|
-
- If you want to describe the initial clue/signal, put it in notes instead of column 5`
|
|
184
|
-
: `初始伏笔池(Markdown表格):
|
|
252
|
+
初始伏笔池(Markdown表格,8 列基础版):
|
|
185
253
|
| hook_id | 起始章节 | 类型 | 状态 | 最近推进 | 预期回收 | 回收节奏 | 备注 |
|
|
186
254
|
|
|
187
255
|
伏笔表规则:
|
|
188
256
|
- 第5列必须是纯数字章节号,不能写自然语言描述
|
|
189
257
|
- 建书阶段所有伏笔都还没正式推进,所以第5列统一填 0
|
|
190
258
|
- 第7列必须填写:立即 / 近期 / 中程 / 慢烧 / 终局 之一
|
|
191
|
-
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
259
|
+
- 初始线索放备注列,不放第5列
|
|
260
|
+
- **初始世界状态 / 初始敌我关系** 如果有关键信息(例如"主角身上带着父亲的笔记本"、"体制已经开始监视码头"),可以作为 startChapter=0 的种子行录入,备注列说明其"初始状态"属性
|
|
261
|
+
|
|
262
|
+
## 最后强调
|
|
263
|
+
- 符合${book.platform}平台口味、${gp.name}题材特征
|
|
264
|
+
- 主角人设鲜明、行为边界清晰
|
|
265
|
+
- 伏笔前后呼应、配角有独立动机不是工具人
|
|
266
|
+
- **story_frame / volume_map / roles 必须是段落密度,不要退化成 bullet**
|
|
267
|
+
- **book_rules 只留 YAML,不要写段落正文**
|
|
268
|
+
- **不要输出 rhythm_principles 或 current_state 独立 section**——节奏原则合并进 volume_map 尾段;角色初始状态写在 roles.当前现状,初始钩子写在 pending_hooks(startChapter=0 行),环境/时代锚(仅历史/年代/都市重生等需要年份的题材)织进 story_frame.世界观底色,不要硬凑`;
|
|
269
|
+
}
|
|
270
|
+
// -------------------------------------------------------------------------
|
|
271
|
+
// Phase 5 段落式 prompt — en
|
|
272
|
+
// -------------------------------------------------------------------------
|
|
273
|
+
buildEnglishFoundationPrompt(book, gp, genreBody, contextBlock, reviewFeedbackBlock, numericalBlock, powerBlock, eraBlock) {
|
|
274
|
+
return `You are the architect of this book. Your only job is to produce **prose-density foundation design** — not tables, not schema, not bullet lists. The book's aura comes from your prose density: the Phase 3 planner reads sparse memos out of your volume_map only if it was written to chapter-level prose; the writer only produces living characters because your role sheets carry contrast details; the reviewer only catches hard errors because your story_frame set the tonal anchors.${contextBlock}${reviewFeedbackBlock}
|
|
275
|
+
|
|
276
|
+
## Book metadata
|
|
277
|
+
- Platform: ${book.platform}
|
|
278
|
+
- Genre: ${gp.name} (${book.genre})
|
|
279
|
+
- Target chapters: ${book.targetChapters}
|
|
280
|
+
- Chapter length: ${book.chapterWordCount}
|
|
281
|
+
- Title: ${book.title}
|
|
282
|
+
|
|
283
|
+
## Genre body
|
|
284
|
+
${genreBody}
|
|
285
|
+
|
|
286
|
+
## Output constraints
|
|
205
287
|
${numericalBlock}
|
|
206
288
|
${powerBlock}
|
|
207
289
|
${eraBlock}
|
|
208
|
-
3. 主角人设鲜明,有明确行为边界
|
|
209
|
-
4. 伏笔前后呼应,不留悬空线
|
|
210
|
-
5. 配角有独立动机,不是工具人`;
|
|
211
|
-
const systemPrompt = `你是一个专业的网络小说架构师。你的任务是为一本新的${gp.name}小说生成完整的基础设定。${contextBlock}${reviewFeedbackBlock}
|
|
212
290
|
|
|
213
|
-
|
|
214
|
-
- 平台:${book.platform}
|
|
215
|
-
- 题材:${gp.name}(${book.genre})
|
|
216
|
-
- 目标章数:${book.targetChapters}章
|
|
217
|
-
- 每章字数:${book.chapterWordCount}字
|
|
291
|
+
## Output contract (5 === SECTION: === blocks)
|
|
218
292
|
|
|
219
|
-
##
|
|
293
|
+
## Deduplication rule (MANDATORY)
|
|
294
|
+
Do not duplicate the same fact across sections. The protagonist's full character arc lives only in roles; world hard-rules live only in story_frame; rhythm principles live only in the last paragraph of volume_map; character initial status lives only in roles.Current_State; initial hooks live only in pending_hooks (start_chapter=0 rows). **When the book is period fiction / historical fanfic / urban reincarnation** — anything pinned to a real year, season, or historic marker — weave the environment/era anchor into story_frame's world-tonal-ground paragraph. **For cultivation / high-fantasy / system genres that have no real-world year, skip it entirely** — do not fabricate an era anchor. If a section repeats content that belongs elsewhere, delete it.
|
|
220
295
|
|
|
221
|
-
|
|
296
|
+
## Output budget (over-budget means cut)
|
|
297
|
+
- story_frame ≤ 3000 chars
|
|
298
|
+
- volume_map ≤ 5000 chars
|
|
299
|
+
- roles ≤ 8000 chars total
|
|
300
|
+
- book_rules ≤ 500 chars (YAML only)
|
|
301
|
+
- pending_hooks ≤ 2000 chars
|
|
222
302
|
|
|
223
|
-
|
|
303
|
+
=== SECTION: story_frame ===
|
|
224
304
|
|
|
225
|
-
|
|
305
|
+
Four prose sections, ~600-900 chars each. No tables. No bullet lists. Real paragraphs. **Do NOT write the protagonist's full character arc here** — that is owned by roles/主要角色/<protagonist>.md. Use a single-line pointer inside this block.
|
|
226
306
|
|
|
227
|
-
|
|
228
|
-
|
|
307
|
+
## 01_Theme_and_Tonal_Ground
|
|
308
|
+
What is this book actually about — not "hero grows from weak to strong" (empty), but a concrete proposition. Then the tonal ground: warm / cold / fierce / severe — which, and why this and not another. End with a one-line pointer to the protagonist role file.
|
|
229
309
|
|
|
230
|
-
|
|
231
|
-
|
|
310
|
+
## 02_Core_Conflict_and_Opponent
|
|
311
|
+
The book's main tension — not "good vs evil" but "because A believes X and B believes Y, they will inevitably collide on Z". At least two opponents: one visible, one structural/systemic. Opponents have their own logic.
|
|
312
|
+
|
|
313
|
+
## 03_World_Tonal_Ground (hard rules + sensory tone + book-specific rules)
|
|
314
|
+
The world's operating rules. 3-5 unbreakable laws written as prose, not bullets. Sensory texture: wet or dry, fast or slow, noisy or quiet — give the writer an anchor. **This paragraph also absorbs the narrative prose that used to live in book_rules** (narrative perspective, core conflict driver, book-specific rules). Write them all here once. Do not repeat them in book_rules.
|
|
315
|
+
|
|
316
|
+
## 04_Endgame_Direction
|
|
317
|
+
What the last chapter roughly feels like. The final shot: where, doing what, around whom, thinking what. A distant target for every planner call downstream.
|
|
318
|
+
|
|
319
|
+
=== SECTION: volume_map ===
|
|
320
|
+
|
|
321
|
+
Prose volume map, **5 sections + 1 closing rhythm paragraph**. **Critical requirement: stay at volume-level prose only**. No chapter-level task assignment.
|
|
322
|
+
|
|
323
|
+
## 01_Volume_Themes_and_Emotional_Curves
|
|
324
|
+
How many volumes? Each volume's theme in one sentence; each volume's emotional curve as a paragraph.
|
|
325
|
+
|
|
326
|
+
## 02_Cross_Volume_Hooks_and_Payoff_Promises
|
|
327
|
+
Volume 1 plants hook A, paid off in volume N; volume 2 plants hook B, paid off in volume M. Prose, not tables. **Stay at volume-level**.
|
|
328
|
+
|
|
329
|
+
## 03_Character_Stage_Goals
|
|
330
|
+
Protagonist's state at end of vol 1, vol 2, ... Supporting characters' stage changes. Stage goals only — full character arc lives in roles.
|
|
331
|
+
|
|
332
|
+
## 04_Volume_End_Mandatory_Changes
|
|
333
|
+
Each volume's last chapter must contain an irreversible event. Prose, one paragraph per volume.
|
|
334
|
+
|
|
335
|
+
## 05_Rhythm_Principles (concrete + universal)
|
|
336
|
+
**Single home for rhythm principles — no separate rhythm_principles section.** 6 rhythm principles. **At least 3 must be concretized for this book**; the rest may stay as universal rules.
|
|
337
|
+
|
|
338
|
+
### Golden First Three Chapters Rule
|
|
339
|
+
- Chapter 1: throw the core conflict immediately; no large background dump
|
|
340
|
+
- Chapter 2: show the core edge / ability / leverage that answers Chapter 1's pressure
|
|
341
|
+
- Chapter 3: establish the first concrete short-term goal that gives readers a reason to continue
|
|
342
|
+
|
|
343
|
+
=== SECTION: roles ===
|
|
344
|
+
|
|
345
|
+
One-file-per-character prose. **The protagonist card is the single source of truth for the protagonist's full character arc** — story_frame no longer carries it, and writer/planner both read it here.
|
|
346
|
+
|
|
347
|
+
---ROLE---
|
|
348
|
+
tier: major
|
|
349
|
+
name: <character name>
|
|
350
|
+
---CONTENT---
|
|
351
|
+
## Core_Tags
|
|
352
|
+
(3-5 tags + one sentence on why)
|
|
353
|
+
|
|
354
|
+
## Contrast_Detail
|
|
355
|
+
(1-2 concrete details that contradict the core tags.)
|
|
356
|
+
|
|
357
|
+
## Back_Story
|
|
358
|
+
(Prose paragraph — how this person became who they are.)
|
|
359
|
+
|
|
360
|
+
## Protagonist_Arc (start → end → cost)
|
|
361
|
+
**Mandatory for the protagonist; optional for other majors with substantial arcs.** Internal displacement, not just growth.
|
|
362
|
+
|
|
363
|
+
## Current_State (initial state at chapter 0)
|
|
364
|
+
(Character-only: initial hooks go in pending_hooks start_chapter=0 rows; environment/era anchors weave into story_frame.)
|
|
365
|
+
|
|
366
|
+
## Relationship_Network
|
|
367
|
+
(With protagonist, with other majors. One line each.)
|
|
368
|
+
|
|
369
|
+
## Inner_Driver
|
|
370
|
+
(What they want, why, what they're willing to pay.)
|
|
371
|
+
|
|
372
|
+
## Growth_Arc
|
|
373
|
+
(Internal displacement across the book.)
|
|
374
|
+
|
|
375
|
+
---ROLE---
|
|
376
|
+
tier: major
|
|
377
|
+
name: <next major>
|
|
378
|
+
---CONTENT---
|
|
379
|
+
...
|
|
380
|
+
|
|
381
|
+
(Aim for 2-3 majors + 2-3 supporting majors. Quality over quantity.)
|
|
382
|
+
|
|
383
|
+
---ROLE---
|
|
384
|
+
tier: minor
|
|
385
|
+
name: <minor name>
|
|
386
|
+
---CONTENT---
|
|
387
|
+
(Simplified: Core_Tags / Contrast_Detail / Current_State / Relationship_to_Protagonist only, 1-2 lines each.)
|
|
388
|
+
|
|
389
|
+
(3-5 minors.)
|
|
232
390
|
|
|
233
391
|
=== SECTION: book_rules ===
|
|
234
|
-
${bookRulesPrompt}
|
|
235
392
|
|
|
236
|
-
|
|
237
|
-
|
|
393
|
+
**Output ONLY the YAML frontmatter block — zero prose.** All narrative guidance moved into story_frame.03_World_Tonal_Ground.
|
|
394
|
+
\`\`\`
|
|
395
|
+
---
|
|
396
|
+
version: "1.0"
|
|
397
|
+
protagonist:
|
|
398
|
+
name: (protagonist name)
|
|
399
|
+
personalityLock: [(3-5 personality keywords)]
|
|
400
|
+
behavioralConstraints: [(3-5 behavioral constraints)]
|
|
401
|
+
genreLock:
|
|
402
|
+
primary: ${book.genre}
|
|
403
|
+
forbidden: [(2-3 forbidden style intrusions)]
|
|
404
|
+
${gp.numericalSystem ? `numericalSystemOverrides:
|
|
405
|
+
hardCap: (decide from setting)
|
|
406
|
+
resourceTypes: [(core resource types)]` : ""}
|
|
407
|
+
prohibitions:
|
|
408
|
+
- (3-5 book-specific prohibitions)
|
|
409
|
+
chapterTypesOverride: []
|
|
410
|
+
fatigueWordsOverride: []
|
|
411
|
+
additionalAuditDimensions: []
|
|
412
|
+
enableFullCastTracking: false
|
|
413
|
+
---
|
|
414
|
+
\`\`\`
|
|
238
415
|
|
|
239
416
|
=== SECTION: pending_hooks ===
|
|
240
|
-
${pendingHooksPrompt}
|
|
241
417
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
418
|
+
Initial hook pool (Markdown table, 8-column base):
|
|
419
|
+
| hook_id | start_chapter | type | status | last_advanced_chapter | expected_payoff | payoff_timing | notes |
|
|
420
|
+
|
|
421
|
+
Rules:
|
|
422
|
+
- Column 5 is a pure chapter number, not narrative description
|
|
423
|
+
- At book creation all planned hooks have last_advanced_chapter = 0
|
|
424
|
+
- Column 7 must be: immediate / near-term / mid-arc / slow-burn / endgame
|
|
425
|
+
- Put initial signal text in notes, not column 5
|
|
426
|
+
- **Initial world / alliance state**: any load-bearing initial condition can be seeded as a start_chapter=0 row with a notes tag indicating its initial-state nature
|
|
427
|
+
|
|
428
|
+
## Final emphasis
|
|
429
|
+
- Fit ${book.platform} platform taste and ${gp.name} genre traits
|
|
430
|
+
- Protagonist persona clear with sharp behavioral boundaries
|
|
431
|
+
- Hooks planted with payoff promises; supporting characters have independent motivation
|
|
432
|
+
- **story_frame / volume_map / roles must be prose density — no bullet-list degradation**
|
|
433
|
+
- **book_rules is YAML only — no prose body**
|
|
434
|
+
- **Do NOT emit rhythm_principles or current_state as separate sections** — rhythm principles live in the last paragraph of volume_map; character initial status goes in roles.Current_State; initial hooks go in pending_hooks (start_chapter=0 rows); environment/era anchors (only when the genre has a real year) are woven into story_frame`;
|
|
254
435
|
}
|
|
255
|
-
|
|
436
|
+
/**
|
|
437
|
+
* Write architect foundation output to disk.
|
|
438
|
+
*
|
|
439
|
+
* @param mode
|
|
440
|
+
* - "init"(默认):首次建书。写架构稿 + 初始化所有运行时状态文件
|
|
441
|
+
* (current_state / pending_hooks / particle_ledger / subplot_board /
|
|
442
|
+
* emotional_arcs)为空模板。
|
|
443
|
+
* - "revise":在已有书上重写架构稿。**只改架构稿相关文件**——outline/ /
|
|
444
|
+
* roles/ / 4 个 legacy shim——**完全不动运行时状态文件**。这和
|
|
445
|
+
* context-transform 注入给 LLM 的 upgrade hint 承诺"只改架构稿不动已写
|
|
446
|
+
* 章节"一致;如果在 revise 模式下触动运行时文件,会把 consolidator 累积
|
|
447
|
+
* 的章节状态、伏笔推进、资源账本等全部重置。
|
|
448
|
+
*/
|
|
449
|
+
async writeFoundationFiles(bookDir, output, numericalSystem = true, language = "zh", mode = "init") {
|
|
256
450
|
const storyDir = join(bookDir, "story");
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
451
|
+
const outlineDir = join(storyDir, "outline");
|
|
452
|
+
const rolesDir = join(storyDir, "roles");
|
|
453
|
+
const rolesMajorDir = join(rolesDir, "主要角色");
|
|
454
|
+
const rolesMinorDir = join(rolesDir, "次要角色");
|
|
455
|
+
await Promise.all([
|
|
456
|
+
mkdir(storyDir, { recursive: true }),
|
|
457
|
+
mkdir(outlineDir, { recursive: true }),
|
|
458
|
+
mkdir(rolesMajorDir, { recursive: true }),
|
|
459
|
+
mkdir(rolesMinorDir, { recursive: true }),
|
|
460
|
+
]);
|
|
461
|
+
const writes = [];
|
|
462
|
+
const storyFrame = output.storyFrame ?? "";
|
|
463
|
+
const volumeMap = output.volumeMap ?? "";
|
|
464
|
+
const rhythmPrinciples = output.rhythmPrinciples ?? "";
|
|
465
|
+
const roles = output.roles ?? [];
|
|
466
|
+
const isPhase5Output = storyFrame.trim().length > 0;
|
|
467
|
+
// debug: 让排查时能一眼看出 LLM 按哪套格式输出、落到哪套文件布局。
|
|
468
|
+
// 如果用户新建书后发现只有 story_bible.md / 没有 outline/,看这行日志能
|
|
469
|
+
// 确认是 LLM 没按新 prompt 输出(走了 legacy 分支),而不是 writeFoundationFiles
|
|
470
|
+
// 本身的 bug。
|
|
471
|
+
this.ctx.logger?.info(`[architect] writeFoundationFiles layout=${isPhase5Output ? "phase5" : "legacy"} ` +
|
|
472
|
+
`storyFrame=${storyFrame.length}chars volumeMap=${volumeMap.length}chars roles=${roles.length}`);
|
|
473
|
+
// revise 模式 + LLM 回退 legacy 输出 → 必须抛错,不写任何文件。
|
|
474
|
+
// 原因:如果走 legacy 分支覆盖 story_bible.md 等 flat 文件,而 outline/
|
|
475
|
+
// 和 roles/ 保持不变,会让 Phase 5 书进入"outline 新 + shim 老 + roles
|
|
476
|
+
// 残留"的混乱状态;读取端的 fallback 逻辑会读到互相矛盾的内容。
|
|
477
|
+
// 抛错让用户立刻感知 LLM 响应异常,而不是默默破坏书的数据完整性。
|
|
478
|
+
if (mode === "revise" && !isPhase5Output) {
|
|
479
|
+
throw new Error("Architect revise mode produced legacy-format output (storyFrame empty). " +
|
|
480
|
+
"This likely means the LLM didn't follow the reviseFrom prompt. " +
|
|
481
|
+
"The book's architecture files have NOT been modified. " +
|
|
482
|
+
"Check prompt / model / temperature and try again.");
|
|
483
|
+
}
|
|
484
|
+
// revise 模式下先清空旧 role 文件,再写本次 architect 输出——避免改名 / 删除 /
|
|
485
|
+
// 合并角色后的旧卡片残留被 readRoleCards 当作有效角色继续注入(见 Bug 3)。
|
|
486
|
+
// 备份由上游 runner.reviseFoundation 在调用前完成,这里可以安全清空。
|
|
487
|
+
// init 模式下目录本来就是空的,不需要清。
|
|
488
|
+
// 放在 if (isPhase5Output) 外面,保证所有 revise 场景都清。
|
|
489
|
+
if (mode === "revise") {
|
|
490
|
+
await rm(rolesMajorDir, { recursive: true, force: true });
|
|
491
|
+
await rm(rolesMinorDir, { recursive: true, force: true });
|
|
492
|
+
await mkdir(rolesMajorDir, { recursive: true });
|
|
493
|
+
await mkdir(rolesMinorDir, { recursive: true });
|
|
494
|
+
}
|
|
495
|
+
if (isPhase5Output) {
|
|
496
|
+
// book_rules 的 YAML frontmatter 提取后拼到 story_frame.md 顶部,作为权威位置。
|
|
497
|
+
const { frontmatter: bookRulesFrontmatter, body: bookRulesBody } = extractYamlFrontmatter(output.bookRules);
|
|
498
|
+
const storyFrameWithFrontmatter = bookRulesFrontmatter
|
|
499
|
+
? `${bookRulesFrontmatter}\n\n${storyFrame.trim()}\n`
|
|
500
|
+
: storyFrame;
|
|
501
|
+
// Phase 5 权威文件
|
|
502
|
+
writes.push(writeFile(join(outlineDir, "story_frame.md"), storyFrameWithFrontmatter, "utf-8"));
|
|
503
|
+
writes.push(writeFile(join(outlineDir, "volume_map.md"), volumeMap, "utf-8"));
|
|
504
|
+
if (rhythmPrinciples.trim()) {
|
|
505
|
+
const rhythmFileName = language === "en" ? "rhythm_principles.md" : "节奏原则.md";
|
|
506
|
+
writes.push(writeFile(join(outlineDir, rhythmFileName), rhythmPrinciples, "utf-8"));
|
|
507
|
+
}
|
|
508
|
+
// 一人一卡
|
|
509
|
+
for (const role of roles) {
|
|
510
|
+
const targetDir = role.tier === "major" ? rolesMajorDir : rolesMinorDir;
|
|
511
|
+
const safeName = role.name.replace(/[/\\:*?"<>|]/g, "_").trim();
|
|
512
|
+
if (!safeName)
|
|
513
|
+
continue;
|
|
514
|
+
writes.push(writeFile(join(targetDir, `${safeName}.md`), role.content, "utf-8"));
|
|
515
|
+
}
|
|
516
|
+
// Legacy shim 文件
|
|
517
|
+
writes.push(writeFile(join(storyDir, "story_bible.md"), this.buildStoryBibleShim(storyFrameWithFrontmatter, language), "utf-8"));
|
|
518
|
+
writes.push(writeFile(join(storyDir, "volume_outline.md"), volumeMap, "utf-8"));
|
|
519
|
+
writes.push(writeFile(join(storyDir, "character_matrix.md"), this.buildCharacterMatrixShim(roles, language), "utf-8"));
|
|
520
|
+
writes.push(writeFile(join(storyDir, "book_rules.md"), this.buildBookRulesShim(bookRulesBody, language), "utf-8"));
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
// Legacy 输出路径(仅 init 模式——revise + legacy 已在上面抛错):
|
|
524
|
+
// LLM 还按老 prompt 输出 story_bible / volume_outline,走 Phase 4 落盘方式。
|
|
525
|
+
writes.push(writeFile(join(storyDir, "story_bible.md"), output.storyBible, "utf-8"));
|
|
526
|
+
writes.push(writeFile(join(storyDir, "volume_outline.md"), output.volumeOutline, "utf-8"));
|
|
527
|
+
writes.push(writeFile(join(storyDir, "book_rules.md"), output.bookRules, "utf-8"));
|
|
528
|
+
writes.push(writeFile(join(storyDir, "character_matrix.md"), language === "en"
|
|
529
|
+
? "# Character Matrix\n\n<!-- One ## section per character. Add new characters as new ## blocks. -->\n"
|
|
530
|
+
: "# 角色矩阵\n\n<!-- 每个角色一个 ## 块,新角色追加新 ## 即可。 -->\n", "utf-8"));
|
|
531
|
+
}
|
|
532
|
+
// 运行时状态文件——**只在 init 模式写**。revise 模式下这些文件已经存在且
|
|
533
|
+
// 被 consolidator 累积了章节状态(伏笔进度、角色位置、资源账本、情感曲线
|
|
534
|
+
// 等),重写会把已写章节的真实状态全部清零,违反 context-transform 里给
|
|
535
|
+
// LLM 的承诺"升级只改架构稿,不动已写的章节"(见 Bug 1)。
|
|
536
|
+
if (mode === "init") {
|
|
537
|
+
// current_state.md — 架构师不再产出结构化初始状态,给占位 seed;运行时由
|
|
538
|
+
// consolidator 每章追加。如果 output 里带了内容(legacy 输出或 reviser
|
|
539
|
+
// 生成),直接用。
|
|
540
|
+
const currentStateSeed = output.currentState?.trim()
|
|
541
|
+
? output.currentState
|
|
542
|
+
: (language === "en"
|
|
543
|
+
? "# Current State\n\n> Seeded at book creation. Runtime state is appended by the consolidator after each chapter. Initial per-character state lives in roles/*.Current_State; load-bearing initial world facts live in pending_hooks rows with start_chapter=0.\n"
|
|
544
|
+
: "# 当前状态\n\n> 建书时占位。运行时每章之后由 consolidator 追加最新状态。每个角色的初始状态详见 roles/*.当前现状;承重的初始世界设定见 pending_hooks 里 startChapter=0 的行。\n");
|
|
545
|
+
writes.push(writeFile(join(storyDir, "current_state.md"), currentStateSeed, "utf-8"));
|
|
546
|
+
writes.push(writeFile(join(storyDir, "pending_hooks.md"), output.pendingHooks, "utf-8"));
|
|
547
|
+
// 运行时 append log 文件,下游 state-validator / consolidator 依赖这些存在。
|
|
548
|
+
if (numericalSystem) {
|
|
549
|
+
writes.push(writeFile(join(storyDir, "particle_ledger.md"), language === "en"
|
|
550
|
+
? "# Resource Ledger\n\n| Chapter | Opening Value | Source | Integrity | Delta | Closing Value | Evidence |\n| --- | --- | --- | --- | --- | --- | --- |\n| 0 | 0 | Initialization | - | 0 | 0 | Initial book state |\n"
|
|
551
|
+
: "# 资源账本\n\n| 章节 | 期初值 | 来源 | 完整度 | 增量 | 期末值 | 依据 |\n|------|--------|------|--------|------|--------|------|\n| 0 | 0 | 初始化 | - | 0 | 0 | 开书初始 |\n", "utf-8"));
|
|
552
|
+
}
|
|
553
|
+
writes.push(writeFile(join(storyDir, "subplot_board.md"), language === "en"
|
|
554
|
+
? "# Subplot Board\n\n| Subplot ID | Subplot | Related Characters | Start Chapter | Last Active Chapter | Chapters Since | Status | Progress Summary | Payoff ETA |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n"
|
|
555
|
+
: "# 支线进度板\n\n| 支线ID | 支线名 | 相关角色 | 起始章 | 最近活跃章 | 距今章数 | 状态 | 进度概述 | 回收ETA |\n|--------|--------|----------|--------|------------|----------|------|----------|---------|\n", "utf-8"));
|
|
556
|
+
writes.push(writeFile(join(storyDir, "emotional_arcs.md"), language === "en"
|
|
557
|
+
? "# Emotional Arcs\n\n| Character | Chapter | Emotional State | Trigger Event | Intensity (1-10) | Arc Direction |\n| --- | --- | --- | --- | --- | --- |\n"
|
|
558
|
+
: "# 情感弧线\n\n| 角色 | 章节 | 情绪状态 | 触发事件 | 强度(1-10) | 弧线方向 |\n|------|------|----------|----------|------------|----------|\n", "utf-8"));
|
|
269
559
|
}
|
|
270
|
-
// Initialize new truth files
|
|
271
|
-
writes.push(writeFile(join(storyDir, "subplot_board.md"), language === "en"
|
|
272
|
-
? "# Subplot Board\n\n| Subplot ID | Subplot | Related Characters | Start Chapter | Last Active Chapter | Chapters Since | Status | Progress Summary | Payoff ETA |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n"
|
|
273
|
-
: "# 支线进度板\n\n| 支线ID | 支线名 | 相关角色 | 起始章 | 最近活跃章 | 距今章数 | 状态 | 进度概述 | 回收ETA |\n|--------|--------|----------|--------|------------|----------|------|----------|---------|\n", "utf-8"), writeFile(join(storyDir, "emotional_arcs.md"), language === "en"
|
|
274
|
-
? "# Emotional Arcs\n\n| Character | Chapter | Emotional State | Trigger Event | Intensity (1-10) | Arc Direction |\n| --- | --- | --- | --- | --- | --- |\n"
|
|
275
|
-
: "# 情感弧线\n\n| 角色 | 章节 | 情绪状态 | 触发事件 | 强度(1-10) | 弧线方向 |\n|------|------|----------|----------|------------|----------|\n", "utf-8"), writeFile(join(storyDir, "character_matrix.md"), language === "en"
|
|
276
|
-
? "# Character Matrix\n\n<!-- One ## section per character. Add new characters as new ## blocks. -->\n"
|
|
277
|
-
: "# 角色矩阵\n\n<!-- 每个角色一个 ## 块,新角色追加新 ## 即可。 -->\n", "utf-8"));
|
|
278
560
|
await Promise.all(writes);
|
|
279
561
|
}
|
|
280
562
|
/**
|
|
@@ -468,7 +750,7 @@ The volume_outline should naturally extend the existing narrative arc. Continue
|
|
|
468
750
|
3. **场景新鲜度**:续写部分至少50%的关键场景发生在导入章节未出现的地点或情境中
|
|
469
751
|
4. **不重复会议**:如果导入章节以会议/讨论结束,续写必须从行动开始,不能再开一轮会`
|
|
470
752
|
: `## 续写方向
|
|
471
|
-
|
|
753
|
+
卷纲应自然延续已有故事线。从导入章节的结尾处接续——推进现有冲突、兑现已埋伏笔、引入从当前局势中有机产生的新变数。不要回顾已知信息。`;
|
|
472
754
|
const workingModeEn = isSeries
|
|
473
755
|
? `## Working Mode
|
|
474
756
|
|
|
@@ -699,24 +981,110 @@ ${trimmed}\n`;
|
|
|
699
981
|
const normalizedName = this.normalizeSectionName(rawName);
|
|
700
982
|
parsedSections.set(normalizedName, content.slice(start, end).trim());
|
|
701
983
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
984
|
+
// Phase 5 新 sections
|
|
985
|
+
const storyFrame = parsedSections.get("story_frame") ?? "";
|
|
986
|
+
const volumeMap = parsedSections.get("volume_map") ?? "";
|
|
987
|
+
const rhythmPrinciples = parsedSections.get("rhythm_principles") ?? "";
|
|
988
|
+
const rolesRaw = parsedSections.get("roles") ?? "";
|
|
989
|
+
// Legacy sections — 当 LLM 还按老 prompt 输出时兜底。
|
|
990
|
+
const legacyStoryBible = parsedSections.get("story_bible") ?? "";
|
|
991
|
+
const legacyVolumeOutline = parsedSections.get("volume_outline") ?? "";
|
|
992
|
+
const bookRules = parsedSections.get("book_rules");
|
|
993
|
+
const currentStateLegacy = parsedSections.get("current_state") ?? "";
|
|
994
|
+
const pendingHooksRaw = parsedSections.get("pending_hooks");
|
|
995
|
+
// 用老名字输出且没有 story_frame/volume_map 时,roles 可空(走 legacy shim fallback)。
|
|
996
|
+
const usingLegacyOutlineNames = !storyFrame && !volumeMap
|
|
997
|
+
&& (legacyStoryBible.length > 0 || legacyVolumeOutline.length > 0);
|
|
998
|
+
const effectiveStoryFrame = storyFrame || legacyStoryBible;
|
|
999
|
+
const effectiveVolumeMap = volumeMap || legacyVolumeOutline;
|
|
1000
|
+
const missing = [];
|
|
1001
|
+
if (!effectiveStoryFrame)
|
|
1002
|
+
missing.push("story_frame");
|
|
1003
|
+
if (!effectiveVolumeMap)
|
|
1004
|
+
missing.push("volume_map");
|
|
1005
|
+
if (!rolesRaw.trim() && !usingLegacyOutlineNames)
|
|
1006
|
+
missing.push("roles");
|
|
1007
|
+
if (!bookRules)
|
|
1008
|
+
missing.push("book_rules");
|
|
1009
|
+
if (!pendingHooksRaw)
|
|
1010
|
+
missing.push("pending_hooks");
|
|
1011
|
+
if (missing.length > 0) {
|
|
1012
|
+
throw new Error(`Architect output missing required section${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}`);
|
|
1013
|
+
}
|
|
1014
|
+
const roles = this.parseRoles(rolesRaw);
|
|
1015
|
+
const pendingHooks = this.normalizePendingHooksSection(this.stripTrailingAssistantCoda(pendingHooksRaw));
|
|
1016
|
+
// Shim-facing 老字段:新 prompt 下用 buildStoryBibleShim 生成指针内容;
|
|
1017
|
+
// 老 prompt 下直接用老内容。volumeOutline 也同理。
|
|
1018
|
+
const storyBible = legacyStoryBible || effectiveStoryFrame;
|
|
1019
|
+
const volumeOutline = legacyVolumeOutline || effectiveVolumeMap;
|
|
712
1020
|
return {
|
|
713
|
-
storyBible
|
|
714
|
-
volumeOutline
|
|
715
|
-
bookRules:
|
|
716
|
-
|
|
717
|
-
|
|
1021
|
+
storyBible,
|
|
1022
|
+
volumeOutline,
|
|
1023
|
+
bookRules: bookRules,
|
|
1024
|
+
// current_state 在 Phase 5 下不是必需 section——writeFoundationFiles 会 seed
|
|
1025
|
+
// 一个占位文件,consolidator 运行时按章追加。
|
|
1026
|
+
currentState: currentStateLegacy,
|
|
1027
|
+
pendingHooks,
|
|
1028
|
+
storyFrame: effectiveStoryFrame,
|
|
1029
|
+
volumeMap: effectiveVolumeMap,
|
|
1030
|
+
rhythmPrinciples,
|
|
1031
|
+
roles,
|
|
718
1032
|
};
|
|
719
1033
|
}
|
|
1034
|
+
/** Parse ---ROLE---...---CONTENT--- 块。畸形块静默丢弃。 */
|
|
1035
|
+
parseRoles(raw) {
|
|
1036
|
+
if (!raw.trim())
|
|
1037
|
+
return [];
|
|
1038
|
+
const blocks = raw.split(/^---ROLE---$/m).map((chunk) => chunk.trim()).filter(Boolean);
|
|
1039
|
+
const roles = [];
|
|
1040
|
+
for (const block of blocks) {
|
|
1041
|
+
const contentSplit = block.split(/^---CONTENT---$/m);
|
|
1042
|
+
if (contentSplit.length < 2)
|
|
1043
|
+
continue;
|
|
1044
|
+
const headerRaw = contentSplit[0].trim();
|
|
1045
|
+
const content = contentSplit.slice(1).join("\n---CONTENT---\n").trim();
|
|
1046
|
+
const tierMatch = headerRaw.match(/tier\s*[::]\s*(major|minor|主要|次要)/i);
|
|
1047
|
+
const nameMatch = headerRaw.match(/name\s*[::]\s*(.+)/i);
|
|
1048
|
+
if (!tierMatch || !nameMatch)
|
|
1049
|
+
continue;
|
|
1050
|
+
const tierValue = tierMatch[1].toLowerCase();
|
|
1051
|
+
const tier = (tierValue === "major" || tierValue === "主要") ? "major" : "minor";
|
|
1052
|
+
const name = nameMatch[1].trim();
|
|
1053
|
+
if (!name || !content)
|
|
1054
|
+
continue;
|
|
1055
|
+
roles.push({ tier, name, content });
|
|
1056
|
+
}
|
|
1057
|
+
return roles;
|
|
1058
|
+
}
|
|
1059
|
+
buildStoryBibleShim(storyFrame, language) {
|
|
1060
|
+
if (language === "en") {
|
|
1061
|
+
return `# Story Bible (compat pointer — deprecated)\n\n> This file is kept for external readers only. The authoritative source is now:\n> - outline/story_frame.md (theme / tonal ground / core conflict / world rules / endgame)\n> - outline/volume_map.md (volume-level plot map)\n> - roles/ directory (one-file-per-character sheets)\n\n## Excerpt from story_frame\n\n${storyFrame.slice(0, 2000)}\n`;
|
|
1062
|
+
}
|
|
1063
|
+
return `# 故事圣经(兼容指针——已废弃)\n\n> 本文件仅为外部读取保留。权威来源已迁移至:\n> - outline/story_frame.md(主题 / 基调 / 核心冲突 / 世界铁律 / 终局)\n> - outline/volume_map.md(卷级分卷地图)\n> - roles/ 文件夹(一人一卡角色档案)\n\n## story_frame 摘录\n\n${storyFrame.slice(0, 2000)}\n`;
|
|
1064
|
+
}
|
|
1065
|
+
buildCharacterMatrixShim(roles, language) {
|
|
1066
|
+
const majorLines = roles.filter((role) => role.tier === "major")
|
|
1067
|
+
.map((role) => `- roles/主要角色/${role.name}.md`);
|
|
1068
|
+
const minorLines = roles.filter((role) => role.tier === "minor")
|
|
1069
|
+
.map((role) => `- roles/次要角色/${role.name}.md`);
|
|
1070
|
+
if (language === "en") {
|
|
1071
|
+
return `# Character Matrix (compat pointer — deprecated)\n\n> This file is kept for external readers only. Authoritative source is now the roles/ directory (one-file-per-character).\n\n## Major characters\n\n${majorLines.join("\n") || "(none)"}\n\n## Minor characters\n\n${minorLines.join("\n") || "(none)"}\n`;
|
|
1072
|
+
}
|
|
1073
|
+
return `# 角色矩阵(兼容指针——已废弃)\n\n> 本文件仅为外部读取保留。权威来源已迁移至 roles/ 文件夹(一人一卡)。\n\n## 主要角色\n\n${majorLines.join("\n") || "(无)"}\n\n## 次要角色\n\n${minorLines.join("\n") || "(无)"}\n`;
|
|
1074
|
+
}
|
|
1075
|
+
buildBookRulesShim(bookRulesBody, language) {
|
|
1076
|
+
const trimmedBody = bookRulesBody.trim();
|
|
1077
|
+
if (language === "en") {
|
|
1078
|
+
const excerpt = trimmedBody
|
|
1079
|
+
? `\n\n## Narrative guidance excerpt\n\n${trimmedBody}\n`
|
|
1080
|
+
: "";
|
|
1081
|
+
return `# Book Rules (compat pointer — deprecated)\n\n> This file is kept for external readers only. The authoritative YAML frontmatter now lives at the top of outline/story_frame.md.${excerpt}`;
|
|
1082
|
+
}
|
|
1083
|
+
const excerpt = trimmedBody
|
|
1084
|
+
? `\n\n## 叙事指引摘录\n\n${trimmedBody}\n`
|
|
1085
|
+
: "";
|
|
1086
|
+
return `# 本书规则(兼容指针——已废弃)\n\n> 本文件仅为外部读取保留。权威 YAML frontmatter 已迁移至 outline/story_frame.md 顶部。${excerpt}`;
|
|
1087
|
+
}
|
|
720
1088
|
normalizeSectionName(name) {
|
|
721
1089
|
return name
|
|
722
1090
|
.normalize("NFKC")
|