Merge remote-tracking branch 'origin/master' into feature/ofetch

This commit is contained in:
DIYgod 2024-03-24 23:30:19 +08:00
commit 9f7de5ab9a
No known key found for this signature in database
46 changed files with 902 additions and 547 deletions

View File

@ -2,4 +2,7 @@ coverage
.vscode
docker-compose.yml
!/.github
!/docs/.vuepress
lib/routes-deprecated
lib/router.js
babel.config.js
scripts/docker/minify-docker.js

View File

@ -11,8 +11,6 @@ updates:
ignore:
- dependency-name: jsrsasign
versions: ['>=11.0.0'] # no longer includes KJUR.crypto.Cipher for RSA
- dependency-name: unified
versions: ['>=10.0.0']
- package-ecosystem: 'github-actions'
directory: '/'

View File

@ -15,7 +15,7 @@ jobs:
build:
runs-on: ubuntu-latest
name: Build assets
timeout-minutes: 5
timeout-minutes: 60
permissions:
contents: write
steps:
@ -33,13 +33,9 @@ jobs:
- name: Install dependencies (yarn)
run: pnpm i
- name: Build assets
run: npm run build
- name: Commit files
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git status
git diff-index --quiet HEAD || (git commit -m "chore: auto build" -a --no-verify && git push "https://${GITHUB_ACTOR}:${{ secrets.GITHUB_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git" HEAD:master)
run: pnpm build
- name: Build docs
run: pnpm build:docs
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
@ -57,6 +53,8 @@ jobs:
run: |
cp -r ./assets/build/docs/en/* ./rsshub-docs/src/routes
cp -r ./assets/build/docs/zh/* ./rsshub-docs/src/zh/routes
cp ./lib/types.ts ./rsshub-docs/.vitepress/theme/types.ts
cp ./scripts/workflow/data.ts ./rsshub-docs/.vitepress/config/data.ts
- name: Commit docs
run: |
cd rsshub-docs

View File

@ -15,16 +15,16 @@ jobs:
- uses: pnpm/action-setup@v3
with:
version: 8
- uses: actions/setup-node@v4 # just need its cache
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: Install dependencies (pnpm) # needed since we need to parse markdown, so we also use got instead
- name: Install dependencies (pnpm) # import remark-parse and unified
run: pnpm i
- name: Generate feedback
uses: actions/github-script@v7
with:
github-token: ${{secrets.GITHUB_TOKEN}}
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const script = require(`${process.env.GITHUB_WORKSPACE}/scripts/workflow/test-issue/call-maintainer.js`)
return script({ github, context, core })
const { default: callMaintainer } = await import('${{ github.workspace }}/scripts/workflow/test-issue/call-maintainer.mjs')
await callMaintainer({ github, context, core })

View File

@ -2,14 +2,14 @@ name: PR - route test
on:
workflow_run:
workflows: [ PR - Docker build test ] # open, reopen, synchronized, edited included
types: [ completed ]
workflows: [PR - Docker build test] # open, reopen, synchronized, edited included
types: [completed]
jobs:
testRoute:
name: Route test
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }} # skip if unsuccessful
if: ${{ github.event.workflow_run.conclusion == 'success' }} # skip if unsuccessful
steps:
- uses: actions/checkout@v4
@ -44,8 +44,8 @@ jobs:
const body = PR.body
const number = PR.number
const sender = PR.user.login
const script = require(`${process.env.GITHUB_WORKSPACE}/scripts/workflow/test-route/identify.js`)
return script({ github, context, core }, body, number, sender)
const { default: identify } = await import('${{ github.workspace }}/scripts/workflow/test-route/identify.mjs')
return identify({ github, context, core }, body, number, sender)
- name: Fetch Docker image
if: (env.TEST_CONTINUE)
@ -72,13 +72,13 @@ jobs:
with:
version: 8
- uses: actions/setup-node@v4 # just need its cache
- uses: actions/setup-node@v4
if: (env.TEST_CONTINUE)
with:
node-version: lts/*
cache: 'pnpm'
- name: Install dependencies (pnpm) # `got` needed since `github.request` disallows HTTP requests
- name: Install dependencies (pnpm) # require js-beautify
if: (env.TEST_CONTINUE)
run: pnpm i
@ -99,9 +99,8 @@ jobs:
const routes = JSON.parse(process.env.TEST_ROUTES)
const number = PR.number
core.info(`${link}, ${routes}, ${number}`)
const got = require("got")
const script = require(`${process.env.GITHUB_WORKSPACE}/scripts/workflow/test-route/test.js`)
return script({ github, context, core, got }, link, routes, number)
const { default: test } = await import('${{ github.workspace }}/scripts/workflow/test-route/test.mjs')
await test({ github, context, core }, link, routes, number)
- name: Pull Request Labeler
if: ${{ failure() }}
@ -114,4 +113,4 @@ jobs:
- name: Print Docker container logs
if: (env.TEST_CONTINUE)
run: docker logs rsshub # logs/combined.log? Not so readable...
run: docker logs rsshub # logs/combined.log? Not so readable...

View File

@ -77,8 +77,8 @@ jobs:
const body = event.comment.body
const number = event.issue.number
const sender = event.comment.user.login
const script = require(`${process.env.GITHUB_WORKSPACE}/scripts/workflow/test-route/identify.js`)
return script({ github, context, core }, body, number, sender)
const { default: identify } = await import('${{ github.workspace }}/scripts/workflow/test-route/identify.mjs')
return identify({ github, context, core }, body, number, sender)
- name: Build RSSHub
if: env.TEST_CONTINUE
@ -107,9 +107,8 @@ jobs:
const routes = JSON.parse(process.env.TEST_ROUTES)
const number = event.issue.number
core.info(`${link}, ${routes}, ${number}`)
const got = require("got")
const script = require(`${process.env.GITHUB_WORKSPACE}/scripts/workflow/test-route/test.js`)
return script({ github, context, core, got }, link, routes, number)
const { default: test } = await import('${{ github.workspace }}/scripts/workflow/test-route/test.mjs')
await test({ github, context, core }, link, routes, number)
- name: Print logs
if: (env.TEST_CONTINUE)

44
.github/workflows/test-full-routes.yml vendored Normal file
View File

@ -0,0 +1,44 @@
name: Build assets
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
name: Build assets
timeout-minutes: 60
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
- name: Use Node.js Active LTS
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: Install dependencies (yarn)
run: pnpm i
- name: Build assets
run: pnpm build
- name: Build full routes test result
continue-on-error: true
run: pnpm vitest:fullroutes
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./assets
user_name: 'github-actions[bot]'
user_email: '41898282+github-actions[bot]@users.noreply.github.com'
keep_files: true

View File

@ -5,7 +5,6 @@ on:
branches-ignore:
- 'dependabot/**'
paths:
- 'test/**'
- 'lib/**'
- 'package.json'
- 'pnpm-lock.yaml'
@ -43,6 +42,8 @@ jobs:
run: pnpm i
- name: Run postinstall script for dependencies
run: pnpm rb
- name: Build routes
run: pnpm build
- name: Test all and generate coverage
run: pnpm run vitest:coverage
env:
@ -84,6 +85,8 @@ jobs:
run: pnpm i
- name: Run postinstall script for dependencies
run: pnpm rb
- name: Build routes
run: pnpm build
- name: Install Chromium
if: ${{ matrix.chromium.dependency != '' }}
# 'chromium-browser' from Ubuntu APT repo is a dummy package. Its version (85.0.4183.83) means

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
lib/routes-deprecated
lib/router.js
babel.config.js
scripts/docker/minify-docker.js

View File

@ -84,7 +84,7 @@ FROM node:21-bookworm-slim AS chromium-downloader
# Yeah, downloading Chromium never needs those dependencies below.
WORKDIR /app
COPY ./.puppeteerrc.js /app/
COPY ./.puppeteerrc.cjs /app/
COPY --from=dep-version-parser /ver/.puppeteer_version /app/.puppeteer_version
ARG TARGETPLATFORM

View File

@ -1,23 +1,16 @@
import { describe, expect, it, vi, afterEach, afterAll, beforeAll } from 'vitest';
import { describe, expect, it, vi, afterEach } from 'vitest';
import Parser from 'rss-parser';
import wait from '@/utils/wait';
process.env.CACHE_EXPIRE = '1';
process.env.CACHE_CONTENT_EXPIRE = '3';
const parser = new Parser();
beforeAll(() => {
process.env.CACHE_EXPIRE = '1';
process.env.CACHE_CONTENT_EXPIRE = '3';
});
afterEach(() => {
delete process.env.CACHE_TYPE;
vi.resetModules();
});
afterAll(() => {
delete process.env.CACHE_EXPIRE;
});
describe('cache', () => {
it('memory', async () => {
process.env.CACHE_TYPE = 'memory';

View File

@ -1,55 +1,8 @@
import { describe, expect, it, afterAll } from 'vitest';
process.env.SOCKET = 'socket';
import { describe, expect, it } from 'vitest';
import app from '@/app';
import Parser from 'rss-parser';
const parser = new Parser();
import { config } from '@/config';
afterAll(() => {
delete process.env.SOCKET;
});
async function checkRSS(response) {
const checkDate = (date) => {
expect(date).toEqual(expect.any(String));
expect(Date.parse(date)).toEqual(expect.any(Number));
expect(Date.now() - +new Date(date)).toBeGreaterThan(-1000 * 60 * 60 * 24 * 5);
expect(Date.now() - +new Date(date)).toBeLessThan(1000 * 60 * 60 * 24 * 30 * 12 * 10);
};
const parsed = await parser.parseString(await response.text());
expect(parsed).toEqual(expect.any(Object));
expect(parsed.title).toEqual(expect.any(String));
expect(parsed.title).not.toBe('RSSHub');
expect(parsed.description).toEqual(expect.any(String));
expect(parsed.link).toEqual(expect.any(String));
expect(parsed.lastBuildDate).toEqual(expect.any(String));
expect(parsed.ttl).toEqual(Math.trunc(config.cache.routeExpire / 60) + '');
expect(parsed.items).toEqual(expect.any(Array));
checkDate(parsed.lastBuildDate);
// check items
const guids: (string | undefined)[] = [];
for (const item of parsed.items) {
expect(item).toEqual(expect.any(Object));
expect(item.title).toEqual(expect.any(String));
expect(item.link).toEqual(expect.any(String));
expect(item.content).toEqual(expect.any(String));
expect(item.guid).toEqual(expect.any(String));
if (item.pubDate) {
expect(item.pubDate).toEqual(expect.any(String));
checkDate(item.pubDate);
}
// guid must be unique
expect(guids).not.toContain(item.guid);
guids.push(item.guid);
}
}
describe('router', () => {
describe('registry', () => {
// root
it(`/`, async () => {
const response = await app.request('/');
@ -58,14 +11,6 @@ describe('router', () => {
expect(response.headers.get('cache-control')).toBe('no-cache');
});
// route
it(`/test/1`, async () => {
const response = await app.request('/test/1');
expect(response.status).toBe(200);
await checkRSS(response);
});
// robots.txt
it('/robots.txt', async () => {
config.disallowRobot = false;

View File

@ -7,8 +7,6 @@ import { serveStatic } from '@hono/node-server/serve-static';
import index from '@/routes/index';
import robotstxt from '@/routes/robots.txt';
import { namespace as testNamespace } from './routes/test/namespace';
import { route as testRoute } from '@/routes/test/index';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -27,18 +25,9 @@ let namespaces: Record<
switch (process.env.NODE_ENV) {
case 'test':
modules = {
'/test/namespace.ts': {
namespace: testNamespace,
},
'/test/index.ts': {
route: testRoute,
},
};
break;
case 'production':
// eslint-disable-next-line n/no-unpublished-require
namespaces = require('../assets/build/routes.json');
// @ts-expect-error
namespaces = await import('../assets/build/routes.json');
break;
default:
modules = directoryImport({

78
lib/routes.test.ts Normal file
View File

@ -0,0 +1,78 @@
import { describe, expect, it } from 'vitest';
import app from '@/app';
import Parser from 'rss-parser';
const parser = new Parser();
import { config } from '@/config';
process.env.ALLOW_USER_SUPPLY_UNSAFE_DOMAIN = 'true';
const routes = {
'/test/:id': '/test/1',
};
if (process.env.FULL_ROUTES_TEST) {
const { namespaces } = await import('@/registry');
for (const namespace in namespaces) {
for (const route in namespaces[namespace].routes) {
const requireConfig = namespaces[namespace].routes[route].features?.requireConfig;
let configs;
if (typeof requireConfig !== 'boolean') {
configs = requireConfig
?.filter((config) => !config.optional)
.map((config) => config.name)
.filter((name) => name !== 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN');
}
if (namespaces[namespace].routes[route].example && !configs?.length) {
routes[`/${namespace}${route}`] = namespaces[namespace].routes[route].example;
}
}
}
}
async function checkRSS(response) {
const checkDate = (date) => {
expect(date).toEqual(expect.any(String));
expect(Date.parse(date)).toEqual(expect.any(Number));
expect(Date.now() - +new Date(date)).toBeGreaterThan(-1000 * 60 * 60 * 24 * 5);
expect(Date.now() - +new Date(date)).toBeLessThan(1000 * 60 * 60 * 24 * 30 * 12 * 10);
};
const parsed = await parser.parseString(await response.text());
expect(parsed).toEqual(expect.any(Object));
expect(parsed.title).toEqual(expect.any(String));
expect(parsed.title).not.toBe('RSSHub');
expect(parsed.description).toEqual(expect.any(String));
expect(parsed.link).toEqual(expect.any(String));
expect(parsed.lastBuildDate).toEqual(expect.any(String));
expect(parsed.ttl).toEqual(Math.trunc(config.cache.routeExpire / 60) + '');
expect(parsed.items).toEqual(expect.any(Array));
checkDate(parsed.lastBuildDate);
// check items
const guids: (string | undefined)[] = [];
for (const item of parsed.items) {
expect(item).toEqual(expect.any(Object));
expect(item.title).toEqual(expect.any(String));
expect(item.link).toEqual(expect.any(String));
expect(item.content).toEqual(expect.any(String));
expect(item.guid).toEqual(expect.any(String));
if (item.pubDate) {
expect(item.pubDate).toEqual(expect.any(String));
checkDate(item.pubDate);
}
// guid must be unique
expect(guids).not.toContain(item.guid);
guids.push(item.guid);
}
}
describe('routes', () => {
for (const route in routes) {
it.concurrent(route, async () => {
const response = await app.request(routes[route]);
expect(response.status).toBe(200);
await checkRSS(response);
});
}
});

View File

@ -31,9 +31,9 @@ export const route: Route = {
| ps4 | sgame | 3ds | psv | jiaocheng | ps3yx | zhuji.md | zhangji.psp | pcgame | zhangji | zhuji | ps4.psjc | ps41.ps4pkg | nsaita.cundang | nsaita.pojie | nsaita.buding | nsaita.zhutie | nsaita.zhuti |`,
};
async function handler(ctx?: Context): Promise<Data> {
const category = (ctx!.req.param('category') ?? 'sgame').replaceAll('.', '/');
const tab = ctx?.req.param('tab') ?? 'all';
async function handler(ctx: Context): Promise<Data> {
const category = (ctx.req.param('category') ?? 'sgame').replaceAll('.', '/');
const tab = ctx.req.param('tab') ?? 'all';
const currentUrl = `https://www.2023game.com/${category}/`;

View File

@ -1,4 +1,5 @@
import got from '@/utils/got';
import type { Route } from '@/types';
export const route: Route = {
path: '/dynamic/:uid?',

View File

@ -0,0 +1,6 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'AppsTorrent',
url: 'appstorrent.ru',
};

View File

@ -0,0 +1,91 @@
import { Data, DataItem, Route } from '@/types';
import cache from '@/utils/cache';
import got, { Options } from '@/utils/got';
import { getCurrentPath } from '@/utils/helpers';
import { parseDate } from '@/utils/parse-date';
import { art } from '@/utils/render';
import { load } from 'cheerio';
import dayjs from 'dayjs';
import { Context } from 'hono';
import * as path from 'node:path';
const __dirname = getCurrentPath(import.meta.url);
export const route: Route = {
path: '/programs',
categories: ['program-update'],
example: '/appstorrent/programs',
name: 'Programs',
maintainers: ['xzzpig'],
handler,
url: 'appstorrent.ru/programs/',
};
async function handler(ctx?: Context): Promise<Data> {
const limit = ctx?.req.query('limit') ? Number.parseInt(ctx?.req.query('limit') ?? '20') : 20;
const baseUrl = 'https://appstorrent.ru';
const currentUrl = `${baseUrl}/programs/`;
const gotOptions: Options = {
http2: true,
};
const response = await got(currentUrl, gotOptions);
const $ = load(response.data as any);
const selector = 'article.soft-item:not(.locked)';
const list = $(selector)
.slice(0, limit)
.toArray()
.map((item) => {
const $item = $(item);
return {
title: $item.find('.subtitle').text().trim(),
link: $item.find('.subtitle a').attr('href')!,
category: [$item.find('.info .category').text().trim()],
version: $item.find('.version').text(),
architecture: $item.find('.architecture').text().trim(),
size: $item.find('.size').text().trim(),
};
});
const items: DataItem[] = await Promise.all(
list.map(
(item) =>
cache.tryGet(item.link, async () => {
const response = await got(item.link, gotOptions);
const $ = load(response.data as any);
const pubDate = parseDate($('.tech-info .date-news a').attr('href')?.replace('https://appstorrent.ru/', '') ?? '');
return {
title: item.title,
link: item.link,
category: item.category,
pubDate,
description: art(path.join(__dirname, 'templates/description.art'), {
cover: baseUrl + $('.main-title img').attr('src')?.trim(),
title: item.title,
pubDate: dayjs(pubDate).format('YYYY-MM-DD'),
version: item.version,
architecture: item.architecture,
compatibility: $('div.right > div.info > div.right-container > div:nth-child(5) > div > span:nth-child(2) > a').text(),
size: item.size,
activation: $('div.right > div.info > div.right-container > div:nth-child(4) > div > span:nth-child(2) > a').text(),
description: $('.content .body-content').first().text(),
changelog: $('.content .body-content').last().text(),
screenshots: $('.screenshots img')
.toArray()
.map((img) => $(img).attr('src'))
.map((src) => baseUrl + src),
}),
} as DataItem;
}) as Promise<DataItem>
)
);
return {
title: $('title').text(),
link: currentUrl.toString(),
allowEmpty: true,
item: items,
};
}

View File

@ -0,0 +1,22 @@
<p>
<img src="{{ cover }}">
<h1>{{ title }}</h1><br>
<b>Public Date</b>: {{pubDate}}<br>
<b>Version</b>: {{version}}<br>
<b>Architecture</b>: {{architecture}}<br>
<b>Compactibility</b>: {{compatibility}}<br>
<b>Size</b>: {{size}}<br>
<b>Activation</b>: {{activation}}<br>
</p>
<b>Description</b>:
<p>
{{ description }}
</p>
<b>Change Log</b>:
<p>
{{ changelog }}
</p>
<b>Screenshots</b>
{{each screenshots}}
<img src="{{ $value }}">
{{/each}}

View File

@ -121,7 +121,6 @@ async function handler(ctx) {
headers: {
Referer: `https://space.bilibili.com/${uid}/`,
},
transformResponse: [(data) => data],
});
const cards = JSONbig.parse(response.body).data.cards;

View File

@ -3,6 +3,7 @@ import got from '@/utils/got';
import cache from './cache';
import utils from './utils';
import { parseDate } from '@/utils/parse-date';
import { queryToBoolean } from '@/utils/readable-social';
const notFoundData = {
title: '此 bilibili 频道不存在',
@ -15,7 +16,7 @@ export const route: Route = {
parameters: {
uid: '用户 id, 可在 UP 主主页中找到',
sid: '合集 id, 可在合集页面的 URL 中找到',
disableEmbed: '默认为开启内嵌视频, 任意值为关闭',
disableEmbed: '空,0与false为开启内嵌视频, 其他任意值为关闭',
sortReverse: '默认:默认排序 1:升序排序',
page: '页码, 默认1',
},
@ -35,7 +36,7 @@ export const route: Route = {
async function handler(ctx) {
const uid = Number.parseInt(ctx.req.param('uid'));
const sid = Number.parseInt(ctx.req.param('sid'));
const disableEmbed = ctx.req.param('disableEmbed');
const disableEmbed = queryToBoolean(ctx.req.param('disableEmbed'));
const sortReverse = Number.parseInt(ctx.req.param('sortReverse')) === 1;
const page = ctx.req.param('page') ? Number.parseInt(ctx.req.param('page')) : 1;
const limit = ctx.req.query('limit') ?? 25;

View File

@ -1,6 +1,6 @@
import { Route } from '@/types';
import cache from '@/utils/cache';
import cherrio from 'cheerio';
import * as cheerio from 'cheerio';
import { parseDate } from '@/utils/parse-date';
import puppeteer from '@/utils/puppeteer';
@ -43,7 +43,7 @@ async function handler() {
const res = await page.evaluate(() => document.documentElement.innerHTML);
await page.close();
const $ = cherrio.load(res);
const $ = cheerio.load(res);
const items = $('div h3 a')
.toArray()
@ -67,7 +67,7 @@ async function handler() {
waitUntil: 'domcontentloaded',
});
const res = await page.evaluate(() => document.documentElement.innerHTML);
const $ = cherrio.load(res);
const $ = cheerio.load(res);
await page.close();
item.description = $('div.article__body').html();

View File

@ -21,9 +21,11 @@ export const route: Route = {
supportPodcast: false,
supportScihub: false,
},
radar: {
source: ['mmda.booru.org/index.php'],
},
radar: [
{
source: ['mmda.booru.org/index.php'],
},
],
name: 'MMDArchive 标签查询',
maintainers: ['N78Wy'],
handler,

View File

@ -1,6 +1,6 @@
import { Route } from '@/types';
import cache from '@/utils/cache';
import cherrio from 'cheerio';
import * as cheerio from 'cheerio';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
import timezone from '@/utils/timezone';
@ -34,7 +34,7 @@ async function handler() {
const url = `${homepage}/f/article/articleList?pageNo=1&pageSize=15&createTimeSort=DESC`;
const response = await got(url);
const $ = cherrio.load(response.data);
const $ = cheerio.load(response.data);
const articles = $('.aw-item').toArray();
const items = await Promise.all(
@ -45,7 +45,7 @@ async function handler() {
return cache.tryGet(link, async () => {
const result = await got(link);
const $ = cherrio.load(result.data);
const $ = cheerio.load(result.data);
return {
title,
author: $('.user_name').text(),

View File

@ -0,0 +1,6 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'dbaplus社群',
url: 'dbaplus.cn',
};

48
lib/routes/dbaplus/rss.ts Normal file
View File

@ -0,0 +1,48 @@
import { Route } from '@/types';
import { parseDate } from '@/utils/parse-date';
import got from '@/utils/got';
import { load } from 'cheerio';
export const route: Route = {
path: '/',
categories: ['programming'],
example: '/dbaplus',
radar: [
{
source: ['dbaplus.cn/'],
},
],
name: '最新文章',
maintainers: ['cnkmmk'],
handler,
url: 'dbaplus.cn/',
};
async function handler() {
const url = 'https://dbaplus.cn';
const response = await got(`${url}/news-9-1.html`);
const $ = load(response.data);
const list = $('div.col-xs-12.col-md-8.pd30 > div.panel.panel-default.categeay > div.panel-body > ul.media-list.clearfix > li.media')
.map((i, e) => {
const element = $(e);
const title = element.find('h3 > a').text();
const link = element.find('h3 > a').attr('href');
const description = element.find('div.mt10.geay').text();
const dateraw = element.find('span.time').text();
return {
title,
description,
link,
pubDate: parseDate(dateraw, 'YYYY年MM月DD日'),
};
})
.get();
return {
title: 'dbaplus社群',
link: url,
item: list,
};
}

View File

@ -37,23 +37,25 @@ export const route: Route = {
supportPodcast: false,
supportScihub: false,
},
radar: {
source: [
'modrinth.com/mod/:id/*',
'modrinth.com/plugin/:id/*',
'modrinth.com/datapack/:id/*',
'modrinth.com/shader/:id/*',
'modrinth.com/resourcepack/:id/*',
'modrinth.com/modpack/:id/*',
'modrinth.com/mod/:id',
'modrinth.com/plugin/:id',
'modrinth.com/datapack/:id',
'modrinth.com/shader/:id',
'modrinth.com/resourcepack/:id',
'modrinth.com/modpack/:id',
],
target: '/project/:id/versions',
},
radar: [
{
source: [
'modrinth.com/mod/:id/*',
'modrinth.com/plugin/:id/*',
'modrinth.com/datapack/:id/*',
'modrinth.com/shader/:id/*',
'modrinth.com/resourcepack/:id/*',
'modrinth.com/modpack/:id/*',
'modrinth.com/mod/:id',
'modrinth.com/plugin/:id',
'modrinth.com/datapack/:id',
'modrinth.com/shader/:id',
'modrinth.com/resourcepack/:id',
'modrinth.com/modpack/:id',
],
target: '/project/:id/versions',
},
],
name: 'Project versions',
maintainers: ['SettingDust'],
handler,

View File

@ -2,6 +2,7 @@ import { parseDate } from '@/utils/parse-date';
import got from '@/utils/got';
import cache from '@/utils/cache';
import CryptoJS from 'crypto-js/crypto-js';
import { Route } from '@/types';
export const route: Route = {
path: '/hqsz',

View File

@ -1,6 +1,6 @@
import { Route } from '@/types';
import cache from '@/utils/cache';
import cherrio from 'cheerio';
import * as cheerio from 'cheerio';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
@ -32,7 +32,7 @@ async function handler() {
const link = 'https://telegram.org/blog';
const res = await got(link);
const $$ = cherrio.load(res.body);
const $$ = cheerio.load(res.body);
const items = await Promise.all(
$$('.dev_blog_card_link_wrap')
@ -42,7 +42,7 @@ async function handler() {
const link = 'https://telegram.org' + $.attr('href');
return cache.tryGet(link, async () => {
const result = await got(link);
const $ = cherrio.load(result.body);
const $ = cheerio.load(result.body);
return {
title: $('#dev_page_title').text(),
link,

View File

@ -1,4 +1,3 @@
import readline from 'node:readline/promises';
import { Api, TelegramClient } from 'telegram';
import { UserAuthParams } from 'telegram/client/auth';
import { StringSession } from 'telegram/sessions';
@ -187,20 +186,3 @@ function streamDocument(obj, thumbSize = '', offset, limit) {
}
export { client, getMediaLink, decodeMedia, getFilename, streamDocument, streamThumbnail };
if (require.main === module) {
Promise.resolve().then(async () => {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const client = await getClient(
{
phoneNumber: () => rl.question('Please enter your phone number: '),
password: () => rl.question('Please enter your password: '),
phoneCode: () => rl.question('Please enter the code you received: '),
onError: (err) => process.stderr.write(err.toString()),
},
''
);
process.stdout.write(`TELEGRAM_SESSION=${client.session.save()}\n`);
process.exit(0);
});
}

View File

@ -79,13 +79,9 @@ async function handler(ctx) {
break;
case 'cache': {
const description = await cache.tryGet(
'test',
() => ({
text: `Cache${++cacheIndex}`,
}),
config.cache.routeExpire * 2
);
const description = await cache.tryGet('test', () => ({
text: `Cache${++cacheIndex}`,
}));
item.push({
title: 'Cache Title',
description: description.text,

View File

@ -10,7 +10,12 @@ export const route: Route = {
example: '/youtube/channel/UCDwDMPOZfxVV0x_dz0eQ8KQ',
parameters: { id: 'YouTube channel id', embed: 'Default to embed the video, set to any value to disable embedding' },
features: {
requireConfig: false,
requireConfig: [
{
name: 'YOUTUBE_KEY',
description: ' YouTube API Key, support multiple keys, split them with `,`, [API Key application](https://console.developers.google.com/)',
},
],
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,

View File

@ -1,5 +1,31 @@
import type { Context } from 'hono';
// Make sure it's synchronise with scripts/workflow/data.ts
type Category =
| 'social-media'
| 'new-media'
| 'traditional-media'
| 'bbs'
| 'blog'
| 'programming'
| 'design'
| 'live'
| 'multimedia'
| 'picture'
| 'anime'
| 'program-update'
| 'university'
| 'forecast'
| 'travel'
| 'shopping'
| 'game'
| 'reading'
| 'government'
| 'study'
| 'journal'
| 'finance'
| 'other';
// rss
export type DataItem = {
title: string;
@ -72,7 +98,7 @@ interface NamespaceItem {
/**
* The classification of the namespace, which will be written into the corresponding classification document
*/
categories?: string[];
categories?: Category[];
/**
* Hints and additional explanations for users using this namespace, it will be inserted into the documentation
@ -117,7 +143,7 @@ interface RouteItem {
/**
* The handler function of the route
*/
handler: (ctx?: Context) => Promise<Data> | Data;
handler: (ctx: Context) => Promise<Data> | Data;
/**
* An example URL of the route
@ -137,7 +163,7 @@ interface RouteItem {
/**
* The classification of the route, which will be written into the corresponding classification documentation
*/
categories?: string[];
categories?: Category[];
/**
* Special features of the route, such as what configuration items it depends on, whether it is strict anti-crawl, whether it supports a certain function and so on

View File

@ -1,4 +1,7 @@
import { config } from '@/config';
import { PacProxyAgent } from 'pac-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { SocksProxyAgent } from 'socks-proxy-agent';
const proxyIsPAC = config.pacUri || config.pacScript;
@ -20,16 +23,13 @@ if (proxyIsPAC) {
proxyUrlHandler = proxy.proxyUrlHandler;
}
let agent = null;
let agent: PacProxyAgent<string> | HttpsProxyAgent<string> | SocksProxyAgent | null = null;
if (proxyIsPAC) {
const { PacProxyAgent } = require('pac-proxy-agent');
agent = new PacProxyAgent(`pac+${proxyUri}`);
} else if (proxyUri) {
if (proxyUri.startsWith('http')) {
const { HttpsProxyAgent } = require('https-proxy-agent');
agent = new HttpsProxyAgent(proxyUri);
} else if (proxyUri.startsWith('socks')) {
const { SocksProxyAgent } = require('socks-proxy-agent');
agent = new SocksProxyAgent(proxyUri);
}
}

View File

@ -2,8 +2,7 @@ import { config } from '@/config';
import puppeteer from 'puppeteer';
import logger from './logger';
import proxy from './proxy';
const proxyChain = require('proxy-chain');
import proxyChain from 'proxy-chain';
import { type PuppeteerExtra, addExtra } from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
@ -27,28 +26,6 @@ const outPuppeteer = async (
let insidePuppeteer: PuppeteerExtra | typeof puppeteer = puppeteer;
if (extraOptions.stealth) {
insidePuppeteer = addExtra(puppeteer);
// workaround for vercel/nft #54, #283, #304
require('puppeteer-extra-plugin-stealth/evasions/chrome.app');
require('puppeteer-extra-plugin-stealth/evasions/chrome.csi');
require('puppeteer-extra-plugin-stealth/evasions/chrome.loadTimes');
require('puppeteer-extra-plugin-stealth/evasions/chrome.runtime');
require('puppeteer-extra-plugin-stealth/evasions/defaultArgs');
require('puppeteer-extra-plugin-stealth/evasions/iframe.contentWindow');
require('puppeteer-extra-plugin-stealth/evasions/media.codecs');
require('puppeteer-extra-plugin-stealth/evasions/navigator.hardwareConcurrency');
require('puppeteer-extra-plugin-stealth/evasions/navigator.languages');
require('puppeteer-extra-plugin-stealth/evasions/navigator.permissions');
require('puppeteer-extra-plugin-stealth/evasions/navigator.plugins');
require('puppeteer-extra-plugin-stealth/evasions/navigator.vendor');
require('puppeteer-extra-plugin-stealth/evasions/navigator.webdriver');
require('puppeteer-extra-plugin-stealth/evasions/sourceurl');
require('puppeteer-extra-plugin-stealth/evasions/user-agent-override');
require('puppeteer-extra-plugin-stealth/evasions/webgl.vendor');
require('puppeteer-extra-plugin-stealth/evasions/window.outerdimensions');
require('puppeteer-extra-plugin-user-preferences');
require('puppeteer-extra-plugin-user-data-dir');
insidePuppeteer.use(StealthPlugin());
}

View File

@ -52,13 +52,12 @@ const Index: FC<{
<div className="text-left w-[800px] space-y-6 !mt-10">
<div className="space-y-2">
<p className="mb-2 font-bold">Helpful Information</p>
<p>Error Message:</p>
<code className="mt-2 block max-h-28 overflow-auto bg-zinc-100 align-bottom w-fit details">{message}</code>
<p>Route: <code className="ml-2 bg-zinc-100">{errorRoute}</code></p>
<p>Full Route: <code className="ml-2 bg-zinc-100">{requestPath}</code></p>
<p>Node Version: <code className="ml-2 bg-zinc-100">{nodeVersion}</code></p>
<p>Git Hash: <code className="ml-2 bg-zinc-100">{gitHash}</code></p>
<p>Git Date: <code className="ml-2 bg-zinc-100">{gitDate?.toUTCString()}</code></p>
<p className="message">Error Message:<br/><code className="mt-2 block max-h-28 overflow-auto bg-zinc-100 align-bottom w-fit details">{message}</code></p>
<p className="message">Route: <code className="ml-2 bg-zinc-100">{errorRoute}</code></p>
<p className="message">Full Route: <code className="ml-2 bg-zinc-100">{requestPath}</code></p>
<p className="message">Node Version: <code className="ml-2 bg-zinc-100">{nodeVersion}</code></p>
<p className="message">Git Hash: <code className="ml-2 bg-zinc-100">{gitHash}</code></p>
<p className="message">Git Date: <code className="ml-2 bg-zinc-100">{gitDate?.toUTCString()}</code></p>
</div>
<div>
<p className="mb-2 font-bold">Report</p>

View File

@ -19,20 +19,23 @@
"files": [
"lib"
],
"type": "module",
"scripts": {
"build": "tsx scripts/workflow/build-routes.ts",
"dev": "cross-env NODE_ENV=dev tsx watch --no-cache lib/index.ts",
"dev:cache": "cross-env NODE_ENV=production tsx watch lib/index.ts",
"build:docs": "tsx scripts/workflow/build-docs.ts",
"dev": "NODE_ENV=dev tsx watch --no-cache lib/index.ts",
"dev:cache": "NODE_ENV=production tsx watch lib/index.ts",
"format": "eslint --cache --fix \"**/*.{ts,js,yml}\" && prettier \"**/*.{ts,js,json}\" --write",
"format:check": "eslint --cache \"**/*.{ts,js,yml}\" && prettier \"**/*.{ts,js,json}\" --check",
"format:staged": "lint-staged",
"vitest": "cross-env NODE_ENV=test vitest",
"vitest:coverage": "cross-env NODE_ENV=test vitest --coverage.enabled --reporter=junit",
"vitest:watch": "cross-env NODE_ENV=test vitest --watch",
"vitest": "NODE_ENV=test vitest",
"vitest:fullroutes": "NODE_ENV=test FULL_ROUTES_TEST=true vitest --reporter=json --reporter=default --outputFile=\"./assets/build/test-full-routes.json\" routes",
"vitest:coverage": "NODE_ENV=test vitest --coverage.enabled --reporter=junit",
"vitest:watch": "NODE_ENV=test vitest --watch",
"lint": "eslint --cache .",
"prepare": "husky || true",
"profiling": "NODE_ENV=production tsx --prof lib/index.ts",
"start": "cross-env NODE_ENV=production tsx lib/index.ts",
"start": "NODE_ENV=production tsx lib/index.ts",
"test": "npm run format:check && npm run vitest:coverage"
},
"lint-staged": {
@ -49,10 +52,10 @@
"dependencies": {
"@hono/node-server": "1.8.2",
"@hono/swagger-ui": "0.2.1",
"@hono/zod-openapi": "0.9.8",
"@hono/zod-openapi": "0.9.9",
"@notionhq/client": "2.2.14",
"@postlight/parser": "2.2.3",
"@sentry/node": "7.107.0",
"@sentry/node": "7.108.0",
"@tonyrl/rand-user-agent": "2.0.55",
"aes-js": "3.1.2",
"art-template": "4.13.2",
@ -110,7 +113,7 @@
"telegram": "2.20.2",
"tiny-async-pool": "2.1.0",
"title": "3.5.3",
"tldts": "6.1.13",
"tldts": "6.1.14",
"tough-cookie": "4.1.3",
"tsx": "4.7.1",
"twitter-api-v2": "1.16.1",
@ -149,7 +152,6 @@
"@typescript-eslint/parser": "7.3.1",
"@vercel/nft": "0.26.4",
"@vitest/coverage-v8": "1.4.0",
"cross-env": "7.0.3",
"eslint": "8.57.0",
"eslint-config-prettier": "9.1.0",
"eslint-nibble": "8.1.0",
@ -159,6 +161,7 @@
"eslint-plugin-yml": "1.13.2",
"fs-extra": "11.2.0",
"husky": "9.0.11",
"js-beautify": "1.15.1",
"lint-staged": "15.2.2",
"mockdate": "3.0.5",
"nock": "13.5.4",
@ -167,7 +170,7 @@
"supertest": "6.3.4",
"tosource": "2.0.0-alpha.3",
"typescript": "5.4.3",
"unified": "9.2.2",
"unified": "11.0.4",
"vite-tsconfig-paths": "4.3.2",
"vitest": "1.4.0"
},

View File

@ -12,8 +12,8 @@ dependencies:
specifier: 0.2.1
version: 0.2.1(hono@4.1.3)
'@hono/zod-openapi':
specifier: 0.9.8
version: 0.9.8(hono@4.1.3)(zod@3.22.4)
specifier: 0.9.9
version: 0.9.9(hono@4.1.3)(zod@3.22.4)
'@notionhq/client':
specifier: 2.2.14
version: 2.2.14
@ -21,8 +21,8 @@ dependencies:
specifier: 2.2.3
version: 2.2.3
'@sentry/node':
specifier: 7.107.0
version: 7.107.0
specifier: 7.108.0
version: 7.108.0
'@tonyrl/rand-user-agent':
specifier: 2.0.55
version: 2.0.55
@ -195,8 +195,8 @@ dependencies:
specifier: 3.5.3
version: 3.5.3
tldts:
specifier: 6.1.13
version: 6.1.13
specifier: 6.1.14
version: 6.1.14
tough-cookie:
specifier: 4.1.3
version: 4.1.3
@ -307,9 +307,6 @@ devDependencies:
'@vitest/coverage-v8':
specifier: 1.4.0
version: 1.4.0(vitest@1.4.0)
cross-env:
specifier: 7.0.3
version: 7.0.3
eslint:
specifier: 8.57.0
version: 8.57.0
@ -337,6 +334,9 @@ devDependencies:
husky:
specifier: 9.0.11
version: 9.0.11
js-beautify:
specifier: 1.15.1
version: 1.15.1
lint-staged:
specifier: 15.2.2
version: 15.2.2
@ -362,8 +362,8 @@ devDependencies:
specifier: 5.4.3
version: 5.4.3
unified:
specifier: 9.2.2
version: 9.2.2
specifier: 11.0.4
version: 11.0.4
vite-tsconfig-paths:
specifier: 4.3.2
version: 4.3.2(typescript@5.4.3)
@ -1915,8 +1915,8 @@ packages:
hono: 4.1.3
dev: false
/@hono/zod-openapi@0.9.8(hono@4.1.3)(zod@3.22.4):
resolution: {integrity: sha512-NS6lvQEGnsjyQkp+aQjMARREz3WGf19y0+RiiJMVrihWlmKbGaJkPuOWpfFQG6FU9q+FVUyUCryXK+3J07GPAw==}
/@hono/zod-openapi@0.9.9(hono@4.1.3)(zod@3.22.4):
resolution: {integrity: sha512-Icak3c8WKNS1gFDWmYs2zJ7ra3js9lDeAXNPf5h3fa0SnJQcjcCrLaG6EZPPAbW92HRwCDeQt8yA/ZVp17HPFg==}
engines: {node: '>=16.0.0'}
peerDependencies:
hono: '>=3.11.3'
@ -1970,6 +1970,18 @@ packages:
resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
dev: false
/@isaacs/cliui@8.0.2:
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
dependencies:
string-width: 5.1.2
string-width-cjs: /string-width@4.2.3
strip-ansi: 7.1.0
strip-ansi-cjs: /strip-ansi@6.0.1
wrap-ansi: 8.1.0
wrap-ansi-cjs: /wrap-ansi@7.0.0
dev: true
/@istanbuljs/schema@0.1.3:
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
engines: {node: '>=8'}
@ -2098,6 +2110,10 @@ packages:
- encoding
dev: false
/@one-ini/wasm@0.1.1:
resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
dev: true
/@otplib/core@12.0.1:
resolution: {integrity: sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==}
dev: false
@ -2131,6 +2147,13 @@ packages:
'@otplib/plugin-thirty-two': 12.0.1
dev: false
/@pkgjs/parseargs@0.11.0:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
requiresBuild: true
dev: true
optional: true
/@pkgr/core@0.1.1:
resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
@ -2334,43 +2357,43 @@ packages:
selderee: 0.11.0
dev: false
/@sentry-internal/tracing@7.107.0:
resolution: {integrity: sha512-le9wM8+OHBbq7m/8P7JUJ1UhSPIty+Z/HmRXc5Z64ODZcOwFV6TmDpYx729IXDdz36XUKmeI+BeM7yQdTTZPfQ==}
/@sentry-internal/tracing@7.108.0:
resolution: {integrity: sha512-zuK5XsTsb+U+hgn3SPetYDAogrXsM16U/LLoMW7+TlC6UjlHGYQvmX3o+M2vntejoU1QZS8m1bCAZSMWEypAEw==}
engines: {node: '>=8'}
dependencies:
'@sentry/core': 7.107.0
'@sentry/types': 7.107.0
'@sentry/utils': 7.107.0
'@sentry/core': 7.108.0
'@sentry/types': 7.108.0
'@sentry/utils': 7.108.0
dev: false
/@sentry/core@7.107.0:
resolution: {integrity: sha512-C7ogye6+KPyBi8NVL0P8Rxx3Ur7Td8ufnjxosVy678lqY+dcYPk/HONROrzUFYW5fMKWL4/KYnwP+x9uHnkDmw==}
/@sentry/core@7.108.0:
resolution: {integrity: sha512-I/VNZCFgLASxHZaD0EtxZRM34WG9w2gozqgrKGNMzAymwmQ3K9g/1qmBy4e6iS3YRptb7J5UhQkZQHrcwBbjWQ==}
engines: {node: '>=8'}
dependencies:
'@sentry/types': 7.107.0
'@sentry/utils': 7.107.0
'@sentry/types': 7.108.0
'@sentry/utils': 7.108.0
dev: false
/@sentry/node@7.107.0:
resolution: {integrity: sha512-UZXkG7uThT2YyPW8AOSKRXp1LbVcBHufa4r1XAwBukA2FKO6HHJPjMUgY6DYVQ6k+BmA56CNfVjYrdLbyjBYYA==}
/@sentry/node@7.108.0:
resolution: {integrity: sha512-pMxc9txnDDkU4Z8k2Uw/DPSLPehNtWV3mjJ3+my0AMORGYrXLkJI93tddlE5z/7k+GEJdj1HsOLgxUN0OU+HGA==}
engines: {node: '>=8'}
dependencies:
'@sentry-internal/tracing': 7.107.0
'@sentry/core': 7.107.0
'@sentry/types': 7.107.0
'@sentry/utils': 7.107.0
'@sentry-internal/tracing': 7.108.0
'@sentry/core': 7.108.0
'@sentry/types': 7.108.0
'@sentry/utils': 7.108.0
dev: false
/@sentry/types@7.107.0:
resolution: {integrity: sha512-H7qcPjPSUWHE/Zf5bR1EE24G0pGVuJgrSx8Tvvl5nKEepswMYlbXHRVSDN0gTk/E5Z7cqf+hUBOpkQgZyps77w==}
/@sentry/types@7.108.0:
resolution: {integrity: sha512-bKtHITmBN3kqtqE5eVvL8mY8znM05vEodENwRpcm6TSrrBjC2RnwNWVwGstYDdHpNfFuKwC8mLY9bgMJcENo8g==}
engines: {node: '>=8'}
dev: false
/@sentry/utils@7.107.0:
resolution: {integrity: sha512-C6PbN5gHh73MRHohnReeQ60N8rrLYa9LciHue3Ru2290eSThg4CzsPnx4SzkGpkSeVlhhptKtKZ+hp/ha3iVuw==}
/@sentry/utils@7.108.0:
resolution: {integrity: sha512-a45yEFD5qtgZaIFRAcFkG8C8lnDzn6t4LfLXuV4OafGAy/3ZAN3XN8wDnrruHkiUezSSANGsLg3bXaLW/JLvJw==}
engines: {node: '>=8'}
dependencies:
'@sentry/types': 7.107.0
'@sentry/types': 7.108.0
dev: false
/@sinclair/typebox@0.27.8:
@ -2691,10 +2714,6 @@ packages:
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
dev: false
/@types/unist@2.0.10:
resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==}
dev: true
/@types/unist@3.0.2:
resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==}
dev: true
@ -2994,6 +3013,11 @@ packages:
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
dev: true
/abbrev@2.0.0:
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
dev: true
/abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
@ -3266,10 +3290,6 @@ packages:
- supports-color
dev: true
/bail@1.0.5:
resolution: {integrity: sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==}
dev: true
/bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
dev: true
@ -3456,7 +3476,7 @@ packages:
http-cache-semantics: 4.1.1
keyv: 4.5.4
mimic-response: 4.0.0
normalize-url: 8.0.1
normalize-url: 8.0.0
responselike: 3.0.0
dev: false
@ -3802,6 +3822,11 @@ packages:
dependencies:
delayed-stream: 1.0.0
/commander@10.0.1:
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
engines: {node: '>=14'}
dev: true
/commander@11.1.0:
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
engines: {node: '>=16'}
@ -3822,6 +3847,13 @@ packages:
/concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
/config-chain@1.1.13:
resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==}
dependencies:
ini: 1.3.8
proto-list: 1.2.4
dev: true
/console-control-strings@1.1.0:
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
dev: true
@ -3872,14 +3904,6 @@ packages:
typescript: 5.4.3
dev: false
/cross-env@7.0.3:
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
hasBin: true
dependencies:
cross-spawn: 7.0.3
dev: true
/cross-spawn@5.1.0:
resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==}
dependencies:
@ -4232,6 +4256,10 @@ packages:
engines: {node: '>=6'}
dev: false
/eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
dev: true
/ecc-jsbn@0.1.2:
resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==}
dependencies:
@ -4245,6 +4273,17 @@ packages:
safe-buffer: 5.2.1
dev: false
/editorconfig@1.0.4:
resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==}
engines: {node: '>=14'}
hasBin: true
dependencies:
'@one-ini/wasm': 0.1.1
commander: 10.0.1
minimatch: 9.0.1
semver: 7.6.0
dev: true
/electron-to-chromium@1.4.669:
resolution: {integrity: sha512-E2SmpffFPrZhBSgf8ibqanRS2mpuk3FIRDzLDwt7WFpfgJMKDHJs0hmacyP0PS1cWsq0dVkwIIzlscNaterkPg==}
dev: true
@ -4260,6 +4299,10 @@ packages:
/emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
/emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
dev: true
/enabled@2.0.0:
resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
dev: false
@ -4936,6 +4979,14 @@ packages:
for-in: 1.0.2
dev: false
/foreground-child@3.1.1:
resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
engines: {node: '>=14'}
dependencies:
cross-spawn: 7.0.3
signal-exit: 4.1.0
dev: true
/forever-agent@0.6.1:
resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
dev: false
@ -5155,6 +5206,18 @@ packages:
is-glob: 4.0.3
dev: true
/glob@10.3.10:
resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==}
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
dependencies:
foreground-child: 3.1.1
jackspeak: 2.3.6
minimatch: 9.0.3
minipass: 5.0.0
path-scurry: 1.10.1
dev: true
/glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
dependencies:
@ -5585,6 +5648,10 @@ packages:
/inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
/ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
dev: true
/inquirer@8.2.6:
resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==}
engines: {node: '>=12.0.0'}
@ -5691,11 +5758,6 @@ packages:
resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
dev: false
/is-buffer@2.0.5:
resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==}
engines: {node: '>=4'}
dev: true
/is-builtin-module@3.2.1:
resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==}
engines: {node: '>=6'}
@ -5761,11 +5823,6 @@ packages:
engines: {node: '>=8'}
dev: true
/is-plain-obj@2.1.0:
resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==}
engines: {node: '>=8'}
dev: true
/is-plain-obj@4.1.0:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
@ -5855,6 +5912,32 @@ packages:
istanbul-lib-report: 3.0.1
dev: true
/jackspeak@2.3.6:
resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
engines: {node: '>=14'}
dependencies:
'@isaacs/cliui': 8.0.2
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
dev: true
/js-beautify@1.15.1:
resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==}
engines: {node: '>=14'}
hasBin: true
dependencies:
config-chain: 1.1.13
editorconfig: 1.0.4
glob: 10.3.10
js-cookie: 3.0.5
nopt: 7.2.0
dev: true
/js-cookie@3.0.5:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
dev: true
/js-tokens@3.0.2:
resolution: {integrity: sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==}
dev: false
@ -6280,7 +6363,6 @@ packages:
/lru-cache@10.2.0:
resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==}
engines: {node: 14 || >=16.14}
dev: false
/lru-cache@4.1.5:
resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==}
@ -6688,6 +6770,13 @@ packages:
dependencies:
brace-expansion: 1.1.11
/minimatch@9.0.1:
resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==}
engines: {node: '>=16 || 14 >=14.17'}
dependencies:
brace-expansion: 2.0.1
dev: true
/minimatch@9.0.3:
resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
engines: {node: '>=16 || 14 >=14.17'}
@ -6856,6 +6945,14 @@ packages:
abbrev: 1.1.1
dev: true
/nopt@7.2.0:
resolution: {integrity: sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
hasBin: true
dependencies:
abbrev: 2.0.0
dev: true
/normalize-package-data@2.5.0:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
dependencies:
@ -6865,8 +6962,8 @@ packages:
validate-npm-package-license: 3.0.4
dev: true
/normalize-url@8.0.1:
resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==}
/normalize-url@8.0.0:
resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==}
engines: {node: '>=14.16'}
dev: false
@ -7188,6 +7285,14 @@ packages:
/path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
/path-scurry@1.10.1:
resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==}
engines: {node: '>=16 || 14 >=14.17'}
dependencies:
lru-cache: 10.2.0
minipass: 5.0.0
dev: true
/path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
@ -7360,6 +7465,10 @@ packages:
engines: {node: '>= 8'}
dev: true
/proto-list@1.2.4:
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
dev: true
/proxy-agent@6.4.0:
resolution: {integrity: sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==}
engines: {node: '>= 14'}
@ -8285,6 +8394,15 @@ packages:
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
/string-width@5.1.2:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
dependencies:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
strip-ansi: 7.1.0
dev: true
/string-width@7.1.0:
resolution: {integrity: sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==}
engines: {node: '>=18'}
@ -8545,15 +8663,15 @@ packages:
hasBin: true
dev: false
/tldts-core@6.1.13:
resolution: {integrity: sha512-M1XP4D13YtXARKroULnLsKKuI1NCRAbJmUGGoXqWinajIDOhTeJf/trYUyBoLVx1/Nx1KBKxCrlW57ZW9cMHAA==}
/tldts-core@6.1.14:
resolution: {integrity: sha512-McyMQkkIUFYhfs3FPTxTn+5mewxERhfwy2x7TWHkBPb1poKaTBJhXehtuMg0FrhXp53J5eXRfvSD/oH/3mk/2A==}
dev: false
/tldts@6.1.13:
resolution: {integrity: sha512-+GxHFKVHvUTg2ieNPTx3b/NpZbgJSTZEDdI4cJzTjVYDuxijeHi1tt7CHHsMjLqyc+T50VVgWs3LIb2LrXOzxw==}
/tldts@6.1.14:
resolution: {integrity: sha512-zGbimRt9fHP68Gbj5fGWmR90xiuRDK/iYukHevT9mcG6Job+HfR119D9DSr6voFWjpStJwOtVfWGpbhNYVwt3A==}
hasBin: true
dependencies:
tldts-core: 6.1.13
tldts-core: 6.1.14
dev: false
/tmp@0.0.33:
@ -8627,10 +8745,6 @@ packages:
engines: {node: '>= 14.0.0'}
dev: false
/trough@1.0.5:
resolution: {integrity: sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==}
dev: true
/trough@2.2.0:
resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
dev: true
@ -8844,24 +8958,6 @@ packages:
vfile: 6.0.1
dev: true
/unified@9.2.2:
resolution: {integrity: sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==}
dependencies:
'@types/unist': 2.0.10
bail: 1.0.5
extend: 3.0.2
is-buffer: 2.0.5
is-plain-obj: 2.1.0
trough: 1.0.5
vfile: 4.2.1
dev: true
/unist-util-stringify-position@2.0.3:
resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==}
dependencies:
'@types/unist': 2.0.10
dev: true
/unist-util-stringify-position@4.0.0:
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
dependencies:
@ -8988,13 +9084,6 @@ packages:
extsprintf: 1.3.0
dev: false
/vfile-message@2.0.4:
resolution: {integrity: sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==}
dependencies:
'@types/unist': 2.0.10
unist-util-stringify-position: 2.0.3
dev: true
/vfile-message@4.0.2:
resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==}
dependencies:
@ -9002,15 +9091,6 @@ packages:
unist-util-stringify-position: 4.0.0
dev: true
/vfile@4.2.1:
resolution: {integrity: sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==}
dependencies:
'@types/unist': 2.0.10
is-buffer: 2.0.5
unist-util-stringify-position: 2.0.3
vfile-message: 2.0.4
dev: true
/vfile@6.0.1:
resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==}
dependencies:
@ -9282,7 +9362,15 @@ packages:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
dev: false
/wrap-ansi@8.1.0:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
dependencies:
ansi-styles: 6.2.1
string-width: 5.1.2
strip-ansi: 7.1.0
dev: true
/wrap-ansi@9.0.0:
resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==}

View File

@ -0,0 +1,122 @@
import { namespaces } from '../../lib/registry';
import fs from 'node:fs';
import * as path from 'node:path';
import { categories } from './data';
import { getCurrentPath } from '../../lib/utils/helpers';
const fullTests = await (await fetch('https://raw.githubusercontent.com/DIYgod/RSSHub/gh-pages/build/test-full-routes.json')).json();
const testResult = fullTests.testResults[0].assertionResults;
const __dirname = getCurrentPath(import.meta.url);
const docs = {};
for (const namespace in namespaces) {
let defaultCategory = namespaces[namespace].categories?.[0];
if (!defaultCategory) {
for (const path in namespaces[namespace].routes) {
if (namespaces[namespace].routes[path].categories) {
defaultCategory = namespaces[namespace].routes[path].categories[0];
break;
}
}
}
if (!defaultCategory) {
defaultCategory = 'other';
}
for (const path in namespaces[namespace].routes) {
const realPath = `/${namespace}${path}`;
const data = namespaces[namespace].routes[path];
const categories = data.categories || namespaces[namespace].categories || [defaultCategory];
// docs.json
for (const category of categories) {
if (!docs[category]) {
docs[category] = {};
}
if (!docs[category][namespace]) {
docs[category][namespace] = {
routes: {},
};
}
docs[category][namespace].name = namespaces[namespace].name;
docs[category][namespace].url = namespaces[namespace].url;
docs[category][namespace].description = namespaces[namespace].description;
docs[category][namespace].routes[realPath] = data;
}
}
}
// Generate markdown
const pinyinCompare = new Intl.Collator('zh-Hans-CN-u-co-pinyin').compare;
const isASCII = (str) => /^[\u0000-\u007F]*$/.test(str);
function generateMd(lang) {
const md = {};
for (const category in docs) {
const nameObj = categories.find((c) => c.link.includes(category));
if (!nameObj) {
throw new Error(`Category not found: ${category}, please double check your spelling.`);
}
md[category] = `# ${`${nameObj.icon} ${nameObj[lang]}`}\n\n`;
const namespaces = Object.keys(docs[category]).sort((a, b) => {
const aname = docs[category][a].name[0];
const bname = docs[category][b].name[0];
const ia = isASCII(aname);
const ib = isASCII(bname);
if (ia && ib) {
return aname.toLowerCase() < bname.toLowerCase() ? -1 : 1;
} else if (ia || ib) {
return ia > ib ? -1 : 1;
} else {
return pinyinCompare(aname, bname);
}
});
for (const namespace of namespaces) {
if (docs[category][namespace].name === 'Unknown') {
docs[category][namespace].name = namespace;
}
md[category] += `## ${docs[category][namespace].name || namespace} ${docs[category][namespace].url ? `<Site url="${docs[category][namespace].url}"/>` : ''}\n\n`;
if (docs[category][namespace].description) {
md[category] += `${docs[category][namespace].description}\n\n`;
}
const realPaths = Object.keys(docs[category][namespace].routes).sort((a, b) => {
const aname = docs[category][namespace].routes[a].name[0];
const bname = docs[category][namespace].routes[b].name[0];
const ia = isASCII(aname);
const ib = isASCII(bname);
if (ia && ib) {
return aname.toLowerCase() < bname.toLowerCase() ? -1 : 1;
} else if (ia || ib) {
return ia > ib ? -1 : 1;
} else {
return pinyinCompare(aname, bname);
}
});
for (const realPath of realPaths) {
const data = docs[category][namespace].routes[realPath];
const test = testResult.find((t) => t.title === realPath);
const parsedTest = test
? {
code: test.status === 'passed' ? 0 : 1,
message: test.failureMessages?.[0],
}
: undefined;
md[category] += `### ${data.name} ${data.url || docs[category][namespace].url ? `<Site url="${data.url || docs[category][namespace].url}" size="sm" />` : ''}\n\n`;
md[category] += `<Route namespace="${namespace}" :data='${JSON.stringify(data).replaceAll(`'`, '&#39;')}' :test='${JSON.stringify(parsedTest)?.replaceAll(`'`, '&#39;')}' />\n\n`;
if (data.description) {
md[category] += `${data.description}\n\n`;
}
}
}
}
fs.mkdirSync(path.join(__dirname, `../../assets/build/docs/${lang}`), { recursive: true });
for (const category in md) {
fs.writeFileSync(path.join(__dirname, `../../assets/build/docs/${lang}/${category}.md`), md[category]);
}
}
generateMd('en');
generateMd('zh');

View File

@ -4,7 +4,9 @@ import { parse } from 'tldts';
import fs from 'node:fs';
import * as path from 'node:path';
import toSource from 'tosource';
import { categories } from './data';
import { getCurrentPath } from '../../lib/utils/helpers';
const __dirname = getCurrentPath(import.meta.url);
const maintainers: Record<string, string[]> = {};
const radar: {
@ -13,7 +15,6 @@ const radar: {
[subdomain: string]: RadarItem[] | string;
};
} = {};
const docs = {};
for (const namespace in namespaces) {
let defaultCategory = namespaces[namespace].categories?.[0];
@ -63,21 +64,6 @@ for (const namespace in namespaces) {
}
}
}
// docs.json
for (const category of categories) {
if (!docs[category]) {
docs[category] = {};
}
if (!docs[category][namespace]) {
docs[category][namespace] = {
routes: {},
};
}
docs[category][namespace].name = namespaces[namespace].name;
docs[category][namespace].url = namespaces[namespace].url;
docs[category][namespace].description = namespaces[namespace].description;
docs[category][namespace].routes[realPath] = data;
}
}
}
@ -85,67 +71,3 @@ fs.writeFileSync(path.join(__dirname, '../../assets/build/radar-rules.json'), JS
fs.writeFileSync(path.join(__dirname, '../../assets/build/radar-rules.js'), `(${toSource(radar)})`);
fs.writeFileSync(path.join(__dirname, '../../assets/build/maintainers.json'), JSON.stringify(maintainers, null, 2));
fs.writeFileSync(path.join(__dirname, '../../assets/build/routes.json'), JSON.stringify(namespaces, null, 2));
// Generate markdown
const pinyinCompare = new Intl.Collator('zh-Hans-CN-u-co-pinyin').compare;
const isASCII = (str) => /^[\u0000-\u007F]*$/.test(str);
function generateMd(lang) {
const md = {};
for (const category in docs) {
const nameObj = categories.find((c) => c.link.includes(category));
md[category] = `# ${`${nameObj!.icon} ${nameObj![lang]}`}\n\n`;
const namespaces = Object.keys(docs[category]).sort((a, b) => {
const aname = docs[category][a].name[0];
const bname = docs[category][b].name[0];
const ia = isASCII(aname);
const ib = isASCII(bname);
if (ia && ib) {
return aname.toLowerCase() < bname.toLowerCase() ? -1 : 1;
} else if (ia || ib) {
return ia > ib ? -1 : 1;
} else {
return pinyinCompare(aname, bname);
}
});
for (const namespace of namespaces) {
if (docs[category][namespace].name === 'Unknown') {
docs[category][namespace].name = namespace;
}
md[category] += `## ${docs[category][namespace].name || namespace} ${docs[category][namespace].url ? `<Site url="${docs[category][namespace].url}"/>` : ''}\n\n`;
if (docs[category][namespace].description) {
md[category] += `${docs[category][namespace].description}\n\n`;
}
const realPaths = Object.keys(docs[category][namespace].routes).sort((a, b) => {
const aname = docs[category][namespace].routes[a].name[0];
const bname = docs[category][namespace].routes[b].name[0];
const ia = isASCII(aname);
const ib = isASCII(bname);
if (ia && ib) {
return aname.toLowerCase() < bname.toLowerCase() ? -1 : 1;
} else if (ia || ib) {
return ia > ib ? -1 : 1;
} else {
return pinyinCompare(aname, bname);
}
});
for (const realPath of realPaths) {
const data = docs[category][namespace].routes[realPath];
md[category] += `### ${data.name} ${data.url || docs[category][namespace].url ? `<Site url="${data.url || docs[category][namespace].url}" size="sm" />` : ''}\n\n`;
md[category] += `<Route namespace="${namespace}" :data='${JSON.stringify(data).replaceAll(`'`, '&#39;')}' />\n\n`;
if (data.description) {
md[category] += `${data.description}\n\n`;
}
}
}
}
fs.mkdirSync(path.join(__dirname, `../../assets/build/docs/${lang}`), { recursive: true });
for (const category in md) {
fs.writeFileSync(path.join(__dirname, `../../assets/build/docs/${lang}/${category}.md`), md[category]);
}
}
generateMd('en');
generateMd('zh');

View File

@ -1,10 +1,9 @@
const unified = require('unified');
const parse = require('remark-parse');
const got = require('got');
import { unified } from 'unified';
import remarkParse from 'remark-parse';
// @TODO maybe we could use label or some other better ways to distinguish bug/feature issues
const matchTitle = ['路由地址', 'Routes'];
const maintainerURL = 'https://raw.githubusercontent.com/DIYgod/RSSHub/gh-pages/build/maintainer.json';
const maintainerURL = 'https://raw.githubusercontent.com/DIYgod/RSSHub/gh-pages/build/maintainers.json';
const successTag = 'Bug Ping: Pinged';
const parseFailTag = 'Bug Ping: Parsing Failed';
const failTag = 'Bug Ping: Not Found';
@ -15,7 +14,7 @@ const route = 'Route';
const dndUsernames = new Set([]);
async function parseBodyRoutes(body, core) {
const ast = await unified().use(parse).parse(body);
const ast = await unified().use(remarkParse).parse(body);
// Is this a bug report?
const title = ast.children[0].children[0].value.trim();
@ -44,10 +43,8 @@ async function parseBodyRoutes(body, core) {
}
async function getMaintainersByRoutes(routes, core) {
// TODO: change me when https://github.com/actions/github-script is run on node20
// const response = await fetch(maintainerURL);
// const maintainers = await response.json();
const maintainers = await got(maintainerURL).json();
const response = await fetch(maintainerURL);
const maintainers = await response.json();
return routes.map((e) => {
const m = maintainers[e];
@ -59,9 +56,9 @@ async function getMaintainersByRoutes(routes, core) {
});
}
module.exports = async ({ github, context, core }) => {
export default async function callMaintainer({ github, context, core }) {
const body = context.payload.issue.body;
const issue_facts = {
const issueFacts = {
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
@ -70,7 +67,7 @@ module.exports = async ({ github, context, core }) => {
const addLabels = (labels) =>
github.rest.issues
.addLabels({
...issue_facts,
...issueFacts,
labels,
})
.catch((error) => {
@ -79,7 +76,7 @@ module.exports = async ({ github, context, core }) => {
const updateIssueState = (state) =>
github.rest.issues
.update({
...issue_facts,
...issueFacts,
state,
})
.catch((error) => {
@ -138,7 +135,7 @@ module.exports = async ({ github, context, core }) => {
}
}
const labels = [`Count: ${successCount}/${routes.length}`];
const labels = [''];
if (failedCount > 0) {
labels.push(failTag);
@ -160,14 +157,14 @@ module.exports = async ({ github, context, core }) => {
// Reply to the issue and notify the maintainers (if any)
await github.rest.issues
.createComment({
...issue_facts,
...issueFacts,
body: `${comments}
> To maintainers: if you are not willing to be disturbed, list your username in \`scripts/workflow/test-issue/call-maintainer.js\`. In this way, your username will be wrapped in an inline code block when tagged so you will not be notified.
如果所有路由都无法匹配issue 将会被自动关闭如果 issue 和路由无关请使用 \`NOROUTE\` 关键词,或者留下评论。我们会重新审核。
If all routes can not be found, the issue will be closed automatically. Please use \`NOROUTE\` for a route-irrelevant issue or leave a comment if it is a mistake.
如果所有路由都无法匹配issue 将会被自动关闭如果 issue 和路由无关请使用 \`NOROUTE\` 关键词,或者留下评论。我们会重新审核。
`,
})
.catch((error) => {
@ -177,4 +174,4 @@ If all routes can not be found, the issue will be closed automatically. Please u
if (failedCount && emptyCount === 0 && successCount === 0) {
await updateIssueState('closed');
}
};
}

View File

@ -1,14 +1,15 @@
const noFound = 'Auto: Route No Found';
const testFailed = 'Auto: Route Test Failed';
const allowedUser = new Set(['dependabot[bot]', 'pull[bot]']); // dependabot and downstream PR requested by pull[bot]
module.exports = async ({ github, context, core }, body, number, sender) => {
export default async function identify({ github, context, core }, body, number, sender) {
core.debug(`sender: ${sender}`);
core.debug(`body: ${body}`);
// Remove all HTML comments before performing the match
const bodyNoCmts = body.replaceAll(/<!--[\S\s]*?-->/g, '');
const m = bodyNoCmts.match(/```routes\s+([\S\s]*?)```/);
core.debug(`match: ${m}`);
let res = null;
let routes = null;
const issueFacts = {
owner: context.repo.owner,
@ -31,11 +32,11 @@ module.exports = async ({ github, context, core }, body, number, sender) => {
core.warning(error);
});
const removeLabel = () =>
const removeLabel = (labelName = noFound) =>
github.rest.issues
.removeLabel({
...issueFacts,
name: noFound,
name: labelName,
})
.catch((error) => {
core.warning(error);
@ -80,8 +81,13 @@ module.exports = async ({ github, context, core }, body, number, sender) => {
.catch((error) => {
core.warning(error);
});
if (pr.pull_request && pr.state === 'closed') {
await updatePrState('open');
if (pr.pull_request) {
if (pr.state === 'closed') {
await updatePrState('open');
}
if (pr.labels.some((e) => e.name === testFailed)) {
await removeLabel(testFailed);
}
}
if (allowedUser.has(sender)) {
@ -94,19 +100,25 @@ module.exports = async ({ github, context, core }, body, number, sender) => {
}
if (m && m[1]) {
res = m[1].trim().split(/\r?\n/);
core.info(`routes detected: ${res}`);
routes = m[1].trim().split(/\r?\n/);
core.info(`routes detected: ${routes}`);
if (res.length && res[0] === 'NOROUTE') {
if (routes.length && routes[0] === 'NOROUTE') {
core.info('PR stated no route, passing');
await removeLabel();
await addLabels(['Auto: Route Test Skipped']);
return;
} else if (res.length && !res.some((e) => e.includes('/:'))) {
} else if (routes.length) {
if (routes.some((e) => e.includes('/:'))) {
await addLabels([noFound]);
return createComment(`Please use actual values in \`routes\` section instead of path parameters.
请在 \`routes\` 部分使用实际值而不是路径参数。`);
}
core.exportVariable('TEST_CONTINUE', true);
await removeLabel();
return res;
return routes;
}
}
@ -119,4 +131,4 @@ module.exports = async ({ github, context, core }, body, number, sender) => {
}
throw new Error('Please follow the PR rules: failed to detect route');
};
}

View File

@ -1,103 +0,0 @@
/* eslint-disable no-await-in-loop */
module.exports = async ({ github, context, core, got }, baseUrl, routes, number) => {
if (routes[0] === 'NOROUTE') {
return;
}
const links = routes.map((e) => {
const l = e.startsWith('/') ? e : `/${e}`;
return `${baseUrl}${l}`;
});
let com_l = [];
let com = `Successfully [generated](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) as following:\n`;
for (const lks of links) {
core.info(`testing route: ${lks}`);
// Intended, one at a time
let success = false;
let detail;
try {
// TODO: change me when https://github.com/actions/github-script is run on node20
// const res = await fetch(lks);
// if (!res.ok) {
// throw res;
// }
// success = true;
// detail = (await res.text()).replace(/\s+(\n|$)/g, '\n');
const res = await got(lks);
if (res && res.body) {
success = true;
detail = res.body.replaceAll(/\s+(\n|$)/g, '\n');
}
} catch (error) {
// TODO: change me when https://github.com/actions/github-script is run on node20
// detail = `HTTPError: Response code ${err.status} (${err.statusText})`;
// const res = await err.text();
// const errInfoList = err.body && res.match(/(?<=<pre class="message">)(.+?)(?=<\/pre>)/gs);
detail = error.toString();
const errInfoList = error.response && error.response.body && error.response.body.match(/(?<=<pre class="message">)(.+?)(?=<\/pre>)/gs);
if (errInfoList) {
detail += '\n\n';
detail += errInfoList
.slice(0, 3)
.map((e) => e.trim())
.join('\n');
}
}
let temp_com = `
<details>
<summary><a href="${lks}">${lks}</a> - ${success ? 'Success ' : '<b>Failed </b>'}</summary>
\`\`\`${success ? 'rss' : ''}`;
temp_com += `
${detail.slice(0, 65300 - temp_com.length)}
\`\`\`
</details>
`;
if (com.length + temp_com.length >= 65500) {
com += '\n\n...';
com_l.push(com);
com = temp_com;
} else {
com += temp_com;
}
}
if (com.length > 0) {
com_l.push(com);
}
if (com_l.length >= 5) {
com_l = com_l.slice(0, 5);
}
if (process.env.PULL_REQUEST) {
await github.rest.issues
.addLabels({
issue_number: number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['Auto: Route Test Complete'],
})
.catch((error) => {
core.warning(error);
});
}
for (const com_s of com_l) {
// Intended, one at a time
await github.rest.issues
.createComment({
issue_number: number,
owner: context.repo.owner,
repo: context.repo.repo,
body: com_s,
})
.catch((error) => {
core.warning(error);
});
}
};

View File

@ -0,0 +1,92 @@
import jsBeautify from 'js-beautify';
export default async function test({ github, context, core }, baseUrl, routes, number) {
if (routes[0] === 'NOROUTE') {
return;
}
const links = routes.map((e) => {
const l = e.startsWith('/') ? e : `/${e}`;
return `${baseUrl}${l}`;
});
let commentList = [];
let comment = `Successfully [generated](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) as following:\n`;
for await (const lks of links) {
core.info(`testing route: ${lks}`);
// Intended, one at a time
let success = false;
let detail;
const res = await fetch(lks);
const body = await res.text();
if (res.ok) {
success = true;
detail = jsBeautify.html(body.replaceAll(/\s+(\n|$)/g, '\n'), { indent_size: 2 });
} else {
detail = `HTTPError: Response code ${res.status} (${res.statusText})`;
const errInfoList = body && body.match(/(?<=<p class="message">)(.+?)(?=<\/p>)/gs);
if (errInfoList) {
detail += '\n\n';
detail += errInfoList
.slice(0, 5)
.map((e) => (e.length > 1000 ? e.slice(0, 1000) + '...' : e).trim())
.join('\n');
}
}
let routeFeedback = `
<details>
<summary><a href="${lks}">${lks}</a> - ${success ? 'Success ' : '<b>Failed </b>'}</summary>
\`\`\`${success ? 'rss' : ''}`;
routeFeedback += `
${detail.slice(0, 65300 - routeFeedback.length)}
\`\`\`
</details>
`;
if (comment.length + routeFeedback.length >= 65500) {
comment += '\n\n...';
commentList.push(comment);
comment = routeFeedback;
} else {
comment += routeFeedback;
}
}
if (comment.length > 0) {
commentList.push(comment);
}
if (commentList.length >= 5) {
commentList = commentList.slice(0, 5);
}
if (process.env.PULL_REQUEST) {
await github.rest.issues
.addLabels({
issue_number: number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['Auto: Route Test Complete'],
})
.catch((error) => {
core.warning(error);
});
}
for await (const comment of commentList) {
// Intended, one at a time
await github.rest.issues
.createComment({
issue_number: number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment,
})
.catch((error) => {
core.warning(error);
});
}
}

View File

@ -2,7 +2,7 @@
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"moduleResolution": "node",
"strict": true,
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx",
@ -11,7 +11,12 @@
},
"esModuleInterop": true,
"noImplicitAny": false,
"outDir": "./dist"
"outDir": "./dist",
"skipLibCheck": true,
"noEmit": true,
"incremental": true,
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["./lib/**/*", "./api/vercel.ts"],
"exclude": ["node_modules", "*.test.*"]