@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
|
@@ -38,6 +38,201 @@ function isBrowserBridgeUnavailable(result: CliResult): boolean {
|
|
|
38
38
|
return /Browser Bridge.*not connected|Extension.*not connected/i.test(text);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function isBaiduChallengeText(text: string): boolean {
|
|
42
|
+
return /百度安全验证|安全验证|请完成验证|captcha/i.test(text);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isBaiduChallenge(result: CliResult): boolean {
|
|
46
|
+
const text = `${result.stderr}\n${result.stdout}`;
|
|
47
|
+
return isBaiduChallengeText(text);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isTransientBrowserDetach(result: CliResult): boolean {
|
|
51
|
+
const text = `${result.stderr}\n${result.stdout}`;
|
|
52
|
+
return /Detached while handling command|No tab with id|Debugger is not attached to the tab/i.test(text);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function runCliWithTransientRetry(args: string[], timeout: number): Promise<CliResult> {
|
|
56
|
+
let result = await runCli(args, { timeout });
|
|
57
|
+
if (result.code !== 0 && isTransientBrowserDetach(result)) {
|
|
58
|
+
result = await runCli(args, { timeout });
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function runJsonCliOrThrow(args: string[], label: string, timeout: number, opts: { retryTransient?: boolean } = {}): Promise<any[] | null> {
|
|
64
|
+
const result = opts.retryTransient
|
|
65
|
+
? await runCliWithTransientRetry(args, timeout)
|
|
66
|
+
: await runCli(args, { timeout });
|
|
67
|
+
if (result.code !== 0) {
|
|
68
|
+
if (isBrowserBridgeUnavailable(result)) {
|
|
69
|
+
console.warn(`${label}: skipped — Browser Bridge extension is unavailable in this environment`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (isBaiduChallenge(result)) {
|
|
73
|
+
console.warn(`${label}: skipped — Baidu challenge page detected`);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`${label} failed:\n${result.stderr || result.stdout}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const data = parseJsonOutput(result.stdout);
|
|
80
|
+
if (!Array.isArray(data)) {
|
|
81
|
+
throw new Error(`${label} returned non-array JSON:\n${result.stdout.slice(0, 500)}`);
|
|
82
|
+
}
|
|
83
|
+
return data;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeTiebaTitle(value: string): string {
|
|
87
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hasTiebaMainPost(data: any[] | null): boolean {
|
|
91
|
+
return Array.isArray(data) && data.some((item: any) => Number(item.floor) === 1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function expectNonEmptyDataOrSkipEnv(data: any[] | null, label: string): data is any[] {
|
|
95
|
+
if (data === null) {
|
|
96
|
+
console.warn(`${label}: skipped — environment is unavailable for browser assertions`);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
expect(data.length).toBeGreaterThanOrEqual(1);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function countTiebaReplies(data: any[] | null): number {
|
|
104
|
+
if (!Array.isArray(data)) return 0;
|
|
105
|
+
return data.filter((item: any) => Number(item.floor) > 1).length;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function maxTiebaFloor(data: any[] | null): number {
|
|
109
|
+
if (!Array.isArray(data) || !data.length) return 0;
|
|
110
|
+
return Math.max(...data.map((item: any) => Number(item.floor) || 0));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getTiebaReplyFloors(data: any[] | null): number[] {
|
|
114
|
+
if (!Array.isArray(data)) return [];
|
|
115
|
+
return data
|
|
116
|
+
.map((item: any) => Number(item.floor) || 0)
|
|
117
|
+
.filter((floor) => floor > 1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function countTiebaReplyFloorOverlap(left: any[] | null, right: any[] | null): number {
|
|
121
|
+
const rightFloors = new Set(getTiebaReplyFloors(right));
|
|
122
|
+
return getTiebaReplyFloors(left).filter((floor) => rightFloors.has(floor)).length;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function pickTiebaReadCandidates(
|
|
126
|
+
posts: any[] | null,
|
|
127
|
+
minReplies: number,
|
|
128
|
+
): Array<{ threadId: string; title: string; replies: number }> {
|
|
129
|
+
if (!Array.isArray(posts) || !posts.length) return [];
|
|
130
|
+
|
|
131
|
+
return [...posts]
|
|
132
|
+
.filter((item: any) => item?.id)
|
|
133
|
+
.map((item: any) => ({
|
|
134
|
+
threadId: String(item.id || '').trim(),
|
|
135
|
+
title: normalizeTiebaTitle(String(item.title || '')),
|
|
136
|
+
replies: Number(item.replies) || 0,
|
|
137
|
+
}))
|
|
138
|
+
.filter((item) => item.threadId && item.title && item.replies >= minReplies)
|
|
139
|
+
.sort((left, right) => right.replies - left.replies);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Pick a live thread that actually exposes enough visible replies for the read assertions.
|
|
144
|
+
*/
|
|
145
|
+
async function getTiebaReadCandidateOrSkip(
|
|
146
|
+
label: string,
|
|
147
|
+
options: { minRepliesOnPage1?: number; requirePage2?: boolean } = {},
|
|
148
|
+
): Promise<{ threadId: string; title: string; replies: number } | null> {
|
|
149
|
+
const minRepliesOnPage1 = Math.max(1, Number(options.minRepliesOnPage1 || 1));
|
|
150
|
+
const requirePage2 = options.requirePage2 === true;
|
|
151
|
+
const posts = await runJsonCliOrThrow(['tieba', 'posts', '李毅', '--limit', '10', '-f', 'json'], `${label} setup`, 90_000, {
|
|
152
|
+
retryTransient: true,
|
|
153
|
+
});
|
|
154
|
+
if (posts === null) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
if (!Array.isArray(posts) || !posts.length) {
|
|
158
|
+
console.warn(`${label}: skipped — could not resolve Tieba posts for setup`);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const minReplies = requirePage2 ? Math.max(minRepliesOnPage1, 50) : minRepliesOnPage1;
|
|
163
|
+
const candidates = pickTiebaReadCandidates(posts, minReplies).slice(0, 5);
|
|
164
|
+
if (!candidates.length) {
|
|
165
|
+
console.warn(`${label}: skipped — could not find a Tieba thread with enough replies from posts metadata`);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const candidate of candidates) {
|
|
170
|
+
const page1Preview = await runJsonCliOrThrow(
|
|
171
|
+
['tieba', 'read', candidate.threadId, '--page', '1', '--limit', String(Math.max(minRepliesOnPage1, 2)), '-f', 'json'],
|
|
172
|
+
`${label} preview page 1`,
|
|
173
|
+
90_000,
|
|
174
|
+
{ retryTransient: true },
|
|
175
|
+
);
|
|
176
|
+
if (page1Preview === null) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (!hasTiebaMainPost(page1Preview) || countTiebaReplies(page1Preview) < minRepliesOnPage1) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (requirePage2) {
|
|
184
|
+
const page2Preview = await runJsonCliOrThrow(
|
|
185
|
+
['tieba', 'read', candidate.threadId, '--page', '2', '--limit', '1', '-f', 'json'],
|
|
186
|
+
`${label} preview page 2`,
|
|
187
|
+
90_000,
|
|
188
|
+
{ retryTransient: true },
|
|
189
|
+
);
|
|
190
|
+
if (page2Preview === null) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
if (hasTiebaMainPost(page2Preview) || countTiebaReplies(page2Preview) < 1) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return candidate;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.warn(`${label}: skipped — could not find a Tieba thread with enough visible replies`);
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
describe('tieba e2e helper guards', () => {
|
|
206
|
+
it('does not treat generic empty-result errors as a Baidu challenge', () => {
|
|
207
|
+
expect(isBaiduChallengeText('tieba posts returned no data\n→ The page structure may have changed — this adapter may be outdated.')).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('still recognizes actual Baidu challenge text', () => {
|
|
211
|
+
expect(isBaiduChallengeText('百度安全验证,请完成验证后继续')).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('counts partial overlap between read pages', () => {
|
|
215
|
+
expect(countTiebaReplyFloorOverlap(
|
|
216
|
+
[{ floor: 1 }, { floor: 23 }, { floor: 27 }, { floor: 28 }, { floor: 29 }, { floor: 30 }],
|
|
217
|
+
[{ floor: 27 }, { floor: 28 }, { floor: 31 }],
|
|
218
|
+
)).toBe(2);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('picks read fixtures from posts metadata in descending reply order', () => {
|
|
222
|
+
expect(pickTiebaReadCandidates([
|
|
223
|
+
{ id: '1', title: '普通帖', replies: 2 },
|
|
224
|
+
{ id: '2', title: '大帖', replies: 120 },
|
|
225
|
+
{ id: '', title: '无效帖', replies: 999 },
|
|
226
|
+
], 50)).toEqual([{
|
|
227
|
+
threadId: '2',
|
|
228
|
+
title: '大帖',
|
|
229
|
+
replies: 120,
|
|
230
|
+
}]);
|
|
231
|
+
|
|
232
|
+
expect(pickTiebaReadCandidates([{ id: '1', title: '普通帖', replies: 2 }], 50)).toEqual([]);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
41
236
|
async function expectImdbDataOrChallengeSkip(args: string[], label: string): Promise<any[] | null> {
|
|
42
237
|
const result = await runCli(args, { timeout: 60_000 });
|
|
43
238
|
if (result.code !== 0) {
|
|
@@ -103,6 +298,99 @@ describe('browser public-data commands E2E', () => {
|
|
|
103
298
|
expectDataOrSkip(data, 'v2ex daily');
|
|
104
299
|
}, 60_000);
|
|
105
300
|
|
|
301
|
+
// ── tieba ──
|
|
302
|
+
it('tieba hot returns trending topics', async () => {
|
|
303
|
+
const data = await runJsonCliOrThrow(['tieba', 'hot', '--limit', '5', '-f', 'json'], 'tieba hot', 60_000, { retryTransient: true });
|
|
304
|
+
if (expectNonEmptyDataOrSkipEnv(data, 'tieba hot')) {
|
|
305
|
+
expect(data[0]).toHaveProperty('title');
|
|
306
|
+
expect(data[0]).toHaveProperty('discussions');
|
|
307
|
+
}
|
|
308
|
+
}, 60_000);
|
|
309
|
+
|
|
310
|
+
it('tieba posts returns forum threads', async () => {
|
|
311
|
+
const data = await runJsonCliOrThrow(['tieba', 'posts', '李毅', '--limit', '20', '-f', 'json'], 'tieba posts', 90_000, { retryTransient: true });
|
|
312
|
+
if (expectNonEmptyDataOrSkipEnv(data, 'tieba posts')) {
|
|
313
|
+
expect(data[0]).toHaveProperty('title');
|
|
314
|
+
expect(String(data[0].id || '')).toMatch(/^\d+$/);
|
|
315
|
+
expect(String(data[0].url || '')).toContain('/p/');
|
|
316
|
+
expect(Number.isFinite(Number(data[0].replies))).toBe(true);
|
|
317
|
+
expect(data.length).toBeLessThanOrEqual(20);
|
|
318
|
+
}
|
|
319
|
+
}, 90_000);
|
|
320
|
+
|
|
321
|
+
it('tieba posts page 2 returns a different forum slice', async () => {
|
|
322
|
+
const data1 = await runJsonCliOrThrow(['tieba', 'posts', '李毅', '--page', '1', '--limit', '5', '-f', 'json'], 'tieba posts page 1', 60_000, { retryTransient: true });
|
|
323
|
+
const data2 = await runJsonCliOrThrow(['tieba', 'posts', '李毅', '--page', '2', '--limit', '5', '-f', 'json'], 'tieba posts page 2', 60_000, { retryTransient: true });
|
|
324
|
+
if (expectNonEmptyDataOrSkipEnv(data1, 'tieba posts page 1') && expectNonEmptyDataOrSkipEnv(data2, 'tieba posts page 2')) {
|
|
325
|
+
const ids1 = data1.map((item: any) => String(item.id || '')).filter(Boolean);
|
|
326
|
+
const ids2 = data2.map((item: any) => String(item.id || '')).filter(Boolean);
|
|
327
|
+
const newIds = ids2.filter((id) => !ids1.includes(id));
|
|
328
|
+
expect(newIds.length).toBeGreaterThan(0);
|
|
329
|
+
}
|
|
330
|
+
}, 90_000);
|
|
331
|
+
|
|
332
|
+
it('tieba search returns results', async () => {
|
|
333
|
+
const data = await runJsonCliOrThrow(['tieba', 'search', '编程', '--limit', '20', '-f', 'json'], 'tieba search', 90_000, { retryTransient: true });
|
|
334
|
+
if (expectNonEmptyDataOrSkipEnv(data, 'tieba search')) {
|
|
335
|
+
expect(data[0]).toHaveProperty('title');
|
|
336
|
+
expect(String(data[0].id || '')).toMatch(/^\d+$/);
|
|
337
|
+
expect(String(data[0].url || '')).toContain('/p/');
|
|
338
|
+
expect(data.length).toBeLessThanOrEqual(20);
|
|
339
|
+
}
|
|
340
|
+
}, 90_000);
|
|
341
|
+
|
|
342
|
+
it('tieba search rejects unsupported pages above 1', async () => {
|
|
343
|
+
const result = await runCli(['tieba', 'search', '编程', '--page', '2', '--limit', '3', '-f', 'json'], {
|
|
344
|
+
timeout: 60_000,
|
|
345
|
+
});
|
|
346
|
+
expect(result.code).toBe(2);
|
|
347
|
+
expect(`${result.stderr}\n${result.stdout}`).toContain('Argument "page" must be one of: 1');
|
|
348
|
+
}, 60_000);
|
|
349
|
+
|
|
350
|
+
it('tieba read returns thread content', async () => {
|
|
351
|
+
const fixture = await getTiebaReadCandidateOrSkip('tieba read');
|
|
352
|
+
if (!fixture) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const data = await runJsonCliOrThrow(['tieba', 'read', fixture.threadId, '--limit', '5', '-f', 'json'], 'tieba read', 90_000, { retryTransient: true });
|
|
356
|
+
if (expectNonEmptyDataOrSkipEnv(data, 'tieba read')) {
|
|
357
|
+
expect(data[0]).toHaveProperty('floor');
|
|
358
|
+
expect(data[0]).toHaveProperty('content');
|
|
359
|
+
expect(data.some((item: any) => Number(item.floor) === 1)).toBe(true);
|
|
360
|
+
expect(normalizeTiebaTitle(String(data[0].content || ''))).toContain(fixture.title);
|
|
361
|
+
}
|
|
362
|
+
}, 90_000);
|
|
363
|
+
|
|
364
|
+
it('tieba read page 2 omits the main post', async () => {
|
|
365
|
+
const fixture = await getTiebaReadCandidateOrSkip('tieba read page', { requirePage2: true });
|
|
366
|
+
if (!fixture) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const data1 = await runJsonCliOrThrow(['tieba', 'read', fixture.threadId, '--page', '1', '--limit', '5', '-f', 'json'], 'tieba read page 1', 90_000, { retryTransient: true });
|
|
370
|
+
const data2 = await runJsonCliOrThrow(['tieba', 'read', fixture.threadId, '--page', '2', '--limit', '5', '-f', 'json'], 'tieba read page 2', 90_000, { retryTransient: true });
|
|
371
|
+
if (expectNonEmptyDataOrSkipEnv(data1, 'tieba read page 1') && expectNonEmptyDataOrSkipEnv(data2, 'tieba read page 2')) {
|
|
372
|
+
const overlap = countTiebaReplyFloorOverlap(data1, data2);
|
|
373
|
+
expect(normalizeTiebaTitle(String(data1[0].content || ''))).toContain(fixture.title);
|
|
374
|
+
expect(hasTiebaMainPost(data1)).toBe(true);
|
|
375
|
+
expect(hasTiebaMainPost(data2)).toBe(false);
|
|
376
|
+
expect(overlap).toBe(0);
|
|
377
|
+
expect(maxTiebaFloor(data2)).toBeGreaterThan(maxTiebaFloor(data1));
|
|
378
|
+
}
|
|
379
|
+
}, 90_000);
|
|
380
|
+
|
|
381
|
+
it('tieba read limit counts replies instead of consuming the main post slot', async () => {
|
|
382
|
+
const fixture = await getTiebaReadCandidateOrSkip('tieba read limit semantics', { minRepliesOnPage1: 2 });
|
|
383
|
+
if (!fixture) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const data = await runJsonCliOrThrow(['tieba', 'read', fixture.threadId, '--page', '1', '--limit', '2', '-f', 'json'], 'tieba read limit semantics', 90_000, { retryTransient: true });
|
|
387
|
+
if (expectNonEmptyDataOrSkipEnv(data, 'tieba read limit semantics')) {
|
|
388
|
+
expect(normalizeTiebaTitle(String(data[0].content || ''))).toContain(fixture.title);
|
|
389
|
+
expect(hasTiebaMainPost(data)).toBe(true);
|
|
390
|
+
expect(countTiebaReplies(data)).toBe(2);
|
|
391
|
+
}
|
|
392
|
+
}, 90_000);
|
|
393
|
+
|
|
106
394
|
// ── imdb ──
|
|
107
395
|
it('imdb top returns chart data', async () => {
|
|
108
396
|
const data = await expectImdbDataOrChallengeSkip(['imdb', 'top', '--limit', '3', '-f', 'json'], 'imdb top');
|
|
@@ -101,6 +101,6 @@ describe('management commands E2E', () => {
|
|
|
101
101
|
// ── unknown command ──
|
|
102
102
|
it('unknown command shows error', async () => {
|
|
103
103
|
const { stderr, code } = await runCli(['nonexistent-command-xyz']);
|
|
104
|
-
expect(code).toBe(
|
|
104
|
+
expect(code).toBe(2);
|
|
105
105
|
});
|
|
106
106
|
});
|
|
@@ -134,7 +134,7 @@ describe('plugin management E2E', () => {
|
|
|
134
134
|
|
|
135
135
|
it('plugin update without name or --all shows error', async () => {
|
|
136
136
|
const { stderr, code } = await runPluginCli(['plugin', 'update']);
|
|
137
|
-
expect(code).toBe(
|
|
137
|
+
expect(code).toBe(2);
|
|
138
138
|
expect(stderr).toContain('specify a plugin name');
|
|
139
139
|
});
|
|
140
140
|
});
|
package/vitest.config.ts
CHANGED