From 14d49eea386fc1571e80b88c46dde9cd5113344c Mon Sep 17 00:00:00 2001 From: karasu Date: Sat, 23 Nov 2024 00:29:14 +0800 Subject: [PATCH] fix(route): linkresearcher (#17681) * fix(route): linkresearcher * Update lib/routes/linkresearcher/index.ts Co-authored-by: Tony * Update lib/routes/linkresearcher/index.ts Co-authored-by: Tony * Update lib/routes/linkresearcher/index.ts Co-authored-by: Tony * Update lib/routes/linkresearcher/namespace.ts Co-authored-by: Tony * feat: bilingual support * feat: add author and doi --------- --- lib/routes/linkresearcher/index.ts | 154 ++++++++++++------ lib/routes/linkresearcher/namespace.ts | 10 +- .../linkresearcher/templates/bilingual.art | 7 + lib/routes/linkresearcher/types.ts | 103 ++++++++++++ 4 files changed, 226 insertions(+), 48 deletions(-) create mode 100644 lib/routes/linkresearcher/templates/bilingual.art create mode 100644 lib/routes/linkresearcher/types.ts diff --git a/lib/routes/linkresearcher/index.ts b/lib/routes/linkresearcher/index.ts index 2f28096de2bad4..65da513d2f3c71 100644 --- a/lib/routes/linkresearcher/index.ts +++ b/lib/routes/linkresearcher/index.ts @@ -1,76 +1,138 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; -import qs from 'query-string'; +import { ViewType, type Data, type DataItem, type Route } from '@/types'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import crypto from 'crypto'; +import type { Context } from 'hono'; +import type { DetailResponse, SearchResultItem } from './types'; +import cache from '@/utils/cache'; +import { getCurrentPath } from '@/utils/helpers'; +import { art } from '@/utils/render'; +import path from 'node:path'; + +const __dirname = getCurrentPath(import.meta.url); +const templatePath = path.join(__dirname, 'templates/bilingual.art'); const baseURL = 'https://www.linkresearcher.com'; +const apiURL = `${baseURL}/api`; export const route: Route = { + name: 'Articles', path: '/:params', - name: 'Unknown', - maintainers: ['y9c'], + example: '/linkresearcher/category=theses&columns=Nature%20导读&subject=生物', + maintainers: ['y9c', 'KarasuShin'], handler, + view: ViewType.Articles, + categories: ['journal'], + parameters: { + params: { + description: 'search parameters, support `category`, `subject`, `columns`, `query`', + }, + }, + zh: { + name: '文章', + }, + 'zh-TW': { + name: '文章', + }, }; -async function handler(ctx) { - // parse params +async function handler(ctx: Context): Promise { + const categoryMap = { theses: '论文', information: '新闻', careers: '职业' } as const; const params = ctx.req.param('params'); - const query = qs.parse(params); - - const categoryMap = { theses: '论文', information: '新闻', careers: '职业' }; - const category = query.category; - let title = categoryMap[category]; - - // get XSRF token from main page - const metaURL = `${baseURL}/${category}`; - const metaResponse = await got(metaURL); - const xsrfToken = metaResponse.headers['set-cookie'][0].split(';')[0].split('=')[1]; - - let data = { filters: { status: false } }; - if (query.subject !== undefined && query.columns !== undefined) { - data = { filters: { status: true, subject: query.subject, columns: query.columns } }; - title = `${title}「${query.subject} & ${query.columns}」`; - } else if (query.subject !== undefined && query.columns === undefined) { - data = { filters: { status: true, subject: query.subject } }; - title = `${title}「${query.subject}」`; - } else if (query.subject === undefined && query.columns !== undefined) { - data = { filters: { status: true, columns: query.columns } }; - title = `${title}「${query.columns}」`; + const filters = new URLSearchParams(params); + + const subject = filters.get('subject'); + const columns = filters.get('columns'); + const query = filters.get('query') ?? ''; + const category = filters.get('category') ?? ('theses' as keyof typeof categoryMap); + + if (!(category in categoryMap)) { + throw new InvalidParameterError('Invalid category'); + } + let title = categoryMap[category] as string; + + const token = crypto.randomUUID(); + + const data: { + filters: { + status: boolean; + subject?: string; + columns?: string; + }; + } = { filters: { status: true } }; + + if (subject) { + data.filters.subject = subject; + title = `${title}「${subject}」`; } - data.query = query.query; + + if (columns) { + data.filters.columns = columns; + title = `${title}「${columns}」`; + } + const dataURL = `${baseURL}/api/${category === 'careers' ? 'articles' : category}/search`; - const pageResponse = await got.post(dataURL, { + const pageResponse = await ofetch<{ + hits: SearchResultItem[]; + }>(dataURL, { + method: 'POST', headers: { 'content-type': 'application/json; charset=UTF-8', - 'x-xsrf-token': xsrfToken, - cookie: `XSRF-TOKEN=${xsrfToken}`, + 'x-xsrf-token': token, + cookie: `XSRF-TOKEN=${token}`, }, - searchParams: { + params: { from: 0, size: 20, type: category === 'careers' ? 'CAREER' : 'SEARCH', }, - json: data, + body: { + ...data, + query, + }, }); - const list = pageResponse.data.hits; + const items = await Promise.all( + pageResponse.hits.map((item) => { + const link = `${baseURL}/${category}/${item.id}`; + return cache.tryGet(link, async () => { + const response = await ofetch(`${apiURL}/${category === 'theses' ? 'theses' : 'information'}/${item.id}`, { + responseType: 'json', + }); + + const dataItem: DataItem = { + title: response.title, + pubDate: parseDate(response.onlineTime), + link, + image: response.cover, + }; + + dataItem.description = + 'zhTextList' in response && 'enTextList' in response + ? art(templatePath, { + zh: response.zhTextList, + en: response.enTextList, + }) + : response.content; + + if ('paperList' in response) { + const { doi, authors } = response.paperList[0]; + dataItem.doi = doi; + dataItem.author = authors.map((author) => ({ name: author })); + } - const out = list.map((item) => ({ - title: item.title, - description: item.content, - pubDate: parseDate(item.createdAt, 'x'), - link: `${metaURL}/${item.id}`, - guid: `${metaURL}/${item.id}`, - doi: item.identCode === undefined ? '' : item.identCode, - author: item.authors === undefined ? '' : item.authors.join(', '), - })); + return dataItem; + }) as unknown as DataItem; + }) + ); return { title: `领研 | ${title}`, description: '领研是链接华人学者的人才及成果平台。领研为国内外高校、科研机构及科技企业提供科研人才招聘服务,也是青年研究者的职业发展指导及线上培训平台;研究者还可将自己的研究论文上传至领研,与超过五十万华人学者分享工作的最新进展。', - image: 'https://www.linkresearcher.com/assets/images/logo-app.png', + image: `${baseURL}/assets/images/logo-app.png`, link: baseURL, - item: out, + item: items, }; } diff --git a/lib/routes/linkresearcher/namespace.ts b/lib/routes/linkresearcher/namespace.ts index 2fde7fa0853d7a..dd99889536c07a 100644 --- a/lib/routes/linkresearcher/namespace.ts +++ b/lib/routes/linkresearcher/namespace.ts @@ -2,6 +2,12 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Link Research', - url: 'linkresearcher', - lang: 'en', + url: 'www.linkresearcher.com', + lang: 'zh-CN', + zh: { + name: '领研', + }, + 'zh-TW': { + name: '領研', + }, }; diff --git a/lib/routes/linkresearcher/templates/bilingual.art b/lib/routes/linkresearcher/templates/bilingual.art new file mode 100644 index 00000000000000..be7d6c1ba93f73 --- /dev/null +++ b/lib/routes/linkresearcher/templates/bilingual.art @@ -0,0 +1,7 @@ +{{ each en }} +{{ if $index !== 0 }} +
+{{ /if }} +

{{ $value }}

+

{{ zh[$index] }}

+{{ /each }} diff --git a/lib/routes/linkresearcher/types.ts b/lib/routes/linkresearcher/types.ts new file mode 100644 index 00000000000000..f93aa153a42a40 --- /dev/null +++ b/lib/routes/linkresearcher/types.ts @@ -0,0 +1,103 @@ +interface BaseItem { + id: string; + title: string; + tags: string[]; + onlineTime: number; + cover: string; +} + +export interface InformationItem extends BaseItem { + summary: string; +} + +export interface ThesesItem extends BaseItem { + authors: string[]; + content: string; + journals: string[]; + publishDate: string; + source: { + sourceType: string; + }; + subject: string; + thesisTitle: string; +} + +export interface ArticleItem extends BaseItem { + columns: string[]; + source: { + logo: string; + sourceId: string; + sourceName: string; + sourceType: string; + }; + summary: string; + type: string; +} + +export type SearchResultItem = InformationItem | ThesesItem | ArticleItem; + +export interface ThesesDetailResponse { + columns: string[]; + content: string; + cover: string; + enTextList: string[]; + id: string; + journals: string[]; + link: string; + onlineTime: number; + original: boolean; + paperList: { + authors: string[]; + checkname: string; + doi: string; + id: string; + journal: string; + link: string; + publishDate: string; + subjects: string[]; + summary: string; + title: string; + translateSummary: string; + type: string; + }[]; + relevant: { + timestamp: number; + type: string; + }[]; + source: { + sourceId: string; + sourceName: string; + }; + sourceKey: string; + sourceType: string; + tags: string[]; + template: boolean; + title: string; + userType: number; + zhTextList?: string[]; +} + +export interface InformationDetailResponse { + columns: string[]; + content: string; + cover: string; + id: string; + onlineTime: number; + original: boolean; + relevant: { + timestamp: number; + type: string; + }[]; + source: { + sourceId: string; + sourceName: string; + }; + sourceKey: string; + subject: string; + summary: string; + tags: string[]; + title: string; + type: string; +} + +export type DetailResponse = ThesesDetailResponse | InformationDetailResponse;