@cloud411716/fancy-webnovel 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client.js +201 -15
- package/cordis.patch.yml +5 -0
- 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 +11 -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 +29 -4
- 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 +143 -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,798 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage JSON -> SurrealDB.
|
|
3
|
+
*
|
|
4
|
+
* The LLM still writes JSON as its generation output, but that JSON is only a
|
|
5
|
+
* transport buffer. This module is the only place that turns it into story
|
|
6
|
+
* data; after a successful import the database is the source of truth.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { dbErrorMessage, StoryDbError } from './client.js';
|
|
12
|
+
import { surrealString, recordLiteral } from './ids.js';
|
|
13
|
+
|
|
14
|
+
const STORY = 'story:main';
|
|
15
|
+
|
|
16
|
+
const STAGE_DIR = {
|
|
17
|
+
'10-scale': '10-scale',
|
|
18
|
+
'20-macro': '20-macro',
|
|
19
|
+
'30-cast': '30-cast',
|
|
20
|
+
'40-rhythm': '40-rhythm',
|
|
21
|
+
'50-writing': '50-writing',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function stageDir(projectRoot, stageId, sub) {
|
|
25
|
+
return path.join(projectRoot, '设定', STAGE_DIR[stageId] || stageId, sub || '');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readJson(file, fallback = null) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
31
|
+
} catch {
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function asArray(value) {
|
|
37
|
+
return Array.isArray(value) ? value : [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function withoutMeta(obj) {
|
|
41
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
42
|
+
const copy = { ...obj };
|
|
43
|
+
delete copy._meta;
|
|
44
|
+
return copy;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function compact(fields) {
|
|
48
|
+
const out = {};
|
|
49
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
50
|
+
if (value !== undefined) out[key] = value;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function upsert(sdb, table, id, fields) {
|
|
56
|
+
const clean = compact(fields);
|
|
57
|
+
delete clean.story; // always the literal story:main, never a bound string
|
|
58
|
+
const keys = Object.keys(clean);
|
|
59
|
+
const sets = ['story = story:main', ...keys.map((key) => key + ' = $' + key)].join(', ');
|
|
60
|
+
await sdb.query(
|
|
61
|
+
`UPSERT type::record(${surrealString(table)}, $id) SET ${sets};`,
|
|
62
|
+
{ id: String(id), ...clean },
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function upsertStageDoc(sdb, stage, name, payload) {
|
|
67
|
+
await sdb.query(
|
|
68
|
+
`UPSERT stage_doc SET story = ${STORY}, stage = $stage, name = $name, payload = $payload
|
|
69
|
+
WHERE story = ${STORY} AND stage = $stage AND name = $name;`,
|
|
70
|
+
{ stage, name, payload },
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function deleteNotIn(sdb, table, ids) {
|
|
75
|
+
if (!Array.isArray(ids) || !ids.length) return;
|
|
76
|
+
await sdb.query(
|
|
77
|
+
`DELETE ${table} WHERE story = ${STORY} AND entity_id NOT IN $ids;`,
|
|
78
|
+
{ ids: ids.map(String) },
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// scale
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
export async function importScale(sdb, projectRoot) {
|
|
87
|
+
const file = stageDir(projectRoot, '10-scale', 'scale.json');
|
|
88
|
+
const scale = readJson(file, null);
|
|
89
|
+
if (!scale) throw new StoryDbError('STORYDB_IMPORT', '缺少 10-scale/scale.json');
|
|
90
|
+
|
|
91
|
+
await sdb.query(
|
|
92
|
+
`UPSERT scale:main SET
|
|
93
|
+
story = ${STORY},
|
|
94
|
+
capacity = $capacity,
|
|
95
|
+
quotas = $quotas,
|
|
96
|
+
rhythm_budget = $rhythm_budget,
|
|
97
|
+
distribution = $distribution,
|
|
98
|
+
constraints = $constraints,
|
|
99
|
+
formulas = $formulas,
|
|
100
|
+
derivations = $derivations,
|
|
101
|
+
genre_profile = $genre_profile,
|
|
102
|
+
formula_version = $formula_version,
|
|
103
|
+
payload = $payload;`,
|
|
104
|
+
{
|
|
105
|
+
capacity: scale.capacity || {},
|
|
106
|
+
quotas: scale.quotas || {},
|
|
107
|
+
rhythm_budget: scale.rhythm_budget || {},
|
|
108
|
+
distribution: scale.distribution || {},
|
|
109
|
+
constraints: scale.constraints || [],
|
|
110
|
+
formulas: scale.formulas || {},
|
|
111
|
+
derivations: scale.derivations || {},
|
|
112
|
+
genre_profile: scale.genre_profile || undefined,
|
|
113
|
+
formula_version: scale.formula_version || undefined,
|
|
114
|
+
payload: scale,
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
await sdb.query(`DELETE volume WHERE story = ${STORY};`);
|
|
119
|
+
await sdb.query(`DELETE act WHERE story = ${STORY};`);
|
|
120
|
+
|
|
121
|
+
const volumes = asArray(scale.structure && scale.structure.volumes);
|
|
122
|
+
for (const volume of volumes) {
|
|
123
|
+
await upsert(sdb, 'volume', volume.volume_id || volume.id, {
|
|
124
|
+
story: STORY,
|
|
125
|
+
volume_id: volume.volume_id || volume.id,
|
|
126
|
+
title: volume.title || undefined,
|
|
127
|
+
chapter_start: Number(volume.chapter_start) || 0,
|
|
128
|
+
chapter_end: Number(volume.chapter_end) || 0,
|
|
129
|
+
goal: volume.goal || undefined,
|
|
130
|
+
core_conflict: volume.core_conflict || undefined,
|
|
131
|
+
emotional_arc: volume.emotional_arc || undefined,
|
|
132
|
+
ending_hook: volume.ending_hook || undefined,
|
|
133
|
+
payload: withoutMeta(volume),
|
|
134
|
+
});
|
|
135
|
+
for (const act of asArray(volume.acts)) {
|
|
136
|
+
await upsert(sdb, 'act', act.act_id || act.id, {
|
|
137
|
+
story: STORY,
|
|
138
|
+
act_id: act.act_id || act.id,
|
|
139
|
+
volume_id: volume.volume_id || volume.id,
|
|
140
|
+
act_index: Number(act.act_index) || 0,
|
|
141
|
+
chapter_start: Number(act.chapter_start) || 0,
|
|
142
|
+
chapter_end: Number(act.chapter_end) || 0,
|
|
143
|
+
goal: act.goal || undefined,
|
|
144
|
+
payload: withoutMeta(act),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
for (const act of asArray(scale.structure && scale.structure.acts)) {
|
|
149
|
+
await upsert(sdb, 'act', act.act_id || act.id, {
|
|
150
|
+
story: STORY,
|
|
151
|
+
act_id: act.act_id || act.id,
|
|
152
|
+
volume_id: act.volume_id || '',
|
|
153
|
+
act_index: Number(act.act_index) || 0,
|
|
154
|
+
chapter_start: Number(act.chapter_start) || 0,
|
|
155
|
+
chapter_end: Number(act.chapter_end) || 0,
|
|
156
|
+
goal: act.goal || undefined,
|
|
157
|
+
payload: withoutMeta(act),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await upsertStageDoc(sdb, '10-scale', 'scale', scale);
|
|
162
|
+
return { stage: '10-scale', ok: true };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// macro
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
const MACRO_FILES = ['world', 'story', 'volumes', 'lines', 'endings', 'emotion', 'roles'];
|
|
170
|
+
|
|
171
|
+
export async function importMacro(sdb, projectRoot) {
|
|
172
|
+
const docs = {};
|
|
173
|
+
for (const name of MACRO_FILES) {
|
|
174
|
+
const doc = readJson(stageDir(projectRoot, '20-macro', name + '.json'), null);
|
|
175
|
+
if (!doc) throw new StoryDbError('STORYDB_IMPORT', '缺少 20-macro/' + name + '.json');
|
|
176
|
+
docs[name] = doc;
|
|
177
|
+
await upsertStageDoc(sdb, '20-macro', name, doc);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// volumes / acts: enrich the scale skeleton with macro details.
|
|
181
|
+
for (const volume of asArray(docs.volumes.volumes)) {
|
|
182
|
+
await sdb.query(
|
|
183
|
+
`UPSERT volume SET
|
|
184
|
+
story = ${STORY}, volume_id = $volume_id, title = $title,
|
|
185
|
+
chapter_start = $chapter_start, chapter_end = $chapter_end,
|
|
186
|
+
goal = $goal, core_conflict = $core_conflict,
|
|
187
|
+
emotional_arc = $emotional_arc, ending_hook = $ending_hook,
|
|
188
|
+
payload = $payload
|
|
189
|
+
WHERE story = ${STORY} AND volume_id = $volume_id;`,
|
|
190
|
+
{
|
|
191
|
+
volume_id: volume.id || volume.volume_id,
|
|
192
|
+
title: volume.title || undefined,
|
|
193
|
+
chapter_start: Number(volume.chapter_start) || 0,
|
|
194
|
+
chapter_end: Number(volume.chapter_end) || 0,
|
|
195
|
+
goal: volume.goal || undefined,
|
|
196
|
+
core_conflict: volume.core_conflict || undefined,
|
|
197
|
+
emotional_arc: volume.emotional_arc || undefined,
|
|
198
|
+
ending_hook: volume.ending_hook || undefined,
|
|
199
|
+
payload: withoutMeta(volume),
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
for (const act of asArray(volume.acts)) {
|
|
203
|
+
await sdb.query(
|
|
204
|
+
`UPSERT act SET
|
|
205
|
+
story = ${STORY}, act_id = $act_id, volume_id = $volume_id,
|
|
206
|
+
act_index = $act_index, chapter_start = $chapter_start,
|
|
207
|
+
chapter_end = $chapter_end, goal = $goal, payload = $payload
|
|
208
|
+
WHERE story = ${STORY} AND act_id = $act_id;`,
|
|
209
|
+
{
|
|
210
|
+
act_id: act.act_id || act.id,
|
|
211
|
+
volume_id: volume.id || volume.volume_id,
|
|
212
|
+
act_index: Number(act.act_index) || 0,
|
|
213
|
+
chapter_start: Number(act.chapter_start) || 0,
|
|
214
|
+
chapter_end: Number(act.chapter_end) || 0,
|
|
215
|
+
goal: act.goal || undefined,
|
|
216
|
+
payload: withoutMeta(act),
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// lines / nodes
|
|
223
|
+
await sdb.query(`DELETE line_node WHERE story = ${STORY};`);
|
|
224
|
+
await sdb.query(`DELETE line WHERE story = ${STORY};`);
|
|
225
|
+
const main = docs.lines.main_line || {};
|
|
226
|
+
if (main.id || main.name) {
|
|
227
|
+
await upsert(sdb, 'line', main.id || 'M001', {
|
|
228
|
+
story: STORY,
|
|
229
|
+
line_id: main.id || 'M001',
|
|
230
|
+
kind: 'main',
|
|
231
|
+
name: main.name || '主线',
|
|
232
|
+
payload: withoutMeta(main),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
for (const node of asArray(main.nodes)) {
|
|
236
|
+
await upsert(sdb, 'line_node', node.id, {
|
|
237
|
+
story: STORY,
|
|
238
|
+
node_id: node.id,
|
|
239
|
+
line_id: main.id || 'M001',
|
|
240
|
+
volume: node.volume || undefined,
|
|
241
|
+
act: node.act || undefined,
|
|
242
|
+
goal: node.goal || undefined,
|
|
243
|
+
turn: node.turn || undefined,
|
|
244
|
+
payload: withoutMeta(node),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
for (const branch of asArray(docs.lines.branches)) {
|
|
248
|
+
await upsert(sdb, 'line', branch.id, {
|
|
249
|
+
story: STORY,
|
|
250
|
+
line_id: branch.id,
|
|
251
|
+
kind: 'branch',
|
|
252
|
+
name: branch.name || branch.id,
|
|
253
|
+
payload: withoutMeta(branch),
|
|
254
|
+
});
|
|
255
|
+
for (const nodeId of asArray(branch.interweaves)) {
|
|
256
|
+
await upsert(sdb, 'line_node', nodeId, {
|
|
257
|
+
story: STORY,
|
|
258
|
+
node_id: nodeId,
|
|
259
|
+
line_id: branch.id,
|
|
260
|
+
payload: { id: nodeId },
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// endings
|
|
266
|
+
await sdb.query(`DELETE ending WHERE story = ${STORY};`);
|
|
267
|
+
for (const ending of asArray(docs.endings.endings)) {
|
|
268
|
+
await upsert(sdb, 'ending', ending.id, {
|
|
269
|
+
story: STORY,
|
|
270
|
+
ending_id: ending.id,
|
|
271
|
+
level: ending.level || undefined,
|
|
272
|
+
type: ending.type || undefined,
|
|
273
|
+
direction: ending.direction || undefined,
|
|
274
|
+
payload: withoutMeta(ending),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return { stage: '20-macro', ok: true };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// cast
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
const ENTITY_SPECS = {
|
|
286
|
+
characters: {
|
|
287
|
+
table: 'character',
|
|
288
|
+
list: 'characters',
|
|
289
|
+
fields: ['role_slot', 'faction', 'description', 'goal', 'flaw', 'arc', 'traits'],
|
|
290
|
+
},
|
|
291
|
+
factions: {
|
|
292
|
+
table: 'faction',
|
|
293
|
+
list: 'factions',
|
|
294
|
+
fields: ['description', 'goal', 'resources', 'territory', 'internal_conflict'],
|
|
295
|
+
},
|
|
296
|
+
locations: {
|
|
297
|
+
table: 'location',
|
|
298
|
+
list: 'locations',
|
|
299
|
+
fields: ['description', 'narrative_function', 'rules'],
|
|
300
|
+
},
|
|
301
|
+
items: {
|
|
302
|
+
table: 'item',
|
|
303
|
+
list: 'items',
|
|
304
|
+
fields: ['category', 'description', 'item_function', 'owner', 'limit'],
|
|
305
|
+
},
|
|
306
|
+
conflicts: {
|
|
307
|
+
table: 'conflict',
|
|
308
|
+
list: 'conflicts',
|
|
309
|
+
fields: ['description', 'core_goal', 'stakes', 'escalation_path', 'resolution_plan', 'participants'],
|
|
310
|
+
},
|
|
311
|
+
foreshadows: {
|
|
312
|
+
table: 'foreshadow',
|
|
313
|
+
list: 'foreshadows',
|
|
314
|
+
fields: ['setup', 'clues', 'payoff_target', 'recovery_requirement', 'red_herrings', 'scope', 'initial_knowledge'],
|
|
315
|
+
},
|
|
316
|
+
payoffs: {
|
|
317
|
+
table: 'payoff',
|
|
318
|
+
list: 'payoffs',
|
|
319
|
+
fields: ['scale', 'setup', 'payoff', 'emotional_effect', 'required_setup'],
|
|
320
|
+
},
|
|
321
|
+
climaxes: {
|
|
322
|
+
table: 'climax',
|
|
323
|
+
list: 'climaxes',
|
|
324
|
+
fields: ['description', 'payoff', 'participants'],
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
export async function importCast(sdb, projectRoot) {
|
|
329
|
+
const files = {};
|
|
330
|
+
for (const name of [...Object.keys(ENTITY_SPECS), 'relations', 'information']) {
|
|
331
|
+
const doc = readJson(stageDir(projectRoot, '30-cast', name + '.json'), null);
|
|
332
|
+
if (!doc) throw new StoryDbError('STORYDB_IMPORT', '缺少 30-cast/' + name + '.json');
|
|
333
|
+
files[name] = doc;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
for (const [fileKey, spec] of Object.entries(ENTITY_SPECS)) {
|
|
337
|
+
const rows = asArray(files[fileKey][spec.list]);
|
|
338
|
+
const ids = rows.map((row) => String(row.id));
|
|
339
|
+
for (const row of rows) {
|
|
340
|
+
const fields = {
|
|
341
|
+
story: STORY,
|
|
342
|
+
entity_id: String(row.id),
|
|
343
|
+
name: row.name || row.id,
|
|
344
|
+
type: row.type || undefined,
|
|
345
|
+
payload: withoutMeta(row),
|
|
346
|
+
};
|
|
347
|
+
for (const key of spec.fields) {
|
|
348
|
+
if (row[key] !== undefined) fields[key] = row[key];
|
|
349
|
+
}
|
|
350
|
+
await upsert(sdb, spec.table, row.id, fields);
|
|
351
|
+
}
|
|
352
|
+
await deleteNotIn(sdb, spec.table, ids);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const gaps = asArray(files.information.information_gaps);
|
|
356
|
+
for (const gap of gaps) {
|
|
357
|
+
await upsert(sdb, 'information', gap.id, {
|
|
358
|
+
story: STORY,
|
|
359
|
+
entity_id: String(gap.id),
|
|
360
|
+
info_name: gap.info_name || gap.name || gap.id,
|
|
361
|
+
conflict: gap.conflict || undefined,
|
|
362
|
+
who_knows: gap.who_knows || [],
|
|
363
|
+
who_does_not_know: gap.who_does_not_know || [],
|
|
364
|
+
resolution_method: gap.resolution_method || undefined,
|
|
365
|
+
writing_ban: gap.writing_ban || undefined,
|
|
366
|
+
initial_knowledge: gap.initial_knowledge || [],
|
|
367
|
+
payload: withoutMeta(gap),
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
await deleteNotIn(sdb, 'information', gaps.map((gap) => String(gap.id)));
|
|
371
|
+
|
|
372
|
+
// Rebuild all cast edges from scratch; they are derived from the documents.
|
|
373
|
+
for (const table of ['relation', 'faction_relation', 'membership', 'conflict_participant', 'climax_participant', 'foreshadow_payoff']) {
|
|
374
|
+
await sdb.query(`DELETE ${table} WHERE story = ${STORY};`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const rels = files.relations;
|
|
378
|
+
for (const rel of asArray(rels.character_relations)) {
|
|
379
|
+
const from = rel.from || rel.from_id;
|
|
380
|
+
const to = rel.to || rel.to_id;
|
|
381
|
+
if (!from || !to) continue;
|
|
382
|
+
await sdb.query(
|
|
383
|
+
`RELATE ${recordLiteral('character', from)}->relation->${recordLiteral('character', to)} SET
|
|
384
|
+
story = ${STORY}, from_id = $from, to_id = $to,
|
|
385
|
+
kind = $kind, initial_state = $initial_state, changes = $changes, payload = $payload;`,
|
|
386
|
+
{
|
|
387
|
+
from: String(from),
|
|
388
|
+
to: String(to),
|
|
389
|
+
kind: rel.kind || rel.type || '未知',
|
|
390
|
+
initial_state: rel.initial_state || undefined,
|
|
391
|
+
changes: rel.changes || [],
|
|
392
|
+
payload: withoutMeta(rel),
|
|
393
|
+
},
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
for (const rel of asArray(rels.faction_relations)) {
|
|
398
|
+
const from = rel.from || rel.from_id;
|
|
399
|
+
const to = rel.to || rel.to_id;
|
|
400
|
+
if (!from || !to) continue;
|
|
401
|
+
await sdb.query(
|
|
402
|
+
`RELATE ${recordLiteral('faction', from)}->faction_relation->${recordLiteral('faction', to)} SET
|
|
403
|
+
story = ${STORY}, from_id = $from, to_id = $to, kind = $kind, payload = $payload;`,
|
|
404
|
+
{
|
|
405
|
+
from: String(from),
|
|
406
|
+
to: String(to),
|
|
407
|
+
kind: rel.kind || rel.type || '未知',
|
|
408
|
+
payload: withoutMeta(rel),
|
|
409
|
+
},
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
for (const member of asArray(rels.memberships)) {
|
|
414
|
+
const character = member.character || member.from || member.character_id;
|
|
415
|
+
const faction = member.faction || member.to || member.faction_id;
|
|
416
|
+
if (!character || !faction) continue;
|
|
417
|
+
await sdb.query(
|
|
418
|
+
`RELATE ${recordLiteral('character', character)}->membership->${recordLiteral('faction', faction)} SET
|
|
419
|
+
story = ${STORY}, role = $role, payload = $payload;`,
|
|
420
|
+
{
|
|
421
|
+
character: String(character),
|
|
422
|
+
faction: String(faction),
|
|
423
|
+
role: member.role || undefined,
|
|
424
|
+
payload: withoutMeta(member),
|
|
425
|
+
},
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
for (const conflict of asArray(files.conflicts.conflicts)) {
|
|
430
|
+
for (const participant of asArray(conflict.participants)) {
|
|
431
|
+
await sdb.query(
|
|
432
|
+
`RELATE ${recordLiteral('conflict', conflict.id)}->conflict_participant->${recordLiteral('character', participant)} SET story = ${STORY};`,
|
|
433
|
+
{ conflict: String(conflict.id), character: String(participant) },
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (const climax of asArray(files.climaxes.climaxes)) {
|
|
439
|
+
for (const participant of asArray(climax.participants)) {
|
|
440
|
+
await sdb.query(
|
|
441
|
+
`RELATE ${recordLiteral('climax', climax.id)}->climax_participant->${recordLiteral('character', participant)} SET story = ${STORY};`,
|
|
442
|
+
{ climax: String(climax.id), character: String(participant) },
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const payoffIds = new Set(asArray(files.payoffs.payoffs).map((row) => String(row.id)));
|
|
448
|
+
for (const fs of asArray(files.foreshadows.foreshadows)) {
|
|
449
|
+
if (fs.payoff_target && payoffIds.has(String(fs.payoff_target))) {
|
|
450
|
+
await sdb.query(
|
|
451
|
+
`RELATE ${recordLiteral('foreshadow', fs.id)}->foreshadow_payoff->${recordLiteral('payoff', fs.payoff_target)} SET story = ${STORY}, relation = $relation;`,
|
|
452
|
+
{ fs: String(fs.id), payoff: String(fs.payoff_target), relation: fs.recovery_requirement || undefined },
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return { stage: '30-cast', ok: true };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
// rhythm
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
function actionState(value, fallback) {
|
|
465
|
+
return value || fallback;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export async function importRhythm(sdb, projectRoot) {
|
|
469
|
+
const doc = readJson(stageDir(projectRoot, '40-rhythm', 'chapter_map.json'), null);
|
|
470
|
+
if (!doc || !Array.isArray(doc.chapters)) {
|
|
471
|
+
throw new StoryDbError('STORYDB_IMPORT', '缺少 40-rhythm/chapter_map.json');
|
|
472
|
+
}
|
|
473
|
+
const chapters = [...doc.chapters].sort((a, b) => Number(a.display_no) - Number(b.display_no));
|
|
474
|
+
|
|
475
|
+
// Rebuild the timeline from scratch so the knowledge replay is deterministic.
|
|
476
|
+
await sdb.query(`DELETE knowledge_event WHERE story = ${STORY};`);
|
|
477
|
+
await sdb.query(`DELETE knowledge_state WHERE story = ${STORY};`);
|
|
478
|
+
for (const table of ['chapter_cast', 'chapter_location', 'foreshadow_action', 'payoff_action', 'info_action', 'ending_action']) {
|
|
479
|
+
await sdb.query(`DELETE ${table} WHERE story = ${STORY};`);
|
|
480
|
+
}
|
|
481
|
+
await sdb.query(`DELETE chapter_metric WHERE story = ${STORY};`);
|
|
482
|
+
|
|
483
|
+
const chapterIds = chapters.map((c) => String(c.chapter_id));
|
|
484
|
+
// Remove chapters that no longer exist, together with their dependents.
|
|
485
|
+
const old = await sdb.rows(`SELECT chapter_id FROM chapter WHERE story = ${STORY};`);
|
|
486
|
+
for (const row of old) {
|
|
487
|
+
if (!chapterIds.includes(String(row.chapter_id))) {
|
|
488
|
+
await sdb.query(`DELETE outline WHERE story = ${STORY} AND chapter_id = $id;`, { id: row.chapter_id });
|
|
489
|
+
await sdb.query(`DELETE chapter_text WHERE story = ${STORY} AND chapter_id = $id;`, { id: row.chapter_id });
|
|
490
|
+
await sdb.query(`DELETE ledger_entry WHERE story = ${STORY} AND chapter_id = $id;`, { id: row.chapter_id });
|
|
491
|
+
await sdb.query(`DELETE chapter WHERE story = ${STORY} AND chapter_id = $id;`, { id: row.chapter_id });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
for (const chapter of chapters) {
|
|
496
|
+
const displayNo = Number(chapter.display_no);
|
|
497
|
+
await sdb.query(
|
|
498
|
+
`UPSERT type::record('chapter', $chapter_id) SET
|
|
499
|
+
story = ${STORY}, chapter_id = $chapter_id, display_no = $display_no,
|
|
500
|
+
title = $title, volume = $volume, act = $act, pov = $pov,
|
|
501
|
+
word_target = $word_target,
|
|
502
|
+
emotion_value = $emotion_value, hook_type = $hook_type, hook_strength = $hook_strength,
|
|
503
|
+
time = $time, why = $why, how = $how, emotion = $emotion, hook = $hook,
|
|
504
|
+
payload = $payload;`,
|
|
505
|
+
{
|
|
506
|
+
chapter_id: String(chapter.chapter_id),
|
|
507
|
+
display_no: displayNo,
|
|
508
|
+
title: chapter.title || '第 ' + displayNo + ' 章',
|
|
509
|
+
volume: chapter.volume || '',
|
|
510
|
+
act: chapter.act || '',
|
|
511
|
+
pov: chapter.pov || undefined,
|
|
512
|
+
word_target: Number(chapter.word_target) || 0,
|
|
513
|
+
emotion_value: Number(chapter.emotion && chapter.emotion.value) || undefined,
|
|
514
|
+
hook_type: chapter.hook && chapter.hook.type || undefined,
|
|
515
|
+
hook_strength: chapter.hook && chapter.hook.strength || undefined,
|
|
516
|
+
time: chapter.time || {},
|
|
517
|
+
why: chapter.why || {},
|
|
518
|
+
how: chapter.how || {},
|
|
519
|
+
emotion: chapter.emotion || {},
|
|
520
|
+
hook: chapter.hook || {},
|
|
521
|
+
payload: withoutMeta(chapter),
|
|
522
|
+
},
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
const chapThing = `type::record('chapter', ${surrealString(String(chapter.chapter_id))})`;
|
|
526
|
+
const chapRec = recordLiteral('chapter', chapter.chapter_id);
|
|
527
|
+
|
|
528
|
+
const cast = [...new Set(asArray(chapter.cast).map(String))];
|
|
529
|
+
for (const character of cast) {
|
|
530
|
+
const role = chapter.pov && String(chapter.pov) === character ? 'pov' : 'cast';
|
|
531
|
+
await sdb.query(
|
|
532
|
+
`RELATE ${chapRec}->chapter_cast->${recordLiteral('character', character)} SET story = ${STORY}, role = $role;`,
|
|
533
|
+
{ character, role },
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
if (chapter.pov && !cast.includes(String(chapter.pov))) {
|
|
537
|
+
await sdb.query(
|
|
538
|
+
`RELATE ${chapRec}->chapter_cast->${recordLiteral('character', chapter.pov)} SET story = ${STORY}, role = 'pov';`,
|
|
539
|
+
{ character: String(chapter.pov) },
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
for (const character of asArray(chapter.mentioned).map(String)) {
|
|
543
|
+
await sdb.query(
|
|
544
|
+
`RELATE ${chapRec}->chapter_cast->${recordLiteral('character', character)} SET story = ${STORY}, role = 'mentioned';`,
|
|
545
|
+
{ character },
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
for (const location of [...new Set(asArray(chapter.locations).map(String))]) {
|
|
550
|
+
await sdb.query(
|
|
551
|
+
`RELATE ${chapRec}->chapter_location->${recordLiteral('location', location)} SET story = ${STORY};`,
|
|
552
|
+
{ location },
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
for (const action of asArray(chapter.foreshadow_actions)) {
|
|
557
|
+
if (!action || !action.id) continue;
|
|
558
|
+
await sdb.query(
|
|
559
|
+
`RELATE ${chapRec}->foreshadow_action->${recordLiteral('foreshadow', action.id)} SET
|
|
560
|
+
story = ${STORY}, chapter_no = $chapter_no, foreshadow_id = $id,
|
|
561
|
+
action = $action, detail = $detail, payload = $payload;`,
|
|
562
|
+
{
|
|
563
|
+
id: String(action.id),
|
|
564
|
+
chapter_no: displayNo,
|
|
565
|
+
action: action.action || '埋设',
|
|
566
|
+
detail: action.detail || undefined,
|
|
567
|
+
payload: action,
|
|
568
|
+
},
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
for (const action of asArray(chapter.payoff_actions)) {
|
|
573
|
+
if (!action || !action.id) continue;
|
|
574
|
+
await sdb.query(
|
|
575
|
+
`RELATE ${chapRec}->payoff_action->${recordLiteral('payoff', action.id)} SET
|
|
576
|
+
story = ${STORY}, chapter_no = $chapter_no, payoff_id = $id,
|
|
577
|
+
action = $action, detail = $detail, payload = $payload;`,
|
|
578
|
+
{
|
|
579
|
+
id: String(action.id),
|
|
580
|
+
chapter_no: displayNo,
|
|
581
|
+
action: action.action || '铺垫',
|
|
582
|
+
detail: action.detail || undefined,
|
|
583
|
+
payload: action,
|
|
584
|
+
},
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
for (const gap of asArray(chapter.information_gaps)) {
|
|
589
|
+
if (!gap || !gap.id) continue;
|
|
590
|
+
await sdb.query(
|
|
591
|
+
`RELATE ${chapRec}->info_action->${recordLiteral('information', gap.id)} SET
|
|
592
|
+
story = ${STORY}, chapter_no = $chapter_no, info_id = $id,
|
|
593
|
+
state = $state, detail = $detail, payload = $payload;`,
|
|
594
|
+
{
|
|
595
|
+
id: String(gap.id),
|
|
596
|
+
chapter_no: displayNo,
|
|
597
|
+
state: actionState(gap.state, 'progressed'),
|
|
598
|
+
detail: gap.detail || undefined,
|
|
599
|
+
payload: gap,
|
|
600
|
+
},
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
for (const action of asArray(chapter.ending_actions)) {
|
|
605
|
+
if (!action || !action.id) continue;
|
|
606
|
+
await sdb.query(
|
|
607
|
+
`RELATE ${chapRec}->ending_action->${recordLiteral('ending', action.id)} SET
|
|
608
|
+
story = ${STORY}, chapter_no = $chapter_no, ending_id = $id,
|
|
609
|
+
action = $action, detail = $detail, payload = $payload;`,
|
|
610
|
+
{
|
|
611
|
+
id: String(action.id),
|
|
612
|
+
chapter_no: displayNo,
|
|
613
|
+
action: action.action || '收束',
|
|
614
|
+
detail: action.detail || undefined,
|
|
615
|
+
payload: action,
|
|
616
|
+
},
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
await sdb.query(
|
|
621
|
+
`UPSERT chapter_metric SET
|
|
622
|
+
story = ${STORY}, chapter = ${chapThing}, chapter_no = $chapter_no,
|
|
623
|
+
emotion_value = $emotion_value, hook_strength = $hook_strength,
|
|
624
|
+
word_target = $word_target
|
|
625
|
+
WHERE story = ${STORY} AND chapter_no = $chapter_no;`,
|
|
626
|
+
{
|
|
627
|
+
chapter_no: displayNo,
|
|
628
|
+
emotion_value: Number(chapter.emotion && chapter.emotion.value) || undefined,
|
|
629
|
+
hook_strength: chapter.hook && chapter.hook.strength || undefined,
|
|
630
|
+
word_target: Number(chapter.word_target) || 0,
|
|
631
|
+
},
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
// Knowledge events are inserted in chapter/array order. The DB event
|
|
635
|
+
// replays them against knowledge_state and rejects contradictions.
|
|
636
|
+
for (const event of asArray(chapter.knowledge_events)) {
|
|
637
|
+
if (!event || !event.subject_id || !event.character) continue;
|
|
638
|
+
const characterId = String(event.character);
|
|
639
|
+
const sourceId = event.source_character ? String(event.source_character) : undefined;
|
|
640
|
+
await sdb.query(
|
|
641
|
+
`CREATE knowledge_event SET
|
|
642
|
+
story = ${STORY},
|
|
643
|
+
chapter = ${chapThing},
|
|
644
|
+
chapter_no = $chapter_no,
|
|
645
|
+
subject_type = $subject_type,
|
|
646
|
+
subject_id = $subject_id,
|
|
647
|
+
character = type::record('character', $character_id),
|
|
648
|
+
character_id = $character_id,
|
|
649
|
+
change = $change,
|
|
650
|
+
coverage = $coverage,
|
|
651
|
+
knowledge = $knowledge,
|
|
652
|
+
how = $how,
|
|
653
|
+
source_character = IF $source_id = NONE THEN NONE ELSE type::record('character', $source_id) END,
|
|
654
|
+
scene = $scene,
|
|
655
|
+
evidence = $evidence,
|
|
656
|
+
payload = $payload;`,
|
|
657
|
+
{
|
|
658
|
+
chapter_no: displayNo,
|
|
659
|
+
subject_type: event.subject_type || 'information',
|
|
660
|
+
subject_id: String(event.subject_id),
|
|
661
|
+
character_id: characterId,
|
|
662
|
+
change: event.change || 'learns',
|
|
663
|
+
coverage: event.coverage || 'partial',
|
|
664
|
+
knowledge: event.knowledge || '',
|
|
665
|
+
how: event.how || 'other',
|
|
666
|
+
source_id: sourceId || undefined,
|
|
667
|
+
scene: event.scene || '',
|
|
668
|
+
evidence: event.evidence || '',
|
|
669
|
+
payload: event,
|
|
670
|
+
},
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
await upsertStageDoc(sdb, '40-rhythm', 'chapter_map', doc);
|
|
676
|
+
return { stage: '40-rhythm', ok: true, chapters: chapters.length };
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// ---------------------------------------------------------------------------
|
|
680
|
+
// outlines
|
|
681
|
+
// ---------------------------------------------------------------------------
|
|
682
|
+
|
|
683
|
+
export async function importOutlines(sdb, projectRoot) {
|
|
684
|
+
const dir = stageDir(projectRoot, '40-rhythm', 'outlines');
|
|
685
|
+
if (!fs.existsSync(dir)) throw new StoryDbError('STORYDB_IMPORT', '缺少 40-rhythm/outlines/');
|
|
686
|
+
const names = fs.readdirSync(dir).filter((name) => name.endsWith('.json'));
|
|
687
|
+
if (!names.length) throw new StoryDbError('STORYDB_IMPORT', '40-rhythm/outlines/ 为空');
|
|
688
|
+
|
|
689
|
+
for (const name of names) {
|
|
690
|
+
const outline = readJson(path.join(dir, name), null);
|
|
691
|
+
if (!outline) continue;
|
|
692
|
+
const chapterId = outline.chapter_id || path.basename(name, '.json');
|
|
693
|
+
const chapter = await sdb.one(
|
|
694
|
+
`SELECT * FROM chapter WHERE story = ${STORY} AND chapter_id = $chapter_id LIMIT 1;`,
|
|
695
|
+
{ chapter_id: chapterId },
|
|
696
|
+
);
|
|
697
|
+
if (!chapter) {
|
|
698
|
+
throw new StoryDbError('STORYDB_IMPORT', '细纲引用了不存在的章节:' + chapterId);
|
|
699
|
+
}
|
|
700
|
+
await sdb.query(
|
|
701
|
+
`UPSERT outline SET
|
|
702
|
+
story = ${STORY}, chapter = type::record('chapter', $chapter_id),
|
|
703
|
+
chapter_id = $chapter_id, payload = $payload
|
|
704
|
+
WHERE story = ${STORY} AND chapter_id = $chapter_id;`,
|
|
705
|
+
{ chapter_id: chapterId, payload: withoutMeta(outline) },
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
return { stage: 'outline', ok: true, outlines: names.length };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ---------------------------------------------------------------------------
|
|
712
|
+
// writing
|
|
713
|
+
// ---------------------------------------------------------------------------
|
|
714
|
+
|
|
715
|
+
function chapterFileName(displayNo) {
|
|
716
|
+
return '第' + String(displayNo).padStart(3, '0') + '章.md';
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export async function importWriting(sdb, projectRoot) {
|
|
720
|
+
const dir = stageDir(projectRoot, '50-writing');
|
|
721
|
+
const ledger = readJson(path.join(dir, 'ledger.json'), { chapters: [] });
|
|
722
|
+
const rows = asArray(ledger.chapters);
|
|
723
|
+
for (const row of rows) {
|
|
724
|
+
const chapterId = row.chapter_id || ('CH' + String(row.ch).padStart(3, '0'));
|
|
725
|
+
const chapter = await sdb.one(
|
|
726
|
+
`SELECT * FROM chapter WHERE story = ${STORY} AND chapter_id = $chapter_id LIMIT 1;`,
|
|
727
|
+
{ chapter_id: chapterId },
|
|
728
|
+
);
|
|
729
|
+
if (!chapter) continue;
|
|
730
|
+
await sdb.query(
|
|
731
|
+
`UPSERT ledger_entry SET
|
|
732
|
+
story = ${STORY}, chapter = type::record('chapter', $chapter_id),
|
|
733
|
+
chapter_id = $chapter_id, payload = $payload
|
|
734
|
+
WHERE story = ${STORY} AND chapter_id = $chapter_id;`,
|
|
735
|
+
{ chapter_id: chapterId, payload: withoutMeta(row) },
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const chaptersDir = path.join(dir, 'chapters');
|
|
740
|
+
const names = fs.existsSync(chaptersDir)
|
|
741
|
+
? fs.readdirSync(chaptersDir).filter((name) => name.endsWith('.md'))
|
|
742
|
+
: [];
|
|
743
|
+
for (const name of names) {
|
|
744
|
+
const match = /第(\d+)章\.md$/.exec(name);
|
|
745
|
+
if (!match) continue;
|
|
746
|
+
const displayNo = Number(match[1]);
|
|
747
|
+
const chapter = await sdb.one(
|
|
748
|
+
`SELECT * FROM chapter WHERE story = ${STORY} AND display_no = $display_no LIMIT 1;`,
|
|
749
|
+
{ display_no: displayNo },
|
|
750
|
+
);
|
|
751
|
+
if (!chapter) continue;
|
|
752
|
+
const content = fs.readFileSync(path.join(chaptersDir, name), 'utf8');
|
|
753
|
+
const wordCount = content.replace(/\s/g, '').length;
|
|
754
|
+
await sdb.query(
|
|
755
|
+
`UPSERT chapter_text SET
|
|
756
|
+
story = ${STORY}, chapter = type::record('chapter', $chapter_id),
|
|
757
|
+
chapter_id = $chapter_id, display_no = $display_no,
|
|
758
|
+
content = $content, word_count = $word_count
|
|
759
|
+
WHERE story = ${STORY} AND chapter_id = $chapter_id;`,
|
|
760
|
+
{
|
|
761
|
+
chapter_id: chapter.chapter_id,
|
|
762
|
+
display_no: displayNo,
|
|
763
|
+
content,
|
|
764
|
+
word_count: wordCount,
|
|
765
|
+
},
|
|
766
|
+
);
|
|
767
|
+
await sdb.query(
|
|
768
|
+
`UPDATE chapter_metric SET word_count = $word_count WHERE story = ${STORY} AND chapter_no = $display_no;`,
|
|
769
|
+
{ word_count: wordCount, display_no: displayNo },
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return { stage: '50-writing', ok: true, chapters: names.length };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// ---------------------------------------------------------------------------
|
|
777
|
+
// dispatch
|
|
778
|
+
// ---------------------------------------------------------------------------
|
|
779
|
+
|
|
780
|
+
export async function importStage(sdb, stageId, projectRoot, opts = {}) {
|
|
781
|
+
try {
|
|
782
|
+
switch (stageId) {
|
|
783
|
+
case '10-scale': return await importScale(sdb, projectRoot);
|
|
784
|
+
case '20-macro': return await importMacro(sdb, projectRoot);
|
|
785
|
+
case '30-cast': return await importCast(sdb, projectRoot);
|
|
786
|
+
case '40-rhythm': return await importRhythm(sdb, projectRoot);
|
|
787
|
+
case 'outline': return await importOutlines(sdb, projectRoot);
|
|
788
|
+
case '50-writing': return await importWriting(sdb, projectRoot);
|
|
789
|
+
default: return { stage: stageId, ok: true, skipped: true };
|
|
790
|
+
}
|
|
791
|
+
} catch (err) {
|
|
792
|
+
if (err instanceof StoryDbError) throw err;
|
|
793
|
+
throw new StoryDbError('STORYDB_IMPORT', '写入故事数据库失败:' + dbErrorMessage(err), {
|
|
794
|
+
stage: stageId,
|
|
795
|
+
cause: dbErrorMessage(err),
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
}
|