feat: new source 36kr

This commit is contained in:
Ou 2024-10-09 15:50:14 +08:00
parent 81e0023c17
commit 24cf2a5a75
8 changed files with 408 additions and 143 deletions

137
auto-imports.d.ts vendored
View File

@ -1,137 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const $fetch: typeof import('ofetch')['$fetch']
const afterAll: typeof import('vitest')['afterAll']
const afterEach: typeof import('vitest')['afterEach']
const appendCorsHeaders: typeof import('h3')['appendCorsHeaders']
const appendCorsPreflightHeaders: typeof import('h3')['appendCorsPreflightHeaders']
const appendHeader: typeof import('h3')['appendHeader']
const appendHeaders: typeof import('h3')['appendHeaders']
const appendResponseHeader: typeof import('h3')['appendResponseHeader']
const appendResponseHeaders: typeof import('h3')['appendResponseHeaders']
const assert: typeof import('vitest')['assert']
const assertMethod: typeof import('h3')['assertMethod']
const beforeAll: typeof import('vitest')['beforeAll']
const beforeEach: typeof import('vitest')['beforeEach']
const callNodeListener: typeof import('h3')['callNodeListener']
const chai: typeof import('vitest')['chai']
const clearResponseHeaders: typeof import('h3')['clearResponseHeaders']
const clearSession: typeof import('h3')['clearSession']
const createApp: typeof import('h3')['createApp']
const createAppEventHandler: typeof import('h3')['createAppEventHandler']
const createError: typeof import('h3')['createError']
const createEvent: typeof import('h3')['createEvent']
const createEventStream: typeof import('h3')['createEventStream']
const createRouter: typeof import('h3')['createRouter']
const day: typeof import('./server/utils/date')['day']
const defaultContentType: typeof import('h3')['defaultContentType']
const defineEventHandler: typeof import('h3')['defineEventHandler']
const defineFallbackSource: typeof import('./server/utils/source')['defineFallbackSource']
const defineLazyEventHandler: typeof import('h3')['defineLazyEventHandler']
const defineNodeListener: typeof import('h3')['defineNodeListener']
const defineNodeMiddleware: typeof import('h3')['defineNodeMiddleware']
const defineRSSHubSource: typeof import('./server/utils/source')['defineRSSHubSource']
const defineRSSSource: typeof import('./server/utils/source')['defineRSSSource']
const defineRequestMiddleware: typeof import('h3')['defineRequestMiddleware']
const defineResponseMiddleware: typeof import('h3')['defineResponseMiddleware']
const defineSource: typeof import('./server/utils/source')['defineSource']
const defineWebSocket: typeof import('h3')['defineWebSocket']
const defineWebSocketHandler: typeof import('h3')['defineWebSocketHandler']
const deleteCookie: typeof import('h3')['deleteCookie']
const describe: typeof import('vitest')['describe']
const dynamicEventHandler: typeof import('h3')['dynamicEventHandler']
const eventHandler: typeof import('h3')['eventHandler']
const expect: typeof import('vitest')['expect']
const fetchWithEvent: typeof import('h3')['fetchWithEvent']
const fromNodeMiddleware: typeof import('h3')['fromNodeMiddleware']
const fromPlainHandler: typeof import('h3')['fromPlainHandler']
const fromWebHandler: typeof import('h3')['fromWebHandler']
const getCookie: typeof import('h3')['getCookie']
const getHeader: typeof import('h3')['getHeader']
const getHeaders: typeof import('h3')['getHeaders']
const getMethod: typeof import('h3')['getMethod']
const getProxyRequestHeaders: typeof import('h3')['getProxyRequestHeaders']
const getQuery: typeof import('h3')['getQuery']
const getRequestFingerprint: typeof import('h3')['getRequestFingerprint']
const getRequestHeader: typeof import('h3')['getRequestHeader']
const getRequestHeaders: typeof import('h3')['getRequestHeaders']
const getRequestHost: typeof import('h3')['getRequestHost']
const getRequestIP: typeof import('h3')['getRequestIP']
const getRequestPath: typeof import('h3')['getRequestPath']
const getRequestProtocol: typeof import('h3')['getRequestProtocol']
const getRequestURL: typeof import('h3')['getRequestURL']
const getRequestWebStream: typeof import('h3')['getRequestWebStream']
const getResponseHeader: typeof import('h3')['getResponseHeader']
const getResponseHeaders: typeof import('h3')['getResponseHeaders']
const getResponseStatus: typeof import('h3')['getResponseStatus']
const getResponseStatusText: typeof import('h3')['getResponseStatusText']
const getRouterParam: typeof import('h3')['getRouterParam']
const getRouterParams: typeof import('h3')['getRouterParams']
const getSession: typeof import('h3')['getSession']
const getValidatedQuery: typeof import('h3')['getValidatedQuery']
const getValidatedRouterParams: typeof import('h3')['getValidatedRouterParams']
const handleCacheHeaders: typeof import('h3')['handleCacheHeaders']
const handleCors: typeof import('h3')['handleCors']
const isCorsOriginAllowed: typeof import('h3')['isCorsOriginAllowed']
const isError: typeof import('h3')['isError']
const isEvent: typeof import('h3')['isEvent']
const isEventHandler: typeof import('h3')['isEventHandler']
const isMethod: typeof import('h3')['isMethod']
const isPreflightRequest: typeof import('h3')['isPreflightRequest']
const isStream: typeof import('h3')['isStream']
const isWebResponse: typeof import('h3')['isWebResponse']
const it: typeof import('vitest')['it']
const lazyEventHandler: typeof import('h3')['lazyEventHandler']
const logger: typeof import('./server/utils/logger')['logger']
const ofetch: typeof import('ofetch')['ofetch']
const parseCookies: typeof import('h3')['parseCookies']
const promisifyNodeListener: typeof import('h3')['promisifyNodeListener']
const proxyRequest: typeof import('h3')['proxyRequest']
const readBody: typeof import('h3')['readBody']
const readFormData: typeof import('h3')['readFormData']
const readMultipartFormData: typeof import('h3')['readMultipartFormData']
const readRawBody: typeof import('h3')['readRawBody']
const readValidatedBody: typeof import('h3')['readValidatedBody']
const removeResponseHeader: typeof import('h3')['removeResponseHeader']
const rss2json: typeof import('./server/utils/rss2json')['rss2json']
const sanitizeStatusCode: typeof import('h3')['sanitizeStatusCode']
const sanitizeStatusMessage: typeof import('h3')['sanitizeStatusMessage']
const sealSession: typeof import('h3')['sealSession']
const send: typeof import('h3')['send']
const sendError: typeof import('h3')['sendError']
const sendIterable: typeof import('h3')['sendIterable']
const sendNoContent: typeof import('h3')['sendNoContent']
const sendProxy: typeof import('h3')['sendProxy']
const sendRedirect: typeof import('h3')['sendRedirect']
const sendStream: typeof import('h3')['sendStream']
const sendWebResponse: typeof import('h3')['sendWebResponse']
const serveStatic: typeof import('h3')['serveStatic']
const setCookie: typeof import('h3')['setCookie']
const setHeader: typeof import('h3')['setHeader']
const setHeaders: typeof import('h3')['setHeaders']
const setResponseHeader: typeof import('h3')['setResponseHeader']
const setResponseHeaders: typeof import('h3')['setResponseHeaders']
const setResponseStatus: typeof import('h3')['setResponseStatus']
const splitCookiesString: typeof import('h3')['splitCookiesString']
const suite: typeof import('vitest')['suite']
const test: typeof import('vitest')['test']
const toEventHandler: typeof import('h3')['toEventHandler']
const toNodeListener: typeof import('h3')['toNodeListener']
const toPlainHandler: typeof import('h3')['toPlainHandler']
const toWebHandler: typeof import('h3')['toWebHandler']
const toWebRequest: typeof import('h3')['toWebRequest']
const tranformToUTC: typeof import('./server/utils/date')['tranformToUTC']
const unsealSession: typeof import('h3')['unsealSession']
const updateSession: typeof import('h3')['updateSession']
const useBase: typeof import('h3')['useBase']
const useSession: typeof import('h3')['useSession']
const vi: typeof import('vitest')['vi']
const vitest: typeof import('vitest')['vitest']
const writeEarlyHints: typeof import('h3')['writeEarlyHints']
}

30
server/sources/36kr.ts Normal file
View File

@ -0,0 +1,30 @@
import type { NewsItem } from "@shared/types"
import { load } from "cheerio"
import { $fetch } from "ofetch"
export default defineSource(async () => {
const url = "https://www.36kr.com/newsflashes"
const response = await $fetch(url)
const $ = load(response)
const news: NewsItem[] = []
const $items = $(".news-list-item")
$items.each((_, el) => {
const $el = $(el)
const $a = $el.find("a")
const url = $a.attr("href")
const title = $a.text()
const relativeDate = $el.find(".time")
if (url && title && relativeDate) {
news.push({
url,
title,
id: url,
extra: {
date: parseRelativeDate(relativeDate.text()),
},
})
}
})
return news.slice(0, 20)
})

View File

@ -31,7 +31,8 @@ export default defineSource(async () => {
title: i.editor_title || load(i.message).text().split("\n")[0],
url: i.shareUrl,
extra: {
date: new Date(i.dateline * 1000).getTime(),
info: i.targetRow?.subTitle,
// date: new Date(i.dateline * 1000).getTime(),
},
})).slice(0, 20)
})

View File

@ -1,4 +1,165 @@
import { describe, expect, it } from "vitest"
import MockDate from "mockdate"
describe("parseRelativeDate", () => {
const second = 1000
const minute = 60 * second
const hour = 60 * minute
const day = 24 * hour
const week = 7 * day
const month = 30 * day
const year = 365 * day
const date = new Date()
const weekday = (d: number) => +new Date(date.getFullYear(), date.getMonth(), date.getDate() + d - (date.getDay() > d ? date.getDay() : date.getDay() + 7))
// 固定时间
MockDate.set(date)
it("s秒钟前", () => {
expect(+new Date(parseRelativeDate("10秒前"))).toBe(+date - 10 * second)
})
it("m分钟前", () => {
expect(+new Date(parseRelativeDate("10分钟前"))).toBe(+date - 10 * minute)
})
it("m分鐘前", () => {
expect(+new Date(parseRelativeDate("10分鐘前"))).toBe(+date - 10 * minute)
})
it("m分钟后", () => {
expect(+new Date(parseRelativeDate("10分钟后"))).toBe(+date + 10 * minute)
})
it("a minute ago", () => {
expect(+new Date(parseRelativeDate("a minute ago"))).toBe(+date - 1 * minute)
})
it("s minutes ago", () => {
expect(+new Date(parseRelativeDate("10 minutes ago"))).toBe(+date - 10 * minute)
})
it("s mins ago", () => {
expect(+new Date(parseRelativeDate("10 mins ago"))).toBe(+date - 10 * minute)
})
it("in s minutes", () => {
expect(+new Date(parseRelativeDate("in 10 minutes"))).toBe(+date + 10 * minute)
})
it("in an hour", () => {
expect(+new Date(parseRelativeDate("in an hour"))).toBe(+date + 1 * hour)
})
it("h小时前", () => {
expect(+new Date(parseRelativeDate("10小时前"))).toBe(+date - 10 * hour)
})
it("h个小时前", () => {
expect(+new Date(parseRelativeDate("10个小时前"))).toBe(+date - 10 * hour)
})
it("d天前", () => {
expect(+new Date(parseRelativeDate("10天前"))).toBe(+date - 10 * day)
})
it("w周前", () => {
expect(+new Date(parseRelativeDate("10周前"))).toBe(+date - 10 * week)
})
it("w星期前", () => {
expect(+new Date(parseRelativeDate("10星期前"))).toBe(+date - 10 * week)
})
it("w个星期前", () => {
expect(+new Date(parseRelativeDate("10个星期前"))).toBe(+date - 10 * week)
})
it("m月前", () => {
expect(+new Date(parseRelativeDate("1月前"))).toBe(+date - 1 * month)
})
it("m个月前", () => {
expect(+new Date(parseRelativeDate("1个月前"))).toBe(+date - 1 * month)
})
it("y年前", () => {
expect(+new Date(parseRelativeDate("1年前"))).toBe(+date - 1 * year)
})
it("y年M个月前", () => {
expect(+new Date(parseRelativeDate("1年1个月前"))).toBe(+date - 1 * year - 1 * month)
})
it("d天H小时前", () => {
expect(+new Date(parseRelativeDate("1天1小时前"))).toBe(+date - 1 * day - 1 * hour)
})
it("h小时m分钟s秒钟前", () => {
expect(+new Date(parseRelativeDate("1小时1分钟1秒钟前"))).toBe(+date - 1 * hour - 1 * minute - 1 * second)
})
it("dd Hh mm ss ago", () => {
expect(+new Date(parseRelativeDate("1d 1h 1m 1s ago"))).toBe(+date - 1 * day - 1 * hour - 1 * minute - 1 * second)
})
it("h小时m分钟s秒钟后", () => {
expect(+new Date(parseRelativeDate("1小时1分钟1秒钟后"))).toBe(+date + 1 * hour + 1 * minute + 1 * second)
})
it("今天", () => {
expect(+new Date(parseRelativeDate("今天"))).toBe(+date.setHours(0, 0, 0, 0))
})
it("today H:m", () => {
expect(+new Date(parseRelativeDate("Today 08:00"))).toBe(+date + 8 * hour)
})
it("today, h:m a", () => {
expect(+new Date(parseRelativeDate("Today, 8:00 pm"))).toBe(+date + 20 * hour)
})
it("tDA H:m:s", () => {
expect(+new Date(parseRelativeDate("TDA 08:00:00"))).toBe(+date + 8 * hour)
})
it("今天 H:m", () => {
expect(+new Date(parseRelativeDate("今天 08:00"))).toBe(+date + 8 * hour)
})
it("今天H点m分", () => {
expect(+new Date(parseRelativeDate("今天8点0分"))).toBe(+date + 8 * hour)
})
it("昨日H点m分s秒", () => {
expect(+new Date(parseRelativeDate("昨日20时0分0秒"))).toBe(+date - 4 * hour)
})
it("前天 H:m", () => {
expect(+new Date(parseRelativeDate("前天 20:00"))).toBe(+date - 1 * day - 4 * hour)
})
it("明天 H:m", () => {
expect(+new Date(parseRelativeDate("明天 20:00"))).toBe(+date + 1 * day + 20 * hour)
})
it("星期几 h:m", () => {
expect(+new Date(parseRelativeDate("星期一 8:00"))).toBe(weekday(1) + 8 * hour)
})
it("周几 h:m", () => {
expect(+new Date(parseRelativeDate("周二 8:00"))).toBe(weekday(2) + 8 * hour)
})
it("星期天 h:m", () => {
expect(+new Date(parseRelativeDate("星期天 8:00"))).toBe(weekday(7) + 8 * hour)
})
it("invalid", () => {
expect(parseRelativeDate("RSSHub")).toBe("RSSHub")
})
})
describe("transform Beijing time to UTC in different timezone", () => {
const a = "2024/10/3 12:26:16"

View File

@ -1,9 +1,17 @@
import dayjs from "dayjs"
import utcPlugin from "dayjs/plugin/utc.js"
import timezonePlugin from "dayjs/plugin/timezone.js"
import customParseFormat from "dayjs/plugin/customParseFormat"
import duration from "dayjs/plugin/duration"
import isSameOrBefore from "dayjs/plugin/isSameOrBefore"
import weekday from "dayjs/plugin/weekday"
dayjs.extend(utcPlugin)
dayjs.extend(timezonePlugin)
dayjs.extend(customParseFormat)
dayjs.extend(duration)
dayjs.extend(isSameOrBefore)
dayjs.extend(weekday)
/**
* UTC
@ -14,3 +22,201 @@ export function tranformToUTC(date: string, format?: string, timezone: string =
}
export const day = dayjs
const words = [
{
startAt: dayjs(),
regExp: /^(?:今[天日]|to?day?)(.*)/,
},
{
startAt: dayjs().subtract(1, "days"),
regExp: /^(?:昨[天日]|y(?:ester)?day?)(.*)/,
},
{
startAt: dayjs().subtract(2, "days"),
regExp: /^(?:前天|(?:the)?d(?:ay)?b(?:eforeyesterda)?y)(.*)/,
},
{
startAt: dayjs().isSameOrBefore(dayjs().weekday(1)) ? dayjs().weekday(1).subtract(1, "week") : dayjs().weekday(1),
regExp: /^(?:周|星期)一(.*)/,
},
{
startAt: dayjs().isSameOrBefore(dayjs().weekday(2)) ? dayjs().weekday(2).subtract(1, "week") : dayjs().weekday(2),
regExp: /^(?:周|星期)二(.*)/,
},
{
startAt: dayjs().isSameOrBefore(dayjs().weekday(3)) ? dayjs().weekday(3).subtract(1, "week") : dayjs().weekday(3),
regExp: /^(?:周|星期)三(.*)/,
},
{
startAt: dayjs().isSameOrBefore(dayjs().weekday(4)) ? dayjs().weekday(4).subtract(1, "week") : dayjs().weekday(4),
regExp: /^(?:周|星期)四(.*)/,
},
{
startAt: dayjs().isSameOrBefore(dayjs().weekday(5)) ? dayjs().weekday(5).subtract(1, "week") : dayjs().weekday(5),
regExp: /^(?:周|星期)五(.*)/,
},
{
startAt: dayjs().isSameOrBefore(dayjs().weekday(6)) ? dayjs().weekday(6).subtract(1, "week") : dayjs().weekday(6),
regExp: /^(?:周|星期)六(.*)/,
},
{
startAt: dayjs().isSameOrBefore(dayjs().weekday(7)) ? dayjs().weekday(7).subtract(1, "week") : dayjs().weekday(7),
regExp: /^(?:周|星期)[天日](.*)/,
},
{
startAt: dayjs().add(1, "days"),
regExp: /^(?:明[天日]|y(?:ester)?day?)(.*)/,
},
{
startAt: dayjs().add(2, "days"),
regExp: /^(?:[后後][天日]|(?:the)?d(?:ay)?a(?:fter)?t(?:omrrow)?)(.*)/,
},
]
const patterns = [
{
unit: "years",
regExp: /(\d+)(?:年|y(?:ea)?rs?)/,
},
{
unit: "months",
regExp: /(\d+)(?:[个個]?月|months?)/,
},
{
unit: "weeks",
regExp: /(\d+)(?:周|[个個]?星期|weeks?)/,
},
{
unit: "days",
regExp: /(\d+)(?:天|日|d(?:ay)?s?)/,
},
{
unit: "hours",
regExp: /(\d+)(?:[个個]?(?:小?时|[時点點])|h(?:(?:ou)?r)?s?)/,
},
{
unit: "minutes",
regExp: /(\d+)(?:分[鐘钟]?|m(?:in(?:ute)?)?s?)/,
},
{
unit: "seconds",
regExp: /(\d+)(?:秒[鐘钟]?|s(?:ec(?:ond)?)?s?)/,
},
]
const patternSize = Object.keys(patterns).length
/**
*
* @param {string} date
*/
function toDate(date: string) {
return date
.toLowerCase()
.replace(/(^an?\s)|(\san?\s)/g, "1") // 替换 `a` 和 `an` 为 `1`
.replace(/几|幾/g, "3") // 如 `几秒钟前` 视作 `3秒钟前`
.replace(/[\s,]/g, "")
} // 移除所有空格
/**
* `['\d+时', ..., '\d+秒']` `{ hours: \d+, ..., seconds: \d+ }`
*
* @param {Array.<string>} matches
*/
function toDurations(matches: string[]) {
const durations: Record<string, string> = {}
let p = 0
for (const m of matches) {
for (; p <= patternSize; p++) {
const match = patterns[p].regExp.exec(m)
if (match) {
durations[patterns[p].unit] = match[1]
break
}
}
}
return durations
}
export const parseDate = (date: string | number, ...options: any) => dayjs(date, ...options).toDate()
export function parseRelativeDate(date: string) {
// 预处理日期字符串 date
const theDate = toDate(date)
// 将 `\d+年\d+月...\d+秒前` 分割成 `['\d+年', ..., '\d+秒前']`
const matches = theDate.match(/\D*\d+(?![:\-/]|(a|p)m)\D+/g)
if (matches) {
// 获得最后的时间单元,如 `\d+秒前`
const lastMatch = matches.pop()
if (lastMatch) {
// 若最后的时间单元含有 `前`、`以前`、`之前` 等标识字段,减去相应的时间长度
// 如 `1分10秒前`
const beforeMatches = /(.*)(?:前|ago)$/.exec(lastMatch)
if (beforeMatches) {
matches.push(beforeMatches[1])
// duration 这个插件有 bug他会重新实现 subtract 这个方法,并且不会处理 weeks。用 ms 就可以调用默认的方法
// 改成这样之后,月又出问题了,我也是服了
return dayjs().subtract(dayjs.duration(toDurations(matches))).toDate()
}
// 若最后的时间单元含有 `后`、`以后`、`之后` 等标识字段,加上相应的时间长度
// 如 `1分10秒后`
const afterMatches = /(?:^in(.*)|(.*)[后後])$/.exec(lastMatch)
if (afterMatches) {
matches.push(afterMatches[1] ?? afterMatches[2])
return dayjs()
.add(dayjs.duration(toDurations(matches)))
.toDate()
}
// 以下处理日期字符串 date 含有特殊词的情形
// 如 `今天1点10分`
matches.push(lastMatch)
}
const firstMatch = matches.shift()
if (firstMatch) {
for (const w of words) {
const wordMatches = w.regExp.exec(firstMatch)
if (wordMatches) {
matches.unshift(wordMatches[1])
// 取特殊词对应日零时为起点,加上相应的时间长度
return w.startAt
.set("hour", 0)
.set("minute", 0)
.set("second", 0)
.set("millisecond", 0)
.add(dayjs.duration(toDurations(matches)))
.toDate()
}
}
}
} else {
// 若日期字符串 date 不匹配 patterns 中所有模式,则默认为 `特殊词 + 标准时间格式` 的情形,此时直接将特殊词替换为对应日期
// 如今天为 `2022-03-22`,则 `今天 20:00` => `2022-03-22 20:00`
for (const w of words) {
const wordMatches = w.regExp.exec(theDate)
if (wordMatches) {
// The default parser of dayjs() can parse '8:00 pm' but not '8:00pm'
// so we need to insert a space in between
return dayjs(`${w.startAt.format("YYYY-MM-DD")} ${/a|pm$/.test(wordMatches[1]) ? wordMatches[1].replace(/a|pm/, " $&") : wordMatches[1]}`).toDate()
}
}
}
return date
}

View File

@ -2,6 +2,7 @@ import { typeSafeObjectFromEntries } from "./type.util"
import type { OriginSource, Source, SourceID } from "./types"
const Time = {
test: 1,
half: 30 * 60 * 1000,
five: 5 * 60 * 1000,
}

View File

@ -154,14 +154,16 @@ function Num({ num }: { num: number }) {
function ExtraInfo({ item }: { item: NewsItem }) {
const relativeTime = useRelativeTime(item?.extra?.date)
if (relativeTime) {
return <>{relativeTime}</>
if (item?.extra?.info) {
return <>{item.extra.info}</>
}
if (item?.extra?.icon) {
return (
<img src={item.extra.icon} className="w-5 inline" />
)
return <img src={item.extra.icon} className="w-5 inline" />
}
if (relativeTime) {
return <>{relativeTime}</>
}
}

View File

@ -25,6 +25,7 @@ export default defineConfig({
imports: ["$fetch", "ofetch"],
}],
dirs: ["server/utils"],
dts: "dist/.nitro/types/nitro-imports.d.ts",
}),
],
})