mirror of
https://github.com/DIYgod/RSSHub.git
synced 2026-09-24 23:12:34 +08:00
@@ -114,4 +114,12 @@ describe('template', () => {
|
||||
expect(response.status).toBe(301);
|
||||
expect(response.headers.get('location')).toBe('/test/1');
|
||||
});
|
||||
|
||||
it(`api`, async () => {
|
||||
const response = await app.request('/api/test');
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toEqual({
|
||||
code: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,11 @@ const middleware: MiddlewareHandler = async (ctx, next) => {
|
||||
const ttl = (cacheModule.status.available && Math.trunc(config.cache.routeExpire / 60)) || 1;
|
||||
await next();
|
||||
|
||||
const apiData = ctx.get('apiData');
|
||||
if (apiData) {
|
||||
return ctx.json(apiData);
|
||||
}
|
||||
|
||||
const data: Data = ctx.get('data');
|
||||
const outputType = ctx.req.query('format') || 'rss';
|
||||
|
||||
|
||||
+68
-1
@@ -1,4 +1,4 @@
|
||||
import type { Namespace, Route } from '@/types';
|
||||
import type { APIRoute, Namespace, Route } from '@/types';
|
||||
import { directoryImport } from 'directory-import';
|
||||
import { Hono, type Handler } from 'hono';
|
||||
import path from 'node:path';
|
||||
@@ -22,6 +22,12 @@ let namespaces: Record<
|
||||
location: string;
|
||||
}
|
||||
>;
|
||||
apiRoutes: Record<
|
||||
string,
|
||||
APIRoute & {
|
||||
location: string;
|
||||
}
|
||||
>;
|
||||
}
|
||||
> = {};
|
||||
|
||||
@@ -48,6 +54,9 @@ if (Object.keys(modules).length) {
|
||||
}
|
||||
| {
|
||||
namespace: Namespace;
|
||||
}
|
||||
| {
|
||||
apiRoute: APIRoute;
|
||||
};
|
||||
const namespace = module.split(/[/\\]/)[1];
|
||||
if ('namespace' in content) {
|
||||
@@ -63,6 +72,7 @@ if (Object.keys(modules).length) {
|
||||
namespaces[namespace] = {
|
||||
name: namespace,
|
||||
routes: {},
|
||||
apiRoutes: {},
|
||||
};
|
||||
}
|
||||
if (Array.isArray(content.route.path)) {
|
||||
@@ -78,6 +88,27 @@ if (Object.keys(modules).length) {
|
||||
location: module.split(/[/\\]/).slice(2).join('/'),
|
||||
};
|
||||
}
|
||||
} else if ('apiRoute' in content) {
|
||||
if (!namespaces[namespace]) {
|
||||
namespaces[namespace] = {
|
||||
name: namespace,
|
||||
routes: {},
|
||||
apiRoutes: {},
|
||||
};
|
||||
}
|
||||
if (Array.isArray(content.apiRoute.path)) {
|
||||
for (const path of content.apiRoute.path) {
|
||||
namespaces[namespace].apiRoutes[path] = {
|
||||
...content.apiRoute,
|
||||
location: module.split(/[/\\]/).slice(2).join('/'),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
namespaces[namespace].apiRoutes[content.apiRoute.path] = {
|
||||
...content.apiRoute,
|
||||
location: module.split(/[/\\]/).slice(2).join('/'),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,6 +174,42 @@ for (const namespace in namespaces) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const namespace in namespaces) {
|
||||
const subApp = app.basePath(`/api/${namespace}`);
|
||||
|
||||
const namespaceData = namespaces[namespace];
|
||||
if (!namespaceData || !namespaceData.apiRoutes) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sortedRoutes = Object.entries(namespaceData.apiRoutes) as [
|
||||
string,
|
||||
APIRoute & {
|
||||
location: string;
|
||||
module?: () => Promise<{ apiRoute: APIRoute }>;
|
||||
},
|
||||
][];
|
||||
|
||||
for (const [path, routeData] of sortedRoutes) {
|
||||
const wrappedHandler: Handler = async (ctx) => {
|
||||
if (!ctx.get('apiData')) {
|
||||
if (typeof routeData.handler !== 'function') {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
const { apiRoute } = await import(`./routes/${namespace}/${routeData.location}`);
|
||||
routeData.handler = apiRoute.handler;
|
||||
} else if (routeData.module) {
|
||||
const { apiRoute } = await routeData.module();
|
||||
routeData.handler = apiRoute.handler;
|
||||
}
|
||||
}
|
||||
const data = await routeData.handler(ctx);
|
||||
ctx.set('apiData', data);
|
||||
}
|
||||
};
|
||||
subApp.get(path, wrappedHandler);
|
||||
}
|
||||
}
|
||||
|
||||
app.get('/', index);
|
||||
app.get('/healthz', healthz);
|
||||
app.get('/robots.txt', robotstxt);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { APIRoute } from '@/types';
|
||||
|
||||
export const apiRoute: APIRoute = {
|
||||
path: '/',
|
||||
maintainers: ['DIYgod'],
|
||||
handler,
|
||||
};
|
||||
|
||||
function handler() {
|
||||
return {
|
||||
code: 0,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { APIRoute } from '@/types';
|
||||
import { checkCookie } from './util';
|
||||
|
||||
export const apiRoute: APIRoute = {
|
||||
path: '/check-cookie',
|
||||
description: '检查小红书 cookie 是否有效',
|
||||
maintainers: ['DIYgod'],
|
||||
handler,
|
||||
};
|
||||
|
||||
async function handler() {
|
||||
const valid = await checkCookie();
|
||||
return {
|
||||
code: valid ? 0 : -1,
|
||||
};
|
||||
}
|
||||
@@ -64,7 +64,7 @@ async function handler(ctx) {
|
||||
if (cookie && category === 'notes') {
|
||||
try {
|
||||
const urlNotePrefix = 'https://www.xiaohongshu.com/explore';
|
||||
const user = await getUserWithCookie(url, cookie);
|
||||
const user = await getUserWithCookie(url);
|
||||
const notes = await renderNotesFulltext(user.notes, urlNotePrefix, displayLivePhoto);
|
||||
return {
|
||||
title: `${user.userPageData.basicInfo.nickname} - 笔记 • 小红书 / RED`,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { config } from '@/config';
|
||||
import logger from '@/utils/logger';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
import puppeteer from '@/utils/puppeteer';
|
||||
import { ofetch } from 'ofetch';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
import { load } from 'cheerio';
|
||||
import cache from '@/utils/cache';
|
||||
|
||||
@@ -235,7 +235,8 @@ async function getFullNote(link, displayLivePhoto) {
|
||||
return data;
|
||||
}
|
||||
|
||||
async function getUserWithCookie(url: string, cookie: string) {
|
||||
async function getUserWithCookie(url: string) {
|
||||
const cookie = config.xiaohongshu.cookie;
|
||||
const res = await ofetch(url, {
|
||||
headers: getHeaders(cookie),
|
||||
});
|
||||
@@ -267,4 +268,12 @@ function extractInitialState($) {
|
||||
return script;
|
||||
}
|
||||
|
||||
export { getUser, getBoard, formatText, formatNote, renderNotesFulltext, getFullNote, getUserWithCookie };
|
||||
async function checkCookie() {
|
||||
const cookie = config.xiaohongshu.cookie;
|
||||
const res = await ofetch('https://edith.xiaohongshu.com/api/sns/web/v2/user/me', {
|
||||
headers: getHeaders(cookie),
|
||||
});
|
||||
return res.code === 0 && !!res.data.user_id;
|
||||
}
|
||||
|
||||
export { getUser, getBoard, formatText, formatNote, renderNotesFulltext, getFullNote, getUserWithCookie, checkCookie };
|
||||
|
||||
+48
-3
@@ -359,14 +359,12 @@ interface RouteItem {
|
||||
view?: ViewType;
|
||||
}
|
||||
|
||||
interface Route extends RouteItem {
|
||||
export interface Route extends RouteItem {
|
||||
ja?: RouteItem;
|
||||
zh?: RouteItem;
|
||||
'zh-TW'?: RouteItem;
|
||||
}
|
||||
|
||||
export type { Route };
|
||||
|
||||
// radar
|
||||
export type RadarItem = {
|
||||
/**
|
||||
@@ -412,3 +410,50 @@ export type RadarDomain = {
|
||||
} & {
|
||||
[subdomain: string]: RadarItem[];
|
||||
};
|
||||
|
||||
export interface APIRoute {
|
||||
/**
|
||||
* The route path, using [Hono routing](https://hono.dev/api/routing) syntax
|
||||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* The GitHub handle of the people responsible for maintaining this route
|
||||
*/
|
||||
maintainers: string[];
|
||||
|
||||
/**
|
||||
* The handler function of the route
|
||||
*/
|
||||
handler: (ctx: Context) =>
|
||||
| Promise<{
|
||||
code: number;
|
||||
message?: string;
|
||||
data?: any;
|
||||
}>
|
||||
| {
|
||||
code: number;
|
||||
message?: string;
|
||||
data?: any;
|
||||
};
|
||||
|
||||
/**
|
||||
* The description of the route parameters
|
||||
*/
|
||||
parameters?: Record<
|
||||
string,
|
||||
{
|
||||
description: string;
|
||||
default?: string;
|
||||
options?: {
|
||||
value: string;
|
||||
label: string;
|
||||
}[];
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Hints and additional explanations for users using this route, it will be appended after the route component, supports markdown
|
||||
*/
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,10 @@ for (const namespace in namespaces) {
|
||||
}
|
||||
data.module = `() => import('@/routes/${namespace}/${data.location}')`;
|
||||
}
|
||||
for (const path in namespaces[namespace].apiRoutes) {
|
||||
const data = namespaces[namespace].apiRoutes[path];
|
||||
data.module = `() => import('@/routes/${namespace}/${data.location}')`;
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(__dirname, '../../assets/build/radar-rules.json'), JSON.stringify(radar, null, 2));
|
||||
|
||||
Reference in New Issue
Block a user