feat: add some sources

This commit is contained in:
Ou 2024-10-21 01:38:58 +08:00
parent 3e425cbfce
commit f24a8834ea
26 changed files with 247 additions and 60 deletions

View File

@ -58,6 +58,7 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-use": "^17.5.1", "react-use": "^17.5.1",
"sonner": "^1.5.0", "sonner": "^1.5.0",
"uncrypto": "^0.1.3",
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {

3
pnpm-lock.yaml generated
View File

@ -113,6 +113,9 @@ importers:
sonner: sonner:
specifier: ^1.5.0 specifier: ^1.5.0
version: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
uncrypto:
specifier: ^0.1.3
version: 0.1.3
zod: zod:
specifier: ^3.23.8 specifier: ^3.23.8
version: 3.23.8 version: 3.23.8

BIN
public/icons/cls.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -20,7 +20,7 @@ export default defineEventHandler(async (event): Promise<SourceResponse> => {
const cacheTable = useCache() const cacheTable = useCache()
const now = Date.now() const now = Date.now()
if (cacheTable) { if (process.env.NODE_ENV === "production" && cacheTable) {
if (process.env.INIT_TABLE !== "false") await cacheTable.init() if (process.env.INIT_TABLE !== "false") await cacheTable.init()
const cache = await cacheTable.get(id) const cache = await cacheTable.get(id)
if (cache) { if (cache) {

View File

@ -2,7 +2,8 @@ import type { NewsItem } from "@shared/types"
import { load } from "cheerio" import { load } from "cheerio"
const quick = defineSource(async () => { const quick = defineSource(async () => {
const url = "https://www.36kr.com/newsflashes" const baseURL = "https://www.36kr.com"
const url = `${baseURL}/newsflashes`
const response = await $fetch(url) as any const response = await $fetch(url) as any
const $ = load(response) const $ = load(response)
const news: NewsItem[] = [] const news: NewsItem[] = []
@ -15,7 +16,7 @@ const quick = defineSource(async () => {
const relativeDate = $el.find(".time") const relativeDate = $el.find(".time")
if (url && title && relativeDate) { if (url && title && relativeDate) {
news.push({ news.push({
url: `https://www.36kr.com${url}`, url: `${baseURL}${url}`,
title, title,
id: url, id: url,
extra: { extra: {

View File

@ -12,7 +12,6 @@ interface Res {
export default defineSource(async () => { export default defineSource(async () => {
const res = await Promise.all(["zhongguo", "guandian", "gj"].map(k => $fetch(`https://china.cankaoxiaoxi.com/json/channel/${k}/list.json`) as Promise<Res>)) const res = await Promise.all(["zhongguo", "guandian", "gj"].map(k => $fetch(`https://china.cankaoxiaoxi.com/json/channel/${k}/list.json`) as Promise<Res>))
if (!res?.[0]?.list?.length) throw new Error("Cannot fetch data")
return res.map(k => k.list).flat().map(k => ({ return res.map(k => k.list).flat().map(k => ({
id: k.data.id, id: k.data.id,
title: k.data.title, title: k.data.title,
@ -20,5 +19,5 @@ export default defineSource(async () => {
date: tranformToUTC(k.data.publishTime), date: tranformToUTC(k.data.publishTime),
}, },
url: k.data.url, url: k.data.url,
})).sort((m, n) => m.extra.date < n.extra.date ? 1 : -1).slice(0, 30) })).sort((m, n) => m.extra.date < n.extra.date ? 1 : -1)
}) })

View File

@ -0,0 +1,68 @@
import { getSearchParams } from "./utils"
interface Item {
id: number
title?: string
brief: string
shareurl: string
// need *1000
ctime: number
// 1
is_ad: number
}
interface TelegraphRes {
data: {
roll_data: Item[]
}
}
interface Depthes {
data: {
top_article: Item[]
depth_list: Item[]
}
}
const depth = defineSource(async () => {
const apiUrl = `https://www.cls.cn/v3/depth/home/assembled/1000`
const res: Depthes = await $fetch(apiUrl, {
query: await getSearchParams(),
})
return res.data.depth_list.sort((m, n) => n.ctime - m.ctime).map((k) => {
return {
id: k.id,
title: k.title || k.brief,
mobileUrl: k.shareurl,
extra: {
date: k.ctime * 1000,
},
url: `https://www.cls.cn/detail/${k.id}`,
}
})
})
// hot 失效
const telegraph = defineSource(async () => {
const apiUrl = `https://www.cls.cn/nodeapi/updateTelegraphList`
const res: TelegraphRes = await $fetch(apiUrl, {
query: await getSearchParams({ }),
})
return res.data.roll_data.filter(k => !k.is_ad).map((k) => {
return {
id: k.id,
title: k.title || k.brief,
mobileUrl: k.shareurl,
extra: {
date: k.ctime * 1000,
},
url: `https://www.cls.cn/detail/${k.id}`,
}
})
})
export default defineSource({
"cls": telegraph,
"cls-telegraph": telegraph,
"cls-depth": depth,
})

View File

@ -0,0 +1,13 @@
// https://github.com/DIYgod/RSSHub/blob/master/lib/routes/cls/utils.ts
const params = {
appName: "CailianpressWeb",
os: "web",
sv: "7.7.5",
}
export async function getSearchParams(moreParams?: any) {
const searchParams = new URLSearchParams({ ...params, ...moreParams })
searchParams.sort()
searchParams.append("sign", await md5(await myCrypto(searchParams.toString(), "SHA-1")))
return searchParams
}

View File

@ -23,9 +23,9 @@ interface Res {
export default defineSource(async () => { export default defineSource(async () => {
const url = "https://api.coolapk.com/v6/page/dataList?url=%2Ffeed%2FstatList%3FcacheExpires%3D300%26statType%3Dday%26sortField%3Ddetailnum%26title%3D%E4%BB%8A%E6%97%A5%E7%83%AD%E9%97%A8&title=%E4%BB%8A%E6%97%A5%E7%83%AD%E9%97%A8&subTitle=&page=1" const url = "https://api.coolapk.com/v6/page/dataList?url=%2Ffeed%2FstatList%3FcacheExpires%3D300%26statType%3Dday%26sortField%3Ddetailnum%26title%3D%E4%BB%8A%E6%97%A5%E7%83%AD%E9%97%A8&title=%E4%BB%8A%E6%97%A5%E7%83%AD%E9%97%A8&subTitle=&page=1"
const r: Res = await $fetch(url, { const r: Res = await $fetch(url, {
headers: genHeaders(), headers: await genHeaders(),
}) })
if (!r.data || r.data.length === 0) throw new Error("Failed to fetch") if (!r.data.length) throw new Error("Failed to fetch")
return r.data.filter(k => k.id).map(i => ({ return r.data.filter(k => k.id).map(i => ({
id: i.id, id: i.id,
title: i.editor_title || load(i.message).text().split("\n")[0], title: i.editor_title || load(i.message).text().split("\n")[0],

View File

@ -1,6 +1,5 @@
// https://github.com/DIYgod/RSSHub/blob/master/lib/routes/coolapk/utils.ts // https://github.com/DIYgod/RSSHub/blob/master/lib/routes/coolapk/utils.ts
import { Buffer } from "node:buffer" import { Buffer } from "node:buffer"
import md5 from "md5"
function getRandomDEVICE_ID() { function getRandomDEVICE_ID() {
const r = [10, 6, 6, 6, 14] const r = [10, 6, 6, 6, 14]
@ -8,22 +7,22 @@ function getRandomDEVICE_ID() {
return id.join("-") return id.join("-")
} }
function get_app_token() { async function get_app_token() {
const DEVICE_ID = getRandomDEVICE_ID() const DEVICE_ID = getRandomDEVICE_ID()
const now = Math.round(Date.now() / 1000) const now = Math.round(Date.now() / 1000)
const hex_now = `0x${now.toString(16)}` const hex_now = `0x${now.toString(16)}`
const md5_now = md5(now.toString()) const md5_now = await md5(now.toString())
const s = `token://com.coolapk.market/c67ef5943784d09750dcfbb31020f0ab?${md5_now}$${DEVICE_ID}&com.coolapk.market` const s = `token://com.coolapk.market/c67ef5943784d09750dcfbb31020f0ab?${md5_now}$${DEVICE_ID}&com.coolapk.market`
const md5_s = md5(Buffer.from(s).toString("base64")) const md5_s = await md5(Buffer.from(s).toString("base64"))
const token = md5_s + DEVICE_ID + hex_now const token = md5_s + DEVICE_ID + hex_now
return token return token
} }
export function genHeaders() { export async function genHeaders() {
return { return {
"X-Requested-With": "XMLHttpRequest", "X-Requested-With": "XMLHttpRequest",
"X-App-Id": "com.coolapk.market", "X-App-Id": "com.coolapk.market",
"X-App-Token": get_app_token(), "X-App-Token": await get_app_token(),
"X-Sdk-Int": "29", "X-Sdk-Int": "29",
"X-Sdk-Locale": "zh-CN", "X-Sdk-Locale": "zh-CN",
"X-App-Version": "11.0", "X-App-Version": "11.0",

View File

@ -1,6 +1,6 @@
interface Res { interface Res {
data?: { data: {
word_list?: { word_list: {
sentence_id: string sentence_id: string
word: string word: string
event_time: string event_time: string
@ -33,16 +33,11 @@ export default defineSource(async () => {
Cookie: `passport_csrf_token=${cookie}`, Cookie: `passport_csrf_token=${cookie}`,
}, },
}) })
if (!res?.data?.word_list || res.data.word_list.length === 0) throw new Error("Cannot fetch data")
return res.data.word_list return res.data.word_list
.slice(0, 30)
.map((k) => { .map((k) => {
return { return {
id: k.sentence_id, id: k.sentence_id,
title: k.word, title: k.word,
extra: {
info: k.hot_value,
},
url: `https://www.douyin.com/hot/${k.sentence_id}`, url: `https://www.douyin.com/hot/${k.sentence_id}`,
} }
}) })

View File

@ -1,4 +1,4 @@
import type { SourceID } from "@shared/types" import type { DisabledSourceID, SourceID } from "@shared/types"
import weibo from "./weibo" import weibo from "./weibo"
import zaobao from "./zaobao" import zaobao from "./zaobao"
import v2ex from "./v2ex" import v2ex from "./v2ex"
@ -6,11 +6,12 @@ import ithome from "./ithome"
import zhihu from "./zhihu" import zhihu from "./zhihu"
import cankaoxiaoxi from "./cankaoxiaoxi" import cankaoxiaoxi from "./cankaoxiaoxi"
import coolapk from "./coolapk" import coolapk from "./coolapk"
import sputniknewscn from "./sputniknewscn"
import kr36 from "./36kr" import kr36 from "./36kr"
import wallstreetcn from "./wallstreetcn" import wallstreetcn from "./wallstreetcn"
import douyin from "./douyin" import douyin from "./douyin"
import toutiao from "./toutiao" import toutiao from "./toutiao"
import cls from "./cls"
import sputniknewscn from "./sputniknewscn"
import type { SourceGetter } from "#/types" import type { SourceGetter } from "#/types"
export const sourcesGetters = { export const sourcesGetters = {
@ -22,8 +23,9 @@ export const sourcesGetters = {
coolapk, coolapk,
cankaoxiaoxi, cankaoxiaoxi,
sputniknewscn, sputniknewscn,
wallstreetcn, ...wallstreetcn,
douyin, douyin,
...cls,
toutiao, toutiao,
...kr36, ...kr36,
} as Record<SourceID, SourceGetter> } as Record<SourceID, SourceGetter> & Partial<Record<DisabledSourceID, SourceGetter>>

View File

@ -28,5 +28,4 @@ export default defineSource(async () => {
} }
}) })
return news.sort((m, n) => n.extra!.date > m.extra!.date ? 1 : -1) return news.sort((m, n) => n.extra!.date > m.extra!.date ? 1 : -1)
.slice(0, 30)
}) })

View File

@ -1,9 +1,8 @@
import * as cheerio from "cheerio" import * as cheerio from "cheerio"
import type { NewsItem } from "@shared/types" import type { NewsItem } from "@shared/types"
import { $fetch } from "ofetch"
export default defineSource(async () => { export default defineSource(async () => {
const response = await $fetch("https://sputniknews.cn/services/widget/lenta/") const response: any = await $fetch("https://sputniknews.cn/services/widget/lenta/")
const $ = cheerio.load(response) const $ = cheerio.load(response)
const $items = $(".lenta__item") const $items = $(".lenta__item")
const news: NewsItem[] = [] const news: NewsItem[] = []
@ -24,5 +23,5 @@ export default defineSource(async () => {
}) })
} }
}) })
return news.slice(0, 30) return news
}) })

View File

@ -1,5 +1,5 @@
interface Res { interface Res {
data?: { data: {
ClusterIdStr: string ClusterIdStr: string
Title: string Title: string
HotValue: string HotValue: string
@ -12,16 +12,11 @@ interface Res {
export default defineSource(async () => { export default defineSource(async () => {
const url = "https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc" const url = "https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc"
const res: Res = await $fetch(url) const res: Res = await $fetch(url)
if (!res.data || res.data.length === 0) throw new Error("Cannot fetch data")
return res.data return res.data
.slice(0, 30)
.map((k) => { .map((k) => {
return { return {
id: k.ClusterIdStr, id: k.ClusterIdStr,
title: k.Title, title: k.Title,
extra: {
info: k.HotValue,
},
url: `https://www.toutiao.com/trending/${k.ClusterIdStr}/`, url: `https://www.toutiao.com/trending/${k.ClusterIdStr}/`,
} }
}) })

View File

@ -18,7 +18,6 @@ interface Res {
const share = defineSource(async () => { const share = defineSource(async () => {
const res = await Promise.all(["create", "ideas", "programmer", "share"].map(k => $fetch(`https://www.v2ex.com/feed/${k}.json`) as Promise< Res>)) const res = await Promise.all(["create", "ideas", "programmer", "share"].map(k => $fetch(`https://www.v2ex.com/feed/${k}.json`) as Promise< Res>))
if (!res?.[0]?.items?.length) throw new Error("Cannot fetch data")
return res.map(k => k.items).flat().map(k => ({ return res.map(k => k.items).flat().map(k => ({
id: k.id, id: k.id,
title: k.title, title: k.title,

View File

@ -1,33 +1,87 @@
interface Res { interface Item {
uri: string
id: number
title?: string
// ad
resource_type?: string
content_text: string
content_short: string
display_time: number
type?: string
}
interface LiveRes {
data: {
items: Item[]
}
}
interface NewsRes {
data: { data: {
items: { items: {
uri: string resource: Item
id: number
title?: string
content_text: string
display_time: number
}[] }[]
} }
} }
// https://github.com/DIYgod/RSSHub/blob/master/lib/routes/wallstreetcn/live.ts interface HotRes {
export default defineSource(async () => { data: {
const category = "global" day_items: Item[]
const apiRootUrl = "https://api-one.wallstcn.com" }
const apiUrl = `${apiRootUrl}/apiv1/content/lives?channel=${category}-channel&limit=30` }
const res: Res = await $fetch(apiUrl) // https://github.com/DIYgod/RSSHub/blob/master/lib/routes/wallstreetcn/live.ts
if (!res?.data?.items || res.data.items.length === 0) throw new Error("Cannot fetch data") const live = defineSource(async () => {
const apiUrl = `https://api-one.wallstcn.com/apiv1/content/lives?channel=global-channel&limit=30`
const res: LiveRes = await $fetch(apiUrl)
return res.data.items return res.data.items
.slice(0, 30)
.map((k) => { .map((k) => {
return { return {
id: k.id, id: k.id,
title: k.title || k.content_text, title: k.title || k.content_text,
extra: { extra: {
date: new Date(k.display_time * 1000).getTime(), date: k.display_time * 1000,
}, },
url: k.uri, url: k.uri,
} }
}) })
}) })
const news = defineSource(async () => {
const apiUrl = `https://api-one.wallstcn.com/apiv1/content/information-flow?channel=global-channel&accept=article&limit=30`
const res: NewsRes = await $fetch(apiUrl)
return res.data.items
.filter(k => k.resource.resource_type !== "ad" && k.resource.type !== "live")
.map(({ resource: h }) => {
return {
id: h.id,
title: h.title || h.content_short,
extra: {
date: h.display_time * 1000,
},
url: h.uri,
}
})
})
const hot = defineSource(async () => {
const apiUrl = `https://api-one.wallstcn.com/apiv1/content/articles/hot?period=all`
const res: HotRes = await $fetch(apiUrl)
return res.data.day_items
.map((h) => {
return {
id: h.id,
title: h.title!,
url: h.uri,
}
})
})
export default defineSource({
"wallstreetcn": live,
"wallstreetcn-quick": live,
"wallstreetcn-news": news,
"wallstreetcn-hot": hot,
})

View File

@ -28,10 +28,8 @@ interface Res {
export default defineSource(async () => { export default defineSource(async () => {
const url = "https://weibo.com/ajax/side/hotSearch" const url = "https://weibo.com/ajax/side/hotSearch"
const res: Res = await $fetch(url) const res: Res = await $fetch(url)
if (!res.ok || res.data.realtime.length === 0) throw new Error("Cannot fetch data")
return res.data.realtime return res.data.realtime
.filter(k => !k.is_ad) .filter(k => !k.is_ad)
.slice(0, 30)
.map((k) => { .map((k) => {
const keyword = k.word_scheme ? k.word_scheme : `#${k.word}#` const keyword = k.word_scheme ? k.word_scheme : `#${k.word}#`
return { return {

View File

@ -31,5 +31,4 @@ export default defineSource(async () => {
} }
}) })
return news.sort((m, n) => n.extra!.date > m.extra!.date ? 1 : -1) return news.sort((m, n) => n.extra!.date > m.extra!.date ? 1 : -1)
.slice(0, 30)
}) })

View File

@ -22,7 +22,6 @@ interface Res {
export default defineSource(async () => { export default defineSource(async () => {
const url = "https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total?limit=20&desktop=true" const url = "https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total?limit=20&desktop=true"
const res: Res = await $fetch(url) const res: Res = await $fetch(url)
if (!res.data || res.data.length === 0) throw new Error("Cannot fetch data")
return res.data return res.data
.slice(0, 30) .slice(0, 30)
.map((k) => { .map((k) => {

24
server/utils/crypto.ts Normal file
View File

@ -0,0 +1,24 @@
import _md5 from "md5"
import { subtle as _ } from "uncrypto"
type T = typeof crypto.subtle
const subtle: T = _
export async function md5(s: string) {
try {
// https://developers.cloudflare.com/workers/runtime-apis/web-crypto/
// cloudflare worker support md5
return await myCrypto(s, "MD5")
} catch {
return _md5(s)
}
}
type Algorithm = "MD5" | "SHA-1" | "SHA-256" | "SHA-384" | "SHA-512"
export async function myCrypto(s: string, algorithm: Algorithm) {
const sUint8 = new TextEncoder().encode(s)
const hashBuffer = await subtle.digest(algorithm, sUint8)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join("")
return hashHex
}

View File

@ -1,8 +1,8 @@
import type { SourceID } from "@shared/types" import type { AllSourceID, SourceID } from "@shared/types"
import defu from "defu" import defu from "defu"
import type { FallbackResponse, RSSHubOption, RSSHubInfo as RSSHubResponse, SourceGetter, SourceOption } from "#/types" import type { FallbackResponse, RSSHubOption, RSSHubInfo as RSSHubResponse, SourceGetter, SourceOption } from "#/types"
type X = SourceGetter | Partial<Record<SourceID, SourceGetter>> type X = SourceGetter | Partial<Record<AllSourceID, SourceGetter>>
export function defineSource<T extends X>(source: T): T { export function defineSource<T extends X>(source: T): T {
return source return source
} }

View File

@ -19,7 +19,7 @@ const originMetadata: Metadata = {
}, },
finance: { finance: {
name: "财经", name: "财经",
sources: ["wallstreetcn", "36kr-quick"], sources: ["cls-telegraph", "cls-depth", "wallstreetcn", "wallstreetcn-hot", "wallstreetcn-news"],
}, },
focus: { focus: {
name: "关注", name: "关注",

View File

@ -52,16 +52,30 @@ export const originSources = {
}, },
"wallstreetcn": { "wallstreetcn": {
name: "华尔街见闻", name: "华尔街见闻",
interval: Time.Fast,
type: "realtime",
color: "blue", color: "blue",
home: "https://wallstreetcn.com/", home: "https://wallstreetcn.com/",
title: "快讯", sub: {
quick: {
type: "realtime",
interval: Time.Fast,
title: "实时快讯",
},
news: {
title: "最新资讯",
interval: Time.Common,
},
hot: {
title: "最热文章",
type: "hottest",
interval: Time.Common,
},
},
}, },
"36kr": { "36kr": {
name: "36氪", name: "36氪",
type: "realtime", type: "realtime",
color: "blue", color: "blue",
disable: true,
home: "https://36kr.com", home: "https://36kr.com",
sub: { sub: {
quick: { quick: {
@ -115,6 +129,22 @@ export const originSources = {
interval: Time.Common, interval: Time.Common,
home: "https://china.cankaoxiaoxi.com", home: "https://china.cankaoxiaoxi.com",
}, },
"cls": {
name: "财联社",
color: "red",
home: "https://www.cls.cn",
sub: {
telegraph: {
title: "电报",
interval: Time.Fast,
type: "realtime",
},
depth: {
title: "深度头条",
interval: Time.Common,
},
},
},
} as const satisfies Record<string, OriginSource> } as const satisfies Record<string, OriginSource>
export const sources = genSources() export const sources = genSources()

View File

@ -15,6 +15,15 @@ export type SourceID = {
}[keyof SubSource] | Key : Key; }[keyof SubSource] | Key : Key;
}[MainSourceID] }[MainSourceID]
export type AllSourceID = {
[Key in MainSourceID]: ConstSources[Key] extends { sub?: infer SubSource } ? keyof {
// @ts-expect-error >_<
[SubKey in keyof SubSource as `${Key}-${SubKey}`]: never
} | Key : Key
}[MainSourceID]
export type DisabledSourceID = Exclude<SourceID, MainSourceID>
export type ColumnID = (typeof columnIds)[number] export type ColumnID = (typeof columnIds)[number]
export type Metadata = Record<ColumnID, Column> export type Metadata = Record<ColumnID, Column>

View File

@ -74,6 +74,7 @@ export default defineConfig({
experimental: { experimental: {
database: true, database: true,
}, },
sourceMap: false,
database: { database: {
default: { default: {
connector: isCF ? "cloudflare-d1" : "sqlite", connector: isCF ? "cloudflare-d1" : "sqlite",