@cloud411716/fancy-webnovel 0.1.76 → 0.1.78

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 (125) hide show
  1. package/infra.js +26 -21
  2. package/package.json +1 -1
  3. package/plugins/fancy-bootstrap/index.js +4 -2
  4. package/plugins/fancy-scan/index.js +1 -2
  5. package/plugins/fancy-scan/scripts/run-scan.js +22 -44
  6. package/plugins/fancy-scan/scripts/scraper-registry.md +3 -5
  7. package/plugins/fancy-scan/scripts/scrapers/cdp-utils.cjs +7 -5
  8. package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.cjs +282 -253
  9. package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.cjs +193 -253
  10. package/plugins/fancy-scan/scripts/scrapers/node_modules/.package-lock.json +20 -0
  11. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/LICENSE +202 -0
  12. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/NOTICE +5 -0
  13. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/README.md +3 -0
  14. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/ThirdPartyNotices.txt +13 -0
  15. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/install_media_pack.ps1 +5 -0
  16. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/install_webkit_wsl.ps1 +33 -0
  17. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh +42 -0
  18. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh +13 -0
  19. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 +24 -0
  20. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh +42 -0
  21. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh +12 -0
  22. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 +24 -0
  23. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh +48 -0
  24. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh +11 -0
  25. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 +23 -0
  26. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh +48 -0
  27. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh +11 -0
  28. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 +23 -0
  29. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh +48 -0
  30. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh +11 -0
  31. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 +24 -0
  32. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/browsers.json +75 -0
  33. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/cli.js +21 -0
  34. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/index.d.ts +17 -0
  35. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/index.js +17 -0
  36. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/index.mjs +28 -0
  37. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/bootstrap.js +88 -0
  38. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/coreBundle.js +74830 -0
  39. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/entry/cliDaemon.js +5 -0
  40. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/entry/dashboardApp.js +3 -0
  41. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/entry/mcp.js +10 -0
  42. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/entry/oopBrowserDownload.js +3 -0
  43. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/package.js +50 -0
  44. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/server/chromium/appIcon.png +0 -0
  45. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/server/electron/loader.js +118 -0
  46. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/serverRegistry.js +7347 -0
  47. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/serverRegistry.js.LICENSE +354 -0
  48. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/cli-client/channelSessions.js +141 -0
  49. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/cli-client/cli.js +6 -0
  50. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/cli-client/help.json +708 -0
  51. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/cli-client/minimist.js +128 -0
  52. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/cli-client/output.js +343 -0
  53. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/cli-client/program.js +404 -0
  54. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/cli-client/registry.js +176 -0
  55. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/cli-client/session.js +258 -0
  56. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/dashboard/appIcon.png +0 -0
  57. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/SKILL.md +420 -0
  58. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/element-attributes.md +23 -0
  59. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/playwright-tests.md +39 -0
  60. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/request-mocking.md +87 -0
  61. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/running-code.md +241 -0
  62. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/session-management.md +225 -0
  63. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/storage-state.md +275 -0
  64. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/test-generation.md +433 -0
  65. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/tracing.md +139 -0
  66. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/video-recording.md +143 -0
  67. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/SKILL.md +143 -0
  68. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +144 -0
  69. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/migration.md +85 -0
  70. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/react.md +67 -0
  71. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/vue.md +75 -0
  72. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-trace/SKILL.md +171 -0
  73. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/utils/extension.js +101 -0
  74. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/utils/socketConnection.js +108 -0
  75. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/utilsBundle.js +90764 -0
  76. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +2179 -0
  77. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/dashboard/assets/codicon-DCmgc-ay.ttf +0 -0
  78. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-1bWoP6pv.svg +1 -0
  79. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-beta-k3eOH_eK.svg +1 -0
  80. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-nightly-Cp5nfeDT.svg +1 -0
  81. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/dashboard/assets/index-CyWAfh-p.js +11 -0
  82. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/dashboard/assets/index-DhC616m4.css +1 -0
  83. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/dashboard/assets/safari-na3_-uQk.svg +1 -0
  84. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/dashboard/index.html +29 -0
  85. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/dashboard/playwright-logo.svg +24 -0
  86. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/htmlReport/index.html +16 -0
  87. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/htmlReport/report.css +2 -0
  88. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/htmlReport/report.js +32 -0
  89. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
  90. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-CwYYHbcZ.js +32 -0
  91. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/recorder/assets/codicon-DCmgc-ay.ttf +0 -0
  92. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/recorder/assets/index-DYjdXIbE.js +129 -0
  93. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/recorder/assets/index-l_lX622x.css +1 -0
  94. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/recorder/index.html +29 -0
  95. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/recorder/playwright-logo.svg +9 -0
  96. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-rXmQmLUY.js +32 -0
  97. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-B-dXF5JN.js +181 -0
  98. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
  99. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-BuZfJS5v.js +7 -0
  100. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
  101. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/codicon.DCmgc-ay.ttf +0 -0
  102. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.BLFoOugd.css +1 -0
  103. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
  104. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/index.KZ4wOW1K.js +1 -0
  105. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/index.html +44 -0
  106. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/manifest.webmanifest +16 -0
  107. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/playwright-logo.svg +9 -0
  108. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
  109. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html +10 -0
  110. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +4 -0
  111. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/uiMode.C7UW1sC9.css +1 -0
  112. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/uiMode.Dzuouizj.js +5 -0
  113. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +18 -0
  114. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
  115. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/webp_codec.LICENSE +173 -0
  116. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
  117. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/xdg-open +1267 -0
  118. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/package.json +34 -0
  119. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/types/protocol.d.ts +24842 -0
  120. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/types/structs.d.ts +52 -0
  121. package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/types/types.d.ts +26188 -0
  122. package/plugins/fancy-scan/scripts/scrapers/package-lock.json +27 -0
  123. package/plugins/fancy-scan/scripts/scrapers/package.json +9 -0
  124. package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.cjs +200 -297
  125. package/plugins/fancy-scan/scripts/scrapers/zhihu-rank-scraper.cjs +80 -80
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "fancy-scan-scrapers",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "fancy-scan-scrapers",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "playwright-core": "^1.40.0"
12
+ }
13
+ },
14
+ "node_modules/playwright-core": {
15
+ "version": "1.62.1",
16
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
17
+ "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
18
+ "license": "Apache-2.0",
19
+ "bin": {
20
+ "playwright-core": "cli.js"
21
+ },
22
+ "engines": {
23
+ "node": ">=20"
24
+ }
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "fancy-scan-scrapers",
3
+ "version": "1.0.0",
4
+ "description": "Platform scrapers for fancy-scan, powered by playwright-core",
5
+ "type": "commonjs",
6
+ "dependencies": {
7
+ "playwright-core": "^1.40.0"
8
+ }
9
+ }
@@ -2,170 +2,59 @@
2
2
  /**
3
3
  * 七猫小说排行榜采集脚本
4
4
  *
5
- * 配合 browser-cdp skill 使用。先启动 Chrome CDP 环境,再运行本脚本。
5
+ * 使用 playwright-core 自己管理浏览器。
6
6
  * 采集策略:tab 切换男生榜/女生榜和榜单类型,滚动加载后从页面文本解析结构化数据。
7
- * 输出 Markdown 格式匹配 scan-output-format.md 规范。
8
7
  *
9
8
  * 用法:
10
9
  * node qimao-rank-scraper.js --channel male --type hot --period day # 男生大热榜日榜
11
10
  * node qimao-rank-scraper.js --channel male --type hot --period month # 男生大热榜月榜
12
11
  * node qimao-rank-scraper.js --channel male --type hot --period all # 日榜+月榜
13
- * node qimao-rank-scraper.js --channel female --type new # 女生新书榜
14
- * node qimao-rank-scraper.js --channel all --type all # 全部采集
15
- *
16
- * 前置:
17
- * node {SKILL_DIR}/browser-cdp/scripts/setup-cdp-chrome.js 9222
12
+ * node qimao-rank-scraper.js --channel female --type new # 女生新书榜
13
+ * node qimao-rank-scraper.js --channel all --type all # 全部采集
14
+ * node qimao-rank-scraper.js --login-wait 60 # 等待手动登录
18
15
  */
19
16
 
20
17
  const fs = require("fs");
21
18
  const path = require("path");
22
- const { ab, sleep, evalJSONBase64, scrollLoad, getArg, localDateStamp, runCli } = require("./cdp-utils.cjs");
19
+ const { chromium } = require("playwright-core");
20
+ const { getArg, localDateStamp, runCli } = require("./cdp-utils.cjs");
23
21
 
24
22
  const RANK_URL = "https://www.qimao.com/paihang";
25
23
 
26
- /** 连通性 + 页面就绪自检 */
27
- function probePage(port) {
28
- return evalJSONBase64(
29
- port,
30
- "JSON.stringify({host:location.host,path:location.pathname,len:(document.body&&document.body.innerText||'').length})"
31
- );
32
- }
33
-
34
24
  const CHANNELS = [
35
- { id: "male", label: "男频", tab: "男生榜", path: "boy" },
25
+ { id: "male", label: "男频", tab: "男生榜", path: "boy" },
36
26
  { id: "female", label: "女频", tab: "女生榜", path: "girl" },
37
27
  ];
38
28
 
39
29
  const RANK_TYPES = [
40
- { id: "hot", label: "大热榜", path: "hot" },
41
- { id: "new", label: "新书榜", path: "new" },
30
+ { id: "hot", label: "大热榜", path: "hot" },
31
+ { id: "new", label: "新书榜", path: "new" },
42
32
  { id: "finish", label: "完结榜", path: "over" },
43
- { id: "collect", label: "收藏榜", path: "collect" },
33
+ { id: "collect",label: "收藏榜", path: "collect" },
44
34
  { id: "update", label: "更新榜", path: "update" },
45
35
  ];
46
36
 
47
37
  const PERIODS = [
48
- { id: "day", label: "日榜", path: "date" },
38
+ { id: "day", label: "日榜", path: "date" },
49
39
  { id: "month", label: "月榜", path: "month" },
50
40
  ];
51
41
 
52
42
  // ---------------------------------------------------------------------------
53
- // 页面操作
43
+ // 工具函数
54
44
  // ---------------------------------------------------------------------------
55
45
 
56
- function rankUrl(channelId, rankTypeId, periodId) {
57
- const channel = CHANNELS.find((item) => item.id === channelId);
58
- const rankType = RANK_TYPES.find((item) => item.id === rankTypeId);
59
- const period = PERIODS.find((item) => item.id === (periodId || "day"));
60
- if (!channel || !rankType || !period) return "";
61
- return `${RANK_URL}/${channel.path}/${rankType.path}/${period.path}/`;
62
- }
63
-
64
- /** 读取页面实际 active 状态;输出文件标签必须由该状态校验后才能使用。 */
65
- function extractObservedSelection(port) {
66
- const js = `JSON.stringify((function(){
67
- function text(selector){var e=document.querySelector(selector);return e?(e.textContent||'').replace(/\\s+/g,'').trim():'';}
68
- return {path:location.pathname,channel:text('.qm-switch-tab .item.active'),rankType:text('.child-tabs-item.menu-tab.active'),period:text('.date-type-tabs .tab.active')};
69
- })())`;
70
- return evalJSONBase64(port, js) || {};
46
+ function sleep(ms) {
47
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
71
48
  }
72
49
 
73
- function selectionMatches(observed, channelId, rankTypeId, periodId) {
74
- const channel = CHANNELS.find((item) => item.id === channelId);
75
- const rankType = RANK_TYPES.find((item) => item.id === rankTypeId);
76
- const period = periodId ? PERIODS.find((item) => item.id === periodId) : null;
77
- if (!channel || !rankType) return false;
78
- const expectedUrl = rankUrl(channelId, rankTypeId, periodId);
79
- if (!expectedUrl) return false;
80
- const expectedPath = new URL(expectedUrl).pathname;
81
- const actualPath = String(observed && observed.path || "").replace(/\/+$/, "/");
82
- return !!(
83
- actualPath === expectedPath &&
84
- String(observed.channel || "").includes(channel.tab) &&
85
- observed.rankType === rankType.label &&
86
- (!period || observed.period === period.label)
87
- );
88
- }
89
-
90
- /**
91
- * 从 DOM 获取书籍链接。每本书有多个 anchor(排名数字/书名/最近更新),
92
- * 按 bookId 聚合后取最像书名的文本(非纯数字、非"最近更新"前缀、最长),
93
- * 否则书名会被排名数字 anchor 覆盖,导致后续按书名回填链接全失败。
94
- */
95
- function extractBookUrls(port) {
96
- const js = `JSON.stringify((function(){
97
- var byId={};var order=[];
98
- Array.from(document.querySelectorAll('a')).forEach(function(a){
99
- var h=a.getAttribute('href')||a.href||'';
100
- var m=h.match(/\\/(?:shuku|book)\\/([0-9]+)/);
101
- if(!m)return; var id=m[1];
102
- var t=(a.innerText||a.textContent||'').replace(/\\s+/g,' ').trim();
103
- if(!byId[id]){byId[id]='';order.push(id);}
104
- if(t&&!/^[0-9]+$/.test(t)&&!/^(最近更新|最新章节|最新)/.test(t)){
105
- if(t.length>byId[id].length)byId[id]=t;
106
- }
107
- });
108
- return order.map(function(id){return {bookId:id,title:byId[id],url:'https://www.qimao.com/shuku/'+id+'/'};});
109
- })())`;
110
- return evalJSONBase64(port, js) || [];
111
- }
112
-
113
- /**
114
- * 从页面 innerText 解析结构化书籍数据。
115
- * 七猫页面文本结构固定:排名→书名→作者→题材→子分类→状态→字数→简介→更新→热度
116
- */
117
- function extractBooksFromText(port) {
118
- const js =
119
- "JSON.stringify((()=>{" +
120
- "var text=document.body.innerText||'';" +
121
- // 找到榜单数据起始位置
122
- "var start=-1;" +
123
- "['日榜','月榜'].forEach(function(m){if(start<0)start=text.indexOf(m)});" +
124
- "if(start<0)return[];" +
125
- "var lines=text.substring(start).split(/\\n/);" +
126
- "var books=[];var cur=null;var fieldIdx=0;" +
127
- "for(var i=0;i<lines.length;i++){" +
128
- " var line=lines[i].trim();" +
129
- " if(!line)continue;" +
130
- // 排行数据结束后的分页器/页脚必须立刻截断;否则“5 / 下一页 / 跳转 / 友情链接”
131
- // 会被串成一条字段齐全的假书目。
132
- " if(/^(上一页|下一页|跳转|友情链接[::]?)$/.test(line)){if(cur&&cur.title)books.push(cur);cur=null;break}" +
133
- // 排名标记:独立数字 1-99
134
- " if(/^\\d{1,2}$/.test(line)&&parseInt(line)<100){" +
135
- " if(cur&&cur.title)books.push(cur);" +
136
- " cur={rank:parseInt(line),title:'',author:'',genre:'',subGenre:'',status:'',words:'',heat:'',update:'',desc:''};" +
137
- " fieldIdx=0;continue" +
138
- " }" +
139
- " if(!cur)continue;" +
140
- // 跳过 UI 文字
141
- " if(/^(加入书架|立即阅读|蝉联|榜首)/.test(line))continue;" +
142
- // 热度
143
- " var hm=line.match(/([\\d.]+)\\s*万\\s*热度/);" +
144
- " if(hm){cur.heat=hm[1]+'万';continue}" +
145
- // 最新更新
146
- " if(line.indexOf('最近更新')===0){cur.update=line.replace(/^最近更新\\s*/,'');continue}" +
147
- // 状态
148
- " if(/^(连载中|已完结)$/.test(line)){cur.status=line;continue}" +
149
- // 字数
150
- " if(/^[\\d.]+万字$/.test(line)){cur.words=line;continue}" +
151
- // 按序填充:书名→作者→题材→子分类
152
- " if(fieldIdx===0){cur.title=line;fieldIdx=1;continue}" +
153
- " if(fieldIdx===1){cur.author=line;fieldIdx=2;continue}" +
154
- " if(fieldIdx===2){cur.genre=line;fieldIdx=3;continue}" +
155
- " if(fieldIdx===3){cur.subGenre=line;fieldIdx=4;continue}" +
156
- // 其余为简介
157
- " cur.desc+=(cur.desc?' ':'')+line" +
158
- "}" +
159
- "if(cur&&cur.title)books.push(cur);" +
160
- "return books" +
161
- "})())";
162
- return evalJSONBase64(port, js) || [];
50
+ function rankUrl(channelId, rankTypeId, periodId) {
51
+ const channel = CHANNELS.find(c => c.id === channelId);
52
+ const rt = RANK_TYPES.find(r => r.id === rankTypeId);
53
+ const period = PERIODS.find(p => p.id === (periodId || "day"));
54
+ if (!channel || !rt || !period) return "";
55
+ return `${RANK_URL}/${channel.path}/${rt.path}/${period.path}/`;
163
56
  }
164
57
 
165
- /**
166
- * 排除分页器等被正文文本解析器误认成的伪书目。
167
- * 七猫榜单尾部会出现“5 / 下一页”这类纯 UI 文本;有效条目必须同时有正排名、书名和作者。
168
- */
169
58
  function isUsableBook(book) {
170
59
  return !!(
171
60
  book &&
@@ -182,188 +71,211 @@ function cleanDesc(value) {
182
71
  const text = String(value || "")
183
72
  .replace(/\s*(?:飙升|上升|下降)\s*\d+\s*名\s*$/g, "")
184
73
  .replace(/\s*(?:上一页|下一页)\s*$/g, "")
185
- .replace(/\s+/g, " ")
186
- .trim();
74
+ .replace(/\s+/g, " ").trim();
187
75
  if (text.length <= 100) return text;
188
76
  const cut = text.slice(0, 100);
189
- const sentence = cut.match(/^[\s\S]*[。!?]/);
77
+ const sentence = cut.match(/[\s\S]*[。!?]/);
190
78
  return (sentence ? sentence[0] : cut) + "...";
191
79
  }
192
80
 
193
81
  function summarizeQuality(books, rawCount) {
194
- const linked = books.filter((book) => book.url).length;
195
- const heated = books.filter((book) => book.heat).length;
82
+ const linked = books.filter(b => b.url).length;
83
+ const heated = books.filter(b => b.heat).length;
196
84
  const fieldCounts = [
197
- ["题材", "genre"],
198
- ["子分类", "subGenre"],
199
- ["状态", "status"],
200
- ["字数", "words"],
201
- ["热度", "heat"],
85
+ ["题材", "genre"], ["子分类", "subGenre"],
86
+ ["状态", "status"], ["字数", "words"], ["热度", "heat"],
202
87
  ].map(([label, field]) => ({
203
- label,
204
- missing: books.filter((book) => !book[field]).length,
88
+ label, missing: books.filter(b => !b[field]).length,
205
89
  }));
206
90
  const problems = [];
207
91
  if (rawCount > books.length) problems.push(`移除无效/UI条目 ${rawCount - books.length} 条`);
208
92
  if (linked < books.length) problems.push(`作品页链接缺失 ${books.length - linked} 条`);
209
- for (const field of fieldCounts) {
210
- if (field.missing) problems.push(`${field.label}缺失 ${field.missing} 条`);
211
- }
93
+ for (const f of fieldCounts) { if (f.missing) problems.push(`${f.label}缺失 ${f.missing} 条`); }
212
94
  if (books.length < 15) problems.push(`[数据稀疏] 实际采集 ${books.length} 条`);
213
- return {
214
- linked,
215
- heated,
216
- problems,
217
- quality: problems.length ? "[存在问题]" : "[OK]",
218
- };
219
- }
220
-
221
- function renderMarkdown(ch, rt, period, url, books, rawCount, now = new Date().toISOString()) {
222
- const periodLabel = period ? period.label : "";
223
- const summary = summarizeQuality(books, rawCount);
224
- const lines = [
225
- `# 七猫 · ${ch.label} · ${rt.label}${periodLabel}`,
226
- "",
227
- `- 数据质量:${summary.quality}`,
228
- `- 有效条目:${books.length} / ${rawCount}`,
229
- `- 问题摘要:${summary.problems.length ? summary.problems.join(";") : "无"}`,
230
- `- 作品页链接:${summary.linked} / ${books.length}`,
231
- `- 热度命中:${summary.heated} / ${books.length}`,
232
- `- 来源:${url}`,
233
- `- 抓取时间:${now}`,
234
- `- 条目数:${books.length}`,
235
- "",
236
- "---",
237
- "",
238
- ];
239
-
240
- for (const b of books) {
241
- try {
242
- lines.push(`### #${b.rank} ${b.title}`);
243
- const meta = [
244
- b.author || "[待补]",
245
- b.genre || "[待补]",
246
- b.subGenre || "[待补]",
247
- b.status || "[待补]",
248
- b.words || "[待补]",
249
- b.heat ? b.heat + "热度" : "[待补]",
250
- ].join(" · ");
251
- lines.push(`*${meta}*`);
252
- if (b.update) lines.push(`**最新更新:** ${b.update}`);
253
- if (b.url) lines.push(`[作品页](${b.url})`);
254
- const desc = cleanDesc(b.desc);
255
- if (desc) {
256
- lines.push("");
257
- lines.push("**简介**");
258
- lines.push("");
259
- lines.push(desc);
260
- }
261
- lines.push("", "---", "");
262
- } catch (bookErr) {
263
- console.error(`[qimao] ${ch.label}${rt.label} 第${b.rank}条处理出错: ${bookErr.message}`);
264
- lines.push("", "---", "");
265
- }
266
- }
267
-
268
- return lines.join("\n");
95
+ return { linked, heated, problems, quality: problems.length ? "[存在问题]" : "[OK]" };
269
96
  }
270
97
 
271
98
  // ---------------------------------------------------------------------------
272
- // 主流程
99
+ // 采集
273
100
  // ---------------------------------------------------------------------------
274
101
 
275
- const args = process.argv.slice(2);
276
- const PORT = parseInt(getArg(args, "--port") || "9222", 10);
277
- const OUTDIR = getArg(args, "--outdir") || ".";
278
- const CHANNEL = getArg(args, "--channel") || "male";
279
- const RANKTYPE = getArg(args, "--type") || "hot";
280
- const PERIOD = getArg(args, "--period") || "day";
102
+ async function scrapeRank(channelId, rankTypeId, periodId) {
103
+ const ch = CHANNELS.find(c => c.id === channelId);
104
+ const rt = RANK_TYPES.find(r => r.id === rankTypeId);
105
+ const period = periodId ? PERIODS.find(p => p.id === periodId) : null;
106
+ if (!ch || !rt) { console.log(" ⚠ 未知频道或榜单类型"); return null; }
281
107
 
282
- function scrapeRank(port, channelId, rankTypeId, periodId) {
283
- const ch = CHANNELS.find((c) => c.id === channelId);
284
- const rt = RANK_TYPES.find((r) => r.id === rankTypeId);
285
- const period = periodId ? PERIODS.find((p) => p.id === periodId) : null;
286
- if (!ch || !rt) {
287
- console.log(" ⚠ 未知频道或榜单类型");
288
- return null;
289
- }
290
-
291
- const periodLabel = period ? period.label : "";
292
108
  const url = rankUrl(channelId, rankTypeId, periodId);
293
- console.log(`\n→ 采集 七猫${ch.label}${rt.label}${periodLabel}...`);
109
+ console.log(`\n→ 采集 七猫${ch.label}${rt.label}${period ? period.label : ""}...`);
110
+
111
+ const browser = await chromium.launch({ headless: true, args: ["--no-sandbox", "--disable-dev-shm-usage"] });
112
+ const context = await browser.newContext();
113
+ const page = await context.newPage();
294
114
 
295
115
  let books, urls, rawCount;
296
116
  try {
297
- ab(port, "open", url);
298
- sleep(3000);
117
+ await page.goto(url, { waitUntil: "networkidle" });
118
+ await page.waitForTimeout(3000);
299
119
 
300
- // 连通性自检:CDP 未起/被重定向时给可操作报错,而非静默产空
301
- const probe = probePage(port);
302
- if (!probe) {
303
- console.error(
304
- ` ✗ CDP 无响应。请确认已用 browser-cdp 启动 Chrome(端口 ${port}),且 agent-browser 可用。`
305
- );
306
- return null;
307
- }
308
- if (probe.host && probe.host.indexOf("qimao") === -1) {
309
- console.error(` ✗ 当前页面非七猫(host=${probe.host}),可能被重定向,已跳过。`);
120
+ if (page.url().indexOf("qimao") === -1) {
121
+ console.error(` ✗ 当前页面非七猫,可能被重定向,已跳过。`);
310
122
  return null;
311
123
  }
312
- const observed = extractObservedSelection(port);
313
- if (!selectionMatches(observed, channelId, rankTypeId, periodId)) {
124
+
125
+ // 验证页面实际选中状态
126
+ const observed = await page.evaluate(() => {
127
+ function text(sel) {
128
+ const e = document.querySelector(sel);
129
+ return e ? (e.textContent || "").replace(/\s+/g, "").trim() : "";
130
+ }
131
+ return {
132
+ path: location.pathname,
133
+ channel: text(".qm-switch-tab .item.active"),
134
+ rankType: text(".child-tabs-item.menu-tab.active"),
135
+ period: text(".date-type-tabs .tab.active"),
136
+ };
137
+ });
138
+
139
+ const expectedUrl = rankUrl(channelId, rankTypeId, periodId);
140
+ const expectedPath = new URL(expectedUrl).pathname;
141
+ const actualPath = String(observed.path || "").replace(/\/+$/, "/");
142
+ const periodLabel = period ? period.label : "";
143
+ if (
144
+ actualPath !== expectedPath ||
145
+ !(observed.channel || "").includes(ch.tab) ||
146
+ observed.rankType !== rt.label ||
147
+ (period && observed.period !== period.label)
148
+ ) {
314
149
  console.error(
315
- ` ✗ 页面实际榜单与请求不一致(请求 ${ch.tab}/${rt.label}/${periodLabel || "日榜"},` +
316
- `实际 ${observed.channel || "?"}/${observed.rankType || "?"}/${observed.period || "?"},path=${observed.path || probe.path || "?"}),已跳过。`
150
+ ` ✗ 页面实际榜单与请求不一致(请求 ${ch.tab}/${rt.label}/${periodLabel},` +
151
+ `实际 ${observed.channel || "?"}/${observed.rankType || "?"}/${observed.period || "?"}),已跳过。`
317
152
  );
318
153
  return null;
319
154
  }
320
155
  console.log(` ✓ 已验证页面实际榜单:${observed.channel}/${observed.rankType}${observed.period ? "/" + observed.period : ""}`);
321
156
 
322
- // 滚动加载更多
323
- scrollLoad(port, 5);
324
- sleep(1000);
157
+ // 滚动加载
158
+ for (let i = 0; i < 5; i++) {
159
+ await page.evaluate(() => window.scrollBy(0, window.innerHeight));
160
+ sleep(1000);
161
+ }
162
+ await page.waitForTimeout(500);
163
+
164
+ // 提取书籍数据(文本解析)
165
+ const rawBooks = await page.evaluate(() => {
166
+ var text = document.body.innerText || "";
167
+ var start = -1;
168
+ ["日榜", "月榜"].forEach(m => { if (start < 0) start = text.indexOf(m); });
169
+ if (start < 0) return [];
170
+ var lines = text.substring(start).split(/\n/);
171
+ var books = [];
172
+ var cur = null;
173
+ var fieldIdx = 0;
174
+ for (var i = 0; i < lines.length; i++) {
175
+ var line = lines[i].trim();
176
+ if (!line) continue;
177
+ if (/^(上一页|下一页|跳转|友情链接[::]?)$/.test(line)) { if (cur && cur.title) books.push(cur); cur = null; break; }
178
+ if (/^\d{1,2}$/.test(line) && parseInt(line) < 100) {
179
+ if (cur && cur.title) books.push(cur);
180
+ cur = { rank: parseInt(line), title: "", author: "", genre: "", subGenre: "", status: "", words: "", heat: "", update: "", desc: "" };
181
+ fieldIdx = 0; continue;
182
+ }
183
+ if (!cur) continue;
184
+ if (/^(加入书架|立即阅读|蝉联|榜首)/.test(line)) continue;
185
+ var hm = line.match(/([\d.]+)\s*万\s*热度/);
186
+ if (hm) { cur.heat = hm[1] + "万"; continue; }
187
+ if (line.indexOf("最近更新") === 0) { cur.update = line.replace(/^最近更新\s*/, ""); continue; }
188
+ if (/^(连载中|已完结)$/.test(line)) { cur.status = line; continue; }
189
+ if (/^[\d.]+万字$/.test(line)) { cur.words = line; continue; }
190
+ if (fieldIdx === 0) { cur.title = line; fieldIdx = 1; continue; }
191
+ if (fieldIdx === 1) { cur.author = line; fieldIdx = 2; continue; }
192
+ if (fieldIdx === 2) { cur.genre = line; fieldIdx = 3; continue; }
193
+ if (fieldIdx === 3) { cur.subGenre = line; fieldIdx = 4; continue; }
194
+ cur.desc += (cur.desc ? " " : "") + line;
195
+ }
196
+ if (cur && cur.title) books.push(cur);
197
+ return books;
198
+ });
325
199
 
326
- // 文本解析获取书籍数据 + DOM 获取链接
327
- const rawBooks = extractBooksFromText(port);
328
200
  rawCount = rawBooks.length;
329
201
  books = rawBooks.filter(isUsableBook);
330
- urls = extractBookUrls(port);
331
- } catch (err) {
332
- console.error(`[qimao] ${ch.label}${rt.label}${periodLabel} 页面加载或提取出错: ${err.message}`);
333
- return null;
202
+
203
+ // 提取作品页链接
204
+ urls = await page.evaluate(() => {
205
+ var byId = {};
206
+ var order = [];
207
+ Array.from(document.querySelectorAll("a")).forEach(a => {
208
+ var h = (a.getAttribute("href") || a.href || "").match(/\/(?:shuku|book)\/([0-9]+)/);
209
+ if (!h) return;
210
+ var id = h[1];
211
+ var t = (a.innerText || a.textContent || "").replace(/\s+/g, " ").trim();
212
+ if (!byId[id]) { byId[id] = ""; order.push(id); }
213
+ if (t && !/^[0-9]+$/.test(t) && !/^(最近更新、最新章节、最新)/.test(t)) {
214
+ if (t.length > byId[id].length) byId[id] = t;
215
+ }
216
+ });
217
+ return order.map(id => ({ bookId: id, title: byId[id], url: "https://www.qimao.com/shuku/" + id + "/" }));
218
+ });
219
+ } finally {
220
+ await browser.close();
334
221
  }
335
222
 
336
223
  if (!books.length) {
337
- console.error(`[qimao] 采集失败:页面结构可能已变(选择器没匹配到数据),请检查榜单URL或更新选择器 (${RANK_URL} ${ch.label}${rt.label}${periodLabel})`);
224
+ console.error(`[qimao] 采集失败:页面结构可能已变。`);
338
225
  return null;
339
226
  }
340
227
 
341
- // 按标题匹配 URL(书名归一后比对,吸收空白差异)
342
- const norm = (s) => (s || "").replace(/\s+/g, "");
228
+ // 按书名匹配 URL
229
+ const norm = s => (s || "").replace(/\s+/g, "");
343
230
  for (const b of books) {
344
- try {
345
- const matched = urls.find((u) => norm(u.title) === norm(b.title));
346
- if (matched) b.url = matched.url;
347
- } catch (matchErr) {
348
- console.error(`[qimao] URL匹配出错(#${b.rank} ${b.title}): ${matchErr.message}`);
349
- }
231
+ const matched = urls.find(u => norm(u.title) === norm(b.title));
232
+ if (matched) b.url = matched.url;
350
233
  }
351
234
 
352
235
  const summary = summarizeQuality(books, rawCount);
353
- console.log(
354
- ` ✓ 提取 ${books.length} 本(链接 ${summary.linked}/${books.length},热度 ${summary.heated}/${books.length})`
355
- );
356
- return renderMarkdown(ch, rt, period, url, books, rawCount);
236
+ console.log(` ✓ 提取 ${books.length} 本(链接 ${summary.linked}/${books.length},热度 ${summary.heated}/${books.length})`);
237
+
238
+ const periodLabel = period ? period.label : "";
239
+ const now = new Date().toISOString();
240
+ const lines = [
241
+ `# 七猫 · ${ch.label} · ${rt.label}${periodLabel}`,
242
+ "",
243
+ `- 数据质量:${summary.quality}`,
244
+ `- 有效条目:${books.length} / ${rawCount}`,
245
+ `- 问题摘要:${summary.problems.length ? summary.problems.join(";") : "无"}`,
246
+ `- 来源:${url}`,
247
+ `- 抓取时间:${now}`,
248
+ "",
249
+ "---",
250
+ "",
251
+ ];
252
+
253
+ for (const b of books) {
254
+ lines.push(`### #${b.rank} ${b.title}`);
255
+ const meta = [
256
+ b.author || "[待补]", b.genre || "[待补]", b.subGenre || "[待补]",
257
+ b.status || "[待补]", b.words || "[待补]",
258
+ b.heat ? b.heat + "热度" : "[待补]",
259
+ ].join(" · ");
260
+ lines.push(`*${meta}*`);
261
+ if (b.update) lines.push(`**最新更新:** ${b.update}`);
262
+ if (b.url) lines.push(`[作品页](${b.url})`);
263
+ const desc = cleanDesc(b.desc);
264
+ if (desc) lines.push("", "**简介**", "", desc);
265
+ lines.push("", "---", "");
266
+ }
267
+
268
+ return lines.join("\n");
357
269
  }
358
270
 
359
271
  function buildTargets(channel, rankType, period) {
360
- const channels = channel === "all" ? CHANNELS.map((item) => item.id) : [channel];
361
- const rankTypes = rankType === "all" ? RANK_TYPES.map((item) => item.id) : [rankType];
272
+ const channels = channel === "all" ? CHANNELS.map(c => c.id) : [channel];
273
+ const rankTypes = rankType === "all" ? RANK_TYPES.map(r => r.id) : [rankType];
362
274
  const targets = [];
363
275
  for (const channelId of channels) {
364
276
  for (const rankTypeId of rankTypes) {
365
277
  if (rankTypeId === "hot") {
366
- const periods = period === "all" ? PERIODS.map((item) => item.id) : [period];
278
+ const periods = period === "all" ? PERIODS.map(p => p.id) : [period];
367
279
  for (const periodId of periods) {
368
280
  targets.push({ channel: channelId, rankType: rankTypeId, period: periodId });
369
281
  }
@@ -376,61 +288,52 @@ function buildTargets(channel, rankType, period) {
376
288
  }
377
289
 
378
290
  function outputFilename(channelId, rankTypeId, periodId, date) {
379
- const channel = CHANNELS.find((item) => item.id === channelId);
380
- const rankType = RANK_TYPES.find((item) => item.id === rankTypeId);
381
- const period = periodId ? PERIODS.find((item) => item.id === periodId) : null;
382
- return `七猫${channel.label}${rankType.label}${period ? period.label : ""}_${date}.md`;
291
+ const ch = CHANNELS.find(c => c.id === channelId);
292
+ const rt = RANK_TYPES.find(r => r.id === rankTypeId);
293
+ const period = periodId ? PERIODS.find(p => p.id === periodId) : null;
294
+ return `七猫${ch.label}${rt.label}${period ? period.label : ""}_${date}.md`;
383
295
  }
384
296
 
385
- function main() {
386
- if (CHANNEL !== "all" && !CHANNELS.some((channel) => channel.id === CHANNEL)) {
297
+ // ---------------------------------------------------------------------------
298
+ // 入口
299
+ // ---------------------------------------------------------------------------
300
+
301
+ const args = process.argv.slice(2);
302
+ const OUTDIR = getArg(args, "--outdir") || ".";
303
+ const CHANNEL = getArg(args, "--channel") || "male";
304
+ const RANKTYPE = getArg(args, "--type") || "hot";
305
+ const PERIOD = getArg(args, "--period") || "day";
306
+ const LOGIN_WAIT = parseInt(getArg(args, "--login-wait") || "0", 10);
307
+
308
+ async function main() {
309
+ if (CHANNEL !== "all" && !CHANNELS.some(c => c.id === CHANNEL)) {
387
310
  throw new Error(`未知 --channel: ${CHANNEL}`);
388
311
  }
389
- if (RANKTYPE !== "all" && !RANK_TYPES.some((rank) => rank.id === RANKTYPE)) {
312
+ if (RANKTYPE !== "all" && !RANK_TYPES.some(r => r.id === RANKTYPE)) {
390
313
  throw new Error(`未知 --type: ${RANKTYPE}`);
391
314
  }
392
- if (PERIOD !== "all" && !PERIODS.some((period) => period.id === PERIOD)) {
315
+ if (PERIOD !== "all" && !PERIODS.some(p => p.id === PERIOD)) {
393
316
  throw new Error(`未知 --period: ${PERIOD}`);
394
317
  }
395
318
  const targets = buildTargets(CHANNEL, RANKTYPE, PERIOD);
396
- let written = 0;
397
- let failed = 0;
319
+ let written = 0, failed = 0;
398
320
 
399
321
  for (const target of targets) {
400
- const content = scrapeRank(PORT, target.channel, target.rankType, target.period);
401
- if (!content) {
402
- failed++;
403
- continue;
404
- }
405
-
322
+ const content = await scrapeRank(target.channel, target.rankType, target.period);
323
+ if (!content) { failed++; continue; }
406
324
  const date = localDateStamp();
407
325
  const filename = outputFilename(target.channel, target.rankType, target.period, date);
408
326
  fs.mkdirSync(OUTDIR, { recursive: true });
409
- const filepath = path.join(OUTDIR, filename);
410
- fs.writeFileSync(filepath, content, "utf-8");
327
+ fs.writeFileSync(path.join(OUTDIR, filename), content, "utf-8");
411
328
  written++;
412
- console.log(` ✓ 已保存: ${filepath}`);
329
+ console.log(` ✓ 已保存: ${filename}`);
413
330
  }
414
- return {
415
- planned: targets.length,
416
- written,
417
- failed,
418
- partial: failed > 0,
419
- partialReasons: [],
420
- };
331
+
332
+ return { planned: targets.length, written, failed, partial: failed > 0, partialReasons: [] };
421
333
  }
422
334
 
423
335
  if (require.main === module) {
424
336
  runCli(main, "七猫采集");
425
337
  }
426
338
 
427
- module.exports = {
428
- extractBooksFromText,
429
- isUsableBook,
430
- cleanDesc,
431
- renderMarkdown,
432
- rankUrl,
433
- selectionMatches,
434
- buildTargets,
435
- outputFilename,
436
- };
339
+ module.exports = { rankUrl, isUsableBook, cleanDesc, buildTargets, outputFilename };