diff --git a/lib/routes/shu/bksy.ts b/lib/routes/shu/bksy.ts
new file mode 100644
index 000000000000..812d91d572cf
--- /dev/null
+++ b/lib/routes/shu/bksy.ts
@@ -0,0 +1,189 @@
+import { load } from 'cheerio';
+
+import type { Route } from '@/types';
+import cache from '@/utils/cache';
+import got from '@/utils/got';
+import { parseDate } from '@/utils/parse-date';
+import timezone from '@/utils/timezone';
+
+const rootUrl = 'https://bksy.shu.edu.cn';
+const image = 'https://www.shu.edu.cn/__local/0/08/C6/1EABE492B0CF228A5564D6E6ABE_779D1EE3_5BF7.png';
+
+const categories = {
+ notice: {
+ title: '通知公告',
+ path: 'tzgg',
+ },
+ news: {
+ title: '新闻',
+ path: 'xw',
+ },
+};
+
+const alias = new Map([
+ ['tzgg', 'notice'],
+ ['xw', 'news'],
+]);
+
+export const route: Route = {
+ path: '/bksy/:type?',
+ categories: ['university'],
+ example: '/shu/bksy/notice',
+ parameters: { type: '分类,默认为通知公告' },
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['bksy.shu.edu.cn/', 'bksy.shu.edu.cn/index/tzgg.htm', 'bksy.shu.edu.cn/index/xw.htm'],
+ target: '/bksy',
+ },
+ ],
+ name: '本科生院',
+ maintainers: ['tuxinghuan', 'GhhG123'],
+ handler,
+ url: 'bksy.shu.edu.cn/',
+ description: `上海大学教务部已更名为本科生院,旧路由 \`/shu/jwb/:type?\` 会重定向至本路由。
+
+| 通知公告 | 新闻 |
+| -------- | ---- |
+| notice | news |`,
+};
+
+export async function handler(ctx) {
+ const routeType = ctx.req.param('type') || 'notice';
+ const type = alias.get(routeType) || routeType;
+ const category = categories[type] || categories.notice;
+ const link = new URL(`index/${category.path}.htm`, rootUrl).href;
+
+ const response = await got.get(link);
+ const $ = load(response.data);
+
+ const list = $('.only-list li')
+ .slice(0, 10)
+ .toArray()
+ .map((element) => {
+ const item = $(element);
+ const rawLink = item.find('a').attr('href');
+
+ return {
+ title: item.find('a').text().trim(),
+ link: rawLink ? new URL(rawLink, link).href : link,
+ pubDate: timezone(parseDate(item.find('span').text().trim(), 'YYYY年MM月DD日'), +8),
+ };
+ });
+
+ const items = await Promise.all(list.map((item) => cache.tryGet(item.link, async () => await getItemDetail(item))));
+
+ return {
+ title: `上海大学本科生院 - ${category.title}`,
+ description: `上海大学本科生院 - ${category.title}`,
+ link,
+ image,
+ item: items,
+ };
+}
+
+async function getItemDetail(item) {
+ const response = await got.get(item.link);
+ const $ = load(response.data);
+ const content = $('.v_news_content').first();
+
+ normalizeContentUrls($, content, item.link);
+
+ const embeddedFiles = getEmbeddedFiles($, content, item.link);
+ content.find('script').remove();
+ const attachments = getAttachments($, item.link);
+ const description = renderDescription(content.html() || item.title, embeddedFiles, attachments);
+ const enclosure = attachments[0] || embeddedFiles[0];
+
+ item.title = $('[id$=_lblTitle]').first().text().trim() || item.title;
+ item.author = $('[id$=_lblUser]').first().text().trim();
+ const pubDate = $('[id$=_lblFB]').first().text().trim();
+ if (pubDate) {
+ item.pubDate = timezone(parseDate(pubDate), +8);
+ }
+ item.description = description;
+
+ if (enclosure) {
+ item.enclosure_url = enclosure.link;
+ item.enclosure_type = getEnclosureType(enclosure);
+ }
+
+ return item;
+}
+
+function normalizeContentUrls($, content, baseUrl) {
+ content.find('a[href]').each((_, element) => {
+ const href = $(element).attr('href');
+ if (href) {
+ $(element).attr('href', new URL(href, baseUrl).href);
+ }
+ });
+
+ content.find('img[src]').each((_, element) => {
+ const src = $(element).attr('src');
+ if (src) {
+ $(element).attr('src', new URL(src, baseUrl).href);
+ }
+ });
+}
+
+function getEmbeddedFiles($, content, baseUrl) {
+ return content
+ .find('script')
+ .toArray()
+ .flatMap((element) => {
+ const script = $(element).html() || '';
+ return [...script.matchAll(/showVsbpdfIframe\(["']([^"']+)["']/g)].map((match, index) => ({
+ title: index === 0 ? '正文 PDF' : `正文 PDF ${index + 1}`,
+ link: new URL(match[1], baseUrl).href,
+ }));
+ });
+}
+
+function getAttachments($, baseUrl) {
+ return $('form[name="_newscontent_fromname"] ul li a[href]')
+ .toArray()
+ .map((element) => {
+ const attachment = $(element);
+
+ return {
+ title: attachment.text().trim() || attachment.attr('title') || '附件',
+ link: new URL(attachment.attr('href'), baseUrl).href,
+ };
+ });
+}
+
+function renderDescription(description, embeddedFiles, attachments) {
+ const files = [...embeddedFiles, ...attachments];
+ if (files.length === 0) {
+ return description;
+ }
+
+ const fileList = files.map((file) => `
${escapeHtml(file.title)}`).join('');
+ return `${description}
附件
`;
+}
+
+function getEnclosureType(file) {
+ const extension = (file.title.split('.').pop() || new URL(file.link).pathname.split('.').pop() || '').toLowerCase();
+ const mimeTypes = {
+ doc: 'application/msword',
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ pdf: 'application/pdf',
+ xls: 'application/vnd.ms-excel',
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ zip: 'application/zip',
+ };
+
+ return mimeTypes[extension] || 'application/octet-stream';
+}
+
+function escapeHtml(text) {
+ return text.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"');
+}
diff --git a/lib/routes/shu/ces.ts b/lib/routes/shu/ces.ts
new file mode 100644
index 000000000000..e5a18e3d6b68
--- /dev/null
+++ b/lib/routes/shu/ces.ts
@@ -0,0 +1,33 @@
+import type { Route } from '@/types';
+
+import { handler } from './cs';
+
+export const route: Route = {
+ path: '/ces/:type?',
+ categories: ['university'],
+ example: '/shu/ces/zytz',
+ parameters: { type: '分类,默认为重要通知' },
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['cs.shu.edu.cn/', 'cs.shu.edu.cn/index/zytz.htm'],
+ target: '/ces/zytz',
+ },
+ ],
+ name: '计算机工程与科学学院(CES 兼容)',
+ maintainers: ['GhhG123', 'linull24'],
+ handler,
+ url: 'cs.shu.edu.cn/',
+ description: `本路由与 \`/shu/cs/:type?\` 使用同一后端。
+
+| 重要通知 |
+| -------- |
+| zytz |`,
+};
diff --git a/lib/routes/shu/cs.ts b/lib/routes/shu/cs.ts
new file mode 100644
index 000000000000..0dcb24cab22b
--- /dev/null
+++ b/lib/routes/shu/cs.ts
@@ -0,0 +1,192 @@
+import { load } from 'cheerio';
+
+import type { Route } from '@/types';
+import cache from '@/utils/cache';
+import got from '@/utils/got';
+import { parseDate } from '@/utils/parse-date';
+import timezone from '@/utils/timezone';
+
+const rootUrl = 'https://cs.shu.edu.cn';
+const image = 'https://www.shu.edu.cn/__local/0/08/C6/1EABE492B0CF228A5564D6E6ABE_779D1EE3_5BF7.png';
+
+const categories = {
+ zytz: {
+ title: '重要通知',
+ path: 'index/zytz.htm',
+ },
+};
+
+export const route: Route = {
+ path: '/cs/:type?',
+ categories: ['university'],
+ example: '/shu/cs/zytz',
+ parameters: { type: '分类,默认为重要通知' },
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['cs.shu.edu.cn/', 'cs.shu.edu.cn/index/zytz.htm'],
+ target: '/cs/zytz',
+ },
+ ],
+ name: '计算机工程与科学学院',
+ maintainers: ['GhhG123', 'linull24'],
+ handler,
+ url: 'cs.shu.edu.cn/',
+ description: `| 重要通知 |
+| -------- |
+| zytz |`,
+};
+
+export async function handler(ctx) {
+ const type = ctx.req.param('type') || 'zytz';
+ const category = categories[type] || categories.zytz;
+ const link = new URL(category.path, rootUrl).href;
+
+ const response = await got.get(link);
+ const $ = load(response.data);
+ const list = $('ul li, .only-list li, .list li, .news_list li')
+ .toArray()
+ .map((element) => {
+ const item = $(element);
+ const anchor = item.find('a').first();
+ const rawLink = anchor.attr('href');
+ if (!rawLink || !rawLink.includes('/info/')) {
+ return null;
+ }
+
+ const title = anchor.attr('title')?.trim() || item.find('h3, h4, .title, .tit, .bt').first().text().trim() || extractTitle(anchor.text());
+
+ if (!title) {
+ return null;
+ }
+
+ const date = extractDate(item.text());
+
+ return {
+ title,
+ link: new URL(rawLink, link).href,
+ pubDate: timezone(parseDate(date), +8),
+ };
+ })
+ .filter(Boolean)
+ .slice(0, 10);
+
+ const items = await Promise.all(list.map((item) => cache.tryGet(item.link, async () => await getItemDetail(item))));
+
+ return {
+ title: `上海大学计算机工程与科学学院 - ${category.title}`,
+ description: `上海大学计算机工程与科学学院 - ${category.title}`,
+ link,
+ image,
+ item: items,
+ };
+}
+
+async function getItemDetail(item) {
+ const response = await got.get(item.link);
+ const $ = load(response.data);
+ const content = $('.v_news_content, .wp_articlecontent, .article-content, #vsb_content, #vsb_content_2').first();
+
+ normalizeContentUrls($, content, item.link);
+
+ const attachments = getAttachments($, item.link);
+ const description = renderDescription(content.html() || item.title, attachments);
+
+ item.title = $('[id$=_lblTitle], .arti_title, .article-title, h1').first().text().trim() || item.title;
+ item.author = $('[id$=_lblUser], .arti_publisher, .article-author').first().text().trim();
+ const pubDate = $('[id$=_lblFB], .arti_update, .article-date').first().text().trim();
+ if (pubDate) {
+ item.pubDate = timezone(parseDate(pubDate), +8);
+ }
+ item.description = description;
+
+ if (attachments[0]) {
+ item.enclosure_url = attachments[0].link;
+ item.enclosure_type = getEnclosureType(attachments[0]);
+ }
+
+ return item;
+}
+
+function normalizeContentUrls($, content, baseUrl) {
+ content.find('a[href]').each((_, element) => {
+ const href = $(element).attr('href');
+ if (href) {
+ $(element).attr('href', new URL(href, baseUrl).href);
+ }
+ });
+
+ content.find('img[src]').each((_, element) => {
+ const src = $(element).attr('src');
+ if (src) {
+ $(element).attr('src', new URL(src, baseUrl).href);
+ }
+ });
+}
+
+function getAttachments($, baseUrl) {
+ return $('form[name="_newscontent_fromname"] ul li a[href], .v_news_content a[href], .wp_articlecontent a[href], .article-content a[href]')
+ .toArray()
+ .filter((element) => {
+ const href = $(element).attr('href') || '';
+ return /download|attach|附件|\.pdf|\.docx?|\.xlsx?|\.zip/i.test(href + $(element).text());
+ })
+ .map((element) => {
+ const attachment = $(element);
+
+ return {
+ title: attachment.text().trim() || attachment.attr('title') || '附件',
+ link: new URL(attachment.attr('href'), baseUrl).href,
+ };
+ });
+}
+
+function extractTitle(text) {
+ return text
+ .replaceAll(/\s+/g, ' ')
+ .replace(/^\d{1,2}\s+\d{4}[-年./]\d{1,2}(?:[-月./]\d{1,2}日?)?\s*/, '')
+ .trim();
+}
+
+function extractDate(text) {
+ const dayFirst = text.match(/(\d{1,2})\s+(\d{4})-(\d{1,2})/);
+ if (dayFirst) {
+ return `${dayFirst[2]}-${dayFirst[3]}-${dayFirst[1].padStart(2, '0')}`;
+ }
+
+ return text.match(/\d{4}[-年./]\d{1,2}[-月./]\d{1,2}/)?.[0] || '';
+}
+
+function renderDescription(description, attachments) {
+ if (attachments.length === 0) {
+ return description;
+ }
+
+ const fileList = attachments.map((file) => `${escapeHtml(file.title)}`).join('');
+ return `${description}
附件
`;
+}
+
+function getEnclosureType(file) {
+ const extension = (file.title.split('.').pop() || new URL(file.link).pathname.split('.').pop() || '').toLowerCase();
+ const mimeTypes = {
+ doc: 'application/msword',
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ pdf: 'application/pdf',
+ xls: 'application/vnd.ms-excel',
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ zip: 'application/zip',
+ };
+
+ return mimeTypes[extension] || 'application/octet-stream';
+}
+
+function escapeHtml(text) {
+ return text.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"');
+}
diff --git a/lib/routes/shu/jwb.ts b/lib/routes/shu/jwb.ts
index 3c072ccd05ac..3df227459a05 100644
--- a/lib/routes/shu/jwb.ts
+++ b/lib/routes/shu/jwb.ts
@@ -1,65 +1,29 @@
-import { load } from 'cheerio';
-
import type { Route } from '@/types';
-import cache from '@/utils/cache';
-import got from '@/utils/got';
-import { parseDate } from '@/utils/parse-date';
-const host = 'https://jwb.shu.edu.cn/';
-const alias = new Map([
- ['notice', 'tzgg'], // 通知公告
- ['news', 'xw'], // 新闻动态
- /* ['policy', 'zcwj'], 政策文件 //BUG */
-]);
+import { handler } from './bksy';
export const route: Route = {
- path: ['/jwb/:type?'],
+ path: '/jwb/:type?',
+ categories: ['university'],
+ example: '/shu/jwb/notice',
+ parameters: { type: '分类,默认为通知公告' },
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
radar: [
{
- source: ['www.shu.edu.cn/index'],
- target: '/:type?',
+ source: ['jwb.shu.edu.cn/', 'jwb.shu.edu.cn/index/tzgg.htm', 'jwb.shu.edu.cn/index/xw.htm'],
+ target: '/jwb',
},
],
- name: '教务部',
+ name: '教务部(已更名为本科生院)',
maintainers: ['tuxinghuan', 'GhhG123'],
handler,
- description: `| 通知通告 | 新闻 | 政策文件 (bug) |
-| -------- | ---- | -------------- |
-| notice | news | policy |`,
+ url: 'bksy.shu.edu.cn/',
+ description: '上海大学教务部已更名为本科生院,本路由与 `/shu/bksy/:type?` 使用同一后端。',
};
-
-async function handler(ctx) {
- const type = ctx.req.param('type') || 'notice';
- const link = `https://jwb.shu.edu.cn/index/${alias.get(type) || type}.htm`;
- const respond = await got.get(link);
- const $ = load(respond.data);
- const title = $('title').text();
- const list = $('.only-list')
- .find('li')
- .slice(0, 10)
- .toArray()
- .map((ele) => ({
- title: $(ele).find('a').text(),
- link: new URL($(ele).find('a').attr('href'), host).href,
- date: $(ele).children('span').text(),
- }));
-
- const all = await Promise.all(
- list.map((item) =>
- cache.tryGet(item.link, async () => {
- const response = await got.get(item.link);
- const $ = load(response.data);
- item.author = $('[id$=_lblUser]').text().trim();
- item.pubDate = parseDate(item.date, 'YYYY年MM月DD日');
- item.description = $('.v_news_content').html() || item.title;
- return item;
- })
- )
- );
- return {
- title,
- link,
- image: 'https://www.shu.edu.cn/__local/0/08/C6/1EABE492B0CF228A5564D6E6ABE_779D1EE3_5BF7.png',
- item: all,
- };
-}
diff --git a/lib/routes/shu/namespace.ts b/lib/routes/shu/namespace.ts
index 35930c2a2167..d443204c13d0 100644
--- a/lib/routes/shu/namespace.ts
+++ b/lib/routes/shu/namespace.ts
@@ -3,6 +3,6 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '上海大学',
url: 'www.shu.edu.cn',
- description: '上海大学相关网网站',
+ description: '上海大学相关网站',
lang: 'zh-CN',
};