@jackwener/opencli 0.1.2 → 0.3.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 +51 -72
- package/README.md +8 -5
- package/README.zh-CN.md +8 -5
- package/SKILL.md +27 -14
- package/dist/browser.d.ts +7 -0
- package/dist/browser.js +85 -2
- 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/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/main.d.ts +1 -1
- package/dist/main.js +10 -3
- package/dist/output.d.ts +1 -1
- package/dist/output.js +12 -8
- package/dist/pipeline/steps/intercept.js +56 -29
- package/dist/pipeline/template.js +74 -15
- package/dist/pipeline/template.test.js +24 -0
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
- package/src/browser.ts +88 -5
- package/src/clis/bilibili/dynamic.ts +34 -0
- package/src/clis/bilibili/ranking.ts +25 -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/main.ts +9 -3
- package/src/output.ts +10 -6
- package/src/pipeline/steps/intercept.ts +58 -28
- package/src/pipeline/template.test.ts +24 -0
- package/src/pipeline/template.ts +72 -14
- package/src/types.ts +3 -0
- package/dist/clis/index.d.ts +0 -22
- package/dist/clis/index.js +0 -34
- package/src/clis/index.ts +0 -46
package/src/clis/zhihu/hot.yaml
CHANGED
|
@@ -17,12 +17,16 @@ pipeline:
|
|
|
17
17
|
const res = await fetch('https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total?limit=50', {
|
|
18
18
|
credentials: 'include'
|
|
19
19
|
});
|
|
20
|
-
const
|
|
20
|
+
const text = await res.text();
|
|
21
|
+
const d = JSON.parse(
|
|
22
|
+
text.replace(/("id"\s*:\s*)(\d{16,})/g, '$1"$2"')
|
|
23
|
+
);
|
|
21
24
|
return (d?.data || []).map((item) => {
|
|
22
25
|
const t = item.target || {};
|
|
26
|
+
const questionId = t.id == null ? '' : String(t.id);
|
|
23
27
|
return {
|
|
24
28
|
title: t.title,
|
|
25
|
-
url: 'https://www.zhihu.com/question/' +
|
|
29
|
+
url: 'https://www.zhihu.com/question/' + questionId,
|
|
26
30
|
answer_count: t.answer_count,
|
|
27
31
|
follower_count: t.follower_count,
|
|
28
32
|
heat: item.detail_text || '',
|
|
@@ -19,7 +19,9 @@ pipeline:
|
|
|
19
19
|
- evaluate: |
|
|
20
20
|
(async () => {
|
|
21
21
|
const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').replace(/<em>/g, '').replace(/<\/em>/g, '').trim();
|
|
22
|
-
const
|
|
22
|
+
const keyword = ${{ args.keyword | json }};
|
|
23
|
+
const limit = ${{ args.limit }};
|
|
24
|
+
const res = await fetch('https://www.zhihu.com/api/v4/search_v3?q=' + encodeURIComponent(keyword) + '&t=general&offset=0&limit=' + limit, {
|
|
23
25
|
credentials: 'include'
|
|
24
26
|
});
|
|
25
27
|
const d = await res.json();
|
package/src/engine.ts
CHANGED
|
@@ -9,7 +9,8 @@ import { type CliCommand, type Arg, Strategy, registerCommand } from './registry
|
|
|
9
9
|
import type { IPage } from './types.js';
|
|
10
10
|
import { executePipeline } from './pipeline.js';
|
|
11
11
|
|
|
12
|
-
export function discoverClis(...dirs: string[]): void {
|
|
12
|
+
export async function discoverClis(...dirs: string[]): Promise<void> {
|
|
13
|
+
const promises: Promise<any>[] = [];
|
|
13
14
|
for (const dir of dirs) {
|
|
14
15
|
if (!fs.existsSync(dir)) continue;
|
|
15
16
|
for (const site of fs.readdirSync(dir)) {
|
|
@@ -19,10 +20,18 @@ export function discoverClis(...dirs: string[]): void {
|
|
|
19
20
|
const filePath = path.join(siteDir, file);
|
|
20
21
|
if (file.endsWith('.yaml') || file.endsWith('.yml')) {
|
|
21
22
|
registerYamlCli(filePath, site);
|
|
23
|
+
} else if (file.endsWith('.js')) {
|
|
24
|
+
// Dynamic import of compiled adapter modules
|
|
25
|
+
promises.push(
|
|
26
|
+
import(`file://${filePath}`).catch((err: any) => {
|
|
27
|
+
process.stderr.write(`Warning: failed to load module ${filePath}: ${err.message}\n`);
|
|
28
|
+
})
|
|
29
|
+
);
|
|
22
30
|
}
|
|
23
31
|
}
|
|
24
32
|
}
|
|
25
33
|
}
|
|
34
|
+
await Promise.all(promises);
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
function registerYamlCli(filePath: string, defaultSite: string): void {
|
package/src/main.ts
CHANGED
|
@@ -12,7 +12,6 @@ import chalk from 'chalk';
|
|
|
12
12
|
import { discoverClis, executeCommand } from './engine.js';
|
|
13
13
|
import { type CliCommand, fullName, getRegistry, strategyLabel } from './registry.js';
|
|
14
14
|
import { render as renderOutput } from './output.js';
|
|
15
|
-
import './clis/index.js';
|
|
16
15
|
import { PlaywrightMCP } from './browser.js';
|
|
17
16
|
import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
|
|
18
17
|
|
|
@@ -25,7 +24,7 @@ const USER_CLIS = path.join(os.homedir(), '.opencli', 'clis');
|
|
|
25
24
|
const pkgJsonPath = path.resolve(__dirname, '..', 'package.json');
|
|
26
25
|
const PKG_VERSION = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')).version ?? '0.0.0';
|
|
27
26
|
|
|
28
|
-
discoverClis(BUILTIN_CLIS, USER_CLIS);
|
|
27
|
+
await discoverClis(BUILTIN_CLIS, USER_CLIS);
|
|
29
28
|
|
|
30
29
|
const program = new Command();
|
|
31
30
|
program.name('opencli').description('Make any website your CLI. Zero setup. AI-powered.').version(PKG_VERSION);
|
|
@@ -104,8 +103,15 @@ for (const [, cmd] of registry) {
|
|
|
104
103
|
if (cmd.browser) {
|
|
105
104
|
result = await browserSession(PlaywrightMCP, async (page) => runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) }));
|
|
106
105
|
} else { result = await executeCommand(cmd, null, kwargs, actionOpts.verbose); }
|
|
106
|
+
if (actionOpts.verbose && (!result || (Array.isArray(result) && result.length === 0))) {
|
|
107
|
+
console.error(chalk.yellow(`[Verbose] Warning: Command returned an empty result. If the website structural API changed or requires authentication, check the network or update the adapter.`));
|
|
108
|
+
}
|
|
107
109
|
renderOutput(result, { fmt: actionOpts.format, columns: cmd.columns, title: `${cmd.site}/${cmd.name}`, elapsed: (Date.now() - startTime) / 1000, source: fullName(cmd) });
|
|
108
|
-
} catch (err: any) {
|
|
110
|
+
} catch (err: any) {
|
|
111
|
+
if (actionOpts.verbose && err.stack) { console.error(chalk.red(err.stack)); }
|
|
112
|
+
else { console.error(chalk.red(`Error: ${err.message ?? err}`)); }
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
}
|
|
109
115
|
});
|
|
110
116
|
}
|
|
111
117
|
|
package/src/output.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Output formatting: table, JSON, Markdown, CSV.
|
|
2
|
+
* Output formatting: table, JSON, Markdown, CSV, YAML.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import Table from 'cli-table3';
|
|
7
|
+
import yaml from 'js-yaml';
|
|
7
8
|
|
|
8
9
|
export interface RenderOptions {
|
|
9
10
|
fmt?: string;
|
|
@@ -23,6 +24,7 @@ export function render(data: any, opts: RenderOptions = {}): void {
|
|
|
23
24
|
case 'json': renderJson(data); break;
|
|
24
25
|
case 'md': case 'markdown': renderMarkdown(data, opts); break;
|
|
25
26
|
case 'csv': renderCsv(data, opts); break;
|
|
27
|
+
case 'yaml': case 'yml': renderYaml(data); break;
|
|
26
28
|
default: renderTable(data, opts); break;
|
|
27
29
|
}
|
|
28
30
|
}
|
|
@@ -32,16 +34,14 @@ function renderTable(data: any, opts: RenderOptions): void {
|
|
|
32
34
|
if (!rows.length) { console.log(chalk.dim('(no data)')); return; }
|
|
33
35
|
const columns = opts.columns ?? Object.keys(rows[0]);
|
|
34
36
|
|
|
35
|
-
const header = columns.map(
|
|
37
|
+
const header = columns.map(c => capitalize(c));
|
|
36
38
|
const table = new Table({
|
|
37
39
|
head: header.map(h => chalk.bold(h)),
|
|
38
40
|
style: { head: [], border: [] },
|
|
39
41
|
wordWrap: true,
|
|
40
42
|
wrapOnWordBoundary: true,
|
|
41
|
-
colWidths: columns.map((
|
|
42
|
-
if (i === 0) return
|
|
43
|
-
if (c === 'url' || c === 'description') return null as any;
|
|
44
|
-
if (c === 'title' || c === 'name' || c === 'repo') return null as any;
|
|
43
|
+
colWidths: columns.map((_c, i) => {
|
|
44
|
+
if (i === 0) return 6;
|
|
45
45
|
return null as any;
|
|
46
46
|
}).filter(() => true),
|
|
47
47
|
});
|
|
@@ -91,6 +91,10 @@ function renderCsv(data: any, opts: RenderOptions): void {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
function renderYaml(data: any): void {
|
|
95
|
+
console.log(yaml.dump(data, { sortKeys: false, lineWidth: 120, noRefs: true }));
|
|
96
|
+
}
|
|
97
|
+
|
|
94
98
|
function capitalize(s: string): string {
|
|
95
99
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
96
100
|
}
|
|
@@ -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] :
|
|
@@ -57,6 +57,30 @@ describe('evalExpr', () => {
|
|
|
57
57
|
it('resolves simple path', () => {
|
|
58
58
|
expect(evalExpr('item.title', { item: { title: 'Test' } })).toBe('Test');
|
|
59
59
|
});
|
|
60
|
+
it('applies join filter', () => {
|
|
61
|
+
expect(evalExpr('item.tags | join(,)', { item: { tags: ['a', 'b', 'c'] } })).toBe('a,b,c');
|
|
62
|
+
});
|
|
63
|
+
it('applies upper filter', () => {
|
|
64
|
+
expect(evalExpr('item.name | upper', { item: { name: 'hello' } })).toBe('HELLO');
|
|
65
|
+
});
|
|
66
|
+
it('applies lower filter', () => {
|
|
67
|
+
expect(evalExpr('item.name | lower', { item: { name: 'HELLO' } })).toBe('hello');
|
|
68
|
+
});
|
|
69
|
+
it('applies truncate filter', () => {
|
|
70
|
+
expect(evalExpr('item.text | truncate(5)', { item: { text: 'Hello World!' } })).toBe('Hello...');
|
|
71
|
+
});
|
|
72
|
+
it('chains filters', () => {
|
|
73
|
+
expect(evalExpr('item.name | upper | truncate(3)', { item: { name: 'hello' } })).toBe('HEL...');
|
|
74
|
+
});
|
|
75
|
+
it('applies length filter', () => {
|
|
76
|
+
expect(evalExpr('item.items | length', { item: { items: [1, 2, 3] } })).toBe(3);
|
|
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
|
+
});
|
|
60
84
|
});
|
|
61
85
|
|
|
62
86
|
describe('render', () => {
|
package/src/pipeline/template.ts
CHANGED
|
@@ -11,8 +11,17 @@ export interface RenderContext {
|
|
|
11
11
|
|
|
12
12
|
export function render(template: any, ctx: RenderContext): any {
|
|
13
13
|
if (typeof template !== 'string') return template;
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Full expression: entire string is a single ${{ ... }}
|
|
15
|
+
// Use [^}] to prevent matching across }} boundaries (e.g. "${{ a }}-${{ b }}")
|
|
16
|
+
const fullMatch = template.match(/^\$\{\{\s*([^}]*(?:\}[^}][^}]*)*)\s*\}\}$/);
|
|
17
|
+
if (fullMatch && !template.includes('}}-') && !template.includes('}}${{')) return evalExpr(fullMatch[1].trim(), ctx);
|
|
18
|
+
// Check if the entire string is a single expression (no other text around it)
|
|
19
|
+
const singleExpr = template.match(/^\$\{\{\s*([\s\S]*?)\s*\}\}$/);
|
|
20
|
+
if (singleExpr) {
|
|
21
|
+
// Verify it's truly a single expression (no other ${{ inside)
|
|
22
|
+
const inner = singleExpr[1];
|
|
23
|
+
if (!inner.includes('${{')) return evalExpr(inner.trim(), ctx);
|
|
24
|
+
}
|
|
16
25
|
return template.replace(/\$\{\{\s*(.*?)\s*\}\}/g, (_m, expr) => String(evalExpr(expr.trim(), ctx)));
|
|
17
26
|
}
|
|
18
27
|
|
|
@@ -22,18 +31,14 @@ export function evalExpr(expr: string, ctx: RenderContext): any {
|
|
|
22
31
|
const data = ctx.data;
|
|
23
32
|
const index = ctx.index ?? 0;
|
|
24
33
|
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const intVal = parseInt(defaultVal!, 10);
|
|
34
|
-
if (!isNaN(intVal) && String(intVal) === defaultVal!.trim()) return intVal;
|
|
35
|
-
return defaultVal!.replace(/^['"]|['"]$/g, '');
|
|
36
|
-
}
|
|
34
|
+
// ── Pipe filters: expr | filter1(arg) | filter2 ──
|
|
35
|
+
// Supports: default(val), join(sep), upper, lower, truncate(n), trim, replace(old,new)
|
|
36
|
+
if (expr.includes('|') && !expr.includes('||')) {
|
|
37
|
+
const segments = expr.split('|').map(s => s.trim());
|
|
38
|
+
const mainExpr = segments[0];
|
|
39
|
+
let result = resolvePath(mainExpr, { args, item, data, index });
|
|
40
|
+
for (let i = 1; i < segments.length; i++) {
|
|
41
|
+
result = applyFilter(segments[i], result);
|
|
37
42
|
}
|
|
38
43
|
return result;
|
|
39
44
|
}
|
|
@@ -66,6 +71,59 @@ export function evalExpr(expr: string, ctx: RenderContext): any {
|
|
|
66
71
|
return resolvePath(expr, { args, item, data, index });
|
|
67
72
|
}
|
|
68
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Apply a named filter to a value.
|
|
76
|
+
* Supported filters:
|
|
77
|
+
* default(val), join(sep), upper, lower, truncate(n), trim,
|
|
78
|
+
* replace(old,new), keys, length, first, last, json
|
|
79
|
+
*/
|
|
80
|
+
function applyFilter(filterExpr: string, value: any): any {
|
|
81
|
+
const match = filterExpr.match(/^(\w+)(?:\((.+)\))?$/);
|
|
82
|
+
if (!match) return value;
|
|
83
|
+
const [, name, rawArgs] = match;
|
|
84
|
+
const filterArg = rawArgs?.replace(/^['"]|['"]$/g, '') ?? '';
|
|
85
|
+
|
|
86
|
+
switch (name) {
|
|
87
|
+
case 'default': {
|
|
88
|
+
if (value === null || value === undefined || value === '') {
|
|
89
|
+
const intVal = parseInt(filterArg, 10);
|
|
90
|
+
if (!isNaN(intVal) && String(intVal) === filterArg.trim()) return intVal;
|
|
91
|
+
return filterArg;
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
case 'join':
|
|
96
|
+
return Array.isArray(value) ? value.join(filterArg || ', ') : value;
|
|
97
|
+
case 'upper':
|
|
98
|
+
return typeof value === 'string' ? value.toUpperCase() : value;
|
|
99
|
+
case 'lower':
|
|
100
|
+
return typeof value === 'string' ? value.toLowerCase() : value;
|
|
101
|
+
case 'trim':
|
|
102
|
+
return typeof value === 'string' ? value.trim() : value;
|
|
103
|
+
case 'truncate': {
|
|
104
|
+
const n = parseInt(filterArg, 10) || 50;
|
|
105
|
+
return typeof value === 'string' && value.length > n ? value.slice(0, n) + '...' : value;
|
|
106
|
+
}
|
|
107
|
+
case 'replace': {
|
|
108
|
+
if (typeof value !== 'string') return value;
|
|
109
|
+
const parts = rawArgs?.split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')) ?? [];
|
|
110
|
+
return parts.length >= 2 ? value.replaceAll(parts[0], parts[1]) : value;
|
|
111
|
+
}
|
|
112
|
+
case 'keys':
|
|
113
|
+
return value && typeof value === 'object' ? Object.keys(value) : value;
|
|
114
|
+
case 'length':
|
|
115
|
+
return Array.isArray(value) ? value.length : typeof value === 'string' ? value.length : value;
|
|
116
|
+
case 'first':
|
|
117
|
+
return Array.isArray(value) ? value[0] : value;
|
|
118
|
+
case 'last':
|
|
119
|
+
return Array.isArray(value) ? value[value.length - 1] : value;
|
|
120
|
+
case 'json':
|
|
121
|
+
return JSON.stringify(value ?? null);
|
|
122
|
+
default:
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
69
127
|
export function resolvePath(pathStr: string, ctx: RenderContext): any {
|
|
70
128
|
const args = ctx.args ?? {};
|
|
71
129
|
const item = ctx.item ?? {};
|
package/src/types.ts
CHANGED
|
@@ -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';
|