@jackwener/opencli 1.5.5 → 1.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -2
- package/README.zh-CN.md +36 -4
- package/dist/browser/daemon-client.d.ts +5 -1
- package/dist/browser/page.d.ts +6 -0
- package/dist/browser/page.js +15 -0
- package/dist/cli-manifest.json +1229 -67
- package/dist/clis/band/bands.d.ts +1 -0
- package/dist/clis/band/bands.js +72 -0
- package/dist/clis/band/mentions.d.ts +1 -0
- package/dist/clis/band/mentions.js +127 -0
- package/dist/clis/band/post.d.ts +1 -0
- package/dist/clis/band/post.js +175 -0
- package/dist/clis/band/posts.d.ts +1 -0
- package/dist/clis/band/posts.js +94 -0
- package/dist/clis/doubao/detail.d.ts +1 -0
- package/dist/clis/doubao/detail.js +33 -0
- package/dist/clis/doubao/detail.test.d.ts +1 -0
- package/dist/clis/doubao/detail.test.js +42 -0
- package/dist/clis/doubao/history.d.ts +1 -0
- package/dist/clis/doubao/history.js +28 -0
- package/dist/clis/doubao/history.test.d.ts +1 -0
- package/dist/clis/doubao/history.test.js +37 -0
- package/dist/clis/doubao/meeting-summary.d.ts +1 -0
- package/dist/clis/doubao/meeting-summary.js +39 -0
- package/dist/clis/doubao/meeting-transcript.d.ts +1 -0
- package/dist/clis/doubao/meeting-transcript.js +36 -0
- package/dist/clis/doubao/utils.d.ts +27 -0
- package/dist/clis/doubao/utils.js +317 -0
- package/dist/clis/doubao/utils.test.d.ts +1 -0
- package/dist/clis/doubao/utils.test.js +24 -0
- package/dist/clis/douyin/_shared/public-api.d.ts +33 -0
- package/dist/clis/douyin/_shared/public-api.js +29 -0
- package/dist/clis/douyin/user-videos.d.ts +5 -0
- package/dist/clis/douyin/user-videos.js +74 -0
- package/dist/clis/douyin/user-videos.test.d.ts +1 -0
- package/dist/clis/douyin/user-videos.test.js +108 -0
- package/dist/clis/ones/common.d.ts +32 -0
- package/dist/clis/ones/common.js +144 -0
- package/dist/clis/ones/enrich-tasks.d.ts +5 -0
- package/dist/clis/ones/enrich-tasks.js +37 -0
- package/dist/clis/ones/login.d.ts +1 -0
- package/dist/clis/ones/login.js +80 -0
- package/dist/clis/ones/logout.d.ts +1 -0
- package/dist/clis/ones/logout.js +17 -0
- package/dist/clis/ones/me.d.ts +1 -0
- package/dist/clis/ones/me.js +30 -0
- package/dist/clis/ones/my-tasks.d.ts +1 -0
- package/dist/clis/ones/my-tasks.js +120 -0
- package/dist/clis/ones/resolve-labels.d.ts +10 -0
- package/dist/clis/ones/resolve-labels.js +64 -0
- package/dist/clis/ones/task-helpers.d.ts +29 -0
- package/dist/clis/ones/task-helpers.js +212 -0
- package/dist/clis/ones/task-helpers.test.d.ts +1 -0
- package/dist/clis/ones/task-helpers.test.js +12 -0
- package/dist/clis/ones/task.d.ts +1 -0
- package/dist/clis/ones/task.js +66 -0
- package/dist/clis/ones/tasks.d.ts +1 -0
- package/dist/clis/ones/tasks.js +79 -0
- package/dist/clis/ones/token-info.d.ts +1 -0
- package/dist/clis/ones/token-info.js +42 -0
- package/dist/clis/ones/worklog.d.ts +11 -0
- package/dist/clis/ones/worklog.js +267 -0
- package/dist/clis/ones/worklog.test.d.ts +1 -0
- package/dist/clis/ones/worklog.test.js +20 -0
- package/dist/clis/spotify/spotify.d.ts +1 -0
- package/dist/clis/spotify/spotify.js +316 -0
- package/dist/clis/spotify/utils.d.ts +21 -0
- package/dist/clis/spotify/utils.js +66 -0
- package/dist/clis/spotify/utils.test.d.ts +1 -0
- package/dist/clis/spotify/utils.test.js +67 -0
- package/dist/clis/tieba/commands.test.d.ts +4 -0
- package/dist/clis/tieba/commands.test.js +79 -0
- package/dist/clis/tieba/hot.d.ts +1 -0
- package/dist/clis/tieba/hot.js +48 -0
- package/dist/clis/tieba/posts.d.ts +1 -0
- package/dist/clis/tieba/posts.js +85 -0
- package/dist/clis/tieba/read.d.ts +1 -0
- package/dist/clis/tieba/read.js +140 -0
- package/dist/clis/tieba/search.d.ts +1 -0
- package/dist/clis/tieba/search.js +108 -0
- package/dist/clis/tieba/utils.d.ts +101 -0
- package/dist/clis/tieba/utils.js +240 -0
- package/dist/clis/tieba/utils.test.d.ts +1 -0
- package/dist/clis/tieba/utils.test.js +290 -0
- package/dist/clis/weread/book.js +100 -13
- package/dist/clis/weread/commands.test.js +221 -0
- package/dist/clis/weread/private-api-regression.test.d.ts +1 -0
- package/dist/{weread-private-api-regression.test.js → clis/weread/private-api-regression.test.js} +92 -30
- package/dist/clis/weread/search-regression.test.d.ts +1 -0
- package/dist/clis/weread/search-regression.test.js +407 -0
- package/dist/clis/weread/search.js +143 -7
- package/dist/clis/weread/shelf.js +13 -95
- package/dist/clis/weread/utils.d.ts +46 -0
- package/dist/clis/weread/utils.js +214 -7
- package/dist/clis/weread/utils.test.js +71 -1
- package/dist/clis/xiaohongshu/publish.d.ts +1 -1
- package/dist/clis/xiaohongshu/publish.js +78 -31
- package/dist/clis/xiaohongshu/publish.test.js +66 -1
- package/dist/clis/xiaohongshu/user-helpers.d.ts +1 -0
- package/dist/clis/xiaohongshu/user-helpers.js +2 -0
- package/dist/clis/xiaohongshu/user-helpers.test.js +18 -0
- package/dist/clis/xueqiu/comments.d.ts +118 -0
- package/dist/clis/xueqiu/comments.js +354 -0
- package/dist/clis/xueqiu/comments.test.d.ts +1 -0
- package/dist/clis/xueqiu/comments.test.js +696 -0
- package/dist/clis/youtube/transcript.js +2 -4
- package/dist/clis/youtube/utils.d.ts +9 -0
- package/dist/clis/youtube/utils.js +67 -3
- package/dist/clis/youtube/utils.test.d.ts +1 -0
- package/dist/clis/youtube/utils.test.js +37 -0
- package/dist/clis/youtube/video.js +16 -15
- package/dist/clis/zsxq/dynamics.d.ts +1 -0
- package/dist/clis/zsxq/dynamics.js +47 -0
- package/dist/clis/zsxq/groups.d.ts +1 -0
- package/dist/clis/zsxq/groups.js +32 -0
- package/dist/clis/zsxq/search.d.ts +1 -0
- package/dist/clis/zsxq/search.js +43 -0
- package/dist/clis/zsxq/search.test.d.ts +1 -0
- package/dist/clis/zsxq/search.test.js +24 -0
- package/dist/clis/zsxq/topic.d.ts +1 -0
- package/dist/clis/zsxq/topic.js +47 -0
- package/dist/clis/zsxq/topic.test.d.ts +1 -0
- package/dist/clis/zsxq/topic.test.js +29 -0
- package/dist/clis/zsxq/topics.d.ts +1 -0
- package/dist/clis/zsxq/topics.js +25 -0
- package/dist/clis/zsxq/topics.test.d.ts +1 -0
- package/dist/clis/zsxq/topics.test.js +24 -0
- package/dist/clis/zsxq/utils.d.ts +97 -0
- package/dist/clis/zsxq/utils.js +230 -0
- package/dist/commanderAdapter.js +1 -1
- package/dist/commanderAdapter.test.js +39 -0
- package/dist/external-clis.yaml +17 -0
- package/dist/types.d.ts +5 -0
- package/docs/.vitepress/config.mts +3 -0
- package/docs/adapters/browser/band.md +63 -0
- package/docs/adapters/browser/ones.md +59 -0
- package/docs/adapters/browser/spotify.md +62 -0
- package/docs/adapters/browser/tieba.md +45 -0
- package/docs/adapters/browser/xueqiu.md +5 -0
- package/docs/adapters/browser/zsxq.md +49 -0
- package/docs/adapters/index.md +5 -2
- package/docs/adapters-doc/ones.md +32 -0
- package/extension/src/background.ts +15 -0
- package/extension/src/cdp.ts +42 -0
- package/extension/src/protocol.ts +5 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +16 -0
- package/src/browser/daemon-client.ts +5 -1
- package/src/browser/page.ts +16 -0
- package/src/clis/band/bands.ts +76 -0
- package/src/clis/band/mentions.ts +134 -0
- package/src/clis/band/post.ts +187 -0
- package/src/clis/band/posts.ts +106 -0
- package/src/clis/doubao/detail.test.ts +53 -0
- package/src/clis/doubao/detail.ts +41 -0
- package/src/clis/doubao/history.test.ts +45 -0
- package/src/clis/doubao/history.ts +32 -0
- package/src/clis/doubao/meeting-summary.ts +53 -0
- package/src/clis/doubao/meeting-transcript.ts +48 -0
- package/src/clis/doubao/utils.test.ts +45 -0
- package/src/clis/doubao/utils.ts +371 -0
- package/src/clis/douyin/_shared/public-api.ts +84 -0
- package/src/clis/douyin/user-videos.test.ts +122 -0
- package/src/clis/douyin/user-videos.ts +101 -0
- package/src/clis/ones/common.ts +187 -0
- package/src/clis/ones/enrich-tasks.ts +47 -0
- package/src/clis/ones/login.ts +103 -0
- package/src/clis/ones/logout.ts +19 -0
- package/src/clis/ones/me.ts +34 -0
- package/src/clis/ones/my-tasks.ts +148 -0
- package/src/clis/ones/resolve-labels.ts +80 -0
- package/src/clis/ones/task-helpers.test.ts +14 -0
- package/src/clis/ones/task-helpers.ts +214 -0
- package/src/clis/ones/task.ts +79 -0
- package/src/clis/ones/tasks.ts +92 -0
- package/src/clis/ones/token-info.ts +46 -0
- package/src/clis/ones/worklog.test.ts +24 -0
- package/src/clis/ones/worklog.ts +306 -0
- package/src/clis/spotify/spotify.ts +328 -0
- package/src/clis/spotify/utils.test.ts +87 -0
- package/src/clis/spotify/utils.ts +92 -0
- package/src/clis/tieba/commands.test.ts +86 -0
- package/src/clis/tieba/hot.ts +52 -0
- package/src/clis/tieba/posts.ts +108 -0
- package/src/clis/tieba/read.ts +158 -0
- package/src/clis/tieba/search.ts +119 -0
- package/src/clis/tieba/utils.test.ts +322 -0
- package/src/clis/tieba/utils.ts +348 -0
- package/src/clis/weread/book.ts +116 -13
- package/src/clis/weread/commands.test.ts +249 -0
- package/src/{weread-private-api-regression.test.ts → clis/weread/private-api-regression.test.ts} +108 -30
- package/src/clis/weread/search-regression.test.ts +440 -0
- package/src/clis/weread/search.ts +189 -9
- package/src/clis/weread/shelf.ts +20 -122
- package/src/clis/weread/utils.test.ts +81 -1
- package/src/clis/weread/utils.ts +264 -7
- package/src/clis/xiaohongshu/publish.test.ts +79 -1
- package/src/clis/xiaohongshu/publish.ts +84 -30
- package/src/clis/xiaohongshu/user-helpers.test.ts +23 -0
- package/src/clis/xiaohongshu/user-helpers.ts +4 -0
- package/src/clis/xueqiu/comments.test.ts +823 -0
- package/src/clis/xueqiu/comments.ts +461 -0
- package/src/clis/youtube/transcript.ts +2 -4
- package/src/clis/youtube/utils.test.ts +43 -0
- package/src/clis/youtube/utils.ts +69 -0
- package/src/clis/youtube/video.ts +16 -15
- package/src/clis/zsxq/dynamics.ts +60 -0
- package/src/clis/zsxq/groups.ts +41 -0
- package/src/clis/zsxq/search.test.ts +29 -0
- package/src/clis/zsxq/search.ts +54 -0
- package/src/clis/zsxq/topic.test.ts +34 -0
- package/src/clis/zsxq/topic.ts +68 -0
- package/src/clis/zsxq/topics.test.ts +29 -0
- package/src/clis/zsxq/topics.ts +36 -0
- package/src/clis/zsxq/utils.ts +351 -0
- package/src/commanderAdapter.test.ts +47 -0
- package/src/commanderAdapter.ts +1 -1
- package/src/external-clis.yaml +17 -0
- package/src/types.ts +5 -0
- package/tests/e2e/band-auth.test.ts +20 -0
- package/tests/e2e/browser-auth-helpers.ts +18 -0
- package/tests/e2e/browser-auth.test.ts +35 -47
- package/tests/e2e/browser-public.test.ts +288 -0
- package/tests/e2e/management.test.ts +1 -1
- package/tests/e2e/plugin-management.test.ts +1 -1
- package/vitest.config.ts +1 -0
- package/SKILL.md +0 -879
- package/dist/weread-private-api-regression.test.d.ts +0 -1
- package/dist/weread-search-regression.test.d.ts +0 -1
- package/dist/weread-search-regression.test.js +0 -39
- package/src/weread-search-regression.test.ts +0 -44
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Tieba parsing helpers used by the browser adapters.
|
|
3
|
+
*/
|
|
4
|
+
export declare const MAX_TIEBA_LIMIT = 20;
|
|
5
|
+
export interface RawTiebaPostCard {
|
|
6
|
+
title?: string;
|
|
7
|
+
author?: string;
|
|
8
|
+
descInfo?: string;
|
|
9
|
+
actionTexts?: string[];
|
|
10
|
+
commentCount?: unknown;
|
|
11
|
+
threadId?: unknown;
|
|
12
|
+
url?: unknown;
|
|
13
|
+
}
|
|
14
|
+
export interface RawTiebaPagePcFeedEntry {
|
|
15
|
+
layout?: string;
|
|
16
|
+
feed?: {
|
|
17
|
+
schema?: unknown;
|
|
18
|
+
log_param?: Array<{
|
|
19
|
+
key?: unknown;
|
|
20
|
+
value?: unknown;
|
|
21
|
+
}>;
|
|
22
|
+
business_info_map?: Record<string, unknown>;
|
|
23
|
+
components?: Array<Record<string, unknown>>;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export interface TiebaPostItem {
|
|
27
|
+
rank: number;
|
|
28
|
+
title: string;
|
|
29
|
+
author: string;
|
|
30
|
+
replies: number;
|
|
31
|
+
last_reply: string;
|
|
32
|
+
id: string;
|
|
33
|
+
url: string;
|
|
34
|
+
}
|
|
35
|
+
export interface RawTiebaSearchItem {
|
|
36
|
+
title?: string;
|
|
37
|
+
forum?: string;
|
|
38
|
+
author?: string;
|
|
39
|
+
time?: string;
|
|
40
|
+
snippet?: string;
|
|
41
|
+
id?: string;
|
|
42
|
+
url?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface TiebaSearchItem {
|
|
45
|
+
rank: number;
|
|
46
|
+
title: string;
|
|
47
|
+
forum: string;
|
|
48
|
+
author: string;
|
|
49
|
+
time: string;
|
|
50
|
+
snippet: string;
|
|
51
|
+
id: string;
|
|
52
|
+
url: string;
|
|
53
|
+
}
|
|
54
|
+
export interface RawTiebaMainPost {
|
|
55
|
+
title?: string;
|
|
56
|
+
author?: string;
|
|
57
|
+
fallbackAuthor?: string;
|
|
58
|
+
contentText?: string;
|
|
59
|
+
structuredText?: string;
|
|
60
|
+
visibleTime?: string;
|
|
61
|
+
structuredTime?: unknown;
|
|
62
|
+
hasMedia?: boolean;
|
|
63
|
+
}
|
|
64
|
+
export interface RawTiebaReply {
|
|
65
|
+
floor?: unknown;
|
|
66
|
+
author?: string;
|
|
67
|
+
content?: string;
|
|
68
|
+
time?: string;
|
|
69
|
+
}
|
|
70
|
+
export interface RawTiebaReadPayload {
|
|
71
|
+
mainPost?: RawTiebaMainPost | null;
|
|
72
|
+
replies?: RawTiebaReply[];
|
|
73
|
+
}
|
|
74
|
+
export interface TiebaReadItem {
|
|
75
|
+
floor: number;
|
|
76
|
+
author: string;
|
|
77
|
+
content: string;
|
|
78
|
+
time: string;
|
|
79
|
+
}
|
|
80
|
+
export interface TiebaReadBuildOptions {
|
|
81
|
+
limit?: unknown;
|
|
82
|
+
includeMainPost?: boolean;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Keep the public CLI limit contract aligned with the real implementation.
|
|
86
|
+
*/
|
|
87
|
+
export declare function normalizeTiebaLimit(value: unknown, fallback?: number): number;
|
|
88
|
+
export declare function normalizeText(value: unknown): string;
|
|
89
|
+
/**
|
|
90
|
+
* Match Tieba PC's signed request contract so forum list fetching stays stable.
|
|
91
|
+
*/
|
|
92
|
+
export declare function signTiebaPcParams(params: Record<string, string>): string;
|
|
93
|
+
export declare function parseTiebaCount(text: string): number;
|
|
94
|
+
export declare function parseTiebaLastReply(text: string): string;
|
|
95
|
+
/**
|
|
96
|
+
* Convert Tieba's signed `page_pc` feed entries into the stable card shape used by the CLI.
|
|
97
|
+
*/
|
|
98
|
+
export declare function buildTiebaPostCardsFromPagePc(rawFeeds: RawTiebaPagePcFeedEntry[]): RawTiebaPostCard[];
|
|
99
|
+
export declare function buildTiebaPostItems(rawCards: RawTiebaPostCard[], requestedLimit: unknown): TiebaPostItem[];
|
|
100
|
+
export declare function buildTiebaSearchItems(rawItems: RawTiebaSearchItem[], requestedLimit: unknown): TiebaSearchItem[];
|
|
101
|
+
export declare function buildTiebaReadItems(payload: RawTiebaReadPayload, options?: TiebaReadBuildOptions): TiebaReadItem[];
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Shared Tieba parsing helpers used by the browser adapters.
|
|
4
|
+
*/
|
|
5
|
+
export const MAX_TIEBA_LIMIT = 20;
|
|
6
|
+
const TIEBA_PC_SIGN_SALT = '36770b1f34c9bbf2e7d1a99d2b82fa9e';
|
|
7
|
+
const TIEBA_TIME_ZONE = 'Asia/Shanghai';
|
|
8
|
+
/**
|
|
9
|
+
* Keep the public CLI limit contract aligned with the real implementation.
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeTiebaLimit(value, fallback = MAX_TIEBA_LIMIT) {
|
|
12
|
+
const parsed = Number(value ?? fallback);
|
|
13
|
+
if (!Number.isFinite(parsed) || parsed < 1)
|
|
14
|
+
return fallback;
|
|
15
|
+
return Math.min(Math.trunc(parsed), MAX_TIEBA_LIMIT);
|
|
16
|
+
}
|
|
17
|
+
export function normalizeText(value) {
|
|
18
|
+
return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Match Tieba PC's signed request contract so forum list fetching stays stable.
|
|
22
|
+
*/
|
|
23
|
+
export function signTiebaPcParams(params) {
|
|
24
|
+
const payload = Object.keys(params)
|
|
25
|
+
.sort((left, right) => left.localeCompare(right))
|
|
26
|
+
.map((key) => `${key}=${params[key]}`)
|
|
27
|
+
.join('') + TIEBA_PC_SIGN_SALT;
|
|
28
|
+
return createHash('md5').update(payload).digest('hex');
|
|
29
|
+
}
|
|
30
|
+
export function parseTiebaCount(text) {
|
|
31
|
+
const value = normalizeText(text).toUpperCase();
|
|
32
|
+
if (!value)
|
|
33
|
+
return 0;
|
|
34
|
+
const compact = value.replace(/[^\d.W万]/g, '');
|
|
35
|
+
if (compact.endsWith('万')) {
|
|
36
|
+
return Math.round(parseFloat(compact.slice(0, -1)) * 10000);
|
|
37
|
+
}
|
|
38
|
+
if (compact.endsWith('W')) {
|
|
39
|
+
return Math.round(parseFloat(compact.slice(0, -1)) * 10000);
|
|
40
|
+
}
|
|
41
|
+
return parseInt(compact.replace(/[^\d]/g, ''), 10) || 0;
|
|
42
|
+
}
|
|
43
|
+
export function parseTiebaLastReply(text) {
|
|
44
|
+
const normalized = normalizeText(text).replace(/^回复于/, '').trim();
|
|
45
|
+
const match = normalized.match(/(刚刚|\d+\s*(?:分钟|小时|天)前|\d{2}-\d{2}(?:\s+\d{2}:\d{2})?|\d{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2})?)/);
|
|
46
|
+
return match ? match[1].trim() : normalized;
|
|
47
|
+
}
|
|
48
|
+
function buildTiebaThreadUrl(id, rawUrl) {
|
|
49
|
+
const explicitUrl = normalizeText(rawUrl);
|
|
50
|
+
if (explicitUrl)
|
|
51
|
+
return explicitUrl;
|
|
52
|
+
return id ? `https://tieba.baidu.com/p/${id}` : '';
|
|
53
|
+
}
|
|
54
|
+
function resolveTiebaThreadId(raw) {
|
|
55
|
+
const direct = normalizeText(raw.threadId);
|
|
56
|
+
if (direct)
|
|
57
|
+
return direct;
|
|
58
|
+
const fromUrl = normalizeText(raw.url).match(/\/p\/(\d+)/);
|
|
59
|
+
return fromUrl ? fromUrl[1] : '';
|
|
60
|
+
}
|
|
61
|
+
function getTiebaFeedComponent(feed, name) {
|
|
62
|
+
const components = Array.isArray(feed?.components) ? feed.components : [];
|
|
63
|
+
const match = components.find((entry) => normalizeText(entry.component) === name);
|
|
64
|
+
if (!match)
|
|
65
|
+
return {};
|
|
66
|
+
const payload = match[name];
|
|
67
|
+
return payload && typeof payload === 'object' ? payload : {};
|
|
68
|
+
}
|
|
69
|
+
function extractTiebaFeedAuthor(feed) {
|
|
70
|
+
const head = getTiebaFeedComponent(feed, 'feed_head');
|
|
71
|
+
const mainData = Array.isArray(head.main_data) ? head.main_data : [];
|
|
72
|
+
for (const item of mainData) {
|
|
73
|
+
const textRecord = item.text;
|
|
74
|
+
const author = normalizeText(textRecord?.text);
|
|
75
|
+
if (author)
|
|
76
|
+
return author;
|
|
77
|
+
}
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
function extractTiebaFeedTitle(feed) {
|
|
81
|
+
const title = getTiebaFeedComponent(feed, 'feed_title');
|
|
82
|
+
const titleData = Array.isArray(title.data) ? title.data : [];
|
|
83
|
+
const firstTitle = titleData[0];
|
|
84
|
+
const textInfo = firstTitle?.text_info;
|
|
85
|
+
return normalizeText(textInfo?.text) || normalizeText(feed?.business_info_map?.title);
|
|
86
|
+
}
|
|
87
|
+
function extractTiebaFeedCommentCount(feed) {
|
|
88
|
+
const social = getTiebaFeedComponent(feed, 'feed_social');
|
|
89
|
+
const commentCount = Number(social.comment_num ?? feed?.business_info_map?.comment_num ?? 0);
|
|
90
|
+
return Number.isFinite(commentCount) ? commentCount : 0;
|
|
91
|
+
}
|
|
92
|
+
function extractTiebaFeedThreadId(feed) {
|
|
93
|
+
const direct = normalizeText(feed?.business_info_map?.thread_id);
|
|
94
|
+
if (direct)
|
|
95
|
+
return direct;
|
|
96
|
+
const logParams = Array.isArray(feed?.log_param) ? feed.log_param : [];
|
|
97
|
+
const fromLog = normalizeText(logParams.find((item) => normalizeText(item?.key) === 'tid')?.value);
|
|
98
|
+
if (fromLog)
|
|
99
|
+
return fromLog;
|
|
100
|
+
const fromSchema = normalizeText(feed?.schema).match(/[?&]tid=(\d+)/);
|
|
101
|
+
return fromSchema ? fromSchema[1] : '';
|
|
102
|
+
}
|
|
103
|
+
function extractTiebaFeedLastReply(feed) {
|
|
104
|
+
const head = getTiebaFeedComponent(feed, 'feed_head');
|
|
105
|
+
const extraData = Array.isArray(head.extra_data) ? head.extra_data : [];
|
|
106
|
+
const first = extraData[0];
|
|
107
|
+
const prefix = normalizeText(first?.business_info_map?.time_prefix);
|
|
108
|
+
const textRecord = first?.text;
|
|
109
|
+
const rawTime = normalizeText(textRecord?.text);
|
|
110
|
+
const formattedTime = /^\d+$/.test(rawTime) ? formatTiebaUnixTime(rawTime) : rawTime;
|
|
111
|
+
return [prefix, formattedTime].filter(Boolean).join('');
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Convert Tieba's signed `page_pc` feed entries into the stable card shape used by the CLI.
|
|
115
|
+
*/
|
|
116
|
+
export function buildTiebaPostCardsFromPagePc(rawFeeds) {
|
|
117
|
+
return rawFeeds
|
|
118
|
+
.filter((entry) => normalizeText(entry.layout) === 'feed' && entry.feed)
|
|
119
|
+
.map((entry) => {
|
|
120
|
+
const feed = entry.feed;
|
|
121
|
+
const threadId = extractTiebaFeedThreadId(feed);
|
|
122
|
+
return {
|
|
123
|
+
title: extractTiebaFeedTitle(feed),
|
|
124
|
+
author: extractTiebaFeedAuthor(feed),
|
|
125
|
+
descInfo: extractTiebaFeedLastReply(feed),
|
|
126
|
+
commentCount: extractTiebaFeedCommentCount(feed),
|
|
127
|
+
actionTexts: [],
|
|
128
|
+
threadId,
|
|
129
|
+
url: buildTiebaThreadUrl(threadId),
|
|
130
|
+
};
|
|
131
|
+
})
|
|
132
|
+
.filter((entry) => normalizeText(entry.title));
|
|
133
|
+
}
|
|
134
|
+
export function buildTiebaPostItems(rawCards, requestedLimit) {
|
|
135
|
+
const limit = normalizeTiebaLimit(requestedLimit);
|
|
136
|
+
return rawCards
|
|
137
|
+
.map((raw) => {
|
|
138
|
+
const title = normalizeText(raw.title);
|
|
139
|
+
const id = resolveTiebaThreadId(raw);
|
|
140
|
+
const actionTexts = Array.isArray(raw.actionTexts) ? raw.actionTexts.map(normalizeText).filter(Boolean) : [];
|
|
141
|
+
const commentText = actionTexts.find((text) => /评论/.test(text)) || actionTexts[actionTexts.length - 1] || '';
|
|
142
|
+
return {
|
|
143
|
+
title,
|
|
144
|
+
author: normalizeText(raw.author),
|
|
145
|
+
replies: Number.isFinite(Number(raw.commentCount))
|
|
146
|
+
? Number(raw.commentCount)
|
|
147
|
+
: parseTiebaCount(commentText),
|
|
148
|
+
last_reply: parseTiebaLastReply(String(raw.descInfo ?? '')),
|
|
149
|
+
id,
|
|
150
|
+
url: buildTiebaThreadUrl(id, raw.url),
|
|
151
|
+
};
|
|
152
|
+
})
|
|
153
|
+
.filter((item) => item.title)
|
|
154
|
+
.slice(0, limit)
|
|
155
|
+
.map((item, index) => ({ rank: index + 1, ...item }));
|
|
156
|
+
}
|
|
157
|
+
export function buildTiebaSearchItems(rawItems, requestedLimit) {
|
|
158
|
+
const limit = normalizeTiebaLimit(requestedLimit);
|
|
159
|
+
return rawItems
|
|
160
|
+
.map((raw) => {
|
|
161
|
+
const url = normalizeText(raw.url);
|
|
162
|
+
const directId = normalizeText(raw.id);
|
|
163
|
+
const idFromUrl = url.match(/\/p\/(\d+)/)?.[1] || '';
|
|
164
|
+
return {
|
|
165
|
+
title: normalizeText(raw.title),
|
|
166
|
+
forum: normalizeText(raw.forum),
|
|
167
|
+
author: normalizeText(raw.author),
|
|
168
|
+
time: normalizeText(raw.time),
|
|
169
|
+
snippet: normalizeText(raw.snippet).slice(0, 200),
|
|
170
|
+
id: directId || idFromUrl,
|
|
171
|
+
url,
|
|
172
|
+
};
|
|
173
|
+
})
|
|
174
|
+
.filter((item) => item.title)
|
|
175
|
+
.slice(0, limit)
|
|
176
|
+
.map((item, index) => ({ rank: index + 1, ...item }));
|
|
177
|
+
}
|
|
178
|
+
function formatTiebaUnixTime(value) {
|
|
179
|
+
const ts = Number(value || 0);
|
|
180
|
+
if (!Number.isFinite(ts) || ts <= 0)
|
|
181
|
+
return '';
|
|
182
|
+
const parts = new Intl.DateTimeFormat('sv-SE', {
|
|
183
|
+
timeZone: TIEBA_TIME_ZONE,
|
|
184
|
+
year: 'numeric',
|
|
185
|
+
month: '2-digit',
|
|
186
|
+
day: '2-digit',
|
|
187
|
+
hour: '2-digit',
|
|
188
|
+
minute: '2-digit',
|
|
189
|
+
hour12: false,
|
|
190
|
+
}).formatToParts(new Date(ts * 1000));
|
|
191
|
+
const values = Object.fromEntries(parts.map((part) => [part.type, part.value]));
|
|
192
|
+
return `${values.year}-${values.month}-${values.day} ${values.hour}:${values.minute}`;
|
|
193
|
+
}
|
|
194
|
+
function parseTiebaReplyTime(text) {
|
|
195
|
+
const normalized = normalizeText(text);
|
|
196
|
+
const withoutFloor = normalized.replace(/^第\d+楼\s+/, '').trim();
|
|
197
|
+
const match = withoutFloor.match(/^(刚刚|昨天|前天|\d+\s*(?:分钟|小时|天)前|\d{2}-\d{2}(?:\s+\d{2}:\d{2})?|\d{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2})?)/);
|
|
198
|
+
return match ? match[1].trim() : withoutFloor;
|
|
199
|
+
}
|
|
200
|
+
function buildMainPostItem(mainPost) {
|
|
201
|
+
if (!mainPost)
|
|
202
|
+
return null;
|
|
203
|
+
const title = normalizeText(mainPost.title);
|
|
204
|
+
const author = normalizeText(mainPost.author) || normalizeText(mainPost.fallbackAuthor);
|
|
205
|
+
const body = normalizeText(mainPost.contentText) || normalizeText(mainPost.structuredText);
|
|
206
|
+
const hasMedia = Boolean(mainPost.hasMedia);
|
|
207
|
+
const content = [title, body || (hasMedia ? '[media]' : '')].filter(Boolean).join(' ').trim();
|
|
208
|
+
if (!content)
|
|
209
|
+
return null;
|
|
210
|
+
return {
|
|
211
|
+
floor: 1,
|
|
212
|
+
author,
|
|
213
|
+
content,
|
|
214
|
+
time: normalizeText(mainPost.visibleTime) || formatTiebaUnixTime(mainPost.structuredTime),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
export function buildTiebaReadItems(payload, options = {}) {
|
|
218
|
+
const fallback = Number.isFinite(Number(options.limit)) ? Number(options.limit) : 30;
|
|
219
|
+
const limit = Math.max(1, Math.trunc(fallback));
|
|
220
|
+
const includeMainPost = options.includeMainPost !== false;
|
|
221
|
+
const items = [];
|
|
222
|
+
const mainPost = buildMainPostItem(payload.mainPost);
|
|
223
|
+
if (includeMainPost && mainPost)
|
|
224
|
+
items.push(mainPost);
|
|
225
|
+
const replies = Array.isArray(payload.replies) ? payload.replies : [];
|
|
226
|
+
const replyItems = [];
|
|
227
|
+
for (const reply of replies) {
|
|
228
|
+
const floor = Number(reply.floor || 0);
|
|
229
|
+
const content = normalizeText(reply.content);
|
|
230
|
+
if (!Number.isFinite(floor) || floor < 1 || !content)
|
|
231
|
+
continue;
|
|
232
|
+
replyItems.push({
|
|
233
|
+
floor,
|
|
234
|
+
author: normalizeText(reply.author),
|
|
235
|
+
content,
|
|
236
|
+
time: parseTiebaReplyTime(String(reply.time ?? '')),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return items.concat(replyItems.slice(0, limit));
|
|
240
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { MAX_TIEBA_LIMIT, buildTiebaPostCardsFromPagePc, buildTiebaPostItems, buildTiebaSearchItems, buildTiebaReadItems, normalizeTiebaLimit, signTiebaPcParams, } from './utils.js';
|
|
3
|
+
describe('normalizeTiebaLimit', () => {
|
|
4
|
+
it('caps list commands at the declared tieba maximum', () => {
|
|
5
|
+
expect(MAX_TIEBA_LIMIT).toBe(20);
|
|
6
|
+
expect(normalizeTiebaLimit(undefined)).toBe(20);
|
|
7
|
+
expect(normalizeTiebaLimit(25)).toBe(20);
|
|
8
|
+
expect(normalizeTiebaLimit(7)).toBe(7);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
describe('signTiebaPcParams', () => {
|
|
12
|
+
it('matches Tieba PC forum-list signing for stable page_pc requests', () => {
|
|
13
|
+
expect(signTiebaPcParams({
|
|
14
|
+
kw: encodeURIComponent('李毅'),
|
|
15
|
+
pn: '1',
|
|
16
|
+
sort_type: '-1',
|
|
17
|
+
is_newfrs: '1',
|
|
18
|
+
is_newfeed: '1',
|
|
19
|
+
rn: '30',
|
|
20
|
+
rn_need: '20',
|
|
21
|
+
tbs: '',
|
|
22
|
+
subapp_type: 'pc',
|
|
23
|
+
_client_type: '20',
|
|
24
|
+
})).toBe('466f2e091dd4ed17c6661a842b5ec342');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
describe('buildTiebaPostCardsFromPagePc', () => {
|
|
28
|
+
it('extracts thread cards from signed page_pc feed payloads', () => {
|
|
29
|
+
const cards = buildTiebaPostCardsFromPagePc([
|
|
30
|
+
{
|
|
31
|
+
layout: 'feed',
|
|
32
|
+
feed: {
|
|
33
|
+
schema: 'tiebaapp://router/portal?params=%7B%22pageParams%22%3A%7B%22tid%22%3A10596901456%7D%7D',
|
|
34
|
+
log_param: [
|
|
35
|
+
{ key: 'tid', value: '10596901456' },
|
|
36
|
+
],
|
|
37
|
+
business_info_map: {
|
|
38
|
+
thread_id: '10596901456',
|
|
39
|
+
title: '崇拜希特勒的人都是日本的汉奸走狗',
|
|
40
|
+
},
|
|
41
|
+
components: [
|
|
42
|
+
{
|
|
43
|
+
component: 'feed_head',
|
|
44
|
+
feed_head: {
|
|
45
|
+
extra_data: [
|
|
46
|
+
{
|
|
47
|
+
business_info_map: { time_prefix: '回复于' },
|
|
48
|
+
text: { text: '1774343231' },
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
main_data: [
|
|
52
|
+
{
|
|
53
|
+
text: { text: '上帝的子民º♬' },
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
component: 'feed_title',
|
|
60
|
+
feed_title: {
|
|
61
|
+
data: [{ text_info: { text: '崇拜希特勒的人都是日本的汉奸走狗' } }],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
component: 'feed_social',
|
|
66
|
+
feed_social: {
|
|
67
|
+
comment_num: 12,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
]);
|
|
74
|
+
expect(cards).toEqual([
|
|
75
|
+
{
|
|
76
|
+
title: '崇拜希特勒的人都是日本的汉奸走狗',
|
|
77
|
+
author: '上帝的子民º♬',
|
|
78
|
+
descInfo: '回复于2026-03-24 17:07',
|
|
79
|
+
commentCount: 12,
|
|
80
|
+
actionTexts: [],
|
|
81
|
+
threadId: '10596901456',
|
|
82
|
+
url: 'https://tieba.baidu.com/p/10596901456',
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe('buildTiebaPostItems', () => {
|
|
88
|
+
it('builds stable thread ids and urls from card props without page hops', () => {
|
|
89
|
+
const items = buildTiebaPostItems([
|
|
90
|
+
{
|
|
91
|
+
title: '我来说个事',
|
|
92
|
+
author: '暴躁的小伙子',
|
|
93
|
+
descInfo: '回复于2分钟前',
|
|
94
|
+
actionTexts: ['分享', '评论 5', '点赞 2'],
|
|
95
|
+
threadId: '10590564788',
|
|
96
|
+
},
|
|
97
|
+
], 5);
|
|
98
|
+
expect(items).toEqual([
|
|
99
|
+
{
|
|
100
|
+
rank: 1,
|
|
101
|
+
title: '我来说个事',
|
|
102
|
+
author: '暴躁的小伙子',
|
|
103
|
+
replies: 5,
|
|
104
|
+
last_reply: '2分钟前',
|
|
105
|
+
id: '10590564788',
|
|
106
|
+
url: 'https://tieba.baidu.com/p/10590564788',
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
it('honors the public 20-item limit contract', () => {
|
|
111
|
+
const raw = Array.from({ length: 25 }, (_, index) => ({
|
|
112
|
+
title: `帖子 ${index + 1}`,
|
|
113
|
+
author: `作者 ${index + 1}`,
|
|
114
|
+
descInfo: '回复于刚刚',
|
|
115
|
+
actionTexts: ['分享', `评论 ${index + 1}`],
|
|
116
|
+
threadId: String(1000 + index),
|
|
117
|
+
}));
|
|
118
|
+
const items = buildTiebaPostItems(raw, 25);
|
|
119
|
+
expect(items).toHaveLength(20);
|
|
120
|
+
expect(items[19]).toMatchObject({
|
|
121
|
+
rank: 20,
|
|
122
|
+
id: '1019',
|
|
123
|
+
url: 'https://tieba.baidu.com/p/1019',
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
it('parses Chinese count units and keeps date-time last-reply text intact', () => {
|
|
127
|
+
const items = buildTiebaPostItems([
|
|
128
|
+
{
|
|
129
|
+
title: '复杂格式帖子',
|
|
130
|
+
author: '作者',
|
|
131
|
+
descInfo: '回复于03-29 11:35',
|
|
132
|
+
actionTexts: ['分享', '评论 1.2万'],
|
|
133
|
+
url: 'https://tieba.baidu.com/p/123456',
|
|
134
|
+
},
|
|
135
|
+
], 5);
|
|
136
|
+
expect(items[0]).toMatchObject({
|
|
137
|
+
replies: 12000,
|
|
138
|
+
last_reply: '03-29 11:35',
|
|
139
|
+
id: '123456',
|
|
140
|
+
url: 'https://tieba.baidu.com/p/123456',
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('buildTiebaSearchItems', () => {
|
|
145
|
+
it('keeps up to 20 search results when the page provides more than 10 cards', () => {
|
|
146
|
+
const raw = Array.from({ length: 25 }, (_, index) => ({
|
|
147
|
+
title: `结果 ${index + 1}`,
|
|
148
|
+
forum: '编程吧',
|
|
149
|
+
author: `作者 ${index + 1}`,
|
|
150
|
+
time: '2026-03-29',
|
|
151
|
+
snippet: `摘要 ${index + 1}`,
|
|
152
|
+
id: String(2000 + index),
|
|
153
|
+
url: `https://tieba.baidu.com/p/${2000 + index}`,
|
|
154
|
+
}));
|
|
155
|
+
const items = buildTiebaSearchItems(raw, 25);
|
|
156
|
+
expect(items).toHaveLength(20);
|
|
157
|
+
expect(items[19]).toMatchObject({
|
|
158
|
+
rank: 20,
|
|
159
|
+
id: '2019',
|
|
160
|
+
url: 'https://tieba.baidu.com/p/2019',
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
it('fills missing search ids from stable thread urls', () => {
|
|
164
|
+
const items = buildTiebaSearchItems([
|
|
165
|
+
{
|
|
166
|
+
title: '搜索结果',
|
|
167
|
+
forum: '编程吧',
|
|
168
|
+
author: '作者',
|
|
169
|
+
time: '2026-03-29 11:35',
|
|
170
|
+
snippet: '摘要',
|
|
171
|
+
id: '',
|
|
172
|
+
url: 'https://tieba.baidu.com/p/654321',
|
|
173
|
+
},
|
|
174
|
+
], 5);
|
|
175
|
+
expect(items[0]).toMatchObject({
|
|
176
|
+
id: '654321',
|
|
177
|
+
url: 'https://tieba.baidu.com/p/654321',
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('buildTiebaReadItems', () => {
|
|
182
|
+
it('prefers visible main-post fields and still keeps floor 1 for media-only threads', () => {
|
|
183
|
+
const items = buildTiebaReadItems({
|
|
184
|
+
mainPost: {
|
|
185
|
+
title: '刚开始读博士的人据说都这样',
|
|
186
|
+
author: '湖水之岸',
|
|
187
|
+
contentText: '',
|
|
188
|
+
structuredText: '',
|
|
189
|
+
visibleTime: '03-24',
|
|
190
|
+
structuredTime: 1774343231,
|
|
191
|
+
hasMedia: true,
|
|
192
|
+
},
|
|
193
|
+
replies: [],
|
|
194
|
+
}, { limit: 5, includeMainPost: true });
|
|
195
|
+
expect(items).toEqual([
|
|
196
|
+
{
|
|
197
|
+
floor: 1,
|
|
198
|
+
author: '湖水之岸',
|
|
199
|
+
content: '刚开始读博士的人据说都这样 [media]',
|
|
200
|
+
time: '03-24',
|
|
201
|
+
},
|
|
202
|
+
]);
|
|
203
|
+
});
|
|
204
|
+
it('falls back to structured main-post data when visible text is missing', () => {
|
|
205
|
+
const items = buildTiebaReadItems({
|
|
206
|
+
mainPost: {
|
|
207
|
+
title: '标题',
|
|
208
|
+
author: '',
|
|
209
|
+
fallbackAuthor: '结构化作者',
|
|
210
|
+
contentText: '',
|
|
211
|
+
structuredText: '结构化正文',
|
|
212
|
+
visibleTime: '',
|
|
213
|
+
structuredTime: 1774343231,
|
|
214
|
+
hasMedia: false,
|
|
215
|
+
},
|
|
216
|
+
replies: [
|
|
217
|
+
{ floor: 2, author: '回复者', content: '二楼内容', time: '第2楼 2026-03-25 12:34 广东' },
|
|
218
|
+
],
|
|
219
|
+
}, { limit: 5, includeMainPost: true });
|
|
220
|
+
expect(items[0]).toMatchObject({
|
|
221
|
+
floor: 1,
|
|
222
|
+
author: '结构化作者',
|
|
223
|
+
content: '标题 结构化正文',
|
|
224
|
+
time: '2026-03-24 17:07',
|
|
225
|
+
});
|
|
226
|
+
expect(items[1]).toMatchObject({
|
|
227
|
+
floor: 2,
|
|
228
|
+
author: '回复者',
|
|
229
|
+
content: '二楼内容',
|
|
230
|
+
time: '2026-03-25 12:34',
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
it('strips trailing location metadata from reply times', () => {
|
|
234
|
+
const items = buildTiebaReadItems({
|
|
235
|
+
mainPost: {
|
|
236
|
+
title: '主楼',
|
|
237
|
+
author: '楼主',
|
|
238
|
+
contentText: '正文',
|
|
239
|
+
visibleTime: '03-24',
|
|
240
|
+
},
|
|
241
|
+
replies: [
|
|
242
|
+
{ floor: 2, author: '二楼', content: '二楼内容', time: '第2楼 3小时前 福建' },
|
|
243
|
+
{ floor: 3, author: '三楼', content: '三楼内容', time: '第3楼 刚刚 江苏' },
|
|
244
|
+
],
|
|
245
|
+
}, { limit: 5, includeMainPost: false });
|
|
246
|
+
expect(items).toEqual([
|
|
247
|
+
{
|
|
248
|
+
floor: 2,
|
|
249
|
+
author: '二楼',
|
|
250
|
+
content: '二楼内容',
|
|
251
|
+
time: '3小时前',
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
floor: 3,
|
|
255
|
+
author: '三楼',
|
|
256
|
+
content: '三楼内容',
|
|
257
|
+
time: '刚刚',
|
|
258
|
+
},
|
|
259
|
+
]);
|
|
260
|
+
});
|
|
261
|
+
it('counts limit as replies and skips main post on later pages', () => {
|
|
262
|
+
const items = buildTiebaReadItems({
|
|
263
|
+
mainPost: {
|
|
264
|
+
title: '主楼',
|
|
265
|
+
author: '楼主',
|
|
266
|
+
contentText: '正文',
|
|
267
|
+
visibleTime: '03-24',
|
|
268
|
+
},
|
|
269
|
+
replies: [
|
|
270
|
+
{ floor: 2, author: '二楼', content: '二楼内容', time: '第2楼 03-25' },
|
|
271
|
+
{ floor: 3, author: '三楼', content: '三楼内容', time: '第3楼 03-26' },
|
|
272
|
+
{ floor: 4, author: '四楼', content: '四楼内容', time: '第4楼 03-27' },
|
|
273
|
+
],
|
|
274
|
+
}, { limit: 2, includeMainPost: true });
|
|
275
|
+
expect(items).toHaveLength(3);
|
|
276
|
+
expect(items.map((item) => item.floor)).toEqual([1, 2, 3]);
|
|
277
|
+
const page2 = buildTiebaReadItems({
|
|
278
|
+
mainPost: {
|
|
279
|
+
title: '主楼',
|
|
280
|
+
author: '楼主',
|
|
281
|
+
contentText: '正文',
|
|
282
|
+
visibleTime: '03-24',
|
|
283
|
+
},
|
|
284
|
+
replies: [
|
|
285
|
+
{ floor: 26, author: '二十六楼', content: '二十六楼内容', time: '第26楼 03-29' },
|
|
286
|
+
],
|
|
287
|
+
}, { limit: 2, includeMainPost: false });
|
|
288
|
+
expect(page2.map((item) => item.floor)).toEqual([26]);
|
|
289
|
+
});
|
|
290
|
+
});
|