@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,78 @@
1
+ // eastmoney money-flow — main-force net inflow ranking (沪深A今日/5日/10日).
2
+ //
3
+ // opencli eastmoney money-flow
4
+ // opencli eastmoney money-flow --range 5d --limit 30
5
+
6
+ import { cli, Strategy } from '@jackwener/opencli/registry';
7
+ import { CliError } from '@jackwener/opencli/errors';
8
+
9
+ const A_MARKET = 'm:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048';
10
+
11
+ // (main, super, big, medium, small) net inflow amount fields
12
+ const RANGES = {
13
+ today: { fid: 'f62', fields: { net: 'f62', netPct: 'f184', super: 'f66', big: 'f72', medium: 'f78', small: 'f84' } },
14
+ '5d': { fid: 'f164', fields: { net: 'f164', netPct: 'f165', super: 'f166', big: 'f169', medium: 'f172', small: 'f175' } },
15
+ '10d': { fid: 'f174', fields: { net: 'f174', netPct: 'f175', super: 'f176', big: 'f179', medium: 'f182', small: 'f185' } },
16
+ };
17
+
18
+ cli({
19
+ site: 'eastmoney',
20
+ name: 'money-flow',
21
+ description: '主力资金净流入排行(今日/5日/10日)',
22
+ domain: 'push2.eastmoney.com',
23
+ strategy: Strategy.PUBLIC,
24
+ browser: false,
25
+ args: [
26
+ { name: 'range', type: 'string', default: 'today', help: '周期:today / 5d / 10d' },
27
+ { name: 'order', type: 'string', default: 'desc', help: '排序:desc (净流入排行) / asc (净流出)' },
28
+ { name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
29
+ ],
30
+ columns: ['rank', 'code', 'name', 'price', 'changePercent', 'mainNet', 'mainNetRatio', 'superNet', 'bigNet', 'mediumNet', 'smallNet'],
31
+ func: async (_page, args) => {
32
+ const rangeKey = String(args.range ?? 'today').toLowerCase();
33
+ const range = RANGES[rangeKey];
34
+ if (!range) {
35
+ throw new CliError('INVALID_ARGUMENT', `Unknown range "${rangeKey}". Valid: ${Object.keys(RANGES).join(', ')}`);
36
+ }
37
+ const po = String(args.order ?? 'desc').toLowerCase() === 'asc' ? '0' : '1';
38
+ const limit = Math.max(1, Math.min(Number(args.limit) || 20, 100));
39
+
40
+ const fieldList = [
41
+ 'f12', 'f14', 'f2', 'f3',
42
+ range.fields.net, range.fields.netPct,
43
+ range.fields.super, range.fields.big, range.fields.medium, range.fields.small,
44
+ ];
45
+
46
+ const url = new URL('https://push2.eastmoney.com/api/qt/clist/get');
47
+ url.searchParams.set('pn', '1');
48
+ url.searchParams.set('pz', String(limit));
49
+ url.searchParams.set('po', po);
50
+ url.searchParams.set('np', '1');
51
+ url.searchParams.set('fltt', '2');
52
+ url.searchParams.set('invt', '2');
53
+ url.searchParams.set('fid', range.fid);
54
+ url.searchParams.set('fs', A_MARKET);
55
+ url.searchParams.set('fields', fieldList.join(','));
56
+ url.searchParams.set('ut', 'b2884a393a59ad64002292a3e90d46a5');
57
+
58
+ const resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
59
+ if (!resp.ok) throw new CliError('HTTP_ERROR', `money-flow failed: HTTP ${resp.status}`);
60
+ const data = await resp.json();
61
+ const diff = Array.isArray(data?.data?.diff) ? data.data.diff : [];
62
+ if (diff.length === 0) throw new CliError('NO_DATA', 'eastmoney returned no money-flow data');
63
+
64
+ return diff.slice(0, limit).map((it, i) => ({
65
+ rank: i + 1,
66
+ code: it.f12,
67
+ name: it.f14,
68
+ price: it.f2,
69
+ changePercent: it.f3,
70
+ mainNet: it[range.fields.net],
71
+ mainNetRatio: it[range.fields.netPct],
72
+ superNet: it[range.fields.super],
73
+ bigNet: it[range.fields.big],
74
+ mediumNet: it[range.fields.medium],
75
+ smallNet: it[range.fields.small],
76
+ }));
77
+ },
78
+ });
@@ -0,0 +1,57 @@
1
+ // eastmoney northbound — live realtime cross-border capital flow (北向/南向).
2
+ //
3
+ // Returns the latest non-empty minute snapshot of cumulative net flow in 万元.
4
+ // opencli eastmoney northbound
5
+ // opencli eastmoney northbound --direction south
6
+
7
+ import { cli, Strategy } from '@jackwener/opencli/registry';
8
+ import { CliError } from '@jackwener/opencli/errors';
9
+
10
+ cli({
11
+ site: 'eastmoney',
12
+ name: 'northbound',
13
+ description: '沪深港通北向/南向资金当日分时净流入(万元)',
14
+ domain: 'push2.eastmoney.com',
15
+ strategy: Strategy.PUBLIC,
16
+ browser: false,
17
+ args: [
18
+ { name: 'direction', type: 'string', default: 'north', help: '方向:north (北向,即外资买A) / south (南向,即内地买港)' },
19
+ { name: 'limit', type: 'int', default: 10, help: '返回最近 N 分钟' },
20
+ ],
21
+ columns: ['time', 'cumulativeNetYi', 'minuteNetYi', 'totalNetYi'],
22
+ func: async (_page, args) => {
23
+ const dir = String(args.direction ?? 'north').toLowerCase();
24
+ if (!['north', 'south', 'n', 's'].includes(dir)) {
25
+ throw new CliError('INVALID_ARGUMENT', `Unknown direction "${dir}". Valid: north / south`);
26
+ }
27
+ const limit = Math.max(1, Math.min(Number(args.limit) || 10, 240));
28
+
29
+ const url = new URL('https://push2.eastmoney.com/api/qt/kamtbs.rtmin/get');
30
+ url.searchParams.set('fields1', 'f1,f2,f3,f4');
31
+ url.searchParams.set('fields2', 'f51,f52,f54,f56');
32
+ url.searchParams.set('ut', 'b2884a393a59ad64002292a3e90d46a5');
33
+
34
+ const resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
35
+ if (!resp.ok) throw new CliError('HTTP_ERROR', `northbound failed: HTTP ${resp.status}`);
36
+ const data = await resp.json();
37
+ const key = (dir === 'south' || dir === 's') ? 's2n' : 'n2s';
38
+ /** @type {string[]} */
39
+ const rows = Array.isArray(data?.data?.[key]) ? data.data[key] : [];
40
+ if (rows.length === 0) throw new CliError('NO_DATA', `No ${key} data returned`);
41
+
42
+ // CSV fields per entry: "HH:MM,cumulative_net(万), minute_net(万), total_net(万)"
43
+ // Drop rows with '-' (after market close or before open). Convert 万元 → 亿元 for readability.
44
+ const valid = rows
45
+ .map((r) => r.split(','))
46
+ .filter((c) => c.length >= 4 && c[1] !== '-');
47
+ if (valid.length === 0) {
48
+ throw new CliError('NO_DATA', `${key} has no valid minute data yet (markets may not be open)`);
49
+ }
50
+ return valid.slice(-limit).map(([time, cum, min, total]) => ({
51
+ time,
52
+ cumulativeNetYi: +(Number(cum) / 10000).toFixed(4),
53
+ minuteNetYi: +(Number(min) / 10000).toFixed(4),
54
+ totalNetYi: +(Number(total) / 10000).toFixed(4),
55
+ }));
56
+ },
57
+ });
@@ -0,0 +1,107 @@
1
+ // eastmoney quote — live quote for one or more stocks (A/HK/US).
2
+ //
3
+ // Data source: push2.eastmoney.com (Tier 1 public JSON, no auth).
4
+ // Supported inputs (comma / space separated):
5
+ // 600000, sh600000, 000001, sz000001, 00700.HK, hk00700, AAPL, us.AAPL
6
+ //
7
+ // opencli eastmoney quote 600000 --fields all
8
+ // opencli eastmoney quote "sh600000,sz000001,00700.HK"
9
+
10
+ import { cli, Strategy } from '@jackwener/opencli/registry';
11
+ import { CliError } from '@jackwener/opencli/errors';
12
+ import { resolveSecid, splitSymbols } from './_secid.js';
13
+
14
+ const FIELDS = [
15
+ 'f12', // code
16
+ 'f13', // market
17
+ 'f14', // name
18
+ 'f2', // price
19
+ 'f3', // changePercent
20
+ 'f4', // change
21
+ 'f5', // volume (手)
22
+ 'f6', // turnover (CNY)
23
+ 'f7', // amplitude %
24
+ 'f8', // turnoverRate %
25
+ 'f9', // peDynamic
26
+ 'f10', // volumeRatio
27
+ 'f15', // high
28
+ 'f16', // low
29
+ 'f17', // open
30
+ 'f18', // prevClose
31
+ 'f20', // marketCap
32
+ 'f21', // floatMarketCap
33
+ 'f23', // priceBook
34
+ ].join(',');
35
+
36
+ function marketLabel(f13) {
37
+ if (f13 === 1) return 'SH';
38
+ if (f13 === 0) return 'SZ/BJ';
39
+ if (f13 === 116) return 'HK';
40
+ if (f13 === 105 || f13 === 106 || f13 === 107) return 'US';
41
+ return String(f13 ?? '');
42
+ }
43
+
44
+ cli({
45
+ site: 'eastmoney',
46
+ name: 'quote',
47
+ description: '个股实时行情(A股 / 港股 / 美股)— 来自 push2.eastmoney.com',
48
+ domain: 'push2.eastmoney.com',
49
+ strategy: Strategy.PUBLIC,
50
+ browser: false,
51
+ args: [
52
+ { name: 'symbols', required: true, positional: true, help: '股票代码(可用逗号/空格分隔多个)' },
53
+ ],
54
+ columns: [
55
+ 'code', 'name', 'market', 'price', 'changePercent', 'change',
56
+ 'open', 'high', 'low', 'prevClose', 'volume', 'turnover',
57
+ 'turnoverRate', 'amplitude', 'peDynamic', 'priceBook',
58
+ 'marketCap', 'floatMarketCap',
59
+ ],
60
+ func: async (_page, args) => {
61
+ const raw = splitSymbols(args.symbols);
62
+ if (raw.length === 0) {
63
+ throw new CliError('INVALID_ARGUMENT', 'At least one symbol is required');
64
+ }
65
+
66
+ /** @type {string[]} */
67
+ const secids = [];
68
+ for (const s of raw) {
69
+ try { secids.push(resolveSecid(s)); }
70
+ catch (err) { throw new CliError('INVALID_ARGUMENT', `Unrecognized symbol "${s}"`); }
71
+ }
72
+
73
+ // Multi-stock in one call via ulist.np
74
+ const url = new URL('https://push2.eastmoney.com/api/qt/ulist.np/get');
75
+ url.searchParams.set('secids', secids.join(','));
76
+ url.searchParams.set('fltt', '2');
77
+ url.searchParams.set('fields', FIELDS);
78
+ url.searchParams.set('ut', 'bd1d9ddb04089700cf9c27f6f7426281');
79
+
80
+ const resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0', Accept: 'application/json' } });
81
+ if (!resp.ok) throw new CliError('HTTP_ERROR', `eastmoney quote failed: HTTP ${resp.status}`);
82
+ const data = await resp.json();
83
+ const diff = Array.isArray(data?.data?.diff) ? data.data.diff : [];
84
+ if (diff.length === 0) throw new CliError('NO_DATA', 'eastmoney returned no quotes', `Check symbols: ${raw.join(', ')}`);
85
+
86
+ return diff.map((it) => ({
87
+ code: it.f12,
88
+ name: it.f14,
89
+ market: marketLabel(it.f13),
90
+ price: it.f2,
91
+ changePercent: it.f3,
92
+ change: it.f4,
93
+ open: it.f17,
94
+ high: it.f15,
95
+ low: it.f16,
96
+ prevClose: it.f18,
97
+ volume: it.f5,
98
+ turnover: it.f6,
99
+ turnoverRate: it.f8,
100
+ amplitude: it.f7,
101
+ peDynamic: it.f9,
102
+ priceBook: it.f23,
103
+ marketCap: it.f20,
104
+ floatMarketCap: it.f21,
105
+ }));
106
+ },
107
+ });
@@ -0,0 +1,94 @@
1
+ // eastmoney rank — market mover list across common segments.
2
+ //
3
+ // Data source: push2.eastmoney.com/api/qt/clist/get (Tier 1 public JSON).
4
+ // opencli eastmoney rank
5
+ // opencli eastmoney rank --market cyb --sort turnover --limit 30
6
+
7
+ import { cli, Strategy } from '@jackwener/opencli/registry';
8
+ import { CliError } from '@jackwener/opencli/errors';
9
+
10
+ const MARKETS = {
11
+ 'hs-a': 'm:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048', // 沪深 A
12
+ 'sh-a': 'm:1+t:2,m:1+t:23', // 沪 A
13
+ 'sz-a': 'm:0+t:6,m:0+t:80', // 深 A
14
+ 'bj-a': 'm:0+t:81+s:2048', // 北证 A
15
+ 'cyb': 'm:0+t:80', // 创业板
16
+ 'kcb': 'm:1+t:23', // 科创板
17
+ 'hk': 'm:116+t:3,m:116+t:4,m:116+t:1,m:116+t:2', // 港股
18
+ 'us': 'm:105,m:106,m:107', // 美股
19
+ };
20
+
21
+ const SORTS = {
22
+ change: { fid: 'f3', order: 'desc' }, // 涨幅榜
23
+ drop: { fid: 'f3', order: 'asc' }, // 跌幅榜
24
+ turnover:{ fid: 'f6', order: 'desc' }, // 成交额
25
+ volume: { fid: 'f5', order: 'desc' }, // 成交量
26
+ amplitude:{ fid:'f7', order: 'desc' }, // 振幅
27
+ rate: { fid: 'f8', order: 'desc' }, // 换手率
28
+ };
29
+
30
+ const FIELDS =
31
+ 'f2,f3,f4,f5,f6,f7,f8,f9,f10,f12,f13,f14,f15,f16,f17,f18,f20,f21,f23';
32
+
33
+ cli({
34
+ site: 'eastmoney',
35
+ name: 'rank',
36
+ description: '东财市场涨跌/成交排行(沪深/北证/创/科/港/美)',
37
+ domain: 'push2.eastmoney.com',
38
+ strategy: Strategy.PUBLIC,
39
+ browser: false,
40
+ args: [
41
+ { name: 'market', type: 'string', default: 'hs-a', help: '市场:hs-a / sh-a / sz-a / bj-a / cyb / kcb / hk / us' },
42
+ { name: 'sort', type: 'string', default: 'change', help: '排序:change / drop / turnover / volume / amplitude / rate' },
43
+ { name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
44
+ ],
45
+ columns: ['rank', 'code', 'name', 'price', 'changePercent', 'change', 'turnover', 'volume', 'turnoverRate', 'peDynamic', 'marketCap'],
46
+ func: async (_page, args) => {
47
+ const market = String(args.market ?? 'hs-a').toLowerCase();
48
+ const sortKey = String(args.sort ?? 'change').toLowerCase();
49
+ const limit = Math.max(1, Math.min(Number(args.limit) || 20, 100));
50
+
51
+ const fs = MARKETS[market];
52
+ if (!fs) {
53
+ throw new CliError('INVALID_ARGUMENT', `Unknown market "${market}". Valid: ${Object.keys(MARKETS).join(', ')}`);
54
+ }
55
+ const sort = SORTS[sortKey];
56
+ if (!sort) {
57
+ throw new CliError('INVALID_ARGUMENT', `Unknown sort "${sortKey}". Valid: ${Object.keys(SORTS).join(', ')}`);
58
+ }
59
+
60
+ const url = new URL('https://push2.eastmoney.com/api/qt/clist/get');
61
+ url.searchParams.set('pn', '1');
62
+ url.searchParams.set('pz', String(limit));
63
+ url.searchParams.set('po', sort.order === 'desc' ? '1' : '0');
64
+ url.searchParams.set('np', '1');
65
+ url.searchParams.set('fltt', '2');
66
+ url.searchParams.set('invt', '2');
67
+ url.searchParams.set('fid', sort.fid);
68
+ url.searchParams.set('fs', fs);
69
+ url.searchParams.set('fields', FIELDS);
70
+ url.searchParams.set('ut', 'bd1d9ddb04089700cf9c27f6f7426281');
71
+
72
+ const resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0', Accept: 'application/json' } });
73
+ if (!resp.ok) throw new CliError('HTTP_ERROR', `eastmoney rank failed: HTTP ${resp.status}`);
74
+ const data = await resp.json();
75
+ const diff = Array.isArray(data?.data?.diff) ? data.data.diff : [];
76
+ if (diff.length === 0) {
77
+ throw new CliError('NO_DATA', 'eastmoney returned no rank data', `market=${market} sort=${sortKey}`);
78
+ }
79
+
80
+ return diff.slice(0, limit).map((it, i) => ({
81
+ rank: i + 1,
82
+ code: it.f12,
83
+ name: it.f14,
84
+ price: it.f2,
85
+ changePercent: it.f3,
86
+ change: it.f4,
87
+ turnover: it.f6,
88
+ volume: it.f5,
89
+ turnoverRate: it.f8,
90
+ peDynamic: it.f9,
91
+ marketCap: it.f20,
92
+ }));
93
+ },
94
+ });
@@ -0,0 +1,76 @@
1
+ // eastmoney sectors — industry / concept / region sector board ranking.
2
+ //
3
+ // opencli eastmoney sectors
4
+ // opencli eastmoney sectors --type concept --sort money-flow --limit 30
5
+
6
+ import { cli, Strategy } from '@jackwener/opencli/registry';
7
+ import { CliError } from '@jackwener/opencli/errors';
8
+
9
+ const SECTOR_TYPES = {
10
+ industry: 'm:90+t:2',
11
+ concept: 'm:90+t:3',
12
+ region: 'm:90+t:1',
13
+ };
14
+
15
+ const SORTS = {
16
+ change: { fid: 'f3', order: 'desc' },
17
+ drop: { fid: 'f3', order: 'asc' },
18
+ 'money-flow': { fid: 'f62', order: 'desc' },
19
+ 'out-flow': { fid: 'f62', order: 'asc' },
20
+ turnover: { fid: 'f6', order: 'desc' },
21
+ };
22
+
23
+ cli({
24
+ site: 'eastmoney',
25
+ name: 'sectors',
26
+ description: '板块排行(行业/概念/地域)按涨跌幅、主力资金或成交额排序',
27
+ domain: 'push2.eastmoney.com',
28
+ strategy: Strategy.PUBLIC,
29
+ browser: false,
30
+ args: [
31
+ { name: 'type', type: 'string', default: 'industry', help: '板块类型:industry / concept / region' },
32
+ { name: 'sort', type: 'string', default: 'change', help: '排序:change / drop / money-flow / out-flow / turnover' },
33
+ { name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
34
+ ],
35
+ columns: ['rank', 'code', 'name', 'price', 'changePercent', 'mainNet', 'leadStock', 'leadChangePercent', 'upCount', 'downCount'],
36
+ func: async (_page, args) => {
37
+ const typeKey = String(args.type ?? 'industry').toLowerCase();
38
+ const fs = SECTOR_TYPES[typeKey];
39
+ if (!fs) throw new CliError('INVALID_ARGUMENT', `Unknown sector type "${typeKey}". Valid: ${Object.keys(SECTOR_TYPES).join(', ')}`);
40
+ const sortKey = String(args.sort ?? 'change').toLowerCase();
41
+ const sort = SORTS[sortKey];
42
+ if (!sort) throw new CliError('INVALID_ARGUMENT', `Unknown sort "${sortKey}". Valid: ${Object.keys(SORTS).join(', ')}`);
43
+ const limit = Math.max(1, Math.min(Number(args.limit) || 20, 100));
44
+
45
+ const url = new URL('https://push2.eastmoney.com/api/qt/clist/get');
46
+ url.searchParams.set('pn', '1');
47
+ url.searchParams.set('pz', String(limit));
48
+ url.searchParams.set('po', sort.order === 'desc' ? '1' : '0');
49
+ url.searchParams.set('np', '1');
50
+ url.searchParams.set('fltt', '2');
51
+ url.searchParams.set('invt', '2');
52
+ url.searchParams.set('fid', sort.fid);
53
+ url.searchParams.set('fs', fs);
54
+ url.searchParams.set('fields', 'f12,f14,f2,f3,f62,f104,f105,f128,f136,f140,f141');
55
+ url.searchParams.set('ut', 'b2884a393a59ad64002292a3e90d46a5');
56
+
57
+ const resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
58
+ if (!resp.ok) throw new CliError('HTTP_ERROR', `sectors failed: HTTP ${resp.status}`);
59
+ const data = await resp.json();
60
+ const diff = Array.isArray(data?.data?.diff) ? data.data.diff : [];
61
+ if (diff.length === 0) throw new CliError('NO_DATA', 'eastmoney returned no sector data');
62
+
63
+ return diff.slice(0, limit).map((it, i) => ({
64
+ rank: i + 1,
65
+ code: it.f12,
66
+ name: it.f14,
67
+ price: it.f2,
68
+ changePercent: it.f3,
69
+ mainNet: it.f62,
70
+ leadStock: it.f128,
71
+ leadChangePercent: it.f136,
72
+ upCount: it.f104,
73
+ downCount: it.f105,
74
+ }));
75
+ },
76
+ });
@@ -0,0 +1,58 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { clampInt, requireNonEmptyQuery } from '../_shared/common.js';
3
+
4
+ cli({
5
+ site: 'google-scholar',
6
+ name: 'search',
7
+ description: 'Google Scholar 学术搜索',
8
+ domain: 'scholar.google.com',
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', 'authors', 'source', 'year', 'cited', '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://scholar.google.com/scholar?q=${encodeURIComponent(query)}&hl=zh-CN`);
21
+ await page.wait(3);
22
+ const data = await page.evaluate(`
23
+ (() => {
24
+ const normalize = v => (v || '').replace(/\\s+/g, ' ').trim();
25
+ const results = [];
26
+ for (const el of document.querySelectorAll('.gs_r.gs_or.gs_scl, .gs_ri')) {
27
+ const container = el.querySelector('.gs_ri') || el;
28
+ const titleEl = container.querySelector('.gs_rt a, h3 a');
29
+ const title = normalize(titleEl?.textContent);
30
+ if (!title) continue;
31
+
32
+ const url = titleEl?.getAttribute('href') || '';
33
+ const infoLine = normalize(container.querySelector('.gs_a')?.textContent);
34
+ const parts = infoLine.split(' - ');
35
+ const authors = (parts[0] || '').trim();
36
+ const sourceParts = (parts[1] || '').split(',');
37
+ const source = sourceParts.slice(0, -1).join(',').trim() || sourceParts[0]?.trim() || '';
38
+ const year = infoLine.match(/(19|20)\\d{2}/)?.[0] || '';
39
+ const citedText = normalize(container.querySelector('.gs_fl a[href*="cites"]')?.textContent);
40
+ const cited = citedText.match(/(\\d+)/)?.[1] || '0';
41
+
42
+ results.push({
43
+ rank: results.length + 1,
44
+ title,
45
+ authors: authors.slice(0, 80),
46
+ source: source.slice(0, 60),
47
+ year,
48
+ cited,
49
+ url,
50
+ });
51
+ if (results.length >= ${limit}) break;
52
+ }
53
+ return results;
54
+ })()
55
+ `);
56
+ return Array.isArray(data) ? data : [];
57
+ },
58
+ });
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '@jackwener/opencli/registry';
3
+ import './search.js';
4
+
5
+ describe('google-scholar search command', () => {
6
+ const command = getRegistry().get('google-scholar/search');
7
+
8
+ it('registers as a public browser command', () => {
9
+ expect(command).toBeDefined();
10
+ expect(command.site).toBe('google-scholar');
11
+ expect(command.strategy).toBe('public');
12
+ expect(command.browser).toBe(true);
13
+ });
14
+
15
+ it('rejects empty queries before browser navigation', async () => {
16
+ const page = { goto: vi.fn() };
17
+ await expect(command.func(page, { query: ' ' })).rejects.toMatchObject({
18
+ name: 'ArgumentError',
19
+ code: 'ARGUMENT',
20
+ });
21
+ expect(page.goto).not.toHaveBeenCalled();
22
+ });
23
+ });
@@ -0,0 +1,39 @@
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-law commands', () => {
7
+ const search = getRegistry().get('gov-law/search');
8
+ const recent = getRegistry().get('gov-law/recent');
9
+
10
+ it('registers both commands as public browser commands', () => {
11
+ expect(search).toBeDefined();
12
+ expect(recent).toBeDefined();
13
+ expect(search.strategy).toBe('public');
14
+ expect(recent.strategy).toBe('public');
15
+ expect(search.browser).toBe(true);
16
+ expect(recent.browser).toBe(true);
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
+
28
+ it('throws a descriptive error when Vue Router is unavailable', async () => {
29
+ const page = {
30
+ goto: vi.fn(),
31
+ wait: vi.fn(),
32
+ evaluate: vi.fn().mockResolvedValue(false),
33
+ };
34
+ await expect(recent.func(page, { limit: 3 })).rejects.toMatchObject({
35
+ name: 'CliError',
36
+ code: 'FRAMEWORK_CHANGED',
37
+ });
38
+ });
39
+ });
@@ -0,0 +1,22 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { clampInt } from '../_shared/common.js';
3
+ import { extractLawResults, navigateViaVueRouter } from './shared.js';
4
+
5
+ cli({
6
+ site: 'gov-law',
7
+ name: 'recent',
8
+ description: '最新法律法规',
9
+ domain: 'flk.npc.gov.cn',
10
+ strategy: Strategy.PUBLIC,
11
+ browser: true,
12
+ args: [
13
+ { name: 'limit', type: 'int', default: 10, help: '返回结果数量 (max 20)' },
14
+ ],
15
+ columns: ['rank', 'title', 'status', 'publish_date', 'type', 'department'],
16
+ navigateBefore: false,
17
+ func: async (page, kwargs) => {
18
+ const limit = clampInt(kwargs.limit, 10, 1, 20);
19
+ await navigateViaVueRouter(page, {});
20
+ return extractLawResults(page, limit);
21
+ },
22
+ });
@@ -0,0 +1,41 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { clampInt, requireNonEmptyQuery } from '../_shared/common.js';
3
+ import { extractLawResults, navigateViaVueRouter } from './shared.js';
4
+
5
+ cli({
6
+ site: 'gov-law',
7
+ name: 'search',
8
+ description: '国家法律法规数据库搜索',
9
+ domain: 'flk.npc.gov.cn',
10
+ strategy: Strategy.PUBLIC,
11
+ browser: true,
12
+ args: [
13
+ { name: 'query', positional: true, required: true, help: '搜索关键词' },
14
+ { name: 'limit', type: 'int', default: 10, help: '返回结果数量 (max 20)' },
15
+ ],
16
+ columns: ['rank', 'title', 'status', 'publish_date', 'type', 'department'],
17
+ navigateBefore: false,
18
+ func: async (page, kwargs) => {
19
+ const limit = clampInt(kwargs.limit, 10, 1, 20);
20
+ const query = requireNonEmptyQuery(kwargs.query);
21
+ await navigateViaVueRouter(page, { searchWord: query });
22
+
23
+ const encodedQuery = JSON.stringify(query);
24
+ await page.evaluate(`
25
+ (async () => {
26
+ const input = document.querySelector('.el-input__inner');
27
+ if (input && !input.value) {
28
+ const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
29
+ setter.call(input, ${encodedQuery});
30
+ input.dispatchEvent(new Event('input', { bubbles: true }));
31
+ input.dispatchEvent(new Event('change', { bubbles: true }));
32
+ await new Promise(r => setTimeout(r, 300));
33
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', keyCode: 13, bubbles: true }));
34
+ input.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', keyCode: 13, bubbles: true }));
35
+ }
36
+ })()
37
+ `);
38
+ await page.wait(3);
39
+ return extractLawResults(page, limit);
40
+ },
41
+ });
@@ -0,0 +1,51 @@
1
+ import { CliError } from '@jackwener/opencli/errors';
2
+
3
+ export async function navigateViaVueRouter(page, query) {
4
+ await page.goto('https://flk.npc.gov.cn/index.html');
5
+ await page.wait(4);
6
+
7
+ const routerAvailable = await page.evaluate(`
8
+ (async () => {
9
+ const app = document.querySelector('#app');
10
+ const router = app?.__vue_app__?.config?.globalProperties?.$router;
11
+ if (!router) return false;
12
+ await router.push({ path: '/search', query: ${JSON.stringify(query)} });
13
+ return true;
14
+ })()
15
+ `);
16
+
17
+ if (!routerAvailable) {
18
+ throw new CliError(
19
+ 'FRAMEWORK_CHANGED',
20
+ 'Could not access Vue Router on flk.npc.gov.cn — the site may have been restructured.',
21
+ 'Please report this issue so the adapter can be updated.',
22
+ );
23
+ }
24
+
25
+ await page.wait(5);
26
+ }
27
+
28
+ export async function extractLawResults(page, limit) {
29
+ const data = await page.evaluate(`
30
+ (async () => {
31
+ const normalize = v => (v || '').replace(/\\s+/g, ' ').trim();
32
+ for (let i = 0; i < 40; i++) {
33
+ if (document.querySelectorAll('.result-item').length > 0) break;
34
+ await new Promise(r => setTimeout(r, 500));
35
+ }
36
+ const results = [];
37
+ for (const el of document.querySelectorAll('.result-item')) {
38
+ const title = normalize(el.querySelector('.title-content')?.textContent);
39
+ if (!title) continue;
40
+ const status = normalize(el.querySelector('[class*="status"]')?.textContent);
41
+ const publish_date = normalize(el.querySelector('.publish-time')?.textContent).replace(/^公布日期[::]\\s*/, '');
42
+ const type = normalize(el.querySelector('.type')?.textContent);
43
+ const department = normalize(el.querySelector('.department')?.textContent);
44
+ results.push({ rank: results.length + 1, title, status, publish_date, type, department });
45
+ if (results.length >= ${limit}) break;
46
+ }
47
+ return results;
48
+ })()
49
+ `);
50
+ return Array.isArray(data) ? data : [];
51
+ }