@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,1446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fancy-story-builder templates — 提示词模板
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { renderInputSnapshot } from './scripts/graph-utils.js';
|
|
9
|
+
import { pickContractSections } from './scripts/stage-contract.js';
|
|
10
|
+
import { readingPowerLegend } from './scripts/reading-power.js';
|
|
11
|
+
import { referenceLogPromptBlock } from './scripts/reference-log.js';
|
|
12
|
+
import { deslopLegend } from './scripts/ai-patterns.js';
|
|
13
|
+
import { normalizeLegacyTopicData, TOPIC_REQUIRED_FIELDS } from '#topic-fields';
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const REFERENCES_DIR = join(__dirname, 'references');
|
|
17
|
+
const PROMPTS_DIR = join(__dirname, 'prompts');
|
|
18
|
+
|
|
19
|
+
// 读取内置参考文件
|
|
20
|
+
// - skeleton-prompt.md 等 prompt 文件从 prompts/ 读
|
|
21
|
+
// - 其他参考文件(glossary / formulas / index)从 references/ 读
|
|
22
|
+
|
|
23
|
+
// 给 LLM 看时去掉 prompt 文件顶部维护者元数据(@meta-only sentinel 之前的内容)
|
|
24
|
+
function cleanPromptForLLM(content) {
|
|
25
|
+
// 找到 '@meta-only' 行的索引,丢掉从开头到该行(含)的所有内容
|
|
26
|
+
const idx = content.indexOf('\n@meta-only\n');
|
|
27
|
+
if (idx === -1) return content; // 没有 sentinel,保持原样
|
|
28
|
+
// 取 sentinel 之后的内容;sentinel 之后的 --- 是 markdown 分隔,让 Markdown 渲染器有上下文
|
|
29
|
+
const after = content.slice(idx + '\n@meta-only\n'.length);
|
|
30
|
+
// 去掉紧跟的 --- 行(如果有),但保留后续章节正文
|
|
31
|
+
return after.replace(/^\s*---\s*\n/, '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 模板里的类型 → 人话(提示词用)。
|
|
36
|
+
*
|
|
37
|
+
* 模板 JSON 是唯一真源:类型标签只能由它推出来,
|
|
38
|
+
* 不能在提示词里另写一份(手写的那份一定会漂移)。
|
|
39
|
+
*
|
|
40
|
+
* @param {Object} spec 模板里的字段定义
|
|
41
|
+
* @returns {string} 类型标签
|
|
42
|
+
*/
|
|
43
|
+
function skeletonTypeLabel(spec) {
|
|
44
|
+
if (!spec || typeof spec !== 'object') return '未定义';
|
|
45
|
+
const t = spec.type;
|
|
46
|
+
if (t === 'array') return skeletonTypeLabel(spec.items || {}) + '数组';
|
|
47
|
+
if (t === 'string') return '字符串';
|
|
48
|
+
if (t === 'number' || t === 'integer') return '数字';
|
|
49
|
+
if (t === 'boolean') return '布尔';
|
|
50
|
+
if (t === 'object') return '对象';
|
|
51
|
+
return String(t || '未定义');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readBuiltInReference(filename) {
|
|
55
|
+
// 提示词文件:优先从 prompts/ 找,再 fallback 到 references/
|
|
56
|
+
if (/^\d{2}-.+\.md$/.test(filename) || /skeleton-prompt|flesh-prompt|rhythm-prompt|writing-prompt/.test(filename)) {
|
|
57
|
+
try {
|
|
58
|
+
return readFileSync(join(PROMPTS_DIR, filename), 'utf-8');
|
|
59
|
+
} catch {
|
|
60
|
+
try {
|
|
61
|
+
return readFileSync(join(REFERENCES_DIR, filename), 'utf-8');
|
|
62
|
+
} catch {
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// 其他:从 references/ 读
|
|
68
|
+
try {
|
|
69
|
+
return readFileSync(join(REFERENCES_DIR, filename), 'utf-8');
|
|
70
|
+
} catch {
|
|
71
|
+
return '';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 选题字段的安全取值:数组/字符串/其它都要能安全转成 prompt 里的文本。
|
|
77
|
+
* 之前用 `hard_constraints?.join(';')`,当该字段是字符串(用户手改/旧格式)时
|
|
78
|
+
* 会抛 TypeError 并逃出命令处理器。
|
|
79
|
+
*/
|
|
80
|
+
function joinField(v, sep = ';', fallback = '无') {
|
|
81
|
+
if (Array.isArray(v)) return v.length ? v.map(x => (x === null || x === undefined ? '' : String(x))).filter(Boolean).join(sep) : fallback;
|
|
82
|
+
if (v === undefined || v === null) return fallback;
|
|
83
|
+
if (typeof v === 'string') return v.trim() ? v : fallback;
|
|
84
|
+
return String(v);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 从选题参数兜底出一份任务输入快照。
|
|
89
|
+
*
|
|
90
|
+
* 正常路径是读骨架阶段的 框架.json(input_snapshot + capacity),
|
|
91
|
+
* 那份快照是骨架校验器验过的,更权威。这里只在"骨架产物还没落盘"时兜底。
|
|
92
|
+
*/
|
|
93
|
+
function snapshotFromTopicData(topicData) {
|
|
94
|
+
const t = normalizeLegacyTopicData(topicData || {});
|
|
95
|
+
const out = {};
|
|
96
|
+
for (const id of TOPIC_REQUIRED_FIELDS) {
|
|
97
|
+
if (id === 'hard_constraints' || id === 'anti_tropes') {
|
|
98
|
+
out[id] = Array.isArray(t[id]) ? t[id] : [];
|
|
99
|
+
} else {
|
|
100
|
+
out[id] = t[id];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 把提示词里**行内反引号**包着的项目内路径改成绝对路径。
|
|
108
|
+
*
|
|
109
|
+
* ## 为什么必须改
|
|
110
|
+
*
|
|
111
|
+
* 插件把参考文件(词表 / 公式 / 阶段指导 / 字段模板)拷到**项目根**的
|
|
112
|
+
* `references/` 与 `模板/` 下,但提示词里写的是相对路径
|
|
113
|
+
* (`references/formulas-skeleton.json`)。
|
|
114
|
+
*
|
|
115
|
+
* 相对路径的前提是"当前目录 = 项目根"。这个前提一旦不成立,
|
|
116
|
+
* 路径就会被拼到别的目录下 —— 典型的是拼成
|
|
117
|
+
* `<项目根>/设定/references/formulas-skeleton.json`:
|
|
118
|
+
* `设定/` 是 llm-wiki 的项目目录,插件**从没**往那里放过参考文件,
|
|
119
|
+
* 于是读不到,模型要么空手写、要么以为参考文件缺失。
|
|
120
|
+
*
|
|
121
|
+
* 插件知道项目根,所以在运行时把路径换成绝对的:基准被钉死,
|
|
122
|
+
* 模型不用猜,工具用哪种读法都对。
|
|
123
|
+
*
|
|
124
|
+
* ## 只改行内反引号里的
|
|
125
|
+
*
|
|
126
|
+
* - **代码块里的命令保持相对**:`node references/upstream-query.js …`
|
|
127
|
+
* 是在项目根下执行的命令,绝对化会与"在项目根运行"的语义打架;
|
|
128
|
+
* - **产物路径保持相对**(`设定/raw/sources/…`):那是模型要写的地方,
|
|
129
|
+
* 相对项目根,和校验器读的路径一致;
|
|
130
|
+
* - **`对标/wiki/` 保持相对**:那是 llm-wiki 的项目,相对路径才是它要的形态。
|
|
131
|
+
*
|
|
132
|
+
* @param {string} text 提示词正文
|
|
133
|
+
* @param {string} projectRoot 项目根绝对路径(没有就原样返回)
|
|
134
|
+
* @returns {string} 绝对化后的正文
|
|
135
|
+
*/
|
|
136
|
+
export function absolutizeInlinePaths(text, projectRoot) {
|
|
137
|
+
if (!text || !projectRoot) return text;
|
|
138
|
+
const root = String(projectRoot).replace(/\/+$/, '');
|
|
139
|
+
if (!root) return text;
|
|
140
|
+
return text.replace(/`((?:references|模板)\/[^`\n]*)`/g, (m, p) => '`' + root + '/' + p + '`');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 标量字段的安全取值:undefined/null 会导致 prompt 里出现字面量 "undefined" */
|
|
144
|
+
function textField(v, fallback = '') {
|
|
145
|
+
if (v === undefined || v === null) return fallback;
|
|
146
|
+
return String(v);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const storyBuilder = {
|
|
150
|
+
/**
|
|
151
|
+
* 通知模板
|
|
152
|
+
*/
|
|
153
|
+
/**
|
|
154
|
+
* 通知模板。
|
|
155
|
+
*
|
|
156
|
+
* 解构列表必须**覆盖每个分支里插值到的变量** —— 少一个就是
|
|
157
|
+
* ReferenceError(2026-10-27 实测:writing_done / writing_incomplete /
|
|
158
|
+
* deslop_done / deslop_incomplete 四个分支全部因为缺 count/failed/written/
|
|
159
|
+
* clean/passed/rolledBack/errored 而抛错,写作与去味阶段每次跑完都在
|
|
160
|
+
* 收尾处崩,而 993 个测试全绿 —— 这条路径零覆盖)。
|
|
161
|
+
*
|
|
162
|
+
* 数值给默认值,是为了让"调用方忘了传某个字段"退化成 0 而不是崩。
|
|
163
|
+
*/
|
|
164
|
+
notify({ type, err, phase, id, fileCount, missingCount, queueCount, detail, source, extra, stage, missing, next,
|
|
165
|
+
count = 0, failed = [], written = 0, clean = 0, passed = 0, rolledBack = 0, errored = 0,
|
|
166
|
+
reused = 0 }) {
|
|
167
|
+
switch (type) {
|
|
168
|
+
case 'help':
|
|
169
|
+
return `📖 故事构建 用法
|
|
170
|
+
|
|
171
|
+
/fancy-build scale - 故事规模:总章数、总字数、卷、幕、配额
|
|
172
|
+
/fancy-build macro - 世界观、故事、主线/支线、结局、角色功能位
|
|
173
|
+
/fancy-build cast - 人物、势力、地点、道具、冲突、伏笔、爽点、高潮、信息差
|
|
174
|
+
/fancy-build rhythm - 章节地图:逐章写人物、地点、冲突、钩子、伏笔/爽点动作
|
|
175
|
+
/fancy-build outline - 细纲和场景;示例:outline CH001 / outline 1-10 / outline all
|
|
176
|
+
/fancy-build write - 正文和章末交接;示例:write CH001 / write 1-10 / write all
|
|
177
|
+
/fancy-build deslop - 去 AI 味;示例:deslop CH001 / deslop 1-10 / deslop all
|
|
178
|
+
/fancy-build help - 显示本帮助
|
|
179
|
+
`;
|
|
180
|
+
|
|
181
|
+
case 'v2_done':
|
|
182
|
+
return err || extra || '✅ 阶段完成。';
|
|
183
|
+
|
|
184
|
+
case 'v2_failed': {
|
|
185
|
+
const text = err || extra || '阶段未完成';
|
|
186
|
+
return /^[❌⚠️]/.test(text) ? text : '❌ ' + text;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
case 'v2_info':
|
|
190
|
+
return err || extra || '';
|
|
191
|
+
|
|
192
|
+
case 'cancelled':
|
|
193
|
+
return '🚫 操作已取消。';
|
|
194
|
+
|
|
195
|
+
case 'no_provider':
|
|
196
|
+
return '⚠️ 当前运行环境不支持交互式提问。请在 dsh-tui 中运行该命令,或确认 Web UI 已正确加载交互组件。';
|
|
197
|
+
|
|
198
|
+
case 'no_project_root':
|
|
199
|
+
return '❌ 无法确定当前项目:' + (err || '会话未提供当前工作目录') + '。请在项目目录下运行,或用 /fancy-project use <名字|UUID> 指定当前项目。';
|
|
200
|
+
|
|
201
|
+
case 'ask_failed':
|
|
202
|
+
return '❌ 交互提问失败:' + (err || '未知错误');
|
|
203
|
+
|
|
204
|
+
case 'not_initialized':
|
|
205
|
+
return '⚠️ 当前没有可用项目。请先运行 /fancy-bootstrap 初始化工作区,再运行 /fancy-topic setup 创建项目。';
|
|
206
|
+
|
|
207
|
+
case 'config_corrupt':
|
|
208
|
+
return '❌ ' + (err || '无法解析 .fancy-deployed 状态文件。') + ' 请检查文件内容,或重新运行 /fancy-bootstrap 初始化工作区。';
|
|
209
|
+
|
|
210
|
+
case 'no_project_root':
|
|
211
|
+
return '❌ ' + (err || '无法确定项目目录');
|
|
212
|
+
|
|
213
|
+
case 'no_topic':
|
|
214
|
+
return '⚠️ 未找到选题信息。请先运行 /fancy-topic 完成选题。';
|
|
215
|
+
|
|
216
|
+
case 'setup_id_unavailable': {
|
|
217
|
+
// 分类原因 → 人话 + **针对该原因**的排查步骤。
|
|
218
|
+
// 此前这里只有一句"未设置 setup_id,请先运行 /fancy-build skeleton",
|
|
219
|
+
// 而用户正是运行着这条命令 —— 死胡同,且把真实原因(token 无效 / 服务没起)藏了。
|
|
220
|
+
// 注意:步骤必须按原因分流。给"连不上服务"的人列一堆 token 步骤,
|
|
221
|
+
// 比不给步骤更糟 —— 会把人引到错误的方向上去。
|
|
222
|
+
const reasonText = {
|
|
223
|
+
unauthorized: 'llm-wiki 拒绝了请求(token 无效或服务端未配置 token)',
|
|
224
|
+
network: '无法连接 llm-wiki 服务',
|
|
225
|
+
api_error: 'llm-wiki 返回了错误',
|
|
226
|
+
unexpected_response: 'llm-wiki 返回了非预期结构'
|
|
227
|
+
};
|
|
228
|
+
const stepsByReason = {
|
|
229
|
+
unauthorized: [
|
|
230
|
+
'1. 在 llm-wiki 的「设置 → API + MCP」里填「访问令牌」(可直接点「生成新 token」)',
|
|
231
|
+
'2. 保存设置后,把同一个 token 写进本项目 .fancy-deployed 的 llm_wiki_token 字段',
|
|
232
|
+
'3. 确认 llm-wiki 正在运行:curl -s -o /dev/null -w "%{http_code}" http://localhost:19828/api/v1/health'
|
|
233
|
+
],
|
|
234
|
+
network: [
|
|
235
|
+
'1. 确认 llm-wiki 正在运行:curl -s -o /dev/null -w "%{http_code}" http://localhost:19828/api/v1/health',
|
|
236
|
+
'2. 确认 .fancy-deployed 里的 llm_wiki_url 指向正确地址(默认 http://localhost:19828/api/v1)',
|
|
237
|
+
'3. 启动 llm-wiki 后重试本命令'
|
|
238
|
+
],
|
|
239
|
+
api_error: [
|
|
240
|
+
'1. 查看 llm-wiki 日志或控制台输出,确认服务端返回的具体错误',
|
|
241
|
+
'2. 确认 .fancy-deployed 里的 llm_wiki_url 与 llm_wiki_token 正确'
|
|
242
|
+
],
|
|
243
|
+
unexpected_response: [
|
|
244
|
+
'1. 确认 llm-wiki 版本与本插件兼容(/api/v1/projects/create 应返回 {ok:true, project:{id}})',
|
|
245
|
+
'2. 确认 .fancy-deployed 里的 llm_wiki_url 末尾就是 /api/v1'
|
|
246
|
+
]
|
|
247
|
+
};
|
|
248
|
+
const steps = stepsByReason[err] || [
|
|
249
|
+
'1. 在 llm-wiki 的「设置 → API + MCP」里确认「访问令牌」已填写、「启用本地 HTTP API」已打开',
|
|
250
|
+
'2. 确认 .fancy-deployed 里的 llm_wiki_url 与 llm_wiki_token 正确'
|
|
251
|
+
];
|
|
252
|
+
const head = '❌ 未能建立 llm-wiki 设定项目:' + (reasonText[err] || err || '未知原因');
|
|
253
|
+
const detailLine = detail ? '\n详情:' + detail : '';
|
|
254
|
+
const consequence =
|
|
255
|
+
'\n\n后果:llm-wiki 尚未识别本项目的「设定」目录,' +
|
|
256
|
+
'本阶段产物不会被 ingest 纳管,后续 /fancy-build maintain sync 与 maintain lint 也会失败。';
|
|
257
|
+
const tail = '\n' + (steps.length + 1) +
|
|
258
|
+
'. 处理完成后重新执行本命令,插件会自动将「设定」目录创建为 llm-wiki 项目。';
|
|
259
|
+
return head + detailLine + consequence + '\n处理办法:\n' + steps.join('\n') + tail;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
case 'setup_id_reused': {
|
|
263
|
+
const sourceText = {
|
|
264
|
+
cache: 'llm-wiki 已建好的设定项目',
|
|
265
|
+
deployed: '本项目 .fancy-deployed 已记录',
|
|
266
|
+
remote: 'llm-wiki 项目列表'
|
|
267
|
+
};
|
|
268
|
+
return '✅ 复用 setup_id=' + (id || '?') + '(来源:' + (sourceText[source] || '已有记录') + ')';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
case 'deploy_failed':
|
|
272
|
+
return '❌ 阶段文件部署失败:' + (err || '未知错误') + '。请检查磁盘空间和目录权限后重试。';
|
|
273
|
+
|
|
274
|
+
case 'schema_copy_failed':
|
|
275
|
+
return '❌ 设定目录准备失败:' + (detail || '未知原因') +
|
|
276
|
+
'。\n\n请确认 llm-wiki 正在运行,且 .fancy-deployed 里的 llm_wiki_url / llm_wiki_token 正确。' +
|
|
277
|
+
'插件会先通过 llm-wiki 重建“设定”项目;如果服务不可用,插件不会以空目录继续执行。';
|
|
278
|
+
|
|
279
|
+
case 'llm_wiki_not_configured':
|
|
280
|
+
return '⚠️ llm_wiki 未配置。请在 .fancy-deployed 中配置 llm_wiki_url 和 llm_wiki_token';
|
|
281
|
+
|
|
282
|
+
case 'api_failed':
|
|
283
|
+
return '❌ 调用 llm_wiki API 失败:' + (err || '未知错误') + (extra ? '\n\n' + extra : '');
|
|
284
|
+
|
|
285
|
+
case 'phase_started':
|
|
286
|
+
// 措辞必须准确:agent.send()/followup() 返回 void,只能知道"提示词已入队",
|
|
287
|
+
// 真正的模型调用在后续 agent loop 里异步发生(DSH 对 whenIdle 的说明亦明确:
|
|
288
|
+
// "does not identify the settlement of any particular message")。
|
|
289
|
+
// 说"阶段开始"会让用户以为模型已经在写;实际 DeepSeek 侧 401 / 限流 / 超时,
|
|
290
|
+
// 插件这一侧完全无感,错误只会出现在会话流里。
|
|
291
|
+
return `✅ ${phase}提示词已发送至会话模型,产物将由模型生成。` + (extra ? '\n\n' + extra : '');
|
|
292
|
+
|
|
293
|
+
case 'flesh_incomplete':
|
|
294
|
+
// 血肉阶段没跑完时不能用"阶段开始"的说法(更不能用 ✅):
|
|
295
|
+
// 2026-10 tt5 实测过静默失败——少产出一个文件、全量校验从没跑过,
|
|
296
|
+
// 而插件早就返回了"阶段开始"。现在血肉阶段改成插件逐批派活,
|
|
297
|
+
// 没过的批次由插件用脚本判出来并回喂重试,仍没过就把清单摆出来。
|
|
298
|
+
return '❌ 血肉阶段未完成:部分文件未通过校验,或全量校验(--all)未通过。'
|
|
299
|
+
+ (extra ? '\n\n' + extra : '');
|
|
300
|
+
|
|
301
|
+
case 'flesh_complete':
|
|
302
|
+
// 收尾自检确认跑完了:这时 LLM 那一轮已经结束,"已投递"的说法反而不准
|
|
303
|
+
return '✅ 血肉阶段完成:8 个 JSON 齐全,全量校验(--all)已通过。'
|
|
304
|
+
+ (extra ? '\n\n' + extra : '');
|
|
305
|
+
|
|
306
|
+
case 'stage_dependency_missing':
|
|
307
|
+
return '❌ ' + (stage || '当前') + '阶段未启动:前置的' + (missing || '上游') + '阶段尚未生成。'
|
|
308
|
+
+ (detail ? '(' + detail + ')' : '')
|
|
309
|
+
+ '\n\n请先执行 `' + (next || '/fancy-build help') + '`,等该阶段完成并通过校验后,再执行当前命令。';
|
|
310
|
+
|
|
311
|
+
case 'upstream_not_ready':
|
|
312
|
+
// 节奏阶段的上游门禁没过:继续跑只会产出废数据,必须在这里挡住
|
|
313
|
+
return '❌ 节奏阶段未启动:' + (err || '上游阶段未通过全量校验')
|
|
314
|
+
+ '\n\n节奏阶段依赖骨架和血肉两个上游,且两者的全量校验(--all)都必须已通过。'
|
|
315
|
+
+ '请先完成上游阶段,并确认各自的 .stage-status.json 中 all_check.status = PASSED,再执行节奏阶段。'
|
|
316
|
+
+ (extra ? '\n\n' + extra : '');
|
|
317
|
+
|
|
318
|
+
case 'rhythm_incomplete':
|
|
319
|
+
// 节奏阶段没跑完时不能用"阶段开始"的说法(更不能用 ✅):
|
|
320
|
+
// 节奏的绝大多数校验是跨文件口径的,单文件过了不代表阶段过了。
|
|
321
|
+
return '❌ 节奏阶段未完成:部分批次未通过校验,或全量校验(--all)未通过。'
|
|
322
|
+
+ (extra ? '\n\n' + extra : '');
|
|
323
|
+
|
|
324
|
+
case 'rhythm_complete':
|
|
325
|
+
return '✅ 节奏阶段完成:4 个 JSON 齐全,全量校验(--all)已通过。'
|
|
326
|
+
+ (extra ? '\n\n' + extra : '');
|
|
327
|
+
|
|
328
|
+
case 'skeleton_incomplete':
|
|
329
|
+
// 骨架阶段没跑完时不能用"阶段开始"的说法(更不能用 ✅):
|
|
330
|
+
// 引用完整性 / 图连通性 / 配额一致性都要全量校验才判得了。
|
|
331
|
+
return '❌ 骨架阶段未完成:部分批次未通过校验,或全量校验(--all)未通过。'
|
|
332
|
+
+ (extra ? '\n\n' + extra : '');
|
|
333
|
+
|
|
334
|
+
case 'skeleton_complete':
|
|
335
|
+
return '✅ 骨架阶段完成:4 个 JSON 齐全,全量校验(--all)已通过。'
|
|
336
|
+
+ (extra ? '\n\n' + extra : '');
|
|
337
|
+
|
|
338
|
+
case 'writing_done':
|
|
339
|
+
// 必须说清"新写"和"复用"各多少 —— 旧文案只写 count,
|
|
340
|
+
// 全章都已存在时 count=0,出来的是"0 章全部写完并通过逐章校验",
|
|
341
|
+
// 用户会以为没有需要写的章,实际可能是全书都没写。
|
|
342
|
+
return '✅ 写作阶段完成:新写 ' + count + ' 章' +
|
|
343
|
+
(reused > 0 ? ',复用 ' + reused + ' 章(已复检通过)' : '') +
|
|
344
|
+
';全部章节逐章校验通过。' + (extra ? '\n\n' + extra : '');
|
|
345
|
+
|
|
346
|
+
case 'writing_incomplete':
|
|
347
|
+
return '❌ 写作阶段结束,但有 ' + failed.length + ' 章未通过校验(已写 ' + written + ' 章' +
|
|
348
|
+
(reused > 0 ? ',复用 ' + reused + ' 章(已复检通过)' : '') +
|
|
349
|
+
')。未通过章号:' + failed.join('、') +
|
|
350
|
+
'。请修复对应章节后重新执行本命令;已完成的章节将自动跳过。' + (extra ? '\n\n' + extra : '');
|
|
351
|
+
|
|
352
|
+
case 'deslop_no_chapters':
|
|
353
|
+
return '❌ 去味阶段没有可处理的章节:写作目录中未找到 `第NNN章.md` 文件。'
|
|
354
|
+
+ '(目录:' + (extra || '未知') + ')请先运行 /fancy-build write all 生成正文。';
|
|
355
|
+
|
|
356
|
+
case 'deslop_done':
|
|
357
|
+
return `✅ 去味阶段完成:${count} 章已处理(${clean} 章原本已符合要求,${passed} 章改写后复扫通过)。`
|
|
358
|
+
+ (extra ? '\n\n' + extra : '');
|
|
359
|
+
|
|
360
|
+
case 'deslop_incomplete':
|
|
361
|
+
return `❌ 去味阶段结束,但有 ${failed.length} 章未通过去味校验(其中 ${rolledBack} 章改动超 35% 已被回滚,${errored} 章执行出错)。`
|
|
362
|
+
+ '问题章号:' + (failed.length ? failed.join('、') : '无') + '。'
|
|
363
|
+
+ '原文已备份在 `设定/raw/sources/写作/.revisions/`,报告见 `审查报告/`。'
|
|
364
|
+
+ (extra ? '\n\n' + extra : '');
|
|
365
|
+
|
|
366
|
+
case 'no_subcommand':
|
|
367
|
+
return `⚠️ 请指定阶段:
|
|
368
|
+
|
|
369
|
+
/fancy-build scale
|
|
370
|
+
/fancy-build macro
|
|
371
|
+
/fancy-build cast
|
|
372
|
+
/fancy-build rhythm
|
|
373
|
+
/fancy-build outline [CH001 | 1-10 | all]
|
|
374
|
+
/fancy-build write [CH001 | 1-10 | all]
|
|
375
|
+
/fancy-build deslop [CH001 | 1-10 | all]
|
|
376
|
+
/fancy-build help`;
|
|
377
|
+
|
|
378
|
+
case 'no_sources_dir':
|
|
379
|
+
return '⚠️ 未找到 raw/sources 目录。请先执行骨架、血肉或节奏阶段,生成设定文件。';
|
|
380
|
+
|
|
381
|
+
case 'no_files_to_ingest':
|
|
382
|
+
return '⚠️ raw/sources 目录中未找到可 ingest 的文件。';
|
|
383
|
+
|
|
384
|
+
case 'ingest_started':
|
|
385
|
+
return `✅ 已启动 ingest,共 ${fileCount} 个文件正在处理。稍后可执行 /fancy-build maintain lint 进行检查。` + (extra ? '\n\n' + extra : '');
|
|
386
|
+
|
|
387
|
+
case 'ingest_failed':
|
|
388
|
+
return '❌ ingest 失败:' + (err || '未知错误');
|
|
389
|
+
|
|
390
|
+
case 'ingest_not_ready':
|
|
391
|
+
return `⚠️ ingest 尚未完成。当前有 ${missingCount} 个文件未处理,${queueCount} 个任务进行中。请等待 ingest 结束后再执行分析。`;
|
|
392
|
+
|
|
393
|
+
case 'sync_done':
|
|
394
|
+
return '✅ 故事对象已同步到 llm_wiki。' + (extra ? '\n\n' + extra : '');
|
|
395
|
+
|
|
396
|
+
case 'sync_failed':
|
|
397
|
+
return '❌ 同步到 llm_wiki 失败。' + (extra ? '\n\n' + extra : '');
|
|
398
|
+
|
|
399
|
+
case 'query_done':
|
|
400
|
+
return extra || '💬 查询完成';
|
|
401
|
+
|
|
402
|
+
case 'lint_done':
|
|
403
|
+
return '❌ 检查发现问题' + (extra ? '\n\n' + extra : '') +
|
|
404
|
+
'\n\n💡 可执行 `/fancy-build fix`:插件会把问题归因到骨架/血肉/节奏,发给 LLM 修复、跑阶段校验并重新 sync。';
|
|
405
|
+
|
|
406
|
+
case 'lint_clean':
|
|
407
|
+
return '✅ 检查通过' + (extra ? '\n\n' + extra : '');
|
|
408
|
+
|
|
409
|
+
case 'stage_materialize_failed':
|
|
410
|
+
return '❌ ' + (phase || '当前') + '阶段数据已完成,但同步到 llm_wiki 失败。'
|
|
411
|
+
+ (extra ? '\n\n' + extra : '');
|
|
412
|
+
|
|
413
|
+
case 'patch_done':
|
|
414
|
+
return extra || '✅ patch 完成';
|
|
415
|
+
|
|
416
|
+
case 'patch_cancelled':
|
|
417
|
+
return '已取消 patch,未修改任何文件。';
|
|
418
|
+
|
|
419
|
+
default:
|
|
420
|
+
return String(type);
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* 子命令询问
|
|
426
|
+
*/
|
|
427
|
+
askSubcommand() {
|
|
428
|
+
return {
|
|
429
|
+
id: 'subcommand',
|
|
430
|
+
question: '请选择要执行的阶段:',
|
|
431
|
+
options: [
|
|
432
|
+
{ label: 'scale', description: '10-scale:只算章/卷/幕/配额/节奏预算' },
|
|
433
|
+
{ label: 'macro', description: '20-macro:世界观、卷纲、幕纲、主支线、结局、角色功能位' },
|
|
434
|
+
{ label: 'cast', description: '30-cast:人物、势力、地点、冲突、伏笔、爽点、关系、信息差' },
|
|
435
|
+
{ label: 'rhythm', description: '40-rhythm:章节地图;--full 同时生成全部细纲' },
|
|
436
|
+
{ label: 'outline', description: '40-rhythm:单章细纲 outline CH001 / outline all' },
|
|
437
|
+
{ label: 'writing', description: '50-writing:按单章细纲写正文' },
|
|
438
|
+
{ label: 'sync', description: '同步阶段闭包到 llm_wiki(首次正式同步在 30-cast)' },
|
|
439
|
+
{ label: 'query', description: '向 llm_wiki 提问:query <问题>' },
|
|
440
|
+
{ label: 'lint', description: 'schema / link / 语义检查(30-cast 通过后)' },
|
|
441
|
+
{ label: 'patch', description: '按 manifest 依赖图修订:patch <意见>' },
|
|
442
|
+
{ label: 'help', description: '显示全部子命令说明' },
|
|
443
|
+
],
|
|
444
|
+
};
|
|
445
|
+
},
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* 生成骨架阶段的系统提示词
|
|
449
|
+
*/
|
|
450
|
+
generateSkeletonSystemPrompt({ topicData, wikiReferences }) {
|
|
451
|
+
const {
|
|
452
|
+
book_name,
|
|
453
|
+
genre,
|
|
454
|
+
target_words,
|
|
455
|
+
target_chapters,
|
|
456
|
+
positioning,
|
|
457
|
+
core_conflict,
|
|
458
|
+
hard_constraints,
|
|
459
|
+
anti_tropes,
|
|
460
|
+
} = topicData;
|
|
461
|
+
|
|
462
|
+
// 读取骨架阶段提示词模板(prompts/01-skeleton-prompt.md)
|
|
463
|
+
let prompt = cleanPromptForLLM(readBuiltInReference('01-skeleton-prompt.md'));
|
|
464
|
+
|
|
465
|
+
// 替换变量
|
|
466
|
+
prompt = prompt.replace(/\$\{genre\}/g, textField(genre));
|
|
467
|
+
prompt = prompt.replace(/\$\{book_name\}/g, textField(book_name));
|
|
468
|
+
prompt = prompt.replace(/\$\{target_words\}/g, textField(target_words));
|
|
469
|
+
prompt = prompt.replace(/\$\{target_chapters\}/g, textField(target_chapters));
|
|
470
|
+
prompt = prompt.replace(/\$\{positioning\}/g, textField(positioning));
|
|
471
|
+
prompt = prompt.replace(/\$\{core_conflict\}/g, textField(core_conflict));
|
|
472
|
+
prompt = prompt.replace(/\$\{hard_constraints\}/g, joinField(hard_constraints));
|
|
473
|
+
prompt = prompt.replace(/\$\{anti_tropes\}/g, joinField(anti_tropes));
|
|
474
|
+
prompt = prompt.replace(/\$\{wikiReferences\}/g, wikiReferences || '');
|
|
475
|
+
|
|
476
|
+
return prompt;
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* 生成血肉阶段的系统提示词
|
|
481
|
+
*
|
|
482
|
+
* inputSnapshot:骨架阶段 框架.json 里的任务输入快照(权威来源)。
|
|
483
|
+
* 缺省时退回 topicData 拼一份(骨架产物还没落盘的兜底)。
|
|
484
|
+
*/
|
|
485
|
+
generateFleshSystemPrompt({ topicData, wikiReferences, inputSnapshot }) {
|
|
486
|
+
const { book_name, genre, target_chapters, positioning, hard_constraints, anti_tropes } = topicData;
|
|
487
|
+
|
|
488
|
+
// 读取血肉阶段提示词模板(prompts/02-flesh-prompt.md)
|
|
489
|
+
let prompt = cleanPromptForLLM(readBuiltInReference('02-flesh-prompt.md'));
|
|
490
|
+
|
|
491
|
+
// 替换变量
|
|
492
|
+
prompt = prompt.replace(/\$\{book_name\}/g, textField(book_name));
|
|
493
|
+
prompt = prompt.replace(/\$\{genre\}/g, textField(genre));
|
|
494
|
+
prompt = prompt.replace(/\$\{target_chapters\}/g, textField(target_chapters));
|
|
495
|
+
// 血肉 prompt 文件暂未引用,但保持幂等
|
|
496
|
+
prompt = prompt.replace(/\$\{hard_constraints\}/g, joinField(hard_constraints));
|
|
497
|
+
prompt = prompt.replace(/\$\{anti_tropes\}/g, joinField(anti_tropes));
|
|
498
|
+
prompt = prompt.replace(/\$\{wikiReferences\}/g, wikiReferences || '');
|
|
499
|
+
|
|
500
|
+
// 任务输入清单:优先用骨架阶段的 input_snapshot 快照
|
|
501
|
+
const snapshot = inputSnapshot || snapshotFromTopicData(topicData);
|
|
502
|
+
prompt = prompt.replace(/\$\{input_snapshot_block\}/g, renderInputSnapshot(snapshot));
|
|
503
|
+
|
|
504
|
+
// 移除残余的 ${...} 占位符
|
|
505
|
+
prompt = prompt.replace(/\$\{[a-z_]+\}/g, '');
|
|
506
|
+
|
|
507
|
+
return prompt;
|
|
508
|
+
},
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* 生成血肉阶段的**单轮派活提示词**。
|
|
512
|
+
*
|
|
513
|
+
* 血肉阶段改成插件驱动之后,提示词不再是"一次投喂、LLM 自己跑完 8 个文件"的
|
|
514
|
+
* 执行手册,而是"每批带一遍"的产物契约 + 本轮任务:
|
|
515
|
+
* - 契约段:从 02-flesh-prompt.md 按小节抽取(总纲/铁律/本文件字段契约/质量正反例…)
|
|
516
|
+
* - 本轮任务:写哪个文件、哪些条目、写成什么格式
|
|
517
|
+
* - 重试轮:脚本判出来的错误清单 + 只改被点名字段的要求
|
|
518
|
+
*
|
|
519
|
+
* @param {Object} opts
|
|
520
|
+
* @param {Object} opts.batch 批次描述(stage-batches.js 产出)
|
|
521
|
+
* @param {number} [opts.attempt] 第几次尝试(1 = 首次,>1 = 重试)
|
|
522
|
+
* @param {Array} [opts.failures] 上一轮校验判出来的错误
|
|
523
|
+
* @param {Array} [opts.relations] 与本批相关的骨架关系(已渲染成文本行)
|
|
524
|
+
* @param {string} [opts.stageDir] 血肉目录(相对项目根)
|
|
525
|
+
* @param {string} [opts.buildDir] 分片目录(相对项目根)
|
|
526
|
+
* @param {Object} [opts.inputSnapshot] 骨架 input_snapshot
|
|
527
|
+
* @param {Object} [opts.topicData]
|
|
528
|
+
* @returns {string} 提示词全文
|
|
529
|
+
*/
|
|
530
|
+
generateFleshBatchPrompt(opts) {
|
|
531
|
+
const o = opts || {};
|
|
532
|
+
const batch = o.batch || {};
|
|
533
|
+
const attempt = o.attempt || 1;
|
|
534
|
+
const isRetry = attempt > 1;
|
|
535
|
+
|
|
536
|
+
const contract = readBuiltInReference('02-flesh-prompt.md');
|
|
537
|
+
const snapshot = o.inputSnapshot || snapshotFromTopicData(o.topicData || {});
|
|
538
|
+
const withSnapshot = absolutizeInlinePaths(
|
|
539
|
+
contract
|
|
540
|
+
.replace(/\$\{input_snapshot_block\}/g, renderInputSnapshot(snapshot))
|
|
541
|
+
.replace(/\$\{benchmark_refs_block\}/g, o.benchmarkRefs || ''),
|
|
542
|
+
o.projectRoot
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
// 首轮带全量契约;重试轮只带"本文件字段契约 + 质量 + 错误码速查",
|
|
546
|
+
// 任务输入在首轮已经建立,后续轮不必重复。
|
|
547
|
+
const names = isRetry
|
|
548
|
+
? ['overview', 'file:' + batch.type, 'quality', 'errors', 'forbidden']
|
|
549
|
+
: ['overview', 'input', 'rules', 'refs', 'file:' + batch.type, 'quality', 'forbidden'];
|
|
550
|
+
const picked = pickContractSections(withSnapshot, names);
|
|
551
|
+
if (picked.missing.length) {
|
|
552
|
+
// 契约小节缺失是提示词文件的结构问题,必须让调用方知道
|
|
553
|
+
throw new Error('血肉契约缺少小节:' + picked.missing.join('、'));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const parts = [picked.text];
|
|
557
|
+
parts.push('');
|
|
558
|
+
parts.push('---');
|
|
559
|
+
parts.push('');
|
|
560
|
+
if (o.benchmarkDigest) {
|
|
561
|
+
// 摘要正文必须每轮都在:重试轮不抽 refs 小节,但参考不能跟着消失。
|
|
562
|
+
parts.push(String(o.benchmarkDigest).trim());
|
|
563
|
+
parts.push('');
|
|
564
|
+
parts.push('---');
|
|
565
|
+
parts.push('');
|
|
566
|
+
}
|
|
567
|
+
parts.push(this.buildFleshBatchTask({ batch, attempt, failures: o.failures, relations: o.relations, stageDir: o.stageDir, buildDir: o.buildDir }));
|
|
568
|
+
|
|
569
|
+
return parts.join('\n');
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* 生成节奏阶段的**单轮派活提示词**。
|
|
574
|
+
*
|
|
575
|
+
* 与血肉同构(契约 + 本轮任务),但多两样:
|
|
576
|
+
* - 本轮涉及的章号在**时序映射**里的对应信息(章节号唯一真源在时序映射,
|
|
577
|
+
* 细纲/规划的章号必须跟它对齐)
|
|
578
|
+
* - 上一批的收尾(章节按时间推进,第 N 章要承接第 N-1 章)
|
|
579
|
+
*
|
|
580
|
+
* @param {Object} opts
|
|
581
|
+
* @param {Object} opts.batch 批次描述
|
|
582
|
+
* @param {number} [opts.attempt] 第几次尝试
|
|
583
|
+
* @param {Array} [opts.failures] 上一轮校验判出来的错误
|
|
584
|
+
* @param {Array} [opts.chapterRefs] 本批章号在时序映射里的对应信息(文本行)
|
|
585
|
+
* @param {string} [opts.prevTail] 上一批收尾(文本)
|
|
586
|
+
* @param {string} [opts.stageDir]
|
|
587
|
+
* @param {string} [opts.buildDir]
|
|
588
|
+
* @param {Object} [opts.inputSnapshot]
|
|
589
|
+
* @param {Object} [opts.topicData]
|
|
590
|
+
* @returns {string} 提示词全文
|
|
591
|
+
*/
|
|
592
|
+
generateRhythmBatchPrompt(opts) {
|
|
593
|
+
const o = opts || {};
|
|
594
|
+
const batch = o.batch || {};
|
|
595
|
+
const attempt = o.attempt || 1;
|
|
596
|
+
const isRetry = attempt > 1;
|
|
597
|
+
|
|
598
|
+
const contract = readBuiltInReference('03-rhythm-prompt.md');
|
|
599
|
+
const snapshot = o.inputSnapshot || snapshotFromTopicData(o.topicData || {});
|
|
600
|
+
const withSnapshot = absolutizeInlinePaths(
|
|
601
|
+
contract
|
|
602
|
+
.replace(/\$\{input_snapshot_block\}/g, renderInputSnapshot(snapshot))
|
|
603
|
+
.replace(/\$\{benchmark_refs_block\}/g, o.benchmarkRefs || ''),
|
|
604
|
+
o.projectRoot
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
const names = isRetry
|
|
608
|
+
? ['overview', 'file:' + batch.type, 'consistency', 'errors', 'forbidden']
|
|
609
|
+
: ['overview', 'input', 'rules', 'refs', 'file:' + batch.type, 'consistency', 'forbidden'];
|
|
610
|
+
const picked = pickContractSections(withSnapshot, names);
|
|
611
|
+
if (picked.missing.length) {
|
|
612
|
+
throw new Error('节奏契约缺少小节:' + picked.missing.join('、'));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const parts = [picked.text, '', '---', ''];
|
|
616
|
+
if (o.benchmarkDigest) {
|
|
617
|
+
// 摘要正文必须每轮都在:重试轮不抽 refs 小节,但参考不能跟着消失。
|
|
618
|
+
parts.push(String(o.benchmarkDigest).trim());
|
|
619
|
+
parts.push('', '---', '');
|
|
620
|
+
}
|
|
621
|
+
parts.push(this.buildRhythmBatchTask({
|
|
622
|
+
batch: batch,
|
|
623
|
+
attempt: attempt,
|
|
624
|
+
failures: o.failures,
|
|
625
|
+
chapterRefs: o.chapterRefs,
|
|
626
|
+
prevTail: o.prevTail,
|
|
627
|
+
extraContext: o.extraContext,
|
|
628
|
+
stageDir: o.stageDir,
|
|
629
|
+
buildDir: o.buildDir
|
|
630
|
+
}));
|
|
631
|
+
return parts.join('\n');
|
|
632
|
+
},
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* 节奏单轮任务区块:写哪个文件、写哪几章/哪些条目、写成什么格式。
|
|
636
|
+
*/
|
|
637
|
+
buildRhythmBatchTask(opts) {
|
|
638
|
+
const o = opts || {};
|
|
639
|
+
const batch = o.batch || {};
|
|
640
|
+
const attempt = o.attempt || 1;
|
|
641
|
+
const isRetry = attempt > 1;
|
|
642
|
+
const stageDir = o.stageDir || '设定/raw/sources/节奏';
|
|
643
|
+
const buildDir = o.buildDir || stageDir + '/.rhythm-build';
|
|
644
|
+
const shardPath = buildDir + '/' + batch.shard;
|
|
645
|
+
const targetPath = stageDir + '/' + batch.file;
|
|
646
|
+
|
|
647
|
+
const lines = [];
|
|
648
|
+
lines.push('# 本轮派活:' + batch.file + ' / ' + (batch.groupKey || 'whole'));
|
|
649
|
+
lines.push('');
|
|
650
|
+
lines.push('这是第 ' + attempt + ' 次写这一批。**只写下面列出的内容**,写到指定文件,写完就停。');
|
|
651
|
+
lines.push('');
|
|
652
|
+
|
|
653
|
+
if (isRetry) {
|
|
654
|
+
lines.push('## 上一轮校验没通过(脚本判出来的错误)');
|
|
655
|
+
lines.push('');
|
|
656
|
+
const failures = o.failures || [];
|
|
657
|
+
if (!failures.length) {
|
|
658
|
+
lines.push('(错误清单为空,按上一轮的字段要求重写这一批。)');
|
|
659
|
+
} else {
|
|
660
|
+
for (const f of failures) {
|
|
661
|
+
lines.push('- [' + (f.code || 'ERROR') + '] ' + (f.path || '(无路径)') + ':' + (f.message || '') + (f.hint ? '(' + f.hint + ')' : ''));
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
lines.push('');
|
|
665
|
+
lines.push('**只修上面点名的字段**,其余内容保持原样。');
|
|
666
|
+
lines.push('');
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
lines.push('## 要写到哪里');
|
|
670
|
+
lines.push('');
|
|
671
|
+
lines.push('- 文件路径:`' + shardPath + '`');
|
|
672
|
+
lines.push('- 这是一个**分片**文件:只包含本批内容,不要包含别的,也不要动 `' + targetPath + '`。');
|
|
673
|
+
lines.push('- 如果需要看已产出的内容(保持一致用),可以读 `' + targetPath + '`,但**不要修改它**。');
|
|
674
|
+
lines.push('');
|
|
675
|
+
|
|
676
|
+
lines.push('## 文件格式');
|
|
677
|
+
lines.push('');
|
|
678
|
+
if (batch.wholeObject) {
|
|
679
|
+
lines.push('这个文件是一个 JSON 对象(没有顶层数组)。顶层键固定为:');
|
|
680
|
+
lines.push('');
|
|
681
|
+
lines.push('```');
|
|
682
|
+
lines.push('{');
|
|
683
|
+
lines.push(' "branch_chapter_total": <整数>,');
|
|
684
|
+
lines.push(' "overlap_chapter_count": <整数>,');
|
|
685
|
+
lines.push(' "main_progress_chapter_count": <整数>,');
|
|
686
|
+
lines.push(' "chapter_hook_rate": { "rate": <小数>, "is_minimum": <true|false> },');
|
|
687
|
+
lines.push(' "derivations": { ...7 条约束名... }');
|
|
688
|
+
lines.push('}');
|
|
689
|
+
lines.push('```');
|
|
690
|
+
lines.push('');
|
|
691
|
+
lines.push('**先算参数再填**:按「节奏参数」那一节的步骤 1-7 反推可行区间,不要试错。');
|
|
692
|
+
} else {
|
|
693
|
+
const arrays = batch.arrays || [batch.groupKey];
|
|
694
|
+
const shape = arrays.map((a) => ' "' + a + '": [ ... ]').join(',\n');
|
|
695
|
+
lines.push('文件内容是一个 JSON 对象,顶层键固定为:');
|
|
696
|
+
lines.push('');
|
|
697
|
+
lines.push('```');
|
|
698
|
+
lines.push('{');
|
|
699
|
+
lines.push(shape);
|
|
700
|
+
lines.push('}');
|
|
701
|
+
lines.push('```');
|
|
702
|
+
lines.push('');
|
|
703
|
+
lines.push('键的顺序、缩进不重要,但**顶层键名不能改**,数组里只能有本批条目。');
|
|
704
|
+
}
|
|
705
|
+
lines.push('');
|
|
706
|
+
|
|
707
|
+
if (batch.byChapter) {
|
|
708
|
+
const chapters = (batch.items || []).map((it) => it.ch);
|
|
709
|
+
lines.push('## 本批要写的章(' + chapters.length + ' 章)');
|
|
710
|
+
lines.push('');
|
|
711
|
+
lines.push(chapters.join(', '));
|
|
712
|
+
lines.push('');
|
|
713
|
+
lines.push('**只写这些章,章号原样抄。**');
|
|
714
|
+
lines.push('');
|
|
715
|
+
} else if (batch.rowsMode === 'optional') {
|
|
716
|
+
lines.push('## 本批负责的关系阶段(可 0 行)');
|
|
717
|
+
lines.push('');
|
|
718
|
+
lines.push('本批写 `' + batch.groupKey + '` 里 `' + (batch.ownerField || 'from') + '` 属于下表这些角色的行:');
|
|
719
|
+
lines.push('');
|
|
720
|
+
lines.push('| id | name |');
|
|
721
|
+
lines.push('|---|---|');
|
|
722
|
+
for (const item of batch.items || []) {
|
|
723
|
+
lines.push('| ' + (item.id || '-') + ' | ' + (item.name || '-') + ' |');
|
|
724
|
+
}
|
|
725
|
+
lines.push('');
|
|
726
|
+
lines.push('**没有关系变化就写空数组 `[]`**;有关系变化才写行,不要为了凑数硬编。');
|
|
727
|
+
lines.push('每行只写一个阶段:`from` / `to` / `stage` / `state` / `start_chapter`,可选 `end_chapter` / `trigger_event` / `description`。');
|
|
728
|
+
lines.push('同一对角色按 `start_chapter` 严格递增;`from` 必须是上表里的角色 id。');
|
|
729
|
+
lines.push('');
|
|
730
|
+
} else if (!batch.wholeObject) {
|
|
731
|
+
lines.push('## 本批要写的条目(' + batch.count + ' 条)');
|
|
732
|
+
lines.push('');
|
|
733
|
+
lines.push('| id | name |');
|
|
734
|
+
lines.push('|---|---|');
|
|
735
|
+
for (const item of batch.items || []) {
|
|
736
|
+
lines.push('| ' + (item.id || '-') + ' | ' + (item.name || '-') + ' |');
|
|
737
|
+
}
|
|
738
|
+
lines.push('');
|
|
739
|
+
lines.push('**id 和 name 原样抄,不要改一个字符。**');
|
|
740
|
+
lines.push('');
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (o.chapterRefs && o.chapterRefs.length) {
|
|
744
|
+
lines.push('## 本批章号在时序映射里的对应信息(章节号唯一真源)');
|
|
745
|
+
lines.push('');
|
|
746
|
+
for (const line of o.chapterRefs) lines.push(line);
|
|
747
|
+
lines.push('');
|
|
748
|
+
lines.push('**本批的章号、钩子、高潮、结局必须与上面一致**;不一致会被全量校验拦下。');
|
|
749
|
+
lines.push('');
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (o.prevTail) {
|
|
753
|
+
lines.push('## 上一批收尾(承接用)');
|
|
754
|
+
lines.push('');
|
|
755
|
+
lines.push(o.prevTail);
|
|
756
|
+
lines.push('');
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (o.extraContext && o.extraContext.length) {
|
|
760
|
+
lines.push('## 本批需要的对照数据(插件从上游读出,原样照抄)');
|
|
761
|
+
lines.push('');
|
|
762
|
+
for (const line of o.extraContext) lines.push(line);
|
|
763
|
+
lines.push('');
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
lines.push(referenceLogPromptBlock('节奏', batch));
|
|
767
|
+
lines.push('## 交付');
|
|
768
|
+
lines.push('');
|
|
769
|
+
lines.push('写完 `' + shardPath + '` 就结束本轮。');
|
|
770
|
+
lines.push('');
|
|
771
|
+
lines.push('**只回一句话**(例:`已写入 骨架-层级.b24.branch_span.json,10 条`),然后停下:');
|
|
772
|
+
lines.push('');
|
|
773
|
+
lines.push('- 不要解释设计思路、不要罗列自己检查过哪些项');
|
|
774
|
+
lines.push('- **不要引用错误码说"满足 XXX 要求"** —— 通过与否由插件的校验脚本判定,不是你说了算');
|
|
775
|
+
lines.push('- 不要宣布完成、不要运行任何校验脚本');
|
|
776
|
+
lines.push('');
|
|
777
|
+
|
|
778
|
+
return lines.join('\n');
|
|
779
|
+
},
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* 血肉单轮任务区块:写哪个文件、写哪几条、写成什么格式。
|
|
783
|
+
*/
|
|
784
|
+
buildFleshBatchTask(opts) {
|
|
785
|
+
const o = opts || {};
|
|
786
|
+
const batch = o.batch || {};
|
|
787
|
+
const attempt = o.attempt || 1;
|
|
788
|
+
const isRetry = attempt > 1;
|
|
789
|
+
const stageDir = o.stageDir || '设定/raw/sources/血肉';
|
|
790
|
+
const buildDir = o.buildDir || stageDir + '/.flesh-build';
|
|
791
|
+
const shardPath = buildDir + '/' + batch.shard;
|
|
792
|
+
const targetPath = stageDir + '/' + batch.file;
|
|
793
|
+
|
|
794
|
+
const lines = [];
|
|
795
|
+
lines.push('# 本轮派活:' + batch.file + ' / ' + batch.groupKey);
|
|
796
|
+
lines.push('');
|
|
797
|
+
lines.push('这是第 ' + attempt + ' 次写这一批。**只写下面列出的条目**,写到指定文件,写完就停。');
|
|
798
|
+
lines.push('');
|
|
799
|
+
|
|
800
|
+
if (isRetry) {
|
|
801
|
+
lines.push('## 上一轮校验没通过(脚本判出来的错误)');
|
|
802
|
+
lines.push('');
|
|
803
|
+
const failures = o.failures || [];
|
|
804
|
+
if (!failures.length) {
|
|
805
|
+
lines.push('(错误清单为空,按上一轮的字段要求重写这一批。)');
|
|
806
|
+
} else {
|
|
807
|
+
for (const f of failures) {
|
|
808
|
+
lines.push('- [' + (f.code || 'ERROR') + '] ' + (f.path || '(无路径)') + ':' + (f.message || '') + (f.hint ? '(' + f.hint + ')' : ''));
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
lines.push('');
|
|
812
|
+
lines.push('**只修上面点名的字段**,其余内容保持原样(包括已经写对的条目)。');
|
|
813
|
+
lines.push('');
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
lines.push('## 要写到哪里');
|
|
817
|
+
lines.push('');
|
|
818
|
+
lines.push('- 文件路径:`' + shardPath + '`');
|
|
819
|
+
lines.push('- 这是一个**分片**文件:只包含本批条目,不要包含别的条目,也不要动 `' + targetPath + '`。');
|
|
820
|
+
lines.push('- 如果需要看本文件已经写了什么(保持一致用),可以读 `' + targetPath + '`,但**不要修改它**。');
|
|
821
|
+
lines.push('');
|
|
822
|
+
|
|
823
|
+
lines.push('## 文件格式');
|
|
824
|
+
lines.push('');
|
|
825
|
+
const arrays = batch.arrays || [batch.groupKey];
|
|
826
|
+
const shape = arrays.map((a) => ' "' + a + '": [ ... ]').join(',\n');
|
|
827
|
+
lines.push('文件内容是一个 JSON 对象,顶层键固定为:');
|
|
828
|
+
lines.push('');
|
|
829
|
+
lines.push('```');
|
|
830
|
+
lines.push('{');
|
|
831
|
+
lines.push(shape);
|
|
832
|
+
lines.push('}');
|
|
833
|
+
lines.push('```');
|
|
834
|
+
lines.push('');
|
|
835
|
+
lines.push('键的顺序、缩进不重要,但**顶层键名不能改**,数组里只能有本批条目。');
|
|
836
|
+
lines.push('');
|
|
837
|
+
|
|
838
|
+
lines.push('## 本批要写的条目(' + batch.count + ' 条)');
|
|
839
|
+
lines.push('');
|
|
840
|
+
if (batch.free) {
|
|
841
|
+
lines.push('这一组没有骨架条目,需要你自己写 **' + batch.count + ' 条**。');
|
|
842
|
+
if (batch.groupKey === 'rule_conflict_plan') {
|
|
843
|
+
lines.push('写规则之间的冲突预案:每条两句话——`contradiction` 是哪两条规则(或规则与核心冲突)打架,`solution` 是怎么化解。');
|
|
844
|
+
}
|
|
845
|
+
} else {
|
|
846
|
+
lines.push('| id | name | 类型 |');
|
|
847
|
+
lines.push('|---|---|---|');
|
|
848
|
+
for (const item of batch.items || []) {
|
|
849
|
+
lines.push('| ' + (item.id || '-') + ' | ' + (item.name || '-') + ' | ' + (item.type || item.category || '-') + ' |');
|
|
850
|
+
}
|
|
851
|
+
lines.push('');
|
|
852
|
+
lines.push('**id 和 name 原样抄,不要改一个字符。**');
|
|
853
|
+
}
|
|
854
|
+
lines.push('');
|
|
855
|
+
|
|
856
|
+
if (batch.detailArray) {
|
|
857
|
+
lines.push('## 道具详情必须同时写');
|
|
858
|
+
lines.push('');
|
|
859
|
+
lines.push('本批每个条目都要在 `' + batch.detailArray + '` 数组里写一条**同 id 同 name** 的详情(模板字段:id/name/type/appearance/origin/function_detail/limit/related_characters/related_conflicts)。');
|
|
860
|
+
lines.push('`related_characters` 写角色**名字**,`related_conflicts` 写冲突**名字**。');
|
|
861
|
+
lines.push('');
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (o.relations && o.relations.length) {
|
|
865
|
+
lines.push('## 骨架里与本批相关的关系(必须照抄)');
|
|
866
|
+
lines.push('');
|
|
867
|
+
for (const line of o.relations) lines.push(line);
|
|
868
|
+
lines.push('');
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
lines.push(referenceLogPromptBlock('血肉', batch));
|
|
872
|
+
lines.push('## 交付');
|
|
873
|
+
lines.push('');
|
|
874
|
+
lines.push('写完 `' + shardPath + '` 就结束本轮。');
|
|
875
|
+
lines.push('');
|
|
876
|
+
lines.push('**只回一句话**(例:`已写入 骨架-层级.b24.branch_span.json,10 条`),然后停下:');
|
|
877
|
+
lines.push('');
|
|
878
|
+
lines.push('- 不要解释设计思路、不要罗列自己检查过哪些项');
|
|
879
|
+
lines.push('- **不要引用错误码说"满足 XXX 要求"** —— 通过与否由插件的校验脚本判定,不是你说了算');
|
|
880
|
+
lines.push('- 不要宣布完成、不要运行任何校验脚本');
|
|
881
|
+
lines.push('');
|
|
882
|
+
|
|
883
|
+
return lines.join('\n');
|
|
884
|
+
},
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* 生成节奏阶段的系统提示词
|
|
888
|
+
*
|
|
889
|
+
* inputSnapshot:骨架阶段 框架.json 里的任务输入快照(权威来源)。
|
|
890
|
+
*
|
|
891
|
+
* @deprecated 节奏阶段已改成插件逐批派活(2026-10),
|
|
892
|
+
* 实际用的是 generateRhythmBatchPrompt —— 它按批次从契约里抽小节,
|
|
893
|
+
* 单轮提示词只有十几 KB。这里保留整份提示词的拼装逻辑,
|
|
894
|
+
* 供骨架阶段迁移到同一 driver 之后做对照与回归。
|
|
895
|
+
*/
|
|
896
|
+
/**
|
|
897
|
+
* 生成骨架阶段的单轮提示词。
|
|
898
|
+
*
|
|
899
|
+
* 骨架是第一个阶段:没有上游可读,任务输入直接来自选题。
|
|
900
|
+
* 契约按批次抽小节 —— 框架那一批带公式拓扑,实体/关系/层级那几批
|
|
901
|
+
* 只带自己那个文件的字段契约,不带公式(公式只有框架用得上)。
|
|
902
|
+
*
|
|
903
|
+
* @param {Object} opts
|
|
904
|
+
* @param {Object} opts.batch 批次描述
|
|
905
|
+
* @param {number} [opts.attempt] 第几次尝试
|
|
906
|
+
* @param {Array} [opts.failures] 上一轮校验判出来的错误
|
|
907
|
+
* @param {Array} [opts.idTables] 可引用的 id 表(文本行,关系批次用)
|
|
908
|
+
* @param {Array} [opts.extraContext] 本批需要的对照数据(文本行)
|
|
909
|
+
* @param {string} [opts.stageDir]
|
|
910
|
+
* @param {string} [opts.buildDir]
|
|
911
|
+
* @param {Object} [opts.inputSnapshot]
|
|
912
|
+
* @param {Object} [opts.topicData]
|
|
913
|
+
* @returns {string} 提示词全文
|
|
914
|
+
*/
|
|
915
|
+
generateSkeletonBatchPrompt(opts) {
|
|
916
|
+
const o = opts || {};
|
|
917
|
+
const batch = o.batch || {};
|
|
918
|
+
const attempt = o.attempt || 1;
|
|
919
|
+
const isRetry = attempt > 1;
|
|
920
|
+
|
|
921
|
+
const contract = readBuiltInReference('01-skeleton-prompt.md');
|
|
922
|
+
const snapshot = o.inputSnapshot || snapshotFromTopicData(o.topicData || {});
|
|
923
|
+
const withSnapshot = absolutizeInlinePaths(
|
|
924
|
+
contract
|
|
925
|
+
.replace(/\$\{input_snapshot_block\}/g, renderInputSnapshot(snapshot))
|
|
926
|
+
// 参考分层说明(插件指导 vs 对标样本)+ 本阶段可读的对标文件清单
|
|
927
|
+
.replace(/\$\{benchmark_refs_block\}/g, o.benchmarkRefs || ''),
|
|
928
|
+
o.projectRoot
|
|
929
|
+
);
|
|
930
|
+
|
|
931
|
+
const names = isRetry
|
|
932
|
+
? ['overview', 'file:' + batch.type, 'consistency', 'errors', 'forbidden']
|
|
933
|
+
: ['overview', 'input', 'rules', 'refs', 'file:' + batch.type, 'consistency', 'forbidden'];
|
|
934
|
+
const picked = pickContractSections(withSnapshot, names);
|
|
935
|
+
if (picked.missing.length) {
|
|
936
|
+
throw new Error('骨架契约缺少小节:' + picked.missing.join('、'));
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const parts = [picked.text, '', '---', ''];
|
|
940
|
+
if (o.benchmarkDigest) {
|
|
941
|
+
// 摘要正文必须每轮都在:重试轮不抽 refs 小节,但参考不能跟着消失。
|
|
942
|
+
parts.push(String(o.benchmarkDigest).trim());
|
|
943
|
+
parts.push('', '---', '');
|
|
944
|
+
}
|
|
945
|
+
parts.push(this.buildSkeletonBatchTask({
|
|
946
|
+
batch: batch,
|
|
947
|
+
attempt: attempt,
|
|
948
|
+
failures: o.failures,
|
|
949
|
+
idTables: o.idTables,
|
|
950
|
+
extraContext: o.extraContext,
|
|
951
|
+
existingEntities: o.existingEntities,
|
|
952
|
+
stageDir: o.stageDir,
|
|
953
|
+
buildDir: o.buildDir
|
|
954
|
+
}));
|
|
955
|
+
return parts.join('\n');
|
|
956
|
+
},
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* 从模板 JSON 生成"本批字段的类型契约"(**动态注入,不手写**)。
|
|
960
|
+
*
|
|
961
|
+
* 为什么要动态生成:模板 JSON 是校验器和提示词的**共同真源**,
|
|
962
|
+
* 手写类型表一定会漂移。
|
|
963
|
+
*
|
|
964
|
+
* 线上实测(tt5 2026-10-25):关系模板里 protagonist_side /
|
|
965
|
+
* secondary_antagonist / third_party 是**字符串数组**,
|
|
966
|
+
* 而提示词只列了字段名、没说是数组(prompts/01-skeleton-prompt.md:346-356),
|
|
967
|
+
* 模型就写成了字符串 → 525 条 TYPE 错误 →
|
|
968
|
+
* 中途校验反复点名 conflict_participants 这批 →
|
|
969
|
+
* 3 次重启上限 → 关系段停摆 → 层级段没开工 → 骨架阶段未完成。
|
|
970
|
+
*
|
|
971
|
+
* @param {Object} batch 批次描述
|
|
972
|
+
* @returns {string[]} 提示词行(空数组 = 本批没有可说明的字段)
|
|
973
|
+
*/
|
|
974
|
+
renderSkeletonFieldTypes(batch) {
|
|
975
|
+
const templateFile = batch && batch.template;
|
|
976
|
+
if (!templateFile) return [];
|
|
977
|
+
let tpl;
|
|
978
|
+
try {
|
|
979
|
+
tpl = JSON.parse(readFileSync(join(__dirname, 'templates', '骨架', templateFile), 'utf-8'));
|
|
980
|
+
} catch {
|
|
981
|
+
return [];
|
|
982
|
+
}
|
|
983
|
+
const props = (tpl && tpl.structure && tpl.structure.properties) || null;
|
|
984
|
+
if (!props || typeof props !== 'object') return [];
|
|
985
|
+
|
|
986
|
+
const lines = [];
|
|
987
|
+
lines.push('## 字段类型(**照这张表写,不要猜**)');
|
|
988
|
+
lines.push('');
|
|
989
|
+
lines.push('下面每个字段的类型都是**硬要求**:类型写错校验器会直接报 TYPE 错误,' +
|
|
990
|
+
'同一个批次反复报同类错误就会触发中途校验的重启上限,整段停摆。');
|
|
991
|
+
lines.push('');
|
|
992
|
+
|
|
993
|
+
const wanted = [];
|
|
994
|
+
for (const a of (batch.arrays || [])) {
|
|
995
|
+
if (a && wanted.indexOf(a) < 0) wanted.push(a);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// 整文件型产物(框架):顶层键不是数组,说明它们各自的类型
|
|
999
|
+
if (!wanted.length) {
|
|
1000
|
+
for (const k of Object.keys(props)) {
|
|
1001
|
+
const spec = props[k];
|
|
1002
|
+
if (!spec || spec.type === 'array') continue;
|
|
1003
|
+
lines.push('- ' + k + ':' + skeletonTypeLabel(spec));
|
|
1004
|
+
}
|
|
1005
|
+
lines.push('');
|
|
1006
|
+
return lines.length > 4 ? lines : [];
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
for (const arrayName of wanted) {
|
|
1010
|
+
const spec = props[arrayName];
|
|
1011
|
+
if (!spec || spec.type !== 'array') continue;
|
|
1012
|
+
const itemProps = (spec.items && spec.items.properties) || null;
|
|
1013
|
+
if (!itemProps) continue;
|
|
1014
|
+
lines.push('**' + arrayName + '** 每一行的字段:');
|
|
1015
|
+
lines.push('');
|
|
1016
|
+
lines.push('| 字段 | 类型 | 必填 | 取值格式 |');
|
|
1017
|
+
lines.push('|---|---|---|---|');
|
|
1018
|
+
for (const field of Object.keys(itemProps)) {
|
|
1019
|
+
const fieldSpec = itemProps[field];
|
|
1020
|
+
lines.push('| ' + field + ' | ' + skeletonTypeLabel(fieldSpec) +
|
|
1021
|
+
' | ' + (fieldSpec && fieldSpec.required ? '是' : '否') +
|
|
1022
|
+
' | ' + (fieldSpec && fieldSpec.pattern ? fieldSpec.pattern : '无额外限制') + ' |');
|
|
1023
|
+
}
|
|
1024
|
+
lines.push('');
|
|
1025
|
+
}
|
|
1026
|
+
return lines.length > 4 ? lines : [];
|
|
1027
|
+
},
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* 骨架单轮任务区块:写哪个文件、写哪个数组、写几条、写成什么格式。
|
|
1031
|
+
*
|
|
1032
|
+
* 与血肉/节奏的差别:
|
|
1033
|
+
* - 实体批次是**配额驱动**的:条数由框架的 quota 决定,
|
|
1034
|
+
* 所以要逐项告诉模型"主角 2 个、核心配角 6 个",
|
|
1035
|
+
* 否则模型只能自己猜个数,校验器会报 QUOTA_MISMATCH。
|
|
1036
|
+
* - 关系批次要填**别类实体**的 id,所以要附可引用的 id 表,
|
|
1037
|
+
* 否则模型只能凭空编 id(SKELETON_REF_MISSING)。
|
|
1038
|
+
*/
|
|
1039
|
+
buildSkeletonBatchTask(opts) {
|
|
1040
|
+
const o = opts || {};
|
|
1041
|
+
const batch = o.batch || {};
|
|
1042
|
+
const attempt = o.attempt || 1;
|
|
1043
|
+
const isRetry = attempt > 1;
|
|
1044
|
+
const stageDir = o.stageDir || '设定/raw/sources/骨架';
|
|
1045
|
+
const buildDir = o.buildDir || stageDir + '/.skeleton-build';
|
|
1046
|
+
const shardPath = buildDir + '/' + batch.shard;
|
|
1047
|
+
const targetPath = stageDir + '/' + batch.file;
|
|
1048
|
+
|
|
1049
|
+
const lines = [];
|
|
1050
|
+
lines.push('# 本轮派活:' + batch.file + ' / ' + (batch.groupKey || '整个文件'));
|
|
1051
|
+
lines.push('');
|
|
1052
|
+
// 行数口径不是"每源实体一行"时,开头不能说"只写下面列出的条目"——
|
|
1053
|
+
// 下面列的是关系的两端(id),不是要写的行数(tt5 实测踩过)。
|
|
1054
|
+
const rowsAreItems = batch.rowsMode !== 'quota';
|
|
1055
|
+
lines.push('这是第 ' + attempt + ' 次写这一批。**' +
|
|
1056
|
+
(rowsAreItems ? '只写下面列出的条目' : '只写下面这个数组') + '**,写到指定文件,写完就停。');
|
|
1057
|
+
lines.push('');
|
|
1058
|
+
|
|
1059
|
+
if (isRetry) {
|
|
1060
|
+
lines.push('## 上一轮校验没通过(脚本判出来的错误)');
|
|
1061
|
+
lines.push('');
|
|
1062
|
+
const failures = o.failures || [];
|
|
1063
|
+
if (!failures.length) {
|
|
1064
|
+
lines.push('(错误清单为空,按上一轮的字段要求重写这一批。)');
|
|
1065
|
+
} else {
|
|
1066
|
+
for (const f of failures) {
|
|
1067
|
+
lines.push('- [' + (f.code || 'ERROR') + '] ' + (f.path || '(无路径)') + ':' + (f.message || '') + (f.hint ? '(' + f.hint + ')' : ''));
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
lines.push('');
|
|
1071
|
+
lines.push('**只修上面点名的字段**,其余内容保持原样(包括已经写对的条目)。');
|
|
1072
|
+
lines.push('');
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
lines.push('## 要写到哪里');
|
|
1076
|
+
lines.push('');
|
|
1077
|
+
lines.push('- 文件路径:`' + shardPath + '`');
|
|
1078
|
+
lines.push('- 这是一个**分片**文件:只包含本批条目,不要包含别的条目,也不要动 `' + targetPath + '`。');
|
|
1079
|
+
lines.push('- 如果需要看本文件已经写了什么(保持一致用),可以读 `' + targetPath + '`,但**不要修改它**。');
|
|
1080
|
+
lines.push('');
|
|
1081
|
+
|
|
1082
|
+
lines.push('## 文件格式');
|
|
1083
|
+
lines.push('');
|
|
1084
|
+
if (batch.wholeObject) {
|
|
1085
|
+
lines.push('文件内容是一个 JSON 对象,**顶层就是整个框架**,顶层键固定为:');
|
|
1086
|
+
lines.push('');
|
|
1087
|
+
lines.push('```');
|
|
1088
|
+
lines.push('{');
|
|
1089
|
+
lines.push(' "capacity": { ... },');
|
|
1090
|
+
lines.push(' "input_snapshot": { ... },');
|
|
1091
|
+
lines.push(' "quota": { ... },');
|
|
1092
|
+
lines.push(' "volume_ratio": [ ... ],');
|
|
1093
|
+
lines.push(' "derivations": { ... }');
|
|
1094
|
+
lines.push('}');
|
|
1095
|
+
lines.push('```');
|
|
1096
|
+
} else {
|
|
1097
|
+
const arrays = batch.arrays || [batch.groupKey];
|
|
1098
|
+
const shape = arrays.map((a) => ' "' + a + '": [ ... ]').join(',\n');
|
|
1099
|
+
lines.push('文件内容是一个 JSON 对象,顶层键固定为:');
|
|
1100
|
+
lines.push('');
|
|
1101
|
+
lines.push('```');
|
|
1102
|
+
lines.push('{');
|
|
1103
|
+
lines.push(shape);
|
|
1104
|
+
lines.push('}');
|
|
1105
|
+
lines.push('```');
|
|
1106
|
+
}
|
|
1107
|
+
lines.push('');
|
|
1108
|
+
lines.push('键的顺序、缩进不重要,但**顶层键名不能改**,数组里只能有本批条目。');
|
|
1109
|
+
lines.push('');
|
|
1110
|
+
const fieldTypes = this.renderSkeletonFieldTypes(batch);
|
|
1111
|
+
if (fieldTypes.length) {
|
|
1112
|
+
lines.push.apply(lines, fieldTypes);
|
|
1113
|
+
lines.push('');
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
if (batch.wholeObject) {
|
|
1117
|
+
lines.push('## 本批要写的内容');
|
|
1118
|
+
lines.push('');
|
|
1119
|
+
lines.push('本批就是**整个 `' + batch.file + '`**:');
|
|
1120
|
+
lines.push('');
|
|
1121
|
+
lines.push('1. `capacity`:目标章节、目标字数、平均每章字数');
|
|
1122
|
+
lines.push('2. `input_snapshot`:任务输入 8 项,原样照抄');
|
|
1123
|
+
lines.push('3. `quota`:28 个配额字段(数量公式算出来的终值)');
|
|
1124
|
+
lines.push('4. `volume_ratio`:每卷的章节比例,长度 = 卷数,比例之和 = 1.0');
|
|
1125
|
+
lines.push('5. `derivations`:30 条公式推导,一条不多一条不少');
|
|
1126
|
+
lines.push('');
|
|
1127
|
+
lines.push('**下一段的实体清单要按这里的 quota 写**,所以 quota 与 derivations 必须先写对。');
|
|
1128
|
+
lines.push('');
|
|
1129
|
+
} else if (batch.quotaCounts) {
|
|
1130
|
+
if (batch.count === 0) {
|
|
1131
|
+
lines.push('## 本批要写的条目(0 条)');
|
|
1132
|
+
lines.push('');
|
|
1133
|
+
lines.push('这个数组的配额是 0,**一条都不写**,但**顶层键必须存在**(写成空数组 `[]`)。');
|
|
1134
|
+
lines.push('');
|
|
1135
|
+
lines.push('少了这个键,校验器会报 `REQUIRED`(必填字段不得为空),而且这个错归不到任何批次,重试也修不好。');
|
|
1136
|
+
lines.push('');
|
|
1137
|
+
} else {
|
|
1138
|
+
lines.push('## 本批要写的条目(共 ' + batch.count + ' 条)');
|
|
1139
|
+
lines.push('');
|
|
1140
|
+
lines.push('条数由框架的 quota 决定,**严格按下面的分项写**:');
|
|
1141
|
+
lines.push('');
|
|
1142
|
+
lines.push('| 分类 | 条数 |');
|
|
1143
|
+
lines.push('|---|---|');
|
|
1144
|
+
for (const k of Object.keys(batch.quotaCounts)) {
|
|
1145
|
+
lines.push('| ' + k + ' | ' + batch.quotaCounts[k] + ' |');
|
|
1146
|
+
}
|
|
1147
|
+
lines.push('');
|
|
1148
|
+
lines.push('合计 **' + batch.count + ' 条**,写进 `' + batch.groupKey + '` 数组。');
|
|
1149
|
+
lines.push('');
|
|
1150
|
+
lines.push('**条数对不上就会报 `QUOTA_MISMATCH`** —— 校验器是拿框架的 quota 逐类核对实体清单的。');
|
|
1151
|
+
lines.push('');
|
|
1152
|
+
}
|
|
1153
|
+
} else if (batch.rowsMode === 'quota' && batch.rowsSpec) {
|
|
1154
|
+
// 行数由框架 quota 精确决定:必须把**数值和口径**都给模型,
|
|
1155
|
+
// 只说"按 quota 写"它拿不到数(关系批次不投喂框架分节,tt5 实测)。
|
|
1156
|
+
const rs = batch.rowsSpec;
|
|
1157
|
+
lines.push('## 这个数组要写 ' + rs.total + ' 行');
|
|
1158
|
+
lines.push('');
|
|
1159
|
+
lines.push('**行数是硬要求**:`' + batch.groupKey + '` 一共写 **' + rs.total + ' 行**。');
|
|
1160
|
+
lines.push('');
|
|
1161
|
+
lines.push('拆解:`quota.' + rs.quotaPath + ' = ' + rs.pairs + '` 是**无向关系对数**,' +
|
|
1162
|
+
'每一对要写成两行 —— `A→B` 一行、`B→A` 一行,所以总行数 = ' + rs.pairs + ' × ' + rs.multiplier + ' = **' + rs.total + '**。');
|
|
1163
|
+
lines.push('');
|
|
1164
|
+
lines.push('写少了或写多了,校验器会报 `QUOTA_MISMATCH`(path: `quota.' + rs.quotaPath + '`)。');
|
|
1165
|
+
lines.push('');
|
|
1166
|
+
lines.push('下面这些是关系的**两端**(可引用的 id),**不是要写的行数**:');
|
|
1167
|
+
lines.push('');
|
|
1168
|
+
lines.push('| id | name | 类型 |');
|
|
1169
|
+
lines.push('|---|---|---|');
|
|
1170
|
+
for (const item of batch.items) {
|
|
1171
|
+
lines.push('| ' + (item.id || '-') + ' | ' + (item.name || '-') + ' | ' + (item.type || item.category || '-') + ' |');
|
|
1172
|
+
}
|
|
1173
|
+
lines.push('');
|
|
1174
|
+
lines.push('**id 和 name 原样抄,不要改一个字符。**');
|
|
1175
|
+
lines.push('');
|
|
1176
|
+
} else if (batch.rowsMode === 'optional') {
|
|
1177
|
+
lines.push('## 本批负责的关系阶段(可 0 行)');
|
|
1178
|
+
lines.push('');
|
|
1179
|
+
lines.push('本批写 `' + batch.groupKey + '` 里 `' + (batch.ownerField || 'from') + '` 属于下表这些角色的行:');
|
|
1180
|
+
lines.push('');
|
|
1181
|
+
lines.push('| id | name | 类型 |');
|
|
1182
|
+
lines.push('|---|---|---|');
|
|
1183
|
+
for (const item of batch.items || []) {
|
|
1184
|
+
lines.push('| ' + (item.id || '-') + ' | ' + (item.name || '-') + ' | ' + (item.type || item.category || '-') + ' |');
|
|
1185
|
+
}
|
|
1186
|
+
lines.push('');
|
|
1187
|
+
lines.push('**没有关系变化就写空数组 `[]`**;有关系变化才写行,不要为了凑数硬编。');
|
|
1188
|
+
lines.push('每行只写一个阶段:`from` / `to` / `stage` / `state` / `start_chapter`,可选 `end_chapter` / `trigger_event` / `description`。');
|
|
1189
|
+
lines.push('同一对角色按 `start_chapter` 严格递增;`from` 必须是上表里的角色 id。');
|
|
1190
|
+
lines.push('');
|
|
1191
|
+
} else if (batch.rowsMode === 'semantic') {
|
|
1192
|
+
// 行数由关系本身决定:**不能说"写 N 条"**。
|
|
1193
|
+
// 以前统一写"本批要写的条目(N 条)…每行一条",模型把 N 当成真实行数,
|
|
1194
|
+
// 结果角色关系被写成"一个角色一条"(tt5 实测:43 个角色只有 38 个有出边)。
|
|
1195
|
+
lines.push('## 本批负责的关系');
|
|
1196
|
+
lines.push('');
|
|
1197
|
+
lines.push('本批写 `' + batch.groupKey + '` 里 **' +
|
|
1198
|
+
(batch.ownerField ? '`' + batch.ownerField + '` 是上表这些实体' : '归属到上表这些实体') +
|
|
1199
|
+
'** 的关系行' + (batch.ownerField ? '(`' + batch.ownerField + '` 就是这一列 id)' : '') + ':');
|
|
1200
|
+
lines.push('');
|
|
1201
|
+
lines.push('| id | name | 类型 |');
|
|
1202
|
+
lines.push('|---|---|---|');
|
|
1203
|
+
for (const item of batch.items) {
|
|
1204
|
+
lines.push('| ' + (item.id || '-') + ' | ' + (item.name || '-') + ' | ' + (item.type || item.category || '-') + ' |');
|
|
1205
|
+
}
|
|
1206
|
+
lines.push('');
|
|
1207
|
+
lines.push('**id 和 name 原样抄,不要改一个字符。**');
|
|
1208
|
+
lines.push('');
|
|
1209
|
+
lines.push('**行数不预设** —— 这个数组写几行由关系本身决定,不是"一个实体一条"。' +
|
|
1210
|
+
'按剧情需要写够就行,不要为了凑数硬凑,也不要漏。');
|
|
1211
|
+
lines.push('');
|
|
1212
|
+
lines.push('硬要求只有两条:');
|
|
1213
|
+
lines.push('');
|
|
1214
|
+
lines.push('- 上表每个实体都要在关系里出现(作为关系的哪一端都算),**不能有谁一条关系都没有**(`G_ISOLATED` 会查)');
|
|
1215
|
+
lines.push('- 字段填法、枚举词表、互斥约束见契约第八节');
|
|
1216
|
+
lines.push('');
|
|
1217
|
+
} else if (batch.items && batch.items.length) {
|
|
1218
|
+
lines.push('## 本批要写的条目(' + batch.count + ' 条)');
|
|
1219
|
+
lines.push('');
|
|
1220
|
+
lines.push('| id | name | 类型 |');
|
|
1221
|
+
lines.push('|---|---|---|');
|
|
1222
|
+
for (const item of batch.items) {
|
|
1223
|
+
lines.push('| ' + (item.id || '-') + ' | ' + (item.name || '-') + ' | ' + (item.type || item.category || '-') + ' |');
|
|
1224
|
+
}
|
|
1225
|
+
lines.push('');
|
|
1226
|
+
lines.push('**id 和 name 原样抄,不要改一个字符。**');
|
|
1227
|
+
lines.push('');
|
|
1228
|
+
lines.push('上表每个条目写一行 `' + batch.groupKey + '`,一行一条,不多不少' +
|
|
1229
|
+
(batch.ownerField ? '(这一行里的 `' + batch.ownerField + '` 填上表对应的 id)' : '') + '。');
|
|
1230
|
+
lines.push('');
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (o.existingEntities && o.existingEntities.length) {
|
|
1234
|
+
lines.push('## 已经存在的实体(同一实体必须复用,不要换名新建)');
|
|
1235
|
+
lines.push('');
|
|
1236
|
+
lines.push('这些是前面批次已经写进实体清单的 id + name。写本批之前先对照:');
|
|
1237
|
+
lines.push('');
|
|
1238
|
+
lines.push('- 要写的实体已经在表里 → **复用原 id 和 name**,不要改一个字,也不要再建一条。');
|
|
1239
|
+
lines.push('- 同一个人的本名、称呼、绰号、关系称谓(哥哥/父亲/老师/老板等)→ 只保留**本名/正式名**那一条,不要为称呼另建实体。');
|
|
1240
|
+
lines.push('- 同一势力的全称、简称、集团/公司后缀 → 只保留已经存在的正式名,不要重复建。');
|
|
1241
|
+
lines.push('- 表里没有、且确实是新实体 → 才按本批配额新建。');
|
|
1242
|
+
lines.push('');
|
|
1243
|
+
for (const line of o.existingEntities) lines.push(line);
|
|
1244
|
+
lines.push('');
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
if (o.idTables && o.idTables.length) {
|
|
1248
|
+
lines.push('## 可引用的 id(只能从下表里选)');
|
|
1249
|
+
lines.push('');
|
|
1250
|
+
for (const line of o.idTables) lines.push(line);
|
|
1251
|
+
lines.push('');
|
|
1252
|
+
lines.push('**id 填错或引用不存在的 id,校验器会报 `SKELETON_REF_MISSING`。**');
|
|
1253
|
+
lines.push('');
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (o.extraContext && o.extraContext.length) {
|
|
1257
|
+
lines.push('## 本批需要的对照数据(插件从上游读出,原样照抄)');
|
|
1258
|
+
lines.push('');
|
|
1259
|
+
for (const line of o.extraContext) lines.push(line);
|
|
1260
|
+
lines.push('');
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
if (Array.isArray(batch.optionalArrays) && batch.optionalArrays.indexOf('other_entities') >= 0) {
|
|
1264
|
+
lines.push('## 题材扩展实体(可选,0-8 条)');
|
|
1265
|
+
lines.push('');
|
|
1266
|
+
lines.push('同一个分片里还要写 `other_entities` 数组。它**不计入上面的条目数**:');
|
|
1267
|
+
lines.push('如果题材里有灵魂、神魂、器灵、妖灵、鬼魂、概念体等**不适合当人物**的存在,就在这里登记;没有就写空数组 `[]`。');
|
|
1268
|
+
lines.push('');
|
|
1269
|
+
lines.push('- id 从 `O001` 起连续编号,三位数字;不要占用人物/势力/道具的 id。');
|
|
1270
|
+
lines.push('- `kind` 写具体类别(灵魂/神魂/器灵/妖灵/鬼魂/概念体/其他),不要只写“其他”。');
|
|
1271
|
+
lines.push('- 只登记会在故事里实际出场、且需要独立关系的存在;不要把普通人物换个名字塞进来。');
|
|
1272
|
+
lines.push('- 它们与人物/其他实体的关系由 `关系.json` 的 `other_relations` 表达(如 `附身`、`寄宿`、`共生`、`镇压`)。');
|
|
1273
|
+
lines.push('');
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
lines.push(referenceLogPromptBlock('骨架', batch));
|
|
1277
|
+
lines.push('## 交付');
|
|
1278
|
+
lines.push('');
|
|
1279
|
+
lines.push('写完 `' + shardPath + '` 就结束本轮。');
|
|
1280
|
+
lines.push('');
|
|
1281
|
+
lines.push('**只回一句话**(例:`已写入 骨架-层级.b24.branch_span.json,10 条`),然后停下:');
|
|
1282
|
+
lines.push('');
|
|
1283
|
+
lines.push('- 不要解释设计思路、不要罗列自己检查过哪些项');
|
|
1284
|
+
lines.push('- **不要引用错误码说"满足 XXX 要求"** —— 通过与否由插件的校验脚本判定,不是你说了算');
|
|
1285
|
+
lines.push('- 不要宣布完成、不要运行任何校验脚本');
|
|
1286
|
+
lines.push('');
|
|
1287
|
+
|
|
1288
|
+
return lines.join('\n');
|
|
1289
|
+
},
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* 生成骨架阶段的系统提示词
|
|
1293
|
+
*
|
|
1294
|
+
* inputSnapshot:骨架阶段 框架.json 里的任务输入快照(权威来源)。
|
|
1295
|
+
* 缺省时退回 topicData 拼一份(骨架产物还没落盘的兜底)。
|
|
1296
|
+
*/
|
|
1297
|
+
generateRhythmSystemPrompt({ topicData, wikiReferences, inputSnapshot }) {
|
|
1298
|
+
const { book_name, genre, target_chapters } = topicData;
|
|
1299
|
+
|
|
1300
|
+
// 读取节奏阶段提示词模板(prompts/03-rhythm-prompt.md)
|
|
1301
|
+
let prompt = cleanPromptForLLM(readBuiltInReference('03-rhythm-prompt.md'));
|
|
1302
|
+
|
|
1303
|
+
// 替换变量
|
|
1304
|
+
prompt = prompt.replace(/\$\{book_name\}/g, textField(book_name));
|
|
1305
|
+
prompt = prompt.replace(/\$\{genre\}/g, textField(genre));
|
|
1306
|
+
prompt = prompt.replace(/\$\{target_chapters\}/g, textField(target_chapters));
|
|
1307
|
+
prompt = prompt.replace(/\$\{wikiReferences\}/g, wikiReferences || '');
|
|
1308
|
+
|
|
1309
|
+
// 任务输入清单:优先用骨架阶段的 input_snapshot 快照
|
|
1310
|
+
const snapshot = inputSnapshot || snapshotFromTopicData(topicData);
|
|
1311
|
+
prompt = prompt.replace(/\$\{input_snapshot_block\}/g, renderInputSnapshot(snapshot));
|
|
1312
|
+
|
|
1313
|
+
// 移除残余的 ${...} 占位符
|
|
1314
|
+
prompt = prompt.replace(/\$\{[a-z_]+\}/g, '');
|
|
1315
|
+
|
|
1316
|
+
return prompt;
|
|
1317
|
+
},
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* 生成写作阶段的「执行须知」(产物契约)
|
|
1321
|
+
*
|
|
1322
|
+
* 写作阶段改成插件驱动、按批派活之后,写作提示词不再是"一次投喂、
|
|
1323
|
+
* LLM 自己跑完 T 章"的执行手册,而是"每批都带一遍"的产物契约:
|
|
1324
|
+
* 写哪些文件、写成什么格式、什么会被校验拦下、收到错误后怎么改。
|
|
1325
|
+
* 流程(读什么、校验什么、推进到哪一章)由 writing-driver.js 负责。
|
|
1326
|
+
*/
|
|
1327
|
+
generateWritingContract({ topicData, benchmarkRefs, benchmarkDigest, projectRoot }) {
|
|
1328
|
+
const { book_name, target_chapters } = topicData || {};
|
|
1329
|
+
|
|
1330
|
+
// 读取写作阶段提示词模板(prompts/04-writing-prompt.md)
|
|
1331
|
+
let prompt = cleanPromptForLLM(readBuiltInReference('04-writing-prompt.md'));
|
|
1332
|
+
|
|
1333
|
+
// 替换变量
|
|
1334
|
+
prompt = prompt.replace(/\$\{book_name\}/g, book_name || '');
|
|
1335
|
+
prompt = prompt.replace(/\$\{target_chapters\}/g, String(target_chapters || ''));
|
|
1336
|
+
// 参考分层说明(插件指导 vs 对标样本)+ 本阶段可读的对标文件清单
|
|
1337
|
+
prompt = prompt.replace(/\$\{benchmark_refs_block\}/g, benchmarkRefs || '');
|
|
1338
|
+
prompt = prompt.replace(/\$\{[a-z_]+\}/g, '');
|
|
1339
|
+
if (benchmarkDigest) {
|
|
1340
|
+
// 摘要正文直接注入,避免模型只收藏路径、写作时完全不看。
|
|
1341
|
+
prompt = prompt + '\n\n' + String(benchmarkDigest).trim();
|
|
1342
|
+
}
|
|
1343
|
+
// 参考文件路径绝对化(基准钉死,见 absolutizeInlinePaths 的说明)
|
|
1344
|
+
prompt = absolutizeInlinePaths(prompt, projectRoot);
|
|
1345
|
+
|
|
1346
|
+
// 追加"合法取值清单" —— 从 reading-power.js(唯一真源)动态生成。
|
|
1347
|
+
//
|
|
1348
|
+
// 为什么动态注入而不是把清单写死在 markdown 里:
|
|
1349
|
+
// 写死两份必然漂移(旧 skill 就是这么坏的:deslop 24 类 / review"19 类" /
|
|
1350
|
+
// 代码 21 类,三份互不一致)。这里保证**模型看到的清单和校验器判定的是同一份**。
|
|
1351
|
+
prompt = prompt + '\n\n' + readingPowerLegend();
|
|
1352
|
+
|
|
1353
|
+
return prompt;
|
|
1354
|
+
},
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* 生成去味阶段的「执行须知」(产物契约)
|
|
1358
|
+
*
|
|
1359
|
+
* 与写作阶段的契约同一套思路:提示词只讲"改成什么样",
|
|
1360
|
+
* 流程(检测哪一章、备份、派活、复扫、判比例、回滚)由 deslop-driver.js 负责。
|
|
1361
|
+
*
|
|
1362
|
+
* 末尾动态追加 24 类 AI 模式清单 —— 从 ai-patterns.js(唯一真源)派生。
|
|
1363
|
+
* 写死一份必然漂移:旧 skill 就坏在这儿(deslop 说 24 类、review 说 19 类、
|
|
1364
|
+
* 代码实现 21 类,三份对不上,模型根本不知道以哪套为准)。
|
|
1365
|
+
*/
|
|
1366
|
+
generateDeslopContract({ topicData, projectRoot } = {}) {
|
|
1367
|
+
const { book_name } = topicData || {};
|
|
1368
|
+
|
|
1369
|
+
// 读取去味阶段提示词模板(prompts/05-deslop-prompt.md)
|
|
1370
|
+
let prompt = cleanPromptForLLM(readBuiltInReference('05-deslop-prompt.md'));
|
|
1371
|
+
|
|
1372
|
+
prompt = prompt.replace(/\$\{book_name\}/g, book_name || '');
|
|
1373
|
+
prompt = prompt.replace(/\$\{[a-z_]+\}/g, '');
|
|
1374
|
+
// 参考文件路径绝对化(基准钉死,见 absolutizeInlinePaths 的说明)
|
|
1375
|
+
prompt = absolutizeInlinePaths(prompt, projectRoot);
|
|
1376
|
+
|
|
1377
|
+
// 追加"AI 模式清单" —— 每一类都带改法,模型照着改
|
|
1378
|
+
prompt = prompt + '\n\n' + deslopLegend();
|
|
1379
|
+
|
|
1380
|
+
return prompt;
|
|
1381
|
+
},
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* 生成分析阶段的系统提示词
|
|
1385
|
+
*/
|
|
1386
|
+
generateAnalyzeSystemPrompt({ topicData, wikiReferences, reviewsContent = '' }) {
|
|
1387
|
+
const { book_name, genre, target_chapters } = topicData;
|
|
1388
|
+
|
|
1389
|
+
// 分析阶段 prompt 直接内置(保持精简,避免和写作 prompt 混淆)
|
|
1390
|
+
const header = `你正在执行**分析阶段**任务,本阶段读上游骨架/血肉/节奏的全文 + llm-wiki 审查结果,输出对全文的一致性 / 矛盾 / 建议的报告。
|
|
1391
|
+
|
|
1392
|
+
**核心目标**:
|
|
1393
|
+
1. 读取 llm-wiki 给你返回的审查结果(reviewsContent)
|
|
1394
|
+
2. 对照骨架/血肉/节奏产物的实际内容,验证每条 review
|
|
1395
|
+
3. 输出可执行的修正建议
|
|
1396
|
+
|
|
1397
|
+
**严格禁令**:
|
|
1398
|
+
- 不得修改 \`模板/\`、\`references/\`、\`设定/\` 下的任何文件
|
|
1399
|
+
- 不得自我追加或修改 review;只对现有 review 做确认/质疑/补充说明
|
|
1400
|
+
- 报告内容**只输出给用户**,不写入任何文件
|
|
1401
|
+
|
|
1402
|
+
**任务输入**:
|
|
1403
|
+
- 书名:${book_name}
|
|
1404
|
+
- 题材:${genre}
|
|
1405
|
+
- 目标章节:${target_chapters}
|
|
1406
|
+
|
|
1407
|
+
---
|
|
1408
|
+
|
|
1409
|
+
`;
|
|
1410
|
+
|
|
1411
|
+
const reviewBlock = reviewsContent
|
|
1412
|
+
? `## LLM Wiki 审查结果(已分类)\n\n${reviewsContent}\n\n请逐项回应:\n\n- 对**矛盾**类:定性强弱(强 / 中 / 弱),提具体段落引用\n- 对**确认**类:回 yes/no + 证据\n- 对**建议**类:判断采纳 / 不采纳,并说明理由\n\n---\n\n`
|
|
1413
|
+
: '';
|
|
1414
|
+
|
|
1415
|
+
const wikiBlock = wikiReferences
|
|
1416
|
+
? `## 同题材参考(仅供风格基线)\n\n${wikiReferences}\n\n---\n\n`
|
|
1417
|
+
: '';
|
|
1418
|
+
|
|
1419
|
+
const outputTemplate = `## 输出格式
|
|
1420
|
+
|
|
1421
|
+
\`\`\`markdown
|
|
1422
|
+
# 分析阶段报告
|
|
1423
|
+
|
|
1424
|
+
## 书名:__BOOK_NAME__
|
|
1425
|
+
|
|
1426
|
+
| # | 类型 | 标题 | 强/中/弱 | 回应 |
|
|
1427
|
+
|---|------|------|-----------|------|
|
|
1428
|
+
| 1 | 矛盾 | ... | ... | ... |
|
|
1429
|
+
|
|
1430
|
+
## 中优先级:影响角色弧光的问题
|
|
1431
|
+
(说明)
|
|
1432
|
+
|
|
1433
|
+
## 低优先级:优化类问题
|
|
1434
|
+
(说明)
|
|
1435
|
+
|
|
1436
|
+
## 修正优先级清单
|
|
1437
|
+
1. 必须改
|
|
1438
|
+
2. 建议改
|
|
1439
|
+
3. 可不改
|
|
1440
|
+
\`\`\`
|
|
1441
|
+
|
|
1442
|
+
请基于 reviewsContent 给出报告。没有 review 时直接说明"没有审查项"。`;
|
|
1443
|
+
|
|
1444
|
+
return header + wikiBlock + reviewBlock + outputTemplate.replace(/__BOOK_NAME__/g, book_name || '未知');
|
|
1445
|
+
},
|
|
1446
|
+
};
|