@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
|
@@ -293,6 +293,7 @@ export function buildSemanticLintPrompt(ctx) {
|
|
|
293
293
|
export function collectFleshEntities(fleshDir) {
|
|
294
294
|
const FILES = {
|
|
295
295
|
'角色.json': ['characters'],
|
|
296
|
+
'其他实体.json': ['other_entities'],
|
|
296
297
|
'冲突.json': ['conflicts'],
|
|
297
298
|
'势力.json': ['factions'],
|
|
298
299
|
'剧情.json': ['main_lines', 'branches'],
|
|
@@ -342,7 +343,10 @@ export function collectSkeletonRelations(skeletonDir) {
|
|
|
342
343
|
}
|
|
343
344
|
const out = [];
|
|
344
345
|
for (const r of (data.character_relations || [])) {
|
|
345
|
-
if (r && r.from && r.to) out.push({ from: r.from, to: r.to, kind: r.relation_kind || '' });
|
|
346
|
+
if (r && r.from && r.to) out.push({ from: r.from, to: r.to, kind: (r.relation_kind || '') + (r.relation_label ? '/' + r.relation_label : '') });
|
|
347
|
+
}
|
|
348
|
+
for (const r of (data.other_relations || [])) {
|
|
349
|
+
if (r && r.from && r.to) out.push({ from: r.from, to: r.to, kind: r.relation_label || '' });
|
|
346
350
|
}
|
|
347
351
|
for (const r of (data.faction_relations || [])) {
|
|
348
352
|
if (r && r.from && r.to) out.push({ from: r.from, to: r.to, kind: r.relation_type || '' });
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* 骨架阶段校验脚本 v2.
|
|
3
|
+
* 骨架阶段校验脚本 v2.5
|
|
4
4
|
* 源码中零反引号(所有字符串使用单/双引号或拼接),避免 markdown 渲染问题。
|
|
5
5
|
*
|
|
6
|
+
* v2.5 变更:
|
|
7
|
+
* - 新增 NAME_ALIAS_SUSPECT:名称归一化(去括号/标点/常见机构后缀)后
|
|
8
|
+
* 相同,但字面不同 → warning,提示可能是同一实体的全称/简称/别名。
|
|
9
|
+
* - CROSS_TYPE_NAME_DUP 改用同一套归一化口径,能抓「企鹅集团 vs 企鹅」
|
|
10
|
+
* 这种跨类型软重复。
|
|
11
|
+
*
|
|
6
12
|
* v2.4 变更(修复批次 P0-2 / P2-4 / P2-8):
|
|
7
13
|
* - P0-2:evalExpr 正则允许 = < > ! & | ? :(原版不允许,导致
|
|
8
14
|
* formulas-skeleton.json 的 ending_elements.ending_count 的 expr_low/expr_high
|
|
@@ -237,7 +243,7 @@ function charsetAndForbiddenCheck(data, errors) {
|
|
|
237
243
|
// ============================================================
|
|
238
244
|
function idUniquenessCheck(data, fileType, errors) {
|
|
239
245
|
const ID_ARRAYS = {
|
|
240
|
-
'实体': ['characters', 'conflicts', 'factions', 'locations', 'items', 'endings', 'foreshadows', 'climaxes', 'pleasures', 'world_rules', 'main_lines', 'branches'],
|
|
246
|
+
'实体': ['characters', 'other_entities', 'conflicts', 'factions', 'locations', 'items', 'endings', 'foreshadows', 'climaxes', 'pleasures', 'world_rules', 'main_lines', 'branches'],
|
|
241
247
|
'层级': ['volumes']
|
|
242
248
|
};
|
|
243
249
|
// 记出现次数:光说"重复"模型不知道该删几条
|
|
@@ -260,12 +266,43 @@ function idUniquenessCheck(data, fileType, errors) {
|
|
|
260
266
|
}
|
|
261
267
|
}
|
|
262
268
|
|
|
269
|
+
const NAME_ARRAYS_BY_FILE = {
|
|
270
|
+
'实体': ['characters', 'other_entities', 'conflicts', 'factions', 'locations', 'items', 'endings', 'foreshadows', 'climaxes', 'pleasures', 'world_rules', 'main_lines', 'branches'],
|
|
271
|
+
'层级': ['volumes']
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 归一化实体名,用于发现“同一实体换了写法”:
|
|
276
|
+
* - 去空白 / 标点 / 括号注释;
|
|
277
|
+
* - 去常见机构后缀(集团/公司/大学/协会…),
|
|
278
|
+
* 这样「企鹅集团」和「企鹅」会落到同一个 key;
|
|
279
|
+
* - 不做拼音/翻译,跨语言别名仍交给 LLM 审计。
|
|
280
|
+
*/
|
|
281
|
+
const GENERIC_NAME_SUFFIXES = [
|
|
282
|
+
'股份有限公司', '有限公司', '集团', '公司', '大学', '学院', '学校',
|
|
283
|
+
'医院', '银行', '协会', '商会', '联盟', '家族', '世家', '王朝',
|
|
284
|
+
'帝国', '组织', '机构', '政府', '部门'
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
function normalizeEntityName(name) {
|
|
288
|
+
let s = String(name || '').trim().toLowerCase();
|
|
289
|
+
s = s.replace(/[((][^))]*[))]/g, '');
|
|
290
|
+
s = s.replace(/[\s·・,,。.、::;;!!??'"“”‘’《》<>【】\[\]()()\-—_/\\|]/g, '');
|
|
291
|
+
let changed = true;
|
|
292
|
+
while (changed) {
|
|
293
|
+
changed = false;
|
|
294
|
+
for (const suffix of GENERIC_NAME_SUFFIXES) {
|
|
295
|
+
if (s.length > suffix.length + 1 && s.endsWith(suffix)) {
|
|
296
|
+
s = s.slice(0, -suffix.length);
|
|
297
|
+
changed = true;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return s;
|
|
302
|
+
}
|
|
303
|
+
|
|
263
304
|
function nameUniquenessCheck(data, fileType, errors) {
|
|
264
|
-
const
|
|
265
|
-
'实体': ['characters', 'conflicts', 'factions', 'locations', 'items', 'endings', 'foreshadows', 'climaxes', 'pleasures', 'world_rules', 'main_lines', 'branches'],
|
|
266
|
-
'层级': ['volumes']
|
|
267
|
-
};
|
|
268
|
-
for (const key of NAME_ARRAYS[fileType] || []) {
|
|
305
|
+
for (const key of NAME_ARRAYS_BY_FILE[fileType] || []) {
|
|
269
306
|
const seen = new Map();
|
|
270
307
|
for (const item of data[key] || []) {
|
|
271
308
|
const n = item.name;
|
|
@@ -279,6 +316,35 @@ function nameUniquenessCheck(data, fileType, errors) {
|
|
|
279
316
|
}
|
|
280
317
|
}
|
|
281
318
|
|
|
319
|
+
/**
|
|
320
|
+
* 软重复检查:名称归一化后相同,但字面不同。
|
|
321
|
+
* 只报 warning,不自动合并 —— 可能是同一实体,也可能是“阳城”与“阳城大学”
|
|
322
|
+
* 这种上下位关系,必须让作者/模型确认。
|
|
323
|
+
*/
|
|
324
|
+
function nameAliasCheck(data, fileType, warnings) {
|
|
325
|
+
for (const key of NAME_ARRAYS_BY_FILE[fileType] || []) {
|
|
326
|
+
const byNormalized = new Map();
|
|
327
|
+
for (const item of data[key] || []) {
|
|
328
|
+
if (!item.name) continue;
|
|
329
|
+
const normalized = normalizeEntityName(item.name);
|
|
330
|
+
if (!normalized) continue;
|
|
331
|
+
if (!byNormalized.has(normalized)) byNormalized.set(normalized, []);
|
|
332
|
+
byNormalized.get(normalized).push({ id: item.id, name: String(item.name) });
|
|
333
|
+
}
|
|
334
|
+
for (const [normalized, entries] of byNormalized) {
|
|
335
|
+
const distinctNames = [...new Set(entries.map((e) => e.name))];
|
|
336
|
+
if (distinctNames.length <= 1) continue;
|
|
337
|
+
warnings.push({
|
|
338
|
+
code: 'NAME_ALIAS_SUSPECT',
|
|
339
|
+
path: key + '.' + distinctNames.join(' / '),
|
|
340
|
+
message: '名称 ' + distinctNames.map((n) => '"' + n + '"').join('、') +
|
|
341
|
+
' 归一化后相同(' + normalized + '),可能是同一实体的全称/简称/别名',
|
|
342
|
+
hint: '确认是同一实体时只保留一个正式名,把另一个的设定合并过去;不要把称呼、简称、集团/公司后缀单独建成新实体'
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
282
348
|
// ============================================================
|
|
283
349
|
// 自环关系检查
|
|
284
350
|
// ============================================================
|
|
@@ -294,6 +360,17 @@ function selfRelationCheck(data, errors) {
|
|
|
294
360
|
});
|
|
295
361
|
}
|
|
296
362
|
}
|
|
363
|
+
for (let i = 0; i < (data.other_relations || []).length; i++) {
|
|
364
|
+
const r = data.other_relations[i];
|
|
365
|
+
if (r.from && r.to && r.from === r.to) {
|
|
366
|
+
errors.push({
|
|
367
|
+
code: 'NO_SELF_RELATION',
|
|
368
|
+
path: 'other_relations[' + i + ']',
|
|
369
|
+
message: '其他实体关系 from 和 to 不能相同(' + r.from + ')',
|
|
370
|
+
hint: '自环关系无意义;改为指向人物或另一个实体'
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
297
374
|
for (let i = 0; i < (data.faction_relations || []).length; i++) {
|
|
298
375
|
const r = data.faction_relations[i];
|
|
299
376
|
if (r.from && r.to && r.from === r.to) {
|
|
@@ -307,6 +384,36 @@ function selfRelationCheck(data, errors) {
|
|
|
307
384
|
}
|
|
308
385
|
}
|
|
309
386
|
|
|
387
|
+
// ============================================================
|
|
388
|
+
// 扩展关系标签检查
|
|
389
|
+
// ============================================================
|
|
390
|
+
// `其他` 是词表外的扩展出口。只写"其他"等于没写关系;
|
|
391
|
+
// 必须用 relation_label 说清是附身、寄生、契约还是别的。
|
|
392
|
+
function relationLabelCheck(data, errors) {
|
|
393
|
+
for (const [i, r] of (data.character_relations || []).entries()) {
|
|
394
|
+
if (!r || r.relation_kind !== '其他') continue;
|
|
395
|
+
const label = typeof r.relation_label === 'string' ? r.relation_label.trim() : '';
|
|
396
|
+
if (!label) {
|
|
397
|
+
errors.push({
|
|
398
|
+
code: 'REL_LABEL_REQUIRED',
|
|
399
|
+
path: 'character_relations[' + i + '].relation_label',
|
|
400
|
+
message: 'relation_kind="其他" 时必须填 relation_label,写清具体关系(如 附身/寄生/契约/共生)',
|
|
401
|
+
hint: 'relation_label 是词表外关系的唯一语义来源;只写"其他"下游无法判断该怎么写'
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
for (const [i, r] of (data.other_relations || []).entries()) {
|
|
406
|
+
if (!r || typeof r.relation_label !== 'string' || !r.relation_label.trim()) {
|
|
407
|
+
errors.push({
|
|
408
|
+
code: 'REL_LABEL_REQUIRED',
|
|
409
|
+
path: 'other_relations[' + i + '].relation_label',
|
|
410
|
+
message: 'other_relations 必须填 relation_label,写清具体关系(如 附身/寄宿/共生/镇压)',
|
|
411
|
+
hint: '非人物实体的关系没有词表可依,relation_label 是唯一语义来源'
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
310
417
|
// ============================================================
|
|
311
418
|
// 关系数组组合重复检查(7 类)
|
|
312
419
|
// ============================================================
|
|
@@ -340,6 +447,20 @@ function relationDupCheck(data, errors) {
|
|
|
340
447
|
crSet.set(k, r.relation_kind);
|
|
341
448
|
}
|
|
342
449
|
|
|
450
|
+
const orSet = new Set();
|
|
451
|
+
for (const r of (data.other_relations || [])) {
|
|
452
|
+
const k = r.from + '→' + r.to;
|
|
453
|
+
if (orSet.has(k)) {
|
|
454
|
+
errors.push({
|
|
455
|
+
code: 'ID_DUP',
|
|
456
|
+
path: 'other_relations.' + k,
|
|
457
|
+
message: '其他实体关系 ' + k + ' 重复',
|
|
458
|
+
hint: '同一对存在只能保留一条关系;不同关系请用 relation_label 写清,不要重复建行'
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
orSet.add(k);
|
|
462
|
+
}
|
|
463
|
+
|
|
343
464
|
const frSet = new Set();
|
|
344
465
|
for (const r of (data.faction_relations || [])) {
|
|
345
466
|
const k = r.from + '→' + r.to;
|
|
@@ -439,7 +560,8 @@ function relationDupCheck(data, errors) {
|
|
|
439
560
|
// ============================================================
|
|
440
561
|
function derivationCheck(data, formulas, errors, warnings) {
|
|
441
562
|
const der = data.derivations || {};
|
|
442
|
-
|
|
563
|
+
// v2 的 custom.* 公式只用于 10-scale 的 scale.json;旧骨架阶段不消费它们。
|
|
564
|
+
const formulaKeys = new Set(Object.keys(formulas).filter((k) => !k.startsWith('custom.')));
|
|
443
565
|
const derKeys = new Set(Object.keys(der));
|
|
444
566
|
|
|
445
567
|
for (const k of formulaKeys) {
|
|
@@ -466,6 +588,7 @@ function derivationCheck(data, formulas, errors, warnings) {
|
|
|
466
588
|
|
|
467
589
|
const ctx = buildFormulaContext(data);
|
|
468
590
|
for (const [k, spec] of Object.entries(formulas)) {
|
|
591
|
+
if (k.startsWith('custom.')) continue;
|
|
469
592
|
const d = der[k];
|
|
470
593
|
if (!d) continue;
|
|
471
594
|
let realLow, realHigh;
|
|
@@ -641,7 +764,8 @@ function relationRefCheck(data, entityData, errors) {
|
|
|
641
764
|
items: new Set((entityData.items || []).map(i => i.id)),
|
|
642
765
|
endings: new Set((entityData.endings || []).map(e => e.id)),
|
|
643
766
|
branches: new Set((entityData.branches || []).map(b => b.id)),
|
|
644
|
-
main_lines: new Set((entityData.main_lines || []).map(m => m.id))
|
|
767
|
+
main_lines: new Set((entityData.main_lines || []).map(m => m.id)),
|
|
768
|
+
other_entities: new Set((entityData.other_entities || []).map(o => o.id))
|
|
645
769
|
};
|
|
646
770
|
|
|
647
771
|
const ref = (val, set, label, pathStr) => {
|
|
@@ -660,6 +784,11 @@ function relationRefCheck(data, entityData, errors) {
|
|
|
660
784
|
ref(r.from, ids.characters, '角色', 'character_relations[' + i + '].from');
|
|
661
785
|
ref(r.to, ids.characters, '角色', 'character_relations[' + i + '].to');
|
|
662
786
|
}
|
|
787
|
+
for (const [i, r] of (data.other_relations || []).entries()) {
|
|
788
|
+
ref(r.from, ids.other_entities, '其他实体', 'other_relations[' + i + '].from');
|
|
789
|
+
if (String(r.to || '').startsWith('C')) ref(r.to, ids.characters, '角色', 'other_relations[' + i + '].to');
|
|
790
|
+
else ref(r.to, ids.other_entities, '其他实体', 'other_relations[' + i + '].to');
|
|
791
|
+
}
|
|
663
792
|
for (const [i, r] of (data.faction_membership || []).entries()) {
|
|
664
793
|
ref(r.character, ids.characters, '角色', 'faction_membership[' + i + '].character');
|
|
665
794
|
ref(r.faction, ids.factions, '势力', 'faction_membership[' + i + '].faction');
|
|
@@ -1121,25 +1250,29 @@ function checkWorldRuleCategory(entityData, errors) {
|
|
|
1121
1250
|
// ============================================================
|
|
1122
1251
|
function crossTypeNameDupCheck(entityData, warnings) {
|
|
1123
1252
|
if (!entityData) return;
|
|
1124
|
-
const types = ['characters', 'factions', 'conflicts', 'locations'];
|
|
1125
|
-
const typeLabels = { characters: '角色', factions: '势力', conflicts: '冲突', locations: '地点' };
|
|
1253
|
+
const types = ['characters', 'other_entities', 'factions', 'conflicts', 'locations'];
|
|
1254
|
+
const typeLabels = { characters: '角色', other_entities: '其他实体', factions: '势力', conflicts: '冲突', locations: '地点' };
|
|
1126
1255
|
const nameMap = new Map();
|
|
1127
1256
|
for (const t of types) {
|
|
1128
1257
|
for (const item of (entityData[t] || [])) {
|
|
1129
1258
|
if (!item.name) continue;
|
|
1130
|
-
|
|
1131
|
-
|
|
1259
|
+
const key = normalizeEntityName(item.name);
|
|
1260
|
+
if (!key) continue;
|
|
1261
|
+
if (!nameMap.has(key)) nameMap.set(key, []);
|
|
1262
|
+
nameMap.get(key).push({ type: t, id: item.id, name: String(item.name) });
|
|
1132
1263
|
}
|
|
1133
1264
|
}
|
|
1134
|
-
for (const [
|
|
1265
|
+
for (const [normalized, entries] of nameMap) {
|
|
1135
1266
|
if (entries.length <= 1) continue;
|
|
1136
1267
|
const typeSet = new Set(entries.map(e => e.type));
|
|
1137
1268
|
if (typeSet.size <= 1) continue;
|
|
1138
1269
|
const typesInvolved = [...typeSet].map(t => typeLabels[t]).join(' / ');
|
|
1270
|
+
const names = [...new Set(entries.map((e) => e.name))];
|
|
1139
1271
|
warnings.push({
|
|
1140
1272
|
code: 'CROSS_TYPE_NAME_DUP',
|
|
1141
|
-
path: 'name: ' +
|
|
1142
|
-
message: '名称
|
|
1273
|
+
path: 'name: ' + names.join(' / '),
|
|
1274
|
+
message: '名称 ' + names.map((n) => '"' + n + '"').join('、') +
|
|
1275
|
+
' 在多个实体类型中重复出现(' + typesInvolved + ';归一化 key=' + normalized + ')',
|
|
1143
1276
|
hint: '不同类型同名可能导致引用歧义(如 faction.members[].name 与 faction.relations[].target_name 无法靠名字区分),建议改名'
|
|
1144
1277
|
});
|
|
1145
1278
|
}
|
|
@@ -1157,7 +1290,7 @@ function buildGraph(entityData, relationData, errors, warnings) {
|
|
|
1157
1290
|
['locations', 'location'], ['items', 'item'], ['endings', 'ending'],
|
|
1158
1291
|
['foreshadows', 'foreshadow'], ['climaxes', 'climax'], ['pleasures', 'pleasure'],
|
|
1159
1292
|
['main_lines', 'main_line'], ['branches', 'branch'],
|
|
1160
|
-
['world_rules', 'world_rule']
|
|
1293
|
+
['world_rules', 'world_rule'], ['other_entities', 'other_entity']
|
|
1161
1294
|
]) {
|
|
1162
1295
|
for (const x of (entityData[arr] || [])) {
|
|
1163
1296
|
graph.addNode(x.id, { type: type, name: x.name });
|
|
@@ -1174,6 +1307,7 @@ function buildGraph(entityData, relationData, errors, warnings) {
|
|
|
1174
1307
|
};
|
|
1175
1308
|
|
|
1176
1309
|
for (const r of (relationData.character_relations || [])) addEdge(r.from, r.to, 'related_to');
|
|
1310
|
+
for (const r of (relationData.other_relations || [])) addEdge(r.from, r.to, r.relation_label || 'related_to');
|
|
1177
1311
|
for (const r of (relationData.faction_membership || [])) addEdge(r.character, r.faction, 'member_of');
|
|
1178
1312
|
for (const r of (relationData.faction_relations || [])) addEdge(r.from, r.to, 'faction_related');
|
|
1179
1313
|
for (const r of (relationData.conflict_participants || [])) {
|
|
@@ -1366,6 +1500,7 @@ function renderFramework(data) {
|
|
|
1366
1500
|
// 实体文件的渲染(12 类实体数组)
|
|
1367
1501
|
const ENTITY_ARRAYS = [
|
|
1368
1502
|
['characters', '角色'],
|
|
1503
|
+
['other_entities', '其他实体'],
|
|
1369
1504
|
['conflicts', '冲突'],
|
|
1370
1505
|
['factions', '势力'],
|
|
1371
1506
|
['locations', '地点'],
|
|
@@ -1393,6 +1528,7 @@ function renderEntity(data) {
|
|
|
1393
1528
|
// 关系文件的渲染(9 类关系数组)
|
|
1394
1529
|
const RELATION_ARRAYS = [
|
|
1395
1530
|
['character_relations', '角色关系'],
|
|
1531
|
+
['other_relations', '其他关系'],
|
|
1396
1532
|
['faction_membership', '势力成员'],
|
|
1397
1533
|
['faction_relations', '势力关系'],
|
|
1398
1534
|
['conflict_participants', '冲突参与'],
|
|
@@ -1568,6 +1704,7 @@ function validateSingle(args) {
|
|
|
1568
1704
|
safeCheck('charsetAndForbiddenCheck', () => charsetAndForbiddenCheck(data, errors), errors);
|
|
1569
1705
|
safeCheck('idUniquenessCheck', () => idUniquenessCheck(data, file, errors), errors);
|
|
1570
1706
|
safeCheck('nameUniquenessCheck', () => nameUniquenessCheck(data, file, errors), errors);
|
|
1707
|
+
safeCheck('nameAliasCheck', () => nameAliasCheck(data, file, warnings), errors);
|
|
1571
1708
|
|
|
1572
1709
|
if (file === '框架') {
|
|
1573
1710
|
safeCheck('frameworkSpecificCheck', () => frameworkSpecificCheck(data, errors, warnings), errors);
|
|
@@ -1576,6 +1713,7 @@ function validateSingle(args) {
|
|
|
1576
1713
|
} else if (file === '关系') {
|
|
1577
1714
|
safeCheck('selfRelationCheck', () => selfRelationCheck(data, errors), errors);
|
|
1578
1715
|
safeCheck('relationDupCheck', () => relationDupCheck(data, errors), errors);
|
|
1716
|
+
safeCheck('relationLabelCheck', () => relationLabelCheck(data, errors), errors);
|
|
1579
1717
|
safeCheck('relationRefCheck', () => relationRefCheck(data, upstreamEntities, errors), errors);
|
|
1580
1718
|
// 支线挂主线的覆盖度:关系文件对着实体清单查(tt5 实测的静默缺失)
|
|
1581
1719
|
safeCheck('checkBranchMainLineCoverage', () => checkBranchMainLineCoverage(upstreamEntities, data, errors), errors);
|
|
@@ -1702,6 +1840,7 @@ function validatePartial(args) {
|
|
|
1702
1840
|
if (file === '关系') {
|
|
1703
1841
|
safeCheck('selfRelationCheck', () => selfRelationCheck(scoped, errors), errors);
|
|
1704
1842
|
safeCheck('relationDupCheck', () => relationDupCheck(scoped, errors), errors);
|
|
1843
|
+
safeCheck('relationLabelCheck', () => relationLabelCheck(scoped, errors), errors);
|
|
1705
1844
|
safeCheck('relationRefCheck', () => relationRefCheck(scoped, upstreamEntities, errors), errors);
|
|
1706
1845
|
safeCheck('characterRelationSymmetryCheck', () => characterRelationSymmetryCheck(scoped, errors), errors);
|
|
1707
1846
|
safeCheck('factionRelationSymmetryCheck', () => factionRelationSymmetryCheck(scoped, errors), errors);
|
|
@@ -1767,6 +1906,7 @@ function validateAll(args) {
|
|
|
1767
1906
|
safeCheck('charsetAndForbiddenCheck', () => charsetAndForbiddenCheck(data, errors), errors);
|
|
1768
1907
|
safeCheck('idUniquenessCheck', () => idUniquenessCheck(data, f.type, errors), errors);
|
|
1769
1908
|
safeCheck('nameUniquenessCheck', () => nameUniquenessCheck(data, f.type, errors), errors);
|
|
1909
|
+
safeCheck('nameAliasCheck', () => nameAliasCheck(data, f.type, warnings), errors);
|
|
1770
1910
|
|
|
1771
1911
|
if (f.type === '框架') {
|
|
1772
1912
|
safeCheck('frameworkSpecificCheck', () => frameworkSpecificCheck(data, errors, warnings), errors);
|
|
@@ -1774,6 +1914,7 @@ function validateAll(args) {
|
|
|
1774
1914
|
} else if (f.type === '关系') {
|
|
1775
1915
|
safeCheck('selfRelationCheck', () => selfRelationCheck(data, errors), errors);
|
|
1776
1916
|
safeCheck('relationDupCheck', () => relationDupCheck(data, errors), errors);
|
|
1917
|
+
safeCheck('relationLabelCheck', () => relationLabelCheck(data, errors), errors);
|
|
1777
1918
|
safeCheck('relationRefCheck', () => relationRefCheck(data, loaded['实体'], errors), errors);
|
|
1778
1919
|
// 支线挂主线的覆盖度(tt5 实测:只写 1 行也 PASSED,9 条支线静默孤儿)
|
|
1779
1920
|
safeCheck('checkBranchMainLineCoverage', () => checkBranchMainLineCoverage(loaded['实体'], data, errors), errors);
|
|
@@ -71,6 +71,15 @@ export const FLESH_FILE_SPECS = [
|
|
|
71
71
|
{ key: 'characters', source: 'characters' }
|
|
72
72
|
]
|
|
73
73
|
},
|
|
74
|
+
{
|
|
75
|
+
type: '其他实体',
|
|
76
|
+
file: '其他实体.json',
|
|
77
|
+
template: '其他实体模板.json',
|
|
78
|
+
optionalSource: 'other_entities',
|
|
79
|
+
groups: [
|
|
80
|
+
{ key: 'other_entities', source: 'other_entities' }
|
|
81
|
+
]
|
|
82
|
+
},
|
|
74
83
|
{
|
|
75
84
|
type: '冲突',
|
|
76
85
|
file: '冲突.json',
|
|
@@ -169,7 +178,9 @@ export const SKELETON_FILE_SPECS = [
|
|
|
169
178
|
file: '实体.json',
|
|
170
179
|
template: '实体模板.json',
|
|
171
180
|
groups: [
|
|
172
|
-
|
|
181
|
+
// 角色批次顺带写可选的题材扩展实体(灵魂/神魂/器灵…)。
|
|
182
|
+
// other_entities 不在 quota 里:没有就写空数组,有就从 O001 起编号。
|
|
183
|
+
{ key: 'characters', quotaDriven: true, maxItems: 20, extraArrays: ['other_entities'], optionalArrays: ['other_entities'] },
|
|
173
184
|
{ key: 'conflicts', quotaDriven: true, maxItems: 20 },
|
|
174
185
|
{ key: 'factions', quotaDriven: true, maxItems: 20 },
|
|
175
186
|
{ key: 'locations', quotaDriven: true, maxItems: 20 },
|
|
@@ -215,6 +226,8 @@ export const SKELETON_FILE_SPECS = [
|
|
|
215
226
|
// · main_line_branch_relation 按主线切(只有 1 条主线)→ 提示词说 1 条
|
|
216
227
|
// → 模型写 1 行 → 10 条支线里 9 条静默没挂主线,校验器还不报。
|
|
217
228
|
{ key: 'character_relations', source: 'characters', ownerField: 'from', refs: ['characters'], rows: 'semantic', maxItems: 8 },
|
|
229
|
+
// 非人物实体参与的关系(附身/寄宿/共生/镇压…)。没有 other_entities 时整组跳过。
|
|
230
|
+
{ key: 'other_relations', source: 'other_entities', ownerField: 'from', refs: ['other_entities', 'characters'], rows: 'semantic', maxItems: 8, optionalSource: 'other_entities' },
|
|
218
231
|
{ key: 'faction_membership', source: 'factions', ownerField: 'faction', refs: ['characters', 'factions'], rows: 'semantic', maxItems: 8 },
|
|
219
232
|
{ key: 'faction_relations', source: 'factions', ownerField: 'from', refs: ['factions'], rows: 'quota', quotaPath: 'faction_relation_count', multiplier: 2, maxItems: 8 },
|
|
220
233
|
{ key: 'conflict_participants', source: 'conflicts', ownerField: 'conflict', refs: ['conflicts', 'characters'], rows: 'perEntity', maxItems: 8 },
|
|
@@ -282,6 +295,9 @@ export const RHYTHM_FILE_SPECS = [
|
|
|
282
295
|
template: '时序映射模板.json',
|
|
283
296
|
groups: [
|
|
284
297
|
{ key: 'character_timeline', source: 'characters', maxItems: 50 },
|
|
298
|
+
// 角色关系的阶段落点:按关系起点角色切批,每批只写 from 属于本批的行。
|
|
299
|
+
{ key: 'relationship_timeline', source: 'characters', ownerField: 'from', rows: 'optional', maxItems: 8 },
|
|
300
|
+
{ key: 'faction_relationship_timeline', source: 'factions', ownerField: 'from', rows: 'optional', maxItems: 8 },
|
|
285
301
|
{ key: 'conflict_timeline', source: 'conflicts', maxItems: 50 },
|
|
286
302
|
{ key: 'foreshadow_timeline', source: 'foreshadows', maxItems: 50 },
|
|
287
303
|
{ key: 'climax_timeline', source: 'climaxes', maxItems: 50 },
|
|
@@ -490,6 +506,11 @@ export function planStageBatches(opts) {
|
|
|
490
506
|
|
|
491
507
|
for (let fileIndex = 0; fileIndex < specs.length; fileIndex++) {
|
|
492
508
|
const spec = specs[fileIndex];
|
|
509
|
+
// 题材扩展文件是条件产物:骨架没有 other_entities 时整份文件不生成,
|
|
510
|
+
// 不能把"不需要写"误报成"切不出批次"。
|
|
511
|
+
if (spec.optionalSource && (!skeletonEntities || !Array.isArray(skeletonEntities[spec.optionalSource]) || skeletonEntities[spec.optionalSource].length === 0)) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
493
514
|
const templateSpec = readJson(path.join(templateDir, spec.template));
|
|
494
515
|
if (!templateSpec) {
|
|
495
516
|
return {
|
|
@@ -504,6 +525,10 @@ export function planStageBatches(opts) {
|
|
|
504
525
|
|
|
505
526
|
for (let groupIndex = 0; groupIndex < spec.groups.length; groupIndex++) {
|
|
506
527
|
const group = spec.groups[groupIndex];
|
|
528
|
+
// 可选关系组:源实体为空时跳过,不要生成"0 条"的空批次。
|
|
529
|
+
if (group.optionalSource && (!skeletonEntities || !Array.isArray(skeletonEntities[group.optionalSource]) || skeletonEntities[group.optionalSource].length === 0)) {
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
507
532
|
|
|
508
533
|
// 整文件就是一个对象(节奏参数.json):只有一批,条目是整个对象
|
|
509
534
|
if (group.wholeObject) {
|
|
@@ -741,6 +766,11 @@ export function computeKeyChapters(opts) {
|
|
|
741
766
|
if (e && typeof e.chapter === 'number') set.add(e.chapter);
|
|
742
767
|
}
|
|
743
768
|
|
|
769
|
+
// 关系变化章
|
|
770
|
+
for (const r of (Array.isArray(timeline.relationship_timeline) ? timeline.relationship_timeline : [])) {
|
|
771
|
+
if (r && typeof r.start_chapter === 'number') set.add(r.start_chapter);
|
|
772
|
+
}
|
|
773
|
+
|
|
744
774
|
// 钩子章
|
|
745
775
|
const hookTimeline = Array.isArray(timeline.hook_timeline) ? timeline.hook_timeline : [];
|
|
746
776
|
for (const h of hookTimeline) {
|
|
@@ -783,7 +813,11 @@ function makeBatch(ctx) {
|
|
|
783
813
|
objectKeys: ctx.objectKeys || [],
|
|
784
814
|
// details 必须跟着 summary 一起合并:校验器要求两者 id 一一对应,
|
|
785
815
|
// 只合并 summary 会让关键道具.json 永远缺 details(FLESH_DETAILS_ORPHAN)。
|
|
786
|
-
arrays: group.key
|
|
816
|
+
arrays: group.key
|
|
817
|
+
? [group.key, ...(group.detail ? [group.detail] : []), ...(group.extraArrays || [])]
|
|
818
|
+
: [],
|
|
819
|
+
extraArrays: (group.extraArrays || []).slice(),
|
|
820
|
+
optionalArrays: (group.optionalArrays || []).slice(),
|
|
787
821
|
summaryArray: group.key,
|
|
788
822
|
detailArray: group.detail || null,
|
|
789
823
|
shard: shard,
|
|
@@ -89,6 +89,7 @@ export function pickContractSections(content, names) {
|
|
|
89
89
|
*/
|
|
90
90
|
const RELATION_TABLES = {
|
|
91
91
|
character_relations: { match: ['from', 'to'], anchor: ['from'] },
|
|
92
|
+
other_relations: { match: ['from', 'to'], anchor: null },
|
|
92
93
|
faction_membership: { match: ['character', 'faction'], anchor: null },
|
|
93
94
|
faction_relations: { match: ['from', 'to'], anchor: ['from'] },
|
|
94
95
|
conflict_participants: { match: ['conflict', 'protagonist_side', 'main_antagonist', 'secondary_antagonist', 'third_party'], anchor: null },
|
|
@@ -258,6 +259,12 @@ export function buildRhythmChapterRefs(opts) {
|
|
|
258
259
|
mainNodesByCh.get(n.chapter).push({ main_id: m.main_id, event_ref: n.event_ref });
|
|
259
260
|
}
|
|
260
261
|
}
|
|
262
|
+
const relationshipByCh = new Map();
|
|
263
|
+
for (const r of (timeline.relationship_timeline || [])) {
|
|
264
|
+
if (!r || typeof r.start_chapter !== 'number') continue;
|
|
265
|
+
if (!relationshipByCh.has(r.start_chapter)) relationshipByCh.set(r.start_chapter, []);
|
|
266
|
+
relationshipByCh.get(r.start_chapter).push(r);
|
|
267
|
+
}
|
|
261
268
|
const branchByCh = new Map();
|
|
262
269
|
for (const b of (timeline.branch_timeline || [])) {
|
|
263
270
|
if (!b || typeof b !== 'object') continue;
|
|
@@ -284,6 +291,11 @@ export function buildRhythmChapterRefs(opts) {
|
|
|
284
291
|
for (const n of (mainNodesByCh.get(ch) || [])) {
|
|
285
292
|
parts.push('主线节点 ' + n.main_id + nameOf(n.main_id, names) + ':' + str(n.event_ref));
|
|
286
293
|
}
|
|
294
|
+
for (const r of (relationshipByCh.get(ch) || [])) {
|
|
295
|
+
parts.push('关系阶段 ' + r.from + nameOf(r.from, names) + ' → ' + r.to + nameOf(r.to, names) +
|
|
296
|
+
':' + str(r.stage) + ' / ' + str(r.state) +
|
|
297
|
+
(r.trigger_event ? '(触发:' + str(r.trigger_event) + ')' : ''));
|
|
298
|
+
}
|
|
287
299
|
for (const b of (branchByCh.get(ch) || [])) {
|
|
288
300
|
parts.push('支线 ' + b);
|
|
289
301
|
}
|