rssany 0.2.0 → 0.3.1

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 (159) hide show
  1. package/README.md +22 -22
  2. package/app/plugins/builtin/agi-eval-evaluation.rssany.js +6 -7
  3. package/app/plugins/builtin/amii-research-talent.rssany.js +6 -7
  4. package/app/plugins/builtin/anthropic-research.rssany.js +6 -8
  5. package/app/plugins/builtin/appen-resources.rssany.js +6 -7
  6. package/app/plugins/builtin/baai-wudao-paper-article.rssany.js +9 -10
  7. package/app/plugins/builtin/baaidata-csdn.rssany.js +6 -7
  8. package/app/plugins/builtin/baidu-research.rssany.js +5 -8
  9. package/app/plugins/builtin/brightdata-blog.rssany.js +6 -11
  10. package/app/plugins/builtin/bytedance-seed-research.rssany.js +5 -7
  11. package/app/plugins/builtin/email.rssany.js +9 -9
  12. package/app/plugins/builtin/five-radar.rssany.js +9 -11
  13. package/app/plugins/builtin/flageval-news.rssany.js +5 -7
  14. package/app/plugins/builtin/google-deepmind-research.rssany.js +6 -8
  15. package/app/plugins/builtin/google-research-datasets.rssany.js +6 -8
  16. package/app/plugins/builtin/google-research.rssany.js +6 -8
  17. package/app/plugins/builtin/hacker-news-newest.rssany.js +7 -9
  18. package/app/plugins/builtin/harvard-dataverse.rssany.js +6 -8
  19. package/app/plugins/builtin/huaweicloud-bbs-blogs.rssany.js +7 -9
  20. package/app/plugins/builtin/lingowhale.rssany.js +7 -9
  21. package/app/plugins/builtin/meituan-tech.rssany.js +7 -10
  22. package/app/plugins/builtin/meta-ai-publications.rssany.js +6 -11
  23. package/app/plugins/builtin/mila-quebec.rssany.js +6 -8
  24. package/app/plugins/builtin/mit-csail-research.rssany.js +7 -9
  25. package/app/plugins/builtin/moonshot.rssany.js +6 -8
  26. package/app/plugins/builtin/opendatalab-news.rssany.js +6 -7
  27. package/app/plugins/builtin/opendatalab.rssany.js +5 -6
  28. package/app/plugins/builtin/opendrivelab-autonomous-driving.rssany.js +6 -7
  29. package/app/plugins/builtin/opendrivelab-embodiedai.rssany.js +7 -8
  30. package/app/plugins/builtin/opendrivelab-publications.rssany.js +6 -8
  31. package/app/plugins/builtin/opendrivelab.rssany.js +7 -8
  32. package/app/plugins/builtin/paperswithcode.rssany.js +6 -8
  33. package/app/plugins/builtin/pjlab-adg-publications.rssany.js +7 -9
  34. package/app/plugins/builtin/rss.rssany.js +11 -12
  35. package/app/plugins/builtin/selectdataset.rssany.js +6 -8
  36. package/app/plugins/builtin/sensetime-tech-achievements.rssany.js +7 -8
  37. package/app/plugins/builtin/supervisely-blog.rssany.js +6 -8
  38. package/app/plugins/builtin/theinformation-briefings.rssany.js +7 -13
  39. package/app/plugins/builtin/uci-ml-repository.rssany.js +6 -7
  40. package/app/plugins/builtin/venturebeat.rssany.js +7 -9
  41. package/app/plugins/builtin/worldlabs.rssany.js +6 -8
  42. package/app/plugins/builtin/x.rssany.js +7 -9
  43. package/app/plugins/builtin/xiaohongshu.rssany.js +119 -56
  44. package/app/plugins/builtin/zhipu-research.rssany.js +5 -8
  45. package/app/plugins/site.rssany.js +25 -26
  46. package/{statics → app/statics}/README.md +7 -7
  47. package/app/webui/build/200.html +51 -0
  48. package/{webui/build/_app/immutable/assets/0.BB88QFoe.css → app/webui/build/_app/immutable/assets/0.DsKls1SN.css} +1 -1
  49. package/app/webui/build/_app/immutable/assets/13.Qu_tY6H9.css +1 -0
  50. package/app/webui/build/_app/immutable/assets/14.DfMfOrS3.css +1 -0
  51. package/app/webui/build/_app/immutable/assets/16.Cw9oSkcO.css +1 -0
  52. package/app/webui/build/_app/immutable/assets/4.Di6rvlY-.css +1 -0
  53. package/{webui/build/_app/immutable/assets/SourcesList.yTBBi3_m.css → app/webui/build/_app/immutable/assets/SourcesList.D5Lso0bo.css} +1 -1
  54. package/{webui/build/_app/immutable/assets/homeFeedPanelStore.CSvlNcpm.css → app/webui/build/_app/immutable/assets/homeFeedPanelStore.CE6xTfsa.css} +1 -1
  55. package/app/webui/build/_app/immutable/chunks/6prdYIKP.js +1 -0
  56. package/{webui/build/_app/immutable/chunks/Xy_fhzQq.js → app/webui/build/_app/immutable/chunks/B-CeeY89.js} +1 -1
  57. package/app/webui/build/_app/immutable/chunks/B2cyTHdf.js +2 -0
  58. package/{webui/build/_app/immutable/chunks/DjNLq3TF.js → app/webui/build/_app/immutable/chunks/B6WG2Sd3.js} +1 -1
  59. package/app/webui/build/_app/immutable/chunks/BA4Gucnq.js +1 -0
  60. package/{webui/build/_app/immutable/chunks/xtNWTdbD.js → app/webui/build/_app/immutable/chunks/BAJAS8BI.js} +1 -1
  61. package/{webui/build/_app/immutable/chunks/Dt2CddFe.js → app/webui/build/_app/immutable/chunks/BkD3yAYe.js} +1 -1
  62. package/{webui/build/_app/immutable/chunks/DFuhmi31.js → app/webui/build/_app/immutable/chunks/C4uF_YIK.js} +1 -1
  63. package/{webui/build/_app/immutable/chunks/Dw782Tjs.js → app/webui/build/_app/immutable/chunks/C8umpVpB.js} +1 -1
  64. package/{webui/build/_app/immutable/chunks/BQqoDzLx.js → app/webui/build/_app/immutable/chunks/CFwxUBGi.js} +1 -1
  65. package/{webui/build/_app/immutable/chunks/tB7QMF3U.js → app/webui/build/_app/immutable/chunks/CGCMIfh3.js} +1 -1
  66. package/{webui/build/_app/immutable/chunks/BK3WtZwv.js → app/webui/build/_app/immutable/chunks/CS53ooo0.js} +1 -1
  67. package/app/webui/build/_app/immutable/chunks/CVW0ymE1.js +1 -0
  68. package/{webui/build/_app/immutable/chunks/B-OsL1Ct.js → app/webui/build/_app/immutable/chunks/ChUctqXA.js} +1 -1
  69. package/{webui/build/_app/immutable/chunks/D5GvRCv7.js → app/webui/build/_app/immutable/chunks/ClknbeNl.js} +1 -1
  70. package/{webui/build/_app/immutable/chunks/Bu9HsS-V.js → app/webui/build/_app/immutable/chunks/CqYSO3Dx.js} +1 -1
  71. package/{webui/build/_app/immutable/chunks/CWNeClHp.js → app/webui/build/_app/immutable/chunks/D6kzEN_P.js} +1 -1
  72. package/app/webui/build/_app/immutable/chunks/DAdOEnFb.js +1 -0
  73. package/{webui/build/_app/immutable/chunks/Cihqbfi5.js → app/webui/build/_app/immutable/chunks/DCEayuDt.js} +1 -1
  74. package/app/webui/build/_app/immutable/chunks/DJ2e04vK.js +36 -0
  75. package/{webui/build/_app/immutable/chunks/DEDI7Ecm.js → app/webui/build/_app/immutable/chunks/DL3Q5sfb.js} +1 -1
  76. package/{webui/build/_app/immutable/chunks/CVzlFH44.js → app/webui/build/_app/immutable/chunks/DVa8Y-mQ.js} +1 -1
  77. package/app/webui/build/_app/immutable/chunks/DkamXS6W.js +36 -0
  78. package/app/webui/build/_app/immutable/chunks/DoRPmqLn.js +2 -0
  79. package/app/webui/build/_app/immutable/chunks/DsxvjlCC.js +13 -0
  80. package/{webui/build/_app/immutable/chunks/Bp63qm3L.js → app/webui/build/_app/immutable/chunks/Dyvi1wBH.js} +1 -1
  81. package/{webui/build/_app/immutable/chunks/CmjOpds-.js → app/webui/build/_app/immutable/chunks/_qj9U-za.js} +1 -1
  82. package/app/webui/build/_app/immutable/chunks/vtBo8kBV.js +1 -0
  83. package/app/webui/build/_app/immutable/entry/app.RFfWi3_i.js +2 -0
  84. package/app/webui/build/_app/immutable/entry/start.DU_kyeGS.js +1 -0
  85. package/{webui/build/_app/immutable/nodes/0.I1lQdWMl.js → app/webui/build/_app/immutable/nodes/0.DK_mcVDm.js} +1 -1
  86. package/app/webui/build/_app/immutable/nodes/1.0PRrU2uQ.js +1 -0
  87. package/{webui/build/_app/immutable/nodes/10.CvfUsqsw.js → app/webui/build/_app/immutable/nodes/10.CsxzlUER.js} +1 -1
  88. package/app/webui/build/_app/immutable/nodes/11.D-PkhIRW.js +1 -0
  89. package/{webui/build/_app/immutable/nodes/12.DVFJuIWI.js → app/webui/build/_app/immutable/nodes/12.GGf-JLUY.js} +1 -1
  90. package/app/webui/build/_app/immutable/nodes/13.DWWcH27k.js +6 -0
  91. package/app/webui/build/_app/immutable/nodes/14.COwSLwDN.js +1 -0
  92. package/app/webui/build/_app/immutable/nodes/15.nDN_AHrs.js +1 -0
  93. package/app/webui/build/_app/immutable/nodes/16.zfSe93Ab.js +24 -0
  94. package/app/webui/build/_app/immutable/nodes/2.AJd2163d.js +1 -0
  95. package/app/webui/build/_app/immutable/nodes/3.CEVEHuaH.js +1 -0
  96. package/app/webui/build/_app/immutable/nodes/4.BT_N8pCh.js +2 -0
  97. package/{webui/build/_app/immutable/nodes/5.B6fR3n6J.js → app/webui/build/_app/immutable/nodes/5.BZScQ2CH.js} +1 -1
  98. package/{webui/build/_app/immutable/nodes/6.j2O5Mwjv.js → app/webui/build/_app/immutable/nodes/6.CkFk8X--.js} +1 -1
  99. package/app/webui/build/_app/immutable/nodes/7.CuQJk7te.js +1 -0
  100. package/{webui/build/_app/immutable/nodes/8.Bw_d63B_.js → app/webui/build/_app/immutable/nodes/8.DIavWJnU.js} +1 -1
  101. package/{webui/build/_app/immutable/nodes/9.pMMi5PP6.js → app/webui/build/_app/immutable/nodes/9.Db30M8x0.js} +1 -1
  102. package/app/webui/build/_app/version.json +1 -0
  103. package/app/webui/build/apple-touch-icon.png +0 -0
  104. package/app/webui/build/favicon.ico +0 -0
  105. package/app/webui/build/favicon.png +0 -0
  106. package/bin/rssany.js +226 -6
  107. package/dist/index.js +209 -152
  108. package/dist/index.js.map +1 -1
  109. package/package.json +22 -16
  110. package/scripts/dev.mjs +114 -0
  111. package/scripts/reset.mjs +1 -1
  112. package/init/config.json +0 -17
  113. package/init/sources.json +0 -353
  114. package/statics/401.html +0 -56
  115. package/statics/404.html +0 -12
  116. package/statics/image.png +0 -0
  117. package/webui/build/200.html +0 -49
  118. package/webui/build/_app/immutable/assets/13.BhO9zvFi.css +0 -1
  119. package/webui/build/_app/immutable/assets/14.CujIhjQK.css +0 -1
  120. package/webui/build/_app/immutable/assets/16.PP9XLDf7.css +0 -1
  121. package/webui/build/_app/immutable/assets/4.9wPHhVwv.css +0 -1
  122. package/webui/build/_app/immutable/chunks/5LVkDJzw.js +0 -1
  123. package/webui/build/_app/immutable/chunks/B2Q1a1-H.js +0 -2
  124. package/webui/build/_app/immutable/chunks/BbWUOQ_m.js +0 -1
  125. package/webui/build/_app/immutable/chunks/Bns1MuyM.js +0 -36
  126. package/webui/build/_app/immutable/chunks/DMWEh-Ek.js +0 -2
  127. package/webui/build/_app/immutable/chunks/bvuf_jZd.js +0 -36
  128. package/webui/build/_app/immutable/chunks/lk5LaiqA.js +0 -1
  129. package/webui/build/_app/immutable/chunks/mW5RwvnK.js +0 -13
  130. package/webui/build/_app/immutable/entry/app.BVkrDt5l.js +0 -2
  131. package/webui/build/_app/immutable/entry/start.D3Q-BMMd.js +0 -1
  132. package/webui/build/_app/immutable/nodes/1.BiQQfx2j.js +0 -1
  133. package/webui/build/_app/immutable/nodes/11.B4LHPNL6.js +0 -1
  134. package/webui/build/_app/immutable/nodes/13.nT3SOzEB.js +0 -1
  135. package/webui/build/_app/immutable/nodes/14.DfaAf0f8.js +0 -1
  136. package/webui/build/_app/immutable/nodes/15.CMzkX9OK.js +0 -1
  137. package/webui/build/_app/immutable/nodes/16.zPgTQNze.js +0 -24
  138. package/webui/build/_app/immutable/nodes/2.BYWOpaxy.js +0 -1
  139. package/webui/build/_app/immutable/nodes/3.B8Viux9S.js +0 -1
  140. package/webui/build/_app/immutable/nodes/4.DTSxpKm7.js +0 -2
  141. package/webui/build/_app/immutable/nodes/7.Bd2USIrl.js +0 -1
  142. package/webui/build/_app/version.json +0 -1
  143. /package/{webui → app/webui}/build/_app/env.js +0 -0
  144. /package/{webui → app/webui}/build/_app/immutable/assets/10.Dj8_pmut.css +0 -0
  145. /package/{webui → app/webui}/build/_app/immutable/assets/11.qYZMiTb0.css +0 -0
  146. /package/{webui → app/webui}/build/_app/immutable/assets/12.DfJcfUWl.css +0 -0
  147. /package/{webui → app/webui}/build/_app/immutable/assets/15.nNGjXhCQ.css +0 -0
  148. /package/{webui → app/webui}/build/_app/immutable/assets/5.B-dPiwB7.css +0 -0
  149. /package/{webui → app/webui}/build/_app/immutable/assets/6.B27N7pdA.css +0 -0
  150. /package/{webui → app/webui}/build/_app/immutable/assets/7.CrNxmd8B.css +0 -0
  151. /package/{webui → app/webui}/build/_app/immutable/assets/8.Cgji2b15.css +0 -0
  152. /package/{webui → app/webui}/build/_app/immutable/assets/9.BsCIAvn3.css +0 -0
  153. /package/{webui → app/webui}/build/_app/immutable/assets/BackToParentRoute.DGk-X5ow.css +0 -0
  154. /package/{webui → app/webui}/build/_app/immutable/chunks/BUApaBEI.js +0 -0
  155. /package/{webui → app/webui}/build/_app/immutable/chunks/Bfc47y5P.js +0 -0
  156. /package/{webui → app/webui}/build/_app/immutable/chunks/CBY2biv-.js +0 -0
  157. /package/{webui → app/webui}/build/_app/immutable/chunks/hp4PFHFv.js +0 -0
  158. /package/{webui → app/webui}/build/_app/immutable/nodes/17.BtYZF6FM.js +0 -0
  159. /package/{webui → app/webui}/build/_app/immutable/nodes/18.BIzqhTqv.js +0 -0
package/dist/index.js CHANGED
@@ -242,7 +242,7 @@ async function migrateFile(from, to) {
242
242
  logger.warn("config", "配置迁移失败", { from, to, err: err instanceof Error ? err.message : String(err) });
243
243
  }
244
244
  }
245
- const INIT_DATA_DIR = join(PACKAGE_ROOT, "init");
245
+ const INIT_DATA_DIR = join(PACKAGE_ROOT, "app/init");
246
246
  const EXAMPLE_SOURCES = join(INIT_DATA_DIR, "sources.json");
247
247
  const EXAMPLE_CONFIG = join(INIT_DATA_DIR, "config.json");
248
248
  async function seedExampleConfigsIfMissing() {
@@ -633,7 +633,8 @@ async function upsertItems(items, sourceUrlOverride) {
633
633
  const nextAuthor = nextAuthorArr?.length ? JSON.stringify(nextAuthorArr) : null;
634
634
  const nextPubDate = pubDateToIsoOrNull(item.pubDate);
635
635
  const nextTags = item.tags?.length ? JSON.stringify(item.tags) : null;
636
- const nextImageUrl = typeof item.imageUrl === "string" && item.imageUrl.trim() ? item.imageUrl.trim() : null;
636
+ const rawImageUrl = item.imageUrl ?? item.coverImg ?? item.cover_img;
637
+ const nextImageUrl = typeof rawImageUrl === "string" && rawImageUrl.trim() ? rawImageUrl.trim() : null;
637
638
  const info = insertStmt.run({
638
639
  id: item.guid,
639
640
  url: item.link,
@@ -654,7 +655,9 @@ async function upsertItems(items, sourceUrlOverride) {
654
655
  const existing = selectExistingStmt.get({ id: item.guid });
655
656
  if (!existing) continue;
656
657
  const shouldRepairTitle = !!nextTitle && !isDateOnlyTitle(nextTitle) && (isDateOnlyTitle(existing.title) || !normalizeText(existing.title));
657
- const shouldRepairSummary = !!nextSummary && normalizeText(existing.summary ?? "").length < nextSummary.length;
658
+ const existingSummaryText = normalizeText(existing.summary ?? "");
659
+ const shouldClearDuplicatedSummary = nextSummary == null && !!nextTitle && existingSummaryText === nextTitle;
660
+ const shouldRepairSummary = !!nextSummary && (existingSummaryText.length < nextSummary.length || /!\[[^\]]*\]\([^)]*\)/.test(existingSummaryText)) || shouldClearDuplicatedSummary;
658
661
  const shouldRepairImageUrl = !!nextImageUrl && !existing.image_url?.trim();
659
662
  const existingAuthorArr = parseAuthorFromDb(existing.author);
660
663
  const shouldRepairAuthor = !!nextAuthorArr?.length && !existingAuthorArr?.length;
@@ -670,7 +673,7 @@ async function upsertItems(items, sourceUrlOverride) {
670
673
  id: item.guid,
671
674
  title: shouldRepairTitle ? nextTitle : existing.title,
672
675
  author: shouldRepairAuthor ? nextAuthor : existing.author ?? null,
673
- summary: shouldRepairSummary ? nextSummary : existing.summary,
676
+ summary: shouldClearDuplicatedSummary ? null : shouldRepairSummary ? nextSummary : existing.summary,
674
677
  imageUrl: shouldRepairImageUrl ? nextImageUrl : existing.image_url ?? null,
675
678
  pubDate: shouldRepairPubDate ? nextPubDate : existing.pub_date,
676
679
  fetchedAt: now2
@@ -682,6 +685,8 @@ async function upsertItems(items, sourceUrlOverride) {
682
685
  async function updateItemContent(item) {
683
686
  return withWriteLock(async () => {
684
687
  const db = await getDb();
688
+ const rawImageUrl = item.imageUrl ?? item.coverImg ?? item.cover_img;
689
+ const nextImageUrl = typeof rawImageUrl === "string" && rawImageUrl.trim() ? rawImageUrl.trim() : null;
685
690
  db.prepare(`
686
691
  UPDATE items SET
687
692
  content = COALESCE(content, @content),
@@ -694,7 +699,7 @@ async function updateItemContent(item) {
694
699
  `).run({
695
700
  id: item.guid,
696
701
  content: item.content ?? null,
697
- imageUrl: typeof item.imageUrl === "string" && item.imageUrl.trim() ? item.imageUrl.trim() : null,
702
+ imageUrl: nextImageUrl,
698
703
  author: (() => {
699
704
  const arr = normalizeAuthor(item.author);
700
705
  return arr?.length ? JSON.stringify(arr) : null;
@@ -705,42 +710,6 @@ async function updateItemContent(item) {
705
710
  });
706
711
  });
707
712
  }
708
- async function queryFeedItems(sourceUrls, limit, offset, opts) {
709
- if (sourceUrls.length === 0) return { items: [], hasMore: false };
710
- const expanded = [...new Set(sourceUrls.map((u) => canonicalHttpSourceRef(u)).filter(Boolean))];
711
- if (expanded.length === 0) return { items: [], hasMore: false };
712
- const db = await getDb();
713
- const placeholders = expanded.map((_, i) => `@u${i}`).join(", ");
714
- const conditions = [`source_url IN (${placeholders})`];
715
- const params = { lim: limit + 1, off: offset };
716
- expanded.forEach((url, i) => {
717
- params[`u${i}`] = url;
718
- });
719
- const sqlParams = params;
720
- if (opts?.since) {
721
- conditions.push("COALESCE(pub_date, fetched_at) >= @since");
722
- params.since = opts.since.length === 10 ? `${opts.since}T00:00:00.000Z` : opts.since;
723
- }
724
- if (opts?.until) {
725
- conditions.push("COALESCE(pub_date, fetched_at) < @until");
726
- if (opts.until.length === 10) {
727
- const d = /* @__PURE__ */ new Date(`${opts.until}T12:00:00Z`);
728
- d.setUTCDate(d.getUTCDate() + 1);
729
- params.until = d.toISOString();
730
- } else {
731
- params.until = opts.until;
732
- }
733
- }
734
- const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
735
- const rows = db.prepare(`
736
- SELECT * FROM items ${where}
737
- ORDER BY COALESCE(pub_date, fetched_at) DESC
738
- LIMIT ${limit + 1} OFFSET ${offset}
739
- `).all(sqlParams);
740
- const hasMore = rows.length > limit;
741
- const items = mapRowsToDbItems(hasMore ? rows.slice(0, limit) : rows);
742
- return { items, hasMore };
743
- }
744
713
  async function queryItems(opts) {
745
714
  const db = await getDb();
746
715
  const { sourceUrl, sourceUrls, author, q, tags: tagsFilter, limit = 20, offset = 0, since, until } = opts;
@@ -789,7 +758,7 @@ async function queryItems(opts) {
789
758
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
790
759
  const sqlParams = params;
791
760
  const rows = db.prepare(`
792
- SELECT i.id, i.url, i.source_url, i.title, i.author, i.summary, i.content, i.tags, i.translations, i.pub_date, i.fetched_at, i.pushed_at
761
+ SELECT i.id, i.url, i.source_url, i.title, i.author, i.summary, i.content, i.image_url, i.tags, i.translations, i.pub_date, i.fetched_at, i.pushed_at
793
762
  FROM items i ${where}
794
763
  ORDER BY COALESCE(i.pub_date, i.fetched_at) DESC
795
764
  LIMIT ${limit} OFFSET ${offset}
@@ -1204,6 +1173,22 @@ function isFrameDetachedError(e) {
1204
1173
  const msg = e instanceof Error ? e.message : String(e);
1205
1174
  return /detached|Navigating frame was detached|Session closed/i.test(msg);
1206
1175
  }
1176
+ const sharedBrowsers = /* @__PURE__ */ new Map();
1177
+ function browserKey(config) {
1178
+ const wantHeadless = config.headless !== false;
1179
+ const executablePath = config.chromeExecutablePath ?? process.env.CHROME_PATH ?? findChromeExecutable() ?? "";
1180
+ const userDataDir = getUserDataDir(config.cacheDir);
1181
+ const proxy = resolveProxy(config) ?? "";
1182
+ return JSON.stringify({
1183
+ headless: wantHeadless,
1184
+ userDataDir: userDataDir ? resolve(userDataDir) : "",
1185
+ proxy,
1186
+ executablePath
1187
+ });
1188
+ }
1189
+ function isBrowserConnected(browser) {
1190
+ return !!browser && browser.connected !== false;
1191
+ }
1207
1192
  async function launchBrowser(config) {
1208
1193
  const wantHeadless = config.headless !== false;
1209
1194
  const executablePath = config.chromeExecutablePath ?? process.env.CHROME_PATH ?? findChromeExecutable();
@@ -1247,29 +1232,53 @@ async function launchBrowser(config) {
1247
1232
  }
1248
1233
  throw lastErr;
1249
1234
  }
1235
+ async function getOrCreateBrowser(config) {
1236
+ const key = browserKey(config);
1237
+ const current = sharedBrowsers.get(key);
1238
+ if (isBrowserConnected(current?.browser)) {
1239
+ return current.browser;
1240
+ }
1241
+ if (current?.promise) {
1242
+ return current.promise;
1243
+ }
1244
+ const slot = {};
1245
+ const promise = launchBrowser({ ...config, proxy: resolveProxy(config) }).then((browser) => {
1246
+ slot.browser = browser;
1247
+ slot.promise = void 0;
1248
+ browser.once("disconnected", () => {
1249
+ if (sharedBrowsers.get(key)?.browser === browser) {
1250
+ sharedBrowsers.delete(key);
1251
+ }
1252
+ });
1253
+ return browser;
1254
+ }).catch((err) => {
1255
+ if (sharedBrowsers.get(key) === slot) {
1256
+ sharedBrowsers.delete(key);
1257
+ }
1258
+ throw err;
1259
+ });
1260
+ slot.promise = promise;
1261
+ sharedBrowsers.set(key, slot);
1262
+ return promise;
1263
+ }
1250
1264
  async function preCheckAuth(authFlow, cacheDir, opts) {
1251
1265
  const { checkAuth, loginUrl, domain } = authFlow;
1252
1266
  if (domain == null || !cacheDir) return true;
1253
1267
  const isHeadless = opts?.headless !== false;
1254
- const browser = await launchBrowser({
1268
+ const browser = await getOrCreateBrowser({
1255
1269
  headless: isHeadless,
1256
1270
  cacheDir,
1257
1271
  proxy: resolveProxy(opts)
1258
1272
  });
1273
+ const page = await browser.newPage();
1259
1274
  try {
1260
- const page = await browser.newPage();
1261
- try {
1262
- await setupPage(page, isHeadless);
1263
- await applyProxyAuthToPage(page, opts);
1264
- await page.goto(loginUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
1265
- await new Promise((resolve2) => setTimeout(resolve2, 3e3));
1266
- return await checkAuth(page, page.url());
1267
- } finally {
1268
- await page.close().catch(() => {
1269
- });
1270
- }
1275
+ await setupPage(page, isHeadless);
1276
+ await applyProxyAuthToPage(page, opts);
1277
+ await page.goto(loginUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
1278
+ await new Promise((resolve2) => setTimeout(resolve2, 3e3));
1279
+ return await checkAuth(page, page.url());
1271
1280
  } finally {
1272
- await browser.close().catch(() => {
1281
+ await page.close().catch(() => {
1273
1282
  });
1274
1283
  }
1275
1284
  }
@@ -1314,10 +1323,11 @@ async function fetchHtml(url, config = {}) {
1314
1323
  waitAfterLoadMs,
1315
1324
  waitForSelector,
1316
1325
  waitForSelectorTimeoutMs,
1326
+ scrollBeforeSnapshot,
1317
1327
  useHttpResponseBody
1318
1328
  } = config;
1319
1329
  const isHeadless = headless !== false;
1320
- const browser = await launchBrowser({
1330
+ const browser = await getOrCreateBrowser({
1321
1331
  headless: isHeadless,
1322
1332
  cacheDir,
1323
1333
  proxy: resolveProxy(config),
@@ -1326,84 +1336,105 @@ async function fetchHtml(url, config = {}) {
1326
1336
  const navigationTimeout = timeoutMs ?? 6e4;
1327
1337
  const maxAttempts = 2;
1328
1338
  let lastError;
1329
- try {
1330
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
1331
- const page = await browser.newPage();
1332
- const isRetry = attempt === 1;
1333
- const waitUntil = isRetry ? "domcontentloaded" : "load";
1334
- const extraWaitMs = isRetry ? Math.min(500, Math.max(0, waitAfterLoadMs ?? 2e3)) : Math.max(0, waitAfterLoadMs ?? 2e3);
1335
- try {
1336
- if (config.browserContext) {
1337
- await config.browserContext(page.browserContext());
1338
- }
1339
- await setupPage(page, isHeadless);
1340
- const extraHeaders = { "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", ...headers ?? {} };
1341
- if (cookies != null && cookies !== "") {
1342
- extraHeaders.cookie = cookies;
1343
- }
1344
- await page.setExtraHTTPHeaders(extraHeaders);
1345
- const proxy = resolveProxy(config);
1346
- if (proxy) {
1347
- const { username, password } = parseProxy(proxy);
1348
- if (username !== void 0 || password !== void 0) {
1349
- await page.authenticate({ username: username ?? "", password: password ?? "" });
1350
- }
1351
- }
1352
- if (timeoutMs != null) {
1353
- await page.setDefaultNavigationTimeout(timeoutMs);
1354
- }
1355
- const response = await page.goto(url, { waitUntil, timeout: navigationTimeout });
1356
- if (extraWaitMs > 0) {
1357
- await new Promise((resolve2) => setTimeout(resolve2, extraWaitMs));
1339
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1340
+ const page = await browser.newPage();
1341
+ const isRetry = attempt === 1;
1342
+ const waitUntil = isRetry ? "domcontentloaded" : "load";
1343
+ const extraWaitMs = isRetry ? Math.min(500, Math.max(0, waitAfterLoadMs ?? 2e3)) : Math.max(0, waitAfterLoadMs ?? 2e3);
1344
+ try {
1345
+ if (config.browserContext) {
1346
+ await config.browserContext(page.browserContext());
1347
+ }
1348
+ await setupPage(page, isHeadless);
1349
+ const extraHeaders = { "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", ...headers ?? {} };
1350
+ if (cookies != null && cookies !== "") {
1351
+ extraHeaders.cookie = cookies;
1352
+ }
1353
+ await page.setExtraHTTPHeaders(extraHeaders);
1354
+ const proxy = resolveProxy(config);
1355
+ if (proxy) {
1356
+ const { username, password } = parseProxy(proxy);
1357
+ if (username !== void 0 || password !== void 0) {
1358
+ await page.authenticate({ username: username ?? "", password: password ?? "" });
1358
1359
  }
1359
- if (waitForSelector != null && waitForSelector !== "" && !isRetry) {
1360
- const selectorTimeout = waitForSelectorTimeoutMs ?? 2e4;
1361
- await page.waitForSelector(waitForSelector, { timeout: selectorTimeout });
1360
+ }
1361
+ if (timeoutMs != null) {
1362
+ await page.setDefaultNavigationTimeout(timeoutMs);
1363
+ }
1364
+ const response = await page.goto(url, { waitUntil, timeout: navigationTimeout });
1365
+ if (extraWaitMs > 0) {
1366
+ await new Promise((resolve2) => setTimeout(resolve2, extraWaitMs));
1367
+ }
1368
+ if (waitForSelector != null && waitForSelector !== "" && !isRetry) {
1369
+ const selectorTimeout = waitForSelectorTimeoutMs ?? 2e4;
1370
+ await page.waitForSelector(waitForSelector, { timeout: selectorTimeout });
1371
+ }
1372
+ if (scrollBeforeSnapshot && !isRetry) {
1373
+ const scrollSelector = scrollBeforeSnapshot.selector ?? null;
1374
+ const rounds = scrollBeforeSnapshot.rounds ?? 6;
1375
+ const pauseMs = scrollBeforeSnapshot.pauseMs ?? 800;
1376
+ for (let i = 0; i < rounds; i++) {
1377
+ const before = await page.evaluate((sel) => {
1378
+ const target = sel ? document.querySelector(sel) : null;
1379
+ const el = target ?? document.scrollingElement ?? document.documentElement;
1380
+ return el?.scrollHeight ?? 0;
1381
+ }, scrollSelector);
1382
+ await page.evaluate((sel) => {
1383
+ const target = sel ? document.querySelector(sel) : null;
1384
+ const el = target ?? document.scrollingElement ?? document.documentElement;
1385
+ if (!el) return;
1386
+ el.scrollTop = el.scrollHeight;
1387
+ window.scrollBy(0, window.innerHeight);
1388
+ }, scrollSelector);
1389
+ await new Promise((resolve2) => setTimeout(resolve2, pauseMs));
1390
+ const after = await page.evaluate((sel) => {
1391
+ const target = sel ? document.querySelector(sel) : null;
1392
+ const el = target ?? document.scrollingElement ?? document.documentElement;
1393
+ return el?.scrollHeight ?? 0;
1394
+ }, scrollSelector);
1395
+ if (after <= before && i >= 2) break;
1362
1396
  }
1363
- if (checkAuth != null || authFlow != null) {
1364
- const authCheck = checkAuth ?? authFlow?.checkAuth;
1365
- if (authCheck != null) {
1366
- const ok = await authCheck(page, url);
1367
- if (!ok) {
1368
- throw new Error("checkAuth failed: 未通过认证检查,请先调用 ensureAuth 进行预处理登录");
1369
- }
1397
+ }
1398
+ if (checkAuth != null || authFlow != null) {
1399
+ const authCheck = checkAuth ?? authFlow?.checkAuth;
1400
+ if (authCheck != null) {
1401
+ const ok = await authCheck(page, url);
1402
+ if (!ok) {
1403
+ throw new Error("checkAuth failed: 未通过认证检查,请先调用 ensureAuth 进行预处理登录");
1370
1404
  }
1371
1405
  }
1372
- let rawBody;
1373
- if (useHttpResponseBody === true && response != null) {
1374
- try {
1375
- rawBody = await response.text();
1376
- } catch {
1377
- rawBody = await page.content();
1378
- }
1379
- } else {
1406
+ }
1407
+ let rawBody;
1408
+ if (useHttpResponseBody === true && response != null) {
1409
+ try {
1410
+ rawBody = await response.text();
1411
+ } catch {
1380
1412
  rawBody = await page.content();
1381
1413
  }
1382
- const finalUrl = response?.url() ?? page.url() ?? String(url);
1383
- const status = response?.status() ?? 0;
1384
- const statusText = response?.statusText() ?? "";
1385
- const rawHeaders = response?.headers() ?? {};
1386
- const normalizedHeaders = headersToRecord(rawHeaders);
1387
- const body = applyPurify(rawBody, purify);
1388
- await page.close().catch(() => {
1389
- });
1390
- return { finalUrl, status, statusText, headers: normalizedHeaders, body };
1391
- } catch (e) {
1392
- lastError = e;
1393
- await page.close().catch(() => {
1394
- });
1395
- if (isRetry || !isFrameDetachedError(e)) {
1396
- throw e;
1397
- }
1398
- logger.warn("scraper", "fetchHtml 因 frame 分离重试", { url, attempt: attempt + 1, err: e instanceof Error ? e.message : String(e) });
1399
- await new Promise((r) => setTimeout(r, 800));
1414
+ } else {
1415
+ rawBody = await page.content();
1400
1416
  }
1417
+ const finalUrl = response?.url() ?? page.url() ?? String(url);
1418
+ const status = response?.status() ?? 0;
1419
+ const statusText = response?.statusText() ?? "";
1420
+ const rawHeaders = response?.headers() ?? {};
1421
+ const normalizedHeaders = headersToRecord(rawHeaders);
1422
+ const body = applyPurify(rawBody, purify);
1423
+ await page.close().catch(() => {
1424
+ });
1425
+ return { finalUrl, status, statusText, headers: normalizedHeaders, body };
1426
+ } catch (e) {
1427
+ lastError = e;
1428
+ await page.close().catch(() => {
1429
+ });
1430
+ if (isRetry || !isFrameDetachedError(e)) {
1431
+ throw e;
1432
+ }
1433
+ logger.warn("scraper", "fetchHtml 因 frame 分离重试", { url, attempt: attempt + 1, err: e instanceof Error ? e.message : String(e) });
1434
+ await new Promise((r) => setTimeout(r, 800));
1401
1435
  }
1402
- throw lastError;
1403
- } finally {
1404
- await browser.close().catch(() => {
1405
- });
1406
1436
  }
1437
+ throw lastError;
1407
1438
  }
1408
1439
  const VALID_INTERVALS = ["1min", "5min", "10min", "30min", "1h", "6h", "12h", "1day", "3day", "7day"];
1409
1440
  function cronToRefreshInterval(cronExpr) {
@@ -1937,6 +1968,7 @@ function buildSiteContext(site, ctx) {
1937
1968
  purify: opts?.purify,
1938
1969
  waitForSelector: opts?.waitForSelector,
1939
1970
  waitForSelectorTimeoutMs: opts?.waitForSelectorTimeoutMs,
1971
+ scrollBeforeSnapshot: opts?.scrollBeforeSnapshot,
1940
1972
  useHttpResponseBody: opts?.useHttpResponseBody
1941
1973
  });
1942
1974
  return { html: res.body, finalUrl: res.finalUrl ?? url, status: res.status };
@@ -1975,6 +2007,7 @@ function createWebSource(site) {
1975
2007
  const authFlow = toAuthFlow(site);
1976
2008
  return {
1977
2009
  id: site.id,
2010
+ name: site.name,
1978
2011
  pattern: site.listUrlPattern,
1979
2012
  priority: 50,
1980
2013
  refreshInterval: site.refreshInterval ?? void 0,
@@ -2731,7 +2764,7 @@ async function generateAndCache(listUrl, key, config, proxy) {
2731
2764
  }
2732
2765
  return { items: out };
2733
2766
  }
2734
- async function getItems(listUrl, config = {}) {
2767
+ async function crawlSource(listUrl, config = {}) {
2735
2768
  const source = getSource(listUrl);
2736
2769
  const proxy = await getEffectiveProxyForListUrl(listUrl, source);
2737
2770
  const headless = resolveHeadlessForFeeder(config);
@@ -2756,6 +2789,10 @@ async function getItems(listUrl, config = {}) {
2756
2789
  if (!config.force) generatingKeys.set(key, task);
2757
2790
  }
2758
2791
  const { items } = await task;
2792
+ return { items };
2793
+ }
2794
+ async function getItems(listUrl, config = {}) {
2795
+ const { items } = await crawlSource(listUrl, config);
2759
2796
  return { items, fromCache: false };
2760
2797
  }
2761
2798
  function feedItemsToRssXml(items, listUrl, lng, opts) {
@@ -2951,7 +2988,7 @@ const DEFAULT_REFRESH = "1day";
2951
2988
  const SOURCES_CONCURRENCY = 1;
2952
2989
  function createPullTask(ref, cacheDir, cronExpr) {
2953
2990
  return async () => {
2954
- await getItems(ref, {
2991
+ await crawlSource(ref, {
2955
2992
  cacheDir,
2956
2993
  cron: cronExpr
2957
2994
  });
@@ -3068,24 +3105,26 @@ function registerSchedulerRoutes(app) {
3068
3105
  });
3069
3106
  }
3070
3107
  const SITE_TEMPLATE_FALLBACK = `/**
3071
- * Site 插件模板(由 /plugins 页添加,位于 .rssany/plugins/)
3072
- * HTML DOM 解析请用 ctx.deps.parseHtml,勿在插件内 import node_modules。
3108
+ * Site plugin template created from the /plugins page.
3109
+ * Plugin protocol: named exports. No export default is required.
3110
+ * Parse HTML with ctx.deps.parseHtml; do not import app dependencies directly.
3073
3111
  */
3074
- export default {
3075
- id: "__PLUGIN_ID__",
3076
- listUrlPattern: __LIST_URL_PATTERN__,
3077
- refreshInterval: "1day",
3078
3112
 
3079
- async fetchItems(sourceId, ctx) {
3080
- const { html, finalUrl } = await ctx.fetchHtml(sourceId, {
3081
- waitMs: 2000,
3082
- purify: true,
3083
- });
3084
- void ctx.deps.parseHtml(html);
3085
- void finalUrl;
3086
- return [];
3087
- },
3088
- };
3113
+ // Predefined fields stay together at the top.
3114
+ export const id = "__PLUGIN_ID__";
3115
+ export const name = "__PLUGIN_ID__";
3116
+ export const listUrlPattern = __LIST_URL_PATTERN__;
3117
+ export const refreshInterval = "1day";
3118
+
3119
+ export async function fetchItems(sourceId, ctx) {
3120
+ const { html, finalUrl } = await ctx.fetchHtml(sourceId, {
3121
+ waitMs: 2000,
3122
+ purify: true,
3123
+ });
3124
+ void ctx.deps.parseHtml(html);
3125
+ void finalUrl;
3126
+ return [];
3127
+ }
3089
3128
  `;
3090
3129
  function isValidNewPluginId(id) {
3091
3130
  return /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/.test(id) && id !== "generic" && id !== "new";
@@ -3154,6 +3193,7 @@ function registerPluginsRoutes(app) {
3154
3193
  const sites = getPluginSites().map((s) => ({
3155
3194
  kind: "site",
3156
3195
  id: s.id,
3196
+ name: s.name ?? s.id,
3157
3197
  listUrlPattern: typeof s.listUrlPattern === "string" ? s.listUrlPattern : String(s.listUrlPattern),
3158
3198
  hasAuth: !!(s.checkAuth && s.loginUrl)
3159
3199
  }));
@@ -3161,6 +3201,7 @@ function registerPluginsRoutes(app) {
3161
3201
  const sources = registeredSources.filter((src) => src.id !== "generic" && !siteIds.has(src.id)).map((src) => ({
3162
3202
  kind: "source",
3163
3203
  id: src.id,
3204
+ name: src.name ?? src.id,
3164
3205
  listUrlPattern: typeof src.pattern === "string" ? src.pattern : String(src.pattern),
3165
3206
  hasAuth: false
3166
3207
  }));
@@ -3263,8 +3304,25 @@ function registerFeedRoutes(app) {
3263
3304
  ref: resolveRef(s),
3264
3305
  label: s.label ?? resolveRef(s)
3265
3306
  }));
3266
- const dateOpts = since || until ? { since: since ?? void 0, until: until ?? void 0 } : void 0;
3267
- const { items: dbItems, hasMore } = await queryFeedItems(sourceRefs, limit, offset, dateOpts);
3307
+ const parseDateBound = (value, endExclusive) => {
3308
+ if (!value) return void 0;
3309
+ if (value.length === 10) {
3310
+ const d2 = /* @__PURE__ */ new Date(endExclusive ? `${value}T12:00:00Z` : `${value}T00:00:00.000Z`);
3311
+ if (endExclusive) d2.setUTCDate(d2.getUTCDate() + 1);
3312
+ return d2;
3313
+ }
3314
+ const d = new Date(value);
3315
+ return Number.isNaN(d.getTime()) ? void 0 : d;
3316
+ };
3317
+ const result = sourceRefs.length > 0 ? await queryItems({
3318
+ sourceUrls: sourceRefs,
3319
+ limit: limit + 1,
3320
+ offset,
3321
+ since: parseDateBound(since ?? void 0, false),
3322
+ until: parseDateBound(until ?? void 0, true)
3323
+ }) : { items: [] };
3324
+ const hasMore = result.items.length > limit;
3325
+ const dbItems = hasMore ? result.items.slice(0, limit) : result.items;
3268
3326
  const items = dbItems.map((item) => {
3269
3327
  const refKey = item.source_url ?? "";
3270
3328
  const base2 = {
@@ -3840,7 +3898,7 @@ function registerTasksRoutes(app) {
3840
3898
  schedule(SOURCES_GROUP, taskId, async () => {
3841
3899
  setTaskRunning(taskId);
3842
3900
  try {
3843
- await getItems(ref, { cacheDir: CACHE_DIR, force: true });
3901
+ await crawlSource(ref, { cacheDir: CACHE_DIR, force: true });
3844
3902
  setTaskDone(taskId, { ok: true });
3845
3903
  } catch (err) {
3846
3904
  const msg = err instanceof Error ? err.message : String(err);
@@ -4247,7 +4305,7 @@ function registerAuthRoutes(app) {
4247
4305
  return c.json({ ok: true, message: "已打开登录窗口,请在弹出的浏览器中完成登录,完成后刷新订阅页面即可。" });
4248
4306
  });
4249
4307
  }
4250
- const STATICS_DIR = join(PACKAGE_ROOT, "statics");
4308
+ const STATICS_DIR = join(PACKAGE_ROOT, "app/statics");
4251
4309
  function parseUrlFromPath(path, prefix) {
4252
4310
  const raw = path.slice(prefix.length) || "";
4253
4311
  const decoded = decodeURIComponent(raw.startsWith("/") ? raw.slice(1) : raw);
@@ -4475,7 +4533,7 @@ function getWebUiBuildDir() {
4475
4533
  if (w.startsWith("/") || /^[A-Za-z]:[\\/]/.test(w)) return w;
4476
4534
  return join(process.cwd(), w);
4477
4535
  }
4478
- return join(PACKAGE_ROOT, "webui/build");
4536
+ return join(PACKAGE_ROOT, "app/webui/build");
4479
4537
  }
4480
4538
  function isBackendOnlyPath(pathname) {
4481
4539
  if (pathname.startsWith("/api")) return true;
@@ -4491,11 +4549,10 @@ function registerWebUiRoutes(app) {
4491
4549
  const absRoot = getWebUiBuildDir();
4492
4550
  if (!existsSync(absRoot)) {
4493
4551
  console.warn(
4494
- "未找到 WebUI 构建目录,跳过根路径静态托管:",
4552
+ "未找到 WebUI 构建目录,静态路由已注册,等待前端 watch 构建:",
4495
4553
  absRoot,
4496
- "(构建前端:pnpm run webui:build)"
4554
+ "(开发模式:npm run dev;单独构建:npm run webui:build)"
4497
4555
  );
4498
- return;
4499
4556
  }
4500
4557
  const relRoot = relative(process.cwd(), absRoot).replace(/\\/g, "/");
4501
4558
  const staticRoot = relRoot === "" || relRoot === "." ? "." : relRoot.startsWith(".") || relRoot.startsWith("/") || /^[A-Za-z]:/.test(relRoot) ? relRoot : `./${relRoot}`;
@@ -4580,7 +4637,7 @@ async function main() {
4580
4637
  const server = serve({ fetch: app.fetch, port: PORT, hostname: "0.0.0.0" });
4581
4638
  server.setMaxListeners(32);
4582
4639
  console.log(
4583
- `RssAny ${getAppVersion()} 服务已启动 http://127.0.0.1:${PORT}/(API + 静态前端,需先 pnpm run webui:build)`
4640
+ `RssAny ${getAppVersion()} 服务已启动 http://127.0.0.1:${PORT}/(API + 静态前端单地址)`
4584
4641
  );
4585
4642
  const lanIp = Object.values(networkInterfaces()).flat().find((iface) => iface?.family === "IPv4" && !iface.internal)?.address;
4586
4643
  if (lanIp) console.log(`局域网访问 http://${lanIp}:${PORT}/`);