@jackwener/opencli 1.7.3 → 1.7.5
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/README.md +81 -59
- package/README.zh-CN.md +93 -67
- package/cli-manifest.json +5015 -2975
- package/clis/antigravity/serve.js +71 -25
- package/clis/baidu-scholar/search.js +87 -0
- package/clis/baidu-scholar/search.test.js +23 -0
- package/clis/bilibili/favorite.js +18 -13
- package/clis/binance/depth.js +3 -4
- package/clis/boss/utils.js +2 -3
- package/clis/chatgpt-app/ax.js +6 -3
- package/clis/deepseek/ask.js +74 -0
- package/clis/deepseek/history.js +25 -0
- package/clis/deepseek/new.js +20 -0
- package/clis/deepseek/read.js +22 -0
- package/clis/deepseek/status.js +24 -0
- package/clis/deepseek/utils.js +208 -0
- package/clis/douban/search.js +1 -0
- package/clis/douban/search.test.js +11 -0
- package/clis/douban/subject.js +20 -93
- package/clis/douban/subject.test.js +11 -0
- package/clis/douban/utils.js +250 -8
- package/clis/douban/utils.test.js +179 -4
- package/clis/doubao/utils.js +319 -130
- package/clis/doubao/utils.test.js +241 -2
- package/clis/eastmoney/_secid.js +78 -0
- package/clis/eastmoney/announcement.js +52 -0
- package/clis/eastmoney/convertible.js +73 -0
- package/clis/eastmoney/etf.js +65 -0
- package/clis/eastmoney/holders.js +78 -0
- package/clis/eastmoney/hot-rank.js +50 -0
- package/clis/eastmoney/hot-rank.test.js +59 -0
- package/clis/eastmoney/index-board.js +96 -0
- package/clis/eastmoney/kline.js +87 -0
- package/clis/eastmoney/kuaixun.js +54 -0
- package/clis/eastmoney/longhu.js +67 -0
- package/clis/eastmoney/money-flow.js +78 -0
- package/clis/eastmoney/northbound.js +57 -0
- package/clis/eastmoney/quote.js +107 -0
- package/clis/eastmoney/rank.js +94 -0
- package/clis/eastmoney/sectors.js +76 -0
- package/clis/google-scholar/search.js +58 -0
- package/clis/google-scholar/search.test.js +23 -0
- package/clis/gov-law/commands.test.js +39 -0
- package/clis/gov-law/recent.js +22 -0
- package/clis/gov-law/search.js +41 -0
- package/clis/gov-law/shared.js +51 -0
- package/clis/gov-policy/commands.test.js +27 -0
- package/clis/gov-policy/recent.js +47 -0
- package/clis/gov-policy/search.js +48 -0
- package/clis/grok/image.test.ts +107 -0
- package/clis/grok/image.ts +356 -0
- package/clis/nowcoder/companies.js +23 -0
- package/clis/nowcoder/creators.js +27 -0
- package/clis/nowcoder/detail.js +61 -0
- package/clis/nowcoder/experience.js +36 -0
- package/clis/nowcoder/hot.js +24 -0
- package/clis/nowcoder/jobs.js +21 -0
- package/clis/nowcoder/notifications.js +29 -0
- package/clis/nowcoder/papers.js +40 -0
- package/clis/nowcoder/practice.js +37 -0
- package/clis/nowcoder/recommend.js +30 -0
- package/clis/nowcoder/referral.js +39 -0
- package/clis/nowcoder/salary.js +40 -0
- package/clis/nowcoder/search.js +49 -0
- package/clis/nowcoder/suggest.js +33 -0
- package/clis/nowcoder/topics.js +27 -0
- package/clis/nowcoder/trending.js +25 -0
- package/clis/tdx/hot-rank.js +47 -0
- package/clis/tdx/hot-rank.test.js +59 -0
- package/clis/ths/hot-rank.js +49 -0
- package/clis/ths/hot-rank.test.js +64 -0
- package/clis/twitter/bookmarks.js +2 -1
- package/clis/twitter/list-add.js +337 -0
- package/clis/twitter/list-add.test.js +15 -0
- package/clis/twitter/list-remove.js +297 -0
- package/clis/twitter/list-remove.test.js +14 -0
- package/clis/twitter/list-tweets.js +185 -0
- package/clis/twitter/list-tweets.test.js +108 -0
- package/clis/twitter/lists.js +134 -47
- package/clis/twitter/lists.test.js +105 -38
- package/clis/uiverse/_shared.js +368 -0
- package/clis/uiverse/_shared.test.js +55 -0
- package/clis/uiverse/code.js +47 -0
- package/clis/uiverse/preview.js +71 -0
- package/clis/wanfang/search.js +66 -0
- package/clis/wanfang/search.test.js +23 -0
- package/clis/web/read.js +1 -1
- package/clis/weixin/download.js +3 -2
- package/clis/xiaohongshu/comments.js +2 -2
- package/clis/xiaohongshu/comments.test.js +46 -25
- package/clis/xiaohongshu/download.js +6 -7
- package/clis/xiaohongshu/download.test.js +17 -5
- package/clis/xiaohongshu/note-helpers.js +46 -12
- package/clis/xiaohongshu/note.js +3 -5
- package/clis/xiaohongshu/note.test.js +52 -25
- package/clis/xiaohongshu/publish.js +149 -28
- package/clis/xiaohongshu/publish.test.js +319 -6
- package/clis/xiaoyuzhou/auth.js +303 -0
- package/clis/xiaoyuzhou/auth.test.js +124 -0
- package/clis/xiaoyuzhou/download.js +53 -0
- package/clis/xiaoyuzhou/download.test.js +135 -0
- package/clis/xiaoyuzhou/episode.js +9 -4
- package/clis/xiaoyuzhou/podcast-episodes.js +15 -11
- package/clis/xiaoyuzhou/podcast.js +9 -4
- package/clis/xiaoyuzhou/transcript.js +76 -0
- package/clis/xiaoyuzhou/transcript.test.js +195 -0
- package/clis/xiaoyuzhou/utils.js +0 -40
- package/clis/xiaoyuzhou/utils.test.js +15 -75
- package/clis/youtube/feed.js +120 -0
- package/clis/youtube/history.js +118 -0
- package/clis/youtube/like.js +62 -0
- package/clis/youtube/playlist.js +97 -0
- package/clis/youtube/subscribe.js +71 -0
- package/clis/youtube/subscriptions.js +57 -0
- package/clis/youtube/unlike.js +62 -0
- package/clis/youtube/unsubscribe.js +71 -0
- package/clis/youtube/utils.js +122 -0
- package/clis/youtube/utils.test.js +32 -1
- package/clis/youtube/watch-later.js +76 -0
- package/clis/zsxq/dynamics.js +1 -1
- package/clis/zsxq/utils.js +6 -3
- package/clis/zsxq/utils.test.js +31 -0
- package/dist/src/browser/base-page.d.ts +1 -1
- package/dist/src/browser/base-page.js +25 -5
- package/dist/src/browser/bridge.d.ts +3 -0
- package/dist/src/browser/bridge.js +52 -15
- package/dist/src/browser/cdp.js +2 -1
- package/dist/src/browser/daemon-client.d.ts +7 -4
- package/dist/src/browser/daemon-client.js +6 -1
- package/dist/src/browser/daemon-client.test.js +40 -1
- package/dist/src/browser/dom-snapshot.js +20 -3
- package/dist/src/browser/page.d.ts +18 -5
- package/dist/src/browser/page.js +96 -15
- package/dist/src/browser/page.test.js +158 -1
- package/dist/src/browser/target-errors.d.ts +23 -0
- package/dist/src/browser/target-errors.js +29 -0
- package/dist/src/browser/target-errors.test.js +61 -0
- package/dist/src/browser/target-resolver.d.ts +57 -0
- package/dist/src/browser/target-resolver.js +298 -0
- package/dist/src/browser/target-resolver.test.js +43 -0
- package/dist/src/browser.test.js +38 -1
- package/dist/src/cli.js +272 -187
- package/dist/src/cli.test.js +167 -90
- package/dist/src/commanderAdapter.d.ts +0 -1
- package/dist/src/commanderAdapter.js +2 -16
- package/dist/src/commanderAdapter.test.js +1 -1
- package/dist/src/commands/daemon.d.ts +4 -2
- package/dist/src/commands/daemon.js +22 -2
- package/dist/src/commands/daemon.test.js +65 -2
- package/dist/src/completion-shared.js +2 -5
- package/dist/src/daemon.js +10 -0
- package/dist/src/doctor.d.ts +1 -0
- package/dist/src/doctor.js +32 -9
- package/dist/src/doctor.test.js +28 -12
- package/dist/src/download/article-download.d.ts +1 -0
- package/dist/src/download/article-download.js +3 -0
- package/dist/src/download/article-download.test.js +39 -0
- package/dist/src/external-clis.yaml +2 -2
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +3 -3
- package/dist/src/output.js +1 -5
- package/dist/src/output.test.js +0 -21
- package/dist/src/pipeline/steps/transform.js +1 -1
- package/dist/src/pipeline/template.d.ts +1 -0
- package/dist/src/pipeline/template.js +11 -3
- package/dist/src/pipeline/template.test.js +3 -0
- package/dist/src/pipeline/transform.test.js +14 -0
- package/dist/src/plugin.d.ts +8 -9
- package/dist/src/plugin.js +24 -28
- package/dist/src/plugin.test.js +16 -60
- package/dist/src/registry.d.ts +1 -0
- package/dist/src/registry.js +3 -2
- package/dist/src/registry.test.js +22 -0
- package/dist/src/types.d.ts +15 -6
- package/package.json +1 -1
- package/clis/twitter/lists-parser.js +0 -77
- package/clis/twitter/lists.d.ts +0 -5
- package/dist/src/cascade.d.ts +0 -46
- package/dist/src/cascade.js +0 -135
- package/dist/src/explore.d.ts +0 -99
- package/dist/src/explore.js +0 -402
- package/dist/src/generate-verified.d.ts +0 -105
- package/dist/src/generate-verified.js +0 -696
- package/dist/src/generate-verified.test.js +0 -925
- package/dist/src/generate.d.ts +0 -46
- package/dist/src/generate.js +0 -117
- package/dist/src/record.d.ts +0 -96
- package/dist/src/record.js +0 -657
- package/dist/src/record.test.js +0 -293
- package/dist/src/skill-generate.d.ts +0 -30
- package/dist/src/skill-generate.js +0 -75
- package/dist/src/skill-generate.test.js +0 -173
- package/dist/src/synthesize.d.ts +0 -97
- package/dist/src/synthesize.js +0 -208
- /package/dist/src/{generate-verified.test.d.ts → browser/target-errors.test.d.ts} +0 -0
- /package/dist/src/{record.test.d.ts → browser/target-resolver.test.d.ts} +0 -0
- /package/dist/src/{skill-generate.test.d.ts → download/article-download.test.d.ts} +0 -0
package/dist/src/explore.js
DELETED
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deep Explore: intelligent API discovery with response analysis.
|
|
3
|
-
*
|
|
4
|
-
* Navigates to the target URL, auto-scrolls to trigger lazy loading,
|
|
5
|
-
* captures network traffic, analyzes JSON responses, and automatically
|
|
6
|
-
* infers CLI capabilities from discovered API endpoints.
|
|
7
|
-
*/
|
|
8
|
-
import * as fs from 'node:fs';
|
|
9
|
-
import * as path from 'node:path';
|
|
10
|
-
import { DEFAULT_BROWSER_EXPLORE_TIMEOUT, browserSession, runWithTimeout } from './runtime.js';
|
|
11
|
-
import { LIMIT_PARAMS } from './constants.js';
|
|
12
|
-
import { detectFramework } from './scripts/framework.js';
|
|
13
|
-
import { discoverStores } from './scripts/store.js';
|
|
14
|
-
import { interactFuzz } from './scripts/interact.js';
|
|
15
|
-
import { log } from './logger.js';
|
|
16
|
-
import { urlToPattern, findArrayPath, flattenFields, detectFieldRoles, inferCapabilityName, inferStrategy, detectAuthFromHeaders, classifyQueryParams, isNoiseUrl, } from './analysis.js';
|
|
17
|
-
// ── Site name detection ────────────────────────────────────────────────────
|
|
18
|
-
const KNOWN_SITE_ALIASES = {
|
|
19
|
-
'x.com': 'twitter', 'twitter.com': 'twitter',
|
|
20
|
-
'news.ycombinator.com': 'hackernews',
|
|
21
|
-
'www.zhihu.com': 'zhihu', 'www.bilibili.com': 'bilibili',
|
|
22
|
-
'search.bilibili.com': 'bilibili',
|
|
23
|
-
'www.v2ex.com': 'v2ex', 'www.reddit.com': 'reddit',
|
|
24
|
-
'www.xiaohongshu.com': 'xiaohongshu', 'www.douban.com': 'douban',
|
|
25
|
-
'www.weibo.com': 'weibo', 'www.bbc.com': 'bbc',
|
|
26
|
-
};
|
|
27
|
-
export function detectSiteName(url) {
|
|
28
|
-
try {
|
|
29
|
-
const host = new URL(url).hostname.toLowerCase();
|
|
30
|
-
if (host in KNOWN_SITE_ALIASES)
|
|
31
|
-
return KNOWN_SITE_ALIASES[host];
|
|
32
|
-
const parts = host.split('.').filter(p => p && p !== 'www');
|
|
33
|
-
if (parts.length >= 2) {
|
|
34
|
-
if (['uk', 'jp', 'cn', 'com'].includes(parts[parts.length - 1]) && parts.length >= 3) {
|
|
35
|
-
return slugify(parts[parts.length - 3]);
|
|
36
|
-
}
|
|
37
|
-
return slugify(parts[parts.length - 2]);
|
|
38
|
-
}
|
|
39
|
-
return parts[0] ? slugify(parts[0]) : 'site';
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
return 'site';
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
export function slugify(value) {
|
|
46
|
-
return value.trim().toLowerCase().replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '') || 'site';
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Parse raw network output from browser page.
|
|
50
|
-
* Handles text format: [GET] url => [200]
|
|
51
|
-
*/
|
|
52
|
-
function parseNetworkRequests(raw) {
|
|
53
|
-
if (typeof raw === 'string') {
|
|
54
|
-
const entries = [];
|
|
55
|
-
for (const line of raw.split('\n')) {
|
|
56
|
-
// Format: [GET] URL => [200]
|
|
57
|
-
const m = line.match(/\[?(GET|POST|PUT|DELETE|PATCH|OPTIONS)\]?\s+(\S+)\s*(?:=>|→)\s*\[?(\d+)\]?/i);
|
|
58
|
-
if (m) {
|
|
59
|
-
const [, method, url, status] = m;
|
|
60
|
-
entries.push({
|
|
61
|
-
method: method.toUpperCase(), url, status: status ? parseInt(status) : null,
|
|
62
|
-
contentType: (url.includes('/api/') || url.includes('/x/') || url.endsWith('.json')) ? 'application/json' : '',
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return entries;
|
|
67
|
-
}
|
|
68
|
-
if (Array.isArray(raw)) {
|
|
69
|
-
return raw.filter(e => e && typeof e === 'object').map(e => {
|
|
70
|
-
// Handle both legacy shape (status/contentType/responseBody) and
|
|
71
|
-
// extension/CDP capture shape (responseStatus/responseContentType/responsePreview)
|
|
72
|
-
let body = e.responseBody;
|
|
73
|
-
if (body === undefined && e.responsePreview !== undefined) {
|
|
74
|
-
const preview = e.responsePreview;
|
|
75
|
-
if (typeof preview === 'string') {
|
|
76
|
-
try {
|
|
77
|
-
body = JSON.parse(preview);
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
body = preview;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return {
|
|
85
|
-
method: (e.method ?? 'GET').toUpperCase(),
|
|
86
|
-
url: String(e.url ?? e.request?.url ?? e.requestUrl ?? ''),
|
|
87
|
-
status: e.status ?? e.responseStatus ?? e.statusCode ?? null,
|
|
88
|
-
contentType: e.contentType ?? e.responseContentType ?? e.response?.contentType ?? '',
|
|
89
|
-
responseBody: body, requestHeaders: e.requestHeaders,
|
|
90
|
-
};
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
return [];
|
|
94
|
-
}
|
|
95
|
-
function analyzeResponseBody(body) {
|
|
96
|
-
if (!body || typeof body !== 'object')
|
|
97
|
-
return null;
|
|
98
|
-
const result = findArrayPath(body);
|
|
99
|
-
if (!result)
|
|
100
|
-
return null;
|
|
101
|
-
const sample = result.items[0];
|
|
102
|
-
const sampleFields = sample && typeof sample === 'object' ? flattenFields(sample, '', 2) : [];
|
|
103
|
-
const detectedFields = detectFieldRoles(sampleFields);
|
|
104
|
-
return { itemPath: result.path || null, itemCount: result.items.length, detectedFields, sampleFields };
|
|
105
|
-
}
|
|
106
|
-
function isBooleanRecord(value) {
|
|
107
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
108
|
-
&& Object.values(value).every(v => typeof v === 'boolean');
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Deterministic sort key for endpoint ordering — transparent, observable signals only.
|
|
112
|
-
* Used by generate/synthesize to pick a stable default candidate.
|
|
113
|
-
* Not exposed externally; AI agents see the raw metadata and decide for themselves.
|
|
114
|
-
*/
|
|
115
|
-
function endpointSortKey(ep) {
|
|
116
|
-
let k = 0;
|
|
117
|
-
// Prefer endpoints with array data (list APIs are more useful for automation)
|
|
118
|
-
const items = ep.responseAnalysis?.itemCount ?? 0;
|
|
119
|
-
if (items > 0)
|
|
120
|
-
k += 100 + Math.min(items, 50);
|
|
121
|
-
// Prefer endpoints with detected semantic fields
|
|
122
|
-
k += Object.keys(ep.responseAnalysis?.detectedFields ?? {}).length * 10;
|
|
123
|
-
// Prefer API-style paths
|
|
124
|
-
if (ep.pattern.includes('/api/') || ep.pattern.includes('/x/'))
|
|
125
|
-
k += 5;
|
|
126
|
-
// Prefer endpoints with query params (more likely to be parameterized APIs)
|
|
127
|
-
if (ep.hasSearchParam || ep.hasPaginationParam || ep.hasLimitParam)
|
|
128
|
-
k += 5;
|
|
129
|
-
return k;
|
|
130
|
-
}
|
|
131
|
-
/** Check whether an endpoint carries useful structured data (any JSON response, not noise). */
|
|
132
|
-
function isUsefulEndpoint(ep) {
|
|
133
|
-
if (isNoiseUrl(ep.url))
|
|
134
|
-
return false;
|
|
135
|
-
return ep.contentType.includes('json');
|
|
136
|
-
}
|
|
137
|
-
// ── Framework detection ────────────────────────────────────────────────────
|
|
138
|
-
const FRAMEWORK_DETECT_JS = detectFramework.toString();
|
|
139
|
-
// ── Store discovery ────────────────────────────────────────────────────────
|
|
140
|
-
const STORE_DISCOVER_JS = discoverStores.toString();
|
|
141
|
-
// ── Auto-Interaction (Fuzzing) ─────────────────────────────────────────────
|
|
142
|
-
const INTERACT_FUZZ_JS = interactFuzz.toString();
|
|
143
|
-
// ── Analysis helpers (extracted from exploreUrl) ───────────────────────────
|
|
144
|
-
/** Filter and deduplicate network endpoints, keeping only useful structured-data APIs. */
|
|
145
|
-
function analyzeEndpoints(networkEntries) {
|
|
146
|
-
const seen = new Map();
|
|
147
|
-
for (const entry of networkEntries) {
|
|
148
|
-
if (!entry.url)
|
|
149
|
-
continue;
|
|
150
|
-
const ct = entry.contentType.toLowerCase();
|
|
151
|
-
if (ct.includes('image/') || ct.includes('font/') || ct.includes('css') || ct.includes('javascript') || ct.includes('wasm'))
|
|
152
|
-
continue;
|
|
153
|
-
if (entry.status && entry.status >= 400)
|
|
154
|
-
continue;
|
|
155
|
-
const pattern = urlToPattern(entry.url);
|
|
156
|
-
const key = `${entry.method}:${pattern}`;
|
|
157
|
-
if (seen.has(key))
|
|
158
|
-
continue;
|
|
159
|
-
const { params: qp, hasSearch, hasPagination, hasLimit } = classifyQueryParams(entry.url);
|
|
160
|
-
const ep = {
|
|
161
|
-
pattern, method: entry.method, url: entry.url, status: entry.status, contentType: ct,
|
|
162
|
-
queryParams: qp, hasSearchParam: hasSearch,
|
|
163
|
-
hasPaginationParam: hasPagination,
|
|
164
|
-
hasLimitParam: hasLimit || qp.some(p => LIMIT_PARAMS.has(p)),
|
|
165
|
-
authIndicators: detectAuthFromHeaders(entry.requestHeaders),
|
|
166
|
-
responseAnalysis: entry.responseBody ? analyzeResponseBody(entry.responseBody) : null,
|
|
167
|
-
};
|
|
168
|
-
seen.set(key, ep);
|
|
169
|
-
}
|
|
170
|
-
// Filter to useful endpoints; deterministic ordering by observable metadata signals
|
|
171
|
-
const analyzed = [...seen.values()]
|
|
172
|
-
.filter(isUsefulEndpoint)
|
|
173
|
-
.sort((a, b) => endpointSortKey(b) - endpointSortKey(a));
|
|
174
|
-
return { analyzed, totalCount: seen.size };
|
|
175
|
-
}
|
|
176
|
-
/** Infer CLI capabilities from analyzed endpoints. */
|
|
177
|
-
function inferCapabilitiesFromEndpoints(endpoints, stores, opts) {
|
|
178
|
-
const capabilities = [];
|
|
179
|
-
const usedNames = new Set();
|
|
180
|
-
for (const ep of endpoints.slice(0, 8)) {
|
|
181
|
-
let capName = inferCapabilityName(ep.url, opts.goal);
|
|
182
|
-
if (usedNames.has(capName)) {
|
|
183
|
-
const suffix = ep.pattern.split('/').filter(s => s && !s.startsWith('{') && !s.includes('.')).pop();
|
|
184
|
-
capName = suffix ? `${capName}_${suffix}` : `${capName}_${usedNames.size}`;
|
|
185
|
-
}
|
|
186
|
-
usedNames.add(capName);
|
|
187
|
-
const cols = [];
|
|
188
|
-
if (ep.responseAnalysis) {
|
|
189
|
-
for (const role of ['title', 'url', 'author', 'score', 'time']) {
|
|
190
|
-
if (ep.responseAnalysis.detectedFields[role])
|
|
191
|
-
cols.push(role);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
const args = [];
|
|
195
|
-
if (ep.hasSearchParam)
|
|
196
|
-
args.push({ name: 'keyword', type: 'str', required: true });
|
|
197
|
-
args.push({ name: 'limit', type: 'int', required: false, default: 20 });
|
|
198
|
-
if (ep.hasPaginationParam)
|
|
199
|
-
args.push({ name: 'page', type: 'int', required: false, default: 1 });
|
|
200
|
-
const epStrategy = inferStrategy(ep.authIndicators);
|
|
201
|
-
let storeHint;
|
|
202
|
-
if ((epStrategy === 'intercept' || ep.authIndicators.includes('signature')) && stores.length > 0) {
|
|
203
|
-
for (const s of stores) {
|
|
204
|
-
const matchingAction = s.actions.find(a => capName.split('_').some(part => a.toLowerCase().includes(part)) ||
|
|
205
|
-
a.toLowerCase().includes('fetch') || a.toLowerCase().includes('get'));
|
|
206
|
-
if (matchingAction) {
|
|
207
|
-
storeHint = { store: s.id, action: matchingAction };
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
capabilities.push({
|
|
213
|
-
name: capName, description: `${opts.site ?? detectSiteName(opts.url)} ${capName}`,
|
|
214
|
-
strategy: storeHint ? 'store-action' : epStrategy,
|
|
215
|
-
endpoint: ep.pattern,
|
|
216
|
-
itemPath: ep.responseAnalysis?.itemPath ?? null,
|
|
217
|
-
recommendedColumns: cols.length ? cols : ['title', 'url'],
|
|
218
|
-
recommendedArgs: args,
|
|
219
|
-
...(storeHint ? { storeHint } : {}),
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
const allAuth = new Set(endpoints.flatMap(ep => ep.authIndicators));
|
|
223
|
-
const topStrategy = allAuth.has('signature') ? 'intercept'
|
|
224
|
-
: allAuth.has('bearer') || allAuth.has('csrf') ? 'header'
|
|
225
|
-
: allAuth.size === 0 ? 'public' : 'cookie';
|
|
226
|
-
return { capabilities, topStrategy, authIndicators: [...allAuth] };
|
|
227
|
-
}
|
|
228
|
-
/** Write explore artifacts (manifest, endpoints, capabilities, auth, stores) to disk. */
|
|
229
|
-
async function writeExploreArtifacts(targetDir, result, analyzedEndpoints, stores) {
|
|
230
|
-
await fs.promises.mkdir(targetDir, { recursive: true });
|
|
231
|
-
const tasks = [
|
|
232
|
-
fs.promises.writeFile(path.join(targetDir, 'manifest.json'), JSON.stringify({
|
|
233
|
-
site: result.site, target_url: result.target_url, final_url: result.final_url, title: result.title,
|
|
234
|
-
framework: result.framework, stores: stores.map(s => ({ type: s.type, id: s.id, actions: s.actions })),
|
|
235
|
-
top_strategy: result.top_strategy, explored_at: new Date().toISOString(),
|
|
236
|
-
}, null, 2)),
|
|
237
|
-
fs.promises.writeFile(path.join(targetDir, 'endpoints.json'), JSON.stringify(analyzedEndpoints.map(ep => ({
|
|
238
|
-
pattern: ep.pattern, method: ep.method, url: ep.url, status: ep.status,
|
|
239
|
-
contentType: ep.contentType, queryParams: ep.queryParams,
|
|
240
|
-
itemPath: ep.responseAnalysis?.itemPath ?? null, itemCount: ep.responseAnalysis?.itemCount ?? 0,
|
|
241
|
-
detectedFields: ep.responseAnalysis?.detectedFields ?? {}, authIndicators: ep.authIndicators,
|
|
242
|
-
})), null, 2)),
|
|
243
|
-
fs.promises.writeFile(path.join(targetDir, 'capabilities.json'), JSON.stringify(result.capabilities, null, 2)),
|
|
244
|
-
fs.promises.writeFile(path.join(targetDir, 'auth.json'), JSON.stringify({
|
|
245
|
-
top_strategy: result.top_strategy, indicators: result.auth_indicators, framework: result.framework,
|
|
246
|
-
}, null, 2)),
|
|
247
|
-
];
|
|
248
|
-
if (stores.length > 0) {
|
|
249
|
-
tasks.push(fs.promises.writeFile(path.join(targetDir, 'stores.json'), JSON.stringify(stores, null, 2)));
|
|
250
|
-
}
|
|
251
|
-
await Promise.all(tasks);
|
|
252
|
-
}
|
|
253
|
-
// ── Main explore function ──────────────────────────────────────────────────
|
|
254
|
-
export async function exploreUrl(url, opts) {
|
|
255
|
-
const waitSeconds = opts.waitSeconds ?? 3.0;
|
|
256
|
-
const exploreTimeout = Math.max(DEFAULT_BROWSER_EXPLORE_TIMEOUT, 45.0 + waitSeconds * 8.0);
|
|
257
|
-
return browserSession(opts.BrowserFactory, async (page) => {
|
|
258
|
-
return runWithTimeout((async () => {
|
|
259
|
-
// Step 1: Navigate
|
|
260
|
-
await page.startNetworkCapture?.().catch(() => { });
|
|
261
|
-
await page.goto(url);
|
|
262
|
-
await page.wait(waitSeconds);
|
|
263
|
-
// Step 2: Auto-scroll to trigger lazy loading intelligently
|
|
264
|
-
await page.autoScroll({ times: 3, delayMs: 1500 }).catch(() => { });
|
|
265
|
-
// Step 2.5: Interactive Fuzzing (if requested)
|
|
266
|
-
if (opts.auto) {
|
|
267
|
-
try {
|
|
268
|
-
// First: targeted clicks by label (e.g. "字幕", "CC", "评论")
|
|
269
|
-
if (opts.clickLabels?.length) {
|
|
270
|
-
for (const label of opts.clickLabels) {
|
|
271
|
-
const safeLabel = JSON.stringify(label);
|
|
272
|
-
await page.evaluate(`
|
|
273
|
-
(() => {
|
|
274
|
-
const el = [...document.querySelectorAll('button, [role="button"], [role="tab"], a, span')]
|
|
275
|
-
.find(e => e.textContent && e.textContent.trim().includes(${safeLabel}));
|
|
276
|
-
if (el) el.click();
|
|
277
|
-
})()
|
|
278
|
-
`);
|
|
279
|
-
await page.wait(1);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
// Then: blind fuzzing on generic interactive elements
|
|
283
|
-
const clicks = await page.evaluate(INTERACT_FUZZ_JS);
|
|
284
|
-
await page.wait(2); // wait for XHRs to settle
|
|
285
|
-
}
|
|
286
|
-
catch (e) {
|
|
287
|
-
log.verbose(`Interactive fuzzing skipped: ${e instanceof Error ? e.message : String(e)}`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
// Step 3: Read page metadata
|
|
291
|
-
const metadata = await readPageMetadata(page);
|
|
292
|
-
// Step 4: Capture network traffic
|
|
293
|
-
const rawNetwork = page.readNetworkCapture
|
|
294
|
-
? await page.readNetworkCapture()
|
|
295
|
-
: await page.networkRequests(false);
|
|
296
|
-
const networkEntries = parseNetworkRequests(rawNetwork);
|
|
297
|
-
// Step 5: For JSON endpoints missing a body, carefully re-fetch in-browser via a pristine iframe
|
|
298
|
-
const jsonEndpoints = networkEntries.filter(e => e.contentType.includes('json') && e.method === 'GET' && e.status === 200 && !e.responseBody);
|
|
299
|
-
await Promise.allSettled(jsonEndpoints.slice(0, 5).map(async (ep) => {
|
|
300
|
-
try {
|
|
301
|
-
const body = await page.evaluate(`async () => {
|
|
302
|
-
let iframe = null;
|
|
303
|
-
try {
|
|
304
|
-
iframe = document.createElement('iframe');
|
|
305
|
-
iframe.style.display = 'none';
|
|
306
|
-
document.body.appendChild(iframe);
|
|
307
|
-
const cleanFetch = iframe.contentWindow.fetch || window.fetch;
|
|
308
|
-
const r = await cleanFetch(${JSON.stringify(ep.url)}, { credentials: 'include' });
|
|
309
|
-
if (!r.ok) return null;
|
|
310
|
-
const d = await r.json();
|
|
311
|
-
return JSON.stringify(d).slice(0, 10000);
|
|
312
|
-
} catch {
|
|
313
|
-
return null;
|
|
314
|
-
} finally {
|
|
315
|
-
if (iframe && iframe.parentNode) iframe.parentNode.removeChild(iframe);
|
|
316
|
-
}
|
|
317
|
-
}`);
|
|
318
|
-
if (body && typeof body === 'string') {
|
|
319
|
-
try {
|
|
320
|
-
ep.responseBody = JSON.parse(body);
|
|
321
|
-
}
|
|
322
|
-
catch { }
|
|
323
|
-
}
|
|
324
|
-
else if (body && typeof body === 'object')
|
|
325
|
-
ep.responseBody = body;
|
|
326
|
-
}
|
|
327
|
-
catch { }
|
|
328
|
-
}));
|
|
329
|
-
// Step 6: Detect framework
|
|
330
|
-
let framework = {};
|
|
331
|
-
try {
|
|
332
|
-
const fw = await page.evaluate(FRAMEWORK_DETECT_JS);
|
|
333
|
-
if (isBooleanRecord(fw))
|
|
334
|
-
framework = fw;
|
|
335
|
-
}
|
|
336
|
-
catch { }
|
|
337
|
-
// Step 6.5: Discover stores (Pinia / Vuex)
|
|
338
|
-
let stores = [];
|
|
339
|
-
if (framework.pinia || framework.vuex) {
|
|
340
|
-
try {
|
|
341
|
-
const raw = await page.evaluate(STORE_DISCOVER_JS);
|
|
342
|
-
if (Array.isArray(raw))
|
|
343
|
-
stores = raw;
|
|
344
|
-
}
|
|
345
|
-
catch { }
|
|
346
|
-
}
|
|
347
|
-
// Step 7+8: Analyze endpoints and infer capabilities
|
|
348
|
-
const { analyzed: analyzedEndpoints, totalCount } = analyzeEndpoints(networkEntries);
|
|
349
|
-
const { capabilities, topStrategy, authIndicators } = inferCapabilitiesFromEndpoints(analyzedEndpoints, stores, { site: opts.site, goal: opts.goal, url });
|
|
350
|
-
// Step 9: Assemble result and write artifacts
|
|
351
|
-
const siteName = opts.site ?? detectSiteName(metadata.url || url);
|
|
352
|
-
const targetDir = opts.outDir ?? path.join('.opencli', 'explore', siteName);
|
|
353
|
-
const result = {
|
|
354
|
-
site: siteName, target_url: url, final_url: metadata.url, title: metadata.title,
|
|
355
|
-
framework, stores, top_strategy: topStrategy,
|
|
356
|
-
endpoint_count: totalCount,
|
|
357
|
-
api_endpoint_count: analyzedEndpoints.length,
|
|
358
|
-
capabilities, auth_indicators: authIndicators,
|
|
359
|
-
};
|
|
360
|
-
await writeExploreArtifacts(targetDir, result, analyzedEndpoints, stores);
|
|
361
|
-
return { ...result, out_dir: targetDir };
|
|
362
|
-
})(), { timeout: exploreTimeout, label: `Explore ${url}` });
|
|
363
|
-
}, { workspace: opts.workspace });
|
|
364
|
-
}
|
|
365
|
-
export function renderExploreSummary(result) {
|
|
366
|
-
const lines = [
|
|
367
|
-
'opencli probe: OK', `Site: ${result.site}`, `URL: ${result.target_url}`,
|
|
368
|
-
`Title: ${result.title || '(none)'}`, `Strategy: ${result.top_strategy}`,
|
|
369
|
-
`Endpoints: ${result.endpoint_count} total, ${result.api_endpoint_count} API`,
|
|
370
|
-
`Capabilities: ${result.capabilities?.length ?? 0}`,
|
|
371
|
-
];
|
|
372
|
-
for (const cap of (result.capabilities ?? []).slice(0, 5)) {
|
|
373
|
-
const storeInfo = cap.storeHint ? ` → ${cap.storeHint.store}.${cap.storeHint.action}()` : '';
|
|
374
|
-
lines.push(` • ${cap.name} (${cap.strategy})${storeInfo}`);
|
|
375
|
-
}
|
|
376
|
-
const fw = result.framework ?? {};
|
|
377
|
-
const fwNames = Object.entries(fw).filter(([, v]) => v).map(([k]) => k);
|
|
378
|
-
if (fwNames.length)
|
|
379
|
-
lines.push(`Framework: ${fwNames.join(', ')}`);
|
|
380
|
-
const stores = result.stores ?? [];
|
|
381
|
-
if (stores.length) {
|
|
382
|
-
lines.push(`Stores: ${stores.length}`);
|
|
383
|
-
for (const s of stores.slice(0, 5)) {
|
|
384
|
-
lines.push(` • ${s.type}/${s.id}: ${s.actions.slice(0, 5).join(', ')}${s.actions.length > 5 ? '...' : ''}`);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
lines.push(`Output: ${result.out_dir}`);
|
|
388
|
-
return lines.join('\n');
|
|
389
|
-
}
|
|
390
|
-
async function readPageMetadata(page) {
|
|
391
|
-
try {
|
|
392
|
-
const result = await page.evaluate(`() => ({ url: window.location.href, title: document.title || '' })`);
|
|
393
|
-
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
|
394
|
-
return {
|
|
395
|
-
url: String(result.url ?? ''),
|
|
396
|
-
title: String(result.title ?? ''),
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
catch { }
|
|
401
|
-
return { url: '', title: '' };
|
|
402
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Verified adapter generation:
|
|
3
|
-
* discover → synthesize → candidate-bound probe → single-session verify.
|
|
4
|
-
*
|
|
5
|
-
* v1 contract keeps scope narrow:
|
|
6
|
-
* - PUBLIC + COOKIE only
|
|
7
|
-
* - read-only JSON API surfaces
|
|
8
|
-
* - single best candidate only
|
|
9
|
-
* - bounded repair: select/itemPath replacement once
|
|
10
|
-
*
|
|
11
|
-
* Contract design principles:
|
|
12
|
-
* 1. machine-readable
|
|
13
|
-
* 2. explicit + explainable
|
|
14
|
-
* 3. testable + versioned
|
|
15
|
-
* 4. taxonomy by skill decision needs (not internal error sources)
|
|
16
|
-
* 5. early hint / terminal outcome share consistent decision language
|
|
17
|
-
*/
|
|
18
|
-
import { type IBrowserFactory } from './runtime.js';
|
|
19
|
-
import { Strategy } from './registry.js';
|
|
20
|
-
export type Stage = 'explore' | 'cascade' | 'synthesize' | 'verify' | 'fallback';
|
|
21
|
-
export type Confidence = 'high' | 'medium' | 'low';
|
|
22
|
-
export type StopReason = 'no-viable-api-surface' | 'auth-too-complex' | 'no-viable-candidate' | 'execution-environment-unavailable';
|
|
23
|
-
export type EscalationReason = 'empty-result' | 'sparse-fields' | 'non-array-result' | 'unsupported-required-args' | 'timeout' | 'selector-mismatch' | 'verify-inconclusive';
|
|
24
|
-
export type SuggestedAction = 'stop' | 'inspect-with-browser' | 'ask-for-login' | 'ask-for-sample-arg' | 'manual-review';
|
|
25
|
-
export type Reusability = 'verified-artifact' | 'unverified-candidate' | 'not-reusable';
|
|
26
|
-
export type EarlyHintReason = 'api-surface-looks-viable' | 'candidate-ready-for-verify' | 'no-viable-api-surface' | 'auth-too-complex' | 'no-viable-candidate';
|
|
27
|
-
export interface EarlyHint {
|
|
28
|
-
stage: 'explore' | 'synthesize' | 'cascade';
|
|
29
|
-
continue: boolean;
|
|
30
|
-
reason: EarlyHintReason;
|
|
31
|
-
confidence: Confidence;
|
|
32
|
-
candidate?: {
|
|
33
|
-
name: string;
|
|
34
|
-
command: string;
|
|
35
|
-
path: string | null;
|
|
36
|
-
reusability: 'unverified-candidate' | 'not-reusable';
|
|
37
|
-
};
|
|
38
|
-
message?: string;
|
|
39
|
-
}
|
|
40
|
-
export type EarlyHintHandler = (hint: EarlyHint) => void;
|
|
41
|
-
type SupportedStrategy = Strategy.PUBLIC | Strategy.COOKIE;
|
|
42
|
-
export interface GenerateStats {
|
|
43
|
-
endpoint_count: number;
|
|
44
|
-
api_endpoint_count: number;
|
|
45
|
-
candidate_count: number;
|
|
46
|
-
verified: boolean;
|
|
47
|
-
repair_attempted: boolean;
|
|
48
|
-
explore_dir: string;
|
|
49
|
-
}
|
|
50
|
-
export interface VerifiedAdapter {
|
|
51
|
-
site: string;
|
|
52
|
-
name: string;
|
|
53
|
-
command: string;
|
|
54
|
-
strategy: SupportedStrategy;
|
|
55
|
-
path: string;
|
|
56
|
-
metadata_path?: string;
|
|
57
|
-
reusability: 'verified-artifact';
|
|
58
|
-
}
|
|
59
|
-
export interface EscalationContext {
|
|
60
|
-
stage: Stage;
|
|
61
|
-
reason: EscalationReason;
|
|
62
|
-
confidence: Confidence;
|
|
63
|
-
suggested_action: SuggestedAction;
|
|
64
|
-
candidate: {
|
|
65
|
-
name: string;
|
|
66
|
-
command: string;
|
|
67
|
-
path: string | null;
|
|
68
|
-
reusability: Reusability;
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
export type GenerateOutcome = {
|
|
72
|
-
status: 'success' | 'blocked' | 'needs-human-check';
|
|
73
|
-
adapter?: VerifiedAdapter;
|
|
74
|
-
reason?: StopReason;
|
|
75
|
-
stage?: Stage;
|
|
76
|
-
confidence?: Confidence;
|
|
77
|
-
escalation?: EscalationContext;
|
|
78
|
-
reusability?: Reusability;
|
|
79
|
-
message?: string;
|
|
80
|
-
stats: GenerateStats;
|
|
81
|
-
};
|
|
82
|
-
export interface GenerateVerifiedOptions {
|
|
83
|
-
url: string;
|
|
84
|
-
BrowserFactory: new () => IBrowserFactory;
|
|
85
|
-
goal?: string | null;
|
|
86
|
-
site?: string;
|
|
87
|
-
waitSeconds?: number;
|
|
88
|
-
top?: number;
|
|
89
|
-
workspace?: string;
|
|
90
|
-
noRegister?: boolean;
|
|
91
|
-
onEarlyHint?: EarlyHintHandler;
|
|
92
|
-
}
|
|
93
|
-
export interface VerifiedArtifactMetadata {
|
|
94
|
-
artifact_kind: 'verified';
|
|
95
|
-
schema_version: 1;
|
|
96
|
-
source_url: string;
|
|
97
|
-
goal: string | null;
|
|
98
|
-
strategy: SupportedStrategy;
|
|
99
|
-
verified: true;
|
|
100
|
-
reusable: true;
|
|
101
|
-
reusability_reason: 'verified-artifact';
|
|
102
|
-
}
|
|
103
|
-
export declare function generateVerifiedFromUrl(opts: GenerateVerifiedOptions): Promise<GenerateOutcome>;
|
|
104
|
-
export declare function renderGenerateVerifiedSummary(result: GenerateOutcome): string;
|
|
105
|
-
export {};
|