@jackwener/opencli 1.7.4 → 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 (126) hide show
  1. package/README.md +71 -49
  2. package/README.zh-CN.md +73 -60
  3. package/cli-manifest.json +3261 -1758
  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/deepseek/ask.js +74 -0
  8. package/clis/deepseek/history.js +25 -0
  9. package/clis/deepseek/new.js +20 -0
  10. package/clis/deepseek/read.js +22 -0
  11. package/clis/deepseek/status.js +24 -0
  12. package/clis/deepseek/utils.js +208 -0
  13. package/clis/eastmoney/_secid.js +78 -0
  14. package/clis/eastmoney/announcement.js +52 -0
  15. package/clis/eastmoney/convertible.js +73 -0
  16. package/clis/eastmoney/etf.js +65 -0
  17. package/clis/eastmoney/holders.js +78 -0
  18. package/clis/eastmoney/index-board.js +96 -0
  19. package/clis/eastmoney/kline.js +87 -0
  20. package/clis/eastmoney/kuaixun.js +54 -0
  21. package/clis/eastmoney/longhu.js +67 -0
  22. package/clis/eastmoney/money-flow.js +78 -0
  23. package/clis/eastmoney/northbound.js +57 -0
  24. package/clis/eastmoney/quote.js +107 -0
  25. package/clis/eastmoney/rank.js +94 -0
  26. package/clis/eastmoney/sectors.js +76 -0
  27. package/clis/google-scholar/search.js +58 -0
  28. package/clis/google-scholar/search.test.js +23 -0
  29. package/clis/gov-law/commands.test.js +39 -0
  30. package/clis/gov-law/recent.js +22 -0
  31. package/clis/gov-law/search.js +41 -0
  32. package/clis/gov-law/shared.js +51 -0
  33. package/clis/gov-policy/commands.test.js +27 -0
  34. package/clis/gov-policy/recent.js +47 -0
  35. package/clis/gov-policy/search.js +48 -0
  36. package/clis/nowcoder/companies.js +23 -0
  37. package/clis/nowcoder/creators.js +27 -0
  38. package/clis/nowcoder/detail.js +61 -0
  39. package/clis/nowcoder/experience.js +36 -0
  40. package/clis/nowcoder/hot.js +24 -0
  41. package/clis/nowcoder/jobs.js +21 -0
  42. package/clis/nowcoder/notifications.js +29 -0
  43. package/clis/nowcoder/papers.js +40 -0
  44. package/clis/nowcoder/practice.js +37 -0
  45. package/clis/nowcoder/recommend.js +30 -0
  46. package/clis/nowcoder/referral.js +39 -0
  47. package/clis/nowcoder/salary.js +40 -0
  48. package/clis/nowcoder/search.js +49 -0
  49. package/clis/nowcoder/suggest.js +33 -0
  50. package/clis/nowcoder/topics.js +27 -0
  51. package/clis/nowcoder/trending.js +25 -0
  52. package/clis/twitter/list-add.js +337 -0
  53. package/clis/twitter/list-add.test.js +15 -0
  54. package/clis/twitter/list-remove.js +297 -0
  55. package/clis/twitter/list-remove.test.js +14 -0
  56. package/clis/twitter/list-tweets.js +185 -0
  57. package/clis/twitter/list-tweets.test.js +108 -0
  58. package/clis/twitter/lists.js +134 -47
  59. package/clis/twitter/lists.test.js +105 -38
  60. package/clis/wanfang/search.js +66 -0
  61. package/clis/wanfang/search.test.js +23 -0
  62. package/clis/web/read.js +1 -1
  63. package/clis/weixin/download.js +3 -2
  64. package/clis/xiaohongshu/publish.js +149 -28
  65. package/clis/xiaohongshu/publish.test.js +319 -6
  66. package/clis/xiaoyuzhou/download.js +8 -4
  67. package/clis/xiaoyuzhou/download.test.js +23 -13
  68. package/clis/xiaoyuzhou/episode.js +9 -4
  69. package/clis/xiaoyuzhou/podcast-episodes.js +15 -11
  70. package/clis/xiaoyuzhou/podcast.js +9 -4
  71. package/clis/xiaoyuzhou/utils.js +0 -40
  72. package/clis/xiaoyuzhou/utils.test.js +15 -75
  73. package/clis/zsxq/dynamics.js +1 -1
  74. package/clis/zsxq/utils.js +6 -3
  75. package/clis/zsxq/utils.test.js +31 -0
  76. package/dist/src/browser/base-page.d.ts +1 -1
  77. package/dist/src/browser/bridge.d.ts +1 -0
  78. package/dist/src/browser/bridge.js +1 -1
  79. package/dist/src/browser/cdp.js +1 -1
  80. package/dist/src/browser/daemon-client.d.ts +6 -4
  81. package/dist/src/browser/daemon-client.js +6 -1
  82. package/dist/src/browser/daemon-client.test.js +40 -1
  83. package/dist/src/browser/dom-snapshot.js +7 -2
  84. package/dist/src/browser/page.d.ts +14 -4
  85. package/dist/src/browser/page.js +48 -7
  86. package/dist/src/browser/page.test.js +97 -0
  87. package/dist/src/cli.js +227 -150
  88. package/dist/src/cli.test.js +167 -90
  89. package/dist/src/commanderAdapter.d.ts +0 -1
  90. package/dist/src/commanderAdapter.js +2 -16
  91. package/dist/src/commanderAdapter.test.js +1 -1
  92. package/dist/src/completion-shared.js +2 -5
  93. package/dist/src/daemon.js +8 -0
  94. package/dist/src/download/article-download.d.ts +1 -0
  95. package/dist/src/download/article-download.js +3 -0
  96. package/dist/src/download/article-download.test.js +39 -0
  97. package/dist/src/plugin.d.ts +1 -8
  98. package/dist/src/plugin.js +1 -27
  99. package/dist/src/plugin.test.js +1 -59
  100. package/dist/src/registry.d.ts +1 -0
  101. package/dist/src/registry.js +3 -2
  102. package/dist/src/registry.test.js +22 -0
  103. package/dist/src/types.d.ts +14 -5
  104. package/package.json +1 -1
  105. package/clis/twitter/lists-parser.js +0 -77
  106. package/clis/twitter/lists.d.ts +0 -5
  107. package/dist/src/cascade.d.ts +0 -46
  108. package/dist/src/cascade.js +0 -135
  109. package/dist/src/explore.d.ts +0 -99
  110. package/dist/src/explore.js +0 -402
  111. package/dist/src/generate-verified.d.ts +0 -105
  112. package/dist/src/generate-verified.js +0 -696
  113. package/dist/src/generate-verified.test.js +0 -925
  114. package/dist/src/generate.d.ts +0 -46
  115. package/dist/src/generate.js +0 -117
  116. package/dist/src/record.d.ts +0 -96
  117. package/dist/src/record.js +0 -657
  118. package/dist/src/record.test.d.ts +0 -1
  119. package/dist/src/record.test.js +0 -293
  120. package/dist/src/skill-generate.d.ts +0 -30
  121. package/dist/src/skill-generate.js +0 -75
  122. package/dist/src/skill-generate.test.d.ts +0 -1
  123. package/dist/src/skill-generate.test.js +0 -173
  124. package/dist/src/synthesize.d.ts +0 -97
  125. package/dist/src/synthesize.js +0 -208
  126. /package/dist/src/{generate-verified.test.d.ts → download/article-download.test.d.ts} +0 -0
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '@jackwener/opencli/registry';
3
+ import './search.js';
4
+ import './recent.js';
5
+
6
+ describe('gov-policy commands', () => {
7
+ const search = getRegistry().get('gov-policy/search');
8
+ const recent = getRegistry().get('gov-policy/recent');
9
+
10
+ it('registers both commands as public browser commands', () => {
11
+ expect(search).toBeDefined();
12
+ expect(recent).toBeDefined();
13
+ expect(search.browser).toBe(true);
14
+ expect(recent.browser).toBe(true);
15
+ expect(search.strategy).toBe('public');
16
+ expect(recent.strategy).toBe('public');
17
+ });
18
+
19
+ it('rejects empty search queries before browser navigation', async () => {
20
+ const page = { goto: vi.fn() };
21
+ await expect(search.func(page, { query: ' ' })).rejects.toMatchObject({
22
+ name: 'ArgumentError',
23
+ code: 'ARGUMENT',
24
+ });
25
+ expect(page.goto).not.toHaveBeenCalled();
26
+ });
27
+ });
@@ -0,0 +1,47 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { clampInt } from '../_shared/common.js';
3
+
4
+ cli({
5
+ site: 'gov-policy',
6
+ name: 'recent',
7
+ description: '国务院最新政策文件',
8
+ domain: 'www.gov.cn',
9
+ strategy: Strategy.PUBLIC,
10
+ browser: true,
11
+ args: [
12
+ { name: 'limit', type: 'int', default: 10, help: '返回结果数量 (max 20)' },
13
+ ],
14
+ columns: ['rank', 'title', 'date', 'source', 'url'],
15
+ navigateBefore: false,
16
+ func: async (page, kwargs) => {
17
+ const limit = clampInt(kwargs.limit, 10, 1, 20);
18
+ await page.goto('https://www.gov.cn/zhengce/zuixin/index.htm');
19
+ await page.wait(4);
20
+ const data = await page.evaluate(`
21
+ (async () => {
22
+ const normalize = v => (v || '').replace(/\\s+/g, ' ').trim();
23
+ for (let i = 0; i < 20; i++) {
24
+ if (document.querySelector('.news_box li, .list li, .list_item, .news-list li')) break;
25
+ await new Promise(r => setTimeout(r, 500));
26
+ }
27
+ const results = [];
28
+ for (const el of document.querySelectorAll('.news_box li, .list li, .list_item, .news-list li')) {
29
+ const titleEl = el.querySelector('a');
30
+ const title = normalize(titleEl?.textContent);
31
+ if (!title || title.length < 4) continue;
32
+
33
+ let url = titleEl?.getAttribute('href') || '';
34
+ if (url && !url.startsWith('http')) url = 'https://www.gov.cn' + url;
35
+
36
+ const date = (el.textContent || '').match(/(\\d{4}[-./]\\d{1,2}[-./]\\d{1,2})/)?.[1] || '';
37
+ const source = normalize(el.querySelector('.source, .from')?.textContent);
38
+
39
+ results.push({ rank: results.length + 1, title, date, source, url });
40
+ if (results.length >= ${limit}) break;
41
+ }
42
+ return results;
43
+ })()
44
+ `);
45
+ return Array.isArray(data) ? data : [];
46
+ },
47
+ });
@@ -0,0 +1,48 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { clampInt, requireNonEmptyQuery } from '../_shared/common.js';
3
+
4
+ cli({
5
+ site: 'gov-policy',
6
+ name: 'search',
7
+ description: '中国政府网政策文件搜索',
8
+ domain: 'sousuo.www.gov.cn',
9
+ strategy: Strategy.PUBLIC,
10
+ browser: true,
11
+ args: [
12
+ { name: 'query', positional: true, required: true, help: '搜索关键词' },
13
+ { name: 'limit', type: 'int', default: 10, help: '返回结果数量 (max 20)' },
14
+ ],
15
+ columns: ['rank', 'title', 'description', 'date', 'url'],
16
+ navigateBefore: false,
17
+ func: async (page, kwargs) => {
18
+ const limit = clampInt(kwargs.limit, 10, 1, 20);
19
+ const query = requireNonEmptyQuery(kwargs.query);
20
+ await page.goto(`https://sousuo.www.gov.cn/sousuo/search.shtml?code=17da70961a7&dataTypeId=107&searchWord=${encodeURIComponent(query)}`);
21
+ await page.wait(5);
22
+ const data = await page.evaluate(`
23
+ (async () => {
24
+ const normalize = v => (v || '').replace(/\\s+/g, ' ').trim();
25
+ for (let i = 0; i < 30; i++) {
26
+ if (document.querySelectorAll('.basic_result_content .item, .js_basic_result_content .item').length > 0) break;
27
+ await new Promise(r => setTimeout(r, 500));
28
+ }
29
+ const results = [];
30
+ for (const el of document.querySelectorAll('.basic_result_content .item, .js_basic_result_content .item')) {
31
+ const titleEl = el.querySelector('a.title, .title a, a.log-anchor');
32
+ const title = normalize(titleEl?.textContent).replace(/<[^>]+>/g, '');
33
+ if (!title || title.length < 4) continue;
34
+
35
+ let url = titleEl?.getAttribute('href') || '';
36
+ if (url && !url.startsWith('http')) url = 'https://www.gov.cn' + url;
37
+
38
+ const description = normalize(el.querySelector('.description')?.textContent).slice(0, 120);
39
+ const date = (el.textContent || '').match(/(\\d{4}[-./]\\d{1,2}[-./]\\d{1,2})/)?.[1] || '';
40
+ results.push({ rank: results.length + 1, title, description, date, url });
41
+ if (results.length >= ${limit}) break;
42
+ }
43
+ return results;
44
+ })()
45
+ `);
46
+ return Array.isArray(data) ? data : [];
47
+ },
48
+ });
@@ -0,0 +1,23 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'nowcoder',
5
+ name: 'companies',
6
+ description: 'Hot companies for interview prep',
7
+ domain: 'www.nowcoder.com',
8
+ strategy: Strategy.PUBLIC,
9
+ browser: false,
10
+ args: [
11
+ { name: 'job', type: 'str', default: '11002', help: 'Job ID (11002=Java, 11003=C++, 11200=Backend, 11203=QA, 11201=Frontend)' },
12
+ ],
13
+ columns: ['rank', 'company', 'companyId'],
14
+ pipeline: [
15
+ { fetch: { url: 'https://gw-c.nowcoder.com/api/sparta/company-question/hot-company-list?jobId=${{ args.job }}' } },
16
+ { select: 'data.result' },
17
+ { map: {
18
+ rank: '${{ index + 1 }}',
19
+ company: '${{ item.companyName }}',
20
+ companyId: '${{ item.companyId }}',
21
+ } },
22
+ ],
23
+ });
@@ -0,0 +1,27 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'nowcoder',
5
+ name: 'creators',
6
+ description: 'Top content creators leaderboard',
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', 'nickname', 'school', 'level', 'heat', 'tag'],
14
+ pipeline: [
15
+ { fetch: { url: 'https://gw-c.nowcoder.com/api/sparta/content/creator/top-list' } },
16
+ { select: 'data.result' },
17
+ { map: {
18
+ rank: '${{ index + 1 }}',
19
+ nickname: `\${{ item.userBrief?.nickname || '' }}`,
20
+ school: `\${{ item.userBrief?.educationInfo || '' }}`,
21
+ level: `\${{ item.userBrief?.honorLevelName || '' }}`,
22
+ heat: '${{ item.hotValue }}',
23
+ tag: `\${{ item.tag || '' }}`,
24
+ } },
25
+ { limit: '${{ args.limit }}' },
26
+ ],
27
+ });
@@ -0,0 +1,61 @@
1
+ import { cli } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'nowcoder',
5
+ name: 'detail',
6
+ description: 'Post detail view (supports ID / UUID / URL)',
7
+ domain: 'www.nowcoder.com',
8
+ args: [
9
+ { name: 'id', positional: true, required: true, help: 'Post ID, UUID, or URL' },
10
+ ],
11
+ columns: ['title', 'author', 'school', 'content', 'likes', 'comments', 'views', 'time', 'location'],
12
+ pipeline: [
13
+ { navigate: 'https://www.nowcoder.com' },
14
+ { evaluate: `(async () => {
15
+ const raw = \${{ args.id | json }};
16
+ const base = 'https://gw-c.nowcoder.com';
17
+ const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&').trim();
18
+
19
+ let id = raw;
20
+ const urlMatch = raw.match(/discuss\\/(\\d+)/);
21
+ if (urlMatch) id = urlMatch[1];
22
+
23
+ let data = null;
24
+
25
+ if (/[a-f]/.test(id) && id.length > 20) {
26
+ const r = await fetch(base + '/api/sparta/detail/moment-data/detail/' + id, {credentials: 'include'});
27
+ const d = await r.json();
28
+ if (d.success && d.data) data = d.data;
29
+ }
30
+
31
+ if (!data && /^\\d+$/.test(id)) {
32
+ const r = await fetch(base + '/api/sparta/detail/content-data/detail/' + id, {credentials: 'include'});
33
+ const d = await r.json();
34
+ if (d.success && d.data) data = d.data;
35
+ }
36
+
37
+ if (!data && /^\\d+$/.test(id)) {
38
+ const r = await fetch(base + '/api/sparta/detail/moment-data/detail/' + id, {credentials: 'include'});
39
+ const d = await r.json();
40
+ if (d.success && d.data) data = d.data;
41
+ }
42
+
43
+ if (!data) throw new Error('Post not found: ' + id);
44
+
45
+ const user = data.userBrief || {};
46
+ const freq = data.frequencyData || {};
47
+ return [{
48
+ title: data.title || '(untitled)',
49
+ author: user.nickname || '',
50
+ school: user.educationInfo || '',
51
+ content: strip(data.content || '').substring(0, 500),
52
+ likes: freq.likeCnt || 0,
53
+ comments: freq.commentCnt || freq.totalCommentCnt || 0,
54
+ views: freq.viewCnt || 0,
55
+ time: data.createdAt ? new Date(data.createdAt).toISOString().slice(0, 19) : '',
56
+ location: data.ip4Location || '',
57
+ }];
58
+ })()
59
+ ` },
60
+ ],
61
+ });
@@ -0,0 +1,36 @@
1
+ import { cli } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'nowcoder',
5
+ name: 'experience',
6
+ description: 'Interview experience 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=818&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
+ rank: i + 1,
23
+ title: item.contentData?.title || '',
24
+ author: item.userBrief?.nickname || '',
25
+ school: item.userBrief?.educationInfo || '',
26
+ likes: item.frequencyData?.likeCnt || 0,
27
+ comments: item.frequencyData?.commentCnt || 0,
28
+ views: item.frequencyData?.viewCnt || 0,
29
+ id: item.contentData?.uuid || item.contentData?.id || item.contentId || '',
30
+ }));
31
+ })()
32
+ ` },
33
+ { filter: 'item.title' },
34
+ { limit: '${{ args.limit }}' },
35
+ ],
36
+ });
@@ -0,0 +1,24 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'nowcoder',
5
+ name: 'hot',
6
+ description: 'Hot search ranking',
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'],
14
+ pipeline: [
15
+ { fetch: { url: 'https://gw-c.nowcoder.com/api/sparta/hot-search/hot-content' } },
16
+ { select: 'data.hotQuery' },
17
+ { map: {
18
+ rank: '${{ item.rank }}',
19
+ title: '${{ item.query }}',
20
+ heat: '${{ item.hotValue }}',
21
+ } },
22
+ { limit: '${{ args.limit }}' },
23
+ ],
24
+ });
@@ -0,0 +1,21 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'nowcoder',
5
+ name: 'jobs',
6
+ description: 'Career category listing',
7
+ domain: 'www.nowcoder.com',
8
+ strategy: Strategy.PUBLIC,
9
+ browser: false,
10
+ args: [],
11
+ columns: ['id', 'career', 'learners'],
12
+ pipeline: [
13
+ { fetch: { url: 'https://gw-c.nowcoder.com/api/sparta/company-question/careerJobLevel1List' } },
14
+ { select: 'data.careerJobSelectors' },
15
+ { map: {
16
+ id: '${{ item.id }}',
17
+ career: '${{ item.name }}',
18
+ learners: `\${{ item.practiceCount || '' }}`,
19
+ } },
20
+ ],
21
+ });
@@ -0,0 +1,29 @@
1
+ import { cli } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'nowcoder',
5
+ name: 'notifications',
6
+ description: 'Unread message summary',
7
+ domain: 'www.nowcoder.com',
8
+ args: [],
9
+ columns: ['type', 'unread'],
10
+ pipeline: [
11
+ { navigate: 'https://www.nowcoder.com' },
12
+ { evaluate: `(async () => {
13
+ const r = await fetch('https://gw-c.nowcoder.com/api/sparta/message/pc/unread/detail', {credentials: 'include'});
14
+ const d = await r.json();
15
+ if (!d.success) throw new Error(d.msg || 'API failed');
16
+ const data = d.data;
17
+ return [
18
+ {type: 'system', unread: data.systemNotice?.unreadCount || 0},
19
+ {type: 'likes', unread: data.likeCollect?.unreadCount || 0},
20
+ {type: 'comments', unread: data.commentMessage?.unreadCount || 0},
21
+ {type: 'follows', unread: data.followMessage?.unreadCount || 0},
22
+ {type: 'messages', unread: data.privateMessage?.unreadCount || 0},
23
+ {type: 'job_apply', unread: data.nowPickJobApply?.unreadCount || 0},
24
+ {type: 'total', unread: data.total?.unreadCount || 0},
25
+ ];
26
+ })()
27
+ ` },
28
+ ],
29
+ });
@@ -0,0 +1,40 @@
1
+ import { cli } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'nowcoder',
5
+ name: 'papers',
6
+ description: 'Interview question bank by company and job',
7
+ domain: 'www.nowcoder.com',
8
+ args: [
9
+ { name: 'job', type: 'str', default: '11002', help: 'Job ID (11002=Java, 11003=C++, 11200=Backend, 11203=QA, 11201=Frontend)' },
10
+ { name: 'company', type: 'str', default: '', help: 'Company ID (e.g. 139=Baidu, 138=Tencent, 239=Huawei)' },
11
+ { name: 'limit', type: 'int', default: 10, help: 'Number of items' },
12
+ ],
13
+ columns: ['rank', 'title', 'company', 'practitioners'],
14
+ pipeline: [
15
+ { navigate: 'https://www.nowcoder.com' },
16
+ { evaluate: `(async () => {
17
+ const jobId = parseInt(\${{ args.job | json }});
18
+ const companyId = \${{ args.company | json }};
19
+ const limit = \${{ args.limit }};
20
+ const body = {jobId, page: 1, pageSize: limit};
21
+ if (companyId) body.companyId = parseInt(companyId);
22
+ const r = await fetch('https://gw-c.nowcoder.com/api/sparta/company-question/get-paper-list', {
23
+ method: 'POST',
24
+ credentials: 'include',
25
+ headers: {'Content-Type': 'application/json'},
26
+ body: JSON.stringify(body)
27
+ });
28
+ const d = await r.json();
29
+ if (!d.success) throw new Error(d.msg || 'API failed');
30
+ return (d.data?.records || []).map((p, i) => ({
31
+ rank: i + 1,
32
+ title: p.paperName || '',
33
+ company: p.companyTag?.name || '',
34
+ practitioners: p.practiceCnt || 0,
35
+ }));
36
+ })()
37
+ ` },
38
+ { limit: '${{ args.limit }}' },
39
+ ],
40
+ });
@@ -0,0 +1,37 @@
1
+ import { cli } from '@jackwener/opencli/registry';
2
+
3
+ cli({
4
+ site: 'nowcoder',
5
+ name: 'practice',
6
+ description: 'Categorized practice questions with progress',
7
+ domain: 'www.nowcoder.com',
8
+ args: [
9
+ { name: 'job', type: 'str', default: '11226', help: 'Career ID (11226=Software, 11227=Hardware, 11229=Product, 11230=Finance)' },
10
+ { name: 'limit', type: 'int', default: 20, help: 'Number of items' },
11
+ ],
12
+ columns: ['category', 'subject', 'total', 'done', 'remaining'],
13
+ pipeline: [
14
+ { navigate: 'https://www.nowcoder.com' },
15
+ { evaluate: `(async () => {
16
+ const jobId = \${{ args.job | json }};
17
+ const limit = \${{ args.limit }};
18
+ const r = await fetch('https://gw-c.nowcoder.com/api/sparta/intelligent/getPCIntelligentList?jobId=' + jobId, {credentials: 'include'});
19
+ const d = await r.json();
20
+ if (!d.success) throw new Error(d.msg || 'API failed');
21
+ const all = [];
22
+ for (const tag of (d.data?.tags || [])) {
23
+ for (const item of (tag.items || [])) {
24
+ all.push({
25
+ category: tag.title || 'recommended',
26
+ subject: item.title,
27
+ total: item.tcount,
28
+ done: item.rcount,
29
+ remaining: item.leftCount,
30
+ });
31
+ }
32
+ }
33
+ return all.slice(0, limit);
34
+ })()
35
+ ` },
36
+ ],
37
+ });
@@ -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
+ });