@jackwener/opencli 1.5.4 → 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 +1284 -67
- package/dist/cli.js +14 -14
- package/dist/clis/antigravity/serve.js +2 -2
- 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/sinafinance/rolling-news.d.ts +4 -0
- package/dist/clis/sinafinance/rolling-news.js +40 -0
- package/dist/clis/sinafinance/stock.d.ts +8 -0
- package/dist/clis/sinafinance/stock.js +117 -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 +27 -4
- package/dist/commanderAdapter.test.js +39 -0
- package/dist/daemon.js +5 -4
- package/dist/errors.d.ts +29 -1
- package/dist/errors.js +49 -11
- package/dist/external-clis.yaml +17 -0
- package/dist/external.js +3 -3
- package/dist/main.js +2 -1
- package/dist/tui.js +2 -1
- 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/sinafinance.md +56 -6
- 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/dist/background.js +1 -2
- package/extension/manifest.json +1 -1
- package/extension/package.json +1 -1
- package/extension/src/background.ts +17 -1
- 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/cli.ts +14 -14
- package/src/clis/antigravity/serve.ts +2 -2
- 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/sinafinance/rolling-news.ts +42 -0
- package/src/clis/sinafinance/stock.ts +127 -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 +26 -3
- package/src/daemon.ts +5 -4
- package/src/errors.ts +71 -10
- package/src/external-clis.yaml +17 -0
- package/src/external.ts +3 -3
- package/src/main.ts +2 -1
- package/src/tui.ts +2 -1
- 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
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* --mode raw: every caption segment as-is with precise timestamps
|
|
11
11
|
*/
|
|
12
12
|
import { cli, Strategy } from '../../registry.js';
|
|
13
|
-
import { parseVideoId } from './utils.js';
|
|
13
|
+
import { parseVideoId, prepareYoutubeApiPage } from './utils.js';
|
|
14
14
|
import { groupTranscriptSegments, formatGroupedTranscript, } from './transcript-group.js';
|
|
15
15
|
import { CommandExecutionError, EmptyResultError } from '../../errors.js';
|
|
16
16
|
cli({
|
|
@@ -28,9 +28,7 @@ cli({
|
|
|
28
28
|
// so we let the renderer auto-detect columns from the data keys.
|
|
29
29
|
func: async (page, kwargs) => {
|
|
30
30
|
const videoId = parseVideoId(kwargs.url);
|
|
31
|
-
|
|
32
|
-
await page.goto(videoUrl);
|
|
33
|
-
await page.wait(3);
|
|
31
|
+
await prepareYoutubeApiPage(page);
|
|
34
32
|
const lang = kwargs.lang || '';
|
|
35
33
|
const mode = kwargs.mode || 'grouped';
|
|
36
34
|
// Step 1: Get caption track URL via Android InnerTube API
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared YouTube utilities — URL parsing, video ID extraction, etc.
|
|
3
3
|
*/
|
|
4
|
+
import type { IPage } from '../../types.js';
|
|
4
5
|
/**
|
|
5
6
|
* Extract a YouTube video ID from a URL or bare video ID string.
|
|
6
7
|
* Supports: watch?v=, youtu.be/, /shorts/, /embed/, /live/, /v/
|
|
7
8
|
*/
|
|
8
9
|
export declare function parseVideoId(input: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Extract a JSON object assigned to a known bootstrap variable inside YouTube HTML.
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractJsonAssignmentFromHtml(html: string, keys: string | string[]): Record<string, unknown> | null;
|
|
14
|
+
/**
|
|
15
|
+
* Prepare a quiet YouTube API-capable page without opening the watch UI.
|
|
16
|
+
*/
|
|
17
|
+
export declare function prepareYoutubeApiPage(page: IPage): Promise<void>;
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared YouTube utilities — URL parsing, video ID extraction, etc.
|
|
3
|
-
*/
|
|
4
1
|
/**
|
|
5
2
|
* Extract a YouTube video ID from a URL or bare video ID string.
|
|
6
3
|
* Supports: watch?v=, youtu.be/, /shorts/, /embed/, /live/, /v/
|
|
@@ -26,3 +23,70 @@ export function parseVideoId(input) {
|
|
|
26
23
|
}
|
|
27
24
|
return input;
|
|
28
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Extract a JSON object assigned to a known bootstrap variable inside YouTube HTML.
|
|
28
|
+
*/
|
|
29
|
+
export function extractJsonAssignmentFromHtml(html, keys) {
|
|
30
|
+
const candidates = Array.isArray(keys) ? keys : [keys];
|
|
31
|
+
for (const key of candidates) {
|
|
32
|
+
const markers = [
|
|
33
|
+
`var ${key} = `,
|
|
34
|
+
`window["${key}"] = `,
|
|
35
|
+
`window.${key} = `,
|
|
36
|
+
`${key} = `,
|
|
37
|
+
];
|
|
38
|
+
for (const marker of markers) {
|
|
39
|
+
const markerIndex = html.indexOf(marker);
|
|
40
|
+
if (markerIndex === -1)
|
|
41
|
+
continue;
|
|
42
|
+
const jsonStart = html.indexOf('{', markerIndex + marker.length);
|
|
43
|
+
if (jsonStart === -1)
|
|
44
|
+
continue;
|
|
45
|
+
let depth = 0;
|
|
46
|
+
let inString = false;
|
|
47
|
+
let escaping = false;
|
|
48
|
+
for (let i = jsonStart; i < html.length; i += 1) {
|
|
49
|
+
const ch = html[i];
|
|
50
|
+
if (inString) {
|
|
51
|
+
if (escaping) {
|
|
52
|
+
escaping = false;
|
|
53
|
+
}
|
|
54
|
+
else if (ch === '\\') {
|
|
55
|
+
escaping = true;
|
|
56
|
+
}
|
|
57
|
+
else if (ch === '"') {
|
|
58
|
+
inString = false;
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (ch === '"') {
|
|
63
|
+
inString = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (ch === '{') {
|
|
67
|
+
depth += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (ch === '}') {
|
|
71
|
+
depth -= 1;
|
|
72
|
+
if (depth === 0) {
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(html.slice(jsonStart, i + 1));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Prepare a quiet YouTube API-capable page without opening the watch UI.
|
|
88
|
+
*/
|
|
89
|
+
export async function prepareYoutubeApiPage(page) {
|
|
90
|
+
await page.goto('https://www.youtube.com', { waitUntil: 'none' });
|
|
91
|
+
await page.wait(2);
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { extractJsonAssignmentFromHtml, prepareYoutubeApiPage } from './utils.js';
|
|
3
|
+
describe('youtube utils', () => {
|
|
4
|
+
it('extractJsonAssignmentFromHtml parses bootstrap objects with nested braces in strings', () => {
|
|
5
|
+
const html = `
|
|
6
|
+
<script>
|
|
7
|
+
var ytInitialPlayerResponse = {
|
|
8
|
+
"title": "brace { inside } string",
|
|
9
|
+
"nested": { "count": 2, "text": "quote \\"value\\"" }
|
|
10
|
+
};
|
|
11
|
+
</script>
|
|
12
|
+
`;
|
|
13
|
+
expect(extractJsonAssignmentFromHtml(html, 'ytInitialPlayerResponse')).toEqual({
|
|
14
|
+
title: 'brace { inside } string',
|
|
15
|
+
nested: { count: 2, text: 'quote "value"' },
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
it('extractJsonAssignmentFromHtml supports window assignments', () => {
|
|
19
|
+
const html = `
|
|
20
|
+
<script>
|
|
21
|
+
window["ytInitialData"] = {"contents":{"items":[1,2,3]}};
|
|
22
|
+
</script>
|
|
23
|
+
`;
|
|
24
|
+
expect(extractJsonAssignmentFromHtml(html, 'ytInitialData')).toEqual({
|
|
25
|
+
contents: { items: [1, 2, 3] },
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
it('prepareYoutubeApiPage loads the quiet API bootstrap page', async () => {
|
|
29
|
+
const page = {
|
|
30
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
31
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
32
|
+
};
|
|
33
|
+
await expect(prepareYoutubeApiPage(page)).resolves.toBeUndefined();
|
|
34
|
+
expect(page.goto).toHaveBeenCalledWith('https://www.youtube.com', { waitUntil: 'none' });
|
|
35
|
+
expect(page.wait).toHaveBeenCalledWith(2);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* YouTube video metadata —
|
|
2
|
+
* YouTube video metadata — fetch watch HTML and parse bootstrap data without opening the watch UI.
|
|
3
3
|
*/
|
|
4
4
|
import { cli, Strategy } from '../../registry.js';
|
|
5
|
-
import { parseVideoId } from './utils.js';
|
|
5
|
+
import { extractJsonAssignmentFromHtml, parseVideoId, prepareYoutubeApiPage } from './utils.js';
|
|
6
6
|
import { CommandExecutionError } from '../../errors.js';
|
|
7
7
|
cli({
|
|
8
8
|
site: 'youtube',
|
|
@@ -16,23 +16,28 @@ cli({
|
|
|
16
16
|
columns: ['field', 'value'],
|
|
17
17
|
func: async (page, kwargs) => {
|
|
18
18
|
const videoId = parseVideoId(kwargs.url);
|
|
19
|
-
|
|
20
|
-
await page.goto(videoUrl);
|
|
21
|
-
await page.wait(3);
|
|
19
|
+
await prepareYoutubeApiPage(page);
|
|
22
20
|
const data = await page.evaluate(`
|
|
23
21
|
(async () => {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
const extractJsonAssignmentFromHtml = ${extractJsonAssignmentFromHtml.toString()};
|
|
23
|
+
|
|
24
|
+
const watchResp = await fetch('/watch?v=' + encodeURIComponent(${JSON.stringify(videoId)}), {
|
|
25
|
+
credentials: 'include',
|
|
26
|
+
});
|
|
27
|
+
if (!watchResp.ok) return { error: 'Watch HTML returned HTTP ' + watchResp.status };
|
|
28
|
+
|
|
29
|
+
const html = await watchResp.text();
|
|
30
|
+
const player = extractJsonAssignmentFromHtml(html, 'ytInitialPlayerResponse');
|
|
31
|
+
const yt = extractJsonAssignmentFromHtml(html, 'ytInitialData');
|
|
32
|
+
if (!player) return { error: 'ytInitialPlayerResponse not found in watch HTML' };
|
|
27
33
|
|
|
28
34
|
const details = player.videoDetails || {};
|
|
29
35
|
const microformat = player.microformat?.playerMicroformatRenderer || {};
|
|
36
|
+
const contents = yt?.contents?.twoColumnWatchNextResults?.results?.results?.contents || [];
|
|
30
37
|
|
|
31
|
-
// Try to get full description from
|
|
38
|
+
// Try to get full description from watch bootstrap data
|
|
32
39
|
let fullDescription = details.shortDescription || '';
|
|
33
40
|
try {
|
|
34
|
-
const contents = yt?.contents?.twoColumnWatchNextResults
|
|
35
|
-
?.results?.results?.contents;
|
|
36
41
|
if (contents) {
|
|
37
42
|
for (const c of contents) {
|
|
38
43
|
const desc = c.videoSecondaryInfoRenderer?.attributedDescription?.content;
|
|
@@ -44,8 +49,6 @@ cli({
|
|
|
44
49
|
// Get like count if available
|
|
45
50
|
let likes = '';
|
|
46
51
|
try {
|
|
47
|
-
const contents = yt?.contents?.twoColumnWatchNextResults
|
|
48
|
-
?.results?.results?.contents;
|
|
49
52
|
if (contents) {
|
|
50
53
|
for (const c of contents) {
|
|
51
54
|
const buttons = c.videoPrimaryInfoRenderer?.videoActions
|
|
@@ -73,8 +76,6 @@ cli({
|
|
|
73
76
|
// Get channel subscriber count if available
|
|
74
77
|
let subscribers = '';
|
|
75
78
|
try {
|
|
76
|
-
const contents = yt?.contents?.twoColumnWatchNextResults
|
|
77
|
-
?.results?.results?.contents;
|
|
78
79
|
if (contents) {
|
|
79
80
|
for (const c of contents) {
|
|
80
81
|
const owner = c.videoSecondaryInfoRenderer?.owner
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { ensureZsxqAuth, ensureZsxqPage, fetchFirstJson, getTopicText, getTopicAuthor, getTopicUrl, } from './utils.js';
|
|
3
|
+
cli({
|
|
4
|
+
site: 'zsxq',
|
|
5
|
+
name: 'dynamics',
|
|
6
|
+
description: '获取所有星球的最新动态',
|
|
7
|
+
domain: 'wx.zsxq.com',
|
|
8
|
+
strategy: Strategy.COOKIE,
|
|
9
|
+
browser: true,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of dynamics to return' },
|
|
12
|
+
],
|
|
13
|
+
columns: ['time', 'group', 'author', 'title', 'comments', 'likes', 'url'],
|
|
14
|
+
func: async (page, kwargs) => {
|
|
15
|
+
await ensureZsxqPage(page);
|
|
16
|
+
await ensureZsxqAuth(page);
|
|
17
|
+
const limit = Math.max(1, Number(kwargs.limit) || 20);
|
|
18
|
+
const { data } = await fetchFirstJson(page, [
|
|
19
|
+
`https://api.zsxq.com/v2/dynamics?scope=general&count=${limit}`,
|
|
20
|
+
]);
|
|
21
|
+
const respData = data?.resp_data || data;
|
|
22
|
+
const dynamics = respData?.dynamics || [];
|
|
23
|
+
return dynamics.slice(0, limit).map((d) => {
|
|
24
|
+
const topic = d.topic;
|
|
25
|
+
if (!topic) {
|
|
26
|
+
return {
|
|
27
|
+
time: d.create_time || '',
|
|
28
|
+
group: '',
|
|
29
|
+
author: '',
|
|
30
|
+
title: `[${d.action || 'unknown'}]`,
|
|
31
|
+
comments: 0,
|
|
32
|
+
likes: 0,
|
|
33
|
+
url: '',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
time: d.create_time || topic.create_time || '',
|
|
38
|
+
group: topic.group?.name || '',
|
|
39
|
+
author: getTopicAuthor(topic),
|
|
40
|
+
title: getTopicText(topic).slice(0, 120),
|
|
41
|
+
comments: topic.comments_count ?? 0,
|
|
42
|
+
likes: topic.likes_count ?? 0,
|
|
43
|
+
url: getTopicUrl(topic.topic_id),
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { ensureZsxqAuth, ensureZsxqPage, fetchFirstJson, getGroupsFromResponse, } from './utils.js';
|
|
3
|
+
cli({
|
|
4
|
+
site: 'zsxq',
|
|
5
|
+
name: 'groups',
|
|
6
|
+
description: '列出当前账号加入的星球',
|
|
7
|
+
domain: 'wx.zsxq.com',
|
|
8
|
+
strategy: Strategy.COOKIE,
|
|
9
|
+
browser: true,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'limit', type: 'int', default: 50, help: 'Number of groups to return' },
|
|
12
|
+
],
|
|
13
|
+
columns: ['group_id', 'name', 'category', 'members', 'topics', 'joined_at', 'url'],
|
|
14
|
+
func: async (page, kwargs) => {
|
|
15
|
+
await ensureZsxqPage(page);
|
|
16
|
+
await ensureZsxqAuth(page);
|
|
17
|
+
const limit = Math.max(1, Number(kwargs.limit) || 50);
|
|
18
|
+
const { data } = await fetchFirstJson(page, [
|
|
19
|
+
`https://api.zsxq.com/v2/groups`,
|
|
20
|
+
]);
|
|
21
|
+
return getGroupsFromResponse(data).slice(0, limit).map((group) => ({
|
|
22
|
+
group_id: group.group_id ?? '',
|
|
23
|
+
name: group.name || '',
|
|
24
|
+
category: group.category?.title || '',
|
|
25
|
+
members: group.statistics?.subscriptions_count ?? 0,
|
|
26
|
+
topics: group.statistics?.topics_count ?? 0,
|
|
27
|
+
joined_at: group.user_specific?.join_time || '',
|
|
28
|
+
valid_until: group.user_specific?.validity?.end_time || '',
|
|
29
|
+
url: group.group_id ? `https://wx.zsxq.com/group/${group.group_id}` : 'https://wx.zsxq.com',
|
|
30
|
+
}));
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { getActiveGroupId, ensureZsxqAuth, ensureZsxqPage, fetchFirstJson, getGroupsFromResponse, getTopicsFromResponse, toTopicRow, } from './utils.js';
|
|
3
|
+
cli({
|
|
4
|
+
site: 'zsxq',
|
|
5
|
+
name: 'search',
|
|
6
|
+
description: '搜索星球内容',
|
|
7
|
+
domain: 'wx.zsxq.com',
|
|
8
|
+
strategy: Strategy.COOKIE,
|
|
9
|
+
browser: true,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'keyword', required: true, positional: true, help: 'Search keyword' },
|
|
12
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of results to return' },
|
|
13
|
+
{ name: 'group_id', help: 'Optional group id; defaults to the active group in Chrome' },
|
|
14
|
+
],
|
|
15
|
+
columns: ['topic_id', 'group', 'author', 'title', 'comments', 'likes', 'time', 'url'],
|
|
16
|
+
func: async (page, kwargs) => {
|
|
17
|
+
await ensureZsxqPage(page);
|
|
18
|
+
await ensureZsxqAuth(page);
|
|
19
|
+
const keyword = String(kwargs.keyword || '').trim();
|
|
20
|
+
const limit = Math.max(1, Number(kwargs.limit) || 20);
|
|
21
|
+
const groupId = String(kwargs.group_id || await getActiveGroupId(page));
|
|
22
|
+
const query = encodeURIComponent(keyword);
|
|
23
|
+
// Resolve group name from groups API
|
|
24
|
+
let groupName = groupId;
|
|
25
|
+
try {
|
|
26
|
+
const { data: groupsData } = await fetchFirstJson(page, [
|
|
27
|
+
`https://api.zsxq.com/v2/groups`,
|
|
28
|
+
]);
|
|
29
|
+
const groups = getGroupsFromResponse(groupsData);
|
|
30
|
+
const found = groups.find(g => String(g.group_id) === groupId);
|
|
31
|
+
if (found?.name)
|
|
32
|
+
groupName = found.name;
|
|
33
|
+
}
|
|
34
|
+
catch { /* ignore */ }
|
|
35
|
+
const { data } = await fetchFirstJson(page, [
|
|
36
|
+
`https://api.zsxq.com/v2/search/groups/${groupId}/topics?keyword=${query}&count=${limit}`,
|
|
37
|
+
]);
|
|
38
|
+
return getTopicsFromResponse(data).slice(0, limit).map((topic) => ({
|
|
39
|
+
...toTopicRow(topic),
|
|
40
|
+
group: groupName,
|
|
41
|
+
}));
|
|
42
|
+
},
|
|
43
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './search.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '../../registry.js';
|
|
3
|
+
import './search.js';
|
|
4
|
+
describe('zsxq search command', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.restoreAllMocks();
|
|
7
|
+
});
|
|
8
|
+
it('requires an explicit group_id when there is no active group context', async () => {
|
|
9
|
+
const command = getRegistry().get('zsxq/search');
|
|
10
|
+
expect(command?.func).toBeTypeOf('function');
|
|
11
|
+
const mockPage = {
|
|
12
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
13
|
+
evaluate: vi.fn()
|
|
14
|
+
.mockResolvedValueOnce(true)
|
|
15
|
+
.mockResolvedValueOnce(null),
|
|
16
|
+
};
|
|
17
|
+
await expect(command.func(mockPage, { keyword: 'opencli', limit: 20 })).rejects.toMatchObject({
|
|
18
|
+
code: 'ARGUMENT',
|
|
19
|
+
message: 'Cannot determine active group_id',
|
|
20
|
+
});
|
|
21
|
+
expect(mockPage.goto).toHaveBeenCalledWith('https://wx.zsxq.com');
|
|
22
|
+
expect(mockPage.evaluate).toHaveBeenCalledTimes(2);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { CliError } from '../../errors.js';
|
|
3
|
+
import { browserJsonRequest, ensureZsxqAuth, ensureZsxqPage, fetchFirstJson, getCommentsFromResponse, getTopicFromResponse, getTopicUrl, summarizeComments, toTopicRow, } from './utils.js';
|
|
4
|
+
cli({
|
|
5
|
+
site: 'zsxq',
|
|
6
|
+
name: 'topic',
|
|
7
|
+
description: '获取单个话题详情和评论',
|
|
8
|
+
domain: 'wx.zsxq.com',
|
|
9
|
+
strategy: Strategy.COOKIE,
|
|
10
|
+
browser: true,
|
|
11
|
+
args: [
|
|
12
|
+
{ name: 'id', required: true, positional: true, help: 'Topic ID' },
|
|
13
|
+
{ name: 'comment_limit', type: 'int', default: 20, help: 'Number of comments to fetch' },
|
|
14
|
+
],
|
|
15
|
+
columns: ['topic_id', 'type', 'author', 'title', 'comments', 'likes', 'comment_preview', 'url'],
|
|
16
|
+
func: async (page, kwargs) => {
|
|
17
|
+
await ensureZsxqPage(page);
|
|
18
|
+
await ensureZsxqAuth(page);
|
|
19
|
+
const topicId = String(kwargs.id);
|
|
20
|
+
const commentLimit = Math.max(1, Number(kwargs.comment_limit) || 20);
|
|
21
|
+
const detailUrl = `https://api.zsxq.com/v2/topics/${topicId}`;
|
|
22
|
+
const detailResp = await browserJsonRequest(page, detailUrl);
|
|
23
|
+
if (detailResp.status === 404) {
|
|
24
|
+
throw new CliError('NOT_FOUND', `Topic ${topicId} not found`);
|
|
25
|
+
}
|
|
26
|
+
if (!detailResp.ok) {
|
|
27
|
+
throw new CliError('FETCH_ERROR', detailResp.error || `Failed to fetch topic ${topicId}`, `Checked endpoint: ${detailUrl}`);
|
|
28
|
+
}
|
|
29
|
+
const commentsResp = await fetchFirstJson(page, [
|
|
30
|
+
`https://api.zsxq.com/v2/topics/${topicId}/comments?sort=asc&count=${commentLimit}`,
|
|
31
|
+
]);
|
|
32
|
+
const topic = getTopicFromResponse(detailResp.data);
|
|
33
|
+
if (!topic)
|
|
34
|
+
throw new CliError('NOT_FOUND', `Topic ${topicId} not found`);
|
|
35
|
+
const comments = getCommentsFromResponse(commentsResp.data);
|
|
36
|
+
const row = toTopicRow({
|
|
37
|
+
...topic,
|
|
38
|
+
comments,
|
|
39
|
+
comments_count: topic.comments_count ?? comments.length,
|
|
40
|
+
});
|
|
41
|
+
return [{
|
|
42
|
+
...row,
|
|
43
|
+
comment_preview: summarizeComments(comments, 5),
|
|
44
|
+
url: getTopicUrl(topic.topic_id ?? topicId),
|
|
45
|
+
}];
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './topic.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '../../registry.js';
|
|
3
|
+
import './topic.js';
|
|
4
|
+
describe('zsxq topic command', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.restoreAllMocks();
|
|
7
|
+
});
|
|
8
|
+
it('maps topic detail 404 responses to NOT_FOUND before fetching comments', async () => {
|
|
9
|
+
const command = getRegistry().get('zsxq/topic');
|
|
10
|
+
expect(command?.func).toBeTypeOf('function');
|
|
11
|
+
const mockPage = {
|
|
12
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
13
|
+
evaluate: vi.fn()
|
|
14
|
+
.mockResolvedValueOnce(true)
|
|
15
|
+
.mockResolvedValueOnce({
|
|
16
|
+
ok: true,
|
|
17
|
+
status: 404,
|
|
18
|
+
url: 'https://api.zsxq.com/v2/topics/404',
|
|
19
|
+
data: null,
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
await expect(command.func(mockPage, { id: '404', comment_limit: 20 })).rejects.toMatchObject({
|
|
23
|
+
code: 'NOT_FOUND',
|
|
24
|
+
message: 'Topic 404 not found',
|
|
25
|
+
});
|
|
26
|
+
expect(mockPage.goto).toHaveBeenCalledWith('https://wx.zsxq.com');
|
|
27
|
+
expect(mockPage.evaluate).toHaveBeenCalledTimes(2);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { getActiveGroupId, ensureZsxqAuth, ensureZsxqPage, fetchFirstJson, getTopicsFromResponse, toTopicRow, } from './utils.js';
|
|
3
|
+
cli({
|
|
4
|
+
site: 'zsxq',
|
|
5
|
+
name: 'topics',
|
|
6
|
+
description: '获取当前星球的话题列表',
|
|
7
|
+
domain: 'wx.zsxq.com',
|
|
8
|
+
strategy: Strategy.COOKIE,
|
|
9
|
+
browser: true,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of topics to return' },
|
|
12
|
+
{ name: 'group_id', help: 'Optional group id; defaults to the active group in Chrome' },
|
|
13
|
+
],
|
|
14
|
+
columns: ['topic_id', 'type', 'author', 'title', 'comments', 'likes', 'time', 'url'],
|
|
15
|
+
func: async (page, kwargs) => {
|
|
16
|
+
await ensureZsxqPage(page);
|
|
17
|
+
await ensureZsxqAuth(page);
|
|
18
|
+
const limit = Math.max(1, Number(kwargs.limit) || 20);
|
|
19
|
+
const groupId = String(kwargs.group_id || await getActiveGroupId(page));
|
|
20
|
+
const { data } = await fetchFirstJson(page, [
|
|
21
|
+
`https://api.zsxq.com/v2/groups/${groupId}/topics?scope=all&count=${limit}`,
|
|
22
|
+
]);
|
|
23
|
+
return getTopicsFromResponse(data).slice(0, limit).map(toTopicRow);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './topics.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '../../registry.js';
|
|
3
|
+
import './topics.js';
|
|
4
|
+
describe('zsxq topics command', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.restoreAllMocks();
|
|
7
|
+
});
|
|
8
|
+
it('requires an explicit group_id when there is no active group context', async () => {
|
|
9
|
+
const command = getRegistry().get('zsxq/topics');
|
|
10
|
+
expect(command?.func).toBeTypeOf('function');
|
|
11
|
+
const mockPage = {
|
|
12
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
13
|
+
evaluate: vi.fn()
|
|
14
|
+
.mockResolvedValueOnce(true)
|
|
15
|
+
.mockResolvedValueOnce(null),
|
|
16
|
+
};
|
|
17
|
+
await expect(command.func(mockPage, { limit: 20 })).rejects.toMatchObject({
|
|
18
|
+
code: 'ARGUMENT',
|
|
19
|
+
message: 'Cannot determine active group_id',
|
|
20
|
+
});
|
|
21
|
+
expect(mockPage.goto).toHaveBeenCalledWith('https://wx.zsxq.com');
|
|
22
|
+
expect(mockPage.evaluate).toHaveBeenCalledTimes(2);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { IPage } from '../../types.js';
|
|
2
|
+
export interface ZsxqUser {
|
|
3
|
+
user_id?: number;
|
|
4
|
+
name?: string;
|
|
5
|
+
avatar_url?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ZsxqGroup {
|
|
8
|
+
group_id?: number;
|
|
9
|
+
name?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
background_url?: string;
|
|
12
|
+
owner?: ZsxqUser;
|
|
13
|
+
statistics?: {
|
|
14
|
+
topics_count?: number;
|
|
15
|
+
answers_count?: number;
|
|
16
|
+
comments_count?: number;
|
|
17
|
+
likes_count?: number;
|
|
18
|
+
subscriptions_count?: number;
|
|
19
|
+
};
|
|
20
|
+
category?: {
|
|
21
|
+
title?: string;
|
|
22
|
+
};
|
|
23
|
+
user_specific?: {
|
|
24
|
+
join_time?: string;
|
|
25
|
+
validity?: {
|
|
26
|
+
end_time?: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface ZsxqComment {
|
|
31
|
+
comment_id?: number;
|
|
32
|
+
create_time?: string;
|
|
33
|
+
text?: string;
|
|
34
|
+
owner?: ZsxqUser;
|
|
35
|
+
likes_count?: number;
|
|
36
|
+
rewards_count?: number;
|
|
37
|
+
repliee?: ZsxqUser;
|
|
38
|
+
}
|
|
39
|
+
export interface ZsxqTopic {
|
|
40
|
+
topic_id?: number;
|
|
41
|
+
create_time?: string;
|
|
42
|
+
comments_count?: number;
|
|
43
|
+
likes_count?: number;
|
|
44
|
+
readers_count?: number;
|
|
45
|
+
reading_count?: number;
|
|
46
|
+
rewards_count?: number;
|
|
47
|
+
title?: string;
|
|
48
|
+
type?: string;
|
|
49
|
+
group?: ZsxqGroup;
|
|
50
|
+
owner?: ZsxqUser;
|
|
51
|
+
user_specific?: Record<string, unknown>;
|
|
52
|
+
talk?: {
|
|
53
|
+
owner?: ZsxqUser;
|
|
54
|
+
text?: string;
|
|
55
|
+
};
|
|
56
|
+
question?: {
|
|
57
|
+
owner?: ZsxqUser;
|
|
58
|
+
text?: string;
|
|
59
|
+
};
|
|
60
|
+
answer?: {
|
|
61
|
+
owner?: ZsxqUser;
|
|
62
|
+
text?: string;
|
|
63
|
+
};
|
|
64
|
+
task?: {
|
|
65
|
+
owner?: ZsxqUser;
|
|
66
|
+
text?: string;
|
|
67
|
+
};
|
|
68
|
+
solution?: {
|
|
69
|
+
owner?: ZsxqUser;
|
|
70
|
+
text?: string;
|
|
71
|
+
};
|
|
72
|
+
show_comments?: ZsxqComment[];
|
|
73
|
+
comments?: ZsxqComment[];
|
|
74
|
+
}
|
|
75
|
+
export interface BrowserFetchResult {
|
|
76
|
+
ok: boolean;
|
|
77
|
+
url?: string;
|
|
78
|
+
status?: number;
|
|
79
|
+
error?: string;
|
|
80
|
+
data?: unknown;
|
|
81
|
+
}
|
|
82
|
+
export declare function ensureZsxqPage(page: IPage): Promise<void>;
|
|
83
|
+
export declare function ensureZsxqAuth(page: IPage): Promise<void>;
|
|
84
|
+
export declare function getCookieValue(page: IPage, name: string): Promise<string | undefined>;
|
|
85
|
+
export declare function getActiveGroupId(page: IPage): Promise<string>;
|
|
86
|
+
export declare function browserJsonRequest(page: IPage, path: string): Promise<BrowserFetchResult>;
|
|
87
|
+
export declare function fetchFirstJson(page: IPage, paths: string[]): Promise<BrowserFetchResult>;
|
|
88
|
+
export declare function unwrapRespData<T>(payload: unknown): T;
|
|
89
|
+
export declare function getTopicsFromResponse(payload: unknown): ZsxqTopic[];
|
|
90
|
+
export declare function getCommentsFromResponse(payload: unknown): ZsxqComment[];
|
|
91
|
+
export declare function getGroupsFromResponse(payload: unknown): ZsxqGroup[];
|
|
92
|
+
export declare function getTopicFromResponse(payload: unknown): ZsxqTopic | null;
|
|
93
|
+
export declare function getTopicAuthor(topic: ZsxqTopic): string;
|
|
94
|
+
export declare function getTopicText(topic: ZsxqTopic): string;
|
|
95
|
+
export declare function getTopicUrl(topicId: number | string | undefined): string;
|
|
96
|
+
export declare function summarizeComments(comments: ZsxqComment[], limit?: number): string;
|
|
97
|
+
export declare function toTopicRow(topic: ZsxqTopic): Record<string, unknown>;
|