@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.
Files changed (197) hide show
  1. package/README.md +81 -59
  2. package/README.zh-CN.md +93 -67
  3. package/cli-manifest.json +5015 -2975
  4. package/clis/antigravity/serve.js +71 -25
  5. package/clis/baidu-scholar/search.js +87 -0
  6. package/clis/baidu-scholar/search.test.js +23 -0
  7. package/clis/bilibili/favorite.js +18 -13
  8. package/clis/binance/depth.js +3 -4
  9. package/clis/boss/utils.js +2 -3
  10. package/clis/chatgpt-app/ax.js +6 -3
  11. package/clis/deepseek/ask.js +74 -0
  12. package/clis/deepseek/history.js +25 -0
  13. package/clis/deepseek/new.js +20 -0
  14. package/clis/deepseek/read.js +22 -0
  15. package/clis/deepseek/status.js +24 -0
  16. package/clis/deepseek/utils.js +208 -0
  17. package/clis/douban/search.js +1 -0
  18. package/clis/douban/search.test.js +11 -0
  19. package/clis/douban/subject.js +20 -93
  20. package/clis/douban/subject.test.js +11 -0
  21. package/clis/douban/utils.js +250 -8
  22. package/clis/douban/utils.test.js +179 -4
  23. package/clis/doubao/utils.js +319 -130
  24. package/clis/doubao/utils.test.js +241 -2
  25. package/clis/eastmoney/_secid.js +78 -0
  26. package/clis/eastmoney/announcement.js +52 -0
  27. package/clis/eastmoney/convertible.js +73 -0
  28. package/clis/eastmoney/etf.js +65 -0
  29. package/clis/eastmoney/holders.js +78 -0
  30. package/clis/eastmoney/hot-rank.js +50 -0
  31. package/clis/eastmoney/hot-rank.test.js +59 -0
  32. package/clis/eastmoney/index-board.js +96 -0
  33. package/clis/eastmoney/kline.js +87 -0
  34. package/clis/eastmoney/kuaixun.js +54 -0
  35. package/clis/eastmoney/longhu.js +67 -0
  36. package/clis/eastmoney/money-flow.js +78 -0
  37. package/clis/eastmoney/northbound.js +57 -0
  38. package/clis/eastmoney/quote.js +107 -0
  39. package/clis/eastmoney/rank.js +94 -0
  40. package/clis/eastmoney/sectors.js +76 -0
  41. package/clis/google-scholar/search.js +58 -0
  42. package/clis/google-scholar/search.test.js +23 -0
  43. package/clis/gov-law/commands.test.js +39 -0
  44. package/clis/gov-law/recent.js +22 -0
  45. package/clis/gov-law/search.js +41 -0
  46. package/clis/gov-law/shared.js +51 -0
  47. package/clis/gov-policy/commands.test.js +27 -0
  48. package/clis/gov-policy/recent.js +47 -0
  49. package/clis/gov-policy/search.js +48 -0
  50. package/clis/grok/image.test.ts +107 -0
  51. package/clis/grok/image.ts +356 -0
  52. package/clis/nowcoder/companies.js +23 -0
  53. package/clis/nowcoder/creators.js +27 -0
  54. package/clis/nowcoder/detail.js +61 -0
  55. package/clis/nowcoder/experience.js +36 -0
  56. package/clis/nowcoder/hot.js +24 -0
  57. package/clis/nowcoder/jobs.js +21 -0
  58. package/clis/nowcoder/notifications.js +29 -0
  59. package/clis/nowcoder/papers.js +40 -0
  60. package/clis/nowcoder/practice.js +37 -0
  61. package/clis/nowcoder/recommend.js +30 -0
  62. package/clis/nowcoder/referral.js +39 -0
  63. package/clis/nowcoder/salary.js +40 -0
  64. package/clis/nowcoder/search.js +49 -0
  65. package/clis/nowcoder/suggest.js +33 -0
  66. package/clis/nowcoder/topics.js +27 -0
  67. package/clis/nowcoder/trending.js +25 -0
  68. package/clis/tdx/hot-rank.js +47 -0
  69. package/clis/tdx/hot-rank.test.js +59 -0
  70. package/clis/ths/hot-rank.js +49 -0
  71. package/clis/ths/hot-rank.test.js +64 -0
  72. package/clis/twitter/bookmarks.js +2 -1
  73. package/clis/twitter/list-add.js +337 -0
  74. package/clis/twitter/list-add.test.js +15 -0
  75. package/clis/twitter/list-remove.js +297 -0
  76. package/clis/twitter/list-remove.test.js +14 -0
  77. package/clis/twitter/list-tweets.js +185 -0
  78. package/clis/twitter/list-tweets.test.js +108 -0
  79. package/clis/twitter/lists.js +134 -47
  80. package/clis/twitter/lists.test.js +105 -38
  81. package/clis/uiverse/_shared.js +368 -0
  82. package/clis/uiverse/_shared.test.js +55 -0
  83. package/clis/uiverse/code.js +47 -0
  84. package/clis/uiverse/preview.js +71 -0
  85. package/clis/wanfang/search.js +66 -0
  86. package/clis/wanfang/search.test.js +23 -0
  87. package/clis/web/read.js +1 -1
  88. package/clis/weixin/download.js +3 -2
  89. package/clis/xiaohongshu/comments.js +2 -2
  90. package/clis/xiaohongshu/comments.test.js +46 -25
  91. package/clis/xiaohongshu/download.js +6 -7
  92. package/clis/xiaohongshu/download.test.js +17 -5
  93. package/clis/xiaohongshu/note-helpers.js +46 -12
  94. package/clis/xiaohongshu/note.js +3 -5
  95. package/clis/xiaohongshu/note.test.js +52 -25
  96. package/clis/xiaohongshu/publish.js +149 -28
  97. package/clis/xiaohongshu/publish.test.js +319 -6
  98. package/clis/xiaoyuzhou/auth.js +303 -0
  99. package/clis/xiaoyuzhou/auth.test.js +124 -0
  100. package/clis/xiaoyuzhou/download.js +53 -0
  101. package/clis/xiaoyuzhou/download.test.js +135 -0
  102. package/clis/xiaoyuzhou/episode.js +9 -4
  103. package/clis/xiaoyuzhou/podcast-episodes.js +15 -11
  104. package/clis/xiaoyuzhou/podcast.js +9 -4
  105. package/clis/xiaoyuzhou/transcript.js +76 -0
  106. package/clis/xiaoyuzhou/transcript.test.js +195 -0
  107. package/clis/xiaoyuzhou/utils.js +0 -40
  108. package/clis/xiaoyuzhou/utils.test.js +15 -75
  109. package/clis/youtube/feed.js +120 -0
  110. package/clis/youtube/history.js +118 -0
  111. package/clis/youtube/like.js +62 -0
  112. package/clis/youtube/playlist.js +97 -0
  113. package/clis/youtube/subscribe.js +71 -0
  114. package/clis/youtube/subscriptions.js +57 -0
  115. package/clis/youtube/unlike.js +62 -0
  116. package/clis/youtube/unsubscribe.js +71 -0
  117. package/clis/youtube/utils.js +122 -0
  118. package/clis/youtube/utils.test.js +32 -1
  119. package/clis/youtube/watch-later.js +76 -0
  120. package/clis/zsxq/dynamics.js +1 -1
  121. package/clis/zsxq/utils.js +6 -3
  122. package/clis/zsxq/utils.test.js +31 -0
  123. package/dist/src/browser/base-page.d.ts +1 -1
  124. package/dist/src/browser/base-page.js +25 -5
  125. package/dist/src/browser/bridge.d.ts +3 -0
  126. package/dist/src/browser/bridge.js +52 -15
  127. package/dist/src/browser/cdp.js +2 -1
  128. package/dist/src/browser/daemon-client.d.ts +7 -4
  129. package/dist/src/browser/daemon-client.js +6 -1
  130. package/dist/src/browser/daemon-client.test.js +40 -1
  131. package/dist/src/browser/dom-snapshot.js +20 -3
  132. package/dist/src/browser/page.d.ts +18 -5
  133. package/dist/src/browser/page.js +96 -15
  134. package/dist/src/browser/page.test.js +158 -1
  135. package/dist/src/browser/target-errors.d.ts +23 -0
  136. package/dist/src/browser/target-errors.js +29 -0
  137. package/dist/src/browser/target-errors.test.js +61 -0
  138. package/dist/src/browser/target-resolver.d.ts +57 -0
  139. package/dist/src/browser/target-resolver.js +298 -0
  140. package/dist/src/browser/target-resolver.test.js +43 -0
  141. package/dist/src/browser.test.js +38 -1
  142. package/dist/src/cli.js +272 -187
  143. package/dist/src/cli.test.js +167 -90
  144. package/dist/src/commanderAdapter.d.ts +0 -1
  145. package/dist/src/commanderAdapter.js +2 -16
  146. package/dist/src/commanderAdapter.test.js +1 -1
  147. package/dist/src/commands/daemon.d.ts +4 -2
  148. package/dist/src/commands/daemon.js +22 -2
  149. package/dist/src/commands/daemon.test.js +65 -2
  150. package/dist/src/completion-shared.js +2 -5
  151. package/dist/src/daemon.js +10 -0
  152. package/dist/src/doctor.d.ts +1 -0
  153. package/dist/src/doctor.js +32 -9
  154. package/dist/src/doctor.test.js +28 -12
  155. package/dist/src/download/article-download.d.ts +1 -0
  156. package/dist/src/download/article-download.js +3 -0
  157. package/dist/src/download/article-download.test.js +39 -0
  158. package/dist/src/external-clis.yaml +2 -2
  159. package/dist/src/logger.d.ts +2 -2
  160. package/dist/src/logger.js +3 -3
  161. package/dist/src/output.js +1 -5
  162. package/dist/src/output.test.js +0 -21
  163. package/dist/src/pipeline/steps/transform.js +1 -1
  164. package/dist/src/pipeline/template.d.ts +1 -0
  165. package/dist/src/pipeline/template.js +11 -3
  166. package/dist/src/pipeline/template.test.js +3 -0
  167. package/dist/src/pipeline/transform.test.js +14 -0
  168. package/dist/src/plugin.d.ts +8 -9
  169. package/dist/src/plugin.js +24 -28
  170. package/dist/src/plugin.test.js +16 -60
  171. package/dist/src/registry.d.ts +1 -0
  172. package/dist/src/registry.js +3 -2
  173. package/dist/src/registry.test.js +22 -0
  174. package/dist/src/types.d.ts +15 -6
  175. package/package.json +1 -1
  176. package/clis/twitter/lists-parser.js +0 -77
  177. package/clis/twitter/lists.d.ts +0 -5
  178. package/dist/src/cascade.d.ts +0 -46
  179. package/dist/src/cascade.js +0 -135
  180. package/dist/src/explore.d.ts +0 -99
  181. package/dist/src/explore.js +0 -402
  182. package/dist/src/generate-verified.d.ts +0 -105
  183. package/dist/src/generate-verified.js +0 -696
  184. package/dist/src/generate-verified.test.js +0 -925
  185. package/dist/src/generate.d.ts +0 -46
  186. package/dist/src/generate.js +0 -117
  187. package/dist/src/record.d.ts +0 -96
  188. package/dist/src/record.js +0 -657
  189. package/dist/src/record.test.js +0 -293
  190. package/dist/src/skill-generate.d.ts +0 -30
  191. package/dist/src/skill-generate.js +0 -75
  192. package/dist/src/skill-generate.test.js +0 -173
  193. package/dist/src/synthesize.d.ts +0 -97
  194. package/dist/src/synthesize.js +0 -208
  195. /package/dist/src/{generate-verified.test.d.ts → browser/target-errors.test.d.ts} +0 -0
  196. /package/dist/src/{record.test.d.ts → browser/target-resolver.test.d.ts} +0 -0
  197. /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(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&nbsp;/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');