1 Commits

Author SHA1 Message Date
anian 0f03ad9801 新增页码选择 2026-07-21 00:54:20 +08:00
7 changed files with 178 additions and 12 deletions
+3
View File
@@ -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编码及百度代理
+6 -1
View File
@@ -52,6 +52,10 @@
<button class="btn btn-default">生成</button>
</span>
</div>
<div class="input-group" style="margin-top: 10px;">
<span class="input-group-addon">页码</span>
<input id="page-input" type="text" class="form-control" value="1" placeholder="1 / 7 / 1-10 / 1,3,5">
</div>
</form>
</div>
<div class="loading" style="display: none;"><p class="text-center">加载中...</p></div>
@@ -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) {
+46
View File
@@ -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);
});
});
+89
View File
@@ -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<number>();
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(','),
};
};
+10 -2
View File
@@ -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 验证(<a href="https://m.weibo.cn/u/${uid}" target="_blank">uid: ${uid}</a>`;
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}`;
+3 -3
View File
@@ -64,12 +64,12 @@ export const createIndexAPI = () => {
return result;
}).catch(err => handleForbiddenErr(err, disable));
}),
getWeiboContentList: (uid: string, containerId: string) => runner.runFunc<WeiboStatus[]>(async (disable) => {
logger.debug(`[getContList] ${uid} ${containerId}`);
getWeiboContentList: (uid: string, containerId: string, page: number = 1) => runner.runFunc<WeiboStatus[]>(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}`,
+21 -6
View File
@@ -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<string>();
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,