@cloud411716/fancy-webnovel 2.2.0 → 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -0
- package/client.js +201 -15
- package/cordis.patch.yml +5 -1
- package/docs/douyin-plugin.md +118 -0
- package/docs/material-library/README.md +404 -0
- package/docs/material-library/import.js +437 -0
- package/docs/material-library/prompt.md +370 -0
- package/docs/material-library/queries.surql +94 -0
- package/docs/material-library/sample.jsonl +14 -0
- package/docs/material-library/schema.surql +46 -0
- package/docs/mcp-profile.example.yml +24 -0
- package/docs/story-graph-design.md +602 -0
- package/docs/story-graph-validation.md +81 -0
- package/docs/workspace-mcp.md +284 -0
- package/index.js +12 -7
- package/lib/atomic-write.js +109 -0
- package/lib/chapter-order.js +100 -0
- package/lib/confirmation.js +80 -0
- package/lib/db-init.js +123 -0
- package/lib/deployed-state.js +11 -11
- package/lib/direction-labels.js +265 -0
- package/lib/error-codes.js +31 -31
- package/lib/infra.js +29 -4
- package/lib/material-db.js +149 -0
- package/lib/path-guard.js +150 -0
- package/lib/projects.js +400 -0
- package/lib/scraper-utils.js +140 -0
- package/lib/surreal-server.js +338 -0
- package/lib/topic-decision.js +860 -0
- package/lib/topic-fields.js +251 -0
- package/lib/topic-schema.js +225 -0
- package/lib/workspace.js +501 -0
- package/package.json +31 -7
- package/plugins/fancy-analyze/analyze-definitions.md +519 -0
- package/plugins/fancy-analyze/index.js +1128 -77
- package/plugins/fancy-analyze/ingest-coverage.js +107 -0
- package/plugins/fancy-analyze/schema-analyze.md +355 -107
- package/plugins/fancy-analyze/scripts/append-summary-chapter.js +612 -0
- package/plugins/fancy-analyze/summary-progress.js +71 -7
- package/plugins/fancy-analyze/templates.js +437 -139
- package/plugins/fancy-bootstrap/index.js +276 -131
- package/plugins/fancy-bootstrap/scripts/init.js +59 -25
- package/plugins/fancy-bootstrap/templates.js +35 -18
- package/plugins/fancy-db/index.js +172 -0
- package/plugins/fancy-douyin/deploy.js +134 -0
- package/plugins/fancy-douyin/index.js +551 -0
- package/plugins/fancy-douyin/prompts/01-analysis-prompt.md +84 -0
- package/plugins/fancy-douyin/scripts/crawl-note.cjs +194 -0
- package/plugins/fancy-douyin/scripts/lib.cjs +197 -0
- package/plugins/fancy-douyin/scripts/scan-account.cjs +128 -0
- package/plugins/fancy-douyin/scripts/start-chrome.cjs +203 -0
- package/plugins/fancy-douyin/state.js +197 -0
- package/plugins/fancy-douyin/templates.js +127 -0
- package/plugins/fancy-export/exporter.js +805 -0
- package/plugins/fancy-export/index.js +63 -0
- package/plugins/fancy-import/import-driver.js +29 -6
- package/plugins/fancy-import/index.js +13 -5
- package/plugins/fancy-import/prompts/01-import-prompt.md +39 -14
- package/plugins/fancy-import/templates.js +17 -8
- package/plugins/fancy-material/exporter.js +153 -0
- package/plugins/fancy-material/genres.js +256 -0
- package/plugins/fancy-material/index.js +835 -0
- package/plugins/fancy-material/inventory.js +145 -0
- package/plugins/fancy-material/kinds.js +313 -0
- package/plugins/fancy-material/queue.js +55 -0
- package/plugins/fancy-material/scripts/query.js +254 -0
- package/plugins/fancy-material/templates.js +258 -0
- package/plugins/fancy-material/tool-deploy.js +138 -0
- package/plugins/fancy-material/validate.js +149 -0
- package/plugins/fancy-project/index.js +133 -0
- package/plugins/fancy-scan/existing-block.js +100 -0
- package/plugins/fancy-scan/index.js +410 -21
- package/plugins/fancy-scan/prompts/01-analysis-prompt.md +302 -0
- package/plugins/fancy-scan/scripts/append-block.js +725 -0
- package/plugins/fancy-scan/scripts/report-validate.js +824 -0
- package/plugins/fancy-scan/scripts/run-scan.js +169 -22
- package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +145 -16
- package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +179 -49
- package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +38 -20
- package/plugins/fancy-scan/templates/blocks.js +155 -0
- package/plugins/fancy-scan/templates/platforms.js +372 -0
- package/plugins/fancy-scan/templates/report-schema.js +90 -0
- package/plugins/fancy-scan/templates.js +545 -146
- package/plugins/fancy-scan/tool-deploy.js +335 -0
- package/plugins/fancy-scan/wait-turn.js +46 -0
- package/plugins/fancy-story-builder/db/README.md +106 -0
- package/plugins/fancy-story-builder/db/client.js +228 -0
- package/plugins/fancy-story-builder/db/commands.js +145 -0
- package/plugins/fancy-story-builder/db/ids.js +39 -0
- package/plugins/fancy-story-builder/db/import.js +798 -0
- package/plugins/fancy-story-builder/db/project.js +299 -0
- package/plugins/fancy-story-builder/db/query.js +361 -0
- package/plugins/fancy-story-builder/db/schema.js +417 -0
- package/plugins/fancy-story-builder/db/stage-sync.js +238 -0
- package/plugins/fancy-story-builder/flesh-output.js +19 -6
- package/plugins/fancy-story-builder/index.js +4105 -396
- package/plugins/fancy-story-builder/patch-driver.js +390 -0
- package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +29 -19
- package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +72 -10
- package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +40 -5
- package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +37 -0
- package/plugins/fancy-story-builder/purpose-template.md +42 -66
- package/plugins/fancy-story-builder/references/formulas-skeleton.json +229 -56
- package/plugins/fancy-story-builder/references/genre-profiles.json +3514 -0
- package/plugins/fancy-story-builder/references/glossary.md +15 -6
- package/plugins/fancy-story-builder/references/index.md +97 -6
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +1 -1
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +25 -7
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +5 -1
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +6 -2
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +15 -5
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +1 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +2 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +1 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +19 -1
- package/plugins/fancy-story-builder/schema-setting.json +1100 -0
- package/plugins/fancy-story-builder/schema-setting.md +201 -293
- package/plugins/fancy-story-builder/scripts/brief-validate.js +164 -0
- package/plugins/fancy-story-builder/scripts/content-quality.js +4 -1
- package/plugins/fancy-story-builder/scripts/flesh-validate.js +135 -12
- package/plugins/fancy-story-builder/scripts/graph-utils.js +146 -47
- package/plugins/fancy-story-builder/scripts/reference-log.js +110 -0
- package/plugins/fancy-story-builder/scripts/rhythm-validate.js +100 -4
- package/plugins/fancy-story-builder/scripts/schema-pages.js +2668 -0
- package/plugins/fancy-story-builder/scripts/semantic-lint.js +5 -1
- package/plugins/fancy-story-builder/scripts/skeleton-validate.js +158 -17
- package/plugins/fancy-story-builder/scripts/stage-batches.js +36 -2
- package/plugins/fancy-story-builder/scripts/stage-contract.js +12 -0
- package/plugins/fancy-story-builder/scripts/story-artifacts.js +933 -0
- package/plugins/fancy-story-builder/scripts/upstream-query.js +1 -1
- package/plugins/fancy-story-builder/scripts/v2/baseline.js +112 -0
- package/plugins/fancy-story-builder/scripts/v2/cast.js +517 -0
- package/plugins/fancy-story-builder/scripts/v2/context.js +434 -0
- package/plugins/fancy-story-builder/scripts/v2/contracts.js +493 -0
- package/plugins/fancy-story-builder/scripts/v2/core.js +419 -0
- package/plugins/fancy-story-builder/scripts/v2/engine.js +1068 -0
- package/plugins/fancy-story-builder/scripts/v2/export-markdown.js +794 -0
- package/plugins/fancy-story-builder/scripts/v2/genre.js +195 -0
- package/plugins/fancy-story-builder/scripts/v2/knowledge.js +321 -0
- package/plugins/fancy-story-builder/scripts/v2/macro.js +409 -0
- package/plugins/fancy-story-builder/scripts/v2/manifest.js +538 -0
- package/plugins/fancy-story-builder/scripts/v2/migrate.js +137 -0
- package/plugins/fancy-story-builder/scripts/v2/prompts.js +1335 -0
- package/plugins/fancy-story-builder/scripts/v2/rhythm.js +618 -0
- package/plugins/fancy-story-builder/scripts/v2/scale.js +843 -0
- package/plugins/fancy-story-builder/scripts/v2/snapshot.js +121 -0
- package/plugins/fancy-story-builder/scripts/v2/stages.js +273 -0
- package/plugins/fancy-story-builder/scripts/v2/writing.js +216 -0
- package/plugins/fancy-story-builder/storydb/README.md +271 -0
- package/plugins/fancy-story-builder/storydb/cli.js +458 -0
- package/plugins/fancy-story-builder/storydb/client.js +351 -0
- package/plugins/fancy-story-builder/storydb/compile.js +452 -0
- package/plugins/fancy-story-builder/storydb/context.js +288 -0
- package/plugins/fancy-story-builder/storydb/definitions.js +699 -0
- package/plugins/fancy-story-builder/storydb/driver.js +106 -0
- package/plugins/fancy-story-builder/storydb/dsh-tools.js +313 -0
- package/plugins/fancy-story-builder/storydb/export.js +175 -0
- package/plugins/fancy-story-builder/storydb/ids.js +31 -0
- package/plugins/fancy-story-builder/storydb/impact.js +170 -0
- package/plugins/fancy-story-builder/storydb/import.js +559 -0
- package/plugins/fancy-story-builder/storydb/mcp.js +99 -0
- package/plugins/fancy-story-builder/storydb/prompts.js +952 -0
- package/plugins/fancy-story-builder/storydb/stage-command.js +379 -0
- package/plugins/fancy-story-builder/storydb/stage.js +426 -0
- package/plugins/fancy-story-builder/storydb/store.js +891 -0
- package/plugins/fancy-story-builder/storydb/tools.js +592 -0
- package/plugins/fancy-story-builder/storydb/validation.js +183 -0
- package/plugins/fancy-story-builder/templates/v2/10-scale/scale-template.json +144 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/emotion-template.json +15 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/endings-template.json +16 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/lines-template.json +28 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/roles-template.json +10 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/story-template.json +19 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/volumes-template.json +24 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/world-template.json +24 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/characters-template.json +49 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/climaxes-template.json +14 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/conflicts-template.json +30 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/factions-template.json +18 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/foreshadows-template.json +29 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/information-template.json +29 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/items-template.json +13 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/locations-template.json +12 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/payoffs-template.json +16 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/relations-template.json +44 -0
- package/plugins/fancy-story-builder/templates/v2/40-rhythm/chapter-map-template.json +81 -0
- package/plugins/fancy-story-builder/templates/v2/40-rhythm/outline-template.json +162 -0
- package/plugins/fancy-story-builder/templates/v2/50-writing/ledger-template.json +46 -0
- package/plugins/fancy-story-builder/templates/v2/60-deslop/report-template.json +10 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//346/227/266/345/272/217/346/230/240/345/260/204/346/250/241/346/235/277.json +267 -44
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//344/270/226/347/225/214/350/247/202/350/247/204/345/210/231/346/250/241/346/235/277.json +121 -19
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/263/351/224/256/351/201/223/345/205/267/346/250/241/346/235/277.json +129 -21
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/266/344/273/226/345/256/236/344/275/223/346/250/241/346/235/277.json +62 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/206/262/347/252/201/346/250/241/346/235/277.json +76 -17
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/211/247/346/203/205/346/250/241/346/235/277.json +56 -8
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/212/277/345/212/233/346/250/241/346/235/277.json +63 -13
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/234/260/347/202/271/346/250/241/346/235/277.json +34 -6
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//347/273/223/345/261/200/346/250/241/346/235/277.json +81 -18
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//350/247/222/350/211/262/346/250/241/346/235/277.json +202 -42
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/205/263/347/263/273/346/250/241/346/235/277.json +160 -27
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/256/236/344/275/223/346/250/241/346/235/277.json +217 -41
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//346/241/206/346/236/266/346/250/241/346/235/277.json +215 -41
- package/plugins/fancy-story-builder/templates.js +272 -74
- package/plugins/fancy-story-builder/wiki-references.js +429 -93
- package/plugins/fancy-story-builder/writing-driver.js +336 -9
- package/plugins/fancy-topic/index.js +414 -61
- package/plugins/fancy-topic/prompts/01-exploration-prompt.md +115 -0
- package/plugins/fancy-topic/scripts/save-suggestion.js +219 -0
- package/plugins/fancy-topic/scripts/select-topic.js +106 -63
- package/plugins/fancy-topic/templates.js +225 -122
- package/plugins/fancy-topic/tool-deploy.js +200 -0
- package/lib/safe-rm.js +0 -94
|
@@ -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 };
|