@jackwener/opencli 0.2.0 → 0.4.0

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 (80) hide show
  1. package/CLI-CREATOR.md +151 -75
  2. package/README.md +11 -8
  3. package/README.zh-CN.md +11 -8
  4. package/SKILL.md +42 -15
  5. package/dist/browser.d.ts +11 -1
  6. package/dist/browser.js +95 -3
  7. package/dist/clis/bilibili/dynamic.d.ts +1 -0
  8. package/dist/clis/bilibili/dynamic.js +33 -0
  9. package/dist/clis/bilibili/ranking.d.ts +1 -0
  10. package/dist/clis/bilibili/ranking.js +24 -0
  11. package/dist/clis/bilibili/subtitle.d.ts +1 -0
  12. package/dist/clis/bilibili/subtitle.js +86 -0
  13. package/dist/clis/reddit/frontpage.yaml +30 -0
  14. package/dist/clis/reddit/hot.yaml +3 -2
  15. package/dist/clis/reddit/search.yaml +34 -0
  16. package/dist/clis/reddit/subreddit.yaml +39 -0
  17. package/dist/clis/twitter/bookmarks.yaml +85 -0
  18. package/dist/clis/twitter/profile.d.ts +1 -0
  19. package/dist/clis/twitter/profile.js +56 -0
  20. package/dist/clis/twitter/search.d.ts +1 -0
  21. package/dist/clis/twitter/search.js +60 -0
  22. package/dist/clis/twitter/timeline.d.ts +1 -0
  23. package/dist/clis/twitter/timeline.js +47 -0
  24. package/dist/clis/xiaohongshu/user.d.ts +1 -0
  25. package/dist/clis/xiaohongshu/user.js +40 -0
  26. package/dist/clis/xueqiu/feed.yaml +53 -0
  27. package/dist/clis/xueqiu/hot-stock.yaml +49 -0
  28. package/dist/clis/xueqiu/hot.yaml +46 -0
  29. package/dist/clis/xueqiu/search.yaml +53 -0
  30. package/dist/clis/xueqiu/stock.yaml +67 -0
  31. package/dist/clis/xueqiu/watchlist.yaml +46 -0
  32. package/dist/clis/zhihu/hot.yaml +6 -2
  33. package/dist/clis/zhihu/search.yaml +3 -1
  34. package/dist/engine.d.ts +1 -1
  35. package/dist/engine.js +9 -1
  36. package/dist/explore.js +50 -0
  37. package/dist/main.d.ts +1 -1
  38. package/dist/main.js +12 -5
  39. package/dist/pipeline/steps/browser.js +4 -8
  40. package/dist/pipeline/steps/fetch.js +19 -6
  41. package/dist/pipeline/steps/intercept.js +56 -29
  42. package/dist/pipeline/steps/tap.js +8 -6
  43. package/dist/pipeline/template.js +3 -1
  44. package/dist/pipeline/template.test.js +6 -0
  45. package/dist/types.d.ts +11 -1
  46. package/package.json +1 -1
  47. package/src/browser.ts +101 -6
  48. package/src/clis/bilibili/dynamic.ts +34 -0
  49. package/src/clis/bilibili/ranking.ts +25 -0
  50. package/src/clis/bilibili/subtitle.ts +100 -0
  51. package/src/clis/reddit/frontpage.yaml +30 -0
  52. package/src/clis/reddit/hot.yaml +3 -2
  53. package/src/clis/reddit/search.yaml +34 -0
  54. package/src/clis/reddit/subreddit.yaml +39 -0
  55. package/src/clis/twitter/bookmarks.yaml +85 -0
  56. package/src/clis/twitter/profile.ts +61 -0
  57. package/src/clis/twitter/search.ts +65 -0
  58. package/src/clis/twitter/timeline.ts +50 -0
  59. package/src/clis/xiaohongshu/user.ts +45 -0
  60. package/src/clis/xueqiu/feed.yaml +53 -0
  61. package/src/clis/xueqiu/hot-stock.yaml +49 -0
  62. package/src/clis/xueqiu/hot.yaml +46 -0
  63. package/src/clis/xueqiu/search.yaml +53 -0
  64. package/src/clis/xueqiu/stock.yaml +67 -0
  65. package/src/clis/xueqiu/watchlist.yaml +46 -0
  66. package/src/clis/zhihu/hot.yaml +6 -2
  67. package/src/clis/zhihu/search.yaml +3 -1
  68. package/src/engine.ts +10 -1
  69. package/src/explore.ts +51 -0
  70. package/src/main.ts +11 -5
  71. package/src/pipeline/steps/browser.ts +4 -7
  72. package/src/pipeline/steps/fetch.ts +22 -6
  73. package/src/pipeline/steps/intercept.ts +58 -28
  74. package/src/pipeline/steps/tap.ts +8 -6
  75. package/src/pipeline/template.test.ts +6 -0
  76. package/src/pipeline/template.ts +3 -1
  77. package/src/types.ts +4 -1
  78. package/dist/clis/index.d.ts +0 -22
  79. package/dist/clis/index.js +0 -34
  80. package/src/clis/index.ts +0 -46
@@ -31,13 +31,10 @@ export async function stepWait(page: IPage, params: any, data: any, args: Record
31
31
  if (typeof params === 'number') await page.wait(params);
32
32
  else if (typeof params === 'object' && params) {
33
33
  if ('text' in params) {
34
- const timeout = params.timeout ?? 10;
35
- const start = Date.now();
36
- while ((Date.now() - start) / 1000 < timeout) {
37
- const snap = await page.snapshot({ raw: true });
38
- if (typeof snap === 'string' && snap.includes(params.text)) break;
39
- await page.wait(0.5);
40
- }
34
+ await page.wait({
35
+ text: String(render(params.text, { args, data })),
36
+ timeout: params.timeout
37
+ });
41
38
  } else if ('time' in params) await page.wait(Number(params.time));
42
39
  } else if (typeof params === 'string') await page.wait(Number(render(params, { args, data })));
43
40
  return data;
@@ -5,6 +5,23 @@
5
5
  import type { IPage } from '../../types.js';
6
6
  import { render } from '../template.js';
7
7
 
8
+ /** Simple async concurrency limiter */
9
+ async function mapConcurrent<T, R>(items: T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]> {
10
+ const results: R[] = new Array(items.length);
11
+ let index = 0;
12
+
13
+ async function worker() {
14
+ while (index < items.length) {
15
+ const i = index++;
16
+ results[i] = await fn(items[i], i);
17
+ }
18
+ }
19
+
20
+ const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
21
+ await Promise.all(workers);
22
+ return results;
23
+ }
24
+
8
25
  /** Single URL fetch helper */
9
26
  async function fetchSingle(
10
27
  page: IPage | null, url: string, method: string,
@@ -48,12 +65,11 @@ export async function stepFetch(page: IPage | null, params: any, data: any, args
48
65
 
49
66
  // Per-item fetch when data is array and URL references item
50
67
  if (Array.isArray(data) && urlTemplate.includes('item')) {
51
- const results: any[] = [];
52
- for (let i = 0; i < data.length; i++) {
53
- const itemUrl = String(render(urlTemplate, { args, data, item: data[i], index: i }));
54
- results.push(await fetchSingle(page, itemUrl, method, queryParams, headers, args, data));
55
- }
56
- return results;
68
+ const concurrency = typeof params?.concurrency === 'number' ? params.concurrency : 5;
69
+ return mapConcurrent(data, concurrency, async (item, index) => {
70
+ const itemUrl = String(render(urlTemplate, { args, data, item, index }));
71
+ return fetchSingle(page, itemUrl, method, queryParams, headers, args, data);
72
+ });
57
73
  }
58
74
  const url = render(urlOrObj, { args, data });
59
75
  return fetchSingle(page, String(url), method, queryParams, headers, args, data);
@@ -14,7 +14,55 @@ export async function stepIntercept(page: IPage, params: any, data: any, args: R
14
14
 
15
15
  if (!capturePattern) return data;
16
16
 
17
- // Step 1: Execute the trigger action
17
+ // Step 1: Inject fetch/XHR interceptor BEFORE trigger
18
+ await page.evaluate(`
19
+ () => {
20
+ window.__opencli_intercepted = window.__opencli_intercepted || [];
21
+ const pattern = ${JSON.stringify(capturePattern)};
22
+
23
+ if (!window.__opencli_fetch_patched) {
24
+ const origFetch = window.fetch;
25
+ window.fetch = async function(...args) {
26
+ const reqUrl = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url) || '';
27
+ const response = await origFetch.apply(this, args);
28
+ setTimeout(async () => {
29
+ try {
30
+ if (reqUrl.includes(pattern)) {
31
+ const clone = response.clone();
32
+ const json = await clone.json();
33
+ window.__opencli_intercepted.push(json);
34
+ }
35
+ } catch(e) {}
36
+ }, 0);
37
+ return response;
38
+ };
39
+ window.__opencli_fetch_patched = true;
40
+ }
41
+
42
+ if (!window.__opencli_xhr_patched) {
43
+ const XHR = XMLHttpRequest.prototype;
44
+ const open = XHR.open;
45
+ const send = XHR.send;
46
+ XHR.open = function(method, url, ...args) {
47
+ this._reqUrl = url;
48
+ return open.call(this, method, url, ...args);
49
+ };
50
+ XHR.send = function(...args) {
51
+ this.addEventListener('load', function() {
52
+ try {
53
+ if (this._reqUrl && this._reqUrl.includes(pattern)) {
54
+ window.__opencli_intercepted.push(JSON.parse(this.responseText));
55
+ }
56
+ } catch(e) {}
57
+ });
58
+ return send.apply(this, args);
59
+ };
60
+ window.__opencli_xhr_patched = true;
61
+ }
62
+ }
63
+ `);
64
+
65
+ // Step 2: Execute the trigger action
18
66
  if (trigger.startsWith('navigate:')) {
19
67
  const url = render(trigger.slice('navigate:'.length), { args, data });
20
68
  await page.goto(String(url));
@@ -29,36 +77,18 @@ export async function stepIntercept(page: IPage, params: any, data: any, args: R
29
77
  await page.scroll('down');
30
78
  }
31
79
 
32
- // Step 2: Wait a bit for network requests to fire
80
+ // Step 3: Wait a bit for network requests to fire
33
81
  await page.wait(Math.min(timeout, 3));
34
82
 
35
- // Step 3: Get network requests and find matching ones
36
- const rawNetwork = await page.networkRequests(false);
37
- const matchingResponses: any[] = [];
38
-
39
- if (typeof rawNetwork === 'string') {
40
- const lines = rawNetwork.split('\n');
41
- for (const line of lines) {
42
- const match = line.match(/\[?(GET|POST)\]?\s+(\S+)\s*(?:=>|→)\s*\[?(\d+)\]?/i);
43
- if (match) {
44
- const [, , url, status] = match;
45
- if (url.includes(capturePattern) && status === '200') {
46
- try {
47
- const body = await page.evaluate(`
48
- async () => {
49
- try {
50
- const resp = await fetch(${JSON.stringify(url)}, { credentials: 'include' });
51
- if (!resp.ok) return null;
52
- return await resp.json();
53
- } catch { return null; }
54
- }
55
- `);
56
- if (body) matchingResponses.push(body);
57
- } catch {}
58
- }
59
- }
83
+ // Step 4: Retrieve captured data
84
+ const matchingResponses = await page.evaluate(`
85
+ () => {
86
+ const data = window.__opencli_intercepted || [];
87
+ window.__opencli_intercepted = []; // clear after reading
88
+ return data;
60
89
  }
61
- }
90
+ `);
91
+
62
92
 
63
93
  // Step 4: Select from response if specified
64
94
  let result = matchingResponses.length === 1 ? matchingResponses[0] :
@@ -42,6 +42,8 @@ export async function stepTap(page: IPage, params: any, data: any, args: Record<
42
42
  async () => {
43
43
  // ── 1. Setup capture proxy (fetch + XHR dual interception) ──
44
44
  let captured = null;
45
+ let captureResolve;
46
+ const capturePromise = new Promise(r => { captureResolve = r; });
45
47
  const capturePattern = ${JSON.stringify(capturePattern)};
46
48
 
47
49
  // Intercept fetch API
@@ -52,7 +54,7 @@ export async function stepTap(page: IPage, params: any, data: any, args: Record<
52
54
  const url = typeof fetchArgs[0] === 'string' ? fetchArgs[0]
53
55
  : fetchArgs[0] instanceof Request ? fetchArgs[0].url : String(fetchArgs[0]);
54
56
  if (capturePattern && url.includes(capturePattern) && !captured) {
55
- try { captured = await resp.clone().json(); } catch {}
57
+ try { captured = await resp.clone().json(); captureResolve(); } catch {}
56
58
  }
57
59
  } catch {}
58
60
  return resp;
@@ -71,13 +73,13 @@ export async function stepTap(page: IPage, params: any, data: any, args: Record<
71
73
  const origHandler = xhr.onreadystatechange;
72
74
  xhr.onreadystatechange = function() {
73
75
  if (xhr.readyState === 4 && !captured) {
74
- try { captured = JSON.parse(xhr.responseText); } catch {}
76
+ try { captured = JSON.parse(xhr.responseText); captureResolve(); } catch {}
75
77
  }
76
78
  if (origHandler) origHandler.apply(this, arguments);
77
79
  };
78
80
  const origOnload = xhr.onload;
79
81
  xhr.onload = function() {
80
- if (!captured) { try { captured = JSON.parse(xhr.responseText); } catch {} }
82
+ if (!captured) { try { captured = JSON.parse(xhr.responseText); captureResolve(); } catch {} }
81
83
  if (origOnload) origOnload.apply(this, arguments);
82
84
  };
83
85
  }
@@ -117,9 +119,9 @@ export async function stepTap(page: IPage, params: any, data: any, args: Record<
117
119
  await ${actionCall};
118
120
 
119
121
  // ── 4. Wait for network response ──
120
- const deadline = Date.now() + ${timeout} * 1000;
121
- while (!captured && Date.now() < deadline) {
122
- await new Promise(r => setTimeout(r, 200));
122
+ if (!captured) {
123
+ const timeoutPromise = new Promise(r => setTimeout(r, ${timeout} * 1000));
124
+ await Promise.race([capturePromise, timeoutPromise]);
123
125
  }
124
126
  } finally {
125
127
  // ── 5. Always restore originals ──
@@ -75,6 +75,12 @@ describe('evalExpr', () => {
75
75
  it('applies length filter', () => {
76
76
  expect(evalExpr('item.items | length', { item: { items: [1, 2, 3] } })).toBe(3);
77
77
  });
78
+ it('applies json filter to strings with quotes', () => {
79
+ expect(evalExpr('args.keyword | json', { args: { keyword: "O'Reilly" } })).toBe('"O\'Reilly"');
80
+ });
81
+ it('applies json filter to nullish values', () => {
82
+ expect(evalExpr('args.keyword | json', { args: {} })).toBe('null');
83
+ });
78
84
  });
79
85
 
80
86
  describe('render', () => {
@@ -75,7 +75,7 @@ export function evalExpr(expr: string, ctx: RenderContext): any {
75
75
  * Apply a named filter to a value.
76
76
  * Supported filters:
77
77
  * default(val), join(sep), upper, lower, truncate(n), trim,
78
- * replace(old,new), keys, length, first, last
78
+ * replace(old,new), keys, length, first, last, json
79
79
  */
80
80
  function applyFilter(filterExpr: string, value: any): any {
81
81
  const match = filterExpr.match(/^(\w+)(?:\((.+)\))?$/);
@@ -117,6 +117,8 @@ function applyFilter(filterExpr: string, value: any): any {
117
117
  return Array.isArray(value) ? value[0] : value;
118
118
  case 'last':
119
119
  return Array.isArray(value) ? value[value.length - 1] : value;
120
+ case 'json':
121
+ return JSON.stringify(value ?? null);
120
122
  default:
121
123
  return value;
122
124
  }
package/src/types.ts CHANGED
@@ -12,7 +12,7 @@ export interface IPage {
12
12
  click(ref: string): Promise<void>;
13
13
  typeText(ref: string, text: string): Promise<void>;
14
14
  pressKey(key: string): Promise<void>;
15
- wait(seconds: number): Promise<void>;
15
+ wait(options: number | { text?: string; time?: number; timeout?: number }): Promise<void>;
16
16
  tabs(): Promise<any>;
17
17
  closeTab(index?: number): Promise<void>;
18
18
  newTab(): Promise<void>;
@@ -20,4 +20,7 @@ export interface IPage {
20
20
  networkRequests(includeStatic?: boolean): Promise<any>;
21
21
  consoleMessages(level?: string): Promise<any>;
22
22
  scroll(direction?: string, amount?: number): Promise<void>;
23
+ autoScroll(options?: { times?: number; delayMs?: number }): Promise<void>;
24
+ installInterceptor(pattern: string): Promise<void>;
25
+ getInterceptedRequests(): Promise<any[]>;
23
26
  }
@@ -1,22 +0,0 @@
1
- /**
2
- * Import all TypeScript CLI adapters so they self-register.
3
- *
4
- * Each TS adapter calls cli() on import, which adds itself to the global registry.
5
- */
6
- import './bilibili/search.js';
7
- import './bilibili/me.js';
8
- import './bilibili/favorite.js';
9
- import './bilibili/history.js';
10
- import './bilibili/feed.js';
11
- import './bilibili/user-videos.js';
12
- import './github/search.js';
13
- import './zhihu/question.js';
14
- import './xiaohongshu/search.js';
15
- import './bbc/news.js';
16
- import './weibo/hot.js';
17
- import './boss/search.js';
18
- import './yahoo-finance/quote.js';
19
- import './reuters/search.js';
20
- import './smzdm/search.js';
21
- import './ctrip/search.js';
22
- import './youtube/search.js';
@@ -1,34 +0,0 @@
1
- /**
2
- * Import all TypeScript CLI adapters so they self-register.
3
- *
4
- * Each TS adapter calls cli() on import, which adds itself to the global registry.
5
- */
6
- // bilibili
7
- import './bilibili/search.js';
8
- import './bilibili/me.js';
9
- import './bilibili/favorite.js';
10
- import './bilibili/history.js';
11
- import './bilibili/feed.js';
12
- import './bilibili/user-videos.js';
13
- // github
14
- import './github/search.js';
15
- // zhihu
16
- import './zhihu/question.js';
17
- // xiaohongshu
18
- import './xiaohongshu/search.js';
19
- // bbc
20
- import './bbc/news.js';
21
- // weibo
22
- import './weibo/hot.js';
23
- // boss
24
- import './boss/search.js';
25
- // yahoo-finance
26
- import './yahoo-finance/quote.js';
27
- // reuters
28
- import './reuters/search.js';
29
- // smzdm
30
- import './smzdm/search.js';
31
- // ctrip
32
- import './ctrip/search.js';
33
- // youtube
34
- import './youtube/search.js';
package/src/clis/index.ts DELETED
@@ -1,46 +0,0 @@
1
- /**
2
- * Import all TypeScript CLI adapters so they self-register.
3
- *
4
- * Each TS adapter calls cli() on import, which adds itself to the global registry.
5
- */
6
-
7
- // bilibili
8
- import './bilibili/search.js';
9
- import './bilibili/me.js';
10
- import './bilibili/favorite.js';
11
- import './bilibili/history.js';
12
- import './bilibili/feed.js';
13
- import './bilibili/user-videos.js';
14
-
15
- // github
16
- import './github/search.js';
17
-
18
- // zhihu
19
- import './zhihu/question.js';
20
-
21
- // xiaohongshu
22
- import './xiaohongshu/search.js';
23
-
24
- // bbc
25
- import './bbc/news.js';
26
-
27
- // weibo
28
- import './weibo/hot.js';
29
-
30
- // boss
31
- import './boss/search.js';
32
-
33
- // yahoo-finance
34
- import './yahoo-finance/quote.js';
35
-
36
- // reuters
37
- import './reuters/search.js';
38
-
39
- // smzdm
40
- import './smzdm/search.js';
41
-
42
- // ctrip
43
- import './ctrip/search.js';
44
-
45
- // youtube
46
- import './youtube/search.js';