81 Commits
Author SHA1 Message Date
ZSCGR 50711a91f2 删除 .github/workflows/release.yml 2025-12-07 20:43:53 +08:00
ZSCGR 1b5fedd508 更新 .github/workflows/release.yml 2025-12-07 20:40:38 +08:00
ZSCGR 02e0616155 更新 .github/workflows/docker.yml
Publish Docker image to Gitea / Build and push to Gitea Registry (Internal) (push) Successful in 4m38s
Release / release (push) Failing after 12s
2025-12-07 19:02:53 +08:00
ZSCGR 06a16c2e03 更新 .github/workflows/docker.yml 2025-12-07 18:46:07 +08:00
ZSCGR 55582bb3f3 更新 .github/workflows/docker.yml 2025-12-07 18:28:21 +08:00
ZSCGR a7e5540122 更新 .github/workflows/docker.yml 2025-12-07 18:05:14 +08:00
ZSCGR 7bd07463a3 更新 .github/workflows/docker.yml 2025-12-07 16:35:26 +08:00
ZSCGR 5608f3e1f2 更新 .github/workflows/release.yml 2025-12-07 16:34:53 +08:00
ZSCGR c5853938ff 更新 .github/workflows/release.yml 2025-12-07 16:32:10 +08:00
ZSCGR ee00b5db5d 更新 .github/workflows/docker.yml 2025-12-07 16:31:02 +08:00
ZSCGR b660dc4613 更新 .github/workflows/release.yml
Publish Docker image to Gitea / Build and push to Gitea Registry (push) Failing after 2m59s
Release / release (push) Failing after 1m4s
2025-12-07 15:59:52 +08:00
ZSCGR 8a6aacc70d 更新 .github/workflows/docker.yml 2025-12-07 15:55:35 +08:00
ZSCGR 9fefe96f7b fix google search 无法解析的结构化数据 2025-11-26 17:16:23 +08:00
ZSCGR 819640c683 解决与原作者代码的冲突 2025-11-23 00:51:46 +08:00
ourongxing fcdf837435 chore: release v0.0.37 2025-11-20 10:14:46 +00:00
ourongxing fa9f552d73 fix: kaopu source 2025-11-20 10:06:54 +00:00
ourongxing 7e88e142b2 fix: weibo 2025-11-20 09:59:51 +00:00
likanugandGitHub b60bb219ec feat: Add new source: 腾讯新闻-综合早报 (#255) 2025-11-20 00:28:55 +08:00
ourongxing 3e870e1e37 chore: remove autorefresh script 2025-11-01 10:09:07 +00:00
ourongxing 3721fd383a feat(source): add steam 2025-10-27 16:58:16 +00:00
ourongxing 8142074b3c chore: release v0.0.36 2025-10-27 16:25:24 +00:00
ourongxing 5a2e2a5e9c chore: minor changes 2025-10-27 16:20:57 +00:00
ourongxing cb9b320996 feat(docker): add PRODUCTHUNT_API_TOKEN environment variable 2025-10-27 16:18:30 +00:00
ourongxing 3b845ce6b5 fix(source): mktnews 2025-10-27 16:14:01 +00:00
ourongxing a0941eff63 fix(source): producthunt by offical api 2025-10-27 16:10:04 +00:00
ourongxing 7e6805ca87 chore(source): opt display style 2025-10-27 15:51:58 +00:00
likanugandGitHub 4b89a57889 feat: Add new source: 豆瓣热门电影 (#237)
* feat: Add new source 豆瓣热门电影

* refactor: Simplify the if condition
2025-10-27 23:38:59 +08:00
5627cbef60 fix: kaopu source (#238)
Co-authored-by: kanson <[email protected]>
2025-10-27 23:38:39 +08:00
ZSCGRandGitHub 57dc2c319c Update sources.json 2025-09-28 18:47:55 +08:00
ZSCGRandGitHub 84543b12b6 Update idcflare.ts 2025-09-22 20:38:02 +08:00
ZSCGRandGitHub 3584efb09b Update idcflare.ts 2025-09-21 21:49:31 +08:00
ZSCGRandGitHub f357e920b7 Update weibo.ts 2025-09-21 21:43:09 +08:00
ZSCGRandGitHub dca03c4f0c Update refresh.ts 2025-09-21 21:42:34 +08:00
ZSCGRandGitHub 576137a62f Update glob.d.ts 2025-09-21 21:31:28 +08:00
ZSCGRandGitHub a896f15c44 Update pinyin.json 2025-09-21 21:11:19 +08:00
ZSCGRandGitHub f78eead5fd Create idcflare.ts 2025-09-21 21:07:18 +08:00
ZSCGRandGitHub 6fe8f03f16 Update sources.json 2025-09-21 21:04:38 +08:00
ZSCGRandGitHub 8e7b35385b Update pre-sources.ts 2025-09-21 21:03:29 +08:00
ourongxing f2698ba2f0 chore: release v0.0.35 2025-09-17 14:01:08 +08:00
ourongxing 5a07c02ad0 fix: adjust layout in NewsListHot component for better alignment 2025-09-17 14:00:40 +08:00
ourongxing ce0baddc78 fix: weibo source 2025-09-17 13:44:19 +08:00
ZSCGRandGitHub 0271066e27 Update pre-sources.ts 2025-08-19 14:03:18 +08:00
ZSCGRandGitHub f2e4ce36de Update pre-sources.ts 2025-08-19 13:55:57 +08:00
ZSCGRandGitHub cce09de022 Update sources.json 2025-08-19 13:45:58 +08:00
ZSCGRandGitHub 9e37d4449f Update sources.json 2025-08-19 13:42:39 +08:00
ZSCGRandGitHub aff39c1e1b Update pinyin.json 2025-08-19 13:29:29 +08:00
ZSCGRandGitHub 6c22566079 Update pinyin.json 2025-08-18 22:48:38 +08:00
ZSCGRandGitHub cfb48a8c25 Update sources.json 2025-08-18 22:24:58 +08:00
ZSCGRandGitHub 0580495208 Update docker.yml 2025-08-18 21:41:02 +08:00
ZSCGRandGitHub a914396685 Update linuxdo.ts 2025-08-18 21:09:59 +08:00
ourongxing 51fa15a4e4 chore: release v0.0.34 2025-08-15 16:36:58 +08:00
ourongxing 3968f658ee chore: update dependencies 2025-08-15 16:36:45 +08:00
521141andGitHub c4c287a40d revert: Remove MySQL support and revert to pre-modification state (#226) 2025-08-15 16:31:40 +08:00
ourongxing ae37735391 fix: show mobile scrolling instruction only on mobile 2025-08-15 01:10:57 +08:00
ourongxing c16797cb59 chore: release v0.0.33 2025-08-13 00:49:31 +08:00
ourongxing 3aa2b91b05 feat: tweak the height of the mobile search modal box 2025-08-13 00:34:35 +08:00
ourongxing 1eda9d52b6 feat: horizontal scrolling on mobile devices
closed #215
2025-08-13 00:33:14 +08:00
f8579bbe4f fix: mysql in docker (#216)
* fix(database): Implement dynamic database connector selection

* refactor: Convert database detection messages to English

* chore: add mysql2 dependency

* Update docker-compose.yml

---------

Co-authored-by: Ou <[email protected]>
2025-08-12 23:21:35 +08:00
ourongxing d1fc4f1410 chore: disable light mode for it's ugly. 2025-08-11 23:23:36 +08:00
ourongxing e9804b9ee2 chore: add github button 2025-08-11 23:19:04 +08:00
ourongxing 640a4550d6 chore: release v0.0.32 2025-08-07 10:20:54 +08:00
ourongxing fc57acf9cc fix: weibo 2025-08-06 20:26:39 +08:00
chenxuanandourongxing 681d137975 fix: Using the hupu official API (#203) 2025-08-06 20:26:39 +08:00
ourongxing ca7d6475f9 chore: update README 2025-07-23 21:08:33 +08:00
ourongxing 27c623d779 docs: update README 2025-07-17 23:26:05 +08:00
ourongxing 3d7026611b chore: release v0.0.31 2025-07-17 19:46:55 +08:00
521141andGitHub bb5fad47cb feat(database): add MySQL database support (#201) 2025-07-17 19:44:01 +08:00
ourongxing 0ee5ef316f docs: update README 2025-07-17 19:30:13 +08:00
ourongxing da3dabcba4 fix(source): filter type 'theme' in wallstreetcn source 2025-06-27 18:20:14 +08:00
ourongxing 332ed38540 fix: mkt news 2025-06-27 18:18:03 +08:00
Johnnyandourongxing 1d522f320c fix(ui): When title wraps, number border stretched (#188) 2025-06-27 17:59:27 +08:00
SleepSheepandGitHub 735a0bca82 feat: add MKTNews as a news source (#185) 2025-06-27 17:55:48 +08:00
ourongxing 0331caf1a6 chore: update gitignore 2025-06-22 12:08:37 +08:00
Ou c738c4c4cf chore: release v0.0.30 2025-05-11 00:55:52 +08:00
Ou 9f1be628e0 feat: support persistent data in docker, using docker volume 2025-05-11 00:55:41 +08:00
Ou 10f5d9a53d fix: docker image 2025-05-11 00:54:37 +08:00
Ou 67f33961d2 chore: release v0.0.29 2025-05-10 18:34:31 +08:00
Ou 1fb665be18 chore: uncomment feature code 2025-05-10 18:31:45 +08:00
Ou 7f9591f465 fix(source): zhihu
fixed #167
2025-05-10 15:20:53 +08:00
Ou b784389021 feat: add streamable http mcp server 2025-05-02 20:59:02 +08:00
Ou 2e32ca13a7 chore: update deps 2025-05-02 20:53:16 +08:00
50 changed files with 5718 additions and 3396 deletions
+3
View File
@@ -0,0 +1,3 @@
# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
.specstory/**
-23
View File
@@ -1,23 +0,0 @@
name: Auto Refresh
on:
schedule:
- cron: '*/20 0-17,22,23 * * *'
workflow_dispatch:
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Refresh
env:
JWT_TOKEN: ${{ secrets.JWT_TOKEN }}
run: npx tsx ./scripts/refresh.ts
+19 -8
View File
@@ -1,4 +1,4 @@
name: Publish Docker image name: Publish Docker image to Gitea
on: on:
push: push:
@@ -8,11 +8,12 @@ on:
jobs: jobs:
build-docker: build-docker:
name: Push Docker image to multiple registries name: Build and push to Gitea Registry (Internal)
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
packages: write packages: write # 必须:允许写入软件包
contents: read contents: read
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -22,19 +23,29 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
with:
# 👇 告诉 Buildx 这个地址允许 HTTP (insecure)
driver-opts: network=1panel-network
config-inline: |
[registry."172.18.0.21:3000"]
http = true
insecure = true
- name: Log in to the Container registry - name: Log in to Gitea Registry (Internal)
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io # ✅ 修改点1: 使用内网地址,避开 Cloudflare 和 WAF
registry: 172.18.0.21:3000
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GGITEA_PAT }}
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ghcr.io/${{ github.repository }} # ✅ 修改点2: 镜像名必须以 gitea:3000 开头,才能推送到内网注册中心
# Gitea 会自动处理映射,你在外网依然可以用 git.chgr.cc 拉取
images: 172.18.0.21:3000/${{ github.repository }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
@@ -46,4 +57,4 @@ jobs:
linux/arm64 linux/arm64
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
-25
View File
@@ -1,25 +0,0 @@
name: Release
on:
push:
branch: main
tags:
- 'v*'
jobs:
release:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: lts/*
- run: npx changelogithub
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
+2 -1
View File
@@ -12,4 +12,5 @@ dev-dist
*.tsbuildinfo *.tsbuildinfo
wrangler.toml wrangler.toml
imports.app.d.ts imports.app.d.ts
package-lock.json package-lock.json
.specstory/
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:20.12.2-alpine as builder FROM node:20.12.2-alpine AS builder
WORKDIR /usr/src WORKDIR /usr/src
COPY . . COPY . .
RUN corepack enable RUN corepack enable
-2
View File
@@ -1,5 +1,3 @@
# NewsNow
![](/public/og-image.png) ![](/public/og-image.png)
[English](./README.md) | [简体中文](README.zh-CN.md) | 日本語 [English](./README.md) | [简体中文](README.zh-CN.md) | 日本語
-2
View File
@@ -1,5 +1,3 @@
# NewsNow
![](/public/og-image.png) ![](/public/og-image.png)
English | [简体中文](README.zh-CN.md) | [日本語](README.ja-JP.md) English | [简体中文](README.zh-CN.md) | [日本語](README.ja-JP.md)
+2 -4
View File
@@ -1,7 +1,3 @@
# NewsNow
<a href="https://hellogithub.com/repository/c2978695e74a423189e9ca2543ab3b36" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=c2978695e74a423189e9ca2543ab3b36&claim_uid=SMJiFwlsKCkWf89&theme=small" alt="FeaturedHelloGitHub" /></a>
![](/public/og-image.png) ![](/public/og-image.png)
[English](./README.md) | 简体中文 | [日本語](README.ja-JP.md) [English](./README.md) | 简体中文 | [日本語](README.ja-JP.md)
@@ -114,3 +110,5 @@ pnpm dev
如果本项目对你有所帮助,可以给小猫买点零食。如果需要定制或者其他帮助,请通过下列方式联系备注。 如果本项目对你有所帮助,可以给小猫买点零食。如果需要定制或者其他帮助,请通过下列方式联系备注。
![](./screenshots/reward.gif) ![](./screenshots/reward.gif)
<a href="https://hellogithub.com/repository/c2978695e74a423189e9ca2543ab3b36" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=c2978695e74a423189e9ca2543ab3b36&claim_uid=SMJiFwlsKCkWf89&theme=small" alt="FeaturedHelloGitHub" /></a>
+17
View File
@@ -0,0 +1,17 @@
services:
newsnow:
build: .
ports:
- '4444:4444'
volumes:
- newsnow_data:/usr/app/.data
environment:
- G_CLIENT_ID=
- G_CLIENT_SECRET=
- JWT_SECRET=
- INIT_TABLE=true
- ENABLE_CACHE=true
volumes:
newsnow_data:
name: newsnow_data
+10 -3
View File
@@ -1,15 +1,22 @@
version: '3'
services: services:
newsnow: newsnow:
image: ghcr.io/ourongxing/newsnow:latest image: ghcr.io/ourongxing/newsnow:latest
container_name: newsnow container_name: newsnow
restart: always
ports: ports:
- '4444:4444' - '4444:4444'
volumes:
- newsnow_data:/usr/app/.data
environment: environment:
- HOST=0.0.0.0
- PORT=4444
- NODE_ENV=production
- G_CLIENT_ID= - G_CLIENT_ID=
- G_CLIENT_SECRET= - G_CLIENT_SECRET=
- JWT_SECRET= - JWT_SECRET=
- INIT_TABLE=true - INIT_TABLE=true
- ENABLE_CACHE=true - ENABLE_CACHE=true
- PRODUCTHUNT_API_TOKEN=
volumes:
newsnow_data:
name: newsnow_data
+2 -1
View File
@@ -2,4 +2,5 @@ G_CLIENT_ID=
G_CLIENT_SECRET= G_CLIENT_SECRET=
JWT_SECRET= JWT_SECRET=
INIT_TABLE=true INIT_TABLE=true
ENABLE_CACHE=true ENABLE_CACHE=true
PRODUCTHUNT_API_TOKEN=
+6 -19
View File
@@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html lang="zh-CN"> <html lang="zh-CN" class="dark">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@@ -15,14 +15,14 @@
<meta property="og:title" content="NewsNow - 优雅的新闻聚合阅读器" /> <meta property="og:title" content="NewsNow - 优雅的新闻聚合阅读器" />
<meta property="og:description" content="实时新闻聚合阅读器,汇集全球热点新闻,提供优雅的阅读体验" /> <meta property="og:description" content="实时新闻聚合阅读器,汇集全球热点新闻,提供优雅的阅读体验" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:url" content="https://newsnow.busiyi.world" /> <meta property="og:url" content="https://hot.chgr.cc" />
<meta property="og:image" content="https://newsnow.busiyi.world/og-image.png" /> <meta property="og:image" content="https://hot.chgr.cc/og-image.png" />
<!-- Twitter Card Meta Tags --> <!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="NewsNow - 优雅的新闻聚合阅读器" /> <meta name="twitter:title" content="NewsNow - 优雅的新闻聚合阅读器" />
<meta name="twitter:description" content="实时新闻聚合阅读器,汇集全球热点新闻,提供优雅的阅读体验" /> <meta name="twitter:description" content="实时新闻聚合阅读器,汇集全球热点新闻,提供优雅的阅读体验" />
<meta name="twitter:image" content="https://newsnow.busiyi.world/og-image.svg" /> <meta name="twitter:image" content="https://hot.chgr.cc/og-image.svg" />
<meta name="theme-color" content="#F14D42" /> <meta name="theme-color" content="#F14D42" />
<link rel="preload" href="/Baloo2-Bold.subset.ttf" as="font" type="font/ttf" crossorigin> <link rel="preload" href="/Baloo2-Bold.subset.ttf" as="font" type="font/ttf" crossorigin>
@@ -34,8 +34,8 @@
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "WebSite", "@type": "WebSite",
"name": "NewsNow", "name": "NewsNow",
"url": "https://newsnow.busiyi.world", "url": "https://hot.chgr.cc",
"description": "实时新闻聚合阅读器,汇集全球热点新闻,提供优雅的阅读体验", "description": "实时新闻聚合阅读器,汇集全球热点新闻,提供优雅的阅读体验"
} }
</script> </script>
@@ -49,19 +49,6 @@
</script> </script>
<script> <script>
function safeParseString(str) {
try {
return JSON.parse(str)
} catch {
return ""
}
}
const theme = safeParseString(localStorage.getItem("color-scheme")) || "dark"
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches
if (theme !== "light") {
document.documentElement.classList.add("dark")
}
const query = new URLSearchParams(window.location.search) const query = new URLSearchParams(window.location.search)
if (query.has("login") && query.has("user") && query.has("jwt")) { if (query.has("login") && query.has("user") && query.has("jwt")) {
localStorage.setItem("user", query.get("user")) localStorage.setItem("user", query.get("user"))
+11 -1
View File
@@ -17,6 +17,11 @@ const nitroOption: Parameters<typeof viteNitro>[0] = {
connector: "better-sqlite3", connector: "better-sqlite3",
}, },
}, },
devDatabase: {
default: {
connector: "better-sqlite3",
},
},
imports: { imports: {
dirs: ["server/utils", "shared"], dirs: ["server/utils", "shared"],
}, },
@@ -38,6 +43,11 @@ if (process.env.VERCEL) {
// } // }
} else if (process.env.CF_PAGES) { } else if (process.env.CF_PAGES) {
nitroOption.preset = "cloudflare-pages" nitroOption.preset = "cloudflare-pages"
nitroOption.unenv = {
alias: {
"safer-buffer": "node:buffer",
},
}
nitroOption.database = { nitroOption.database = {
default: { default: {
connector: "cloudflare-d1", connector: "cloudflare-d1",
@@ -57,4 +67,4 @@ if (process.env.VERCEL) {
export default function () { export default function () {
return viteNitro(nitroOption) return viteNitro(nitroOption)
} }
+9 -5
View File
@@ -1,9 +1,9 @@
{ {
"name": "newsnow", "name": "newsnow",
"type": "module", "type": "module",
"version": "0.0.28", "version": "0.0.37",
"private": true, "private": true,
"packageManager": "pnpm@10.5.2", "packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748",
"author": { "author": {
"url": "https://github.com/ourongxing/", "url": "https://github.com/ourongxing/",
"email": "[email protected]", "email": "[email protected]",
@@ -30,11 +30,12 @@
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@formkit/auto-animate": "^0.8.2", "@formkit/auto-animate": "^0.8.2",
"@iconify-json/si": "^1.2.3", "@iconify-json/si": "^1.2.3",
"@modelcontextprotocol/sdk": "^1.11.0",
"@tanstack/react-query-devtools": "^5.66.11", "@tanstack/react-query-devtools": "^5.66.11",
"@tanstack/react-router": "^1.112.0", "@tanstack/react-router": "^1.112.0",
"@unocss/reset": "^66.0.0", "@unocss/reset": "^66.0.0",
"ahooks": "^3.8.4", "ahooks": "^3.8.4",
"better-sqlite3": "^11.8.1", "better-sqlite3": "^11.10.0",
"cheerio": "^1.0.0", "cheerio": "^1.0.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.4", "cmdk": "^1.0.4",
@@ -54,6 +55,7 @@
"overlayscrollbars": "^2.11.1", "overlayscrollbars": "^2.11.1",
"pnpm": "^10.5.2", "pnpm": "^10.5.2",
"react": "^19.0.0", "react": "^19.0.0",
"react-device-detect": "^2.2.3",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-use": "^17.6.0", "react-use": "^17.6.0",
"uncrypto": "^0.1.3", "uncrypto": "^0.1.3",
@@ -99,7 +101,7 @@
"vitest": "^3.0.7", "vitest": "^3.0.7",
"workbox-build": "^7.3.0", "workbox-build": "^7.3.0",
"workbox-window": "^7.3.0", "workbox-window": "^7.3.0",
"wrangler": "^3.111.0" "wrangler": "4.14.1"
}, },
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {
@@ -109,16 +111,18 @@
"@napi-rs/pinyin", "@napi-rs/pinyin",
"@parcel/watcher", "@parcel/watcher",
"@swc/core", "@swc/core",
"better-sqlite3",
"esbuild", "esbuild",
"better-sqlite3",
"sharp", "sharp",
"simple-git-hooks", "simple-git-hooks",
"unrs-resolver",
"workerd" "workerd"
] ]
}, },
"resolutions": { "resolutions": {
"cross-spawn": ">=7.0.6", "cross-spawn": ">=7.0.6",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"nitropack": "npm:[email protected]",
"picomatch": "^4.0.2", "picomatch": "^4.0.2",
"react": "^19", "react": "^19",
"db0": "^0.3.1", "db0": "^0.3.1",
+4833 -3040
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url> <url>
<loc>https://newsnow.busiyi.world/</loc> <loc>https://hot.chgr.cc/</loc>
<lastmod>2025-01-18</lastmod> <lastmod>2025-01-18</lastmod>
<changefreq>always</changefreq> <changefreq>always</changefreq>
<priority>1.0</priority> <priority>1.0</priority>
-5
View File
@@ -1,5 +0,0 @@
import sources from "../shared/sources.json"
Promise.all(Object.keys(sources).map(id =>
fetch(`https://newsnow.busiyi.world/api/s?id=${id}`),
)).catch(console.error)
+30
View File
@@ -0,0 +1,30 @@
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"
import { getServer } from "#/mcp/server"
export default defineEventHandler(async (event) => {
const req = event.node.req
const res = event.node.res
const server = getServer()
try {
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined })
transport.onerror = console.error.bind(console)
await server.connect(transport)
await transport.handleRequest(req, res, await readBody(event))
res.on("close", () => {
// console.log("Request closed")
transport.close()
server.close()
})
return res
} catch (e) {
console.error(e)
return {
jsonrpc: "2.0",
error: {
code: -32603,
message: "Internal server error",
},
id: null,
}
}
})
+1 -1
View File
@@ -82,4 +82,4 @@ export async function getCacheTable() {
} catch (e) { } catch (e) {
logger.error("failed to init database ", e) logger.error("failed to init database ", e)
} }
} }
+1 -1
View File
@@ -66,4 +66,4 @@ export class UserTable {
if (!state.success) throw new Error(`delete user ${key} failed`) if (!state.success) throw new Error(`delete user ${key} failed`)
logger.success(`delete user ${key}`) logger.success(`delete user ${key}`)
} }
} }
+5
View File
@@ -8,6 +8,7 @@ declare module 'glob:./sources/{*.ts,**/index.ts}' {
export const chongbuluo: typeof import('./sources/chongbuluo') export const chongbuluo: typeof import('./sources/chongbuluo')
export const cls: typeof import('./sources/cls/index') export const cls: typeof import('./sources/cls/index')
export const coolapk: typeof import('./sources/coolapk/index') export const coolapk: typeof import('./sources/coolapk/index')
export const douban: typeof import('./sources/douban')
export const douyin: typeof import('./sources/douyin') export const douyin: typeof import('./sources/douyin')
export const fastbull: typeof import('./sources/fastbull') export const fastbull: typeof import('./sources/fastbull')
export const gelonghui: typeof import('./sources/gelonghui') export const gelonghui: typeof import('./sources/gelonghui')
@@ -22,6 +23,8 @@ declare module 'glob:./sources/{*.ts,**/index.ts}' {
export const kaopu: typeof import('./sources/kaopu') export const kaopu: typeof import('./sources/kaopu')
export const kuaishou: typeof import('./sources/kuaishou') export const kuaishou: typeof import('./sources/kuaishou')
export const linuxdo: typeof import('./sources/linuxdo') export const linuxdo: typeof import('./sources/linuxdo')
export const idcflare: typeof import('./sources/idcflare')
export const mktnews: typeof import('./sources/mktnews')
export const nowcoder: typeof import('./sources/nowcoder') export const nowcoder: typeof import('./sources/nowcoder')
export const pcbeta: typeof import('./sources/pcbeta') export const pcbeta: typeof import('./sources/pcbeta')
export const producthunt: typeof import('./sources/producthunt') export const producthunt: typeof import('./sources/producthunt')
@@ -29,6 +32,8 @@ declare module 'glob:./sources/{*.ts,**/index.ts}' {
export const solidot: typeof import('./sources/solidot') export const solidot: typeof import('./sources/solidot')
export const sputniknewscn: typeof import('./sources/sputniknewscn') export const sputniknewscn: typeof import('./sources/sputniknewscn')
export const sspai: typeof import('./sources/sspai') export const sspai: typeof import('./sources/sspai')
export const steam: typeof import('./sources/steam')
export const tencent: typeof import('./sources/tencent')
export const thepaper: typeof import('./sources/thepaper') export const thepaper: typeof import('./sources/thepaper')
export const tieba: typeof import('./sources/tieba') export const tieba: typeof import('./sources/tieba')
export const toutiao: typeof import('./sources/toutiao') export const toutiao: typeof import('./sources/toutiao')
+10
View File
@@ -0,0 +1,10 @@
import sources from "../../shared/sources.json"
export const description = Object.entries(sources).filter(([_, source]) => {
if (source.redirect) {
return false
}
return true
}).map(([id, source]) => {
return source.title ? `${source.name}-${source.title} id is ${id}` : `${source.name} id is ${id}`
}).join(";")
+44
View File
@@ -0,0 +1,44 @@
import { z } from "zod"
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"
import packageJSON from "../../package.json"
import { description } from "./desc.js"
export function getServer() {
const server = new McpServer(
{
name: "NewsNow",
version: packageJSON.version,
},
{ capabilities: { logging: {} } },
)
server.tool(
"get_hotest_latest_news",
`get hotest or latest news from source by {id}, return {count: 10} news.`,
{
id: z.string().describe(`source id. e.g. ${description}`),
count: z.any().default(10).describe("count of news to return."),
},
async ({ id, count }): Promise<CallToolResult> => {
let n = Number(count)
if (Number.isNaN(n) || n < 1) {
n = 10
}
const res: SourceResponse = await $fetch(`/api/s?id=${id}`)
return {
content: res.items.slice(0, count).map((item) => {
return {
text: `[${item.title}](${item.url})`,
type: "text",
}
}),
}
},
)
server.server.onerror = console.error.bind(console)
return server
}
+1 -1
View File
@@ -6,7 +6,7 @@ export default defineEventHandler(async (event) => {
if (!url.pathname.startsWith("/api")) return if (!url.pathname.startsWith("/api")) return
if (["JWT_SECRET", "G_CLIENT_ID", "G_CLIENT_SECRET"].find(k => !process.env[k])) { if (["JWT_SECRET", "G_CLIENT_ID", "G_CLIENT_SECRET"].find(k => !process.env[k])) {
event.context.disabledLogin = true event.context.disabledLogin = true
if (["/api/s", "/api/proxy", "/api/latest"].every(p => !url.pathname.startsWith(p))) if (["/api/s", "/api/proxy", "/api/latest", "/api/mcp"].every(p => !url.pathname.startsWith(p)))
throw createError({ statusCode: 506, message: "Server not configured, disable login" }) throw createError({ statusCode: 506, message: "Server not configured, disable login" })
} else { } else {
if (["/api/s", "/api/me"].find(p => url.pathname.startsWith(p))) { if (["/api/s", "/api/me"].find(p => url.pathname.startsWith(p))) {
+47
View File
@@ -0,0 +1,47 @@
interface HotMoviesRes {
category: string
tags: []
items: MovieItem[]
recommend_tags: []
total: number
type: string
}
interface MovieItem {
rating: {
count: number
max: number
star_count: number
value: number
}
title: string
pic: {
large: string
normal: string
}
is_new: boolean
uri: string
episodes_info: string
card_subtitle: string
type: string
id: string
}
export default defineSource(async () => {
const baseURL = "https://m.douban.com/rexxar/api/v2/subject/recent_hot/movie"
const res: HotMoviesRes = await myFetch(baseURL, {
headers: {
Referer: "https://movie.douban.com/",
Accept: "application/json, text/plain, */*",
},
})
return res.items.map(movie => ({
id: movie.id,
title: movie.title,
url: `https://movie.douban.com/subject/${movie.id}`,
extra: {
info: movie.card_subtitle.split(" / ").slice(0, 3).join(" / "),
hover: movie.card_subtitle,
},
}))
})
+33 -16
View File
@@ -1,20 +1,37 @@
interface Res { interface HotItem {
data: { id: string
title: string title: string
hot: string url: string
url: string mobileUrl: string
mobil_url: string
}[]
} }
export default defineSource(async () => { export default defineSource(async () => {
const r: Res = await myFetch(`https://api.vvhan.com/api/hotlist/huPu`) // 获取虎扑新热榜页面的HTML内容
return r.data.map((k) => { const html = await myFetch(`https://bbs.hupu.com/topic-daily-hot`)
return {
id: k.url, // 正则表达式匹配新的热榜项结构
title: k.title, const regex = /<li class="bbs-sl-web-post-body">[\s\S]*?<a href="(\/[^"]+?\.html)"[^>]*?class="p-title"[^>]*>([^<]+)<\/a>/g
url: k.url,
mobileUrl: k.mobil_url, const result: HotItem[] = []
} let match
})
// 将赋值操作移到循环内部,修复no-cond-assign警告
while (true) {
match = regex.exec(html)
if (!match) break
const [, path, title] = match
// 构建完整URL
const url = `https://bbs.hupu.com${path}`
result.push({
id: path,
title: title.trim(),
url,
mobileUrl: url,
})
}
return result
}) })
+61
View File
@@ -0,0 +1,61 @@
interface Res {
topic_list: {
can_create_topic: boolean
more_topics_url: string
per_page: number
top_tags: string[]
topics: {
id: number
title: string
fancy_title: string
posts_count: number
reply_count: number
highest_post_number: number
image_url: null | string
created_at: Date
last_posted_at: Date
bumped: boolean
bumped_at: Date
unseen: boolean
pinned: boolean
excerpt?: string
visible: boolean
closed: boolean
archived: boolean
like_count: number
has_summary: boolean
last_poster_username: string
category_id: number
pinned_globally: boolean
}[]
}
}
const hot = defineSource(async () => {
const res = await myFetch<Res>("https://n8n.chgr.cc/webhook/api?name=idcflare")
return res.topic_list.topics
.filter(k => k.visible && !k.archived && !k.pinned)
.map(k => ({
id: k.id,
title: k.title,
url: `https://idcflare.com/t/topic/${k.id}`,
}))
})
const latest = defineSource(async () => {
const res = await myFetch<Res>("https://idcflare.com/latest.json?order=created")
return res.topic_list.topics
.filter(k => k.visible && !k.archived && !k.pinned)
.map(k => ({
id: k.id,
title: k.title,
pubDate: new Date(k.created_at).valueOf(),
url: `https://idcflare.com/t/topic/${k.id}`,
}))
})
export default defineSource({
"idcflare": latest,
"idcflare-latest": latest,
"idcflare-hot": hot,
})
+4 -4
View File
@@ -2,17 +2,17 @@ type Res = {
description: string description: string
link: string link: string
// Date // Date
pubDate: string pub_date: string
publisher: string publisher: string
title: string title: string
}[] }[]
export default defineSource(async () => { export default defineSource(async () => {
const res = await Promise.all(["https://kaopucdn.azureedge.net/jsondata/news_list_beta_hans_0.json", "https://kaopucdn.azureedge.net/jsondata/news_list_beta_hans_1.json"].map(url => myFetch(url) as Promise<Res>)) const res: Res = await $fetch("https://kaopustorage.blob.core.windows.net/news-prod/news_list_hans_0.json")
return res.flat().filter(k => ["财新", "公视"].every(h => k.publisher !== h)).map((k) => { return res.filter(k => ["财新", "公视"].every(h => k.publisher !== h)).map((k) => {
return { return {
id: k.link, id: k.link,
title: k.title, title: k.title,
pubDate: k.pubDate, pubDate: k.pub_date,
extra: { extra: {
hover: k.description, hover: k.description,
info: k.publisher, info: k.publisher,
+1 -1
View File
@@ -32,7 +32,7 @@ interface Res {
} }
const hot = defineSource(async () => { const hot = defineSource(async () => {
const res = await myFetch<Res>("https://linux.do/top/daily.json") const res = await myFetch<Res>("https://n8n.chgr.cc/webhook/api?name=linuxdo")
return res.topic_list.topics return res.topic_list.topics
.filter(k => k.visible && !k.archived && !k.pinned) .filter(k => k.visible && !k.archived && !k.pinned)
.map(k => ({ .map(k => ({
+66
View File
@@ -0,0 +1,66 @@
interface Report {
id: string
type: number
time: string
important: number
data: {
content: string
pic: string
title: string
}
remark: string[]
hot: boolean
hot_start: string
hot_end: string
classify: {
id: number
pid: number
name: string
parent: string
}[]
}
interface Res {
data: {
id: number
name: string
pid: number
child: {
id: number
name: string
pid: number
flash_list: Report[]
}[]
}[]
}
const flash = defineSource(async () => {
const res: Res = await myFetch("https://api.mktnews.net/api/flash/host")
const categories = ["policy", "AI", "financial"] as const
const typeMap = { policy: "Policy", AI: "AI", financial: "Financial" } as const
const allReports = categories.flatMap((category) => {
const categoryData = res.data.find(item => item.name === category)
if (!categoryData?.child) return []
return categoryData.child.flatMap(subCategory =>
(subCategory.flash_list || []).map(item => ({ ...item, type: typeMap[category] })),
)
})
return allReports
.sort((a, b) => b.time.localeCompare(a.time))
.map(item => ({
id: item.id,
title: item.data.title || item.data.content.match(/^【([^】]*)】(.*)$/)?.[1] || item.data.content,
pubDate: item.time,
extra: { info: item.type, hover: item.data.content },
url: `https://mktnews.net/flashDetail.html?id=${item.id}`,
}))
})
export default defineSource({
"mktnews": flash,
"mktnews-flash": flash,
})
+45 -17
View File
@@ -1,28 +1,56 @@
import * as cheerio from "cheerio" import process from "node:process"
import type { NewsItem } from "@shared/types" import type { NewsItem } from "@shared/types"
export default defineSource(async () => { export default defineSource(async () => {
const baseURL = "https://www.producthunt.com" const apiToken = process.env.PRODUCTHUNT_API_TOKEN
const html: any = await myFetch(baseURL) const token = `Bearer ${apiToken}`
const $ = cheerio.load(html) if (!apiToken) {
const $main = $("[data-test=homepage-section-0] [data-test^=post-item]") throw new Error("PRODUCTHUNT_API_TOKEN is not set")
}
const query = `
query {
posts(first: 30, order: VOTES) {
edges {
node {
id
name
tagline
votesCount
url
slug
}
}
}
}
`
const response: any = await myFetch("https://api.producthunt.com/v2/api/graphql", {
method: "POST",
headers: {
"Authorization": token,
"Content-Type": "application/json",
"Accept": "application/json",
},
body: JSON.stringify({ query }),
})
const news: NewsItem[] = [] const news: NewsItem[] = []
$main.each((_, el) => { const posts = response?.data?.posts?.edges || []
const a = $(el).find("a").first()
const url = a.attr("href") for (const edge of posts) {
const title = $(el).find("a[data-test^=post-name]").text().replace(/^\d+\.\s*/, "") const post = edge.node
const id = $(el).attr("data-test")?.replace("post-item-", "") if (post.id && post.name) {
const vote = $(el).find("[data-test=vote-button]").text()
if (url && id && title) {
news.push({ news.push({
url: `${baseURL}${url}`, id: post.id,
title, title: post.name,
id, url: post.url || `https://www.producthunt.com/posts/${post.slug}`,
extra: { extra: {
info: `△︎ ${vote}`, info: ` △︎ ${post.votesCount || 0}`,
hover: post.tagline,
}, },
}) })
} }
}) }
return news return news
}) })
+32
View File
@@ -0,0 +1,32 @@
import * as cheerio from "cheerio"
import type { NewsItem } from "@shared/types"
export default defineSource(async () => {
const response: any = await myFetch("https://store.steampowered.com/stats/stats/")
const $ = cheerio.load(response)
const $rows = $("#detailStats tr.player_count_row")
const news: NewsItem[] = []
$rows.each((_, el) => {
const $el = $(el)
const $a = $el.find("a.gameLink")
const url = $a.attr("href")
const gameName = $a.text().trim()
const currentPlayers = $el.find("td:first-child .currentServers").text().trim()
if (url && gameName && currentPlayers) {
const title = gameName
news.push({
url,
title,
id: url,
pubDate: Date.now(),
extra: {
info: currentPlayers,
},
})
}
})
return news
})
+63
View File
@@ -0,0 +1,63 @@
import { myFetch } from "#/utils/fetch"
import { defineSource } from "#/utils/source"
interface WapRes {
ret: number
msg: string
data: {
id: number
name: string
lead: string
cover?: string
shareTitle: string
shareAbstract: string
sharePic: string
is724: boolean
is724Paper: boolean
head_cmsid: string
feed_style: number
head_article: {
live_info: string
title: string
img: string
pub_time: string
media_name: string
}
paperInfo: any
tabs: {
id: string
channel_id: string
name: string
source: string
type: string
articleList: any[]
article_count: number
sub_tab: string
}[]
banner: string
}
}
/**
* 综合早报
*/
const comprehensiveNews = defineSource(async () => {
const url = "https://i.news.qq.com/web_backend/v2/getTagInfo?tagId=aEWqxLtdgmQ%3D"
const res = await myFetch<WapRes>(url, {
headers: {
Referer: "https://news.qq.com/",
},
})
return res.data.tabs[0].articleList.map(news => ({
id: news.id,
title: news.title,
url: news.link_info.url,
extra: {
hover: news.desc,
},
}))
})
export default defineSource({
"tencent-hot": comprehensiveNews,
})
+1 -1
View File
@@ -52,7 +52,7 @@ const news = defineSource(async () => {
const res: NewsRes = await myFetch(apiUrl) const res: NewsRes = await myFetch(apiUrl)
return res.data.items return res.data.items
.filter(k => k.resource_type !== "ad" && k.resource.type !== "live" && k.resource.uri) .filter(k => k.resource_type !== "theme" && k.resource_type !== "ad" && k.resource.type !== "live" && k.resource.uri)
.map(({ resource: h }) => { .map(({ resource: h }) => {
return { return {
id: h.id, id: h.id,
+47 -43
View File
@@ -1,48 +1,52 @@
interface Res { import * as cheerio from "cheerio"
ok: number // 1 is ok
data: {
realtime:
{
num: number // 看上去是个 id
emoticon: string
icon?: string // 热,新 icon url
icon_width: number
icon_height: number
is_ad?: number // 1
note: string
small_icon_desc: string
icon_desc?: string // 如果是 荐 ,就是广告
topic_flag: number
icon_desc_color: string
flag: number
word_scheme: string
small_icon_desc_color: string
realpos: number
label_name: string
word: string // 热搜词
rank: number
}[]
}
}
export default defineSource(async () => { export default defineSource(async () => {
const url = "https://weibo.com/ajax/side/hotSearch" const baseurl = "https://s.weibo.com"
const res: Res = await myFetch(url) const url = `${baseurl}/top/summary?cate=realtimehot`
return res.data.realtime
.filter(k => !k.is_ad) const html = await myFetch(url, {
.map((k) => { headers: {
const keyword = k.word_scheme ? k.word_scheme : `#${k.word}#` "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
return { // https://github.com/v5tech/weibo-trending-hot-search
id: k.word, "Cookie": "SUB=_2AkMWIuNSf8NxqwJRmP8dy2rhaoV2ygrEieKgfhKJJRMxHRl-yT9jqk86tRB6PaLNvQZR6zYUcYVT1zSjoSreQHidcUq7",
title: k.word, "referer": url,
extra: { },
icon: k.icon && { })
url: proxyPicture(k.icon),
scale: 1.5, const $ = cheerio.load(html)
const rows = $("#pl_top_realtimehot table tbody tr").slice(1)
const hotNews: NewsItem[] = []
rows.each((_, row) => {
const $row = $(row)
const $link = $row.find("td.td-02 a").filter((_, el) => {
const href = $(el).attr("href")
return !!(href && !href.includes("javascript:void(0);"))
}).first()
if ($link.length) {
const title = $link.text().trim()
const href = $link.attr("href")
if (title && href) {
const $flag = $row.find("td.td-03").text().trim()
const flagUrl = {
: "https://simg.s.weibo.com/moter/flags/1_0.png",
: "https://simg.s.weibo.com/moter/flags/2_0.png",
}[$flag]
hotNews.push({
id: title,
title,
url: `${baseurl}${href}`,
mobileUrl: `${baseurl}${href}`,
extra: {
icon: flagUrl ? { url: proxyPicture(flagUrl), scale: 1.5 } : undefined,
}, },
}, })
url: `https://s.weibo.com/weibo?q=${encodeURIComponent(keyword)}`,
mobileUrl: `https://m.weibo.cn/search?containerid=231522type%3D1%26q%3D${encodeURIComponent(keyword)}&_T_WM=16922097837&v_p=42`,
} }
}) }
})
return hotNews
}) })
+34 -19
View File
@@ -1,38 +1,53 @@
interface Res { interface Res {
data: { data: {
card_label?: { type: "hot_list_feed"
icon: string style_type: "1"
night_icon: string feed_specific: {
answer_count: 411
} }
target: { target: {
id: number title_area: {
title: string text: string
url: string }
created: number excerpt_area: {
answer_count: number text: string
follower_count: number }
bound_topic_ids: number[] image_area: {
comment_count: number url: string
is_following: boolean }
excerpt: string metrics_area: {
text: string
font_color: string
background: string
weight: string
}
label_area: {
type: "trend"
trend: number
night_color: string
normal_color: string
}
link: {
url: string
}
} }
}[] }[]
} }
export default defineSource({ export default defineSource({
zhihu: async () => { zhihu: 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-list-web?limit=20&desktop=true"
const res: Res = await myFetch(url) const res: Res = await myFetch(url)
return res.data return res.data
.map((k) => { .map((k) => {
const urlId = k.target.url?.match(/(\d+)$/)?.[1]
return { return {
id: k.target.id, id: k.target.link.url.match(/(\d+)$/)?.[1] ?? k.target.link.url,
title: k.target.title, title: k.target.title_area.text,
extra: { extra: {
icon: k.card_label?.night_icon && proxyPicture(k.card_label.night_icon), info: k.target.metrics_area.text,
hover: k.target.excerpt_area.text,
}, },
url: `https://www.zhihu.com/question/${urlId || k.target.id}`, url: k.target.link.url,
} }
}) })
}, },
+14 -10
View File
@@ -4,9 +4,10 @@
"weibo": "weibo-shishiresou", "weibo": "weibo-shishiresou",
"zaobao": "lianhezaobao", "zaobao": "lianhezaobao",
"coolapk": "kuan-jinrizuire", "coolapk": "kuan-jinrizuire",
"wallstreetcn-quick": "huaerjiejianwen-shishikuaixun", "mktnews-flash": "MKTNews-kuaixun",
"wallstreetcn-news": "huaerjiejianwen-zuixinzixun", "wallstreetcn-quick": "huaerjiejianwen-kuaixun",
"wallstreetcn-hot": "huaerjiejianwen-zuirewenzhang", "wallstreetcn-news": "huaerjiejianwen-zuixin",
"wallstreetcn-hot": "huaerjiejianwen-zuire",
"36kr-quick": "36ke-kuaixun", "36kr-quick": "36ke-kuaixun",
"douyin": "douyin", "douyin": "douyin",
"hupu": "hupu-zhugandaoretie", "hupu": "hupu-zhugandaoretie",
@@ -16,8 +17,7 @@
"thepaper": "pengpaixinwen-rebang", "thepaper": "pengpaixinwen-rebang",
"sputniknewscn": "weixingtongxunshe", "sputniknewscn": "weixingtongxunshe",
"cankaoxiaoxi": "cankaoxiaoxi", "cankaoxiaoxi": "cankaoxiaoxi",
"pcbeta-windows11": "yuanjingluntan-Windows 11", "pcbeta-windows11": "yuanjingluntan-Win11",
"pcbeta-windows": "yuanjingluntan-Windows ziyuan",
"cls-telegraph": "cailianshe-dianbao", "cls-telegraph": "cailianshe-dianbao",
"cls-depth": "cailianshe-shendu", "cls-depth": "cailianshe-shendu",
"cls-hot": "cailianshe-remen", "cls-hot": "cailianshe-remen",
@@ -36,14 +36,18 @@
"kaopu": "kaopuxinwen", "kaopu": "kaopuxinwen",
"jin10": "jinshishuju", "jin10": "jinshishuju",
"baidu": "baiduresou", "baidu": "baiduresou",
"linuxdo-latest": "LINUX DO-zuixin", "linuxdo-latest": "LINUX DO-zuixin",
"linuxdo-hot": "LINUX DO-jinrizuire", "linuxdo-hot": "LINUX DO-jinrizuire",
"ghxi": "guoheboke", "idcflare-latest": "IDC Flare-zuixin",
"smzdm": "shenmezhidemai", "idcflare-hot": "IDC Flare-jinrizuire",
"nowcoder": "niuke", "nowcoder": "niuke",
"sspai": "shaoshupai", "sspai": "shaoshupai",
"juejin": "xitujuejin", "juejin": "xitujuejin",
"ifeng": "fenghuangwang-redianzixun", "ifeng": "fenghuangwang-redianzixun",
"chongbuluo-latest": "chongbuluo-zuixin", "chongbuluo-latest": "chongbuluo-zuixin",
"chongbuluo-hot": "chongbuluo-zuire" "chongbuluo-hot": "chongbuluo-zuire",
} "douban": "douban-remendianying",
"steam": "Steam-zaixianrenshu",
"tencent-hot": "tengxunxinwen-zonghezaobao"
}
+95 -24
View File
@@ -57,6 +57,18 @@ export const originSources = {
title: "今日最热", title: "今日最热",
home: "https://coolapk.com", home: "https://coolapk.com",
}, },
"mktnews": {
name: "MKTNews",
column: "finance",
home: "https://mktnews.net",
color: "indigo",
interval: Time.Realtime,
sub: {
flash: {
title: "快讯",
},
},
},
"wallstreetcn": { "wallstreetcn": {
name: "华尔街见闻", name: "华尔街见闻",
color: "blue", color: "blue",
@@ -66,14 +78,14 @@ export const originSources = {
quick: { quick: {
type: "realtime", type: "realtime",
interval: Time.Fast, interval: Time.Fast,
title: "实时快讯", title: "快讯",
}, },
news: { news: {
title: "最新资讯", title: "最新",
interval: Time.Common, interval: Time.Common,
}, },
hot: { hot: {
title: "最热文章", title: "最热",
type: "hottest", type: "hottest",
interval: Time.Common, interval: Time.Common,
}, },
@@ -159,7 +171,7 @@ export const originSources = {
home: "https://bbs.pcbeta.com", home: "https://bbs.pcbeta.com",
sub: { sub: {
windows11: { windows11: {
title: "Windows 11", title: "Win11",
type: "realtime", type: "realtime",
interval: Time.Fast, interval: Time.Fast,
}, },
@@ -167,6 +179,7 @@ export const originSources = {
title: "Windows 资源", title: "Windows 资源",
type: "realtime", type: "realtime",
interval: Time.Fast, interval: Time.Fast,
disable: true,
}, },
}, },
}, },
@@ -323,7 +336,6 @@ export const originSources = {
column: "tech", column: "tech",
color: "slate", color: "slate",
home: "https://linux.do/", home: "https://linux.do/",
disable: "cf",
sub: { sub: {
latest: { latest: {
title: "最新", title: "最新",
@@ -336,12 +348,31 @@ export const originSources = {
home: "https://linux.do/hot", home: "https://linux.do/hot",
}, },
}, },
},
"idcflare": {
name: "IDC Flare",
column: "tech",
color: "slate",
home: "https://idcflare.com/",
sub: {
latest: {
title: "最新",
home: "https://idcflare.com/latest",
},
hot: {
title: "今日最热",
type: "hottest",
interval: Time.Common,
home: "https://idcflare.com/hot",
},
},
}, },
"ghxi": { "ghxi": {
name: "果核剥壳", name: "果核剥壳",
column: "china", column: "china",
color: "yellow", color: "yellow",
home: "https://www.ghxi.com/", home: "https://www.ghxi.com/",
disable: true,
}, },
"smzdm": { "smzdm": {
name: "什么值得买", name: "什么值得买",
@@ -349,6 +380,7 @@ export const originSources = {
color: "red", color: "red",
type: "hottest", type: "hottest",
home: "https://www.smzdm.com", home: "https://www.smzdm.com",
disable: true,
}, },
"nowcoder": { "nowcoder": {
name: "牛客", name: "牛客",
@@ -398,6 +430,36 @@ export const originSources = {
}, },
}, },
}, },
"douban": {
name: "豆瓣",
column: "china",
title: "热门电影",
color: "green",
type: "hottest",
home: "https://www.douban.com",
},
"steam": {
name: "Steam",
column: "world",
title: "在线人数",
color: "blue",
type: "hottest",
home: "https://store.steampowered.com",
},
"tencent": {
name: "腾讯新闻",
column: "china",
color: "blue",
home: "https://news.qq.com",
sub: {
hot: {
title: "综合早报",
type: "hottest",
interval: Time.Common,
home: "https://news.qq.com/tag/aEWqxLtdgmQ=",
},
},
},
} as const satisfies Record<string, OriginSource> } as const satisfies Record<string, OriginSource>
export function genSources() { export function genSources() {
@@ -417,29 +479,38 @@ export function genSources() {
if (source.sub && Object.keys(source.sub).length) { if (source.sub && Object.keys(source.sub).length) {
Object.entries(source.sub).forEach(([subId, subSource], i) => { Object.entries(source.sub).forEach(([subId, subSource], i) => {
if (i === 0) { if (i === 0) {
_.push([id, { _.push([
redirect: `${id}-${subId}`, id,
...parent, {
...subSource, redirect: `${id}-${subId}`,
}] as [any, Source]) ...parent,
...subSource,
},
] as [any, Source])
} }
_.push([`${id}-${subId}`, { ...parent, ...subSource }] as [any, Source]) _.push([`${id}-${subId}`, { ...parent, ...subSource }] as [
any,
Source,
])
}) })
} else { } else {
_.push([id, { _.push([
title: source.title, id,
...parent, {
}]) title: source.title,
...parent,
},
])
} }
}) })
return typeSafeObjectFromEntries(_.filter(([_, v]) => { return typeSafeObjectFromEntries(
if (v.disable === "cf" && process.env.CF_PAGES) { _.filter(([_, v]) => {
return false if (v.disable === "cf" && process.env.CF_PAGES) {
} else if (v.disable === true) { return false
return false } else {
} else { return v.disable !== true
return true }
} }),
})) )
} }
+71 -52
View File
@@ -51,6 +51,23 @@
"color": "green", "color": "green",
"interval": 600000 "interval": 600000
}, },
"mktnews": {
"redirect": "mktnews-flash",
"name": "MKTNews",
"column": "finance",
"home": "https://mktnews.net",
"color": "indigo",
"interval": 120000,
"title": "快讯"
},
"mktnews-flash": {
"name": "MKTNews",
"column": "finance",
"home": "https://mktnews.net",
"color": "indigo",
"interval": 120000,
"title": "快讯"
},
"wallstreetcn": { "wallstreetcn": {
"redirect": "wallstreetcn-quick", "redirect": "wallstreetcn-quick",
"name": "华尔街见闻", "name": "华尔街见闻",
@@ -59,7 +76,7 @@
"home": "https://wallstreetcn.com/", "home": "https://wallstreetcn.com/",
"color": "blue", "color": "blue",
"interval": 300000, "interval": 300000,
"title": "实时快讯" "title": "快讯"
}, },
"wallstreetcn-quick": { "wallstreetcn-quick": {
"name": "华尔街见闻", "name": "华尔街见闻",
@@ -68,7 +85,7 @@
"home": "https://wallstreetcn.com/", "home": "https://wallstreetcn.com/",
"color": "blue", "color": "blue",
"interval": 300000, "interval": 300000,
"title": "实时快讯" "title": "快讯"
}, },
"wallstreetcn-news": { "wallstreetcn-news": {
"name": "华尔街见闻", "name": "华尔街见闻",
@@ -76,7 +93,7 @@
"home": "https://wallstreetcn.com/", "home": "https://wallstreetcn.com/",
"color": "blue", "color": "blue",
"interval": 1800000, "interval": 1800000,
"title": "最新资讯" "title": "最新"
}, },
"wallstreetcn-hot": { "wallstreetcn-hot": {
"name": "华尔街见闻", "name": "华尔街见闻",
@@ -85,7 +102,7 @@
"home": "https://wallstreetcn.com/", "home": "https://wallstreetcn.com/",
"color": "blue", "color": "blue",
"interval": 1800000, "interval": 1800000,
"title": "最热文章" "title": "最热"
}, },
"36kr": { "36kr": {
"redirect": "36kr-quick", "redirect": "36kr-quick",
@@ -181,7 +198,7 @@
"home": "https://bbs.pcbeta.com", "home": "https://bbs.pcbeta.com",
"color": "blue", "color": "blue",
"interval": 300000, "interval": 300000,
"title": "Windows 11" "title": "Win11"
}, },
"pcbeta-windows11": { "pcbeta-windows11": {
"name": "远景论坛", "name": "远景论坛",
@@ -190,16 +207,7 @@
"home": "https://bbs.pcbeta.com", "home": "https://bbs.pcbeta.com",
"color": "blue", "color": "blue",
"interval": 300000, "interval": 300000,
"title": "Windows 11" "title": "Win11"
},
"pcbeta-windows": {
"name": "远景论坛",
"type": "realtime",
"column": "tech",
"home": "https://bbs.pcbeta.com",
"color": "blue",
"interval": 300000,
"title": "Windows 资源"
}, },
"cls": { "cls": {
"redirect": "cls-telegraph", "redirect": "cls-telegraph",
@@ -406,49 +414,23 @@
"color": "blue", "color": "blue",
"interval": 600000 "interval": 600000
}, },
"linuxdo": {
"redirect": "linuxdo-latest",
"name": "LINUX DO",
"disable": "cf",
"column": "tech",
"home": "https://linux.do/latest",
"color": "slate",
"interval": 600000,
"title": "最新"
},
"linuxdo-latest": {
"name": "LINUX DO",
"disable": "cf",
"column": "tech",
"home": "https://linux.do/latest",
"color": "slate",
"interval": 600000,
"title": "最新"
},
"linuxdo-hot": { "linuxdo-hot": {
"name": "LINUX DO", "name": "LINUX DO",
"type": "hottest", "type": "hottest",
"disable": "cf",
"column": "tech", "column": "tech",
"home": "https://linux.do/hot", "home": "https://linux.do",
"color": "slate", "color": "gray",
"interval": 1800000, "interval": 600000,
"title": "今日最热" "title": "今日最热"
}, },
"ghxi": { "idcflare-hot": {
"name": "果核剥壳", "name": "IDC Flare",
"column": "china",
"home": "https://www.ghxi.com/",
"color": "yellow",
"interval": 600000
},
"smzdm": {
"name": "什么值得买",
"type": "hottest", "type": "hottest",
"column": "china", "column": "tech",
"home": "https://www.smzdm.com", "home": "https://idcflare.com",
"color": "red", "color": "gray",
"interval": 600000 "interval": 600000,
"title": "今日最热"
}, },
"nowcoder": { "nowcoder": {
"name": "牛客", "name": "牛客",
@@ -508,5 +490,42 @@
"color": "green", "color": "green",
"interval": 1800000, "interval": 1800000,
"title": "最热" "title": "最热"
},
"douban": {
"title": "热门电影",
"name": "豆瓣",
"type": "hottest",
"column": "china",
"home": "https://www.douban.com",
"color": "green",
"interval": 600000
},
"steam": {
"title": "在线人数",
"name": "Steam",
"type": "hottest",
"column": "world",
"home": "https://store.steampowered.com",
"color": "blue",
"interval": 600000
},
"tencent": {
"redirect": "tencent-hot",
"name": "腾讯新闻",
"type": "hottest",
"column": "china",
"home": "https://news.qq.com/tag/aEWqxLtdgmQ=",
"color": "blue",
"interval": 1800000,
"title": "综合早报"
},
"tencent-hot": {
"name": "腾讯新闻",
"type": "hottest",
"column": "china",
"home": "https://news.qq.com/tag/aEWqxLtdgmQ=",
"color": "blue",
"interval": 1800000,
"title": "综合早报"
} }
} }
+65 -45
View File
@@ -8,8 +8,10 @@ import { useThrottleFn } from "ahooks"
import { useAutoAnimate } from "@formkit/auto-animate/react" import { useAutoAnimate } from "@formkit/auto-animate/react"
import { motion } from "framer-motion" import { motion } from "framer-motion"
import { useWindowSize } from "react-use" import { useWindowSize } from "react-use"
import { isMobile } from "react-device-detect"
import { DndContext } from "../common/dnd" import { DndContext } from "../common/dnd"
import { useSortable } from "../common/dnd/useSortable" import { useSortable } from "../common/dnd/useSortable"
import { OverlayScrollbar } from "../common/overlay-scrollbar"
import type { ItemsProps } from "./card" import type { ItemsProps } from "./card"
import { CardWrapper } from "./card" import { CardWrapper } from "./card"
import { currentSourcesAtom } from "~/atoms" import { currentSourcesAtom } from "~/atoms"
@@ -26,58 +28,76 @@ export function Dnd() {
return Math.min(width - 32, WIDTH) return Math.min(width - 32, WIDTH)
}, [width]) }, [width])
if (!items.length) return null
return ( return (
<DndWrapper items={items} setItems={setItems}> <DndWrapper items={items} setItems={setItems} isSingleColumn={isMobile}>
<motion.ol <OverlayScrollbar defer className="overflow-x-auto">
className="grid w-full gap-6" <motion.ol
ref={parent} className={isMobile
style={{ ? "flex px-2 gap-6 pb-4 scroll-smooth"
gridTemplateColumns: `repeat(auto-fill, minmax(${minWidth}px, 1fr))`, : "grid w-full gap-6"}
}} ref={parent}
initial="hidden" style={isMobile
animate="visible" ? {
variants={{ // 横向滚动布局
hidden: { }
opacity: 0, : {
}, gridTemplateColumns: `repeat(auto-fill, minmax(${minWidth}px, 1fr))`,
visible: { }}
opacity: 1, initial="hidden"
transition: { animate="visible"
delayChildren: 0.1, variants={{
staggerChildren: 0.1, hidden: {
opacity: 0,
}, },
}, visible: {
}} opacity: 1,
> transition: {
{items.map(id => ( delayChildren: 0.1,
<motion.li staggerChildren: 0.1,
key={id}
transition={{
type: "tween",
duration: AnimationDuration / 1000,
}}
variants={{
hidden: {
y: 20,
opacity: 0,
}, },
visible: { },
y: 0, }}
opacity: 1, >
}, {items.map((id, index) => (
}} <motion.li
> key={id}
<SortableCardWrapper id={id} /> className={$(isMobile && "flex-shrink-0", isMobile && index === items.length - 1 && "mr-2")}
</motion.li> style={isMobile ? { width: `${width - 16 > WIDTH ? WIDTH : width - 16}px` } : undefined}
))} transition={{
</motion.ol> type: "tween",
duration: AnimationDuration / 1000,
}}
variants={{
hidden: {
y: 20,
opacity: 0,
},
visible: {
y: 0,
opacity: 1,
},
}}
>
<SortableCardWrapper id={id} />
</motion.li>
))}
</motion.ol>
</OverlayScrollbar>
{isMobile && (
<div className="flex justify-center">
<span className="text-sm text-gray-500 text-center"></span>
</div>
)}
</DndWrapper> </DndWrapper>
) )
} }
function DndWrapper({ items, setItems, children }: PropsWithChildren<{ function DndWrapper({ items, setItems, isSingleColumn, children }: PropsWithChildren<{
items: SourceID[] items: SourceID[]
setItems: (items: SourceID[]) => void setItems: (items: SourceID[]) => void
isSingleColumn: boolean
}>) { }>) {
const onDropTargetChange = useCallback(({ location, source }: BaseEventPayload<ElementDragType>) => { const onDropTargetChange = useCallback(({ location, source }: BaseEventPayload<ElementDragType>) => {
const traget = location.current.dropTargets[0] const traget = location.current.dropTargets[0]
@@ -91,10 +111,10 @@ function DndWrapper({ items, setItems, children }: PropsWithChildren<{
startIndex: fromIndex, startIndex: fromIndex,
indexOfTarget: toIndex, indexOfTarget: toIndex,
closestEdgeOfTarget, closestEdgeOfTarget,
axis: "vertical", axis: isSingleColumn ? "horizontal" : "vertical",
}) })
setItems(update) setItems(update)
}, [items, setItems]) }, [items, setItems, isSingleColumn])
// 避免动画干扰 // 避免动画干扰
const { run } = useThrottleFn(onDropTargetChange, { const { run } = useThrottleFn(onDropTargetChange, {
leading: true, leading: true,
+1 -1
View File
@@ -19,7 +19,7 @@
} }
[cmdk-list] { [cmdk-list] {
--at-apply: px-3 flex flex-col gap-2 items-stretch h-400px; --at-apply: px-3 flex flex-col gap-2 items-stretch max-h-[400px] h-[calc(100vh-100px)];
} }
[cmdk-group-heading] { [cmdk-group-heading] {
+7
View File
@@ -17,6 +17,12 @@ function GoTop() {
) )
} }
function Github() {
return (
<button type="button" title="Github" className="i-ph:github-logo-duotone btn" onClick={() => window.open(Homepage)} />
)
}
function Refresh() { function Refresh() {
const currentSources = useAtomValue(currentSourcesAtom) const currentSources = useAtomValue(currentSourcesAtom)
const { refresh } = useRefetch() const { refresh } = useRefetch()
@@ -65,6 +71,7 @@ export function Header() {
<span className="justify-self-end flex gap-2 items-center text-xl text-primary-600 dark:text-primary"> <span className="justify-self-end flex gap-2 items-center text-xl text-primary-600 dark:text-primary">
<GoTop /> <GoTop />
<Refresh /> <Refresh />
<Github />
<Menu /> <Menu />
</span> </span>
</> </>
+14 -14
View File
@@ -1,16 +1,16 @@
import { motion } from "framer-motion" import { motion } from "framer-motion"
function ThemeToggle() { // function ThemeToggle() {
const { isDark, toggleDark } = useDark() // const { isDark, toggleDark } = useDark()
return ( // return (
<li onClick={toggleDark} className="cursor-pointer [&_*]:cursor-pointer transition-all"> // <li onClick={toggleDark} className="cursor-pointer [&_*]:cursor-pointer transition-all">
<span className={$("inline-block", isDark ? "i-ph-moon-stars-duotone" : "i-ph-sun-dim-duotone")} /> // <span className={$("inline-block", isDark ? "i-ph-moon-stars-duotone" : "i-ph-sun-dim-duotone")} />
<span> // <span>
{isDark ? "浅色模式" : "深色模式"} // {isDark ? "浅色模式" : "深色模式"}
</span> // </span>
</li> // </li>
) // )
} // }
export function Menu() { export function Menu() {
const { loggedIn, login, logout, userInfo, enableLogin } = useLogin() const { loggedIn, login, logout, userInfo, enableLogin } = useLogin()
@@ -64,7 +64,7 @@ export function Menu() {
<span>Github </span> <span>Github </span>
</li> </li>
))} ))}
<ThemeToggle /> {/* <ThemeToggle /> */}
<li onClick={() => window.open(Homepage)} className="cursor-pointer [&_*]:cursor-pointer transition-all"> <li onClick={() => window.open(Homepage)} className="cursor-pointer [&_*]:cursor-pointer transition-all">
<span className="i-ph:github-logo-duotone inline-block" /> <span className="i-ph:github-logo-duotone inline-block" />
<span>Star on Github </span> <span>Star on Github </span>
@@ -75,7 +75,7 @@ export function Menu() {
> >
<img <img
alt="GitHub stars badge" alt="GitHub stars badge"
src="https://img.shields.io/github/stars/ourongxing/newsnow?logo=github" src="https://img.shields.io/github/stars/ourongxing/newsnow?logo=github&style=flat&labelColor=%235e3c40&color=%23614447"
/> />
</a> </a>
<a <a
@@ -83,7 +83,7 @@ export function Menu() {
> >
<img <img
alt="GitHub forks badge" alt="GitHub forks badge"
src="https://img.shields.io/github/forks/ourongxing/newsnow?logo=github" src="https://img.shields.io/github/forks/ourongxing/newsnow?logo=github&style=flat&labelColor=%235e3c40&color=%23614447"
/> />
</a> </a>
</li> </li>
+8 -5
View File
@@ -4,6 +4,7 @@ import { Outlet, createRootRouteWithContext } from "@tanstack/react-router"
import { TanStackRouterDevtools } from "@tanstack/router-devtools" import { TanStackRouterDevtools } from "@tanstack/router-devtools"
import { ReactQueryDevtools } from "@tanstack/react-query-devtools" import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
import type { QueryClient } from "@tanstack/react-query" import type { QueryClient } from "@tanstack/react-query"
import { isMobile } from "react-device-detect"
import { Header } from "~/components/header" import { Header } from "~/components/header"
import { GlobalOverlayScrollbar } from "~/components/common/overlay-scrollbar" import { GlobalOverlayScrollbar } from "~/components/common/overlay-scrollbar"
import { Footer } from "~/components/footer" import { Footer } from "~/components/footer"
@@ -30,11 +31,13 @@ function RootComponent() {
usePWA() usePWA()
return ( return (
<> <>
<GlobalOverlayScrollbar className={$([ <GlobalOverlayScrollbar
"h-full overflow-x-auto px-4", className={$([
"md:(px-10)", !isMobile && "px-4",
"lg:(px-24)", "h-full overflow-x-auto",
])} "md:(px-10)",
"lg:(px-24)",
])}
> >
<header <header
className={$([ className={$([
+1
View File
@@ -8,6 +8,7 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"strict": true, "strict": true,
"allowJs": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,