@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.
- package/CLI-CREATOR.md +594 -0
- package/README.md +124 -39
- package/README.zh-CN.md +151 -0
- package/SKILL.md +178 -102
- package/dist/bilibili.d.ts +6 -5
- package/dist/browser.d.ts +3 -1
- package/dist/browser.js +44 -2
- package/dist/cascade.d.ts +46 -0
- package/dist/cascade.js +180 -0
- package/dist/clis/bbc/news.js +42 -0
- package/dist/clis/bilibili/hot.yaml +38 -0
- package/dist/clis/boss/search.js +47 -0
- package/dist/clis/ctrip/search.d.ts +1 -0
- package/dist/clis/ctrip/search.js +62 -0
- package/dist/clis/hackernews/top.yaml +36 -0
- package/dist/clis/index.d.ts +10 -1
- package/dist/clis/index.js +19 -1
- package/dist/clis/reddit/hot.yaml +46 -0
- package/dist/clis/reuters/search.d.ts +1 -0
- package/dist/clis/reuters/search.js +52 -0
- package/dist/clis/smzdm/search.d.ts +1 -0
- package/dist/clis/smzdm/search.js +66 -0
- package/dist/clis/twitter/trending.yaml +40 -0
- package/dist/clis/v2ex/hot.yaml +25 -0
- package/dist/clis/v2ex/latest.yaml +25 -0
- package/dist/clis/v2ex/topic.yaml +27 -0
- package/dist/clis/weibo/hot.d.ts +1 -0
- package/dist/clis/weibo/hot.js +41 -0
- package/dist/clis/xiaohongshu/feed.yaml +32 -0
- package/dist/clis/xiaohongshu/notifications.yaml +38 -0
- package/dist/clis/xiaohongshu/search.d.ts +5 -0
- package/dist/clis/xiaohongshu/search.js +68 -0
- package/dist/clis/yahoo-finance/quote.d.ts +1 -0
- package/dist/clis/yahoo-finance/quote.js +74 -0
- package/dist/clis/youtube/search.d.ts +1 -0
- package/dist/clis/youtube/search.js +60 -0
- package/dist/clis/zhihu/hot.yaml +42 -0
- package/dist/clis/zhihu/question.d.ts +1 -0
- package/dist/clis/zhihu/question.js +39 -0
- package/dist/clis/zhihu/search.yaml +55 -0
- package/dist/engine.d.ts +2 -1
- package/dist/explore.d.ts +23 -13
- package/dist/explore.js +293 -422
- package/dist/generate.js +2 -1
- package/dist/main.js +21 -2
- package/dist/pipeline/executor.d.ts +9 -0
- package/dist/pipeline/executor.js +88 -0
- package/dist/pipeline/index.d.ts +5 -0
- package/dist/pipeline/index.js +5 -0
- package/dist/pipeline/steps/browser.d.ts +12 -0
- package/dist/pipeline/steps/browser.js +68 -0
- package/dist/pipeline/steps/fetch.d.ts +5 -0
- package/dist/pipeline/steps/fetch.js +50 -0
- package/dist/pipeline/steps/intercept.d.ts +5 -0
- package/dist/pipeline/steps/intercept.js +75 -0
- package/dist/pipeline/steps/tap.d.ts +12 -0
- package/dist/pipeline/steps/tap.js +130 -0
- package/dist/pipeline/steps/transform.d.ts +8 -0
- package/dist/pipeline/steps/transform.js +53 -0
- package/dist/pipeline/template.d.ts +16 -0
- package/dist/pipeline/template.js +115 -0
- package/dist/pipeline/template.test.d.ts +4 -0
- package/dist/pipeline/template.test.js +102 -0
- package/dist/pipeline/transform.test.d.ts +4 -0
- package/dist/pipeline/transform.test.js +90 -0
- package/dist/pipeline.d.ts +5 -7
- package/dist/pipeline.js +5 -313
- package/dist/registry.d.ts +3 -2
- package/dist/runtime.d.ts +2 -1
- package/dist/synthesize.d.ts +11 -8
- package/dist/synthesize.js +142 -118
- package/dist/types.d.ts +27 -0
- package/dist/types.js +7 -0
- package/package.json +9 -4
- package/src/bilibili.ts +9 -7
- package/src/browser.ts +41 -3
- package/src/cascade.ts +218 -0
- package/src/clis/bbc/news.ts +42 -0
- package/src/clis/boss/search.ts +47 -0
- package/src/clis/ctrip/search.ts +62 -0
- package/src/clis/index.ts +28 -1
- package/src/clis/reddit/hot.yaml +46 -0
- package/src/clis/reuters/search.ts +52 -0
- package/src/clis/smzdm/search.ts +66 -0
- package/src/clis/v2ex/hot.yaml +5 -9
- package/src/clis/v2ex/latest.yaml +5 -8
- package/src/clis/v2ex/topic.yaml +27 -0
- package/src/clis/weibo/hot.ts +41 -0
- package/src/clis/xiaohongshu/feed.yaml +32 -0
- package/src/clis/xiaohongshu/notifications.yaml +38 -0
- package/src/clis/xiaohongshu/search.ts +71 -0
- package/src/clis/yahoo-finance/quote.ts +74 -0
- package/src/clis/youtube/search.ts +60 -0
- package/src/clis/zhihu/hot.yaml +22 -8
- package/src/clis/zhihu/question.ts +45 -0
- package/src/clis/zhihu/search.yaml +55 -0
- package/src/engine.ts +2 -1
- package/src/explore.ts +303 -465
- package/src/generate.ts +3 -1
- package/src/main.ts +18 -2
- package/src/pipeline/executor.ts +98 -0
- package/src/pipeline/index.ts +6 -0
- package/src/pipeline/steps/browser.ts +67 -0
- package/src/pipeline/steps/fetch.ts +60 -0
- package/src/pipeline/steps/intercept.ts +78 -0
- package/src/pipeline/steps/tap.ts +137 -0
- package/src/pipeline/steps/transform.ts +50 -0
- package/src/pipeline/template.test.ts +107 -0
- package/src/pipeline/template.ts +101 -0
- package/src/pipeline/transform.test.ts +107 -0
- package/src/pipeline.ts +5 -292
- package/src/registry.ts +4 -2
- package/src/runtime.ts +3 -1
- package/src/synthesize.ts +142 -137
- package/src/types.ts +23 -0
- package/vitest.config.ts +7 -0
- package/dist/clis/github/search.js +0 -20
- package/dist/clis/zhihu/search.js +0 -58
- package/dist/promote.d.ts +0 -1
- package/dist/promote.js +0 -3
- package/dist/register.d.ts +0 -2
- package/dist/register.js +0 -2
- package/dist/scaffold.d.ts +0 -2
- package/dist/scaffold.js +0 -2
- package/dist/smoke.d.ts +0 -2
- package/dist/smoke.js +0 -2
- package/src/clis/github/search.ts +0 -21
- package/src/clis/github/trending.yaml +0 -58
- package/src/clis/zhihu/search.ts +0 -65
- package/src/promote.ts +0 -3
- package/src/register.ts +0 -2
- package/src/scaffold.ts +0 -2
- package/src/smoke.ts +0 -2
- /package/dist/clis/{github/search.d.ts → bbc/news.d.ts} +0 -0
- /package/dist/clis/{zhihu → boss}/search.d.ts +0 -0
package/dist/generate.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { exploreUrl } from './explore.js';
|
|
11
11
|
import { synthesizeFromExplore } from './synthesize.js';
|
|
12
|
-
|
|
12
|
+
// TODO: implement real CLI registration (copy candidate YAML to user clis dir)
|
|
13
|
+
function registerCandidates(_opts) { return { ok: true, count: 0 }; }
|
|
13
14
|
const CAPABILITY_ALIASES = {
|
|
14
15
|
search: ['search', '搜索', '查找', 'query', 'keyword'],
|
|
15
16
|
hot: ['hot', '热门', '热榜', '热搜', 'popular', 'top', 'ranking'],
|
package/dist/main.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* opencli — Make any website your CLI. AI-powered.
|
|
4
4
|
*/
|
|
5
|
+
import * as fs from 'node:fs';
|
|
5
6
|
import * as os from 'node:os';
|
|
6
7
|
import * as path from 'node:path';
|
|
7
8
|
import { fileURLToPath } from 'node:url';
|
|
@@ -17,9 +18,12 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
17
18
|
const __dirname = path.dirname(__filename);
|
|
18
19
|
const BUILTIN_CLIS = path.resolve(__dirname, 'clis');
|
|
19
20
|
const USER_CLIS = path.join(os.homedir(), '.opencli', 'clis');
|
|
21
|
+
// Read version from package.json (single source of truth)
|
|
22
|
+
const pkgJsonPath = path.resolve(__dirname, '..', 'package.json');
|
|
23
|
+
const PKG_VERSION = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')).version ?? '0.0.0';
|
|
20
24
|
discoverClis(BUILTIN_CLIS, USER_CLIS);
|
|
21
25
|
const program = new Command();
|
|
22
|
-
program.name('opencli').description('Make any website your CLI. Zero setup. AI-powered.').version(
|
|
26
|
+
program.name('opencli').description('Make any website your CLI. Zero setup. AI-powered.').version(PKG_VERSION);
|
|
23
27
|
// ── Built-in commands ──────────────────────────────────────────────────────
|
|
24
28
|
program.command('list').description('List all available CLI commands').option('--json', 'JSON output')
|
|
25
29
|
.action((opts) => {
|
|
@@ -53,12 +57,27 @@ program.command('validate').description('Validate CLI definitions').argument('[t
|
|
|
53
57
|
.action(async (target) => { const { validateClisWithTarget, renderValidationReport } = await import('./validate.js'); console.log(renderValidationReport(validateClisWithTarget([BUILTIN_CLIS, USER_CLIS], target))); });
|
|
54
58
|
program.command('verify').description('Validate + smoke test').argument('[target]').option('--smoke', 'Run smoke tests', false)
|
|
55
59
|
.action(async (target, opts) => { const { verifyClis, renderVerifyReport } = await import('./verify.js'); const r = await verifyClis({ builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, target, smoke: opts.smoke }); console.log(renderVerifyReport(r)); process.exitCode = r.ok ? 0 : 1; });
|
|
56
|
-
program.command('explore').description('Explore a website').argument('<url>').option('--site <name>').option('--goal <text>').option('--wait <s>', '', '3')
|
|
60
|
+
program.command('explore').alias('probe').description('Explore a website: discover APIs, stores, and recommend strategies').argument('<url>').option('--site <name>').option('--goal <text>').option('--wait <s>', '', '3')
|
|
57
61
|
.action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: PlaywrightMCP, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait) }))); });
|
|
58
62
|
program.command('synthesize').description('Synthesize CLIs from explore').argument('<target>').option('--top <n>', '', '3')
|
|
59
63
|
.action(async (target, opts) => { const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js'); console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) }))); });
|
|
60
64
|
program.command('generate').description('One-shot: explore → synthesize → register').argument('<url>').option('--goal <text>').option('--site <name>')
|
|
61
65
|
.action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const r = await generateCliFromUrl({ url, BrowserFactory: PlaywrightMCP, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; });
|
|
66
|
+
program.command('cascade').description('Strategy cascade: find simplest working strategy').argument('<url>').option('--site <name>')
|
|
67
|
+
.action(async (url, opts) => {
|
|
68
|
+
const { cascadeProbe, renderCascadeResult } = await import('./cascade.js');
|
|
69
|
+
const result = await browserSession(PlaywrightMCP, async (page) => {
|
|
70
|
+
// Navigate to the site first for cookie context
|
|
71
|
+
try {
|
|
72
|
+
const siteUrl = new URL(url);
|
|
73
|
+
await page.goto(`${siteUrl.protocol}//${siteUrl.host}`);
|
|
74
|
+
await page.wait(2);
|
|
75
|
+
}
|
|
76
|
+
catch { }
|
|
77
|
+
return cascadeProbe(page, url);
|
|
78
|
+
});
|
|
79
|
+
console.log(renderCascadeResult(result));
|
|
80
|
+
});
|
|
62
81
|
// ── Dynamic site commands ──────────────────────────────────────────────────
|
|
63
82
|
const registry = getRegistry();
|
|
64
83
|
const siteGroups = new Map();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline executor: runs YAML pipeline steps sequentially.
|
|
3
|
+
*/
|
|
4
|
+
import type { IPage } from '../types.js';
|
|
5
|
+
export interface PipelineContext {
|
|
6
|
+
args?: Record<string, any>;
|
|
7
|
+
debug?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function executePipeline(page: IPage | null, pipeline: any[], ctx?: PipelineContext): Promise<any>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline executor: runs YAML pipeline steps sequentially.
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { stepNavigate, stepClick, stepType, stepWait, stepPress, stepSnapshot, stepEvaluate } from './steps/browser.js';
|
|
6
|
+
import { stepFetch } from './steps/fetch.js';
|
|
7
|
+
import { stepSelect, stepMap, stepFilter, stepSort, stepLimit } from './steps/transform.js';
|
|
8
|
+
import { stepIntercept } from './steps/intercept.js';
|
|
9
|
+
import { stepTap } from './steps/tap.js';
|
|
10
|
+
/** Registry of all available step handlers */
|
|
11
|
+
const STEP_HANDLERS = {
|
|
12
|
+
navigate: stepNavigate,
|
|
13
|
+
fetch: stepFetch,
|
|
14
|
+
select: stepSelect,
|
|
15
|
+
evaluate: stepEvaluate,
|
|
16
|
+
snapshot: stepSnapshot,
|
|
17
|
+
click: stepClick,
|
|
18
|
+
type: stepType,
|
|
19
|
+
wait: stepWait,
|
|
20
|
+
press: stepPress,
|
|
21
|
+
map: stepMap,
|
|
22
|
+
filter: stepFilter,
|
|
23
|
+
sort: stepSort,
|
|
24
|
+
limit: stepLimit,
|
|
25
|
+
intercept: stepIntercept,
|
|
26
|
+
tap: stepTap,
|
|
27
|
+
};
|
|
28
|
+
export async function executePipeline(page, pipeline, ctx = {}) {
|
|
29
|
+
const args = ctx.args ?? {};
|
|
30
|
+
const debug = ctx.debug ?? false;
|
|
31
|
+
let data = null;
|
|
32
|
+
const total = pipeline.length;
|
|
33
|
+
for (let i = 0; i < pipeline.length; i++) {
|
|
34
|
+
const step = pipeline[i];
|
|
35
|
+
if (!step || typeof step !== 'object')
|
|
36
|
+
continue;
|
|
37
|
+
for (const [op, params] of Object.entries(step)) {
|
|
38
|
+
if (debug)
|
|
39
|
+
debugStepStart(i + 1, total, op, params);
|
|
40
|
+
const handler = STEP_HANDLERS[op];
|
|
41
|
+
if (handler) {
|
|
42
|
+
data = await handler(page, params, data, args);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
if (debug)
|
|
46
|
+
process.stderr.write(` ${chalk.yellow('⚠')} Unknown step: ${op}\n`);
|
|
47
|
+
}
|
|
48
|
+
// Detect error objects returned by steps (e.g. tap store not found)
|
|
49
|
+
if (data && typeof data === 'object' && !Array.isArray(data) && data.error) {
|
|
50
|
+
process.stderr.write(` ${chalk.yellow('⚠')} ${chalk.yellow(op)}: ${data.error}\n`);
|
|
51
|
+
if (data.hint)
|
|
52
|
+
process.stderr.write(` ${chalk.dim('💡')} ${chalk.dim(data.hint)}\n`);
|
|
53
|
+
}
|
|
54
|
+
if (debug)
|
|
55
|
+
debugStepResult(op, data);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return data;
|
|
59
|
+
}
|
|
60
|
+
function debugStepStart(stepNum, total, op, params) {
|
|
61
|
+
let preview = '';
|
|
62
|
+
if (typeof params === 'string') {
|
|
63
|
+
preview = params.length <= 80 ? ` → ${params}` : ` → ${params.slice(0, 77)}...`;
|
|
64
|
+
}
|
|
65
|
+
else if (params && typeof params === 'object' && !Array.isArray(params)) {
|
|
66
|
+
preview = ` (${Object.keys(params).join(', ')})`;
|
|
67
|
+
}
|
|
68
|
+
process.stderr.write(` ${chalk.dim(`[${stepNum}/${total}]`)} ${chalk.bold.cyan(op)}${preview}\n`);
|
|
69
|
+
}
|
|
70
|
+
function debugStepResult(op, data) {
|
|
71
|
+
if (data === null || data === undefined) {
|
|
72
|
+
process.stderr.write(` ${chalk.dim('→ (no data)')}\n`);
|
|
73
|
+
}
|
|
74
|
+
else if (Array.isArray(data)) {
|
|
75
|
+
process.stderr.write(` ${chalk.dim(`→ ${data.length} items`)}\n`);
|
|
76
|
+
}
|
|
77
|
+
else if (typeof data === 'object') {
|
|
78
|
+
const keys = Object.keys(data).slice(0, 5);
|
|
79
|
+
process.stderr.write(` ${chalk.dim(`→ dict (${keys.join(', ')}${Object.keys(data).length > 5 ? '...' : ''})`)}\n`);
|
|
80
|
+
}
|
|
81
|
+
else if (typeof data === 'string') {
|
|
82
|
+
const p = data.slice(0, 60).replace(/\n/g, '\\n');
|
|
83
|
+
process.stderr.write(` ${chalk.dim(`→ "${p}${data.length > 60 ? '...' : ''}"`)}\n`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
process.stderr.write(` ${chalk.dim(`→ ${typeof data}`)}\n`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline step: navigate, click, type, wait, press, snapshot.
|
|
3
|
+
* Browser interaction primitives.
|
|
4
|
+
*/
|
|
5
|
+
import type { IPage } from '../../types.js';
|
|
6
|
+
export declare function stepNavigate(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
7
|
+
export declare function stepClick(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
8
|
+
export declare function stepType(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
9
|
+
export declare function stepWait(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
10
|
+
export declare function stepPress(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
11
|
+
export declare function stepSnapshot(page: IPage, params: any, _data: any, _args: Record<string, any>): Promise<any>;
|
|
12
|
+
export declare function stepEvaluate(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline step: navigate, click, type, wait, press, snapshot.
|
|
3
|
+
* Browser interaction primitives.
|
|
4
|
+
*/
|
|
5
|
+
import { render, normalizeEvaluateSource } from '../template.js';
|
|
6
|
+
export async function stepNavigate(page, params, data, args) {
|
|
7
|
+
const url = render(params, { args, data });
|
|
8
|
+
await page.goto(String(url));
|
|
9
|
+
return data;
|
|
10
|
+
}
|
|
11
|
+
export async function stepClick(page, params, data, args) {
|
|
12
|
+
await page.click(String(render(params, { args, data })).replace(/^@/, ''));
|
|
13
|
+
return data;
|
|
14
|
+
}
|
|
15
|
+
export async function stepType(page, params, data, args) {
|
|
16
|
+
if (typeof params === 'object' && params) {
|
|
17
|
+
const ref = String(render(params.ref ?? '', { args, data })).replace(/^@/, '');
|
|
18
|
+
const text = String(render(params.text ?? '', { args, data }));
|
|
19
|
+
await page.typeText(ref, text);
|
|
20
|
+
if (params.submit)
|
|
21
|
+
await page.pressKey('Enter');
|
|
22
|
+
}
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
export async function stepWait(page, params, data, args) {
|
|
26
|
+
if (typeof params === 'number')
|
|
27
|
+
await page.wait(params);
|
|
28
|
+
else if (typeof params === 'object' && params) {
|
|
29
|
+
if ('text' in params) {
|
|
30
|
+
const timeout = params.timeout ?? 10;
|
|
31
|
+
const start = Date.now();
|
|
32
|
+
while ((Date.now() - start) / 1000 < timeout) {
|
|
33
|
+
const snap = await page.snapshot({ raw: true });
|
|
34
|
+
if (typeof snap === 'string' && snap.includes(params.text))
|
|
35
|
+
break;
|
|
36
|
+
await page.wait(0.5);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if ('time' in params)
|
|
40
|
+
await page.wait(Number(params.time));
|
|
41
|
+
}
|
|
42
|
+
else if (typeof params === 'string')
|
|
43
|
+
await page.wait(Number(render(params, { args, data })));
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
46
|
+
export async function stepPress(page, params, data, args) {
|
|
47
|
+
await page.pressKey(String(render(params, { args, data })));
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
export async function stepSnapshot(page, params, _data, _args) {
|
|
51
|
+
const opts = (typeof params === 'object' && params) ? params : {};
|
|
52
|
+
return page.snapshot({ interactive: opts.interactive ?? false, compact: opts.compact ?? false, maxDepth: opts.max_depth, raw: opts.raw ?? false });
|
|
53
|
+
}
|
|
54
|
+
export async function stepEvaluate(page, params, data, args) {
|
|
55
|
+
const js = String(render(params, { args, data }));
|
|
56
|
+
let result = await page.evaluate(normalizeEvaluateSource(js));
|
|
57
|
+
// MCP may return JSON as a string — auto-parse it
|
|
58
|
+
if (typeof result === 'string') {
|
|
59
|
+
const trimmed = result.trim();
|
|
60
|
+
if ((trimmed.startsWith('[') && trimmed.endsWith(']')) || (trimmed.startsWith('{') && trimmed.endsWith('}'))) {
|
|
61
|
+
try {
|
|
62
|
+
result = JSON.parse(trimmed);
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline step: fetch — HTTP API requests.
|
|
3
|
+
*/
|
|
4
|
+
import { render } from '../template.js';
|
|
5
|
+
/** Single URL fetch helper */
|
|
6
|
+
async function fetchSingle(page, url, method, queryParams, headers, args, data) {
|
|
7
|
+
const renderedParams = {};
|
|
8
|
+
for (const [k, v] of Object.entries(queryParams))
|
|
9
|
+
renderedParams[k] = String(render(v, { args, data }));
|
|
10
|
+
const renderedHeaders = {};
|
|
11
|
+
for (const [k, v] of Object.entries(headers))
|
|
12
|
+
renderedHeaders[k] = String(render(v, { args, data }));
|
|
13
|
+
let finalUrl = url;
|
|
14
|
+
if (Object.keys(renderedParams).length > 0) {
|
|
15
|
+
const qs = new URLSearchParams(renderedParams).toString();
|
|
16
|
+
finalUrl = `${finalUrl}${finalUrl.includes('?') ? '&' : '?'}${qs}`;
|
|
17
|
+
}
|
|
18
|
+
if (page === null) {
|
|
19
|
+
const resp = await fetch(finalUrl, { method: method.toUpperCase(), headers: renderedHeaders });
|
|
20
|
+
return resp.json();
|
|
21
|
+
}
|
|
22
|
+
const headersJs = JSON.stringify(renderedHeaders);
|
|
23
|
+
const escapedUrl = finalUrl.replace(/"/g, '\\"');
|
|
24
|
+
return page.evaluate(`
|
|
25
|
+
async () => {
|
|
26
|
+
const resp = await fetch("${escapedUrl}", {
|
|
27
|
+
method: "${method}", headers: ${headersJs}, credentials: "include"
|
|
28
|
+
});
|
|
29
|
+
return await resp.json();
|
|
30
|
+
}
|
|
31
|
+
`);
|
|
32
|
+
}
|
|
33
|
+
export async function stepFetch(page, params, data, args) {
|
|
34
|
+
const urlOrObj = typeof params === 'string' ? params : (params?.url ?? '');
|
|
35
|
+
const method = params?.method ?? 'GET';
|
|
36
|
+
const queryParams = params?.params ?? {};
|
|
37
|
+
const headers = params?.headers ?? {};
|
|
38
|
+
const urlTemplate = String(urlOrObj);
|
|
39
|
+
// Per-item fetch when data is array and URL references item
|
|
40
|
+
if (Array.isArray(data) && urlTemplate.includes('item')) {
|
|
41
|
+
const results = [];
|
|
42
|
+
for (let i = 0; i < data.length; i++) {
|
|
43
|
+
const itemUrl = String(render(urlTemplate, { args, data, item: data[i], index: i }));
|
|
44
|
+
results.push(await fetchSingle(page, itemUrl, method, queryParams, headers, args, data));
|
|
45
|
+
}
|
|
46
|
+
return results;
|
|
47
|
+
}
|
|
48
|
+
const url = render(urlOrObj, { args, data });
|
|
49
|
+
return fetchSingle(page, String(url), method, queryParams, headers, args, data);
|
|
50
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline step: intercept — declarative XHR interception.
|
|
3
|
+
*/
|
|
4
|
+
import { render } from '../template.js';
|
|
5
|
+
export async function stepIntercept(page, params, data, args) {
|
|
6
|
+
const cfg = typeof params === 'object' ? params : {};
|
|
7
|
+
const trigger = cfg.trigger ?? '';
|
|
8
|
+
const capturePattern = cfg.capture ?? '';
|
|
9
|
+
const timeout = cfg.timeout ?? 8;
|
|
10
|
+
const selectPath = cfg.select ?? null;
|
|
11
|
+
if (!capturePattern)
|
|
12
|
+
return data;
|
|
13
|
+
// Step 1: Execute the trigger action
|
|
14
|
+
if (trigger.startsWith('navigate:')) {
|
|
15
|
+
const url = render(trigger.slice('navigate:'.length), { args, data });
|
|
16
|
+
await page.goto(String(url));
|
|
17
|
+
}
|
|
18
|
+
else if (trigger.startsWith('evaluate:')) {
|
|
19
|
+
const js = trigger.slice('evaluate:'.length);
|
|
20
|
+
const { normalizeEvaluateSource } = await import('../template.js');
|
|
21
|
+
await page.evaluate(normalizeEvaluateSource(render(js, { args, data })));
|
|
22
|
+
}
|
|
23
|
+
else if (trigger.startsWith('click:')) {
|
|
24
|
+
const ref = render(trigger.slice('click:'.length), { args, data });
|
|
25
|
+
await page.click(String(ref).replace(/^@/, ''));
|
|
26
|
+
}
|
|
27
|
+
else if (trigger === 'scroll') {
|
|
28
|
+
await page.scroll('down');
|
|
29
|
+
}
|
|
30
|
+
// Step 2: Wait a bit for network requests to fire
|
|
31
|
+
await page.wait(Math.min(timeout, 3));
|
|
32
|
+
// Step 3: Get network requests and find matching ones
|
|
33
|
+
const rawNetwork = await page.networkRequests(false);
|
|
34
|
+
const matchingResponses = [];
|
|
35
|
+
if (typeof rawNetwork === 'string') {
|
|
36
|
+
const lines = rawNetwork.split('\n');
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
const match = line.match(/\[?(GET|POST)\]?\s+(\S+)\s*(?:=>|→)\s*\[?(\d+)\]?/i);
|
|
39
|
+
if (match) {
|
|
40
|
+
const [, , url, status] = match;
|
|
41
|
+
if (url.includes(capturePattern) && status === '200') {
|
|
42
|
+
try {
|
|
43
|
+
const body = await page.evaluate(`
|
|
44
|
+
async () => {
|
|
45
|
+
try {
|
|
46
|
+
const resp = await fetch(${JSON.stringify(url)}, { credentials: 'include' });
|
|
47
|
+
if (!resp.ok) return null;
|
|
48
|
+
return await resp.json();
|
|
49
|
+
} catch { return null; }
|
|
50
|
+
}
|
|
51
|
+
`);
|
|
52
|
+
if (body)
|
|
53
|
+
matchingResponses.push(body);
|
|
54
|
+
}
|
|
55
|
+
catch { }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Step 4: Select from response if specified
|
|
61
|
+
let result = matchingResponses.length === 1 ? matchingResponses[0] :
|
|
62
|
+
matchingResponses.length > 1 ? matchingResponses : data;
|
|
63
|
+
if (selectPath && result) {
|
|
64
|
+
let current = result;
|
|
65
|
+
for (const part of String(selectPath).split('.')) {
|
|
66
|
+
if (current && typeof current === 'object' && !Array.isArray(current)) {
|
|
67
|
+
current = current[part];
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
result = current ?? result;
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline step: tap — declarative Store Action Bridge.
|
|
3
|
+
*
|
|
4
|
+
* Generates a self-contained IIFE that:
|
|
5
|
+
* 1. Injects fetch + XHR dual interception proxy
|
|
6
|
+
* 2. Finds the Pinia/Vuex store and calls the action
|
|
7
|
+
* 3. Captures the response matching the URL pattern
|
|
8
|
+
* 4. Auto-cleans up interception in finally block
|
|
9
|
+
* 5. Returns the captured data (optionally sub-selected)
|
|
10
|
+
*/
|
|
11
|
+
import type { IPage } from '../../types.js';
|
|
12
|
+
export declare function stepTap(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline step: tap — declarative Store Action Bridge.
|
|
3
|
+
*
|
|
4
|
+
* Generates a self-contained IIFE that:
|
|
5
|
+
* 1. Injects fetch + XHR dual interception proxy
|
|
6
|
+
* 2. Finds the Pinia/Vuex store and calls the action
|
|
7
|
+
* 3. Captures the response matching the URL pattern
|
|
8
|
+
* 4. Auto-cleans up interception in finally block
|
|
9
|
+
* 5. Returns the captured data (optionally sub-selected)
|
|
10
|
+
*/
|
|
11
|
+
import { render } from '../template.js';
|
|
12
|
+
export async function stepTap(page, params, data, args) {
|
|
13
|
+
const cfg = typeof params === 'object' ? params : {};
|
|
14
|
+
const storeName = String(render(cfg.store ?? '', { args, data }));
|
|
15
|
+
const actionName = String(render(cfg.action ?? '', { args, data }));
|
|
16
|
+
const capturePattern = String(render(cfg.capture ?? '', { args, data }));
|
|
17
|
+
const timeout = cfg.timeout ?? 5;
|
|
18
|
+
const selectPath = cfg.select ? String(render(cfg.select, { args, data })) : null;
|
|
19
|
+
const framework = cfg.framework ?? null;
|
|
20
|
+
const actionArgs = cfg.args ?? [];
|
|
21
|
+
if (!storeName || !actionName)
|
|
22
|
+
throw new Error('tap: store and action are required');
|
|
23
|
+
// Build select chain for the captured response
|
|
24
|
+
const selectChain = selectPath
|
|
25
|
+
? selectPath.split('.').map((p) => `?.[${JSON.stringify(p)}]`).join('')
|
|
26
|
+
: '';
|
|
27
|
+
// Serialize action arguments
|
|
28
|
+
const actionArgsRendered = actionArgs.map((a) => {
|
|
29
|
+
const rendered = render(a, { args, data });
|
|
30
|
+
return JSON.stringify(rendered);
|
|
31
|
+
});
|
|
32
|
+
const actionCall = actionArgsRendered.length
|
|
33
|
+
? `store[${JSON.stringify(actionName)}](${actionArgsRendered.join(', ')})`
|
|
34
|
+
: `store[${JSON.stringify(actionName)}]()`;
|
|
35
|
+
const js = `
|
|
36
|
+
async () => {
|
|
37
|
+
// ── 1. Setup capture proxy (fetch + XHR dual interception) ──
|
|
38
|
+
let captured = null;
|
|
39
|
+
const capturePattern = ${JSON.stringify(capturePattern)};
|
|
40
|
+
|
|
41
|
+
// Intercept fetch API
|
|
42
|
+
const origFetch = window.fetch;
|
|
43
|
+
window.fetch = async function(...fetchArgs) {
|
|
44
|
+
const resp = await origFetch.apply(this, fetchArgs);
|
|
45
|
+
try {
|
|
46
|
+
const url = typeof fetchArgs[0] === 'string' ? fetchArgs[0]
|
|
47
|
+
: fetchArgs[0] instanceof Request ? fetchArgs[0].url : String(fetchArgs[0]);
|
|
48
|
+
if (capturePattern && url.includes(capturePattern) && !captured) {
|
|
49
|
+
try { captured = await resp.clone().json(); } catch {}
|
|
50
|
+
}
|
|
51
|
+
} catch {}
|
|
52
|
+
return resp;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Intercept XMLHttpRequest
|
|
56
|
+
const origXhrOpen = XMLHttpRequest.prototype.open;
|
|
57
|
+
const origXhrSend = XMLHttpRequest.prototype.send;
|
|
58
|
+
XMLHttpRequest.prototype.open = function(method, url) {
|
|
59
|
+
this.__tapUrl = String(url);
|
|
60
|
+
return origXhrOpen.apply(this, arguments);
|
|
61
|
+
};
|
|
62
|
+
XMLHttpRequest.prototype.send = function(body) {
|
|
63
|
+
if (capturePattern && this.__tapUrl?.includes(capturePattern)) {
|
|
64
|
+
const xhr = this;
|
|
65
|
+
const origHandler = xhr.onreadystatechange;
|
|
66
|
+
xhr.onreadystatechange = function() {
|
|
67
|
+
if (xhr.readyState === 4 && !captured) {
|
|
68
|
+
try { captured = JSON.parse(xhr.responseText); } catch {}
|
|
69
|
+
}
|
|
70
|
+
if (origHandler) origHandler.apply(this, arguments);
|
|
71
|
+
};
|
|
72
|
+
const origOnload = xhr.onload;
|
|
73
|
+
xhr.onload = function() {
|
|
74
|
+
if (!captured) { try { captured = JSON.parse(xhr.responseText); } catch {} }
|
|
75
|
+
if (origOnload) origOnload.apply(this, arguments);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return origXhrSend.apply(this, arguments);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
// ── 2. Find store ──
|
|
83
|
+
let store = null;
|
|
84
|
+
const storeName = ${JSON.stringify(storeName)};
|
|
85
|
+
const fw = ${JSON.stringify(framework)};
|
|
86
|
+
|
|
87
|
+
const app = document.querySelector('#app');
|
|
88
|
+
if (!fw || fw === 'pinia') {
|
|
89
|
+
try {
|
|
90
|
+
const pinia = app?.__vue_app__?.config?.globalProperties?.$pinia;
|
|
91
|
+
if (pinia?._s) store = pinia._s.get(storeName);
|
|
92
|
+
} catch {}
|
|
93
|
+
}
|
|
94
|
+
if (!store && (!fw || fw === 'vuex')) {
|
|
95
|
+
try {
|
|
96
|
+
const vuexStore = app?.__vue_app__?.config?.globalProperties?.$store
|
|
97
|
+
?? app?.__vue__?.$store;
|
|
98
|
+
if (vuexStore) {
|
|
99
|
+
store = { [${JSON.stringify(actionName)}]: (...a) => vuexStore.dispatch(storeName + '/' + ${JSON.stringify(actionName)}, ...a) };
|
|
100
|
+
}
|
|
101
|
+
} catch {}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!store) return { error: 'Store not found: ' + storeName, hint: 'Page may not be fully loaded or store name may be incorrect' };
|
|
105
|
+
if (typeof store[${JSON.stringify(actionName)}] !== 'function') {
|
|
106
|
+
return { error: 'Action not found: ' + ${JSON.stringify(actionName)} + ' on store ' + storeName,
|
|
107
|
+
hint: 'Available: ' + Object.keys(store).filter(k => typeof store[k] === 'function' && !k.startsWith('$') && !k.startsWith('_')).join(', ') };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── 3. Call store action ──
|
|
111
|
+
await ${actionCall};
|
|
112
|
+
|
|
113
|
+
// ── 4. Wait for network response ──
|
|
114
|
+
const deadline = Date.now() + ${timeout} * 1000;
|
|
115
|
+
while (!captured && Date.now() < deadline) {
|
|
116
|
+
await new Promise(r => setTimeout(r, 200));
|
|
117
|
+
}
|
|
118
|
+
} finally {
|
|
119
|
+
// ── 5. Always restore originals ──
|
|
120
|
+
window.fetch = origFetch;
|
|
121
|
+
XMLHttpRequest.prototype.open = origXhrOpen;
|
|
122
|
+
XMLHttpRequest.prototype.send = origXhrSend;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!captured) return { error: 'No matching response captured for pattern: ' + capturePattern };
|
|
126
|
+
return captured${selectChain} ?? captured;
|
|
127
|
+
}
|
|
128
|
+
`;
|
|
129
|
+
return page.evaluate(js);
|
|
130
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline steps: data transforms — select, map, filter, sort, limit.
|
|
3
|
+
*/
|
|
4
|
+
export declare function stepSelect(_page: any, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
5
|
+
export declare function stepMap(_page: any, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
6
|
+
export declare function stepFilter(_page: any, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
7
|
+
export declare function stepSort(_page: any, params: any, data: any, _args: Record<string, any>): Promise<any>;
|
|
8
|
+
export declare function stepLimit(_page: any, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline steps: data transforms — select, map, filter, sort, limit.
|
|
3
|
+
*/
|
|
4
|
+
import { render, evalExpr } from '../template.js';
|
|
5
|
+
export async function stepSelect(_page, params, data, args) {
|
|
6
|
+
const pathStr = String(render(params, { args, data }));
|
|
7
|
+
if (data && typeof data === 'object') {
|
|
8
|
+
let current = data;
|
|
9
|
+
for (const part of pathStr.split('.')) {
|
|
10
|
+
if (current && typeof current === 'object' && !Array.isArray(current))
|
|
11
|
+
current = current[part];
|
|
12
|
+
else if (Array.isArray(current) && /^\d+$/.test(part))
|
|
13
|
+
current = current[parseInt(part, 10)];
|
|
14
|
+
else
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return current;
|
|
18
|
+
}
|
|
19
|
+
return data;
|
|
20
|
+
}
|
|
21
|
+
export async function stepMap(_page, params, data, args) {
|
|
22
|
+
if (!data || typeof data !== 'object')
|
|
23
|
+
return data;
|
|
24
|
+
let items = Array.isArray(data) ? data : [data];
|
|
25
|
+
if (!Array.isArray(data) && typeof data === 'object' && 'data' in data)
|
|
26
|
+
items = data.data;
|
|
27
|
+
const result = [];
|
|
28
|
+
for (let i = 0; i < items.length; i++) {
|
|
29
|
+
const item = items[i];
|
|
30
|
+
const row = {};
|
|
31
|
+
for (const [key, template] of Object.entries(params))
|
|
32
|
+
row[key] = render(template, { args, data, item, index: i });
|
|
33
|
+
result.push(row);
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
export async function stepFilter(_page, params, data, args) {
|
|
38
|
+
if (!Array.isArray(data))
|
|
39
|
+
return data;
|
|
40
|
+
return data.filter((item, i) => evalExpr(String(params), { args, item, index: i }));
|
|
41
|
+
}
|
|
42
|
+
export async function stepSort(_page, params, data, _args) {
|
|
43
|
+
if (!Array.isArray(data))
|
|
44
|
+
return data;
|
|
45
|
+
const key = typeof params === 'object' ? (params.by ?? '') : String(params);
|
|
46
|
+
const reverse = typeof params === 'object' ? params.order === 'desc' : false;
|
|
47
|
+
return [...data].sort((a, b) => { const va = a[key] ?? ''; const vb = b[key] ?? ''; const cmp = va < vb ? -1 : va > vb ? 1 : 0; return reverse ? -cmp : cmp; });
|
|
48
|
+
}
|
|
49
|
+
export async function stepLimit(_page, params, data, args) {
|
|
50
|
+
if (!Array.isArray(data))
|
|
51
|
+
return data;
|
|
52
|
+
return data.slice(0, Number(render(params, { args, data })));
|
|
53
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline template engine: ${{ ... }} expression rendering.
|
|
3
|
+
*/
|
|
4
|
+
export interface RenderContext {
|
|
5
|
+
args?: Record<string, any>;
|
|
6
|
+
data?: any;
|
|
7
|
+
item?: any;
|
|
8
|
+
index?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function render(template: any, ctx: RenderContext): any;
|
|
11
|
+
export declare function evalExpr(expr: string, ctx: RenderContext): any;
|
|
12
|
+
export declare function resolvePath(pathStr: string, ctx: RenderContext): any;
|
|
13
|
+
/**
|
|
14
|
+
* Normalize JavaScript source for browser evaluate() calls.
|
|
15
|
+
*/
|
|
16
|
+
export declare function normalizeEvaluateSource(source: string): string;
|