@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
|
@@ -183,7 +183,10 @@ export const RELATION_KIND_META = {
|
|
|
183
183
|
'对立': { direction: 'symmetric', group: 'attitude' },
|
|
184
184
|
'敌对': { direction: 'symmetric', group: 'attitude' },
|
|
185
185
|
'竞争': { direction: 'symmetric', group: 'attitude' },
|
|
186
|
-
'中立': { direction: 'symmetric', group: 'attitude' }
|
|
186
|
+
'中立': { direction: 'symmetric', group: 'attitude' },
|
|
187
|
+
// 扩展出口:具体语义由 relation_label 说明(附身/寄生/契约/共生…)。
|
|
188
|
+
// 单列为 other 维度,避免和既有立场关系互斥。
|
|
189
|
+
'其他': { direction: 'directed', group: 'other' }
|
|
187
190
|
};
|
|
188
191
|
|
|
189
192
|
// 有向关系:同一对角色上双向互指同一个值即自相矛盾(互相服从/互相提携/互为师徒)
|
|
@@ -401,9 +404,18 @@ export const FIELD_CN = {
|
|
|
401
404
|
// 通用(跨阶段通用)
|
|
402
405
|
id: 'ID', name: '名称', description: '描述', type: '类型', notes: '备注', meta: '元数据',
|
|
403
406
|
kind: '种类', level: '层级', scale: '范围', pattern: '正则',
|
|
404
|
-
// 选题输入(input_snapshot
|
|
405
|
-
|
|
407
|
+
// 选题输入(input_snapshot)—— 字段清单与 fancy-topic 同源
|
|
408
|
+
// (本文件会被复制到项目 references/ 下独立运行,不能 import #topic-fields;
|
|
409
|
+
// 字段清单的漂移由 topic-direction 用例里的渲染断言兜住。)
|
|
410
|
+
book_name: '书名', genre: '题材', target_words: '目标字数', target_chapters: '目标章节',
|
|
411
|
+
positioning: '一句话定位', direction_title: '选题方向', core_conflict: '核心冲突',
|
|
406
412
|
hard_constraints: '硬门禁', anti_tropes: '反套路',
|
|
413
|
+
reader_value: '读者与情绪价值', golden_finger: '金手指', core_setting: '核心设定',
|
|
414
|
+
payoff_model: '爽点模式', emotion_curve: '情绪价值曲线', opening_hook: '开篇切入点',
|
|
415
|
+
pacing_plan: '节奏规划', character_design: '人物设定', differentiation: '差异化策略',
|
|
416
|
+
risk_validation: '风险与验证',
|
|
417
|
+
// 旧版字段(读旧骨架产物时仍能显示中文名)
|
|
418
|
+
core_summary: '一句话故事', direction_design: '方向设计',
|
|
407
419
|
// 容量(capacity)
|
|
408
420
|
target_chapters: '目标章节', target_words: '目标字数', avg_words_per_chapter: '平均每章字数',
|
|
409
421
|
// 角色数量(character_counts)
|
|
@@ -597,10 +609,18 @@ export const FIELD_CN = {
|
|
|
597
609
|
stance: '立场', personality_tone: '性格基调', relationship_stage_tone: '关系阶段语气',
|
|
598
610
|
mirror_type: '镜像类型', stage: '阶段', personality: '性格',
|
|
599
611
|
presentation_method: '呈现方式',
|
|
612
|
+
// 题材扩展 / 关系阶段
|
|
613
|
+
traits: '独有特征', extensions: '题材扩展', other_entities: '其他实体',
|
|
614
|
+
other_relations: '其他关系', relation_label: '关系标签',
|
|
615
|
+
relationship_timeline: '关系阶段时序', state: '关系状态', trigger: '触发事件',
|
|
616
|
+
trigger_event: '触发事件', timing: '时间位置', abilities: '能力', limits: '限制',
|
|
617
|
+
start_chapter: '阶段开始章', end_chapter: '阶段结束章',
|
|
600
618
|
// 骨架 · 层级
|
|
601
619
|
volumes: '卷列表',
|
|
602
620
|
// 节奏 · 时序映射
|
|
603
|
-
character_timeline: '角色时序',
|
|
621
|
+
character_timeline: '角色时序', relationship_timeline: '关系阶段时序',
|
|
622
|
+
faction_relationship_timeline: '势力关系时序',
|
|
623
|
+
other_entities: '其他实体', other_relations: '其他关系', conflict_timeline: '冲突时序',
|
|
604
624
|
foreshadow_timeline: '伏笔时序', climax_timeline: '高潮时序',
|
|
605
625
|
pleasure_timeline: '爽点时序', hook_timeline: '钩子时序',
|
|
606
626
|
main_line_timeline: '主线时序', branch_timeline: '支线时序',
|
|
@@ -616,7 +636,14 @@ export const FIELD_CN = {
|
|
|
616
636
|
faction_relations: '势力关系',
|
|
617
637
|
function_detail: '功能详情', // 旧键名 functional_detail
|
|
618
638
|
info_gaps: '信息差', // 旧键名 information_gaps
|
|
619
|
-
related_conflicts: '涉及冲突'
|
|
639
|
+
related_conflicts: '涉及冲突', // 旧键名 related_conflict
|
|
640
|
+
// 新档案/每章细纲字段(wiki 正文渲染用)
|
|
641
|
+
title: '标题', status: '状态', pov: '视角', pov_name: '视角人物', relationship_type: '关系类型',
|
|
642
|
+
elements: '要素', how_to_write: '写作方法', must_write: '必须写',
|
|
643
|
+
must_not_write: '绝对不能写', information_boundaries: '信息边界',
|
|
644
|
+
timeline_events: '时间线事件', hooks: '钩子', cast: '出场角色',
|
|
645
|
+
foreshadow_actions: '伏笔动作', participants: '参与方',
|
|
646
|
+
participant_names: '参与方名称', mandatory: '必须满足', forbidden: '禁止内容'
|
|
620
647
|
};
|
|
621
648
|
|
|
622
649
|
// 上下文相关的 key→label(不同父 key 同一字段名有不同语义)
|
|
@@ -918,6 +945,7 @@ export function renderArray(arr, indent, parentKey, indexOffset = 0) {
|
|
|
918
945
|
// (历史缺陷:势力的 faction_relations 并不存在;世界观规则漏了 rule_conflict_plan。)
|
|
919
946
|
export const FLESH_DISPATCH = {
|
|
920
947
|
'角色': ['characters'],
|
|
948
|
+
'其他实体': ['other_entities'],
|
|
921
949
|
'冲突': ['conflicts'],
|
|
922
950
|
'势力': ['factions'],
|
|
923
951
|
'剧情': ['main_lines', 'branches'],
|
|
@@ -934,7 +962,7 @@ export const FLESH_DISPATCH = {
|
|
|
934
962
|
export const RHYTHM_DISPATCH = {
|
|
935
963
|
'章节规划': ['chapters'],
|
|
936
964
|
'节奏参数': [],
|
|
937
|
-
'时序映射': ['character_timeline', 'conflict_timeline', 'foreshadow_timeline',
|
|
965
|
+
'时序映射': ['character_timeline', 'relationship_timeline', 'faction_relationship_timeline', 'conflict_timeline', 'foreshadow_timeline',
|
|
938
966
|
'climax_timeline', 'pleasure_timeline', 'hook_timeline',
|
|
939
967
|
'main_line_timeline', 'branch_timeline', 'location_timeline',
|
|
940
968
|
'item_timeline', 'ending_timeline'],
|
|
@@ -950,7 +978,10 @@ const ARR_CN = {
|
|
|
950
978
|
core_items: '核心道具', branch_items: '支线道具', symbolic_items: '象征道具', details: '道具详情',
|
|
951
979
|
chapters: '章节数组', metrics: '节奏指标', feasibility: '可行性约束', coverage: '覆盖校验',
|
|
952
980
|
outlines: '章节细纲',
|
|
953
|
-
|
|
981
|
+
other_entities: '其他实体', other_relations: '其他关系', relationship_timeline: '关系阶段时序',
|
|
982
|
+
character_timeline: '角色时序', relationship_timeline: '关系阶段时序',
|
|
983
|
+
faction_relationship_timeline: '势力关系时序',
|
|
984
|
+
other_entities: '其他实体', other_relations: '其他关系', conflict_timeline: '冲突时序',
|
|
954
985
|
foreshadow_timeline: '伏笔时序', climax_timeline: '高潮时序',
|
|
955
986
|
pleasure_timeline: '爽点时序', hook_timeline: '钩子时序',
|
|
956
987
|
main_line_timeline: '主线时序', branch_timeline: '支线时序',
|
|
@@ -1012,6 +1043,7 @@ export function summarizeStage(stageDir) {
|
|
|
1012
1043
|
'关系': ['关系.json'],
|
|
1013
1044
|
'层级': ['层级.json'],
|
|
1014
1045
|
'角色': ['角色.json'],
|
|
1046
|
+
'其他实体': ['其他实体.json'],
|
|
1015
1047
|
'冲突': ['冲突.json'],
|
|
1016
1048
|
'势力': ['势力.json'],
|
|
1017
1049
|
'剧情': ['剧情.json'],
|
|
@@ -1141,6 +1173,78 @@ export function summarizeRhythm(rhythmDir) {
|
|
|
1141
1173
|
* 返回 null 表示"骨架还没产出框架.json 或没有 input_snapshot",
|
|
1142
1174
|
* 调用方需要自己决定兜底策略(不能静默当成空输入)。
|
|
1143
1175
|
*/
|
|
1176
|
+
const INPUT_RENDER_FIELDS = [
|
|
1177
|
+
['book_name', '书名', 'text'],
|
|
1178
|
+
['genre', '题材', 'text'],
|
|
1179
|
+
['target_words', '目标字数', 'text'],
|
|
1180
|
+
['target_chapters', '目标章节', 'text'],
|
|
1181
|
+
['direction_title', '选题方向', 'text'],
|
|
1182
|
+
['positioning', '一句话定位', 'design'],
|
|
1183
|
+
['core_conflict', '核心冲突', 'text'],
|
|
1184
|
+
['hard_constraints', '硬门禁', 'list'],
|
|
1185
|
+
['anti_tropes', '反套路', 'list'],
|
|
1186
|
+
['reader_value', '读者与情绪价值', 'design'],
|
|
1187
|
+
['golden_finger', '金手指', 'design'],
|
|
1188
|
+
['core_setting', '核心设定', 'design'],
|
|
1189
|
+
['payoff_model', '爽点模式', 'design'],
|
|
1190
|
+
['emotion_curve', '情绪价值曲线', 'design'],
|
|
1191
|
+
['opening_hook', '开篇切入点', 'design'],
|
|
1192
|
+
['pacing_plan', '节奏规划', 'design'],
|
|
1193
|
+
['character_design', '人物设定', 'design'],
|
|
1194
|
+
['differentiation', '差异化策略', 'design'],
|
|
1195
|
+
['risk_validation', '风险与验证', 'design'],
|
|
1196
|
+
];
|
|
1197
|
+
|
|
1198
|
+
/** 旧版 direction_design 标签 → 新字段 ID(本文件要独立运行,不能 import) */
|
|
1199
|
+
const LEGACY_DESIGN_LABELS = [
|
|
1200
|
+
['定位', 'positioning'],
|
|
1201
|
+
['读者与情绪价值', 'reader_value'],
|
|
1202
|
+
['金手指', 'golden_finger'],
|
|
1203
|
+
['核心设定', 'core_setting'],
|
|
1204
|
+
['爽点模式', 'payoff_model'],
|
|
1205
|
+
['情绪价值曲线', 'emotion_curve'],
|
|
1206
|
+
['开篇切入点', 'opening_hook'],
|
|
1207
|
+
['节奏规划', 'pacing_plan'],
|
|
1208
|
+
['人物设定', 'character_design'],
|
|
1209
|
+
['差异化策略', 'differentiation'],
|
|
1210
|
+
['风险与验证', 'risk_validation'],
|
|
1211
|
+
];
|
|
1212
|
+
|
|
1213
|
+
function escapeRegExp(text) {
|
|
1214
|
+
return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* 本文件会被复制到项目 references/ 下独立执行,不能依赖包的 import alias。
|
|
1219
|
+
* 这里保留一份最小的旧格式迁移逻辑;字段清单的漂移由用例里的渲染断言兜住。
|
|
1220
|
+
*/
|
|
1221
|
+
function normalizeLegacyTopicData(data) {
|
|
1222
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return data;
|
|
1223
|
+
const out = { ...data };
|
|
1224
|
+
if (typeof out.direction_design === 'string' && out.direction_design.trim()) {
|
|
1225
|
+
for (const [label, id] of LEGACY_DESIGN_LABELS) {
|
|
1226
|
+
if (typeof out[id] === 'string' && out[id].trim()) continue;
|
|
1227
|
+
const re = new RegExp('^\\s*(?:[-*+]\\s+)?\\*{0,2}' + escapeRegExp(label)
|
|
1228
|
+
+ '\\*{0,2}\\s*[::]\\s*(.*)$', 'm');
|
|
1229
|
+
const hit = re.exec(out.direction_design);
|
|
1230
|
+
if (hit && hit[1].trim()) out[id] = hit[1].trim();
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if ((!out.positioning || !String(out.positioning).trim())
|
|
1234
|
+
&& typeof out.core_summary === 'string' && out.core_summary.trim()) {
|
|
1235
|
+
out.positioning = out.core_summary;
|
|
1236
|
+
}
|
|
1237
|
+
delete out.direction_design;
|
|
1238
|
+
delete out.core_summary;
|
|
1239
|
+
return out;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* 读取骨架阶段的 input_snapshot(任务输入快照)。
|
|
1244
|
+
*
|
|
1245
|
+
* 返回 null 表示"骨架还没产出框架.json 或没有 input_snapshot",
|
|
1246
|
+
* 调用方需要自己决定兜底策略(不能静默当成空输入)。
|
|
1247
|
+
*/
|
|
1144
1248
|
export function readInputSnapshot(skeletonDir) {
|
|
1145
1249
|
const p = _join(skeletonDir, '框架.json');
|
|
1146
1250
|
if (!_existsSync(p)) return null;
|
|
@@ -1151,63 +1255,58 @@ export function readInputSnapshot(skeletonDir) {
|
|
|
1151
1255
|
return null;
|
|
1152
1256
|
}
|
|
1153
1257
|
if (!data || typeof data !== 'object') return null;
|
|
1154
|
-
const
|
|
1155
|
-
if (!
|
|
1258
|
+
const rawSnap = data.input_snapshot;
|
|
1259
|
+
if (!rawSnap || typeof rawSnap !== 'object' || Array.isArray(rawSnap)) return null;
|
|
1260
|
+
const snap = normalizeLegacyTopicData(rawSnap);
|
|
1156
1261
|
const cap = (data.capacity && typeof data.capacity === 'object') ? data.capacity : {};
|
|
1157
|
-
return {
|
|
1158
|
-
book_name: snap.book_name,
|
|
1159
|
-
genre: snap.genre,
|
|
1160
|
-
core_summary: snap.core_summary,
|
|
1161
|
-
core_conflict: snap.core_conflict,
|
|
1162
|
-
hard_constraints: Array.isArray(snap.hard_constraints) ? snap.hard_constraints : [],
|
|
1163
|
-
anti_tropes: Array.isArray(snap.anti_tropes) ? snap.anti_tropes : [],
|
|
1164
|
-
target_chapters: cap.target_chapters,
|
|
1165
|
-
target_words: cap.target_words
|
|
1166
|
-
};
|
|
1167
|
-
}
|
|
1168
1262
|
|
|
1169
|
-
|
|
1170
|
-
const
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
};
|
|
1263
|
+
const out = {};
|
|
1264
|
+
for (const [id] of INPUT_RENDER_FIELDS) out[id] = snap[id];
|
|
1265
|
+
if (cap.target_chapters !== undefined) out.target_chapters = cap.target_chapters;
|
|
1266
|
+
if (cap.target_words !== undefined) out.target_words = cap.target_words;
|
|
1267
|
+
for (const key of ['hard_constraints', 'anti_tropes']) {
|
|
1268
|
+
out[key] = Array.isArray(out[key]) ? out[key] : [];
|
|
1269
|
+
}
|
|
1270
|
+
return out;
|
|
1271
|
+
}
|
|
1179
1272
|
|
|
1180
1273
|
/**
|
|
1181
1274
|
* 把任务输入快照渲染成提示词里的一节 markdown。
|
|
1182
1275
|
*
|
|
1183
1276
|
* 输出形如:
|
|
1184
1277
|
* - 书名:XXX
|
|
1185
|
-
* -
|
|
1186
|
-
*
|
|
1187
|
-
* -
|
|
1188
|
-
*
|
|
1189
|
-
* - 核心冲突:XXX
|
|
1278
|
+
* - 一句话定位:
|
|
1279
|
+
* 以家族为经营单元的长线修仙群像文……
|
|
1280
|
+
* - 金手指:
|
|
1281
|
+
* 铜镜式家族传承……
|
|
1190
1282
|
* - 硬门禁:
|
|
1191
1283
|
* 1. ...
|
|
1192
|
-
* - 反套路:
|
|
1193
|
-
* 1. ...
|
|
1194
1284
|
*/
|
|
1195
1285
|
export function renderInputSnapshot(snapshot) {
|
|
1196
1286
|
if (!snapshot || typeof snapshot !== 'object') return '_(任务输入快照缺失)_\n';
|
|
1287
|
+
const snap = normalizeLegacyTopicData(snapshot);
|
|
1197
1288
|
const lines = [];
|
|
1198
|
-
for (const [
|
|
1199
|
-
const v =
|
|
1289
|
+
for (const [id, label, type] of INPUT_RENDER_FIELDS) {
|
|
1290
|
+
const v = snap[id];
|
|
1200
1291
|
if (v === undefined || v === null || v === '') continue;
|
|
1292
|
+
if (type === 'list') {
|
|
1293
|
+
if (!Array.isArray(v) || v.length === 0) continue;
|
|
1294
|
+
lines.push('- ' + label + ':');
|
|
1295
|
+
v.forEach((item, i) => {
|
|
1296
|
+
lines.push(' ' + (i + 1) + '. ' + String(item).replace(/\n/g, ' '));
|
|
1297
|
+
});
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
if (type === 'design') {
|
|
1301
|
+
lines.push('- ' + label + ':');
|
|
1302
|
+
String(v).split(/\r?\n/).forEach((ln) => {
|
|
1303
|
+
if (ln.trim() === '') return;
|
|
1304
|
+
lines.push(' ' + ln.trim());
|
|
1305
|
+
});
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1201
1308
|
lines.push('- ' + label + ':' + String(v).replace(/\n/g, ' '));
|
|
1202
1309
|
}
|
|
1203
|
-
for (const [k, label] of Object.entries(INPUT_LIST_CN)) {
|
|
1204
|
-
const v = snapshot[k];
|
|
1205
|
-
if (!Array.isArray(v) || v.length === 0) continue;
|
|
1206
|
-
lines.push('- ' + label + ':');
|
|
1207
|
-
v.forEach((item, i) => {
|
|
1208
|
-
lines.push(' ' + (i + 1) + '. ' + String(item).replace(/\n/g, ' '));
|
|
1209
|
-
});
|
|
1210
|
-
}
|
|
1211
1310
|
return lines.length ? lines.join('\n') + '\n' : '_(任务输入快照为空)_\n';
|
|
1212
1311
|
}
|
|
1213
1312
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reference-log.js —— 参考决策台账
|
|
3
|
+
*
|
|
4
|
+
* 目的:LLM 每做一个会进入产物的设计决策,都要留下“依据了哪份参考、
|
|
5
|
+
* 具体哪一条、为什么这么用”的可追溯记录。
|
|
6
|
+
*
|
|
7
|
+
* 文件放在 `设定/.reference-decisions.md`:
|
|
8
|
+
* - 不属于阶段产物,不参与 schema/ingest;
|
|
9
|
+
* - 人类可读的 Markdown 表格;
|
|
10
|
+
* - 只追加,不覆盖已有行。
|
|
11
|
+
*
|
|
12
|
+
* 这里只负责初始化、计数和生成提示词块;真正的“读取参考并做决策”
|
|
13
|
+
* 由 LLM 执行。插件在阶段收尾时把行数报给用户,缺失时给出警告。
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
|
|
18
|
+
export const REFERENCE_LOG_PATH = '设定/.reference-decisions.md';
|
|
19
|
+
export const REFERENCE_LOG_COLUMNS = ['阶段', '批次', '决策对象', '决策内容', '参考文件', '参考条目', '备注'];
|
|
20
|
+
|
|
21
|
+
export const REFERENCE_LOG_HEADER = [
|
|
22
|
+
'# 参考决策记录',
|
|
23
|
+
'',
|
|
24
|
+
'> 本文件由 LLM 在每次设计决策后追加。只追加,不覆盖。',
|
|
25
|
+
'> 参考文件可以是 `references` 目录下的方法论,也可以是 `对标/wiki/` 下的总结。',
|
|
26
|
+
'',
|
|
27
|
+
'| ' + REFERENCE_LOG_COLUMNS.join(' | ') + ' |',
|
|
28
|
+
'|' + REFERENCE_LOG_COLUMNS.map(() => '---').join('|') + '|',
|
|
29
|
+
'',
|
|
30
|
+
].join('\n');
|
|
31
|
+
|
|
32
|
+
/** 确保 `设定/.reference-decisions.md` 存在;不覆盖已有内容。 */
|
|
33
|
+
export function ensureReferenceDecisionLog(projectRoot) {
|
|
34
|
+
if (!projectRoot) return false;
|
|
35
|
+
const dir = join(projectRoot, '设定');
|
|
36
|
+
if (!existsSync(dir)) return false;
|
|
37
|
+
const file = join(projectRoot, REFERENCE_LOG_PATH);
|
|
38
|
+
if (existsSync(file)) return true;
|
|
39
|
+
try {
|
|
40
|
+
mkdirSync(dir, { recursive: true });
|
|
41
|
+
writeFileSync(file, REFERENCE_LOG_HEADER, { encoding: 'utf-8', flag: 'wx' });
|
|
42
|
+
return true;
|
|
43
|
+
} catch {
|
|
44
|
+
// 并发创建时另一个批次可能已经写了;文件存在就算成功。
|
|
45
|
+
return existsSync(file);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 解析参考决策台账里的数据行。 */
|
|
50
|
+
export function parseReferenceDecisionRows(projectRoot) {
|
|
51
|
+
const file = join(projectRoot, REFERENCE_LOG_PATH);
|
|
52
|
+
if (!existsSync(file)) return [];
|
|
53
|
+
let text = '';
|
|
54
|
+
try { text = readFileSync(file, 'utf-8'); } catch { return []; }
|
|
55
|
+
const rows = [];
|
|
56
|
+
for (const line of text.split(/\r?\n/)) {
|
|
57
|
+
const trimmed = line.trim();
|
|
58
|
+
if (!trimmed.startsWith('|')) continue;
|
|
59
|
+
if (/^\|\s*---/.test(trimmed)) continue;
|
|
60
|
+
if (trimmed.startsWith('| ' + REFERENCE_LOG_COLUMNS[0] + ' |')) continue;
|
|
61
|
+
const cells = trimmed.replace(/^\|/, '').replace(/\|$/, '').split('|').map((x) => x.trim());
|
|
62
|
+
if (cells.length < REFERENCE_LOG_COLUMNS.length) continue;
|
|
63
|
+
rows.push(Object.fromEntries(REFERENCE_LOG_COLUMNS.map((c, i) => [c, cells[i] || ''])));
|
|
64
|
+
}
|
|
65
|
+
return rows;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function countReferenceDecisionRows(projectRoot) {
|
|
69
|
+
return parseReferenceDecisionRows(projectRoot).length;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 阶段收尾提示:把台账行数报给用户,空台账必须显眼。 */
|
|
73
|
+
export function referenceLogNote(projectRoot) {
|
|
74
|
+
const count = countReferenceDecisionRows(projectRoot);
|
|
75
|
+
if (count > 0) {
|
|
76
|
+
return '📚 参考决策记录:' + count + ' 条(' + REFERENCE_LOG_PATH + ')';
|
|
77
|
+
}
|
|
78
|
+
return '⚠️ 参考决策记录为空:本次 LLM 没有按契约把“决策依据了哪份参考”写入 `' +
|
|
79
|
+
REFERENCE_LOG_PATH + '`。';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 批次提示词里的强制记录块。
|
|
84
|
+
*
|
|
85
|
+
* 这里不把“参考文件”限死在 对标/wiki:方法论 references 目录也是参考。
|
|
86
|
+
*/
|
|
87
|
+
export function referenceLogPromptBlock(stageLabel, batch, options = {}) {
|
|
88
|
+
const batchName = (batch && (batch.type || batch.groupKey || batch.file)) || '本批';
|
|
89
|
+
const allowed = options.allowedFileText || ('除本批分片外,`' + REFERENCE_LOG_PATH + '` 是本批唯一允许写的额外文件;不要修改 wiki、模板、references 目录或其它阶段。');
|
|
90
|
+
return [
|
|
91
|
+
'## 参考决策记录(必须)',
|
|
92
|
+
'',
|
|
93
|
+
'本批开始前必须实际读取本批参考清单中的 `references` 目录指导文档和 `对标/wiki` 目录总结;',
|
|
94
|
+
'插件注入的摘要只用于快速定位,**不能替代原文件**。本批列出的“本批优先参考”必须逐份核对;阶段级清单用于全局判断,不要求逐篇通读。',
|
|
95
|
+
'',
|
|
96
|
+
'每完成一个会进入产物的独立设计决策,就要向 `' + REFERENCE_LOG_PATH + '` 追加一行:',
|
|
97
|
+
'',
|
|
98
|
+
'| 阶段 | 批次 | 决策对象 | 决策内容 | 参考文件 | 参考条目 | 备注 |',
|
|
99
|
+
'|---|---|---|---|---|---|---|',
|
|
100
|
+
'| ' + stageLabel + ' | ' + batchName + ' | 例如 FS004 | 例如 设计“反向验证”式伏笔 | 例如 对标/wiki 下的伏笔布局总结 | 例如 用反向验证排除“整体替换”式解释 | 例如 让读者先看到结果再反推原因 |',
|
|
101
|
+
'',
|
|
102
|
+
'记录规则:',
|
|
103
|
+
'1. **每个独立决策至少一行**:每个实体、每条关系、每个伏笔/爽点/冲突节点、每章规划、每章细纲、每章正文都算独立决策;同一对象的多个字段如果依据不同参考,拆成多行。',
|
|
104
|
+
'2. `参考文件` 必须写实际读过的路径;`参考条目` 必须写原文里的标题/原则名,不能只写文件名;原文有“来源章节 / 来源实体”时,把对应章节或实体一并写进 `参考条目`。',
|
|
105
|
+
'3. `备注` 写清“为什么用、怎么用、哪些地方没有采用”,不要写“参考了”。',
|
|
106
|
+
'4. **只追加数据行**,不要重复写表头,不要覆盖或删除已有行;备注里的 `|` 写成 `\\|`。',
|
|
107
|
+
'5. ' + allowed,
|
|
108
|
+
'',
|
|
109
|
+
].join('\n');
|
|
110
|
+
}
|
|
@@ -240,7 +240,9 @@ const TIMELINE_KEY_FIELD = {
|
|
|
240
240
|
branch_timeline: 'branch_id',
|
|
241
241
|
location_timeline: 'loc_id',
|
|
242
242
|
item_timeline: 'item_id',
|
|
243
|
-
ending_timeline: 'ending_id'
|
|
243
|
+
ending_timeline: 'ending_id',
|
|
244
|
+
relationship_timeline: 'from',
|
|
245
|
+
faction_relationship_timeline: 'from'
|
|
244
246
|
};
|
|
245
247
|
|
|
246
248
|
/**
|
|
@@ -390,7 +392,7 @@ function buildIdSets(skeleton, flesh) {
|
|
|
390
392
|
// ============================================================
|
|
391
393
|
// 时序映射完整性
|
|
392
394
|
// ============================================================
|
|
393
|
-
function checkTimelineIntegrity(timeline, idSets, errors) {
|
|
395
|
+
function checkTimelineIntegrity(timeline, idSets, errors, flesh) {
|
|
394
396
|
const check = (id, set, label, p) => {
|
|
395
397
|
if (!id) return;
|
|
396
398
|
if (!set.has(id)) {
|
|
@@ -398,6 +400,100 @@ function checkTimelineIntegrity(timeline, idSets, errors) {
|
|
|
398
400
|
}
|
|
399
401
|
};
|
|
400
402
|
|
|
403
|
+
|
|
404
|
+
// 角色关系的阶段变化:同一对角色按 start_chapter 递增,且不能倒回上一阶段。
|
|
405
|
+
const relationRows = timeline.relationship_timeline || [];
|
|
406
|
+
const relationByPair = new Map();
|
|
407
|
+
for (const [i, r] of relationRows.entries()) {
|
|
408
|
+
check(r.from, idSets.char, '角色', 'relationship_timeline[' + i + '].from');
|
|
409
|
+
check(r.to, idSets.char, '角色', 'relationship_timeline[' + i + '].to');
|
|
410
|
+
if (typeof r.start_chapter === 'number' && typeof r.end_chapter === 'number' && r.start_chapter > r.end_chapter) {
|
|
411
|
+
errors.push({
|
|
412
|
+
code: 'RHYTHM_CHAP_ORDER',
|
|
413
|
+
path: 'relationship_timeline[' + i + ']',
|
|
414
|
+
message: '关系阶段 start_chapter(' + r.start_chapter + ') > end_chapter(' + r.end_chapter + ')',
|
|
415
|
+
hint: '阶段结束章必须不早于开始章'
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
if (!r.from || !r.to) continue;
|
|
419
|
+
const key = [r.from, r.to].sort().join('↔');
|
|
420
|
+
if (!relationByPair.has(key)) relationByPair.set(key, []);
|
|
421
|
+
relationByPair.get(key).push(r);
|
|
422
|
+
}
|
|
423
|
+
for (const [pair, rows] of relationByPair) {
|
|
424
|
+
const starts = rows.map((r) => r.start_chapter).filter((n) => typeof n === 'number');
|
|
425
|
+
for (let i = 1; i < starts.length; i++) {
|
|
426
|
+
if (starts[i] <= starts[i - 1]) {
|
|
427
|
+
errors.push({
|
|
428
|
+
code: 'RHYTHM_RELATION_ORDER',
|
|
429
|
+
path: 'relationship_timeline[' + pair + ']',
|
|
430
|
+
message: '关系 ' + pair + ' 的阶段开始章必须严格递增:' + starts.join(' → '),
|
|
431
|
+
hint: '每个阶段只写自己的开始章;后一阶段必须晚于前一阶段'
|
|
432
|
+
});
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// 势力关系的阶段变化:与角色关系同一套口径。
|
|
439
|
+
const factionRows = timeline.faction_relationship_timeline || [];
|
|
440
|
+
const factionByPair = new Map();
|
|
441
|
+
for (const [i, r] of factionRows.entries()) {
|
|
442
|
+
check(r.from, idSets.faction, '势力', 'faction_relationship_timeline[' + i + '].from');
|
|
443
|
+
check(r.to, idSets.faction, '势力', 'faction_relationship_timeline[' + i + '].to');
|
|
444
|
+
if (typeof r.start_chapter === 'number' && typeof r.end_chapter === 'number' && r.start_chapter > r.end_chapter) {
|
|
445
|
+
errors.push({
|
|
446
|
+
code: 'RHYTHM_CHAP_ORDER',
|
|
447
|
+
path: 'faction_relationship_timeline[' + i + ']',
|
|
448
|
+
message: '势力关系阶段 start_chapter(' + r.start_chapter + ') > end_chapter(' + r.end_chapter + ')',
|
|
449
|
+
hint: '阶段结束章必须不早于开始章'
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
if (!r.from || !r.to) continue;
|
|
453
|
+
const key = [r.from, r.to].sort().join('↔');
|
|
454
|
+
if (!factionByPair.has(key)) factionByPair.set(key, []);
|
|
455
|
+
factionByPair.get(key).push(r);
|
|
456
|
+
}
|
|
457
|
+
for (const [pair, rows] of factionByPair) {
|
|
458
|
+
const starts = rows.map((r) => r.start_chapter).filter((n) => typeof n === 'number');
|
|
459
|
+
for (let i = 1; i < starts.length; i++) {
|
|
460
|
+
if (starts[i] <= starts[i - 1]) {
|
|
461
|
+
errors.push({
|
|
462
|
+
code: 'RHYTHM_RELATION_ORDER',
|
|
463
|
+
path: 'faction_relationship_timeline[' + pair + ']',
|
|
464
|
+
message: '势力关系 ' + pair + ' 的阶段开始章必须严格递增:' + starts.join(' → '),
|
|
465
|
+
hint: '每个阶段只写自己的开始章;后一阶段必须晚于前一阶段'
|
|
466
|
+
});
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// 血肉里写了 changes[] 的关系,节奏必须给它至少一个阶段落点。
|
|
473
|
+
const changedPairs = new Set();
|
|
474
|
+
const fleshChars = (flesh && flesh.characters && flesh.characters.characters) || [];
|
|
475
|
+
const idByName = new Map(fleshChars.filter((c) => c && c.id && c.name).map((c) => [c.name, c.id]));
|
|
476
|
+
for (const c of fleshChars) {
|
|
477
|
+
if (!c || !Array.isArray(c.relationships)) continue;
|
|
478
|
+
for (const rel of c.relationships) {
|
|
479
|
+
if (!rel || !Array.isArray(rel.changes) || rel.changes.length === 0) continue;
|
|
480
|
+
const targetId = idByName.get(rel.target_name);
|
|
481
|
+
// 没有 name→id 表时跳过覆盖检查;引用错误由血肉阶段负责。
|
|
482
|
+
if (!c.id || !targetId) continue;
|
|
483
|
+
changedPairs.add([c.id, targetId].sort().join('↔'));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
for (const pair of changedPairs) {
|
|
487
|
+
if (!relationByPair.has(pair)) {
|
|
488
|
+
errors.push({
|
|
489
|
+
code: 'RHYTHM_RELATION_TIMELINE_MISSING',
|
|
490
|
+
path: 'relationship_timeline',
|
|
491
|
+
message: '血肉阶段写了 ' + pair + ' 的关系变化,但时序映射没有对应阶段',
|
|
492
|
+
hint: '在 relationship_timeline 里为这对角色补 start_chapter;章节落点属于节奏阶段'
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
401
497
|
for (const [i, r] of (timeline.character_timeline || []).entries()) {
|
|
402
498
|
check(r.char_id, idSets.char, '角色', 'character_timeline[' + i + '].char_id');
|
|
403
499
|
if (typeof r.appear_start === 'number' && typeof r.appear_end === 'number' && r.appear_start > r.appear_end) {
|
|
@@ -1068,7 +1164,7 @@ function validateSingle(args) {
|
|
|
1068
1164
|
if (file === '章节规划') {
|
|
1069
1165
|
safeCheck('checkChapterPlan', () => checkChapterPlan(data.chapters, T, skeleton.hierarchy, skeleton.framework.volume_ratio, errors, warnings), errors);
|
|
1070
1166
|
} else if (file === '时序映射') {
|
|
1071
|
-
safeCheck('checkTimelineIntegrity', () => checkTimelineIntegrity(data, idSets, errors), errors);
|
|
1167
|
+
safeCheck('checkTimelineIntegrity', () => checkTimelineIntegrity(data, idSets, errors, skeleton.flesh), errors);
|
|
1072
1168
|
} else if (file === '节奏参数') {
|
|
1073
1169
|
safeCheck('checkRhythmInfeasible', () => checkRhythmInfeasible(data, skeleton.framework, fm.constraints, errors), errors);
|
|
1074
1170
|
safeCheck('checkRhythmDerivations', () => checkRhythmDerivations(data, fm.constraints, errors, warnings), errors);
|
|
@@ -1162,7 +1258,7 @@ function validateAll(args) {
|
|
|
1162
1258
|
if (f.type === '章节规划') {
|
|
1163
1259
|
safeCheck('checkChapterPlan', () => checkChapterPlan(data.chapters, T, skeleton.hierarchy, skeleton.framework.volume_ratio, errors, warnings), errors);
|
|
1164
1260
|
} else if (f.type === '时序映射') {
|
|
1165
|
-
safeCheck('checkTimelineIntegrity', () => checkTimelineIntegrity(data, idSets, errors), errors);
|
|
1261
|
+
safeCheck('checkTimelineIntegrity', () => checkTimelineIntegrity(data, idSets, errors, skeleton.flesh), errors);
|
|
1166
1262
|
} else if (f.type === '节奏参数') {
|
|
1167
1263
|
safeCheck('checkRhythmInfeasible', () => checkRhythmInfeasible(data, skeleton.framework, fm.constraints, errors), errors);
|
|
1168
1264
|
safeCheck('checkRhythmDerivations', () => checkRhythmDerivations(data, fm.constraints, errors, warnings), errors);
|