@jackwener/opencli 0.1.0 → 0.1.2

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 (135) hide show
  1. package/CLI-CREATOR.md +594 -0
  2. package/README.md +124 -39
  3. package/README.zh-CN.md +151 -0
  4. package/SKILL.md +178 -102
  5. package/dist/bilibili.d.ts +6 -5
  6. package/dist/browser.d.ts +3 -1
  7. package/dist/browser.js +44 -2
  8. package/dist/cascade.d.ts +46 -0
  9. package/dist/cascade.js +180 -0
  10. package/dist/clis/bbc/news.js +42 -0
  11. package/dist/clis/bilibili/hot.yaml +38 -0
  12. package/dist/clis/boss/search.js +47 -0
  13. package/dist/clis/ctrip/search.d.ts +1 -0
  14. package/dist/clis/ctrip/search.js +62 -0
  15. package/dist/clis/hackernews/top.yaml +36 -0
  16. package/dist/clis/index.d.ts +10 -1
  17. package/dist/clis/index.js +19 -1
  18. package/dist/clis/reddit/hot.yaml +46 -0
  19. package/dist/clis/reuters/search.d.ts +1 -0
  20. package/dist/clis/reuters/search.js +52 -0
  21. package/dist/clis/smzdm/search.d.ts +1 -0
  22. package/dist/clis/smzdm/search.js +66 -0
  23. package/dist/clis/twitter/trending.yaml +40 -0
  24. package/dist/clis/v2ex/hot.yaml +25 -0
  25. package/dist/clis/v2ex/latest.yaml +25 -0
  26. package/dist/clis/v2ex/topic.yaml +27 -0
  27. package/dist/clis/weibo/hot.d.ts +1 -0
  28. package/dist/clis/weibo/hot.js +41 -0
  29. package/dist/clis/xiaohongshu/feed.yaml +32 -0
  30. package/dist/clis/xiaohongshu/notifications.yaml +38 -0
  31. package/dist/clis/xiaohongshu/search.d.ts +5 -0
  32. package/dist/clis/xiaohongshu/search.js +68 -0
  33. package/dist/clis/yahoo-finance/quote.d.ts +1 -0
  34. package/dist/clis/yahoo-finance/quote.js +74 -0
  35. package/dist/clis/youtube/search.d.ts +1 -0
  36. package/dist/clis/youtube/search.js +60 -0
  37. package/dist/clis/zhihu/hot.yaml +42 -0
  38. package/dist/clis/zhihu/question.d.ts +1 -0
  39. package/dist/clis/zhihu/question.js +39 -0
  40. package/dist/clis/zhihu/search.yaml +55 -0
  41. package/dist/engine.d.ts +2 -1
  42. package/dist/explore.d.ts +23 -13
  43. package/dist/explore.js +293 -422
  44. package/dist/generate.js +2 -1
  45. package/dist/main.js +21 -2
  46. package/dist/pipeline/executor.d.ts +9 -0
  47. package/dist/pipeline/executor.js +88 -0
  48. package/dist/pipeline/index.d.ts +5 -0
  49. package/dist/pipeline/index.js +5 -0
  50. package/dist/pipeline/steps/browser.d.ts +12 -0
  51. package/dist/pipeline/steps/browser.js +68 -0
  52. package/dist/pipeline/steps/fetch.d.ts +5 -0
  53. package/dist/pipeline/steps/fetch.js +50 -0
  54. package/dist/pipeline/steps/intercept.d.ts +5 -0
  55. package/dist/pipeline/steps/intercept.js +75 -0
  56. package/dist/pipeline/steps/tap.d.ts +12 -0
  57. package/dist/pipeline/steps/tap.js +130 -0
  58. package/dist/pipeline/steps/transform.d.ts +8 -0
  59. package/dist/pipeline/steps/transform.js +53 -0
  60. package/dist/pipeline/template.d.ts +16 -0
  61. package/dist/pipeline/template.js +115 -0
  62. package/dist/pipeline/template.test.d.ts +4 -0
  63. package/dist/pipeline/template.test.js +102 -0
  64. package/dist/pipeline/transform.test.d.ts +4 -0
  65. package/dist/pipeline/transform.test.js +90 -0
  66. package/dist/pipeline.d.ts +5 -7
  67. package/dist/pipeline.js +5 -313
  68. package/dist/registry.d.ts +3 -2
  69. package/dist/runtime.d.ts +2 -1
  70. package/dist/synthesize.d.ts +11 -8
  71. package/dist/synthesize.js +142 -118
  72. package/dist/types.d.ts +27 -0
  73. package/dist/types.js +7 -0
  74. package/package.json +9 -4
  75. package/src/bilibili.ts +9 -7
  76. package/src/browser.ts +41 -3
  77. package/src/cascade.ts +218 -0
  78. package/src/clis/bbc/news.ts +42 -0
  79. package/src/clis/boss/search.ts +47 -0
  80. package/src/clis/ctrip/search.ts +62 -0
  81. package/src/clis/index.ts +28 -1
  82. package/src/clis/reddit/hot.yaml +46 -0
  83. package/src/clis/reuters/search.ts +52 -0
  84. package/src/clis/smzdm/search.ts +66 -0
  85. package/src/clis/v2ex/hot.yaml +5 -9
  86. package/src/clis/v2ex/latest.yaml +5 -8
  87. package/src/clis/v2ex/topic.yaml +27 -0
  88. package/src/clis/weibo/hot.ts +41 -0
  89. package/src/clis/xiaohongshu/feed.yaml +32 -0
  90. package/src/clis/xiaohongshu/notifications.yaml +38 -0
  91. package/src/clis/xiaohongshu/search.ts +71 -0
  92. package/src/clis/yahoo-finance/quote.ts +74 -0
  93. package/src/clis/youtube/search.ts +60 -0
  94. package/src/clis/zhihu/hot.yaml +22 -8
  95. package/src/clis/zhihu/question.ts +45 -0
  96. package/src/clis/zhihu/search.yaml +55 -0
  97. package/src/engine.ts +2 -1
  98. package/src/explore.ts +303 -465
  99. package/src/generate.ts +3 -1
  100. package/src/main.ts +18 -2
  101. package/src/pipeline/executor.ts +98 -0
  102. package/src/pipeline/index.ts +6 -0
  103. package/src/pipeline/steps/browser.ts +67 -0
  104. package/src/pipeline/steps/fetch.ts +60 -0
  105. package/src/pipeline/steps/intercept.ts +78 -0
  106. package/src/pipeline/steps/tap.ts +137 -0
  107. package/src/pipeline/steps/transform.ts +50 -0
  108. package/src/pipeline/template.test.ts +107 -0
  109. package/src/pipeline/template.ts +101 -0
  110. package/src/pipeline/transform.test.ts +107 -0
  111. package/src/pipeline.ts +5 -292
  112. package/src/registry.ts +4 -2
  113. package/src/runtime.ts +3 -1
  114. package/src/synthesize.ts +142 -137
  115. package/src/types.ts +23 -0
  116. package/vitest.config.ts +7 -0
  117. package/dist/clis/github/search.js +0 -20
  118. package/dist/clis/zhihu/search.js +0 -58
  119. package/dist/promote.d.ts +0 -1
  120. package/dist/promote.js +0 -3
  121. package/dist/register.d.ts +0 -2
  122. package/dist/register.js +0 -2
  123. package/dist/scaffold.d.ts +0 -2
  124. package/dist/scaffold.js +0 -2
  125. package/dist/smoke.d.ts +0 -2
  126. package/dist/smoke.js +0 -2
  127. package/src/clis/github/search.ts +0 -21
  128. package/src/clis/github/trending.yaml +0 -58
  129. package/src/clis/zhihu/search.ts +0 -65
  130. package/src/promote.ts +0 -3
  131. package/src/register.ts +0 -2
  132. package/src/scaffold.ts +0 -2
  133. package/src/smoke.ts +0 -2
  134. /package/dist/clis/{github/search.d.ts → bbc/news.d.ts} +0 -0
  135. /package/dist/clis/{zhihu → boss}/search.d.ts +0 -0
@@ -0,0 +1,25 @@
1
+ site: v2ex
2
+ name: hot
3
+ description: V2EX 热门话题
4
+ domain: www.v2ex.com
5
+ strategy: public
6
+ browser: false
7
+
8
+ args:
9
+ limit:
10
+ type: int
11
+ default: 20
12
+ description: Number of topics
13
+
14
+ pipeline:
15
+ - fetch:
16
+ url: https://www.v2ex.com/api/topics/hot.json
17
+
18
+ - map:
19
+ rank: ${{ index + 1 }}
20
+ title: ${{ item.title }}
21
+ replies: ${{ item.replies }}
22
+
23
+ - limit: ${{ args.limit }}
24
+
25
+ columns: [rank, title, replies]
@@ -0,0 +1,25 @@
1
+ site: v2ex
2
+ name: latest
3
+ description: V2EX 最新话题
4
+ domain: www.v2ex.com
5
+ strategy: public
6
+ browser: false
7
+
8
+ args:
9
+ limit:
10
+ type: int
11
+ default: 20
12
+ description: Number of topics
13
+
14
+ pipeline:
15
+ - fetch:
16
+ url: https://www.v2ex.com/api/topics/latest.json
17
+
18
+ - map:
19
+ rank: ${{ index + 1 }}
20
+ title: ${{ item.title }}
21
+ replies: ${{ item.replies }}
22
+
23
+ - limit: ${{ args.limit }}
24
+
25
+ columns: [rank, title, replies]
@@ -0,0 +1,27 @@
1
+ site: v2ex
2
+ name: topic
3
+ description: V2EX 主题详情和回复
4
+ domain: www.v2ex.com
5
+ strategy: public
6
+ browser: false
7
+
8
+ args:
9
+ id:
10
+ type: str
11
+ required: true
12
+ description: Topic ID
13
+
14
+ pipeline:
15
+ - fetch:
16
+ url: https://www.v2ex.com/api/topics/show.json
17
+ params:
18
+ id: ${{ args.id }}
19
+
20
+ - map:
21
+ title: ${{ item.title }}
22
+ replies: ${{ item.replies }}
23
+ url: ${{ item.url }}
24
+
25
+ - limit: 1
26
+
27
+ columns: [title, replies, url]
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Weibo hot search — browser cookie API.
3
+ * Source: bb-sites/weibo/hot.js
4
+ */
5
+ import { cli, Strategy } from '../../registry.js';
6
+ cli({
7
+ site: 'weibo',
8
+ name: 'hot',
9
+ description: '微博热搜',
10
+ domain: 'weibo.com',
11
+ strategy: Strategy.COOKIE,
12
+ args: [
13
+ { name: 'limit', type: 'int', default: 30, help: 'Number of items (max 50)' },
14
+ ],
15
+ columns: ['rank', 'word', 'hot_value', 'category', 'label', 'url'],
16
+ func: async (page, kwargs) => {
17
+ const count = Math.min(kwargs.limit || 30, 50);
18
+ await page.goto('https://weibo.com');
19
+ await page.wait(2);
20
+ const data = await page.evaluate(`
21
+ (async () => {
22
+ const resp = await fetch('/ajax/statuses/hot_band', {credentials: 'include'});
23
+ if (!resp.ok) return {error: 'HTTP ' + resp.status};
24
+ const data = await resp.json();
25
+ if (!data.ok) return {error: 'API error'};
26
+ const bandList = data.data?.band_list || [];
27
+ return bandList.map((item, i) => ({
28
+ rank: item.realpos || (i + 1),
29
+ word: item.word,
30
+ hot_value: item.num || 0,
31
+ category: item.category || '',
32
+ label: item.label_name || '',
33
+ url: 'https://s.weibo.com/weibo?q=' + encodeURIComponent('#' + item.word + '#')
34
+ }));
35
+ })()
36
+ `);
37
+ if (!Array.isArray(data))
38
+ return [];
39
+ return data.slice(0, count);
40
+ },
41
+ });
@@ -0,0 +1,32 @@
1
+ site: xiaohongshu
2
+ name: feed
3
+ description: "小红书首页推荐 Feed (via Pinia Store Action)"
4
+ domain: www.xiaohongshu.com
5
+ strategy: intercept
6
+ browser: true
7
+
8
+ args:
9
+ limit:
10
+ type: int
11
+ default: 20
12
+ description: Number of items to return
13
+
14
+ columns: [title, author, likes, type, url]
15
+
16
+ pipeline:
17
+ - navigate: https://www.xiaohongshu.com/explore
18
+ - wait: 3
19
+ - tap:
20
+ store: feed
21
+ action: fetchFeeds
22
+ capture: homefeed
23
+ select: data.items
24
+ timeout: 8
25
+ - map:
26
+ id: ${{ item.id }}
27
+ title: ${{ item.note_card.display_title }}
28
+ type: ${{ item.note_card.type }}
29
+ author: ${{ item.note_card.user.nickname }}
30
+ likes: ${{ item.note_card.interact_info.liked_count }}
31
+ url: https://www.xiaohongshu.com/explore/${{ item.id }}
32
+ - limit: ${{ args.limit | default(20) }}
@@ -0,0 +1,38 @@
1
+ site: xiaohongshu
2
+ name: notifications
3
+ description: "小红书通知 (mentions/likes/connections)"
4
+ domain: www.xiaohongshu.com
5
+ strategy: intercept
6
+ browser: true
7
+
8
+ args:
9
+ type:
10
+ type: str
11
+ default: mentions
12
+ description: "Notification type: mentions, likes, or connections"
13
+ limit:
14
+ type: int
15
+ default: 20
16
+ description: Number of notifications to return
17
+
18
+ columns: [rank, user, action, content, note, time]
19
+
20
+ pipeline:
21
+ - navigate: https://www.xiaohongshu.com/notification
22
+ - wait: 3
23
+ - tap:
24
+ store: notification
25
+ action: getNotification
26
+ args:
27
+ - ${{ args.type | default('mentions') }}
28
+ capture: /you/
29
+ select: data.message_list
30
+ timeout: 8
31
+ - map:
32
+ rank: ${{ index + 1 }}
33
+ user: ${{ item.user_info.nickname }}
34
+ action: ${{ item.title }}
35
+ content: ${{ item.comment_info.content }}
36
+ note: ${{ item.item_info.content }}
37
+ time: ${{ item.time }}
38
+ - limit: ${{ args.limit | default(20) }}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Xiaohongshu search — trigger search via Pinia store + XHR interception.
3
+ * Inspired by bb-sites/xiaohongshu/search.js but adapted for opencli pipeline.
4
+ */
5
+ export {};
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Xiaohongshu search — trigger search via Pinia store + XHR interception.
3
+ * Inspired by bb-sites/xiaohongshu/search.js but adapted for opencli pipeline.
4
+ */
5
+ import { cli, Strategy } from '../../registry.js';
6
+ cli({
7
+ site: 'xiaohongshu',
8
+ name: 'search',
9
+ description: '搜索小红书笔记',
10
+ domain: 'www.xiaohongshu.com',
11
+ strategy: Strategy.COOKIE,
12
+ args: [
13
+ { name: 'keyword', required: true, help: 'Search keyword' },
14
+ { name: 'limit', type: 'int', default: 20, help: 'Number of results' },
15
+ ],
16
+ columns: ['rank', 'title', 'author', 'likes', 'type'],
17
+ func: async (page, kwargs) => {
18
+ await page.goto('https://www.xiaohongshu.com');
19
+ await page.wait(2);
20
+ const data = await page.evaluate(`
21
+ (async () => {
22
+ const app = document.querySelector('#app')?.__vue_app__;
23
+ const pinia = app?.config?.globalProperties?.$pinia;
24
+ if (!pinia?._s) return {error: 'Page not ready'};
25
+
26
+ const searchStore = pinia._s.get('search');
27
+ if (!searchStore) return {error: 'Search store not found'};
28
+
29
+ let captured = null;
30
+ const origOpen = XMLHttpRequest.prototype.open;
31
+ const origSend = XMLHttpRequest.prototype.send;
32
+ XMLHttpRequest.prototype.open = function(m, u) { this.__url = u; return origOpen.apply(this, arguments); };
33
+ XMLHttpRequest.prototype.send = function(b) {
34
+ if (this.__url?.includes('search/notes')) {
35
+ const x = this;
36
+ const orig = x.onreadystatechange;
37
+ x.onreadystatechange = function() { if (x.readyState === 4 && !captured) { try { captured = JSON.parse(x.responseText); } catch {} } if (orig) orig.apply(this, arguments); };
38
+ }
39
+ return origSend.apply(this, arguments);
40
+ };
41
+
42
+ try {
43
+ searchStore.mutateSearchValue('${kwargs.keyword}');
44
+ await searchStore.loadMore();
45
+ await new Promise(r => setTimeout(r, 800));
46
+ } finally {
47
+ XMLHttpRequest.prototype.open = origOpen;
48
+ XMLHttpRequest.prototype.send = origSend;
49
+ }
50
+
51
+ if (!captured?.success) return {error: captured?.msg || 'Search failed'};
52
+ return (captured.data?.items || []).map(i => ({
53
+ title: i.note_card?.display_title || '',
54
+ type: i.note_card?.type || '',
55
+ url: 'https://www.xiaohongshu.com/explore/' + i.id,
56
+ author: i.note_card?.user?.nickname || '',
57
+ likes: i.note_card?.interact_info?.liked_count || '0',
58
+ }));
59
+ })()
60
+ `);
61
+ if (!Array.isArray(data))
62
+ return [];
63
+ return data.slice(0, kwargs.limit).map((item, i) => ({
64
+ rank: i + 1,
65
+ ...item,
66
+ }));
67
+ },
68
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Yahoo Finance stock quote — multi-strategy API fallback.
3
+ * Source: bb-sites/yahoo-finance/quote.js
4
+ */
5
+ import { cli, Strategy } from '../../registry.js';
6
+ cli({
7
+ site: 'yahoo-finance',
8
+ name: 'quote',
9
+ description: 'Yahoo Finance 股票行情',
10
+ domain: 'finance.yahoo.com',
11
+ strategy: Strategy.COOKIE,
12
+ args: [
13
+ { name: 'symbol', required: true, help: 'Stock ticker (e.g. AAPL, MSFT, TSLA)' },
14
+ ],
15
+ columns: ['symbol', 'name', 'price', 'change', 'changePercent', 'open', 'high', 'low', 'volume', 'marketCap'],
16
+ func: async (page, kwargs) => {
17
+ const symbol = kwargs.symbol.toUpperCase().trim();
18
+ await page.goto(`https://finance.yahoo.com/quote/${encodeURIComponent(symbol)}/`);
19
+ await page.wait(3);
20
+ const data = await page.evaluate(`
21
+ (async () => {
22
+ const sym = '${symbol}';
23
+
24
+ // Strategy 1: v8 chart API
25
+ try {
26
+ const chartUrl = 'https://query1.finance.yahoo.com/v8/finance/chart/' + encodeURIComponent(sym) + '?interval=1d&range=1d';
27
+ const resp = await fetch(chartUrl);
28
+ if (resp.ok) {
29
+ const d = await resp.json();
30
+ const chart = d?.chart?.result?.[0];
31
+ if (chart) {
32
+ const meta = chart.meta || {};
33
+ const prevClose = meta.previousClose || meta.chartPreviousClose;
34
+ const price = meta.regularMarketPrice;
35
+ const change = price != null && prevClose != null ? (price - prevClose) : null;
36
+ const changePct = change != null && prevClose ? ((change / prevClose) * 100) : null;
37
+ return {
38
+ symbol: meta.symbol || sym, name: meta.shortName || meta.longName || sym,
39
+ price: price != null ? Number(price.toFixed(2)) : null,
40
+ change: change != null ? change.toFixed(2) : null,
41
+ changePercent: changePct != null ? changePct.toFixed(2) + '%' : null,
42
+ open: chart.indicators?.quote?.[0]?.open?.[0] || null,
43
+ high: meta.regularMarketDayHigh || null,
44
+ low: meta.regularMarketDayLow || null,
45
+ volume: meta.regularMarketVolume || null,
46
+ marketCap: null, currency: meta.currency, exchange: meta.exchangeName,
47
+ };
48
+ }
49
+ }
50
+ } catch(e) {}
51
+
52
+ // Strategy 2: Parse from page
53
+ const titleEl = document.querySelector('title');
54
+ const priceEl = document.querySelector('[data-testid="qsp-price"]');
55
+ const changeEl = document.querySelector('[data-testid="qsp-price-change"]');
56
+ const changePctEl = document.querySelector('[data-testid="qsp-price-change-percent"]');
57
+ if (priceEl) {
58
+ return {
59
+ symbol: sym,
60
+ name: titleEl ? titleEl.textContent.split('(')[0].trim() : sym,
61
+ price: priceEl.textContent.replace(/,/g, ''),
62
+ change: changeEl ? changeEl.textContent : null,
63
+ changePercent: changePctEl ? changePctEl.textContent : null,
64
+ open: null, high: null, low: null, volume: null, marketCap: null,
65
+ };
66
+ }
67
+ return {error: 'Could not fetch quote for ' + sym};
68
+ })()
69
+ `);
70
+ if (!data || data.error)
71
+ return [];
72
+ return [data];
73
+ },
74
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ /**
2
+ * YouTube search — innertube API via browser session.
3
+ * Source: bb-sites/youtube/search.js
4
+ */
5
+ import { cli, Strategy } from '../../registry.js';
6
+ cli({
7
+ site: 'youtube',
8
+ name: 'search',
9
+ description: 'Search YouTube videos',
10
+ domain: 'www.youtube.com',
11
+ strategy: Strategy.COOKIE,
12
+ args: [
13
+ { name: 'query', required: true, help: 'Search query' },
14
+ { name: 'limit', type: 'int', default: 20, help: 'Max results (max 50)' },
15
+ ],
16
+ columns: ['rank', 'title', 'channel', 'views', 'duration', 'url'],
17
+ func: async (page, kwargs) => {
18
+ const limit = Math.min(kwargs.limit || 20, 50);
19
+ await page.goto('https://www.youtube.com');
20
+ await page.wait(2);
21
+ const data = await page.evaluate(`
22
+ (async () => {
23
+ const cfg = window.ytcfg?.data_ || {};
24
+ const apiKey = cfg.INNERTUBE_API_KEY;
25
+ const context = cfg.INNERTUBE_CONTEXT;
26
+ if (!apiKey || !context) return {error: 'YouTube config not found'};
27
+
28
+ const resp = await fetch('/youtubei/v1/search?key=' + apiKey + '&prettyPrint=false', {
29
+ method: 'POST', credentials: 'include',
30
+ headers: {'Content-Type': 'application/json'},
31
+ body: JSON.stringify({context, query: '${kwargs.query.replace(/'/g, "\\'")}'})
32
+ });
33
+ if (!resp.ok) return {error: 'HTTP ' + resp.status};
34
+
35
+ const data = await resp.json();
36
+ const contents = data.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents || [];
37
+ const videos = [];
38
+ for (const section of contents) {
39
+ for (const item of (section.itemSectionRenderer?.contents || [])) {
40
+ if (item.videoRenderer && videos.length < ${limit}) {
41
+ const v = item.videoRenderer;
42
+ videos.push({
43
+ rank: videos.length + 1,
44
+ title: v.title?.runs?.[0]?.text || '',
45
+ channel: v.ownerText?.runs?.[0]?.text || '',
46
+ views: v.viewCountText?.simpleText || v.shortViewCountText?.simpleText || '',
47
+ duration: v.lengthText?.simpleText || 'LIVE',
48
+ url: 'https://www.youtube.com/watch?v=' + v.videoId
49
+ });
50
+ }
51
+ }
52
+ }
53
+ return videos;
54
+ })()
55
+ `);
56
+ if (!Array.isArray(data))
57
+ return [];
58
+ return data;
59
+ },
60
+ });
@@ -0,0 +1,42 @@
1
+ site: zhihu
2
+ name: hot
3
+ description: 知乎热榜
4
+ domain: www.zhihu.com
5
+
6
+ args:
7
+ limit:
8
+ type: int
9
+ default: 20
10
+ description: Number of items to return
11
+
12
+ pipeline:
13
+ - navigate: https://www.zhihu.com
14
+
15
+ - evaluate: |
16
+ (async () => {
17
+ const res = await fetch('https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total?limit=50', {
18
+ credentials: 'include'
19
+ });
20
+ const d = await res.json();
21
+ return (d?.data || []).map((item) => {
22
+ const t = item.target || {};
23
+ return {
24
+ title: t.title,
25
+ url: 'https://www.zhihu.com/question/' + t.id,
26
+ answer_count: t.answer_count,
27
+ follower_count: t.follower_count,
28
+ heat: item.detail_text || '',
29
+ };
30
+ });
31
+ })()
32
+
33
+ - map:
34
+ rank: ${{ index + 1 }}
35
+ title: ${{ item.title }}
36
+ heat: ${{ item.heat }}
37
+ answers: ${{ item.answer_count }}
38
+ url: ${{ item.url }}
39
+
40
+ - limit: ${{ args.limit }}
41
+
42
+ columns: [rank, title, heat, answers]
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ cli({
3
+ site: 'zhihu',
4
+ name: 'question',
5
+ description: '知乎问题详情和回答',
6
+ domain: 'www.zhihu.com',
7
+ strategy: Strategy.COOKIE,
8
+ args: [
9
+ { name: 'id', required: true, help: 'Question ID (numeric)' },
10
+ { name: 'limit', type: 'int', default: 5, help: 'Number of answers' },
11
+ ],
12
+ columns: ['rank', 'author', 'votes', 'content'],
13
+ func: async (page, kwargs) => {
14
+ const { id, limit = 5 } = kwargs;
15
+ const stripHtml = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&').trim();
16
+ // Fetch question detail and answers in parallel via evaluate
17
+ const result = await page.evaluate(`
18
+ async () => {
19
+ const [qResp, aResp] = await Promise.all([
20
+ fetch('https://www.zhihu.com/api/v4/questions/${id}?include=data[*].detail,excerpt,answer_count,follower_count,visit_count', {credentials: 'include'}),
21
+ fetch('https://www.zhihu.com/api/v4/questions/${id}/answers?limit=${limit}&offset=0&sort_by=default&include=data[*].content,voteup_count,comment_count,author', {credentials: 'include'})
22
+ ]);
23
+ if (!qResp.ok || !aResp.ok) return { error: true };
24
+ const q = await qResp.json();
25
+ const a = await aResp.json();
26
+ return { question: q, answers: a.data || [] };
27
+ }
28
+ `);
29
+ if (!result || result.error)
30
+ throw new Error('Failed to fetch question. Are you logged in?');
31
+ const answers = (result.answers ?? []).slice(0, Number(limit)).map((a, i) => ({
32
+ rank: i + 1,
33
+ author: a.author?.name ?? 'anonymous',
34
+ votes: a.voteup_count ?? 0,
35
+ content: stripHtml(a.content ?? '').slice(0, 200),
36
+ }));
37
+ return answers;
38
+ },
39
+ });
@@ -0,0 +1,55 @@
1
+ site: zhihu
2
+ name: search
3
+ description: 知乎搜索
4
+ domain: www.zhihu.com
5
+
6
+ args:
7
+ keyword:
8
+ type: str
9
+ required: true
10
+ description: Search keyword
11
+ limit:
12
+ type: int
13
+ default: 10
14
+ description: Number of results
15
+
16
+ pipeline:
17
+ - navigate: https://www.zhihu.com
18
+
19
+ - evaluate: |
20
+ (async () => {
21
+ const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&').replace(/<em>/g, '').replace(/<\/em>/g, '').trim();
22
+ const res = await fetch('https://www.zhihu.com/api/v4/search_v3?q=' + encodeURIComponent('${{ args.keyword }}') + '&t=general&offset=0&limit=${{ args.limit }}', {
23
+ credentials: 'include'
24
+ });
25
+ const d = await res.json();
26
+ return (d?.data || [])
27
+ .filter(item => item.type === 'search_result')
28
+ .map(item => {
29
+ const obj = item.object || {};
30
+ const q = obj.question || {};
31
+ return {
32
+ type: obj.type,
33
+ title: strip(obj.title || q.name || ''),
34
+ excerpt: strip(obj.excerpt || '').substring(0, 100),
35
+ author: obj.author?.name || '',
36
+ votes: obj.voteup_count || 0,
37
+ url: obj.type === 'answer'
38
+ ? 'https://www.zhihu.com/question/' + q.id + '/answer/' + obj.id
39
+ : obj.type === 'article'
40
+ ? 'https://zhuanlan.zhihu.com/p/' + obj.id
41
+ : 'https://www.zhihu.com/question/' + obj.id,
42
+ };
43
+ });
44
+ })()
45
+
46
+ - map:
47
+ rank: ${{ index + 1 }}
48
+ title: ${{ item.title }}
49
+ type: ${{ item.type }}
50
+ author: ${{ item.author }}
51
+ votes: ${{ item.votes }}
52
+
53
+ - limit: ${{ args.limit }}
54
+
55
+ columns: [rank, title, type, author, votes]
package/dist/engine.d.ts CHANGED
@@ -2,5 +2,6 @@
2
2
  * CLI discovery: finds YAML/TS CLI definitions and registers them.
3
3
  */
4
4
  import { type CliCommand } from './registry.js';
5
+ import type { IPage } from './types.js';
5
6
  export declare function discoverClis(...dirs: string[]): void;
6
- export declare function executeCommand(cmd: CliCommand, page: any, kwargs: Record<string, any>, debug?: boolean): Promise<any>;
7
+ export declare function executeCommand(cmd: CliCommand, page: IPage | null, kwargs: Record<string, any>, debug?: boolean): Promise<any>;
package/dist/explore.d.ts CHANGED
@@ -1,17 +1,27 @@
1
1
  /**
2
2
  * Deep Explore: intelligent API discovery with response analysis.
3
3
  *
4
- * Unlike simple page snapshots, Deep Explore intercepts network traffic,
5
- * analyzes response schemas, and automatically infers capabilities that
6
- * can be turned into CLI commands.
7
- *
8
- * Flow:
9
- * 1. Navigate to target URL
10
- * 2. Auto-scroll to trigger lazy loading
11
- * 3. Capture network requests (with body analysis)
12
- * 4. For each JSON response: detect list fields, infer columns, analyze auth
13
- * 5. Detect frontend framework (Vue/React/Pinia/Next.js)
14
- * 6. Generate structured capabilities.json
4
+ * Navigates to the target URL, auto-scrolls to trigger lazy loading,
5
+ * captures network traffic, analyzes JSON responses, and automatically
6
+ * infers CLI capabilities from discovered API endpoints.
15
7
  */
16
- export declare function exploreUrl(url: string, opts?: any): Promise<any>;
17
- export declare function renderExploreSummary(result: any): string;
8
+ export declare function detectSiteName(url: string): string;
9
+ export declare function slugify(value: string): string;
10
+ export interface DiscoveredStore {
11
+ type: 'pinia' | 'vuex';
12
+ id: string;
13
+ actions: string[];
14
+ stateKeys: string[];
15
+ }
16
+ export declare function exploreUrl(url: string, opts: {
17
+ BrowserFactory: new () => any;
18
+ site?: string;
19
+ goal?: string;
20
+ authenticated?: boolean;
21
+ outDir?: string;
22
+ waitSeconds?: number;
23
+ query?: string;
24
+ clickLabels?: string[];
25
+ auto?: boolean;
26
+ }): Promise<Record<string, any>>;
27
+ export declare function renderExploreSummary(result: Record<string, any>): string;