rssany 0.1.2 → 0.1.5

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 (75) hide show
  1. package/README.md +28 -50
  2. package/app/plugins/builtin/agi-eval-evaluation.rssany.js +188 -0
  3. package/app/plugins/builtin/amii-research-talent.rssany.js +73 -0
  4. package/app/plugins/builtin/anthropic-research.rssany.js +155 -0
  5. package/app/plugins/builtin/appen-resources.rssany.js +155 -0
  6. package/app/plugins/builtin/baai-wudao-paper-article.rssany.js +185 -0
  7. package/app/plugins/builtin/baaidata-csdn.rssany.js +242 -0
  8. package/app/plugins/builtin/baidu-research.rssany.js +222 -0
  9. package/app/plugins/builtin/brightdata-blog.rssany.js +301 -0
  10. package/app/plugins/builtin/bytedance-seed-research.rssany.js +231 -0
  11. package/app/plugins/builtin/five-radar.rssany.js +490 -0
  12. package/app/plugins/builtin/flageval-news.rssany.js +118 -0
  13. package/app/plugins/builtin/google-deepmind-research.rssany.js +223 -0
  14. package/app/plugins/builtin/google-research-datasets.rssany.js +171 -0
  15. package/app/plugins/builtin/google-research.rssany.js +220 -0
  16. package/app/plugins/builtin/google.rssany.js +187 -0
  17. package/app/plugins/builtin/hacker-news-newest.rssany.js +130 -0
  18. package/app/plugins/builtin/harvard-dataverse.rssany.js +166 -0
  19. package/app/plugins/builtin/huaweicloud-bbs-blogs.rssany.js +185 -0
  20. package/app/plugins/builtin/lingowhale.rssany.js +119 -0
  21. package/app/plugins/builtin/meituan-tech.rssany.js +130 -0
  22. package/app/plugins/builtin/meta-ai-publications.rssany.js +221 -0
  23. package/app/plugins/builtin/mila-quebec.rssany.js +199 -0
  24. package/app/plugins/builtin/mit-csail-research.rssany.js +208 -0
  25. package/app/plugins/builtin/moonshot.rssany.js +127 -0
  26. package/app/plugins/builtin/opendatalab-news.rssany.js +174 -0
  27. package/app/plugins/builtin/opendatalab.rssany.js +109 -0
  28. package/app/plugins/builtin/opendrivelab-autonomous-driving.rssany.js +114 -0
  29. package/app/plugins/builtin/opendrivelab-embodiedai.rssany.js +114 -0
  30. package/app/plugins/builtin/opendrivelab-publications.rssany.js +130 -0
  31. package/app/plugins/builtin/opendrivelab.rssany.js +333 -0
  32. package/app/plugins/builtin/paperswithcode.rssany.js +227 -0
  33. package/app/plugins/builtin/pjlab-adg-publications.rssany.js +202 -0
  34. package/app/plugins/builtin/rss.rssany.js +11 -1
  35. package/app/plugins/builtin/selectdataset.rssany.js +206 -0
  36. package/app/plugins/builtin/sensetime-tech-achievements.rssany.js +154 -0
  37. package/app/plugins/builtin/supervisely-blog.rssany.js +159 -0
  38. package/app/plugins/builtin/uci-ml-repository.rssany.js +111 -0
  39. package/app/plugins/builtin/venturebeat.rssany.js +97 -0
  40. package/app/plugins/builtin/worldlabs.rssany.js +129 -0
  41. package/app/plugins/builtin/x.rssany.js +159 -0
  42. package/app/plugins/builtin/xiaohongshu.rssany.js +283 -0
  43. package/app/plugins/builtin/zhipu-research.rssany.js +334 -0
  44. package/dist/index.js +79 -9
  45. package/dist/index.js.map +1 -1
  46. package/package.json +1 -1
  47. package/webui/build/200.html +6 -6
  48. package/webui/build/_app/immutable/assets/0.BB88QFoe.css +1 -0
  49. package/webui/build/_app/immutable/assets/{homeFeedPanelStore.BopJZtHu.css → homeFeedPanelStore.iOmfP2qL.css} +1 -1
  50. package/webui/build/_app/immutable/chunks/CZD-YNDw.js +31 -0
  51. package/webui/build/_app/immutable/chunks/{DcAshVxe.js → D6VIKef0.js} +1 -1
  52. package/webui/build/_app/immutable/chunks/{EIZIMsXK.js → Dbqx2mXq.js} +1 -1
  53. package/webui/build/_app/immutable/chunks/DeX-oq5W.js +41 -0
  54. package/webui/build/_app/immutable/chunks/{BXCWEhUd.js → dhB8G5Is.js} +1 -1
  55. package/webui/build/_app/immutable/entry/{app.DdgnooOk.js → app.XPso7q7g.js} +2 -2
  56. package/webui/build/_app/immutable/entry/start.Db4snNCd.js +1 -0
  57. package/webui/build/_app/immutable/nodes/0.BKTQePmA.js +11 -0
  58. package/webui/build/_app/immutable/nodes/{1.5DFDaT4c.js → 1.BS3_Rfxm.js} +1 -1
  59. package/webui/build/_app/immutable/nodes/{10.OVK4i9XE.js → 10.CyyxDCIS.js} +1 -1
  60. package/webui/build/_app/immutable/nodes/{11.Dhn_rO4A.js → 11.CtYgIaGj.js} +1 -1
  61. package/webui/build/_app/immutable/nodes/{14.B_KpJLxn.js → 14.D5OEGPR2.js} +1 -1
  62. package/webui/build/_app/immutable/nodes/{15.RaWaA-0I.js → 15.B4dFN1Gk.js} +1 -1
  63. package/webui/build/_app/immutable/nodes/{16.DSUgqolV.js → 16.M7ZII7tl.js} +1 -1
  64. package/webui/build/_app/immutable/nodes/{3.wQvGs9w-.js → 3.7r8v7qkm.js} +1 -1
  65. package/webui/build/_app/immutable/nodes/{5.CCtn90c0.js → 5.CHIzoGrb.js} +1 -1
  66. package/webui/build/_app/immutable/nodes/{6.C2_mjW1u.js → 6.BDBqx-GY.js} +1 -1
  67. package/webui/build/_app/immutable/nodes/{7.Dwz6W7A1.js → 7.D5czsDmz.js} +1 -1
  68. package/webui/build/_app/immutable/nodes/{8.DzkEw6rx.js → 8.pjVNsCdV.js} +1 -1
  69. package/webui/build/_app/immutable/nodes/{9.DtlXEwe1.js → 9.CsARv1BH.js} +1 -1
  70. package/webui/build/_app/version.json +1 -1
  71. package/webui/build/_app/immutable/assets/0.C6Q_nuW9.css +0 -1
  72. package/webui/build/_app/immutable/chunks/CkUAV0m0.js +0 -41
  73. package/webui/build/_app/immutable/chunks/CtijX1u3.js +0 -31
  74. package/webui/build/_app/immutable/entry/start.DhJaJZhR.js +0 -1
  75. package/webui/build/_app/immutable/nodes/0.BE05Cuc4.js +0 -11
@@ -0,0 +1,333 @@
1
+ let _deps;
2
+
3
+ // OpenDriveLab 首页插件:解析首页展示内容并输出 FeedItem(不含 enrich)
4
+
5
+
6
+ const SITE_ID = "opendrivelab";
7
+ const NAVIGATION_TITLES = new Set([
8
+ "news",
9
+ "recruit",
10
+ "research",
11
+ "publication",
12
+ "dataset",
13
+ "event",
14
+ "more",
15
+ "team",
16
+ "sponsor",
17
+ "opendrivelab",
18
+ "embodied ai",
19
+ "autonomous driving",
20
+ ]);
21
+ const ACTION_LABELS = new Set([
22
+ "paper",
23
+ "page",
24
+ "blog",
25
+ "code",
26
+ "github",
27
+ "dataset",
28
+ "demo",
29
+ "video",
30
+ "poster",
31
+ "slides",
32
+ "community",
33
+ "cite",
34
+ "checkout at mmlab.hk/mm-hand",
35
+ ]);
36
+ const NAVIGATION_PATHS = new Set([
37
+ "/",
38
+ "/embodiedai",
39
+ "/autonomousdriving",
40
+ "/publications",
41
+ "/events",
42
+ "/team",
43
+ "/recruit",
44
+ "/ccai9025",
45
+ ]);
46
+
47
+ function normalizeText(text) {
48
+ return (text ?? "").replace(/\s+/g, " ").trim();
49
+ }
50
+
51
+ function hashGuid(input) {
52
+ return _deps.createHash("sha256").update(input).digest("hex");
53
+ }
54
+
55
+ function toAbsoluteHttpUrl(rawHref, baseUrl) {
56
+ if (!rawHref) return null;
57
+ const href = rawHref.trim();
58
+ if (!href || href.startsWith("#") || href.startsWith("javascript:") || href.startsWith("mailto:")) return null;
59
+ try {
60
+ const url = new URL(href, baseUrl);
61
+ if (!/^https?:$/i.test(url.protocol)) return null;
62
+ return url.href;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function normalizePath(pathname) {
69
+ if (!pathname) return "/";
70
+ const trimmed = pathname.replace(/\/+$/, "");
71
+ return (trimmed || "/").toLowerCase();
72
+ }
73
+
74
+ function isBlockedPage(root, html, finalUrl) {
75
+ const text = normalizeText(root.textContent).toLowerCase();
76
+ const body = (html ?? "").toLowerCase();
77
+ const url = (finalUrl ?? "").toLowerCase();
78
+ if (url.includes("/cdn-cgi/challenge")) return true;
79
+ if (body.includes("__cf_chl_opt")) return true;
80
+ if (body.includes("/cdn-cgi/challenge-platform")) return true;
81
+ if (text.includes("just a moment")) return true;
82
+ if (text.includes("checking your browser")) return true;
83
+ if (text.includes("attention required")) return true;
84
+ return text.includes("captcha");
85
+ }
86
+
87
+ function isNoiseTitle(text) {
88
+ const title = normalizeText(text);
89
+ if (!title) return true;
90
+ const lower = title.toLowerCase();
91
+ if (NAVIGATION_TITLES.has(lower)) return true;
92
+ if (/^\d+\s*\/\s*\d+$/.test(lower)) return true;
93
+ if (title.length < 8) return true;
94
+ return false;
95
+ }
96
+
97
+ function isActionLabel(text) {
98
+ const lower = normalizeText(text).toLowerCase();
99
+ if (!lower) return true;
100
+ if (ACTION_LABELS.has(lower)) return true;
101
+ if (/(best paper|award|finalist|position paper)/i.test(lower)) return true;
102
+ return false;
103
+ }
104
+
105
+ function findContentContainer(node) {
106
+ let current = node;
107
+ for (let i = 0; i < 8 && current; i += 1) {
108
+ if (current.nodeType !== _deps.NodeType.ELEMENT_NODE) {
109
+ current = current.parentNode ?? null;
110
+ continue;
111
+ }
112
+ const anchors = current.querySelectorAll?.("a[href]") ?? [];
113
+ if (anchors.length >= 1 && anchors.length <= 20) return current;
114
+ current = current.parentNode ?? null;
115
+ }
116
+ return node.parentNode ?? node;
117
+ }
118
+
119
+ function parseDateFromText(text) {
120
+ const normalized = normalizeText(text);
121
+ if (!normalized) return undefined;
122
+
123
+ let m = normalized.match(/\b(20\d{2})[./-](\d{1,2})[./-](\d{1,2})\b/);
124
+ if (m) {
125
+ const [, y, mm, dd] = m;
126
+ const date = new Date(Date.UTC(Number(y), Number(mm) - 1, Number(dd), 12, 0, 0));
127
+ if (!Number.isNaN(date.getTime())) return date;
128
+ }
129
+
130
+ m = normalized.match(/\b(January|February|March|April|May|June|July|August|September|October|November|December)\s*,?\s*(20\d{2})\b/i);
131
+ if (m) {
132
+ const monthMap = {
133
+ january: 0,
134
+ february: 1,
135
+ march: 2,
136
+ april: 3,
137
+ may: 4,
138
+ june: 5,
139
+ july: 6,
140
+ august: 7,
141
+ september: 8,
142
+ october: 9,
143
+ november: 10,
144
+ december: 11,
145
+ };
146
+ const monthIndex = monthMap[m[1].toLowerCase()];
147
+ const year = Number(m[2]);
148
+ const date = new Date(Date.UTC(year, monthIndex, 1, 12, 0, 0));
149
+ if (!Number.isNaN(date.getTime())) return date;
150
+ }
151
+
152
+ m = normalized.match(/\b(20\d{2})\b/);
153
+ if (m) {
154
+ const year = Number(m[1]);
155
+ const date = new Date(Date.UTC(year, 0, 1, 12, 0, 0));
156
+ if (!Number.isNaN(date.getTime())) return date;
157
+ }
158
+ return undefined;
159
+ }
160
+
161
+ function parseDateFromLink(link) {
162
+ try {
163
+ const url = new URL(link);
164
+ if (!/(^|\.)arxiv\.org$/i.test(url.hostname)) return undefined;
165
+ const m = url.pathname.match(/\/abs\/(\d{2})(\d{2})\.\d+/);
166
+ if (!m) return undefined;
167
+ const year = 2000 + Number(m[1]);
168
+ const month = Number(m[2]);
169
+ if (month < 1 || month > 12) return undefined;
170
+ const date = new Date(Date.UTC(year, month - 1, 1, 12, 0, 0));
171
+ return Number.isNaN(date.getTime()) ? undefined : date;
172
+ } catch {
173
+ return undefined;
174
+ }
175
+ }
176
+
177
+ function pickPrimaryLink(container, title, baseUrl) {
178
+ const anchors = container.querySelectorAll("a[href]");
179
+ const candidates = [];
180
+
181
+ for (const anchor of anchors) {
182
+ const text = normalizeText(anchor.textContent);
183
+ if (!text) continue;
184
+ const link = toAbsoluteHttpUrl(anchor.getAttribute("href"), baseUrl);
185
+ if (!link) continue;
186
+ const path = normalizePath(new URL(link).pathname);
187
+ candidates.push({ text, textLower: text.toLowerCase(), link, path });
188
+ }
189
+
190
+ if (candidates.length === 0) return null;
191
+
192
+ const normalizedTitle = normalizeText(title).toLowerCase();
193
+ const titleMatch = candidates.find((item) => item.textLower === normalizedTitle);
194
+ if (titleMatch) return titleMatch.link;
195
+
196
+ const actionMatch = candidates.find((item) => ACTION_LABELS.has(item.textLower));
197
+ if (actionMatch) return actionMatch.link;
198
+
199
+ const nonNav = candidates.find((item) => !NAVIGATION_PATHS.has(item.path));
200
+ return (nonNav ?? candidates[0]).link;
201
+ }
202
+
203
+ function pickSummary(container, title) {
204
+ const texts = [];
205
+ for (const selector of ["i", "p", "h2", "h3", "span"]) {
206
+ for (const node of container.querySelectorAll(selector)) {
207
+ const text = normalizeText(node.textContent);
208
+ if (!text || text === title) continue;
209
+ if (isActionLabel(text)) continue;
210
+ if (/^\d+\s*\/\s*\d+$/.test(text)) continue;
211
+ if (parseDateFromText(text) && text.length <= 24) continue;
212
+ texts.push(text);
213
+ }
214
+ }
215
+
216
+ const unique = [...new Set(texts)];
217
+ return unique.find((text) => text.length >= 20 && text.length <= 400);
218
+ }
219
+
220
+ function extractPubDate(headingNode, container, link) {
221
+ const texts = [];
222
+ const aroundHeading = normalizeText(headingNode.parentNode?.textContent);
223
+ if (aroundHeading) texts.push(aroundHeading);
224
+ const containerText = normalizeText(container.textContent);
225
+ if (containerText) texts.push(containerText);
226
+
227
+ let cursor = container.parentNode ?? null;
228
+ for (let i = 0; i < 3 && cursor; i += 1) {
229
+ const t = normalizeText(cursor.textContent);
230
+ if (t && t.length <= 3000) texts.push(t);
231
+ cursor = cursor.parentNode ?? null;
232
+ }
233
+
234
+ for (const text of texts) {
235
+ const date = parseDateFromText(text);
236
+ if (date) return date;
237
+ }
238
+
239
+ return parseDateFromLink(link) ?? new Date();
240
+ }
241
+
242
+ function toFeedItem({ title, link, pubDate, summary }) {
243
+ return {
244
+ guid: hashGuid(link),
245
+ title,
246
+ link,
247
+ pubDate,
248
+ author: "OpenDriveLab",
249
+ summary: summary || undefined,
250
+ sourceId: SITE_ID,
251
+ };
252
+ }
253
+
254
+ function parseFromHeadings(root, finalUrl, seen) {
255
+ const items = [];
256
+ const headings = root.querySelectorAll("h1, h2, h3");
257
+
258
+ for (const heading of headings) {
259
+ const title = normalizeText(heading.textContent);
260
+ if (isNoiseTitle(title)) continue;
261
+
262
+ const container = findContentContainer(heading);
263
+ const link = pickPrimaryLink(container, title, finalUrl);
264
+ if (!link || seen.has(link)) continue;
265
+
266
+ seen.add(link);
267
+ items.push(
268
+ toFeedItem({
269
+ title,
270
+ link,
271
+ summary: pickSummary(container, title),
272
+ pubDate: extractPubDate(heading, container, link),
273
+ }),
274
+ );
275
+ }
276
+
277
+ return items;
278
+ }
279
+
280
+ function parseFromTitleAnchors(root, finalUrl, seen) {
281
+ const items = [];
282
+ const anchors = root.querySelectorAll("a[href]");
283
+
284
+ for (const anchor of anchors) {
285
+ const title = normalizeText(anchor.textContent);
286
+ if (!title || title.length < 20) continue;
287
+ if (isNoiseTitle(title) || isActionLabel(title)) continue;
288
+
289
+ const link = toAbsoluteHttpUrl(anchor.getAttribute("href"), finalUrl);
290
+ if (!link || seen.has(link)) continue;
291
+
292
+ const path = normalizePath(new URL(link).pathname);
293
+ if (NAVIGATION_PATHS.has(path)) continue;
294
+
295
+ const container = findContentContainer(anchor);
296
+ seen.add(link);
297
+ items.push(
298
+ toFeedItem({
299
+ title,
300
+ link,
301
+ summary: pickSummary(container, title),
302
+ pubDate: extractPubDate(anchor, container, link),
303
+ }),
304
+ );
305
+ }
306
+
307
+ return items;
308
+ }
309
+
310
+ async function fetchItems(sourceId, ctx) {
311
+ _deps = ctx.deps;
312
+ const { html, finalUrl } = await ctx.fetchHtml(sourceId, { waitMs: 4500 });
313
+ const root = _deps.parseHtml(html);
314
+
315
+ const seenLinks = new Set();
316
+ const items = [
317
+ ...parseFromHeadings(root, finalUrl, seenLinks),
318
+ ...parseFromTitleAnchors(root, finalUrl, seenLinks),
319
+ ];
320
+
321
+ if (items.length > 0) return items;
322
+
323
+ if (isBlockedPage(root, html, finalUrl)) {
324
+ throw new Error(`[${SITE_ID}] 命中站点风控验证页,当前会话无法稳定抓取`);
325
+ }
326
+ throw new Error(`[${SITE_ID}] 未解析到首页条目,页面结构可能已变化`);
327
+ }
328
+
329
+ export default {
330
+ id: SITE_ID,
331
+ listUrlPattern: /^https?:\/\/(www\.)?opendrivelab\.com\/?(\?.*)?$/i,
332
+ fetchItems,
333
+ };
@@ -0,0 +1,227 @@
1
+ let _deps;
2
+
3
+
4
+
5
+ const SITE_ID = "paperswithcode";
6
+ const API_ORIGIN = "https://paperswithcode.co";
7
+ const DEFAULT_TRENDING_LIMIT = 30;
8
+ const DEFAULT_MAX_AGE_DAYS = 180;
9
+ const DEFAULT_LATEST_PAGE_SIZE = 30;
10
+
11
+
12
+ function normalizeText(text) {
13
+ return (text ?? "").replace(/\s+/g, " ").trim();
14
+ }
15
+
16
+
17
+ function hashGuid(input) {
18
+ return _deps.createHash("sha256").update(input).digest("hex");
19
+ }
20
+
21
+
22
+ function toValidDate(raw) {
23
+ const text = normalizeText(raw);
24
+ if (!text) return new Date();
25
+ const parsed = new Date(text);
26
+ return Number.isNaN(parsed.getTime()) ? new Date() : parsed;
27
+ }
28
+
29
+
30
+ function parsePositiveInt(raw, fallback, { min = 1, max = 200 } = {}) {
31
+ const n = Number.parseInt(normalizeText(raw), 10);
32
+ if (Number.isNaN(n)) return fallback;
33
+ return Math.min(max, Math.max(min, n));
34
+ }
35
+
36
+
37
+ function resolveMode(sourceUrl) {
38
+ const sort = normalizeText(sourceUrl.searchParams.get("sort")).toLowerCase();
39
+ if (["latest", "new", "date", "date_published"].includes(sort)) return "latest";
40
+ if (["trending", "hot", "popular"].includes(sort)) return "trending";
41
+ return "trending";
42
+ }
43
+
44
+
45
+ function toPaperLink(arxivId) {
46
+ const id = normalizeText(arxivId);
47
+ if (!id) return null;
48
+ return `${API_ORIGIN}/paper/${encodeURIComponent(id)}`;
49
+ }
50
+
51
+
52
+ function joinCategories(tasks) {
53
+ if (!Array.isArray(tasks)) return undefined;
54
+ const values = tasks
55
+ .map((task) => {
56
+ if (typeof task === "string") return normalizeText(task);
57
+ if (task && typeof task === "object") return normalizeText(task.name);
58
+ return "";
59
+ })
60
+ .filter(Boolean);
61
+ return values.length > 0 ? values : undefined;
62
+ }
63
+
64
+
65
+ function buildSummaryFromTrending(item) {
66
+ const repoName = normalizeText(item?.repository?.name);
67
+ const repoOwner = normalizeText(item?.repository?.owner);
68
+ const stars = Number(item?.repository?.num_stars ?? 0);
69
+ const tasks = joinCategories(item?.tasks);
70
+ const parts = [];
71
+
72
+ if (repoOwner && repoName) {
73
+ parts.push(`Repo: ${repoOwner}/${repoName}`);
74
+ }
75
+ if (stars > 0) {
76
+ parts.push(`Stars: ${stars}`);
77
+ }
78
+ if (tasks?.length) {
79
+ parts.push(`Tasks: ${tasks.join(", ")}`);
80
+ }
81
+ return parts.length ? parts.join(" | ") : undefined;
82
+ }
83
+
84
+
85
+ function mapTrendingItem(item) {
86
+ const title = normalizeText(item?.title);
87
+ const link = toPaperLink(item?.arxiv_id);
88
+ if (!title || !link) return null;
89
+
90
+ return {
91
+ guid: hashGuid(link),
92
+ title,
93
+ link,
94
+ pubDate: toValidDate(item?.date_published),
95
+ summary: buildSummaryFromTrending(item),
96
+ sourceId: SITE_ID,
97
+ };
98
+ }
99
+
100
+
101
+ function mapLatestItem(item) {
102
+ const title = normalizeText(item?.title);
103
+ const link = toPaperLink(item?.arxiv_id) || normalizeText(item?.url_abs);
104
+ if (!title || !link) return null;
105
+
106
+ const summary = normalizeText(item?.abstract);
107
+ const firstAuthor = Array.isArray(item?.authors) ? normalizeText(item.authors[0]) : "";
108
+
109
+ return {
110
+ guid: hashGuid(link),
111
+ title,
112
+ link,
113
+ pubDate: toValidDate(item?.published),
114
+ author: firstAuthor || undefined,
115
+ summary: summary || undefined,
116
+ sourceId: SITE_ID,
117
+ };
118
+ }
119
+
120
+
121
+ async function fetchJson(url) {
122
+ const res = await fetch(url, {
123
+ headers: {
124
+ Accept: "application/json",
125
+ "User-Agent":
126
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
127
+ },
128
+ });
129
+ if (!res.ok) {
130
+ throw new Error(`HTTP ${res.status}`);
131
+ }
132
+ try {
133
+ return await res.json();
134
+ } catch {
135
+ throw new Error("接口返回非 JSON 数据");
136
+ }
137
+ }
138
+
139
+
140
+ function dedupeByLink(items) {
141
+ const seen = new Set();
142
+ const output = [];
143
+ for (const item of items) {
144
+ if (!item || !item.link) continue;
145
+ if (seen.has(item.link)) continue;
146
+ seen.add(item.link);
147
+ output.push(item);
148
+ }
149
+ return output;
150
+ }
151
+
152
+
153
+ async function fetchTrendingItems(sourceUrl) {
154
+ const limit = parsePositiveInt(sourceUrl.searchParams.get("limit"), DEFAULT_TRENDING_LIMIT, { max: 100 });
155
+ const maxAgeDays = parsePositiveInt(
156
+ sourceUrl.searchParams.get("max_age_days"),
157
+ DEFAULT_MAX_AGE_DAYS,
158
+ { max: 3650 }
159
+ );
160
+
161
+ const apiUrl = new URL("/api/v1/papers/trending", API_ORIGIN);
162
+ apiUrl.searchParams.set("limit", String(limit));
163
+ apiUrl.searchParams.set("max_age_days", String(maxAgeDays));
164
+
165
+ const payload = await fetchJson(apiUrl);
166
+ const rows = Array.isArray(payload) ? payload : [];
167
+ return dedupeByLink(rows.map(mapTrendingItem).filter(Boolean));
168
+ }
169
+
170
+
171
+ async function fetchLatestItems(sourceUrl) {
172
+ const pageSize = parsePositiveInt(sourceUrl.searchParams.get("page_size"), DEFAULT_LATEST_PAGE_SIZE, {
173
+ max: 100,
174
+ });
175
+ const page = parsePositiveInt(sourceUrl.searchParams.get("page"), 1, { max: 1000 });
176
+
177
+ const apiUrl = new URL("/api/v1/papers/", API_ORIGIN);
178
+ apiUrl.searchParams.set("page", String(page));
179
+ apiUrl.searchParams.set("page_size", String(pageSize));
180
+ apiUrl.searchParams.set("order_by", "date_published");
181
+ apiUrl.searchParams.set("order_dir", "desc");
182
+ apiUrl.searchParams.set("include_resources", "true");
183
+
184
+ const payload = await fetchJson(apiUrl);
185
+ const rows = Array.isArray(payload?.results) ? payload.results : [];
186
+ return dedupeByLink(rows.map(mapLatestItem).filter(Boolean));
187
+ }
188
+
189
+
190
+ async function fetchItems(sourceId, _ctx) {
191
+ _deps = _ctx.deps;
192
+ let sourceUrl;
193
+ try {
194
+ sourceUrl = new URL(sourceId);
195
+ } catch {
196
+ throw new Error(`[${SITE_ID}] 无效 URL: ${sourceId}`);
197
+ }
198
+
199
+ const mode = resolveMode(sourceUrl);
200
+ const errors = [];
201
+
202
+ const tryMode = async (m) => {
203
+ try {
204
+ return m === "latest" ? await fetchLatestItems(sourceUrl) : await fetchTrendingItems(sourceUrl);
205
+ } catch (err) {
206
+ const msg = err instanceof Error ? err.message : String(err);
207
+ errors.push(`${m}: ${msg}`);
208
+ return [];
209
+ }
210
+ };
211
+
212
+ const primary = await tryMode(mode);
213
+ if (primary.length > 0) return primary;
214
+
215
+ const fallbackMode = mode === "trending" ? "latest" : "trending";
216
+ const fallback = await tryMode(fallbackMode);
217
+ if (fallback.length > 0) return fallback;
218
+
219
+ throw new Error(`[${SITE_ID}] 未解析到条目(${errors.join(" | ")})`);
220
+ }
221
+
222
+
223
+ export default {
224
+ id: SITE_ID,
225
+ listUrlPattern: /^https?:\/\/(www\.)?paperswithcode\.(co|com)(?:\/(?:papers)?\/?)?(?:\?.*)?$/i,
226
+ fetchItems,
227
+ };