From 6800f031a48800adbf923c4aa01493d8fd762b9b Mon Sep 17 00:00:00 2001
From: Franklittleboy <54013763+Franklittleboy@users.noreply.github.com>
Date: Mon, 11 May 2026 22:39:46 +0800
Subject: [PATCH 01/15] feat: add GXNAS blog route
---
lib/routes/gxnas/namespace.ts | 8 ++++++++
1 file changed, 8 insertions(+)
create mode 100644 lib/routes/gxnas/namespace.ts
diff --git a/lib/routes/gxnas/namespace.ts b/lib/routes/gxnas/namespace.ts
new file mode 100644
index 000000000000..3009596de728
--- /dev/null
+++ b/lib/routes/gxnas/namespace.ts
@@ -0,0 +1,8 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'GXNAS博客',
+ url: 'wp.gxnas.com',
+ description: 'GXNAS博客 - NAS技术交流',
+ lang: 'zh-CN',
+};
From f461e2e59b6c22fc9f55cca3b087f46e84f10045 Mon Sep 17 00:00:00 2001
From: Franklittleboy <54013763+Franklittleboy@users.noreply.github.com>
Date: Mon, 11 May 2026 22:40:09 +0800
Subject: [PATCH 02/15] feat: add GXNAS blog route - index
---
lib/routes/gxnas/index.ts | 71 +++++++++++++++++++++++++++++++++++++++
1 file changed, 71 insertions(+)
create mode 100644 lib/routes/gxnas/index.ts
diff --git a/lib/routes/gxnas/index.ts b/lib/routes/gxnas/index.ts
new file mode 100644
index 000000000000..2c1d5862497e
--- /dev/null
+++ b/lib/routes/gxnas/index.ts
@@ -0,0 +1,71 @@
+import { load } from 'cheerio';
+
+import type { DataItem, Route } from '@/types';
+import ofetch from '@/utils/ofetch';
+import { parseDate } from '@/utils/parse-date';
+
+export const route: Route = {
+ path: '/',
+ categories: ['blog'],
+ example: '/gxnas',
+ radar: [
+ {
+ source: ['wp.gxnas.com/'],
+ },
+ ],
+ url: 'wp.gxnas.com/',
+ name: '最新文章',
+ maintainers: ['Franklittleboy'],
+ handler,
+ description: 'GXNAS博客最新文章',
+};
+
+async function handler() {
+ const rootUrl = 'https://wp.gxnas.com';
+
+ const response = await ofetch(rootUrl);
+ const $ = load(response);
+
+ const items = $('.article-panel')
+ .toArray()
+ .map((el) => {
+ const $el = $(el);
+
+ const titleEl = $el.find('.header .title a');
+ const title = titleEl.text().trim();
+ const link = titleEl.attr('href');
+
+ const categoryEl = $el.find('.header .label');
+ const category = categoryEl.text().trim();
+
+ const contentEl = $el.find('.content');
+ const description = contentEl.html() || '';
+
+ const thumbEl = $el.find('.a-thumb img');
+ const image = thumbEl.attr('src');
+
+ // Date format: 2023年11月17日
+ const dateText = $el.find('.a-meta span').first().text().trim();
+ let pubDate: Date | undefined;
+ const m = dateText.match(/(\d{4})年(\d{1,2})月(\d{1,2})日/);
+ if (m) {
+ pubDate = new Date(parseInt(m[1]), parseInt(m[2]) - 1, parseInt(m[3]));
+ }
+
+ return {
+ title,
+ link,
+ description: image ? `
${description}` : description,
+ category,
+ pubDate: pubDate ? parseDate(pubDate) : undefined,
+ } as DataItem;
+ })
+ .filter((item) => item.title && item.link);
+
+ return {
+ title: 'GXNAS博客',
+ link: rootUrl,
+ item: items,
+ description: 'GXNAS博客 - NAS博客|NAS社区|NAS交流|NAS技术|群晖教程|软路由',
+ };
+}
From dcf1c4e77d661ce93f98539e096db38834d95df6 Mon Sep 17 00:00:00 2001
From: Franklittleboy <54013763+Franklittleboy@users.noreply.github.com>
Date: Mon, 11 May 2026 23:48:27 +0800
Subject: [PATCH 03/15] fix: resolve oxlint errors and mark antiCrawler feature
- Replace parseInt with Number.parseInt (unicorn/prefer-number-properties)
- Simplify ternary pubDate assignment (no-unneeded-ternary)
- Add features block with antiCrawler: true for wp.gxnas.com
---
lib/routes/gxnas/index.ts | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/lib/routes/gxnas/index.ts b/lib/routes/gxnas/index.ts
index 2c1d5862497e..a0204d93b629 100644
--- a/lib/routes/gxnas/index.ts
+++ b/lib/routes/gxnas/index.ts
@@ -18,6 +18,14 @@ export const route: Route = {
maintainers: ['Franklittleboy'],
handler,
description: 'GXNAS博客最新文章',
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: true,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
};
async function handler() {
@@ -47,9 +55,10 @@ async function handler() {
// Date format: 2023年11月17日
const dateText = $el.find('.a-meta span').first().text().trim();
let pubDate: Date | undefined;
+
const m = dateText.match(/(\d{4})年(\d{1,2})月(\d{1,2})日/);
if (m) {
- pubDate = new Date(parseInt(m[1]), parseInt(m[2]) - 1, parseInt(m[3]));
+ pubDate = new Date(Number.parseInt(m[1]), Number.parseInt(m[2]) - 1, Number.parseInt(m[3]));
}
return {
@@ -57,7 +66,7 @@ async function handler() {
link,
description: image ? `
${description}` : description,
category,
- pubDate: pubDate ? parseDate(pubDate) : undefined,
+ pubDate: pubDate && parseDate(pubDate),
} as DataItem;
})
.filter((item) => item.title && item.link);
From 6ae8224b773554b94a42830dc5376a6ccfbdb9a1 Mon Sep 17 00:00:00 2001
From: Franklittleboy <54013763+Franklittleboy@users.noreply.github.com>
Date: Tue, 12 May 2026 19:38:48 +0800
Subject: [PATCH 04/15] =?UTF-8?q?fix(gxnas):=20=E4=BD=BF=E7=94=A8=20FlareS?=
=?UTF-8?q?olverr=20=E7=BB=95=E8=BF=87=20Cloudflare=20JS=20Challenge?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docker-compose.yml | 34 ++++++++++++++++------------------
lib/routes/gxnas/index.ts | 23 +++++++++++++++++++++--
2 files changed, 37 insertions(+), 20 deletions(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index 261d910457b0..7d986405c3e3 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,9 +1,6 @@
services:
rsshub:
- # two ways to enable Playwright:
- # * comment out marked lines, then use this image instead: diygod/rsshub:chromium-bundled
- # * (consumes more disk space and memory) leave everything unchanged
- image: diygod/rsshub # or ghcr.io/diygod/rsshub
+ build: .
restart: always
ports:
- '1200:1200'
@@ -11,7 +8,7 @@ services:
NODE_ENV: production
CACHE_TYPE: redis
REDIS_URL: 'redis://redis:6379/'
- PLAYWRIGHT_WS_ENDPOINT: 'ws://browserless:3000' # marked
+ FLARESOLVERR_URL: http://flaresolverr:8191/v1
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:1200/healthz']
interval: 30s
@@ -19,20 +16,21 @@ services:
retries: 3
depends_on:
- redis
- - browserless # marked
- browserless: # marked
- image: browserless/chrome # marked
- restart: always # marked
- ulimits: # marked
- core: # marked
- hard: 0 # marked
- soft: 0 # marked
- healthcheck: # marked
- test: ['CMD', 'curl', '-f', 'http://localhost:3000/pressure'] # marked
- interval: 30s # marked
- timeout: 10s # marked
- retries: 3 # marked
+ redis:
+ image: redis:alpine
+ restart: always
+ volumes:
+ - redis-data:/data
+ healthcheck:
+ test: ['CMD', 'redis-cli', 'ping']
+ interval: 30s
+ timeout: 10s
+ retries: 5
+ start_period: 5s
+
+volumes:
+ redis-data:
redis:
image: redis:alpine
diff --git a/lib/routes/gxnas/index.ts b/lib/routes/gxnas/index.ts
index a0204d93b629..f923726ca46b 100644
--- a/lib/routes/gxnas/index.ts
+++ b/lib/routes/gxnas/index.ts
@@ -3,6 +3,9 @@ import { load } from 'cheerio';
import type { DataItem, Route } from '@/types';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
+import logger from '@/utils/logger';
+
+const FLARESOLVERR_URL = process.env.FLARESOLVERR_URL || 'http://flaresolverr:8191/v1';
export const route: Route = {
path: '/',
@@ -31,8 +34,24 @@ export const route: Route = {
async function handler() {
const rootUrl = 'https://wp.gxnas.com';
- const response = await ofetch(rootUrl);
- const $ = load(response);
+ logger.debug(`Fetching ${rootUrl} via FlareSolverr at ${FLARESOLVERR_URL}`);
+
+ const result = await ofetch(FLARESOLVERR_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: {
+ cmd: 'request.get',
+ url: rootUrl,
+ maxTimeout: 60000,
+ },
+ });
+
+ const html = result?.solution?.response;
+ if (!html) {
+ throw new Error('FlareSolverr 返回内容为空');
+ }
+
+ const $ = load(html);
const items = $('.article-panel')
.toArray()
From b950d62fa2c60099b95d04adb1b53970c4b5808a Mon Sep 17 00:00:00 2001
From: Franklittleboy <54013763+Franklittleboy@users.noreply.github.com>
Date: Tue, 12 May 2026 19:43:54 +0800
Subject: [PATCH 05/15] =?UTF-8?q?fix:=20=E6=94=B9=E4=B8=BA=E4=BD=BF?=
=?UTF-8?q?=E7=94=A8=20franksun123/rsshub=20=E9=95=9C=E5=83=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docker-compose.yml | 17 +----------------
1 file changed, 1 insertion(+), 16 deletions(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index 7d986405c3e3..aea16817003e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,6 @@
services:
rsshub:
- build: .
+ image: franksun123/rsshub
restart: always
ports:
- '1200:1200'
@@ -29,20 +29,5 @@ services:
retries: 5
start_period: 5s
-volumes:
- redis-data:
-
- redis:
- image: redis:alpine
- restart: always
- volumes:
- - redis-data:/data
- healthcheck:
- test: ['CMD', 'redis-cli', 'ping']
- interval: 30s
- timeout: 10s
- retries: 5
- start_period: 5s
-
volumes:
redis-data:
From bf5880db76135ccec074591fd8986ceae129d6a3 Mon Sep 17 00:00:00 2001
From: Franklittleboy <54013763+Franklittleboy@users.noreply.github.com>
Date: Tue, 12 May 2026 20:20:10 +0800
Subject: [PATCH 06/15] =?UTF-8?q?chore:=20=E7=AE=80=E5=8C=96=20docker-comp?=
=?UTF-8?q?ose=EF=BC=8C=E5=8E=BB=E6=8E=89=20browserless=20=E5=92=8C=20redi?=
=?UTF-8?q?s?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docker-compose.yml | 21 +--------------------
1 file changed, 1 insertion(+), 20 deletions(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index aea16817003e..ee758e866ea0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,28 +6,9 @@ services:
- '1200:1200'
environment:
NODE_ENV: production
- CACHE_TYPE: redis
- REDIS_URL: 'redis://redis:6379/'
- FLARESOLVERR_URL: http://flaresolverr:8191/v1
+ FLARESOLVERR_URL: http://192.168.1.190:8191/v1
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:1200/healthz']
interval: 30s
timeout: 10s
retries: 3
- depends_on:
- - redis
-
- redis:
- image: redis:alpine
- restart: always
- volumes:
- - redis-data:/data
- healthcheck:
- test: ['CMD', 'redis-cli', 'ping']
- interval: 30s
- timeout: 10s
- retries: 5
- start_period: 5s
-
-volumes:
- redis-data:
From f84d063ace6df72bc78f5338501f2662e074a22c Mon Sep 17 00:00:00 2001
From: Franklittleboy <54013763+Franklittleboy@users.noreply.github.com>
Date: Tue, 12 May 2026 22:31:12 +0800
Subject: [PATCH 07/15] =?UTF-8?q?fix(gxnas):=20=E4=BD=BF=E7=94=A8=20FlareS?=
=?UTF-8?q?olverr=20session=20=E5=A4=8D=E7=94=A8=E6=B5=8F=E8=A7=88?=
=?UTF-8?q?=E5=99=A8=E5=AE=9E=E4=BE=8B=E8=A7=A3=20Cloudflare=20challenge?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
lib/routes/gxnas/index.ts | 36 ++++++++++++++++++++++++++++--------
1 file changed, 28 insertions(+), 8 deletions(-)
diff --git a/lib/routes/gxnas/index.ts b/lib/routes/gxnas/index.ts
index f923726ca46b..e610da9336e0 100644
--- a/lib/routes/gxnas/index.ts
+++ b/lib/routes/gxnas/index.ts
@@ -1,11 +1,34 @@
import { load } from 'cheerio';
-
import type { DataItem, Route } from '@/types';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
import logger from '@/utils/logger';
const FLARESOLVERR_URL = process.env.FLARESOLVERR_URL || 'http://flaresolverr:8191/v1';
+const SESSION_ID = 'gxnas_session';
+
+// Session singleton: create once, reuse across requests
+let sessionCreated = false;
+
+async function ensureSession() {
+ if (sessionCreated) {
+ return;
+ }
+ try {
+ logger.debug(`Creating FlareSolverr session: ${SESSION_ID}`);
+ await ofetch(FLARESOLVERR_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: { cmd: 'sessions.create', session: SESSION_ID },
+ });
+ sessionCreated = true;
+ logger.debug(`FlareSolverr session created: ${SESSION_ID}`);
+ } catch (e) {
+ // Session might already exist from a previous run
+ logger.debug(`Session creation failed (may already exist): ${(e as Error).message}`);
+ sessionCreated = true;
+ }
+}
export const route: Route = {
path: '/',
@@ -34,15 +57,17 @@ export const route: Route = {
async function handler() {
const rootUrl = 'https://wp.gxnas.com';
- logger.debug(`Fetching ${rootUrl} via FlareSolverr at ${FLARESOLVERR_URL}`);
+ await ensureSession();
+ logger.debug(`Fetching ${rootUrl} via FlareSolverr session ${SESSION_ID}`);
const result = await ofetch(FLARESOLVERR_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: {
cmd: 'request.get',
url: rootUrl,
- maxTimeout: 60000,
+ session: SESSION_ID,
+ maxTimeout: 120000,
},
});
@@ -57,24 +82,19 @@ async function handler() {
.toArray()
.map((el) => {
const $el = $(el);
-
const titleEl = $el.find('.header .title a');
const title = titleEl.text().trim();
const link = titleEl.attr('href');
-
const categoryEl = $el.find('.header .label');
const category = categoryEl.text().trim();
-
const contentEl = $el.find('.content');
const description = contentEl.html() || '';
-
const thumbEl = $el.find('.a-thumb img');
const image = thumbEl.attr('src');
// Date format: 2023年11月17日
const dateText = $el.find('.a-meta span').first().text().trim();
let pubDate: Date | undefined;
-
const m = dateText.match(/(\d{4})年(\d{1,2})月(\d{1,2})日/);
if (m) {
pubDate = new Date(Number.parseInt(m[1]), Number.parseInt(m[2]) - 1, Number.parseInt(m[3]));
From 3cc32625e831ec8b6e0aad36a8368334031b92cc Mon Sep 17 00:00:00 2001
From: Franklittleboy <54013763+Franklittleboy@users.noreply.github.com>
Date: Tue, 12 May 2026 22:32:41 +0800
Subject: [PATCH 08/15] =?UTF-8?q?chore:=20docker-compose=20=E6=94=B9?=
=?UTF-8?q?=E4=B8=BA=20host=20=E7=BD=91=E7=BB=9C=E6=A8=A1=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docker-compose.yml | 28 ++++++++++++++++++----------
1 file changed, 18 insertions(+), 10 deletions(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index ee758e866ea0..268a8b9e7e0e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,14 +1,22 @@
+version: '3.8'
services:
rsshub:
- image: franksun123/rsshub
+ image: franksun123/rsshub:latest
+ container_name: rsshubfrank
restart: always
- ports:
- - '1200:1200'
+ network_mode: host
environment:
- NODE_ENV: production
- FLARESOLVERR_URL: http://192.168.1.190:8191/v1
- healthcheck:
- test: ['CMD', 'curl', '-f', 'http://localhost:1200/healthz']
- interval: 30s
- timeout: 10s
- retries: 3
+ - NODE_ENV=production
+ - CACHE_TYPE=redis
+ - REDIS_URL=redis://127.0.0.1:6379/0
+ - FLARESOLVERR_URL=http://127.0.0.1:8191/v1
+ depends_on:
+ - redis
+
+ redis:
+ image: redis:alpine
+ container_name: rsshub-redis
+ restart: always
+ network_mode: host
+ volumes:
+ - ./redis-data:/data
From 32913b482a5ce5c22018ca96102f2f541ac0d2c1 Mon Sep 17 00:00:00 2001
From: Franklittleboy <54013763+Franklittleboy@users.noreply.github.com>
Date: Tue, 12 May 2026 22:36:00 +0800
Subject: [PATCH 09/15] =?UTF-8?q?fix(gxnas):=20FlareSolverr=20=E8=B6=85?=
=?UTF-8?q?=E6=97=B6=E6=94=B9=E4=B8=BA180=E7=A7=92?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
lib/routes/gxnas/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/routes/gxnas/index.ts b/lib/routes/gxnas/index.ts
index e610da9336e0..240d2bd84617 100644
--- a/lib/routes/gxnas/index.ts
+++ b/lib/routes/gxnas/index.ts
@@ -67,7 +67,7 @@ async function handler() {
cmd: 'request.get',
url: rootUrl,
session: SESSION_ID,
- maxTimeout: 120000,
+ maxTimeout: 180000,
},
});
From fa64d453c69cecec8c8f042c39ca2cb16be87e50 Mon Sep 17 00:00:00 2001
From: Franklittleboy <54013763+Franklittleboy@users.noreply.github.com>
Date: Wed, 13 May 2026 11:08:32 +0800
Subject: [PATCH 10/15] fix(route/gxnas): replace FlareSolverr with built-in
Playwright for Cloudflare bypass
- Remove external FlareSolverr dependency (requires separate Docker service)
- Use RSSHub's built-in getPlaywrightPage() utility instead
- Set requirePuppeteer: true in route features
- Intercept requests to only load document resources (faster page load)
- This aligns with other anti-crawler routes like javdb, sotwe, xueqiu
Note: CI will still fail due to Cloudflare blocking GitHub Actions IPs,
same as javdb (#21269) and sotwe routes. Self-hosted instances with
Chromium work fine.
---
lib/routes/gxnas/index.ts | 60 ++++++++++-----------------------------
1 file changed, 15 insertions(+), 45 deletions(-)
diff --git a/lib/routes/gxnas/index.ts b/lib/routes/gxnas/index.ts
index 240d2bd84617..6a2b35c299ee 100644
--- a/lib/routes/gxnas/index.ts
+++ b/lib/routes/gxnas/index.ts
@@ -1,34 +1,7 @@
import { load } from 'cheerio';
import type { DataItem, Route } from '@/types';
-import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
-import logger from '@/utils/logger';
-
-const FLARESOLVERR_URL = process.env.FLARESOLVERR_URL || 'http://flaresolverr:8191/v1';
-const SESSION_ID = 'gxnas_session';
-
-// Session singleton: create once, reuse across requests
-let sessionCreated = false;
-
-async function ensureSession() {
- if (sessionCreated) {
- return;
- }
- try {
- logger.debug(`Creating FlareSolverr session: ${SESSION_ID}`);
- await ofetch(FLARESOLVERR_URL, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: { cmd: 'sessions.create', session: SESSION_ID },
- });
- sessionCreated = true;
- logger.debug(`FlareSolverr session created: ${SESSION_ID}`);
- } catch (e) {
- // Session might already exist from a previous run
- logger.debug(`Session creation failed (may already exist): ${(e as Error).message}`);
- sessionCreated = true;
- }
-}
+import { getPlaywrightPage } from '@/utils/playwright';
export const route: Route = {
path: '/',
@@ -43,10 +16,12 @@ export const route: Route = {
name: '最新文章',
maintainers: ['Franklittleboy'],
handler,
- description: 'GXNAS博客最新文章',
+ description: `::: warning
+该网站受 Cloudflare 保护,自建实例需要 Chromium 支持(默认已包含)。
+:::`,
features: {
requireConfig: false,
- requirePuppeteer: false,
+ requirePuppeteer: true,
antiCrawler: true,
supportBT: false,
supportPodcast: false,
@@ -57,24 +32,19 @@ export const route: Route = {
async function handler() {
const rootUrl = 'https://wp.gxnas.com';
- await ensureSession();
+ const { page, destroy } = await getPlaywrightPage('about:blank');
- logger.debug(`Fetching ${rootUrl} via FlareSolverr session ${SESSION_ID}`);
- const result = await ofetch(FLARESOLVERR_URL, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: {
- cmd: 'request.get',
- url: rootUrl,
- session: SESSION_ID,
- maxTimeout: 180000,
- },
+ // Only load document resources to speed up page load
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ request.resourceType() === 'document' ? request.continue() : request.abort();
});
- const html = result?.solution?.response;
- if (!html) {
- throw new Error('FlareSolverr 返回内容为空');
- }
+ await page.goto(rootUrl, { waitUntil: 'domcontentloaded' });
+
+ const html = await page.content();
+ await page.close();
+ await destroy();
const $ = load(html);
From 850be736a7dc03bfe07d6bb0254979ba840be69f Mon Sep 17 00:00:00 2001
From: Franklittleboy <54013763+Franklittleboy@users.noreply.github.com>
Date: Wed, 13 May 2026 11:27:50 +0800
Subject: [PATCH 11/15] fix(route/gxnas): sort imports for oxlint
simple-import-sort rule
---
lib/routes/gxnas/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/routes/gxnas/index.ts b/lib/routes/gxnas/index.ts
index 6a2b35c299ee..190a03372db8 100644
--- a/lib/routes/gxnas/index.ts
+++ b/lib/routes/gxnas/index.ts
@@ -1,7 +1,7 @@
import { load } from 'cheerio';
import type { DataItem, Route } from '@/types';
-import { parseDate } from '@/utils/parse-date';
import { getPlaywrightPage } from '@/utils/playwright';
+import { parseDate } from '@/utils/parse-date';
export const route: Route = {
path: '/',
From 700b9dcd15fd2cb040ba2743c729abbaed3b946a Mon Sep 17 00:00:00 2001
From: Franklittleboy <54013763+Franklittleboy@users.noreply.github.com>
Date: Wed, 13 May 2026 12:14:22 +0800
Subject: [PATCH 12/15] fix(route/gxnas): fix simple-import-sort order - type
imports after value imports
---
lib/routes/gxnas/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/routes/gxnas/index.ts b/lib/routes/gxnas/index.ts
index 190a03372db8..92d5c581dbba 100644
--- a/lib/routes/gxnas/index.ts
+++ b/lib/routes/gxnas/index.ts
@@ -1,7 +1,7 @@
import { load } from 'cheerio';
-import type { DataItem, Route } from '@/types';
import { getPlaywrightPage } from '@/utils/playwright';
import { parseDate } from '@/utils/parse-date';
+import type { DataItem, Route } from '@/types';
export const route: Route = {
path: '/',
From d17b4116e2c3137bfd69ca4dbb7743ea4a0f8f56 Mon Sep 17 00:00:00 2001
From: Franklittleboy <54013763+Franklittleboy@users.noreply.github.com>
Date: Wed, 13 May 2026 12:25:33 +0800
Subject: [PATCH 13/15] fix(route/gxnas): correct simple-import-sort order per
eslint autofix
---
lib/routes/gxnas/index.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/routes/gxnas/index.ts b/lib/routes/gxnas/index.ts
index 92d5c581dbba..6a2b35c299ee 100644
--- a/lib/routes/gxnas/index.ts
+++ b/lib/routes/gxnas/index.ts
@@ -1,7 +1,7 @@
import { load } from 'cheerio';
-import { getPlaywrightPage } from '@/utils/playwright';
-import { parseDate } from '@/utils/parse-date';
import type { DataItem, Route } from '@/types';
+import { parseDate } from '@/utils/parse-date';
+import { getPlaywrightPage } from '@/utils/playwright';
export const route: Route = {
path: '/',
From e6ec27cf6f131ea2161f36fa52b3951c8ffbfedf Mon Sep 17 00:00:00 2001
From: Franklittleboy <54013763+Franklittleboy@users.noreply.github.com>
Date: Wed, 13 May 2026 13:58:06 +0800
Subject: [PATCH 14/15] fix(route/gxnas): add blank line between external and
internal imports for simple-import-sort
---
lib/routes/gxnas/index.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/lib/routes/gxnas/index.ts b/lib/routes/gxnas/index.ts
index 6a2b35c299ee..bb661b2ac5d0 100644
--- a/lib/routes/gxnas/index.ts
+++ b/lib/routes/gxnas/index.ts
@@ -1,4 +1,5 @@
import { load } from 'cheerio';
+
import type { DataItem, Route } from '@/types';
import { parseDate } from '@/utils/parse-date';
import { getPlaywrightPage } from '@/utils/playwright';
From 3263c9a76053374f5d727cc6a2fd782f04afbb96 Mon Sep 17 00:00:00 2001
From: Franklittleboy <54013763+Franklittleboy@users.noreply.github.com>
Date: Wed, 13 May 2026 15:33:59 +0800
Subject: [PATCH 15/15] revert(route/gxnas): restore FlareSolverr approach -
Playwright fails Cloudflare JS challenge
Playwright with request interception blocks Cloudflare challenge scripts,
preventing the browser from passing the JS verification. FlareSolverr
works because it lets the full challenge flow complete before returning
the final HTML.
Also fixed: simple-import-sort compliance (blank line between external/
internal groups, alphabetical order within groups).
---
lib/routes/gxnas/index.ts | 61 ++++++++++++++++++++++++++++++---------
1 file changed, 47 insertions(+), 14 deletions(-)
diff --git a/lib/routes/gxnas/index.ts b/lib/routes/gxnas/index.ts
index bb661b2ac5d0..e610353da5bc 100644
--- a/lib/routes/gxnas/index.ts
+++ b/lib/routes/gxnas/index.ts
@@ -1,8 +1,35 @@
import { load } from 'cheerio';
import type { DataItem, Route } from '@/types';
+import logger from '@/utils/logger';
+import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
-import { getPlaywrightPage } from '@/utils/playwright';
+
+const FLARESOLVERR_URL = process.env.FLARESOLVERR_URL || 'http://flaresolverr:8191/v1';
+const SESSION_ID = 'gxnas_session';
+
+// Session singleton: create once, reuse across requests
+let sessionCreated = false;
+
+async function ensureSession() {
+ if (sessionCreated) {
+ return;
+ }
+ try {
+ logger.debug(`Creating FlareSolverr session: ${SESSION_ID}`);
+ await ofetch(FLARESOLVERR_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: { cmd: 'sessions.create', session: SESSION_ID },
+ });
+ sessionCreated = true;
+ logger.debug(`FlareSolverr session created: ${SESSION_ID}`);
+ } catch (e) {
+ // Session might already exist from a previous run
+ logger.debug(`Session creation failed (may already exist): ${(e as Error).message}`);
+ sessionCreated = true;
+ }
+}
export const route: Route = {
path: '/',
@@ -17,12 +44,13 @@ export const route: Route = {
name: '最新文章',
maintainers: ['Franklittleboy'],
handler,
- description: `::: warning
-该网站受 Cloudflare 保护,自建实例需要 Chromium 支持(默认已包含)。
+ description: `:::warning
+该网站受 Cloudflare 保护,需要配置 FlareSolverr 服务([文档](https://github.com/FlareSolverr/FlareSolverr))。
+环境变量 \`FLARESOLVERR_URL\` 默认为 \`http://flaresolverr:8191/v1\`。
:::`,
features: {
requireConfig: false,
- requirePuppeteer: true,
+ requirePuppeteer: false,
antiCrawler: true,
supportBT: false,
supportPodcast: false,
@@ -33,19 +61,24 @@ export const route: Route = {
async function handler() {
const rootUrl = 'https://wp.gxnas.com';
- const { page, destroy } = await getPlaywrightPage('about:blank');
+ await ensureSession();
- // Only load document resources to speed up page load
- await page.setRequestInterception(true);
- page.on('request', (request) => {
- request.resourceType() === 'document' ? request.continue() : request.abort();
+ logger.debug(`Fetching ${rootUrl} via FlareSolverr session ${SESSION_ID}`);
+ const result = await ofetch(FLARESOLVERR_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: {
+ cmd: 'request.get',
+ url: rootUrl,
+ session: SESSION_ID,
+ maxTimeout: 180000,
+ },
});
- await page.goto(rootUrl, { waitUntil: 'domcontentloaded' });
-
- const html = await page.content();
- await page.close();
- await destroy();
+ const html = result?.solution?.response;
+ if (!html) {
+ throw new Error('FlareSolverr 返回内容为空');
+ }
const $ = load(html);