@jackwener/opencli 0.3.0 → 0.4.1
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 +190 -5
- package/README.md +6 -6
- package/README.zh-CN.md +6 -6
- package/SKILL.md +19 -3
- package/dist/browser.d.ts +5 -1
- package/dist/browser.js +33 -5
- package/dist/build-manifest.d.ts +11 -0
- package/dist/build-manifest.js +101 -0
- package/dist/cli-manifest.json +1273 -0
- package/dist/clis/bilibili/following.d.ts +1 -0
- package/dist/clis/bilibili/following.js +41 -0
- package/dist/clis/bilibili/subtitle.d.ts +1 -0
- package/dist/clis/bilibili/subtitle.js +86 -0
- package/dist/engine.d.ts +13 -0
- package/dist/engine.js +122 -17
- package/dist/explore.js +50 -0
- package/dist/main.js +2 -2
- package/dist/pipeline/steps/browser.js +4 -8
- package/dist/pipeline/steps/fetch.js +74 -5
- package/dist/pipeline/steps/tap.js +8 -6
- package/dist/registry.d.ts +3 -0
- package/dist/types.d.ts +5 -1
- package/package.json +3 -2
- package/src/browser.ts +33 -6
- package/src/build-manifest.ts +133 -0
- package/src/clis/bilibili/following.ts +50 -0
- package/src/clis/bilibili/subtitle.ts +100 -0
- package/src/engine.ts +123 -17
- package/src/explore.ts +51 -0
- package/src/main.ts +2 -2
- package/src/pipeline/steps/browser.ts +4 -7
- package/src/pipeline/steps/fetch.ts +83 -5
- package/src/pipeline/steps/tap.ts +8 -6
- package/src/registry.ts +3 -0
- package/src/types.ts +1 -1
|
@@ -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,
|
|
@@ -39,6 +56,46 @@ async function fetchSingle(
|
|
|
39
56
|
`);
|
|
40
57
|
}
|
|
41
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Batch fetch: send all URLs into the browser as a single evaluate() call.
|
|
61
|
+
* This eliminates N-1 cross-process IPC round trips, performing all fetches
|
|
62
|
+
* inside the V8 engine and returning results as one JSON array.
|
|
63
|
+
*/
|
|
64
|
+
async function fetchBatchInBrowser(
|
|
65
|
+
page: IPage, urls: string[], method: string,
|
|
66
|
+
headers: Record<string, string>, concurrency: number,
|
|
67
|
+
): Promise<any[]> {
|
|
68
|
+
const headersJs = JSON.stringify(headers);
|
|
69
|
+
const urlsJs = JSON.stringify(urls);
|
|
70
|
+
return page.evaluate(`
|
|
71
|
+
async () => {
|
|
72
|
+
const urls = ${urlsJs};
|
|
73
|
+
const method = "${method}";
|
|
74
|
+
const headers = ${headersJs};
|
|
75
|
+
const concurrency = ${concurrency};
|
|
76
|
+
|
|
77
|
+
const results = new Array(urls.length);
|
|
78
|
+
let idx = 0;
|
|
79
|
+
|
|
80
|
+
async function worker() {
|
|
81
|
+
while (idx < urls.length) {
|
|
82
|
+
const i = idx++;
|
|
83
|
+
try {
|
|
84
|
+
const resp = await fetch(urls[i], { method, headers, credentials: "include" });
|
|
85
|
+
results[i] = await resp.json();
|
|
86
|
+
} catch (e) {
|
|
87
|
+
results[i] = { error: e.message };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const workers = Array.from({ length: Math.min(concurrency, urls.length) }, () => worker());
|
|
93
|
+
await Promise.all(workers);
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
96
|
+
`);
|
|
97
|
+
}
|
|
98
|
+
|
|
42
99
|
export async function stepFetch(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any> {
|
|
43
100
|
const urlOrObj = typeof params === 'string' ? params : (params?.url ?? '');
|
|
44
101
|
const method = params?.method ?? 'GET';
|
|
@@ -48,12 +105,33 @@ export async function stepFetch(page: IPage | null, params: any, data: any, args
|
|
|
48
105
|
|
|
49
106
|
// Per-item fetch when data is array and URL references item
|
|
50
107
|
if (Array.isArray(data) && urlTemplate.includes('item')) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
108
|
+
const concurrency = typeof params?.concurrency === 'number' ? params.concurrency : 5;
|
|
109
|
+
|
|
110
|
+
// Render all URLs upfront
|
|
111
|
+
const renderedHeaders: Record<string, string> = {};
|
|
112
|
+
for (const [k, v] of Object.entries(headers)) renderedHeaders[k] = String(render(v, { args, data }));
|
|
113
|
+
const renderedParams: Record<string, string> = {};
|
|
114
|
+
for (const [k, v] of Object.entries(queryParams)) renderedParams[k] = String(render(v, { args, data }));
|
|
115
|
+
|
|
116
|
+
const urls = data.map((item: any, index: number) => {
|
|
117
|
+
let url = String(render(urlTemplate, { args, data, item, index }));
|
|
118
|
+
if (Object.keys(renderedParams).length > 0) {
|
|
119
|
+
const qs = new URLSearchParams(renderedParams).toString();
|
|
120
|
+
url = `${url}${url.includes('?') ? '&' : '?'}${qs}`;
|
|
121
|
+
}
|
|
122
|
+
return url;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// BATCH IPC: if browser is available, batch all fetches into a single evaluate() call
|
|
126
|
+
if (page !== null) {
|
|
127
|
+
return fetchBatchInBrowser(page, urls, method.toUpperCase(), renderedHeaders, concurrency);
|
|
55
128
|
}
|
|
56
|
-
|
|
129
|
+
|
|
130
|
+
// Non-browser: use concurrent pool (already optimized)
|
|
131
|
+
return mapConcurrent(data, concurrency, async (item, index) => {
|
|
132
|
+
const itemUrl = String(render(urlTemplate, { args, data, item, index }));
|
|
133
|
+
return fetchSingle(null, itemUrl, method, queryParams, headers, args, data);
|
|
134
|
+
});
|
|
57
135
|
}
|
|
58
136
|
const url = render(urlOrObj, { args, data });
|
|
59
137
|
return fetchSingle(page, String(url), method, queryParams, headers, args, data);
|
|
@@ -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 ──
|
package/src/registry.ts
CHANGED
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>;
|