@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,1335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2/prompts.js —— 2.0 阶段提示词构建。
|
|
3
|
+
*
|
|
4
|
+
* 提示词统一按下面的中文结构组织:
|
|
5
|
+
* 角色 → 背景 → 选题 → 任务 → 可读取的输入 → 本文件要求
|
|
6
|
+
* → 执行步骤 → 参考数据 → 禁止事项 → 校验与返工 → 上一轮错误 → 输出格式
|
|
7
|
+
*
|
|
8
|
+
* 这里只给模型“本轮要做什么、能读什么、写成什么样、怎么算过”。
|
|
9
|
+
* 阶段 README 里给人看的“阅读顺序 / 自包含 / 契约全文”不再原样塞进提示词,
|
|
10
|
+
* 避免模型把文档说明当成额外任务。
|
|
11
|
+
*
|
|
12
|
+
* 所有提示词只允许模型读取:
|
|
13
|
+
* - 当前阶段目录
|
|
14
|
+
* - _context/ 上游快照
|
|
15
|
+
* - 项目根 模板/ 与 references/
|
|
16
|
+
* 明确禁止读取插件源码、node_modules。
|
|
17
|
+
*/
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { asArray, readJson, stageDir, stageFile } from './core.js';
|
|
21
|
+
import { contractFor } from './contracts.js';
|
|
22
|
+
import { referenceLogPromptBlock } from '../reference-log.js';
|
|
23
|
+
import { renderBenchmarkDigest, renderBenchmarkGuidance } from '../../wiki-references.js';
|
|
24
|
+
|
|
25
|
+
const CN_NUM = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十',
|
|
26
|
+
'十一', '十二', '十三', '十四', '十五', '十六', '十七', '十八', '十九', '二十'];
|
|
27
|
+
|
|
28
|
+
function h2(n, title) {
|
|
29
|
+
return '## ' + (CN_NUM[n - 1] || String(n)) + '、' + title;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ordered(items, start = 1) {
|
|
33
|
+
return asArray(items).map((item, i) => (start + i) + '. ' + item);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function nextSection(parts) {
|
|
37
|
+
// 只统计本文件自己的编号小节和附录;注入的对标摘要/参考记录里也有
|
|
38
|
+
// `##` 标题,不能把它们算进后续小节编号。
|
|
39
|
+
return (parts.join('\n').match(/^## (?:[一二三四五六七八九十]+、|附:)/gm) || []).length + 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function pushHeading(parts, title) {
|
|
43
|
+
parts.push(h2(nextSection(parts), title));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function roleBlock(role, background) {
|
|
47
|
+
return [
|
|
48
|
+
'# 角色:' + role,
|
|
49
|
+
'',
|
|
50
|
+
h2(1, '背景'),
|
|
51
|
+
'',
|
|
52
|
+
...asArray(background).filter(Boolean),
|
|
53
|
+
'',
|
|
54
|
+
].join('\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function topicBlock(topicData) {
|
|
58
|
+
const topic = topicBrief(topicData);
|
|
59
|
+
if (!topic || !Object.keys(topic).length) return '';
|
|
60
|
+
return [
|
|
61
|
+
h2(3, '选题'),
|
|
62
|
+
'',
|
|
63
|
+
'```json',
|
|
64
|
+
JSON.stringify(topic, null, 2),
|
|
65
|
+
'```',
|
|
66
|
+
'',
|
|
67
|
+
'说明:选题 JSON 里的 `genre` 字段就是本书的题材;`platform` 字段是目标平台。',
|
|
68
|
+
'',
|
|
69
|
+
].join('\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function failureBlock(failures) {
|
|
73
|
+
if (!Array.isArray(failures) || !failures.length) return '';
|
|
74
|
+
const lines = ['## 附:上一轮校验错误(必须逐条改掉)', ''];
|
|
75
|
+
let n = 0;
|
|
76
|
+
for (const e of failures.slice(0, 30)) {
|
|
77
|
+
n += 1;
|
|
78
|
+
const code = e.code ? '[' + e.code + '] ' : '';
|
|
79
|
+
const p = e.path ? e.path + ':' : '';
|
|
80
|
+
lines.push(n + '. ' + code + p + (e.message || e.detail || ''));
|
|
81
|
+
if (e.expected !== undefined) lines.push(' - 期望:' + JSON.stringify(e.expected));
|
|
82
|
+
if (e.actual !== undefined) lines.push(' - 实际:' + JSON.stringify(e.actual));
|
|
83
|
+
if (e.hint) lines.push(' - 提示:' + e.hint);
|
|
84
|
+
}
|
|
85
|
+
if (failures.length > 30) lines.push((n + 1) + '. … 其余 ' + (failures.length - 30) + ' 个错误');
|
|
86
|
+
lines.push('');
|
|
87
|
+
return lines.join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sectionBlock(section, number) {
|
|
91
|
+
if (!section) return '';
|
|
92
|
+
const lines = [h2(number, '本文件要求:' + section.title), ''];
|
|
93
|
+
lines.push(...ordered(section.body));
|
|
94
|
+
lines.push('');
|
|
95
|
+
return lines.join('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const FORBIDDEN_EXPLAIN = {
|
|
99
|
+
'人物姓名': '任何具体人名、称号、绰号或称呼,例如“叶凡”“林老板”“师尊”。',
|
|
100
|
+
'具体人物姓名': '任何具体人名、称号、绰号或称呼,例如“叶凡”“林老板”“师尊”。',
|
|
101
|
+
'势力名称': '任何组织、宗门、公司、家族、国家、帮派的名字,例如“青云宗”“星河集团”。',
|
|
102
|
+
'具体势力名称': '任何组织、宗门、公司、家族、国家、帮派的名字,例如“青云宗”“星河集团”。',
|
|
103
|
+
'地点名称': '任何城市、街道、建筑、房间、地图区域的专名,例如“北京”“藏经阁”。',
|
|
104
|
+
'道具名称': '任何武器、法宝、信物、商品、系统道具的具体名字,例如“诛仙剑”“空间戒指”。',
|
|
105
|
+
'剧情句': '任何一句具体发生的事,例如“他去杀了城主”。',
|
|
106
|
+
'章节标题': '任何“第X章 ...”形式的标题。',
|
|
107
|
+
'章节号': '任何章节编号、章节标题或章节字段。',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
function forbiddenBlock(stageId, extra = [], number = 13) {
|
|
111
|
+
const contract = contractFor(stageId);
|
|
112
|
+
const items = [...asArray(contract && contract.forbidden), ...asArray(extra)];
|
|
113
|
+
if (!items.length) return '';
|
|
114
|
+
const lines = [
|
|
115
|
+
h2(number, '禁止事项'),
|
|
116
|
+
'',
|
|
117
|
+
'本阶段不允许出现以下内容;下面每一项都按“具体名称 / 具体内容”理解,即使是举例、引用、占位名也算出现。',
|
|
118
|
+
'',
|
|
119
|
+
];
|
|
120
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
121
|
+
const item = items[i];
|
|
122
|
+
const explain = FORBIDDEN_EXPLAIN[item];
|
|
123
|
+
lines.push((i + 1) + '. ' + item + (explain ? ':' + explain : ''));
|
|
124
|
+
}
|
|
125
|
+
lines.push('');
|
|
126
|
+
return lines.join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function checksBlock(stageId, checksOverride) {
|
|
130
|
+
const contract = contractFor(stageId);
|
|
131
|
+
const checks = asArray(checksOverride || (contract && contract.checks));
|
|
132
|
+
if (!checks.length) return '';
|
|
133
|
+
return ['插件会按以下契约检查(跨文件项以整阶段为准):', '', ...ordered(checks), ''].join('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 所有阶段共用的一段说明:
|
|
138
|
+
* 校验是插件跑的,不是模型跑脚本;失败会回喂,模型只改当前文件。
|
|
139
|
+
*/
|
|
140
|
+
function validationBlock(stageId, opts = {}) {
|
|
141
|
+
const validator = opts.validator || '本阶段校验器';
|
|
142
|
+
const lines = [
|
|
143
|
+
opts.appendix ? '## 附:校验与返工' : h2(opts.number || 14, '校验与返工'),
|
|
144
|
+
'',
|
|
145
|
+
...ordered([
|
|
146
|
+
'校验由插件自动执行:你不需要运行任何校验脚本,也不要读取校验器源码。',
|
|
147
|
+
'你写完后,插件会在本轮结束时运行 ' + validator + '。',
|
|
148
|
+
'如果校验失败,插件会把错误逐条放进下一轮的“上一轮校验错误”;每条错误里有 `path`、`expected`、`actual`、`hint`,你按 `path` 找到对应字段逐条修改当前文件。',
|
|
149
|
+
'错误可能引用其它文件;不要直接改上游,先检查当前文件是否与它不一致。',
|
|
150
|
+
'不要改上游文件,不要改其它阶段文件,不要新建临时脚本或 Python 脚本来拼 JSON。',
|
|
151
|
+
'连续多轮仍失败时,插件会停止并把错误报告给用户,不会把阶段标记为通过。',
|
|
152
|
+
]),
|
|
153
|
+
'',
|
|
154
|
+
checksBlock(stageId, opts.checks),
|
|
155
|
+
];
|
|
156
|
+
return lines.join('\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function inputBlock(paths, opts = {}) {
|
|
160
|
+
const lines = [h2(5, '可读取的输入'), ''];
|
|
161
|
+
lines.push(...ordered(asArray(paths).map((p) => '`' + p + '`')));
|
|
162
|
+
lines.push('');
|
|
163
|
+
if (opts.template) lines.push('结构模板:`' + opts.template + '`。模板只规定字段,不规定具体内容。', '');
|
|
164
|
+
lines.push('只读上面列出的路径,以及项目里的 `模板/v2/`、`references/`、`对标/`(如果存在)。');
|
|
165
|
+
lines.push('需要上游信息时读当前阶段目录的 `_context/` 快照;不要读插件源码、`node_modules/` 或其它项目。');
|
|
166
|
+
lines.push('');
|
|
167
|
+
return lines.join('\n');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function fileTableBlock(rows) {
|
|
171
|
+
if (!rows.length) return '';
|
|
172
|
+
const lines = [
|
|
173
|
+
h2(2, '文件说明'),
|
|
174
|
+
'',
|
|
175
|
+
'| 路径 | 提示词中的称呼 | 作用 |',
|
|
176
|
+
'|---|---|---|',
|
|
177
|
+
];
|
|
178
|
+
for (const [file, name, purpose] of rows) {
|
|
179
|
+
lines.push('| `' + file + '` | ' + name + ' | ' + purpose + ' |');
|
|
180
|
+
}
|
|
181
|
+
lines.push('');
|
|
182
|
+
return lines.join('\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function platformNameOf(topicData) {
|
|
186
|
+
return String((topicData && (topicData.platform || topicData.target_platform)) || '').trim();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function platformNoticeBlock(topicData, number = 11) {
|
|
190
|
+
const platform = platformNameOf(topicData);
|
|
191
|
+
const lines = [h2(number, '平台提醒'), ''];
|
|
192
|
+
if (!platform) {
|
|
193
|
+
lines.push(...ordered([
|
|
194
|
+
'选题没有声明平台,默认按通用网文规则计算。',
|
|
195
|
+
'`genre-profiles.json` 里的 `platform` 是按番茄小说整理的;没有平台信息时,只能参考它的题材倍率,不要照搬番茄的章节字数和钩子密度。',
|
|
196
|
+
]));
|
|
197
|
+
} else if (/番茄|fanqie/i.test(platform)) {
|
|
198
|
+
lines.push(...ordered([
|
|
199
|
+
'当前选题平台:' + platform,
|
|
200
|
+
'单章字数参考 2200 字;节奏公式仍以 2000 字/章为基准,用 `chapter_scale` 换算。',
|
|
201
|
+
'前 30 章是黄金期,钩子密度可以高于全书平均;卷长不宜过长。',
|
|
202
|
+
]));
|
|
203
|
+
} else {
|
|
204
|
+
lines.push(...ordered([
|
|
205
|
+
'当前选题平台:' + platform,
|
|
206
|
+
'`genre-profiles.json` 的 `platform` / `platform_checks` / `platform_rules` 是按番茄小说整理的,和当前平台不一致,不能直接照搬。',
|
|
207
|
+
'当前平台只使用 `genre-profiles.json` 里的题材分类和倍率;章节字数、钩子密度、卷长按当前平台要求和选题定位处理。',
|
|
208
|
+
]));
|
|
209
|
+
}
|
|
210
|
+
lines.push('');
|
|
211
|
+
return lines.join('\n');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function withoutMetaDeep(value) {
|
|
215
|
+
if (Array.isArray(value)) return value.map(withoutMetaDeep);
|
|
216
|
+
if (!value || typeof value !== 'object') return value;
|
|
217
|
+
const out = {};
|
|
218
|
+
for (const [key, item] of Object.entries(value)) {
|
|
219
|
+
if (key === '_meta') continue;
|
|
220
|
+
out[key] = withoutMetaDeep(item);
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 只把下游真正需要的上游字段放进提示词。
|
|
227
|
+
* 例如 20-macro 不需要 scale.formulas / derivations,也不需要 8 份题材 profile。
|
|
228
|
+
*/
|
|
229
|
+
function digestContextData(stageId, name, data) {
|
|
230
|
+
const clean = withoutMetaDeep(data);
|
|
231
|
+
if (name === 'baseline.json') {
|
|
232
|
+
return {
|
|
233
|
+
input: withoutMetaDeep(clean.input || {}),
|
|
234
|
+
references: clean.references || [],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (name === 'scale.json') {
|
|
238
|
+
return {
|
|
239
|
+
capacity: clean.capacity,
|
|
240
|
+
structure: clean.structure,
|
|
241
|
+
quotas: clean.quotas,
|
|
242
|
+
rhythm_budget: clean.rhythm_budget,
|
|
243
|
+
distribution: clean.distribution,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return clean;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function contextDigest(projectRoot, stageId, number = 8) {
|
|
250
|
+
const lines = [];
|
|
251
|
+
const dir = path.join(stageDir(projectRoot, stageId), '_context');
|
|
252
|
+
let names = [];
|
|
253
|
+
try {
|
|
254
|
+
names = fs.readdirSync(dir).filter((n) => n.endsWith('.json')).sort();
|
|
255
|
+
} catch {
|
|
256
|
+
names = [];
|
|
257
|
+
}
|
|
258
|
+
// 写作阶段已经有逐章细纲和人物小档案;全量 cast/rhythm 快照只会重复烧 token。
|
|
259
|
+
const skip = stageId === '50-writing' ? new Set(['cast.json', 'rhythm.json']) : new Set();
|
|
260
|
+
for (const name of names) {
|
|
261
|
+
if (skip.has(name)) continue;
|
|
262
|
+
const data = readJson(path.join(dir, name), null);
|
|
263
|
+
if (!data) continue;
|
|
264
|
+
lines.push('### _context/' + name);
|
|
265
|
+
lines.push('');
|
|
266
|
+
lines.push('```json');
|
|
267
|
+
lines.push(JSON.stringify(digestContextData(stageId, name, data)));
|
|
268
|
+
lines.push('```');
|
|
269
|
+
lines.push('');
|
|
270
|
+
}
|
|
271
|
+
return lines.length
|
|
272
|
+
? [h2(number, '上游只读快照(_context)'), '', '这些是已经通过校验的上游事实,只读,不要修改。', '', ...lines].join('\n')
|
|
273
|
+
: '';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function readContextFile(projectRoot, stageId, name) {
|
|
277
|
+
return readJson(path.join(stageDir(projectRoot, stageId), '_context', name), null);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function pickFields(obj, fields) {
|
|
281
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
282
|
+
const out = {};
|
|
283
|
+
for (const key of fields) {
|
|
284
|
+
if (obj[key] !== undefined) out[key] = obj[key];
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function compactList(list, fields) {
|
|
290
|
+
return asArray(list).map((item) => pickFields(item, fields));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function compactCastForMap(cast) {
|
|
294
|
+
const c = cast || {};
|
|
295
|
+
// 章节地图只需要“可引用的 ID + 选择时需要的最小语义”;
|
|
296
|
+
// 道具、势力、高潮、关系不参与章节地图字段,不塞进来。
|
|
297
|
+
return {
|
|
298
|
+
characters: compactList(c.characters && c.characters.characters, ['id', 'name', 'type', 'role_slot', 'faction']),
|
|
299
|
+
locations: compactList(c.locations && c.locations.locations, ['id', 'name', 'type']),
|
|
300
|
+
conflicts: compactList(c.conflicts && c.conflicts.conflicts, ['id', 'name', 'type', 'participants']),
|
|
301
|
+
foreshadows: compactList(c.foreshadows && c.foreshadows.foreshadows, ['id', 'name', 'type', 'scope']),
|
|
302
|
+
payoffs: compactList(c.payoffs && c.payoffs.payoffs, ['id', 'name', 'type', 'scale']),
|
|
303
|
+
information: compactList(c.information && c.information.information_gaps, ['id', 'info_name', 'conflict']),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** 章节地图只看“有哪些实体、怎么分布”,不把每个实体的长描述全塞进去。 */
|
|
308
|
+
function chapterMapContext(projectRoot, number = 7) {
|
|
309
|
+
const scale = readContextFile(projectRoot, '40-rhythm', 'scale.json');
|
|
310
|
+
const macro = readContextFile(projectRoot, '40-rhythm', 'macro.json');
|
|
311
|
+
const cast = readContextFile(projectRoot, '40-rhythm', 'cast.json');
|
|
312
|
+
if (!scale && !macro && !cast) return contextDigest(projectRoot, '40-rhythm');
|
|
313
|
+
const data = {
|
|
314
|
+
scale: {
|
|
315
|
+
capacity: scale && scale.capacity,
|
|
316
|
+
structure: scale && scale.structure,
|
|
317
|
+
},
|
|
318
|
+
macro: {
|
|
319
|
+
world: pickFields(macro && macro.world, ['premise', 'theme', 'tone']),
|
|
320
|
+
story: pickFields(macro && macro.story, ['core_question', 'core_conflict']),
|
|
321
|
+
volumes: macro && macro.volumes,
|
|
322
|
+
lines: macro && macro.lines,
|
|
323
|
+
endings: macro && macro.endings,
|
|
324
|
+
emotion: macro && macro.emotion,
|
|
325
|
+
},
|
|
326
|
+
cast: compactCastForMap(cast),
|
|
327
|
+
};
|
|
328
|
+
return [
|
|
329
|
+
h2(number, '上游只读摘要(_context)'),
|
|
330
|
+
'',
|
|
331
|
+
'这里只保留和章节地图有关的字段;完整实体档案在 `_context/` 里,需要时再读。',
|
|
332
|
+
'',
|
|
333
|
+
'```json',
|
|
334
|
+
JSON.stringify(withoutMetaDeep(data)),
|
|
335
|
+
'```',
|
|
336
|
+
'',
|
|
337
|
+
].join('\n');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function branchIdOf(value) {
|
|
341
|
+
const m = /^(B\d{3})/.exec(String(value || ''));
|
|
342
|
+
return m ? m[1] : '';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** 单章细纲只带本章会用到的主线节点、支线、冲突、伏笔、人物和地点。 */
|
|
346
|
+
function outlineContext(projectRoot, chapter, number = 13) {
|
|
347
|
+
const scale = readContextFile(projectRoot, '40-rhythm', 'scale.json');
|
|
348
|
+
const macro = readContextFile(projectRoot, '40-rhythm', 'macro.json');
|
|
349
|
+
const cast = readContextFile(projectRoot, '40-rhythm', 'cast.json');
|
|
350
|
+
if (!scale && !macro && !cast) return contextDigest(projectRoot, '40-rhythm');
|
|
351
|
+
const c = chapter || {};
|
|
352
|
+
const why = c.why || {};
|
|
353
|
+
const castIds = new Set(asArray(c.cast).map(String));
|
|
354
|
+
const locationIds = new Set(asArray(c.locations).map(String));
|
|
355
|
+
const conflictIds = new Set(asArray(why.conflict).map(String));
|
|
356
|
+
const mainNodeIds = new Set(asArray(why.main_line_nodes).map(String));
|
|
357
|
+
const branchIds = new Set(asArray(why.branch_nodes).map(branchIdOf).filter(Boolean));
|
|
358
|
+
const foreshadowIds = new Set(asArray(c.foreshadow_actions).map((a) => String(a && a.id)).filter(Boolean));
|
|
359
|
+
const payoffIds = new Set(asArray(c.payoff_actions).map((a) => String(a && a.id)).filter(Boolean));
|
|
360
|
+
const infoIds = new Set(asArray(c.information_gaps).map((a) => String(a && a.id)).filter(Boolean));
|
|
361
|
+
const endingIds = new Set(asArray(c.ending_actions).map((a) => String(a && a.id)).filter(Boolean));
|
|
362
|
+
|
|
363
|
+
const volume = asArray(macro && macro.volumes && macro.volumes.volumes)
|
|
364
|
+
.find((v) => (v.volume_id || v.id) === c.volume) || null;
|
|
365
|
+
const act = asArray(volume && volume.acts)
|
|
366
|
+
.find((a) => a.act_id === c.act)
|
|
367
|
+
|| asArray(macro && macro.volumes && macro.volumes.acts).find((a) => a.act_id === c.act)
|
|
368
|
+
|| null;
|
|
369
|
+
const characters = asArray(cast && cast.characters && cast.characters.characters)
|
|
370
|
+
.filter((x) => castIds.has(String(x.id)));
|
|
371
|
+
const factions = new Set(characters.map((x) => x.faction).filter(Boolean));
|
|
372
|
+
const displayNo = Number(c.display_no);
|
|
373
|
+
const targetChapters = Number(scale && scale.capacity && scale.capacity.target_chapters);
|
|
374
|
+
const endings = asArray(macro && macro.endings && macro.endings.endings)
|
|
375
|
+
.filter((e) => {
|
|
376
|
+
if (endingIds.size) return endingIds.has(String(e.id));
|
|
377
|
+
// 没有显式 ending_actions 时,不要把全书 5 个结局塞进每一章:
|
|
378
|
+
// 卷级结局只给卷末章,全书级结局只给最后一章。
|
|
379
|
+
if (e.level === '全书级') return displayNo === targetChapters;
|
|
380
|
+
if (e.volume && volume) {
|
|
381
|
+
return e.volume === (volume.volume_id || volume.id) && displayNo === Number(volume.chapter_end);
|
|
382
|
+
}
|
|
383
|
+
return false;
|
|
384
|
+
});
|
|
385
|
+
const data = {
|
|
386
|
+
scale: { capacity: scale && scale.capacity },
|
|
387
|
+
macro: {
|
|
388
|
+
world: pickFields(macro && macro.world, ['premise', 'theme', 'tone', 'world_rules']),
|
|
389
|
+
story: pickFields(macro && macro.story, ['core_question', 'core_conflict', 'structure']),
|
|
390
|
+
volume: volume ? {
|
|
391
|
+
volume_id: volume.volume_id || volume.id,
|
|
392
|
+
title: volume.title,
|
|
393
|
+
goal: volume.goal,
|
|
394
|
+
core_conflict: volume.core_conflict,
|
|
395
|
+
emotional_arc: volume.emotional_arc,
|
|
396
|
+
chapter_start: volume.chapter_start,
|
|
397
|
+
chapter_end: volume.chapter_end,
|
|
398
|
+
} : null,
|
|
399
|
+
act,
|
|
400
|
+
main_line_nodes: asArray(macro && macro.lines && macro.lines.main_line && macro.lines.main_line.nodes)
|
|
401
|
+
.filter((n) => mainNodeIds.has(String(n.id))),
|
|
402
|
+
branches: asArray(macro && macro.lines && macro.lines.branches)
|
|
403
|
+
.filter((b) => branchIds.has(String(b.id))),
|
|
404
|
+
endings,
|
|
405
|
+
},
|
|
406
|
+
cast: {
|
|
407
|
+
characters,
|
|
408
|
+
factions: asArray(cast && cast.factions && cast.factions.factions)
|
|
409
|
+
.filter((x) => factions.has(x.id)),
|
|
410
|
+
locations: asArray(cast && cast.locations && cast.locations.locations)
|
|
411
|
+
.filter((x) => locationIds.has(String(x.id))),
|
|
412
|
+
conflicts: asArray(cast && cast.conflicts && cast.conflicts.conflicts)
|
|
413
|
+
.filter((x) => conflictIds.has(String(x.id))),
|
|
414
|
+
foreshadows: asArray(cast && cast.foreshadows && cast.foreshadows.foreshadows)
|
|
415
|
+
.filter((x) => foreshadowIds.has(String(x.id))),
|
|
416
|
+
payoffs: asArray(cast && cast.payoffs && cast.payoffs.payoffs)
|
|
417
|
+
.filter((x) => payoffIds.has(String(x.id))),
|
|
418
|
+
information: asArray(cast && cast.information && cast.information.information_gaps)
|
|
419
|
+
.filter((x) => infoIds.has(String(x.id))),
|
|
420
|
+
relations: [
|
|
421
|
+
...asArray(cast && cast.relations && cast.relations.character_relations),
|
|
422
|
+
...asArray(cast && cast.relations && cast.relations.faction_relations),
|
|
423
|
+
].filter((r) => castIds.has(String(r.from)) || castIds.has(String(r.to)) || factions.has(r.from) || factions.has(r.to)),
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
return [
|
|
427
|
+
h2(number, '本章相关上游(_context 摘要)'),
|
|
428
|
+
'',
|
|
429
|
+
'只保留本章会用到的主线节点、支线、冲突、伏笔、人物和地点;完整上游在 `_context/` 里。',
|
|
430
|
+
'',
|
|
431
|
+
'```json',
|
|
432
|
+
JSON.stringify(withoutMetaDeep(data)),
|
|
433
|
+
'```',
|
|
434
|
+
'',
|
|
435
|
+
].join('\n');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function rhythmFormulaBlock(projectRoot, number = 9) {
|
|
439
|
+
const candidates = [
|
|
440
|
+
path.join(projectRoot, 'references', 'formulas-rhythm.json'),
|
|
441
|
+
path.join(projectRoot, '设定', '00-baseline', 'formulas-rhythm.json'),
|
|
442
|
+
];
|
|
443
|
+
for (const file of candidates) {
|
|
444
|
+
try {
|
|
445
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
446
|
+
return [h2(number, '节奏公式与参考值(只读)'), '', '```json', JSON.stringify(data), '```', ''].join('\n');
|
|
447
|
+
} catch {
|
|
448
|
+
// try next
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return '';
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function projectTopic(projectRoot) {
|
|
455
|
+
const baseline = readJson(path.join(stageDir(projectRoot, '00-baseline'), '_context', 'baseline.json'), null)
|
|
456
|
+
|| readJson(stageFile(projectRoot, '00-baseline', 'input.json'), null);
|
|
457
|
+
return (baseline && baseline.input) || baseline || {};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function sectionForFile(stageId, file) {
|
|
461
|
+
const contract = contractFor(stageId);
|
|
462
|
+
if (!contract) return null;
|
|
463
|
+
const base = String(file || '').replace(/\.json$/i, '').split('/').pop();
|
|
464
|
+
return asArray(contract.sections).find((s) => s.id === base) || null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** 只取选题里与数量决策有关的字段,避免把 _meta、空字段一起塞给模型。 */
|
|
468
|
+
function topicBrief(topicData) {
|
|
469
|
+
const topic = topicData || {};
|
|
470
|
+
const keys = [
|
|
471
|
+
'book_name', 'genre', 'platform', 'positioning', 'one_line_pitch',
|
|
472
|
+
'target_chapters', 'target_words', 'core_conflict', 'reader_value',
|
|
473
|
+
'golden_finger', 'core_setting', 'selling_points', 'hard_constraints', 'anti_tropes',
|
|
474
|
+
'direction_title', 'payoff_model', 'emotion_curve', 'opening_hook', 'pacing_plan',
|
|
475
|
+
'character_design', 'differentiation', 'risk_validation',
|
|
476
|
+
];
|
|
477
|
+
const out = {};
|
|
478
|
+
for (const key of keys) {
|
|
479
|
+
const value = topic[key];
|
|
480
|
+
if (value === undefined || value === null || value === '') continue;
|
|
481
|
+
if (Array.isArray(value) && !value.length) continue;
|
|
482
|
+
out[key] = value;
|
|
483
|
+
}
|
|
484
|
+
return out;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const REFERENCE_PHASE_BY_STAGE = {
|
|
488
|
+
'10-scale': 'skeleton',
|
|
489
|
+
'20-macro': 'skeleton',
|
|
490
|
+
'30-cast': 'flesh',
|
|
491
|
+
'40-rhythm': 'rhythm',
|
|
492
|
+
'50-writing': 'writing',
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const STAGE_LABEL = {
|
|
496
|
+
'10-scale': '数量',
|
|
497
|
+
'20-macro': '骨架',
|
|
498
|
+
'30-cast': '血肉',
|
|
499
|
+
'40-rhythm': '节奏',
|
|
500
|
+
'50-writing': '写作',
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const STAGE_GUIDE_FILES = {
|
|
504
|
+
'10-scale': [
|
|
505
|
+
'references/骨架指导/00-index.md',
|
|
506
|
+
'references/骨架指导/05-quota.md',
|
|
507
|
+
'references/骨架指导/06-audit.md',
|
|
508
|
+
'references/glossary.md',
|
|
509
|
+
],
|
|
510
|
+
'20-macro': [
|
|
511
|
+
'references/骨架指导/00-index.md',
|
|
512
|
+
'references/骨架指导/01-positioning.md',
|
|
513
|
+
'references/骨架指导/03-entities.md',
|
|
514
|
+
'references/骨架指导/04-conflict.md',
|
|
515
|
+
'references/骨架指导/05-quota.md',
|
|
516
|
+
'references/骨架指导/06-audit.md',
|
|
517
|
+
'references/glossary.md',
|
|
518
|
+
],
|
|
519
|
+
'30-cast': [
|
|
520
|
+
'references/血肉指导/00-index.md',
|
|
521
|
+
'references/血肉指导/01-character.md',
|
|
522
|
+
'references/血肉指导/02-worldbuilding.md',
|
|
523
|
+
'references/血肉指导/03-relations.md',
|
|
524
|
+
'references/血肉指导/04-emotion-methods.md',
|
|
525
|
+
'references/血肉指导/05-audit.md',
|
|
526
|
+
'references/glossary.md',
|
|
527
|
+
],
|
|
528
|
+
'40-rhythm': [
|
|
529
|
+
'references/节奏指导/00-index.md',
|
|
530
|
+
'references/节奏指导/01-upgrade-rhythm.md',
|
|
531
|
+
'references/节奏指导/02-arc-rhythm.md',
|
|
532
|
+
'references/节奏指导/03-highpoint-hooks.md',
|
|
533
|
+
'references/节奏指导/04-satisfaction.md',
|
|
534
|
+
'references/节奏指导/05-foreshadow-density.md',
|
|
535
|
+
'references/节奏指导/06-chapter-outline.md',
|
|
536
|
+
'references/节奏指导/07-audit.md',
|
|
537
|
+
'references/formulas-rhythm.json',
|
|
538
|
+
'references/glossary.md',
|
|
539
|
+
],
|
|
540
|
+
'50-writing': [
|
|
541
|
+
'references/写作指导/00-index.md',
|
|
542
|
+
'references/写作指导/01-scene.md',
|
|
543
|
+
'references/写作指导/02-chapter-workflow.md',
|
|
544
|
+
'references/写作指导/03-opening-chapter.md',
|
|
545
|
+
'references/写作指导/04-dialogue-mastery.md',
|
|
546
|
+
'references/写作指导/05-genre-formulas.md',
|
|
547
|
+
'references/写作指导/06-tone-ensemble.md',
|
|
548
|
+
'references/写作指导/07-ledger-audit.md',
|
|
549
|
+
'references/写作指导/08-diagnosis-revision.md',
|
|
550
|
+
'references/glossary.md',
|
|
551
|
+
],
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const STAGE_BATCH_BY_FILE = {
|
|
555
|
+
'20-macro': {
|
|
556
|
+
world: '实体',
|
|
557
|
+
story: '框架',
|
|
558
|
+
volumes: '层级',
|
|
559
|
+
lines: '框架',
|
|
560
|
+
endings: '框架',
|
|
561
|
+
emotion: '实体',
|
|
562
|
+
roles: '关系',
|
|
563
|
+
},
|
|
564
|
+
'30-cast': {
|
|
565
|
+
characters: '角色',
|
|
566
|
+
factions: '势力',
|
|
567
|
+
locations: '地点',
|
|
568
|
+
items: '关键道具',
|
|
569
|
+
conflicts: '冲突',
|
|
570
|
+
foreshadows: '剧情',
|
|
571
|
+
climaxes: '剧情',
|
|
572
|
+
payoffs: '剧情',
|
|
573
|
+
relations: '角色',
|
|
574
|
+
information: '剧情',
|
|
575
|
+
},
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
function existingReferenceFiles(projectRoot, paths) {
|
|
579
|
+
return asArray(paths).filter((rel) => {
|
|
580
|
+
try {
|
|
581
|
+
return fs.existsSync(path.join(projectRoot, rel));
|
|
582
|
+
} catch {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function guideFilesFor(stageId, opts = {}) {
|
|
589
|
+
if (opts.batchType === '章节细纲') {
|
|
590
|
+
return [
|
|
591
|
+
'references/节奏指导/00-index.md',
|
|
592
|
+
'references/节奏指导/03-highpoint-hooks.md',
|
|
593
|
+
'references/节奏指导/05-foreshadow-density.md',
|
|
594
|
+
'references/节奏指导/06-chapter-outline.md',
|
|
595
|
+
'references/节奏指导/07-audit.md',
|
|
596
|
+
'references/formulas-rhythm.json',
|
|
597
|
+
'references/glossary.md',
|
|
598
|
+
];
|
|
599
|
+
}
|
|
600
|
+
return STAGE_GUIDE_FILES[stageId] || [];
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* 每个阶段都必须显式列出的参考块:
|
|
605
|
+
* - references/<阶段>指导/ 的原文文件(方法论,决定下限)
|
|
606
|
+
* - 对标/wiki/ 的阶段清单、本批优先清单和摘要(市场样本,校准上限)
|
|
607
|
+
* - 参考决策台账的强制记录规则(每项输出都要留来源)
|
|
608
|
+
*/
|
|
609
|
+
function referenceDecisionBlock(projectRoot, stageId, opts = {}) {
|
|
610
|
+
const phase = opts.phase || REFERENCE_PHASE_BY_STAGE[stageId] || 'skeleton';
|
|
611
|
+
const batchType = opts.batchType || '';
|
|
612
|
+
const files = existingReferenceFiles(projectRoot, guideFilesFor(stageId, opts));
|
|
613
|
+
const guidance = renderBenchmarkGuidance(projectRoot, phase, batchType);
|
|
614
|
+
const digest = renderBenchmarkDigest(projectRoot, phase, { type: batchType });
|
|
615
|
+
const logBlock = referenceLogPromptBlock(
|
|
616
|
+
opts.stageLabel || STAGE_LABEL[stageId] || stageId,
|
|
617
|
+
{ type: opts.batchName || batchType || stageId },
|
|
618
|
+
{
|
|
619
|
+
allowedFileText: opts.allowedFileText
|
|
620
|
+
|| ('除本轮输出文件外,`设定/.reference-decisions.md` 是本轮唯一允许写的额外文件;不要修改 wiki、模板、references 目录或其它阶段。'),
|
|
621
|
+
},
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
const lines = [];
|
|
625
|
+
lines.push(opts.number ? h2(opts.number, '参考文件与参考决策(必须实际读取)') : '## 参考文件与参考决策(必须实际读取)');
|
|
626
|
+
lines.push('');
|
|
627
|
+
if (files.length) {
|
|
628
|
+
lines.push('### 本阶段方法论文件(先读原文,决定下限)');
|
|
629
|
+
lines.push('');
|
|
630
|
+
for (const file of files) lines.push('- `' + file + '`');
|
|
631
|
+
lines.push('');
|
|
632
|
+
} else {
|
|
633
|
+
lines.push('### 本阶段方法论文件');
|
|
634
|
+
lines.push('');
|
|
635
|
+
lines.push('项目 `references/` 下没有可读的指导文件;按硬契约、选题和上游产物写,不要凭空编造方法论。');
|
|
636
|
+
lines.push('');
|
|
637
|
+
}
|
|
638
|
+
if (guidance) {
|
|
639
|
+
lines.push(guidance);
|
|
640
|
+
} else {
|
|
641
|
+
lines.push('### 对标样本');
|
|
642
|
+
lines.push('');
|
|
643
|
+
lines.push('项目还没有 `对标/wiki/` 总结文件;不要编造“对标分析”,也不要引用不存在的路径。');
|
|
644
|
+
lines.push('');
|
|
645
|
+
}
|
|
646
|
+
if (digest) {
|
|
647
|
+
lines.push(digest);
|
|
648
|
+
lines.push('');
|
|
649
|
+
}
|
|
650
|
+
lines.push(logBlock);
|
|
651
|
+
return lines.join('\n');
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function referenceInputPaths(projectRoot, stageId, opts = {}) {
|
|
655
|
+
const files = existingReferenceFiles(projectRoot, guideFilesFor(stageId, opts));
|
|
656
|
+
const hasBenchmark = (() => {
|
|
657
|
+
try {
|
|
658
|
+
return fs.existsSync(path.join(projectRoot, '对标', 'wiki'));
|
|
659
|
+
} catch {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
})();
|
|
663
|
+
return [...files, ...(hasBenchmark ? ['对标/wiki/'] : [])];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export function buildStageFilePrompt(opts) {
|
|
667
|
+
const {
|
|
668
|
+
projectRoot, stageId, file, templateFile, attempt = 1, failures = [], extra = '',
|
|
669
|
+
} = opts || {};
|
|
670
|
+
const contract = contractFor(stageId);
|
|
671
|
+
const section = sectionForFile(stageId, file);
|
|
672
|
+
const fileBase = String(file || '').replace(/\.json$/i, '').split('/').pop();
|
|
673
|
+
const batchType = (STAGE_BATCH_BY_FILE[stageId] && STAGE_BATCH_BY_FILE[stageId][fileBase]) || '';
|
|
674
|
+
const parts = [];
|
|
675
|
+
parts.push(roleBlock(
|
|
676
|
+
'网文项目的阶段编辑(' + stageId + ')',
|
|
677
|
+
[
|
|
678
|
+
'你在一个按阶段推进的网文项目里工作。当前阶段是 `' + stageId + '`:' + (contract && contract.purpose ? contract.purpose : ''),
|
|
679
|
+
'本轮只写一个文件,不要同时处理其它阶段或其它文件。',
|
|
680
|
+
],
|
|
681
|
+
));
|
|
682
|
+
parts.push(fileTableBlock([
|
|
683
|
+
['设定/' + stageId + '/', '当前阶段目录', '本阶段已经生成的文件。'],
|
|
684
|
+
['设定/' + stageId + '/_context/', '上游快照', '已经通过校验的上游事实,只读。'],
|
|
685
|
+
...(templateFile ? [['模板/v2/' + templateFile, '结构模板', '本文件的字段结构。']] : []),
|
|
686
|
+
['设定/' + stageId + '/' + file, '输出文件', '你本轮要写的文件。'],
|
|
687
|
+
]));
|
|
688
|
+
parts.push(topicBlock(projectTopic(projectRoot)));
|
|
689
|
+
pushHeading(parts, '任务');
|
|
690
|
+
parts.push('');
|
|
691
|
+
parts.push('写文件:`设定/' + stageId + '/' + file + '`。');
|
|
692
|
+
parts.push('');
|
|
693
|
+
parts.push(inputBlock([
|
|
694
|
+
'设定/' + stageId + '/',
|
|
695
|
+
'设定/' + stageId + '/_context/',
|
|
696
|
+
...(templateFile ? ['模板/v2/' + templateFile] : []),
|
|
697
|
+
...referenceInputPaths(projectRoot, stageId, { batchType }),
|
|
698
|
+
], { template: templateFile ? '模板/v2/' + templateFile : '' }));
|
|
699
|
+
if (section) parts.push(sectionBlock(section, nextSection(parts)));
|
|
700
|
+
if (extra) {
|
|
701
|
+
pushHeading(parts, '本阶段其它文件(写当前文件时必须与它们保持一致)');
|
|
702
|
+
parts.push('');
|
|
703
|
+
parts.push(extra);
|
|
704
|
+
parts.push('');
|
|
705
|
+
}
|
|
706
|
+
const digest = contextDigest(projectRoot, stageId, nextSection(parts));
|
|
707
|
+
if (digest) parts.push(digest);
|
|
708
|
+
parts.push(referenceDecisionBlock(projectRoot, stageId, {
|
|
709
|
+
number: nextSection(parts),
|
|
710
|
+
batchType,
|
|
711
|
+
batchName: file,
|
|
712
|
+
}));
|
|
713
|
+
pushHeading(parts, '执行步骤');
|
|
714
|
+
parts.push('');
|
|
715
|
+
parts.push('1. 先读 `references/` 里的本阶段方法论,再核对 `对标/wiki/` 的本批参考;摘要只用于定位,不能替代原文。');
|
|
716
|
+
parts.push('2. 读结构模板和 `_context/` 上游快照,确认本文件需要哪些实体、数量和上游引用。');
|
|
717
|
+
parts.push('3. 只写当前文件;不要顺手改别的文件,也不要把上游快照复制回上游目录。');
|
|
718
|
+
parts.push('4. 每做一个会进入本文件的设计决策,就按“参考决策记录”的格式向 `设定/.reference-decisions.md` 追加一行。');
|
|
719
|
+
parts.push('5. 写完后按“本文件要求”和“禁止事项”自检一遍。');
|
|
720
|
+
parts.push('6. 等插件校验;如果收到错误,按错误路径修改当前文件。');
|
|
721
|
+
parts.push('');
|
|
722
|
+
parts.push(forbiddenBlock(stageId, [
|
|
723
|
+
'不要修改其它阶段或其它文件;`设定/.reference-decisions.md` 是唯一例外。',
|
|
724
|
+
'不要生成 wiki 页面。',
|
|
725
|
+
'不要写 `_meta`、依赖图或下游索引,这些由插件补。',
|
|
726
|
+
], nextSection(parts)));
|
|
727
|
+
parts.push(validationBlock(stageId, { number: nextSection(parts) }));
|
|
728
|
+
parts.push(failureBlock(failures));
|
|
729
|
+
pushHeading(parts, '输出格式');
|
|
730
|
+
parts.push('');
|
|
731
|
+
parts.push('1. 写一个 JSON 对象,顶层业务字段与“本文件要求”一致。');
|
|
732
|
+
parts.push('2. 不要写 markdown 代码围栏,不要把 JSON 包在字符串里。');
|
|
733
|
+
parts.push('3. 不要自己补 `formulas`、`derivations`、`_meta` 这类插件生成的字段。');
|
|
734
|
+
parts.push('4. 参考决策按表格追加到 `设定/.reference-decisions.md`,不要覆盖已有行。');
|
|
735
|
+
parts.push('5. 写完后只汇报:文件路径、条目数量、参考决策行数、无法确定的地方。');
|
|
736
|
+
parts.push('');
|
|
737
|
+
return parts.join('\n');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
export function buildChapterMapPrompt(opts) {
|
|
741
|
+
const {
|
|
742
|
+
projectRoot, volumes, volumeId, startCh, endCh, attempt = 1, failures = [], existing = '', currentChunk = null,
|
|
743
|
+
} = opts || {};
|
|
744
|
+
const contract = contractFor('40-rhythm');
|
|
745
|
+
const chapterMapSection = asArray(contract && contract.sections).find((s) => s.id === 'chapter-map');
|
|
746
|
+
const parts = [];
|
|
747
|
+
parts.push(roleBlock(
|
|
748
|
+
'网文项目的章节地图编辑(40-rhythm)',
|
|
749
|
+
[
|
|
750
|
+
'你在把宏观卷纲和实体设定落到每一章。本轮只负责一个 chunk,不写细纲。',
|
|
751
|
+
'`40-rhythm` 的目标:' + (contract && contract.purpose ? contract.purpose : ''),
|
|
752
|
+
],
|
|
753
|
+
));
|
|
754
|
+
parts.push(fileTableBlock([
|
|
755
|
+
['设定/40-rhythm/', '当前阶段目录', '本阶段已经生成的章节地图和细纲。'],
|
|
756
|
+
['设定/40-rhythm/_context/', '上游快照', 'scale / macro / cast 的只读快照。'],
|
|
757
|
+
['模板/v2/40-rhythm/chapter-map-template.json', '结构模板', '章节地图分片的字段结构。'],
|
|
758
|
+
['设定/40-rhythm/.chapter-map/' + volumes + '.json', '输出文件', '你本轮要写的章节地图分片。'],
|
|
759
|
+
]));
|
|
760
|
+
parts.push(topicBlock(projectTopic(projectRoot)));
|
|
761
|
+
pushHeading(parts, '任务');
|
|
762
|
+
parts.push('');
|
|
763
|
+
parts.push((currentChunk ? '修订文件:' : '写文件:') + '`设定/40-rhythm/.chapter-map/' + volumes + '.json`。');
|
|
764
|
+
parts.push('所属卷:`' + (volumeId || volumes) + '`。');
|
|
765
|
+
parts.push('章节范围:第 ' + startCh + '-' + endCh + ' 章。');
|
|
766
|
+
parts.push('');
|
|
767
|
+
if (currentChunk) {
|
|
768
|
+
parts.push('这是返工轮:下面给出了本 chunk 的当前内容。只改“上一轮校验错误”指向的章节或缺失动作;其它章节必须原样保留,不要顺手重写整个 chunk。');
|
|
769
|
+
}
|
|
770
|
+
parts.push('这是章节地图分片,不是全书审校。不要重新规划全书,不要逐条检查所有 payoff 的 required_setup;如果某个 payoff 的 required_setup 指向后续卷,本 chunk 不要兑现,交给后续卷处理,但每个 payoff 最终都必须有落点。写完文件后立即结束,不要继续读文件或自我审计。');
|
|
771
|
+
parts.push('');
|
|
772
|
+
parts.push('覆盖硬约束:本 chunk 内出现的每个伏笔,`回收 / 推进 / 强化` 之前必须有更早的 `埋设`;如果本章节范围是它第一次出现,先写 `埋设`。全书每个爽点至少要有一次 `payoff_actions` 落点;属于本卷的爽点不要因为“后面还有章节”就全部跳过。');
|
|
773
|
+
parts.push('节奏硬约束:第 1-3 章的 `hook.strength` 必须是 `强`;不要连续 3 章使用同一个 `emotion.value`,需要平流时至少让其中一章差 1。');
|
|
774
|
+
parts.push('结局落点:`设定/20-macro/endings.json` 里的每个结局节点,至少要在对应卷末或全书末的章节写一次 `ending_actions`,动作写 `收束`。');
|
|
775
|
+
parts.push('知识时间线硬约束:只要本章有人对某条信息差/伏笔的知道程度发生变化,就必须写 `knowledge_events`。每个事件写清 subject_type、subject_id、character、change、coverage、knowledge、how、source_character、scene、evidence。同一角色对同一对象已经 `full` 后,不能在本章或后续章节重新写成 `partial/unknown/misinforms`;要写“遗忘/纠正”必须明确用 `forgets/corrects`。');
|
|
776
|
+
parts.push('知识事件取值映射(校验器强制,写错会被拒):`learns → coverage 只能是 partial 或 full`;`partially_learns → partial`;`confirms → full`;`corrects → partial 或 full`;`forgets → unknown`;`misinforms → false`。`how=told` 时必须写 `source_character`,且不能等于 `character` 自己。');
|
|
777
|
+
parts.push('');
|
|
778
|
+
parts.push(inputBlock([
|
|
779
|
+
'设定/40-rhythm/',
|
|
780
|
+
'设定/40-rhythm/_context/',
|
|
781
|
+
'模板/v2/40-rhythm/chapter-map-template.json',
|
|
782
|
+
...referenceInputPaths(projectRoot, '40-rhythm', { batchType: '章节规划' }),
|
|
783
|
+
], { template: '模板/v2/40-rhythm/chapter-map-template.json' }));
|
|
784
|
+
if (chapterMapSection) parts.push(sectionBlock(chapterMapSection, nextSection(parts)));
|
|
785
|
+
parts.push('节奏公式以 2000 字/章为基准;本项目 `capacity.chapter_scale` 是换算系数,所有“持续 N 章”的参考值都要乘它。');
|
|
786
|
+
parts.push('');
|
|
787
|
+
if (existing) {
|
|
788
|
+
pushHeading(parts, '已生成的相邻章节(用于衔接,不要重复)');
|
|
789
|
+
parts.push('');
|
|
790
|
+
parts.push(existing);
|
|
791
|
+
parts.push('');
|
|
792
|
+
}
|
|
793
|
+
const digest = chapterMapContext(projectRoot, nextSection(parts));
|
|
794
|
+
if (digest) parts.push(digest);
|
|
795
|
+
parts.push(rhythmFormulaBlock(projectRoot, nextSection(parts)));
|
|
796
|
+
parts.push(referenceDecisionBlock(projectRoot, '40-rhythm', {
|
|
797
|
+
number: nextSection(parts),
|
|
798
|
+
batchType: '章节规划',
|
|
799
|
+
batchName: '章节地图 ' + volumes,
|
|
800
|
+
allowedFileText: '除本 chunk 外,`设定/.reference-decisions.md` 是本轮唯一允许写的额外文件;不要修改其它 chunk、章节地图、模板或上游文件。',
|
|
801
|
+
}));
|
|
802
|
+
if (currentChunk) {
|
|
803
|
+
pushHeading(parts, '本 chunk 当前内容(返工基线,未涉及章节必须保留)');
|
|
804
|
+
parts.push('');
|
|
805
|
+
parts.push('```json');
|
|
806
|
+
parts.push(JSON.stringify(currentChunk));
|
|
807
|
+
parts.push('```');
|
|
808
|
+
parts.push('');
|
|
809
|
+
}
|
|
810
|
+
pushHeading(parts, '执行步骤');
|
|
811
|
+
parts.push('');
|
|
812
|
+
parts.push('1. 先读 `references/节奏指导/` 的方法论和 `对标/wiki/` 的本批参考;摘要只用于定位,做决策前核对原文对应条目。');
|
|
813
|
+
parts.push('2. 读 `_context/scale.json`,确认本卷的章节区间、幕区间和平均每章字数。');
|
|
814
|
+
parts.push('3. 读 `_context/macro.json` 和 `_context/cast.json`,只取本卷相关的主线节点、支线、冲突、伏笔、人物和地点。');
|
|
815
|
+
parts.push('4. 按第 ' + startCh + '-' + endCh + ' 章逐章写 5W:谁、何时、何地、为什么、怎么写。');
|
|
816
|
+
parts.push('5. 检查伏笔先埋后收、爽点先铺垫后兑现,章节落点不要越过本卷;本卷内应出现的条目至少各落一次。');
|
|
817
|
+
parts.push('6. 每个会进入章节地图的设计决策,按“参考决策记录”的格式向 `设定/.reference-decisions.md` 追加一行(每章规划至少一行)。');
|
|
818
|
+
parts.push('7. 只写这个 chunk,不要合并其它 chunk,不要写 `chapter_map.json`。');
|
|
819
|
+
parts.push('8. 写完后只确认文件已落盘并汇报;不要继续读其它 chunk,不要做第二轮全书检查。');
|
|
820
|
+
parts.push('');
|
|
821
|
+
parts.push(forbiddenBlock('40-rhythm', [
|
|
822
|
+
'本 chunk 之外的内容不要写。',
|
|
823
|
+
'不要修改其它 chunk、章节地图或上游文件;`设定/.reference-decisions.md` 是唯一例外。',
|
|
824
|
+
'不要创建 Python/临时脚本,也不要运行脚本生成 JSON;直接写 JSON。',
|
|
825
|
+
], nextSection(parts)));
|
|
826
|
+
parts.push(validationBlock('40-rhythm', { validator: '40-rhythm 的章节地图校验', number: nextSection(parts) }));
|
|
827
|
+
parts.push(failureBlock(failures));
|
|
828
|
+
pushHeading(parts, '输出格式');
|
|
829
|
+
parts.push('');
|
|
830
|
+
parts.push('顶层写 `{ "chapters": [ ... ] }`,只包含第 ' + startCh + '-' + endCh + ' 章。');
|
|
831
|
+
parts.push('`chapter_id` 初始生成一律使用 `CH` + 三位 display_no,例如第 1 章 → `CH001`;返工轮保留已有 chapter_id。');
|
|
832
|
+
parts.push('每章必须有:title、volume、act、pov、cast、locations、time、why、how、foreshadow_actions、payoff_actions、information_gaps、knowledge_events、emotion、hook、word_target。');
|
|
833
|
+
parts.push('`ending_actions` 可选;但每个结局节点(E001...)至少要在对应章节出现一次,格式 `{ "id": "E001", "action": "收束", "detail": "..." }`。');
|
|
834
|
+
parts.push('`knowledge_events` 可选但强烈建议每章都写(没有变化就写空数组)。格式:`{ "subject_type": "information|foreshadow", "subject_id": "IG-...|FS...", "character": "C001", "change": "learns|partially_learns|confirms|corrects|forgets|misinforms", "coverage": "unknown|partial|full|false", "knowledge": "具体知道什么", "how": "witnessed|told|inferred|document|rumor|experience|other", "source_character": "C002 或 null", "scene": "场景/地点", "evidence": "依据" }`。');
|
|
835
|
+
parts.push('字段值约束:`foreshadow_actions[].action` 用 埋设 / 回收 / 推进 / 强化;`information_gaps[].state` 只能是 created / progressed / resolved(不要写中文“推进/回收”);`payoff_actions[].action` 用 铺垫 / 兑现 / 回收。');
|
|
836
|
+
parts.push('参考决策按表格追加到 `设定/.reference-decisions.md`,不要覆盖已有行。');
|
|
837
|
+
parts.push('不要在 JSON 里写 markdown。');
|
|
838
|
+
parts.push('');
|
|
839
|
+
return parts.join('\n');
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export function buildOutlinePrompt(opts) {
|
|
843
|
+
const {
|
|
844
|
+
projectRoot, chapter, chapterMap, previous, next, castDigest, macroDigest, attempt = 1, failures = [],
|
|
845
|
+
} = opts || {};
|
|
846
|
+
const contract = contractFor('40-rhythm');
|
|
847
|
+
const outlineSection = asArray(contract && contract.sections).find((s) => s.id === 'outline');
|
|
848
|
+
const coverageSection = asArray(contract && contract.sections).find((s) => s.id === 'coverage');
|
|
849
|
+
const id = chapter.chapter_id;
|
|
850
|
+
const parts = [];
|
|
851
|
+
parts.push(roleBlock(
|
|
852
|
+
'网文项目的单章细纲编辑(40-rhythm)',
|
|
853
|
+
[
|
|
854
|
+
'你在把章节地图扩写成一份能直接交给写作模型的可落地细纲。',
|
|
855
|
+
'本轮只写第 ' + (chapter.number || chapter.display_no) + ' 章(`' + id + '`),不要写正文,也不要改章节地图。',
|
|
856
|
+
],
|
|
857
|
+
));
|
|
858
|
+
parts.push(fileTableBlock([
|
|
859
|
+
['设定/40-rhythm/', '当前阶段目录', '章节地图和已有细纲。'],
|
|
860
|
+
['设定/40-rhythm/_context/', '上游快照', 'scale / macro / cast 的只读快照。'],
|
|
861
|
+
['模板/v2/40-rhythm/outline-template.json', '结构模板', '单章细纲的字段结构。'],
|
|
862
|
+
['设定/40-rhythm/outlines/' + id + '.json', '输出文件', '你本轮要写的单章细纲。'],
|
|
863
|
+
]));
|
|
864
|
+
parts.push(topicBlock(projectTopic(projectRoot)));
|
|
865
|
+
pushHeading(parts, '任务');
|
|
866
|
+
parts.push('');
|
|
867
|
+
parts.push('写文件:`设定/40-rhythm/outlines/' + id + '.json`。');
|
|
868
|
+
parts.push('');
|
|
869
|
+
parts.push('这一份文件必须能单独交给写作模型:它不需要再读别的章节细纲,就能知道谁出场、何时、何地、为什么、怎么写、怎么结束。');
|
|
870
|
+
parts.push('');
|
|
871
|
+
parts.push(inputBlock([
|
|
872
|
+
'设定/40-rhythm/',
|
|
873
|
+
'设定/40-rhythm/_context/',
|
|
874
|
+
'模板/v2/40-rhythm/outline-template.json',
|
|
875
|
+
...referenceInputPaths(projectRoot, '40-rhythm', { batchType: '章节细纲' }),
|
|
876
|
+
], { template: '模板/v2/40-rhythm/outline-template.json' }));
|
|
877
|
+
if (outlineSection) parts.push(sectionBlock(outlineSection, nextSection(parts)));
|
|
878
|
+
if (coverageSection) parts.push(sectionBlock(coverageSection, nextSection(parts)));
|
|
879
|
+
parts.push('节奏公式以 2000 字/章为基准;本项目 `capacity.chapter_scale` 是换算系数,场景节拍和钩子密度要按它调整。');
|
|
880
|
+
parts.push('');
|
|
881
|
+
pushHeading(parts, '本章章节地图');
|
|
882
|
+
parts.push('');
|
|
883
|
+
parts.push('```json');
|
|
884
|
+
parts.push(JSON.stringify(chapterMap, null, 2));
|
|
885
|
+
parts.push('```');
|
|
886
|
+
parts.push('');
|
|
887
|
+
if (previous) {
|
|
888
|
+
pushHeading(parts, '上一章摘要');
|
|
889
|
+
parts.push('');
|
|
890
|
+
parts.push(previous);
|
|
891
|
+
parts.push('');
|
|
892
|
+
}
|
|
893
|
+
if (next) {
|
|
894
|
+
pushHeading(parts, '下一章摘要');
|
|
895
|
+
parts.push('');
|
|
896
|
+
parts.push(next);
|
|
897
|
+
parts.push('');
|
|
898
|
+
}
|
|
899
|
+
if (macroDigest) {
|
|
900
|
+
pushHeading(parts, '宏观上下文(只取本章相关)');
|
|
901
|
+
parts.push('');
|
|
902
|
+
parts.push(macroDigest);
|
|
903
|
+
parts.push('');
|
|
904
|
+
}
|
|
905
|
+
if (castDigest) {
|
|
906
|
+
pushHeading(parts, '本章实体档案(只取本章相关)');
|
|
907
|
+
parts.push('');
|
|
908
|
+
parts.push(castDigest);
|
|
909
|
+
parts.push('');
|
|
910
|
+
}
|
|
911
|
+
const digest = outlineContext(projectRoot, chapter, nextSection(parts));
|
|
912
|
+
if (digest) parts.push(digest);
|
|
913
|
+
parts.push(rhythmFormulaBlock(projectRoot, nextSection(parts)));
|
|
914
|
+
parts.push(referenceDecisionBlock(projectRoot, '40-rhythm', {
|
|
915
|
+
number: nextSection(parts),
|
|
916
|
+
batchType: '章节细纲',
|
|
917
|
+
batchName: '细纲 ' + id,
|
|
918
|
+
allowedFileText: '除本章细纲外,`设定/.reference-decisions.md` 是本轮唯一允许写的额外文件;不要修改章节地图、其它细纲或上游文件。',
|
|
919
|
+
}));
|
|
920
|
+
pushHeading(parts, '执行步骤');
|
|
921
|
+
parts.push('');
|
|
922
|
+
parts.push('1. 先读 `references/节奏指导/` 的方法论和 `对标/wiki/` 的本批参考;摘要只用于定位,做决策前核对原文对应条目。');
|
|
923
|
+
parts.push('2. 把本章章节地图当作硬约束,确认 pov、cast、locations、why、how 和 word_target。');
|
|
924
|
+
parts.push('3. 结合上游宏观与实体档案,把 `why` 展开成人物动机、阻力和赌注。');
|
|
925
|
+
parts.push('4. 把 `how` 拆成可执行的场景节拍;每个场景必须有目标、阻碍、转折、结果。');
|
|
926
|
+
parts.push('5. 检查 coverage 是否覆盖本章的主线、支线、冲突、伏笔、爽点、信息差。');
|
|
927
|
+
parts.push('6. 写完后检查 continuity 是否能接上上一章、下一章。');
|
|
928
|
+
parts.push('7. 对照章节地图的 `knowledge_events`,把本章每个人物“知道了什么、怎么知道、在什么场景知道”写进 `knowledge_events` 和 `cast[].knowledge`;状态必须和章节地图回放结果一致。');
|
|
929
|
+
parts.push('8. 每个会进入细纲的设计决策,按“参考决策记录”的格式向 `设定/.reference-decisions.md` 追加一行(本章至少一行)。');
|
|
930
|
+
parts.push('');
|
|
931
|
+
parts.push('不要发明章节地图没有的剧情节点;不要引用不存在的实体。');
|
|
932
|
+
parts.push('');
|
|
933
|
+
parts.push(forbiddenBlock('40-rhythm', [
|
|
934
|
+
'不要写正文,不要改章节地图,不要改上游文件;`设定/.reference-decisions.md` 是唯一例外。',
|
|
935
|
+
], nextSection(parts)));
|
|
936
|
+
parts.push(validationBlock('40-rhythm', { validator: '40-rhythm 的单章细纲校验', number: nextSection(parts) }));
|
|
937
|
+
parts.push(failureBlock(failures));
|
|
938
|
+
pushHeading(parts, '输出格式');
|
|
939
|
+
parts.push('');
|
|
940
|
+
parts.push('顶层:`{ "_meta": { "chapter": ... }, "chapter": { ... } }`。');
|
|
941
|
+
parts.push('chapter 至少包含:chapter_id、number、title、volume、act、word_target、pov、time、locations、cast、why、how、knowledge_events、continuity、coverage、hard_constraints、soft_suggestions。');
|
|
942
|
+
parts.push('how.scenes 至少 1 个,每个场景写 goal、obstacle、turn、outcome。');
|
|
943
|
+
parts.push('hard_constraints 写 must_write、must_not_write、foreshadow_actions、payoff_actions、information_state。');
|
|
944
|
+
parts.push('soft_suggestions 写 scene_split、emotion_trend、description_focus、pace,允许写作时调整。');
|
|
945
|
+
parts.push('coverage 必须覆盖本章的主线节点、支线、冲突、伏笔、爽点、信息差。');
|
|
946
|
+
parts.push('knowledge_events 与章节地图同结构;cast[].knowledge 写本章结束时该角色的知识快照(subject_type、subject_id、state、coverage、knowledge、how、scene、evidence)。');
|
|
947
|
+
parts.push('知识事件取值映射(校验器强制):learns → partial|full;partially_learns → partial;confirms → full;corrects → partial|full;forgets → unknown;misinforms → false;how=told 必须写 source_character。');
|
|
948
|
+
parts.push('continuity 写 from_prev 与 to_next。');
|
|
949
|
+
parts.push('参考决策按表格追加到 `设定/.reference-decisions.md`,不要覆盖已有行。');
|
|
950
|
+
parts.push('');
|
|
951
|
+
return parts.join('\n');
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
export function buildWritingPrompt(opts) {
|
|
955
|
+
const { projectRoot, chapters, outlines, dossiers, prevTail, ledger, attempt = 1, failures = [] } = opts || {};
|
|
956
|
+
const contract = contractFor('50-writing');
|
|
957
|
+
const writingSection = asArray(contract && contract.sections).find((s) => s.id === 'writing');
|
|
958
|
+
const ledgerSection = asArray(contract && contract.sections).find((s) => s.id === 'ledger');
|
|
959
|
+
const parts = [];
|
|
960
|
+
parts.push(roleBlock(
|
|
961
|
+
'网文项目的正文写作者(50-writing)',
|
|
962
|
+
[
|
|
963
|
+
'你只按单章细纲写正文,不重新设计剧情。',
|
|
964
|
+
'本轮要写:' + asArray(chapters).join('、') + '。',
|
|
965
|
+
],
|
|
966
|
+
));
|
|
967
|
+
parts.push(fileTableBlock([
|
|
968
|
+
['设定/50-writing/', '当前阶段目录', '已写正文和章台账。'],
|
|
969
|
+
['设定/50-writing/_context/', '上游快照', 'scale / macro / cast 的只读快照。'],
|
|
970
|
+
['设定/40-rhythm/outlines/', '细纲目录', '每章的可落地细纲。'],
|
|
971
|
+
['模板/v2/50-writing/ledger-template.json', '结构模板', '章台账的字段结构。'],
|
|
972
|
+
['设定/50-writing/chapters/', '正文输出目录', '你本轮要写的正文。'],
|
|
973
|
+
['设定/50-writing/ledger.json', '台账输出文件', '你本轮要更新的章台账。'],
|
|
974
|
+
]));
|
|
975
|
+
parts.push(topicBlock(projectTopic(projectRoot)));
|
|
976
|
+
pushHeading(parts, '任务');
|
|
977
|
+
parts.push('');
|
|
978
|
+
parts.push('按细纲写正文,并更新章台账。');
|
|
979
|
+
parts.push('');
|
|
980
|
+
parts.push(inputBlock([
|
|
981
|
+
'设定/50-writing/',
|
|
982
|
+
'设定/50-writing/_context/',
|
|
983
|
+
'设定/40-rhythm/outlines/',
|
|
984
|
+
'模板/v2/50-writing/ledger-template.json',
|
|
985
|
+
...referenceInputPaths(projectRoot, '50-writing', { batchType: 'default' }),
|
|
986
|
+
], { template: '模板/v2/50-writing/ledger-template.json' }));
|
|
987
|
+
if (writingSection) parts.push(sectionBlock(writingSection, nextSection(parts)));
|
|
988
|
+
if (ledgerSection) parts.push(sectionBlock(ledgerSection, nextSection(parts)));
|
|
989
|
+
pushHeading(parts, '本轮细纲');
|
|
990
|
+
parts.push('');
|
|
991
|
+
for (const ch of asArray(chapters)) {
|
|
992
|
+
const outline = outlines && outlines.get(ch);
|
|
993
|
+
if (!outline) continue;
|
|
994
|
+
parts.push('### 第 ' + ch + ' 章细纲(硬约束)');
|
|
995
|
+
parts.push('');
|
|
996
|
+
parts.push('```json');
|
|
997
|
+
parts.push(JSON.stringify(outline, null, 2));
|
|
998
|
+
parts.push('```');
|
|
999
|
+
parts.push('');
|
|
1000
|
+
}
|
|
1001
|
+
if (dossiers) {
|
|
1002
|
+
pushHeading(parts, '本章人物档案');
|
|
1003
|
+
parts.push('');
|
|
1004
|
+
parts.push(dossiers);
|
|
1005
|
+
parts.push('');
|
|
1006
|
+
}
|
|
1007
|
+
if (prevTail) {
|
|
1008
|
+
pushHeading(parts, '前情衔接');
|
|
1009
|
+
parts.push('');
|
|
1010
|
+
parts.push('```');
|
|
1011
|
+
parts.push(prevTail);
|
|
1012
|
+
parts.push('```');
|
|
1013
|
+
parts.push('');
|
|
1014
|
+
}
|
|
1015
|
+
if (ledger) {
|
|
1016
|
+
pushHeading(parts, '当前台账快照');
|
|
1017
|
+
parts.push('');
|
|
1018
|
+
parts.push(JSON.stringify(ledger, null, 2));
|
|
1019
|
+
parts.push('');
|
|
1020
|
+
}
|
|
1021
|
+
parts.push(referenceDecisionBlock(projectRoot, '50-writing', {
|
|
1022
|
+
number: nextSection(parts),
|
|
1023
|
+
batchType: 'default',
|
|
1024
|
+
batchName: '正文 ' + asArray(chapters).join('、'),
|
|
1025
|
+
allowedFileText: '除本轮正文与 `ledger.json` 外,`设定/.reference-decisions.md` 是本轮唯一允许写的额外文件;不要修改细纲、章节地图或上游文件。',
|
|
1026
|
+
}));
|
|
1027
|
+
pushHeading(parts, '执行步骤');
|
|
1028
|
+
parts.push('');
|
|
1029
|
+
parts.push('1. 先读 `references/写作指导/` 的方法论和 `对标/wiki/` 的本批参考;摘要只用于定位,写作前核对原文对应条目。');
|
|
1030
|
+
parts.push('2. 逐章读细纲,先确认本章 must_write、must_not_write、ending 和 word_target。');
|
|
1031
|
+
parts.push('3. 按 how.scenes 的顺序写,不要把场景写成摘要。');
|
|
1032
|
+
parts.push('4. 写完后检查人物、地点、时间、知识状态是否和细纲一致。');
|
|
1033
|
+
parts.push('5. 每章至少向 `设定/.reference-decisions.md` 追加一行,写清参考文件、原文条目和实际采用的写法。');
|
|
1034
|
+
parts.push('6. 更新 ledger.json:每章一条,重写时替换同章旧条目。');
|
|
1035
|
+
parts.push('');
|
|
1036
|
+
parts.push(forbiddenBlock('50-writing', [], nextSection(parts)));
|
|
1037
|
+
parts.push(validationBlock('50-writing', { validator: '50-writing 的正文与台账校验', number: nextSection(parts) }));
|
|
1038
|
+
parts.push(failureBlock(failures));
|
|
1039
|
+
pushHeading(parts, '输出格式');
|
|
1040
|
+
parts.push('');
|
|
1041
|
+
for (const ch of asArray(chapters)) {
|
|
1042
|
+
parts.push('- 第 ' + ch + ' 章 → `设定/50-writing/chapters/第' + String(ch).padStart(3, '0') + '章.md`');
|
|
1043
|
+
}
|
|
1044
|
+
parts.push('- 台账 → `设定/50-writing/ledger.json`');
|
|
1045
|
+
parts.push('- `ledger.knowledge_changes` 用结构化对象:subject_type、subject_id、character、from、to、coverage、knowledge、how、source_character、scene、evidence;不要只写 gained/lost 字符串。');
|
|
1046
|
+
parts.push('');
|
|
1047
|
+
parts.push('每章第一行 `# 第N章 标题`,N 与 display_no 一致。');
|
|
1048
|
+
parts.push('必须写细纲 must_write 的内容,绝对不能违反 must_not_write。');
|
|
1049
|
+
parts.push('本章结尾必须按细纲 ending.type / text / hook 收束。');
|
|
1050
|
+
parts.push('参考决策按表格追加到 `设定/.reference-decisions.md`,不要覆盖已有行。');
|
|
1051
|
+
parts.push('');
|
|
1052
|
+
return parts.join('\n');
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
export function buildOutlineRepairPrompt(opts) {
|
|
1056
|
+
return buildOutlinePrompt(opts);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* 10-scale 的 LLM 选值提示词。
|
|
1061
|
+
*
|
|
1062
|
+
* 公式只给区间;LLM 结合题材 profile、章节字数基准和参考骨架选择最终数量。
|
|
1063
|
+
* 插件随后用 validateScale 校验结构、求和和区间。
|
|
1064
|
+
*/
|
|
1065
|
+
export function buildScalePrompt(opts) {
|
|
1066
|
+
const { projectRoot, topicData, attempt = 1, failures = [] } = opts || {};
|
|
1067
|
+
const T = Number(topicData && topicData.target_chapters) || 0;
|
|
1068
|
+
const words = Number(topicData && topicData.target_words) || 0;
|
|
1069
|
+
const avgWords = T > 0 ? Math.round(words / T) : 0;
|
|
1070
|
+
const chapterScale = avgWords > 0 ? Number((2000 / avgWords).toFixed(4)) : null;
|
|
1071
|
+
const parts = [];
|
|
1072
|
+
parts.push(roleBlock(
|
|
1073
|
+
'网文项目的数量规划师(10-scale)',
|
|
1074
|
+
[
|
|
1075
|
+
'你只决定“多少”:章、卷、幕、各类实体配额和节奏预算。你不写人物、势力、地点和剧情。',
|
|
1076
|
+
'这一阶段的产物会决定下游 20-macro / 30-cast / 40-rhythm 要生成多少内容,所以数字必须能落地。',
|
|
1077
|
+
],
|
|
1078
|
+
));
|
|
1079
|
+
parts.push(fileTableBlock([
|
|
1080
|
+
['references/genre-profiles.json', '题材配置文件', '各类参考题材的倍率、结构、平台基准和题材匹配规则。'],
|
|
1081
|
+
['references/formulas-skeleton.json', '公式文件', '基础公式、变量、倍率分类、覆盖规则和平台规则。'],
|
|
1082
|
+
['模板/v2/10-scale/scale-template.json', '输出模板', 'scale.json 的字段结构。'],
|
|
1083
|
+
['设定/00-baseline/input.json', '选题基线', '已经固化的选题字段;提示词里的“选题”是本次决策用的完整选题。'],
|
|
1084
|
+
['设定/10-scale/scale.json', '输出文件', '你本轮要写的最终数量表。'],
|
|
1085
|
+
]));
|
|
1086
|
+
parts.push(topicBlock(topicData));
|
|
1087
|
+
parts.push(h2(4, '任务'));
|
|
1088
|
+
parts.push('');
|
|
1089
|
+
parts.push('写文件:`设定/10-scale/scale.json`。');
|
|
1090
|
+
parts.push('先把 `references/genre-profiles.json`(题材配置文件)里最接近选题的题材选出来,再按 `references/formulas-skeleton.json`(公式文件)计算,最后把选择的题材 key 和所有最终值写进目标文件。');
|
|
1091
|
+
parts.push('你只写文件,不要在聊天里输出完整 JSON;写完后只汇报文件路径、题材 key 和条目数量。');
|
|
1092
|
+
parts.push('插件会在写完后自动运行 `validateScale`;如果失败,下一轮会给错误列表,你按 `path` 修改目标文件,不需要自己运行脚本。');
|
|
1093
|
+
parts.push('');
|
|
1094
|
+
parts.push(inputBlock([
|
|
1095
|
+
'设定/10-scale/',
|
|
1096
|
+
'设定/00-baseline/',
|
|
1097
|
+
'模板/v2/10-scale/scale-template.json',
|
|
1098
|
+
'references/genre-profiles.json',
|
|
1099
|
+
'references/formulas-skeleton.json',
|
|
1100
|
+
...referenceInputPaths(projectRoot, '10-scale', { batchType: '' }),
|
|
1101
|
+
], { template: '模板/v2/10-scale/scale-template.json' }));
|
|
1102
|
+
|
|
1103
|
+
parts.push(h2(6, '执行流程(按顺序做,每步都有检查)'));
|
|
1104
|
+
parts.push('');
|
|
1105
|
+
parts.push('0. 先读 `references/骨架指导/` 的方法论和 `对标/wiki/` 的本批参考;摘要只用于定位,做数量决策前核对原文对应条目。');
|
|
1106
|
+
parts.push('1. 读 `## 三、选题`,锁定 `target_chapters` 和 `target_words`。检查:两个都是正整数;这两个数来自选题,任何情况下都不允许改。总章数就是公式变量 T。');
|
|
1107
|
+
parts.push('2. 读 `references/genre-profiles.json`,按 `## 七、题材匹配` 选一个 profile。检查:profile key 必须存在于文件的 `profiles` 里;没有明显匹配就用 `default`;记下这个 key。');
|
|
1108
|
+
parts.push('3. 读 `references/formulas-skeleton.json`,按 `## 八、公式读取与计算` 的 5 步算出每条公式的最终区间。检查:每条公式都使用它自己的 `multiplier_category` 和 `scale_field`,不要自造字段。');
|
|
1109
|
+
parts.push('4. 决定卷/幕结构。检查:各卷 `chapters` 之和 = T;卷 `chapter_start` / `chapter_end` 连续覆盖 1..T;每卷 `acts` 的章数之和 = 本卷章数;`structure.acts` 包含全部卷幕;`total_acts` = `structure.acts` 的长度;各卷 `ratio` 之和 = 1。');
|
|
1110
|
+
parts.push('5. 决定 `quotas`。检查:所有配额是非负整数;只写 `scale_field` 指向的字段;每个值都落在“公式 × 题材倍率”后的区间内;偏离区间时写 `derivation_reasons`。');
|
|
1111
|
+
parts.push('6. 决定 `distribution`。检查:只拆 `chapters`、`volume_supporting`、`volume_antagonist`、`phase_conflicts`、`foreshadows`、`climaxes`、`pleasures`;每卷 `chapters` 必须等于该卷章数;不要拆核心人物、核心反派、核心冲突、地点、道具、世界规则、结局、主线、支线、信息差、关系。');
|
|
1112
|
+
parts.push('7. 决定 `rhythm_budget`。检查:钩子、揭示、反转、情绪峰值都有 `min` 和 `target`;`golden_three` 有 `hook_strength` 和 `must_establish`。');
|
|
1113
|
+
parts.push('8. 写 `设定/10-scale/scale.json`。检查:顶层字段齐全;文件里是纯 JSON,没有 markdown 围栏;`genre_profile` 写的是目标文件的顶层字段。');
|
|
1114
|
+
parts.push('9. 等插件校验。失败时按文末“附:校验与返工”逐条修改,不要改上游文件。');
|
|
1115
|
+
parts.push('');
|
|
1116
|
+
|
|
1117
|
+
parts.push(h2(7, '题材匹配'));
|
|
1118
|
+
parts.push('');
|
|
1119
|
+
parts.push('### 7.1 匹配规则');
|
|
1120
|
+
parts.push('');
|
|
1121
|
+
parts.push('读取 `references/genre-profiles.json`。这个文件是题材匹配和倍率的唯一来源,提示词里不再重复它的内容。');
|
|
1122
|
+
parts.push('');
|
|
1123
|
+
parts.push(...ordered([
|
|
1124
|
+
'`platform`:平台通用基准。只有当选题的 `platform` 字段和文件里的平台名一致时,才直接使用这些数值;不一致时只作参考。',
|
|
1125
|
+
'`profiles`:每个 key 是一个题材配置。用 `label`、`aliases`、`channel`、`category`、`structure_feature` 判断它覆盖什么题材。',
|
|
1126
|
+
'`profiles.<key>.special_rules`:该题材的额外节奏和结构要求,选值时必须参考。',
|
|
1127
|
+
'`profiles.<key>.overrides`:该题材对个别公式区间的覆盖。',
|
|
1128
|
+
'`base_overrides`:对所有题材都生效的公式区间覆盖。',
|
|
1129
|
+
'`profile_match_rules`:匹配顺序和默认值规则。',
|
|
1130
|
+
'`platform_checks`:平台对爽文、情感、悬疑、种田等类型的额外校验提醒;同样只在平台匹配时直接使用。',
|
|
1131
|
+
'`default`:选题和所有题材都不接近时的默认配置。',
|
|
1132
|
+
]));
|
|
1133
|
+
parts.push('');
|
|
1134
|
+
parts.push('匹配顺序:');
|
|
1135
|
+
parts.push('');
|
|
1136
|
+
parts.push(...ordered([
|
|
1137
|
+
'先看选题 JSON 的 `genre` 字段(它就是本书题材),看是否能命中某个 profile 的 `category` 或 `aliases`;',
|
|
1138
|
+
'再看 `positioning` / `core_setting` / `reader_value` 里是否出现 profile 的 `aliases` 关键词;',
|
|
1139
|
+
'仍然没有明显匹配时,使用 `default`,不要硬套一个不相关的题材。',
|
|
1140
|
+
]));
|
|
1141
|
+
parts.push('');
|
|
1142
|
+
parts.push('把最终选择的 profile key 写进**你要生成的目标文件** `设定/10-scale/scale.json` 的顶层字段 `genre_profile`。');
|
|
1143
|
+
parts.push('不是写进 `references/genre-profiles.json`,也不是修改模板;`模板/v2/10-scale/scale-template.json` 里的 `genre_profile` 只是字段示例。');
|
|
1144
|
+
parts.push('不要自己发明 key,也不要选一个文件里不存在的题材。');
|
|
1145
|
+
parts.push('');
|
|
1146
|
+
parts.push('### 7.2 选值原则');
|
|
1147
|
+
parts.push('');
|
|
1148
|
+
parts.push(...ordered([
|
|
1149
|
+
'卷数:优先让每卷接近 profile 的 `structure.preferred_chapters_per_volume`;经营基建类偏中长卷,单元剧/悬疑类偏短卷,升级流可以偏长。',
|
|
1150
|
+
'配额:默认取区间中值,再按题材特征上下浮动;不要全部取上界,也不要全部取下界。',
|
|
1151
|
+
'distribution:按 profile 的 `distribution_weights` 和题材节奏分配,不要平均分;前段多埋伏笔,后段多回收,高潮和爽点向卷末集中。',
|
|
1152
|
+
]));
|
|
1153
|
+
parts.push('');
|
|
1154
|
+
|
|
1155
|
+
parts.push(h2(8, '公式读取与计算'));
|
|
1156
|
+
parts.push('');
|
|
1157
|
+
parts.push('读取 `references/formulas-skeleton.json`。公式和倍率都在文件里,提示词不再给出公式区间或推荐值。');
|
|
1158
|
+
parts.push('');
|
|
1159
|
+
parts.push('### 8.1 强制计算顺序');
|
|
1160
|
+
parts.push('');
|
|
1161
|
+
parts.push('每条公式都必须严格按下面 5 步计算,不能跳步,也不能先乘倍率再覆盖:');
|
|
1162
|
+
parts.push('');
|
|
1163
|
+
parts.push('1. **算基础区间**:用 `expr_low` / `expr_high` 和当前 T、V、BC、CC 等变量算出基础区间。');
|
|
1164
|
+
parts.push('2. **应用全局覆盖**:如果 `base_overrides` 里有这条公式,用覆盖值替换第 1 步的基础区间。');
|
|
1165
|
+
parts.push('3. **应用题材覆盖**:如果当前 profile 的 `overrides` 里有这条公式,用覆盖值替换第 2 步的区间。');
|
|
1166
|
+
parts.push('4. **乘题材倍率**:取这条公式的 `multiplier_category`,去当前 profile 的 `multipliers` 里找同名倍率相乘;没有分类或倍率缺失时按 1 处理。');
|
|
1167
|
+
parts.push('5. **取整**:下界向上取整(`ceil`),上界向下取整(`floor`);如果取整后上界小于下界,把上界改成和下界相等。取整只会让区间变窄,不会变宽。');
|
|
1168
|
+
parts.push('');
|
|
1169
|
+
parts.push('### 8.2 计算依赖顺序');
|
|
1170
|
+
parts.push('');
|
|
1171
|
+
parts.push(...ordered([
|
|
1172
|
+
'先算 `structure.volume_count`,因为它决定 V;卷数优先看 profile 的 `structure.volume_chapters` / `preferred_chapters_per_volume`,不要直接套 `expr_low` / `expr_high`。',
|
|
1173
|
+
'再算只依赖 T / V 的公式:支线数、卷级配角/反派、阶段冲突、势力、地点、伏笔、高潮、爽点、世界规则、结局、固定人物。',
|
|
1174
|
+
'再算依赖前面配额的公式:势力关系依赖势力数,交织冲突依赖支线数,核心道具依赖核心冲突数。',
|
|
1175
|
+
'最后算 `quotas.information_gaps` 和 `quotas.relations.character` 这两条自定义公式;它们在 `references/formulas-skeleton.json` 的 `custom_formulas` 里,不在 `formulas` 里。',
|
|
1176
|
+
]));
|
|
1177
|
+
parts.push('');
|
|
1178
|
+
parts.push('### 8.3 计算示例');
|
|
1179
|
+
parts.push('');
|
|
1180
|
+
parts.push('下面只演示顺序,数字不是本书的最终答案:');
|
|
1181
|
+
parts.push('');
|
|
1182
|
+
parts.push('```text');
|
|
1183
|
+
parts.push('公式:小伏笔 V*3 ~ V*8,V=6');
|
|
1184
|
+
parts.push('1. 基础区间:18 ~ 48');
|
|
1185
|
+
parts.push('2. base_overrides 覆盖为 V*8 ~ V*15:48 ~ 90');
|
|
1186
|
+
parts.push('3. profile.overrides 覆盖为 50 ~ 100:50 ~ 100');
|
|
1187
|
+
parts.push('4. multipliers.foreshadows = 1.1:55 ~ 110');
|
|
1188
|
+
parts.push('5. 取整:55 ~ 110');
|
|
1189
|
+
parts.push('```');
|
|
1190
|
+
parts.push('');
|
|
1191
|
+
parts.push('如果你先乘倍率再覆盖,得到的结果会落在错误区间,`validateScale` 会报 `SCALE_FORMULA_RANGE`。');
|
|
1192
|
+
parts.push('');
|
|
1193
|
+
parts.push('### 8.4 超出区间时怎么处理');
|
|
1194
|
+
parts.push('');
|
|
1195
|
+
parts.push('如果某个值确实要超出乘倍率后的合法区间,必须在 `derivation_reasons` 里写理由。');
|
|
1196
|
+
parts.push('有理由时,这条 `SCALE_FORMULA_RANGE` 只算 **warning**,阶段仍可通过;没有理由时才是 **error**,阶段不通过。');
|
|
1197
|
+
parts.push('`derivation_reasons` 的 key 可以写公式 key(如 `custom.information_gaps`),也可以写目标字段路径(如 `quotas.information_gaps`)。');
|
|
1198
|
+
parts.push('');
|
|
1199
|
+
|
|
1200
|
+
parts.push(h2(9, '字段说明'));
|
|
1201
|
+
parts.push('');
|
|
1202
|
+
parts.push('### 9.1 顶层字段');
|
|
1203
|
+
parts.push('');
|
|
1204
|
+
parts.push('`scale.json` 只写下面这些顶层字段:');
|
|
1205
|
+
parts.push('');
|
|
1206
|
+
parts.push(...ordered([
|
|
1207
|
+
'`formula_version`:写 `references/genre-profiles.json` 的 `version`。',
|
|
1208
|
+
'`genre_profile`:你从题材配置文件选中的 profile key。',
|
|
1209
|
+
'`capacity`:规模参数。`target_chapters` / `target_words` 来自选题,不能改;`avg_words_per_chapter` 是平均每章字数;`base_words_per_chapter` 是节奏公式基准,默认 2000;`chapter_scale` 是 2000 / 平均每章字数。',
|
|
1210
|
+
'`structure`:卷和幕的结构。`volumes` 里每卷写 chapters、chapter_start、chapter_end、ratio 和 acts;`structure.acts` 是全部卷幕的扁平表。',
|
|
1211
|
+
'`quotas`:下游要生成多少实体;字段清单见 9.2。',
|
|
1212
|
+
'`rhythm_budget`:全书的钩子、揭示、反转、情绪峰值预算,以及黄金三章要求。',
|
|
1213
|
+
'`distribution`:按卷拆分的配额;字段清单见 9.3。',
|
|
1214
|
+
'`derivation_reasons`:可选;只写偏离公式推荐值的原因。',
|
|
1215
|
+
]));
|
|
1216
|
+
parts.push('不要写 `formulas`、`derivations`、`_meta`;这三个由插件补。');
|
|
1217
|
+
parts.push('');
|
|
1218
|
+
parts.push('### 9.2 quotas 必填字段');
|
|
1219
|
+
parts.push('');
|
|
1220
|
+
parts.push('只写下面这些路径,不要写 `faction_count`、`branch_count`、`ending_count` 之类的别名字段:');
|
|
1221
|
+
parts.push('');
|
|
1222
|
+
parts.push('```text');
|
|
1223
|
+
parts.push('quotas.characters: protagonist, core_supporting, volume_supporting, core_antagonist, volume_antagonist');
|
|
1224
|
+
parts.push('quotas.factions');
|
|
1225
|
+
parts.push('quotas.faction_relations');
|
|
1226
|
+
parts.push('quotas.locations');
|
|
1227
|
+
parts.push('quotas.items: core, branch, symbolic');
|
|
1228
|
+
parts.push('quotas.conflicts: core, phase, branch');
|
|
1229
|
+
parts.push('quotas.foreshadows: small, medium, large');
|
|
1230
|
+
parts.push('quotas.climaxes: small, medium, large');
|
|
1231
|
+
parts.push('quotas.pleasures: small, medium, large');
|
|
1232
|
+
parts.push('quotas.endings');
|
|
1233
|
+
parts.push('quotas.world_rules: core, information, social');
|
|
1234
|
+
parts.push('quotas.main_lines');
|
|
1235
|
+
parts.push('quotas.branches');
|
|
1236
|
+
parts.push('quotas.information_gaps');
|
|
1237
|
+
parts.push('quotas.relations: character, faction, membership');
|
|
1238
|
+
parts.push('```');
|
|
1239
|
+
parts.push('');
|
|
1240
|
+
parts.push('### 9.3 distribution 只拆这些');
|
|
1241
|
+
parts.push('');
|
|
1242
|
+
parts.push('每卷只写:');
|
|
1243
|
+
parts.push('');
|
|
1244
|
+
parts.push('```text');
|
|
1245
|
+
parts.push('chapters, volume_supporting, volume_antagonist, phase_conflicts, foreshadows, climaxes, pleasures');
|
|
1246
|
+
parts.push('```');
|
|
1247
|
+
parts.push('不要把核心人物、核心反派、核心冲突、地点、道具、世界规则、结局、主线、支线、信息差、关系拆到卷里;这些是全书级配额。');
|
|
1248
|
+
parts.push('');
|
|
1249
|
+
parts.push('### 9.4 structure.acts 和 total_acts');
|
|
1250
|
+
parts.push('');
|
|
1251
|
+
parts.push('1. 每卷的 `volumes[i].acts` 写本卷的幕。');
|
|
1252
|
+
parts.push('2. 把全部卷的幕按顺序复制到 `structure.acts`,形成扁平表。');
|
|
1253
|
+
parts.push('3. `structure.total_acts` 必须等于 `structure.acts` 数组长度。');
|
|
1254
|
+
parts.push('');
|
|
1255
|
+
parts.push('### 9.5 术语统一');
|
|
1256
|
+
parts.push('');
|
|
1257
|
+
parts.push('本文档统一用词:配额 = `quotas`,幕 = `acts`,倍率 = `multipliers`,覆盖 = `overrides`。');
|
|
1258
|
+
parts.push('');
|
|
1259
|
+
|
|
1260
|
+
parts.push(h2(10, '硬约束'));
|
|
1261
|
+
parts.push('');
|
|
1262
|
+
parts.push('1. 各卷 `chapters` 之和 = `capacity.target_chapters`;');
|
|
1263
|
+
parts.push('2. 卷 `chapter_start` / `chapter_end` 连续覆盖 1..T;');
|
|
1264
|
+
parts.push('3. 每卷 `acts` 至少 1 幕,幕章数之和 = 本卷章数;');
|
|
1265
|
+
parts.push('4. `structure.acts` 包含所有卷的全部幕,`total_acts` 等于它的长度;');
|
|
1266
|
+
parts.push('5. 各卷 `ratio` 之和 = 1;');
|
|
1267
|
+
parts.push('6. 所有配额是非负整数;');
|
|
1268
|
+
parts.push('7. `distribution` 的 `chapters` 必须等于对应卷的 `chapters`;');
|
|
1269
|
+
parts.push('8. 不写任何人物名、势力名、地点名、道具名、剧情内容或章节标题。');
|
|
1270
|
+
parts.push('');
|
|
1271
|
+
|
|
1272
|
+
parts.push(platformNoticeBlock(topicData, 11));
|
|
1273
|
+
parts.push(h2(12, '章节字数基准'));
|
|
1274
|
+
parts.push('');
|
|
1275
|
+
parts.push('本书目标 `' + T + '` 章、`' + words + '` 字,平均每章约 `' + avgWords + '` 字,`chapter_scale = 2000 / avg = ' + (chapterScale ?? '') + '`。');
|
|
1276
|
+
parts.push('');
|
|
1277
|
+
parts.push('`chapter_scale` 只用于“时长类”参考:');
|
|
1278
|
+
parts.push('');
|
|
1279
|
+
parts.push(...ordered([
|
|
1280
|
+
'需要乘:桥段时长、高潮间隔、连续压制/反转周期、伏笔回收周期、卷长参考。',
|
|
1281
|
+
'不需要乘:总章数、总字数、配额数量、钩子/揭示/爽点总数或密度。',
|
|
1282
|
+
]));
|
|
1283
|
+
parts.push('');
|
|
1284
|
+
|
|
1285
|
+
parts.push(forbiddenBlock('10-scale', [], 13));
|
|
1286
|
+
parts.push(h2(14, '输出前检查清单'));
|
|
1287
|
+
parts.push('');
|
|
1288
|
+
parts.push(...ordered([
|
|
1289
|
+
'顶层字段齐全:formula_version、genre_profile、capacity、structure、quotas、rhythm_budget、distribution、derivation_reasons。',
|
|
1290
|
+
'`genre_profile` 是 `references/genre-profiles.json` 里真实存在的 key。',
|
|
1291
|
+
'`capacity.target_chapters` / `target_words` 没有被改;`avg_words_per_chapter` 和 `chapter_scale` 计算正确。',
|
|
1292
|
+
'各卷 chapters 之和 = T;章节区间连续;每卷幕章数之和 = 本卷章数;`ratio` 之和 = 1。',
|
|
1293
|
+
'`structure.acts` 是全部卷幕的扁平表,`total_acts` 等于它的长度。',
|
|
1294
|
+
'`quotas` 只写第九节列出的字段,全部是非负整数,并且落在公式 × 题材倍率后的区间内。',
|
|
1295
|
+
'`distribution` 只拆允许的字段,每卷 chapters 等于该卷章数。',
|
|
1296
|
+
'`rhythm_budget` 的钩子、揭示、反转、情绪峰值和黄金三章字段齐全。',
|
|
1297
|
+
'没有人物姓名、势力名称、地点名称、道具名称、剧情句、章节标题。',
|
|
1298
|
+
'文件内容是纯 JSON,没有 markdown 代码围栏。',
|
|
1299
|
+
]));
|
|
1300
|
+
parts.push('');
|
|
1301
|
+
|
|
1302
|
+
parts.push(referenceDecisionBlock(projectRoot, '10-scale', {
|
|
1303
|
+
batchType: '',
|
|
1304
|
+
batchName: 'scale',
|
|
1305
|
+
allowedFileText: '除 `scale.json` 外,`设定/.reference-decisions.md` 是本轮唯一允许写的额外文件;不要修改题材配置、公式或模板。',
|
|
1306
|
+
}));
|
|
1307
|
+
parts.push(failureBlock(failures));
|
|
1308
|
+
parts.push(h2(15, '输出格式'));
|
|
1309
|
+
parts.push('');
|
|
1310
|
+
parts.push('只写 `设定/10-scale/scale.json`。文件内容必须是一个 JSON 对象,除此之外不要写任何东西。');
|
|
1311
|
+
parts.push('不要在文件里加 ```json 围栏,也不要在聊天里重复完整 JSON;写完后只汇报文件路径、题材 key 和条目数量。');
|
|
1312
|
+
parts.push('`formula_version` 写 `references/genre-profiles.json` 的 `version`;`genre_profile` 是目标文件的顶层字段。');
|
|
1313
|
+
parts.push('每个会进入 `scale.json` 的数量决策(profile 选择、卷/幕结构、各配额、节奏预算),按“参考决策记录”的格式向 `设定/.reference-decisions.md` 追加一行。');
|
|
1314
|
+
parts.push('');
|
|
1315
|
+
parts.push('`derivation_reasons` 可选,格式示例:');
|
|
1316
|
+
parts.push('');
|
|
1317
|
+
parts.push('```json');
|
|
1318
|
+
parts.push('{ "structure.volume_count": "悬疑单元案,5 卷比 3 卷更适合每卷闭环", "quotas.information_gaps": "悬疑需要更高信息差密度" }');
|
|
1319
|
+
parts.push('```');
|
|
1320
|
+
parts.push('');
|
|
1321
|
+
parts.push(validationBlock('10-scale', {
|
|
1322
|
+
appendix: true,
|
|
1323
|
+
validator: 'validateScale',
|
|
1324
|
+
checks: [
|
|
1325
|
+
'genre_profile 必须存在于 references/genre-profiles.json',
|
|
1326
|
+
'各卷 chapters 之和 = capacity.target_chapters',
|
|
1327
|
+
'各卷 acts 的章数之和 = 本卷 chapters',
|
|
1328
|
+
'structure.acts 包含全部卷幕,total_acts 等于它的长度',
|
|
1329
|
+
'各卷 ratio 之和 = 1',
|
|
1330
|
+
'所有配额是非负整数,且落在公式 × 题材倍率后的区间内',
|
|
1331
|
+
'没有人物/势力/地点/剧情内容',
|
|
1332
|
+
],
|
|
1333
|
+
}));
|
|
1334
|
+
return parts.join('\n');
|
|
1335
|
+
}
|