@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.
- package/CLI-CREATOR.md +151 -75
- package/README.md +11 -8
- package/README.zh-CN.md +11 -8
- package/SKILL.md +42 -15
- package/dist/browser.d.ts +11 -1
- package/dist/browser.js +95 -3
- package/dist/clis/bilibili/dynamic.d.ts +1 -0
- package/dist/clis/bilibili/dynamic.js +33 -0
- package/dist/clis/bilibili/ranking.d.ts +1 -0
- package/dist/clis/bilibili/ranking.js +24 -0
- package/dist/clis/bilibili/subtitle.d.ts +1 -0
- package/dist/clis/bilibili/subtitle.js +86 -0
- package/dist/clis/reddit/frontpage.yaml +30 -0
- package/dist/clis/reddit/hot.yaml +3 -2
- package/dist/clis/reddit/search.yaml +34 -0
- package/dist/clis/reddit/subreddit.yaml +39 -0
- package/dist/clis/twitter/bookmarks.yaml +85 -0
- package/dist/clis/twitter/profile.d.ts +1 -0
- package/dist/clis/twitter/profile.js +56 -0
- package/dist/clis/twitter/search.d.ts +1 -0
- package/dist/clis/twitter/search.js +60 -0
- package/dist/clis/twitter/timeline.d.ts +1 -0
- package/dist/clis/twitter/timeline.js +47 -0
- package/dist/clis/xiaohongshu/user.d.ts +1 -0
- package/dist/clis/xiaohongshu/user.js +40 -0
- package/dist/clis/xueqiu/feed.yaml +53 -0
- package/dist/clis/xueqiu/hot-stock.yaml +49 -0
- package/dist/clis/xueqiu/hot.yaml +46 -0
- package/dist/clis/xueqiu/search.yaml +53 -0
- package/dist/clis/xueqiu/stock.yaml +67 -0
- package/dist/clis/xueqiu/watchlist.yaml +46 -0
- package/dist/clis/zhihu/hot.yaml +6 -2
- package/dist/clis/zhihu/search.yaml +3 -1
- package/dist/engine.d.ts +1 -1
- package/dist/engine.js +9 -1
- package/dist/explore.js +50 -0
- package/dist/main.d.ts +1 -1
- package/dist/main.js +12 -5
- package/dist/pipeline/steps/browser.js +4 -8
- package/dist/pipeline/steps/fetch.js +19 -6
- package/dist/pipeline/steps/intercept.js +56 -29
- package/dist/pipeline/steps/tap.js +8 -6
- package/dist/pipeline/template.js +3 -1
- package/dist/pipeline/template.test.js +6 -0
- package/dist/types.d.ts +11 -1
- package/package.json +1 -1
- package/src/browser.ts +101 -6
- package/src/clis/bilibili/dynamic.ts +34 -0
- package/src/clis/bilibili/ranking.ts +25 -0
- package/src/clis/bilibili/subtitle.ts +100 -0
- package/src/clis/reddit/frontpage.yaml +30 -0
- package/src/clis/reddit/hot.yaml +3 -2
- package/src/clis/reddit/search.yaml +34 -0
- package/src/clis/reddit/subreddit.yaml +39 -0
- package/src/clis/twitter/bookmarks.yaml +85 -0
- package/src/clis/twitter/profile.ts +61 -0
- package/src/clis/twitter/search.ts +65 -0
- package/src/clis/twitter/timeline.ts +50 -0
- package/src/clis/xiaohongshu/user.ts +45 -0
- package/src/clis/xueqiu/feed.yaml +53 -0
- package/src/clis/xueqiu/hot-stock.yaml +49 -0
- package/src/clis/xueqiu/hot.yaml +46 -0
- package/src/clis/xueqiu/search.yaml +53 -0
- package/src/clis/xueqiu/stock.yaml +67 -0
- package/src/clis/xueqiu/watchlist.yaml +46 -0
- package/src/clis/zhihu/hot.yaml +6 -2
- package/src/clis/zhihu/search.yaml +3 -1
- package/src/engine.ts +10 -1
- package/src/explore.ts +51 -0
- package/src/main.ts +11 -5
- package/src/pipeline/steps/browser.ts +4 -7
- package/src/pipeline/steps/fetch.ts +22 -6
- package/src/pipeline/steps/intercept.ts +58 -28
- package/src/pipeline/steps/tap.ts +8 -6
- package/src/pipeline/template.test.ts +6 -0
- package/src/pipeline/template.ts +3 -1
- package/src/types.ts +4 -1
- package/dist/clis/index.d.ts +0 -22
- package/dist/clis/index.js +0 -34
- 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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
const itemUrl = String(render(urlTemplate, { args, data, item
|
|
54
|
-
|
|
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:
|
|
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
|
|
80
|
+
// Step 3: Wait a bit for network requests to fire
|
|
33
81
|
await page.wait(Math.min(timeout, 3));
|
|
34
82
|
|
|
35
|
-
// Step
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
await
|
|
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', () => {
|
package/src/pipeline/template.ts
CHANGED
|
@@ -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(
|
|
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
|
}
|
package/dist/clis/index.d.ts
DELETED
|
@@ -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';
|
package/dist/clis/index.js
DELETED
|
@@ -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';
|