Skip to content

Commit 69e1b77

Browse files
committed
fix: refresh stale news sources
1 parent f3702fc commit 69e1b77

13 files changed

Lines changed: 151 additions & 257 deletions

File tree

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ RUN pnpm run build
77

88
FROM node:20.12.2-alpine
99
WORKDIR /usr/app
10+
RUN apk add --no-cache curl
1011
COPY --from=builder /usr/src/dist/output ./output
1112
ENV HOST=0.0.0.0 PORT=4444 NODE_ENV=production
1213
EXPOSE $PORT

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
},
1212
"homepage": "https://github.com/ourongxing/newsnow",
1313
"scripts": {
14-
"dev": "npm run presource && vite dev",
14+
"dev": "npm run presource && cross-env NODE_OPTIONS=--use-env-proxy vite dev",
1515
"build": "npm run presource && vite build",
1616
"lint": "eslint",
1717
"presource": "tsx ./scripts/favicon.ts && tsx ./scripts/source.ts",

server/sources/_36kr.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const renqi = defineSource(async () => {
7979
})
8080
}
8181
})
82-
return articles
82+
return articles.length ? articles : quick()
8383
})
8484

8585
export default defineSource({

server/sources/cankaoxiaoxi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ interface Res {
1111
}
1212

1313
export default defineSource(async () => {
14-
const res = await Promise.all(["zhongguo", "guandian", "gj"].map(k => myFetch(`https://china.cankaoxiaoxi.com/json/channel/${k}/list.json`) as Promise<Res>))
14+
const res = await Promise.all(["zhongguo", "guandian", "gj"].map(k => myFetch(`http://china.cankaoxiaoxi.com/json/channel/${k}/list.json`) as Promise<Res>))
1515
return res.map(k => k.list).flat().map(k => ({
1616
id: k.data.id,
1717
title: k.data.title,

server/sources/cls/index.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,16 @@ const hot = defineSource(async () => {
5959
})
6060

6161
const telegraph = defineSource(async () => {
62-
const apiUrl = `https://www.cls.cn/nodeapi/updateTelegraphList`
62+
const apiUrl = `https://www.cls.cn/v1/roll/get_roll_list`
6363
const res: TelegraphRes = await myFetch(apiUrl, {
64-
query: Object.fromEntries(await getSearchParams()),
64+
query: Object.fromEntries(await getSearchParams({
65+
last_time: Math.floor(Date.now() / 1000),
66+
refresh_type: 1,
67+
rn: 30,
68+
})),
69+
headers: {
70+
Referer: "https://www.cls.cn/telegraph",
71+
},
6572
})
6673
return res.data.roll_data.filter(k => !k.is_ad).map((k) => {
6774
return {

server/sources/fastbull.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,26 @@ const express = defineSource(async () => {
55
const baseURL = "https://www.fastbull.com"
66
const html: any = await myFetch(`${baseURL}/cn/express-news`)
77
const $ = cheerio.load(html)
8-
const $main = $(".news-list")
8+
const $main = $(".content-list.news-list")
99
const news: NewsItem[] = []
1010
$main.each((_, el) => {
11-
const a = $(el).find(".title_name")
12-
const url = a.attr("href")
11+
const $el = $(el)
12+
const a = $el.find(".title_name")
13+
const url = $el.find("[data-href]").attr("data-href") || $el.find("[data-id]").attr("data-id")
1314
const titleText = a.text()
1415
const title = titleText.match(/(.+)/)?.[1] ?? titleText
15-
const date = $(el).attr("data-date")
16+
const date = $el.attr("data-date")
1617
if (url && title && date) {
18+
const pathname = url.startsWith("/") ? url : `/cn/fastshort/${url}`
1719
news.push({
18-
url: baseURL + url,
20+
url: baseURL + pathname,
1921
title: title.length < 4 ? titleText : title,
2022
id: url,
2123
pubDate: Number(date),
2224
})
2325
}
2426
})
25-
return news
27+
return news.sort((a, b) => Number(b.pubDate) - Number(a.pubDate))
2628
})
2729

2830
const news = defineSource(async () => {
@@ -38,14 +40,14 @@ const news = defineSource(async () => {
3840
const date = a.find("[data-date]").attr("data-date")
3941
if (url && title && date) {
4042
news.push({
41-
url: baseURL + url,
43+
url: url.startsWith("http") ? url : baseURL + url,
4244
title,
4345
id: url,
4446
pubDate: Number(date),
4547
})
4648
}
4749
})
48-
return news
50+
return news.sort((a, b) => Number(b.pubDate) - Number(a.pubDate))
4951
})
5052

5153
export default defineSource(

server/sources/freebuf.ts

Lines changed: 61 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -1,184 +1,74 @@
1-
import * as cheerio from "cheerio"
2-
3-
// 定义文章统计信息接口
4-
interface ArticleStats {
5-
views: number
6-
collections: number
7-
}
8-
9-
// 定义作者信息接口
10-
interface AuthorInfo {
11-
name: string
12-
avatar?: string
13-
profileUrl?: string
14-
}
15-
16-
// 定义文章数据接口
17-
interface ArticleData {
18-
title: string
19-
url: string
20-
description: string
21-
publishTime: string
22-
author: AuthorInfo
23-
stats: ArticleStats
24-
album?: string
25-
image?: string
26-
category?: string
27-
}
28-
29-
// 辅助函数:安全提取文本
30-
function safeExtract($element: cheerio.Cheerio<any>, selector: string): string {
31-
const result = $element.find(selector).first().text().trim()
32-
return result || ""
33-
}
34-
35-
// 辅助函数:安全提取属性
36-
function safeExtractAttribute($element: cheerio.Cheerio<any>, selector: string, attribute: string): string {
37-
return $element.find(selector).first().attr(attribute) || ""
38-
}
39-
40-
// 辅助函数:格式化URL
41-
function formatUrl(url: string | undefined, baseUrl: string = "https://www.freebuf.com"): string {
42-
if (!url) return ""
43-
return url.startsWith("http") ? url : `${baseUrl}${url}`
44-
}
45-
46-
// 辅助函数:提取统计信息
47-
function extractStats($article: cheerio.Cheerio<any>): ArticleStats {
48-
const stats: ArticleStats = { views: 0, collections: 0 }
49-
50-
// 提取围观数
51-
const viewElement = $article.find("a:contains(\"围观\")")
52-
if (viewElement.length) {
53-
const viewText = viewElement.find("span").first().text()
54-
stats.views = Number.parseInt(viewText) || 0
1+
import process from "node:process"
2+
import { XMLParser } from "fast-xml-parser"
3+
import type { NewsItem } from "@shared/types"
4+
5+
interface RSSItem {
6+
title?: string
7+
link?: string
8+
description?: string
9+
pubDate?: string
10+
guid?: string | {
11+
"#text"?: string
12+
"$text"?: string
5513
}
56-
57-
// 提取收藏数
58-
const collectElement = $article.find("a:contains(\"收藏\")")
59-
if (collectElement.length) {
60-
const collectText = collectElement.find("span").first().text()
61-
stats.collections = Number.parseInt(collectText) || 0
62-
}
63-
64-
return stats
6514
}
6615

67-
// 辅助函数:提取作者信息
68-
function extractAuthor($article: cheerio.Cheerio<any>): AuthorInfo {
69-
const author: AuthorInfo = { name: "" }
70-
71-
const authorLink = $article.find(".item-bottom a").first()
72-
if (authorLink.length) {
73-
author.name = authorLink.find("span").last().text().trim()
74-
author.profileUrl = formatUrl(authorLink.attr("href"))
75-
76-
const avatarImg = authorLink.find(".ant-avatar img")
77-
if (avatarImg.length) {
78-
author.avatar = avatarImg.attr("src")
79-
}
16+
async function fetchByCurl(url: string) {
17+
if (process.env.CF_PAGES) return ""
18+
19+
try {
20+
const { execFile } = await import("node:child_process")
21+
const { promisify } = await import("node:util")
22+
const execFileAsync = promisify(execFile)
23+
const { stdout } = await execFileAsync("curl", [
24+
"-L",
25+
"--max-time",
26+
"15",
27+
"-A",
28+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
29+
url,
30+
], {
31+
maxBuffer: 1024 * 1024,
32+
})
33+
return stdout
34+
} catch {
35+
return ""
8036
}
81-
82-
return author
8337
}
8438

85-
// 辅助函数:提取分类信息
86-
function extractCategory($article: cheerio.Cheerio<any>): string {
87-
// 从URL路径推断分类
88-
const articleUrl = $article.find(".title-left .title").parent().attr("href") || ""
89-
if (articleUrl.includes("/articles/web/")) return "Web安全"
90-
if (articleUrl.includes("/articles/database/")) return "数据安全"
91-
if (articleUrl.includes("/articles/network/")) return "网络安全"
92-
if (articleUrl.includes("/articles/mobile/")) return "移动安全"
93-
if (articleUrl.includes("/articles/cloud/")) return "云安全"
94-
95-
return ""
96-
}
97-
98-
// 通过截取freebuf的文章url获取新闻id
99-
function extractIdFromUrl(url: string): string {
100-
// 找到最后一个斜杠
101-
const lastPart = url.slice(url.lastIndexOf("/") + 1) // "460614.html"
102-
// 去掉 .html,只保留数字
103-
const match = lastPart.match(/\d+/)
104-
return match ? match[0] : ""
39+
function getText(value: any) {
40+
if (!value) return ""
41+
if (typeof value === "string") return value
42+
return value["#text"] || value.$text || ""
10543
}
10644

10745
export default defineSource(async () => {
108-
const baseUrl = "https://www.freebuf.com"
109-
const html = await myFetch<any>(baseUrl, {
110-
headers: {
111-
"User-Agent":
112-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
113-
"Referer": "https://www.freebuf.com/",
114-
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
115-
},
116-
})
117-
const $ = cheerio.load(html)
118-
const articles: ArticleData[] = []
119-
// 遍历每个文章项
120-
$(".article-item").each((index: number, articleElement) => {
121-
const $article = $(articleElement)
122-
123-
try {
124-
// 提取文章标题和URL
125-
const titleLink = $article.find(".title-left .title").parent()
126-
const title = titleLink.find(".title").text().trim()
127-
const url = formatUrl(titleLink.attr("href"), baseUrl)
128-
129-
// 如果标题为空,跳过此项
130-
if (!title) return
46+
const url = "https://www.freebuf.com/feed"
47+
const xmlText = await myFetch<string>(url, {
48+
responseType: "text" as any,
49+
}).catch(() => fetchByCurl(url))
13150

132-
// 提取文章描述
133-
const description = safeExtract($article, ".item-right .text-line-2")
51+
if (!xmlText) return []
13452

135-
// 提取发布时间
136-
const publishTime = safeExtract($article, ".item-bottom span:last-child")
137-
138-
// 提取作者信息
139-
const author = extractAuthor($article)
140-
141-
// 提取统计信息
142-
const stats = extractStats($article)
143-
144-
// 提取专辑信息
145-
const album = safeExtract($article, ".from-column span")
146-
147-
// 提取图片
148-
const image = safeExtractAttribute($article, ".img-view img", "src")
149-
150-
// 提取分类
151-
const category = extractCategory($article)
152-
153-
// 构建完整的文章对象
154-
const article: ArticleData = {
155-
title,
156-
url,
157-
description,
158-
publishTime,
159-
author,
160-
stats,
161-
album: album || undefined,
162-
image: image || undefined,
163-
category: category || undefined,
164-
}
165-
166-
articles.push(article)
167-
} catch (error) {
168-
console.warn(`解析第${index + 1}篇文章时出错:`, error instanceof Error ? error.message : String(error))
169-
}
53+
const xml = new XMLParser({
54+
attributeNamePrefix: "",
55+
textNodeName: "#text",
56+
ignoreAttributes: false,
17057
})
171-
// 转换数据格式
172-
return articles.map(item => ({
173-
id: extractIdFromUrl(item.url),
174-
title: item.title,
175-
url: item.url,
176-
extra: {
177-
hover: item.description,
178-
time: item.publishTime,
179-
author: item.author,
180-
stats: item.stats,
181-
album: item.album,
182-
},
183-
}))
58+
const result = xml.parse(xmlText)
59+
const items = result?.rss?.channel?.item
60+
const list: RSSItem[] = Array.isArray(items) ? items : items ? [items] : []
61+
62+
return list.map<NewsItem>((item) => {
63+
const link = getText(item.link)
64+
return {
65+
id: getText(item.guid) || link,
66+
title: getText(item.title),
67+
url: link,
68+
pubDate: item.pubDate,
69+
extra: {
70+
hover: getText(item.description),
71+
},
72+
}
73+
}).filter(item => item.id && item.title && item.url)
18474
})

server/sources/jin10.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export default defineSource(async () => {
3737
return {
3838
id: k.id,
3939
title: title ?? text,
40-
pubDate: parseRelativeDate(k.time, "Asia/Shanghai").valueOf(),
40+
pubDate: tranformToUTC(k.time),
4141
url: `https://flash.jin10.com/detail/${k.id}`,
4242
extra: {
4343
hover: desc,

0 commit comments

Comments
 (0)