@jackwener/opencli 1.7.3 → 1.7.5
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 +81 -59
- package/README.zh-CN.md +93 -67
- package/cli-manifest.json +5015 -2975
- package/clis/antigravity/serve.js +71 -25
- package/clis/baidu-scholar/search.js +87 -0
- package/clis/baidu-scholar/search.test.js +23 -0
- package/clis/bilibili/favorite.js +18 -13
- package/clis/binance/depth.js +3 -4
- package/clis/boss/utils.js +2 -3
- package/clis/chatgpt-app/ax.js +6 -3
- package/clis/deepseek/ask.js +74 -0
- package/clis/deepseek/history.js +25 -0
- package/clis/deepseek/new.js +20 -0
- package/clis/deepseek/read.js +22 -0
- package/clis/deepseek/status.js +24 -0
- package/clis/deepseek/utils.js +208 -0
- package/clis/douban/search.js +1 -0
- package/clis/douban/search.test.js +11 -0
- package/clis/douban/subject.js +20 -93
- package/clis/douban/subject.test.js +11 -0
- package/clis/douban/utils.js +250 -8
- package/clis/douban/utils.test.js +179 -4
- package/clis/doubao/utils.js +319 -130
- package/clis/doubao/utils.test.js +241 -2
- package/clis/eastmoney/_secid.js +78 -0
- package/clis/eastmoney/announcement.js +52 -0
- package/clis/eastmoney/convertible.js +73 -0
- package/clis/eastmoney/etf.js +65 -0
- package/clis/eastmoney/holders.js +78 -0
- package/clis/eastmoney/hot-rank.js +50 -0
- package/clis/eastmoney/hot-rank.test.js +59 -0
- package/clis/eastmoney/index-board.js +96 -0
- package/clis/eastmoney/kline.js +87 -0
- package/clis/eastmoney/kuaixun.js +54 -0
- package/clis/eastmoney/longhu.js +67 -0
- package/clis/eastmoney/money-flow.js +78 -0
- package/clis/eastmoney/northbound.js +57 -0
- package/clis/eastmoney/quote.js +107 -0
- package/clis/eastmoney/rank.js +94 -0
- package/clis/eastmoney/sectors.js +76 -0
- package/clis/google-scholar/search.js +58 -0
- package/clis/google-scholar/search.test.js +23 -0
- package/clis/gov-law/commands.test.js +39 -0
- package/clis/gov-law/recent.js +22 -0
- package/clis/gov-law/search.js +41 -0
- package/clis/gov-law/shared.js +51 -0
- package/clis/gov-policy/commands.test.js +27 -0
- package/clis/gov-policy/recent.js +47 -0
- package/clis/gov-policy/search.js +48 -0
- package/clis/grok/image.test.ts +107 -0
- package/clis/grok/image.ts +356 -0
- package/clis/nowcoder/companies.js +23 -0
- package/clis/nowcoder/creators.js +27 -0
- package/clis/nowcoder/detail.js +61 -0
- package/clis/nowcoder/experience.js +36 -0
- package/clis/nowcoder/hot.js +24 -0
- package/clis/nowcoder/jobs.js +21 -0
- package/clis/nowcoder/notifications.js +29 -0
- package/clis/nowcoder/papers.js +40 -0
- package/clis/nowcoder/practice.js +37 -0
- package/clis/nowcoder/recommend.js +30 -0
- package/clis/nowcoder/referral.js +39 -0
- package/clis/nowcoder/salary.js +40 -0
- package/clis/nowcoder/search.js +49 -0
- package/clis/nowcoder/suggest.js +33 -0
- package/clis/nowcoder/topics.js +27 -0
- package/clis/nowcoder/trending.js +25 -0
- package/clis/tdx/hot-rank.js +47 -0
- package/clis/tdx/hot-rank.test.js +59 -0
- package/clis/ths/hot-rank.js +49 -0
- package/clis/ths/hot-rank.test.js +64 -0
- package/clis/twitter/bookmarks.js +2 -1
- package/clis/twitter/list-add.js +337 -0
- package/clis/twitter/list-add.test.js +15 -0
- package/clis/twitter/list-remove.js +297 -0
- package/clis/twitter/list-remove.test.js +14 -0
- package/clis/twitter/list-tweets.js +185 -0
- package/clis/twitter/list-tweets.test.js +108 -0
- package/clis/twitter/lists.js +134 -47
- package/clis/twitter/lists.test.js +105 -38
- package/clis/uiverse/_shared.js +368 -0
- package/clis/uiverse/_shared.test.js +55 -0
- package/clis/uiverse/code.js +47 -0
- package/clis/uiverse/preview.js +71 -0
- package/clis/wanfang/search.js +66 -0
- package/clis/wanfang/search.test.js +23 -0
- package/clis/web/read.js +1 -1
- package/clis/weixin/download.js +3 -2
- package/clis/xiaohongshu/comments.js +2 -2
- package/clis/xiaohongshu/comments.test.js +46 -25
- package/clis/xiaohongshu/download.js +6 -7
- package/clis/xiaohongshu/download.test.js +17 -5
- package/clis/xiaohongshu/note-helpers.js +46 -12
- package/clis/xiaohongshu/note.js +3 -5
- package/clis/xiaohongshu/note.test.js +52 -25
- package/clis/xiaohongshu/publish.js +149 -28
- package/clis/xiaohongshu/publish.test.js +319 -6
- package/clis/xiaoyuzhou/auth.js +303 -0
- package/clis/xiaoyuzhou/auth.test.js +124 -0
- package/clis/xiaoyuzhou/download.js +53 -0
- package/clis/xiaoyuzhou/download.test.js +135 -0
- package/clis/xiaoyuzhou/episode.js +9 -4
- package/clis/xiaoyuzhou/podcast-episodes.js +15 -11
- package/clis/xiaoyuzhou/podcast.js +9 -4
- package/clis/xiaoyuzhou/transcript.js +76 -0
- package/clis/xiaoyuzhou/transcript.test.js +195 -0
- package/clis/xiaoyuzhou/utils.js +0 -40
- package/clis/xiaoyuzhou/utils.test.js +15 -75
- package/clis/youtube/feed.js +120 -0
- package/clis/youtube/history.js +118 -0
- package/clis/youtube/like.js +62 -0
- package/clis/youtube/playlist.js +97 -0
- package/clis/youtube/subscribe.js +71 -0
- package/clis/youtube/subscriptions.js +57 -0
- package/clis/youtube/unlike.js +62 -0
- package/clis/youtube/unsubscribe.js +71 -0
- package/clis/youtube/utils.js +122 -0
- package/clis/youtube/utils.test.js +32 -1
- package/clis/youtube/watch-later.js +76 -0
- package/clis/zsxq/dynamics.js +1 -1
- package/clis/zsxq/utils.js +6 -3
- package/clis/zsxq/utils.test.js +31 -0
- package/dist/src/browser/base-page.d.ts +1 -1
- package/dist/src/browser/base-page.js +25 -5
- package/dist/src/browser/bridge.d.ts +3 -0
- package/dist/src/browser/bridge.js +52 -15
- package/dist/src/browser/cdp.js +2 -1
- package/dist/src/browser/daemon-client.d.ts +7 -4
- package/dist/src/browser/daemon-client.js +6 -1
- package/dist/src/browser/daemon-client.test.js +40 -1
- package/dist/src/browser/dom-snapshot.js +20 -3
- package/dist/src/browser/page.d.ts +18 -5
- package/dist/src/browser/page.js +96 -15
- package/dist/src/browser/page.test.js +158 -1
- package/dist/src/browser/target-errors.d.ts +23 -0
- package/dist/src/browser/target-errors.js +29 -0
- package/dist/src/browser/target-errors.test.js +61 -0
- package/dist/src/browser/target-resolver.d.ts +57 -0
- package/dist/src/browser/target-resolver.js +298 -0
- package/dist/src/browser/target-resolver.test.js +43 -0
- package/dist/src/browser.test.js +38 -1
- package/dist/src/cli.js +272 -187
- package/dist/src/cli.test.js +167 -90
- package/dist/src/commanderAdapter.d.ts +0 -1
- package/dist/src/commanderAdapter.js +2 -16
- package/dist/src/commanderAdapter.test.js +1 -1
- package/dist/src/commands/daemon.d.ts +4 -2
- package/dist/src/commands/daemon.js +22 -2
- package/dist/src/commands/daemon.test.js +65 -2
- package/dist/src/completion-shared.js +2 -5
- package/dist/src/daemon.js +10 -0
- package/dist/src/doctor.d.ts +1 -0
- package/dist/src/doctor.js +32 -9
- package/dist/src/doctor.test.js +28 -12
- package/dist/src/download/article-download.d.ts +1 -0
- package/dist/src/download/article-download.js +3 -0
- package/dist/src/download/article-download.test.js +39 -0
- package/dist/src/external-clis.yaml +2 -2
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +3 -3
- package/dist/src/output.js +1 -5
- package/dist/src/output.test.js +0 -21
- package/dist/src/pipeline/steps/transform.js +1 -1
- package/dist/src/pipeline/template.d.ts +1 -0
- package/dist/src/pipeline/template.js +11 -3
- package/dist/src/pipeline/template.test.js +3 -0
- package/dist/src/pipeline/transform.test.js +14 -0
- package/dist/src/plugin.d.ts +8 -9
- package/dist/src/plugin.js +24 -28
- package/dist/src/plugin.test.js +16 -60
- package/dist/src/registry.d.ts +1 -0
- package/dist/src/registry.js +3 -2
- package/dist/src/registry.test.js +22 -0
- package/dist/src/types.d.ts +15 -6
- package/package.json +1 -1
- package/clis/twitter/lists-parser.js +0 -77
- package/clis/twitter/lists.d.ts +0 -5
- package/dist/src/cascade.d.ts +0 -46
- package/dist/src/cascade.js +0 -135
- package/dist/src/explore.d.ts +0 -99
- package/dist/src/explore.js +0 -402
- package/dist/src/generate-verified.d.ts +0 -105
- package/dist/src/generate-verified.js +0 -696
- package/dist/src/generate-verified.test.js +0 -925
- package/dist/src/generate.d.ts +0 -46
- package/dist/src/generate.js +0 -117
- package/dist/src/record.d.ts +0 -96
- package/dist/src/record.js +0 -657
- package/dist/src/record.test.js +0 -293
- package/dist/src/skill-generate.d.ts +0 -30
- package/dist/src/skill-generate.js +0 -75
- package/dist/src/skill-generate.test.js +0 -173
- package/dist/src/synthesize.d.ts +0 -97
- package/dist/src/synthesize.js +0 -208
- /package/dist/src/{generate-verified.test.d.ts → browser/target-errors.test.d.ts} +0 -0
- /package/dist/src/{record.test.d.ts → browser/target-resolver.test.d.ts} +0 -0
- /package/dist/src/{skill-generate.test.d.ts → download/article-download.test.d.ts} +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
cli({
|
|
4
|
+
site: 'nowcoder',
|
|
5
|
+
name: 'recommend',
|
|
6
|
+
description: 'Recommended feed',
|
|
7
|
+
domain: 'www.nowcoder.com',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'page', type: 'int', default: 1, help: 'Page number' },
|
|
12
|
+
{ name: 'limit', type: 'int', default: 15, help: 'Number of items' },
|
|
13
|
+
],
|
|
14
|
+
columns: ['rank', 'title', 'author', 'likes', 'comments', 'views', 'id'],
|
|
15
|
+
pipeline: [
|
|
16
|
+
{ fetch: { url: 'https://gw-c.nowcoder.com/api/sparta/home/recommend?page=${{ args.page }}&size=${{ args.limit }}' } },
|
|
17
|
+
{ select: 'data.records' },
|
|
18
|
+
{ map: {
|
|
19
|
+
rank: '${{ index + 1 }}',
|
|
20
|
+
title: `\${{ item.momentData?.title || item.longContentData?.title || item.contentData?.title || '' }}`,
|
|
21
|
+
author: `\${{ item.userBrief?.nickname || '' }}`,
|
|
22
|
+
likes: '${{ item.frequencyData?.likeCnt || 0 }}',
|
|
23
|
+
comments: '${{ item.frequencyData?.commentCnt || 0 }}',
|
|
24
|
+
views: '${{ item.frequencyData?.viewCnt || 0 }}',
|
|
25
|
+
id: `\${{ item.momentData?.uuid || item.longContentData?.uuid || item.contentData?.uuid || item.contentId || '' }}`,
|
|
26
|
+
} },
|
|
27
|
+
{ filter: 'item.title' },
|
|
28
|
+
{ limit: '${{ args.limit }}' },
|
|
29
|
+
],
|
|
30
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { cli } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
cli({
|
|
4
|
+
site: 'nowcoder',
|
|
5
|
+
name: 'referral',
|
|
6
|
+
description: 'Internal referral posts',
|
|
7
|
+
domain: 'www.nowcoder.com',
|
|
8
|
+
args: [
|
|
9
|
+
{ name: 'page', type: 'int', default: 1, help: 'Page number' },
|
|
10
|
+
{ name: 'limit', type: 'int', default: 15, help: 'Number of items' },
|
|
11
|
+
],
|
|
12
|
+
columns: ['rank', 'title', 'author', 'school', 'likes', 'comments', 'views', 'id'],
|
|
13
|
+
pipeline: [
|
|
14
|
+
{ navigate: 'https://www.nowcoder.com' },
|
|
15
|
+
{ evaluate: `(async () => {
|
|
16
|
+
const page = \${{ args.page }};
|
|
17
|
+
const limit = \${{ args.limit }};
|
|
18
|
+
const r = await fetch('https://gw-c.nowcoder.com/api/sparta/home/tab/content?tabId=861&categoryType=1&pageNo=' + page + '&pageSize=' + limit, {credentials: 'include'});
|
|
19
|
+
const d = await r.json();
|
|
20
|
+
if (!d.success) throw new Error(d.msg || 'API failed');
|
|
21
|
+
return (d.data?.records || []).map((item, i) => {
|
|
22
|
+
const content = item.contentData || item.momentData || {};
|
|
23
|
+
return {
|
|
24
|
+
rank: i + 1,
|
|
25
|
+
title: content.title || '',
|
|
26
|
+
author: item.userBrief?.nickname || '',
|
|
27
|
+
school: item.userBrief?.educationInfo || '',
|
|
28
|
+
likes: item.frequencyData?.likeCnt || 0,
|
|
29
|
+
comments: item.frequencyData?.commentCnt || 0,
|
|
30
|
+
views: item.frequencyData?.viewCnt || 0,
|
|
31
|
+
id: item.momentData?.uuid || item.contentData?.uuid || item.contentId || '',
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
})()
|
|
35
|
+
` },
|
|
36
|
+
{ filter: 'item.title' },
|
|
37
|
+
{ limit: '${{ args.limit }}' },
|
|
38
|
+
],
|
|
39
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { cli } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
cli({
|
|
4
|
+
site: 'nowcoder',
|
|
5
|
+
name: 'salary',
|
|
6
|
+
description: 'Salary disclosure posts',
|
|
7
|
+
domain: 'www.nowcoder.com',
|
|
8
|
+
args: [
|
|
9
|
+
{ name: 'page', type: 'int', default: 1, help: 'Page number' },
|
|
10
|
+
{ name: 'limit', type: 'int', default: 15, help: 'Number of items' },
|
|
11
|
+
],
|
|
12
|
+
columns: ['rank', 'title', 'author', 'school', 'likes', 'comments', 'views', 'id'],
|
|
13
|
+
pipeline: [
|
|
14
|
+
{ navigate: 'https://www.nowcoder.com' },
|
|
15
|
+
{ evaluate: `(async () => {
|
|
16
|
+
const page = \${{ args.page }};
|
|
17
|
+
const limit = \${{ args.limit }};
|
|
18
|
+
const r = await fetch('https://gw-c.nowcoder.com/api/sparta/home/tab/content?tabId=858&categoryType=1&pageNo=' + page + '&pageSize=' + limit, {credentials: 'include'});
|
|
19
|
+
const d = await r.json();
|
|
20
|
+
if (!d.success) throw new Error(d.msg || 'API failed');
|
|
21
|
+
return (d.data?.records || []).map((item, i) => {
|
|
22
|
+
const moment = item.momentData || {};
|
|
23
|
+
const content = item.contentData || {};
|
|
24
|
+
return {
|
|
25
|
+
rank: i + 1,
|
|
26
|
+
title: moment.title || content.title || '',
|
|
27
|
+
author: item.userBrief?.nickname || '',
|
|
28
|
+
school: item.userBrief?.educationInfo || '',
|
|
29
|
+
likes: item.frequencyData?.likeCnt || 0,
|
|
30
|
+
comments: item.frequencyData?.commentCnt || 0,
|
|
31
|
+
views: item.frequencyData?.viewCnt || 0,
|
|
32
|
+
id: moment.uuid || content.uuid || item.contentId || '',
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
})()
|
|
36
|
+
` },
|
|
37
|
+
{ filter: 'item.title' },
|
|
38
|
+
{ limit: '${{ args.limit }}' },
|
|
39
|
+
],
|
|
40
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { cli } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
cli({
|
|
4
|
+
site: 'nowcoder',
|
|
5
|
+
name: 'search',
|
|
6
|
+
description: 'Full-text search',
|
|
7
|
+
domain: 'www.nowcoder.com',
|
|
8
|
+
args: [
|
|
9
|
+
{ name: 'query', positional: true, required: true, help: 'Search keyword' },
|
|
10
|
+
{ name: 'type', type: 'str', default: 'all', help: 'Search type (all/post/question/user/job)' },
|
|
11
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Number of results' },
|
|
12
|
+
],
|
|
13
|
+
columns: ['rank', 'title', 'author', 'school', 'content', 'id'],
|
|
14
|
+
pipeline: [
|
|
15
|
+
{ navigate: 'https://www.nowcoder.com' },
|
|
16
|
+
{ evaluate: `(async () => {
|
|
17
|
+
const query = \${{ args.query | json }};
|
|
18
|
+
const type = \${{ args.type | json }};
|
|
19
|
+
const limit = \${{ args.limit }};
|
|
20
|
+
const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/ /g, ' ').trim();
|
|
21
|
+
const r = await fetch('https://gw-c.nowcoder.com/api/sparta/pc/search', {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
credentials: 'include',
|
|
24
|
+
headers: {'Content-Type': 'application/json'},
|
|
25
|
+
body: JSON.stringify({query, type, page: 1, pageSize: limit})
|
|
26
|
+
});
|
|
27
|
+
const d = await r.json();
|
|
28
|
+
if (!d.success) throw new Error(d.msg || 'search failed');
|
|
29
|
+
return (d.data?.records || []).map((item, i) => {
|
|
30
|
+
const data = item.data || {};
|
|
31
|
+
const moment = data.momentData || {};
|
|
32
|
+
const contentData = data.contentData || {};
|
|
33
|
+
const user = data.userBrief || {};
|
|
34
|
+
const uuid = moment.uuid || contentData.uuid || '';
|
|
35
|
+
const id = data.contentId || '';
|
|
36
|
+
return {
|
|
37
|
+
rank: i + 1,
|
|
38
|
+
title: moment.title || contentData.title || user.nickname || '',
|
|
39
|
+
author: user.nickname || '',
|
|
40
|
+
school: user.educationInfo || '',
|
|
41
|
+
content: strip(moment.content || contentData.content || ''),
|
|
42
|
+
id: uuid || id,
|
|
43
|
+
};
|
|
44
|
+
}).filter(r => r.title);
|
|
45
|
+
})()
|
|
46
|
+
` },
|
|
47
|
+
{ limit: '${{ args.limit }}' },
|
|
48
|
+
],
|
|
49
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { cli } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
cli({
|
|
4
|
+
site: 'nowcoder',
|
|
5
|
+
name: 'suggest',
|
|
6
|
+
description: 'Search suggestions',
|
|
7
|
+
domain: 'www.nowcoder.com',
|
|
8
|
+
args: [
|
|
9
|
+
{ name: 'query', positional: true, required: true, help: 'Search keyword' },
|
|
10
|
+
],
|
|
11
|
+
columns: ['rank', 'suggestion', 'type'],
|
|
12
|
+
pipeline: [
|
|
13
|
+
{ navigate: 'https://www.nowcoder.com' },
|
|
14
|
+
{ evaluate: `(async () => {
|
|
15
|
+
const query = \${{ args.query | json }};
|
|
16
|
+
const r = await fetch('https://gw-c.nowcoder.com/api/sparta/search/suggest', {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
credentials: 'include',
|
|
19
|
+
headers: {'Content-Type': 'application/json'},
|
|
20
|
+
body: JSON.stringify({query})
|
|
21
|
+
});
|
|
22
|
+
const d = await r.json();
|
|
23
|
+
if (!d.success) throw new Error(d.msg || 'suggest failed');
|
|
24
|
+
return (d.data?.records || []).map((item, i) => ({
|
|
25
|
+
rank: i + 1,
|
|
26
|
+
suggestion: item.name || '',
|
|
27
|
+
type: item.typeName || 'general',
|
|
28
|
+
}));
|
|
29
|
+
})()
|
|
30
|
+
` },
|
|
31
|
+
{ limit: '10' },
|
|
32
|
+
],
|
|
33
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
cli({
|
|
4
|
+
site: 'nowcoder',
|
|
5
|
+
name: 'topics',
|
|
6
|
+
description: 'Hot discussion topics',
|
|
7
|
+
domain: 'www.nowcoder.com',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Number of items' },
|
|
12
|
+
],
|
|
13
|
+
columns: ['rank', 'topic', 'views', 'posts', 'heat', 'id'],
|
|
14
|
+
pipeline: [
|
|
15
|
+
{ fetch: { url: 'https://gw-c.nowcoder.com/api/sparta/subject/hot-subject' } },
|
|
16
|
+
{ select: 'data.result' },
|
|
17
|
+
{ map: {
|
|
18
|
+
rank: '${{ index + 1 }}',
|
|
19
|
+
topic: '${{ item.content }}',
|
|
20
|
+
views: '${{ item.viewCount }}',
|
|
21
|
+
posts: '${{ item.momentCount }}',
|
|
22
|
+
heat: '${{ item.hotValue }}',
|
|
23
|
+
id: '${{ item.uuid || item.id || "" }}',
|
|
24
|
+
} },
|
|
25
|
+
{ limit: '${{ args.limit }}' },
|
|
26
|
+
],
|
|
27
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
cli({
|
|
4
|
+
site: 'nowcoder',
|
|
5
|
+
name: 'trending',
|
|
6
|
+
description: 'Trending posts',
|
|
7
|
+
domain: 'www.nowcoder.com',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Number of items' },
|
|
12
|
+
],
|
|
13
|
+
columns: ['rank', 'title', 'heat', 'id'],
|
|
14
|
+
pipeline: [
|
|
15
|
+
{ fetch: { url: 'https://gw-c.nowcoder.com/api/sparta/hot-search/top-hot-pc' } },
|
|
16
|
+
{ select: 'data.result' },
|
|
17
|
+
{ map: {
|
|
18
|
+
rank: '${{ index + 1 }}',
|
|
19
|
+
title: '${{ item.title }}',
|
|
20
|
+
heat: '${{ item.hotValueFromDolphin }}',
|
|
21
|
+
id: '${{ item.uuid || item.id || "" }}',
|
|
22
|
+
} },
|
|
23
|
+
{ limit: '${{ args.limit }}' },
|
|
24
|
+
],
|
|
25
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
const TDX_HOT_URL = 'https://pul.tdx.com.cn/site/app/gzhbd/tdx-topsearch/page-main.html?pageName=page_topsearch&tabClickIndex=0&subtabIndex=0';
|
|
4
|
+
|
|
5
|
+
cli({
|
|
6
|
+
site: 'tdx',
|
|
7
|
+
name: 'hot-rank',
|
|
8
|
+
description: '通达信热搜榜',
|
|
9
|
+
domain: 'pul.tdx.com.cn',
|
|
10
|
+
strategy: Strategy.COOKIE,
|
|
11
|
+
navigateBefore: true,
|
|
12
|
+
args: [
|
|
13
|
+
{ name: 'limit', type: 'int', default: 20, help: '返回数量' },
|
|
14
|
+
],
|
|
15
|
+
columns: ['rank', 'symbol', 'name', 'changePercent', 'heat', 'tags'],
|
|
16
|
+
func: async (page, kwargs) => {
|
|
17
|
+
await page.goto(TDX_HOT_URL);
|
|
18
|
+
await page.wait({ timeout: 15000 });
|
|
19
|
+
const data = await page.evaluate(`
|
|
20
|
+
(() => {
|
|
21
|
+
const cleanText = (el) => (el?.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
22
|
+
const cells = document.querySelectorAll('div.top-cell[data-code]');
|
|
23
|
+
const results = [];
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
cells.forEach((cell, idx) => {
|
|
26
|
+
const symbol = cell.getAttribute('data-code') || '';
|
|
27
|
+
const name = cell.getAttribute('data-name') || '';
|
|
28
|
+
if (!symbol || !name || seen.has(symbol)) return;
|
|
29
|
+
seen.add(symbol);
|
|
30
|
+
const tagEls = cell.querySelectorAll('div.tips-item.gnbk');
|
|
31
|
+
const tags = Array.from(tagEls).map(t => cleanText(t)).filter(Boolean).join(',');
|
|
32
|
+
results.push({
|
|
33
|
+
rank: idx + 1,
|
|
34
|
+
symbol,
|
|
35
|
+
name,
|
|
36
|
+
changePercent: cleanText(cell.querySelector('div.top-zf')),
|
|
37
|
+
heat: cleanText(cell.querySelector('div.hotN')),
|
|
38
|
+
tags,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
return results;
|
|
42
|
+
})()
|
|
43
|
+
`);
|
|
44
|
+
if (!Array.isArray(data)) return [];
|
|
45
|
+
return data.slice(0, kwargs.limit);
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
+
import './hot-rank.js';
|
|
4
|
+
|
|
5
|
+
describe('tdx hot-rank command', () => {
|
|
6
|
+
it('registers the command with correct metadata', () => {
|
|
7
|
+
const command = getRegistry().get('tdx/hot-rank');
|
|
8
|
+
expect(command).toBeDefined();
|
|
9
|
+
expect(command).toMatchObject({
|
|
10
|
+
site: 'tdx',
|
|
11
|
+
name: 'hot-rank',
|
|
12
|
+
description: expect.stringContaining('通达信'),
|
|
13
|
+
domain: 'pul.tdx.com.cn',
|
|
14
|
+
navigateBefore: true,
|
|
15
|
+
});
|
|
16
|
+
expect(command.columns).toEqual(['rank', 'symbol', 'name', 'changePercent', 'heat', 'tags']);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('returns hot stock data from the page', async () => {
|
|
20
|
+
const command = getRegistry().get('tdx/hot-rank');
|
|
21
|
+
const mockData = [
|
|
22
|
+
{ rank: 1, symbol: '600519', name: '贵州茅台', changePercent: '+2.35%', heat: '1285', tags: '白酒', },
|
|
23
|
+
{ rank: 2, symbol: '000001', name: '平安银行', changePercent: '-0.80%', heat: '856', tags: '银行', },
|
|
24
|
+
];
|
|
25
|
+
const page = {
|
|
26
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
27
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
evaluate: vi.fn().mockResolvedValue(mockData),
|
|
29
|
+
};
|
|
30
|
+
const result = await command.func(page, { limit: 20 });
|
|
31
|
+
expect(result).toHaveLength(2);
|
|
32
|
+
expect(result[0]).toEqual(mockData[0]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('respects the limit parameter', async () => {
|
|
36
|
+
const command = getRegistry().get('tdx/hot-rank');
|
|
37
|
+
const mockData = Array.from({ length: 30 }, (_, i) => ({
|
|
38
|
+
rank: i + 1, symbol: `${i}`, name: `stock${i}`, changePercent: '0%', heat: '0', tags: '',
|
|
39
|
+
}));
|
|
40
|
+
const page = {
|
|
41
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
42
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
43
|
+
evaluate: vi.fn().mockResolvedValue(mockData),
|
|
44
|
+
};
|
|
45
|
+
const result = await command.func(page, { limit: 10 });
|
|
46
|
+
expect(result).toHaveLength(10);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns empty array when evaluate returns non-array', async () => {
|
|
50
|
+
const command = getRegistry().get('tdx/hot-rank');
|
|
51
|
+
const page = {
|
|
52
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
53
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
54
|
+
evaluate: vi.fn().mockResolvedValue(null),
|
|
55
|
+
};
|
|
56
|
+
const result = await command.func(page, { limit: 20 });
|
|
57
|
+
expect(result).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
const THS_HOT_URL = 'https://eq.10jqka.com.cn/webpage/ths-hot-list/index.html?showStatusBar=true';
|
|
4
|
+
|
|
5
|
+
cli({
|
|
6
|
+
site: 'ths',
|
|
7
|
+
name: 'hot-rank',
|
|
8
|
+
description: '同花顺热股榜',
|
|
9
|
+
domain: 'eq.10jqka.com.cn',
|
|
10
|
+
strategy: Strategy.COOKIE,
|
|
11
|
+
navigateBefore: true,
|
|
12
|
+
args: [
|
|
13
|
+
{ name: 'limit', type: 'int', default: 20, help: '返回数量' },
|
|
14
|
+
],
|
|
15
|
+
columns: ['rank', 'name', 'changePercent', 'heat', 'tags'],
|
|
16
|
+
func: async (page, kwargs) => {
|
|
17
|
+
await page.goto(THS_HOT_URL);
|
|
18
|
+
await page.wait({ timeout: 15000 });
|
|
19
|
+
const data = await page.evaluate(`
|
|
20
|
+
(() => {
|
|
21
|
+
const cleanText = (el) => (el?.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
22
|
+
const cards = document.querySelectorAll('div.pt-22.pb-24.bgc-white.border');
|
|
23
|
+
const results = [];
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
cards.forEach((card, idx) => {
|
|
26
|
+
const row = card.querySelector('div.flex.bgc-white');
|
|
27
|
+
if (!row) return;
|
|
28
|
+
const nameEl = row.querySelector('span.ellipsis');
|
|
29
|
+
const name = cleanText(nameEl);
|
|
30
|
+
if (!name || seen.has(name)) return;
|
|
31
|
+
seen.add(name);
|
|
32
|
+
const tagEls = card.querySelectorAll('div.tag.PFSC-R');
|
|
33
|
+
const tags = Array.from(tagEls).map(t => cleanText(t)).filter(Boolean).join(',');
|
|
34
|
+
const rankEl = row.querySelector('div.THSMF-M.bold');
|
|
35
|
+
results.push({
|
|
36
|
+
rank: cleanText(rankEl) || String(idx + 1),
|
|
37
|
+
name,
|
|
38
|
+
changePercent: cleanText(row.querySelector('div.range')),
|
|
39
|
+
heat: cleanText(row.querySelector('div.col4 > span')),
|
|
40
|
+
tags,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
return results;
|
|
44
|
+
})()
|
|
45
|
+
`);
|
|
46
|
+
if (!Array.isArray(data)) return [];
|
|
47
|
+
return data.slice(0, kwargs.limit);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
+
import './hot-rank.js';
|
|
4
|
+
|
|
5
|
+
describe('ths hot-rank command', () => {
|
|
6
|
+
it('registers the command with correct metadata', () => {
|
|
7
|
+
const command = getRegistry().get('ths/hot-rank');
|
|
8
|
+
expect(command).toBeDefined();
|
|
9
|
+
expect(command).toMatchObject({
|
|
10
|
+
site: 'ths',
|
|
11
|
+
name: 'hot-rank',
|
|
12
|
+
description: expect.stringContaining('同花顺'),
|
|
13
|
+
domain: 'eq.10jqka.com.cn',
|
|
14
|
+
navigateBefore: true,
|
|
15
|
+
});
|
|
16
|
+
expect(command.columns).toEqual(['rank', 'name', 'changePercent', 'heat', 'tags']);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('includes tags column', () => {
|
|
20
|
+
const command = getRegistry().get('ths/hot-rank');
|
|
21
|
+
expect(command.columns).toContain('tags');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns hot stock data with tags field', async () => {
|
|
25
|
+
const command = getRegistry().get('ths/hot-rank');
|
|
26
|
+
const mockData = [
|
|
27
|
+
{ rank: 1, name: '圣阳股份', changePercent: '+10.00%', heat: '28.5万', tags: '动力电池回收,钠离子电池' },
|
|
28
|
+
];
|
|
29
|
+
const page = {
|
|
30
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
31
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
32
|
+
evaluate: vi.fn().mockResolvedValue(mockData),
|
|
33
|
+
};
|
|
34
|
+
const result = await command.func(page, { limit: 20 });
|
|
35
|
+
expect(result).toHaveLength(1);
|
|
36
|
+
expect(result[0].tags).toBe('动力电池回收,钠离子电池');
|
|
37
|
+
expect(result[0].name).toBe('圣阳股份');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('respects the limit parameter', async () => {
|
|
41
|
+
const command = getRegistry().get('ths/hot-rank');
|
|
42
|
+
const mockData = Array.from({ length: 30 }, (_, i) => ({
|
|
43
|
+
rank: i + 1, name: `stock${i}`, changePercent: '0%', heat: '0', tags: '',
|
|
44
|
+
}));
|
|
45
|
+
const page = {
|
|
46
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
47
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
48
|
+
evaluate: vi.fn().mockResolvedValue(mockData),
|
|
49
|
+
};
|
|
50
|
+
const result = await command.func(page, { limit: 10 });
|
|
51
|
+
expect(result).toHaveLength(10);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns empty array when evaluate returns non-array', async () => {
|
|
55
|
+
const command = getRegistry().get('ths/hot-rank');
|
|
56
|
+
const page = {
|
|
57
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
58
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
59
|
+
evaluate: vi.fn().mockResolvedValue(null),
|
|
60
|
+
};
|
|
61
|
+
const result = await command.func(page, { limit: 20 });
|
|
62
|
+
expect(result).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -60,6 +60,7 @@ function extractBookmarkTweet(result, seen) {
|
|
|
60
60
|
text: noteText || legacy.full_text || '',
|
|
61
61
|
likes: legacy.favorite_count || 0,
|
|
62
62
|
retweets: legacy.retweet_count || 0,
|
|
63
|
+
bookmarks: legacy.bookmark_count || 0,
|
|
63
64
|
created_at: legacy.created_at || '',
|
|
64
65
|
url: `https://x.com/${screenName}/status/${tw.rest_id}`,
|
|
65
66
|
};
|
|
@@ -106,7 +107,7 @@ cli({
|
|
|
106
107
|
args: [
|
|
107
108
|
{ name: 'limit', type: 'int', default: 20 },
|
|
108
109
|
],
|
|
109
|
-
columns: ['author', 'text', 'likes', 'url'],
|
|
110
|
+
columns: ['author', 'text', 'likes', 'retweets', 'bookmarks', 'url'],
|
|
110
111
|
func: async (page, kwargs) => {
|
|
111
112
|
const limit = kwargs.limit || 20;
|
|
112
113
|
await page.goto('https://x.com');
|