24 Commits

Author SHA1 Message Date
imsyy
bf80a2e22c feat: 新增 虎扑 & 新浪网 & 新浪新闻
- 修复历史上的今天 #63
2024-06-25 13:47:40 +08:00
imsyy
6988df58f1 🐞 fix: 修复历史上的今天 2024-06-24 16:02:06 +08:00
imsyy
bcff976a4d feat: 新增 CSDN 2024-06-17 14:06:22 +08:00
imsyy
62a8880ae4 feat: 输出美化 2024-06-13 17:51:44 +08:00
imsyy
e6f89c4868 🔧 build: 依赖更新 2024-06-13 16:53:59 +08:00
imsyy
fa80a29772 feat: 新增 历史上的今天 2024-06-13 14:09:16 +08:00
imsyy
b8f7c1ad23 🐞 fix: 修复错误包含 ts 文件 2024-06-13 10:12:06 +08:00
imsyy
d5217c3dff 🔧 build: 修正类型错误 2024-06-12 17:37:37 +08:00
imsyy
e1beb5b534 🐞 fix: 修复 hellogithub #60 2024-06-11 09:19:41 +08:00
imsyy
14bc5a1dce feat: 支持 Vercel 部署 2024-06-07 16:03:22 +08:00
imsyy
11addd20ca 🐳 chore: 修复工作流 2024-06-07 09:07:09 +08:00
imsyy
23375519ba 🐳 chore: 修复工作流 2024-06-06 17:50:06 +08:00
imsyy
2ba46c5e0e 🐳 chore: 修复工作流 2024-06-06 17:31:20 +08:00
imsyy
d4a52a6b24 🐞 fix: 修复 Docker 运行出错 2024-06-06 16:27:01 +08:00
imsyy
d73ca1170c feat: 新增 hostloc #43 2024-06-06 11:36:28 +08:00
imsyy
93d90eb653 🐳 chore: Publish npm 2024-06-06 09:39:09 +08:00
imsyy
a3bb42d26c feat: 新增 吾爱破解 2024-06-05 18:16:43 +08:00
imsyy
093312ea8c 🔧 build: fix document 2024-06-04 10:44:19 +08:00
imsyy
13f019a5aa 🐞 fix: 修复哔哩哔哩偶发性失败 #48 #54 2024-05-29 10:11:06 +08:00
imsyy
ce5d381093 Merge branch 'master' of github.com:imsyy/DailyHotApi 2024-05-16 18:11:05 +08:00
imsyy
679fdab87e feat: 新增 虎嗅 & 爱范儿 2024-05-16 18:10:34 +08:00
底层用户
8c8a86957b Merge pull request #49 from zhouzongyan/master
更新知乎日报的链接
2024-05-06 09:46:59 +08:00
allen.zhou
8e077640b1 更新知乎日报 2024-04-30 15:58:44 +08:00
allen.zhou
5555cc6f00 优化知乎拉取 2024-04-30 15:47:14 +08:00
65 changed files with 2520 additions and 1117 deletions

View File

@@ -3,9 +3,10 @@ name: Publish Docker image
on: on:
release: release:
types: [published] types: [published]
workflow_dispatch:
jobs: jobs:
push_to_registry: build-docker:
name: Push Docker image to multiple registries name: Push Docker image to multiple registries
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -15,6 +16,12 @@ jobs:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
@@ -36,11 +43,14 @@ jobs:
imsyy/dailyhot-api imsyy/dailyhot-api
ghcr.io/${{ github.repository }} ghcr.io/${{ github.repository }}
- name: Build and push Docker image - name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: |
linux/amd64
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 }}

31
.github/workflows/npm.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Publish npm package
on:
release:
types: [created]
jobs:
publish-npm:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20]
steps:
- name: Check out the repo
uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Publish to npm
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

2
.npmignore Normal file
View File

@@ -0,0 +1,2 @@
node_modules
dist/logs/

View File

@@ -10,22 +10,22 @@ We pledge to act and interact in ways that contribute to an open, welcoming, div
Examples of behavior that contributes to a positive environment for our community include: Examples of behavior that contributes to a positive environment for our community include:
- Demonstrating empathy and kindness toward other people - Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences - Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback - Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall community - Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include: Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or - The use of sexualized language or imagery, and sexual attention or
advances of any kind advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks - Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment - Public or private harassment
- Publishing others' private information, such as a physical or email - Publishing others' private information, such as a physical or email
address, without their explicit permission address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a - Other conduct which could reasonably be considered inappropriate in a
professional setting professional setting
## Enforcement Responsibilities ## Enforcement Responsibilities

View File

@@ -1,8 +1,17 @@
FROM node:20-alpine AS base FROM node:20-alpine AS base
ENV NODE_ENV=docker
# 安装 Puppeteer 所需的依赖库
RUN apk add libc6-compat
# RUN apk add chromium nss freetype harfbuzz ca-certificates
# 配置 Chromium
# ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
# ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
FROM base AS builder FROM base AS builder
RUN apk add --no-cache libc6-compat
RUN npm install -g pnpm RUN npm install -g pnpm
WORKDIR /app WORKDIR /app
@@ -10,9 +19,9 @@ COPY package*json tsconfig.json pnpm-lock.yaml .env ./
COPY src ./src COPY src ./src
COPY public ./public COPY public ./public
RUN pnpm install && \ RUN pnpm install
pnpm build && \ RUN pnpm build
pnpm prune --production RUN pnpm prune --production
FROM base AS runner FROM base AS runner
WORKDIR /app WORKDIR /app

View File

@@ -2,6 +2,12 @@
<img alt="logo" height="120" src="./public/favicon.png" width="120"/> <img alt="logo" height="120" src="./public/favicon.png" width="120"/>
<h2>今日热榜</h2> <h2>今日热榜</h2>
<p>一个聚合热门数据的 API 接口</p> <p>一个聚合热门数据的 API 接口</p>
<br />
<img src="https://img.shields.io/github/last-commit/imsyy/DailyHotApi" alt="last commit"/>
<img src="https://img.shields.io/github/languages/code-size/imsyy/DailyHotApi" alt="code size"/>
<img src="https://img.shields.io/docker/image-size/imsyy/dailyhot-api" alt="docker-image-size"/>
<img src="https://github.com/imsyy/DailyHotApi/actions/workflows/docker.yml/badge.svg" alt="Publish Docker image"/>
<img src="https://github.com/imsyy/DailyHotApi/actions/workflows/npm.yml/badge.svg" alt="Publish npm package"/>
</div> </div>
## 🚩 特性 ## 🚩 特性
@@ -23,6 +29,9 @@
> 🟢 状态正常 / 🟠 可能失效 / ❌ 无法使用 / ⚠️ 需要科学上网 > 🟢 状态正常 / 🟠 可能失效 / ❌ 无法使用 / ⚠️ 需要科学上网
<details>
<summary>查看全部接口状态</summary>
| **站点** | **类别** | **调用名称** | **状态** | | **站点** | **类别** | **调用名称** | **状态** |
| ---------------- | ------------ | -------------- | -------- | | ---------------- | ------------ | -------------- | -------- |
| 哔哩哔哩 | 热门榜 | bilibili | 🟢 | | 哔哩哔哩 | 热门榜 | bilibili | 🟢 |
@@ -43,9 +52,17 @@
| 今日头条 | 热榜 | toutiao | 🟢 | | 今日头条 | 热榜 | toutiao | 🟢 |
| 36 氪 | 热榜 | 36kr | 🟢 | | 36 氪 | 热榜 | 36kr | 🟢 |
| 51CTO | 推荐榜 | 51cto | 🟢 | | 51CTO | 推荐榜 | 51cto | 🟢 |
| CSDN | 排行榜 | csdn | 🟢 |
| 稀土掘金 | 热榜 | juejin | 🟢 | | 稀土掘金 | 热榜 | juejin | 🟢 |
| 腾讯新闻 | 热点榜 | qq-news | 🟢 | | 腾讯新闻 | 热点榜 | qq-news | 🟢 |
| 新浪网 | 热榜 | sina | 🟢 |
| 新浪新闻 | 热点榜 | sina-news | 🟢 |
| 网易新闻 | 热点榜 | netease-news | 🟢 | | 网易新闻 | 热点榜 | netease-news | 🟢 |
| 吾爱破解 | 榜单 | 52pojie | ❌ |
| 全球主机交流 | 榜单 | hostloc | ❌ |
| 虎嗅 | 24小时 | huxiu | 🟢 |
| 虎扑 | 步行街热帖 | hupu | 🟢 |
| 爱范儿 | 快讯 | ifanr | 🟢 |
| 英雄联盟 | 更新公告 | lol | 🟢 | | 英雄联盟 | 更新公告 | lol | 🟢 |
| 原神 | 最新消息 | genshin | 🟢 | | 原神 | 最新消息 | genshin | 🟢 |
| 崩坏3 | 最新动态 | honkai | 🟢 | | 崩坏3 | 最新动态 | honkai | 🟢 |
@@ -56,6 +73,28 @@
| HelloGitHub | Trending | hellogithub | 🟢 | | HelloGitHub | Trending | hellogithub | 🟢 |
| 中央气象台 | 全国气象预警 | weatheralarm | 🟢 | | 中央气象台 | 全国气象预警 | weatheralarm | 🟢 |
| 中国地震台 | 地震速报 | earthquake | 🟢 | | 中国地震台 | 地震速报 | earthquake | 🟢 |
| 历史上的今天 | 月-日 | history | 🟢 |
</details>
## ⚙️ 使用
本项目支持 `Node.js` 调用,可在安装完成后调用 `serveHotApi` 来开启服务器
```bash
pnpm add dailyhot-api
```
```js
import serveHotApi from "dailyhot-api";
/**
* 启动服务器
* @param {Number} [port] - 端口号
* @returns {Promise<void>}
*/
serveHotApi(3000);
```
## ⚙️ 部署 ## ⚙️ 部署
@@ -119,6 +158,12 @@ npm run start
成功启动后程序会在控制台输出可访问的地址 成功启动后程序会在控制台输出可访问的地址
### Vercel 部署
本项目支持通过 `Vercel` 进行一键部署,点击下方按钮或前往 [项目仓库](https://github.com/imsyy/DailyHotApi-Vercel) 进行手动部署
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/imsyys-projects/clone?repository-url=https%3A%2F%2Fgithub.com%2Fimsyy%2FDailyHotApi-Vercel)
### Railway 部署 ### Railway 部署
本项目支持使用 [Railway](https://railway.app/) 一键部署,请先将本项目 fork 到您的仓库中,即可使用一键部署。 本项目支持使用 [Railway](https://railway.app/) 一键部署,请先将本项目 fork 到您的仓库中,即可使用一键部署。
@@ -127,12 +172,6 @@ npm run start
本项目支持使用 [Zeabur](https://zeabur.com/) 一键部署,请先将本项目 fork 到您的仓库中,即可使用一键部署。 本项目支持使用 [Zeabur](https://zeabur.com/) 一键部署,请先将本项目 fork 到您的仓库中,即可使用一键部署。
### Vercel 部署
> 🚧 Vercel 部署支持正在修复中
若您目前仅能通过 `Vercel` 进行部署,那么请暂时不要使用最新版本
## ⚠️ 须知 ## ⚠️ 须知
- 本项目为了避免频繁请求官方数据,默认对数据做了缓存处理,默认为 `60` 分钟,如需更改,请自行修改配置 - 本项目为了避免频繁请求官方数据,默认对数据做了缓存处理,默认为 `60` 分钟,如需更改,请自行修改配置

View File

@@ -1,6 +1,6 @@
{ {
"name": "dailyhot_api", "name": "dailyhot-api",
"version": "2.0.0-rc.2", "version": "2.0.3",
"description": "An Api on Today's Hot list", "description": "An Api on Today's Hot list",
"keywords": [ "keywords": [
"API", "API",
@@ -12,39 +12,52 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/DIYgod/RSSHub.git" "url": "git+https://github.com/imsyy/DailyHotApi.git"
}, },
"license": "MIT", "license": "MIT",
"author": "imsyy", "author": "imsyy",
"main": "src/index.ts", "main": "dist/index.js",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"files": [
"LICENSE",
"README.md",
"dist/**/*",
"!dist/logs/**/*"
],
"scripts": { "scripts": {
"format": "prettier --write .", "format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix", "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
"dev": "tsx watch --no-cache src/index.ts", "dev": "cross-env NODE_ENV=development tsx watch --no-cache src/index.ts",
"dev:cache": "tsx watch src/index.ts", "dev:cache": "cross-env NODE_ENV=development tsx watch src/index.ts",
"build": "tsc --project tsconfig.json", "build": "tsc --project tsconfig.json",
"start": "tsx src/index.ts" "start": "tsx src/index.ts"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@hono/node-server": "^1.9.1", "@hono/node-server": "^1.11.2",
"axios": "^1.6.8", "axios": "^1.7.2",
"chalk": "^5.3.0",
"cheerio": "1.0.0-rc.12", "cheerio": "1.0.0-rc.12",
"dayjs": "^1.11.11",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"feed": "^4.2.2", "feed": "^4.2.2",
"hono": "^4.2.2", "hono": "^4.4.5",
"md5": "^2.3.0", "md5": "^2.3.0",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"rss-parser": "^3.13.0",
"winston": "^3.13.0" "winston": "^3.13.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.12.5", "@types/node": "^20.14.2",
"@typescript-eslint/eslint-plugin": "^7.5.0", "@typescript-eslint/eslint-plugin": "^7.13.0",
"@typescript-eslint/parser": "^7.5.0", "@typescript-eslint/parser": "^7.13.0",
"cross-env": "^7.0.3",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"prettier": "^3.2.5", "prettier": "^3.3.2",
"tsx": "^3.14.0", "tsx": "^3.14.0",
"typescript": "^5.4.4" "typescript": "^5.4.5"
}, },
"engines": { "engines": {
"node": ">=20" "node": ">=20"

2205
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -3,6 +3,8 @@ import { cors } from "hono/cors";
import { config } from "./config.js"; import { config } from "./config.js";
import { serveStatic } from "@hono/node-server/serve-static"; import { serveStatic } from "@hono/node-server/serve-static";
import { compress } from "hono/compress"; import { compress } from "hono/compress";
import { prettyJSON } from "hono/pretty-json";
import { trimTrailingSlash } from "hono/trailing-slash";
import logger from "./utils/logger.js"; import logger from "./utils/logger.js";
import registry from "./registry.js"; import registry from "./registry.js";
import robotstxt from "./robots.txt.js"; import robotstxt from "./robots.txt.js";
@@ -15,6 +17,12 @@ const app = new Hono();
// 压缩响应 // 压缩响应
app.use(compress()); app.use(compress());
// prettyJSON
app.use(prettyJSON());
// 尾部斜杠重定向
app.use(trimTrailingSlash());
// CORS // CORS
app.use( app.use(
"*", "*",

View File

@@ -27,7 +27,7 @@ const getNumericEnvVariable = (key: string, defaultValue: number): number => {
const value = getEnvVariable(key) ?? String(defaultValue); const value = getEnvVariable(key) ?? String(defaultValue);
const parsedValue = parseInt(value, 10); const parsedValue = parseInt(value, 10);
if (isNaN(parsedValue)) { if (isNaN(parsedValue)) {
return defaultValue return defaultValue;
} }
return parsedValue; return parsedValue;
}; };

View File

@@ -3,13 +3,23 @@ import { config } from "./config.js";
import logger from "./utils/logger.js"; import logger from "./utils/logger.js";
import app from "./app.js"; import app from "./app.js";
logger.info(`🔥 DailyHot API 成功在端口 ${config.PORT} 上运行`);
logger.info(`🔗 Local: 👉 http://localhost:${config.PORT}`);
// 启动服务器 // 启动服务器
const server = serve({ const serveHotApi: (port?: number) => void = (port: number = config.PORT) => {
fetch: app.fetch, try {
port: config.PORT, const apiServer = serve({
}); fetch: app.fetch,
port,
});
logger.info(`🔥 DailyHot API 成功在端口 ${port} 上运行`);
logger.info(`🔗 Local: 👉 http://localhost:${port}`);
return apiServer;
} catch (error) {
logger.error(error);
}
};
export default server; if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "docker") {
serveHotApi(config.PORT);
}
export default serveHotApi;

View File

@@ -14,6 +14,9 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
let allRoutePath: Array<string> = []; let allRoutePath: Array<string> = [];
const routersDirName: string = "routes"; const routersDirName: string = "routes";
// 排除路由
const excludeRoutes: Array<string> = ["52pojie", "hostloc"];
// 建立完整目录路径 // 建立完整目录路径
const routersDirPath = path.join(__dirname, routersDirName); const routersDirPath = path.join(__dirname, routersDirName);
@@ -29,7 +32,11 @@ const findTsFiles = (dirPath: string, allFiles: string[] = [], basePath: string
if (stat.isDirectory()) { if (stat.isDirectory()) {
// 如果是文件夹,递归查找 // 如果是文件夹,递归查找
findTsFiles(fullPath, allFiles, relativePath); findTsFiles(fullPath, allFiles, relativePath);
} else if (stat.isFile() && (item.endsWith(".ts") || item.endsWith(".js"))) { } else if (
stat.isFile() &&
(item.endsWith(".ts") || item.endsWith(".js")) &&
!item.endsWith(".d.ts")
) {
// 符合条件 // 符合条件
allFiles.push(relativePath.replace(/\.(ts|js)$/, "")); allFiles.push(relativePath.replace(/\.(ts|js)$/, ""));
} }
@@ -47,6 +54,10 @@ if (fs.existsSync(routersDirPath) && fs.statSync(routersDirPath).isDirectory())
// 注册全部路由 // 注册全部路由
for (let index = 0; index < allRoutePath.length; index++) { for (let index = 0; index < allRoutePath.length; index++) {
const router = allRoutePath[index]; const router = allRoutePath[index];
// 是否处于排除名单
if (excludeRoutes.includes(router)) {
continue;
}
const listApp = app.basePath(`/${router}`); const listApp = app.basePath(`/${router}`);
// 返回榜单 // 返回榜单
listApp.get("/", async (c) => { listApp.get("/", async (c) => {
@@ -85,6 +96,16 @@ for (let index = 0; index < allRoutePath.length; index++) {
...listData, ...listData,
}); });
}); });
// 请求方式错误
listApp.all("*", (c) =>
c.json(
{
code: 405,
message: "Method Not Allowed",
},
405,
),
);
} }
// 获取全部路由 // 获取全部路由
@@ -93,10 +114,20 @@ app.get("/all", (c) =>
{ {
code: 200, code: 200,
count: allRoutePath.length, count: allRoutePath.length,
routes: allRoutePath.map((path) => ({ routes: allRoutePath.map((path) => {
name: path, // 是否处于排除名单
path: `/${path}`, if (excludeRoutes.includes(path)) {
})), return {
name: path,
path: null,
message: "该接口暂时下线",
};
}
return {
name: path,
path: `/${path}`,
};
}),
}, },
200, 200,
), ),

View File

@@ -1,6 +1,7 @@
export type RouterType = { export type RouterType = {
"36kr": { "36kr": {
itemId: number; itemId: number;
publishTime: number;
templateMaterial: { templateMaterial: {
widgetTitle: string; widgetTitle: string;
authorName: string; authorName: string;
@@ -16,6 +17,7 @@ export type RouterType = {
hotEvent: { hotEvent: {
hotScore: number; hotScore: number;
}; };
timestamp: number;
miniProShareImage: string; miniProShareImage: string;
}; };
"netease-news": { "netease-news": {
@@ -23,6 +25,7 @@ export type RouterType = {
imgsrc: string; imgsrc: string;
source: string; source: string;
docid: string; docid: string;
ptime: string;
}; };
"zhihu-daily": { "zhihu-daily": {
id: number; id: number;
@@ -38,16 +41,28 @@ export type RouterType = {
cover: string; cover: string;
abstract: string; abstract: string;
source_id: number; source_id: number;
pubdate: string;
};
discuz: {
title: string;
link: string;
guid: string;
content?: string;
pubDate?: string;
author?: string;
}; };
bilibili: { bilibili: {
bvid: string; bvid: string;
title: string; title: string;
desc: string; desc?: string;
pic: string; pubdate: string;
owner: { pic?: string;
author?: string;
video_review?: number;
owner?: {
name: string; name: string;
}; };
stat: { stat?: {
view: number; view: number;
}; };
short_link_v2?: string; short_link_v2?: string;
@@ -72,12 +87,14 @@ export type RouterType = {
note: string; note: string;
category: string; category: string;
raw_hot: number; raw_hot: number;
onboard_time: number;
}; };
zhihu: { zhihu: {
target: { target: {
id: number; id: number;
title: string; title: string;
excerpt: string; excerpt: string;
created: number;
}; };
children: [ children: [
{ {
@@ -90,6 +107,7 @@ export type RouterType = {
sentence_id: string; sentence_id: string;
word: string; word: string;
hot_value: number; hot_value: number;
event_time: number;
}; };
baidu: { baidu: {
index: number; index: number;
@@ -107,6 +125,7 @@ export type RouterType = {
subject: string; subject: string;
content: string; content: string;
cover: string; cover: string;
created_at: number;
}; };
stat: { stat: {
view_num: number; view_num: number;
@@ -128,6 +147,7 @@ export type RouterType = {
intro: string; intro: string;
cover: string; cover: string;
author: string; author: string;
publishTime: string;
}; };
}; };
toutiao: { toutiao: {
@@ -143,6 +163,7 @@ export type RouterType = {
name: string; name: string;
pic: string; pic: string;
praiseTimes: string; praiseTimes: string;
pubTimeLong: number;
}; };
sspai: { sspai: {
id: number; id: number;
@@ -150,6 +171,7 @@ export type RouterType = {
summary: string; summary: string;
banner: string; banner: string;
like_count: number; like_count: number;
released_time: number;
author: { author: {
nickname: string; nickname: string;
}; };
@@ -160,6 +182,7 @@ export type RouterType = {
sTitle: string; sTitle: string;
iTotalPlay: string; iTotalPlay: string;
iDocID: string; iDocID: string;
sCreated: string;
}; };
ngabbs: { ngabbs: {
tid: number; tid: number;
@@ -167,6 +190,7 @@ export type RouterType = {
author: string; author: string;
tpcurl: string; tpcurl: string;
replies: number; replies: number;
postdate: number;
}; };
tieba: { tieba: {
topic_id: number; topic_id: number;
@@ -175,6 +199,7 @@ export type RouterType = {
topic_pic: string; topic_pic: string;
topic_url: string; topic_url: string;
discuss_num: number; discuss_num: number;
create_time: number;
}; };
acfun: { acfun: {
dougaId: string; dougaId: string;
@@ -183,6 +208,7 @@ export type RouterType = {
contentDesc: string; contentDesc: string;
likeCount: number; likeCount: number;
coverUrl: string; coverUrl: string;
contributeTime: number;
}; };
hellogithub: { hellogithub: {
item_id: string; item_id: string;
@@ -191,6 +217,7 @@ export type RouterType = {
description: string; description: string;
summary: string; summary: string;
clicks_total: number; clicks_total: number;
updated_at: string;
}; };
v2ex: { v2ex: {
title: string; title: string;
@@ -214,4 +241,70 @@ export type RouterType = {
url: string; url: string;
pic: string; pic: string;
}; };
huxiu: {
object_id: number;
content: string;
url: string;
user_info: {
username: string;
};
publish_time: string;
};
ifanr: {
buzz_original_url: string;
id: number;
post_content: string;
post_id: number;
post_title: string;
like_count: number;
comment_count: number;
created_at: number;
};
csdn: {
nickName: string;
articleTitle: string;
articleDetailUrl: string;
picList: [string];
hotRankScore: string;
period: string;
productId: string;
};
history: {
year: string;
title: string;
link: string;
desc: string;
cover: string;
pic_share: string;
};
hupu: {
tid: number;
title: string;
replies: number;
username: string;
time: string;
url: string;
};
sina: {
base: {
base: {
uniqueId: string;
url: string;
};
};
info: {
hotValue: string;
title: string;
};
};
"sina-news": {
id: string;
title: string;
media: string;
url: string;
create_date: string;
create_time: string;
top_num: string;
time: string;
};
}; };

View File

@@ -1,6 +1,7 @@
import type { RouterData, ListContext, Options } from "../types.js"; import type { RouterData, ListContext, Options } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { post } from "../utils/getData.js"; import { post } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (c: ListContext, noCache: boolean) => { export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "hot"; const type = c.req.query("type") || "hot";
@@ -9,7 +10,7 @@ export const handleRoute = async (c: ListContext, noCache: boolean) => {
name: "36kr", name: "36kr",
title: "36氪", title: "36氪",
type: "热榜", type: "热榜",
parameData: { params: {
type: { type: {
name: "热榜分类", name: "热榜分类",
type: { type: {
@@ -65,6 +66,7 @@ const getList = async (options: Options, noCache: boolean) => {
title: item.widgetTitle, title: item.widgetTitle,
cover: item.widgetImage, cover: item.widgetImage,
author: item.authorName, author: item.authorName,
timestamp: getTime(v.publishTime),
hot: item.statCollect, hot: item.statCollect,
url: `https://www.36kr.com/p/${v.itemId}`, url: `https://www.36kr.com/p/${v.itemId}`,
mobileUrl: `https://m.36kr.com/p/${v.itemId}`, mobileUrl: `https://m.36kr.com/p/${v.itemId}`,

View File

@@ -2,6 +2,7 @@ import type { RouterData } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { getToken, sign } from "../utils/getToken/51cto.js"; import { getToken, sign } from "../utils/getToken/51cto.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (_: undefined, noCache: boolean) => { export const handleRoute = async (_: undefined, noCache: boolean) => {
const { fromCache, data, updateTime } = await getList(noCache); const { fromCache, data, updateTime } = await getList(noCache);
@@ -47,6 +48,7 @@ const getList = async (noCache: boolean) => {
title: v.title, title: v.title,
cover: v.cover, cover: v.cover,
desc: v.abstract, desc: v.abstract,
timestamp: getTime(v.pubdate),
url: v.url, url: v.url,
mobileUrl: v.url, mobileUrl: v.url,
})), })),

66
src/routes/52pojie.ts Normal file
View File

@@ -0,0 +1,66 @@
import type { RouterData, ListContext, Options } from "../types.js";
import type { RouterType } from "../router.types.js";
import { web } from "../utils/getData.js";
import { extractRss, parseRSS } from "../utils/parseRSS.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "hot";
const { fromCache, data, updateTime } = await getList({ type }, noCache);
const routeData: RouterData = {
name: "52pojie",
title: "吾爱破解",
type: "榜单",
params: {
type: {
name: "榜单分类",
type: {
tech: "新鲜出炉",
newthread: "技术分享",
hot: "人气热门",
digest: "精华采撷",
},
},
},
link: "https://www.52pojie.cn/",
total: data?.length || 0,
updateTime,
fromCache,
data,
};
return routeData;
};
const getList = async (options: Options, noCache: boolean) => {
const { type } = options;
const url = `https://www.52pojie.cn/forum.php?mod=guide&view=${type}&rss=1`;
const result = await web({
url,
noCache,
userAgent:
"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Mobile Safari/537.36",
});
const parseData = async () => {
if (typeof result?.data === "string") {
const rssContent = extractRss(result.data);
return await parseRSS(rssContent);
} else {
return [];
}
};
const list = await parseData();
return {
fromCache: result.fromCache,
updateTime: result.updateTime,
data: list.map((v: RouterType["discuz"]) => ({
id: v.guid,
title: v.title,
desc: v.content,
author: v.author,
timestamp: getTime(v.pubDate),
hot: null,
url: v.link,
mobileUrl: v.link,
})),
};
};

View File

@@ -1,6 +1,7 @@
import type { RouterData, ListContext, Options } from "../types.js"; import type { RouterData, ListContext, Options } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (c: ListContext, noCache: boolean) => { export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "-1"; const type = c.req.query("type") || "-1";
@@ -11,7 +12,7 @@ export const handleRoute = async (c: ListContext, noCache: boolean) => {
title: "AcFun", title: "AcFun",
type: "排行榜", type: "排行榜",
description: "AcFun是一家弹幕视频网站致力于为每一个人带来欢乐。", description: "AcFun是一家弹幕视频网站致力于为每一个人带来欢乐。",
parameData: { params: {
type: { type: {
name: "频道", name: "频道",
type: { type: {
@@ -67,6 +68,7 @@ const getList = async (options: Options, noCache: boolean) => {
desc: v.contentDesc, desc: v.contentDesc,
cover: v.coverUrl, cover: v.coverUrl,
author: v.userName, author: v.userName,
timestamp: getTime(v.contributeTime),
hot: v.likeCount, hot: v.likeCount,
url: `https://www.acfun.cn/v/ac${v.dougaId}`, url: `https://www.acfun.cn/v/ac${v.dougaId}`,
mobileUrl: `https://m.acfun.cn/v/?ac=${v.dougaId}`, mobileUrl: `https://m.acfun.cn/v/?ac=${v.dougaId}`,

View File

@@ -9,7 +9,7 @@ export const handleRoute = async (c: ListContext, noCache: boolean) => {
name: "baidu", name: "baidu",
title: "百度", title: "百度",
type: "热搜榜", type: "热搜榜",
parameData: { params: {
type: { type: {
name: "热搜类别", name: "热搜类别",
type: { type: {
@@ -55,6 +55,7 @@ const getList = async (options: Options, noCache: boolean) => {
desc: v.desc, desc: v.desc,
cover: v.img, cover: v.img,
author: v.show?.length ? v.show : "", author: v.show?.length ? v.show : "",
timestamp: null,
hot: Number(v.hotScore), hot: Number(v.hotScore),
url: `https://www.baidu.com/s?wd=${encodeURIComponent(v.query)}`, url: `https://www.baidu.com/s?wd=${encodeURIComponent(v.query)}`,
mobileUrl: v.rawUrl, mobileUrl: v.rawUrl,

View File

@@ -2,6 +2,7 @@ import type { RouterData, ListContext, Options } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import getBiliWbi from "../utils/getToken/bilibili.js"; import getBiliWbi from "../utils/getToken/bilibili.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (c: ListContext, noCache: boolean) => { export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "0"; const type = c.req.query("type") || "0";
@@ -11,7 +12,7 @@ export const handleRoute = async (c: ListContext, noCache: boolean) => {
title: "哔哩哔哩", title: "哔哩哔哩",
type: "热门榜", type: "热门榜",
description: "你所热爱的,就是你的生活", description: "你所热爱的,就是你的生活",
parameData: { params: {
type: { type: {
name: "排行榜分区", name: "排行榜分区",
type: { type: {
@@ -53,19 +54,52 @@ const getList = async (options: Options, noCache: boolean) => {
}, },
noCache, noCache,
}); });
const list = result.data.data.list; // 是否触发风控
return { if (result.data?.data?.list?.length > 0) {
fromCache: result.fromCache, const list = result.data.data.list;
updateTime: result.updateTime, return {
data: list.map((v: RouterType["bilibili"]) => ({ fromCache: result.fromCache,
id: v.bvid, updateTime: result.updateTime,
title: v.title, data: list.map((v: RouterType["bilibili"]) => ({
desc: v.desc, id: v.bvid,
cover: v.pic.replace(/http:/, "https:"), title: v.title,
author: v.owner.name, desc: v.desc || "该视频暂无简介",
hot: v.stat.view, cover: v.pic.replace(/http:/, "https:"),
url: v.short_link_v2 || `https://www.bilibili.com/video/${v.bvid}`, author: v.owner.name,
mobileUrl: `https://m.bilibili.com/video/${v.bvid}`, timestamp: getTime(v.pubdate),
})), hot: v.stat.view,
}; url: v.short_link_v2 || `https://www.bilibili.com/video/${v.bvid}`,
mobileUrl: `https://m.bilibili.com/video/${v.bvid}`,
})),
};
}
// 采用备用接口
else {
const url = `https://api.bilibili.com/x/web-interface/ranking?jsonp=jsonp?rid=${type}&type=1&callback=__jp0`;
const result = await get({
url,
headers: {
Referer: `https://www.bilibili.com/ranking/all`,
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
},
noCache,
});
const list = result.data.data.list;
return {
fromCache: result.fromCache,
updateTime: result.updateTime,
data: list.map((v: RouterType["bilibili"]) => ({
id: v.bvid,
title: v.title,
desc: v.desc || "该视频暂无简介",
cover: v.pic.replace(/http:/, "https:"),
author: v.author,
timestamp: null,
hot: v.video_review,
url: `https://www.bilibili.com/video/${v.bvid}`,
mobileUrl: `https://m.bilibili.com/video/${v.bvid}`,
})),
};
}
}; };

41
src/routes/csdn.ts Normal file
View File

@@ -0,0 +1,41 @@
import type { RouterData } from "../types.js";
import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (_: undefined, noCache: boolean) => {
const { fromCache, data, updateTime } = await getList(noCache);
const routeData: RouterData = {
name: "csdn",
title: "CSDN",
type: "排行榜",
description: "专业开发者社区",
link: "https://www.csdn.net/",
total: data?.length || 0,
updateTime,
fromCache,
data,
};
return routeData;
};
const getList = async (noCache: boolean) => {
const url = "https://blog.csdn.net/phoenix/web/blog/hot-rank?page=0&pageSize=30";
const result = await get({ url, noCache });
const list = result.data.data;
return {
fromCache: result.fromCache,
updateTime: result.updateTime,
data: list.map((v: RouterType["csdn"]) => ({
id: v.productId,
title: v.articleTitle,
cover: v.picList?.[0] || null,
desc: null,
author: v.nickName,
timestamp: getTime(v.period),
hot: Number(v.hotRankScore),
url: v.articleDetailUrl,
mobileUrl: v.articleDetailUrl,
})),
};
};

View File

@@ -42,6 +42,8 @@ const getList = async (noCache: boolean) => {
title: dom.find("h3 a").text().trim(), title: dom.find("h3 a").text().trim(),
cover: dom.find(".pic-wrap img").attr("src"), cover: dom.find(".pic-wrap img").attr("src"),
desc: dom.find(".block p").text().trim(), desc: dom.find(".block p").text().trim(),
timestamp: dom.find("span.pubtime").text().trim(),
hot: null,
url, url,
mobileUrl: `https://m.douban.com/group/topic/${getNumbers(url)}/`, mobileUrl: `https://m.douban.com/group/topic/${getNumbers(url)}/`,
}; };

View File

@@ -18,14 +18,14 @@ export const handleRoute = async (_: undefined, noCache: boolean) => {
}; };
// 数据处理 // 数据处理
const getNumbers = (text: string | undefined) => { const getNumbers = (text: string | undefined): number => {
if (!text) return 10000000; if (!text) return 0;
const regex = /\d+/; const regex = /\d+/;
const match = text.match(regex); const match = text.match(regex);
if (match) { if (match) {
return Number(match[0]); return Number(match[0]);
} else { } else {
return 10000000; return 0;
} }
}; };
@@ -44,15 +44,17 @@ const getList = async (noCache: boolean) => {
const listData = listDom.toArray().map((item) => { const listData = listDom.toArray().map((item) => {
const dom = $(item); const dom = $(item);
const url = dom.find("a").attr("href") || undefined; const url = dom.find("a").attr("href") || undefined;
const score = dom.find(".rating_nums").text() ?? "0.0"; const scoreDom = dom.find(".rating_nums");
const score = scoreDom.length > 0 ? scoreDom.text() : "0.0";
return { return {
id: getNumbers(url), id: getNumbers(url),
title: `${score}${dom.find("a").attr("title")}`, title: `${score}${dom.find("a").attr("title")}`,
cover: dom.find("img").attr("src"), cover: dom.find("img").attr("src"),
desc: dom.find("p.pl").text(), desc: dom.find("p.pl").text(),
timestamp: null,
hot: getNumbers(dom.find("span.pl").text()), hot: getNumbers(dom.find("span.pl").text()),
url, url,
mobileUrl:`https://m.douban.com/movie/subject/${getNumbers(url)}/`, mobileUrl: `https://m.douban.com/movie/subject/${getNumbers(url)}/`,
}; };
}); });
return { return {

View File

@@ -1,6 +1,7 @@
import type { RouterData } from "../types.js"; import type { RouterData } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (_: undefined, noCache: boolean) => { export const handleRoute = async (_: undefined, noCache: boolean) => {
const { fromCache, data, updateTime } = await getList(noCache); const { fromCache, data, updateTime } = await getList(noCache);
@@ -51,6 +52,7 @@ const getList = async (noCache: boolean) => {
data: list.map((v: RouterType["douyin"]) => ({ data: list.map((v: RouterType["douyin"]) => ({
id: v.sentence_id, id: v.sentence_id,
title: v.word, title: v.word,
timestamp: getTime(v.event_time),
hot: v.hot_value, hot: v.hot_value,
url: `https://www.douyin.com/hot/${encodeURIComponent(v.sentence_id)}`, url: `https://www.douyin.com/hot/${encodeURIComponent(v.sentence_id)}`,
mobileUrl: `https://www.douyin.com/hot/${encodeURIComponent(v.sentence_id)}`, mobileUrl: `https://www.douyin.com/hot/${encodeURIComponent(v.sentence_id)}`,

View File

@@ -1,6 +1,7 @@
import type { RouterData, ListContext, Options } from "../types.js"; import type { RouterData, ListContext, Options } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
const mappings = { const mappings = {
O_TIME: "发震时刻(UTC+8)", O_TIME: "发震时刻(UTC+8)",
@@ -32,7 +33,7 @@ export const handleRoute = async (c: ListContext, noCache: boolean) => {
name: "earthquake", name: "earthquake",
title: "中国地震台", title: "中国地震台",
type: "地震速报", type: "地震速报",
parameData: { params: {
type: { type: {
name: "速报分类", name: "速报分类",
type: { type: {
@@ -68,6 +69,8 @@ const getList = async (options: Options, noCache: boolean) => {
id: NEW_DID, id: NEW_DID,
title: `${LOCATION_C}发生${M}级地震`, title: `${LOCATION_C}发生${M}级地震`,
desc: contentBuilder.join("\n"), desc: contentBuilder.join("\n"),
timestamp: getTime(v["O_TIME"]),
hot: null,
url: `https://news.ceic.ac.cn/${NEW_DID}.html`, url: `https://news.ceic.ac.cn/${NEW_DID}.html`,
mobileUrl: `https://news.ceic.ac.cn/${NEW_DID}.html`, mobileUrl: `https://news.ceic.ac.cn/${NEW_DID}.html`,
}; };

View File

@@ -1,6 +1,7 @@
import type { RouterData, ListContext, Options } from "../types.js"; import type { RouterData, ListContext, Options } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (c: ListContext, noCache: boolean) => { export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "1"; const type = c.req.query("type") || "1";
@@ -9,7 +10,7 @@ export const handleRoute = async (c: ListContext, noCache: boolean) => {
name: "genshin", name: "genshin",
title: "原神", title: "原神",
type: "最新动态", type: "最新动态",
parameData: { params: {
type: { type: {
name: "榜单分类", name: "榜单分类",
type: { type: {
@@ -44,6 +45,7 @@ const getList = async (options: Options, noCache: boolean) => {
desc: data.content, desc: data.content,
cover: data.cover, cover: data.cover,
author: v.user.nickname, author: v.user.nickname,
timestamp: getTime(data.created_at),
hot: v.stat.view_num, hot: v.stat.view_num,
url: `https://www.miyoushe.com/ys/article/${data.post_id}`, url: `https://www.miyoushe.com/ys/article/${data.post_id}`,
mobileUrl: `https://m.miyoushe.com/ys/#/article/${data.post_id}`, mobileUrl: `https://m.miyoushe.com/ys/#/article/${data.post_id}`,

View File

@@ -1,21 +1,22 @@
import type { RouterData, ListContext, Options } from "../types.js"; import type { RouterData, ListContext, Options } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (c: ListContext, noCache: boolean) => { export const handleRoute = async (c: ListContext, noCache: boolean) => {
const sort = c.req.query("sort") || "hot"; const sort = c.req.query("sort") || "featured";
const { fromCache, data, updateTime } = await getList({ sort }, noCache); const { fromCache, data, updateTime } = await getList({ sort }, noCache);
const routeData: RouterData = { const routeData: RouterData = {
name: "hellogithub", name: "hellogithub",
title: "HelloGitHub", title: "HelloGitHub",
type: "热门仓库", type: "热门仓库",
description: "分享 GitHub 上有趣、入门级的开源项目", description: "分享 GitHub 上有趣、入门级的开源项目",
parameData: { params: {
sort: { sort: {
name: "排行榜分区", name: "排行榜分区",
type: { type: {
hot: "热门", featured: "精选",
last: "最新", all: "全部",
}, },
}, },
}, },
@@ -41,6 +42,7 @@ const getList = async (options: Options, noCache: boolean) => {
title: v.title, title: v.title,
desc: v.summary, desc: v.summary,
author: v.author, author: v.author,
timestamp: getTime(v.updated_at),
hot: v.clicks_total, hot: v.clicks_total,
url: `https://hellogithub.com/repository/${v.item_id}`, url: `https://hellogithub.com/repository/${v.item_id}`,
mobileUrl: `https://hellogithub.com/repository/${v.item_id}`, mobileUrl: `https://hellogithub.com/repository/${v.item_id}`,

56
src/routes/history.ts Normal file
View File

@@ -0,0 +1,56 @@
import type { RouterData, ListContext, Options } from "../types.js";
import type { RouterType } from "../router.types.js";
import { load } from "cheerio";
import { get } from "../utils/getData.js";
import { getCurrentDateTime } from "../utils/getTime.js";
export const handleRoute = async (c: ListContext, noCache: boolean) => {
// 获取日期
const day = c.req.query("day") || getCurrentDateTime(true).day;
const month = c.req.query("month") || getCurrentDateTime(true).month;
const { fromCache, data, updateTime } = await getList({ month, day }, noCache);
const routeData: RouterData = {
name: "history",
title: "历史上的今天",
type: `${month}-${day}`,
params: {
month: "月份",
day: "日期",
},
link: "https://baike.baidu.com/calendar",
total: data?.length || 0,
updateTime,
fromCache,
data,
};
return routeData;
};
const getList = async (options: Options, noCache: boolean) => {
const { month, day } = options;
const monthStr = month.toString().padStart(2, "0");
const dayStr = day.toString().padStart(2, "0");
const url = `https://baike.baidu.com/cms/home/eventsOnHistory/${monthStr}.json`;
const result = await get({
url,
noCache,
params: {
_: new Date().getTime(),
},
});
const list = result.data[monthStr][monthStr + dayStr];
return {
fromCache: result.fromCache,
updateTime: result.updateTime,
data: list.map((v: RouterType["history"], index: number) => ({
id: index,
title: load(v.title).text().trim(),
cover: v.cover ? v.pic_share : null || null,
desc: load(v.desc).text().trim(),
year: v.year,
timestamp: null,
hot: null,
url: v.link,
mobileUrl: v.link,
})),
};
};

View File

@@ -1,6 +1,7 @@
import type { RouterData, ListContext, Options } from "../types.js"; import type { RouterData, ListContext, Options } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (c: ListContext, noCache: boolean) => { export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "1"; const type = c.req.query("type") || "1";
@@ -9,7 +10,7 @@ export const handleRoute = async (c: ListContext, noCache: boolean) => {
name: "honkai", name: "honkai",
title: "崩坏3", title: "崩坏3",
type: "最新动态", type: "最新动态",
parameData: { params: {
type: { type: {
name: "榜单分类", name: "榜单分类",
type: { type: {
@@ -44,6 +45,7 @@ const getList = async (options: Options, noCache: boolean) => {
desc: data.content, desc: data.content,
cover: data.cover || v.image_list[0].url, cover: data.cover || v.image_list[0].url,
author: v.user.nickname, author: v.user.nickname,
timestamp: getTime(data.created_at),
hot: v.stat.view_num, hot: v.stat.view_num,
url: `https://www.miyoushe.com/bh3/article/${data.post_id}`, url: `https://www.miyoushe.com/bh3/article/${data.post_id}`,
mobileUrl: `https://m.miyoushe.com/bh3/#/article/${data.post_id}`, mobileUrl: `https://m.miyoushe.com/bh3/#/article/${data.post_id}`,

66
src/routes/hostloc.ts Normal file
View File

@@ -0,0 +1,66 @@
import type { RouterData, ListContext, Options } from "../types.js";
import type { RouterType } from "../router.types.js";
import { web } from "../utils/getData.js";
import { extractRss, parseRSS } from "../utils/parseRSS.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "hot";
const { fromCache, data, updateTime } = await getList({ type }, noCache);
const routeData: RouterData = {
name: "hostloc",
title: "全球主机交流",
type: "榜单",
params: {
type: {
name: "榜单分类",
type: {
hot: "最新热门",
digest: "最新精华",
new: "最新回复",
newthread: "最新发表",
},
},
},
link: "https://hostloc.com/",
total: data?.length || 0,
updateTime,
fromCache,
data,
};
return routeData;
};
const getList = async (options: Options, noCache: boolean) => {
const { type } = options;
const url = `https://hostloc.com/forum.php?mod=guide&view=${type}&rss=1`;
const result = await web({
url,
noCache,
userAgent:
"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Mobile Safari/537.36",
});
const parseData = async () => {
if (typeof result?.data === "string") {
const rssContent = extractRss(result.data);
return await parseRSS(rssContent);
} else {
return [];
}
};
const list = await parseData();
return {
fromCache: result.fromCache,
updateTime: result.updateTime,
data: list.map((v: RouterType["discuz"]) => ({
id: v.guid,
title: v.title,
desc: v.content,
author: v.author,
timestamp: getTime(v.pubDate),
hot: null,
url: v.link,
mobileUrl: v.link,
})),
};
};

51
src/routes/hupu.ts Normal file
View File

@@ -0,0 +1,51 @@
import type { RouterData, ListContext, Options } from "../types.js";
import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js";
export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "1";
const { fromCache, data, updateTime } = await getList({ type }, noCache);
const routeData: RouterData = {
name: "hupu",
title: "虎扑",
type: "步行街热帖",
params: {
type: {
name: "榜单分类",
type: {
1: "主干道",
6: "恋爱区",
11: "校园区",
12: "历史区",
612: "摄影区",
},
},
},
link: "https://bbs.hupu.com/all-gambia",
total: data?.length || 0,
updateTime,
fromCache,
data,
};
return routeData;
};
const getList = async (options: Options, noCache: boolean) => {
const { type } = options;
const url = `https://m.hupu.com/api/v2/bbs/topicThreads?topicId=${type}&page=1`;
const result = await get({ url, noCache });
const list = result.data.data.topicThreads;
return {
fromCache: result.fromCache,
updateTime: result.updateTime,
data: list.map((v: RouterType["hupu"]) => ({
id: v.tid,
title: v.title,
author: v.username,
hot: v.replies,
timestamp: null,
url: `https://bbs.hupu.com/${v.tid}.html`,
mobileUrl: v.url,
})),
};
};

54
src/routes/huxiu.ts Normal file
View File

@@ -0,0 +1,54 @@
import type { RouterData } from "../types.js";
import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (_: undefined, noCache: boolean) => {
const { fromCache, data, updateTime } = await getList(noCache);
const routeData: RouterData = {
name: "huxiu",
title: "虎嗅",
type: "24小时",
link: "https://www.huxiu.com/moment/",
total: data?.length || 0,
updateTime,
fromCache,
data,
};
return routeData;
};
// 标题处理
const titleProcessing = (text: string) => {
const paragraphs = text.split("<br><br>");
const title = paragraphs.shift().replace(/。$/, "");
const intro = paragraphs.join("<br><br>");
return { title, intro };
};
const getList = async (noCache: boolean) => {
const url = `https://www.huxiu.com/moment/`;
const result = await get({
url,
noCache,
});
// 正则查找
const pattern =
/<script>[\s\S]*?window\.__INITIAL_STATE__\s*=\s*(\{[\s\S]*?\});[\s\S]*?<\/script>/;
const matchResult = result.data.match(pattern);
const jsonObject = JSON.parse(matchResult[1]).moment.momentList.moment_list.datalist;
return {
fromCache: result.fromCache,
updateTime: result.updateTime,
data: jsonObject.map((v: RouterType["huxiu"]) => ({
id: v.object_id,
title: titleProcessing(v.content).title,
desc: titleProcessing(v.content).intro,
author: v.user_info.username,
timestamp: getTime(v.publish_time),
hot: null,
url: v.url || "https://www.huxiu.com/moment/",
mobileUrl: v.url || "https://m.huxiu.com/moment/",
})),
};
};

39
src/routes/ifanr.ts Normal file
View File

@@ -0,0 +1,39 @@
import type { RouterData } from "../types.js";
import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (_: undefined, noCache: boolean) => {
const { fromCache, data, updateTime } = await getList(noCache);
const routeData: RouterData = {
name: "ifanr",
title: "爱范儿",
type: "快讯",
description: "15秒了解全球新鲜事",
link: "https://www.ifanr.com/digest/",
total: data?.length || 0,
updateTime,
fromCache,
data,
};
return routeData;
};
const getList = async (noCache: boolean) => {
const url = "https://sso.ifanr.com/api/v5/wp/buzz/?limit=20&offset=0";
const result = await get({ url, noCache });
const list = result.data.objects;
return {
fromCache: result.fromCache,
updateTime: result.updateTime,
data: list.map((v: RouterType["ifanr"]) => ({
id: v.id,
title: v.post_title,
desc: v.post_content,
timestamp: getTime(v.created_at),
hot: v.like_count || v.comment_count,
url: `https://www.ifanr.com/${v.id}` || v.buzz_original_url,
mobileUrl: `https://www.ifanr.com/digest/${v.id}` || v.buzz_original_url,
})),
};
};

View File

@@ -1,6 +1,7 @@
import type { RouterData } from "../types.js"; import type { RouterData } from "../types.js";
import { load } from "cheerio"; import { load } from "cheerio";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (_: undefined, noCache: boolean) => { export const handleRoute = async (_: undefined, noCache: boolean) => {
const { fromCache, data, updateTime } = await getList(noCache); const { fromCache, data, updateTime } = await getList(noCache);
@@ -35,11 +36,15 @@ const getList = async (noCache: boolean) => {
const listData = listDom.toArray().map((item) => { const listData = listDom.toArray().map((item) => {
const dom = $(item); const dom = $(item);
const href = dom.find("a").attr("href"); const href = dom.find("a").attr("href");
const time = dom.find("span.time").text().trim();
const match = time.match(/'([^']+)'/);
const dateTime = match ? match[1] : null;
return { return {
id: href ? Number(replaceLink(href, true)) : 100000, id: href ? Number(replaceLink(href, true)) : 100000,
title: dom.find(".newsbody h2").text().trim(), title: dom.find(".newsbody h2").text().trim(),
desc: dom.find(".newsbody p").text().trim(), desc: dom.find(".newsbody p").text().trim(),
cover: dom.find("img").attr("data-original"), cover: dom.find("img").attr("data-original"),
timestamp: getTime(dateTime),
hot: Number(dom.find(".comment").text().replace(/\D/g, "")), hot: Number(dom.find(".comment").text().replace(/\D/g, "")),
url: href || undefined, url: href || undefined,
mobileUrl: href ? replaceLink(href) : undefined, mobileUrl: href ? replaceLink(href) : undefined,

View File

@@ -43,6 +43,7 @@ const getList = async (noCache: boolean) => {
id: href ? Number(replaceLink(href, true)) : 100000, id: href ? Number(replaceLink(href, true)) : 100000,
title: dom.find(".plc-title").text().trim(), title: dom.find(".plc-title").text().trim(),
cover: dom.find("img").attr("data-original"), cover: dom.find("img").attr("data-original"),
timestamp: dom.find("span.post-time").text().trim(),
hot: Number(dom.find(".review-num").text().replace(/\D/g, "")), hot: Number(dom.find(".review-num").text().replace(/\D/g, "")),
url: href ? replaceLink(href) : undefined, url: href ? replaceLink(href) : undefined,
mobileUrl: href || undefined, mobileUrl: href || undefined,

View File

@@ -45,6 +45,8 @@ const getList = async (noCache: boolean) => {
cover: dom.find("img").attr("src"), cover: dom.find("img").attr("src"),
desc: dom.find("p.abstract").text()?.trim(), desc: dom.find("p.abstract").text()?.trim(),
author: dom.find("a.nickname").text()?.trim(), author: dom.find("a.nickname").text()?.trim(),
hot: null,
timestamp: null,
url: `https://www.jianshu.com${href}`, url: `https://www.jianshu.com${href}`,
mobileUrl: `https://www.jianshu.com${href}`, mobileUrl: `https://www.jianshu.com${href}`,
}; };

View File

@@ -29,6 +29,7 @@ const getList = async (noCache: boolean) => {
title: v.content.title, title: v.content.title,
author: v.author.name, author: v.author.name,
hot: v.content_counter.hot_rank, hot: v.content_counter.hot_rank,
timestamp: null,
url: `https://juejin.cn/post/${v.content.content_id}`, url: `https://juejin.cn/post/${v.content.content_id}`,
mobileUrl: `https://juejin.cn/post/${v.content.content_id}`, mobileUrl: `https://juejin.cn/post/${v.content.content_id}`,
})), })),

View File

@@ -1,6 +1,7 @@
import type { RouterData } from "../types.js"; import type { RouterData } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (_: undefined, noCache: boolean) => { export const handleRoute = async (_: undefined, noCache: boolean) => {
const { fromCache, data, updateTime } = await getList(noCache); const { fromCache, data, updateTime } = await getList(noCache);
@@ -31,6 +32,7 @@ const getList = async (noCache: boolean) => {
cover: `https:${v.sIMG}`, cover: `https:${v.sIMG}`,
author: v.sAuthor, author: v.sAuthor,
hot: Number(v.iTotalPlay), hot: Number(v.iTotalPlay),
timestamp: getTime(v.sCreated),
url: `https://lol.qq.com/news/detail.shtml?docid=${encodeURIComponent(v.iDocID)}`, url: `https://lol.qq.com/news/detail.shtml?docid=${encodeURIComponent(v.iDocID)}`,
mobileUrl: `https://lol.qq.com/news/detail.shtml?docid=${encodeURIComponent(v.iDocID)}`, mobileUrl: `https://lol.qq.com/news/detail.shtml?docid=${encodeURIComponent(v.iDocID)}`,
})), })),

View File

@@ -1,6 +1,7 @@
import type { RouterData } from "../types.js"; import type { RouterData } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (_: undefined, noCache: boolean) => { export const handleRoute = async (_: undefined, noCache: boolean) => {
const { fromCache, data, updateTime } = await getList(noCache); const { fromCache, data, updateTime } = await getList(noCache);
@@ -29,6 +30,8 @@ const getList = async (noCache: boolean) => {
title: v.title, title: v.title,
cover: v.imgsrc, cover: v.imgsrc,
author: v.source, author: v.source,
hot: null,
timestamp: getTime(v.ptime),
url: `https://www.163.com/dy/article/${v.docid}.html`, url: `https://www.163.com/dy/article/${v.docid}.html`,
mobileUrl: `https://m.163.com/dy/article/${v.docid}.html`, mobileUrl: `https://m.163.com/dy/article/${v.docid}.html`,
})), })),

View File

@@ -1,6 +1,7 @@
import type { RouterData } from "../types.js"; import type { RouterData } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { post } from "../utils/getData.js"; import { post } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (_: undefined, noCache: boolean) => { export const handleRoute = async (_: undefined, noCache: boolean) => {
const { fromCache, data, updateTime } = await getList(noCache); const { fromCache, data, updateTime } = await getList(noCache);
@@ -48,6 +49,7 @@ const getList = async (noCache: boolean) => {
title: v.subject, title: v.subject,
author: v.author, author: v.author,
hot: v.replies, hot: v.replies,
timestamp: getTime(v.postdate),
url: `https://bbs.nga.cn${v.tpcurl}`, url: `https://bbs.nga.cn${v.tpcurl}`,
mobileUrl: `https://bbs.nga.cn${v.tpcurl}`, mobileUrl: `https://bbs.nga.cn${v.tpcurl}`,
})), })),

View File

@@ -1,6 +1,7 @@
import type { RouterData } from "../types.js"; import type { RouterData } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (_: undefined, noCache: boolean) => { export const handleRoute = async (_: undefined, noCache: boolean) => {
const { fromCache, data, updateTime } = await getList(noCache); const { fromCache, data, updateTime } = await getList(noCache);
@@ -19,7 +20,8 @@ export const handleRoute = async (_: undefined, noCache: boolean) => {
const getList = async (noCache: boolean) => { const getList = async (noCache: boolean) => {
const url = `https://r.inews.qq.com/gw/event/hot_ranking_list?page_size=50`; const url = `https://r.inews.qq.com/gw/event/hot_ranking_list?page_size=50`;
const result = await get({ url, noCache });const list = result.data.idlist[0].newslist.slice(1); const result = await get({ url, noCache });
const list = result.data.idlist[0].newslist.slice(1);
return { return {
fromCache: result.fromCache, fromCache: result.fromCache,
updateTime: result.updateTime, updateTime: result.updateTime,
@@ -30,6 +32,7 @@ const getList = async (noCache: boolean) => {
cover: v.miniProShareImage, cover: v.miniProShareImage,
author: v.source, author: v.source,
hot: v.hotEvent.hotScore, hot: v.hotEvent.hotScore,
timestamp: getTime(v.timestamp),
url: `https://new.qq.com/rain/a/${v.id}`, url: `https://new.qq.com/rain/a/${v.id}`,
mobileUrl: `https://view.inews.qq.com/k/${v.id}`, mobileUrl: `https://view.inews.qq.com/k/${v.id}`,
})), })),

137
src/routes/sina-news.ts Normal file
View File

@@ -0,0 +1,137 @@
import type { RouterData, ListContext, Options } from "../types.js";
import type { RouterType } from "../router.types.js";
import { getTime, getCurrentDateTime } from "../utils/getTime.js";
import { get } from "../utils/getData.js";
// 榜单类别
const listType = {
"1": {
name: "总排行",
www: "news",
params: "www_www_all_suda_suda",
},
"2": {
name: "视频排行",
www: "news",
params: "video_news_all_by_vv",
},
"3": {
name: "图片排行",
www: "news",
params: "total_slide_suda",
},
"4": {
name: "国内新闻",
www: "news",
params: "news_china_suda",
},
"5": {
name: "国际新闻",
www: "news",
params: "news_world_suda",
},
"6": {
name: "社会新闻",
www: "news",
params: "news_society_suda",
},
"7": {
name: "体育新闻",
www: "sports",
params: "sports_suda",
},
"8": {
name: "财经新闻",
www: "finance",
params: "finance_0_suda",
},
"9": {
name: "娱乐新闻",
www: "ent",
params: "ent_suda",
},
"10": {
name: "科技新闻",
www: "tech",
params: "tech_news_suda",
},
"11": {
name: "军事新闻",
www: "news",
params: "news_mil_suda",
},
};
export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "1";
const { fromCache, data, updateTime } = await getList({ type }, noCache);
const routeData: RouterData = {
name: "sina-news",
title: "新浪新闻",
type: listType[type].name,
params: {
type: {
name: "榜单分类",
type: Object.fromEntries(Object.entries(listType).map(([key, value]) => [key, value.name])),
},
},
link: "https://sinanews.sina.cn/",
total: data?.length || 0,
updateTime,
fromCache,
data,
};
return routeData;
};
// JSONP 处理
const parseData = (data: string) => {
// 移除前后多余空白
if (!data) throw new Error("Input data is empty or invalid");
// 提取 JSON 字符串部分
const prefix = "var data = ";
if (!data.startsWith(prefix))
throw new Error("Input data does not start with the expected prefix");
let jsonString = data.slice(prefix.length).trim();
// 确保字符串以 ';' 结尾并移除它
if (jsonString.endsWith(";")) {
jsonString = jsonString.slice(0, -1).trim();
} else {
throw new Error("Input data does not end with a semicolon");
}
// 格式是否正确
if (jsonString.startsWith("{") && jsonString.endsWith("}")) {
// 解析为 JSON 对象
try {
const jsonData = JSON.parse(jsonString);
return jsonData;
} catch (error) {
throw new Error("Failed to parse JSON: " + error.message);
}
} else {
throw new Error("Invalid JSON format");
}
};
const getList = async (options: Options, noCache: boolean) => {
const { type } = options;
// 必要数据
const { params, www } = listType[type];
const { year, month, day } = getCurrentDateTime(true);
const url = `https://top.${www}.sina.com.cn/ws/GetTopDataList.php?top_type=day&top_cat=${params}&top_time=${year + month + day}&top_show_num=50`;
const result = await get({ url, noCache });
const list = parseData(result.data).data;
return {
fromCache: result.fromCache,
updateTime: result.updateTime,
data: list.map((v: RouterType["sina-news"]) => ({
id: v.id,
title: v.title,
author: v.media || null,
hot: parseFloat(v.top_num.replace(/,/g, "")),
timestamp: getTime(v.create_date + " " + v.create_time),
url: v.url,
mobileUrl: v.url,
})),
};
};

62
src/routes/sina.ts Normal file
View File

@@ -0,0 +1,62 @@
import type { RouterData, ListContext, Options } from "../types.js";
import type { RouterType } from "../router.types.js";
import { parseChineseNumber } from "../utils/getNum.js";
import { get } from "../utils/getData.js";
export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "1";
const { fromCache, data, updateTime } = await getList({ type }, noCache);
const routeData: RouterData = {
name: "sina",
title: "新浪网",
type: "热榜太多,一个就够",
params: {
type: {
name: "榜单分类",
type: {
all: "新浪热榜",
hotcmnt: "热议榜",
minivideo: "视频热榜",
ent: "娱乐热榜",
ai: "AI热榜",
auto: "汽车热榜",
mother: "育儿热榜",
fashion: "时尚热榜",
travel: "旅游热榜",
esg: "ESG热榜",
},
},
},
link: "https://sinanews.sina.cn/",
total: data?.length || 0,
updateTime,
fromCache,
data,
};
return routeData;
};
const getList = async (options: Options, noCache: boolean) => {
const { type } = options;
const url = `https://newsapp.sina.cn/api/hotlist?newsId=HB-1-snhs%2Ftop_news_list-${type}`;
const result = await get({ url, noCache });
const list = result.data.data.hotList;
return {
fromCache: result.fromCache,
updateTime: result.updateTime,
data: list.map((v: RouterType["sina"]) => {
const base = v.base;
const info = v.info;
return {
id: base.base.uniqueId,
title: info.title,
desc: null,
author: null,
timestamp: null,
hot: parseChineseNumber(info.hotValue),
url: base.base.url,
mobileUrl: base.base.url,
};
}),
};
};

View File

@@ -1,6 +1,7 @@
import type { RouterData, ListContext, Options } from "../types.js"; import type { RouterData, ListContext, Options } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (c: ListContext, noCache: boolean) => { export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "热门文章"; const type = c.req.query("type") || "热门文章";
@@ -9,7 +10,7 @@ export const handleRoute = async (c: ListContext, noCache: boolean) => {
name: "sspai", name: "sspai",
title: "少数派", title: "少数派",
type: "热榜", type: "热榜",
parameData: { params: {
type: { type: {
name: "分类", name: "分类",
type: ["热门文章", "应用推荐", "生活方式", "效率技巧", "少数派播客"], type: ["热门文章", "应用推荐", "生活方式", "效率技巧", "少数派播客"],
@@ -38,6 +39,7 @@ const getList = async (options: Options, noCache: boolean) => {
desc: v.summary, desc: v.summary,
cover: v.banner, cover: v.banner,
author: v.author.nickname, author: v.author.nickname,
timestamp: getTime(v.released_time),
hot: v.like_count, hot: v.like_count,
url: `https://sspai.com/post/${v.id}`, url: `https://sspai.com/post/${v.id}`,
mobileUrl: `https://sspai.com/post/${v.id}`, mobileUrl: `https://sspai.com/post/${v.id}`,

View File

@@ -1,6 +1,7 @@
import type { RouterData, ListContext, Options } from "../types.js"; import type { RouterData, ListContext, Options } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (c: ListContext, noCache: boolean) => { export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "1"; const type = c.req.query("type") || "1";
@@ -9,7 +10,7 @@ export const handleRoute = async (c: ListContext, noCache: boolean) => {
name: "starrail", name: "starrail",
title: "崩坏:星穹铁道", title: "崩坏:星穹铁道",
type: "最新动态", type: "最新动态",
parameData: { params: {
type: { type: {
name: "榜单分类", name: "榜单分类",
type: { type: {
@@ -44,6 +45,7 @@ const getList = async (options: Options, noCache: boolean) => {
desc: data.content, desc: data.content,
cover: data.cover, cover: data.cover,
author: v.user.nickname, author: v.user.nickname,
timestamp: getTime(data.created_at),
hot: v.stat.view_num, hot: v.stat.view_num,
url: `https://www.miyoushe.com/sr/article/${data.post_id}`, url: `https://www.miyoushe.com/sr/article/${data.post_id}`,
mobileUrl: `https://m.miyoushe.com/sr/#/article/${data.post_id}`, mobileUrl: `https://m.miyoushe.com/sr/#/article/${data.post_id}`,

View File

@@ -1,6 +1,7 @@
import type { RouterData } from "../types.js"; import type { RouterData } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (_: undefined, noCache: boolean) => { export const handleRoute = async (_: undefined, noCache: boolean) => {
const { fromCache, data, updateTime } = await getList(noCache); const { fromCache, data, updateTime } = await getList(noCache);
@@ -29,6 +30,7 @@ const getList = async (noCache: boolean) => {
title: v.name, title: v.name,
cover: v.pic, cover: v.pic,
hot: Number(v.praiseTimes), hot: Number(v.praiseTimes),
timestamp: getTime(v.pubTimeLong),
url: `https://www.thepaper.cn/newsDetail_forward_${v.contId}`, url: `https://www.thepaper.cn/newsDetail_forward_${v.contId}`,
mobileUrl: `https://m.thepaper.cn/newsDetail_forward_${v.contId}`, mobileUrl: `https://m.thepaper.cn/newsDetail_forward_${v.contId}`,
})), })),

View File

@@ -1,6 +1,7 @@
import type { RouterData } from "../types.js"; import type { RouterData } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (_: undefined, noCache: boolean) => { export const handleRoute = async (_: undefined, noCache: boolean) => {
const { fromCache, data, updateTime } = await getList(noCache); const { fromCache, data, updateTime } = await getList(noCache);
@@ -31,6 +32,7 @@ const getList = async (noCache: boolean) => {
desc: v.topic_desc, desc: v.topic_desc,
cover: v.topic_pic, cover: v.topic_pic,
hot: v.discuss_num, hot: v.discuss_num,
timestamp: getTime(v.create_time),
url: v.topic_url, url: v.topic_url,
mobileUrl: v.topic_url, mobileUrl: v.topic_url,
})), })),

View File

@@ -1,6 +1,7 @@
import type { RouterData } from "../types.js"; import type { RouterData } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (_: undefined, noCache: boolean) => { export const handleRoute = async (_: undefined, noCache: boolean) => {
const { fromCache, data, updateTime } = await getList(noCache); const { fromCache, data, updateTime } = await getList(noCache);
@@ -28,6 +29,7 @@ const getList = async (noCache: boolean) => {
id: v.ClusterIdStr, id: v.ClusterIdStr,
title: v.Title, title: v.Title,
cover: v.Image.url, cover: v.Image.url,
timestamp: getTime(v.ClusterIdStr),
hot: Number(v.HotValue), hot: Number(v.HotValue),
url: `https://www.toutiao.com/trending/${v.ClusterIdStr}/`, url: `https://www.toutiao.com/trending/${v.ClusterIdStr}/`,
mobileUrl: `https://api.toutiaoapi.com/feoffline/amos_land/new/html/main/index.html?topic_id=${v.ClusterIdStr}`, mobileUrl: `https://api.toutiaoapi.com/feoffline/amos_land/new/html/main/index.html?topic_id=${v.ClusterIdStr}`,

View File

@@ -9,7 +9,7 @@ export const handleRoute = async (c: ListContext, noCache: boolean) => {
name: "v2ex", name: "v2ex",
title: "V2EX", title: "V2EX",
type: "主题榜", type: "主题榜",
parameData: { params: {
type: { type: {
name: "榜单分类", name: "榜单分类",
type: { type: {
@@ -31,7 +31,7 @@ const getList = async (options: Options, noCache: boolean) => {
const { type } = options; const { type } = options;
const url = `https://www.v2ex.com/api/topics/${type}.json`; const url = `https://www.v2ex.com/api/topics/${type}.json`;
const result = await get({ url, noCache }); const result = await get({ url, noCache });
const list = result.data.data.list; const list = result.data;
return { return {
fromCache: result.fromCache, fromCache: result.fromCache,
updateTime: result.updateTime, updateTime: result.updateTime,
@@ -40,6 +40,7 @@ const getList = async (options: Options, noCache: boolean) => {
title: v.title, title: v.title,
desc: v.content, desc: v.content,
author: v.member.username, author: v.member.username,
timestamp: null,
hot: v.replies, hot: v.replies,
url: v.url, url: v.url,
mobileUrl: v.url, mobileUrl: v.url,

View File

@@ -1,6 +1,7 @@
import type { RouterData, ListContext, Options } from "../types.js"; import type { RouterData, ListContext, Options } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (c: ListContext, noCache: boolean) => { export const handleRoute = async (c: ListContext, noCache: boolean) => {
const province = c.req.query("province") || ""; const province = c.req.query("province") || "";
@@ -9,7 +10,7 @@ export const handleRoute = async (c: ListContext, noCache: boolean) => {
name: "weatheralarm", name: "weatheralarm",
title: "中央气象台", title: "中央气象台",
type: type || "全国气象预警", type: type || "全国气象预警",
parameData: { params: {
province: { province: {
name: "预警区域", name: "预警区域",
value: "省份名称( 例如:广东省 ", value: "省份名称( 例如:广东省 ",
@@ -38,6 +39,8 @@ const getList = async (options: Options, noCache: boolean) => {
title: v.title, title: v.title,
desc: v.issuetime + " " + v.title, desc: v.issuetime + " " + v.title,
cover: v.pic, cover: v.pic,
timestamp: getTime(v.issuetime),
hot: null,
url: `http://nmc.cn${v.url}`, url: `http://nmc.cn${v.url}`,
mobileUrl: `http://nmc.cn${v.url}`, mobileUrl: `http://nmc.cn${v.url}`,
})), })),

View File

@@ -1,6 +1,7 @@
import type { RouterData } from "../types.js"; import type { RouterData } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (_: undefined, noCache: boolean) => { export const handleRoute = async (_: undefined, noCache: boolean) => {
const { fromCache, data, updateTime } = await getList(noCache); const { fromCache, data, updateTime } = await getList(noCache);
@@ -32,6 +33,7 @@ const getList = async (noCache: boolean) => {
title: v.word, title: v.word,
desc: v.note || key, desc: v.note || key,
author: v.category, author: v.category,
timestamp: getTime(v.onboard_time),
hot: v.raw_hot, hot: v.raw_hot,
url: `https://s.weibo.com/weibo?q=${encodeURIComponent(key)}&t=31&band_rank=1&Refer=top`, url: `https://s.weibo.com/weibo?q=${encodeURIComponent(key)}&t=31&band_rank=1&Refer=top`,
mobileUrl: `https://s.weibo.com/weibo?q=${encodeURIComponent( mobileUrl: `https://s.weibo.com/weibo?q=${encodeURIComponent(

View File

@@ -2,6 +2,7 @@ import type { RouterData } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import getWereadID from "../utils/getToken/weread.js"; import getWereadID from "../utils/getToken/weread.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (_: undefined, noCache: boolean) => { export const handleRoute = async (_: undefined, noCache: boolean) => {
const { fromCache, data, updateTime } = await getList(noCache); const { fromCache, data, updateTime } = await getList(noCache);
@@ -40,6 +41,7 @@ const getList = async (noCache: boolean) => {
author: data.author, author: data.author,
desc: data.intro, desc: data.intro,
cover: data.cover.replace("s_", "t9_"), cover: data.cover.replace("s_", "t9_"),
timestamp: getTime(data.publishTime),
hot: v.readingCount, hot: v.readingCount,
url: `https://weread.qq.com/web/bookDetail/${getWereadID(data.bookId)}`, url: `https://weread.qq.com/web/bookDetail/${getWereadID(data.bookId)}`,
mobileUrl: `https://weread.qq.com/web/bookDetail/${getWereadID(data.bookId)}`, mobileUrl: `https://weread.qq.com/web/bookDetail/${getWereadID(data.bookId)}`,

View File

@@ -19,13 +19,13 @@ export const handleRoute = async (_: undefined, noCache: boolean) => {
}; };
const getList = async (noCache: boolean) => { const getList = async (noCache: boolean) => {
const url = `https://news-at.zhihu.com/api/4/news/latest`; const url = `https://daily.zhihu.com/api/4/news/latest`;
const result = await get({ const result = await get({
url, url,
noCache, noCache,
headers: { headers: {
Referer: "https://news-at.zhihu.com/api/4/news/latest", Referer: "https://daily.zhihu.com/api/4/news/latest",
Host: "news-at.zhihu.com", Host: "daily.zhihu.com",
}, },
}); });
const list = result.data.stories.filter((el: RouterType["zhihu-daily"]) => el.type === 0); const list = result.data.stories.filter((el: RouterType["zhihu-daily"]) => el.type === 0);
@@ -37,6 +37,8 @@ const getList = async (noCache: boolean) => {
title: v.title, title: v.title,
cover: v.images[0], cover: v.images[0],
author: v.hint, author: v.hint,
hot: null,
timestamp: null,
url: v.url, url: v.url,
mobileUrl: v.url, mobileUrl: v.url,
})), })),

View File

@@ -1,6 +1,7 @@
import type { RouterData } from "../types.js"; import type { RouterData } from "../types.js";
import type { RouterType } from "../router.types.js"; import type { RouterType } from "../router.types.js";
import { get } from "../utils/getData.js"; import { get } from "../utils/getData.js";
import { getTime } from "../utils/getTime.js";
export const handleRoute = async (_: undefined, noCache: boolean) => { export const handleRoute = async (_: undefined, noCache: boolean) => {
const { fromCache, data, updateTime } = await getList(noCache); const { fromCache, data, updateTime } = await getList(noCache);
@@ -31,6 +32,7 @@ const getList = async (noCache: boolean) => {
title: data.title, title: data.title,
desc: data.excerpt, desc: data.excerpt,
cover: v.children[0].thumbnail, cover: v.children[0].thumbnail,
timestamp: getTime(data.created),
hot: parseInt(v.detail_text.replace(/[^\d]/g, "")) * 10000, hot: parseInt(v.detail_text.replace(/[^\d]/g, "")) * 10000,
url: `https://www.zhihu.com/question/${data.id}`, url: `https://www.zhihu.com/question/${data.id}`,
mobileUrl: `https://www.zhihu.com/question/${data.id}`, mobileUrl: `https://www.zhihu.com/question/${data.id}`,

View File

@@ -10,7 +10,8 @@ export type ListItem = {
cover?: string; cover?: string;
author?: string; author?: string;
desc?: string; desc?: string;
hot?: number; hot: number | null;
timestamp: number | string | null;
url: string | undefined; url: string | undefined;
mobileUrl: string | undefined; mobileUrl: string | undefined;
}; };
@@ -21,7 +22,7 @@ export type RouterData = {
title: string; title: string;
type: string; type: string;
description?: string; description?: string;
parameData?: Record<string, string | object>; params?: Record<string, string | object>;
total: number; total: number;
link?: string; link?: string;
updateTime: string; updateTime: string;
@@ -50,7 +51,15 @@ export type Post = {
originaInfo?: boolean; originaInfo?: boolean;
}; };
export type Web = {
url: string;
timeout?: number;
noCache?: boolean;
ttl?: number;
userAgent?: string;
};
// 参数类型 // 参数类型
export type Options = { export type Options = {
[key: string]: string | undefined; [key: string]: string | number | undefined;
}; };

View File

@@ -1,6 +1,7 @@
import type { Get, Post } from "../types.ts"; import type { Get, Post, Web } from "../types.ts";
import { config } from "../config.js"; import { config } from "../config.js";
import { getCache, setCache, delCache } from "./cache.js"; import { getCache, setCache, delCache } from "./cache.js";
// import { Cluster } from "puppeteer-cluster";
import logger from "./logger.js"; import logger from "./logger.js";
import axios from "axios"; import axios from "axios";
@@ -11,6 +12,28 @@ const request = axios.create({
withCredentials: true, withCredentials: true,
}); });
// puppeteer-cluster
// export const createCluster = async () => {
// return await Cluster.launch({
// concurrency: Cluster.CONCURRENCY_BROWSER,
// maxConcurrency: 5,
// });
// };
// Cluster
// const cluster = await createCluster();
const cluster = null;
// Cluster configuration
// cluster.task(async ({ page, data: { url, userAgent } }) => {
// if (userAgent) {
// await page.setUserAgent(userAgent);
// }
// await page.goto(url, { waitUntil: "networkidle0" });
// const pageContent = await page.content();
// return pageContent;
// });
// 请求拦截 // 请求拦截
request.interceptors.request.use( request.interceptors.request.use(
(request) => { (request) => {
@@ -98,3 +121,33 @@ export const post = async (options: Post) => {
throw error; throw error;
} }
}; };
// puppeteer
export const web = async (options: Web) => {
const { url, noCache, ttl = config.CACHE_TTL, userAgent } = options;
logger.info("使用 Puppeteer 发起页面请求", options);
try {
// 检查缓存
if (noCache) {
delCache(url);
} else {
const cachedData = getCache(url);
if (cachedData) {
logger.info("采用缓存", { url });
return { fromCache: true, data: cachedData.data, updateTime: cachedData.updateTime };
}
}
// 缓存不存在时使用 Puppeteer 请求页面
logger.info("启动浏览器请求页面", { url });
const pageContent = await cluster.execute({ url, userAgent });
// 存储新获取的数据到缓存
const updateTime = new Date().toISOString();
setCache(url, { data: pageContent, updateTime }, ttl);
// 返回数据
logger.info("页面内容获取成功");
return { fromCache: false, data: pageContent, updateTime };
} catch (error) {
logger.error("Puppeteer 请求出错", error);
throw error;
}
};

20
src/utils/getNum.ts Normal file
View File

@@ -0,0 +1,20 @@
export const parseChineseNumber = (chineseNumber: string): number => {
// 单位对照表
const units: { [key: string]: number } = {
亿: 1e8,
: 1e4,
: 1e3,
: 1e2,
};
// 遍历单位对照表
for (const unit in units) {
if (chineseNumber.includes(unit)) {
// 转换为数字
const numberPart = parseFloat(chineseNumber.replace(unit, ""));
return numberPart * units[unit];
}
}
return parseFloat(chineseNumber);
};

82
src/utils/getTime.ts Normal file
View File

@@ -0,0 +1,82 @@
import dayjs from "dayjs";
interface CurrentDateTime {
year: string;
month: string;
day: string;
hour: string;
minute: string;
second: string;
}
export const getTime = (timeInput: string | number): number | null => {
try {
let num: number;
// 处理字符串的情况
if (typeof timeInput === "string") {
// 尝试将字符串直接转换为数字
num = Number(timeInput);
if (isNaN(num)) {
// 将各种分隔符替换为标准格式
let standardizedInput = timeInput
.replace(/(\d{4})-(\d{2})-(\d{2})-(\d{2})/, "$1-$2-$3 $4") // "YYYY-MM-DD-HH" -> "YYYY-MM-DD HH"
.replace(/(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):?(\d{2})?:?(\d{2})?/, "$1-$2-$3 $4:$5:$6") // "YYYY-MM-DDTHH:mm:ss" -> "YYYY-MM-DD HH:mm:ss"
.replace(/(\d{4})[-/](\d{2})[-/](\d{2})/, "$1-$2-$3"); // "YYYY/MM/DD" or "YYYY-MM-DD" -> "YYYY-MM-DD"
// 减少解析过程中可能的多余空格
standardizedInput = standardizedInput.replace(/\s+/, " ").trim();
// 处理标准化后的日期时间字符串
const formatPatterns = [
"YYYY-MM-DD HH:mm:ss",
"YYYY-MM-DD HH:mm",
"YYYY-MM-DD HH",
"YYYY-MM-DD",
];
let parsedDate: dayjs.Dayjs | null = null;
for (const pattern of formatPatterns) {
parsedDate = dayjs(standardizedInput, pattern, true);
if (parsedDate.isValid()) {
break;
}
}
if (parsedDate && parsedDate.isValid()) {
return parsedDate.valueOf();
} else {
return null;
}
}
} else {
num = timeInput;
}
// 是否为毫秒级时间戳
if (num > 946684800000) {
// 以2000年作为毫秒时间戳参考点
return num;
} else {
return num * 1000;
}
} catch (error) {
return null;
}
};
export const getCurrentDateTime = (padZero: boolean = false): CurrentDateTime => {
const now = dayjs();
// 补零
const pad = (num: number): string => (num < 10 ? `0${num}` : `${num}`);
return {
year: now.year().toString(),
month: padZero ? pad(now.month() + 1) : (now.month() + 1).toString(),
day: padZero ? pad(now.date()) : now.date().toString(),
hour: padZero ? pad(now.hour()) : now.hour().toString(),
minute: padZero ? pad(now.minute()) : now.minute().toString(),
second: padZero ? pad(now.second()) : now.second().toString(),
};
};

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import crypto from "crypto"; import crypto from "crypto";
/** /**
@@ -13,7 +14,7 @@ const getWereadID = (bookId: string) => {
// 取哈希结果的前三个字符作为初始值 // 取哈希结果的前三个字符作为初始值
let strSub = str.substring(0, 3); let strSub = str.substring(0, 3);
// 判断书籍 ID 的类型并进行转换 // 判断书籍 ID 的类型并进行转换
let fa; let fa: (string | any[])[];
if (/^\d*$/.test(bookId)) { if (/^\d*$/.test(bookId)) {
// 如果书籍 ID 只包含数字,则将其拆分成长度为 9 的子字符串,并转换为十六进制表示 // 如果书籍 ID 只包含数字,则将其拆分成长度为 9 的子字符串,并转换为十六进制表示
const chunks = []; const chunks = [];

View File

@@ -1,26 +1,54 @@
import { config } from "../config.js"; import { config } from "../config.js";
import { createLogger, format, transports } from "winston"; import { createLogger, format, transports } from "winston";
import path from "path"; import path from "path";
import chalk from "chalk";
let pathOption: (typeof transports.File)[] = []; let pathOption: (typeof transports.File)[] = [];
// 日志输出目录 // 日志输出目录
if (config.USE_LOG_FILE) { if (config.USE_LOG_FILE) {
pathOption = [ try {
new transports.File({ pathOption = [
filename: path.resolve("logs/error.log"), new transports.File({
level: "error", filename: path.resolve("logs/error.log"),
maxsize: 1024 * 1024, level: "error",
maxFiles: 1, maxsize: 1024 * 1024,
}), maxFiles: 1,
new transports.File({ }),
filename: path.resolve("logs/logger.log"), new transports.File({
maxsize: 1024 * 1024, filename: path.resolve("logs/logger.log"),
maxFiles: 1, maxsize: 1024 * 1024,
}), maxFiles: 1,
]; }),
];
} catch (error) {
console.error("Failed to initialize log files. Logging to a file will be skipped.", error);
pathOption = [];
}
} }
// 定义不同日志级别的彩色块
const levelColors: { [key: string]: string } = {
error: chalk.bgRed(" ERROR "),
warn: chalk.bgYellow(" WARN "),
info: chalk.bgBlue(" INFO "),
debug: chalk.bgGreen(" DEBUG "),
default: chalk.bgWhite(" LOG "),
};
// 自定义控制台日志输出格式
const consoleFormat = format.printf(({ level, message, timestamp, stack }) => {
// 获取原始日志级别
const originalLevel = Object.keys(levelColors).find((lvl) => level.includes(lvl)) || "default";
const colorLevel = levelColors[originalLevel] || levelColors.default;
let logMessage = `${colorLevel} [${timestamp}] ${message}`;
if (stack) {
logMessage += `\n${stack}`;
}
return logMessage;
});
// logger // logger
const logger = createLogger({ const logger = createLogger({
// 最低的日志级别 // 最低的日志级别
@@ -39,11 +67,15 @@ const logger = createLogger({
// 控制台输出 // 控制台输出
if (process.env.NODE_ENV !== "production") { if (process.env.NODE_ENV !== "production") {
logger.add( try {
new transports.Console({ logger.add(
format: format.combine(format.colorize(), format.simple()), new transports.Console({
}), format: format.combine(format.colorize(), consoleFormat),
); }),
);
} catch (error) {
console.error("Failed to add console transport. Console logging will be skipped.", error);
}
} }
export default logger; export default logger;

41
src/utils/parseRSS.ts Normal file
View File

@@ -0,0 +1,41 @@
import RSSParser from "rss-parser";
import logger from "./logger.js";
export const extractRss = (content: string): string | null => {
// 匹配 <rss> 标签及内容
const rssRegex = /(<rss[\s\S]*?<\/rss>)/i;
const matches = content.match(rssRegex);
return matches ? matches[0] : null;
};
export const parseRSS = async (rssContent: string) => {
const parser = new RSSParser();
// 是否为网址
const isUrl = (url: string) => {
try {
new URL(url);
return true;
} catch (_) {
return false;
}
};
try {
const feed = isUrl(rssContent)
? await parser.parseURL(rssContent)
: await parser.parseString(rssContent);
const items = feed.items.map((item) => ({
title: item.title, // 文章标题
link: item.link, // 文章链接
pubDate: item.pubDate, // 发布日期
author: item.creator ?? item.author, // 作者
content: item.content, // 内容
contentSnippet: item.contentSnippet, // 内容摘要
guid: item.guid, // 全局唯一标识符
categories: item.categories, // 分类
}));
// 返回解析数据
return items;
} catch (error) {
logger.error("解析 RSS 内容时出错:", error);
return null;
}
};

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,7 @@
import type { FC } from "hono/jsx"; import type { FC } from "hono/jsx";
import { css, Style } from "hono/css"; import { css, Style } from "hono/css";
type LayoutProps = { const Layout: FC = (props) => {
title: string;
children: JSX.Element | JSX.Element[];
};
const Layout: FC<LayoutProps> = (props) => {
const globalClass = css` const globalClass = css`
:-hono-global { :-hono-global {
* { * {

View File

@@ -20,7 +20,13 @@ const NotFound: FC = () => {
</div> </div>
<div class="control"> <div class="control">
<button id="home-button"> <button id="home-button">
<svg className="btn-icon" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"> <svg
className="btn-icon"
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
>
<path <path
fill="currentColor" fill="currentColor"
d="M10 19v-5h4v5c0 .55.45 1 1 1h3c.55 0 1-.45 1-1v-7h1.7c.46 0 .68-.57.33-.87L12.67 3.6c-.38-.34-.96-.34-1.34 0l-8.36 7.53c-.34.3-.13.87.33.87H5v7c0 .55.45 1 1 1h3c.55 0 1-.45 1-1" d="M10 19v-5h4v5c0 .55.45 1 1 1h3c.55 0 1-.45 1-1v-7h1.7c.46 0 .68-.57.33-.87L12.67 3.6c-.38-.34-.96-.34-1.34 0l-8.36 7.53c-.34.3-.13.87.33.87H5v7c0 .55.45 1 1 1h3c.55 0 1-.45 1-1"

View File

@@ -3,11 +3,13 @@
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"declaration": true,
"declarationMap": true,
"strict": false, "strict": false,
"types": ["node"], "types": ["node"],
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "hono/jsx", "jsxImportSource": "hono/jsx",
"outDir": "./dist" "outDir": "./dist"
}, },
"exclude": ["node_modules", "*.test.*"] "exclude": ["node_modules", "*.test.*", "./dist/**/*"]
} }