@cloud411716/fancy-webnovel 1.0.46 → 2.2.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.
Files changed (134) hide show
  1. package/client.js +183 -0
  2. package/cordis.patch.yml +0 -14
  3. package/index.js +21 -37
  4. package/lib/chinese-num.js +111 -0
  5. package/lib/deployed-state.js +141 -0
  6. package/lib/error-codes.js +104 -0
  7. package/lib/infra.js +569 -0
  8. package/lib/llm-wiki.js +109 -0
  9. package/lib/safe-rm.js +94 -0
  10. package/lib/scraper-utils.js +165 -0
  11. package/package.json +37 -5
  12. package/plugins/fancy-analyze/command-input.js +65 -0
  13. package/plugins/fancy-analyze/index.js +973 -0
  14. package/plugins/fancy-analyze/schema-analyze.md +379 -0
  15. package/plugins/fancy-analyze/schema-write.md +178 -0
  16. package/plugins/fancy-analyze/scripts/split_source.js +292 -0
  17. package/plugins/fancy-analyze/summary-progress.js +431 -0
  18. package/plugins/fancy-analyze/templates.js +881 -0
  19. package/plugins/fancy-bootstrap/index.js +168 -0
  20. package/plugins/fancy-bootstrap/scripts/init.js +143 -0
  21. package/plugins/fancy-bootstrap/scripts/path-utils.js +21 -0
  22. package/plugins/fancy-bootstrap/templates.js +54 -0
  23. package/plugins/fancy-import/import-core.js +1118 -0
  24. package/plugins/fancy-import/import-driver.js +581 -0
  25. package/plugins/fancy-import/index.js +730 -0
  26. package/plugins/fancy-import/prompts/01-import-prompt.md +106 -0
  27. package/plugins/fancy-import/templates.js +49 -0
  28. package/plugins/fancy-scan/index.js +319 -0
  29. package/plugins/fancy-scan/scripts/run-scan.js +520 -0
  30. package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +794 -0
  31. package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +339 -0
  32. package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +611 -0
  33. package/plugins/fancy-scan/templates.js +295 -0
  34. package/plugins/fancy-story-builder/deploy-manifest.js +239 -0
  35. package/plugins/fancy-story-builder/deslop-driver.js +821 -0
  36. package/plugins/fancy-story-builder/flesh-output.js +155 -0
  37. package/plugins/fancy-story-builder/index.js +3152 -0
  38. package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +553 -0
  39. package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +540 -0
  40. package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +504 -0
  41. package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +188 -0
  42. package/plugins/fancy-story-builder/prompts/05-deslop-prompt.md +96 -0
  43. package/plugins/fancy-story-builder/purpose-template.md +107 -0
  44. package/plugins/fancy-story-builder/references/formulas-rhythm.json +302 -0
  45. package/plugins/fancy-story-builder/references/formulas-skeleton.json +229 -0
  46. package/plugins/fancy-story-builder/references/glossary.md +585 -0
  47. package/plugins/fancy-story-builder/references/index.md +490 -0
  48. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/00-index.md +174 -0
  49. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/01-scene.md +150 -0
  50. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/02-chapter-workflow.md +99 -0
  51. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/03-opening-chapter.md +104 -0
  52. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/04-dialogue-mastery.md +274 -0
  53. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/05-genre-formulas.md +432 -0
  54. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/06-tone-ensemble.md +259 -0
  55. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/07-ledger-audit.md +222 -0
  56. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/08-diagnosis-revision.md +205 -0
  57. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/09-reading-power.md +135 -0
  58. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/10-deslop.md +129 -0
  59. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +98 -0
  60. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/01-upgrade-rhythm.md +300 -0
  61. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/02-arc-rhythm.md +168 -0
  62. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/03-highpoint-hooks.md +189 -0
  63. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/04-satisfaction.md +226 -0
  64. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/05-foreshadow-density.md +195 -0
  65. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/06-chapter-outline.md +226 -0
  66. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +163 -0
  67. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/00-index.md +100 -0
  68. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +239 -0
  69. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/02-worldbuilding.md +245 -0
  70. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +181 -0
  71. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/04-emotion-methods.md +286 -0
  72. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +177 -0
  73. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +91 -0
  74. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +208 -0
  75. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/02-packaging.md +135 -0
  76. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +305 -0
  77. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/04-conflict.md +328 -0
  78. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/05-quota.md +187 -0
  79. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +142 -0
  80. package/plugins/fancy-story-builder/rhythm-output.js +140 -0
  81. package/plugins/fancy-story-builder/schema-setting.md +324 -0
  82. package/plugins/fancy-story-builder/scripts/ai-patterns.js +619 -0
  83. package/plugins/fancy-story-builder/scripts/check-guide-compliance.js +289 -0
  84. package/plugins/fancy-story-builder/scripts/content-quality.js +413 -0
  85. package/plugins/fancy-story-builder/scripts/deslop-validate.js +143 -0
  86. package/plugins/fancy-story-builder/scripts/error-classes.js +112 -0
  87. package/plugins/fancy-story-builder/scripts/flesh-validate.js +1189 -0
  88. package/plugins/fancy-story-builder/scripts/formula-engine.js +112 -0
  89. package/plugins/fancy-story-builder/scripts/graph-utils.js +1385 -0
  90. package/plugins/fancy-story-builder/scripts/label-coverage.js +171 -0
  91. package/plugins/fancy-story-builder/scripts/prompt-drift-check.js +441 -0
  92. package/plugins/fancy-story-builder/scripts/reading-power.js +298 -0
  93. package/plugins/fancy-story-builder/scripts/rhythm-validate.js +1216 -0
  94. package/plugins/fancy-story-builder/scripts/semantic-lint.js +377 -0
  95. package/plugins/fancy-story-builder/scripts/skeleton-quota.js +145 -0
  96. package/plugins/fancy-story-builder/scripts/skeleton-validate.js +1832 -0
  97. package/plugins/fancy-story-builder/scripts/stage-batches.js +1371 -0
  98. package/plugins/fancy-story-builder/scripts/stage-contract.js +417 -0
  99. package/plugins/fancy-story-builder/scripts/upstream-query.js +198 -0
  100. package/plugins/fancy-story-builder/scripts/writing-rules.js +167 -0
  101. package/plugins/fancy-story-builder/scripts/writing-validate.js +682 -0
  102. package/plugins/fancy-story-builder/skeleton-output.js +199 -0
  103. package/plugins/fancy-story-builder/stage-driver.js +1049 -0
  104. 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 +204 -0
  105. 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
  106. 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
  107. 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
  108. 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 +66 -0
  109. 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 +76 -0
  110. 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 +72 -0
  111. 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 +37 -0
  112. 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 +53 -0
  113. 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 +40 -0
  114. 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 +79 -0
  115. 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 +132 -0
  116. 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 +133 -0
  117. 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 +160 -0
  118. 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
  119. 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 +134 -0
  120. package/plugins/fancy-story-builder/templates.js +1248 -0
  121. package/plugins/fancy-story-builder/wiki-references.js +204 -0
  122. package/plugins/fancy-story-builder/writing-driver.js +1061 -0
  123. package/plugins/fancy-topic/index.js +704 -0
  124. package/plugins/fancy-topic/scripts/select-topic.js +274 -0
  125. package/plugins/fancy-topic/templates.js +373 -0
  126. package/commands/bootstrap/index.js +0 -164
  127. package/commands/bootstrap/init-project.js +0 -121
  128. package/commands/scan/index.js +0 -283
  129. package/commands/scan/platforms/fanqie.js +0 -146
  130. package/commands/scan/platforms/jinjiang.js +0 -143
  131. package/commands/scan/platforms/qidian.js +0 -102
  132. package/commands/scan/platforms/qimao.js +0 -127
  133. package/commands/scan/scraper.js +0 -195
  134. package/infra.js +0 -160
@@ -1,146 +0,0 @@
1
- /**
2
- * fanqie.js — 番茄小说排行榜采集
3
- *
4
- * 策略(优先级递减):
5
- * 1. 移动端页面(__INITIAL_STATE__ 内嵌数据)— 无需字体解密
6
- * 2. Playwright 浏览器 — 仅作为 fallback
7
- *
8
- * Playwright 通过 ctx.effect() 注册生命周期,Ctrl+C 时自动清理。
9
- * 使用 browserContext({ signal }) 确保 page.evaluate 也能响应中止信号。
10
- */
11
-
12
- export const platform = 'fanqie';
13
- export const label = '番茄';
14
-
15
- export const rankList = [
16
- { id: '1_2', label: '男频阅读榜', channel: '1', type: '2' },
17
- { id: '1_1', label: '男频新书榜', channel: '1', type: '1' },
18
- { id: '0_2', label: '女频阅读榜', channel: '0', type: '2' },
19
- { id: '0_1', label: '女频新书榜', channel: '0', type: '1' },
20
- ];
21
-
22
- // 移动端页面(__INITIAL_STATE__ 内嵌数据)
23
- const MOBILE_BASE = 'https://m.reader.qq.com';
24
-
25
- /**
26
- * @param {object} ctx
27
- * @param {string} channel
28
- * @param {string} type
29
- * @param {AbortSignal} signal
30
- */
31
- async function mobileFetch(ctx, channel, type, signal) {
32
- const url = `${MOBILE_BASE}/rank/${channel}_${type}.html`;
33
- let timerDispose;
34
- const timeout = new Promise((_, reject) => {
35
- timerDispose = ctx.timeout(() => reject(new Error('timeout')), 15000);
36
- });
37
- try {
38
- const res = await Promise.race([
39
- fetch(url, {
40
- headers: {
41
- 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)',
42
- 'Accept': 'text/html',
43
- },
44
- signal,
45
- }),
46
- timeout,
47
- ]);
48
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
49
- const html = await res.text();
50
- const m = html.match(/window\.__INITIAL_STATE__\s*=\s*(\{[\s\S]*?\});/);
51
- if (!m) throw new Error('__INITIAL_STATE__ not found');
52
- try {
53
- return JSON.parse(m[1]);
54
- } catch (_) {
55
- throw new Error('__INITIAL_STATE__ JSON parse failed');
56
- }
57
- } finally {
58
- try { timerDispose?.(); } catch (_) {}
59
- }
60
- }
61
-
62
- /**
63
- * Scrape one rank list. Tries mobile → Playwright.
64
- * @param {string} rankId - e.g. '1_2'
65
- * @param {object} ctx - Cordis plugin context
66
- * @param {AbortSignal} signal
67
- * @returns {Promise<{ ok: boolean, books: object[], error?: string }>}
68
- */
69
- export async function scrapeRank(rankId, ctx, signal) {
70
- const [channel, type] = rankId.split('_');
71
-
72
- // 策略1: 移动端页面
73
- try {
74
- const data = await mobileFetch(ctx, channel, type, signal);
75
- const books = parseMobileData(data);
76
- if (books.length > 0) return { ok: true, books };
77
- } catch (_) { /* try next strategy */ }
78
-
79
- // 策略2: Playwright(通过 ctx.effect 注册生命周期,Ctrl+C 自动清理)
80
- let browser;
81
- const disposeEffect = ctx.effect(() => {
82
- return () => { browser?.close(); };
83
- }, 'fanqie: browser cleanup');
84
-
85
- try {
86
- const { chromium } = await import('playwright-core');
87
- browser = await chromium.launch({ headless: true });
88
- const context = await browser.newContext({ signal });
89
- const page = await context.newPage();
90
- const url = `https://www.fanqie.com/rank/${channel}_${type}/`;
91
-
92
- await page.goto(url, { waitUntil: 'networkidle', signal });
93
-
94
- const books = await page.evaluate(() => {
95
- const s = window.__INITIAL_STATE__ || {};
96
- let list = null;
97
- for (const c of [s.rank, s.rankData, s.page]) {
98
- if (c?.book_list) { list = c.book_list; break; }
99
- if (c?.bookList) { list = c.bookList; break; }
100
- if (c?.rankList) { list = c.rankList; break; }
101
- }
102
- if (!list || !Array.isArray(list)) return null;
103
- return list.map((b, i) => ({
104
- rank: b.rank ?? (i + 1),
105
- title: b.title ?? b.bookName ?? '',
106
- author: b.author ?? '',
107
- category: '',
108
- words: b.wordCount ?? 0,
109
- intro: (b.description ?? '').slice(0, 150),
110
- cover: b.coverUrl ?? '',
111
- bookId: b.bookId ?? '',
112
- }));
113
- });
114
-
115
- // 嵌套 try-finally:context.close() 失败不影响 disposeEffect 执行
116
- try {
117
- await context.close();
118
- } finally {
119
- disposeEffect();
120
- }
121
-
122
- if (books && books.length > 0) return { ok: true, books };
123
- } catch (_) { /* exhausted */ }
124
-
125
- return { ok: false, books: [], error: 'all strategies exhausted' };
126
- }
127
-
128
- function parseMobileData(data) {
129
- if (!data || typeof data !== 'object') return [];
130
- let list = null;
131
- const cands = [data.rank?.book_list, data.rank?.bookList, data.page?.bookList, data];
132
- for (const c of cands) {
133
- if (Array.isArray(c) && c.length > 0) { list = c; break; }
134
- }
135
- if (!list) return [];
136
- return list.map((b, i) => ({
137
- rank: b.rank ?? (i + 1),
138
- title: b.title ?? b.name ?? '',
139
- author: b.author ?? '',
140
- category: '',
141
- words: b.wordCount ?? 0,
142
- intro: (b.description ?? '').slice(0, 150),
143
- cover: b.cover ?? '',
144
- bookId: b.bookId ?? '',
145
- }));
146
- }
@@ -1,143 +0,0 @@
1
- /**
2
- * jinjiang.js — 晋江文学城排行榜采集
3
- *
4
- * 策略:纯 HTTP + gb18030 解码。
5
- * 详情页使用 fetch + arrayBuffer + TextDecoder('gb18030')。
6
- * 无需 Playwright。
7
- */
8
-
9
- export const platform = 'jinjiang';
10
- export const label = '晋江';
11
-
12
- export const rankList = [
13
- { id: '5', label: '月榜', channel: 'all', type: '5' },
14
- { id: '7', label: '总分榜', channel: 'all', type: '7' },
15
- { id: '4', label: '季度榜', channel: 'all', type: '4' },
16
- { id: '12', label: '收入金榜', channel: 'all', type: '12' },
17
- { id: '16', label: '完结金榜', channel: 'all', type: '16' },
18
- { id: '17', label: '新手金榜', channel: 'all', type: '17' },
19
- ];
20
-
21
- const BASE_URL = 'https://www.jjwxc.net/topten.php';
22
- const DETAIL_BASE = 'https://www.jjwxc.net/onebook.php';
23
-
24
- /**
25
- * Scrape one rank list.
26
- * @param {string} rankId - orderstr, e.g. '5'
27
- * @param {object} ctx - Cordis plugin context
28
- * @param {AbortSignal} signal
29
- * @returns {Promise<{ ok: boolean, books: object[], error?: string }>}
30
- */
31
- export async function scrapeRank(rankId, ctx, signal) {
32
- const url = `${BASE_URL}?orderstr=${rankId}`;
33
-
34
- // 1. 抓列表页
35
- const { ok: listOk, status, body, error: listError } = await httpGet(ctx, url, signal);
36
- if (!listOk) return { ok: false, books: [], error: listError ?? `HTTP ${status}` };
37
-
38
- // 2. 解析书籍 ID 列表
39
- const bookIds = extractBookIds(body);
40
- if (!bookIds.length) return { ok: false, books: [], error: 'no book ids found' };
41
-
42
- // 3. 批量补采详情(每批 6 本)
43
- const CHUNK = 6;
44
- const books = [];
45
- for (let i = 0; i < bookIds.length; i += CHUNK) {
46
- if (signal?.aborted) break;
47
- const chunk = bookIds.slice(i, i + CHUNK);
48
- const details = await Promise.all(chunk.map(id => fetchDetail(ctx, id, signal)));
49
- for (const d of details) {
50
- if (d) books.push(d);
51
- }
52
- }
53
-
54
- return books.length > 0
55
- ? { ok: true, books }
56
- : { ok: false, books: [], error: 'no books collected' };
57
- }
58
-
59
- async function httpGet(ctx, url, signal) {
60
- let timerDispose;
61
- const timeout = new Promise((_, reject) => {
62
- timerDispose = ctx.timeout(() => reject(new Error('timeout')), 15000);
63
- });
64
- try {
65
- const res = await Promise.race([
66
- fetch(url, {
67
- headers: {
68
- 'User-Agent': 'Mozilla/5.0 (compatible; fancy-webnovel/1.0)',
69
- 'Accept': 'text/html',
70
- 'Accept-Language': 'zh-CN',
71
- },
72
- signal,
73
- }),
74
- timeout,
75
- ]);
76
- if (!res.ok) return { ok: false, status: res.status, body: '', error: `HTTP ${res.status}` };
77
- const body = await res.text();
78
- return { ok: true, status: res.status, body };
79
- } catch (err) {
80
- return { ok: false, status: 0, body: '', error: err.message };
81
- } finally {
82
- try { timerDispose?.(); } catch (_) {}
83
- }
84
- }
85
-
86
- function extractBookIds(html) {
87
- const ids = [];
88
- // 两种布局:表格行和交替行
89
- const tableRows = html.match(/<tr[^>]*>[\s\S]*?<\/tr>/gi) ?? [];
90
- for (const row of tableRows) {
91
- const m = row.match(/onebook\.php\?novelid=(\d+)/);
92
- if (m) ids.push(m[1]);
93
- }
94
- // 交替行布局: 书名 / 作者 交替
95
- if (!ids.length) {
96
- const novelLinks = html.match(/novelid=(\d+)/g) ?? [];
97
- for (const link of novelLinks) {
98
- const id = link.match(/(\d+)/)?.[1];
99
- if (id && !ids.includes(id)) ids.push(id);
100
- }
101
- }
102
- return [...new Set(ids)].slice(0, 60); // 最多 60 本
103
- }
104
-
105
- async function fetchDetail(ctx, novelid, signal) {
106
- let timerDispose;
107
- const timeout = new Promise((_, reject) => {
108
- timerDispose = ctx.timeout(() => reject(new Error('timeout')), 10000);
109
- });
110
- try {
111
- const res = await Promise.race([
112
- fetch(`${DETAIL_BASE}?novelid=${novelid}`, { signal }),
113
- timeout,
114
- ]);
115
- if (!res.ok) return null;
116
- const buf = await res.arrayBuffer();
117
- const h = new TextDecoder('gb18030').decode(new Uint8Array(buf));
118
-
119
- const prop = (/** @type {string} */ name) => {
120
- const m = h.match(new RegExp(`itemprop="${name}"[^>]*>([^<]*)<`));
121
- return m ? m[1].trim() : '';
122
- };
123
-
124
- return {
125
- rank: 0,
126
- title: prop('title') || prop('novelname') || '',
127
- author: prop('author') || '',
128
- category: '',
129
- words: parseInt(prop('wordCount').replace(/[^\d]/g, ''), 10) || 0,
130
- collect: prop('collectedCount') || '',
131
- nutrition: prop('nutritionCount') || '',
132
- score: prop('scoreCount') || '',
133
- status: (h.match(/(连载中|已完结|完结)/) || [, ''])[1],
134
- intro: (h.match(/<meta[^>]+name="description"[^>]+content="([^"]*)"/i)?.[1] ?? '').slice(0, 150),
135
- cover: '',
136
- bookId: novelid,
137
- };
138
- } catch (_) {
139
- return null;
140
- } finally {
141
- try { timerDispose?.(); } catch (_) {}
142
- }
143
- }
@@ -1,102 +0,0 @@
1
- /**
2
- * qidian.js — 起点排行榜采集
3
- *
4
- * 策略:纯 HTTP mobile SSR。移动端页面在 script 标签里内嵌完整的
5
- * pageContext JSON,绕过了服务端渲染的加密,直接提取书籍数据。
6
- * 无需 Playwright。
7
- *
8
- * Rank paths (mobile, slug-based):
9
- * /rank/hotsales/ 畅销榜
10
- * /rank/yuepiao/ 月票榜
11
- * /rank/newbook/ 新书榜
12
- * /rank/newauthor/ 新人作者榜
13
- */
14
-
15
- const MOBILE_BASE = 'https://m.qidian.com';
16
-
17
- // rank.id → URL slug, matches RANK_PATHS keys used by scrapeRank()
18
- const RANK_PATHS = {
19
- hotsales: '/rank/hotsales/',
20
- yuepiao: '/rank/yuepiao/',
21
- newbook: '/rank/newbook/',
22
- newauthor: '/rank/newauthor/',
23
- };
24
-
25
- export const platform = 'qidian';
26
- export const label = '起点';
27
-
28
- export const rankList = [
29
- { id: 'hotsales', label: '畅销榜', channel: '1', type: 'hotsales' },
30
- { id: 'yuepiao', label: '月票榜', channel: '1', type: 'yuepiao' },
31
- { id: 'newbook', label: '新书榜', channel: '1', type: 'newbook' },
32
- { id: 'newauthor', label: '新人作者榜', channel: '1', type: 'newauthor' },
33
- ];
34
-
35
- const HEADERS = {
36
- 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
37
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
38
- 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
39
- 'Accept-Encoding': 'identity',
40
- };
41
-
42
- /**
43
- * Scrape one rank list.
44
- * @param {string} rankId - rank id (e.g. 'hotsales')
45
- * @param {object} ctx - Cordis plugin context
46
- * @param {AbortSignal} signal
47
- * @returns {Promise<{ ok: boolean, books: object[], error?: string }>}
48
- */
49
- export async function scrapeRank(rankId, ctx, signal) {
50
- const path = RANK_PATHS[rankId];
51
- if (!path) return { ok: false, books: [], error: `unknown rank: ${rankId}` };
52
-
53
- const url = MOBILE_BASE + path;
54
-
55
- const { ok, status, body, error } = await httpGet(ctx, url, signal);
56
- if (!ok) return { ok: false, books: [], error: error ?? `HTTP ${status}` };
57
-
58
- // Extract pageContext from <script id="vite-plugin-ssr_pageContext">
59
- const m = body.match(/<script[^>]+id=["']vite-plugin-ssr_pageContext["'][^>]*>([\s\S]*?)<\/script>/i);
60
- if (!m) return { ok: false, books: [], error: 'pageContext not found' };
61
-
62
- let pageContext;
63
- try {
64
- pageContext = JSON.parse(m[1]);
65
- } catch {
66
- return { ok: false, books: [], error: 'pageContext JSON parse failed' };
67
- }
68
-
69
- const records = pageContext?.pageContext?.pageProps?.pageData?.records ?? [];
70
- if (!records.length) return { ok: false, books: [], error: 'no book records found' };
71
-
72
- const books = records.map((r) => ({
73
- rank: r.rankNum ?? 0,
74
- title: r.bName ?? '',
75
- author: r.bAuth ?? '',
76
- category: r.cat ?? '',
77
- words: r.cnt ?? '0',
78
- intro: (r.desc ?? '').slice(0, 150),
79
- bookId: r.bid ?? '',
80
- }));
81
-
82
- return { ok: true, books };
83
- }
84
-
85
- async function httpGet(ctx, url, signal) {
86
- let timerDispose;
87
- const timeout = new Promise((_, reject) => {
88
- timerDispose = ctx.timeout(() => reject(new Error('timeout')), 15000);
89
- });
90
- try {
91
- const res = await Promise.race([
92
- fetch(url, { headers: HEADERS, signal }),
93
- timeout,
94
- ]);
95
- const body = await res.text();
96
- return { ok: res.ok, status: res.status, body };
97
- } catch (err) {
98
- return { ok: false, status: 0, body: '', error: err.message };
99
- } finally {
100
- try { timerDispose?.(); } catch (_) {}
101
- }
102
- }
@@ -1,127 +0,0 @@
1
- /**
2
- * qimao.js — 七猫小说排行榜采集
3
- *
4
- * 策略:纯 HTTP。榜单页通过 XHR 加载 JSON 数据。
5
- * 无需 Playwright。
6
- */
7
-
8
- export const platform = 'qimao';
9
- export const label = '七猫';
10
-
11
- export const rankList = [
12
- { id: 'male_hot', label: '男生大热榜', channel: 'male', type: 'hot' },
13
- { id: 'male_new', label: '男生新书榜', channel: 'male', type: 'new' },
14
- { id: 'female_hot', label: '女生大热榜', channel: 'female', type: 'hot' },
15
- { id: 'female_new', label: '女生新书榜', channel: 'female', type: 'new' },
16
- ];
17
-
18
- const RANK_URL = 'https://www.qimao.com/paihang';
19
-
20
- /**
21
- * Scrape one rank list.
22
- * @param {string} rankId - e.g. 'male_hot'
23
- * @param {object} ctx - Cordis plugin context
24
- * @param {AbortSignal} signal
25
- * @returns {Promise<{ ok: boolean, books: object[], error?: string }>}
26
- */
27
- export async function scrapeRank(rankId, ctx, signal) {
28
- const [channelId, rankTypeId] = rankId.split('_');
29
-
30
- const channelMap = { male: 'boy', female: 'girl' };
31
- const rankTypeMap = { hot: 'hot', new: 'new' };
32
- const channel = channelMap[channelId] ?? channelId;
33
- const rankType = rankTypeMap[rankTypeId] ?? rankTypeId;
34
-
35
- const url = `${RANK_URL}/${channel}/${rankType}/date/`;
36
-
37
- const { ok, status, body, error } = await httpGet(ctx, url, signal);
38
- if (!ok) return { ok: false, books: [], error: error ?? `HTTP ${status}` };
39
-
40
- const books = parseBooks(body, channelId, rankTypeId);
41
- return books.length > 0
42
- ? { ok: true, books }
43
- : { ok: false, books: [], error: 'no books found in page' };
44
- }
45
-
46
- async function httpGet(ctx, url, signal) {
47
- let timerDispose;
48
- const timeout = new Promise((_, reject) => {
49
- timerDispose = ctx.timeout(() => reject(new Error('timeout')), 15000);
50
- });
51
- try {
52
- const res = await Promise.race([
53
- fetch(url, {
54
- headers: {
55
- 'User-Agent': 'Mozilla/5.0 (compatible; fancy-webnovel/1.0)',
56
- 'Accept': 'text/html,application/xhtml+xml',
57
- 'Accept-Language': 'zh-CN,zh;q=0.9',
58
- },
59
- signal,
60
- }),
61
- timeout,
62
- ]);
63
- const body = await res.text();
64
- return { ok: res.ok, status: res.status, body };
65
- } catch (err) {
66
- return { ok: false, status: 0, body: '', error: err.message };
67
- } finally {
68
- try { timerDispose?.(); } catch (_) {}
69
- }
70
- }
71
-
72
- function parseBooks(html, channelId, rankTypeId) {
73
- const books = [];
74
-
75
- // 尝试从 JSON 数据块提取
76
- const jsonMatch = html.match(/var\s+RANK_DATA\s*=\s*(\{.*?\});/s)
77
- ?? html.match(/window\.__INITIAL_STATE__\s*=\s*(\{.*?\});/s);
78
-
79
- if (jsonMatch) {
80
- try {
81
- const data = JSON.parse(jsonMatch[1]);
82
- const list = data?.rankList ?? data?.bookList ?? data?.books ?? [];
83
- return normalizeBooks(list, channelId, rankTypeId);
84
- } catch (_) { /* fall through */ }
85
- }
86
-
87
- // DOM 解析: 从页面 HTML 提取书籍信息
88
- // 匹配模式: <a class="book-title">书名</a> ... <span class="author">作者</span>
89
- const titleMatches = [...html.matchAll(/class="(?:book-)?title[^"]*"[^>]*>([^<]+)<\/a>/g)];
90
- const authorMatches = [...html.matchAll(/class="author[^"]*"[^>]*>([^<]+)<\/a>/g)];
91
- const rankMatches = [...html.matchAll(/class="(?:rank|num)[^"]*"[^>]*>(\d+)<\/span>/g)];
92
-
93
- const count = Math.min(titleMatches.length, authorMatches.length);
94
- for (let i = 0; i < count; i++) {
95
- const title = (titleMatches[i][1] ?? '').trim();
96
- const author = (authorMatches[i][1] ?? '').trim();
97
- if (!title || /^(上一页|下一页|跳转)$/.test(title)) continue;
98
- if (!author || /^(上一页|下一页|跳转|友情链接)$/.test(author)) continue;
99
-
100
- books.push({
101
- rank: rankMatches[i] ? parseInt(rankMatches[i][1], 10) : (i + 1),
102
- title,
103
- author,
104
- category: '',
105
- words: 0,
106
- intro: '',
107
- cover: '',
108
- bookId: '',
109
- });
110
- }
111
-
112
- return books;
113
- }
114
-
115
- function normalizeBooks(list, channelId, rankTypeId) {
116
- if (!Array.isArray(list)) return [];
117
- return list.map((b, i) => ({
118
- rank: b.rank ?? (i + 1),
119
- title: b.title ?? b.bookTitle ?? '',
120
- author: b.author ?? '',
121
- category: b.categoryName ?? b.typeName ?? '',
122
- words: b.wordCount ?? b.words ?? 0,
123
- intro: (b.description ?? '').slice(0, 150),
124
- cover: b.cover ?? b.thumbnail ?? '',
125
- bookId: b.bookId ?? b.id ?? '',
126
- }));
127
- }