real-browser-mcp-server 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/general_issue.yaml +58 -0
- package/.github/SETUP.md +111 -0
- package/.github/workflows/publish.yml +135 -0
- package/Dockerfile +79 -0
- package/LICENSE.md +21 -0
- package/README.md +244 -0
- package/lib/cjs/adblocker.bin +0 -0
- package/lib/cjs/index.js +249 -0
- package/lib/cjs/module/pageController.js +70 -0
- package/lib/cjs/module/turnstile.js +62 -0
- package/lib/esm/adblocker.bin +0 -0
- package/lib/esm/index.mjs +245 -0
- package/lib/esm/module/pageController.mjs +68 -0
- package/lib/esm/module/turnstile.mjs +59 -0
- package/package.json +50 -0
- package/src/ai/action-parser.js +274 -0
- package/src/ai/core.js +378 -0
- package/src/ai/element-finder.js +466 -0
- package/src/ai/index.js +82 -0
- package/src/ai/page-analyzer.js +304 -0
- package/src/ai/selector-healer.js +236 -0
- package/src/index.js +121 -0
- package/src/mcp/handlers.js +5071 -0
- package/src/mcp/index.js +190 -0
- package/src/mcp/server.js +144 -0
- package/src/mcp/tools.js +18 -0
- package/src/shared/tools.js +618 -0
- package/test/cjs/test.js +259 -0
- package/test/esm/package.json +13 -0
- package/test/esm/test.js +226 -0
- package/test/esm/test_option2.js +46 -0
- package/test/esm/test_playwright_ghost.js +30 -0
- package/typings.d.ts +79 -0
package/lib/cjs/index.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
const { PlaywrightBlocker } = require("@ghostery/adblocker-playwright");
|
|
2
|
+
const { pageController } = require("./module/pageController.js");
|
|
3
|
+
|
|
4
|
+
// Dynamically load pure ESM playwright-ghost to be fully compatible with CommonJS
|
|
5
|
+
const playwrightGhostPromise = import("playwright-ghost/patchright");
|
|
6
|
+
const recommendedPromise = import("playwright-ghost/plugins/recommended");
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
let adBlockerInstance = null;
|
|
11
|
+
let adBlockerPromise = null;
|
|
12
|
+
function getAdBlocker() {
|
|
13
|
+
if (!adBlockerPromise) {
|
|
14
|
+
const cachePath = path.join(__dirname, 'adblocker.bin');
|
|
15
|
+
adBlockerPromise = PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch, {
|
|
16
|
+
path: cachePath,
|
|
17
|
+
read: fs.promises.readFile,
|
|
18
|
+
write: fs.promises.writeFile,
|
|
19
|
+
}).then(blocker => {
|
|
20
|
+
adBlockerInstance = blocker;
|
|
21
|
+
return blocker;
|
|
22
|
+
}).catch(err => {
|
|
23
|
+
console.error('[adblocker] Failed to initialize adblocker:', err.message);
|
|
24
|
+
return null;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return adBlockerPromise;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function loadEnvFile() {
|
|
31
|
+
const envPaths = [
|
|
32
|
+
path.join(process.cwd(), '.env'),
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
let currentDir = process.cwd();
|
|
36
|
+
for (let i = 0; i < 5; i++) {
|
|
37
|
+
const envPath = path.join(currentDir, '.env');
|
|
38
|
+
if (fs.existsSync(envPath) && !envPaths.includes(envPath)) {
|
|
39
|
+
envPaths.push(envPath);
|
|
40
|
+
}
|
|
41
|
+
const parentDir = path.dirname(currentDir);
|
|
42
|
+
if (parentDir === currentDir) break;
|
|
43
|
+
currentDir = parentDir;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const envPath of envPaths) {
|
|
47
|
+
try {
|
|
48
|
+
if (fs.existsSync(envPath)) {
|
|
49
|
+
const envContent = fs.readFileSync(envPath, 'utf-8');
|
|
50
|
+
envContent.split('\n').forEach(line => {
|
|
51
|
+
const trimmed = line.trim();
|
|
52
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
53
|
+
const [key, ...valueParts] = trimmed.split('=');
|
|
54
|
+
const value = valueParts.join('=').replace(/^["']|["']$/g, '');
|
|
55
|
+
if (key && !process.env[key]) {
|
|
56
|
+
process.env[key] = value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// Silently ignore .env loading errors
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
loadEnvFile();
|
|
69
|
+
|
|
70
|
+
function getDefaultHeadless() {
|
|
71
|
+
const envHeadless = (process.env.HEADLESS || '').toLowerCase();
|
|
72
|
+
return envHeadless === 'true';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function applyPuppeteerShims(browser, page) {
|
|
76
|
+
if (page._shimsApplied) return page;
|
|
77
|
+
page._shimsApplied = true;
|
|
78
|
+
|
|
79
|
+
// Enable ad blocker
|
|
80
|
+
if (adBlockerInstance) {
|
|
81
|
+
adBlockerInstance.enableBlockingInPage(page).catch(() => {});
|
|
82
|
+
} else {
|
|
83
|
+
getAdBlocker().then(blocker => {
|
|
84
|
+
if (blocker) {
|
|
85
|
+
blocker.enableBlockingInPage(page).catch(() => {});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Cookie shims (Puppeteer → Playwright context)
|
|
91
|
+
if (!page.cookies) {
|
|
92
|
+
page.cookies = async (urls) => {
|
|
93
|
+
return await page.context().cookies(urls ? (Array.isArray(urls) ? urls : [urls]) : []);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!page.setCookie) {
|
|
98
|
+
page.setCookie = async (...cookies) => {
|
|
99
|
+
const formatted = cookies.map(c => ({
|
|
100
|
+
name: c.name,
|
|
101
|
+
value: c.value,
|
|
102
|
+
domain: c.domain || new URL(page.url()).hostname,
|
|
103
|
+
path: c.path || '/',
|
|
104
|
+
expires: c.expires || undefined,
|
|
105
|
+
httpOnly: c.httpOnly,
|
|
106
|
+
secure: c.secure,
|
|
107
|
+
sameSite: c.sameSite
|
|
108
|
+
}));
|
|
109
|
+
await page.context().addCookies(formatted);
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!page.deleteCookie) {
|
|
114
|
+
page.deleteCookie = async (...cookies) => {
|
|
115
|
+
await page.context().clearCookies();
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Navigation shims
|
|
120
|
+
if (!page.waitForNetworkIdle) {
|
|
121
|
+
page.waitForNetworkIdle = async (options = {}) => {
|
|
122
|
+
try {
|
|
123
|
+
await page.waitForLoadState('networkidle', { timeout: options.timeout });
|
|
124
|
+
} catch (e) {
|
|
125
|
+
// Ignore networkidle timeouts if they are non-fatal
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!page.evaluateOnNewDocument) {
|
|
131
|
+
page.evaluateOnNewDocument = async (fn, ...args) => {
|
|
132
|
+
return await page.addInitScript(fn, ...args);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Ghost Cursor integration via playwright-ghost auto-hooking
|
|
137
|
+
page.realCursor = {
|
|
138
|
+
move: async (selector, options = {}) => {
|
|
139
|
+
try {
|
|
140
|
+
await page.hover(selector, options);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
// Silently fallback if element is not found
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
page.realClick = async (selector, options = {}) => {
|
|
147
|
+
await page.click(selector, options);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Mouse wheel shim (Puppeteer object → Playwright positional args)
|
|
151
|
+
if (page.mouse && page.mouse.wheel) {
|
|
152
|
+
const originalWheel = page.mouse.wheel.bind(page.mouse);
|
|
153
|
+
page.mouse.wheel = async (x, y) => {
|
|
154
|
+
if (typeof x === 'object' && x !== null) {
|
|
155
|
+
const deltaX = x.deltaX || 0;
|
|
156
|
+
const deltaY = x.deltaY || 0;
|
|
157
|
+
return await originalWheel(deltaX, deltaY);
|
|
158
|
+
}
|
|
159
|
+
return await originalWheel(x, y);
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Browser shims
|
|
164
|
+
if (!browser.process) {
|
|
165
|
+
browser.process = () => ({
|
|
166
|
+
pid: browser._childProcess?.pid || 1337
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!browser.pages) {
|
|
171
|
+
browser.pages = async () => {
|
|
172
|
+
return browser.contexts().flatMap(c => c.pages());
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return page;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function connect({
|
|
180
|
+
args = [],
|
|
181
|
+
headless = getDefaultHeadless(),
|
|
182
|
+
proxy = {},
|
|
183
|
+
turnstile = false,
|
|
184
|
+
} = {}) {
|
|
185
|
+
let playwrightProxy = undefined;
|
|
186
|
+
if (proxy && proxy.host && proxy.port) {
|
|
187
|
+
playwrightProxy = {
|
|
188
|
+
server: `${proxy.host}:${proxy.port}`
|
|
189
|
+
};
|
|
190
|
+
if (proxy.username && proxy.password) {
|
|
191
|
+
playwrightProxy.username = proxy.username;
|
|
192
|
+
playwrightProxy.password = proxy.password;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const chromiumArgs = [
|
|
197
|
+
'--disable-blink-features=AutomationControlled',
|
|
198
|
+
'--no-sandbox',
|
|
199
|
+
'--disable-setuid-sandbox',
|
|
200
|
+
...args
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
// Load the dynamic ES Module imports
|
|
204
|
+
const { default: playwright } = await playwrightGhostPromise;
|
|
205
|
+
const { default: recommended } = await recommendedPromise;
|
|
206
|
+
const { chromium } = playwright;
|
|
207
|
+
|
|
208
|
+
const browser = await chromium.launch({
|
|
209
|
+
headless,
|
|
210
|
+
args: chromiumArgs,
|
|
211
|
+
proxy: playwrightProxy,
|
|
212
|
+
plugins: [recommended()]
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Ensure ad blocker is ready
|
|
216
|
+
await getAdBlocker();
|
|
217
|
+
|
|
218
|
+
const context = await browser.newContext({
|
|
219
|
+
viewport: null,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
let page = await context.newPage();
|
|
223
|
+
|
|
224
|
+
applyPuppeteerShims(browser, page);
|
|
225
|
+
|
|
226
|
+
page = await pageController({
|
|
227
|
+
browser,
|
|
228
|
+
page,
|
|
229
|
+
proxy,
|
|
230
|
+
turnstile,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
context.on('page', async (newPage) => {
|
|
234
|
+
applyPuppeteerShims(browser, newPage);
|
|
235
|
+
await pageController({
|
|
236
|
+
browser,
|
|
237
|
+
page: newPage,
|
|
238
|
+
proxy,
|
|
239
|
+
turnstile,
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
browser,
|
|
245
|
+
page,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = { connect };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const { checkTurnstile } = require('./turnstile.js');
|
|
2
|
+
|
|
3
|
+
async function pageController({ browser, page, proxy, turnstile }) {
|
|
4
|
+
if (page._pageControllerApplied) return page;
|
|
5
|
+
page._pageControllerApplied = true;
|
|
6
|
+
|
|
7
|
+
let solveStatus = turnstile;
|
|
8
|
+
|
|
9
|
+
page.on('close', () => {
|
|
10
|
+
solveStatus = false;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
async function turnstileSolver() {
|
|
14
|
+
while (solveStatus) {
|
|
15
|
+
await checkTurnstile({ page }).catch(() => { });
|
|
16
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
17
|
+
}
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (solveStatus) {
|
|
22
|
+
turnstileSolver();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// === POPUP AD BLOCKING ===
|
|
26
|
+
const context = page.context();
|
|
27
|
+
context.on('page', async (newPage) => {
|
|
28
|
+
try {
|
|
29
|
+
const opener = await newPage.opener();
|
|
30
|
+
if (opener) {
|
|
31
|
+
const url = newPage.url();
|
|
32
|
+
const isAdPopup = url === 'about:blank' ||
|
|
33
|
+
url.includes('ad') ||
|
|
34
|
+
url.includes('pop') ||
|
|
35
|
+
url.includes('click') ||
|
|
36
|
+
url.includes('redirect') ||
|
|
37
|
+
url.includes('track');
|
|
38
|
+
if (isAdPopup) {
|
|
39
|
+
await newPage.close().catch(() => { });
|
|
40
|
+
console.error('[popup-blocker] Blocked popup ad:', url.substring(0, 50));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// Ignore errors
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// NOTE: JS stealth overrides are commented out because Patchright natively handles automation hiding.
|
|
49
|
+
// Manual JS overrides trigger Pixelscan fingerprint masking detectors.
|
|
50
|
+
/*
|
|
51
|
+
await page.addInitScript(() => {
|
|
52
|
+
// ========== HARDWARE CONCURRENCY & DEVICE MEMORY FIX ==========
|
|
53
|
+
Object.defineProperty(navigator, 'hardwareConcurrency', {
|
|
54
|
+
get: () => 8,
|
|
55
|
+
configurable: true
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if ('deviceMemory' in navigator) {
|
|
59
|
+
Object.defineProperty(navigator, 'deviceMemory', {
|
|
60
|
+
get: () => 8,
|
|
61
|
+
configurable: true
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
return page;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { pageController };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const checkTurnstile = async ({ page }) => {
|
|
2
|
+
try {
|
|
3
|
+
const elements = await page.locator('[name="cf-turnstile-response"]').all();
|
|
4
|
+
if (elements.length <= 0) {
|
|
5
|
+
const coordinates = await page.evaluate(() => {
|
|
6
|
+
let coordinates = [];
|
|
7
|
+
document.querySelectorAll('div').forEach(item => {
|
|
8
|
+
try {
|
|
9
|
+
let itemCoordinates = item.getBoundingClientRect();
|
|
10
|
+
let itemCss = window.getComputedStyle(item);
|
|
11
|
+
if (itemCss.margin == "0px" && itemCss.padding == "0px" && itemCoordinates.width > 290 && itemCoordinates.width <= 310 && !item.querySelector('*')) {
|
|
12
|
+
coordinates.push({ x: itemCoordinates.x, y: item.getBoundingClientRect().y, w: item.getBoundingClientRect().width, h: item.getBoundingClientRect().height });
|
|
13
|
+
}
|
|
14
|
+
} catch (err) { }
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
if (coordinates.length <= 0) {
|
|
18
|
+
document.querySelectorAll('div').forEach(item => {
|
|
19
|
+
try {
|
|
20
|
+
let itemCoordinates = item.getBoundingClientRect();
|
|
21
|
+
if (itemCoordinates.width > 290 && itemCoordinates.width <= 310 && !item.querySelector('*')) {
|
|
22
|
+
coordinates.push({ x: itemCoordinates.x, y: item.getBoundingClientRect().y, w: item.getBoundingClientRect().width, h: item.getBoundingClientRect().height });
|
|
23
|
+
}
|
|
24
|
+
} catch (err) { }
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return coordinates;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
for (const item of coordinates) {
|
|
31
|
+
try {
|
|
32
|
+
let x = item.x + 30;
|
|
33
|
+
let y = item.y + item.h / 2;
|
|
34
|
+
await page.mouse.click(x, y);
|
|
35
|
+
} catch (err) { }
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const element of elements) {
|
|
41
|
+
try {
|
|
42
|
+
// Get the parent element bounding box
|
|
43
|
+
const box = await element.evaluate(el => {
|
|
44
|
+
const parent = el.parentElement;
|
|
45
|
+
if (!parent) return null;
|
|
46
|
+
const rect = parent.getBoundingClientRect();
|
|
47
|
+
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
48
|
+
});
|
|
49
|
+
if (box) {
|
|
50
|
+
let x = box.x + 30;
|
|
51
|
+
let y = box.y + box.height / 2;
|
|
52
|
+
await page.mouse.click(x, y);
|
|
53
|
+
}
|
|
54
|
+
} catch (err) { }
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { checkTurnstile };
|
|
Binary file
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import playwright from "playwright-ghost/patchright";
|
|
2
|
+
import recommended from "playwright-ghost/plugins/recommended";
|
|
3
|
+
const { chromium } = playwright;
|
|
4
|
+
import { PlaywrightBlocker } from "@ghostery/adblocker-playwright";
|
|
5
|
+
import { pageController } from "./module/pageController.mjs";
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
let adBlockerInstance = null;
|
|
14
|
+
let adBlockerPromise = null;
|
|
15
|
+
function getAdBlocker() {
|
|
16
|
+
if (!adBlockerPromise) {
|
|
17
|
+
const cachePath = path.join(__dirname, 'adblocker.bin');
|
|
18
|
+
adBlockerPromise = PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch, {
|
|
19
|
+
path: cachePath,
|
|
20
|
+
read: fs.promises.readFile,
|
|
21
|
+
write: fs.promises.writeFile,
|
|
22
|
+
}).then(blocker => {
|
|
23
|
+
adBlockerInstance = blocker;
|
|
24
|
+
return blocker;
|
|
25
|
+
}).catch(err => {
|
|
26
|
+
console.error('[adblocker] Failed to initialize adblocker:', err.message);
|
|
27
|
+
return null;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return adBlockerPromise;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loadEnvFile() {
|
|
34
|
+
const envPaths = [
|
|
35
|
+
path.join(process.cwd(), '.env'),
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
let currentDir = process.cwd();
|
|
39
|
+
for (let i = 0; i < 5; i++) {
|
|
40
|
+
const envPath = path.join(currentDir, '.env');
|
|
41
|
+
if (fs.existsSync(envPath) && !envPaths.includes(envPath)) {
|
|
42
|
+
envPaths.push(envPath);
|
|
43
|
+
}
|
|
44
|
+
const parentDir = path.dirname(currentDir);
|
|
45
|
+
if (parentDir === currentDir) break;
|
|
46
|
+
currentDir = parentDir;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const envPath of envPaths) {
|
|
50
|
+
try {
|
|
51
|
+
if (fs.existsSync(envPath)) {
|
|
52
|
+
const envContent = fs.readFileSync(envPath, 'utf-8');
|
|
53
|
+
envContent.split('\n').forEach(line => {
|
|
54
|
+
const trimmed = line.trim();
|
|
55
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
56
|
+
const [key, ...valueParts] = trimmed.split('=');
|
|
57
|
+
const value = valueParts.join('=').replace(/^["']|["']$/g, '');
|
|
58
|
+
if (key && !process.env[key]) {
|
|
59
|
+
process.env[key] = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// Silently ignore .env loading errors
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
loadEnvFile();
|
|
72
|
+
|
|
73
|
+
function getDefaultHeadless() {
|
|
74
|
+
const envHeadless = (process.env.HEADLESS || '').toLowerCase();
|
|
75
|
+
return envHeadless === 'true';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function applyPuppeteerShims(browser, page) {
|
|
79
|
+
if (page._shimsApplied) return page;
|
|
80
|
+
page._shimsApplied = true;
|
|
81
|
+
|
|
82
|
+
// Enable ad blocker
|
|
83
|
+
if (adBlockerInstance) {
|
|
84
|
+
adBlockerInstance.enableBlockingInPage(page).catch(() => {});
|
|
85
|
+
} else {
|
|
86
|
+
getAdBlocker().then(blocker => {
|
|
87
|
+
if (blocker) {
|
|
88
|
+
blocker.enableBlockingInPage(page).catch(() => {});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Cookie shims (Puppeteer → Playwright context)
|
|
94
|
+
if (!page.cookies) {
|
|
95
|
+
page.cookies = async (urls) => {
|
|
96
|
+
return await page.context().cookies(urls ? (Array.isArray(urls) ? urls : [urls]) : []);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!page.setCookie) {
|
|
101
|
+
page.setCookie = async (...cookies) => {
|
|
102
|
+
const formatted = cookies.map(c => ({
|
|
103
|
+
name: c.name,
|
|
104
|
+
value: c.value,
|
|
105
|
+
domain: c.domain || new URL(page.url()).hostname,
|
|
106
|
+
path: c.path || '/',
|
|
107
|
+
expires: c.expires || undefined,
|
|
108
|
+
httpOnly: c.httpOnly,
|
|
109
|
+
secure: c.secure,
|
|
110
|
+
sameSite: c.sameSite
|
|
111
|
+
}));
|
|
112
|
+
await page.context().addCookies(formatted);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!page.deleteCookie) {
|
|
117
|
+
page.deleteCookie = async (...cookies) => {
|
|
118
|
+
await page.context().clearCookies();
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Navigation shims
|
|
123
|
+
if (!page.waitForNetworkIdle) {
|
|
124
|
+
page.waitForNetworkIdle = async (options = {}) => {
|
|
125
|
+
try {
|
|
126
|
+
await page.waitForLoadState('networkidle', { timeout: options.timeout });
|
|
127
|
+
} catch (e) {
|
|
128
|
+
// Ignore networkidle timeouts if they are non-fatal
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!page.evaluateOnNewDocument) {
|
|
134
|
+
page.evaluateOnNewDocument = async (fn, ...args) => {
|
|
135
|
+
return await page.addInitScript(fn, ...args);
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Ghost Cursor integration via playwright-ghost auto-hooking
|
|
140
|
+
page.realCursor = {
|
|
141
|
+
move: async (selector, options = {}) => {
|
|
142
|
+
try {
|
|
143
|
+
await page.hover(selector, options);
|
|
144
|
+
} catch (e) {
|
|
145
|
+
// Silently fallback if element is not found
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
page.realClick = async (selector, options = {}) => {
|
|
150
|
+
await page.click(selector, options);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Mouse wheel shim (Puppeteer object → Playwright positional args)
|
|
154
|
+
if (page.mouse && page.mouse.wheel) {
|
|
155
|
+
const originalWheel = page.mouse.wheel.bind(page.mouse);
|
|
156
|
+
page.mouse.wheel = async (x, y) => {
|
|
157
|
+
if (typeof x === 'object' && x !== null) {
|
|
158
|
+
const deltaX = x.deltaX || 0;
|
|
159
|
+
const deltaY = x.deltaY || 0;
|
|
160
|
+
return await originalWheel(deltaX, deltaY);
|
|
161
|
+
}
|
|
162
|
+
return await originalWheel(x, y);
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Browser shims
|
|
167
|
+
if (!browser.process) {
|
|
168
|
+
browser.process = () => ({
|
|
169
|
+
pid: browser._childProcess?.pid || 1337
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!browser.pages) {
|
|
174
|
+
browser.pages = async () => {
|
|
175
|
+
return browser.contexts().flatMap(c => c.pages());
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return page;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function connect({
|
|
183
|
+
args = [],
|
|
184
|
+
headless = getDefaultHeadless(),
|
|
185
|
+
proxy = {},
|
|
186
|
+
turnstile = false,
|
|
187
|
+
} = {}) {
|
|
188
|
+
let playwrightProxy = undefined;
|
|
189
|
+
if (proxy && proxy.host && proxy.port) {
|
|
190
|
+
playwrightProxy = {
|
|
191
|
+
server: `${proxy.host}:${proxy.port}`
|
|
192
|
+
};
|
|
193
|
+
if (proxy.username && proxy.password) {
|
|
194
|
+
playwrightProxy.username = proxy.username;
|
|
195
|
+
playwrightProxy.password = proxy.password;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const chromiumArgs = [
|
|
200
|
+
'--disable-blink-features=AutomationControlled',
|
|
201
|
+
'--no-sandbox',
|
|
202
|
+
'--disable-setuid-sandbox',
|
|
203
|
+
...args
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
const browser = await chromium.launch({
|
|
207
|
+
headless,
|
|
208
|
+
args: chromiumArgs,
|
|
209
|
+
proxy: playwrightProxy,
|
|
210
|
+
plugins: [recommended()]
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Ensure ad blocker is ready
|
|
214
|
+
await getAdBlocker();
|
|
215
|
+
|
|
216
|
+
const context = await browser.newContext({
|
|
217
|
+
viewport: null,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
let page = await context.newPage();
|
|
221
|
+
|
|
222
|
+
applyPuppeteerShims(browser, page);
|
|
223
|
+
|
|
224
|
+
page = await pageController({
|
|
225
|
+
browser,
|
|
226
|
+
page,
|
|
227
|
+
proxy,
|
|
228
|
+
turnstile,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
context.on('page', async (newPage) => {
|
|
232
|
+
applyPuppeteerShims(browser, newPage);
|
|
233
|
+
await pageController({
|
|
234
|
+
browser,
|
|
235
|
+
page: newPage,
|
|
236
|
+
proxy,
|
|
237
|
+
turnstile,
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
browser,
|
|
243
|
+
page,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { checkTurnstile } from './turnstile.mjs';
|
|
2
|
+
|
|
3
|
+
export async function pageController({ browser, page, proxy, turnstile }) {
|
|
4
|
+
if (page._pageControllerApplied) return page;
|
|
5
|
+
page._pageControllerApplied = true;
|
|
6
|
+
|
|
7
|
+
let solveStatus = turnstile;
|
|
8
|
+
|
|
9
|
+
page.on('close', () => {
|
|
10
|
+
solveStatus = false;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
async function turnstileSolver() {
|
|
14
|
+
while (solveStatus) {
|
|
15
|
+
await checkTurnstile({ page }).catch(() => { });
|
|
16
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
17
|
+
}
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (solveStatus) {
|
|
22
|
+
turnstileSolver();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// === POPUP AD BLOCKING ===
|
|
26
|
+
const context = page.context();
|
|
27
|
+
context.on('page', async (newPage) => {
|
|
28
|
+
try {
|
|
29
|
+
const opener = await newPage.opener();
|
|
30
|
+
if (opener) {
|
|
31
|
+
const url = newPage.url();
|
|
32
|
+
const isAdPopup = url === 'about:blank' ||
|
|
33
|
+
url.includes('ad') ||
|
|
34
|
+
url.includes('pop') ||
|
|
35
|
+
url.includes('click') ||
|
|
36
|
+
url.includes('redirect') ||
|
|
37
|
+
url.includes('track');
|
|
38
|
+
if (isAdPopup) {
|
|
39
|
+
await newPage.close().catch(() => { });
|
|
40
|
+
console.error('[popup-blocker] Blocked popup ad:', url.substring(0, 50));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// Ignore errors
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// NOTE: JS stealth overrides are commented out because Patchright natively handles automation hiding.
|
|
49
|
+
// Manual JS overrides trigger Pixelscan fingerprint masking detectors.
|
|
50
|
+
/*
|
|
51
|
+
await page.addInitScript(() => {
|
|
52
|
+
// ========== HARDWARE CONCURRENCY & DEVICE MEMORY FIX ==========
|
|
53
|
+
Object.defineProperty(navigator, 'hardwareConcurrency', {
|
|
54
|
+
get: () => 8,
|
|
55
|
+
configurable: true
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if ('deviceMemory' in navigator) {
|
|
59
|
+
Object.defineProperty(navigator, 'deviceMemory', {
|
|
60
|
+
get: () => 8,
|
|
61
|
+
configurable: true
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
return page;
|
|
68
|
+
}
|