@cloud411716/fancy-webnovel 1.0.46 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client.js +369 -0
- package/cordis.patch.yml +4 -13
- 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 +26 -38
- package/lib/atomic-write.js +109 -0
- package/lib/chapter-order.js +100 -0
- package/lib/chinese-num.js +111 -0
- package/lib/confirmation.js +80 -0
- package/lib/db-init.js +123 -0
- package/lib/deployed-state.js +141 -0
- package/lib/direction-labels.js +265 -0
- package/lib/error-codes.js +104 -0
- package/lib/infra.js +594 -0
- package/lib/llm-wiki.js +109 -0
- 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 +305 -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 +62 -5
- package/plugins/fancy-analyze/analyze-definitions.md +519 -0
- package/plugins/fancy-analyze/command-input.js +65 -0
- package/plugins/fancy-analyze/index.js +2024 -0
- package/plugins/fancy-analyze/ingest-coverage.js +107 -0
- package/plugins/fancy-analyze/schema-analyze.md +627 -0
- package/plugins/fancy-analyze/schema-write.md +178 -0
- package/plugins/fancy-analyze/scripts/append-summary-chapter.js +612 -0
- package/plugins/fancy-analyze/scripts/split_source.js +292 -0
- package/plugins/fancy-analyze/summary-progress.js +495 -0
- package/plugins/fancy-analyze/templates.js +1179 -0
- package/plugins/fancy-bootstrap/index.js +313 -0
- package/plugins/fancy-bootstrap/scripts/init.js +177 -0
- package/plugins/fancy-bootstrap/scripts/path-utils.js +21 -0
- package/plugins/fancy-bootstrap/templates.js +71 -0
- 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-core.js +1118 -0
- package/plugins/fancy-import/import-driver.js +604 -0
- package/plugins/fancy-import/index.js +738 -0
- package/plugins/fancy-import/prompts/01-import-prompt.md +131 -0
- package/plugins/fancy-import/templates.js +58 -0
- package/plugins/fancy-material/exporter.js +153 -0
- package/plugins/fancy-material/genres.js +256 -0
- package/plugins/fancy-material/index.js +835 -0
- package/plugins/fancy-material/inventory.js +143 -0
- package/plugins/fancy-material/kinds.js +313 -0
- package/plugins/fancy-material/queue.js +55 -0
- package/plugins/fancy-material/scripts/query.js +254 -0
- package/plugins/fancy-material/templates.js +258 -0
- package/plugins/fancy-material/tool-deploy.js +138 -0
- package/plugins/fancy-material/validate.js +149 -0
- package/plugins/fancy-project/index.js +133 -0
- package/plugins/fancy-scan/existing-block.js +100 -0
- package/plugins/fancy-scan/index.js +708 -0
- 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 +667 -0
- package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +923 -0
- package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +469 -0
- package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +629 -0
- 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 +694 -0
- 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/deploy-manifest.js +239 -0
- package/plugins/fancy-story-builder/deslop-driver.js +821 -0
- package/plugins/fancy-story-builder/flesh-output.js +168 -0
- package/plugins/fancy-story-builder/index.js +6861 -0
- package/plugins/fancy-story-builder/patch-driver.js +390 -0
- package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +563 -0
- package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +602 -0
- package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +539 -0
- package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +225 -0
- package/plugins/fancy-story-builder/prompts/05-deslop-prompt.md +96 -0
- package/plugins/fancy-story-builder/purpose-template.md +83 -0
- package/plugins/fancy-story-builder/references/formulas-rhythm.json +302 -0
- package/plugins/fancy-story-builder/references/formulas-skeleton.json +402 -0
- package/plugins/fancy-story-builder/references/genre-profiles.json +3514 -0
- package/plugins/fancy-story-builder/references/glossary.md +594 -0
- package/plugins/fancy-story-builder/references/index.md +581 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/00-index.md +174 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/01-scene.md +150 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/02-chapter-workflow.md +99 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/03-opening-chapter.md +104 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/04-dialogue-mastery.md +274 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/05-genre-formulas.md +432 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/06-tone-ensemble.md +259 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/07-ledger-audit.md +222 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/08-diagnosis-revision.md +205 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/09-reading-power.md +135 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/10-deslop.md +129 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +98 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/01-upgrade-rhythm.md +300 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/02-arc-rhythm.md +168 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/03-highpoint-hooks.md +189 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/04-satisfaction.md +226 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/05-foreshadow-density.md +195 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/06-chapter-outline.md +226 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +181 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/00-index.md +100 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +243 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/02-worldbuilding.md +245 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +185 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/04-emotion-methods.md +286 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +187 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +91 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +209 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/02-packaging.md +135 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +305 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/04-conflict.md +328 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/05-quota.md +187 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +160 -0
- package/plugins/fancy-story-builder/rhythm-output.js +140 -0
- package/plugins/fancy-story-builder/schema-setting.json +1100 -0
- package/plugins/fancy-story-builder/schema-setting.md +232 -0
- package/plugins/fancy-story-builder/scripts/ai-patterns.js +619 -0
- package/plugins/fancy-story-builder/scripts/brief-validate.js +164 -0
- package/plugins/fancy-story-builder/scripts/check-guide-compliance.js +289 -0
- package/plugins/fancy-story-builder/scripts/content-quality.js +416 -0
- package/plugins/fancy-story-builder/scripts/deslop-validate.js +143 -0
- package/plugins/fancy-story-builder/scripts/error-classes.js +112 -0
- package/plugins/fancy-story-builder/scripts/flesh-validate.js +1312 -0
- package/plugins/fancy-story-builder/scripts/formula-engine.js +112 -0
- package/plugins/fancy-story-builder/scripts/graph-utils.js +1484 -0
- package/plugins/fancy-story-builder/scripts/label-coverage.js +171 -0
- package/plugins/fancy-story-builder/scripts/prompt-drift-check.js +441 -0
- package/plugins/fancy-story-builder/scripts/reading-power.js +298 -0
- package/plugins/fancy-story-builder/scripts/reference-log.js +110 -0
- package/plugins/fancy-story-builder/scripts/rhythm-validate.js +1312 -0
- package/plugins/fancy-story-builder/scripts/schema-pages.js +2668 -0
- package/plugins/fancy-story-builder/scripts/semantic-lint.js +381 -0
- package/plugins/fancy-story-builder/scripts/skeleton-quota.js +145 -0
- package/plugins/fancy-story-builder/scripts/skeleton-validate.js +1973 -0
- package/plugins/fancy-story-builder/scripts/stage-batches.js +1405 -0
- package/plugins/fancy-story-builder/scripts/stage-contract.js +429 -0
- package/plugins/fancy-story-builder/scripts/story-artifacts.js +933 -0
- package/plugins/fancy-story-builder/scripts/upstream-query.js +198 -0
- 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/scripts/writing-rules.js +167 -0
- package/plugins/fancy-story-builder/scripts/writing-validate.js +682 -0
- package/plugins/fancy-story-builder/skeleton-output.js +199 -0
- package/plugins/fancy-story-builder/stage-driver.js +1049 -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 +427 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//347/253/240/350/212/202/347/273/206/347/272/262/346/250/241/346/235/277.json +34 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//347/253/240/350/212/202/350/247/204/345/210/222/346/250/241/346/235/277.json +24 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//350/212/202/345/245/217/345/217/202/346/225/260/346/250/241/346/235/277.json +27 -0
- 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 +168 -0
- 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 +184 -0
- 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 +131 -0
- 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 +85 -0
- 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 +103 -0
- 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 +68 -0
- 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 +142 -0
- 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 +292 -0
- 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 +266 -0
- 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 +336 -0
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/261/202/347/272/247/346/250/241/346/235/277.json +65 -0
- 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 +308 -0
- package/plugins/fancy-story-builder/templates.js +1446 -0
- package/plugins/fancy-story-builder/wiki-references.js +540 -0
- package/plugins/fancy-story-builder/writing-driver.js +1388 -0
- package/plugins/fancy-topic/index.js +1057 -0
- 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 +317 -0
- package/plugins/fancy-topic/templates.js +476 -0
- package/plugins/fancy-topic/tool-deploy.js +200 -0
- package/commands/bootstrap/index.js +0 -164
- package/commands/bootstrap/init-project.js +0 -121
- package/commands/scan/index.js +0 -283
- package/commands/scan/platforms/fanqie.js +0 -146
- package/commands/scan/platforms/jinjiang.js +0 -143
- package/commands/scan/platforms/qidian.js +0 -102
- package/commands/scan/platforms/qimao.js +0 -127
- package/commands/scan/scraper.js +0 -195
- package/infra.js +0 -160
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2/genre.js —— 题材 profile:结构、倍率、卷级分配权重、质量阈值。
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const PLUGIN_FILE = path.join(__dirname, '..', '..', 'references', 'genre-profiles.json');
|
|
10
|
+
|
|
11
|
+
export const PROFILE_ALIASES = {
|
|
12
|
+
'玄幻': 'upgrade', '仙侠': 'upgrade', '升级流': 'upgrade',
|
|
13
|
+
'都市': 'urban', '都市爽文': 'urban',
|
|
14
|
+
'悬疑': 'suspense', '推理': 'suspense', '规则怪谈': 'suspense',
|
|
15
|
+
'言情': 'romance', '情感': 'romance',
|
|
16
|
+
'群像': 'ensemble', '正剧': 'ensemble',
|
|
17
|
+
'无限流': 'infinite', '副本': 'infinite',
|
|
18
|
+
'历史': 'history', '军事': 'history',
|
|
19
|
+
'末世': 'apocalypse', '科幻': 'apocalypse',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const MULTIPLIER_BY_FORMULA = {
|
|
23
|
+
'character_elements.volume_supporting_count': 'volume_supporting',
|
|
24
|
+
'character_elements.volume_antagonist_count': 'volume_antagonist',
|
|
25
|
+
'conflict_elements.phase_conflict_count': 'conflicts',
|
|
26
|
+
'conflict_elements.branch_conflict_count': 'conflicts',
|
|
27
|
+
'faction_elements.faction_count': 'factions',
|
|
28
|
+
'faction_elements.faction_relation_count': 'faction_relations',
|
|
29
|
+
'location_elements.key_location_count': 'locations',
|
|
30
|
+
'foreshadow_elements.small_foreshadow_count': 'foreshadows',
|
|
31
|
+
'foreshadow_elements.medium_foreshadow_count': 'foreshadows',
|
|
32
|
+
'foreshadow_elements.large_foreshadow_count': 'foreshadows',
|
|
33
|
+
'climax_elements.small_climax_count': 'climaxes',
|
|
34
|
+
'climax_elements.medium_climax_count': 'climaxes',
|
|
35
|
+
'climax_elements.large_climax_count': 'climaxes',
|
|
36
|
+
'pleasure_elements.small_pleasure_count': 'pleasures',
|
|
37
|
+
'pleasure_elements.medium_pleasure_count': 'pleasures',
|
|
38
|
+
'pleasure_elements.large_pleasure_count': 'pleasures',
|
|
39
|
+
'item_elements.core_item_count': 'items',
|
|
40
|
+
'item_elements.branch_item_count': 'items',
|
|
41
|
+
'item_elements.symbolic_item_count': 'items',
|
|
42
|
+
'world_rule_counts.core': 'world_rules',
|
|
43
|
+
'world_rule_counts.information': 'world_rules',
|
|
44
|
+
'world_rule_counts.social': 'world_rules',
|
|
45
|
+
'fixed.character_elements.protagonist_count': 'protagonist',
|
|
46
|
+
'fixed.character_elements.core_supporting_count': 'core_supporting',
|
|
47
|
+
'fixed.character_elements.core_antagonist_count': 'core_antagonist',
|
|
48
|
+
'fixed.conflict_elements.core_conflict_count': 'conflicts',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function loadGenreProfiles(projectRoot) {
|
|
52
|
+
const candidates = [];
|
|
53
|
+
if (projectRoot) candidates.push(path.join(projectRoot, 'references', 'genre-profiles.json'));
|
|
54
|
+
if (projectRoot) candidates.push(path.join(projectRoot, '设定', '00-baseline', 'genre-profiles.json'));
|
|
55
|
+
candidates.push(PLUGIN_FILE);
|
|
56
|
+
for (const file of candidates) {
|
|
57
|
+
try {
|
|
58
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
59
|
+
if (data && data.profiles) {
|
|
60
|
+
const base = data.base_overrides || {};
|
|
61
|
+
for (const profile of Object.values(data.profiles)) {
|
|
62
|
+
profile.overrides = { ...base, ...(profile.overrides || {}) };
|
|
63
|
+
}
|
|
64
|
+
return data;
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// try next
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
version: '2.1',
|
|
72
|
+
base_words_per_chapter: 2000,
|
|
73
|
+
default_profile: 'default',
|
|
74
|
+
profiles: {
|
|
75
|
+
default: {
|
|
76
|
+
label: '通用',
|
|
77
|
+
structure: { type: 'serial', volume_chapters: [40, 120], preferred_chapters_per_volume: 80, acts: { lt10: 1, '10_25': 2, gt25: 3 } },
|
|
78
|
+
multipliers: {},
|
|
79
|
+
distribution_weights: {},
|
|
80
|
+
quality: {},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function detectProfileKey(topicData, profilesDoc) {
|
|
87
|
+
const profiles = (profilesDoc && profilesDoc.profiles) || {};
|
|
88
|
+
const explicit = topicData && (topicData.genre_profile || topicData.genreProfile);
|
|
89
|
+
if (explicit && profiles[explicit]) return explicit;
|
|
90
|
+
const genre = String(topicData && (topicData.genre || topicData.category) || '');
|
|
91
|
+
if (genre && profiles[genre]) return genre;
|
|
92
|
+
for (const [key, val] of Object.entries(profiles)) {
|
|
93
|
+
const aliases = (val && val.aliases) || [];
|
|
94
|
+
if (aliases.some((alias) => alias && genre.includes(alias))) return key;
|
|
95
|
+
}
|
|
96
|
+
for (const [key, val] of Object.entries(profiles)) {
|
|
97
|
+
if (val && val.label && genre.includes(val.label)) return key;
|
|
98
|
+
}
|
|
99
|
+
for (const [alias, key] of Object.entries(PROFILE_ALIASES)) {
|
|
100
|
+
if (genre.includes(alias) && profiles[key]) return key;
|
|
101
|
+
}
|
|
102
|
+
const keys = Object.keys(profiles);
|
|
103
|
+
return profiles.default ? 'default' : (keys[0] || 'default');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function getProfile(profilesDoc, key) {
|
|
107
|
+
const profiles = (profilesDoc && profilesDoc.profiles) || {};
|
|
108
|
+
const base = (profilesDoc && profilesDoc.base_overrides) || {};
|
|
109
|
+
const profile = profiles[key] || profiles.default || profiles[Object.keys(profiles)[0]] || {
|
|
110
|
+
label: '通用', structure: {}, multipliers: {}, distribution_weights: {}, quality: {},
|
|
111
|
+
};
|
|
112
|
+
return { ...profile, overrides: { ...base, ...(profile.overrides || {}) } };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function multiplierForFormula(key, profile) {
|
|
116
|
+
const cat = MULTIPLIER_BY_FORMULA[key];
|
|
117
|
+
if (!cat) return 1;
|
|
118
|
+
const value = profile && profile.multipliers && profile.multipliers[cat];
|
|
119
|
+
return Number.isFinite(Number(value)) ? Number(value) : 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function multiplierForCategory(category, profile) {
|
|
123
|
+
const value = profile && profile.multipliers && profile.multipliers[category];
|
|
124
|
+
return Number.isFinite(Number(value)) ? Number(value) : 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 把倍率作用到区间。低值向上取整,高值向下取整;
|
|
129
|
+
* 区间为空时以低值为准,保证 LLM 始终有合法值可选。
|
|
130
|
+
*/
|
|
131
|
+
export function applyMultiplier(low, high, multiplier, opts = {}) {
|
|
132
|
+
const m = Number.isFinite(Number(multiplier)) && Number(multiplier) > 0 ? Number(multiplier) : 1;
|
|
133
|
+
const allowZero = Boolean(opts.allowZero);
|
|
134
|
+
const rawLow = Number(low);
|
|
135
|
+
const rawHigh = Number(high);
|
|
136
|
+
let nextLow = Math.ceil(rawLow * m);
|
|
137
|
+
let nextHigh = Math.floor(rawHigh * m);
|
|
138
|
+
if (!allowZero && rawLow > 0) nextLow = Math.max(1, nextLow);
|
|
139
|
+
if (nextHigh < nextLow) nextHigh = nextLow;
|
|
140
|
+
return { low: nextLow, high: nextHigh, multiplier: m };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function structureVolumeRange(T, profile) {
|
|
144
|
+
const total = Number(T) || 0;
|
|
145
|
+
const cfg = profile && profile.structure && profile.structure.volume_chapters;
|
|
146
|
+
const minCh = cfg && Number(cfg[0]) > 0 ? Number(cfg[0]) : 40;
|
|
147
|
+
const maxCh = cfg && Number(cfg[1]) >= minCh ? Number(cfg[1]) : 120;
|
|
148
|
+
const low = Math.max(1, Math.ceil(total / maxCh));
|
|
149
|
+
const high = Math.max(low, Math.min(Math.max(1, Math.ceil(total / minCh)), 30));
|
|
150
|
+
return { low, high };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function preferredVolumeCount(T, profile, low, high) {
|
|
154
|
+
const pref = Number(profile && profile.structure && profile.structure.preferred_chapters_per_volume);
|
|
155
|
+
const p = Number.isFinite(pref) && pref > 0 ? pref : 80;
|
|
156
|
+
return Math.min(high, Math.max(low, Math.round((Number(T) || 0) / p)));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function actCountForChapters(chapters, profile) {
|
|
160
|
+
const n = Number(chapters) || 0;
|
|
161
|
+
const acts = (profile && profile.structure && profile.structure.acts) || {};
|
|
162
|
+
if (n < 10) return Number(acts.lt10) || 1;
|
|
163
|
+
if (n <= 25) return Number(acts['10_25']) || 2;
|
|
164
|
+
return Number(acts.gt25) || 3;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function formulaOverrides(profile) {
|
|
168
|
+
return (profile && profile.overrides) || {};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 按卷位置生成权重。V=1 全是 1;V=2 用 first/last;
|
|
173
|
+
* V>=3 用 first + middle... + climax + last。
|
|
174
|
+
*/
|
|
175
|
+
export function distributionWeights(profile, type, V) {
|
|
176
|
+
const n = Math.max(1, Number(V) || 1);
|
|
177
|
+
const cfg = (profile && profile.distribution_weights && profile.distribution_weights[type]) || {};
|
|
178
|
+
const first = Number.isFinite(Number(cfg.first)) ? Number(cfg.first) : 1;
|
|
179
|
+
const middle = Number.isFinite(Number(cfg.middle)) ? Number(cfg.middle) : 1;
|
|
180
|
+
const climax = Number.isFinite(Number(cfg.climax)) ? Number(cfg.climax) : middle;
|
|
181
|
+
const last = Number.isFinite(Number(cfg.last)) ? Number(cfg.last) : middle;
|
|
182
|
+
if (n === 1) return [1];
|
|
183
|
+
if (n === 2) return [first, last];
|
|
184
|
+
const out = [first];
|
|
185
|
+
for (let i = 1; i < n - 2; i += 1) out.push(middle);
|
|
186
|
+
out.push(climax);
|
|
187
|
+
out.push(last);
|
|
188
|
+
return out.slice(0, n);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function qualityOf(profile, key, fallback) {
|
|
192
|
+
const q = (profile && profile.quality) || {};
|
|
193
|
+
const value = q[key];
|
|
194
|
+
return value === undefined ? fallback : value;
|
|
195
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2/knowledge.js —— 角色知识时间线。
|
|
3
|
+
*
|
|
4
|
+
* 设计原则:知识不是“某一章谁在谁不在”的静态名单,而是一条可回放的事件流:
|
|
5
|
+
*
|
|
6
|
+
* subject_type + subject_id + character + chapter
|
|
7
|
+
* + change(获知/部分获知/确认/纠正/遗忘/误信)
|
|
8
|
+
* + coverage(unknown/partial/full/false)
|
|
9
|
+
* + knowledge(具体知道了什么)
|
|
10
|
+
* + how(亲眼看到/他人告知/推断/文书/传闻/亲身经历/其他)
|
|
11
|
+
* + source_character / scene / evidence
|
|
12
|
+
*
|
|
13
|
+
* 只要按章节顺序回放事件,就能知道每个角色在任意章节结束时到底知道什么。
|
|
14
|
+
* 这样“第 10 章已经知道,第 20 章又写不知道”会在结构化数据层直接报错。
|
|
15
|
+
*/
|
|
16
|
+
import { asArray } from './core.js';
|
|
17
|
+
|
|
18
|
+
export const KNOWLEDGE_SUBJECT_TYPES = ['information', 'foreshadow'];
|
|
19
|
+
export const KNOWLEDGE_STATES = ['unknown', 'partial', 'full', 'false'];
|
|
20
|
+
export const KNOWLEDGE_CHANGES = ['learns', 'partially_learns', 'confirms', 'corrects', 'forgets', 'misinforms'];
|
|
21
|
+
export const KNOWLEDGE_HOWS = ['witnessed', 'told', 'inferred', 'document', 'rumor', 'experience', 'other', 'none'];
|
|
22
|
+
|
|
23
|
+
export function knowledgeKey(subjectType, subjectId, character) {
|
|
24
|
+
return [subjectType, subjectId, character].map(String).join('|');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function eventKey(event) {
|
|
28
|
+
return [event && event.subject_type, event && event.subject_id, event && event.character, event && event.change, event && event.coverage].map(String).join('|');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function stateAfterChange(change, coverage) {
|
|
32
|
+
if (coverage) return coverage;
|
|
33
|
+
if (change === 'forgets') return 'unknown';
|
|
34
|
+
if (change === 'misinforms') return 'false';
|
|
35
|
+
if (change === 'confirms') return 'full';
|
|
36
|
+
if (change === 'partially_learns') return 'partial';
|
|
37
|
+
return 'full';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function infoIds(cast) {
|
|
41
|
+
return new Set(asArray(cast && cast.information && cast.information.information_gaps).map((g) => g && g.id).filter(Boolean));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function foreshadowIds(cast) {
|
|
45
|
+
return new Set(asArray(cast && cast.foreshadows && cast.foreshadows.foreshadows).map((g) => g && g.id).filter(Boolean));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function charIds(cast) {
|
|
49
|
+
return new Set(asArray(cast && cast.characters && cast.characters.characters).map((c) => c && c.id).filter(Boolean));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function knownCharacters(value) {
|
|
53
|
+
return new Set(asArray(value).map(String));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 从 30-cast 的初始知识/旧版 who_knows 生成时间线起点。 */
|
|
57
|
+
export function initialKnowledgeStates(cast) {
|
|
58
|
+
const states = new Map();
|
|
59
|
+
for (const gap of asArray(cast && cast.information && cast.information.information_gaps)) {
|
|
60
|
+
if (!gap || !gap.id) continue;
|
|
61
|
+
const rows = asArray(gap.initial_knowledge);
|
|
62
|
+
if (rows.length) {
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
if (!row || !row.character) continue;
|
|
65
|
+
states.set(knowledgeKey('information', gap.id, row.character), {
|
|
66
|
+
state: KNOWLEDGE_STATES.includes(row.state) ? row.state : 'unknown',
|
|
67
|
+
coverage: KNOWLEDGE_STATES.includes(row.coverage) ? row.coverage : (row.state || 'unknown'),
|
|
68
|
+
knowledge: row.knowledge || '',
|
|
69
|
+
how: row.how || 'none',
|
|
70
|
+
source_character: row.source_character || null,
|
|
71
|
+
scene: row.scene || '',
|
|
72
|
+
evidence: row.evidence || '',
|
|
73
|
+
chapter: 0,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// 旧版字段的语义:who_knows = 开篇已完全知道;who_does_not_know = 不知道。
|
|
79
|
+
for (const character of knownCharacters(gap.who_knows)) {
|
|
80
|
+
states.set(knowledgeKey('information', gap.id, character), {
|
|
81
|
+
state: 'full', coverage: 'full', knowledge: gap.info_name || '', how: 'none',
|
|
82
|
+
source_character: null, scene: '', evidence: '', chapter: 0,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
for (const character of knownCharacters(gap.who_does_not_know)) {
|
|
86
|
+
const key = knowledgeKey('information', gap.id, character);
|
|
87
|
+
if (!states.has(key)) {
|
|
88
|
+
states.set(key, {
|
|
89
|
+
state: 'unknown', coverage: 'unknown', knowledge: '不知道这条信息', how: 'none',
|
|
90
|
+
source_character: null, scene: '', evidence: '', chapter: 0,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const fs of asArray(cast && cast.foreshadows && cast.foreshadows.foreshadows)) {
|
|
96
|
+
if (!fs || !fs.id) continue;
|
|
97
|
+
for (const row of asArray(fs.initial_knowledge)) {
|
|
98
|
+
if (!row || !row.character) continue;
|
|
99
|
+
states.set(knowledgeKey('foreshadow', fs.id, row.character), {
|
|
100
|
+
state: KNOWLEDGE_STATES.includes(row.state) ? row.state : 'unknown',
|
|
101
|
+
coverage: KNOWLEDGE_STATES.includes(row.coverage) ? row.coverage : (row.state || 'unknown'),
|
|
102
|
+
knowledge: row.knowledge || '',
|
|
103
|
+
how: row.how || 'none',
|
|
104
|
+
source_character: row.source_character || null,
|
|
105
|
+
scene: row.scene || '',
|
|
106
|
+
evidence: row.evidence || '',
|
|
107
|
+
chapter: 0,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return states;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function validateInitialKnowledge(rows, ctx = {}) {
|
|
115
|
+
const errors = [];
|
|
116
|
+
const warnings = [];
|
|
117
|
+
const characters = charIds(ctx.cast);
|
|
118
|
+
const seen = new Set();
|
|
119
|
+
asArray(rows).forEach((row, index) => {
|
|
120
|
+
const p = (ctx.path || 'initial_knowledge') + '[' + index + ']';
|
|
121
|
+
if (!row || !row.character) {
|
|
122
|
+
errors.push({ code: 'KNOWLEDGE_INITIAL_CHARACTER', path: p + '.character', message: '初始知识必须写人物' });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (!characters.has(row.character)) {
|
|
126
|
+
errors.push({ code: 'KNOWLEDGE_INITIAL_CHARACTER_REF', path: p + '.character', message: '初始知识引用了不存在的人物:' + row.character });
|
|
127
|
+
}
|
|
128
|
+
const key = String(row.character);
|
|
129
|
+
if (seen.has(key)) errors.push({ code: 'KNOWLEDGE_INITIAL_DUP', path: p + '.character', message: '同一人物重复登记初始知识:' + row.character });
|
|
130
|
+
seen.add(key);
|
|
131
|
+
if (!KNOWLEDGE_STATES.includes(row.state)) {
|
|
132
|
+
errors.push({ code: 'KNOWLEDGE_INITIAL_STATE', path: p + '.state', message: '初始知识状态必须是 unknown/partial/full/false', actual: row.state });
|
|
133
|
+
}
|
|
134
|
+
if (!KNOWLEDGE_HOWS.includes(row.how || 'none')) {
|
|
135
|
+
errors.push({ code: 'KNOWLEDGE_INITIAL_HOW', path: p + '.how', message: '初始知识的来源方式不合法', actual: row.how });
|
|
136
|
+
}
|
|
137
|
+
if ((row.how === 'told') && !row.source_character) {
|
|
138
|
+
errors.push({ code: 'KNOWLEDGE_INITIAL_SOURCE', path: p + '.source_character', message: 'how=told 时必须写 source_character' });
|
|
139
|
+
}
|
|
140
|
+
if (row.source_character && !characters.has(row.source_character)) {
|
|
141
|
+
errors.push({ code: 'KNOWLEDGE_INITIAL_SOURCE_REF', path: p + '.source_character', message: '初始知识来源人物不存在:' + row.source_character });
|
|
142
|
+
}
|
|
143
|
+
if (!row.knowledge || !String(row.knowledge).trim()) {
|
|
144
|
+
warnings.push({ code: 'KNOWLEDGE_INITIAL_EMPTY', path: p + '.knowledge', message: '初始知识没有写具体知道/不知道什么' });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return { errors, warnings };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 单条知识事件的结构校验。 */
|
|
151
|
+
export function validateKnowledgeEvent(event, ctx = {}) {
|
|
152
|
+
const errors = [];
|
|
153
|
+
const warnings = [];
|
|
154
|
+
const p = ctx.path || 'knowledge_events';
|
|
155
|
+
if (!event || typeof event !== 'object') {
|
|
156
|
+
errors.push({ code: 'KNOWLEDGE_EVENT_OBJECT', path: p, message: '知识事件必须是对象' });
|
|
157
|
+
return { errors, warnings };
|
|
158
|
+
}
|
|
159
|
+
if (!KNOWLEDGE_SUBJECT_TYPES.includes(event.subject_type)) {
|
|
160
|
+
errors.push({ code: 'KNOWLEDGE_SUBJECT_TYPE', path: p + '.subject_type', message: 'subject_type 必须是 information/foreshadow', actual: event.subject_type });
|
|
161
|
+
}
|
|
162
|
+
const subjectSet = event.subject_type === 'information' ? infoIds(ctx.cast) : foreshadowIds(ctx.cast);
|
|
163
|
+
if (!event.subject_id || !subjectSet.has(event.subject_id)) {
|
|
164
|
+
errors.push({ code: 'KNOWLEDGE_SUBJECT_REF', path: p + '.subject_id', message: '知识事件引用了不存在的对象:' + event.subject_id });
|
|
165
|
+
}
|
|
166
|
+
if (!event.character || !charIds(ctx.cast).has(event.character)) {
|
|
167
|
+
errors.push({ code: 'KNOWLEDGE_CHARACTER_REF', path: p + '.character', message: '知识事件引用了不存在的人物:' + event.character });
|
|
168
|
+
}
|
|
169
|
+
if (!KNOWLEDGE_CHANGES.includes(event.change)) {
|
|
170
|
+
errors.push({ code: 'KNOWLEDGE_CHANGE', path: p + '.change', message: 'change 必须是 learns/partially_learns/confirms/corrects/forgets/misinforms', actual: event.change });
|
|
171
|
+
}
|
|
172
|
+
if (!KNOWLEDGE_STATES.includes(event.coverage)) {
|
|
173
|
+
errors.push({ code: 'KNOWLEDGE_COVERAGE', path: p + '.coverage', message: 'coverage 必须是 unknown/partial/full/false', actual: event.coverage });
|
|
174
|
+
} else {
|
|
175
|
+
const expected = {
|
|
176
|
+
learns: ['partial', 'full'],
|
|
177
|
+
partially_learns: ['partial'],
|
|
178
|
+
confirms: ['full'],
|
|
179
|
+
corrects: ['partial', 'full'],
|
|
180
|
+
forgets: ['unknown'],
|
|
181
|
+
misinforms: ['false'],
|
|
182
|
+
}[event.change] || [];
|
|
183
|
+
if (expected.length && !expected.includes(event.coverage)) {
|
|
184
|
+
errors.push({ code: 'KNOWLEDGE_COVERAGE_CHANGE', path: p + '.coverage', message: 'change 与 coverage 不匹配', expected: expected.join('/'), actual: event.coverage });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (!KNOWLEDGE_HOWS.includes(event.how)) {
|
|
188
|
+
errors.push({ code: 'KNOWLEDGE_HOW', path: p + '.how', message: 'how 必须是 witnessed/told/inferred/document/rumor/experience/other', actual: event.how });
|
|
189
|
+
}
|
|
190
|
+
if (event.how === 'told' && !event.source_character) {
|
|
191
|
+
errors.push({ code: 'KNOWLEDGE_SOURCE', path: p + '.source_character', message: 'how=told 时必须写 source_character' });
|
|
192
|
+
}
|
|
193
|
+
if (event.source_character && !charIds(ctx.cast).has(event.source_character)) {
|
|
194
|
+
errors.push({ code: 'KNOWLEDGE_SOURCE_REF', path: p + '.source_character', message: '来源人物不存在:' + event.source_character });
|
|
195
|
+
}
|
|
196
|
+
if (event.source_character && event.source_character === event.character) {
|
|
197
|
+
errors.push({ code: 'KNOWLEDGE_SOURCE_SELF', path: p + '.source_character', message: '来源人物不能是自己' });
|
|
198
|
+
}
|
|
199
|
+
if (!event.knowledge || !String(event.knowledge).trim()) {
|
|
200
|
+
errors.push({ code: 'KNOWLEDGE_WHAT', path: p + '.knowledge', message: '必须写清楚角色具体知道了什么' });
|
|
201
|
+
}
|
|
202
|
+
if (!event.scene || !String(event.scene).trim()) {
|
|
203
|
+
warnings.push({ code: 'KNOWLEDGE_SCENE', path: p + '.scene', message: '建议写清楚在什么场景/地点知道' });
|
|
204
|
+
}
|
|
205
|
+
if (!event.evidence || !String(event.evidence).trim()) {
|
|
206
|
+
warnings.push({ code: 'KNOWLEDGE_EVIDENCE', path: p + '.evidence', message: '建议写清楚依据/看到/听到了什么' });
|
|
207
|
+
}
|
|
208
|
+
if (ctx.chapter && event.character && ctx.chapter.cast && !asArray(ctx.chapter.cast).map(String).includes(String(event.character))
|
|
209
|
+
&& !asArray(ctx.chapter.mentioned).map(String).includes(String(event.character))) {
|
|
210
|
+
warnings.push({ code: 'KNOWLEDGE_CHARACTER_NOT_PRESENT', path: p + '.character', message: '人物既不在本章出场也不在提及列表里' });
|
|
211
|
+
}
|
|
212
|
+
return { errors, warnings };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 回放全书知识事件,返回每个章节结束时的知识快照。 */
|
|
216
|
+
export function replayKnowledge(chapters, cast) {
|
|
217
|
+
const states = initialKnowledgeStates(cast);
|
|
218
|
+
const snapshots = new Map();
|
|
219
|
+
const events = [];
|
|
220
|
+
const errors = [];
|
|
221
|
+
const warnings = [];
|
|
222
|
+
const sorted = [...asArray(chapters)].sort((a, b) => Number(a.display_no) - Number(b.display_no));
|
|
223
|
+
for (const chapter of sorted) {
|
|
224
|
+
const displayNo = Number(chapter.display_no);
|
|
225
|
+
for (const event of asArray(chapter.knowledge_events)) {
|
|
226
|
+
const checked = validateKnowledgeEvent(event, { cast, chapter, path: 'chapter_map.chapters[' + (displayNo - 1) + '].knowledge_events' });
|
|
227
|
+
errors.push(...checked.errors);
|
|
228
|
+
warnings.push(...checked.warnings);
|
|
229
|
+
const key = knowledgeKey(event.subject_type, event.subject_id, event.character);
|
|
230
|
+
const previous = states.get(key) || { state: 'unknown', coverage: 'unknown' };
|
|
231
|
+
const nextState = stateAfterChange(event.change, event.coverage);
|
|
232
|
+
const basePath = 'chapter_map.chapters[' + (displayNo - 1) + '].knowledge_events';
|
|
233
|
+
if (previous.state === 'full' && ['learns', 'partially_learns', 'misinforms'].includes(event.change)) {
|
|
234
|
+
errors.push({
|
|
235
|
+
code: 'KNOWLEDGE_ORDER',
|
|
236
|
+
path: basePath,
|
|
237
|
+
message: '第 ' + displayNo + ' 章:' + event.character + ' 已经知道 ' + event.subject_id + ',不能再次写成“' + event.change + '”',
|
|
238
|
+
expected: '已知道 full',
|
|
239
|
+
actual: previous.state + ' -> ' + nextState,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (previous.state === 'false' && ['learns', 'partially_learns', 'confirms'].includes(event.change)) {
|
|
243
|
+
errors.push({
|
|
244
|
+
code: 'KNOWLEDGE_ORDER',
|
|
245
|
+
path: basePath,
|
|
246
|
+
message: '第 ' + displayNo + ' 章:' + event.character + ' 对 ' + event.subject_id + ' 之前是误信,必须先 corrects 才能写成 ' + event.change,
|
|
247
|
+
expected: 'corrects',
|
|
248
|
+
actual: previous.state + ' -> ' + nextState,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (previous.state === 'unknown' && event.change === 'confirms') {
|
|
252
|
+
errors.push({
|
|
253
|
+
code: 'KNOWLEDGE_ORDER',
|
|
254
|
+
path: basePath,
|
|
255
|
+
message: '第 ' + displayNo + ' 章:' + event.character + ' 尚未学会 ' + event.subject_id + ',不能直接 confirm',
|
|
256
|
+
expected: 'learns/partially_learns',
|
|
257
|
+
actual: 'unknown -> full',
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
if (previous.state === 'unknown' && event.change === 'forgets') {
|
|
261
|
+
warnings.push({
|
|
262
|
+
code: 'KNOWLEDGE_FORGET_UNKNOWN',
|
|
263
|
+
path: basePath,
|
|
264
|
+
message: '第 ' + displayNo + ' 章:' + event.character + ' 本来不知道 ' + event.subject_id + ',forgets 没有实际变化',
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (previous.state === 'full' && event.change === 'confirms') {
|
|
268
|
+
warnings.push({
|
|
269
|
+
code: 'KNOWLEDGE_REDUNDANT',
|
|
270
|
+
path: basePath,
|
|
271
|
+
message: '第 ' + displayNo + ' 章:' + event.character + ' 已完全知道 ' + event.subject_id + ',重复 confirm',
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
const next = {
|
|
275
|
+
state: nextState,
|
|
276
|
+
coverage: event.coverage || nextState,
|
|
277
|
+
knowledge: event.knowledge || '',
|
|
278
|
+
how: event.how || 'other',
|
|
279
|
+
source_character: event.source_character || null,
|
|
280
|
+
scene: event.scene || '',
|
|
281
|
+
evidence: event.evidence || '',
|
|
282
|
+
chapter: displayNo,
|
|
283
|
+
};
|
|
284
|
+
states.set(key, next);
|
|
285
|
+
events.push({ ...event, chapter: displayNo, before: previous.state, after: next.state });
|
|
286
|
+
}
|
|
287
|
+
snapshots.set(displayNo, new Map(states));
|
|
288
|
+
}
|
|
289
|
+
return { states, snapshots, events, errors, warnings };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** 校验细纲/台账里的知识快照是否与章节地图回放一致。 */
|
|
293
|
+
export function validateKnowledgeSnapshot(rows, expectedStates, ctx = {}) {
|
|
294
|
+
const errors = [];
|
|
295
|
+
const warnings = [];
|
|
296
|
+
asArray(rows).forEach((row, index) => {
|
|
297
|
+
if (!row || typeof row !== 'object') return;
|
|
298
|
+
const p = (ctx.path || 'knowledge') + '[' + index + ']';
|
|
299
|
+
if (!row.subject_type || !row.subject_id || !row.character) {
|
|
300
|
+
errors.push({ code: 'KNOWLEDGE_SNAPSHOT_FIELDS', path: p, message: '知识快照必须写 subject_type、subject_id、character' });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const key = knowledgeKey(row.subject_type, row.subject_id, row.character);
|
|
304
|
+
const expected = expectedStates.get(key);
|
|
305
|
+
const state = row.state || row.coverage;
|
|
306
|
+
if (!expected) {
|
|
307
|
+
warnings.push({ code: 'KNOWLEDGE_SNAPSHOT_UNKNOWN', path: p, message: '章节地图没有登记这条知识:' + key });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (state && state !== expected.state) {
|
|
311
|
+
errors.push({
|
|
312
|
+
code: 'KNOWLEDGE_CONFLICT',
|
|
313
|
+
path: p + '.state',
|
|
314
|
+
message: '第 ' + (ctx.chapterNo || '?') + ' 章:' + row.character + ' 对 ' + row.subject_id + ' 的状态与章节地图冲突',
|
|
315
|
+
expected: expected.state,
|
|
316
|
+
actual: state,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
return { errors, warnings };
|
|
321
|
+
}
|