34 Commits
Author SHA1 Message Date
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
37 changed files with 5415 additions and 3298 deletions
+3
View File
@@ -0,0 +1,3 @@
# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
.specstory/**
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GGITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
+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
+9 -3
View File
@@ -1,15 +1,21 @@
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
volumes:
newsnow_data:
name: newsnow_data
+1 -14
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" />
@@ -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.34",
"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: 15 KiB

+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}`)
} }
} }
+1
View File
@@ -22,6 +22,7 @@ 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 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')
+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))) {
+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
}) })
+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 => ({
+62
View File
@@ -0,0 +1,62 @@
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 flash_list = res.data.find(item => item.name === category)?.child[0]?.flash_list || []
return 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,
pubDate: item.time,
extra: { info: item.type },
url: `https://mktnews.net/flashDetail.html?id=${item.id}`,
}))
})
export default defineSource({
"mktnews": flash,
"mktnews-flash": flash,
})
+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,
+130 -34
View File
@@ -1,48 +1,144 @@
interface Res { export interface Root {
ok: number // 1 is ok ok: number
data: { data: Data
realtime: }
{
num: number // 看上去是个 id export interface Data {
emoticon: string mapi: any[]
icon?: string // 热,新 icon url cards: Card[]
icon_width: number cardlistInfo: CardlistInfo
icon_height: number scheme: string
is_ad?: number // 1 showAppTips: number
note: string }
small_icon_desc: string
icon_desc?: string // 如果是 荐 ,就是广告 export interface Card {
topic_flag: number card_group: CardGroup[]
icon_desc_color: string card_type: number
flag: number show_type: number
word_scheme: string itemid?: string
small_icon_desc_color: string title?: string
realpos: number }
label_name: string
word: string // 热搜词 export interface CardGroup {
rank: number actionlog?: Actionlog
}[] card_type: number
} pic?: string
itemid?: string
icon?: string
icon_width?: number
icon_height?: number
desc?: string
scheme: string
desc_extr: any
promotion?: Promotion
display_arrow?: number
show_type?: number
title?: string
is_show_arrow?: number
left_tag_img?: string
sub_title?: string
}
export interface Actionlog {
luicode: string
lfid: string
uicode: string
act_code: number
act_type: number
fid: string
ext: string
}
export interface Promotion {
monitor_url: MonitorUrl[]
}
export interface MonitorUrl {
third_party_click: string
third_party_show: string
type: string
monitor_name?: string
}
export interface CardlistInfo {
select_id: string
title_top: string
show_style: number
starttime: number
can_shared: number
v_p: string
containerid: string
page_type: string
nick: string
cardlist_head_cards: CardlistHeadCard[]
enable_load_imge_scrolling: number
page_title: string
search_request_id: string
load_more_threshold: number
pagesize: number
page_common_ext: string
cardlist_menus: any[]
total: number
headbg_animation: string
page_size: number
page: any
}
export interface CardlistHeadCard {
title_top?: string
show_menu: boolean
head_type_name: string
head_type: number
head_data?: HeadData
channel_list?: ChannelList[]
}
export interface HeadData {
cover_url: string
show_navi_mask: boolean
show_title: boolean
data_type: number
scheme: string
}
export interface ChannelList {
actionlog: Actionlog2
scheme: string
containerid: string
name: string
default_add: number
id: number
must_show: number
replaced_name?: string
}
export interface Actionlog2 {
luicode: string
lfid: string
uicode: string
act_code: number
act_type: number
fid: string
ext: string
} }
export default defineSource(async () => { export default defineSource(async () => {
const url = "https://weibo.com/ajax/side/hotSearch" const url = "https://m.weibo.cn/api/container/getIndex?containerid=106003type%3D25%26t%3D3%26disable_hot%3D1%26filter_type%3Drealtimehot"
const res: Res = await myFetch(url) const res: Root = await myFetch(url)
return res.data.realtime return res.data.cards[0].card_group
.filter(k => !k.is_ad) .filter((k, i) => i !== 0 && k.desc && !k.actionlog?.ext.includes("ads_word"))
.map((k) => { .map((k) => {
const keyword = k.word_scheme ? k.word_scheme : `#${k.word}#`
return { return {
id: k.word, id: k.desc!,
title: k.word, title: k.desc!,
extra: { extra: {
icon: k.icon && { icon: k.icon && {
url: proxyPicture(k.icon), url: proxyPicture(k.icon),
scale: 1.5, scale: 1.5,
}, },
}, },
url: `https://s.weibo.com/weibo?q=${encodeURIComponent(keyword)}`, url: `https://s.weibo.com/weibo?q=${encodeURIComponent(`#${k.desc}#`)}`,
mobileUrl: `https://m.weibo.cn/search?containerid=231522type%3D1%26q%3D${encodeURIComponent(keyword)}&_T_WM=16922097837&v_p=42`, mobileUrl: k.scheme,
} }
}) })
}) })
+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,
} }
}) })
}, },
+5 -9
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,10 +36,6 @@
"kaopu": "kaopuxinwen", "kaopu": "kaopuxinwen",
"jin10": "jinshishuju", "jin10": "jinshishuju",
"baidu": "baiduresou", "baidu": "baiduresou",
"linuxdo-latest": "LINUX DO-zuixin",
"linuxdo-hot": "LINUX DO-jinrizuire",
"ghxi": "guoheboke",
"smzdm": "shenmezhidemai",
"nowcoder": "niuke", "nowcoder": "niuke",
"sspai": "shaoshupai", "sspai": "shaoshupai",
"juejin": "xitujuejin", "juejin": "xitujuejin",
+50 -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,7 @@ export const originSources = {
column: "tech", column: "tech",
color: "slate", color: "slate",
home: "https://linux.do/", home: "https://linux.do/",
disable: "cf", disable: true,
sub: { sub: {
latest: { latest: {
title: "最新", title: "最新",
@@ -342,6 +355,7 @@ export const originSources = {
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 +363,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: "牛客",
@@ -417,29 +432,40 @@ 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 if (v.disable === true) {
} else { return false
return true } else {
} return true
})) }
}),
)
} }
+24 -50
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,25 +414,6 @@
"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",
@@ -435,21 +424,6 @@
"interval": 1800000, "interval": 1800000,
"title": "今日最热" "title": "今日最热"
}, },
"ghxi": {
"name": "果核剥壳",
"column": "china",
"home": "https://www.ghxi.com/",
"color": "yellow",
"interval": 600000
},
"smzdm": {
"name": "什么值得买",
"type": "hottest",
"column": "china",
"home": "https://www.smzdm.com",
"color": "red",
"interval": 600000
},
"nowcoder": { "nowcoder": {
"name": "牛客", "name": "牛客",
"type": "hottest", "type": "hottest",
@@ -509,4 +483,4 @@
"interval": 1800000, "interval": 1800000,
"title": "最热" "title": "最热"
} }
} }
+2 -2
View File
@@ -236,11 +236,11 @@ function NewsListHot({ items }: { items: NewsItem[] }) {
key={item.id} key={item.id}
title={item.extra?.hover} title={item.extra?.hover}
className={$( className={$(
"flex gap-2 items-center items-stretch relative cursor-pointer [&_*]:cursor-pointer transition-all", "flex gap-2 items-start items-stretch relative cursor-pointer [&_*]:cursor-pointer transition-all",
"hover:bg-neutral-400/10 rounded-md pr-1 visited:(text-neutral-400)", "hover:bg-neutral-400/10 rounded-md pr-1 visited:(text-neutral-400)",
)} )}
> >
<span className={$("bg-neutral-400/10 min-w-6 flex justify-center items-center rounded-md text-sm")}> <span className={$("bg-neutral-400/10 min-w-6 flex justify-center items-center rounded-md text-sm h-6 flex-shrink-0")}>
{i + 1} {i + 1}
</span> </span>
{!!item.extra?.diff && <DiffNumber diff={item.extra.diff} />} {!!item.extra?.diff && <DiffNumber diff={item.extra.diff} />}
+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,