From 0f03ad9801412f486d5728bf0eecf42835f1af94 Mon Sep 17 00:00:00 2001 From: anian Date: Tue, 21 Jul 2026 00:54:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=A1=B5=E7=A0=81=E9=80=89?= =?UTF-8?q?=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 ++ public/index.html | 7 ++- src/modules/page.spec.ts | 46 ++++++++++++++++ src/modules/page.ts | 89 +++++++++++++++++++++++++++++++ src/modules/routes.ts | 12 ++++- src/modules/weibo/api/indexAPI.ts | 6 +-- src/modules/weibo/weibo.ts | 27 +++++++--- 7 files changed, 178 insertions(+), 12 deletions(-) create mode 100644 src/modules/page.spec.ts create mode 100644 src/modules/page.ts diff --git a/README.md b/README.md index 7cff49a..aecbbd9 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ > 基于原作者v0.3.0代码修改 +#### v0.3.0-2 +1. 新增页码URL参数`?page=1-3`、`?page=1,3,5`(需cookie;支持混用,如`?page=1-3,10,8`) + #### v0.3.0-1 1. 新增视频/Live图链接以适配日记簿朋友圈 2. 图片链接不再转URL编码及百度代理 diff --git a/public/index.html b/public/index.html index cbb522c..3fc3048 100644 --- a/public/index.html +++ b/public/index.html @@ -52,6 +52,10 @@ +
+ 页码 + +
@@ -123,7 +127,8 @@ function showResult(uid) { } var baseUrl = location.href.split("?")[0]; if (baseUrl.substr(-1, 1) !== "/") baseUrl += "/"; - var feedUrl = baseUrl + "rss/user/" + uid; + var page = $.trim($("#page-input").val()) || "1"; + var feedUrl = baseUrl + "rss/user/" + uid + "?page=" + encodeURIComponent(page); $("#foo").val(feedUrl); for (var reader in prefix) { diff --git a/src/modules/page.spec.ts b/src/modules/page.spec.ts new file mode 100644 index 0000000..1b61592 --- /dev/null +++ b/src/modules/page.spec.ts @@ -0,0 +1,46 @@ +import { describe, expect, test } from "@jest/globals"; +import { parsePageSpec, PageInvalidError } from "./page"; + +describe('page spec parser', () => { + test('defaults to first page', () => { + expect(parsePageSpec(undefined)).toEqual({ + pages: [1], + key: '1', + }); + }); + + test('parses a single page', () => { + expect(parsePageSpec('7')).toEqual({ + pages: [7], + key: '7', + }); + }); + + test('parses a page range', () => { + expect(parsePageSpec('1-3')).toEqual({ + pages: [1, 2, 3], + key: '1,2,3', + }); + }); + + test('parses a page list', () => { + expect(parsePageSpec('1,3,5')).toEqual({ + pages: [1, 3, 5], + key: '1,3,5', + }); + }); + + test('parses a mixed page spec', () => { + expect(parsePageSpec('1-3, 5')).toEqual({ + pages: [1, 2, 3, 5], + key: '1,2,3,5', + }); + }); + + test('rejects invalid page input', () => { + expect(() => parsePageSpec('0')).toThrow(PageInvalidError); + expect(() => parsePageSpec('3-1')).toThrow(PageInvalidError); + expect(() => parsePageSpec('abc')).toThrow(PageInvalidError); + expect(() => parsePageSpec('1,,3')).toThrow(PageInvalidError); + }); +}); diff --git a/src/modules/page.ts b/src/modules/page.ts new file mode 100644 index 0000000..7faa35d --- /dev/null +++ b/src/modules/page.ts @@ -0,0 +1,89 @@ +export class PageInvalidError extends Error { + constructor(page: string) { + super(`page: ${page}`); + } +} + +export interface PageSpec { + pages: number[]; + key: string; +} + +const SINGLE_PAGE_PATTERN = /^[1-9]\d*$/; +const PAGE_RANGE_PATTERN = /^([1-9]\d*)\s*-\s*([1-9]\d*)$/; + +const normalizePages = (pages: number[]) => { + const seen = new Set(); + const normalized: number[] = []; + + for (const page of pages) { + if (seen.has(page)) { + continue; + } + seen.add(page); + normalized.push(page); + } + + return normalized; +}; + +const parsePageToken = (token: string) => { + if (SINGLE_PAGE_PATTERN.test(token)) { + return [Number(token)]; + } + + const rangeMatch = token.match(PAGE_RANGE_PATTERN); + if (rangeMatch) { + const start = Number(rangeMatch[1]); + const end = Number(rangeMatch[2]); + if (end < start) { + throw new PageInvalidError(token); + } + + return Array.from({ length: end - start + 1 }, (_, index) => start + index); + } + + throw new PageInvalidError(token); +}; + +export const parsePageSpec = (page: unknown): PageSpec => { + if (page === undefined || page === null) { + return { + pages: [1], + key: '1', + }; + } + + if (Array.isArray(page)) { + throw new PageInvalidError(page.join(',')); + } + + const rawPage = String(page).trim(); + if (!rawPage) { + return { + pages: [1], + key: '1', + }; + } + + const tokens = rawPage.split(','); + if (tokens.some(token => !token.trim())) { + throw new PageInvalidError(rawPage); + } + + const pages: number[] = []; + for (const token of tokens) { + const tokenPages = parsePageToken(token.trim()); + pages.push(...tokenPages); + } + + const normalizedPages = normalizePages(pages); + if (!normalizedPages.length) { + throw new PageInvalidError(rawPage); + } + + return { + pages: normalizedPages, + key: normalizedPages.join(','), + }; +}; diff --git a/src/modules/routes.ts b/src/modules/routes.ts index 7af095e..f76321b 100644 --- a/src/modules/routes.ts +++ b/src/modules/routes.ts @@ -8,6 +8,7 @@ import config from '../config'; import { DomainNotFoundError, statusToHTML, UserNotFoundError, WeiboCookieInvalidError } from './weibo/weibo'; import { ThrottledError } from './throttler'; import { logger } from './logger'; +import { PageInvalidError, parsePageSpec } from './page'; export class UidInvalidError extends Error { constructor(uid: string) { @@ -32,10 +33,12 @@ export const registerRoutes = ( throw new UidInvalidError(uid); } + const pageSpec = parsePageSpec(ctx.query.page); + // get data let cacheMiss = false; const xmlData = await ctx.cache.memo(async () => { - const weiboData = await ctx.weibo.fetchUserLatestWeibo(uid); + const weiboData = await ctx.weibo.fetchUserLatestWeibo(uid, pageSpec.pages); if (weiboData) { // basic info const feed = new NodeRSS({ @@ -59,7 +62,7 @@ export const registerRoutes = ( cacheMiss = true; return feed.xml(); } - }, `xml-${uid}`, config.cacheTTL.rssXml); + }, `xml-${uid}-page-${pageSpec.key}`, config.cacheTTL.rssXml); // send data ctx.set('Content-Type', 'text/xml'); @@ -78,6 +81,11 @@ export const registerRoutes = ( ctx.body = `找不到用户,可能用户仅登录可见,不支持订阅。可以通过打开 https://m.weibo.cn/u/:uid 验证(uid: ${uid})`; return; } + if (error instanceof PageInvalidError) { + ctx.status = 400; + ctx.body = `page 参数格式有误。支持单页如 7,范围如 1-10,或列表如 1,3,5。page: ${error.message.replace(/^page:\s*/, '')}`; + return; + } if (error instanceof WeiboCookieInvalidError) { ctx.status = 401; ctx.body = `微博 Cookie 可能已过期或失效,请更新 WEIBO_COOKIE 后重试。uid: ${uid}`; diff --git a/src/modules/weibo/api/indexAPI.ts b/src/modules/weibo/api/indexAPI.ts index b857531..d191b4a 100644 --- a/src/modules/weibo/api/indexAPI.ts +++ b/src/modules/weibo/api/indexAPI.ts @@ -64,12 +64,12 @@ export const createIndexAPI = () => { return result; }).catch(err => handleForbiddenErr(err, disable)); }), - getWeiboContentList: (uid: string, containerId: string) => runner.runFunc(async (disable) => { - logger.debug(`[getContList] ${uid} ${containerId}`); + getWeiboContentList: (uid: string, containerId: string, page: number = 1) => runner.runFunc(async (disable) => { + logger.debug(`[getContList] ${uid} ${containerId} page=${page}`); await waitMs(Math.floor(Math.random() * 100)); return await axiosInstance({ method: 'get', - url: `https://m.weibo.cn/api/container/getIndex?type=uid&value=${uid}&containerid=${containerId}`, + url: `https://m.weibo.cn/api/container/getIndex?type=uid&value=${uid}&containerid=${containerId}&page=${page}`, headers: { 'Mweibo-Pwa': 1, 'Referer': `https://m.weibo.cn/u/${uid}`, diff --git a/src/modules/weibo/weibo.ts b/src/modules/weibo/weibo.ts index 315a179..a656d9f 100644 --- a/src/modules/weibo/weibo.ts +++ b/src/modules/weibo/weibo.ts @@ -42,15 +42,30 @@ export class WeiboData { /** * get user's weibo */ - fetchUserLatestWeibo = async (uid: string) => { + fetchUserLatestWeibo = async (uid: string, pages: number[] = [1]) => { const indexInfo = await this.cache.memo(() => this.getIndexUserInfo(uid), `info-${uid}`, config.cacheTTL.apiIndexInfo); const { containerId } = indexInfo; - const statusList = await this.cache.memo(async () => { - const wbList = await this.getWeiboContentList(uid, containerId); - return await Promise.all( - wbList.map(status => this.fillStatusWithLongText(status)) + const statusList: WeiboStatus[] = []; + const seenStatusIds = new Set(); + + for (const page of pages) { + const wbList = await this.cache.memo( + () => this.getWeiboContentList(uid, containerId, page), + `list-${uid}-page-${page}`, + config.cacheTTL.apiStatusList, ); - }, `list-${uid}`, config.cacheTTL.apiStatusList); + const freshList = wbList.filter((status) => { + if (seenStatusIds.has(status.id)) { + return false; + } + seenStatusIds.add(status.id); + return true; + }); + const pageStatusList = await Promise.all( + freshList.map(status => this.fillStatusWithLongText(status)) + ); + statusList.push(...pageStatusList); + } return { ...indexInfo,