@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,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* blocks.js —— 按平台生成的提示词**动态段落**
|
|
3
|
+
*
|
|
4
|
+
* 这些段落依赖平台的数据形态(平铺 / 分组)、热度指标与标签来源,
|
|
5
|
+
* 不能写死在静态提示词里。它们只负责**拼文本**,不做任何校验 ——
|
|
6
|
+
* 校验在 scripts/report-validate.js 与 scripts/append-block.js 里。
|
|
7
|
+
*
|
|
8
|
+
* 静态的任务契约在 prompts/01-analysis-prompt.md;
|
|
9
|
+
* 本文件生成的内容通过 `${...}` 占位符注入。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 3.2 的「数据形态」段 —— **按平台说**(评审 P0-1)。
|
|
14
|
+
*
|
|
15
|
+
* 以前四个平台统一写成"题材组",起点/七猫实际是平铺的。
|
|
16
|
+
*/
|
|
17
|
+
export function dataShapeBlock(ds) {
|
|
18
|
+
if (ds.grouped && ds.genreFrom === 'group') {
|
|
19
|
+
return [
|
|
20
|
+
`- **数据形态**:文件按**题材分组** —— 每个 \`${ds.groupTitle}\` 是一个题材组,`,
|
|
21
|
+
` 组里每本书一个 \`${ds.bookHeading}\`。`,
|
|
22
|
+
` **这是分层样本,不是全市场随机样本**:每组本数相同。`,
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
if (ds.grouped) {
|
|
26
|
+
return [
|
|
27
|
+
`- **数据形态**:文件按**频道分组**(${ds.groupTitle})—— 频道是榜单范围,**不是题材**。`,
|
|
28
|
+
` 每个 \`${ds.bookHeading}\` 是一本书;题材取自每本书自己的 \`${ds.genreField}\` 字段。`,
|
|
29
|
+
` 这是**完整频道榜**,不是"每题材前 N 本"的分层样本;各题材样本量可能不均。`,
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
return [
|
|
33
|
+
`- **数据形态**:文件是**平铺的书目列表** —— 每个 \`${ds.bookHeading}\` 就是**一本书**,`,
|
|
34
|
+
` **不是题材组**。文件里**没有** \`## 某题材 — N 本\` 这样的分组标题。`,
|
|
35
|
+
` **书名永远不是题材名** —— \`#1 玄鉴仙族\` 里的"玄鉴仙族"是书名。`,
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 3.3 的「题材口径」段 —— 分组平台 vs 字段平台。
|
|
41
|
+
*/
|
|
42
|
+
export function genreRuleBlock(ds, multiList = false) {
|
|
43
|
+
if (ds.genreFrom === 'group') {
|
|
44
|
+
return [
|
|
45
|
+
`- **题材以数据文件里的官方分组为准**(${ds.groupTitle} 里的题材名就是平台给的题材),`,
|
|
46
|
+
` **不要另造题材分类** —— 自造分类与采集端的分组不一致,题材频次会失真。`,
|
|
47
|
+
` 统计题材频次时**只算具体的题材组、跳过"全部"**,否则同一本书会被数两遍、占比加起来超过 100%。`,
|
|
48
|
+
` 同一本书只归入它所在的**那一个**官方题材。`,
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
return [
|
|
52
|
+
`- **题材取自每本书自己的 \`${ds.genreField}\` 字段**(如 \`${ds.genreExample}\`),`,
|
|
53
|
+
` **不是**从标题推断的 —— 标题里只有书名。`,
|
|
54
|
+
...(ds.grouped ? [
|
|
55
|
+
` **频道/分组标题只是榜单范围,不是题材** —— 不要把"全站"之类当题材。`,
|
|
56
|
+
] : []),
|
|
57
|
+
` **不要另造题材分类**:自造分类与采集端给的题材值不一致,题材频次会失真。`,
|
|
58
|
+
...(multiList ? [
|
|
59
|
+
` 同一本书在多个榜单里出现时,**它的题材字段应当一致**;若发现不一致,`,
|
|
60
|
+
` 在"一、数据与方法"里写明你采信了哪一个、为什么。`,
|
|
61
|
+
] : []),
|
|
62
|
+
` **子分类只作细分**,主表按大类聚合(细分可能有几十个、每个只有 1-2 本)。`,
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 3.2 的「标签」规则 —— 没有官方标签字段的平台**不做标签频次**(评审 P0-4)。
|
|
68
|
+
*
|
|
69
|
+
* 实测(起点 2026-10-29):9 个文件 0 个 `**标签:**`,
|
|
70
|
+
* 简介里的 `【…】` 有出版说明(`【本作品简体实体书由次元书馆出版】`)、
|
|
71
|
+
* 营销语(`【均订2万,放心追看!】`),分隔符也不是 `+` 而是 `、`。
|
|
72
|
+
* 照旧规则统计,会把"均订2万"算成标签。
|
|
73
|
+
*/
|
|
74
|
+
export function tagRuleBlock(ds) {
|
|
75
|
+
if (ds.hasTags) {
|
|
76
|
+
return [
|
|
77
|
+
` - **标签**:用 \`**标签:**\` 行(平台官方分类,比从简介猜准);`,
|
|
78
|
+
` 那一行没有就记"无标签",**不要根据书名猜标签**`,
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
return [
|
|
82
|
+
` - **标签**:本平台的数据文件里**没有** \`**标签:**\` 字段 ——`,
|
|
83
|
+
` **不要做标签频次分析**(没有数据源)。`,
|
|
84
|
+
` 如果确实要用简介里的 \`【…】\`,那只算**推断**,且必须:`,
|
|
85
|
+
` - 只认**用 \`、\` 或 \`,\` 分隔**的标签串(多个 \`【】\` 要逐个看);`,
|
|
86
|
+
` - \`【本作品简体实体书由…出版】\`、\`【均订2万,放心追看!】\` 这类是**说明/营销语**,不是标签;`,
|
|
87
|
+
` - **标注"推断"**并报出覆盖率(如"X/N 本出现可识别的标签串");`,
|
|
88
|
+
` - 覆盖率太低就**直接删掉标签这一项分析**,不要硬凑`,
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 3.3 的「竞争度指标」段 —— 样本形态 + 指标口径 + 高/中/低阈值。
|
|
94
|
+
*
|
|
95
|
+
* - P0-2:平铺数据的样本量极不均匀,"前 3 ÷ 总量"在 1-3 本的题材上恒为 1.000;
|
|
96
|
+
* - P1-3:高/中/低必须有阈值,否则不同日期的报告不可比;
|
|
97
|
+
* - P1-4:同质化必须"与众数比",两两比会恒为 100%。
|
|
98
|
+
*/
|
|
99
|
+
export function competitionBlock(ds, h, multiList = false) {
|
|
100
|
+
const sampleNote = ds.stratified
|
|
101
|
+
? [
|
|
102
|
+
` 这份数据是"每题材前 N 本"的分层样本,**每组本数相同**,`,
|
|
103
|
+
` 因此"书目数占比"和"头部集中度(前 N ÷ N)"在这种数据上**恒等于常数、没有区分度**:`,
|
|
104
|
+
` **不要拿它们当指标**(这类表在所有题材上并列,得不出差异)。`,
|
|
105
|
+
]
|
|
106
|
+
: [
|
|
107
|
+
` 这份数据是**榜单样本**,**各题材的样本量极不均匀**`,
|
|
108
|
+
` (同一次扫榜里,有的题材有 20 本,有的只有 1 本)。`,
|
|
109
|
+
` **样本量不均的直接后果**:`,
|
|
110
|
+
` - 只有 1-3 本的题材,"前 3 名 ÷ 总量"恒等于 1.000、"中位数"就是那一本的值 —— **没有区分度**;`,
|
|
111
|
+
...(multiList ? [
|
|
112
|
+
` - 某题材出现在**越多榜单**里,它的书目数与${h.label}总量就机械性越大 ——`,
|
|
113
|
+
` 这是**榜单选择偏差**,不是市场需求,**不能**读成"某题材需求是另一个的 N 倍"。`,
|
|
114
|
+
] : []),
|
|
115
|
+
];
|
|
116
|
+
const shareName = ds.grouped ? '需求份额' : '样本内份额';
|
|
117
|
+
const shareFormula = ds.grouped
|
|
118
|
+
? ` - **${shareName}** = 该题材${h.label}总量 ÷ 全部题材${h.label}总量(给出百分比)`
|
|
119
|
+
: ` - **${shareName}** = 该题材${h.label}总量 ÷ **样本内**全部题材${h.label}总量(给出百分比);`;
|
|
120
|
+
return [
|
|
121
|
+
`- **竞争度指标(5 项)—— 按下面的口径算,不要凭感觉打分**:`,
|
|
122
|
+
...sampleNote,
|
|
123
|
+
` **每个指标都要带样本量**,并按样本量分档:`,
|
|
124
|
+
` - **样本量 ≥5 本** → 正常打分(高/中/低);`,
|
|
125
|
+
` - **3-4 本** → 头部集中度 / 同质化程度可写数值或“只描述”,差异化空间写“只描述”;上述三项都不打高/中/低;`,
|
|
126
|
+
` - **<3 本** → 头部集中度 / 同质化程度 / 差异化空间三项标“样本不足”,这三项不参与排序;`,
|
|
127
|
+
` - **份额列不受样本量分档影响**:始终按总量算百分比(可带高/中/低档),样本不足只影响其余三项打分。`,
|
|
128
|
+
` **改用${h.label}数据**:`,
|
|
129
|
+
` - **需求规模** = 该题材**有${h.label}值的书**的总量,并给出**均值**、**中位数**与**样本量**`,
|
|
130
|
+
...(ds.grouped
|
|
131
|
+
? [shareFormula]
|
|
132
|
+
: [shareFormula,
|
|
133
|
+
` **这是榜单样本内部的份额,不是市场份额** —— 报告里要写"样本内份额",不许简写成"份额"`]),
|
|
134
|
+
` - **头部集中度** = 该题材**前 3 名**${h.label}之和 ÷ 该题材${h.label}总量`,
|
|
135
|
+
` - **同质化程度** = 该题材里,与**该题材众数组合**在`,
|
|
136
|
+
` "金手指 / 主角身份 / **简介核心冲突**"三项中**至少两项一致**的书目数 ÷ N`,
|
|
137
|
+
` (只一致一项或零项 → 计 0)。`,
|
|
138
|
+
` **是"与众数比",不是"两两比"** —— 两两比的话几乎每本都能找到同类,结果恒为 100%。`,
|
|
139
|
+
` **先定编码规则再统计**:同一维度里机制相近的归一类(如"系统/签到/模拟器/游戏面板"算一类),`,
|
|
140
|
+
` 差别大的分开算;在"一、数据与方法"里把你用的类别列出来,并写出**众数组合**与**匹配本数**`,
|
|
141
|
+
` ("简介核心冲突"的类别自定,但要在方法节写清,如 复仇/生存/争霸/日常经营/探案规则/恋爱/其他)`,
|
|
142
|
+
` - **差异化空间** = 该题材**还没被满足**的读者需求。`,
|
|
143
|
+
` **数据里没有评论,也没有第一章** —— 只能从**简介标签、核心卖点、同题材书目的空白处**推断,`,
|
|
144
|
+
` **必须标注"推断"**;不许声称知道评论里说什么`,
|
|
145
|
+
` **高/中/低必须用统一阈值**(每次凭感觉的话,不同日期的报告没法比):`,
|
|
146
|
+
` - **${shareName} / 需求规模**:用**本次快照内的分位数** —— 前 ⌊n/3⌋ 个为高、后 ⌊n/3⌋ 个为低、其余为中`,
|
|
147
|
+
` (n 是题材数;n<3 时不分档。份额列**必须写百分比**,不能写成"3 本仅描述")`,
|
|
148
|
+
` - **头部集中度**:≥0.8 高 / 0.5-0.8 中 / <0.5 低;**<3 本的题材标"样本不足",3-4 本只描述、不打分**。`,
|
|
149
|
+
` 表里写 **0–1 的小数**(0.56、0.4286),不要写百分比`,
|
|
150
|
+
` - **同质化程度**:≥0.7 高 / 0.4-0.7 中 / <0.4 低(前提是先定好编码类别);同样写 0–1 的小数`,
|
|
151
|
+
` - **差异化空间**:3 级 rubric —— **高** = 题材内 ≥3 本共享同一组合、且至少 2 个合理组合缺席;`,
|
|
152
|
+
` **中** = 有主导组合但仍有可见变体空间;**低** = 头部书已覆盖多种组合;`,
|
|
153
|
+
` 3-4 本只描述、不打分;<3 本标“样本不足”`,
|
|
154
|
+
];
|
|
155
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* platforms.js —— 平台/榜单/热度指标/数据形态的**单一事实来源**
|
|
3
|
+
*
|
|
4
|
+
* 这些表同时被三处使用:
|
|
5
|
+
* 1. 提示词渲染(templates.js):平台名、榜单名、指标名、数据形态;
|
|
6
|
+
* 2. 采集编排(index.js / run-scan.js):榜单 id 与 path;
|
|
7
|
+
* 3. 报告校验(templates/report-schema.js):题材来源、表头口径。
|
|
8
|
+
*
|
|
9
|
+
* 以前它们散在 templates.js 里,提示词和脚本各写一份时必然漂移。
|
|
10
|
+
* 现在只定义一次,所有地方从这里取。
|
|
11
|
+
*/
|
|
12
|
+
import { SCAN_MODE_API, SCAN_MODE_OCR } from '#scraper-utils';
|
|
13
|
+
|
|
14
|
+
export const PLATFORM_TABLE = [
|
|
15
|
+
['1', 'qidian', '起点'],
|
|
16
|
+
['2', 'fanqie', '番茄'],
|
|
17
|
+
['3', 'jinjiang','晋江'],
|
|
18
|
+
['4', 'qimao', '七猫'],
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 各平台的**热度指标**(2026-10-28)
|
|
23
|
+
*
|
|
24
|
+
* ## 为什么要按平台区分
|
|
25
|
+
*
|
|
26
|
+
* 每个平台衡量"这本书有多火"用的**指标根本不同**,数值之间不可比:
|
|
27
|
+
*
|
|
28
|
+
* · 起点 —— **月票**(`monthTicket`,精确整数,实测 130343)
|
|
29
|
+
* · 番茄 —— **在读**(`60.2万`)
|
|
30
|
+
* · 晋江 —— **积分**(`1053978496`,采集文件里不带千分位逗号)
|
|
31
|
+
* · 七猫 —— **热度**(`3853.8万`)
|
|
32
|
+
*
|
|
33
|
+
* 之前提示词里写死"在读",那么起点/晋江/七猫的数据就**没法算需求规模**
|
|
34
|
+
* —— 起点压根没有"在读"这个字段(起点此前连热度指标都没有,
|
|
35
|
+
* 2026-10-28 才补上月票)。
|
|
36
|
+
*
|
|
37
|
+
* ## 量纲也要说清
|
|
38
|
+
*
|
|
39
|
+
* 四个指标的数量级差几个数量级(月票 13 万 vs 晋江积分 10 亿),
|
|
40
|
+
* **只能在本平台内部横向比较**,不能跨平台比大小。
|
|
41
|
+
* 所以计算口径一律是"本平台内各题材的占比/均值/中位数"。
|
|
42
|
+
*
|
|
43
|
+
* ## 主指标 vs 辅助指标
|
|
44
|
+
*
|
|
45
|
+
* 每个平台有一个**主指标**(`label`)—— 需求规模/需求份额/头部集中度
|
|
46
|
+
* 三项公式都用它。起点额外有**辅助指标**(`aux`):
|
|
47
|
+
* 总推荐 / 周推荐 / 打赏。
|
|
48
|
+
*
|
|
49
|
+
* 辅助指标**只作交叉验证,不进那三项公式** —— 它们的口径和量级都不同:
|
|
50
|
+
* 总推荐是开书至今的**历史累计**(老书天然占优),周推荐是**周增量**,
|
|
51
|
+
* 打赏是**次数不是金额**。实测对照(2026-10-28 月票榜):
|
|
52
|
+
*
|
|
53
|
+
* · 夜无疆 月票 123406 / 总推荐 12531500 / 周推荐 41826 / 打赏 今日 19
|
|
54
|
+
* → 老书盘子大,但本周打赏很淡
|
|
55
|
+
* · 武道! 月票 96884 / 总推荐 162880 / 周推荐 4765 / 打赏 今日 3
|
|
56
|
+
* → 新书起量(月票高、总推荐还很小)
|
|
57
|
+
* · 玄鉴仙族 月票 132564 / 总推荐 7059484 / 周推荐 34655 / 打赏 今日 352
|
|
58
|
+
* → 打赏远高于同榜其它书,付费黏性明显
|
|
59
|
+
*
|
|
60
|
+
* 把它们相加或互相代入占比,得出的数没有任何意义。
|
|
61
|
+
*/
|
|
62
|
+
export const HEAT_METRIC = {
|
|
63
|
+
qidian: {
|
|
64
|
+
field: '月票',
|
|
65
|
+
label: '月票',
|
|
66
|
+
unit: '张',
|
|
67
|
+
note: '精确整数,取自作品详情页的 monthTicket',
|
|
68
|
+
sample: '月票:130343',
|
|
69
|
+
sampleValue: '130343 张',
|
|
70
|
+
sampleLine: '- **月票:** 130343',
|
|
71
|
+
precise: true,
|
|
72
|
+
precisionNote:
|
|
73
|
+
'**原样记下精确整数**,不要四舍五入(130343 不要写成"约 13 万张")',
|
|
74
|
+
// 主指标在分析里的角色
|
|
75
|
+
role: '读者当月投出的月票数',
|
|
76
|
+
caveat: '精确整数,跨书可比',
|
|
77
|
+
// 辅助指标(只有起点有)
|
|
78
|
+
aux: [
|
|
79
|
+
{
|
|
80
|
+
label: '总推荐',
|
|
81
|
+
unit: '票',
|
|
82
|
+
sampleLine: '- **总推荐:** 12531500',
|
|
83
|
+
meaning: '开书至今累计获得的推荐票数',
|
|
84
|
+
caveat: '**历史累计** —— 老书天然占优,不代表当前热度',
|
|
85
|
+
usage: '**总推荐 vs 周推荐**:总推荐远大于周推荐 = 老书的累计盘子(体量/资历大);两者量级接近 = 新书起量',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
label: '周推荐',
|
|
89
|
+
unit: '票',
|
|
90
|
+
sampleLine: '- **周推荐:** 41826',
|
|
91
|
+
meaning: '最近一周获得的推荐票数',
|
|
92
|
+
caveat: '单点值 —— 只能横向比,**不能据此判断趋势**',
|
|
93
|
+
usage: '**周推荐 vs 月票**:同题材里周推荐更高 = 最近一周推荐热度更高;月票高而周推荐低 = 热度主要来自本月前段',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
label: '打赏',
|
|
97
|
+
unit: '人次',
|
|
98
|
+
sampleLine: '- **打赏:** 今日 19 / 本周 52',
|
|
99
|
+
meaning: '今日/本周的**打赏次数**',
|
|
100
|
+
caveat: '**是次数不是金额** —— 页面不公开打赏金额,别写成"打赏 XX 元"',
|
|
101
|
+
usage: '**打赏 vs 月票**:打赏与月票同向 = 铁粉付费黏性强;月票高而打赏低 = 可能是订阅或活动拉动,不是长期铁粉盘',
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
fanqie: {
|
|
106
|
+
field: '在读',
|
|
107
|
+
label: '在读',
|
|
108
|
+
unit: '人',
|
|
109
|
+
note: '精确人数,取自番茄明文接口的 readCount',
|
|
110
|
+
sample: '在读:597999',
|
|
111
|
+
sampleValue: '597999',
|
|
112
|
+
sampleLine: '- **在读:** 597999',
|
|
113
|
+
precise: false,
|
|
114
|
+
precisionNote:
|
|
115
|
+
'**原样记下文件里的数值**,不要换算成"约 60 万"',
|
|
116
|
+
role: '平台给出的当前在读人数',
|
|
117
|
+
caveat: '精确人数;榜页显示的"59.8万"是它的舍入形式 —— **原样记录,不要换算**',
|
|
118
|
+
aux: [],
|
|
119
|
+
},
|
|
120
|
+
jinjiang: {
|
|
121
|
+
field: '积分',
|
|
122
|
+
label: '积分',
|
|
123
|
+
unit: '分',
|
|
124
|
+
note: '形如 1053978496,纯整数不带千分位逗号',
|
|
125
|
+
sample: '积分:1053978496',
|
|
126
|
+
sampleValue: '1053978496 分',
|
|
127
|
+
sampleLine: '- **积分:** 1053978496',
|
|
128
|
+
precise: true,
|
|
129
|
+
precisionNote:
|
|
130
|
+
'**原样记下精确整数**,不要四舍五入(1053978496 不要写成"约 10 亿分")',
|
|
131
|
+
role: '作品累计获得的积分',
|
|
132
|
+
caveat: '精确整数,跨书可比',
|
|
133
|
+
aux: [],
|
|
134
|
+
},
|
|
135
|
+
qimao: {
|
|
136
|
+
field: '热度',
|
|
137
|
+
label: '热度',
|
|
138
|
+
unit: '万',
|
|
139
|
+
note: '形如 3853.8万',
|
|
140
|
+
sample: '热度:3853.8万',
|
|
141
|
+
sampleValue: '3853.8万',
|
|
142
|
+
sampleLine: '- **热度:** 3853.8万',
|
|
143
|
+
precise: false,
|
|
144
|
+
precisionNote:
|
|
145
|
+
'**原样记下文件里的数值**,不要二次换算或取整(3853.8万 不要写成"约 3854 万")',
|
|
146
|
+
role: '平台给出的作品热度值',
|
|
147
|
+
caveat: '平台给出的数值,原样记录',
|
|
148
|
+
aux: [],
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 取某平台的热度指标描述;未知平台给通用兜底。
|
|
154
|
+
* @param {string} platform
|
|
155
|
+
* @returns {{field: string, label: string, unit: string, note: string,
|
|
156
|
+
* sample: string, sampleValue: string, sampleLine: string,
|
|
157
|
+
* precise: boolean, precisionNote: string, role: string, caveat: string,
|
|
158
|
+
* aux: Array<{label: string, unit: string, sampleLine: string,
|
|
159
|
+
* meaning: string, caveat: string, usage: string}>}}
|
|
160
|
+
*/
|
|
161
|
+
export function heatMetric(platform) {
|
|
162
|
+
return HEAT_METRIC[platform] || {
|
|
163
|
+
field: '热度',
|
|
164
|
+
label: '热度',
|
|
165
|
+
unit: '',
|
|
166
|
+
note: '以数据文件里给出的热度指标为准',
|
|
167
|
+
sample: '热度:…',
|
|
168
|
+
sampleValue: '…',
|
|
169
|
+
sampleLine: '- **热度:** …',
|
|
170
|
+
precise: false,
|
|
171
|
+
precisionNote: '**原样记下文件里的数值**,不要二次换算或取整',
|
|
172
|
+
role: '平台给出的热度值',
|
|
173
|
+
caveat: '以数据文件里给出的热度指标为准',
|
|
174
|
+
aux: [],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 选题方向的 11 项设计标签 —— **单一事实来源**见 `lib/direction-labels.js`。
|
|
180
|
+
*
|
|
181
|
+
* 这 11 个标签同时出现在:方向模板、写入后校验、fancy-topic 的 11 个
|
|
182
|
+
* 平级字段、story-builder 的任务输入快照。之前每处各写一遍,已经漂移过一次 ——
|
|
183
|
+
* 提示词写 `- 定位:`,实际报告写 `- **定位**:`,而 5.3 根本不查标签,
|
|
184
|
+
* 模型于是自己"适配"了一轮格式(把校验正则改成加粗形式)。
|
|
185
|
+
*
|
|
186
|
+
* **标签一律加粗**(`**定位**:`)—— 实际产物已经是这个形式,
|
|
187
|
+
* 顺势定为规范比强行改回不加粗更省事;校验也按加粗形式查。
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 各平台的**数据文件形态**(2026-10-29 提示词评审 P0-1)
|
|
192
|
+
*
|
|
193
|
+
* ## 为什么要有这个(提示词里最严重的一条错)
|
|
194
|
+
*
|
|
195
|
+
* 提示词原来对**四个平台统一**描述成:
|
|
196
|
+
*
|
|
197
|
+
* "文件里每个 `## 某题材 — N 本` 是一个题材组,每组取该题材的前 N 本(通常是 10 本)"
|
|
198
|
+
* "题材以数据文件里的官方分组为准(`## 都市日常 — 10 本` 的二级标题就是题材)"
|
|
199
|
+
*
|
|
200
|
+
* 但那是**番茄/晋江**的格式。起点和七猫是**平铺**的:
|
|
201
|
+
*
|
|
202
|
+
* - 起点(2026-10-29 实测 9 个文件 180 条):`## #1 玄鉴仙族` ——
|
|
203
|
+
* `##` 下面是**书名**,文件里**一个** `## 某题材 — N 本` 都没有;
|
|
204
|
+
* - 七猫:`### #1 书名` —— 同上。
|
|
205
|
+
*
|
|
206
|
+
* 按原文执行,模型在起点/七猫上找不到任何题材组:
|
|
207
|
+
* 要么卡住,要么把 `## #1 玄鉴仙族` 当成题材名 —— 整份报告失真。
|
|
208
|
+
*
|
|
209
|
+
* 所以数据形态必须**按平台说**,不能一句话套四个平台。
|
|
210
|
+
*
|
|
211
|
+
* ## 形态差异一览
|
|
212
|
+
*
|
|
213
|
+
* | 平台 | 分组标题 | 书目标题 | 题材来源 | `**标签:**` | `**排名变动:**` |
|
|
214
|
+
* | --- | --- | --- | --- | --- | --- |
|
|
215
|
+
* | 番茄 | `## 题材 — N 本` | `### #N 书名` | 分组标题 | 有 | 有 |
|
|
216
|
+
* | 晋江 | `## 频道 — N 本` | `### #N 书名` | `**题材:**` 字段(频道名不是题材) | 有 | 无 |
|
|
217
|
+
* | 起点 | **无** | `## #N 书名` | `**题材:**` 字段(大类·子类) | **无** | **无** |
|
|
218
|
+
* | 七猫 | **无** | `### #N 书名` | `**题材:**` + `**子分类:**` | **无** | **无** |
|
|
219
|
+
*/
|
|
220
|
+
export const DATA_SHAPE = {
|
|
221
|
+
qidian: {
|
|
222
|
+
grouped: false,
|
|
223
|
+
stratified: false,
|
|
224
|
+
groupTitle: null,
|
|
225
|
+
bookHeading: '## #N 书名',
|
|
226
|
+
genreFrom: 'field',
|
|
227
|
+
genreField: '**题材:** 大类·子类',
|
|
228
|
+
genreExample: '仙侠·修真文明',
|
|
229
|
+
hasTags: false,
|
|
230
|
+
hasRankChange: false,
|
|
231
|
+
},
|
|
232
|
+
fanqie: {
|
|
233
|
+
grouped: true,
|
|
234
|
+
stratified: true,
|
|
235
|
+
groupTitle: '## 题材名 — N 本',
|
|
236
|
+
bookHeading: '### #N 书名',
|
|
237
|
+
genreFrom: 'group',
|
|
238
|
+
genreField: null,
|
|
239
|
+
genreExample: '都市异能',
|
|
240
|
+
hasTags: true,
|
|
241
|
+
hasRankChange: true,
|
|
242
|
+
},
|
|
243
|
+
jinjiang: {
|
|
244
|
+
grouped: true,
|
|
245
|
+
stratified: false,
|
|
246
|
+
groupTitle: '## 频道名 — N 本',
|
|
247
|
+
bookHeading: '### #N 书名',
|
|
248
|
+
// 晋江的 ## 是**频道**(全站),不是题材;题材在每本书的 **题材:** 字段里。
|
|
249
|
+
genreFrom: 'field',
|
|
250
|
+
genreField: '**题材:**',
|
|
251
|
+
genreExample: '原创-言情-架空历史-爱情',
|
|
252
|
+
hasTags: true,
|
|
253
|
+
hasRankChange: false,
|
|
254
|
+
},
|
|
255
|
+
qimao: {
|
|
256
|
+
grouped: false,
|
|
257
|
+
stratified: false,
|
|
258
|
+
groupTitle: null,
|
|
259
|
+
bookHeading: '### #N 书名',
|
|
260
|
+
genreFrom: 'field',
|
|
261
|
+
genreField: '**题材:** / **子分类:**',
|
|
262
|
+
genreExample: '都市 / 都市异能',
|
|
263
|
+
hasTags: false,
|
|
264
|
+
hasRankChange: false,
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/** 取某平台的数据形态;未知平台按"平铺 + 题材字段"兜底(更保守)。 */
|
|
269
|
+
export function dataShape(platform) {
|
|
270
|
+
return DATA_SHAPE[platform] || {
|
|
271
|
+
grouped: false,
|
|
272
|
+
stratified: false,
|
|
273
|
+
groupTitle: null,
|
|
274
|
+
bookHeading: '#N 书名',
|
|
275
|
+
genreFrom: 'field',
|
|
276
|
+
genreField: '**题材:**',
|
|
277
|
+
genreExample: '(文件里的题材值)',
|
|
278
|
+
hasTags: false,
|
|
279
|
+
hasRankChange: false,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 各平台的榜单配置(与 /home/cloud/dsh-fancy-webnovel/commands/scan/platforms/ 保持一致)
|
|
284
|
+
export const RANK_LISTS = {
|
|
285
|
+
qidian: [
|
|
286
|
+
{ id: 'hotsales', label: '畅销榜' },
|
|
287
|
+
{ id: 'yuepiao', label: '月票榜' },
|
|
288
|
+
{ id: 'readindex', label: '阅读榜' },
|
|
289
|
+
{ id: 'newfans', label: '书友榜' },
|
|
290
|
+
{ id: 'rec', label: '推荐榜' },
|
|
291
|
+
{ id: 'update', label: '更新榜' },
|
|
292
|
+
{ id: 'sign', label: '签约榜' },
|
|
293
|
+
{ id: 'newbook', label: '新书榜' },
|
|
294
|
+
{ id: 'newauthor', label: '新人作者榜' },
|
|
295
|
+
],
|
|
296
|
+
fanqie: [
|
|
297
|
+
{ id: '1_2', label: '男频阅读榜' },
|
|
298
|
+
{ id: '1_1', label: '男频新书榜' },
|
|
299
|
+
{ id: '0_2', label: '女频阅读榜' },
|
|
300
|
+
{ id: '0_1', label: '女频新书榜' },
|
|
301
|
+
],
|
|
302
|
+
jinjiang: [
|
|
303
|
+
{ id: '5', label: '月榜' },
|
|
304
|
+
{ id: '7', label: '总分榜' },
|
|
305
|
+
{ id: '4', label: '季度榜' },
|
|
306
|
+
{ id: '3', label: '新晋作者榜' },
|
|
307
|
+
],
|
|
308
|
+
qimao: [
|
|
309
|
+
{ id: 'boy/hot/month', label: '男生大热月榜' },
|
|
310
|
+
{ id: 'boy/new/month', label: '男生新书月榜' },
|
|
311
|
+
{ id: 'girl/hot/month', label: '女生大热月榜' },
|
|
312
|
+
{ id: 'girl/new/month', label: '女生新书月榜' },
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
export function platformLabel(platform) {
|
|
317
|
+
const row = PLATFORM_TABLE.find(r => r[1] === platform);
|
|
318
|
+
return row ? row[2] : platform;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* 各平台的扫描方式选项(UI 层)。
|
|
323
|
+
*
|
|
324
|
+
* 目前只有番茄有两条取数路径:
|
|
325
|
+
* api —— 走番茄自家明文接口 /api/book/info 拿书名/作者/简介;
|
|
326
|
+
* ocr —— 渲染后截图 + tesseract 识别。
|
|
327
|
+
*
|
|
328
|
+
* 两条路径的利弊直接写进 description:用户在提问面板上就能看到
|
|
329
|
+
* "精度/耗时" 与 "是否依赖番茄内部接口" 的取舍,不必先跑一次才知道。
|
|
330
|
+
*/
|
|
331
|
+
export const SCAN_MODE_OPTIONS = {
|
|
332
|
+
fanqie: [
|
|
333
|
+
{
|
|
334
|
+
id: SCAN_MODE_API,
|
|
335
|
+
label: 'API扫描(默认)',
|
|
336
|
+
description:
|
|
337
|
+
'优势:书名/作者/简介取自番茄明文接口,零识别误差、精度 100%,整榜耗时约为 OCR 扫描的 1/5;' +
|
|
338
|
+
'劣势:依赖番茄 /api/book/info 接口,该接口若变更或临时不可用时,简介会缺失',
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
id: SCAN_MODE_OCR,
|
|
342
|
+
label: 'OCR扫描',
|
|
343
|
+
description:
|
|
344
|
+
'优势:渲染后截图识别,不依赖番茄内部接口,接口失效也能采到;' +
|
|
345
|
+
'劣势:每本约 0.5 秒且存在认字误差(实测简介字符错误率 8%~17%),整榜耗时约为 API 扫描的 5 倍',
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* 扫描方式 id → 展示用名称(去掉"(默认)"后缀,只在结果回执里展示)。
|
|
352
|
+
*/
|
|
353
|
+
export function scanModeLabel(mode) {
|
|
354
|
+
const hit = (SCAN_MODE_OPTIONS.fanqie || []).find(m => m.id === mode);
|
|
355
|
+
return hit ? hit.label.replace(/(默认)$/, '') : String(mode || '');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* 把 DSH 回传的 label 解析成扫描方式 id。
|
|
360
|
+
*
|
|
361
|
+
* DSH 的 selected 回传的是 label 字符串(options.value 会被运行时静默忽略),
|
|
362
|
+
* 所以只能按 label 的字母前缀判断 —— 不能依赖 label 里带 id。
|
|
363
|
+
* 解析不出合法 id 时返回 null,由调用方按"无效选择"处理。
|
|
364
|
+
*/
|
|
365
|
+
export function parseScanMode(selected) {
|
|
366
|
+
const s = String(selected == null ? '' : selected).trim();
|
|
367
|
+
if (!s) return null;
|
|
368
|
+
const upper = s.toUpperCase();
|
|
369
|
+
if (upper.startsWith('API')) return SCAN_MODE_API;
|
|
370
|
+
if (upper.startsWith('OCR')) return SCAN_MODE_OCR;
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* report-schema.js —— 扫榜报告的**结构契约**(提示词与校验脚本同源)
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么要有这个文件
|
|
5
|
+
*
|
|
6
|
+
* 评审 F14 指出:提示词里写一份、校验脚本里写一份,**口径必然漂**。
|
|
7
|
+
* 提示词说"3 张表",脚本查的是另一种表头;提示词说"11 项标签",
|
|
8
|
+
* 脚本数的是另一个清单 —— 校验通过了,报告其实不合格。
|
|
9
|
+
*
|
|
10
|
+
* 现在**只定义一次**:
|
|
11
|
+
* - 提示词从这儿取章节标题、取表头、取数字要求;
|
|
12
|
+
* - `scripts/append-block.js` 也从这儿取。
|
|
13
|
+
*
|
|
14
|
+
* 改一处,两边同时变。
|
|
15
|
+
*
|
|
16
|
+
* ## 这不是"内容质量"契约
|
|
17
|
+
*
|
|
18
|
+
* 这里定义的全是**可机械判定**的东西:章节标题在不在、表头对不对、
|
|
19
|
+
* 方向几个、标签齐不齐、字数够不够。
|
|
20
|
+
* "选得好不好""洞察深不深"不在这里 —— 那是模型的事,脚本管不了,
|
|
21
|
+
* 也不该管(管了就成了"为了过校验而写废话")。
|
|
22
|
+
*/
|
|
23
|
+
import { DIRECTION_LABELS } from '#direction-labels';
|
|
24
|
+
|
|
25
|
+
/** 报告的七个章节 —— 标题**逐字**固定,顺序也固定 */
|
|
26
|
+
export const SECTION_TITLES = [
|
|
27
|
+
'一、数据与方法',
|
|
28
|
+
'二、市场结构(单日快照)',
|
|
29
|
+
'三、热门题材分析',
|
|
30
|
+
'四、用户画像',
|
|
31
|
+
'五、竞争度评估',
|
|
32
|
+
'六、选题方向建议',
|
|
33
|
+
'七、风险与验证',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 机械判定阈值 —— 提示词与脚本共用。
|
|
38
|
+
*
|
|
39
|
+
* 数字都取"能被脚本精确验证"的值:
|
|
40
|
+
* - 字数用**汉字数**而不是字符数(标点、表格符号、markdown 标记不算);
|
|
41
|
+
* - 表格数用**分隔行**数(`| --- | --- |` 那种),不是"看起来像表格的行";
|
|
42
|
+
* - 标签要求"加粗 + 全角冒号 + 正文 ≥20 汉字",避免"写个标题充数"。
|
|
43
|
+
*/
|
|
44
|
+
export const REPORT_RULES = {
|
|
45
|
+
/** 方向个数 */
|
|
46
|
+
minDirections: 3,
|
|
47
|
+
maxDirections: 4,
|
|
48
|
+
/** 每个方向的 11 项设计标签 */
|
|
49
|
+
minTagsPerDirection: DIRECTION_LABELS.length,
|
|
50
|
+
/** 每个标签后面的正文汉字数下限(防止"写个标签名充数") */
|
|
51
|
+
minTagChars: 20,
|
|
52
|
+
/** 全文汉字数区间 */
|
|
53
|
+
minCjkTotal: 4000,
|
|
54
|
+
maxCjkTotal: 15000,
|
|
55
|
+
/** 纯正文汉字数下限(排除表格行、标题行、标签行之后的正文量) */
|
|
56
|
+
minPureCjk: 3000,
|
|
57
|
+
/** 表格张数 */
|
|
58
|
+
minTables: 3,
|
|
59
|
+
/** 至少几处"《书名》…月票 N 张"式的引用 */
|
|
60
|
+
minCitations: 2,
|
|
61
|
+
/** 至少几处"推断"标记 */
|
|
62
|
+
minInferMarks: 3,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 三张表的表头 —— **按平台**给("需求份额"在平铺平台要叫"样本内份额")。
|
|
67
|
+
*
|
|
68
|
+
* @param {string} metricLabel 热度指标名(如"月票"、"在读")
|
|
69
|
+
* @param {boolean} grouped 数据是否按题材分组(番茄/晋江)
|
|
70
|
+
* @returns {{topic: string, competition: string, choice: string}}
|
|
71
|
+
*/
|
|
72
|
+
export function tableHeaders(metricLabel, grouped) {
|
|
73
|
+
const share = grouped ? '需求份额' : '样本内份额';
|
|
74
|
+
return {
|
|
75
|
+
topic: `题材 | 书目数 | ${metricLabel}总量 | 均值 | 中位数 | ${share}`,
|
|
76
|
+
competition: `题材 | ${share} | 头部集中度 | 同质化程度 | 差异化空间 | 判断`,
|
|
77
|
+
choice: '方向 | 题材 | 金手指 | 情绪价值 | 差异化 | 风险 | 优先级',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 表名 → 表头(脚本按顺序校验三张表) */
|
|
82
|
+
export const TABLE_ORDER = ['topic', 'competition', 'choice'];
|
|
83
|
+
|
|
84
|
+
/** 表格分隔行(`| --- | --- |` 那种) */
|
|
85
|
+
export const TABLE_SEP_RE = /^\s*\|(?:\s*:?-{2,}:?\s*\|)+\s*$/;
|
|
86
|
+
|
|
87
|
+
/** 方向标题 */
|
|
88
|
+
export const DIRECTION_RE = /^###\s*方向\s*(\d+)\s*[::]/;
|
|
89
|
+
|
|
90
|
+
export { DIRECTION_LABELS };
|