@xbrowser/cli 0.14.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/README.md +858 -0
- package/dist/admin-6UTU2RZ2.js +281 -0
- package/dist/admin-MDGF4CET.js +285 -0
- package/dist/admin-RPJJ5CAF.js +282 -0
- package/dist/browser-GWBH6OJK.js +46 -0
- package/dist/browser-I2HJZ7IP.js +48 -0
- package/dist/browser-R7B255ML.js +46 -0
- package/dist/chunk-2ONMTDLK.js +2050 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-43VX3TYN.js +83 -0
- package/dist/chunk-ATFTAKMN.js +267 -0
- package/dist/chunk-DESA2KMG.js +77 -0
- package/dist/chunk-DTJRVA76.js +206 -0
- package/dist/chunk-F3ZWFCJJ.js +2051 -0
- package/dist/chunk-FF5WHQHN.js +135 -0
- package/dist/chunk-HINTG75P.js +77 -0
- package/dist/chunk-KDYXFLAC.js +1503 -0
- package/dist/chunk-KTSQU4QT.js +29 -0
- package/dist/chunk-L53IDAWK.js +68 -0
- package/dist/chunk-M7CMBPCA.js +100 -0
- package/dist/chunk-NFGO7J2I.js +29 -0
- package/dist/chunk-OLB6UJ25.js +438 -0
- package/dist/chunk-OPRXFZVE.js +52 -0
- package/dist/chunk-RS6YYWTK.js +685 -0
- package/dist/chunk-VEDJ5XSQ.js +196 -0
- package/dist/chunk-VEKPHQBR.js +47 -0
- package/dist/chunk-VUJDJCIN.js +437 -0
- package/dist/chunk-YEN2ODUI.js +14 -0
- package/dist/chunk-ZZ2TFWIV.js +1382 -0
- package/dist/cli.js +11012 -0
- package/dist/convert-4DUWZIKH.js +205 -0
- package/dist/convert-EKQVHKB4.js +11 -0
- package/dist/daemon-client-3IJD6X4B.js +59 -0
- package/dist/daemon-client-GX2UYIW4.js +241 -0
- package/dist/daemon-client-XWSSQBEA.js +58 -0
- package/dist/daemon-main.js +9910 -0
- package/dist/extract-EGRXZSSK.js +67 -0
- package/dist/extract-JUOQQX4V.js +11 -0
- package/dist/filter-OLAE26HN.js +51 -0
- package/dist/filter-VID2GGZ7.js +9 -0
- package/dist/human-interaction-QPHNDD76.js +8 -0
- package/dist/index.d.ts +2313 -0
- package/dist/index.js +13839 -0
- package/dist/marketplace-FCVN5OTZ.js +706 -0
- package/dist/marketplace-FPT5YLKB.js +351 -0
- package/dist/marketplace-W545W4FR.js +706 -0
- package/dist/network-store-2S5HATEV.js +194 -0
- package/dist/network-store-BN6QEZ7R.js +196 -0
- package/dist/network-store-YAF5OIBH.js +12 -0
- package/dist/parse-action-dsl-DRSPBALP.js +72 -0
- package/dist/parse-action-dsl-T3DYC33D.js +74 -0
- package/dist/proxy-WKGUCH2C.js +7 -0
- package/dist/session-recorder-ILSSV2UC.js +6 -0
- package/dist/session-recorder-XET3DNML.js +7 -0
- package/package.json +111 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// src/daemon/network-scorer.ts
|
|
2
|
+
var DEFAULT_WEIGHTS = {
|
|
3
|
+
method: { post: 30, get: 10, other: 5 },
|
|
4
|
+
resourceType: { api: 20, document: 0, static: -50, other: 0 },
|
|
5
|
+
size: { goodRange: 20, tooLarge: -10 },
|
|
6
|
+
content: { isJson: 10, hasDataArray: 15, urlContainsApi: 10 }
|
|
7
|
+
};
|
|
8
|
+
var STATIC_TYPES = /* @__PURE__ */ new Set(["stylesheet", "image", "font", "media"]);
|
|
9
|
+
var DATA_ARRAY_KEYS = /* @__PURE__ */ new Set(["data", "list", "items", "results", "records"]);
|
|
10
|
+
function calcMethodScore(entry, weights) {
|
|
11
|
+
const m = entry.method.toUpperCase();
|
|
12
|
+
if (m === "POST" || m === "PUT" || m === "DELETE") return weights.method.post;
|
|
13
|
+
if (m === "GET") return weights.method.get;
|
|
14
|
+
return weights.method.other;
|
|
15
|
+
}
|
|
16
|
+
function calcResourceTypeScore(entry, weights) {
|
|
17
|
+
const rt = entry.resourceType.toLowerCase();
|
|
18
|
+
if (rt === "xhr" || rt === "fetch") return weights.resourceType.api;
|
|
19
|
+
if (rt === "document") return weights.resourceType.document;
|
|
20
|
+
if (STATIC_TYPES.has(rt)) return weights.resourceType.static;
|
|
21
|
+
return weights.resourceType.other;
|
|
22
|
+
}
|
|
23
|
+
function calcSizeScore(entry, weights) {
|
|
24
|
+
if (entry.size > 1024 * 1024) return weights.size.tooLarge;
|
|
25
|
+
const isJson = entry.contentType.toLowerCase().includes("json");
|
|
26
|
+
if (entry.body !== void 0 && isJson && entry.size > 0 && entry.size < 100 * 1024) {
|
|
27
|
+
return weights.size.goodRange;
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
function calcContentScore(entry, weights) {
|
|
32
|
+
let score = 0;
|
|
33
|
+
if (entry.contentType.toLowerCase().includes("json")) {
|
|
34
|
+
score += weights.content.isJson;
|
|
35
|
+
}
|
|
36
|
+
if (typeof entry.body === "object" && entry.body !== null && !Array.isArray(entry.body)) {
|
|
37
|
+
for (const key of Object.keys(entry.body)) {
|
|
38
|
+
if (DATA_ARRAY_KEYS.has(key) && Array.isArray(entry.body[key])) {
|
|
39
|
+
score += weights.content.hasDataArray;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const urlLower = entry.url.toLowerCase();
|
|
45
|
+
if (urlLower.includes("/api/") || urlLower.includes("/v1/") || urlLower.includes("/v2/") || urlLower.includes("/graphql")) {
|
|
46
|
+
score += weights.content.urlContainsApi;
|
|
47
|
+
}
|
|
48
|
+
return score;
|
|
49
|
+
}
|
|
50
|
+
function scoreEntry(entry, weights = DEFAULT_WEIGHTS) {
|
|
51
|
+
const method = calcMethodScore(entry, weights);
|
|
52
|
+
const resourceType = calcResourceTypeScore(entry, weights);
|
|
53
|
+
const size = calcSizeScore(entry, weights);
|
|
54
|
+
const content = calcContentScore(entry, weights);
|
|
55
|
+
return {
|
|
56
|
+
...entry,
|
|
57
|
+
score: method + resourceType + size + content,
|
|
58
|
+
scoreBreakdown: { method, resourceType, size, content }
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function scoreEntries(entries, weights = DEFAULT_WEIGHTS, feedbackFn) {
|
|
62
|
+
return entries.map((e) => {
|
|
63
|
+
const scored = scoreEntry(e, weights);
|
|
64
|
+
if (feedbackFn) {
|
|
65
|
+
scored.score += feedbackFn(e.path, e.method);
|
|
66
|
+
}
|
|
67
|
+
return scored;
|
|
68
|
+
}).sort((a, b) => b.score - a.score);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/daemon/network-store.ts
|
|
72
|
+
var NetworkCaptureStore = class {
|
|
73
|
+
stores = /* @__PURE__ */ new Map();
|
|
74
|
+
maxEntries;
|
|
75
|
+
constructor(maxEntries = 2e3) {
|
|
76
|
+
this.maxEntries = maxEntries;
|
|
77
|
+
}
|
|
78
|
+
getStore(sessionName) {
|
|
79
|
+
let store = this.stores.get(sessionName);
|
|
80
|
+
if (!store) {
|
|
81
|
+
store = { entries: [], counter: 0 };
|
|
82
|
+
this.stores.set(sessionName, store);
|
|
83
|
+
}
|
|
84
|
+
return store;
|
|
85
|
+
}
|
|
86
|
+
add(sessionName, entry) {
|
|
87
|
+
const store = this.getStore(sessionName);
|
|
88
|
+
store.counter += 1;
|
|
89
|
+
const fullEntry = { ...entry, id: store.counter };
|
|
90
|
+
store.entries.push(fullEntry);
|
|
91
|
+
if (store.entries.length > this.maxEntries) {
|
|
92
|
+
store.entries.splice(0, store.entries.length - this.maxEntries);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
list(sessionName, options) {
|
|
96
|
+
const store = this.getStore(sessionName);
|
|
97
|
+
let entries = store.entries;
|
|
98
|
+
if (options?.filter) {
|
|
99
|
+
const f = options.filter.toLowerCase();
|
|
100
|
+
entries = entries.filter(
|
|
101
|
+
(e) => e.url.toLowerCase().includes(f) || e.path.toLowerCase().includes(f) || e.contentType.toLowerCase().includes(f)
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (options?.method) {
|
|
105
|
+
const m = options.method.toUpperCase();
|
|
106
|
+
entries = entries.filter((e) => e.method === m);
|
|
107
|
+
}
|
|
108
|
+
const total = entries.length;
|
|
109
|
+
const offset = options?.offset ?? 0;
|
|
110
|
+
const limit = options?.limit ?? 50;
|
|
111
|
+
const captures = entries.slice(offset, offset + limit);
|
|
112
|
+
return { session: sessionName, total, captures };
|
|
113
|
+
}
|
|
114
|
+
inspect(sessionName, id) {
|
|
115
|
+
const store = this.getStore(sessionName);
|
|
116
|
+
const capture = store.entries.find((e) => e.id === id) ?? null;
|
|
117
|
+
return { session: sessionName, capture };
|
|
118
|
+
}
|
|
119
|
+
clear(sessionName) {
|
|
120
|
+
this.stores.delete(sessionName);
|
|
121
|
+
}
|
|
122
|
+
top(sessionName, options) {
|
|
123
|
+
const store = this.getStore(sessionName);
|
|
124
|
+
const feedbackFn = options?.feedbackFn;
|
|
125
|
+
const scored = feedbackFn ? scoreEntries(store.entries, void 0, feedbackFn) : scoreEntries(store.entries);
|
|
126
|
+
const minScore = options?.minScore ?? 0;
|
|
127
|
+
const filtered = scored.filter((e) => e.score >= minScore);
|
|
128
|
+
const limit = options?.limit ?? 20;
|
|
129
|
+
return { session: sessionName, entries: filtered.slice(0, limit) };
|
|
130
|
+
}
|
|
131
|
+
around(sessionName, commandId, cmdLogStore, windowMs = 5e3) {
|
|
132
|
+
const cmd = cmdLogStore.findEntry(sessionName, commandId);
|
|
133
|
+
if (!cmd) return null;
|
|
134
|
+
const netStore = this.getStore(sessionName);
|
|
135
|
+
const before = netStore.entries.filter(
|
|
136
|
+
(e) => e.timestamp >= cmd.timestamp - windowMs && e.timestamp < cmd.timestamp
|
|
137
|
+
);
|
|
138
|
+
const after = netStore.entries.filter(
|
|
139
|
+
(e) => e.timestamp >= cmd.timestamp && e.timestamp < cmd.timestamp + windowMs
|
|
140
|
+
);
|
|
141
|
+
return { command: cmd, before, after, afterCount: after.length };
|
|
142
|
+
}
|
|
143
|
+
clearAll() {
|
|
144
|
+
this.stores.clear();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
var CommandLogStore = class {
|
|
148
|
+
stores = /* @__PURE__ */ new Map();
|
|
149
|
+
maxEntries;
|
|
150
|
+
constructor(maxEntries = 500) {
|
|
151
|
+
this.maxEntries = maxEntries;
|
|
152
|
+
}
|
|
153
|
+
getStore(sessionName) {
|
|
154
|
+
let store = this.stores.get(sessionName);
|
|
155
|
+
if (!store) {
|
|
156
|
+
store = { entries: [], counter: 0 };
|
|
157
|
+
this.stores.set(sessionName, store);
|
|
158
|
+
}
|
|
159
|
+
return store;
|
|
160
|
+
}
|
|
161
|
+
add(sessionName, entry) {
|
|
162
|
+
const store = this.getStore(sessionName);
|
|
163
|
+
store.counter += 1;
|
|
164
|
+
const fullEntry = { ...entry, id: store.counter };
|
|
165
|
+
store.entries.push(fullEntry);
|
|
166
|
+
if (store.entries.length > this.maxEntries) {
|
|
167
|
+
store.entries.splice(0, store.entries.length - this.maxEntries);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
list(sessionName, options) {
|
|
171
|
+
const store = this.getStore(sessionName);
|
|
172
|
+
const offset = options?.offset ?? 0;
|
|
173
|
+
const limit = options?.limit ?? 50;
|
|
174
|
+
return store.entries.slice(offset, offset + limit);
|
|
175
|
+
}
|
|
176
|
+
findEntry(sessionName, id) {
|
|
177
|
+
const store = this.stores.get(sessionName);
|
|
178
|
+
return store?.entries.find((e) => e.id === id) ?? null;
|
|
179
|
+
}
|
|
180
|
+
clear(sessionName) {
|
|
181
|
+
this.stores.delete(sessionName);
|
|
182
|
+
}
|
|
183
|
+
clearAll() {
|
|
184
|
+
this.stores.clear();
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
var commandLogStore = new CommandLogStore();
|
|
188
|
+
var networkStore = new NetworkCaptureStore();
|
|
189
|
+
|
|
190
|
+
export {
|
|
191
|
+
scoreEntries,
|
|
192
|
+
NetworkCaptureStore,
|
|
193
|
+
CommandLogStore,
|
|
194
|
+
commandLogStore,
|
|
195
|
+
networkStore
|
|
196
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/utils/proxy-fetch.ts
|
|
2
|
+
var patched = false;
|
|
3
|
+
async function ensureProxyFetch() {
|
|
4
|
+
if (patched) return;
|
|
5
|
+
patched = true;
|
|
6
|
+
if (process.env.https_proxy && !process.env.HTTPS_PROXY) {
|
|
7
|
+
process.env.HTTPS_PROXY = process.env.https_proxy;
|
|
8
|
+
}
|
|
9
|
+
if (process.env.http_proxy && !process.env.HTTP_PROXY) {
|
|
10
|
+
process.env.HTTP_PROXY = process.env.http_proxy;
|
|
11
|
+
}
|
|
12
|
+
if (process.env.all_proxy && !process.env.ALL_PROXY) {
|
|
13
|
+
process.env.ALL_PROXY = process.env.all_proxy;
|
|
14
|
+
}
|
|
15
|
+
const proxyUrl = process.env.https_proxy || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.HTTP_PROXY || process.env.all_proxy || process.env.ALL_PROXY;
|
|
16
|
+
if (!proxyUrl) return;
|
|
17
|
+
try {
|
|
18
|
+
const undici = await import("undici");
|
|
19
|
+
const EnvHttpProxyAgent = undici.EnvHttpProxyAgent;
|
|
20
|
+
const uFetch = undici.fetch;
|
|
21
|
+
const UFormData = undici.FormData;
|
|
22
|
+
if (EnvHttpProxyAgent && uFetch && UFormData) {
|
|
23
|
+
const agent = new EnvHttpProxyAgent();
|
|
24
|
+
globalThis.fetch = ((input, init) => {
|
|
25
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
26
|
+
const body = init?.body;
|
|
27
|
+
if (body instanceof globalThis.FormData && !(body instanceof UFormData)) {
|
|
28
|
+
const ufd = new UFormData();
|
|
29
|
+
body.forEach((value, key) => {
|
|
30
|
+
if (value instanceof Blob) {
|
|
31
|
+
ufd.append(key, value, value.name || "file");
|
|
32
|
+
} else {
|
|
33
|
+
ufd.append(key, value);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
return uFetch(url, { ...init, body: ufd, dispatcher: agent });
|
|
37
|
+
}
|
|
38
|
+
return uFetch(url, { ...init, dispatcher: agent });
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
ensureProxyFetch
|
|
47
|
+
};
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCaptchaConfig
|
|
3
|
+
} from "./chunk-M7CMBPCA.js";
|
|
4
|
+
|
|
5
|
+
// src/human-interaction.ts
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
|
|
8
|
+
// src/screencast.ts
|
|
9
|
+
var ScreencastCapturer = class {
|
|
10
|
+
interval;
|
|
11
|
+
quality;
|
|
12
|
+
type;
|
|
13
|
+
maxWidth;
|
|
14
|
+
maxHeight;
|
|
15
|
+
isCapturing = false;
|
|
16
|
+
frameCallback = null;
|
|
17
|
+
// CDP Cast state
|
|
18
|
+
cdpSession = null;
|
|
19
|
+
sessionId = "";
|
|
20
|
+
// Fallback polling state
|
|
21
|
+
fallbackTimer = null;
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.interval = options.interval || 100;
|
|
24
|
+
this.quality = options.quality || 60;
|
|
25
|
+
this.type = options.type || "jpeg";
|
|
26
|
+
this.maxWidth = options.width || 1920;
|
|
27
|
+
this.maxHeight = options.height || 1080;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Start screencast capture using CDP Page.startScreencast.
|
|
31
|
+
*
|
|
32
|
+
* If CDP session creation fails (non-Chromium, restricted access),
|
|
33
|
+
* automatically falls back to `page.screenshot()` polling.
|
|
34
|
+
*/
|
|
35
|
+
async startCapture(page, sessionId, onFrame) {
|
|
36
|
+
if (this.isCapturing) {
|
|
37
|
+
throw new Error("Screencast is already capturing");
|
|
38
|
+
}
|
|
39
|
+
this.isCapturing = true;
|
|
40
|
+
this.frameCallback = onFrame;
|
|
41
|
+
this.sessionId = sessionId;
|
|
42
|
+
try {
|
|
43
|
+
const cdp = await page.context().newCDPSession(page);
|
|
44
|
+
this.cdpSession = cdp;
|
|
45
|
+
cdp.on("Page.screencastFrame", async (params) => {
|
|
46
|
+
if (!this.frameCallback) return;
|
|
47
|
+
try {
|
|
48
|
+
await cdp.send("Page.screencastFrameAck", { sessionId: params.sessionId });
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
const viewport = {
|
|
52
|
+
width: params.metadata?.deviceWidth || this.maxWidth,
|
|
53
|
+
height: params.metadata?.deviceHeight || this.maxHeight
|
|
54
|
+
};
|
|
55
|
+
this.frameCallback({
|
|
56
|
+
id: crypto.randomUUID(),
|
|
57
|
+
sessionId: this.sessionId,
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
data: Buffer.from(params.data, "base64"),
|
|
60
|
+
url: page.url(),
|
|
61
|
+
viewport
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
await cdp.send("Page.startScreencast", {
|
|
65
|
+
format: this.type === "png" ? "png" : "jpeg",
|
|
66
|
+
quality: this.type === "jpeg" ? this.quality : void 0,
|
|
67
|
+
maxWidth: this.maxWidth,
|
|
68
|
+
maxHeight: this.maxHeight
|
|
69
|
+
});
|
|
70
|
+
} catch {
|
|
71
|
+
this.cdpSession = null;
|
|
72
|
+
this.startFallbackPolling(page, sessionId);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Fallback: periodic page.screenshot() polling when CDP Cast is unavailable.
|
|
77
|
+
*/
|
|
78
|
+
startFallbackPolling(page, sessionId) {
|
|
79
|
+
const captureLoop = async () => {
|
|
80
|
+
if (!this.frameCallback) return;
|
|
81
|
+
try {
|
|
82
|
+
const frame = await this.captureFrame(page, sessionId);
|
|
83
|
+
this.frameCallback(frame);
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
captureLoop();
|
|
88
|
+
this.fallbackTimer = setInterval(captureLoop, this.interval);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Capture a single screenshot frame from the page (fallback mode).
|
|
92
|
+
*/
|
|
93
|
+
async captureFrame(page, sessionId) {
|
|
94
|
+
let viewport = page.viewportSize();
|
|
95
|
+
if (!viewport) {
|
|
96
|
+
try {
|
|
97
|
+
viewport = await page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight }));
|
|
98
|
+
} catch {
|
|
99
|
+
viewport = { width: 1920, height: 1080 };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const screenshot = await page.screenshot({
|
|
103
|
+
type: this.type,
|
|
104
|
+
quality: this.type === "jpeg" ? this.quality : void 0
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
id: crypto.randomUUID(),
|
|
108
|
+
sessionId,
|
|
109
|
+
timestamp: Date.now(),
|
|
110
|
+
data: screenshot,
|
|
111
|
+
url: page.url(),
|
|
112
|
+
viewport: viewport || { width: 0, height: 0 }
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Stop the current screencast capture.
|
|
117
|
+
*/
|
|
118
|
+
async stopCapture() {
|
|
119
|
+
if (this.cdpSession) {
|
|
120
|
+
try {
|
|
121
|
+
this.cdpSession.off("Page.screencastFrame", () => {
|
|
122
|
+
});
|
|
123
|
+
await this.cdpSession.send("Page.stopScreencast");
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
await this.cdpSession.detach();
|
|
128
|
+
} catch {
|
|
129
|
+
}
|
|
130
|
+
this.cdpSession = null;
|
|
131
|
+
}
|
|
132
|
+
if (this.fallbackTimer) {
|
|
133
|
+
clearInterval(this.fallbackTimer);
|
|
134
|
+
this.fallbackTimer = null;
|
|
135
|
+
}
|
|
136
|
+
this.isCapturing = false;
|
|
137
|
+
this.frameCallback = null;
|
|
138
|
+
}
|
|
139
|
+
isActive() {
|
|
140
|
+
return this.isCapturing;
|
|
141
|
+
}
|
|
142
|
+
setInterval(interval) {
|
|
143
|
+
this.interval = interval;
|
|
144
|
+
}
|
|
145
|
+
setQuality(quality) {
|
|
146
|
+
this.quality = quality;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// src/captcha-detector.ts
|
|
151
|
+
var CAPTCHA_SELECTORS = [
|
|
152
|
+
{ selector: 'iframe[src*="recaptcha"]', type: "recaptcha", confidence: "high" },
|
|
153
|
+
{ selector: ".g-recaptcha", type: "recaptcha", confidence: "high" },
|
|
154
|
+
{ selector: "#recaptcha", type: "recaptcha", confidence: "high" },
|
|
155
|
+
{ selector: "[data-sitekey]", type: "recaptcha", confidence: "medium" },
|
|
156
|
+
{ selector: 'iframe[src*="hcaptcha"]', type: "hcaptcha", confidence: "high" },
|
|
157
|
+
{ selector: ".h-captcha", type: "hcaptcha", confidence: "high" },
|
|
158
|
+
{ selector: 'iframe[src*="challenges.cloudflare.com"]', type: "turnstile", confidence: "high" },
|
|
159
|
+
{ selector: ".cf-turnstile", type: "turnstile", confidence: "high" },
|
|
160
|
+
{ selector: 'iframe[src*="captcha"]', type: "generic", confidence: "medium" },
|
|
161
|
+
{ selector: "[data-captcha]", type: "generic", confidence: "medium" },
|
|
162
|
+
{ selector: ".captcha-container", type: "generic", confidence: "medium" },
|
|
163
|
+
{ selector: "#captcha", type: "generic", confidence: "medium" },
|
|
164
|
+
{ selector: ".captcha-image", type: "generic", confidence: "medium" },
|
|
165
|
+
{ selector: "#captcha_image", type: "generic", confidence: "medium" },
|
|
166
|
+
// 小红书滑块验证
|
|
167
|
+
{ selector: ".captcha-verify-image", type: "xhs-slider", confidence: "high" },
|
|
168
|
+
{ selector: ".verify-wrap", type: "xhs-slider", confidence: "medium" },
|
|
169
|
+
{ selector: '[class*="slider-verify"]', type: "xhs-slider", confidence: "medium" },
|
|
170
|
+
{ selector: '[class*="captcha-verify"]', type: "xhs-slider", confidence: "medium" },
|
|
171
|
+
{ selector: '[class*="verify-image"]', type: "xhs-slider", confidence: "low" }
|
|
172
|
+
];
|
|
173
|
+
var CAPTCHA_TEXT_PATTERNS = [
|
|
174
|
+
"verify you are human",
|
|
175
|
+
"prove you are not a robot",
|
|
176
|
+
"complete the challenge",
|
|
177
|
+
"are you a robot",
|
|
178
|
+
"human verification",
|
|
179
|
+
"security check",
|
|
180
|
+
"prove you're human",
|
|
181
|
+
"not a robot",
|
|
182
|
+
// 小红书验证提示
|
|
183
|
+
/xiaohongshu.*verif/i,
|
|
184
|
+
/拖动滑块/,
|
|
185
|
+
/请完成验证/,
|
|
186
|
+
/slide.*verify/i
|
|
187
|
+
];
|
|
188
|
+
var CaptchaDetector = class {
|
|
189
|
+
/**
|
|
190
|
+
* Scan the page for visible CAPTCHA elements or challenge text.
|
|
191
|
+
*
|
|
192
|
+
* @param page - The Playwright page to scan.
|
|
193
|
+
* @returns Detection result with type, selector, and confidence level.
|
|
194
|
+
*/
|
|
195
|
+
static async detect(page) {
|
|
196
|
+
for (const rule of CAPTCHA_SELECTORS) {
|
|
197
|
+
try {
|
|
198
|
+
const el = await page.$(rule.selector);
|
|
199
|
+
if (el) {
|
|
200
|
+
const visible = await el.isVisible().catch(() => false);
|
|
201
|
+
if (visible) {
|
|
202
|
+
return {
|
|
203
|
+
detected: true,
|
|
204
|
+
type: rule.type,
|
|
205
|
+
selector: rule.selector,
|
|
206
|
+
confidence: rule.confidence
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const bodyText = await page.textContent("body").catch(() => "");
|
|
215
|
+
if (bodyText) {
|
|
216
|
+
for (const pattern of CAPTCHA_TEXT_PATTERNS) {
|
|
217
|
+
const matches = pattern instanceof RegExp ? pattern.test(bodyText) : bodyText.toLowerCase().includes(pattern);
|
|
218
|
+
if (matches) {
|
|
219
|
+
return {
|
|
220
|
+
detected: true,
|
|
221
|
+
type: "text-challenge",
|
|
222
|
+
confidence: "low"
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
229
|
+
return { detected: false, confidence: "low" };
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Check whether a previously detected CAPTCHA has been solved.
|
|
233
|
+
*
|
|
234
|
+
* @param page - The Playwright page to check.
|
|
235
|
+
* @param previousSelector - The selector from a previous detection result.
|
|
236
|
+
* @returns `true` if the CAPTCHA is no longer visible.
|
|
237
|
+
*/
|
|
238
|
+
static async isSolved(page, previousSelector) {
|
|
239
|
+
if (previousSelector) {
|
|
240
|
+
try {
|
|
241
|
+
const el = await page.$(previousSelector);
|
|
242
|
+
if (!el) return true;
|
|
243
|
+
const visible = await el.isVisible().catch(() => false);
|
|
244
|
+
if (!visible) return true;
|
|
245
|
+
} catch {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const result = await this.detect(page);
|
|
250
|
+
return !result.detected;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// src/webhook.ts
|
|
255
|
+
var WebhookNotifier = class {
|
|
256
|
+
url;
|
|
257
|
+
constructor(url) {
|
|
258
|
+
this.url = url || process.env.XBROWSER_NOTIFY_URL || null;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Send a webhook notification payload.
|
|
262
|
+
*
|
|
263
|
+
* @param payload - The event payload to send.
|
|
264
|
+
* @returns `true` if the request succeeded (HTTP 2xx), `false` otherwise.
|
|
265
|
+
*/
|
|
266
|
+
async notify(payload) {
|
|
267
|
+
if (!this.url) return false;
|
|
268
|
+
try {
|
|
269
|
+
const response = await fetch(this.url, {
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers: { "Content-Type": "application/json" },
|
|
272
|
+
body: JSON.stringify(payload),
|
|
273
|
+
signal: AbortSignal.timeout(5e3)
|
|
274
|
+
});
|
|
275
|
+
return response.ok;
|
|
276
|
+
} catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// src/utils/shell-escape.ts
|
|
283
|
+
function shellEscape(value) {
|
|
284
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/human-interaction.ts
|
|
288
|
+
var HumanInteractionManager = class {
|
|
289
|
+
wsServer;
|
|
290
|
+
page;
|
|
291
|
+
capturer;
|
|
292
|
+
webhook;
|
|
293
|
+
autoOpen;
|
|
294
|
+
constructor(wsServer, page) {
|
|
295
|
+
this.wsServer = wsServer;
|
|
296
|
+
this.page = page;
|
|
297
|
+
this.capturer = new ScreencastCapturer();
|
|
298
|
+
const cfg = getCaptchaConfig();
|
|
299
|
+
this.webhook = new WebhookNotifier(cfg.notifyUrl);
|
|
300
|
+
this.autoOpen = cfg.autoOpen;
|
|
301
|
+
this.wsServer.registerSession("default", page);
|
|
302
|
+
}
|
|
303
|
+
async sendWebhook(event, overrides = {}) {
|
|
304
|
+
await this.webhook.notify({
|
|
305
|
+
event,
|
|
306
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
307
|
+
url: this.page.url(),
|
|
308
|
+
previewUrl: `http://localhost:${this.wsServer.getPort()}`,
|
|
309
|
+
...overrides
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
tryAutoOpen(previewUrl) {
|
|
313
|
+
if (!this.autoOpen) return;
|
|
314
|
+
try {
|
|
315
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : "start";
|
|
316
|
+
execSync(`${cmd} ${shellEscape(previewUrl)}`, { stdio: "ignore" });
|
|
317
|
+
} catch {
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Wait for a human to solve a CAPTCHA or complete an interaction.
|
|
322
|
+
*
|
|
323
|
+
* Starts screencast streaming, sends webhook and broadcast notifications,
|
|
324
|
+
* then polls for CAPTCHA resolution or waits for a manual solve signal.
|
|
325
|
+
*
|
|
326
|
+
* @param options - Configuration for timeout, auto-detection, and reason text.
|
|
327
|
+
* @returns Result indicating whether the CAPTCHA was solved and by what method.
|
|
328
|
+
*/
|
|
329
|
+
async waitForHuman(options = {}) {
|
|
330
|
+
const {
|
|
331
|
+
reason = "Human interaction required",
|
|
332
|
+
timeout = 120,
|
|
333
|
+
autoDetect = true,
|
|
334
|
+
detectInterval = 2e3
|
|
335
|
+
} = options;
|
|
336
|
+
const captcha = await CaptchaDetector.detect(this.page);
|
|
337
|
+
const captchaInfo = captcha.detected ? captcha : void 0;
|
|
338
|
+
await this.capturer.startCapture(this.page, "default", (frame) => {
|
|
339
|
+
this.wsServer.broadcast({
|
|
340
|
+
type: "screenshot",
|
|
341
|
+
data: {
|
|
342
|
+
sessionId: frame.sessionId,
|
|
343
|
+
id: frame.id,
|
|
344
|
+
timestamp: frame.timestamp,
|
|
345
|
+
data: frame.data,
|
|
346
|
+
url: frame.url,
|
|
347
|
+
viewport: frame.viewport
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
const previewUrl = `http://localhost:${this.wsServer.getPort()}`;
|
|
352
|
+
await this.sendWebhook("captcha-detected", {
|
|
353
|
+
reason: captchaInfo ? `${captchaInfo.type ?? "unknown"} CAPTCHA detected` : reason,
|
|
354
|
+
timeout,
|
|
355
|
+
targetUrl: this.page.url()
|
|
356
|
+
});
|
|
357
|
+
this.wsServer.broadcast({
|
|
358
|
+
type: "captcha-detected",
|
|
359
|
+
sessionId: "default",
|
|
360
|
+
url: this.page.url(),
|
|
361
|
+
reason: captchaInfo ? `${captchaInfo.type ?? "unknown"} CAPTCHA detected` : reason,
|
|
362
|
+
timeout
|
|
363
|
+
});
|
|
364
|
+
this.tryAutoOpen(previewUrl);
|
|
365
|
+
console.log("");
|
|
366
|
+
console.log("\u26A0\uFE0F \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
367
|
+
console.log(`\u26A0\uFE0F ${captchaInfo ? (captchaInfo.type ?? "UNKNOWN").toUpperCase() + " CAPTCHA" : "INTERACTION"} REQUIRED`);
|
|
368
|
+
console.log(`\u26A0\uFE0F URL: ${this.page.url()}`);
|
|
369
|
+
console.log(`\u26A0\uFE0F `);
|
|
370
|
+
console.log(`\u26A0\uFE0F Solve via:`);
|
|
371
|
+
console.log(`\u26A0\uFE0F \u{1F4FA} Preview: ${previewUrl}`);
|
|
372
|
+
console.log(`\u26A0\uFE0F \u{1F310} Direct: ${this.page.url()}`);
|
|
373
|
+
console.log(`\u26A0\uFE0F \u23ED\uFE0F Skip`);
|
|
374
|
+
console.log(`\u26A0\uFE0F \u274C Abort`);
|
|
375
|
+
console.log(`\u26A0\uFE0F `);
|
|
376
|
+
console.log(`\u26A0\uFE0F \u23F3 Waiting... (${timeout}s timeout)`);
|
|
377
|
+
console.log("\u26A0\uFE0F \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
378
|
+
console.log("");
|
|
379
|
+
return new Promise((resolve) => {
|
|
380
|
+
let resolved = false;
|
|
381
|
+
let pollTimer = null;
|
|
382
|
+
let timeoutTimer = null;
|
|
383
|
+
const cleanup = () => {
|
|
384
|
+
if (pollTimer) clearInterval(pollTimer);
|
|
385
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
386
|
+
this.wsServer.removeListener("human-solved", onHumanSolved);
|
|
387
|
+
this.capturer.stopCapture();
|
|
388
|
+
};
|
|
389
|
+
if (autoDetect && captchaInfo) {
|
|
390
|
+
pollTimer = setInterval(async () => {
|
|
391
|
+
if (resolved) return;
|
|
392
|
+
try {
|
|
393
|
+
const solved = await CaptchaDetector.isSolved(this.page, captchaInfo.selector);
|
|
394
|
+
if (solved) {
|
|
395
|
+
resolved = true;
|
|
396
|
+
cleanup();
|
|
397
|
+
this.wsServer.broadcast({ type: "resolved", sessionId: "default" });
|
|
398
|
+
console.log("\u2705 CAPTCHA auto-detected as solved!");
|
|
399
|
+
this.sendWebhook("captcha-resolved", { reason: "auto-detected" });
|
|
400
|
+
resolve({ solved: true, method: "auto-detected" });
|
|
401
|
+
}
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
}, detectInterval);
|
|
405
|
+
}
|
|
406
|
+
const onHumanSolved = () => {
|
|
407
|
+
if (!resolved) {
|
|
408
|
+
resolved = true;
|
|
409
|
+
cleanup();
|
|
410
|
+
this.wsServer.broadcast({ type: "resolved", sessionId: "default" });
|
|
411
|
+
console.log("\u2705 CAPTCHA solved via preview!");
|
|
412
|
+
this.sendWebhook("captcha-resolved", { reason: "preview" });
|
|
413
|
+
resolve({ solved: true, method: "preview" });
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
this.wsServer.on("human-solved", onHumanSolved);
|
|
417
|
+
if (timeout > 0) {
|
|
418
|
+
timeoutTimer = setTimeout(() => {
|
|
419
|
+
if (!resolved) {
|
|
420
|
+
resolved = true;
|
|
421
|
+
cleanup();
|
|
422
|
+
console.log("\u23F0 Timeout - skipping");
|
|
423
|
+
this.sendWebhook("captcha-resolved", { reason: "timeout" });
|
|
424
|
+
resolve({ solved: false, method: "timeout" });
|
|
425
|
+
}
|
|
426
|
+
}, timeout * 1e3);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
export {
|
|
433
|
+
ScreencastCapturer,
|
|
434
|
+
CaptchaDetector,
|
|
435
|
+
WebhookNotifier,
|
|
436
|
+
HumanInteractionManager
|
|
437
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/utils/json-file.ts
|
|
2
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
3
|
+
function readJsonFile(filePath, defaultValue) {
|
|
4
|
+
try {
|
|
5
|
+
const content = readFileSync(filePath, "utf-8");
|
|
6
|
+
return JSON.parse(content);
|
|
7
|
+
} catch {
|
|
8
|
+
return defaultValue;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
readJsonFile
|
|
14
|
+
};
|