@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
|
@@ -23,6 +23,7 @@ import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
|
23
23
|
import { join } from 'path';
|
|
24
24
|
|
|
25
25
|
import { countEffectiveChars } from './scripts/content-quality.js';
|
|
26
|
+
import { absolutizeInlinePaths } from './templates.js';
|
|
26
27
|
|
|
27
28
|
// 每批章数
|
|
28
29
|
export const WRITING_BATCH_SIZE = 10;
|
|
@@ -61,6 +62,9 @@ export function chapterFileName(ch) {
|
|
|
61
62
|
export function writingDirs(projectRoot) {
|
|
62
63
|
const src = join(projectRoot, '设定', 'raw', 'sources');
|
|
63
64
|
return {
|
|
65
|
+
// 项目根:提示词里的参考文件路径要靠它绝对化
|
|
66
|
+
// (参考文件在**项目根**的 references/ 下,相对路径会被拼到 设定/ 底下读不到)
|
|
67
|
+
root: projectRoot,
|
|
64
68
|
sources: src,
|
|
65
69
|
skeleton: join(src, '骨架'),
|
|
66
70
|
flesh: join(src, '血肉'),
|
|
@@ -192,6 +196,43 @@ export function readWritingState(dirs, T) {
|
|
|
192
196
|
return { existing, ledgers, prevTail, prevChapter, invalid };
|
|
193
197
|
}
|
|
194
198
|
|
|
199
|
+
/** 读取每章细纲(节奏/细纲/第NNN章.json)。 */
|
|
200
|
+
function readChapterBriefs(dirs) {
|
|
201
|
+
const out = new Map();
|
|
202
|
+
const dir = join(dirs.rhythm, '细纲');
|
|
203
|
+
if (!existsSync(dir)) return out;
|
|
204
|
+
let files = [];
|
|
205
|
+
try { files = readdirSync(dir); } catch { return out; }
|
|
206
|
+
for (const name of files) {
|
|
207
|
+
const match = /^第(\d+)章\.json$/.exec(name);
|
|
208
|
+
if (!match) continue;
|
|
209
|
+
const data = readJsonSafe(join(dir, name));
|
|
210
|
+
if (!data || typeof data.ch !== 'number') continue;
|
|
211
|
+
out.set(data.ch, data);
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 读取角色完整档案(写作时只取本章 cast 的小档案)。 */
|
|
217
|
+
function readCharacterProfiles(dirs) {
|
|
218
|
+
const data = readJsonSafe(join(dirs.flesh, '角色.json')) || {};
|
|
219
|
+
const map = new Map();
|
|
220
|
+
for (const c of (data.characters || [])) {
|
|
221
|
+
if (c && c.id) map.set(c.id, c);
|
|
222
|
+
}
|
|
223
|
+
return map;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** 读取题材扩展实体档案(其他实体.json,可不存在)。 */
|
|
227
|
+
function readOtherEntityProfiles(dirs) {
|
|
228
|
+
const data = readJsonSafe(join(dirs.flesh, '其他实体.json')) || {};
|
|
229
|
+
const map = new Map();
|
|
230
|
+
for (const o of (data.other_entities || [])) {
|
|
231
|
+
if (o && o.id) map.set(o.id, o);
|
|
232
|
+
}
|
|
233
|
+
return map;
|
|
234
|
+
}
|
|
235
|
+
|
|
195
236
|
export function loadWritingContext(projectRoot) {
|
|
196
237
|
const dirs = writingDirs(projectRoot);
|
|
197
238
|
const framework = readJsonSafe(join(dirs.skeleton, '框架.json'));
|
|
@@ -216,10 +257,18 @@ export function loadWritingContext(projectRoot) {
|
|
|
216
257
|
const chapterContext = buildChapterContext(planRaw, timelineRaw, entity, fleshChars, roster);
|
|
217
258
|
|
|
218
259
|
const state = readWritingState(dirs, T);
|
|
260
|
+
const briefs = readChapterBriefs(dirs);
|
|
261
|
+
const characterProfiles = readCharacterProfiles(dirs);
|
|
262
|
+
const otherEntityProfiles = readOtherEntityProfiles(dirs);
|
|
263
|
+
const skeletonRelations = readJsonSafe(join(dirs.skeleton, '关系.json')) || {};
|
|
219
264
|
|
|
220
265
|
return {
|
|
221
266
|
T,
|
|
222
267
|
outlines,
|
|
268
|
+
briefs,
|
|
269
|
+
characterProfiles,
|
|
270
|
+
otherEntityProfiles,
|
|
271
|
+
otherRelations: Array.isArray(skeletonRelations.other_relations) ? skeletonRelations.other_relations : [],
|
|
223
272
|
chapterContext,
|
|
224
273
|
existing: state.existing,
|
|
225
274
|
prevTail: state.prevTail,
|
|
@@ -267,6 +316,10 @@ export function refreshWritingContext(context) {
|
|
|
267
316
|
* 本章该出场谁、该推进哪条线。
|
|
268
317
|
*/
|
|
269
318
|
export function buildChapterContext(planRaw, timelineRaw, entity, fleshChars, roster) {
|
|
319
|
+
const plannedChapters = ((planRaw && planRaw.chapters) || [])
|
|
320
|
+
.map((c) => (c && typeof c.ch === 'number' ? c.ch : 0))
|
|
321
|
+
.filter((n) => n > 0);
|
|
322
|
+
const maxChapter = plannedChapters.length ? Math.max(...plannedChapters) : 0;
|
|
270
323
|
const nameById = new Map();
|
|
271
324
|
for (const c of (roster && roster.characters) || []) nameById.set(c.id, c.name);
|
|
272
325
|
for (const c of (roster && roster.fleshCharacters) || []) nameById.set(c.id, c.name);
|
|
@@ -296,7 +349,7 @@ export function buildChapterContext(planRaw, timelineRaw, entity, fleshChars, ro
|
|
|
296
349
|
volume: null, act: null, sceneFocus: '',
|
|
297
350
|
characters: [], locations: [], items: [],
|
|
298
351
|
conflicts: [], foreshadows: [], climax: null, pleasure: null,
|
|
299
|
-
hook: null, ending: null
|
|
352
|
+
hook: null, ending: null, relationships: []
|
|
300
353
|
});
|
|
301
354
|
}
|
|
302
355
|
return ctx.get(ch);
|
|
@@ -378,6 +431,39 @@ export function buildChapterContext(planRaw, timelineRaw, entity, fleshChars, ro
|
|
|
378
431
|
get(it.ch).hook = { head: it.hook_head || '', tail: it.hook_tail || '', strength: it.hook_strength || '' };
|
|
379
432
|
}
|
|
380
433
|
|
|
434
|
+
// 关系阶段:把 relationship_timeline 的每个阶段铺到它生效的章节区间。
|
|
435
|
+
// 没有 end_chapter 时,默认持续到同一对关系的下一个阶段开始前(或全书结束)。
|
|
436
|
+
const relRows = (t.relationship_timeline || []).filter((r) =>
|
|
437
|
+
r && r.from && r.to && typeof r.start_chapter === 'number');
|
|
438
|
+
const rowsByPair = new Map();
|
|
439
|
+
for (const r of relRows) {
|
|
440
|
+
const key = [r.from, r.to].sort().join('↔');
|
|
441
|
+
if (!rowsByPair.has(key)) rowsByPair.set(key, []);
|
|
442
|
+
rowsByPair.get(key).push(r);
|
|
443
|
+
}
|
|
444
|
+
for (const rows of rowsByPair.values()) {
|
|
445
|
+
rows.sort((a, b) => a.start_chapter - b.start_chapter);
|
|
446
|
+
for (let i = 0; i < rows.length; i++) {
|
|
447
|
+
const r = rows[i];
|
|
448
|
+
const explicitEnd = typeof r.end_chapter === 'number' ? r.end_chapter : null;
|
|
449
|
+
const nextStart = i + 1 < rows.length ? rows[i + 1].start_chapter : null;
|
|
450
|
+
const end = explicitEnd !== null ? explicitEnd : (nextStart !== null ? nextStart - 1 : maxChapter);
|
|
451
|
+
for (let ch = r.start_chapter; ch <= Math.min(end, maxChapter); ch++) {
|
|
452
|
+
get(ch).relationships.push({
|
|
453
|
+
from: r.from,
|
|
454
|
+
to: r.to,
|
|
455
|
+
from_name: nameById.get(r.from) || r.from,
|
|
456
|
+
to_name: nameById.get(r.to) || r.to,
|
|
457
|
+
stage: r.stage || '',
|
|
458
|
+
state: r.state || '',
|
|
459
|
+
trigger_event: r.trigger_event || '',
|
|
460
|
+
start_chapter: r.start_chapter,
|
|
461
|
+
changed_this_chapter: ch === r.start_chapter,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
381
467
|
return ctx;
|
|
382
468
|
}
|
|
383
469
|
|
|
@@ -403,6 +489,7 @@ export function buildEntityRoster(dirs) {
|
|
|
403
489
|
locations: collectNamed(entity.locations),
|
|
404
490
|
items: collectNamed(entity.items),
|
|
405
491
|
endings: collectNamed(entity.endings),
|
|
492
|
+
otherEntities: collectNamed(entity.other_entities),
|
|
406
493
|
fleshCharacters: collectNamed(flesh.characters)
|
|
407
494
|
};
|
|
408
495
|
}
|
|
@@ -473,6 +560,213 @@ function formatOutline(o) {
|
|
|
473
560
|
return lines.length ? lines.join('\n') : '(本章无细纲)';
|
|
474
561
|
}
|
|
475
562
|
|
|
563
|
+
/**
|
|
564
|
+
* 把每章细纲渲染成写作提示词里的“硬约束”。
|
|
565
|
+
*
|
|
566
|
+
* 这里不是复述 JSON,而是把 LLM 写作时必须遵守的东西按“写什么/怎么写/必须/禁止”重排:
|
|
567
|
+
* - 本章时间线:这一章在故事时间上发生什么;
|
|
568
|
+
* - 出场角色/地点/道具/冲突/伏笔/高潮/爽点/结局:本章必须组织的要素;
|
|
569
|
+
* - 信息差:谁还不知道什么,因此本章绝对不能提;
|
|
570
|
+
* - 开场/推进/转折/收束:这一章的结构与技法;
|
|
571
|
+
* - must_write / must_not_write:硬性清单。
|
|
572
|
+
*/
|
|
573
|
+
function formatBrief(brief) {
|
|
574
|
+
if (!brief || typeof brief !== 'object') return '';
|
|
575
|
+
const lines = [];
|
|
576
|
+
lines.push('## 本章细纲:第 ' + brief.ch + ' 章(硬约束,必须执行)');
|
|
577
|
+
lines.push('');
|
|
578
|
+
lines.push('- 卷/幕:' + (brief.volume || '未指定') + ' / ' + (brief.act || '未指定'));
|
|
579
|
+
lines.push('- 视角:' + (brief.pov_name || brief.pov || '未指定') + (brief.pov ? '(' + brief.pov + ')' : ''));
|
|
580
|
+
if (brief.scene_focus) lines.push('- 场景焦点:' + brief.scene_focus);
|
|
581
|
+
if (brief.function_main) lines.push('- 主功能:' + brief.function_main + (brief.function_aux ? ';辅助:' + brief.function_aux : ''));
|
|
582
|
+
if (brief.emotion && (brief.emotion.value !== null || brief.emotion.phase)) {
|
|
583
|
+
lines.push('- 情绪:' + (brief.emotion.phase || '') + (brief.emotion.value !== null ? '(强度 ' + brief.emotion.value + ')' : ''));
|
|
584
|
+
}
|
|
585
|
+
if (brief.word_target) lines.push('- 字数目标:约 ' + brief.word_target + ' 字');
|
|
586
|
+
if (brief.hooks) {
|
|
587
|
+
lines.push('- 钩子:章首 ' + (brief.hooks.head || '未指定') + ' / 章尾 ' + (brief.hooks.tail || '未指定') + (brief.hooks.strength ? '(强度 ' + brief.hooks.strength + ')' : ''));
|
|
588
|
+
}
|
|
589
|
+
lines.push('');
|
|
590
|
+
|
|
591
|
+
const tl = Array.isArray(brief.timeline_events) ? brief.timeline_events : [];
|
|
592
|
+
if (tl.length) {
|
|
593
|
+
lines.push('### 本章时间线(必须覆盖)');
|
|
594
|
+
for (const e of tl) lines.push('- ' + (e.type || '事件') + ':' + (e.description || e.ref || ''));
|
|
595
|
+
lines.push('');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const relationshipChanges = Array.isArray(brief.relationship_changes) ? brief.relationship_changes : [];
|
|
599
|
+
if (relationshipChanges.length) {
|
|
600
|
+
lines.push('### 本章关系阶段变化(必须发生,不能写成没发生过)');
|
|
601
|
+
for (const r of relationshipChanges) {
|
|
602
|
+
lines.push('- ' + (r.from_name || r.from) + ' → ' + (r.to_name || r.to) + ':' +
|
|
603
|
+
(r.stage ? '【' + r.stage + '】' : '') + (r.state || '') +
|
|
604
|
+
(r.trigger_event ? ';触发:' + r.trigger_event : '') +
|
|
605
|
+
(r.description ? ';' + r.description : ''));
|
|
606
|
+
}
|
|
607
|
+
lines.push('');
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const cast = Array.isArray(brief.cast) ? brief.cast : [];
|
|
611
|
+
if (cast.length) {
|
|
612
|
+
lines.push('### 本章必须出场');
|
|
613
|
+
lines.push('- ' + cast.map((c) => c.id + '(' + c.name + (c.reason ? ',' + c.reason : '') + ')').join('、'));
|
|
614
|
+
lines.push('');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (brief.location) lines.push('### 本章地点\n- ' + brief.location.id + '(' + brief.location.name + ')\n');
|
|
618
|
+
if (Array.isArray(brief.items) && brief.items.length) {
|
|
619
|
+
lines.push('### 本章道具\n- ' + brief.items.map((x) => x.id + '(' + x.name + ')').join('、') + '\n');
|
|
620
|
+
}
|
|
621
|
+
if (Array.isArray(brief.conflicts) && brief.conflicts.length) {
|
|
622
|
+
lines.push('### 本章冲突');
|
|
623
|
+
for (const c of brief.conflicts) lines.push('- ' + c.id + '(' + c.name + '):' + (c.node_type || '推进'));
|
|
624
|
+
lines.push('');
|
|
625
|
+
}
|
|
626
|
+
if (Array.isArray(brief.foreshadow_actions) && brief.foreshadow_actions.length) {
|
|
627
|
+
lines.push('### 本章伏笔动作');
|
|
628
|
+
for (const f of brief.foreshadow_actions) lines.push('- ' + f.action + ' ' + f.id + '(' + f.name + ')');
|
|
629
|
+
lines.push('');
|
|
630
|
+
}
|
|
631
|
+
const big = [];
|
|
632
|
+
if (brief.climax) big.push('高潮 ' + brief.climax.id + '(' + brief.climax.name + ')');
|
|
633
|
+
if (brief.pleasure) big.push('爽点 ' + brief.pleasure.id + '(' + brief.pleasure.name + ')');
|
|
634
|
+
if (brief.ending) big.push('结局节点 ' + brief.ending.id + '(' + brief.ending.name + ')');
|
|
635
|
+
if (big.length) lines.push('### 本章必须兑现\n- ' + big.join('\n- ') + '\n');
|
|
636
|
+
|
|
637
|
+
const gaps = Array.isArray(brief.information_gaps) ? brief.information_gaps : [];
|
|
638
|
+
if (gaps.length) {
|
|
639
|
+
lines.push('### 本章信息差(必须处理)');
|
|
640
|
+
for (const g of gaps) {
|
|
641
|
+
lines.push('- [' + g.status + '] ' + g.info_name + '(' + g.id + ')');
|
|
642
|
+
if (g.who_knows && g.who_knows.length) lines.push(' - 已知:' + g.who_knows.join('、'));
|
|
643
|
+
if (g.who_does_not_know && g.who_does_not_know.length) lines.push(' - 尚不知道:' + g.who_does_not_know.join('、'));
|
|
644
|
+
for (const line of (g.forbidden_mentions || [])) lines.push(' - ⛔ ' + line);
|
|
645
|
+
}
|
|
646
|
+
lines.push('');
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const boundaries = Array.isArray(brief.information_boundaries) ? brief.information_boundaries : [];
|
|
650
|
+
if (boundaries.length) {
|
|
651
|
+
lines.push('### 信息边界(角色不知道/误以为的事,不能写漏)');
|
|
652
|
+
for (const b of boundaries) {
|
|
653
|
+
if (b.unknown_secrets) lines.push('- ⛔ ' + b.name + '(' + b.id + ')不知道:' + String(b.unknown_secrets).replace(/[。;;]+$/, '') + '。本章不得让其提及、知情或据此行动。');
|
|
654
|
+
if (b.false_beliefs) lines.push('- ⛔ ' + b.name + '(' + b.id + ')误以为:' + String(b.false_beliefs).replace(/[。;;]+$/, '') + '。除非本章信息差动作明确揭示,否则不得让旁白或他人直接纠正。');
|
|
655
|
+
}
|
|
656
|
+
lines.push('');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const how = brief.how_to_write || {};
|
|
660
|
+
lines.push('### 怎么写(按这个结构组织本章)');
|
|
661
|
+
if (how.pov_rule) lines.push('- 视角规则:' + how.pov_rule);
|
|
662
|
+
if (how.opening && how.opening.technique) {
|
|
663
|
+
lines.push('- 开场技法' + (how.opening.hook_type ? '(' + how.opening.hook_type + ')' : '') + ':' + how.opening.technique);
|
|
664
|
+
}
|
|
665
|
+
if (how.ending && how.ending.technique) {
|
|
666
|
+
lines.push('- 收束技法' + (how.ending.hook_type ? '(' + how.ending.hook_type + ')' : '') + ':' + how.ending.technique);
|
|
667
|
+
}
|
|
668
|
+
const beats = Array.isArray(how.scene_beats) ? how.scene_beats : [];
|
|
669
|
+
if (beats.length) {
|
|
670
|
+
for (const b of beats) {
|
|
671
|
+
lines.push('- ' + (b.name || '节拍') + ':' + (b.goal || ''));
|
|
672
|
+
for (const x of (b.must || [])) lines.push(' - 必须:' + x);
|
|
673
|
+
for (const x of (b.avoid || [])) lines.push(' - 避免:' + x);
|
|
674
|
+
}
|
|
675
|
+
} else {
|
|
676
|
+
if (how.opening) lines.push('- 开场:' + how.opening.technique);
|
|
677
|
+
if (how.development) lines.push('- 推进:' + how.development.goal);
|
|
678
|
+
if (how.turn) lines.push('- 转折:' + how.turn.goal);
|
|
679
|
+
if (how.ending) lines.push('- 收束:' + how.ending.technique);
|
|
680
|
+
}
|
|
681
|
+
lines.push('');
|
|
682
|
+
|
|
683
|
+
const must = Array.isArray(brief.must_write) ? brief.must_write : (Array.isArray(brief.mandatory) ? brief.mandatory : []);
|
|
684
|
+
if (must.length) {
|
|
685
|
+
lines.push('### 必须写');
|
|
686
|
+
for (const x of must) lines.push('- ' + x);
|
|
687
|
+
lines.push('');
|
|
688
|
+
}
|
|
689
|
+
const forbidden = Array.isArray(brief.must_not_write) ? brief.must_not_write : (Array.isArray(brief.forbidden) ? brief.forbidden : []);
|
|
690
|
+
if (forbidden.length) {
|
|
691
|
+
lines.push('### 绝对不允许写(违反视为本章失败)');
|
|
692
|
+
for (const x of forbidden) lines.push('- ⛔ ' + x);
|
|
693
|
+
lines.push('');
|
|
694
|
+
}
|
|
695
|
+
return lines.join('\n');
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** 本章 cast 的完整小档案(语言/缺陷/目标/关系/信息状态)。 */
|
|
699
|
+
function formatCharacterDossiers(context, briefs) {
|
|
700
|
+
const profiles = context.characterProfiles || new Map();
|
|
701
|
+
const ids = [];
|
|
702
|
+
for (const brief of briefs || []) {
|
|
703
|
+
for (const c of (brief.cast || [])) if (c && c.id && !ids.includes(c.id)) ids.push(c.id);
|
|
704
|
+
}
|
|
705
|
+
if (!ids.length || !profiles.size) return '';
|
|
706
|
+
const lines = ['## 本章角色档案(写对话/动作前先看)'];
|
|
707
|
+
let count = 0;
|
|
708
|
+
for (const id of ids) {
|
|
709
|
+
if (count >= 12) break;
|
|
710
|
+
const c = profiles.get(id);
|
|
711
|
+
if (!c) continue;
|
|
712
|
+
count += 1;
|
|
713
|
+
const lp = c.language_profile || {};
|
|
714
|
+
lines.push('');
|
|
715
|
+
lines.push('### ' + id + ' ' + (c.name || ''));
|
|
716
|
+
if (lp.catchphrase) lines.push('- 口头禅:' + lp.catchphrase);
|
|
717
|
+
if (lp.speech_rhythm) lines.push('- 说话节奏:' + lp.speech_rhythm);
|
|
718
|
+
if (lp.info_preference) lines.push('- 信息偏好:' + lp.info_preference);
|
|
719
|
+
if (lp.stance) lines.push('- 立场姿态:' + lp.stance);
|
|
720
|
+
if (lp.personality_tone) lines.push('- 语气:' + lp.personality_tone);
|
|
721
|
+
if (c.core_flaw) lines.push('- 核心缺陷:' + c.core_flaw);
|
|
722
|
+
if (c.goals) lines.push('- 目标:短期=' + (c.goals.short_term || '') + ';中期=' + (c.goals.medium_term || '') + ';长期=' + (c.goals.long_term || ''));
|
|
723
|
+
if (Array.isArray(c.traits) && c.traits.length) {
|
|
724
|
+
lines.push('- 独有特征:' + c.traits.join(';'));
|
|
725
|
+
}
|
|
726
|
+
if (Array.isArray(c.behavior_repeat_points) && c.behavior_repeat_points.length) {
|
|
727
|
+
lines.push('- 习惯动作:' + c.behavior_repeat_points.join(';'));
|
|
728
|
+
}
|
|
729
|
+
const targetNames = new Set((briefs || []).flatMap((b) => (b.cast || []).map((x) => x.name)));
|
|
730
|
+
const rels = (c.relationships || []).filter((r) => targetNames.has(r.target_name));
|
|
731
|
+
if (rels.length) {
|
|
732
|
+
lines.push('- 与本章角色关系:' + rels.map((r) => r.target_name + '(' + r.relationship_type + ')' + (r.description ? ':' + r.description : '')).join(';'));
|
|
733
|
+
}
|
|
734
|
+
if (c.information_state) {
|
|
735
|
+
const info = c.information_state;
|
|
736
|
+
if (info.known_secrets) lines.push('- 已知:' + info.known_secrets);
|
|
737
|
+
if (info.unknown_secrets) lines.push('- 不知道:' + info.unknown_secrets);
|
|
738
|
+
if (info.false_beliefs) lines.push('- 错误认知:' + info.false_beliefs);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return lines.length > 1 ? lines.join('\n') : '';
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function formatOtherEntityDossiers(context, briefs) {
|
|
745
|
+
const profiles = context.otherEntityProfiles || new Map();
|
|
746
|
+
const relations = context.otherRelations || [];
|
|
747
|
+
if (!profiles.size || !relations.length) return '';
|
|
748
|
+
const castIds = new Set();
|
|
749
|
+
for (const brief of briefs || []) for (const c of (brief.cast || [])) if (c && c.id) castIds.add(c.id);
|
|
750
|
+
if (!castIds.size) return '';
|
|
751
|
+
const lines = ['## 本章特殊存在(非人物实体,不能写成普通角色)'];
|
|
752
|
+
let count = 0;
|
|
753
|
+
for (const [id, o] of profiles) {
|
|
754
|
+
const rel = relations.find((r) => r && (r.from === id || r.to === id) &&
|
|
755
|
+
(castIds.has(r.from) || castIds.has(r.to)));
|
|
756
|
+
if (!rel) continue;
|
|
757
|
+
count += 1;
|
|
758
|
+
if (count > 6) break;
|
|
759
|
+
lines.push('');
|
|
760
|
+
lines.push('### ' + id + ' ' + (o.name || '') + '(' + (o.kind || '其他实体') + ')');
|
|
761
|
+
lines.push('- 与本章角色关系:' + (rel.relation_label || '相关') + '(' + rel.from + ' → ' + rel.to + ')');
|
|
762
|
+
if (Array.isArray(o.traits) && o.traits.length) lines.push('- 独有特征:' + o.traits.join(';'));
|
|
763
|
+
if (Array.isArray(o.abilities) && o.abilities.length) lines.push('- 能力:' + o.abilities.join(';'));
|
|
764
|
+
if (Array.isArray(o.limits) && o.limits.length) lines.push('- 限制:' + o.limits.join(';'));
|
|
765
|
+
if (o.description) lines.push('- 描述:' + o.description);
|
|
766
|
+
}
|
|
767
|
+
return count ? lines.join('\n') : '';
|
|
768
|
+
}
|
|
769
|
+
|
|
476
770
|
function formatRoster(roster) {
|
|
477
771
|
const seg = (label, arr) => {
|
|
478
772
|
if (!arr || arr.length === 0) return '';
|
|
@@ -483,7 +777,8 @@ function formatRoster(roster) {
|
|
|
483
777
|
seg('势力', roster.factions),
|
|
484
778
|
seg('地点', roster.locations),
|
|
485
779
|
seg('道具', roster.items),
|
|
486
|
-
seg('冲突', roster.conflicts)
|
|
780
|
+
seg('冲突', roster.conflicts),
|
|
781
|
+
seg('其他实体', roster.otherEntities)
|
|
487
782
|
].filter(Boolean);
|
|
488
783
|
return parts.join('\n');
|
|
489
784
|
}
|
|
@@ -551,6 +846,18 @@ function formatChapterContext(context, ch) {
|
|
|
551
846
|
if (c.climax) lines.push('- 本章高潮:' + c.climax.id + '(' + c.climax.name + ')');
|
|
552
847
|
if (c.pleasure) lines.push('- 本章爽点:' + c.pleasure.id + '(' + c.pleasure.name + ')');
|
|
553
848
|
if (c.ending) lines.push('- 本章结局:' + c.ending.id + '(' + c.ending.name + ')');
|
|
849
|
+
{
|
|
850
|
+
const present = new Set((c.characters || []).map((x) => x.id));
|
|
851
|
+
const rels = (c.relationships || []).filter((r) => present.has(r.from) && present.has(r.to));
|
|
852
|
+
if (rels.length) {
|
|
853
|
+
for (const r of rels) {
|
|
854
|
+
lines.push('- 关系阶段:' + r.from_name + ' → ' + r.to_name + ' ' +
|
|
855
|
+
(r.stage ? '【' + r.stage + '】' : '') + (r.state || '') +
|
|
856
|
+
(r.trigger_event ? '(触发:' + r.trigger_event + ')' : '') +
|
|
857
|
+
(r.changed_this_chapter ? '(本章发生变化)' : ''));
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
554
861
|
if (c.hook) {
|
|
555
862
|
lines.push('- 钩子:章首 ' + (c.hook.head || '未指定') + ' / 章尾 ' + (c.hook.tail || '未指定') +
|
|
556
863
|
(c.hook.strength ? '(强度 ' + c.hook.strength + ')' : ''));
|
|
@@ -571,6 +878,18 @@ export function buildBatchPrompt(context, batch, attempt, failures, contract) {
|
|
|
571
878
|
const isRetry = attempt > 1 && failures && failures.length > 0;
|
|
572
879
|
const targetChapters = isRetry ? failures.map((f) => f.ch) : batch.chapters;
|
|
573
880
|
const parts = [];
|
|
881
|
+
const briefsInBatch = [];
|
|
882
|
+
const pushChapter = (ch) => {
|
|
883
|
+
const brief = context.briefs && context.briefs.get(ch);
|
|
884
|
+
if (brief) {
|
|
885
|
+
briefsInBatch.push(brief);
|
|
886
|
+
parts.push(formatBrief(brief));
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
const cc = formatChapterContext(context, ch);
|
|
890
|
+
if (cc) parts.push(cc);
|
|
891
|
+
parts.push(formatOutline(context.outlines.get(ch)));
|
|
892
|
+
};
|
|
574
893
|
|
|
575
894
|
if (contract) {
|
|
576
895
|
parts.push(contract.trim());
|
|
@@ -595,10 +914,8 @@ export function buildBatchPrompt(context, batch, attempt, failures, contract) {
|
|
|
595
914
|
parts.push('- [' + e.code + '] ' + (e.path || '(无路径)') + ':' + e.message + (e.hint ? '(' + e.hint + ')' : ''));
|
|
596
915
|
}
|
|
597
916
|
parts.push('');
|
|
598
|
-
const cc = formatChapterContext(context, f.ch);
|
|
599
|
-
if (cc) parts.push(cc);
|
|
600
917
|
parts.push('该章细纲:');
|
|
601
|
-
|
|
918
|
+
pushChapter(f.ch);
|
|
602
919
|
parts.push('');
|
|
603
920
|
}
|
|
604
921
|
parts.push('**只重写上面列出的章**,其它已通过的章不要动,也不要重复写。');
|
|
@@ -613,13 +930,22 @@ export function buildBatchPrompt(context, batch, attempt, failures, contract) {
|
|
|
613
930
|
for (const ch of batch.chapters) {
|
|
614
931
|
parts.push('### 第 ' + ch + ' 章');
|
|
615
932
|
parts.push('');
|
|
616
|
-
|
|
617
|
-
if (cc) parts.push(cc);
|
|
618
|
-
parts.push(formatOutline(context.outlines.get(ch)));
|
|
933
|
+
pushChapter(ch);
|
|
619
934
|
parts.push('');
|
|
620
935
|
}
|
|
621
936
|
}
|
|
622
937
|
|
|
938
|
+
const dossiers = formatCharacterDossiers(context, briefsInBatch);
|
|
939
|
+
if (dossiers) {
|
|
940
|
+
parts.push(dossiers);
|
|
941
|
+
parts.push('');
|
|
942
|
+
}
|
|
943
|
+
const otherDossiers = formatOtherEntityDossiers(context, briefsInBatch);
|
|
944
|
+
if (otherDossiers) {
|
|
945
|
+
parts.push(otherDossiers);
|
|
946
|
+
parts.push('');
|
|
947
|
+
}
|
|
948
|
+
|
|
623
949
|
parts.push('## 落盘路径');
|
|
624
950
|
parts.push('');
|
|
625
951
|
for (const ch of targetChapters) {
|
|
@@ -667,7 +993,8 @@ export function buildBatchPrompt(context, batch, attempt, failures, contract) {
|
|
|
667
993
|
parts.push('5. 全部写完后,用一段话汇报:本轮写了哪几章、每章标题与字数、埋/收了哪些伏笔、哪些章没有细纲。');
|
|
668
994
|
parts.push('');
|
|
669
995
|
|
|
670
|
-
|
|
996
|
+
// 参考文件路径绝对化:基准钉死到项目根,模型不会把 references/ 拼到 设定/ 底下
|
|
997
|
+
return absolutizeInlinePaths(parts.join('\n'), (context && context.dirs && context.dirs.root) || '');
|
|
671
998
|
}
|
|
672
999
|
|
|
673
1000
|
// ============================================================
|