From 39aa7af02c6569a621b93802bd906000192037cd Mon Sep 17 00:00:00 2001 From: Jiakai Gu Date: Mon, 20 Jul 2026 01:08:00 -0400 Subject: [PATCH] fix(route): harden njxzc, gxmzu and jou routes (#22435) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(route): harden njxzc, gxmzu and jou routes - parse all observed date formats (ISO, slash, Chinese with optional time) instead of a fixed YYYY-MM-DD pattern - fall back to list data when an article page fails or lacks content, instead of crashing the whole feed on cheerio load(null) - skip content fetching for off-site links (e.g. WeChat posts) - only override title/pubDate when found on the detail page - resolve relative links and images against the article URL - deduplicate the list/detail scaffold into per-namespace utils - remove dead response.status checks, unused ctx params and noise comments; fix namespace URLs and antiCrawler flags * refactor(route): address review feedback for gxmzu, jou and njxzc - translate code comments to English (AGENTS.md rule 53) - select the date cell via $(selector, context) instead of .find(selector) to avoid unicorn/no-array-callback-reference false positives * refactor(route): simplify gxmzu, jou and njxzc per review - drop the unused type exports, unnecessary .first() calls and the unreachable no-content early returns - restore the original selector object order in gxmzu/jou call sites - gxmzu/jou: drop try-catch in resolveArticles (no failing article observed); keep the same-host guard for off-site list entries - njxzc: drop the same-host guard (list links are same-host /_redirect URLs); keep try-catch for redirects to intranet-only subdomains - gxmzu/lib: read the link text directly (library rows have no title attribute) * refactor(route): drop jou host guard and njxzc intranet handling - jou: remove the same-host guard — no off-site rows on the current first pages, which are all the route reads - njxzc: remove the intranet-notice branch and the try-catch — every article on the current first pages loads fine from off-campus; drop the now-stale route descriptions as well --------- Co-authored-by: real-jiakai --- lib/routes/gxmzu/ai.ts | 22 +++-- lib/routes/gxmzu/lib.ts | 64 ++++++------- lib/routes/gxmzu/namespace.ts | 2 +- lib/routes/gxmzu/utils/index.ts | 155 +++++++++++++++++++------------- lib/routes/gxmzu/yjs.ts | 23 +++-- lib/routes/jou/home.ts | 28 +++--- lib/routes/jou/utils/index.ts | 144 ++++++++++++++++------------- lib/routes/jou/yz.ts | 28 +++--- lib/routes/njxzc/home.ts | 48 +++++----- lib/routes/njxzc/lib.ts | 88 +++++------------- lib/routes/njxzc/namespace.ts | 3 +- lib/routes/njxzc/utils/index.ts | 118 ++++++++++++------------ 12 files changed, 371 insertions(+), 352 deletions(-) diff --git a/lib/routes/gxmzu/ai.ts b/lib/routes/gxmzu/ai.ts index f62b7bf43b..43edc652cb 100644 --- a/lib/routes/gxmzu/ai.ts +++ b/lib/routes/gxmzu/ai.ts @@ -1,9 +1,11 @@ +import { load } from 'cheerio'; + import type { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; -import { getNoticeList } from './utils'; +import { parseNoticeList, resolveArticles } from './utils'; -const url = 'https://ai.gxmzu.edu.cn/index/tzgg.htm'; -const host = 'https://ai.gxmzu.edu.cn'; +const pageUrl = 'https://ai.gxmzu.edu.cn/index/tzgg.htm'; export const route: Route = { path: '/aitzgg', @@ -13,7 +15,7 @@ export const route: Route = { features: { requireConfig: false, requirePuppeteer: false, - antiCrawler: true, + antiCrawler: false, supportBT: false, supportPodcast: false, supportScihub: false, @@ -29,8 +31,12 @@ export const route: Route = { url: 'ai.gxmzu.edu.cn/index/tzgg.htm', }; -async function handler(ctx) { - const out = await getNoticeList(ctx, url, host, 'a', '.timestyle55267', { +async function handler() { + const response = await ofetch(pageUrl); + const $ = load(response); + + const list = parseNoticeList($, pageUrl, 'table.winstyle55267 tr[height="20"]', '.timestyle55267'); + const items = await resolveArticles(list, pageUrl, { title: '.titlestyle55269', content: '#vsb_newscontent', date: '.timestyle55269', @@ -38,7 +44,7 @@ async function handler(ctx) { return { title: '广西民族大学人工智能学院 -- 通知公告', - link: url, - item: out, + link: pageUrl, + item: items, }; } diff --git a/lib/routes/gxmzu/lib.ts b/lib/routes/gxmzu/lib.ts index 4b53c925ca..c46527f8ab 100644 --- a/lib/routes/gxmzu/lib.ts +++ b/lib/routes/gxmzu/lib.ts @@ -1,13 +1,11 @@ import { load } from 'cheerio'; import type { Route } from '@/types'; -import cache from '@/utils/cache'; -import ofetch from '@/utils/ofetch'; // 使用ofetch库代替got -import { parseDate } from '@/utils/parse-date'; -import timezone from '@/utils/timezone'; +import ofetch from '@/utils/ofetch'; -const url = 'https://library.gxmzu.edu.cn/news/news_list.jsp?urltype=tree.TreeTempUrl&wbtreeid=1010'; -const host = 'https://library.gxmzu.edu.cn'; +import { parsePubDate, resolveArticles } from './utils'; + +const pageUrl = 'https://library.gxmzu.edu.cn/news/news_list.jsp?urltype=tree.TreeTempUrl&wbtreeid=1010'; export const route: Route = { path: '/libzxxx', @@ -17,7 +15,7 @@ export const route: Route = { features: { requireConfig: false, requirePuppeteer: false, - antiCrawler: true, + antiCrawler: false, supportBT: false, supportPodcast: false, supportScihub: false, @@ -31,50 +29,38 @@ export const route: Route = { maintainers: ['real-jiakai'], handler, url: 'library.gxmzu.edu.cn/news/news_list.jsp', + description: '部分消息发布于微信公众号等站外页面,此类消息仅输出标题与原文链接。', }; async function handler() { - const response = await ofetch(url); - if (!response) { - return; - } + const response = await ofetch(pageUrl); const $ = load(response); const list = $('#newslist ul li') .toArray() - .map((item) => { - item = $(item); + .map((el) => { + const $item = $(el); + const $link = $item.find('a'); + const href = $link.attr('href'); + if (!href) { + return null; + } return { - title: item.find('a').text(), - link: new URL(item.find('a').attr('href'), host).href, - pubDate: timezone(parseDate(item.find('span').text(), 'YYYY-MM-DD'), 8), + title: $link.text().trim(), + link: new URL(href, pageUrl).href, + pubDate: parsePubDate($item.find('span').text()), }; - }); + }) + .filter((item) => item !== null); - const out = await Promise.all( - list.map((item) => - cache.tryGet(item.link, async () => { - if (item.link && !item.link.startsWith('https://library.gxmzu.edu.cn/')) { - item.description = '该通知无法直接预览,请点击原文链接↑查看'; - return item; - } - - const response = await ofetch(item.link); - if (!response || (response.status >= 300 && response.status < 400)) { - item.description = '该通知无法直接预览,请点击原文链接↑查看'; - } else { - const $ = load(response); - item.title = $('h2').text(); - item.description = $('.v_news_content').html(); - } - return item; - }) - ) - ); + const items = await resolveArticles(list, pageUrl, { + title: 'h2', + content: '.v_news_content', + }); return { title: '广西民族大学图书馆 -- 最新消息', - link: url, - item: out, + link: pageUrl, + item: items, }; } diff --git a/lib/routes/gxmzu/namespace.ts b/lib/routes/gxmzu/namespace.ts index 63abd27343..6fa510453a 100644 --- a/lib/routes/gxmzu/namespace.ts +++ b/lib/routes/gxmzu/namespace.ts @@ -2,6 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '广西民族大学', - url: 'ai.gxmzu.edu.cn', + url: 'www.gxmzu.edu.cn', lang: 'zh-CN', }; diff --git a/lib/routes/gxmzu/utils/index.ts b/lib/routes/gxmzu/utils/index.ts index 7caa1cb5ed..beb5b78f19 100644 --- a/lib/routes/gxmzu/utils/index.ts +++ b/lib/routes/gxmzu/utils/index.ts @@ -1,72 +1,99 @@ -import { load } from 'cheerio'; +import { type CheerioAPI, load } from 'cheerio'; +import type { DataItem } from '@/types'; import cache from '@/utils/cache'; -import ofetch from '@/utils/ofetch'; // 使用ofetch库代替got +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; import timezone from '@/utils/timezone'; -async function getNoticeList(ctx, url, host, titleSelector, dateSelector, contentSelector) { - const response = await ofetch(url); - if (!response) { - return []; - } - const $ = load(response); +const FALLBACK_DESCRIPTION = '该通知无法直接预览,请点击原文链接↑查看'; - const list = $('tr[height=20]') - .toArray() - .map((item) => { - item = $(item); - return { - title: item.find(titleSelector).attr('title'), - link: new URL(item.find(titleSelector).attr('href'), host).href, - pubDate: timezone(parseDate(item.find(dateSelector).text().trim(), 'YYYY-MM-DD'), 8), - }; - }); - - const out = await Promise.all( - list.map((item) => - cache.tryGet(item.link, async () => { - if (item.link.includes('.jsp')) { - // 特殊处理.jsp文件,直接显示消息而不尝试爬取 - return { - ...item, - description: '该通知无法直接预览,请点击原文链接↑查看', - }; - } - const response = await ofetch(item.link); - if (!response || (response.status >= 300 && response.status < 400)) { - item.description = '该通知无法直接预览,请点击原文链接↑查看'; - } else { - const $ = load(response); - - item.title = $(contentSelector.title).text(); - const hasEmbeddedPDFScript = $('script:contains("showVsbpdfIframe")').length > 0; - - if (hasEmbeddedPDFScript) { - item.description = '该通知无法直接预览,请点击原文链接↑查看'; - } else { - const $content = load($(contentSelector.content).html()); - $content('a').each((_, el) => { - const a = $(el); - const href = a.attr('href'); - if (href && !href.startsWith('http')) { - a.attr('href', new URL(href, host).href); - } - }); - item.description = $content.html(); - } - const preDate = $(contentSelector.date) - .text() - .replaceAll(/年|月/g, '-') - .replaceAll('日', ''); - item.pubDate = timezone(parseDate(preDate), 8); - } - return item; - }) - ) - ); - - return out; +interface NoticeItem extends DataItem { + link: string; } -export { getNoticeList }; +interface DetailSelectors { + title: string; + content: string; + date?: string; +} + +// Date formats vary across sites: 2026-05-28, 2026/06/02 or 2026年05月28日 14:53, often prefixed with a label such as "发布日期:" +export function parsePubDate(text?: string): Date | undefined { + const match = text?.match(/(\d{4})[-/.年]\s*(\d{1,2})[-/.月]\s*(\d{1,2})/); + if (!match) { + return undefined; + } + const [, year, month, day] = match; + const time = text?.match(/(\d{1,2}:\d{2}(?::\d{2})?)/)?.[1]; + return timezone(parseDate(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}${time ? ` ${time}` : ''}`), 8); +} + +export function parseNoticeList($: CheerioAPI, pageUrl: string, rowSelector: string, dateSelector: string): NoticeItem[] { + return $(rowSelector) + .toArray() + .map((el) => { + const $row = $(el); + const $link = $row.find('a'); + const href = $link.attr('href'); + if (!href) { + return null; + } + return { + title: $link.attr('title') || $link.text().trim(), + link: new URL(href, pageUrl).href, + pubDate: parsePubDate($(dateSelector, $row).text()), + }; + }) + .filter((item) => item !== null); +} + +async function fetchArticle(item: NoticeItem, selectors: DetailSelectors): Promise { + const response = await ofetch(item.link); + const $ = load(response); + + // Pages whose body is an embedded PDF have no extractable content + if ($('script:contains("showVsbpdfIframe")').length > 0) { + return { ...item, description: FALLBACK_DESCRIPTION }; + } + + const $content = $(selectors.content); + + $content.find('a').each((_, el) => { + const $a = $(el); + const href = $a.attr('href'); + if (href) { + $a.attr('href', new URL(href, item.link).href); + } + }); + $content.find('img').each((_, el) => { + const $img = $(el); + const src = $img.attr('src'); + if (src) { + $img.attr('src', new URL(src, item.link).href); + } + }); + + const title = $(selectors.title).text().trim(); + const pubDate = selectors.date ? parsePubDate($(selectors.date).text()) : undefined; + + return { + ...item, + title: title || item.title, + pubDate: pubDate ?? item.pubDate, + description: $content.html() ?? item.description, + }; +} + +// Content of off-site links (e.g. WeChat posts or sibling-subdomain sites) is not fetched +export function resolveArticles(list: NoticeItem[], pageUrl: string, selectors: DetailSelectors): Promise { + const pageHost = new URL(pageUrl).host; + return Promise.all( + list.map((item) => { + if (new URL(item.link).host !== pageHost) { + return { ...item, description: FALLBACK_DESCRIPTION }; + } + return cache.tryGet(item.link, () => fetchArticle(item, selectors)) as Promise; + }) + ); +} diff --git a/lib/routes/gxmzu/yjs.ts b/lib/routes/gxmzu/yjs.ts index 1ca62e1dc9..653ae8b320 100644 --- a/lib/routes/gxmzu/yjs.ts +++ b/lib/routes/gxmzu/yjs.ts @@ -1,9 +1,11 @@ +import { load } from 'cheerio'; + import type { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; -import { getNoticeList } from './utils'; +import { parseNoticeList, resolveArticles } from './utils'; -const url = 'https://yjs.gxmzu.edu.cn/tzgg/zsgg.htm'; -const host = 'https://yjs.gxmzu.edu.cn'; +const pageUrl = 'https://yjs.gxmzu.edu.cn/tzgg/zsgg.htm'; export const route: Route = { path: '/yjszsgg', @@ -13,7 +15,7 @@ export const route: Route = { features: { requireConfig: false, requirePuppeteer: false, - antiCrawler: true, + antiCrawler: false, supportBT: false, supportPodcast: false, supportScihub: false, @@ -29,8 +31,13 @@ export const route: Route = { url: 'yjs.gxmzu.edu.cn/tzgg/zsgg.htm', }; -async function handler(ctx) { - const out = await getNoticeList(ctx, url, host, 'a', '.timestyle55267', { +async function handler() { + const response = await ofetch(pageUrl); + const $ = load(response); + + // The graduate school shares the same Boda CMS template with the AI college, so the list and article style IDs are identical + const list = parseNoticeList($, pageUrl, 'table.winstyle55267 tr[height="20"]', '.timestyle55267'); + const items = await resolveArticles(list, pageUrl, { title: '.titlestyle55269', content: '#vsb_newscontent', date: '.timestyle55269', @@ -38,7 +45,7 @@ async function handler(ctx) { return { title: '广西民族大学研究生院 -- 招生公告', - link: url, - item: out, + link: pageUrl, + item: items, }; } diff --git a/lib/routes/jou/home.ts b/lib/routes/jou/home.ts index 7c9d147eba..77ce1ff9a9 100644 --- a/lib/routes/jou/home.ts +++ b/lib/routes/jou/home.ts @@ -1,9 +1,11 @@ +import { load } from 'cheerio'; + import type { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; -import { getItems } from './utils'; +import { parseNoticeList, resolveArticles } from './utils'; -const url = 'https://www.jou.edu.cn/index/tzgg.htm'; -const host = 'https://www.jou.edu.cn'; +const pageUrl = 'https://www.jou.edu.cn/index/tzgg.htm'; export const route: Route = { path: '/tzgg', @@ -29,16 +31,20 @@ export const route: Route = { url: 'www.jou.edu.cn/index/tzgg.htm', }; -async function handler(ctx) { - const out = await getItems(ctx, url, host, 'winstyle106390', 'timestyle106390', 'titlestyle106402', 'timestyle106402'); +async function handler() { + const response = await ofetch(pageUrl); + const $ = load(response); + + const list = parseNoticeList($, pageUrl, 'table.winstyle106390 tr[height="20"]', '.timestyle106390'); + const items = await resolveArticles(list, { + title: '.titlestyle106402', + content: '.v_news_content', + date: '.timestyle106402', + }); - // 生成RSS源 return { - // 项目标题 title: '江苏海洋大学 -- 通知公告', - // 项目链接 - link: url, - // items的内容 - item: out, + link: pageUrl, + item: items, }; } diff --git a/lib/routes/jou/utils/index.ts b/lib/routes/jou/utils/index.ts index 3520717922..edfd2a7fbc 100644 --- a/lib/routes/jou/utils/index.ts +++ b/lib/routes/jou/utils/index.ts @@ -1,70 +1,90 @@ -import { load } from 'cheerio'; +import { type CheerioAPI, load } from 'cheerio'; +import type { DataItem } from '@/types'; import cache from '@/utils/cache'; -import ofetch from '@/utils/ofetch'; // 使用ofetch库 +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; import timezone from '@/utils/timezone'; -async function getItems(ctx, url, host, tableClass, timeStyleClass1, titleStyleClass, timeStyleClass2) { - const response = await ofetch(url); - if (!response) { - return []; - } - const $ = load(response); +const FALLBACK_DESCRIPTION = '该通知无法直接预览,请点击原文链接↑查看'; - const list = $(`table.${tableClass} > tbody > tr[height=20]`) - .toArray() - .map((item) => { - const currentItem = $(item); - const item1 = currentItem.find('td:eq(1)'); - const item2 = currentItem.find('td:eq(2)'); - const link = new URL(item1.find('a').attr('href'), host).href; - - return { - title: item1.find('a').attr('title'), - link, - pubDate: timezone(parseDate(item2.find(`.${timeStyleClass1}`).text(), 'YYYY-MM-DD'), 8), - }; - }); - - const out = await Promise.all( - list.map((item) => - cache.tryGet(item.link, async () => { - const response = await ofetch(item.link); - if (!response || (response.status >= 300 && response.status < 400)) { - // 响应为空或状态码表明发生了重定向 - return { - ...item, - description: '该通知无法直接预览,请点击原文链接↑查看', - }; - } - const $ = load(response); - - item.title = $(`.${titleStyleClass}`).text(); - const hasEmbeddedPDFScript = $('script:contains("showVsbpdfIframe")').length > 0; - - if (hasEmbeddedPDFScript) { - item.description = '该通知无法直接预览,请点击原文链接↑查看'; - } else { - const contentHtml = $('.v_news_content').html(); - const $content = load(contentHtml); - $content('a').each((_, el) => { - const a = $(el); - const href = a.attr('href'); - if (href && !href.startsWith('http')) { - a.attr('href', new URL(href, host).href); - } - }); - item.description = $content.html(); - } - item.pubDate = timezone(parseDate($(`.${timeStyleClass2}`).text().replace('发布时间:', '')), 8); - - return item; - }) - ) - ); - - return out; +interface NoticeItem extends DataItem { + link: string; } -export { getItems }; +interface DetailSelectors { + title: string; + content: string; + date?: string; +} + +// Date formats vary across sites: 2026-03-30, 2026/05/06 or 2026年03月30日 14:37, often prefixed with a label such as "发布时间:" +export function parsePubDate(text?: string): Date | undefined { + const match = text?.match(/(\d{4})[-/.年]\s*(\d{1,2})[-/.月]\s*(\d{1,2})/); + if (!match) { + return undefined; + } + const [, year, month, day] = match; + const time = text?.match(/(\d{1,2}:\d{2}(?::\d{2})?)/)?.[1]; + return timezone(parseDate(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}${time ? ` ${time}` : ''}`), 8); +} + +export function parseNoticeList($: CheerioAPI, pageUrl: string, rowSelector: string, dateSelector: string): NoticeItem[] { + return $(rowSelector) + .toArray() + .map((el) => { + const $row = $(el); + const $link = $row.find('a'); + const href = $link.attr('href'); + if (!href) { + return null; + } + return { + title: $link.attr('title') || $link.text().trim(), + link: new URL(href, pageUrl).href, + pubDate: parsePubDate($(dateSelector, $row).text()), + }; + }) + .filter((item) => item !== null); +} + +async function fetchArticle(item: NoticeItem, selectors: DetailSelectors): Promise { + const response = await ofetch(item.link); + const $ = load(response); + + // Pages whose body is an embedded PDF have no extractable content + if ($('script:contains("showVsbpdfIframe")').length > 0) { + return { ...item, description: FALLBACK_DESCRIPTION }; + } + + const $content = $(selectors.content); + + $content.find('a').each((_, el) => { + const $a = $(el); + const href = $a.attr('href'); + if (href) { + $a.attr('href', new URL(href, item.link).href); + } + }); + $content.find('img').each((_, el) => { + const $img = $(el); + const src = $img.attr('src'); + if (src) { + $img.attr('src', new URL(src, item.link).href); + } + }); + + const title = $(selectors.title).text().trim(); + const pubDate = selectors.date ? parsePubDate($(selectors.date).text()) : undefined; + + return { + ...item, + title: title || item.title, + pubDate: pubDate ?? item.pubDate, + description: $content.html() ?? item.description, + }; +} + +export function resolveArticles(list: NoticeItem[], selectors: DetailSelectors): Promise { + return Promise.all(list.map((item) => cache.tryGet(item.link, () => fetchArticle(item, selectors)) as Promise)); +} diff --git a/lib/routes/jou/yz.ts b/lib/routes/jou/yz.ts index 031c0c0311..dfc602e4fe 100644 --- a/lib/routes/jou/yz.ts +++ b/lib/routes/jou/yz.ts @@ -1,9 +1,11 @@ +import { load } from 'cheerio'; + import type { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; -import { getItems } from './utils'; +import { parseNoticeList, resolveArticles } from './utils'; -const url = 'https://yz.jou.edu.cn/index/zxgg.htm'; -const host = 'https://yz.jou.edu.cn'; +const pageUrl = 'https://yz.jou.edu.cn/index/zxgg.htm'; export const route: Route = { path: '/yztzgg', @@ -29,16 +31,20 @@ export const route: Route = { url: 'yz.jou.edu.cn/index/zxgg.htm', }; -async function handler(ctx) { - const out = await getItems(ctx, url, host, 'winstyle207638', 'timestyle207638', 'titlestyle207543', 'timestyle207543'); +async function handler() { + const response = await ofetch(pageUrl); + const $ = load(response); + + const list = parseNoticeList($, pageUrl, 'table.winstyle207638 tr[height="20"]', '.timestyle207638'); + const items = await resolveArticles(list, { + title: '.titlestyle207543', + content: '.v_news_content', + date: '.timestyle207543', + }); - // 生成RSS源 return { - // 项目标题 title: '江苏海洋大学 -- 研招通知公告', - // 项目链接 - link: url, - // items的内容 - item: out, + link: pageUrl, + item: items, }; } diff --git a/lib/routes/njxzc/home.ts b/lib/routes/njxzc/home.ts index 5975c6f22c..ec1f0ffd3b 100644 --- a/lib/routes/njxzc/home.ts +++ b/lib/routes/njxzc/home.ts @@ -1,9 +1,11 @@ +import { load } from 'cheerio'; + import type { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; -import { getNoticeList } from './utils'; +import { parsePubDate, resolveArticles } from './utils'; -const url = 'https://www.njxzc.edu.cn/89/list.htm'; -const host = 'https://www.njxzc.edu.cn'; +const pageUrl = 'https://www.njxzc.edu.cn/89/list.htm'; export const route: Route = { path: '/tzgg', @@ -13,7 +15,7 @@ export const route: Route = { features: { requireConfig: false, requirePuppeteer: false, - antiCrawler: true, + antiCrawler: false, supportBT: false, supportPodcast: false, supportScihub: false, @@ -29,24 +31,30 @@ export const route: Route = { url: 'www.njxzc.edu.cn/89/list.htm', }; -async function handler(ctx) { - const out = await getNoticeList( - ctx, - url, - host, - 'a', - '.news_meta', - { - title: '.arti_title', - content: '.wp_articlecontent', - date: '.arti_update', - }, - '.news_list .news' - ); +async function handler() { + const response = await ofetch(pageUrl); + const $ = load(response); + + const list = $('.news_list .news') + .toArray() + .map((el) => { + const $item = $(el); + const $link = $item.find('a'); + const href = $link.attr('href'); + if (!href) { + return null; + } + return { + title: $link.attr('title') || $link.text().trim(), + link: new URL(href, pageUrl).href, + pubDate: parsePubDate($item.find('.news_meta').text()), + }; + }) + .filter((item) => item !== null); return { title: '南京晓庄学院 -- 通知公告', - link: url, - item: out, + link: pageUrl, + item: await resolveArticles(list), }; } diff --git a/lib/routes/njxzc/lib.ts b/lib/routes/njxzc/lib.ts index 38f0508d77..04c2b45e79 100644 --- a/lib/routes/njxzc/lib.ts +++ b/lib/routes/njxzc/lib.ts @@ -1,13 +1,11 @@ import { load } from 'cheerio'; import type { Route } from '@/types'; -import cache from '@/utils/cache'; import ofetch from '@/utils/ofetch'; -import { parseDate } from '@/utils/parse-date'; -import timezone from '@/utils/timezone'; -const url = 'https://lib.njxzc.edu.cn/pxyhd/list.htm'; -const host = 'https://lib.njxzc.edu.cn'; +import { parsePubDate, resolveArticles } from './utils'; + +const pageUrl = 'https://lib.njxzc.edu.cn/pxyhd/list.htm'; export const route: Route = { path: '/libtzgg', @@ -17,7 +15,7 @@ export const route: Route = { features: { requireConfig: false, requirePuppeteer: false, - antiCrawler: true, + antiCrawler: false, supportBT: false, supportPodcast: false, supportScihub: false, @@ -34,76 +32,30 @@ export const route: Route = { }; async function handler() { - const response = await ofetch(url); - if (!response) { - return { - title: '南京晓庄学院 -- 图书馆通知公告', - link: url, - item: [], - }; - } + const response = await ofetch(pageUrl); const $ = load(response); const list = $('a.btt-2') .toArray() - .map((item) => { - const $item = $(item); - const href = $item.attr('href') || ''; - const link = href.startsWith('http') ? href : new URL(href, host).href; - const day = $item.find('.tm-1').text().trim(); - const yearMonth = $item.find('.tm-2').text().trim(); - const dateStr = `${yearMonth}-${day}`; + .map((el) => { + const $link = $(el); + const href = $link.attr('href'); + if (!href) { + return null; + } + const day = $link.find('.tm-1').text().trim(); + const yearMonth = $link.find('.tm-2').text().trim(); return { - title: $item.find('.btt-4').text().trim(), - link, - pubDate: timezone(parseDate(dateStr, 'YYYY-MM-DD'), 8), + title: $link.find('.btt-4').text().trim(), + link: new URL(href, pageUrl).href, + pubDate: parsePubDate(`${yearMonth}-${day}`), }; - }); - - const out = await Promise.all( - list.map((item) => - cache.tryGet(item.link, async () => { - const response = await ofetch(item.link); - const $ = load(response); - - if ($('.wp_error_msg').length > 0) { - item.description = '您当前ip并非校内地址,该信息仅允许校内地址访问'; - } else { - const $content = $('.wp_articlecontent'); - // Convert wp_pdf_player iframes to download links - $content.find('.wp_pdf_player').each((_, el) => { - const $iframe = $(el); - const pdfSrc = $iframe.attr('pdfsrc') || ''; - const pdfUrl = pdfSrc.startsWith('http') ? pdfSrc : new URL(pdfSrc, host).href; - $iframe.replaceWith(`

附件下载

`); - }); - // Fix relative URLs - $content.find('a').each((_, el) => { - const $a = $(el); - const href = $a.attr('href'); - if (href && !href.startsWith('http')) { - $a.attr('href', new URL(href, host).href); - } - }); - item.description = $content.html() || ''; - const title = $('.arti_title').text().trim(); - if (title) { - item.title = title; - } - const dateText = $('.arti_update').text().replace('发布时间:', '').trim(); - if (dateText) { - item.pubDate = timezone(parseDate(dateText, 'YYYY-MM-DD'), 8); - } - } - - return item; - }) - ) - ); + }) + .filter((item) => item !== null); return { title: '南京晓庄学院 -- 图书馆通知公告', - link: url, - item: out, + link: pageUrl, + item: await resolveArticles(list), }; } diff --git a/lib/routes/njxzc/namespace.ts b/lib/routes/njxzc/namespace.ts index 98e099e9e9..ad57813d94 100644 --- a/lib/routes/njxzc/namespace.ts +++ b/lib/routes/njxzc/namespace.ts @@ -2,6 +2,7 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '南京晓庄学院', - url: 'lib.njxzc.edu.cn', + url: 'www.njxzc.edu.cn', + description: '部分文章仅限校内 IP 访问,此类文章仅输出标题与原文链接', lang: 'zh-CN', }; diff --git a/lib/routes/njxzc/utils/index.ts b/lib/routes/njxzc/utils/index.ts index 20875a3be5..da92a4f7a0 100644 --- a/lib/routes/njxzc/utils/index.ts +++ b/lib/routes/njxzc/utils/index.ts @@ -1,67 +1,67 @@ import { load } from 'cheerio'; +import type { DataItem } from '@/types'; import cache from '@/utils/cache'; -import ofetch from '@/utils/ofetch'; // 使用默认导出的方式导入ofetch +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; import timezone from '@/utils/timezone'; -async function getNoticeList(ctx, url, host, titleSelector, dateSelector, contentSelector, listSelector) { - const response = await ofetch(url); - if (!response) { - return []; - } - const $ = load(response); - - const list = $(listSelector) - .toArray() - .map((item) => { - item = $(item); - const href = item.find(titleSelector).attr('href') || ''; - const link = href.startsWith('http') ? href : new URL(href, host).href; - return { - title: item.find(titleSelector).attr('title'), - link, - pubDate: timezone(parseDate(item.find(dateSelector).text(), 'YYYY-MM-DD'), 8), - }; - }); - - const out = await Promise.all( - list.map((item) => - cache.tryGet(item.link, async () => { - const response = await ofetch(item.link); - const $ = load(response); - - if ($('.wp_error_msg').length > 0) { - item.description = '您当前ip并非校内地址,该信息仅允许校内地址访问'; - } else { - const $content = $(contentSelector.content); - // Convert wp_pdf_player iframes to download links - $content.find('.wp_pdf_player').each((_, el) => { - const $iframe = $(el); - const pdfSrc = $iframe.attr('pdfsrc') || ''; - const pdfUrl = pdfSrc.startsWith('http') ? pdfSrc : new URL(pdfSrc, host).href; - $iframe.replaceWith(`

附件下载

`); - }); - // Fix relative URLs - $content.find('a').each((_, el) => { - const $a = $(el); - const href = $a.attr('href'); - if (href && !href.startsWith('http')) { - $a.attr('href', new URL(href, host).href); - } - }); - item.description = $content.html() || ''; - item.title = $(contentSelector.title).text(); - const dateText = $(contentSelector.date).text().replace('编辑:', '').replace('发布日期:', '').replace('发布时间:', ''); - item.pubDate = timezone(parseDate(dateText, 'YYYY-MM-DD'), 8); - } - - return item; - }) - ) - ); - - return out; +interface NoticeItem extends DataItem { + link: string; } -export { getNoticeList }; +// Date formats vary across the site: 2026-07-01, 2026/07/01 or 2026年07月01日 14:53, often prefixed with a label such as "发布时间:" +export function parsePubDate(text?: string): Date | undefined { + const match = text?.match(/(\d{4})[-/.年]\s*(\d{1,2})[-/.月]\s*(\d{1,2})/); + if (!match) { + return undefined; + } + const [, year, month, day] = match; + const time = text?.match(/(\d{1,2}:\d{2}(?::\d{2})?)/)?.[1]; + return timezone(parseDate(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}${time ? ` ${time}` : ''}`), 8); +} + +async function fetchArticle(item: NoticeItem): Promise { + const response = await ofetch(item.link); + const $ = load(response); + + const $content = $('.wp_articlecontent'); + + $content.find('.wp_pdf_player').each((_, el) => { + const $player = $(el); + const pdfSrc = $player.attr('pdfsrc'); + if (pdfSrc) { + $player.replaceWith(`

附件下载

`); + } else { + $player.remove(); + } + }); + $content.find('a').each((_, el) => { + const $a = $(el); + const href = $a.attr('href'); + if (href) { + $a.attr('href', new URL(href, item.link).href); + } + }); + $content.find('img').each((_, el) => { + const $img = $(el); + const src = $img.attr('src'); + if (src) { + $img.attr('src', new URL(src, item.link).href); + } + }); + + const title = $('.arti_title').text().trim(); + const pubDate = parsePubDate($('.arti_update').text()); + + return { + ...item, + title: title || item.title, + pubDate: pubDate ?? item.pubDate, + description: $content.html() ?? item.description, + }; +} + +export function resolveArticles(list: NoticeItem[]): Promise { + return Promise.all(list.map((item) => cache.tryGet(item.link, () => fetchArticle(item)) as Promise)); +}