@cloud411716/fancy-webnovel 2.2.0 → 3.0.3
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 +65 -0
- package/client.js +201 -15
- package/cordis.patch.yml +5 -1
- package/docs/douyin-plugin.md +118 -0
- package/docs/material-library/README.md +404 -0
- package/docs/material-library/import.js +437 -0
- package/docs/material-library/prompt.md +370 -0
- package/docs/material-library/queries.surql +94 -0
- package/docs/material-library/sample.jsonl +14 -0
- package/docs/material-library/schema.surql +46 -0
- package/docs/mcp-profile.example.yml +24 -0
- package/docs/story-graph-design.md +602 -0
- package/docs/story-graph-validation.md +81 -0
- package/docs/workspace-mcp.md +284 -0
- package/index.js +12 -7
- package/lib/atomic-write.js +109 -0
- package/lib/chapter-order.js +100 -0
- package/lib/confirmation.js +80 -0
- package/lib/db-init.js +123 -0
- package/lib/deployed-state.js +11 -11
- package/lib/direction-labels.js +265 -0
- package/lib/error-codes.js +31 -31
- package/lib/infra.js +29 -4
- package/lib/material-db.js +149 -0
- package/lib/path-guard.js +150 -0
- package/lib/projects.js +400 -0
- package/lib/scraper-utils.js +140 -0
- package/lib/surreal-server.js +338 -0
- package/lib/topic-decision.js +860 -0
- package/lib/topic-fields.js +251 -0
- package/lib/topic-schema.js +225 -0
- package/lib/workspace.js +501 -0
- package/package.json +31 -7
- package/plugins/fancy-analyze/analyze-definitions.md +519 -0
- package/plugins/fancy-analyze/index.js +1128 -77
- package/plugins/fancy-analyze/ingest-coverage.js +107 -0
- package/plugins/fancy-analyze/schema-analyze.md +355 -107
- package/plugins/fancy-analyze/scripts/append-summary-chapter.js +612 -0
- package/plugins/fancy-analyze/summary-progress.js +71 -7
- package/plugins/fancy-analyze/templates.js +437 -139
- package/plugins/fancy-bootstrap/index.js +276 -131
- package/plugins/fancy-bootstrap/scripts/init.js +59 -25
- package/plugins/fancy-bootstrap/templates.js +35 -18
- package/plugins/fancy-db/index.js +172 -0
- package/plugins/fancy-douyin/deploy.js +134 -0
- package/plugins/fancy-douyin/index.js +551 -0
- package/plugins/fancy-douyin/prompts/01-analysis-prompt.md +84 -0
- package/plugins/fancy-douyin/scripts/crawl-note.cjs +194 -0
- package/plugins/fancy-douyin/scripts/lib.cjs +197 -0
- package/plugins/fancy-douyin/scripts/scan-account.cjs +128 -0
- package/plugins/fancy-douyin/scripts/start-chrome.cjs +203 -0
- package/plugins/fancy-douyin/state.js +197 -0
- package/plugins/fancy-douyin/templates.js +127 -0
- package/plugins/fancy-export/exporter.js +805 -0
- package/plugins/fancy-export/index.js +63 -0
- package/plugins/fancy-import/import-driver.js +29 -6
- package/plugins/fancy-import/index.js +13 -5
- package/plugins/fancy-import/prompts/01-import-prompt.md +39 -14
- package/plugins/fancy-import/templates.js +17 -8
- package/plugins/fancy-material/exporter.js +153 -0
- package/plugins/fancy-material/genres.js +256 -0
- package/plugins/fancy-material/index.js +835 -0
- package/plugins/fancy-material/inventory.js +145 -0
- package/plugins/fancy-material/kinds.js +313 -0
- package/plugins/fancy-material/queue.js +55 -0
- package/plugins/fancy-material/scripts/query.js +254 -0
- package/plugins/fancy-material/templates.js +258 -0
- package/plugins/fancy-material/tool-deploy.js +138 -0
- package/plugins/fancy-material/validate.js +149 -0
- package/plugins/fancy-project/index.js +133 -0
- package/plugins/fancy-scan/existing-block.js +100 -0
- package/plugins/fancy-scan/index.js +410 -21
- package/plugins/fancy-scan/prompts/01-analysis-prompt.md +302 -0
- package/plugins/fancy-scan/scripts/append-block.js +725 -0
- package/plugins/fancy-scan/scripts/report-validate.js +824 -0
- package/plugins/fancy-scan/scripts/run-scan.js +169 -22
- package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +145 -16
- package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +179 -49
- package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +38 -20
- package/plugins/fancy-scan/templates/blocks.js +155 -0
- package/plugins/fancy-scan/templates/platforms.js +372 -0
- package/plugins/fancy-scan/templates/report-schema.js +90 -0
- package/plugins/fancy-scan/templates.js +545 -146
- package/plugins/fancy-scan/tool-deploy.js +335 -0
- package/plugins/fancy-scan/wait-turn.js +46 -0
- package/plugins/fancy-story-builder/db/README.md +106 -0
- package/plugins/fancy-story-builder/db/client.js +228 -0
- package/plugins/fancy-story-builder/db/commands.js +145 -0
- package/plugins/fancy-story-builder/db/ids.js +39 -0
- package/plugins/fancy-story-builder/db/import.js +798 -0
- package/plugins/fancy-story-builder/db/project.js +299 -0
- package/plugins/fancy-story-builder/db/query.js +361 -0
- package/plugins/fancy-story-builder/db/schema.js +417 -0
- package/plugins/fancy-story-builder/db/stage-sync.js +238 -0
- package/plugins/fancy-story-builder/flesh-output.js +19 -6
- package/plugins/fancy-story-builder/index.js +4105 -396
- package/plugins/fancy-story-builder/patch-driver.js +390 -0
- package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +29 -19
- package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +72 -10
- package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +40 -5
- package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +37 -0
- package/plugins/fancy-story-builder/purpose-template.md +42 -66
- package/plugins/fancy-story-builder/references/formulas-skeleton.json +229 -56
- package/plugins/fancy-story-builder/references/genre-profiles.json +3514 -0
- package/plugins/fancy-story-builder/references/glossary.md +15 -6
- package/plugins/fancy-story-builder/references/index.md +97 -6
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +1 -1
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +25 -7
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +5 -1
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +6 -2
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +15 -5
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +1 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +2 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +1 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +19 -1
- package/plugins/fancy-story-builder/schema-setting.json +1100 -0
- package/plugins/fancy-story-builder/schema-setting.md +201 -293
- package/plugins/fancy-story-builder/scripts/brief-validate.js +164 -0
- package/plugins/fancy-story-builder/scripts/content-quality.js +4 -1
- package/plugins/fancy-story-builder/scripts/flesh-validate.js +135 -12
- package/plugins/fancy-story-builder/scripts/graph-utils.js +146 -47
- package/plugins/fancy-story-builder/scripts/reference-log.js +110 -0
- package/plugins/fancy-story-builder/scripts/rhythm-validate.js +100 -4
- package/plugins/fancy-story-builder/scripts/schema-pages.js +2668 -0
- package/plugins/fancy-story-builder/scripts/semantic-lint.js +5 -1
- package/plugins/fancy-story-builder/scripts/skeleton-validate.js +158 -17
- package/plugins/fancy-story-builder/scripts/stage-batches.js +36 -2
- package/plugins/fancy-story-builder/scripts/stage-contract.js +12 -0
- package/plugins/fancy-story-builder/scripts/story-artifacts.js +933 -0
- package/plugins/fancy-story-builder/scripts/upstream-query.js +1 -1
- package/plugins/fancy-story-builder/scripts/v2/baseline.js +112 -0
- package/plugins/fancy-story-builder/scripts/v2/cast.js +517 -0
- package/plugins/fancy-story-builder/scripts/v2/context.js +434 -0
- package/plugins/fancy-story-builder/scripts/v2/contracts.js +493 -0
- package/plugins/fancy-story-builder/scripts/v2/core.js +419 -0
- package/plugins/fancy-story-builder/scripts/v2/engine.js +1068 -0
- package/plugins/fancy-story-builder/scripts/v2/export-markdown.js +794 -0
- package/plugins/fancy-story-builder/scripts/v2/genre.js +195 -0
- package/plugins/fancy-story-builder/scripts/v2/knowledge.js +321 -0
- package/plugins/fancy-story-builder/scripts/v2/macro.js +409 -0
- package/plugins/fancy-story-builder/scripts/v2/manifest.js +538 -0
- package/plugins/fancy-story-builder/scripts/v2/migrate.js +137 -0
- package/plugins/fancy-story-builder/scripts/v2/prompts.js +1335 -0
- package/plugins/fancy-story-builder/scripts/v2/rhythm.js +618 -0
- package/plugins/fancy-story-builder/scripts/v2/scale.js +843 -0
- package/plugins/fancy-story-builder/scripts/v2/snapshot.js +121 -0
- package/plugins/fancy-story-builder/scripts/v2/stages.js +273 -0
- package/plugins/fancy-story-builder/scripts/v2/writing.js +216 -0
- package/plugins/fancy-story-builder/storydb/README.md +271 -0
- package/plugins/fancy-story-builder/storydb/cli.js +458 -0
- package/plugins/fancy-story-builder/storydb/client.js +351 -0
- package/plugins/fancy-story-builder/storydb/compile.js +452 -0
- package/plugins/fancy-story-builder/storydb/context.js +288 -0
- package/plugins/fancy-story-builder/storydb/definitions.js +699 -0
- package/plugins/fancy-story-builder/storydb/driver.js +106 -0
- package/plugins/fancy-story-builder/storydb/dsh-tools.js +313 -0
- package/plugins/fancy-story-builder/storydb/export.js +175 -0
- package/plugins/fancy-story-builder/storydb/ids.js +31 -0
- package/plugins/fancy-story-builder/storydb/impact.js +170 -0
- package/plugins/fancy-story-builder/storydb/import.js +559 -0
- package/plugins/fancy-story-builder/storydb/mcp.js +99 -0
- package/plugins/fancy-story-builder/storydb/prompts.js +952 -0
- package/plugins/fancy-story-builder/storydb/stage-command.js +379 -0
- package/plugins/fancy-story-builder/storydb/stage.js +426 -0
- package/plugins/fancy-story-builder/storydb/store.js +891 -0
- package/plugins/fancy-story-builder/storydb/tools.js +592 -0
- package/plugins/fancy-story-builder/storydb/validation.js +183 -0
- package/plugins/fancy-story-builder/templates/v2/10-scale/scale-template.json +144 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/emotion-template.json +15 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/endings-template.json +16 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/lines-template.json +28 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/roles-template.json +10 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/story-template.json +19 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/volumes-template.json +24 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/world-template.json +24 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/characters-template.json +49 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/climaxes-template.json +14 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/conflicts-template.json +30 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/factions-template.json +18 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/foreshadows-template.json +29 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/information-template.json +29 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/items-template.json +13 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/locations-template.json +12 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/payoffs-template.json +16 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/relations-template.json +44 -0
- package/plugins/fancy-story-builder/templates/v2/40-rhythm/chapter-map-template.json +81 -0
- package/plugins/fancy-story-builder/templates/v2/40-rhythm/outline-template.json +162 -0
- package/plugins/fancy-story-builder/templates/v2/50-writing/ledger-template.json +46 -0
- package/plugins/fancy-story-builder/templates/v2/60-deslop/report-template.json +10 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//346/227/266/345/272/217/346/230/240/345/260/204/346/250/241/346/235/277.json +267 -44
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//344/270/226/347/225/214/350/247/202/350/247/204/345/210/231/346/250/241/346/235/277.json +121 -19
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/263/351/224/256/351/201/223/345/205/267/346/250/241/346/235/277.json +129 -21
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/266/344/273/226/345/256/236/344/275/223/346/250/241/346/235/277.json +62 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/206/262/347/252/201/346/250/241/346/235/277.json +76 -17
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/211/247/346/203/205/346/250/241/346/235/277.json +56 -8
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/212/277/345/212/233/346/250/241/346/235/277.json +63 -13
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/234/260/347/202/271/346/250/241/346/235/277.json +34 -6
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//347/273/223/345/261/200/346/250/241/346/235/277.json +81 -18
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//350/247/222/350/211/262/346/250/241/346/235/277.json +202 -42
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/205/263/347/263/273/346/250/241/346/235/277.json +160 -27
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/256/236/344/275/223/346/250/241/346/235/277.json +217 -41
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//346/241/206/346/236/266/346/250/241/346/235/277.json +215 -41
- package/plugins/fancy-story-builder/templates.js +272 -74
- package/plugins/fancy-story-builder/wiki-references.js +429 -93
- package/plugins/fancy-story-builder/writing-driver.js +336 -9
- package/plugins/fancy-topic/index.js +414 -61
- package/plugins/fancy-topic/prompts/01-exploration-prompt.md +115 -0
- package/plugins/fancy-topic/scripts/save-suggestion.js +219 -0
- package/plugins/fancy-topic/scripts/select-topic.js +106 -63
- package/plugins/fancy-topic/templates.js +225 -122
- package/plugins/fancy-topic/tool-deploy.js +200 -0
- package/lib/safe-rm.js +0 -94
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* story-artifacts.js —— 把聚合 JSON 拆成“可按对象阅读”的档案 + 每章一份细纲
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要这一层:
|
|
5
|
+
* - 现有阶段产物是聚合 JSON(实体.json / 冲突.json / 时序映射.json / 章节细纲.json),
|
|
6
|
+
* 人和别的 LLM 很难“只读一个伏笔/一个爽点/一章细纲”。
|
|
7
|
+
* - llm_wiki 的 lint 需要页面级对象与链接,聚合 JSON 只能整文件 ingest,检查粒度太粗。
|
|
8
|
+
* - 写作阶段需要每章一份硬约束:本章时间线、必须埋/收的伏笔、必须兑现的爽点/高潮、
|
|
9
|
+
* 必须出场的人、以及“谁还不知道什么,所以本章绝对不能提”。
|
|
10
|
+
*
|
|
11
|
+
* 设计原则:
|
|
12
|
+
* - 聚合 JSON 仍是校验器的事实源;这里生成的是**确定性投影**,不引入 LLM 判断。
|
|
13
|
+
* - 每个对象一个文件,每章一个细纲文件;写入保持幂等。
|
|
14
|
+
* - 信息差只从已有数据推导(角色 information_state + 冲突 information_gaps + 时序 info_gaps),
|
|
15
|
+
* 不凭空编造“谁知道什么”。
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
|
|
20
|
+
const SRC = ['设定', 'raw', 'sources'];
|
|
21
|
+
const ARTIFACT_DIR = ['设定', 'raw', 'sources', '档案'];
|
|
22
|
+
const BRIEF_DIR = ['设定', 'raw', 'sources', '节奏', '细纲'];
|
|
23
|
+
|
|
24
|
+
function pad3(n) {
|
|
25
|
+
return String(n).padStart(3, '0');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function pad2(n) {
|
|
29
|
+
return String(n).padStart(2, '0');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readJson(file, fallback = null) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
35
|
+
} catch {
|
|
36
|
+
return fallback;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function writeJson(file, value) {
|
|
41
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
42
|
+
const tmp = file + '.tmp-' + process.pid;
|
|
43
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + '\n', 'utf-8');
|
|
44
|
+
fs.renameSync(tmp, file);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function chapterLink(ch) {
|
|
48
|
+
return 'CH' + pad3(ch);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function uniqBy(items, keyFn) {
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const item of items || []) {
|
|
55
|
+
if (!item) continue;
|
|
56
|
+
const key = keyFn(item);
|
|
57
|
+
if (!key || seen.has(key)) continue;
|
|
58
|
+
seen.add(key);
|
|
59
|
+
out.push(item);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function flattenParticipants(row) {
|
|
65
|
+
const out = [];
|
|
66
|
+
if (!row) return out;
|
|
67
|
+
for (const value of [row.protagonist_side, row.main_antagonist, row.secondary_antagonist, row.third_party]) {
|
|
68
|
+
if (Array.isArray(value)) out.push(...value);
|
|
69
|
+
else if (value) out.push(value);
|
|
70
|
+
}
|
|
71
|
+
return out.filter(Boolean);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function namesToIds(names, byName) {
|
|
75
|
+
const ids = [];
|
|
76
|
+
for (const name of names || []) {
|
|
77
|
+
const id = byName.get(String(name).trim());
|
|
78
|
+
if (id) ids.push(id);
|
|
79
|
+
}
|
|
80
|
+
return [...new Set(ids)];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
/** 章首钩子的写法提示(来自 glossary 的 7 式)。 */
|
|
85
|
+
const HEAD_TECHNIQUES = {
|
|
86
|
+
'悬念对话': '用一句信息不完整的对话开场,让读者先感到“话里有话”;先不解释说话人真正知道什么。',
|
|
87
|
+
'闪前碎片': '先给未来结果的一个碎片,再回到当前时间线;碎片要具体,但不能把结果解释完。',
|
|
88
|
+
'倒计时': '在前 100 字内给出明确时限或剩余量,并让读者知道失败会付出什么代价。',
|
|
89
|
+
'神秘独白': '用第一人称内心独白制造诡异感或不可靠感;独白里要有具体细节,不能只写情绪。',
|
|
90
|
+
'反差场景': '把两个反差极大的场景并置,用断裂感制造问题;不要先解释为什么反差。',
|
|
91
|
+
'未完成动作': '让一个动作进行到一半被截断,停在“接下来会怎样”的悬念上。',
|
|
92
|
+
'意象预示': '用环境意象反复暗示后文;意象要具体可复现,不要写成抽象抒情。',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** 章尾钩子的写法提示(来自 glossary 的 13 式)。 */
|
|
96
|
+
const TAIL_TECHNIQUES = {
|
|
97
|
+
'突然揭示': '抛出改变全局的信息,但只给证据和反应,不把结论解释完。',
|
|
98
|
+
'紧急危机': '在章尾抛出下章必须回应的威胁,停在动作或决定之前。',
|
|
99
|
+
'未完成动作': '让动作被一个新变量打断,读者必须追下一章才知道结果。',
|
|
100
|
+
'身份反转': '让身份真相偏离读者预期;只给一个可验证的证据,不要当场解释全部。',
|
|
101
|
+
'两难抉择': '把角色逼到两个坏选项之间,停在选择之前。',
|
|
102
|
+
'神秘物品': '让一个重要但含义未知的物件出现;写清它被谁拿到、改变了什么,不解释它是什么。',
|
|
103
|
+
'倒计时': '把剩余时间压到读者能算出来的程度,停在时限即将到点的一刻。',
|
|
104
|
+
'承诺/威胁': '让某个人物公开宣布行动意图,停在“他真的要这么做”的瞬间。',
|
|
105
|
+
'离奇消失': '让一个人或物在不可能的条件下消失;只写见证者的反应,不给解释。',
|
|
106
|
+
'隐藏含义': '让表面正常的对话或动作暗藏第二层信息;用双关、停顿或异常细节收尾。',
|
|
107
|
+
'意象钩子': '让前文反复出现的意象发生变化;变化本身就是信息,不要解释。',
|
|
108
|
+
'回声钩子': '让章尾句呼应开头,但关键细节已经变了;读者能感到“同一句话,意义不同”。',
|
|
109
|
+
'留白钩子': '只展示人物反应,不揭示发生了什么;留白要具体到读者能追问“到底怎么了”。',
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
function headTechnique(type) {
|
|
113
|
+
return HEAD_TECHNIQUES[type] || '在前 100 字内抛出一个具体问题或异常,让读者带着问题进入本章。';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function tailTechnique(type) {
|
|
117
|
+
return TAIL_TECHNIQUES[type] || '在章尾留下一个具体、可追问的未解问题,不要把所有信息解释完。';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderEvent(event) {
|
|
121
|
+
return {
|
|
122
|
+
type: event.type,
|
|
123
|
+
ref: event.ref || '',
|
|
124
|
+
description: event.description || '',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 读取一次生成所需的全部上游数据。
|
|
130
|
+
*/
|
|
131
|
+
export function loadArtifactInputs(projectRoot) {
|
|
132
|
+
const skeletonDir = path.join(projectRoot, ...SRC, '骨架');
|
|
133
|
+
const fleshDir = path.join(projectRoot, ...SRC, '血肉');
|
|
134
|
+
const rhythmDir = path.join(projectRoot, ...SRC, '节奏');
|
|
135
|
+
const framework = readJson(path.join(skeletonDir, '框架.json'), {}) || {};
|
|
136
|
+
const entities = readJson(path.join(skeletonDir, '实体.json'), {}) || {};
|
|
137
|
+
const relations = readJson(path.join(skeletonDir, '关系.json'), {}) || {};
|
|
138
|
+
const fleshConflicts = readJson(path.join(fleshDir, '冲突.json'), {}) || {};
|
|
139
|
+
const fleshChars = readJson(path.join(fleshDir, '角色.json'), {}) || {};
|
|
140
|
+
const plan = readJson(path.join(rhythmDir, '章节规划.json'), {}) || {};
|
|
141
|
+
const timeline = readJson(path.join(rhythmDir, '时序映射.json'), {}) || {};
|
|
142
|
+
const outlines = readJson(path.join(rhythmDir, '章节细纲.json'), {}) || {};
|
|
143
|
+
|
|
144
|
+
const characterById = new Map();
|
|
145
|
+
const characterByName = new Map();
|
|
146
|
+
for (const c of (entities.characters || [])) {
|
|
147
|
+
if (!c || !c.id) continue;
|
|
148
|
+
characterById.set(c.id, c);
|
|
149
|
+
if (c.name) characterByName.set(String(c.name), c.id);
|
|
150
|
+
}
|
|
151
|
+
const factionById = new Map((entities.factions || []).filter((x) => x && x.id).map((x) => [x.id, x]));
|
|
152
|
+
const locationById = new Map((entities.locations || []).filter((x) => x && x.id).map((x) => [x.id, x]));
|
|
153
|
+
const itemById = new Map((entities.items || []).filter((x) => x && x.id).map((x) => [x.id, x]));
|
|
154
|
+
const conflictById = new Map((entities.conflicts || []).filter((x) => x && x.id).map((x) => [x.id, x]));
|
|
155
|
+
const foreshadowById = new Map((entities.foreshadows || []).filter((x) => x && x.id).map((x) => [x.id, x]));
|
|
156
|
+
const climaxById = new Map((entities.climaxes || []).filter((x) => x && x.id).map((x) => [x.id, x]));
|
|
157
|
+
const pleasureById = new Map((entities.pleasures || []).filter((x) => x && x.id).map((x) => [x.id, x]));
|
|
158
|
+
const endingById = new Map((entities.endings || []).filter((x) => x && x.id).map((x) => [x.id, x]));
|
|
159
|
+
|
|
160
|
+
const fleshConflictById = new Map();
|
|
161
|
+
for (const c of (fleshConflicts.conflicts || [])) {
|
|
162
|
+
if (c && c.id) fleshConflictById.set(c.id, c);
|
|
163
|
+
}
|
|
164
|
+
const fleshCharacterById = new Map();
|
|
165
|
+
for (const c of (fleshChars.characters || [])) {
|
|
166
|
+
if (c && c.id) fleshCharacterById.set(c.id, c);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const participantsByConflict = new Map();
|
|
170
|
+
for (const row of (relations.conflict_participants || [])) {
|
|
171
|
+
if (row && row.conflict) participantsByConflict.set(row.conflict, flattenParticipants(row));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const outlineByCh = new Map();
|
|
175
|
+
for (const o of (outlines.outlines || [])) {
|
|
176
|
+
if (o && typeof o.ch === 'number') outlineByCh.set(o.ch, o);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const timelineByCh = new Map();
|
|
180
|
+
const ensureCh = (ch) => {
|
|
181
|
+
if (!timelineByCh.has(ch)) {
|
|
182
|
+
timelineByCh.set(ch, {
|
|
183
|
+
events: [],
|
|
184
|
+
foreshadowActions: [],
|
|
185
|
+
climax: null,
|
|
186
|
+
pleasure: null,
|
|
187
|
+
ending: null,
|
|
188
|
+
conflicts: [],
|
|
189
|
+
infoGaps: [],
|
|
190
|
+
hooks: null,
|
|
191
|
+
arcStages: [],
|
|
192
|
+
relationshipChanges: [],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return timelineByCh.get(ch);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const mainLineById = new Map((entities.main_lines || []).filter((x) => x && x.id).map((x) => [x.id, x]));
|
|
199
|
+
const branchById = new Map((entities.branches || []).filter((x) => x && x.id).map((x) => [x.id, x]));
|
|
200
|
+
|
|
201
|
+
for (const row of (timeline.main_line_timeline || [])) {
|
|
202
|
+
if (!row || !Array.isArray(row.key_nodes)) continue;
|
|
203
|
+
for (const node of row.key_nodes) {
|
|
204
|
+
if (!node || typeof node.chapter !== 'number') continue;
|
|
205
|
+
ensureCh(node.chapter).events.push(renderEvent({
|
|
206
|
+
type: '主线节点',
|
|
207
|
+
ref: row.main_id + (mainLineById.get(row.main_id) ? '(' + mainLineById.get(row.main_id).name + ')' : ''),
|
|
208
|
+
description: node.event_ref || '',
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const row of (timeline.conflict_timeline || [])) {
|
|
213
|
+
if (!row || !row.conflict_id) continue;
|
|
214
|
+
for (const node of (row.escalation_nodes || [])) {
|
|
215
|
+
if (!node || typeof node.chapter !== 'number') continue;
|
|
216
|
+
const e = ensureCh(node.chapter);
|
|
217
|
+
const name = conflictById.get(row.conflict_id)?.name || row.conflict_id;
|
|
218
|
+
e.events.push(renderEvent({ type: '冲突升级', ref: row.conflict_id + '(' + name + ')', description: node.node_type || '' }));
|
|
219
|
+
e.conflicts.push({ id: row.conflict_id, name, node_type: node.node_type || '' });
|
|
220
|
+
}
|
|
221
|
+
if (typeof row.resolution_chapter === 'number') {
|
|
222
|
+
const e = ensureCh(row.resolution_chapter);
|
|
223
|
+
const name = conflictById.get(row.conflict_id)?.name || row.conflict_id;
|
|
224
|
+
e.events.push(renderEvent({ type: '冲突收束', ref: row.conflict_id + '(' + name + ')', description: 'resolution' }));
|
|
225
|
+
}
|
|
226
|
+
for (const gap of (row.info_gaps || [])) {
|
|
227
|
+
if (!gap || typeof gap.created !== 'number') continue;
|
|
228
|
+
const statusAt = (ch) => {
|
|
229
|
+
if (gap.created === ch) return 'created';
|
|
230
|
+
if (gap.resolved === ch) return 'resolved';
|
|
231
|
+
if (gap.created < ch && (typeof gap.resolved !== 'number' || gap.resolved > ch)) return 'open';
|
|
232
|
+
return null;
|
|
233
|
+
};
|
|
234
|
+
for (const ch of [gap.created, gap.resolved]) {
|
|
235
|
+
if (typeof ch !== 'number') continue;
|
|
236
|
+
const e = ensureCh(ch);
|
|
237
|
+
e.infoGaps.push({ conflict_id: row.conflict_id, info_name: gap.info_name, status: statusAt(ch) });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
for (const row of (timeline.branch_timeline || [])) {
|
|
242
|
+
if (!row || !row.branch_id || !Array.isArray(row.interweaves)) continue;
|
|
243
|
+
for (const node of row.interweaves) {
|
|
244
|
+
if (!node || typeof node.chapter !== 'number') continue;
|
|
245
|
+
ensureCh(node.chapter).events.push(renderEvent({
|
|
246
|
+
type: '支线交织',
|
|
247
|
+
ref: row.branch_id + (branchById.get(row.branch_id) ? '(' + branchById.get(row.branch_id).name + ')' : ''),
|
|
248
|
+
description: node.description || '',
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
for (const row of (timeline.relationship_timeline || [])) {
|
|
253
|
+
if (!row || typeof row !== 'object') continue;
|
|
254
|
+
const fromId = row.from;
|
|
255
|
+
const toId = row.to;
|
|
256
|
+
if (!fromId || !toId) continue;
|
|
257
|
+
const fromName = characterById.get(fromId)?.name || fromId;
|
|
258
|
+
const toName = characterById.get(toId)?.name || toId;
|
|
259
|
+
const ch = typeof row.start_chapter === 'number' ? row.start_chapter : null;
|
|
260
|
+
if (ch === null) continue;
|
|
261
|
+
const entry = {
|
|
262
|
+
from: fromId,
|
|
263
|
+
to: toId,
|
|
264
|
+
from_name: fromName,
|
|
265
|
+
to_name: toName,
|
|
266
|
+
stage: row.stage || '',
|
|
267
|
+
state: row.state || '',
|
|
268
|
+
start_chapter: ch,
|
|
269
|
+
end_chapter: typeof row.end_chapter === 'number' ? row.end_chapter : null,
|
|
270
|
+
trigger_event: row.trigger_event || '',
|
|
271
|
+
description: row.description || '',
|
|
272
|
+
};
|
|
273
|
+
ensureCh(ch).relationshipChanges.push(entry);
|
|
274
|
+
ensureCh(ch).events.push(renderEvent({
|
|
275
|
+
type: '关系变化',
|
|
276
|
+
ref: fromName + ' → ' + toName,
|
|
277
|
+
description: (row.stage ? row.stage + ':' : '') + (row.state || '') +
|
|
278
|
+
(row.trigger_event ? '(触发:' + row.trigger_event + ')' : ''),
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
for (const row of (timeline.foreshadow_timeline || [])) {
|
|
283
|
+
if (!row || !row.fs_id) continue;
|
|
284
|
+
const fsRow = foreshadowById.get(row.fs_id) || { id: row.fs_id, name: row.fs_id };
|
|
285
|
+
if (typeof row.buried_chapter === 'number') {
|
|
286
|
+
ensureCh(row.buried_chapter).foreshadowActions.push({ id: row.fs_id, name: fsRow.name, action: '埋设' });
|
|
287
|
+
}
|
|
288
|
+
if (typeof row.recovered_chapter === 'number') {
|
|
289
|
+
ensureCh(row.recovered_chapter).foreshadowActions.push({ id: row.fs_id, name: fsRow.name, action: '回收' });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
for (const row of (timeline.climax_timeline || [])) {
|
|
293
|
+
if (!row || typeof row.chapter !== 'number') continue;
|
|
294
|
+
const x = climaxById.get(row.climax_id) || { id: row.climax_id, name: row.climax_id };
|
|
295
|
+
ensureCh(row.chapter).climax = { id: x.id, name: x.name };
|
|
296
|
+
}
|
|
297
|
+
for (const row of (timeline.pleasure_timeline || [])) {
|
|
298
|
+
if (!row || typeof row.chapter !== 'number') continue;
|
|
299
|
+
const x = pleasureById.get(row.pleasure_id) || { id: row.pleasure_id, name: row.pleasure_id };
|
|
300
|
+
ensureCh(row.chapter).pleasure = { id: x.id, name: x.name };
|
|
301
|
+
}
|
|
302
|
+
for (const row of (timeline.ending_timeline || [])) {
|
|
303
|
+
if (!row || typeof row.chapter !== 'number') continue;
|
|
304
|
+
const x = endingById.get(row.ending_id) || { id: row.ending_id, name: row.ending_id };
|
|
305
|
+
ensureCh(row.chapter).ending = { id: x.id, name: x.name };
|
|
306
|
+
}
|
|
307
|
+
for (const row of (timeline.hook_timeline || [])) {
|
|
308
|
+
if (!row || typeof row.ch !== 'number') continue;
|
|
309
|
+
ensureCh(row.ch).hooks = {
|
|
310
|
+
head: row.hook_head || '',
|
|
311
|
+
tail: row.hook_tail || '',
|
|
312
|
+
strength: row.hook_strength || '',
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
for (const row of (timeline.character_timeline || [])) {
|
|
316
|
+
if (!row || !row.char_id) continue;
|
|
317
|
+
for (const stage of (row.arc_stages || [])) {
|
|
318
|
+
if (!stage || typeof stage.chapter !== 'number') continue;
|
|
319
|
+
ensureCh(stage.chapter).arcStages.push({ id: row.char_id, name: characterById.get(row.char_id)?.name || row.char_id, stage: stage.stage_name || '' });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
projectRoot,
|
|
325
|
+
skeletonDir,
|
|
326
|
+
fleshDir,
|
|
327
|
+
rhythmDir,
|
|
328
|
+
framework,
|
|
329
|
+
entities,
|
|
330
|
+
relations,
|
|
331
|
+
plan,
|
|
332
|
+
timeline,
|
|
333
|
+
outlines,
|
|
334
|
+
characterById,
|
|
335
|
+
characterByName,
|
|
336
|
+
factionById,
|
|
337
|
+
locationById,
|
|
338
|
+
itemById,
|
|
339
|
+
conflictById,
|
|
340
|
+
foreshadowById,
|
|
341
|
+
climaxById,
|
|
342
|
+
pleasureById,
|
|
343
|
+
endingById,
|
|
344
|
+
fleshConflictById,
|
|
345
|
+
fleshCharacterById,
|
|
346
|
+
participantsByConflict,
|
|
347
|
+
outlineByCh,
|
|
348
|
+
timelineByCh,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** 把冲突的 info_gaps 与时序映射里的 created/resolved 合并。 */
|
|
353
|
+
export function collectInfoGaps(inputs) {
|
|
354
|
+
const out = [];
|
|
355
|
+
const conflictTimeline = new Map();
|
|
356
|
+
for (const row of (inputs.timeline.conflict_timeline || [])) {
|
|
357
|
+
if (row && row.conflict_id) conflictTimeline.set(row.conflict_id, row);
|
|
358
|
+
}
|
|
359
|
+
// 没有节奏阶段时也要能从血肉冲突里拆出信息差;节奏只负责补 created/resolved。
|
|
360
|
+
const conflictIds = new Set([
|
|
361
|
+
...inputs.fleshConflictById.keys(),
|
|
362
|
+
...conflictTimeline.keys(),
|
|
363
|
+
]);
|
|
364
|
+
for (const conflictId of conflictIds) {
|
|
365
|
+
const row = conflictTimeline.get(conflictId) || {};
|
|
366
|
+
const flesh = inputs.fleshConflictById.get(conflictId) || {};
|
|
367
|
+
const baseGaps = Array.isArray(flesh.information_gaps) ? flesh.information_gaps : [];
|
|
368
|
+
const timelineGaps = Array.isArray(row.info_gaps) ? row.info_gaps : [];
|
|
369
|
+
const count = Math.max(baseGaps.length, timelineGaps.length);
|
|
370
|
+
for (let i = 0; i < count; i += 1) {
|
|
371
|
+
const base = baseGaps[i] || {};
|
|
372
|
+
const tl = timelineGaps[i] || {};
|
|
373
|
+
const infoName = tl.info_name || base.info_name;
|
|
374
|
+
if (!infoName) continue;
|
|
375
|
+
const id = 'IG-' + conflictId + '-' + pad2(i + 1);
|
|
376
|
+
const whoKnows = base.who_knows || [];
|
|
377
|
+
const whoDoesNot = base.who_does_not_know || [];
|
|
378
|
+
out.push({
|
|
379
|
+
id,
|
|
380
|
+
conflict_id: conflictId,
|
|
381
|
+
info_name: infoName,
|
|
382
|
+
who_knows: whoKnows,
|
|
383
|
+
who_does_not_know: whoDoesNot,
|
|
384
|
+
who_knows_ids: namesToIds(whoKnows, inputs.characterByName),
|
|
385
|
+
who_does_not_know_ids: namesToIds(whoDoesNot, inputs.characterByName),
|
|
386
|
+
forbidden_mentions: whoDoesNot.map((name) => name + ' 尚不知道「' + infoName + '」,不得让其提及、知情或据此行动。'),
|
|
387
|
+
resolution_method: base.resolution_method || '',
|
|
388
|
+
created: typeof tl.created === 'number' ? tl.created : null,
|
|
389
|
+
resolved: typeof tl.resolved === 'number' ? tl.resolved : null,
|
|
390
|
+
status: (typeof tl.created === 'number' && typeof tl.resolved === 'number')
|
|
391
|
+
? (tl.resolved <= tl.created ? 'invalid' : 'planned')
|
|
392
|
+
: 'planned',
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** 生成一个“章节时间线”文件的内容。 */
|
|
400
|
+
function buildTimelineFile(inputs, ch) {
|
|
401
|
+
const e = inputs.timelineByCh.get(ch) || {
|
|
402
|
+
events: [], foreshadowActions: [], climax: null, pleasure: null,
|
|
403
|
+
ending: null, conflicts: [], infoGaps: [], hooks: null, arcStages: [],
|
|
404
|
+
};
|
|
405
|
+
return {
|
|
406
|
+
ch,
|
|
407
|
+
chapter: chapterLink(ch),
|
|
408
|
+
events: e.events,
|
|
409
|
+
foreshadow_actions: e.foreshadowActions,
|
|
410
|
+
climax: e.climax,
|
|
411
|
+
pleasure: e.pleasure,
|
|
412
|
+
ending: e.ending,
|
|
413
|
+
conflicts: e.conflicts,
|
|
414
|
+
information_gaps: e.infoGaps,
|
|
415
|
+
hooks: e.hooks,
|
|
416
|
+
arc_stages: e.arcStages,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** 选出本章真正需要出场的角色(不再把“全书都存在”的人全塞进来)。 */
|
|
421
|
+
function selectCast(inputs, ch, activeConflicts) {
|
|
422
|
+
const out = [];
|
|
423
|
+
const firstAppear = [];
|
|
424
|
+
const push = (id, reason) => {
|
|
425
|
+
if (!id || !inputs.characterById.has(id)) return;
|
|
426
|
+
out.push({ id, reason });
|
|
427
|
+
};
|
|
428
|
+
for (const c of (inputs.entities.characters || [])) {
|
|
429
|
+
if (c && c.type === '主角') push(c.id, '主角');
|
|
430
|
+
}
|
|
431
|
+
for (const row of (inputs.timeline.character_timeline || [])) {
|
|
432
|
+
if (!row || !row.char_id) continue;
|
|
433
|
+
if (row.appear_start === ch) firstAppear.push(row.char_id);
|
|
434
|
+
if (row.exit_chapter === ch) push(row.char_id, '退场');
|
|
435
|
+
for (const stage of (row.arc_stages || [])) {
|
|
436
|
+
if (stage && stage.chapter === ch) push(row.char_id, '弧光节点');
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
for (const conflict of activeConflicts) {
|
|
440
|
+
for (const id of (inputs.participantsByConflict.get(conflict.id) || [])) push(id, '冲突参与');
|
|
441
|
+
}
|
|
442
|
+
// 首次出场只补到能支撑一场戏的最小规模;不再把“全书都存在”的人全塞进来。
|
|
443
|
+
for (const id of firstAppear) {
|
|
444
|
+
if (out.length >= 6) break;
|
|
445
|
+
if (!out.some((x) => x.id === id)) push(id, '首次出场');
|
|
446
|
+
}
|
|
447
|
+
const unique = uniqBy(out, (x) => x.id).slice(0, 6);
|
|
448
|
+
return unique.map((x) => {
|
|
449
|
+
const c = inputs.characterById.get(x.id) || {};
|
|
450
|
+
return {
|
|
451
|
+
id: x.id,
|
|
452
|
+
name: c.name || x.id,
|
|
453
|
+
role: c.type || '',
|
|
454
|
+
reason: x.reason,
|
|
455
|
+
};
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** 找本章的地点/道具:优先匹配 scene_focus 里的名字,其次首次出场。 */
|
|
460
|
+
function selectLocation(inputs, ch, sceneFocus) {
|
|
461
|
+
const focus = String(sceneFocus || '');
|
|
462
|
+
for (const l of (inputs.entities.locations || [])) {
|
|
463
|
+
if (!l || !l.name) continue;
|
|
464
|
+
if (focus.includes(String(l.name))) return { id: l.id, name: l.name };
|
|
465
|
+
}
|
|
466
|
+
for (const row of (inputs.timeline.location_timeline || [])) {
|
|
467
|
+
if (row && row.appear_start === ch && inputs.locationById.has(row.loc_id)) {
|
|
468
|
+
const l = inputs.locationById.get(row.loc_id);
|
|
469
|
+
return { id: l.id, name: l.name };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function selectItems(inputs, ch, sceneFocus) {
|
|
476
|
+
const focus = String(sceneFocus || '');
|
|
477
|
+
const out = [];
|
|
478
|
+
for (const item of (inputs.entities.items || [])) {
|
|
479
|
+
if (item && item.name && focus.includes(String(item.name))) out.push({ id: item.id, name: item.name });
|
|
480
|
+
}
|
|
481
|
+
for (const row of (inputs.timeline.item_timeline || [])) {
|
|
482
|
+
if (out.length >= 3) break;
|
|
483
|
+
if (row && row.appear_start === ch && inputs.itemById.has(row.item_id)) {
|
|
484
|
+
const item = inputs.itemById.get(row.item_id);
|
|
485
|
+
out.push({ id: item.id, name: item.name });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return uniqBy(out, (x) => x.id).slice(0, 3);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** 本章的信息差:created/open/resolved 三种状态。 */
|
|
492
|
+
function selectInfoGaps(inputs, ch, allInfoGaps, cast, activeConflicts) {
|
|
493
|
+
const castIds = new Set((cast || []).map((x) => x.id));
|
|
494
|
+
const relevantConflicts = new Set((activeConflicts || []).map((x) => x.id));
|
|
495
|
+
const out = [];
|
|
496
|
+
for (const gap of allInfoGaps) {
|
|
497
|
+
if (typeof gap.created !== 'number') continue;
|
|
498
|
+
let status = null;
|
|
499
|
+
if (gap.created === ch) status = 'created';
|
|
500
|
+
else if (gap.resolved === ch) status = 'resolved';
|
|
501
|
+
else if (gap.created < ch && (typeof gap.resolved !== 'number' || gap.resolved > ch)) status = 'open';
|
|
502
|
+
if (!status) continue;
|
|
503
|
+
// 只保留本章真正相关的信息差:本章创建/解决,或所属冲突在本章有升级节点。
|
|
504
|
+
if (status === 'open' && !relevantConflicts.has(gap.conflict_id)) continue;
|
|
505
|
+
const forbidden = [];
|
|
506
|
+
if (status !== 'resolved') {
|
|
507
|
+
for (const id of gap.who_does_not_know_ids || []) {
|
|
508
|
+
if (!castIds.has(id)) continue;
|
|
509
|
+
const c = inputs.characterById.get(id) || {};
|
|
510
|
+
forbidden.push(
|
|
511
|
+
(c.name || id) + '(' + id + ')尚不知道「' + gap.info_name + '」,' +
|
|
512
|
+
'本章不得让其提及、知情、据此行动,也不得用旁白提前揭示。'
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
out.push({
|
|
517
|
+
id: gap.id,
|
|
518
|
+
conflict_id: gap.conflict_id,
|
|
519
|
+
info_name: gap.info_name,
|
|
520
|
+
status,
|
|
521
|
+
who_knows: gap.who_knows,
|
|
522
|
+
who_does_not_know: gap.who_does_not_know,
|
|
523
|
+
who_knows_ids: gap.who_knows_ids,
|
|
524
|
+
who_does_not_know_ids: gap.who_does_not_know_ids,
|
|
525
|
+
resolution_method: gap.resolution_method,
|
|
526
|
+
created: gap.created,
|
|
527
|
+
resolved: gap.resolved,
|
|
528
|
+
forbidden_mentions: forbidden,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
return out;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** 生成每章细纲。 */
|
|
535
|
+
export function buildChapterBriefs(inputs) {
|
|
536
|
+
const chapters = Array.isArray(inputs.plan.chapters) ? inputs.plan.chapters : [];
|
|
537
|
+
if (!chapters.length) return { count: 0, files: [] };
|
|
538
|
+
const allInfoGaps = collectInfoGaps(inputs);
|
|
539
|
+
const byCh = new Map();
|
|
540
|
+
const avgWords = Number(inputs.framework?.capacity?.avg_words_per_chapter) || 0;
|
|
541
|
+
const files = [];
|
|
542
|
+
|
|
543
|
+
for (const plan of chapters) {
|
|
544
|
+
if (!plan || typeof plan.ch !== 'number') continue;
|
|
545
|
+
const ch = plan.ch;
|
|
546
|
+
const tl = inputs.timelineByCh.get(ch) || {
|
|
547
|
+
events: [], foreshadowActions: [], climax: null, pleasure: null,
|
|
548
|
+
ending: null, conflicts: [], infoGaps: [], hooks: null, arcStages: [],
|
|
549
|
+
relationshipChanges: [],
|
|
550
|
+
};
|
|
551
|
+
const outline = inputs.outlineByCh.get(ch) || {};
|
|
552
|
+
const activeConflicts = uniqBy(tl.conflicts, (x) => x.id);
|
|
553
|
+
const cast = selectCast(inputs, ch, activeConflicts);
|
|
554
|
+
const location = selectLocation(inputs, ch, plan.scene_focus || '');
|
|
555
|
+
const items = selectItems(inputs, ch, plan.scene_focus || '');
|
|
556
|
+
const infoGaps = selectInfoGaps(inputs, ch, allInfoGaps, cast, activeConflicts);
|
|
557
|
+
const foreshadowActions = tl.foreshadowActions || [];
|
|
558
|
+
const hooks = tl.hooks || {
|
|
559
|
+
head: outline.hook_head || '',
|
|
560
|
+
tail: outline.hook_tail || '',
|
|
561
|
+
strength: outline.hook_strength || '',
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const wordTarget = typeof outline.word_target === 'number' ? outline.word_target : avgWords;
|
|
565
|
+
const pov = cast.find((x) => x.reason === '主角')?.id || cast[0]?.id || '';
|
|
566
|
+
const povName = cast.find((x) => x.id === pov)?.name || pov;
|
|
567
|
+
const locationName = location ? location.name : '(本章未指定地点)';
|
|
568
|
+
const headType = hooks.head || '';
|
|
569
|
+
const tailType = hooks.tail || '';
|
|
570
|
+
const strength = hooks.strength || '';
|
|
571
|
+
|
|
572
|
+
const mustWrite = [];
|
|
573
|
+
if (outline.function_main) mustWrite.push('主功能:' + outline.function_main);
|
|
574
|
+
if (outline.function_aux) mustWrite.push('辅助功能:' + outline.function_aux);
|
|
575
|
+
if (plan.scene_focus) mustWrite.push('场景焦点:' + plan.scene_focus);
|
|
576
|
+
if (headType) mustWrite.push('章首钩子:' + headType + '(前 100 字内兑现)');
|
|
577
|
+
if (tailType) mustWrite.push('章尾钩子:' + tailType + '(章尾后 100 字内兑现)');
|
|
578
|
+
if (strength) mustWrite.push('钩子强度:' + strength);
|
|
579
|
+
if (cast.length) mustWrite.push('必须出场:' + cast.map((x) => x.id + '(' + x.name + ')').join('、'));
|
|
580
|
+
if (activeConflicts.length) {
|
|
581
|
+
mustWrite.push('必须推进冲突:' + activeConflicts.map((x) => x.id + '(' + x.name + ') · ' + (x.node_type || '推进')).join('、'));
|
|
582
|
+
}
|
|
583
|
+
const relationshipChanges = tl.relationshipChanges || [];
|
|
584
|
+
if (relationshipChanges.length) {
|
|
585
|
+
mustWrite.push('必须发生的关系变化:' + relationshipChanges.map((x) =>
|
|
586
|
+
x.from_name + '→' + x.to_name + ' 进入「' + x.stage + ' / ' + x.state + '」' +
|
|
587
|
+
(x.trigger_event ? '(触发:' + x.trigger_event + ')' : '')
|
|
588
|
+
).join('、'));
|
|
589
|
+
}
|
|
590
|
+
if (foreshadowActions.length) {
|
|
591
|
+
mustWrite.push('必须完成的伏笔动作:' + foreshadowActions.map((x) => x.action + ' ' + x.id + '(' + x.name + ')').join('、'));
|
|
592
|
+
}
|
|
593
|
+
if (tl.climax) mustWrite.push('必须兑现高潮:' + tl.climax.id + '(' + tl.climax.name + ')');
|
|
594
|
+
if (tl.pleasure) mustWrite.push('必须兑现爽点:' + tl.pleasure.id + '(' + tl.pleasure.name + ')');
|
|
595
|
+
if (tl.ending) mustWrite.push('必须抵达结局节点:' + tl.ending.id + '(' + tl.ending.name + ')');
|
|
596
|
+
if (infoGaps.length) {
|
|
597
|
+
mustWrite.push('必须处理的信息差:' + infoGaps.map((x) => x.status + '「' + x.info_name + '」').join('、'));
|
|
598
|
+
}
|
|
599
|
+
if (wordTarget) mustWrite.push('字数目标:约 ' + wordTarget + ' 字');
|
|
600
|
+
|
|
601
|
+
const mustNotWrite = [];
|
|
602
|
+
for (const gap of infoGaps) {
|
|
603
|
+
for (const line of gap.forbidden_mentions) mustNotWrite.push(line);
|
|
604
|
+
}
|
|
605
|
+
const openForeshadows = [];
|
|
606
|
+
let futureForeshadowCount = 0;
|
|
607
|
+
for (const row of (inputs.timeline.foreshadow_timeline || [])) {
|
|
608
|
+
if (!row || !row.fs_id) continue;
|
|
609
|
+
if (typeof row.recovered_chapter !== 'number' || row.recovered_chapter <= ch) continue;
|
|
610
|
+
const fs = inputs.foreshadowById.get(row.fs_id) || { id: row.fs_id, name: row.fs_id };
|
|
611
|
+
if (typeof row.buried_chapter === 'number' && row.buried_chapter > ch) {
|
|
612
|
+
futureForeshadowCount += 1;
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
openForeshadows.push(row.fs_id + '(' + fs.name + ',第 ' + row.recovered_chapter + ' 章回收)');
|
|
616
|
+
}
|
|
617
|
+
for (const line of openForeshadows.slice(0, 8)) {
|
|
618
|
+
mustNotWrite.push('不得提前揭示伏笔 ' + line + ' 的真相。');
|
|
619
|
+
}
|
|
620
|
+
if (openForeshadows.length > 8) {
|
|
621
|
+
mustNotWrite.push('另有 ' + (openForeshadows.length - 8) + ' 条已埋未收伏笔:本章不得提前揭示它们的真相。');
|
|
622
|
+
}
|
|
623
|
+
if (futureForeshadowCount > 0) {
|
|
624
|
+
mustNotWrite.push('不得提前埋设/提及任何尚未到埋设章的伏笔;本章只允许执行 foreshadow_actions 中列出的动作。');
|
|
625
|
+
}
|
|
626
|
+
if (cast.length) {
|
|
627
|
+
mustNotWrite.push('不得让未列入本章 cast 的角色成为视角中心或关键行动人;需要新角色时必须先登记。');
|
|
628
|
+
}
|
|
629
|
+
if (relationshipChanges.length) {
|
|
630
|
+
mustNotWrite.push('不得把本章已经变化的关系写回变化前的状态;除非本章明确写了回退事件。');
|
|
631
|
+
}
|
|
632
|
+
mustNotWrite.push('不得提及未在本章时间线/信息差/伏笔动作中出现的冲突、伏笔、结局。');
|
|
633
|
+
mustNotWrite.push('不得改写已建立的世界观规则、角色信息状态与已知事实。');
|
|
634
|
+
|
|
635
|
+
const informationState = {};
|
|
636
|
+
const informationBoundaries = [];
|
|
637
|
+
for (const c of cast) {
|
|
638
|
+
const profile = inputs.fleshCharacterById.get(c.id);
|
|
639
|
+
if (!profile || !profile.information_state) continue;
|
|
640
|
+
informationState[c.id] = profile.information_state;
|
|
641
|
+
const info = profile.information_state;
|
|
642
|
+
if (info.unknown_secrets || info.false_beliefs) {
|
|
643
|
+
informationBoundaries.push({
|
|
644
|
+
id: c.id,
|
|
645
|
+
name: c.name,
|
|
646
|
+
unknown_secrets: info.unknown_secrets || '',
|
|
647
|
+
false_beliefs: info.false_beliefs || '',
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const opening = {
|
|
653
|
+
hook_type: headType,
|
|
654
|
+
strength,
|
|
655
|
+
technique: headTechnique(headType),
|
|
656
|
+
must: [
|
|
657
|
+
'前 100 字内让读者看见一个具体异常、问题或时限。',
|
|
658
|
+
'让读者知道本章要追的具体问题是什么。',
|
|
659
|
+
headType ? '章首钩子类型必须是「' + headType + '」。' : '',
|
|
660
|
+
].filter(Boolean),
|
|
661
|
+
avoid: [
|
|
662
|
+
'不要先写大段环境、背景或人物履历再进入钩子。',
|
|
663
|
+
'不要把章首钩子拖到场景中段才出现。',
|
|
664
|
+
'不要用旁白直接解释钩子的答案。',
|
|
665
|
+
],
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
const ending = {
|
|
669
|
+
hook_type: tailType,
|
|
670
|
+
strength,
|
|
671
|
+
technique: tailTechnique(tailType),
|
|
672
|
+
must: [
|
|
673
|
+
'章尾后 100 字内留下一个具体、可追问的未解问题。',
|
|
674
|
+
'章尾必须让读者明确知道“下一章要追什么”。',
|
|
675
|
+
tailType ? '章尾钩子类型必须是「' + tailType + '」。' : '',
|
|
676
|
+
].filter(Boolean),
|
|
677
|
+
avoid: [
|
|
678
|
+
'不要把本章所有问题解释完再收尾。',
|
|
679
|
+
'不要用总结式旁白代替钩子。',
|
|
680
|
+
'不要连续使用同一种章尾钩子(高潮期例外需注明)。',
|
|
681
|
+
],
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
const timelineSummary = (tl.events || []).map((e) => (e.type || '事件') + ':' + (e.description || e.ref || '')).join(';');
|
|
685
|
+
const sceneBeats = [
|
|
686
|
+
{
|
|
687
|
+
name: '开场',
|
|
688
|
+
goal: '在' + locationName + '用「' + (headType || '具体异常') + '」开场,前 100 字兑现场景焦点:' + (plan.scene_focus || '让读者知道本章要追什么'),
|
|
689
|
+
must: [headType ? '章首钩子:' + headType : '给出具体异常/问题', cast.length ? '至少让 ' + cast.slice(0, 3).map((x) => x.name).join('、') + ' 中的一人进入场景' : ''],
|
|
690
|
+
avoid: ['不要先讲背景设定', '不要让人物只站着说话'],
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
name: '推进',
|
|
694
|
+
goal: '按「' + (outline.function_main || '推进主线') + '」推进' + (timelineSummary ? ',必须覆盖:' + timelineSummary : '') + (activeConflicts.length ? ';让冲突 ' + activeConflicts.map((x) => x.id).join('、') + ' 的实际压力落到角色身上' : ''),
|
|
695
|
+
must: [activeConflicts.length ? '推进 ' + activeConflicts.map((x) => x.id + '(' + x.name + ')').join('、') : '推进本章主功能', timelineSummary ? '时间线:' + timelineSummary : ''].filter(Boolean),
|
|
696
|
+
avoid: ['不要用说明代替行动', '不要同时开三条以上新线'],
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
name: '转折/兑现',
|
|
700
|
+
goal: (tl.climax ? '兑现高潮 ' + tl.climax.id + ';' : '') + (tl.pleasure ? '兑现爽点 ' + tl.pleasure.id + ';' : '') + (infoGaps.length ? '让信息差发生作用:' + infoGaps.map((x) => x.info_name).join('、') : '让角色做出不可逆选择'),
|
|
701
|
+
must: [
|
|
702
|
+
tl.climax ? '高潮:' + tl.climax.id : '',
|
|
703
|
+
tl.pleasure ? '爽点:' + tl.pleasure.id : '',
|
|
704
|
+
foreshadowActions.length ? '伏笔动作:' + foreshadowActions.map((x) => x.action + ' ' + x.id).join('、') : '',
|
|
705
|
+
].filter(Boolean),
|
|
706
|
+
avoid: ['不要把高潮/爽点写成旁白总结', '不要提前揭示未到回收章的伏笔'],
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
name: '收束',
|
|
710
|
+
goal: '用「' + (tailType || '未解问题') + '」收束,并留下下一章必须回应的具体问题',
|
|
711
|
+
must: [tailType ? '章尾钩子:' + tailType : '留下具体未解问题', tl.ending ? '抵达结局节点:' + tl.ending.id : ''].filter(Boolean),
|
|
712
|
+
avoid: ['不要解释完所有问题', '不要用“欲知后事如何”式空钩子'],
|
|
713
|
+
},
|
|
714
|
+
];
|
|
715
|
+
|
|
716
|
+
const elements = {
|
|
717
|
+
characters: cast,
|
|
718
|
+
location,
|
|
719
|
+
items,
|
|
720
|
+
conflicts: activeConflicts,
|
|
721
|
+
foreshadows: foreshadowActions,
|
|
722
|
+
climax: tl.climax,
|
|
723
|
+
pleasure: tl.pleasure,
|
|
724
|
+
ending: tl.ending,
|
|
725
|
+
timeline: tl.events || [],
|
|
726
|
+
relationship_changes: tl.relationshipChanges || [],
|
|
727
|
+
information_gaps: infoGaps,
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
const brief = {
|
|
731
|
+
id: 'OB-' + chapterLink(ch),
|
|
732
|
+
ch,
|
|
733
|
+
title: '第' + ch + '章',
|
|
734
|
+
volume: plan.volume || '',
|
|
735
|
+
act: plan.act || 0,
|
|
736
|
+
pov,
|
|
737
|
+
pov_name: povName,
|
|
738
|
+
scene_focus: plan.scene_focus || '',
|
|
739
|
+
function_main: outline.function_main || '',
|
|
740
|
+
function_aux: outline.function_aux || '',
|
|
741
|
+
emotion: {
|
|
742
|
+
value: typeof outline.emotion_value === 'number' ? outline.emotion_value : null,
|
|
743
|
+
phase: outline.emotion_phase || '',
|
|
744
|
+
},
|
|
745
|
+
word_target: wordTarget,
|
|
746
|
+
faction_appearance: outline.faction_appearance || '',
|
|
747
|
+
elements,
|
|
748
|
+
cast,
|
|
749
|
+
location,
|
|
750
|
+
items,
|
|
751
|
+
conflicts: activeConflicts,
|
|
752
|
+
timeline_events: tl.events || [],
|
|
753
|
+
relationship_changes: tl.relationshipChanges || [],
|
|
754
|
+
foreshadow_actions: foreshadowActions,
|
|
755
|
+
climax: tl.climax,
|
|
756
|
+
pleasure: tl.pleasure,
|
|
757
|
+
ending: tl.ending,
|
|
758
|
+
hooks,
|
|
759
|
+
information_gaps: infoGaps,
|
|
760
|
+
information_state: informationState,
|
|
761
|
+
information_boundaries: informationBoundaries,
|
|
762
|
+
how_to_write: {
|
|
763
|
+
pov: povName + '(' + (pov || '未指定') + ')',
|
|
764
|
+
pov_rule: '保持' + (povName || '视角人物') + '的限知视角;他不知道的信息不得写成已知。',
|
|
765
|
+
opening,
|
|
766
|
+
development: sceneBeats[1],
|
|
767
|
+
turn: sceneBeats[2],
|
|
768
|
+
ending,
|
|
769
|
+
scene_beats: sceneBeats,
|
|
770
|
+
technique_refs: [
|
|
771
|
+
'对标/wiki/开头钩子/开头钩子-总结.md',
|
|
772
|
+
'对标/wiki/结尾收束/结尾收束-总结.md',
|
|
773
|
+
'对标/wiki/叙事视角/叙事视角-总结.md',
|
|
774
|
+
'对标/wiki/节奏韵律/节奏韵律-总结.md',
|
|
775
|
+
'对标/wiki/章节结构/章节结构-总结.md',
|
|
776
|
+
'对标/wiki/对话技巧/对话技巧-总结.md',
|
|
777
|
+
'对标/wiki/描写技法/描写技法-总结.md',
|
|
778
|
+
],
|
|
779
|
+
},
|
|
780
|
+
must_write: mustWrite,
|
|
781
|
+
must_not_write: mustNotWrite,
|
|
782
|
+
source: {
|
|
783
|
+
plan: '设定/raw/sources/节奏/章节规划.json',
|
|
784
|
+
timeline: '设定/raw/sources/节奏/时序映射.json',
|
|
785
|
+
outline: '设定/raw/sources/节奏/章节细纲.json',
|
|
786
|
+
flesh_conflicts: '设定/raw/sources/血肉/冲突.json',
|
|
787
|
+
flesh_characters: '设定/raw/sources/血肉/角色.json',
|
|
788
|
+
},
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
const file = path.join(inputs.projectRoot, ...BRIEF_DIR, '第' + pad3(ch) + '章.json');
|
|
792
|
+
writeJson(file, brief);
|
|
793
|
+
byCh.set(ch, brief);
|
|
794
|
+
files.push(file);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// 索引:让别的 LLM 一眼知道 600 章细纲在哪、有哪些文件。
|
|
798
|
+
const index = {
|
|
799
|
+
generated_at: new Date().toISOString(),
|
|
800
|
+
chapter_count: files.length,
|
|
801
|
+
dir: '设定/raw/sources/节奏/细纲',
|
|
802
|
+
files: files.map((file) => path.basename(file)),
|
|
803
|
+
};
|
|
804
|
+
writeJson(path.join(inputs.projectRoot, ...BRIEF_DIR, 'index.json'), index);
|
|
805
|
+
return { count: files.length, files, byCh };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** 生成伏笔/爽点/冲突/信息差/时间线的独立档案文件。 */
|
|
809
|
+
export function buildObjectArtifacts(inputs, infoGaps) {
|
|
810
|
+
const root = path.join(inputs.projectRoot, ...ARTIFACT_DIR);
|
|
811
|
+
const counts = {
|
|
812
|
+
foreshadows: 0,
|
|
813
|
+
pleasures: 0,
|
|
814
|
+
conflicts: 0,
|
|
815
|
+
info_gaps: 0,
|
|
816
|
+
timeline_chapters: 0,
|
|
817
|
+
};
|
|
818
|
+
const index = {
|
|
819
|
+
generated_at: new Date().toISOString(),
|
|
820
|
+
dir: '设定/raw/sources/档案',
|
|
821
|
+
foreshadows: [],
|
|
822
|
+
pleasures: [],
|
|
823
|
+
conflicts: [],
|
|
824
|
+
info_gaps: [],
|
|
825
|
+
timeline: [],
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
for (const row of (inputs.entities.foreshadows || [])) {
|
|
829
|
+
if (!row || !row.id) continue;
|
|
830
|
+
const tl = (inputs.timeline.foreshadow_timeline || []).find((x) => x && x.fs_id === row.id) || {};
|
|
831
|
+
const obj = {
|
|
832
|
+
id: row.id,
|
|
833
|
+
name: row.name || row.id,
|
|
834
|
+
type: row.type || '',
|
|
835
|
+
description: row.name || row.id,
|
|
836
|
+
buried_chapter: typeof tl.buried_chapter === 'number' ? tl.buried_chapter : null,
|
|
837
|
+
recovered_chapter: typeof tl.recovered_chapter === 'number' ? tl.recovered_chapter : null,
|
|
838
|
+
status: typeof tl.recovered_chapter === 'number' ? 'planned' : 'open',
|
|
839
|
+
};
|
|
840
|
+
writeJson(path.join(root, '伏笔', row.id + '.json'), obj);
|
|
841
|
+
index.foreshadows.push(obj.id);
|
|
842
|
+
counts.foreshadows += 1;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
for (const row of (inputs.entities.pleasures || [])) {
|
|
846
|
+
if (!row || !row.id) continue;
|
|
847
|
+
const tl = (inputs.timeline.pleasure_timeline || []).find((x) => x && x.pleasure_id === row.id) || {};
|
|
848
|
+
const obj = {
|
|
849
|
+
id: row.id,
|
|
850
|
+
name: row.name || row.id,
|
|
851
|
+
scale: row.scale || '',
|
|
852
|
+
kind: row.kind || '',
|
|
853
|
+
chapter: typeof tl.chapter === 'number' ? tl.chapter : null,
|
|
854
|
+
description: row.name || row.id,
|
|
855
|
+
};
|
|
856
|
+
writeJson(path.join(root, '爽点', row.id + '.json'), obj);
|
|
857
|
+
index.pleasures.push(obj.id);
|
|
858
|
+
counts.pleasures += 1;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
for (const row of (inputs.entities.conflicts || [])) {
|
|
862
|
+
if (!row || !row.id) continue;
|
|
863
|
+
const flesh = inputs.fleshConflictById.get(row.id) || {};
|
|
864
|
+
const tl = (inputs.timeline.conflict_timeline || []).find((x) => x && x.conflict_id === row.id) || {};
|
|
865
|
+
const obj = {
|
|
866
|
+
id: row.id,
|
|
867
|
+
name: row.name || row.id,
|
|
868
|
+
type: row.type || '',
|
|
869
|
+
scale: row.scale || '',
|
|
870
|
+
description: flesh.description || row.name || row.id,
|
|
871
|
+
core_goal: flesh.core_goal || '',
|
|
872
|
+
influence_scope: flesh.influence_scope || '',
|
|
873
|
+
focus_points: flesh.focus_points || [],
|
|
874
|
+
surface_contradiction: flesh.surface_contradiction || '',
|
|
875
|
+
deep_contradiction: flesh.deep_contradiction || '',
|
|
876
|
+
ultimate_contradiction: flesh.ultimate_contradiction || '',
|
|
877
|
+
// 血肉阶段的内容字段必须原样保留。档案是“合并视图”,不是有损投影。
|
|
878
|
+
escalation_path: Array.isArray(flesh.escalation_path) ? flesh.escalation_path : [],
|
|
879
|
+
resolution_plan: flesh.resolution_plan || null,
|
|
880
|
+
// 节奏阶段只补“发生在第几章”,没有节奏时保留空数组/null,不覆盖血肉内容。
|
|
881
|
+
escalation_nodes: Array.isArray(tl.escalation_nodes) ? tl.escalation_nodes : [],
|
|
882
|
+
resolution_chapter: typeof tl.resolution_chapter === 'number' ? tl.resolution_chapter : null,
|
|
883
|
+
information_gaps: infoGaps.filter((x) => x.conflict_id === row.id),
|
|
884
|
+
// 参与方是关系数据,来自骨架 关系.json 的 conflict_participants;
|
|
885
|
+
// 不在血肉冲突里重复存,但档案要把它并入。
|
|
886
|
+
participants: inputs.participantsByConflict.get(row.id) || [],
|
|
887
|
+
participant_names: (inputs.participantsByConflict.get(row.id) || [])
|
|
888
|
+
.map((id) => inputs.characterById.get(id)?.name || id),
|
|
889
|
+
};
|
|
890
|
+
writeJson(path.join(root, '冲突', row.id + '.json'), obj);
|
|
891
|
+
index.conflicts.push(obj.id);
|
|
892
|
+
counts.conflicts += 1;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
for (const gap of infoGaps) {
|
|
896
|
+
writeJson(path.join(root, '信息差', gap.id + '.json'), gap);
|
|
897
|
+
index.info_gaps.push(gap.id);
|
|
898
|
+
counts.info_gaps += 1;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// 每章一份时间线,方便别的 LLM 按章阅读,也方便 lint 检查“本章事件是否完整”。
|
|
902
|
+
for (const plan of (inputs.plan.chapters || [])) {
|
|
903
|
+
if (!plan || typeof plan.ch !== 'number') continue;
|
|
904
|
+
const obj = buildTimelineFile(inputs, plan.ch);
|
|
905
|
+
writeJson(path.join(root, '时间线', 'CH' + pad3(plan.ch) + '.json'), obj);
|
|
906
|
+
index.timeline.push(obj.chapter);
|
|
907
|
+
counts.timeline_chapters += 1;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
index.counts = counts;
|
|
911
|
+
writeJson(path.join(root, 'index.json'), index);
|
|
912
|
+
return { counts, index };
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* 生成全部独立档案与每章细纲。
|
|
917
|
+
*
|
|
918
|
+
* 幂等:重复执行只会覆盖同名文件;不会删除用户新增的文件。
|
|
919
|
+
*
|
|
920
|
+
* @param {string} projectRoot
|
|
921
|
+
* @returns {{counts: object, briefCount: number, briefs: Map<number, object>}}
|
|
922
|
+
*/
|
|
923
|
+
export function buildStoryArtifacts(projectRoot) {
|
|
924
|
+
const inputs = loadArtifactInputs(projectRoot);
|
|
925
|
+
const infoGaps = collectInfoGaps(inputs);
|
|
926
|
+
const objects = buildObjectArtifacts(inputs, infoGaps);
|
|
927
|
+
const briefs = buildChapterBriefs(inputs);
|
|
928
|
+
return {
|
|
929
|
+
counts: objects.counts,
|
|
930
|
+
briefCount: briefs.count,
|
|
931
|
+
briefs: briefs.byCh || new Map(),
|
|
932
|
+
};
|
|
933
|
+
}
|